3
0
mirror of https://github.com/ergochat/ergo.git synced 2025-02-16 13:40:48 +01:00

implement draft/pre-away (#2044)

* implement draft/pre-away
* clean up some subtleties in auto-away aggregation.
* consistently apply auto-away only to always-on
* `AWAY *` should not produce user-visible changes wherever possible
This commit is contained in:
Shivaram Lingamneni 2023-02-04 21:50:14 -08:00 committed by GitHub
parent 12f7796933
commit 1da11ae8ae
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 63 additions and 38 deletions

View File

@ -195,6 +195,12 @@ CAPDEFS = [
url="https://github.com/ircv3/ircv3-specifications/pull/503", url="https://github.com/ircv3/ircv3-specifications/pull/503",
standard="proposed IRCv3", standard="proposed IRCv3",
), ),
CapDef(
identifier="Preaway",
name="draft/pre-away",
url="https://github.com/ircv3/ircv3-specifications/pull/514",
standard="proposed IRCv3",
),
CapDef( CapDef(
identifier="StandardReplies", identifier="StandardReplies",
name="standard-replies", name="standard-replies",

View File

@ -7,7 +7,7 @@ package caps
const ( const (
// number of recognized capabilities: // number of recognized capabilities:
numCapabs = 31 numCapabs = 32
// length of the uint32 array that represents the bitset: // length of the uint32 array that represents the bitset:
bitsetLen = 1 bitsetLen = 1
) )
@ -65,6 +65,10 @@ const (
// https://github.com/ircv3/ircv3-specifications/pull/503 // https://github.com/ircv3/ircv3-specifications/pull/503
Persistence Capability = iota 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": // ReadMarker is the draft IRCv3 capability named "draft/read-marker":
// https://github.com/ircv3/ircv3-specifications/pull/489 // https://github.com/ircv3/ircv3-specifications/pull/489
ReadMarker Capability = iota ReadMarker Capability = iota
@ -154,6 +158,7 @@ var (
"draft/languages", "draft/languages",
"draft/multiline", "draft/multiline",
"draft/persistence", "draft/persistence",
"draft/pre-away",
"draft/read-marker", "draft/read-marker",
"draft/relaymsg", "draft/relaymsg",
"echo-message", "echo-message",

View File

@ -1222,14 +1222,11 @@ func (client *Client) destroy(session *Session) {
client.destroyed = true client.destroyed = true
} }
becameAutoAway := false wasAway := client.awayMessage
var awayMessage string if client.autoAwayEnabledNoMutex(config) {
if alwaysOn && persistenceEnabled(config.Accounts.Multiclient.AutoAway, client.accountSettings.AutoAway) {
wasAway := client.awayMessage != ""
client.setAutoAwayNoMutex(config) client.setAutoAwayNoMutex(config)
awayMessage = client.awayMessage
becameAutoAway = !wasAway && awayMessage != ""
} }
nowAway := client.awayMessage
if client.registrationTimer != nil { if client.registrationTimer != nil {
// unconditionally stop; if the client is still unregistered it must be destroyed // 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) client.server.stats.Remove(registered, invisible, operator)
} }
if becameAutoAway { if !shouldDestroy && wasAway != nowAway {
dispatchAwayNotify(client, true, awayMessage) dispatchAwayNotify(client, nowAway)
} }
if !shouldDestroy { if !shouldDestroy {

View File

@ -84,7 +84,7 @@ func (clients *ClientManager) Remove(client *Client) error {
// SetNick sets a client's nickname, validating it against nicknames in use // SetNick sets a client's nickname, validating it against nicknames in use
// XXX: dryRun validates a client's ability to claim a nick, without // XXX: dryRun validates a client's ability to claim a nick, without
// actually claiming it // 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() config := client.server.Config()
var newCfNick, newSkeleton string var newCfNick, newSkeleton string
@ -204,7 +204,7 @@ func (clients *ClientManager) SetNick(client *Client, session *Session, newNick
return "", errNicknameInUse, false return "", errNicknameInUse, false
} }
} }
reattachSuccessful, numSessions, lastSeen, back := currentClient.AddSession(session) reattachSuccessful, numSessions, lastSeen, wasAway, nowAway := currentClient.AddSession(session)
if !reattachSuccessful { if !reattachSuccessful {
return "", errNicknameInUse, false return "", errNicknameInUse, false
} }
@ -219,7 +219,7 @@ func (clients *ClientManager) SetNick(client *Client, session *Session, newNick
currentClient.SetRealname(realname) currentClient.SetRealname(realname)
} }
// successful reattach! // successful reattach!
return newNick, nil, back return newNick, nil, wasAway != nowAway
} else if currentClient == client && currentClient.Nick() == newNick { } else if currentClient == client && currentClient.Nick() == newNick {
return "", errNoop, false return "", errNoop, false
} }

View File

