implement a channel forwarding mode

Fixes #1260
This commit is contained in:
Shivaram Lingamneni 2020-12-14 05:00:21 -05:00
parent 9033d97c6f
commit ba72d3acfc
10 changed files with 101 additions and 28 deletions

@ -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

@ -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) {

@ -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) {

@ -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 {

@ -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()
}

@ -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)
}

@ -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 {

@ -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
}

@ -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}

@ -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"