diff --git a/DEVELOPING.md b/DEVELOPING.md index 63aba519..3accd60f 100644 --- a/DEVELOPING.md +++ b/DEVELOPING.md @@ -184,3 +184,14 @@ We also support grabbing translations directly from CrowdIn. To do this: 4. Run `crowdin download` This will download a bunch of updated files and put them in the right place + + +## Adding a mode + +When adding a mode, keep in mind the following places it may need to be referenced: + +1. The mode needs to be defined in the `irc/modes` subpackage +1. It may need to be special-cased in `modes.RplMyInfo()` +1. It may need to be added to the `CHANMODES` ISUPPORT token +1. It may need special handling in `ApplyUserModeChanges` or `ApplyChannelModeChanges` +1. It may need special persistence handling code diff --git a/irc/config.go b/irc/config.go index 53d59e16..f169522c 100644 --- a/irc/config.go +++ b/irc/config.go @@ -1287,7 +1287,7 @@ func (config *Config) generateISupport() (err error) { isupport.Add("BOT", "B") isupport.Add("CASEMAPPING", "ascii") isupport.Add("CHANLIMIT", fmt.Sprintf("%s:%d", chanTypes, config.Channels.MaxChannelsPerClient)) - isupport.Add("CHANMODES", strings.Join([]string{modes.Modes{modes.BanMask, modes.ExceptMask, modes.InviteMask}.String(), modes.Modes{modes.Key}.String(), modes.Modes{modes.UserLimit}.String(), modes.Modes{modes.InviteOnly, modes.Moderated, modes.NoOutside, modes.OpOnlyTopic, modes.ChanRoleplaying, modes.Secret, modes.NoCTCP, modes.RegisteredOnly, modes.RegisteredOnlySpeak}.String()}, ",")) + isupport.Add("CHANMODES", chanmodesToken) if config.History.Enabled && config.History.ChathistoryMax > 0 { isupport.Add("draft/CHATHISTORY", strconv.Itoa(config.History.ChathistoryMax)) } diff --git a/irc/modes/modes.go b/irc/modes/modes.go index 5fa51109..cf7cc1f2 100644 --- a/irc/modes/modes.go +++ b/irc/modes/modes.go @@ -6,6 +6,7 @@ package modes import ( + "fmt" "sort" "strings" @@ -450,3 +451,22 @@ func RplMyInfo() (param1, param2, param3 string) { return userModes.String(), channelModes.String(), channelParametrizedModes.String() } + +func ChanmodesToken() (result string) { + // https://modern.ircdocs.horse#chanmodes-parameter + // type A: listable modes with parameters + A := Modes{BanMask, ExceptMask, InviteMask} + // type B: modes with parameters + B := Modes{Key} + // type C: modes that take a parameter only when set, never when unset + C := Modes{UserLimit} + // type D: modes without parameters + D := Modes{InviteOnly, Moderated, NoOutside, OpOnlyTopic, ChanRoleplaying, Secret, NoCTCP, RegisteredOnly, RegisteredOnlySpeak, Auditorium, OpModerated} + + sort.Sort(ByCodepoint(A)) + sort.Sort(ByCodepoint(B)) + sort.Sort(ByCodepoint(C)) + sort.Sort(ByCodepoint(D)) + + return fmt.Sprintf("%s,%s,%s,%s", A.String(), B.String(), C.String(), D.String()) +} diff --git a/irc/modes/modes_test.go b/irc/modes/modes_test.go index 858fa836..27f04888 100644 --- a/irc/modes/modes_test.go +++ b/irc/modes/modes_test.go @@ -5,6 +5,7 @@ package modes import ( "reflect" + "strings" "testing" ) @@ -219,6 +220,15 @@ func TestHighestChannelUserMode(t *testing.T) { } } +func TestChanmodesToken(t *testing.T) { + tok := ChanmodesToken() + for _, mode := range SupportedChannelModes { + if strings.IndexRune(tok, rune(mode)) == -1 { + t.Errorf("+%s not included in ChanmodesToken()", mode) + } + } +} + func TestModeChangesString(t *testing.T) { m := ModeChanges{ ModeChange{Op: Add, Mode: RegisteredOnly}, diff --git a/irc/server.go b/irc/server.go index 328166f7..9e679e01 100644 --- a/irc/server.go +++ b/irc/server.go @@ -39,6 +39,9 @@ var ( // three final parameters of 004 RPL_MYINFO, enumerating our supported modes rplMyInfo1, rplMyInfo2, rplMyInfo3 = modes.RplMyInfo() + // CHANMODES isupport token + chanmodesToken = modes.ChanmodesToken() + // whitelist of caps to serve on the STS-only listener. In particular, // never advertise SASL, to discourage people from sending their passwords: stsOnlyCaps = caps.NewSet(caps.STS, caps.MessageTags, caps.ServerTime, caps.Batch, caps.LabeledResponse, caps.EchoMessage, caps.Nope)