on master help sign in
Simplify code and improve type safety: use wildcard patterns, factor broadcastOfflineGossip, bind token once, combine where clauses, type LookupResult.foundServer as OptionalServerName
commit01f5f207fb604677f732e61a361f792f91a2b5c4 authoriko <mail@iko.soy> dateFri 10 Apr 2026, 12:01 (6 months ago) parentf4311e3
27 files changed+106−113
client/App.fs+19−19 client/bin/Debug/net10.0-ios/iossimulator-arm64/Skrepka.app/Skrepka.dll client/bin/Debug/net10.0-ios/iossimulator-arm64/Skrepka.app/Skrepka.pdb client/bin/Debug/net10.0-ios/iossimulator-arm64/Skrepka.app/_CodeSignature/CodeResources+15−4 client/bin/Debug/net10.0-ios/iossimulator-arm64/Skrepka.dll client/bin/Debug/net10.0-ios/iossimulator-arm64/Skrepka.pdb client/bin/Debug/net10.0-maccatalyst/maccatalyst-arm64/Skrepka.app/Contents/MonoBundle/Skrepka.dll client/bin/Debug/net10.0-maccatalyst/maccatalyst-arm64/Skrepka.app/Contents/MonoBundle/Skrepka.pdb client/bin/Debug/net10.0-maccatalyst/maccatalyst-arm64/Skrepka.app/Contents/_CodeSignature/CodeResources+13−2 client/bin/Debug/net10.0-maccatalyst/maccatalyst-arm64/Skrepka.dll client/bin/Debug/net10.0-maccatalyst/maccatalyst-arm64/Skrepka.pdb client/obj/Debug/net10.0-ios/iossimulator-arm64/Skrepka.AssemblyInfo.fs+1−1 client/obj/Debug/net10.0-ios/iossimulator-arm64/Skrepka.AssemblyInfoInputs.cache+1−1 client/obj/Debug/net10.0-ios/iossimulator-arm64/Skrepka.dll client/obj/Debug/net10.0-ios/iossimulator-arm64/Skrepka.fsproj.FileListAbsolute.txt−23 client/obj/Debug/net10.0-ios/iossimulator-arm64/Skrepka.pdb client/obj/Debug/net10.0-ios/iossimulator-arm64/Skrepka.sourcelink.json+1−1 client/obj/Debug/net10.0-ios/iossimulator-arm64/ref/Skrepka.dll client/obj/Debug/net10.0-ios/iossimulator-arm64/refint/Skrepka.dll client/obj/Debug/net10.0-maccatalyst/maccatalyst-arm64/Skrepka.AssemblyInfo.fs+1−1 client/obj/Debug/net10.0-maccatalyst/maccatalyst-arm64/Skrepka.AssemblyInfoInputs.cache+1−1 client/obj/Debug/net10.0-maccatalyst/maccatalyst-arm64/Skrepka.dll client/obj/Debug/net10.0-maccatalyst/maccatalyst-arm64/Skrepka.pdb client/obj/Debug/net10.0-maccatalyst/maccatalyst-arm64/Skrepka.sourcelink.json+1−1 client/obj/Debug/net10.0-maccatalyst/maccatalyst-arm64/ref/Skrepka.dll client/obj/Debug/net10.0-maccatalyst/maccatalyst-arm64/refint/Skrepka.dll server.knot+53−59
client/App.fs+19−19
@@ -99,7 +99,9 @@ module App =
9999 messages |> Map.tryFind pk |> Option.defaultValue []
100100
101101 let private appendMessage (pk: string) (msg: ChatMessage) (messages: Map<string, ChatMessage list>) =
102− messages |> Map.add pk (messagesFor pk messages @ [ msg ])
102+ let existing = messagesFor pk messages
103+ if existing |> List.exists (fun m -> m.Id = msg.Id) then messages
104+ else Map.add pk (existing @ [ msg ]) messages
103105
104106 let private addContactIfNew (contact: Contact) (contacts: Contact list) =
105107 if contacts |> List.exists (fun c -> c.Pubkey = contact.Pubkey) then contacts
@@ -199,12 +201,9 @@ module App =
199201 incoming
200202 |> List.fold
201203 (fun m (fromPubkey, chatMsg) ->
202− if messagesFor fromPubkey m.Messages |> List.exists (fun x -> x.Id = chatMsg.Id) then
203− m
204− else
205− { m with
206− Contacts = m.Contacts |> addContactIfNew { Pubkey = fromPubkey; Nickname = truncKey fromPubkey }
207− Messages = m.Messages |> appendMessage fromPubkey chatMsg })
204+ { m with
205+ Contacts = m.Contacts |> addContactIfNew { Pubkey = fromPubkey; Nickname = truncKey fromPubkey }
206+ Messages = m.Messages |> appendMessage fromPubkey chatMsg })
208207 { model with PollStatus = status; PollCursor = newCursor }
209208
210209 let cmds =
@@ -212,7 +211,7 @@ module App =
212211 | Some session -> [ CmdPoll(session, model'.PollCursor) ]
213212 | None -> []
214213
215− model', cmds @ (if not incoming.IsEmpty then saveCmd model' else [])
214+ model', cmds @ (if incoming.IsEmpty then [] else saveCmd model')
216215
217216 | SetNewPubkey pk ->
218217 match model.Page with
@@ -245,19 +244,19 @@ module App =
245244 | DismissError -> { model with Error = None }, []
246245
247246 | StateLoaded(identity, dataOpt) ->
248− let model' = { model with Identity = Some identity; Conn = Connecting }
249−
250247 let model' =
251248 match dataOpt with
252249 | Some data ->
253− { model' with
250+ { model with
251+ Identity = Some identity
252+ Conn = Connecting
254253 Contacts = data.Contacts
255254 Messages = data.Messages
256255 ServerUrl = if data.ServerUrl <> "" then data.ServerUrl else model.ServerUrl
257256 PollCursor = data.PollCursor
258257 Page = Conversations }
259−
260− | None -> { model' with Page = Settings }
258+ | None ->
259+ { model with Identity = Some identity; Conn = Connecting; Page = Settings }
261260
262261 model', [ CmdConnect(model'.ServerUrl, identity) ]
263262
@@ -327,11 +326,12 @@ module App =
327326 try
328327 let! response = ApiClient.poll session.Url session.Token cursor
329328 let messages, ackIds, errors =
330− response.Events
331− |> Array.fold (fun (msgs, acks, errs) evt ->
332− match decryptEvent session.Identity.PrivKey evt.Payload with
333− | Ok msg -> (msg :: msgs, evt.Id :: acks, errs)
334− | Error err -> (msgs, acks, err :: errs))
329+ Array.foldBack
330+ (fun (evt: ApiClient.PollEvent) (msgs, acks, errs) ->
331+ match decryptEvent session.Identity.PrivKey evt.Payload with
332+ | Ok msg -> (msg :: msgs, evt.Id :: acks, errs)
333+ | Error err -> (msgs, acks, err :: errs))
334+ response.Events
335335 ([], [], [])
336336
337337 if not ackIds.IsEmpty then
@@ -343,7 +343,7 @@ module App =
343343 $"[{now}] evts:{response.Events.Length} msgs:{messages.Length} errs:{errors.Length}"
344344 + (match errors with err :: _ -> $" [{err}]" | [] -> "")
345345
346− return PollResult(List.rev messages, status, response.Cursor)
346+ return PollResult(messages, status, response.Cursor)
347347 with
348348 | :? TimeoutException ->
349349 return PollResult([], "polling...", cursor)
client/bin/Debug/net10.0-ios/iossimulator-arm64/Skrepka.app/Skrepka.dll

