on master help sign in
Simplify and improve type safety: thread Profile type through envelope/events, extract applyEvent helper, optimize with Set and Map.change
commitc2421eb18cd299aa1aa49d08e6aac0aff8c6c2b0 authoriko <mail@iko.soy> dateSat 11 Apr 2026, 14:26 (6 months ago) parent7b4b58d
28 files changed+92−129
client/App.fs+27−33 client/Store.fs+8−6 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+4−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+2−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+45−55
client/App.fs+27−33
@@ -40,7 +40,7 @@ module App =
4040 type DecryptedEvent =
4141 | IncomingChat of senderPubkey: string * message: ChatMessage
4242 | IncomingDeliveryAck of senderPubkey: string * ackIds: string list
43− | IncomingProfile of senderPubkey: string * displayName: string * bio: string * photo: string
43+ | IncomingProfile of senderPubkey: string * profile: Profile
4444
4545 type PollReceived = { Events: int; Messages: int; Errors: int; FirstError: string option }
4646
@@ -149,16 +149,28 @@ module App =
149149 else Map.add pk (existing @ [ msg ]) messages
150150
151151 let private markDelivered (pk: string) (ackIds: string list) (messages: Map<string, ChatMessage list>) =
152− match Map.tryFind pk messages with
153− | Some msgs ->
154− msgs
155− |> List.map (fun m -> if m.IsOutgoing && List.contains m.Id ackIds then { m with Status = DeliveryStatus.Delivered } else m)
156− |> fun updated -> Map.add pk updated messages
157− | None -> messages
152+ let ackSet = Set.ofList ackIds
153+ messages |> Map.change pk (Option.map (List.map (fun m ->
154+ if m.IsOutgoing && Set.contains m.Id ackSet then { m with Status = DeliveryStatus.Delivered } else m)))
158155
159156 let private newContact pubkey nickname : Contact =
160157 { Pubkey = pubkey; Nickname = nickname; DisplayName = ""; Bio = ""; PhotoBase64 = "" }
161158
159+ let private withProfile (p: Profile) (c: Contact) =
160+ { c with DisplayName = p.DisplayName; Bio = p.Bio; PhotoBase64 = p.PhotoBase64 }
161+
162+ let private applyEvent (m: Model) = function
163+ | IncomingChat(senderPubkey, message) ->
164+ { m with
165+ Contacts =
166+ if Map.containsKey senderPubkey m.Contacts then m.Contacts
167+ else Map.add senderPubkey (newContact senderPubkey (truncKey senderPubkey)) m.Contacts
168+ Messages = m.Messages |> appendMessage senderPubkey message }
169+ | IncomingDeliveryAck(senderPubkey, ackIds) ->
170+ { m with Messages = m.Messages |> markDelivered senderPubkey ackIds }
171+ | IncomingProfile(senderPubkey, profile) ->
172+ { m with Contacts = m.Contacts |> Map.change senderPubkey (Option.map (withProfile profile)) }
173+
162174 let private trySession model =
163175 match model.Auth with
164176 | Identified(id, Online token) -> Some { Url = model.ServerUrl; Token = token; Identity = id }
@@ -265,25 +277,7 @@ module App =
265277 | SendOk -> model, []
266278
267279 | PollResult(events, status, newCursor) ->
268− let model' =
269− events
270− |> List.fold
271− (fun (m: Model) evt ->
272− match evt with
273− | IncomingChat(senderPubkey, message) ->
274− { m with
275− Contacts =
276− if Map.containsKey senderPubkey m.Contacts then m.Contacts
277− else Map.add senderPubkey (newContact senderPubkey (truncKey senderPubkey)) m.Contacts
278− Messages = m.Messages |> appendMessage senderPubkey message }
279− | IncomingDeliveryAck(senderPubkey, ackIds) ->
280− { m with Messages = m.Messages |> markDelivered senderPubkey ackIds }
281− | IncomingProfile(senderPubkey, displayName, bio, photo) ->
282− { m with
283− Contacts =
284− m.Contacts |> Map.change senderPubkey (Option.map (fun c ->
285− { c with DisplayName = displayName; Bio = bio; PhotoBase64 = photo })) })
286− { model with PollStatus = status; PollCursor = newCursor }
280+ let model' = events |> List.fold applyEvent { model with PollStatus = status; PollCursor = newCursor }
287281
288282 model',
289283 [ match trySession model' with
@@ -374,15 +368,15 @@ module App =
374368 type private Envelope =
375369 | TextMessage of id: string * body: string
376370 | DeliveryAck of id: string * ackIds: string list
377− | ProfileMessage of id: string * displayName: string * bio: string * photo: string
371+ | ProfileMessage of id: string * profile: Profile
378372
379373 let private serializeEnvelope = function
380374 | TextMessage(id, body) ->
381375 JsonSerializer.Serialize({| ``type`` = "text"; id = id; body = body |})
382376 | DeliveryAck(id, ackIds) ->
383377 JsonSerializer.Serialize({| ``type`` = "delivery.ack"; id = id; ack_ids = ackIds |})
384− | ProfileMessage(id, displayName, bio, photo) ->
385− JsonSerializer.Serialize({| ``type`` = "profile"; id = id; timestamp = DateTimeOffset.UtcNow.ToUnixTimeSeconds(); display_name = displayName; bio = bio; photo = photo |})
378+ | ProfileMessage(id, profile) ->
379+ JsonSerializer.Serialize({| ``type`` = "profile"; id = id; timestamp = DateTimeOffset.UtcNow.ToUnixTimeSeconds(); display_name = profile.DisplayName; bio = profile.Bio; photo = profile.PhotoBase64 |})
386380
387381 let private sendEnvelope (session: Session) (recipientHex: string) (envelope: Envelope) =
388382 async {
@@ -410,7 +404,7 @@ module App =
410404 let displayName = root.GetProperty("display_name").GetString()
411405 let bio = tryGetJsonString root "bio" |> Option.defaultValue ""
412406 let photo = tryGetJsonString root "photo" |> Option.defaultValue ""
413− Some(ProfileMessage(root.GetProperty("id").GetString(), displayName, bio, photo))
407+ Some(ProfileMessage(root.GetProperty("id").GetString(), { DisplayName = displayName; Bio = bio; PhotoBase64 = photo }))
414408 | _ -> None
415409
416410 let private decryptEvent (privKey: byte[]) (payload: EventPayload) =
@@ -428,8 +422,8 @@ module App =
428422 Status = DeliveryStatus.Delivered }))
429423 | Some(DeliveryAck(_, ackIds)) ->
430424 Ok(IncomingDeliveryAck(senderHex, ackIds))
431− | Some(ProfileMessage(_, displayName, bio, photo)) ->
432− Ok(IncomingProfile(senderHex, displayName, bio, photo))
425+ | Some(ProfileMessage(_, profile)) ->
426+ Ok(IncomingProfile(senderHex, profile))
433427 | None -> Error "unknown envelope type"
434428 | None -> Error "decrypt failed"
435429 with ex ->
@@ -548,7 +542,7 @@ module App =
548542 Cmd.ofEffect (fun _ ->
549543 async {
550544 for recipientHex in recipientHexes do
551− try do! sendEnvelope session recipientHex (ProfileMessage(Guid.NewGuid().ToString(), profile.DisplayName, profile.Bio, profile.PhotoBase64)) |> Async.Ignore
545+ try do! sendEnvelope session recipientHex (ProfileMessage(Guid.NewGuid().ToString(), profile)) |> Async.Ignore
552546 with _ -> ()
553547 } |> Async.Start)
554548
client/Store.fs+8−6
@@ -58,6 +58,8 @@ module Store =
5858 Bio: string
5959 PhotoBase64: string }
6060
61+ let private orEmpty s = s |> Option.ofObj |> Option.defaultValue ""
62+
6163 let private toChatMessage (d: MessageDoc) : ChatMessage =
6264 { Id = d.Id; Body = d.Body; Timestamp = DateTimeOffset.FromUnixTimeSeconds d.TimestampUnix; IsOutgoing = d.IsOutgoing
6365 Status = match d.Status with 1 -> DeliveryStatus.Delivered | _ -> DeliveryStatus.Sent }
@@ -91,9 +93,9 @@ module Store =
9193 db.GetCollection<ProfileDoc>("profile").FindById(BsonValue "me")
9294 |> Option.ofObj
9395 |> Option.map (fun p ->
94− { DisplayName = p.DisplayName |> Option.ofObj |> Option.defaultValue ""
95− Bio = p.Bio |> Option.ofObj |> Option.defaultValue ""
96− PhotoBase64 = p.PhotoBase64 |> Option.ofObj |> Option.defaultValue "" })
96+ { DisplayName = orEmpty p.DisplayName
97+ Bio = orEmpty p.Bio
98+ PhotoBase64 = orEmpty p.PhotoBase64 })
9799 with _ -> None
98100
99101 let saveProfile (profile: Profile) =
@@ -119,9 +121,9 @@ module Store =
119121 db.GetCollection<Contact>("contacts").FindAll()
120122 |> Seq.map (fun c ->
121123 { c with
122− DisplayName = c.DisplayName |> Option.ofObj |> Option.defaultValue ""
123− Bio = c.Bio |> Option.ofObj |> Option.defaultValue ""
124− PhotoBase64 = c.PhotoBase64 |> Option.ofObj |> Option.defaultValue "" })
124+ DisplayName = orEmpty c.DisplayName
125+ Bio = orEmpty c.Bio
126+ PhotoBase64 = orEmpty c.PhotoBase64 })
125127 |> Seq.map (fun c -> c.Pubkey, c)
126128 |> Map.ofSeq
127129
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+4−4
@@ -166,11 +166,11 @@
166166 </data>
167167 <key>Skrepka.dll</key>
168168 <data>
169− XggCDQCHSf8dQC6LksWyNcwMo/0=
169+ zm59/PSrWKZiLU1jxPLjSpv2s0s=
170170 </data>
171171 <key>Skrepka.pdb</key>
172172 <data>
173− FRcfi2R+hf5HmuuoEB+4IrQgTRI=
173+ 9JiX44lGMitB4IFkSrPpNqppv9Y=
174174 </data>
175175 <key>Sodium.Core.dll</key>
176176 <data>
@@ -1393,14 +1393,14 @@
13931393 <dict>
13941394 <key>hash2</key>
13951395 <data>
1396− 4Obn19booe5w8nme/HekTocjkHnr5cKKZ7bpnmVCwZE=
1396+ yDZ9r78oz/SwMDjgSqMWBuIABfcf+pqLJmP3a/t/3qA=
13971397 </data>
13981398 </dict>
13991399 <key>Skrepka.pdb</key>
14001400 <dict>
14011401 <key>hash2</key>
14021402 <data>
1403− 0QGn820sZRBznKRlggmNFzaLbJHqeqweO4QTDBDd6YE=
1403+ Bne0biQx+iOwUvETBgS0ZpBPnqj9iMIcJsbMtjEGsB8=
14041404 </data>
14051405 </dict>
14061406 <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+2−2
@@ -251,14 +251,14 @@
251251 <dict>
252252 <key>hash2</key>
253253 <data>
254− P6Dn2mj+cOkqtrabVP1bG8x1eg30NhlSjO1UYUi+eKc=
254+ /muL9+VQW3p0CxjUNmMSZit0d+SjXG5P0rlAa3orDmo=
255255 </data>
256256 </dict>
257257 <key>MonoBundle/Skrepka.pdb</key>
258258 <dict>
259259 <key>hash2</key>
260260 <data>
261− 3XVw9RVc9dugW7Y8G78UD4MMYYewWf/pYJJpWHzCq/c=
261+ SGyPeKxWxldHdxyu67USmcmURx0ELpgiV7C4wpDifcU=
262262 </data>
263263 </dict>
264264 <key>MonoBundle/Sodium.Core.dll</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+50200f4e8cc7b94c2acf338b402efe56d4dce414")>]
13+[<assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0+7b4b58d8a12eba7ce8c31b169029ead687ed7426")>]
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−e311e4b6d1989fb2125900b4d17724c47240c7e78f825d88678e9bf38444e433
1+5f10364172f6e789ac911d4e11e6aa100fa841aceb13f823dff6f01aacb0ac84
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
@@ -334,26 +334,3 @@
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
336336/Users/iko/Developer/skrepka/client/bin/Debug/net10.0-ios/iossimulator-arm64/LiteDB.dll
337−/Users/iko/Developer/skrepka/client/obj/Debug/net10.0-ios/iossimulator-arm64/resourcestamps/Fabulous.stamp
338−/Users/iko/Developer/skrepka/client/obj/Debug/net10.0-ios/iossimulator-arm64/resourcestamps/Fabulous.MauiControls.stamp
339−/Users/iko/Developer/skrepka/client/obj/Debug/net10.0-ios/iossimulator-arm64/resourcestamps/FSharp.Core.stamp
340−/Users/iko/Developer/skrepka/client/obj/Debug/net10.0-ios/iossimulator-arm64/resourcestamps/LiteDB.stamp
341−/Users/iko/Developer/skrepka/client/obj/Debug/net10.0-ios/iossimulator-arm64/resourcestamps/Microsoft.Extensions.Configuration.Abstractions.stamp
342−/Users/iko/Developer/skrepka/client/obj/Debug/net10.0-ios/iossimulator-arm64/resourcestamps/Microsoft.Extensions.Configuration.stamp
343−/Users/iko/Developer/skrepka/client/obj/Debug/net10.0-ios/iossimulator-arm64/resourcestamps/Microsoft.Extensions.DependencyInjection.Abstractions.stamp
344−/Users/iko/Developer/skrepka/client/obj/Debug/net10.0-ios/iossimulator-arm64/resourcestamps/Microsoft.Extensions.DependencyInjection.stamp
345−/Users/iko/Developer/skrepka/client/obj/Debug/net10.0-ios/iossimulator-arm64/resourcestamps/Microsoft.Extensions.Diagnostics.Abstractions.stamp
346−/Users/iko/Developer/skrepka/client/obj/Debug/net10.0-ios/iossimulator-arm64/resourcestamps/Microsoft.Extensions.FileProviders.Abstractions.stamp
347−/Users/iko/Developer/skrepka/client/obj/Debug/net10.0-ios/iossimulator-arm64/resourcestamps/Microsoft.Extensions.Hosting.Abstractions.stamp
348−/Users/iko/Developer/skrepka/client/obj/Debug/net10.0-ios/iossimulator-arm64/resourcestamps/Microsoft.Extensions.Logging.Abstractions.stamp
349−/Users/iko/Developer/skrepka/client/obj/Debug/net10.0-ios/iossimulator-arm64/resourcestamps/Microsoft.Extensions.Logging.stamp
350−/Users/iko/Developer/skrepka/client/obj/Debug/net10.0-ios/iossimulator-arm64/resourcestamps/Microsoft.Extensions.Options.stamp
351−/Users/iko/Developer/skrepka/client/obj/Debug/net10.0-ios/iossimulator-arm64/resourcestamps/Microsoft.Extensions.Primitives.stamp
352−/Users/iko/Developer/skrepka/client/obj/Debug/net10.0-ios/iossimulator-arm64/resourcestamps/Microsoft.Maui.Controls.Compatibility.stamp
353−/Users/iko/Developer/skrepka/client/obj/Debug/net10.0-ios/iossimulator-arm64/resourcestamps/Microsoft.Maui.Controls.stamp
354−/Users/iko/Developer/skrepka/client/obj/Debug/net10.0-ios/iossimulator-arm64/resourcestamps/Microsoft.Maui.Controls.Xaml.stamp
355−/Users/iko/Developer/skrepka/client/obj/Debug/net10.0-ios/iossimulator-arm64/resourcestamps/Microsoft.Maui.stamp
356−/Users/iko/Developer/skrepka/client/obj/Debug/net10.0-ios/iossimulator-arm64/resourcestamps/Microsoft.Maui.Essentials.stamp
357−/Users/iko/Developer/skrepka/client/obj/Debug/net10.0-ios/iossimulator-arm64/resourcestamps/Microsoft.Maui.Graphics.stamp
358−/Users/iko/Developer/skrepka/client/obj/Debug/net10.0-ios/iossimulator-arm64/resourcestamps/Sodium.Core.stamp
359−/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.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/50200f4e8cc7b94c2acf338b402efe56d4dce414/*"}}
1+{"documents":{"/Users/iko/Developer/skrepka/*":"https://raw.githubusercontent.com/ilyakooo0/skrepka/7b4b58d8a12eba7ce8c31b169029ead687ed7426/*"}}
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+26d71bfd7b4764c7381338d2d47a0b86c18d9a9b")>]
13+[<assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0+7b4b58d8a12eba7ce8c31b169029ead687ed7426")>]
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−ce21a6c11004b9877da85245c10ed4a19bfe33da82c2f55d2943c6c7b8845665
1+ecb7ceeceb37b90c5ee8f96b06919afc09665607f70abf08cc16906c5f16c356
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/26d71bfd7b4764c7381338d2d47a0b86c18d9a9b/*"}}
1+{"documents":{"/Users/iko/Developer/skrepka/*":"https://raw.githubusercontent.com/ilyakooo0/skrepka/7b4b58d8a12eba7ce8c31b169029ead687ed7426/*"}}
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+45−55
@@ -43,6 +43,8 @@ offlineGossipMinTtl : Int<S>
4343offlineGossipMinTtl = 60 -- minimum TTL for offline gossip events
4444maxForwardRetriesPerServer = 100
4545federationEnabled = federationSecret != ""
46+federationAuth = "Bearer " ++ federationSecret
47+federationUrl = \server -> federationScheme ++ server
4648
4749-- ============================================
4850-- Types
@@ -84,10 +86,11 @@ impl ToJSON MessageStatus where
8486 MsUnauthorized {} -> "unauthorized"
8587 MsRejected {} -> "rejected")
8688
87−type PubkeyHex = Text where \s -> isValidHex s && not (tooLong s maxPubkeyLen) && s == normalizeHex s
88−type BlobHex = Text where \s -> isValidHex s && not (tooLong s maxBlobLen) && s == normalizeHex s
89−type SignatureHex = Text where \s -> isValidHex s && not (tooLong s maxSignatureLen) && s == normalizeHex s
90−isValidMessageId = \s -> isValidHex s && not (tooLong s maxMessageIdLen) && s == normalizeHex s
89+isValidBoundedHex = \maxLen s -> isValidHex s && not (tooLong s maxLen) && s == normalizeHex s
90+type PubkeyHex = Text where isValidBoundedHex maxPubkeyLen
91+type BlobHex = Text where isValidBoundedHex maxBlobLen
92+type SignatureHex = Text where isValidBoundedHex maxSignatureLen
93+isValidMessageId = isValidBoundedHex maxMessageIdLen
9194type MessageIdHex = Text where isValidMessageId
9295type OptionalMessageId = Text where \s -> s == "" || isValidMessageId s
9396isValidServerName = \s -> not (tooLong s maxServerNameLen) && s == normalizeServerName s && not (isBadServerName s)
@@ -235,44 +238,30 @@ isAllowedServerChar = \c -> contains c "0123456789abcdefghijklmnopqrstuvwxyz-.:"
235238containsBadUrlChar = \s -> any (chars s) (\c -> not (isAllowedServerChar c))
236239
237240isCgnatRange = \s ->
238− if take 4 s != "100." then False {}
239− else let d4 = take 1 (drop 4 s) in
241+ take 4 s == "100."
242+ && let d4 = take 1 (drop 4 s) in
240243 let d5 = take 1 (drop 5 s) in
241244 let d6 = take 1 (drop 6 s) in
242245 let d7 = take 1 (drop 7 s) in
243− if contains d4 "789" && isDigitChar d5 && d6 == "."
244− && isAllDigitsOrDotsUntilColonOrEnd (drop 7 s)
245− then True {}
246− else if d4 == "6" && contains d5 "456789" && d6 == "."
247− && isAllDigitsOrDotsUntilColonOrEnd (drop 7 s)
248− then True {}
249− else if d4 == "1" && contains d5 "01" && isDigitChar d6 && d7 == "."
250− && isAllDigitsOrDotsUntilColonOrEnd (drop 8 s)
251− then True {}
252− else if d4 == "1" && d5 == "2" && contains d6 "01234567" && d7 == "."
253− && isAllDigitsOrDotsUntilColonOrEnd (drop 8 s)
254− then True {}
255− else False {}
246+ let twoDigitOctet = d6 == "." && isAllDigitsOrDotsUntilColonOrEnd (drop 7 s) in
247+ let threeDigitOctet = d7 == "." && isAllDigitsOrDotsUntilColonOrEnd (drop 8 s) in
248+ ((contains d4 "789" && isDigitChar d5 && twoDigitOctet)
249+ || (d4 == "6" && contains d5 "456789" && twoDigitOctet)
250+ || (d4 == "1" && contains d5 "01" && isDigitChar d6 && threeDigitOctet)
251+ || (d4 == "1" && d5 == "2" && contains d6 "01234567" && threeDigitOctet))
256252
257253containsMultipleColons = \s -> count (do c <- chars s; where c == ":"; yield c) > 1
258254
259−hasOctalOctet = \s ->
260− if take 1 s == "0" && isDigitChar (take 1 (drop 1 s)) && isAllDigitsOrDotsUntilColonOrEnd (drop 2 s) then True {}
261− else hasOctalOctetAfterDot s
262−
263−hasOctalOctetAfterDot = \s ->
255+hasPatternAfterDot = \pred s ->
264256 if s == "" then False {}
265− else if take 1 s == "." && take 1 (drop 1 s) == "0" && isDigitChar (take 1 (drop 2 s)) && isAllDigitsOrDotsUntilColonOrEnd (drop 3 s) then True {}
266− else hasOctalOctetAfterDot (drop 1 s)
257+ else if take 1 s == "." && pred (drop 1 s) then True {}
258+ else hasPatternAfterDot pred (drop 1 s)
267259
268−hasHexPrefix = \s ->
269− if take 2 s == "0x" || take 2 s == "0X" then True {}
270− else hasHexPrefixAfterDot s
260+isOctalStart = \s -> take 1 s == "0" && isDigitChar (take 1 (drop 1 s)) && isAllDigitsOrDotsUntilColonOrEnd (drop 2 s)
261+hasOctalOctet = \s -> isOctalStart s || hasPatternAfterDot isOctalStart s
271262
272−hasHexPrefixAfterDot = \s ->
273− if s == "" then False {}
274− else if take 1 s == "." && (take 2 (drop 1 s) == "0x" || take 2 (drop 1 s) == "0X") then True {}
275− else hasHexPrefixAfterDot (drop 1 s)
263+isHexStart = \s -> take 2 s == "0x" || take 2 s == "0X"
264+hasHexPrefix = \s -> isHexStart s || hasPatternAfterDot isHexStart s
276265
277266isAllDigitsOrDotsUntilColonOrEnd = \s ->
278267 if s == "" then True {}
@@ -324,18 +313,18 @@ matchesHostname = \sn name ->
324313 let nameLen = count (chars name) in
325314 take nameLen sn == name && (drop nameLen sn == "" || take 1 (drop nameLen sn) == ":")
326315
316+matchesHostnameOrSubdomain = \sn name ->
317+ matchesHostname sn name || containsSuffix ("." ++ name) sn
318+
327319isBadServerName = \sn ->
328320 sn == ""
329321 || take 1 sn == "."
330322 || take 1 sn == ":"
331323 || containsBadUrlChar sn
332− || matchesHostname sn "localhost"
324+ || matchesHostnameOrSubdomain sn "localhost"
333325 || matchesHostname sn "localhost.localdomain"
334− || containsSuffix ".localhost" sn
335− || matchesHostname sn "local"
336− || containsSuffix ".local" sn
337− || matchesHostname sn "internal"
338− || containsSuffix ".internal" sn
326+ || matchesHostnameOrSubdomain sn "local"
327+ || matchesHostnameOrSubdomain sn "internal"
339328 || matchesHostname sn "metadata"
340329 || take 4 sn == "127." || take 3 sn == "10."
341330 || take 8 sn == "192.168." || take 8 sn == "169.254."
@@ -594,8 +583,8 @@ forEachBatch = \items batchSize process ->
594583sendBatchedGossipToServer : ServerName -> [GossipEvent] -> Int -> IO {network} {}
595584sendBatchedGossipToServer = \server allEvents hops ->
596585 forEachBatch allEvents maxGossipEvents (\batch -> do
597− _ <- fetch (federationScheme ++ server) (RecvGossip {
598− authorization: "Bearer " ++ federationSecret,
586+ _ <- fetch (federationUrl server) (RecvGossip {
587+ authorization: federationAuth,
599588 events: batch, hopsRemaining: hops, fromServer: serverHostN})
600589 yield {})
601590
@@ -637,10 +626,10 @@ retryForwardsForServer = \entries remaining ->
637626 Nothing {} -> yield {}
638627 Just {value: f} -> do
639628 let rest = do x <- entries; where x.messageId != f.messageId; yield x
640− result <- fetch (federationScheme ++ f.toServer) (ForwardMessage {
629+ result <- fetch (federationUrl f.toServer) (ForwardMessage {
641630 toKey: f.toKey, fromKey: f.fromKey, encryptedBlob: f.encryptedBlob,
642631 timestamp: f.timestamp, originServer: serverHostN, messageId: f.messageId,
643− authorization: "Bearer " ++ federationSecret})
632+ authorization: federationAuth})
644633 whenOk result (removeForward f.messageId f.toServer)
645634 retryForwardsForServer rest (remaining - 1)
646635
@@ -654,8 +643,8 @@ retryPendingForwards = \pending -> do
654643sendBatchedAcksToServer : [{messageId: MessageIdHex, toKey: PubkeyHex, originServer: ServerName, | a}] -> ServerName -> IO {network} {}
655644sendBatchedAcksToServer = \serverAcks server ->
656645 forEachBatch serverAcks maxMessageIds (\batch -> do
657− result <- fetch (federationScheme ++ server) (FederationAck {
658− authorization: "Bearer " ++ federationSecret,
646+ result <- fetch (federationUrl server) (FederationAck {
647+ authorization: federationAuth,
659648 messageIds: (do a <- batch; yield a.messageId),
660649 originServer: server, fromServer: serverHostN})
661650 whenOk result (atomic do
@@ -679,8 +668,8 @@ retryEvictedAcks = \evictedEntries pendingAcks ->
679668 in do
680669 let evictedOriginServers = collectPeerServers evictedToRetry (\a -> a.originServer)
681670 sendToEach evictedOriginServers (\server -> do
682− fetchResult <- fetch (federationScheme ++ server) (FederationAck {
683− authorization: "Bearer " ++ federationSecret,
671+ fetchResult <- fetch (federationUrl server) (FederationAck {
672+ authorization: federationAuth,
684673 messageIds: (do a <- evictedToRetry; where a.originServer == server; yield a.messageId),
685674 originServer: server, fromServer: serverHostN})
686675 case fetchResult of
@@ -737,6 +726,7 @@ pollOnce = \token cursor -> do
737726 let lastReceivedAt = fold (\acc m -> max acc m.receivedAt) clampedCursor limitedMsgs
738727 yield PollDone {events: evts, cursor: lastReceivedAt}
739728
729+pollLoop : Text -> Int -> Int<Ms> -> IO {clock} PollResult
740730pollLoop = \token cursor deadline -> do
741731 result <- pollOnce token cursor
742732 case result of
@@ -864,15 +854,16 @@ serve = \req -> case req of
864854 SendDone {status, remoteServers, fromKey} ->
865855 when (count remoteServers > 0 && federationEnabled) (do
866856 sendToEach remoteServers (\server -> do
867− fwdResult <- fetch (federationScheme ++ server) (ForwardMessage {
857+ fwdResult <- fetch (federationUrl server) (ForwardMessage {
868858 toKey: to, fromKey: fromKey, encryptedBlob: encryptedBlob,
869859 timestamp: clampedTs, originServer: serverHostN, messageId: msgId,
870− authorization: "Bearer " ++ federationSecret})
860+ authorization: federationAuth})
871861 whenOk fwdResult (removeForward msgId server))
872862 case status of
873863 MsQueued {} -> println ("sendMessage: forwards at capacity (" ++ show maxForwardsEntries ++ "), skipping forward tracking")
874864 _ -> yield {})
875− _ -> yield {}
865+ SendUnauthorized {} -> yield {}
866+ SendRejected {} -> yield {}
876867 broadcastOfflineGossip t result.pr.peerServers result.pr.expiredLocal)
877868 yield (case result.send of
878869 SendUnauthorized {} -> respond {status: MsUnauthorized {}, messageId: ""}
@@ -912,9 +903,9 @@ serve = \req -> case req of
912903 when federationEnabled (do
913904 sendAcksAndClean pendingAcks
914905 retryEvictedAcks evictedEntries pendingAcks)
915− _ -> yield {}
906+ AckUnauthorized {} -> yield {}
916907 broadcastOfflineGossip t result.pr.peerServers result.pr.expiredLocal)
917− yield (respond {ok: case result.ack of AckDone _ -> True {}; _ -> False {}})
908+ yield (respond {ok: case result.ack of AckDone _ -> True {}; AckUnauthorized {} -> False {}})
918909
919910 LookupPresence {pubkey, authorization, respond} -> do
920911 t <- now
@@ -990,8 +981,7 @@ serve = \req -> case req of
990981 ForwardMessage {toKey, fromKey, encryptedBlob, timestamp, originServer, messageId, authorization, respond} -> do
991982 t <- now
992983 let authorized = verifyFederationAuth authorization
993− let validOrigin = originServer != serverHostN
994− if not authorized || not validOrigin
984+ if not authorized || originServer == serverHostN
995985 then rejectFederation t respond
996986 else do
997987 let clampedTs = clampTimestamp timestamp t
@@ -1013,7 +1003,7 @@ serve = \req -> case req of
10131003 encryptedBlob: encryptedBlob, timestamp: clampedTs, origin: FederatedOrigin {server: originServer}, receivedAt: seqTs}]
10141004 yield {pr: pr, forward: ForwardStored {}}
10151005 forkBroadcastOffline t result.pr
1016− yield (respond {ok: case result.forward of ForwardFull {} -> False {}; _ -> True {}})
1006+ yield (respond {ok: case result.forward of ForwardDuplicate {} -> True {}; ForwardStored {} -> True {}; ForwardFull {} -> False {}})
10171007
10181008 FederationAck {messageIds, originServer, fromServer, authorization, respond} -> do
10191009 t <- now