on master help sign in
Simplify server code and improve type safety: introduce named types for persisted state, recursive genToken, remove dead code; add client persistence layer
commit82cb0cdcfaf50735d5a3e12b5af1927d4f49fc36 authoriko <mail@iko.soy> dateThu 9 Apr 2026, 23:19 (6 months ago) parent064c373
13 files changed+275−54
client/App.fs+124−18 client/Skrepka.fsproj+1 client/Store.fs+93 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.fsproj.CoreCompileInputs.cache+1−1 client/obj/Debug/net10.0-ios/iossimulator-arm64/Skrepka.fsproj.FileListAbsolute.txt+22 client/obj/Debug/net10.0-ios/iossimulator-arm64/Skrepka.sourcelink.json+1−1 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.fsproj.CoreCompileInputs.cache+1−1 client/obj/Debug/net10.0-maccatalyst/maccatalyst-arm64/Skrepka.sourcelink.json+1−1 server.knot+27−28
client/App.fs+124−18
@@ -4,6 +4,7 @@ open System
44open System.Text.Json
55open Fabulous
66open Fabulous.Maui
7+open System.Collections.Generic
78open Microsoft.Maui.Graphics
89
910open type Fabulous.Maui.View
@@ -81,6 +82,7 @@ module App =
8182 | DoPoll
8283 | CopyPubKey
8384 | DismissError
85+ | StateLoaded of identity: (byte[] * string) option * data: Store.DataDto option
8486
8587 // ── CmdMsg ──
8688
@@ -90,6 +92,9 @@ module App =
9092 | CmdSend of session: Session * recipientHex: string * text: string * messageId: string
9193 | CmdPoll of session: Session * cursor: uint64
9294 | CmdCopyToClipboard of string
95+ | CmdLoadState
96+ | CmdSaveIdentity of privKey: byte[] * pubKeyHex: string
97+ | CmdSaveData of contacts: Contact list * messages: Map<string, ChatMessage list> * serverUrl: string * cursor: uint64
9398
9499 // ── Helpers ──
95100
@@ -123,6 +128,9 @@ module App =
123128 | Online session -> Some session
124129 | _ -> None
125130
131+ let private saveCmd model =
132+ [ CmdSaveData(model.Contacts, model.Messages, model.ServerUrl, model.PollCursor) ]
133+
126134 // ── Init ──
127135
128136 let init () =
@@ -139,7 +147,7 @@ module App =
139147 Error = None
140148 PollCursor = 0UL
141149 LastSendStatus = "" },
142− []
150+ [ CmdLoadState ]
143151
144152 // ── Update ──
145153
@@ -150,7 +158,8 @@ module App =
150158 | GenIdentity -> model, [ CmdGenIdentity ]
151159
152160 | IdentityReady identity ->
153− { model with Identity = Some identity; Page = Settings }, []
161+ { model with Identity = Some identity; Page = Settings },
162+ [ CmdSaveIdentity(identity.PrivKey, identity.PubKeyHex) ]
154163
155164 | SetServerUrl url -> { model with ServerUrl = url }, []
156165
@@ -187,10 +196,12 @@ module App =
187196 IsOutgoing = true }
188197 let msgs = messagesFor pk model.Messages
189198
190− { model with
191− ComposeText = ""
192− Messages = model.Messages |> Map.add pk (msgs @ [ outMsg ]) },
193− [ CmdSend(session, pk, text, id) ]
199+ let model' =
200+ { model with
201+ ComposeText = ""
202+ Messages = model.Messages |> Map.add pk (msgs @ [ outMsg ]) }
203+
204+ model', CmdSend(session, pk, text, id) :: saveCmd model'
194205 | _ -> model, []
195206
196207 | Sent status -> { model with LastSendStatus = status }, []
@@ -216,9 +227,12 @@ module App =
216227 Messages = m.Messages |> Map.add msg.FromPubkey (msgs @ [ chatMsg ]) })
217228 { model with PollStatus = status; PollCursor = newCursor }
218229
219− match trySession model' with
220− | Some session -> model', [ CmdPoll(session, model'.PollCursor) ]
221− | None -> model', []
230+ let cmds =
231+ match trySession model' with
232+ | Some session -> [ CmdPoll(session, model'.PollCursor) ]
233+ | None -> []
234+
235+ model', cmds @ (if not incoming.IsEmpty then saveCmd model' else [])
222236
223237 | DoPoll ->
224238 match trySession model with
@@ -234,19 +248,23 @@ module App =
234248 { Pubkey = model.NewPubkey
235249 Nickname = model.NewNickname }
236250
237− { model with
238− Contacts = model.Contacts |> upsertContact contact
239− NewPubkey = ""
240− NewNickname = ""
241− Page = Conversations },
242− []
251+ let model' =
252+ { model with
253+ Contacts = model.Contacts |> upsertContact contact
254+ NewPubkey = ""
255+ NewNickname = ""
256+ Page = Conversations }
257+
258+ model', saveCmd model'
243259 else
244260 model, []
245261
246262 | DoDeleteContact pk ->
247− { model with
248− Contacts = model.Contacts |> List.filter (fun c -> c.Pubkey <> pk) },
249− []
263+ let model' =
264+ { model with
265+ Contacts = model.Contacts |> List.filter (fun c -> c.Pubkey <> pk) }
266+
267+ model', saveCmd model'
250268
251269 | CopyPubKey ->
252270 match model.Identity with
@@ -255,6 +273,56 @@ module App =
255273
256274 | DismissError -> { model with Error = None }, []
257275
276+ | StateLoaded(identityOpt, dataOpt) ->
277+ match identityOpt with
278+ | None -> model, []
279+ | Some(privKey, pubKeyHex) ->
280+ let identity = { PrivKey = privKey; PubKeyHex = pubKeyHex }
281+
282+ let model' =
283+ match dataOpt with
284+ | Some data ->
285+ let contacts =
286+ data.Contacts
287+ |> Array.toList
288+ |> List.map (fun c ->
289+ { Pubkey = c.Pubkey; Nickname = c.Nickname }: Contact)
290+
291+ let messages =
292+ data.Messages
293+ |> Seq.fold
294+ (fun acc (KeyValue(pk, dtos)) ->
295+ let msgs =
296+ dtos
297+ |> Array.toList
298+ |> List.map (fun m ->
299+ { Id = m.Id
300+ Body = m.Body
301+ Timestamp = DateTimeOffset.FromUnixTimeSeconds m.TimestampUnix
302+ IsOutgoing = m.IsOutgoing })
303+
304+ acc |> Map.add pk msgs)
305+ Map.empty
306+
307+ { model with
308+ Identity = Some identity
309+ Contacts = contacts
310+ Messages = messages
311+ ServerUrl =
312+ if data.ServerUrl <> "" then data.ServerUrl
313+ else model.ServerUrl
314+ PollCursor = data.PollCursor
315+ Page = Conversations
316+ Conn = Connecting }
317+
318+ | None ->
319+ { model with
320+ Identity = Some identity
321+ Page = Settings
322+ Conn = Connecting }
323+
324+ model', [ CmdConnect(model'.ServerUrl, identity) ]
325+
258326 // ── mapCmd ──
259327
260328 let private asyncCmd (op: Async<Msg>) : Cmd<Msg> =
@@ -369,6 +437,44 @@ module App =
369437 Microsoft.Maui.ApplicationModel.DataTransfer.Clipboard.Default.SetTextAsync(text)
370438 |> ignore)
371439
440+ | CmdLoadState ->
441+ asyncCmd (async {
442+ let! identity = Store.loadIdentity ()
443+ let data = Store.loadData ()
444+ return StateLoaded(identity, data)
445+ })
446+
447+ | CmdSaveIdentity(privKey, pubKeyHex) ->
448+ Cmd.ofEffect (fun _ -> Store.saveIdentity privKey pubKeyHex |> ignore)
449+
450+ | CmdSaveData(contacts, messages, serverUrl, cursor) ->
451+ Cmd.ofEffect (fun _ ->
452+ let contactDtos =
453+ contacts
454+ |> List.map (fun c ->
455+ { Store.ContactDto.Pubkey = c.Pubkey
456+ Store.ContactDto.Nickname = c.Nickname })
457+ |> Array.ofList
458+
459+ let messageDtos = Dictionary<string, Store.MessageDto array>()
460+
461+ messages
462+ |> Map.iter (fun pk msgs ->
463+ messageDtos[pk] <-
464+ msgs
465+ |> List.map (fun m ->
466+ { Store.MessageDto.Id = m.Id
467+ Store.MessageDto.Body = m.Body
468+ Store.MessageDto.TimestampUnix = m.Timestamp.ToUnixTimeSeconds()
469+ Store.MessageDto.IsOutgoing = m.IsOutgoing })
470+ |> Array.ofList)
471+
472+ Store.saveData
473+ { Contacts = contactDtos
474+ Messages = messageDtos
475+ ServerUrl = serverUrl
476+ PollCursor = cursor })
477+
372478 // ── View ──
373479
374480 let private viewSetup () =
client/Skrepka.fsproj+1
@@ -23,6 +23,7 @@
2323 <ItemGroup>
2424 <Compile Include="Crypto.fs" />
2525 <Compile Include="ApiClient.fs" />
26+ <Compile Include="Store.fs" />
2627 <Compile Include="App.fs" />
2728 <Compile Include="MauiProgram.fs" />
2829 </ItemGroup>
client/Store.fsnew file+93
@@ -0,0 +1,93 @@
1+namespace Skrepka
2+
3+open System
4+open System.Collections.Generic
5+open System.IO
6+open System.Text.Json
7+open Microsoft.Maui.Storage
8+
9+module Store =
10+
11+ [<CLIMutable>]
12+ type ContactDto =
13+ { Pubkey: string
14+ Nickname: string }
15+
16+ [<CLIMutable>]
17+ type MessageDto =
18+ { Id: string
19+ Body: string
20+ TimestampUnix: int64
21+ IsOutgoing: bool }
22+
23+ [<CLIMutable>]
24+ type DataDto =
25+ { Contacts: ContactDto array
26+ Messages: Dictionary<string, MessageDto array>
27+ ServerUrl: string
28+ PollCursor: uint64 }
29+
30+ let private jsonOpts =
31+ JsonSerializerOptions(WriteIndented = true, PropertyNameCaseInsensitive = true)
32+
33+ let private dataPath () =
34+ Path.Combine(FileSystem.AppDataDirectory, "data.json")
35+
36+ let loadIdentity () =
37+ async {
38+ let! privKeyB64 =
39+ SecureStorage.Default.GetAsync("identity_privkey")
40+ |> Async.AwaitTask
41+
42+ let! pubKeyHex =
43+ SecureStorage.Default.GetAsync("identity_pubkey")
44+ |> Async.AwaitTask
45+
46+ if isNull privKeyB64 || isNull pubKeyHex then
47+ return None
48+ else
49+ return Some(Convert.FromBase64String privKeyB64, pubKeyHex)
50+ }
51+
52+ let saveIdentity (privKey: byte[]) (pubKeyHex: string) =
53+ task {
54+ do! SecureStorage.Default.SetAsync("identity_privkey", Convert.ToBase64String privKey)
55+ do! SecureStorage.Default.SetAsync("identity_pubkey", pubKeyHex)
56+ }
57+
58+ let loadData () =
59+ let path = dataPath ()
60+
61+ if File.Exists path then
62+ try
63+ let json = File.ReadAllText path
64+ let dto = JsonSerializer.Deserialize<DataDto>(json, jsonOpts)
65+
66+ Some
67+ { Contacts =
68+ if Object.ReferenceEquals(dto.Contacts, null) then
69+ [||]
70+ else
71+ dto.Contacts
72+ Messages =
73+ if Object.ReferenceEquals(dto.Messages, null) then
74+ Dictionary()
75+ else
76+ dto.Messages
77+ ServerUrl =
78+ if Object.ReferenceEquals(dto.ServerUrl, null) then
79+ ""
80+ else
81+ dto.ServerUrl
82+ PollCursor = dto.PollCursor }
83+ with _ ->
84+ None
85+ else
86+ None
87+
88+ let saveData (dto: DataDto) =
89+ try
90+ let json = JsonSerializer.Serialize(dto, jsonOpts)
91+ File.WriteAllText(dataPath (), json)
92+ with _ ->
93+ ()
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+952040910192b24cfad159457c94cebd8040466a")>]
13+[<assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0+064c373a75a8e5a105124031d8e31ae203b8d89e")>]
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−be41ec7873ea59244c1fc90c226419f96480720bb1f4b6c0a8bec31bba3b7b3d
1+f41c0d91e35e0ceb4a21baecd346576ff07aafed94a6c649ee68a7dc99ffef0e
client/obj/Debug/net10.0-ios/iossimulator-arm64/Skrepka.fsproj.CoreCompileInputs.cache+1−1
@@ -1 +1 @@
1−c22c2ff1c980717cfdafcd9ffa558434c122b39b14f9a7fbb50e077ac6891188
1+2216f8a345100d0cead52a75571b23423658891765293a7281aed4c708bdb260
client/obj/Debug/net10.0-ios/iossimulator-arm64/Skrepka.fsproj.FileListAbsolute.txt+22
@@ -333,3 +333,25 @@
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
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/952040910192b24cfad159457c94cebd8040466a/*"}}
1+{"documents":{"/Users/iko/Developer/skrepka/*":"https://raw.githubusercontent.com/ilyakooo0/skrepka/064c373a75a8e5a105124031d8e31ae203b8d89e/*"}}
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+952040910192b24cfad159457c94cebd8040466a")>]
13+[<assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0+064c373a75a8e5a105124031d8e31ae203b8d89e")>]
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−5791a1e99cb6c99782086d8078b57b4c711d3c1cac05e3842ccb2c434657a739
1+6c134c0e309419636d3e51226a485ae9feaa2ce98c5cbcd888b8c9cc4e35dc78
client/obj/Debug/net10.0-maccatalyst/maccatalyst-arm64/Skrepka.fsproj.CoreCompileInputs.cache+1−1
@@ -1 +1 @@
1−1180f8d2f486b20840f9b5a18a5a8f38858cdd15ce8a43dc95101f27d253caa7
1+6b4f2ff0c8a36db93ba0242efaacfe4c365d973eced5156b3c65b39c8c644b10
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/952040910192b24cfad159457c94cebd8040466a/*"}}
1+{"documents":{"/Users/iko/Developer/skrepka/*":"https://raw.githubusercontent.com/ilyakooo0/skrepka/064c373a75a8e5a105124031d8e31ae203b8d89e/*"}}
server.knot+27−28
@@ -58,23 +58,30 @@ type OptionalServerName = Text where \s -> s == "" || (not (tooLong s maxServerN
5858
5959type Timestamp = Int<Ms>
6060
61+type Challenge = {pubkey: PubkeyHex, challenge: Text, expiresAt: Timestamp}
62+type Session = {token: Text, pubkey: PubkeyHex, expiresAt: Timestamp}
63+type Message = {messageId: MessageIdHex, toKey: PubkeyHex, fromKey: PubkeyHex, encryptedBlob: BlobHex, timestamp: Timestamp, originServer: OptionalServerName, receivedAt: Timestamp}
64+type PresenceEntry = {pubkey: PubkeyHex, server: Text, expiresAt: Timestamp, timestamp: Timestamp}
65+type ForwardEntry = {messageId: MessageIdHex, toServer: ServerName, createdAt: Timestamp, toKey: PubkeyHex, fromKey: PubkeyHex, encryptedBlob: BlobHex, timestamp: Timestamp}
66+type AckedEntry = {messageId: MessageIdHex, toKey: PubkeyHex, ackedAt: Timestamp, originServer: ServerName}
67+
6168-- ============================================
6269-- Persisted State
6370-- ============================================
6471
65−*challenges : [{pubkey: PubkeyHex, challenge: Text, expiresAt: Timestamp}]
72+*challenges : [Challenge]
6673
67−*sessions : [{token: Text, pubkey: PubkeyHex, expiresAt: Timestamp}]
74+*sessions : [Session]
6875
69−*messages : [{messageId: MessageIdHex, toKey: PubkeyHex, fromKey: PubkeyHex, encryptedBlob: BlobHex, timestamp: Timestamp, originServer: OptionalServerName, receivedAt: Timestamp}]
76+*messages : [Message]
7077
71−*presence : [{pubkey: PubkeyHex, server: Text, expiresAt: Timestamp, timestamp: Timestamp}]
78+*presence : [PresenceEntry]
7279
7380*seqCounter : Timestamp
7481
75−*forwards : [{messageId: MessageIdHex, toServer: ServerName, createdAt: Timestamp, toKey: PubkeyHex, fromKey: PubkeyHex, encryptedBlob: BlobHex, timestamp: Timestamp}]
82+*forwards : [ForwardEntry]
7683
77−*ackedIds : [{messageId: MessageIdHex, toKey: PubkeyHex, ackedAt: Timestamp, originServer: ServerName}]
84+*ackedIds : [AckedEntry]
7885
7986*lastPrunedAt : Timestamp
8087
@@ -156,16 +163,12 @@ intTo3Hex = \n ->
156163 let b2 = r0 - b1 * 256 in
157164 byteToHex b0 ++ byteToHex b1 ++ byteToHex b2
158165
159−genToken = do
160− a <- randomInt 16777216
161− b <- randomInt 16777216
162− c <- randomInt 16777216
163− d <- randomInt 16777216
164− e <- randomInt 16777216
165− f <- randomInt 16777216
166− g <- randomInt 16777216
167− h <- randomInt 16777216
168− yield (intTo3Hex a ++ intTo3Hex b ++ intTo3Hex c ++ intTo3Hex d ++ intTo3Hex e ++ intTo3Hex f ++ intTo3Hex g ++ intTo3Hex h)
166+genTokenN = \n -> if n <= 0 then yield "" else do
167+ chunk <- randomInt 16777216
168+ rest <- genTokenN (n - 1)
169+ yield (intTo3Hex chunk ++ rest)
170+
171+genToken = genTokenN 8
169172
170173tooLong = \s maxLen -> not (drop maxLen s == "")
171174
@@ -572,7 +575,7 @@ maybePrune = \t -> do
572575 set *lastPrunedAt = t
573576 pruneExpired t
574577
575−sendBatchedGossipToServer : Text -> [{eventType: GossipEventType, pubkey: Text, server: Text, timestamp: Timestamp, ttl: Int<S>}] -> IO {network} {}
578+sendBatchedGossipToServer : Text -> [GossipEvent] -> IO {network} {}
576579sendBatchedGossipToServer = \server allEvents ->
577580 if count allEvents == 0
578581 then yield {}
@@ -587,7 +590,7 @@ sendBatchedGossipToServer = \server allEvents ->
587590 Err _ -> yield {}
588591 sendBatchedGossipToServer server rest
589592
590−sendBatchedGossip : [Text] -> [{eventType: GossipEventType, pubkey: Text, server: Text, timestamp: Timestamp, ttl: Int<S>}] -> IO {network} {}
593+sendBatchedGossip : [Text] -> [GossipEvent] -> IO {network} {}
591594sendBatchedGossip = \peerServers allEvents ->
592595 if count allEvents == 0
593596 then yield {}
@@ -780,12 +783,10 @@ serve = \req -> case req of
780783 Nothing {} -> False {}
781784 _ -> verify (bytesFromHex pubkey) (textToBytes challenge) (bytesFromHex signature)
782785 set *challenges = if sigValid
783− then case matched of
784− Just {value: _} -> do
785− c <- challenges
786− where not (c.pubkey == pubkey && c.challenge == challenge)
787− yield c
788− Nothing {} -> challenges
786+ then do
787+ c <- challenges
788+ where not (c.pubkey == pubkey && c.challenge == challenge)
789+ yield c
789790 else challenges
790791 let sessionsCapped = capPerKey sessions (\s -> s.pubkey == pubkey) maxSessionsPerKey (\s -> s.expiresAt) t
791792 let activeSessionsCapped = do s <- sessionsCapped; where s.expiresAt > t; yield s
@@ -917,9 +918,7 @@ serve = \req -> case req of
917918 where m.messageId == mid
918919 where m.toKey == s.pubkey
919920 yield m
920− set *messages = case session of
921− Nothing {} -> messages
922− Just {value: _} -> diff messages ackedMessages
921+ set *messages = diff messages ackedMessages
923922 let ackedFederated = do
924923 m <- ackedMessages
925924 where isFederatedOrigin m.originServer
@@ -1153,7 +1152,7 @@ serve = \req -> case req of
11531152 else messages
11541153 yield {peerServers: pr.peerServers, expiredLocal: pr.expiredLocal, inserted: shouldInsert, alreadyExists: exists}
11551154 forkBroadcastOffline t result
1156− yield (respond {ok: result.inserted || (authorized && validOrigin && result.alreadyExists)})
1155+ yield (respond {ok: result.inserted || result.alreadyExists})
11571156
11581157 FederationAck {messageIds, originServer, fromServer, authorization, respond} -> do
11591158 let fromServer = normalizeServerName fromServer