@@ -180,21 +180,22 @@ requireClientIp = True {}
180180-- polling pubkey. The server cannot read the blob.
181181type PollEvent = {encryptedBlob: BlobHex}
182182
183−-- Kind of presence event flowing through federation gossip. `GossipUnknown` is
184−-- the parser's fallback for forward-compatibility with future event types.
185−data GossipEventType = GossipOnline {} | GossipOffline {} | GossipUnknown {}
183+-- One presence change exchanged between federated servers. Per-variant
184+-- payloads keep room for future event kinds that aren't tied to a pubkey.
185+-- `GossipUnknown` is the parser's fallback for forward-compatibility.
186+data GossipEventType
187+ = GossipOnline {pubkey: PubkeyHex}
188+ | GossipOffline {pubkey: PubkeyHex}
189+ | GossipUnknown {}
186190
187191impl ToJSON GossipEventType where
188− toJson et = toJson (case et of
189− GossipOnline {} -> "online"
190− GossipOffline {} -> "offline"
191− GossipUnknown {} -> "unknown")
192+ toJson et = case et of
193+ GossipOnline {pubkey} -> toJson {eventType: "online", pubkey}
194+ GossipOffline {pubkey} -> toJson {eventType: "offline", pubkey}
195+ GossipUnknown {} -> toJson {eventType: "unknown"}
192196
193197impl FromJSON GossipEventType where
194− parseJson t = (\s -> if s == "online" then GossipOnline {} else if s == "offline" then GossipOffline {} else GossipUnknown {}) (parseJson t : Text)
195−
196−-- One presence change exchanged between federated servers.
197−type GossipEvent = {eventType: GossipEventType, pubkey: PubkeyHex}
198+ parseJson t = (\raw -> if raw.eventType == "online" then GossipOnline {raw.pubkey} else if raw.eventType == "offline" then GossipOffline {raw.pubkey} else GossipUnknown {}) (parseJson t : {eventType: Text, pubkey: PubkeyHex})
198199
199200-- /messages is all-or-nothing per call: either every message in the batch is
200201-- enqueued (delivered, federated, or buffered for federation), or none are
@@ -322,7 +323,7 @@ route ClientApi where
322323 POST {messages: [{to: PubkeyHex, encryptedBlob: BlobHex}]} /messages headers {authorization: Text, clientIp: Text} -> {} = SendMessages
323324
324325route FederationApi where
325− POST {events: [GossipEvent], fromServer: ServerName} /federation/gossip -> {} = RecvGossip
326+ POST {events: [GossipEventType], fromServer: ServerName} /federation/gossip -> {} = RecvGossip
326327 POST {toKey: PubkeyHex, encryptedBlob: BlobHex, fromServer: ServerName} /federation/forward -> {} = ForwardMessage
327328 POST {forwards: [{toKey: PubkeyHex, encryptedBlob: BlobHex}], fromServer: ServerName} /federation/forward-batch -> {results: [{ok: Bool}]} = FederationBatchForward
328329
@@ -692,13 +693,9 @@ pruneExpired = \t -> atomic do
692693-- a peer record into `forwardFailures` (shared with forward retries) so both
693694-- subsystems back off together.
694695
695−-- Build a gossip event with the originating pubkey's status.
696−mkGossip : GossipEventType -> PubkeyHex -> GossipEvent
697−mkGossip = \etype pk -> {eventType: etype, pubkey: pk}
698−
699696-- Fan out one bundle of gossip events to each peer, skipping peers in backoff.
700697-- Records or clears federation-failure counters per call.
701−sendBatchedGossip : [Text] -> [GossipEvent] -> IO {} {}
698+sendBatchedGossip : [Text] -> [GossipEventType] -> IO {} {}
702699sendBatchedGossip = \peers events -> forEach peers (\server -> do
703700 t <- now
704701 failures <- *forwardFailures
@@ -713,7 +710,9 @@ sendBatchedGossip = \peers events -> forEach peers (\server -> do
713710broadcastOfflineGossip : [{pubkey: PubkeyHex | a}] -> IO {r *presence} {}
714711broadcastOfflineGossip = \expiredLocal -> do
715712 presence <- *presence
716− when (federationEnabled && count expiredLocal > 0) (sendBatchedGossip (knownPeerServersFromPresence presence) (pluck expiredLocal (\p -> mkGossip (GossipOffline {}) p.pubkey)))
713+ when (federationEnabled && count expiredLocal > 0) (sendBatchedGossip (knownPeerServersFromPresence presence) (pluck expiredLocal (\p -> GossipOffline {
714+ p.pubkey
715+ })))
717716
718717-- ============================================
719718-- Forward retries
@@ -802,7 +801,9 @@ refreshLocalPresence = \t -> do
802801 let updated = union kept refreshed
803802 *presence = updated
804803 yield {refreshed, peers: knownPeerServersFromPresence updated})
805− when (count refreshState.refreshed > 0) (fork (sendBatchedGossip refreshState.peers (pluck refreshState.refreshed (\p -> mkGossip (GossipOnline {}) p.pubkey))))
804+ when (count refreshState.refreshed > 0) (fork (sendBatchedGossip refreshState.peers (pluck refreshState.refreshed (\p -> GossipOnline {
805+ p.pubkey
806+ }))))
806807
807808-- ============================================
808809-- Handler: challenge / verify
@@ -893,9 +894,7 @@ handleVerifyAuth = \pubkey challenge signature revokeOthers clientIp -> do
893894 if not result.valid
894895 then yield (Err {error: {status: 401, message: "invalid"}})
895896 else do
896− when federationEnabled (fork (sendBatchedGossip result.peers [
897− mkGossip (GossipOnline {}) pubkey
898− ]))
897+ when federationEnabled (fork (sendBatchedGossip result.peers [GossipOnline {pubkey}]))
899898 yield (Ok {value: {token, expiresAt: expiry}})
900899
901900-- ============================================
@@ -930,9 +929,7 @@ pollOnce = \authorization cursor clientIp t -> do
930929 Nothing {} -> yield (Err {error: {status: 401, message: "unauthorized"}})
931930 Just {value: s} -> do
932931 pollResult <- doPoll s t
933− yield (Ok {
934− value: {pollResult.events, pollResult.cursor}
935− })
932+ yield (Ok {value: {pollResult.events, pollResult.cursor}})
936933
937934-- One pass: collect events at `t`. If empty, retry-wait on `pollWakeup`
938935-- (bumped by send/heartbeat) and re-collect once at the new wall time.
@@ -1000,14 +997,16 @@ handleSendMessages = \messages authorization clientIp -> do
1000997 decision <- atomic (do
1001998 checked <- mapEach limited (\m -> do
1002999 c <- checkSendRate senderKey m.to t
1003− yield {error: c.error, remoteServers: c.remoteServers, to: m.to, encryptedBlob: m.encryptedBlob})
1000+ yield {c.error, c.remoteServers, m.to, m.encryptedBlob})
10041001 case findFirst checked (\c -> c.error != "") of
1005− Just {value: bad} -> yield (Err {error: bad.error})
1002+ Just {value: bad} -> yield (Err {bad.error})
10061003 Nothing {} -> do
10071004 forEach checked (\c -> commitSendMessage c.to c.encryptedBlob c.remoteServers t)
10081005 yield (Ok {value: checked}))
10091006 case decision of
1010− Err {error: code} -> yield (Err {error: {status: if code == "self_send" then 400 else 429, message: code}})
1007+ Err {error: code} -> yield (Err {
1008+ error: {status: if code == "self_send" then 400 else 429, message: code}
1009+ })
10111010 Ok {value: checked} -> do
10121011 when federationEnabled (forEach checked (\c -> fork (forkForwardsTo c.to c.encryptedBlob c.remoteServers)))
10131012 yield (Ok {value: {}}))
@@ -1110,7 +1109,7 @@ forkForwardsTo = \to encryptedBlob servers -> forEach servers (\server -> do
11101109-- pubkeys with a `GossipOnline` event (so Online wins on collision). For keys
11111110-- that transition offline → online, flushes any locally-buffered messages via
11121111-- `forwardQueuedToServer`.
1113−handleRecvGossip : [{eventType: <GossipOffline {} | GossipOnline {} | a>, pubkey: PubkeyHex | b}] -> Text -> IO {rw *presence, clock} (Result HttpError {})
1112+handleRecvGossip : [GossipEventType] -> Text -> IO {rw *presence, clock} (Result HttpError {})
11141113handleRecvGossip = \events fromServer -> do
11151114 t <- now
11161115 if not isRemote fromServer || isBadServerName fromServer || count events > maxGossipEvents
@@ -1118,10 +1117,18 @@ handleRecvGossip = \events fromServer -> do
11181117 else do
11191118 newlyOnline <- atomic (do
11201119 pres <- *presence
1121− let touchedPubkeys = pluck events (\e -> e.pubkey)
1122− let onlinePubkeys = pluck (filter events (\e -> case e.eventType of
1123− GossipOnline {} -> True {}
1124− _ -> False {})) (\e -> e.pubkey)
1120+ let isOnline = \e -> case e of
1121+ GossipOnline {pubkey: _} -> True {}
1122+ _ -> False {}
1123+ let isOffline = \e -> case e of
1124+ GossipOffline {pubkey: _} -> True {}
1125+ _ -> False {}
1126+ let pubkeyOf = \e -> case e of
1127+ GossipOnline {pubkey} -> pubkey
1128+ GossipOffline {pubkey} -> pubkey
1129+ _ -> ""
1130+ let onlinePubkeys = pluck (filter events isOnline) pubkeyOf
1131+ let touchedPubkeys = pluck (filter events (\e -> isOnline e || isOffline e)) pubkeyOf
11251132 let kept = filter pres (\p -> p.server != fromServer || not elem p.pubkey touchedPubkeys)
11261133 let added = map (\pk -> {pubkey: pk, server: fromServer, expiresAt: t + onlineGossipTtl}) onlinePubkeys
11271134 *presence = union kept added