Binary file not shown.

client/bin/Debug/net10.0-ios/iossimulator-arm64/Skrepka.app/Skrepka.pdb

Binary file not shown.

client/bin/Debug/net10.0-ios/iossimulator-arm64/Skrepka.app/_CodeSignature/CodeResources+15−4
@@ -32,6 +32,10 @@
3232 <data>
3333 vxA0uDp+BuUEE5Xf1wcoVCW/EYs=
3434 </data>
35+ <key>MauiInfo.plist</key>
36+ <data>
37+ iEtuUGCV6JM3gxS/pO33swhmpp8=
38+ </data>
3539 <key>MauiSplash.storyboardc/01J-lp-oVM-view-Ze5-6b-2t3.nib</key>
3640 <data>
3741 Y78DTyz7+rkuDbhphGJe+YK2rLQ=
@@ -162,11 +166,11 @@
162166 </data>
163167 <key>Skrepka.dll</key>
164168 <data>
165− fWgnzhQubQm/pXCMxdaeASe90IU=
169+ 1YPTBw0a3jfRaHp/FqqEqcamsFo=
166170 </data>
167171 <key>Skrepka.pdb</key>
168172 <data>
169− EreX5kfzNb2S+VnlrBsyMswiGnM=
173+ 318b9V+eeWqqTJ1X1o5LHMWnywY=
170174 </data>
171175 <key>Sodium.Core.dll</key>
172176 <data>
@@ -1161,6 +1165,13 @@
11611165 rjGsapNUkhe56LgUl9HugxZYrebfnh7iDyg48i3l4hg=
11621166 </data>
11631167 </dict>
1168+ <key>MauiInfo.plist</key>
1169+ <dict>
1170+ <key>hash2</key>
1171+ <data>
1172+ tx0KXqEC+DvcCGyogQTEQWHcCWJYYqH24NtPwhpQEjo=
1173+ </data>
1174+ </dict>
11641175 <key>MauiSplash.storyboardc/01J-lp-oVM-view-Ze5-6b-2t3.nib</key>
11651176 <dict>
11661177 <key>hash2</key>
@@ -1382,14 +1393,14 @@
13821393 <dict>
13831394 <key>hash2</key>
13841395 <data>
1385− kLMnGG+nlD35s66fyt9JcYg7ILRHQOazpZrn0F9ecAs=
1396+ w1+88siJc/avY1PQmPGLGB46y4akJb8sDoczgEVI4fM=
13861397 </data>
13871398 </dict>
13881399 <key>Skrepka.pdb</key>
13891400 <dict>
13901401 <key>hash2</key>
13911402 <data>
1392− SO6AxLeTTGVHAEWR0d25BmHOxy5yz4NIYe7juescHjM=
1403+ gaPNj44wAMOFLNjNCIJfaKHiU9IgJDuOQItWdMdbM2Y=
13931404 </data>
13941405 </dict>
13951406 <key>Sodium.Core.dll</key>
client/bin/Debug/net10.0-ios/iossimulator-arm64/Skrepka.dll

