From 25974b6881112b30583cceeab8a3e5fd6cd7a817 Mon Sep 17 00:00:00 2001 From: Shivaram Lingamneni Date: Sun, 19 May 2019 02:14:36 -0400 Subject: [PATCH 1/5] fix #487 --- irc/client.go | 12 ++++++++++-- irc/handlers.go | 9 +++++++-- 2 files changed, 17 insertions(+), 4 deletions(-) diff --git a/irc/client.go b/irc/client.go index 3bd371ca..33dfe9a1 100644 --- a/irc/client.go +++ b/irc/client.go @@ -600,7 +600,8 @@ func (client *Client) tryResumeChannels() { func (client *Client) replayPrivmsgHistory(rb *ResponseBuffer, items []history.Item, complete bool) { var batchID string - nick := client.Nick() + details := client.Details() + nick := details.nick if 0 < len(items) { batchID = rb.StartNestedHistoryBatch(nick) } @@ -626,7 +627,14 @@ func (client *Client) replayPrivmsgHistory(rb *ResponseBuffer, items []history.I if allowTags { tags = item.Tags } - rb.AddSplitMessageFromClient(item.Nick, item.AccountName, tags, command, nick, item.Message) + if item.Params[0] == "" { + // this message was sent *to* the client from another nick + rb.AddSplitMessageFromClient(item.Nick, item.AccountName, tags, command, nick, item.Message) + } else { + // this message was sent *from* the client to another nick; the target is item.Params[0] + // substitute the client's current nickmask in case they changed nick + rb.AddSplitMessageFromClient(details.nickMask, item.AccountName, tags, command, item.Params[0], item.Message) + } } rb.EndNestedBatch(batchID) diff --git a/irc/handlers.go b/irc/handlers.go index 0745074d..88abf70e 100644 --- a/irc/handlers.go +++ b/irc/handlers.go @@ -2060,12 +2060,17 @@ func messageHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *R rb.Add(nil, server.name, RPL_AWAY, cnick, tnick, user.AwayMessage()) } - user.history.Add(history.Item{ + item := history.Item{ Type: histType, Message: splitMsg, Nick: nickMaskString, AccountName: accountName, - }) + } + // add to the target's history: + user.history.Add(item) + // add this to the client's history as well, recording the target: + item.Params[0] = tnick + client.history.Add(item) } } return false From 8fc588375b0202f55f63c3f6623612ddf0a2f73e Mon Sep 17 00:00:00 2001 From: Shivaram Lingamneni Date: Sun, 19 May 2019 04:27:44 -0400 Subject: [PATCH 2/5] implement user preferences system --- irc/accounts.go | 213 ++++++++++++++++++----------- irc/caps/set.go | 6 - irc/channel.go | 23 +++- irc/chanserv.go | 2 +- irc/client.go | 1 + irc/client_lookup_set.go | 17 ++- irc/config.go | 54 ++++---- irc/database.go | 38 +++++- irc/errors.go | 4 +- irc/getters.go | 13 ++ irc/handlers.go | 4 +- irc/hostserv.go | 2 +- irc/idletimer.go | 6 +- irc/nickserv.go | 285 +++++++++++++++++++++++++++++++++++---- irc/services.go | 12 +- oragono.yaml | 2 +- 16 files changed, 515 insertions(+), 167 deletions(-) diff --git a/irc/accounts.go b/irc/accounts.go index c617f14b..81dfecb4 100644 --- a/irc/accounts.go +++ b/irc/accounts.go @@ -29,7 +29,7 @@ const ( keyAccountRegTime = "account.registered.time %s" keyAccountCredentials = "account.credentials %s" keyAccountAdditionalNicks = "account.additionalnicks %s" - keyAccountEnforcement = "account.customenforcement %s" + keyAccountSettings = "account.settings %s" keyAccountVHost = "account.vhost %s" keyCertToAccount = "account.creds.certfp %s" keyAccountChannels = "account.channels %s" @@ -55,14 +55,14 @@ type AccountManager struct { accountToClients map[string][]*Client nickToAccount map[string]string skeletonToAccount map[string]string - accountToMethod map[string]NickReservationMethod + accountToMethod map[string]NickEnforcementMethod } func (am *AccountManager) Initialize(server *Server) { am.accountToClients = make(map[string][]*Client) am.nickToAccount = make(map[string]string) am.skeletonToAccount = make(map[string]string) - am.accountToMethod = make(map[string]NickReservationMethod) + am.accountToMethod = make(map[string]NickEnforcementMethod) am.server = server am.buildNickToAccountIndex() @@ -76,7 +76,7 @@ func (am *AccountManager) buildNickToAccountIndex() { nickToAccount := make(map[string]string) skeletonToAccount := make(map[string]string) - accountToMethod := make(map[string]NickReservationMethod) + accountToMethod := make(map[string]NickEnforcementMethod) existsPrefix := fmt.Sprintf(keyAccountExists, "") am.serialCacheUpdateMutex.Lock() @@ -109,12 +109,16 @@ func (am *AccountManager) buildNickToAccountIndex() { } } - if methodStr, err := tx.Get(fmt.Sprintf(keyAccountEnforcement, account)); err == nil { - method, err := nickReservationFromString(methodStr) - if err == nil { - accountToMethod[account] = method + if rawPrefs, err := tx.Get(fmt.Sprintf(keyAccountSettings, account)); err == nil { + var prefs AccountSettings + err := json.Unmarshal([]byte(rawPrefs), &prefs) + if err == nil && prefs.NickEnforcement != NickEnforcementOptional { + accountToMethod[account] = prefs.NickEnforcement + } else { + am.server.logger.Error("internal", "corrupt account creds", account) } } + return true }) return err @@ -180,36 +184,44 @@ func (am *AccountManager) NickToAccount(nick string) string { return am.nickToAccount[cfnick] } +// given an account, combine stored enforcement method with the config settings +// to compute the actual enforcement method +func configuredEnforcementMethod(config *Config, storedMethod NickEnforcementMethod) (result NickEnforcementMethod) { + if !config.Accounts.NickReservation.Enabled { + return NickEnforcementNone + } + result = storedMethod + // if they don't have a custom setting, or customization is disabled, use the default + if result == NickEnforcementOptional || !config.Accounts.NickReservation.AllowCustomEnforcement { + result = config.Accounts.NickReservation.Method + } + if result == NickEnforcementOptional { + // enforcement was explicitly enabled neither in the config or by the user + result = NickEnforcementNone + } + return +} + // Given a nick, looks up the account that owns it and the method (none/timeout/strict) // used to enforce ownership. -func (am *AccountManager) EnforcementStatus(cfnick, skeleton string) (account string, method NickReservationMethod) { +func (am *AccountManager) EnforcementStatus(cfnick, skeleton string) (account string, method NickEnforcementMethod) { config := am.server.Config() if !config.Accounts.NickReservation.Enabled { - return "", NickReservationNone + return "", NickEnforcementNone } am.RLock() defer am.RUnlock() - // given an account, combine stored enforcement method with the config settings - // to compute the actual enforcement method - finalEnforcementMethod := func(account_ string) (result NickReservationMethod) { - result = am.accountToMethod[account_] - // if they don't have a custom setting, or customization is disabled, use the default - if result == NickReservationOptional || !config.Accounts.NickReservation.AllowCustomEnforcement { - result = config.Accounts.NickReservation.Method - } - if result == NickReservationOptional { - // enforcement was explicitly enabled neither in the config or by the user - result = NickReservationNone - } - return + finalEnforcementMethod := func(account_ string) (result NickEnforcementMethod) { + storedMethod := am.accountToMethod[account_] + return configuredEnforcementMethod(config, storedMethod) } nickAccount := am.nickToAccount[cfnick] skelAccount := am.skeletonToAccount[skeleton] if nickAccount == "" && skelAccount == "" { - return "", NickReservationNone + return "", NickEnforcementNone } else if nickAccount != "" && (skelAccount == nickAccount || skelAccount == "") { return nickAccount, finalEnforcementMethod(nickAccount) } else if skelAccount != "" && nickAccount == "" { @@ -220,75 +232,47 @@ func (am *AccountManager) EnforcementStatus(cfnick, skeleton string) (account st nickMethod := finalEnforcementMethod(nickAccount) skelMethod := finalEnforcementMethod(skelAccount) switch { - case skelMethod == NickReservationNone: + case skelMethod == NickEnforcementNone: return nickAccount, nickMethod - case nickMethod == NickReservationNone: + case nickMethod == NickEnforcementNone: return skelAccount, skelMethod default: // nobody can use this nick - return "!", NickReservationStrict + return "!", NickEnforcementStrict } } } -func (am *AccountManager) BouncerAllowed(account string, session *Session) bool { - // TODO stub - config := am.server.Config() - if !config.Accounts.Bouncer.Enabled { - return false - } - if config.Accounts.Bouncer.AllowedByDefault { - return true - } - return session != nil && session.capabilities.Has(caps.Bouncer) -} - -// Looks up the enforcement method stored in the database for an account -// (typically you want EnforcementStatus instead, which respects the config) -func (am *AccountManager) getStoredEnforcementStatus(account string) string { - am.RLock() - defer am.RUnlock() - return nickReservationToString(am.accountToMethod[account]) -} - // Sets a custom enforcement method for an account and stores it in the database. -func (am *AccountManager) SetEnforcementStatus(account string, method NickReservationMethod) (err error) { +func (am *AccountManager) SetEnforcementStatus(account string, method NickEnforcementMethod) (finalSettings AccountSettings, err error) { config := am.server.Config() if !(config.Accounts.NickReservation.Enabled && config.Accounts.NickReservation.AllowCustomEnforcement) { - return errFeatureDisabled + err = errFeatureDisabled + return } - var serialized string - if method == NickReservationOptional { - serialized = "" // normally this is "default", but we're going to delete the key - } else { - serialized = nickReservationToString(method) + setter := func(in AccountSettings) (out AccountSettings, err error) { + out = in + out.NickEnforcement = method + return out, nil } - key := fmt.Sprintf(keyAccountEnforcement, account) + _, err = am.ModifyAccountSettings(account, setter) + if err != nil { + return + } + // this update of the data plane is racey, but it's probably fine am.Lock() defer am.Unlock() - currentMethod := am.accountToMethod[account] - if method != currentMethod { - if method == NickReservationOptional { - delete(am.accountToMethod, account) - } else { - am.accountToMethod[account] = method - } - - return am.server.store.Update(func(tx *buntdb.Tx) (err error) { - if serialized != "" { - _, _, err = tx.Set(key, nickReservationToString(method), nil) - } else { - _, err = tx.Delete(key) - } - return - }) + if method == NickEnforcementOptional { + delete(am.accountToMethod, account) + } else { + am.accountToMethod[account] = method } - return nil + return } func (am *AccountManager) AccountToClients(account string) (result []*Client) { @@ -813,6 +797,12 @@ func (am *AccountManager) deserializeRawAccount(raw rawClientAccount) (result Cl // pretend they have no vhost and move on } } + if raw.Settings != "" { + e := json.Unmarshal([]byte(raw.Settings), &result.Settings) + if e != nil { + am.server.logger.Warning("internal", "could not unmarshal settings for account", result.Name, e.Error()) + } + } return } @@ -825,6 +815,7 @@ func (am *AccountManager) loadRawAccount(tx *buntdb.Tx, casefoldedAccount string callbackKey := fmt.Sprintf(keyAccountCallback, casefoldedAccount) nicksKey := fmt.Sprintf(keyAccountAdditionalNicks, casefoldedAccount) vhostKey := fmt.Sprintf(keyAccountVHost, casefoldedAccount) + settingsKey := fmt.Sprintf(keyAccountSettings, casefoldedAccount) _, e := tx.Get(accountKey) if e == buntdb.ErrNotFound { @@ -838,6 +829,7 @@ func (am *AccountManager) loadRawAccount(tx *buntdb.Tx, casefoldedAccount string result.Callback, _ = tx.Get(callbackKey) result.AdditionalNicks, _ = tx.Get(nicksKey) result.VHost, _ = tx.Get(vhostKey) + result.Settings, _ = tx.Get(settingsKey) if _, e = tx.Get(verifiedKey); e == nil { result.Verified = true @@ -861,7 +853,7 @@ func (am *AccountManager) Unregister(account string) error { verificationCodeKey := fmt.Sprintf(keyAccountVerificationCode, casefoldedAccount) verifiedKey := fmt.Sprintf(keyAccountVerified, casefoldedAccount) nicksKey := fmt.Sprintf(keyAccountAdditionalNicks, casefoldedAccount) - enforcementKey := fmt.Sprintf(keyAccountEnforcement, casefoldedAccount) + settingsKey := fmt.Sprintf(keyAccountSettings, casefoldedAccount) vhostKey := fmt.Sprintf(keyAccountVHost, casefoldedAccount) vhostQueueKey := fmt.Sprintf(keyVHostQueueAcctToId, casefoldedAccount) channelsKey := fmt.Sprintf(keyAccountChannels, casefoldedAccount) @@ -892,7 +884,7 @@ func (am *AccountManager) Unregister(account string) error { tx.Delete(registeredTimeKey) tx.Delete(callbackKey) tx.Delete(verificationCodeKey) - tx.Delete(enforcementKey) + tx.Delete(settingsKey) rawNicks, _ = tx.Get(nicksKey) tx.Delete(nicksKey) credText, err = tx.Get(credentialsKey) @@ -980,19 +972,13 @@ func (am *AccountManager) AuthenticateByCertFP(client *Client) error { } var account string - var rawAccount rawClientAccount certFPKey := fmt.Sprintf(keyCertToAccount, client.certfp) - err := am.server.store.Update(func(tx *buntdb.Tx) error { - var err error + err := am.server.store.View(func(tx *buntdb.Tx) error { account, _ = tx.Get(certFPKey) if account == "" { return errAccountInvalidCredentials } - rawAccount, err = am.loadRawAccount(tx, account) - if err != nil || !rawAccount.Verified { - return errAccountUnverified - } return nil }) @@ -1001,14 +987,57 @@ func (am *AccountManager) AuthenticateByCertFP(client *Client) error { } // ok, we found an account corresponding to their certificate - clientAccount, err := am.deserializeRawAccount(rawAccount) + clientAccount, err := am.LoadAccount(account) if err != nil { return err + } else if !clientAccount.Verified { + return errAccountUnverified } am.Login(client, clientAccount) return nil } +type settingsMunger func(input AccountSettings) (output AccountSettings, err error) + +func (am *AccountManager) ModifyAccountSettings(account string, munger settingsMunger) (newSettings AccountSettings, err error) { + casefoldedAccount, err := CasefoldName(account) + if err != nil { + return newSettings, errAccountDoesNotExist + } + // TODO implement this in general via a compare-and-swap API + accountData, err := am.LoadAccount(casefoldedAccount) + if err != nil { + return + } else if !accountData.Verified { + return newSettings, errAccountUnverified + } + newSettings, err = munger(accountData.Settings) + if err != nil { + return + } + text, err := json.Marshal(newSettings) + if err != nil { + return + } + key := fmt.Sprintf(keyAccountSettings, casefoldedAccount) + serializedValue := string(text) + err = am.server.store.Update(func(tx *buntdb.Tx) (err error) { + _, _, err = tx.Set(key, serializedValue, nil) + return + }) + if err != nil { + err = errAccountUpdateFailed + return + } + // success, push new settings into the client objects + am.Lock() + defer am.Unlock() + for _, client := range am.accountToClients[casefoldedAccount] { + client.SetAccountSettings(newSettings) + } + return +} + // represents someone's status in hostserv type VHostInfo struct { ApprovedVHost string @@ -1237,6 +1266,9 @@ func (am *AccountManager) Login(client *Client, account ClientAccount) { am.Lock() defer am.Unlock() am.accountToClients[casefoldedAccount] = append(am.accountToClients[casefoldedAccount], client) + for _, client := range am.accountToClients[casefoldedAccount] { + client.SetAccountSettings(account.Settings) + } } func (am *AccountManager) Logout(client *Client) { @@ -1283,6 +1315,21 @@ type AccountCredentials struct { Certificate string // fingerprint } +type BouncerAllowedSetting int + +const ( + BouncerAllowedServerDefault BouncerAllowedSetting = iota + BouncerDisallowedByUser + BouncerAllowedByUser +) + +type AccountSettings struct { + AutoreplayLines *int + NickEnforcement NickEnforcementMethod + AllowBouncer BouncerAllowedSetting + AutoreplayJoins bool +} + // ClientAccount represents a user account. type ClientAccount struct { // Name of the account. @@ -1293,6 +1340,7 @@ type ClientAccount struct { Verified bool AdditionalNicks []string VHost VHostInfo + Settings AccountSettings } // convenience for passing around raw serialized account data @@ -1304,6 +1352,7 @@ type rawClientAccount struct { Verified bool AdditionalNicks string VHost string + Settings string } // logoutOfAccount logs the client out of their current account. diff --git a/irc/caps/set.go b/irc/caps/set.go index 12b6fd16..0e5f7949 100644 --- a/irc/caps/set.go +++ b/irc/caps/set.go @@ -114,9 +114,3 @@ func (s *Set) String(version Version, values *Values) string { return strings.Join(strs, " ") } - -// returns whether we should send `znc.in/self-message`-style echo messages -// to sessions other than that which originated the message -func (capabs *Set) SelfMessagesEnabled() bool { - return capabs.Has(EchoMessage) || capabs.Has(ZNCSelfMessage) -} diff --git a/irc/channel.go b/irc/channel.go index 90306380..48e51cef 100644 --- a/irc/channel.go +++ b/irc/channel.go @@ -620,7 +620,17 @@ func (channel *Channel) Join(client *Client, key string, isSajoin bool, rb *Resp // TODO #259 can be implemented as Flush(false) (i.e., nonblocking) while holding joinPartMutex rb.Flush(true) - replayLimit := channel.server.Config().History.AutoreplayOnJoin + var replayLimit int + customReplayLimit := client.AccountSettings().AutoreplayLines + if customReplayLimit != nil { + replayLimit = *customReplayLimit + maxLimit := channel.server.Config().History.ChathistoryMax + if maxLimit < replayLimit { + replayLimit = maxLimit + } + } else { + replayLimit = channel.server.Config().History.AutoreplayOnJoin + } if 0 < replayLimit { // TODO don't replay the client's own JOIN line? items := channel.history.Latest(replayLimit) @@ -782,6 +792,7 @@ func (channel *Channel) replayHistoryItems(rb *ResponseBuffer, items []history.I client := rb.target eventPlayback := rb.session.capabilities.Has(caps.EventPlayback) extendedJoin := rb.session.capabilities.Has(caps.ExtendedJoin) + playJoinsAsPrivmsg := (!autoreplay || client.AccountSettings().AutoreplayJoins) if len(items) == 0 { return @@ -808,7 +819,7 @@ func (channel *Channel) replayHistoryItems(rb *ResponseBuffer, items []history.I rb.AddFromClient(item.Message.Time, item.Message.Msgid, item.Nick, item.AccountName, nil, "HEVENT", "JOIN", chname) } } else { - if autoreplay { + if !playJoinsAsPrivmsg { continue // #474 } var message string @@ -823,7 +834,7 @@ func (channel *Channel) replayHistoryItems(rb *ResponseBuffer, items []history.I if eventPlayback { rb.AddFromClient(item.Message.Time, item.Message.Msgid, item.Nick, item.AccountName, nil, "HEVENT", "PART", chname, item.Message.Message) } else { - if autoreplay { + if !playJoinsAsPrivmsg { continue // #474 } message := fmt.Sprintf(client.t("%[1]s left the channel (%[2]s)"), nick, item.Message.Message) @@ -840,7 +851,7 @@ func (channel *Channel) replayHistoryItems(rb *ResponseBuffer, items []history.I if eventPlayback { rb.AddFromClient(item.Message.Time, item.Message.Msgid, item.Nick, item.AccountName, nil, "HEVENT", "QUIT", item.Message.Message) } else { - if autoreplay { + if !playJoinsAsPrivmsg { continue // #474 } message := fmt.Sprintf(client.t("%[1]s quit (%[2]s)"), nick, item.Message.Message) @@ -989,7 +1000,7 @@ func (channel *Channel) SendSplitMessage(command string, minPrefixMode modes.Mod } // send echo-message to other connected sessions for _, session := range client.Sessions() { - if session == rb.session || !session.capabilities.SelfMessagesEnabled() { + if session == rb.session { continue } var tagsToUse map[string]string @@ -998,7 +1009,7 @@ func (channel *Channel) SendSplitMessage(command string, minPrefixMode modes.Mod } if histType == history.Tagmsg && session.capabilities.Has(caps.MessageTags) { session.sendFromClientInternal(false, message.Time, message.Msgid, nickmask, account, tagsToUse, command, chname) - } else { + } else if histType != history.Tagmsg { session.sendSplitMsgFromClientInternal(false, nickmask, account, tagsToUse, command, chname, message) } } diff --git a/irc/chanserv.go b/irc/chanserv.go index b37ba98d..4cfd2bf9 100644 --- a/irc/chanserv.go +++ b/irc/chanserv.go @@ -86,7 +86,7 @@ referenced by their registered account names, not their nicknames.`, // csNotice sends the client a notice from ChanServ func csNotice(rb *ResponseBuffer, text string) { - rb.Add(nil, "ChanServ", "NOTICE", rb.target.Nick(), text) + rb.Add(nil, "ChanServ!ChanServ@localhost", "NOTICE", rb.target.Nick(), text) } func csAmodeHandler(server *Server, client *Client, command string, params []string, rb *ResponseBuffer) { diff --git a/irc/client.go b/irc/client.go index 33dfe9a1..3beb80d6 100644 --- a/irc/client.go +++ b/irc/client.go @@ -48,6 +48,7 @@ type ResumeDetails struct { type Client struct { account string accountName string // display name of the account: uncasefolded, '*' if not logged in + accountSettings AccountSettings atime time.Time away bool awayMessage string diff --git a/irc/client_lookup_set.go b/irc/client_lookup_set.go index 65ac7127..1108f0d1 100644 --- a/irc/client_lookup_set.go +++ b/irc/client_lookup_set.go @@ -145,7 +145,20 @@ func (clients *ClientManager) SetNick(client *Client, session *Session, newNick reservedAccount, method := client.server.accounts.EnforcementStatus(newcfnick, newSkeleton) account := client.Account() - bouncerAllowed := client.server.accounts.BouncerAllowed(account, session) + config := client.server.Config() + var bouncerAllowed bool + if config.Accounts.Bouncer.Enabled { + if session != nil && session.capabilities.Has(caps.Bouncer) { + bouncerAllowed = true + } else { + settings := client.AccountSettings() + if config.Accounts.Bouncer.AllowedByDefault && settings.AllowBouncer != BouncerDisallowedByUser { + bouncerAllowed = true + } else if settings.AllowBouncer == BouncerAllowedByUser { + bouncerAllowed = true + } + } + } clients.Lock() defer clients.Unlock() @@ -168,7 +181,7 @@ func (clients *ClientManager) SetNick(client *Client, session *Session, newNick if skeletonHolder != nil && skeletonHolder != client { return errNicknameInUse } - if method == NickReservationStrict && reservedAccount != "" && reservedAccount != account { + if method == NickEnforcementStrict && reservedAccount != "" && reservedAccount != account { return errNicknameReserved } clients.removeInternal(client) diff --git a/irc/config.go b/irc/config.go index 8aea256b..965aa9d5 100644 --- a/irc/config.go +++ b/irc/config.go @@ -112,63 +112,63 @@ type VHostConfig struct { } `yaml:"user-requests"` } -type NickReservationMethod int +type NickEnforcementMethod int const ( - // NickReservationOptional is the zero value; it serializes to + // NickEnforcementOptional is the zero value; it serializes to // "optional" in the yaml config, and "default" as an arg to `NS ENFORCE`. // in both cases, it means "defer to the other source of truth", i.e., // in the config, defer to the user's custom setting, and as a custom setting, - // defer to the default in the config. if both are NickReservationOptional then + // defer to the default in the config. if both are NickEnforcementOptional then // there is no enforcement. - NickReservationOptional NickReservationMethod = iota - NickReservationNone - NickReservationWithTimeout - NickReservationStrict + // XXX: these are serialized as numbers in the database, so beware of collisions + // when refactoring (any numbers currently in use must keep their meanings, or + // else be fixed up by a schema change) + NickEnforcementOptional NickEnforcementMethod = iota + NickEnforcementNone + NickEnforcementWithTimeout + NickEnforcementStrict ) -func nickReservationToString(method NickReservationMethod) string { +func nickReservationToString(method NickEnforcementMethod) string { switch method { - case NickReservationOptional: + case NickEnforcementOptional: return "default" - case NickReservationNone: + case NickEnforcementNone: return "none" - case NickReservationWithTimeout: + case NickEnforcementWithTimeout: return "timeout" - case NickReservationStrict: + case NickEnforcementStrict: return "strict" default: return "" } } -func nickReservationFromString(method string) (NickReservationMethod, error) { - switch method { +func nickReservationFromString(method string) (NickEnforcementMethod, error) { + switch strings.ToLower(method) { case "default": - return NickReservationOptional, nil + return NickEnforcementOptional, nil case "optional": - return NickReservationOptional, nil + return NickEnforcementOptional, nil case "none": - return NickReservationNone, nil + return NickEnforcementNone, nil case "timeout": - return NickReservationWithTimeout, nil + return NickEnforcementWithTimeout, nil case "strict": - return NickReservationStrict, nil + return NickEnforcementStrict, nil default: - return NickReservationOptional, fmt.Errorf("invalid nick-reservation.method value: %s", method) + return NickEnforcementOptional, fmt.Errorf("invalid nick-reservation.method value: %s", method) } } -func (nr *NickReservationMethod) UnmarshalYAML(unmarshal func(interface{}) error) error { - var orig, raw string +func (nr *NickEnforcementMethod) UnmarshalYAML(unmarshal func(interface{}) error) error { + var orig string var err error if err = unmarshal(&orig); err != nil { return err } - if raw, err = Casefold(orig); err != nil { - return err - } - method, err := nickReservationFromString(raw) + method, err := nickReservationFromString(orig) if err == nil { *nr = method } @@ -178,7 +178,7 @@ func (nr *NickReservationMethod) UnmarshalYAML(unmarshal func(interface{}) error type NickReservationConfig struct { Enabled bool AdditionalNickLimit int `yaml:"additional-nick-limit"` - Method NickReservationMethod + Method NickEnforcementMethod AllowCustomEnforcement bool `yaml:"allow-custom-enforcement"` RenameTimeout time.Duration `yaml:"rename-timeout"` RenamePrefix string `yaml:"rename-prefix"` diff --git a/irc/database.go b/irc/database.go index c85cebc0..62e478b6 100644 --- a/irc/database.go +++ b/irc/database.go @@ -22,7 +22,7 @@ const ( // 'version' of the database schema keySchemaVersion = "db.version" // latest schema of the db - latestDbSchema = "5" + latestDbSchema = "6" ) type SchemaChanger func(*Config, *buntdb.Tx) error @@ -409,6 +409,37 @@ func schemaChangeV4ToV5(config *Config, tx *buntdb.Tx) error { return nil } +// custom nick enforcement was a separate db key, now it's part of settings +func schemaChangeV5ToV6(config *Config, tx *buntdb.Tx) error { + accountToEnforcement := make(map[string]NickEnforcementMethod) + prefix := "account.customenforcement " + tx.AscendGreaterOrEqual("", prefix, func(key, value string) bool { + if !strings.HasPrefix(key, prefix) { + return false + } + account := strings.TrimPrefix(key, prefix) + method, err := nickReservationFromString(value) + if err == nil { + accountToEnforcement[account] = method + } else { + log.Printf("skipping corrupt custom enforcement value for %s\n", account) + } + return true + }) + + for account, method := range accountToEnforcement { + var settings AccountSettings + settings.NickEnforcement = method + text, err := json.Marshal(settings) + if err != nil { + return err + } + tx.Delete(prefix + account) + tx.Set(fmt.Sprintf("account.settings %s", account), string(text), nil) + } + return nil +} + func init() { allChanges := []SchemaChange{ { @@ -431,6 +462,11 @@ func init() { TargetVersion: "5", Changer: schemaChangeV4ToV5, }, + { + InitialVersion: "5", + TargetVersion: "6", + Changer: schemaChangeV5ToV6, + }, } // build the index diff --git a/irc/errors.go b/irc/errors.go index 1fa8e5f7..14e833ab 100644 --- a/irc/errors.go +++ b/irc/errors.go @@ -19,10 +19,10 @@ var ( errAccountNickReservationFailed = errors.New("Could not (un)reserve nick") errAccountNotLoggedIn = errors.New("You're not logged into an account") errAccountTooManyNicks = errors.New("Account has too many reserved nicks") - errAccountUnverified = errors.New("Account is not yet verified") + errAccountUnverified = errors.New(`Account is not yet verified`) errAccountVerificationFailed = errors.New("Account verification failed") errAccountVerificationInvalidCode = errors.New("Invalid account verification code") - errAccountUpdateFailed = errors.New("Error while updating your account information") + errAccountUpdateFailed = errors.New(`Error while updating your account information`) errAccountMustHoldNick = errors.New(`You must hold that nickname in order to register it`) errCallbackFailed = errors.New("Account verification could not be sent") errCertfpAlreadyExists = errors.New(`An account already exists for your certificate fingerprint`) diff --git a/irc/getters.go b/irc/getters.go index d231d66a..13018bba 100644 --- a/irc/getters.go +++ b/irc/getters.go @@ -275,6 +275,19 @@ func (client *Client) SetAccountName(account string) (changed bool) { return } +func (client *Client) AccountSettings() (result AccountSettings) { + client.stateMutex.RLock() + result = client.accountSettings + client.stateMutex.RUnlock() + return +} + +func (client *Client) SetAccountSettings(settings AccountSettings) { + client.stateMutex.Lock() + client.accountSettings = settings + client.stateMutex.Unlock() +} + func (client *Client) Languages() (languages []string) { client.stateMutex.RLock() languages = client.languages diff --git a/irc/handlers.go b/irc/handlers.go index 88abf70e..038d296a 100644 --- a/irc/handlers.go +++ b/irc/handlers.go @@ -2046,12 +2046,12 @@ func messageHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *R } // an echo-message may need to go out to other client sessions: for _, session := range client.Sessions() { - if session == rb.session || !rb.session.capabilities.SelfMessagesEnabled() { + if session == rb.session { continue } if histType == history.Tagmsg && rb.session.capabilities.Has(caps.MessageTags) { session.sendFromClientInternal(false, splitMsg.Time, splitMsg.Msgid, nickMaskString, accountName, clientOnlyTags, msg.Command, tnick) - } else { + } else if histType != history.Tagmsg { session.sendSplitMsgFromClientInternal(false, nickMaskString, accountName, clientOnlyTags, msg.Command, tnick, splitMsg) } } diff --git a/irc/hostserv.go b/irc/hostserv.go index a0d7d49c..cc81fbb6 100644 --- a/irc/hostserv.go +++ b/irc/hostserv.go @@ -131,7 +131,7 @@ for the rejection.`, // hsNotice sends the client a notice from HostServ func hsNotice(rb *ResponseBuffer, text string) { - rb.Add(nil, "HostServ", "NOTICE", rb.target.Nick(), text) + rb.Add(nil, "HostServ!HostServ@localhost", "NOTICE", rb.target.Nick(), text) } // hsNotifyChannel notifies the designated channel of new vhost activity diff --git a/irc/idletimer.go b/irc/idletimer.go index fe158e46..8b7bc8e8 100644 --- a/irc/idletimer.go +++ b/irc/idletimer.go @@ -198,7 +198,7 @@ func (nt *NickTimer) Initialize(client *Client) { } config := &client.server.Config().Accounts.NickReservation - enabled := config.Enabled && (config.Method == NickReservationWithTimeout || config.AllowCustomEnforcement) + enabled := config.Enabled && (config.Method == NickEnforcementWithTimeout || config.AllowCustomEnforcement) nt.Lock() defer nt.Unlock() @@ -235,7 +235,7 @@ func (nt *NickTimer) Touch(rb *ResponseBuffer) { cfnick, skeleton := nt.client.uniqueIdentifiers() account := nt.client.Account() accountForNick, method := nt.client.server.accounts.EnforcementStatus(cfnick, skeleton) - enforceTimeout := method == NickReservationWithTimeout + enforceTimeout := method == NickEnforcementWithTimeout var shouldWarn, shouldRename bool @@ -258,7 +258,7 @@ func (nt *NickTimer) Touch(rb *ResponseBuffer) { if enforceTimeout && delinquent && (accountChanged || nt.timer == nil) { nt.timer = time.AfterFunc(nt.timeout, nt.processTimeout) shouldWarn = true - } else if method == NickReservationStrict && delinquent { + } else if method == NickEnforcementStrict && delinquent { shouldRename = true // this can happen if reservation was enabled by rehash } }() diff --git a/irc/nickserv.go b/irc/nickserv.go index 4a3496ee..1b14b0f0 100644 --- a/irc/nickserv.go +++ b/irc/nickserv.go @@ -5,6 +5,8 @@ package irc import ( "fmt" + "strconv" + "strings" "time" "github.com/goshuirc/irc-go/ircfmt" @@ -25,10 +27,6 @@ func servCmdRequiresNickRes(config *Config) bool { return config.Accounts.AuthenticationEnabled && config.Accounts.NickReservation.Enabled } -func nsEnforceEnabled(config *Config) bool { - return servCmdRequiresNickRes(config) && config.Accounts.NickReservation.AllowCustomEnforcement -} - func servCmdRequiresBouncerEnabled(config *Config) bool { return config.Accounts.Bouncer.Enabled } @@ -61,20 +59,14 @@ DROP de-links the given (or your current) nickname from your user account.`, authRequired: true, }, "enforce": { + hidden: true, handler: nsEnforceHandler, help: `Syntax: $bENFORCE [method]$b -ENFORCE lets you specify a custom enforcement mechanism for your registered -nicknames. Your options are: -1. 'none' [no enforcement, overriding the server default] -2. 'timeout' [anyone using the nick must authenticate before a deadline, - or else they will be renamed] -3. 'strict' [you must already be authenticated to use the nick] -4. 'default' [use the server default] -With no arguments, queries your current enforcement status.`, - helpShort: `$bENFORCE$b lets you change how your nicknames are reserved.`, +ENFORCE is an alias for $bGET enforce$b and $bSET enforce$b. See the help +entry for $bSET$b for more information.`, authRequired: true, - enabled: nsEnforceEnabled, + enabled: servCmdRequiresAccreg, }, "ghost": { handler: nsGhostHandler, @@ -194,12 +186,257 @@ password by supplying their username and then the desired password.`, enabled: servCmdRequiresAuthEnabled, minParams: 2, }, + "get": { + handler: nsGetHandler, + help: `Syntax: $bGET $b + +GET queries the current values of your account settings. For more information +on the settings and their possible values, see HELP SET.`, + helpShort: `$bGET$b queries the current values of your account settings`, + authRequired: true, + enabled: servCmdRequiresAccreg, + minParams: 1, + }, + "saget": { + handler: nsGetHandler, + help: `Syntax: $bSAGET $b + +SAGET queries the values of someone else's account settings. For more +information on the settings and their possible values, see HELP SET.`, + helpShort: `$bSAGET$b queries the current values of another user's account settings`, + enabled: servCmdRequiresAccreg, + minParams: 2, + capabs: []string{"accreg"}, + }, + "set": { + handler: nsSetHandler, + help: `Syntax $bSET $b + +Set modifies your account settings. The following settings ara available: + +$bENFORCE$b +'enforce' lets you specify a custom enforcement mechanism for your registered +nicknames. Your options are: +1. 'none' [no enforcement, overriding the server default] +2. 'timeout' [anyone using the nick must authenticate before a deadline, + or else they will be renamed] +3. 'strict' [you must already be authenticated to use the nick] +4. 'default' [use the server default] + +$bBOUNCER$b +If 'bouncer' is enabled and you are already logged in and using a nick, a +second client of yours that authenticates with SASL and requests the same nick +is allowed to attach to the nick as well (this is comparable to the behavior +of IRC "bouncers" like ZNC). Your options are 'on' (allow this behavior), +'off' (disallow it), and 'default' (use the server default value). + +$bAUTOREPLAY-LINES$b +'autoreplay-lines' controls the number of lines of channel history that will +be replayed to you automatically when joining a channel. Your options are any +positive number, 0 to disable the feature, and 'default' to use the server +default. + +$bAUTOREPLAY-JOINS$b +'autoreplay-joins' controls whether autoreplayed channel history will include +lines for join and part. This provides more information about the context of +messages, but may be spammy. Your options are 'on' and 'off'. +`, + helpShort: `$bSET$b modifies your account settings`, + authRequired: true, + enabled: servCmdRequiresAccreg, + minParams: 2, + }, + "saset": { + handler: nsSetHandler, + help: `Syntax: $bSASET $b`, + helpShort: `$bSASET$b modifies another user's account settings`, + enabled: servCmdRequiresAccreg, + minParams: 3, + capabs: []string{"accreg"}, + }, } ) // nsNotice sends the client a notice from NickServ func nsNotice(rb *ResponseBuffer, text string) { - rb.Add(nil, "NickServ", "NOTICE", rb.target.Nick(), text) + // XXX i can't figure out how to use OragonoServices[servicename].prefix here + // without creating a compile-time initialization loop + rb.Add(nil, "NickServ!NickServ@localhost", "NOTICE", rb.target.Nick(), text) +} + +func nsGetHandler(server *Server, client *Client, command string, params []string, rb *ResponseBuffer) { + var account string + if command == "saget" { + account = params[0] + params = params[1:] + } else { + account = client.Account() + } + + accountData, err := server.accounts.LoadAccount(account) + if err == errAccountDoesNotExist { + nsNotice(rb, client.t("No such account")) + return + } else if err != nil { + nsNotice(rb, client.t("Error loading account data")) + return + } + + displaySetting(params[0], accountData.Settings, client, rb) +} + +func displaySetting(settingName string, settings AccountSettings, client *Client, rb *ResponseBuffer) { + config := client.server.Config() + switch strings.ToLower(settingName) { + case "enforce": + storedValue := settings.NickEnforcement + serializedStoredValue := nickReservationToString(storedValue) + nsNotice(rb, fmt.Sprintf(client.t("Your stored nickname enforcement setting is: %s"), serializedStoredValue)) + serializedActualValue := nickReservationToString(configuredEnforcementMethod(config, storedValue)) + nsNotice(rb, fmt.Sprintf(client.t("Given current server settings, your nickname is enforced with: %s"), serializedActualValue)) + case "autoreplay-lines": + if settings.AutoreplayLines == nil { + nsNotice(rb, fmt.Sprintf(client.t("You will receive the server default of %d lines of autoreplayed history"), config.History.AutoreplayOnJoin)) + } else { + nsNotice(rb, fmt.Sprintf(client.t("You will receive %d lines of autoreplayed history"), *settings.AutoreplayLines)) + } + case "autoreplay-joins": + if settings.AutoreplayJoins { + nsNotice(rb, client.t("You will see JOINs and PARTs in autoreplayed history lines")) + } else { + nsNotice(rb, client.t("You will not see JOINs and PARTs in autoreplayed history lines")) + } + case "bouncer": + if !config.Accounts.Bouncer.Enabled { + nsNotice(rb, fmt.Sprintf(client.t("This feature has been disabled by the server administrators"))) + } else { + switch settings.AllowBouncer { + case BouncerAllowedServerDefault: + if config.Accounts.Bouncer.AllowedByDefault { + nsNotice(rb, fmt.Sprintf(client.t("Bouncer functionality is currently enabled for your account, but you can opt out"))) + } else { + nsNotice(rb, fmt.Sprintf(client.t("Bouncer functionality is currently disabled for your account, but you can opt in"))) + } + case BouncerDisallowedByUser: + nsNotice(rb, fmt.Sprintf(client.t("Bouncer functionality is currently disabled for your account"))) + case BouncerAllowedByUser: + nsNotice(rb, fmt.Sprintf(client.t("Bouncer functionality is currently enabled for your account"))) + } + } + default: + nsNotice(rb, client.t("No such setting")) + } +} + +func stringToBool(str string) (result bool, err error) { + switch strings.ToLower(str) { + case "on": + result = true + case "off": + result = false + case "true": + result = true + case "false": + result = false + default: + err = errInvalidParams + } + return +} + +func nsSetHandler(server *Server, client *Client, command string, params []string, rb *ResponseBuffer) { + var account string + if command == "saset" { + account = params[0] + params = params[1:] + } else { + account = client.Account() + } + + var munger settingsMunger + var finalSettings AccountSettings + var err error + switch strings.ToLower(params[0]) { + case "pass": + nsNotice(rb, client.t("To change a password, use the PASSWD command. For details, /msg NickServ HELP PASSWD")) + return + case "enforce": + var method NickEnforcementMethod + method, err = nickReservationFromString(params[1]) + if err != nil { + err = errInvalidParams + break + } + // updating enforcement settings is special-cased, because it requires + // an update to server.accounts.accountToMethod + finalSettings, err = server.accounts.SetEnforcementStatus(account, method) + if err == nil { + finalSettings.NickEnforcement = method // success + } + case "autoreplay-lines": + var newValue *int + if strings.ToLower(params[1]) != "default" { + val, err_ := strconv.Atoi(params[1]) + if err_ != nil || val < 0 { + err = errInvalidParams + break + } + newValue = new(int) + *newValue = val + } + munger = func(in AccountSettings) (out AccountSettings, err error) { + out = in + out.AutoreplayLines = newValue + return + } + case "bouncer": + var newValue BouncerAllowedSetting + if strings.ToLower(params[1]) == "default" { + newValue = BouncerAllowedServerDefault + } else { + var enabled bool + enabled, err = stringToBool(params[1]) + if enabled { + newValue = BouncerAllowedByUser + } else { + newValue = BouncerDisallowedByUser + } + } + if err == nil { + munger = func(in AccountSettings) (out AccountSettings, err error) { + out = in + out.AllowBouncer = newValue + return + } + } + case "autoreplay-joins": + var newValue bool + newValue, err = stringToBool(params[1]) + if err == nil { + munger = func(in AccountSettings) (out AccountSettings, err error) { + out = in + out.AutoreplayJoins = newValue + return + } + } + default: + err = errInvalidParams + } + + if munger != nil { + finalSettings, err = server.accounts.ModifyAccountSettings(account, munger) + } + + switch err { + case nil: + nsNotice(rb, client.t("Successfully changed your account settings")) + displaySetting(params[0], finalSettings, client, rb) + case errInvalidParams, errAccountDoesNotExist, errFeatureDisabled, errAccountUnverified, errAccountUpdateFailed: + nsNotice(rb, client.t(err.Error())) + default: + // unknown error + nsNotice(rb, client.t("An error occurred")) + } } func nsDropHandler(server *Server, client *Client, command string, params []string, rb *ResponseBuffer) { @@ -568,22 +805,12 @@ func nsPasswdHandler(server *Server, client *Client, command string, params []st } func nsEnforceHandler(server *Server, client *Client, command string, params []string, rb *ResponseBuffer) { + newParams := []string{"enforce"} if len(params) == 0 { - status := server.accounts.getStoredEnforcementStatus(client.Account()) - nsNotice(rb, fmt.Sprintf(client.t("Your current nickname enforcement is: %s"), status)) + nsGetHandler(server, client, "get", newParams, rb) } else { - method, err := nickReservationFromString(params[0]) - if err != nil { - nsNotice(rb, client.t("Invalid parameters")) - return - } - err = server.accounts.SetEnforcementStatus(client.Account(), method) - if err == nil { - nsNotice(rb, client.t("Enforcement method set")) - } else { - server.logger.Error("internal", "couldn't store NS ENFORCE data", err.Error()) - nsNotice(rb, client.t("An error occurred")) - } + newParams = append(newParams, params[0]) + nsSetHandler(server, client, "set", newParams, rb) } } diff --git a/irc/services.go b/irc/services.go index b8a63f1a..9b459946 100644 --- a/irc/services.go +++ b/irc/services.go @@ -18,6 +18,7 @@ import ( type ircService struct { Name string ShortName string + prefix string CommandAliases []string Commands map[string]*serviceCommand HelpBanner string @@ -31,6 +32,7 @@ type serviceCommand struct { help string helpShort string authRequired bool + hidden bool enabled func(*Config) bool // is this command enabled in the server config? minParams int maxParams int // split into at most n params, with last param containing remaining unsplit text @@ -139,7 +141,7 @@ func servicePrivmsgHandler(service *ircService, server *Server, client *Client, func serviceRunCommand(service *ircService, server *Server, client *Client, cmd *serviceCommand, commandName string, params []string, rb *ResponseBuffer) { nick := rb.target.Nick() sendNotice := func(notice string) { - rb.Add(nil, service.Name, "NOTICE", nick, notice) + rb.Add(nil, service.prefix, "NOTICE", nick, notice) } if cmd == nil { @@ -180,7 +182,7 @@ func serviceHelpHandler(service *ircService, server *Server, client *Client, par nick := rb.target.Nick() config := server.Config() sendNotice := func(notice string) { - rb.Add(nil, service.Name, "NOTICE", nick, notice) + rb.Add(nil, service.prefix, "NOTICE", nick, notice) } sendNotice(ircfmt.Unescape(fmt.Sprintf("*** $b%s HELP$b ***", service.Name))) @@ -194,7 +196,7 @@ func serviceHelpHandler(service *ircService, server *Server, client *Client, par if 0 < len(commandInfo.capabs) && !client.HasRoleCapabs(commandInfo.capabs...) { continue } - if commandInfo.aliasOf != "" { + if commandInfo.aliasOf != "" || commandInfo.hidden { continue // don't show help lines for aliases } if commandInfo.enabled != nil && !commandInfo.enabled(config) { @@ -241,6 +243,8 @@ func initializeServices() { oragonoServicesByCommandAlias = make(map[string]*ircService) for serviceName, service := range OragonoServices { + service.prefix = fmt.Sprintf("%s!%s@localhost", service.Name, service.Name) + // make `/MSG ServiceName HELP` work correctly service.Commands["help"] = &servHelpCmd @@ -257,7 +261,7 @@ func initializeServices() { // force devs to write a help entry for every command for commandName, commandInfo := range service.Commands { - if commandInfo.aliasOf == "" && (commandInfo.help == "" || commandInfo.helpShort == "") { + if commandInfo.aliasOf == "" && !commandInfo.hidden && (commandInfo.help == "" || commandInfo.helpShort == "") { log.Fatal(fmt.Sprintf("help entry missing for %s command %s", serviceName, commandName)) } } diff --git a/oragono.yaml b/oragono.yaml index bd3c7534..4b483e12 100644 --- a/oragono.yaml +++ b/oragono.yaml @@ -134,7 +134,7 @@ server: # defaults to true when unset for that reason. force-trailing: true - # some clients (ZNC 1.6.x and lower, Pidgin 2.12 and lower, Adium) do not + # some clients (ZNC 1.6.x and lower, Pidgin 2.12 and lower) do not # respond correctly to SASL messages with the server name as a prefix: # https://github.com/znc/znc/issues/1212 # this works around that bug, allowing them to use SASL. From f10ed05f86a22c8e63f34a7be02ffdae669994aa Mon Sep 17 00:00:00 2001 From: Shivaram Lingamneni Date: Sun, 19 May 2019 05:47:41 -0400 Subject: [PATCH 3/5] fix a lock access in ResumeManager --- irc/resume.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/irc/resume.go b/irc/resume.go index 9b9b1d05..1556bb02 100644 --- a/irc/resume.go +++ b/irc/resume.go @@ -18,7 +18,7 @@ type resumeTokenPair struct { } type ResumeManager struct { - sync.RWMutex // level 2 + sync.Mutex // level 2 resumeIDtoCreds map[string]resumeTokenPair server *Server @@ -59,8 +59,8 @@ func (rm *ResumeManager) VerifyToken(token string) (client *Client) { return } - rm.RLock() - defer rm.RUnlock() + rm.Lock() + defer rm.Unlock() id := token[:utils.SecretTokenLength] pair, ok := rm.resumeIDtoCreds[id] From 38b228af6a5d356454068d589dcfd50bb951454c Mon Sep 17 00:00:00 2001 From: Shivaram Lingamneni Date: Mon, 20 May 2019 02:56:49 -0400 Subject: [PATCH 4/5] review fixes --- irc/errors.go | 7 ++++-- irc/nickserv.go | 58 ++++++++++++++++++++--------------------------- irc/services.go | 21 +++++++++++++---- irc/utils/args.go | 21 +++++++++++++++++ 4 files changed, 67 insertions(+), 40 deletions(-) diff --git a/irc/errors.go b/irc/errors.go index 14e833ab..df47f8d2 100644 --- a/irc/errors.go +++ b/irc/errors.go @@ -5,7 +5,10 @@ package irc -import "errors" +import ( + "errors" + "github.com/oragono/oragono/irc/utils" +) // Runtime Errors var ( @@ -40,7 +43,7 @@ var ( errInvalidUsername = errors.New("Invalid username") errFeatureDisabled = errors.New(`That feature is disabled`) errBanned = errors.New("IP or nickmask banned") - errInvalidParams = errors.New("Invalid parameters") + errInvalidParams = utils.ErrInvalidParams ) // Socket Errors diff --git a/irc/nickserv.go b/irc/nickserv.go index 1b14b0f0..ab139df5 100644 --- a/irc/nickserv.go +++ b/irc/nickserv.go @@ -12,6 +12,7 @@ import ( "github.com/goshuirc/irc-go/ircfmt" "github.com/oragono/oragono/irc/modes" + "github.com/oragono/oragono/irc/utils" ) // "enabled" callbacks for specific nickserv commands @@ -209,46 +210,51 @@ information on the settings and their possible values, see HELP SET.`, capabs: []string{"accreg"}, }, "set": { - handler: nsSetHandler, - help: `Syntax $bSET $b + handler: nsSetHandler, + helpShort: `$bSET$b modifies your account settings`, + // these are broken out as separate strings so they can be translated separately + helpStrings: []string{ + `Syntax $bSET $b -Set modifies your account settings. The following settings ara available: +Set modifies your account settings. The following settings are available:`, -$bENFORCE$b + `$bENFORCE$b 'enforce' lets you specify a custom enforcement mechanism for your registered nicknames. Your options are: 1. 'none' [no enforcement, overriding the server default] 2. 'timeout' [anyone using the nick must authenticate before a deadline, or else they will be renamed] 3. 'strict' [you must already be authenticated to use the nick] -4. 'default' [use the server default] +4. 'default' [use the server default]`, -$bBOUNCER$b + `$bBOUNCER$b If 'bouncer' is enabled and you are already logged in and using a nick, a second client of yours that authenticates with SASL and requests the same nick is allowed to attach to the nick as well (this is comparable to the behavior of IRC "bouncers" like ZNC). Your options are 'on' (allow this behavior), -'off' (disallow it), and 'default' (use the server default value). +'off' (disallow it), and 'default' (use the server default value).`, -$bAUTOREPLAY-LINES$b + `$bAUTOREPLAY-LINES$b 'autoreplay-lines' controls the number of lines of channel history that will be replayed to you automatically when joining a channel. Your options are any positive number, 0 to disable the feature, and 'default' to use the server -default. +default.`, -$bAUTOREPLAY-JOINS$b + `$bAUTOREPLAY-JOINS$b 'autoreplay-joins' controls whether autoreplayed channel history will include lines for join and part. This provides more information about the context of -messages, but may be spammy. Your options are 'on' and 'off'. -`, - helpShort: `$bSET$b modifies your account settings`, +messages, but may be spammy. Your options are 'on' and 'off'.`, + }, authRequired: true, enabled: servCmdRequiresAccreg, minParams: 2, }, "saset": { - handler: nsSetHandler, - help: `Syntax: $bSASET $b`, + handler: nsSetHandler, + help: `Syntax: $bSASET $b + +SASET modifies the values of someone else's account settings. For more +information on the settings and their possible values, see HELP SET.`, helpShort: `$bSASET$b modifies another user's account settings`, enabled: servCmdRequiresAccreg, minParams: 3, @@ -328,22 +334,6 @@ func displaySetting(settingName string, settings AccountSettings, client *Client } } -func stringToBool(str string) (result bool, err error) { - switch strings.ToLower(str) { - case "on": - result = true - case "off": - result = false - case "true": - result = true - case "false": - result = false - default: - err = errInvalidParams - } - return -} - func nsSetHandler(server *Server, client *Client, command string, params []string, rb *ResponseBuffer) { var account string if command == "saset" { @@ -395,7 +385,7 @@ func nsSetHandler(server *Server, client *Client, command string, params []strin newValue = BouncerAllowedServerDefault } else { var enabled bool - enabled, err = stringToBool(params[1]) + enabled, err = utils.StringToBool(params[1]) if enabled { newValue = BouncerAllowedByUser } else { @@ -411,7 +401,7 @@ func nsSetHandler(server *Server, client *Client, command string, params []strin } case "autoreplay-joins": var newValue bool - newValue, err = stringToBool(params[1]) + newValue, err = utils.StringToBool(params[1]) if err == nil { munger = func(in AccountSettings) (out AccountSettings, err error) { out = in @@ -787,7 +777,7 @@ func nsPasswdHandler(server *Server, client *Client, command string, params []st } } default: - errorMessage = "Invalid parameters" + errorMessage = `Invalid parameters` } if errorMessage != "" { diff --git a/irc/services.go b/irc/services.go index 9b459946..8bde6ac4 100644 --- a/irc/services.go +++ b/irc/services.go @@ -30,6 +30,7 @@ type serviceCommand struct { capabs []string // oper capabs the given user has to have to access this command handler func(server *Server, client *Client, command string, params []string, rb *ResponseBuffer) help string + helpStrings []string helpShort string authRequired bool hidden bool @@ -228,8 +229,18 @@ func serviceHelpHandler(service *ircService, server *Server, client *Client, par if commandInfo == nil { sendNotice(client.t(fmt.Sprintf("Unknown command. To see available commands, run /%s HELP", service.ShortName))) } else { - for _, line := range strings.Split(ircfmt.Unescape(client.t(commandInfo.help)), "\n") { - sendNotice(line) + helpStrings := commandInfo.helpStrings + if helpStrings == nil { + hsArray := [1]string{commandInfo.help} + helpStrings = hsArray[:] + } + for i, helpString := range helpStrings { + if 0 < i { + sendNotice("") + } + for _, line := range strings.Split(ircfmt.Unescape(client.t(helpString)), "\n") { + sendNotice(line) + } } } } @@ -261,8 +272,10 @@ func initializeServices() { // force devs to write a help entry for every command for commandName, commandInfo := range service.Commands { - if commandInfo.aliasOf == "" && !commandInfo.hidden && (commandInfo.help == "" || commandInfo.helpShort == "") { - log.Fatal(fmt.Sprintf("help entry missing for %s command %s", serviceName, commandName)) + if commandInfo.aliasOf == "" && !commandInfo.hidden { + if (commandInfo.help == "" && commandInfo.helpStrings == nil) || commandInfo.helpShort == "" { + log.Fatal(fmt.Sprintf("help entry missing for %s command %s", serviceName, commandName)) + } } } } diff --git a/irc/utils/args.go b/irc/utils/args.go index 8f087423..6fe42a6b 100644 --- a/irc/utils/args.go +++ b/irc/utils/args.go @@ -3,6 +3,15 @@ package utils +import ( + "errors" + "strings" +) + +var ( + ErrInvalidParams = errors.New("Invalid parameters") +) + // ArgsToStrings takes the arguments and splits them into a series of strings, // each argument separated by delim and each string bounded by maxLength. func ArgsToStrings(maxLength int, arguments []string, delim string) []string { @@ -33,3 +42,15 @@ func ArgsToStrings(maxLength int, arguments []string, delim string) []string { return messages } + +func StringToBool(str string) (result bool, err error) { + switch strings.ToLower(str) { + case "on", "true", "t", "yes", "y": + result = true + case "off", "false", "f", "no", "n": + result = false + default: + err = ErrInvalidParams + } + return +} From d5ebebaa5bef16036206b9bd04cdf9901cf0d868 Mon Sep 17 00:00:00 2001 From: Shivaram Lingamneni Date: Mon, 20 May 2019 12:34:30 -0400 Subject: [PATCH 5/5] add uncommitted test file --- irc/utils/args_test.go | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 irc/utils/args_test.go diff --git a/irc/utils/args_test.go b/irc/utils/args_test.go new file mode 100644 index 00000000..9346ea3a --- /dev/null +++ b/irc/utils/args_test.go @@ -0,0 +1,23 @@ +// Copyright (c) 2019 Shivaram Lingamneni +// released under the MIT license + +package utils + +import "testing" + +func TestStringToBool(t *testing.T) { + val, err := StringToBool("on") + assertEqual(val, true, t) + assertEqual(err, nil, t) + + val, err = StringToBool("n") + assertEqual(val, false, t) + assertEqual(err, nil, t) + + val, err = StringToBool("OFF") + assertEqual(val, false, t) + assertEqual(err, nil, t) + + val, err = StringToBool("default") + assertEqual(err, ErrInvalidParams, t) +}