Status: a service is asked only for the statuses it has - Matrix has no idle and sends offline for invisible, Kick and Sneedchat none; do-not-disturb is online everywhere but Discord, which has the real one

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
This commit is contained in:
2026-10-09 11:26:52 -04:00
co-authored by Claude Sonnet 5.5
parent 43135c1005
commit a8441ab349
3 changed files with 70 additions and 8 deletions
+6 -7
View File
@@ -172,8 +172,8 @@ pub(super) async fn joined_member_ids(base: &str, access_token: &str, room_id: &
/// Pushes a status to the homeserver.
///
/// Matrix has three presence values - online, unavailable and offline. Idle
/// maps to unavailable, which is the closest honest answer to "am I here".
/// Matrix has three presence values - online, unavailable and offline. Unavailable
/// is idle, which is not offered: only online and offline (invisible) are sent.
pub async fn apply_status(state: &AppState, config: &MatrixAccountConfig, status: &str) -> Result<()> {
match push_presence(state, config, status).await {
// A homeserver limits how often presence may change, and some ask for
@@ -217,12 +217,11 @@ async fn push_presence(state: &AppState, config: &MatrixAccountConfig, status: &
.get_matrix(&config.account_id())
.context("account is no longer configured")?;
let access_token = account.access_token;
// Matrix has three, and they do not line up one for one. Do-not-disturb
// is somebody present who does not want interrupting, which is closest to
// unavailable; invisible has no equivalent at all, and offline is the
// honest answer - it is what invisible means to everybody looking.
// Matrix has online, unavailable and offline. Unavailable is idle, which is
// not offered here, so what is sent is online - do-not-disturb is moho's own
// switch for its notifications and has nothing to say to the server - or
// offline for invisible, which is what invisible means to everybody looking.
let presence = match status {
"idle" | "dnd" => "unavailable",
"invisible" => "offline",
_ => "online",
};
+54
View File
@@ -976,3 +976,57 @@ mod tests {
assert!(!v.as_object().unwrap().contains_key("remoteId"));
}
}
/// Whether a service has anything to make of a status.
///
/// Offered by what each can actually express, and not by what could be faked
/// out of something nearby: Discord has all four; IRC has away and back, which
/// are idle and online; Matrix has
/// online and offline presence, so invisible is real and idle is not; Kick and
/// Sneedchat have no presence at all.
///
/// Do-not-disturb is online with moho's own desktop notifications silenced,
/// which every service can have, so it is offered everywhere. Only Discord has
/// a real state for it - which also keeps its phone notifications from being
/// pushed - and is sent it; for everything else the account stays online.
pub fn status_supported(account_id: &str, status: &str) -> bool {
match status {
"online" | "dnd" => true,
"idle" => account_id.starts_with("discord:") || !(account_id.starts_with("matrix:") || account_id.starts_with("kick:") || account_id.starts_with("sneedchat:")),
"invisible" => account_id.starts_with("discord:") || account_id.starts_with("matrix:"),
_ => false,
}
}
#[cfg(test)]
mod status_tests {
use super::status_supported;
#[test]
fn discord_has_all_of_them() {
for s in ["online", "idle", "dnd", "invisible"] {
assert!(status_supported("discord:1", s), "{s}");
}
}
#[test]
fn irc_has_away_and_back_and_no_invisible() {
assert!(status_supported("[email protected]", "idle"));
assert!(status_supported("[email protected]", "dnd"));
assert!(!status_supported("[email protected]", "invisible"));
}
#[test]
fn matrix_has_offline_presence_and_no_idle() {
assert!(status_supported("matrix:@me:poa.st", "invisible"));
assert!(!status_supported("matrix:@me:poa.st", "idle"));
}
#[test]
fn kick_and_sneedchat_have_nothing_but_do_not_disturb_is_still_a_switch() {
for account in ["kick:me", "sneedchat:me"] {
assert!(status_supported(account, "online") && status_supported(account, "dnd"), "{account}");
assert!(!status_supported(account, "idle") && !status_supported(account, "invisible"), "{account}");
}
}
}
+10 -1
View File
@@ -1482,6 +1482,13 @@ pub async fn dispatch(
if !matches!(status, "online" | "idle" | "dnd" | "invisible") {
return (None, Some("status must be online, idle, dnd or invisible".to_string()));
}
// What the service has no such state for is left alone: nothing is
// recorded and nothing is sent, so an account stays what it was.
// Matrix has no idle and answers an idle request by saying nothing
// of the kind; Kick and Sneedchat have no status at all.
if !crate::model::status_supported(account_id, status) {
return (Some(serde_json::json!({ "ok": true, "applied": false, "ignored": true })), None);
}
// Recorded before it is applied: a status set while disconnected
// still has to survive to the next connection.
state.runtime.set_account_status(account_id, status);
@@ -1502,7 +1509,9 @@ pub async fn dispatch(
} else if let Some(sender) = state.runtime.irc_sender(account_id) {
// IRC has only away and back.
let result = match status {
"online" => sender.send(irc::proto::Command::AWAY(None)),
// Do not disturb is moho's own switch for its notifications:
// to the network it is still online, and not away.
"online" | "dnd" => sender.send(irc::proto::Command::AWAY(None)),
_ => sender.send(irc::proto::Command::AWAY(Some("Idle".to_string()))),
};
result.is_ok()