From ba72d3acfc0c29e99ab9d14e6ee3290c90b1efed Mon Sep 17 00:00:00 2001 From: Shivaram Lingamneni Date: Mon, 14 Dec 2020 05:00:21 -0500 Subject: [PATCH] implement a channel forwarding mode Fixes #1260 --- distrib/atheme/atheme2json.py | 7 ++++++ irc/channel.go | 45 +++++++++++++++++++++++------------ irc/channelmanager.go | 10 ++++---- irc/channelreg.go | 7 ++++++ irc/getters.go | 6 +++++ irc/handlers.go | 13 +++++++--- irc/import.go | 6 +++++ irc/modes.go | 25 ++++++++++++++++++- irc/modes/modes.go | 9 +++---- irc/numerics.go | 1 + 10 files changed, 101 insertions(+), 28 deletions(-) diff --git a/distrib/atheme/atheme2json.py b/distrib/atheme/atheme2json.py index d9ba8ee7..2659b92d 100755 --- a/distrib/atheme/atheme2json.py +++ b/distrib/atheme/atheme2json.py @@ -94,6 +94,13 @@ def convert(infile): out['channels'][chname]['topicSetBy'] = parts[3] elif category == 'private:topic:ts': out['channels'][chname]['topicSetAt'] = to_unixnano(parts[3]) + elif category == 'private:mlockext': + # the channel forward mode is +L on insp/unreal, +f on charybdis + # charybdis has a +L ("large banlist") taking no argument + # and unreal has a +f ("flood limit") taking two colon-delimited numbers, + # so check for an argument that starts with a # + if parts[3].startswith('L#') or parts[3].startswith('f#'): + out['channels'][chname]['forward'] = parts[3][1:] elif category == 'CA': # channel access lists # CA #mychannel shivaram +AFORafhioqrstv 1600134478 shivaram diff --git a/irc/channel.go b/irc/channel.go index e9773f46..6595e965 100644 --- a/irc/channel.go +++ b/irc/channel.go @@ -28,6 +28,7 @@ type Channel struct { flags modes.ModeSet lists map[modes.Mode]*UserMaskSet key string + forward string members MemberSet membersCache []*Client // allow iteration over channel members without holding the lock name string @@ -133,6 +134,7 @@ func (channel *Channel) applyRegInfo(chanReg RegisteredChannel) { channel.key = chanReg.Key channel.userLimit = chanReg.UserLimit channel.settings = chanReg.Settings + channel.forward = chanReg.Forward for _, mode := range chanReg.Modes { channel.flags.SetMode(mode, true) @@ -163,6 +165,7 @@ func (channel *Channel) ExportRegistration(includeFlags uint) (info RegisteredCh if includeFlags&IncludeModes != 0 { info.Key = channel.key + info.Forward = channel.forward info.Modes = channel.flags.AllModes() info.UserLimit = channel.userLimit } @@ -612,20 +615,27 @@ func (channel *Channel) modeStrings(client *Client) (result []string) { isMember := hasPrivs || channel.members[client] != nil showKey := isMember && (channel.key != "") showUserLimit := channel.userLimit > 0 + showForward := channel.forward != "" - mods := "+" + var mods strings.Builder + mods.WriteRune('+') // flags with args if showKey { - mods += modes.Key.String() + mods.WriteRune(rune(modes.Key)) } if showUserLimit { - mods += modes.UserLimit.String() + mods.WriteRune(rune(modes.UserLimit)) + } + if showForward { + mods.WriteRune(rune(modes.Forward)) } - mods += channel.flags.String() + for _, m := range channel.flags.AllModes() { + mods.WriteRune(rune(m)) + } - result = []string{mods} + result = []string{mods.String()} // args for flags with args: The order must match above to keep // positional arguments in place. @@ -635,6 +645,9 @@ func (channel *Channel) modeStrings(client *Client) (result []string) { if showUserLimit { result = append(result, strconv.Itoa(channel.userLimit)) } + if showForward { + result = append(result, channel.forward) + } return } @@ -694,7 +707,7 @@ func (channel *Channel) AddHistoryItem(item history.Item, account string) (err e } // Join joins the given client to this channel (if they can be joined). -func (channel *Channel) Join(client *Client, key string, isSajoin bool, rb *ResponseBuffer) error { +func (channel *Channel) Join(client *Client, key string, isSajoin bool, rb *ResponseBuffer) (joinErr error, forward string) { details := client.Details() channel.stateMutex.RLock() @@ -707,11 +720,12 @@ func (channel *Channel) Join(client *Client, key string, isSajoin bool, rb *Resp chcount := len(channel.members) _, alreadyJoined := channel.members[client] persistentMode := channel.accountToUMode[details.account] + forward = channel.forward channel.stateMutex.RUnlock() if alreadyJoined { // no message needs to be sent - return nil + return nil, "" } // 0. SAJOIN always succeeds @@ -723,32 +737,33 @@ func (channel *Channel) Join(client *Client, key string, isSajoin bool, rb *Resp client.CheckInvited(chcfname, createdAt) if !hasPrivs { if limit != 0 && chcount >= limit { - return errLimitExceeded + return errLimitExceeded, forward } if chkey != "" && !utils.SecretTokensMatch(chkey, key) { - return errWrongChannelKey + return errWrongChannelKey, forward } if channel.flags.HasMode(modes.InviteOnly) && !channel.lists[modes.InviteMask].Match(details.nickMaskCasefolded) { - return errInviteOnly + return errInviteOnly, forward } if channel.lists[modes.BanMask].Match(details.nickMaskCasefolded) && !channel.lists[modes.ExceptMask].Match(details.nickMaskCasefolded) && !channel.lists[modes.InviteMask].Match(details.nickMaskCasefolded) { - return errBanned + // do not forward people who are banned: + return errBanned, "" } if details.account == "" && (channel.flags.HasMode(modes.RegisteredOnly) || channel.server.Defcon() <= 2) { - return errRegisteredOnly + return errRegisteredOnly, forward } } if joinErr := client.addChannel(channel, rb == nil); joinErr != nil { - return joinErr + return joinErr, "" } client.server.logger.Debug("join", fmt.Sprintf("%s joined channel %s", details.nick, chname)) @@ -795,7 +810,7 @@ func (channel *Channel) Join(client *Client, key string, isSajoin bool, rb *Resp } if rb == nil { - return nil + return nil, "" } var modestr string @@ -858,7 +873,7 @@ func (channel *Channel) Join(client *Client, key string, isSajoin bool, rb *Resp rb.Flush(true) channel.autoReplayHistory(client, rb, message.Msgid) - return nil + return nil, "" } func (channel *Channel) autoReplayHistory(client *Client, rb *ResponseBuffer, skipMsgid string) { diff --git a/irc/channelmanager.go b/irc/channelmanager.go index be37e0ff..9376a887 100644 --- a/irc/channelmanager.go +++ b/irc/channelmanager.go @@ -83,12 +83,12 @@ func (cm *ChannelManager) Get(name string) (channel *Channel) { } // Join causes `client` to join the channel named `name`, creating it if necessary. -func (cm *ChannelManager) Join(client *Client, name string, key string, isSajoin bool, rb *ResponseBuffer) error { +func (cm *ChannelManager) Join(client *Client, name string, key string, isSajoin bool, rb *ResponseBuffer) (err error, forward string) { server := client.server casefoldedName, err := CasefoldChannel(name) skeleton, skerr := Skeleton(name) if err != nil || skerr != nil || len(casefoldedName) > server.Config().Limits.ChannelLen { - return errNoSuchChannel + return errNoSuchChannel, "" } channel, err := func() (*Channel, error) { @@ -128,15 +128,15 @@ func (cm *ChannelManager) Join(client *Client, name string, key string, isSajoin }() if err != nil { - return err + return err, "" } channel.EnsureLoaded() - err = channel.Join(client, key, isSajoin, rb) + err, forward = channel.Join(client, key, isSajoin, rb) cm.maybeCleanup(channel, true) - return err + return } func (cm *ChannelManager) maybeCleanup(channel *Channel, afterJoin bool) { diff --git a/irc/channelreg.go b/irc/channelreg.go index 5cb6f636..9cc7f8a5 100644 --- a/irc/channelreg.go +++ b/irc/channelreg.go @@ -35,6 +35,7 @@ const ( keyChannelAccountToUMode = "channel.accounttoumode %s" keyChannelUserLimit = "channel.userlimit %s" keyChannelSettings = "channel.settings %s" + keyChannelForward = "channel.forward %s" keyChannelPurged = "channel.purged %s" ) @@ -56,6 +57,7 @@ var ( keyChannelAccountToUMode, keyChannelUserLimit, keyChannelSettings, + keyChannelForward, } ) @@ -94,6 +96,8 @@ type RegisteredChannel struct { Modes []modes.Mode // Key represents the channel key / password Key string + // Forward is the forwarding/overflow (+f) channel + Forward string // UserLimit is the user limit (0 for no limit) UserLimit int // AccountToUMode maps user accounts to their persistent channel modes (e.g., +q, +h) @@ -208,6 +212,7 @@ func (reg *ChannelRegistry) LoadChannel(nameCasefolded string) (info RegisteredC password, _ := tx.Get(fmt.Sprintf(keyChannelPassword, channelKey)) modeString, _ := tx.Get(fmt.Sprintf(keyChannelModes, channelKey)) userLimitString, _ := tx.Get(fmt.Sprintf(keyChannelUserLimit, channelKey)) + forward, _ := tx.Get(fmt.Sprintf(keyChannelForward, channelKey)) banlistString, _ := tx.Get(fmt.Sprintf(keyChannelBanlist, channelKey)) exceptlistString, _ := tx.Get(fmt.Sprintf(keyChannelExceptlist, channelKey)) invitelistString, _ := tx.Get(fmt.Sprintf(keyChannelInvitelist, channelKey)) @@ -249,6 +254,7 @@ func (reg *ChannelRegistry) LoadChannel(nameCasefolded string) (info RegisteredC AccountToUMode: accountToUMode, UserLimit: int(userLimit), Settings: settings, + Forward: forward, } return nil }) @@ -361,6 +367,7 @@ func (reg *ChannelRegistry) saveChannel(tx *buntdb.Tx, channelInfo RegisteredCha modeString := modes.Modes(channelInfo.Modes).String() tx.Set(fmt.Sprintf(keyChannelModes, channelKey), modeString, nil) tx.Set(fmt.Sprintf(keyChannelUserLimit, channelKey), strconv.Itoa(channelInfo.UserLimit), nil) + tx.Set(fmt.Sprintf(keyChannelForward, channelKey), channelInfo.Forward, nil) } if includeFlags&IncludeLists != 0 { diff --git a/irc/getters.go b/irc/getters.go index cd3a939e..811a1e02 100644 --- a/irc/getters.go +++ b/irc/getters.go @@ -517,3 +517,9 @@ func (channel *Channel) SetSettings(settings ChannelSettings) { channel.stateMutex.Unlock() channel.MarkDirty(IncludeSettings) } + +func (channel *Channel) setForward(forward string) { + channel.stateMutex.Lock() + channel.forward = forward + channel.stateMutex.Unlock() +} diff --git a/irc/handlers.go b/irc/handlers.go index 4c154c3e..8008939b 100644 --- a/irc/handlers.go +++ b/irc/handlers.go @@ -1192,9 +1192,16 @@ func joinHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Resp if len(keys) > i { key = keys[i] } - err := server.channels.Join(client, name, key, false, rb) + err, forward := server.channels.Join(client, name, key, false, rb) if err != nil { - sendJoinError(client, name, rb, err) + if forward != "" { + rb.Add(nil, server.name, ERR_LINKCHANNEL, client.Nick(), utils.SafeErrorParam(name), forward, client.t("Forwarding to another channel")) + name = forward + err, _ = server.channels.Join(client, name, key, false, rb) + } + if err != nil { + sendJoinError(client, name, rb, err) + } } } return false @@ -1255,7 +1262,7 @@ func sajoinHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Re channels := strings.Split(channelString, ",") for _, chname := range channels { - err := server.channels.Join(target, chname, "", true, rb) + err, _ := server.channels.Join(target, chname, "", true, rb) if err != nil { sendJoinError(client, chname, rb, err) } diff --git a/irc/import.go b/irc/import.go index 92e0682e..95894fdc 100644 --- a/irc/import.go +++ b/irc/import.go @@ -44,6 +44,7 @@ type channelImport struct { Modes string Key string Limit int + Forward string } type databaseImport struct { @@ -187,6 +188,11 @@ func doImportDBGeneric(config *Config, dbImport databaseImport, credsType Creden if chInfo.Limit > 0 { tx.Set(fmt.Sprintf(keyChannelUserLimit, cfchname), strconv.Itoa(chInfo.Limit), nil) } + if chInfo.Forward != "" { + if _, err := CasefoldChannel(chInfo.Forward); err == nil { + tx.Set(fmt.Sprintf(keyChannelForward, cfchname), chInfo.Forward, nil) + } + } } if warnSkeletons { diff --git a/irc/modes.go b/irc/modes.go index b79bb681..1fd27e19 100644 --- a/irc/modes.go +++ b/irc/modes.go @@ -12,6 +12,7 @@ import ( "github.com/oragono/oragono/irc/modes" "github.com/oragono/oragono/irc/sno" + "github.com/oragono/oragono/irc/utils" ) var ( @@ -254,6 +255,28 @@ func (channel *Channel) ApplyChannelModeChanges(client *Client, isSamode bool, c applied = append(applied, change) } + case modes.Forward: + switch change.Op { + case modes.Add: + ch := client.server.channels.Get(change.Arg) + if ch == nil { + rb.Add(nil, client.server.name, ERR_INVALIDMODEPARAM, details.nick, utils.SafeErrorParam(change.Arg), fmt.Sprintf(client.t("No such channel"))) + } else if ch == channel { + rb.Add(nil, client.server.name, ERR_INVALIDMODEPARAM, details.nick, utils.SafeErrorParam(change.Arg), fmt.Sprintf(client.t("You can't forward a channel to itself"))) + } else { + if !ch.ClientIsAtLeast(client, modes.ChannelOperator) { + rb.Add(nil, client.server.name, ERR_CHANOPRIVSNEEDED, details.nick, ch.Name(), client.t("You must be a channel operator in the channel you are forwarding to")) + } else { + change.Arg = ch.Name() + channel.setForward(change.Arg) + applied = append(applied, change) + } + } + case modes.Remove: + channel.setForward("") + applied = append(applied, change) + } + case modes.Key: switch change.Op { case modes.Add: @@ -302,7 +325,7 @@ func (channel *Channel) ApplyChannelModeChanges(client *Client, isSamode bool, c case modes.BanMask, modes.ExceptMask, modes.InviteMask: includeFlags |= IncludeLists case modes.ChannelFounder, modes.ChannelAdmin, modes.ChannelOperator, modes.Halfop, modes.Voice: - // these are never persisted currently, but might be in the future (see discussion on #729) + // these are persisted on the client object, via (*Channel).applyModeToMember default: includeFlags |= IncludeModes } diff --git a/irc/modes/modes.go b/irc/modes/modes.go index cf7cc1f2..8019c3f9 100644 --- a/irc/modes/modes.go +++ b/irc/modes/modes.go @@ -24,7 +24,7 @@ var ( SupportedChannelModes = Modes{ BanMask, ChanRoleplaying, ExceptMask, InviteMask, InviteOnly, Key, Moderated, NoOutside, OpOnlyTopic, RegisteredOnly, RegisteredOnlySpeak, - Secret, UserLimit, NoCTCP, Auditorium, OpModerated, + Secret, UserLimit, NoCTCP, Auditorium, OpModerated, Forward, } ) @@ -130,6 +130,7 @@ const ( UserLimit Mode = 'l' // flag arg NoCTCP Mode = 'C' // flag OpModerated Mode = 'U' // flag + Forward Mode = 'f' // flag arg ) var ( @@ -277,7 +278,7 @@ func ParseChannelModeChanges(params ...string) (ModeChanges, map[rune]bool) { } else { continue } - case UserLimit: + case UserLimit, Forward: // don't require value when removing if change.Op == Add { if len(params) > skipArgs { @@ -445,7 +446,7 @@ func RplMyInfo() (param1, param2, param3 string) { sort.Sort(ByCodepoint(channelModes)) // XXX enumerate these by hand, i can't see any way to DRY this - channelParametrizedModes := Modes{BanMask, ExceptMask, InviteMask, Key, UserLimit} + channelParametrizedModes := Modes{BanMask, ExceptMask, InviteMask, Key, UserLimit, Forward} channelParametrizedModes = append(channelParametrizedModes, ChannelUserModes...) sort.Sort(ByCodepoint(channelParametrizedModes)) @@ -459,7 +460,7 @@ func ChanmodesToken() (result string) { // type B: modes with parameters B := Modes{Key} // type C: modes that take a parameter only when set, never when unset - C := Modes{UserLimit} + C := Modes{UserLimit, Forward} // type D: modes without parameters D := Modes{InviteOnly, Moderated, NoOutside, OpOnlyTopic, ChanRoleplaying, Secret, NoCTCP, RegisteredOnly, RegisteredOnlySpeak, Auditorium, OpModerated} diff --git a/irc/numerics.go b/irc/numerics.go index 9aa09bab..01b22499 100644 --- a/irc/numerics.go +++ b/irc/numerics.go @@ -149,6 +149,7 @@ const ( ERR_YOUWILLBEBANNED = "466" ERR_KEYSET = "467" ERR_INVALIDUSERNAME = "468" + ERR_LINKCHANNEL = "470" ERR_CHANNELISFULL = "471" ERR_UNKNOWNMODE = "472" ERR_INVITEONLYCHAN = "473"