@ -89,8 +89,9 @@ func init() {
minParams: 1, minParams: 1,
}, },
"AWAY": { "AWAY": {
handler: awayHandler, handler: awayHandler,
minParams: 0, usablePreReg: true,
minParams: 0,
}, },
"BATCH": { "BATCH": {
handler: batchHandler, handler: batchHandler,

View File

@ -92,7 +92,7 @@ func (client *Client) AllSessionData(currentSession *Session, hasPrivs bool) (da
return 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() config := client.server.Config()
client.stateMutex.Lock() client.stateMutex.Lock()
defer client.stateMutex.Unlock() 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.setLastSeen(time.Now().UTC(), session.deviceID)
} }
client.sessions = newSessions client.sessions = newSessions
// TODO(#1551) there should be a cap to opt out of this behavior on a session wasAway = client.awayMessage
if persistenceEnabled(config.Accounts.Multiclient.AutoAway, client.accountSettings.AutoAway) { if client.autoAwayEnabledNoMutex(config) {
client.awayMessage = "" client.setAutoAwayNoMutex(config)
if len(client.sessions) == 1 { } else {
back = true 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) { func (client *Client) removeSession(session *Session) (success bool, length int) {
@ -195,7 +203,7 @@ func (client *Client) Away() (result bool, message string) {
return return
} }
func (session *Session) SetAway(awayMessage string) { func (session *Session) SetAway(awayMessage string) (wasAway, nowAway string) {
client := session.client client := session.client
config := client.server.Config() config := client.server.Config()
@ -205,15 +213,21 @@ func (session *Session) SetAway(awayMessage string) {
session.awayMessage = awayMessage session.awayMessage = awayMessage
session.awayAt = time.Now().UTC() session.awayAt = time.Now().UTC()
autoAway := client.registered && client.alwaysOn && persistenceEnabled(config.Accounts.Multiclient.AutoAway, client.accountSettings.AutoAway) wasAway = client.awayMessage
if autoAway { if client.autoAwayEnabledNoMutex(config) {
client.setAutoAwayNoMutex(config) client.setAutoAwayNoMutex(config)
} else { } else if awayMessage != "*" {
client.awayMessage = awayMessage client.awayMessage = awayMessage
} } // else: `AWAY *`, should not modify publicly visible away state
nowAway = client.awayMessage
return 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) { func (client *Client) setAutoAwayNoMutex(config *Config) {
// aggregate the away statuses of the individual sessions: // aggregate the away statuses of the individual sessions:
var globalAwayState string var globalAwayState string
@ -223,8 +237,8 @@ func (client *Client) setAutoAwayNoMutex(config *Config) {
// a session is active, we are not auto-away // a session is active, we are not auto-away
client.awayMessage = "" client.awayMessage = ""
return return
} else if cSession.awayAt.After(awaySetAt) { } else if cSession.awayAt.After(awaySetAt) && cSession.awayMessage != "*" {
// choose the latest available away message from any session // choose the latest valid away message from any session
globalAwayState = cSession.awayMessage globalAwayState = cSession.awayMessage
awaySetAt = cSession.awayAt awaySetAt = cSession.awayAt
} }

View File

@ -447,32 +447,34 @@ func authScramHandler(server *Server, client *Client, session *Session, value []
// AWAY [<message>] // AWAY [<message>]
func awayHandler(server *Server, client *Client, msg ircmsg.Message, rb *ResponseBuffer) bool { 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 var awayMessage string
if len(msg.Params) > 0 { if len(msg.Params) > 0 {
awayMessage = msg.Params[0] awayMessage = msg.Params[0]
awayMessage = ircutils.TruncateUTF8Safe(awayMessage, server.Config().Limits.AwayLen) 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")) rb.Add(nil, server.name, RPL_NOWAWAY, client.nick, client.t("You have been marked as being away"))
} else { } else {
rb.Add(nil, server.name, RPL_UNAWAY, client.nick, client.t("You are no longer marked as being away")) 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 return false
} }
func dispatchAwayNotify(client *Client, isAway bool, awayMessage string) { func dispatchAwayNotify(client *Client, awayMessage string) {
// dispatch away-notify // dispatch away-notify
details := client.Details() details := client.Details()
isBot := client.HasMode(modes.Bot) isBot := client.HasMode(modes.Bot)
for session := range client.FriendsMonitors(caps.AwayNotify) { for session := range client.FriendsMonitors(caps.AwayNotify) {
if isAway { if awayMessage != "" {
session.sendFromClientInternal(false, time.Time{}, "", details.nickMask, details.accountName, isBot, nil, "AWAY", awayMessage) session.sendFromClientInternal(false, time.Time{}, "", details.nickMask, details.accountName, isBot, nil, "AWAY", awayMessage)
} else { } else {
session.sendFromClientInternal(false, time.Time{}, "", details.nickMask, details.accountName, isBot, nil, "AWAY") session.sendFromClientInternal(false, time.Time{}, "", details.nickMask, details.accountName, isBot, nil, "AWAY")

View File

@ -34,7 +34,7 @@ func performNickChange(server *Server, client *Client, target *Client, session *
origNickMask := details.nickMask origNickMask := details.nickMask
isSanick := client != target 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 err == errNicknameInUse {
if !isSanick { if !isSanick {
rb.Add(nil, server.name, ERR_NICKNAMEINUSE, details.nick, utils.SafeErrorParam(nickname), client.t("Nickname is already in use")) 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 { if awayChanged {
dispatchAwayNotify(session.client, false, "") dispatchAwayNotify(session.client, session.client.AwayMessage())
} }
for _, channel := range target.Channels() { for _, channel := range target.Channels() {