Binary file not shown.

client/bin/Debug/net10.0-ios/iossimulator-arm64/Skrepka.pdb

Binary file not shown.

client/bin/Debug/net10.0-maccatalyst/maccatalyst-arm64/Skrepka.app/Contents/MonoBundle/Skrepka.dll

Binary file not shown.

client/bin/Debug/net10.0-maccatalyst/maccatalyst-arm64/Skrepka.app/Contents/MonoBundle/Skrepka.pdb

Binary file not shown.

client/bin/Debug/net10.0-maccatalyst/maccatalyst-arm64/Skrepka.app/Contents/_CodeSignature/CodeResources+13−2
@@ -12,6 +12,10 @@
1212 <data>
1313 c3wF+z6XtMjJ+O5rJymqsoYbzDg=
1414 </data>
15+ <key>Resources/MauiInfo.plist</key>
16+ <data>
17+ iEtuUGCV6JM3gxS/pO33swhmpp8=
18+ </data>
1519 <key>Resources/MonoTouchDebugConfiguration.txt</key>
1620 <data>
1721 oowkf1sL6T19nanXUObgmuayWqM=
@@ -247,14 +251,14 @@
247251 <dict>
248252 <key>hash2</key>
249253 <data>
250− Ll855YGoZZ4EHbGdnm7YJ2/UrFtklAGRlFW9NlLbdXI=
254+ ACzMr65iZCRxNkmPNzA6fyn/yv2+20NTfHo+2J3hOVo=
251255 </data>
252256 </dict>
253257 <key>MonoBundle/Skrepka.pdb</key>
254258 <dict>
255259 <key>hash2</key>
256260 <data>
257− J4e+LdQyWbJGoueMWBhlFQg9tI6hYFwkIT6qA3pNgNU=
261+ 7pMDhAJJCLuEC9ASqtHFdvdW1xJFyw0foQ6ySH91mrU=
258262 </data>
259263 </dict>
260264 <key>MonoBundle/Sodium.Core.dll</key>
@@ -1790,6 +1794,13 @@
17901794 HfYxSjf/+d0hBTO933j7O0S+icn+eoL8QQEDzydpDZM=
17911795 </data>
17921796 </dict>
1797+ <key>Resources/MauiInfo.plist</key>
1798+ <dict>
1799+ <key>hash2</key>
1800+ <data>
1801+ tx0KXqEC+DvcCGyogQTEQWHcCWJYYqH24NtPwhpQEjo=
1802+ </data>
1803+ </dict>
17931804 <key>Resources/MonoTouchDebugConfiguration.txt</key>
17941805 <dict>
17951806 <key>hash2</key>
client/bin/Debug/net10.0-maccatalyst/maccatalyst-arm64/Skrepka.dll

Binary file not shown.

client/bin/Debug/net10.0-maccatalyst/maccatalyst-arm64/Skrepka.pdb

Binary file not shown.

