3
0
mirror of https://github.com/ergochat/ergo.git synced 2024-11-22 03:49:27 +01:00

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

View File

@ -94,6 +94,13 @@ def convert(infile):
out['channels'][chname]['topicSetBy'] = parts[3] out['channels'][chname]['topicSetBy'] = parts[3]
elif category == 'private:topic:ts': elif category == 'private:topic:ts':
out['channels'][chname]['topicSetAt'] = to_unixnano(parts[3]) 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': elif category == 'CA':
# channel access lists # channel access lists
# CA #mychannel shivaram +AFORafhioqrstv 1600134478 shivaram # CA #mychannel shivaram +AFORafhioqrstv 1600134478 shivaram

View File

@ -28,6 +28,7 @@ type Channel struct {
flags modes.ModeSet flags modes.ModeSet
lists map[modes.Mode]*UserMaskSet lists map[modes.Mode]*UserMaskSet
key string key string
forward string
members MemberSet members MemberSet
membersCache []*Client // allow iteration over channel members without holding the lock membersCache []*Client // allow iteration over channel members without holding the lock
name string name string
@ -133,6 +134,7 @@ func (channel *Channel) applyRegInfo(chanReg RegisteredChannel) {
channel.key = chanReg.Key channel.key = chanReg.Key
channel.userLimit = chanReg.UserLimit channel.userLimit = chanReg.UserLimit
channel.settings = chanReg.Settings channel.settings = chanReg.Settings
channel.forward = chanReg.Forward
for _, mode := range chanReg.Modes { for _, mode := range chanReg.Modes {
channel.flags.SetMode(mode, true) channel.flags.SetMode(mode, true)
@ -163,6 +165,7 @@ func (channel *Channel) ExportRegistration(includeFlags uint) (info RegisteredCh
if includeFlags&IncludeModes != 0 { if includeFlags&IncludeModes != 0 {
info.Key = channel.key info.Key = channel.key
info.Forward = channel.forward
info.Modes = channel.flags.AllModes() info.Modes = channel.flags.AllModes()
info.UserLimit = channel.userLimit info.UserLimit = channel.userLimit
} }
@ -612,20 +615,27 @@ func (channel *Channel) modeStrings(client *Client) (result []string) {
isMember := hasPrivs || channel.members[client] != nil isMember := hasPrivs || channel.members[client] != nil
showKey := isMember && (channel.key != "") showKey := isMember && (channel.key != "")
showUserLimit := channel.userLimit > 0 showUserLimit := channel.userLimit > 0
showForward := channel.forward != ""
mods := "+" var mods strings.Builder
mods.WriteRune('+')
// flags with args // flags with args
if showKey { if showKey {
mods += modes.Key.String() mods.WriteRune(rune(modes.Key))
} }
if showUserLimit { 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 // args for flags with args: The order must match above to keep
// positional arguments in place. // positional arguments in place.
@ -635,6 +645,9 @@ func (channel *Channel) modeStrings(client *Client) (result []string) {
if showUserLimit { if showUserLimit {
result = append(result, strconv.Itoa(channel.userLimit)) result = append(result, strconv.Itoa(channel.userLimit))
} }
if showForward {
result = append(result, channel.forward)
}
return 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). // 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() details := client.Details()
channel.stateMutex.RLock() channel.stateMutex.RLock()
@ -707,11 +720,12 @@ func (channel *Channel) Join(client *Client, key string, isSajoin bool, rb *Resp
chcount := len(channel.members) chcount := len(channel.members)
_, alreadyJoined := channel.members[client] _, alreadyJoined := channel.members[client]
persistentMode := channel.accountToUMode[details.account] persistentMode := channel.accountToUMode[details.account]
forward = channel.forward
channel.stateMutex.RUnlock() channel.stateMutex.RUnlock()
if alreadyJoined { if alreadyJoined {
// no message needs to be sent // no message needs to be sent
return nil return nil, ""
} }
// 0. SAJOIN always succeeds // 0. SAJOIN always succeeds
@ -723,32 +737,33 @@ func (channel *Channel) Join(client *Client, key string, isSajoin bool, rb *Resp
client.CheckInvited(chcfname, createdAt) client.CheckInvited(chcfname, createdAt)
if !hasPrivs { if !hasPrivs {
if limit != 0 && chcount >= limit { if limit != 0 && chcount >= limit {
return errLimitExceeded return errLimitExceeded, forward
} }
if chkey != "" && !utils.SecretTokensMatch(chkey, key) { if chkey != "" && !utils.SecretTokensMatch(chkey, key) {
return errWrongChannelKey return errWrongChannelKey, forward
} }
if channel.flags.HasMode(modes.InviteOnly) && if channel.flags.HasMode(modes.InviteOnly) &&
!channel.lists[modes.InviteMask].Match(details.nickMaskCasefolded) { !channel.lists[modes.InviteMask].Match(details.nickMaskCasefolded) {
return errInviteOnly return errInviteOnly, forward
} }
if channel.lists[modes.BanMask].Match(details.nickMaskCasefolded) && if channel.lists[modes.BanMask].Match(details.nickMaskCasefolded) &&
!channel.lists[modes.ExceptMask].Match(details.nickMaskCasefolded) && !channel.lists[modes.ExceptMask].Match(details.nickMaskCasefolded) &&
!channel.lists[modes.InviteMask].Match(details.nickMaskCasefolded) { !channel.lists[modes.InviteMask].Match(details.nickMaskCasefolded) {
return errBanned // do not forward people who are banned:
return errBanned, ""
} }
if details.account == "" && if details.account == "" &&
(channel.flags.HasMode(modes.RegisteredOnly) || channel.server.Defcon() <= 2) { (channel.flags.HasMode(modes.RegisteredOnly) || channel.server.Defcon() <= 2) {
return errRegisteredOnly return errRegisteredOnly, forward
} }
} }
if joinErr := client.addChannel(channel, rb == nil); joinErr != nil { 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)) 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 { if rb == nil {
return nil return nil, ""
} }
var modestr string var modestr string
@ -858,7 +873,7 @@ func (channel *Channel) Join(client *Client, key string, isSajoin bool, rb *Resp
rb.Flush(true) rb.Flush(true)
channel.autoReplayHistory(client, rb, message.Msgid) channel.autoReplayHistory(client, rb, message.Msgid)
return nil return nil, ""
} }
func (channel *Channel) autoReplayHistory(client *Client, rb *ResponseBuffer, skipMsgid string) { func (channel *Channel) autoReplayHistory(client *Client, rb *ResponseBuffer, skipMsgid string) {

View File

@ -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. // 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 server := client.server
casefoldedName, err := CasefoldChannel(name) casefoldedName, err := CasefoldChannel(name)
skeleton, skerr := Skeleton(name) skeleton, skerr := Skeleton(name)
if err != nil || skerr != nil || len(casefoldedName) > server.Config().Limits.ChannelLen { if err != nil || skerr != nil || len(casefoldedName) > server.Config().Limits.ChannelLen {
return errNoSuchChannel return errNoSuchChannel, ""
} }
channel, err := func() (*Channel, error) { channel, err := func() (*Channel, error) {
@ -128,15 +128,15 @@ func (cm *ChannelManager) Join(client *Client, name string, key string, isSajoin
}() }()
if err != nil { if err != nil {
return err return err, ""
} }
channel.EnsureLoaded() channel.EnsureLoaded()
err = channel.Join(client, key, isSajoin, rb) err, forward = channel.Join(client, key, isSajoin, rb)
cm.maybeCleanup(channel, true) cm.maybeCleanup(channel, true)
return err return
} }
func (cm *ChannelManager) maybeCleanup(channel *Channel, afterJoin bool) { func (cm *ChannelManager) maybeCleanup(channel *Channel, afterJoin bool) {

View File

@ -35,6 +35,7 @@ const (
keyChannelAccountToUMode = "channel.accounttoumode %s" keyChannelAccountToUMode = "channel.accounttoumode %s"
keyChannelUserLimit = "channel.userlimit %s" keyChannelUserLimit = "channel.userlimit %s"
keyChannelSettings = "channel.settings %s" keyChannelSettings = "channel.settings %s"
keyChannelForward = "channel.forward %s"
keyChannelPurged = "channel.purged %s" keyChannelPurged = "channel.purged %s"
) )
@ -56,6 +57,7 @@ var (
keyChannelAccountToUMode, keyChannelAccountToUMode,
keyChannelUserLimit, keyChannelUserLimit,
keyChannelSettings, keyChannelSettings,
keyChannelForward,
} }
) )
@ -94,6 +96,8 @@ type RegisteredChannel struct {
Modes []modes.Mode Modes []modes.Mode
// Key represents the channel key / password // Key represents the channel key / password
Key string Key string
// Forward is the forwarding/overflow (+f) channel
Forward string
// UserLimit is the user limit (0 for no limit) // UserLimit is the user limit (0 for no limit)
UserLimit int UserLimit int
// AccountToUMode maps user accounts to their persistent channel modes (e.g., +q, +h) // 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)) password, _ := tx.Get(fmt.Sprintf(keyChannelPassword, channelKey))
modeString, _ := tx.Get(fmt.Sprintf(keyChannelModes, channelKey)) modeString, _ := tx.Get(fmt.Sprintf(keyChannelModes, channelKey))
userLimitString, _ := tx.Get(fmt.Sprintf(keyChannelUserLimit, channelKey)) userLimitString, _ := tx.Get(fmt.Sprintf(keyChannelUserLimit, channelKey))
forward, _ := tx.Get(fmt.Sprintf(keyChannelForward, channelKey))
banlistString, _ := tx.Get(fmt.Sprintf(keyChannelBanlist, channelKey)) banlistString, _ := tx.Get(fmt.Sprintf(keyChannelBanlist, channelKey))
exceptlistString, _ := tx.Get(fmt.Sprintf(keyChannelExceptlist, channelKey)) exceptlistString, _ := tx.Get(fmt.Sprintf(keyChannelExceptlist, channelKey))
invitelistString, _ := tx.Get(fmt.Sprintf(keyChannelInvitelist, channelKey)) invitelistString, _ := tx.Get(fmt.Sprintf(keyChannelInvitelist, channelKey))
@ -249,6 +254,7 @@ func (reg *ChannelRegistry) LoadChannel(nameCasefolded string) (info RegisteredC
AccountToUMode: accountToUMode, AccountToUMode: accountToUMode,
UserLimit: int(userLimit), UserLimit: int(userLimit),
Settings: settings, Settings: settings,
Forward: forward,
} }
return nil return nil
}) })
@ -361,6 +367,7 @@ func (reg *ChannelRegistry) saveChannel(tx *buntdb.Tx, channelInfo RegisteredCha
modeString := modes.Modes(channelInfo.Modes).String() modeString := modes.Modes(channelInfo.Modes).String()
tx.Set(fmt.Sprintf(keyChannelModes, channelKey), modeString, nil) tx.Set(fmt.Sprintf(keyChannelModes, channelKey), modeString, nil)
tx.Set(fmt.Sprintf(keyChannelUserLimit, channelKey), strconv.Itoa(channelInfo.UserLimit), 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 { if includeFlags&IncludeLists != 0 {

View File

@ -517,3 +517,9 @@ func (channel *Channel) SetSettings(settings ChannelSettings) {
channel.stateMutex.Unlock() channel.stateMutex.Unlock()
channel.MarkDirty(IncludeSettings) channel.MarkDirty(IncludeSettings)
} }
func (channel *Channel) setForward(forward string) {
channel.stateMutex.Lock()
channel.forward = forward
channel.stateMutex.Unlock()
}

View File

@ -1192,9 +1192,16 @@ func joinHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Resp
if len(keys) > i { if len(keys) > i {
key = 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 { 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 return false
@ -1255,7 +1262,7 @@ func sajoinHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Re
channels := strings.Split(channelString, ",") channels := strings.Split(channelString, ",")
for _, chname := range channels { for _, chname := range channels {
err := server.channels.Join(target, chname, "", true, rb) err, _ := server.channels.Join(target, chname, "", true, rb)
if err != nil { if err != nil {
sendJoinError(client, chname, rb, err) sendJoinError(client, chname, rb, err)
} }

View File

@ -44,6 +44,7 @@ type channelImport struct {
Modes string Modes string
Key string Key string
Limit int Limit int
Forward string
} }
type databaseImport struct { type databaseImport struct {
@ -187,6 +188,11 @@ func doImportDBGeneric(config *Config, dbImport databaseImport, credsType Creden
if chInfo.Limit > 0 { if chInfo.Limit > 0 {
tx.Set(fmt.Sprintf(keyChannelUserLimit, cfchname), strconv.Itoa(chInfo.Limit), nil) 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 { if warnSkeletons {

View File

@ -12,6 +12,7 @@ import (
"github.com/oragono/oragono/irc/modes" "github.com/oragono/oragono/irc/modes"
"github.com/oragono/oragono/irc/sno" "github.com/oragono/oragono/irc/sno"
"github.com/oragono/oragono/irc/utils"
) )
var ( var (
@ -254,6 +255,28 @@ func (channel *Channel) ApplyChannelModeChanges(client *Client, isSamode bool, c
applied = append(applied, change) 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: case modes.Key:
switch change.Op { switch change.Op {
case modes.Add: case modes.Add:
@ -302,7 +325,7 @@ func (channel *Channel) ApplyChannelModeChanges(client *Client, isSamode bool, c
case modes.BanMask, modes.ExceptMask, modes.InviteMask: case modes.BanMask, modes.ExceptMask, modes.InviteMask:
includeFlags |= IncludeLists includeFlags |= IncludeLists
case modes.ChannelFounder, modes.ChannelAdmin, modes.ChannelOperator, modes.Halfop, modes.Voice: 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: default:
includeFlags |= IncludeModes includeFlags |= IncludeModes
} }

View File

@ -24,7 +24,7 @@ var (
SupportedChannelModes = Modes{ SupportedChannelModes = Modes{
BanMask, ChanRoleplaying, ExceptMask, InviteMask, InviteOnly, Key, BanMask, ChanRoleplaying, ExceptMask, InviteMask, InviteOnly, Key,
Moderated, NoOutside, OpOnlyTopic, RegisteredOnly, RegisteredOnlySpeak, 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 UserLimit Mode = 'l' // flag arg
NoCTCP Mode = 'C' // flag NoCTCP Mode = 'C' // flag
OpModerated Mode = 'U' // flag OpModerated Mode = 'U' // flag
Forward Mode = 'f' // flag arg
) )
var ( var (
@ -277,7 +278,7 @@ func ParseChannelModeChanges(params ...string) (ModeChanges, map[rune]bool) {
} else { } else {
continue continue
} }
case UserLimit: case UserLimit, Forward:
// don't require value when removing // don't require value when removing
if change.Op == Add { if change.Op == Add {
if len(params) > skipArgs { if len(params) > skipArgs {
@ -445,7 +446,7 @@ func RplMyInfo() (param1, param2, param3 string) {
sort.Sort(ByCodepoint(channelModes)) sort.Sort(ByCodepoint(channelModes))
// XXX enumerate these by hand, i can't see any way to DRY this // 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...) channelParametrizedModes = append(channelParametrizedModes, ChannelUserModes...)
sort.Sort(ByCodepoint(channelParametrizedModes)) sort.Sort(ByCodepoint(channelParametrizedModes))
@ -459,7 +460,7 @@ func ChanmodesToken() (result string) {
// type B: modes with parameters // type B: modes with parameters
B := Modes{Key} B := Modes{Key}
// type C: modes that take a parameter only when set, never when unset // 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 // type D: modes without parameters
D := Modes{InviteOnly, Moderated, NoOutside, OpOnlyTopic, ChanRoleplaying, Secret, NoCTCP, RegisteredOnly, RegisteredOnlySpeak, Auditorium, OpModerated} D := Modes{InviteOnly, Moderated, NoOutside, OpOnlyTopic, ChanRoleplaying, Secret, NoCTCP, RegisteredOnly, RegisteredOnlySpeak, Auditorium, OpModerated}

View File

@ -149,6 +149,7 @@ const (
ERR_YOUWILLBEBANNED = "466" ERR_YOUWILLBEBANNED = "466"
ERR_KEYSET = "467" ERR_KEYSET = "467"
ERR_INVALIDUSERNAME = "468" ERR_INVALIDUSERNAME = "468"
ERR_LINKCHANNEL = "470"
ERR_CHANNELISFULL = "471" ERR_CHANNELISFULL = "471"
ERR_UNKNOWNMODE = "472" ERR_UNKNOWNMODE = "472"
ERR_INVITEONLYCHAN = "473" ERR_INVITEONLYCHAN = "473"