diff --git a/gencapdefs.py b/gencapdefs.py index d1cd2c89..19ca4baa 100644 --- a/gencapdefs.py +++ b/gencapdefs.py @@ -195,6 +195,12 @@ CAPDEFS = [ url="https://github.com/ircv3/ircv3-specifications/pull/503", standard="proposed IRCv3", ), + CapDef( + identifier="Preaway", + name="draft/pre-away", + url="https://github.com/ircv3/ircv3-specifications/pull/514", + standard="proposed IRCv3", + ), CapDef( identifier="StandardReplies", name="standard-replies", diff --git a/irc/caps/defs.go b/irc/caps/defs.go index cb7d5db0..8339660a 100644 --- a/irc/caps/defs.go +++ b/irc/caps/defs.go @@ -7,7 +7,7 @@ package caps const ( // number of recognized capabilities: - numCapabs = 31 + numCapabs = 32 // length of the uint32 array that represents the bitset: bitsetLen = 1 ) @@ -65,6 +65,10 @@ const ( // https://github.com/ircv3/ircv3-specifications/pull/503 Persistence Capability = iota + // Preaway is the proposed IRCv3 capability named "draft/pre-away": + // https://github.com/ircv3/ircv3-specifications/pull/514 + Preaway Capability = iota + // ReadMarker is the draft IRCv3 capability named "draft/read-marker": // https://github.com/ircv3/ircv3-specifications/pull/489 ReadMarker Capability = iota @@ -154,6 +158,7 @@ var ( "draft/languages", "draft/multiline", "draft/persistence", + "draft/pre-away", "draft/read-marker", "draft/relaymsg", "echo-message", diff --git a/irc/client.go b/irc/client.go index 146abeab..f26985aa 100644 --- a/irc/client.go +++ b/irc/client.go @@ -1222,14 +1222,11 @@ func (client *Client) destroy(session *Session) { client.destroyed = true } - becameAutoAway := false - var awayMessage string - if alwaysOn && persistenceEnabled(config.Accounts.Multiclient.AutoAway, client.accountSettings.AutoAway) { - wasAway := client.awayMessage != "" + wasAway := client.awayMessage + if client.autoAwayEnabledNoMutex(config) { client.setAutoAwayNoMutex(config) - awayMessage = client.awayMessage - becameAutoAway = !wasAway && awayMessage != "" } + nowAway := client.awayMessage if client.registrationTimer != nil { // unconditionally stop; if the client is still unregistered it must be destroyed @@ -1279,8 +1276,8 @@ func (client *Client) destroy(session *Session) { client.server.stats.Remove(registered, invisible, operator) } - if becameAutoAway { - dispatchAwayNotify(client, true, awayMessage) + if !shouldDestroy && wasAway != nowAway { + dispatchAwayNotify(client, nowAway) } if !shouldDestroy { diff --git a/irc/client_lookup_set.go b/irc/client_lookup_set.go index c97c89ff..a68aaf53 100644 --- a/irc/client_lookup_set.go +++ b/irc/client_lookup_set.go @@ -84,7 +84,7 @@ func (clients *ClientManager) Remove(client *Client) error { // SetNick sets a client's nickname, validating it against nicknames in use // XXX: dryRun validates a client's ability to claim a nick, without // actually claiming it -func (clients *ClientManager) SetNick(client *Client, session *Session, newNick string, dryRun bool) (setNick string, err error, returnedFromAway bool) { +func (clients *ClientManager) SetNick(client *Client, session *Session, newNick string, dryRun bool) (setNick string, err error, awayChanged bool) { config := client.server.Config() var newCfNick, newSkeleton string @@ -204,7 +204,7 @@ func (clients *ClientManager) SetNick(client *Client, session *Session, newNick return "", errNicknameInUse, false } } - reattachSuccessful, numSessions, lastSeen, back := currentClient.AddSession(session) + reattachSuccessful, numSessions, lastSeen, wasAway, nowAway := currentClient.AddSession(session) if !reattachSuccessful { return "", errNicknameInUse, false } @@ -219,7 +219,7 @@ func (clients *ClientManager) SetNick(client *Client, session *Session, newNick currentClient.SetRealname(realname) } // successful reattach! - return newNick, nil, back + return newNick, nil, wasAway != nowAway } else if currentClient == client && currentClient.Nick() == newNick { return "", errNoop, false } diff --git a/irc/commands.go b/irc/commands.go index e4b8d15b..d3f828b6 100644 --- a/irc/commands.go +++ b/irc/commands.go @@ -89,8 +89,9 @@ func init() { minParams: 1, }, "AWAY": { - handler: awayHandler, - minParams: 0, + handler: awayHandler, + usablePreReg: true, + minParams: 0, }, "BATCH": { handler: batchHandler, diff --git a/irc/getters.go b/irc/getters.go index 2746cdb5..19b162f4 100644 --- a/irc/getters.go +++ b/irc/getters.go @@ -92,7 +92,7 @@ func (client *Client) AllSessionData(currentSession *Session, hasPrivs bool) (da return } -func (client *Client) AddSession(session *Session) (success bool, numSessions int, lastSeen time.Time, back bool) { +func (client *Client) AddSession(session *Session) (success bool, numSessions int, lastSeen time.Time, wasAway, nowAway string) { config := client.server.Config() client.stateMutex.Lock() defer client.stateMutex.Unlock() @@ -113,14 +113,22 @@ func (client *Client) AddSession(session *Session) (success bool, numSessions in client.setLastSeen(time.Now().UTC(), session.deviceID) } client.sessions = newSessions - // TODO(#1551) there should be a cap to opt out of this behavior on a session - if persistenceEnabled(config.Accounts.Multiclient.AutoAway, client.accountSettings.AutoAway) { - client.awayMessage = "" - if len(client.sessions) == 1 { - back = true + wasAway = client.awayMessage + if client.autoAwayEnabledNoMutex(config) { + client.setAutoAwayNoMutex(config) + } else { + if session.awayMessage != "" && session.awayMessage != "*" { + // set the away message + client.awayMessage = session.awayMessage + } else if session.awayMessage == "" && !session.awayAt.IsZero() { + // weird edge case: explicit `AWAY` or `AWAY :` during pre-registration makes the client back + client.awayMessage = "" } + // else: the client sent no AWAY command at all, no-op + // or: the client sent `AWAY *`, which should not modify the publicly visible away state } - return true, len(client.sessions), lastSeen, back + nowAway = client.awayMessage + return true, len(client.sessions), lastSeen, wasAway, nowAway } func (client *Client) removeSession(session *Session) (success bool, length int) { @@ -195,7 +203,7 @@ func (client *Client) Away() (result bool, message string) { return } -func (session *Session) SetAway(awayMessage string) { +func (session *Session) SetAway(awayMessage string) (wasAway, nowAway string) { client := session.client config := client.server.Config() @@ -205,15 +213,21 @@ func (session *Session) SetAway(awayMessage string) { session.awayMessage = awayMessage session.awayAt = time.Now().UTC() - autoAway := client.registered && client.alwaysOn && persistenceEnabled(config.Accounts.Multiclient.AutoAway, client.accountSettings.AutoAway) - if autoAway { + wasAway = client.awayMessage + if client.autoAwayEnabledNoMutex(config) { client.setAutoAwayNoMutex(config) - } else { + } else if awayMessage != "*" { client.awayMessage = awayMessage - } + } // else: `AWAY *`, should not modify publicly visible away state + nowAway = client.awayMessage return } +func (client *Client) autoAwayEnabledNoMutex(config *Config) bool { + return client.registered && client.alwaysOn && + persistenceEnabled(config.Accounts.Multiclient.AutoAway, client.accountSettings.AutoAway) +} + func (client *Client) setAutoAwayNoMutex(config *Config) { // aggregate the away statuses of the individual sessions: var globalAwayState string @@ -223,8 +237,8 @@ func (client *Client) setAutoAwayNoMutex(config *Config) { // a session is active, we are not auto-away client.awayMessage = "" return - } else if cSession.awayAt.After(awaySetAt) { - // choose the latest available away message from any session + } else if cSession.awayAt.After(awaySetAt) && cSession.awayMessage != "*" { + // choose the latest valid away message from any session globalAwayState = cSession.awayMessage awaySetAt = cSession.awayAt } diff --git a/irc/handlers.go b/irc/handlers.go index 0436bd59..6dae89ed 100644 --- a/irc/handlers.go +++ b/irc/handlers.go @@ -447,32 +447,34 @@ func authScramHandler(server *Server, client *Client, session *Session, value [] // AWAY [] func awayHandler(server *Server, client *Client, msg ircmsg.Message, rb *ResponseBuffer) bool { - var isAway bool + // #1996: `AWAY :` is treated the same as `AWAY` var awayMessage string if len(msg.Params) > 0 { awayMessage = msg.Params[0] awayMessage = ircutils.TruncateUTF8Safe(awayMessage, server.Config().Limits.AwayLen) } - isAway = (awayMessage != "") // #1996 - rb.session.SetAway(awayMessage) + wasAway, nowAway := rb.session.SetAway(awayMessage) - if isAway { + if nowAway != "" { rb.Add(nil, server.name, RPL_NOWAWAY, client.nick, client.t("You have been marked as being away")) } else { rb.Add(nil, server.name, RPL_UNAWAY, client.nick, client.t("You are no longer marked as being away")) } - dispatchAwayNotify(client, isAway, awayMessage) + if client.registered && wasAway != nowAway { + dispatchAwayNotify(client, nowAway) + } // else: we'll send it (if applicable) after reattach + return false } -func dispatchAwayNotify(client *Client, isAway bool, awayMessage string) { +func dispatchAwayNotify(client *Client, awayMessage string) { // dispatch away-notify details := client.Details() isBot := client.HasMode(modes.Bot) for session := range client.FriendsMonitors(caps.AwayNotify) { - if isAway { + if awayMessage != "" { session.sendFromClientInternal(false, time.Time{}, "", details.nickMask, details.accountName, isBot, nil, "AWAY", awayMessage) } else { session.sendFromClientInternal(false, time.Time{}, "", details.nickMask, details.accountName, isBot, nil, "AWAY") diff --git a/irc/nickname.go b/irc/nickname.go index 3fb36609..b180c005 100644 --- a/irc/nickname.go +++ b/irc/nickname.go @@ -34,7 +34,7 @@ func performNickChange(server *Server, client *Client, target *Client, session * origNickMask := details.nickMask isSanick := client != target - assignedNickname, err, back := client.server.clients.SetNick(target, session, nickname, false) + assignedNickname, err, awayChanged := client.server.clients.SetNick(target, session, nickname, false) if err == errNicknameInUse { if !isSanick { rb.Add(nil, server.name, ERR_NICKNAMEINUSE, details.nick, utils.SafeErrorParam(nickname), client.t("Nickname is already in use")) @@ -115,8 +115,8 @@ func performNickChange(server *Server, client *Client, target *Client, session * } } - if back { - dispatchAwayNotify(session.client, false, "") + if awayChanged { + dispatchAwayNotify(session.client, session.client.AwayMessage()) } for _, channel := range target.Channels() {