diff --git a/irc/client.go b/irc/client.go index b2b2a0e6..10a3be2a 100644 --- a/irc/client.go +++ b/irc/client.go @@ -337,7 +337,6 @@ func (server *Server) RunClient(conn IRCConn) { session.idletimer.Initialize(session) session.resetFakelag() - ApplyUserModeChanges(client, config.Accounts.defaultUserModes, false, nil) if wConn.Secure { client.SetMode(modes.TLS, true) } diff --git a/irc/config.go b/irc/config.go index 277413e1..d997f6d4 100644 --- a/irc/config.go +++ b/irc/config.go @@ -256,7 +256,7 @@ type AccountConfig struct { exemptedNets []net.IPNet } `yaml:"require-sasl"` DefaultUserModes *string `yaml:"default-user-modes"` - defaultUserModes modes.ModeChanges + defaultUserModes modes.Modes LDAP ldap.ServerConfig LoginThrottling ThrottleConfig `yaml:"login-throttling"` SkipServerPassword bool `yaml:"skip-server-password"` diff --git a/irc/handlers.go b/irc/handlers.go index 53fa2486..b9810b25 100644 --- a/irc/handlers.go +++ b/irc/handlers.go @@ -2724,6 +2724,8 @@ func whoisHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Res } clientNick := client.Nick() rb.Add(nil, client.server.name, RPL_WHOISUSER, clientNick, service.Name, service.Name, "localhost", "*", fmt.Sprintf(client.t("Network service, for more info /msg %s HELP"), service.Name)) + // #1080: + rb.Add(nil, client.server.name, RPL_WHOISOPERATOR, clientNick, service.Name, client.t("is a network service")) // hehe if client.HasMode(modes.TLS) { rb.Add(nil, client.server.name, RPL_WHOISSECURE, clientNick, service.Name, client.t("is using a secure connection")) diff --git a/irc/modes.go b/irc/modes.go index 06dd8e51..acea6b9d 100644 --- a/irc/modes.go +++ b/irc/modes.go @@ -23,7 +23,7 @@ var ( // DefaultUserModes are set on all users when they login. // this can be overridden in the `accounts` config, with the `default-user-modes` key - DefaultUserModes = modes.ModeChanges{} + DefaultUserModes = modes.Modes{} ) // ApplyUserModeChanges applies the given changes, and returns the applied changes. @@ -110,32 +110,35 @@ func ApplyUserModeChanges(client *Client, changes modes.ModeChanges, force bool, return applied } +// parseDefaultModes uses the provided mode change parser to parse the rawModes. +func parseDefaultModes(rawModes string, parser func(params ...string) (modes.ModeChanges, map[rune]bool)) modes.Modes { + modeChangeStrings := strings.Fields(rawModes) + modeChanges, _ := parser(modeChangeStrings...) + defaultModes := make(modes.Modes, 0) + for _, modeChange := range modeChanges { + if modeChange.Op == modes.Add { + defaultModes = append(defaultModes, modeChange.Mode) + } + } + return defaultModes +} + // ParseDefaultChannelModes parses the `default-modes` line of the config func ParseDefaultChannelModes(rawModes *string) modes.Modes { if rawModes == nil { // not present in config, fall back to compile-time default return DefaultChannelModes } - modeChangeStrings := strings.Fields(*rawModes) - modeChanges, _ := modes.ParseChannelModeChanges(modeChangeStrings...) - defaultChannelModes := make(modes.Modes, 0) - for _, modeChange := range modeChanges { - if modeChange.Op == modes.Add { - defaultChannelModes = append(defaultChannelModes, modeChange.Mode) - } - } - return defaultChannelModes + return parseDefaultModes(*rawModes, modes.ParseChannelModeChanges) } // ParseDefaultUserModes parses the `default-user-modes` line of the config -func ParseDefaultUserModes(rawModes *string) modes.ModeChanges { +func ParseDefaultUserModes(rawModes *string) modes.Modes { if rawModes == nil { // not present in config, fall back to compile-time default return DefaultUserModes } - modeChangeStrings := strings.Fields(*rawModes) - modeChanges, _ := modes.ParseUserModeChanges(modeChangeStrings...) - return modeChanges + return parseDefaultModes(*rawModes, modes.ParseUserModeChanges) } // #1021: channel key must be valid as a non-final parameter diff --git a/irc/modes/modes.go b/irc/modes/modes.go index 9e311298..ae2d9224 100644 --- a/irc/modes/modes.go +++ b/irc/modes/modes.go @@ -6,6 +6,7 @@ package modes import ( + "sort" "strings" "github.com/oragono/oragono/irc/utils" @@ -418,3 +419,27 @@ func (set *ModeSet) HighestChannelUserMode() (result Mode) { } return } + +type ByCodepoint Modes + +func (a ByCodepoint) Len() int { return len(a) } +func (a ByCodepoint) Swap(i, j int) { a[i], a[j] = a[j], a[i] } +func (a ByCodepoint) Less(i, j int) bool { return a[i] < a[j] } + +func RplMyInfo() (param1, param2, param3 string) { + userModes := make(Modes, len(SupportedUserModes)) + copy(userModes, SupportedUserModes) + sort.Sort(ByCodepoint(userModes)) + + channelModes := make(Modes, len(SupportedChannelModes)+len(ChannelUserModes)) + copy(channelModes, SupportedChannelModes) + copy(channelModes[len(SupportedChannelModes):], ChannelUserModes) + 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 = append(channelParametrizedModes, ChannelUserModes...) + sort.Sort(ByCodepoint(channelParametrizedModes)) + + return userModes.String(), channelModes.String(), channelParametrizedModes.String() +} diff --git a/irc/modes_test.go b/irc/modes_test.go index ca7aa6a8..005d0555 100644 --- a/irc/modes_test.go +++ b/irc/modes_test.go @@ -43,19 +43,19 @@ func TestParseDefaultUserModes(t *testing.T) { var parseTests = []struct { raw *string - expected modes.ModeChanges + expected modes.Modes }{ - {&iR, modes.ModeChanges{{Mode: modes.Invisible, Op: modes.Add}, {Mode: modes.RegisteredOnly, Op: modes.Add}}}, - {&i, modes.ModeChanges{{Mode: modes.Invisible, Op: modes.Add}}}, - {&empty, modes.ModeChanges{}}, - {&rminusi, modes.ModeChanges{{Mode: modes.RegisteredOnly, Op: modes.Add}}}, - {nil, modes.ModeChanges{}}, + {&iR, modes.Modes{modes.Invisible, modes.RegisteredOnly}}, + {&i, modes.Modes{modes.Invisible}}, + {&empty, modes.Modes{}}, + {&rminusi, modes.Modes{modes.RegisteredOnly}}, + {nil, modes.Modes{}}, } for _, testcase := range parseTests { result := ParseDefaultUserModes(testcase.raw) if !reflect.DeepEqual(result, testcase.expected) { - t.Errorf("expected modes %v, got %v", testcase.expected, result) + t.Errorf("expected modes %s, got %s", testcase.expected, result) } } } diff --git a/irc/nickname.go b/irc/nickname.go index 20f493f9..2cf52e4f 100644 --- a/irc/nickname.go +++ b/irc/nickname.go @@ -18,6 +18,9 @@ import ( var ( restrictedNicknames = []string{ "=scene=", // used for rp commands + "Global", // global announcements on some networks + // common services not implemented by us: + "MemoServ", "BotServ", "OperServ", } restrictedCasefoldedNicks = make(map[string]bool) diff --git a/irc/server.go b/irc/server.go index 17a279a1..f60c0fc2 100644 --- a/irc/server.go +++ b/irc/server.go @@ -36,10 +36,8 @@ var ( // common error line to sub values into errorMsg = "ERROR :%s\r\n" - // supportedUserModesString acts as a cache for when we introduce users - supportedUserModesString = modes.SupportedUserModes.String() - // supportedChannelModesString acts as a cache for when we introduce users - supportedChannelModesString = modes.SupportedChannelModes.String() + // three final parameters of 004 RPL_MYINFO, enumerating our supported modes + rplMyInfo1, rplMyInfo2, rplMyInfo3 = modes.RplMyInfo() // whitelist of caps to serve on the STS-only listener. In particular, // never advertise SASL, to discourage people from sending their passwords: @@ -266,11 +264,18 @@ func (server *Server) tryRegister(c *Client, session *Session) (exiting bool) { return true } + // Apply default user modes (without updating the invisible counter) + // The number of invisible users will be updated by server.stats.Register + // if we're using default user mode +i. + for _, defaultMode := range server.Config().Accounts.defaultUserModes { + c.SetMode(defaultMode, true) + } + // registration has succeeded: c.SetRegistered() // count new user in statistics - server.stats.Register() + server.stats.Register(c.HasMode(modes.Invisible)) server.monitorManager.AlertAbout(c.Nick(), c.NickCasefolded(), true) server.playRegistrationBurst(session) @@ -290,8 +295,7 @@ func (server *Server) playRegistrationBurst(session *Session) { session.Send(nil, server.name, RPL_WELCOME, d.nick, fmt.Sprintf(c.t("Welcome to the Internet Relay Network %s"), d.nick)) session.Send(nil, server.name, RPL_YOURHOST, d.nick, fmt.Sprintf(c.t("Your host is %[1]s, running version %[2]s"), server.name, Ver)) session.Send(nil, server.name, RPL_CREATED, d.nick, fmt.Sprintf(c.t("This server was created %s"), server.ctime.Format(time.RFC1123))) - //TODO(dan): Look at adding last optional [] parameter - session.Send(nil, server.name, RPL_MYINFO, d.nick, server.name, Ver, supportedUserModesString, supportedChannelModesString) + session.Send(nil, server.name, RPL_MYINFO, d.nick, server.name, Ver, rplMyInfo1, rplMyInfo2, rplMyInfo3) if c.isSTSOnly { for _, line := range server.Config().Server.STS.bannerLines { diff --git a/irc/stats.go b/irc/stats.go index a54f7269..ec765177 100644 --- a/irc/stats.go +++ b/irc/stats.go @@ -41,9 +41,12 @@ func (s *Stats) AddRegistered(invisible, operator bool) { } // Transition a client from unregistered to registered -func (s *Stats) Register() { +func (s *Stats) Register(invisible bool) { s.mutex.Lock() s.Unknown -= 1 + if invisible { + s.Invisible += 1 + } s.Total += 1 s.setMax() s.mutex.Unlock() diff --git a/irc/utils/args.go b/irc/utils/args.go index 64475da1..ea3f0c96 100644 --- a/irc/utils/args.go +++ b/irc/utils/args.go @@ -35,7 +35,7 @@ func ArgsToStrings(maxLength int, arguments []string, delim string) []string { continue } - if len(buffer) > 1 { + if len(buffer) > 0 { buffer += delim } buffer += arguments[0] diff --git a/irc/utils/args_test.go b/irc/utils/args_test.go index 5c84b26a..b846d122 100644 --- a/irc/utils/args_test.go +++ b/irc/utils/args_test.go @@ -5,6 +5,14 @@ package utils import "testing" +func TestArgsToStrings(t *testing.T) { + val := ArgsToStrings(512, []string{"a", "b", "c"}, ",") + assertEqual(val, []string{"a,b,c"}, t) + + val = ArgsToStrings(10, []string{"abcd", "efgh", "ijkl"}, ",") + assertEqual(val, []string{"abcd,efgh", "ijkl"}, t) +} + func TestStringToBool(t *testing.T) { val, err := StringToBool("on") assertEqual(val, true, t) diff --git a/languages/ro-RO-irc.lang.json b/languages/ro-RO-irc.lang.json index da024063..9b2a5c81 100644 --- a/languages/ro-RO-irc.lang.json +++ b/languages/ro-RO-irc.lang.json @@ -235,7 +235,7 @@ "That nickname is already reserved by someone else": "Pseudonimul este rezervat de altcineva", "That nickname is not registered": "Pseudonimul nu este înregistrat", "That vhost isn't being offered by the server": "Gazda virtuală nu este oferită de către server", - "The following vhosts are available and can be chosen with /HOSTSERV TAKE:": "", + "The following vhosts are available and can be chosen with /HOSTSERV TAKE:": "Următoarele gazde virtuale sunt disponibile și pot fi alese cu comanda /HOSTSERV TAKE:", "The server does not offer any vhosts": "Serverul nu oferă nici o gazdă virtuală", "The server does not offer any vhosts, but you can request one with /HOSTSERV REQUEST": "Serverul nu oferă nici o gazdă virtuală, actualmente. Poți cere una, cu comanda /HOSTSERV REQUEST", "The stored channel history setting is: %s": "Setarea pentru stocarea istoricului mesajelor canalului este: %s", @@ -317,7 +317,7 @@ "You're not logged into an account": "Nu te-ai autentificat la niciun cont", "You're not on that channel": "Nu te afli pe acel canal", "You're now logged in as %s": "Te-ai autentificat ca: %s", - "Your account credentials are managed externally and cannot be changed here": "", + "Your account credentials are managed externally and cannot be changed here": "Credențialele contului tău sunt administrate extern și nu pot fi modificate aici", "Your account is not configured to receive autoreplayed missed messages": "Contul tău nu este configurat pentru derularea automată a mesajelor pierdute", "Your client does not support BRB": "Clientul folosit de tine nu suportă BRB", "Your host is %[1]s, running version %[2]s": "Gazda ta este %[1]s, rulând versiunea %[2]s",