client/obj/Debug/net10.0-ios/iossimulator-arm64/Skrepka.AssemblyInfo.fs+1−1
@@ -10,7 +10,7 @@ open System.Reflection
1010[<assembly: System.Reflection.AssemblyCompanyAttribute("Skrepka")>]
1111[<assembly: System.Reflection.AssemblyConfigurationAttribute("Debug")>]
1212[<assembly: System.Reflection.AssemblyFileVersionAttribute("1.0.0.0")>]
13−[<assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0+64e0fdcd67fd42117a3538c089bb99c276931127")>]
13+[<assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0+f4311e3ec7c47d3491f648d03dad91cf7a73b5d9")>]
1414[<assembly: System.Reflection.AssemblyProductAttribute("Skrepka")>]
1515[<assembly: System.Reflection.AssemblyTitleAttribute("Skrepka")>]
1616[<assembly: System.Reflection.AssemblyVersionAttribute("1.0.0.0")>]
client/obj/Debug/net10.0-ios/iossimulator-arm64/Skrepka.AssemblyInfoInputs.cache+1−1
@@ -1 +1 @@
1−e50f958afa3607e0c2176340e354fd0c5d565507a4ce35d8cd858d8feeb3f6a1
1+1e95aacb0126fb0fa0c97c9af194457ae6f1585fbb05c5244efbdd807cb04e1c
client/obj/Debug/net10.0-ios/iossimulator-arm64/Skrepka.dll

Binary file not shown.

client/obj/Debug/net10.0-ios/iossimulator-arm64/Skrepka.fsproj.FileListAbsolute.txt−23
@@ -333,27 +333,4 @@
333333/Users/iko/Developer/skrepka/client/obj/Debug/net10.0-ios/iossimulator-arm64/Skrepka.pdb
334334/Users/iko/Developer/skrepka/client/obj/Debug/net10.0-ios/iossimulator-arm64/Skrepka.genruntimeconfig.cache
335335/Users/iko/Developer/skrepka/client/obj/Debug/net10.0-ios/iossimulator-arm64/ref/Skrepka.dll
336−/Users/iko/Developer/skrepka/client/obj/Debug/net10.0-ios/iossimulator-arm64/resourcestamps/Fabulous.stamp
337−/Users/iko/Developer/skrepka/client/obj/Debug/net10.0-ios/iossimulator-arm64/resourcestamps/Fabulous.MauiControls.stamp
338−/Users/iko/Developer/skrepka/client/obj/Debug/net10.0-ios/iossimulator-arm64/resourcestamps/FSharp.Core.stamp
339−/Users/iko/Developer/skrepka/client/obj/Debug/net10.0-ios/iossimulator-arm64/resourcestamps/Microsoft.Extensions.Configuration.Abstractions.stamp
340−/Users/iko/Developer/skrepka/client/obj/Debug/net10.0-ios/iossimulator-arm64/resourcestamps/Microsoft.Extensions.Configuration.stamp
341−/Users/iko/Developer/skrepka/client/obj/Debug/net10.0-ios/iossimulator-arm64/resourcestamps/Microsoft.Extensions.DependencyInjection.Abstractions.stamp
342−/Users/iko/Developer/skrepka/client/obj/Debug/net10.0-ios/iossimulator-arm64/resourcestamps/Microsoft.Extensions.DependencyInjection.stamp
343−/Users/iko/Developer/skrepka/client/obj/Debug/net10.0-ios/iossimulator-arm64/resourcestamps/Microsoft.Extensions.Diagnostics.Abstractions.stamp
344−/Users/iko/Developer/skrepka/client/obj/Debug/net10.0-ios/iossimulator-arm64/resourcestamps/Microsoft.Extensions.FileProviders.Abstractions.stamp
345−/Users/iko/Developer/skrepka/client/obj/Debug/net10.0-ios/iossimulator-arm64/resourcestamps/Microsoft.Extensions.Hosting.Abstractions.stamp
346−/Users/iko/Developer/skrepka/client/obj/Debug/net10.0-ios/iossimulator-arm64/resourcestamps/Microsoft.Extensions.Logging.Abstractions.stamp
347−/Users/iko/Developer/skrepka/client/obj/Debug/net10.0-ios/iossimulator-arm64/resourcestamps/Microsoft.Extensions.Logging.stamp
348−/Users/iko/Developer/skrepka/client/obj/Debug/net10.0-ios/iossimulator-arm64/resourcestamps/Microsoft.Extensions.Options.stamp
349−/Users/iko/Developer/skrepka/client/obj/Debug/net10.0-ios/iossimulator-arm64/resourcestamps/Microsoft.Extensions.Primitives.stamp
350−/Users/iko/Developer/skrepka/client/obj/Debug/net10.0-ios/iossimulator-arm64/resourcestamps/Microsoft.Maui.Controls.Compatibility.stamp
351−/Users/iko/Developer/skrepka/client/obj/Debug/net10.0-ios/iossimulator-arm64/resourcestamps/Microsoft.Maui.Controls.stamp
352−/Users/iko/Developer/skrepka/client/obj/Debug/net10.0-ios/iossimulator-arm64/resourcestamps/Microsoft.Maui.Controls.Xaml.stamp
353−/Users/iko/Developer/skrepka/client/obj/Debug/net10.0-ios/iossimulator-arm64/resourcestamps/Microsoft.Maui.stamp
354−/Users/iko/Developer/skrepka/client/obj/Debug/net10.0-ios/iossimulator-arm64/resourcestamps/Microsoft.Maui.Essentials.stamp
355−/Users/iko/Developer/skrepka/client/obj/Debug/net10.0-ios/iossimulator-arm64/resourcestamps/Microsoft.Maui.Graphics.stamp
356−/Users/iko/Developer/skrepka/client/obj/Debug/net10.0-ios/iossimulator-arm64/resourcestamps/Sodium.Core.stamp
357−/Users/iko/Developer/skrepka/client/obj/Debug/net10.0-ios/iossimulator-arm64/unpack/Microsoft.Maui.Essentials/BundleResource/xml/microsoft_maui_essentials_fileprovider_file_paths.xml
358336/Users/iko/Developer/skrepka/client/bin/Debug/net10.0-ios/iossimulator-arm64/LiteDB.dll
359−/Users/iko/Developer/skrepka/client/obj/Debug/net10.0-ios/iossimulator-arm64/resourcestamps/LiteDB.stamp
client/obj/Debug/net10.0-ios/iossimulator-arm64/Skrepka.pdb

Binary file not shown.

client/obj/Debug/net10.0-ios/iossimulator-arm64/Skrepka.sourcelink.json+1−1
@@ -1 +1 @@
1−{"documents":{"/Users/iko/Developer/skrepka/*":"https://raw.githubusercontent.com/ilyakooo0/skrepka/64e0fdcd67fd42117a3538c089bb99c276931127/*"}}
1+{"documents":{"/Users/iko/Developer/skrepka/*":"https://raw.githubusercontent.com/ilyakooo0/skrepka/f4311e3ec7c47d3491f648d03dad91cf7a73b5d9/*"}}
client/obj/Debug/net10.0-ios/iossimulator-arm64/ref/Skrepka.dll

Binary file not shown.

client/obj/Debug/net10.0-ios/iossimulator-arm64/refint/Skrepka.dll

Binary file not shown.

client/obj/Debug/net10.0-maccatalyst/maccatalyst-arm64/Skrepka.AssemblyInfo.fs+1−1
@@ -10,7 +10,7 @@ open System.Reflection
1010[<assembly: System.Reflection.AssemblyCompanyAttribute("Skrepka")>]
1111[<assembly: System.Reflection.AssemblyConfigurationAttribute("Debug")>]
1212[<assembly: System.Reflection.AssemblyFileVersionAttribute("1.0.0.0")>]
13−[<assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0+e692eb5d42ef866622de97d2bc08d09fdb62a11b")>]
13+[<assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0+f4311e3ec7c47d3491f648d03dad91cf7a73b5d9")>]
1414[<assembly: System.Reflection.AssemblyProductAttribute("Skrepka")>]
1515[<assembly: System.Reflection.AssemblyTitleAttribute("Skrepka")>]
1616[<assembly: System.Reflection.AssemblyVersionAttribute("1.0.0.0")>]
client/obj/Debug/net10.0-maccatalyst/maccatalyst-arm64/Skrepka.AssemblyInfoInputs.cache+1−1
@@ -1 +1 @@
1−4dcdec3fa3f1ebbb1a84fb886e9dd06477af7c654b1d2094630d80c574da8beb
1+0e18d76dae776b5ef2d17435d35ec648e83dd873ca4b5da4371eead24ce9a533
client/obj/Debug/net10.0-maccatalyst/maccatalyst-arm64/Skrepka.dll

Binary file not shown.

client/obj/Debug/net10.0-maccatalyst/maccatalyst-arm64/Skrepka.pdb

Binary file not shown.

client/obj/Debug/net10.0-maccatalyst/maccatalyst-arm64/Skrepka.sourcelink.json+1−1
@@ -1 +1 @@
1−{"documents":{"/Users/iko/Developer/skrepka/*":"https://raw.githubusercontent.com/ilyakooo0/skrepka/e692eb5d42ef866622de97d2bc08d09fdb62a11b/*"}}
1+{"documents":{"/Users/iko/Developer/skrepka/*":"https://raw.githubusercontent.com/ilyakooo0/skrepka/f4311e3ec7c47d3491f648d03dad91cf7a73b5d9/*"}}
client/obj/Debug/net10.0-maccatalyst/maccatalyst-arm64/ref/Skrepka.dll

Binary file not shown.

client/obj/Debug/net10.0-maccatalyst/maccatalyst-arm64/refint/Skrepka.dll

Binary file not shown.

server.knot+53−59
@@ -74,7 +74,7 @@ type PollResult = PollUnauthorized {} | PollAuthorized {events: [PollEvent], cur
7474type VerifyResult = VerifyFailed {} | VerifyOk {peerServers: [Text]}
7575type SendResult = SendUnauthorized {} | SendRejected {} | SendDone {status: MessageStatus, remoteServers: [Text], fromKey: PubkeyHex}
7676type AckResult = AckUnauthorized {} | AckDone {pendingAcks: [AckedEntry], evictedEntries: [AckedEntry]}
77−type LookupResult = LookupUnauthorized {} | LookupDone {foundServer: Text}
77+type LookupResult = LookupUnauthorized {} | LookupDone {foundServer: OptionalServerName}
7878
7979
8080-- ============================================
@@ -151,6 +151,10 @@ route Api = AuthApi | ClientApi | FederationApi | MaintenanceApi
151151any = \xs pred -> fold (\acc x -> if acc then acc else pred x) (False {}) xs
152152none = \xs pred -> not (any xs pred)
153153when = \cond action -> if cond then action else yield {}
154+whenOk = \result action -> case result of
155+ Ok _ -> action
156+ Err _ -> yield {}
157+clamp = \lo hi x -> if x < lo then lo else if x > hi then hi else x
154158headOf = \items -> fold (\acc x -> case acc of
155159 Nothing {} -> Just {value: x}
156160 _ -> acc) Nothing {} items
@@ -204,11 +208,6 @@ isAllowedServerChar = \c ->
204208 || c == "m" || c == "n" || c == "o" || c == "p" || c == "q" || c == "r"
205209 || c == "s" || c == "t" || c == "u" || c == "v" || c == "w" || c == "x"
206210 || c == "y" || c == "z"
207− || c == "A" || c == "B" || c == "C" || c == "D" || c == "E" || c == "F"
208− || c == "G" || c == "H" || c == "I" || c == "J" || c == "K" || c == "L"
209− || c == "M" || c == "N" || c == "O" || c == "P" || c == "Q" || c == "R"
210− || c == "S" || c == "T" || c == "U" || c == "V" || c == "W" || c == "X"
211− || c == "Y" || c == "Z"
212211
213212containsBadUrlChar = \s ->
214213 if s == "" then False {}
@@ -373,9 +372,9 @@ authenticateSession = \sessions token t ->
373372 Just {value: s} -> Authorized {pubkey: s.pubkey}
374373
375374extractToken = \authHeader ->
376− if toLower (take 7 authHeader) == "bearer "
377− && not (tooLong (drop 7 authHeader) maxTokenLen)
378− then drop 7 authHeader
375+ let token = drop 7 authHeader in
376+ if toLower (take 7 authHeader) == "bearer " && not (tooLong token maxTokenLen)
377+ then token
379378 else ""
380379
381380verifySecret = \token secret ->
@@ -388,9 +387,9 @@ verifyMaintenanceAuth = \authorization ->
388387 let token = extractToken authorization in
389388 verifySecret token federationSecret || verifySecret token maintenanceSecret
390389
391−clampTtl = \ttl -> if ttl < 1 then 1 else if ttl > maxPresenceTtl then maxPresenceTtl else ttl
390+clampTtl = clamp 1 maxPresenceTtl
392391
393−clampTimestamp = \timestamp t -> if timestamp > t then t else if timestamp < t - messageMaxAge then t - messageMaxAge else timestamp
392+clampTimestamp = \timestamp t -> clamp (t - messageMaxAge) t timestamp
394393
395394nextSeqTs = \t seqCounter -> if t > seqCounter then t else seqCounter + 1
396395
@@ -500,8 +499,7 @@ removeOffline = \presenceSet offlineOnlyKeys trustedOffline t ->
500499countMessagesFor = \messages toKey t ->
501500 count (do
502501 m <- messages
503− where m.toKey == toKey
504− where m.receivedAt >= t - messageMaxAge
502+ where m.toKey == toKey && m.receivedAt >= t - messageMaxAge
505503 yield m)
506504
507505
@@ -595,7 +593,8 @@ sendBatchedGossip = \peerServers allEvents ->
595593broadcastOfflineGossip = \t peerServers expiredLocal -> do
596594 let expiredEntries = mergeByKey (\a b -> a.pubkey == b.pubkey) [] (do
597595 p <- expiredLocal
598− yield {pubkey: p.pubkey, ttl: (let rawTtl = (p.expiresAt - p.timestamp) / 1000 in if rawTtl < offlineGossipMinTtl then offlineGossipMinTtl else if rawTtl > maxPresenceTtl then maxPresenceTtl else rawTtl), expiresAt: p.expiresAt})
596+ let ttl = clamp offlineGossipMinTtl maxPresenceTtl ((p.expiresAt - p.timestamp) / 1000)
597+ yield {pubkey: p.pubkey, ttl: ttl, expiresAt: p.expiresAt})
599598 when (federationEnabled && count expiredEntries > 0 && count peerServers > 0) (do
600599 let gossipEvents = do
601600 e <- expiredEntries
@@ -605,6 +604,10 @@ broadcastOfflineGossip = \t peerServers expiredLocal -> do
605604forkBroadcastOffline = \t result ->
606605 fork (broadcastOfflineGossip t result.peerServers result.expiredLocal)
607606
607+maybePruneAndBroadcast = \t -> do
608+ pruneResult <- atomic do maybePrune t
609+ forkBroadcastOffline t pruneResult
610+
608611removeForward = \mid srv -> atomic do
609612 forwards <- *forwards
610613 set *forwards = do
@@ -624,9 +627,7 @@ retryForwardsForServer = \entries remaining ->
624627 toKey: f.toKey, fromKey: f.fromKey, encryptedBlob: f.encryptedBlob,
625628 timestamp: f.timestamp, originServer: serverHostN, messageId: f.messageId,
626629 authorization: "Bearer " ++ federationSecret})
627− case result of
628− Ok _ -> removeForward f.messageId f.toServer
629− Err _ -> yield {}
630+ whenOk result (removeForward f.messageId f.toServer)
630631 retryForwardsForServer rest (remaining - 1)
631632
632633retryPendingForwards : [ForwardEntry] -> IO {network} {}
@@ -643,15 +644,12 @@ sendBatchedAcksToServer = \serverAcks server ->
643644 authorization: "Bearer " ++ federationSecret,
644645 messageIds: (do a <- batch; yield a.messageId),
645646 originServer: server, fromServer: serverHostN})
646− case result of
647− Ok _ ->
648− atomic do
649− ackedIds <- *ackedIds
650− set *ackedIds = do
651− a <- ackedIds
652− where not (a.originServer == server && any batch (\sa -> sa.messageId == a.messageId && sa.toKey == a.toKey))
653− yield a
654− Err _ -> yield {})
647+ whenOk result (atomic do
648+ ackedIds <- *ackedIds
649+ set *ackedIds = do
650+ a <- ackedIds
651+ where not (a.originServer == server && any batch (\sa -> sa.messageId == a.messageId && sa.toKey == a.toKey))
652+ yield a))
655653
656654sendAcksAndClean : [{messageId: MessageIdHex, toKey: PubkeyHex, originServer: ServerName, | a}] -> [ServerName] -> IO {network} {}
657655sendAcksAndClean = \pendingAcks servers ->
@@ -685,12 +683,10 @@ pollOnce = \token cursor -> do
685683 seqCounter <- *seqCounter
686684 messages <- *messages
687685 let cursorCeil = if seqCounter > t then seqCounter else t
688− let clampedCursor = if cursor < 0 then 0 else if cursor > cursorCeil then cursorCeil else cursor
686+ let clampedCursor = clamp 0 cursorCeil cursor
689687 let matchingMsgs = do
690688 m <- messages
691− where m.toKey == pubkey
692− where m.receivedAt >= t - messageMaxAge
693− where m.receivedAt > clampedCursor
689+ where m.toKey == pubkey && m.receivedAt >= t - messageMaxAge && m.receivedAt > clampedCursor
694690 yield m
695691 let limitedMsgs = boundedSmallest matchingMsgs maxPollEvents (\m -> m.receivedAt)
696692 let evts = do
@@ -771,31 +767,32 @@ serve = \req -> case req of
771767 then collectPeerServers updatedPresence (\p -> p.server)
772768 else []
773769 yield {pr: pr, verify: VerifyOk {peerServers: peerServers}}
774− fork (case result.verify of
775− VerifyOk {peerServers} -> do
776− when federationEnabled
777− (sendToEach peerServers (\server -> do
778− _ <- fetch (federationScheme ++ server) (RecvGossip {
779− authorization: "Bearer " ++ federationSecret,
780− events: [{eventType: "online", pubkey: pubkey, server: serverHostN, timestamp: t, ttl: onlineGossipTtl}], hopsRemaining: 2, fromServer: serverHostN})
781− yield {}))
782− broadcastOfflineGossip t result.pr.peerServers (do p <- result.pr.expiredLocal; where not (p.pubkey == pubkey); yield p)
783− VerifyFailed {} ->
784− broadcastOfflineGossip t result.pr.peerServers result.pr.expiredLocal)
770+ fork (do
771+ case result.verify of
772+ VerifyOk {peerServers} ->
773+ when federationEnabled
774+ (sendToEach peerServers (\server -> do
775+ _ <- fetch (federationScheme ++ server) (RecvGossip {
776+ authorization: "Bearer " ++ federationSecret,
777+ events: [{eventType: "online", pubkey: pubkey, server: serverHostN, timestamp: t, ttl: onlineGossipTtl}], hopsRemaining: 2, fromServer: serverHostN})
778+ yield {}))
779+ _ -> yield {}
780+ let expiredLocal = case result.verify of
781+ VerifyOk _ -> do p <- result.pr.expiredLocal; where not (p.pubkey == pubkey); yield p
782+ _ -> result.pr.expiredLocal
783+ broadcastOfflineGossip t result.pr.peerServers expiredLocal)
785784 yield (case result.verify of
786785 VerifyFailed _ -> respond {token: "", expiresAt: 0}
787786 VerifyOk _ -> respond {token: token, expiresAt: expiry})
788787
789788 Poll {cursor, timeout, authorization, respond} -> do
790789 t <- now
791− let clampedTimeout = if timeout < 0 then 0 else if timeout > 60000 then 60000 else timeout
790+ let clampedTimeout = clamp 0 60000 timeout
792791 let deadline = t + clampedTimeout
793792 let token = extractToken authorization
794793 result <- pollLoop token cursor deadline
795794 tAfterPoll <- now
796− pruneResult <- atomic do
797− maybePrune tAfterPoll
798− forkBroadcastOffline tAfterPoll pruneResult
795+ maybePruneAndBroadcast tAfterPoll
799796 yield (case result of
800797 PollUnauthorized {} -> respond {events: [], cursor: -1, authorized: False {}}
801798 PollAuthorized {events, cursor: c} -> respond {events: events, cursor: c, authorized: True {}})
@@ -825,7 +822,7 @@ serve = \req -> case req of
825822 set *messages = union messages
826823 [{messageId: msgId, toKey: to, fromKey: fromKey,
827824 encryptedBlob: encryptedBlob, timestamp: clampedTs, originServer: "", receivedAt: seqTs}]
828− let toPresence = do p <- presence; where p.pubkey == to; where p.expiresAt > t; yield p
825+ let toPresence = do p <- presence; where p.pubkey == to && p.expiresAt > t; yield p
829826 let localOnline = any toPresence (\p -> p.server == serverHostN)
830827 let remoteServers = do p <- toPresence; where not (p.server == serverHostN); yield p.server
831828 forwards <- *forwards
@@ -847,13 +844,10 @@ serve = \req -> case req of
847844 toKey: to, fromKey: fromKey, encryptedBlob: encryptedBlob,
848845 timestamp: clampedTs, originServer: serverHostN, messageId: msgId,
849846 authorization: "Bearer " ++ federationSecret})
850− case fwdResult of
851− Ok _ -> removeForward msgId server
852− Err _ -> yield {})
847+ whenOk fwdResult (removeForward msgId server))
853848 when (status == "queued")
854849 (println ("sendMessage: forwards at capacity (" ++ show maxForwardsEntries ++ "), skipping forward tracking")))
855− SendUnauthorized {} -> yield {}
856− SendRejected {} -> yield {}
850+ _ -> yield {}
857851 broadcastOfflineGossip t result.pr.peerServers result.pr.expiredLocal)
858852 yield (case result.send of
859853 SendUnauthorized _ -> respond {status: "unauthorized", messageId: ""}
@@ -923,7 +917,7 @@ serve = \req -> case req of
923917 when (dropped > 0)
924918 (println ("ackMessages: ackedIds at capacity (" ++ show maxAckedIds ++ "), " ++ show dropped ++ " ack entries permanently lost for " ++ server))
925919 _ -> yield {}))
926− AckUnauthorized {} -> yield {}
920+ _ -> yield {}
927921 broadcastOfflineGossip t result.pr.peerServers result.pr.expiredLocal)
928922 yield (case result.ack of
929923 AckUnauthorized _ -> respond {ok: False {}}
@@ -943,12 +937,13 @@ serve = \req -> case req of
943937 presence <- *presence
944938 let valid = do
945939 p <- presence
946− where p.pubkey == pubkey
947− where p.expiresAt > t
940+ where p.pubkey == pubkey && p.expiresAt > t
948941 yield p
949− let foundServer = if any valid (\p -> p.server == serverHostN)
950− then serverHostN
951− else (fold (\acc p -> if p.expiresAt > acc.best then {server: p.server, best: p.expiresAt} else acc) {server: "", best: 0} valid).server
942+ let foundServer = (fold (\acc p ->
943+ if acc.server == serverHostN then acc
944+ else if p.server == serverHostN then {server: serverHostN, best: p.expiresAt}
945+ else if p.expiresAt > acc.best then {server: p.server, best: p.expiresAt}
946+ else acc) {server: "", best: 0} valid).server
952947 yield {pr: pr, lookup: LookupDone {foundServer: foundServer}}
953948 forkBroadcastOffline t result.pr
954949 yield (case result.lookup of
@@ -962,8 +957,7 @@ serve = \req -> case req of
962957 let fromServer = normalizeServerName fromServer
963958 if not authorized || cappedHops < 1 || fromServer == serverHostN || count events > maxGossipEvents
964959 then do
965− pruneResult <- atomic do maybePrune t
966− forkBroadcastOffline t pruneResult
960+ maybePruneAndBroadcast t
967961 yield (respond {ok: False {}})
968962 else do
969963 let allValidEntries = validGossipEvents events t