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
c2421ebiko
client/App.fs+27−33
@@ -40,7 +40,7 @@ module App =
40 type DecryptedEvent =
40 type DecryptedEvent =
41 | IncomingChat of senderPubkey: string * message: ChatMessage
41 | IncomingChat of senderPubkey: string * message: ChatMessage
42 | IncomingDeliveryAck of senderPubkey: string * ackIds: string list
42 | 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
44
44
45 type PollReceived = { Events: int; Messages: int; Errors: int; FirstError: string option }
45 type PollReceived = { Events: int; Messages: int; Errors: int; FirstError: string option }
46
46
@@ -149,16 +149,28 @@ module App =
149 else Map.add pk (existing @ [ msg ]) messages
149 else Map.add pk (existing @ [ msg ]) messages
150
150
151 let private markDelivered (pk: string) (ackIds: string list) (messages: Map<string, ChatMessage list>) =
151 let private markDelivered (pk: string) (ackIds: string list) (messages: Map<string, ChatMessage list>) =
152− match Map.tryFind pk messages with
152+ let ackSet = Set.ofList ackIds
153− | Some msgs ->
153+ messages |> Map.change pk (Option.map (List.map (fun m ->
154− msgs
155− |> List.map (fun m -> if m.IsOutgoing && List.contains m.Id ackIds then { m with Status = DeliveryStatus.Delivered } else m)
154+ if m.IsOutgoing && Set.contains m.Id ackSet then { m with Status = DeliveryStatus.Delivered } else m)))
156− |> fun updated -> Map.add pk updated messages
157− | None -> messages
158
155
159 let private newContact pubkey nickname : Contact =
156 let private newContact pubkey nickname : Contact =
160 { Pubkey = pubkey; Nickname = nickname; DisplayName = ""; Bio = ""; PhotoBase64 = "" }
157 { Pubkey = pubkey; Nickname = nickname; DisplayName = ""; Bio = ""; PhotoBase64 = "" }
161
158
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+
162 let private trySession model =
174 let private trySession model =
163 match model.Auth with
175 match model.Auth with
164 | Identified(id, Online token) -> Some { Url = model.ServerUrl; Token = token; Identity = id }
176 | Identified(id, Online token) -> Some { Url = model.ServerUrl; Token = token; Identity = id }
@@ -265,25 +277,7 @@ module App =
265 | SendOk -> model, []
277 | SendOk -> model, []
266
278
267 | PollResult(events, status, newCursor) ->
279 | 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 }
287
281
288 model',
282 model',
289 [ match trySession model' with
283 [ match trySession model' with
@@ -374,15 +368,15 @@ module App =
374 type private Envelope =
368 type private Envelope =
375 | TextMessage of id: string * body: string
369 | TextMessage of id: string * body: string
376 | DeliveryAck of id: string * ackIds: string list
370 | 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
378
372
379 let private serializeEnvelope = function
373 let private serializeEnvelope = function
380 | TextMessage(id, body) ->
374 | TextMessage(id, body) ->
381 JsonSerializer.Serialize({| ``type`` = "text"; id = id; body = body |})
375 JsonSerializer.Serialize({| ``type`` = "text"; id = id; body = body |})
382 | DeliveryAck(id, ackIds) ->
376 | DeliveryAck(id, ackIds) ->
383 JsonSerializer.Serialize({| ``type`` = "delivery.ack"; id = id; ack_ids = ackIds |})
377 JsonSerializer.Serialize({| ``type`` = "delivery.ack"; id = id; ack_ids = ackIds |})
384− | ProfileMessage(id, displayName, bio, photo) ->
378+ | ProfileMessage(id, profile) ->
385− JsonSerializer.Serialize({| ``type`` = "profile"; id = id; timestamp = DateTimeOffset.UtcNow.ToUnixTimeSeconds(); display_name = displayName; bio = bio; photo = photo |})
379+ JsonSerializer.Serialize({| ``type`` = "profile"; id = id; timestamp = DateTimeOffset.UtcNow.ToUnixTimeSeconds(); display_name = profile.DisplayName; bio = profile.Bio; photo = profile.PhotoBase64 |})
386
380
387 let private sendEnvelope (session: Session) (recipientHex: string) (envelope: Envelope) =
381 let private sendEnvelope (session: Session) (recipientHex: string) (envelope: Envelope) =
388 async {
382 async {
@@ -410,7 +404,7 @@ module App =
410 let displayName = root.GetProperty("display_name").GetString()
404 let displayName = root.GetProperty("display_name").GetString()
411 let bio = tryGetJsonString root "bio" |> Option.defaultValue ""
405 let bio = tryGetJsonString root "bio" |> Option.defaultValue ""
412 let photo = tryGetJsonString root "photo" |> Option.defaultValue ""
406 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 }))
414 | _ -> None
408 | _ -> None
415
409
416 let private decryptEvent (privKey: byte[]) (payload: EventPayload) =
410 let private decryptEvent (privKey: byte[]) (payload: EventPayload) =
@@ -428,8 +422,8 @@ module App =
428 Status = DeliveryStatus.Delivered }))
422 Status = DeliveryStatus.Delivered }))
429 | Some(DeliveryAck(_, ackIds)) ->
423 | Some(DeliveryAck(_, ackIds)) ->
430 Ok(IncomingDeliveryAck(senderHex, ackIds))
424 Ok(IncomingDeliveryAck(senderHex, ackIds))
431− | Some(ProfileMessage(_, displayName, bio, photo)) ->
425+ | Some(ProfileMessage(_, profile)) ->
432− Ok(IncomingProfile(senderHex, displayName, bio, photo))
426+ Ok(IncomingProfile(senderHex, profile))
433 | None -> Error "unknown envelope type"
427 | None -> Error "unknown envelope type"
434 | None -> Error "decrypt failed"
428 | None -> Error "decrypt failed"
435 with ex ->
429 with ex ->
@@ -548,7 +542,7 @@ module App =
548 Cmd.ofEffect (fun _ ->
542 Cmd.ofEffect (fun _ ->
549 async {
543 async {
550 for recipientHex in recipientHexes do
544 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
552 with _ -> ()
546 with _ -> ()
553 } |> Async.Start)
547 } |> Async.Start)
554
548
client/Store.fs+8−6
@@ -58,6 +58,8 @@ module Store =
58 Bio: string
58 Bio: string
59 PhotoBase64: string }
59 PhotoBase64: string }
60
60
61+ let private orEmpty s = s |> Option.ofObj |> Option.defaultValue ""
62+
61 let private toChatMessage (d: MessageDoc) : ChatMessage =
63 let private toChatMessage (d: MessageDoc) : ChatMessage =
62 { Id = d.Id; Body = d.Body; Timestamp = DateTimeOffset.FromUnixTimeSeconds d.TimestampUnix; IsOutgoing = d.IsOutgoing
64 { Id = d.Id; Body = d.Body; Timestamp = DateTimeOffset.FromUnixTimeSeconds d.TimestampUnix; IsOutgoing = d.IsOutgoing
63 Status = match d.Status with 1 -> DeliveryStatus.Delivered | _ -> DeliveryStatus.Sent }
65 Status = match d.Status with 1 -> DeliveryStatus.Delivered | _ -> DeliveryStatus.Sent }
@@ -91,9 +93,9 @@ module Store =
91 db.GetCollection<ProfileDoc>("profile").FindById(BsonValue "me")
93 db.GetCollection<ProfileDoc>("profile").FindById(BsonValue "me")
92 |> Option.ofObj
94 |> Option.ofObj
93 |> Option.map (fun p ->
95 |> Option.map (fun p ->
94− { DisplayName = p.DisplayName |> Option.ofObj |> Option.defaultValue ""
96+ { DisplayName = orEmpty p.DisplayName
95− Bio = p.Bio |> Option.ofObj |> Option.defaultValue ""
97+ Bio = orEmpty p.Bio
96− PhotoBase64 = p.PhotoBase64 |> Option.ofObj |> Option.defaultValue "" })
98+ PhotoBase64 = orEmpty p.PhotoBase64 })
97 with _ -> None
99 with _ -> None
98
100
99 let saveProfile (profile: Profile) =
101 let saveProfile (profile: Profile) =
@@ -119,9 +121,9 @@ module Store =
119 db.GetCollection<Contact>("contacts").FindAll()
121 db.GetCollection<Contact>("contacts").FindAll()
120 |> Seq.map (fun c ->
122 |> Seq.map (fun c ->
121 { c with
123 { c with
122− DisplayName = c.DisplayName |> Option.ofObj |> Option.defaultValue ""
124+ DisplayName = orEmpty c.DisplayName
123− Bio = c.Bio |> Option.ofObj |> Option.defaultValue ""
125+ Bio = orEmpty c.Bio
124− PhotoBase64 = c.PhotoBase64 |> Option.ofObj |> Option.defaultValue "" })
126+ PhotoBase64 = orEmpty c.PhotoBase64 })
125 |> Seq.map (fun c -> c.Pubkey, c)
127 |> Seq.map (fun c -> c.Pubkey, c)
126 |> Map.ofSeq
128 |> Map.ofSeq
127
129
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 @@
166 </data>
166 </data>
167 <key>Skrepka.dll</key>
167 <key>Skrepka.dll</key>
168 <data>
168 <data>
169− XggCDQCHSf8dQC6LksWyNcwMo/0=
169+ zm59/PSrWKZiLU1jxPLjSpv2s0s=
170 </data>
170 </data>
171 <key>Skrepka.pdb</key>
171 <key>Skrepka.pdb</key>
172 <data>
172 <data>
173− FRcfi2R+hf5HmuuoEB+4IrQgTRI=
173+ 9JiX44lGMitB4IFkSrPpNqppv9Y=
174 </data>
174 </data>
175 <key>Sodium.Core.dll</key>
175 <key>Sodium.Core.dll</key>
176 <data>
176 <data>
@@ -1393,14 +1393,14 @@
1393 <dict>
1393 <dict>
1394 <key>hash2</key>
1394 <key>hash2</key>
1395 <data>
1395 <data>
1396− 4Obn19booe5w8nme/HekTocjkHnr5cKKZ7bpnmVCwZE=
1396+ yDZ9r78oz/SwMDjgSqMWBuIABfcf+pqLJmP3a/t/3qA=
1397 </data>
1397 </data>
1398 </dict>
1398 </dict>
1399 <key>Skrepka.pdb</key>
1399 <key>Skrepka.pdb</key>
1400 <dict>
1400 <dict>
1401 <key>hash2</key>
1401 <key>hash2</key>
1402 <data>
1402 <data>
1403− 0QGn820sZRBznKRlggmNFzaLbJHqeqweO4QTDBDd6YE=
1403+ Bne0biQx+iOwUvETBgS0ZpBPnqj9iMIcJsbMtjEGsB8=
1404 </data>
1404 </data>
1405 </dict>
1405 </dict>
1406 <key>Sodium.Core.dll</key>
1406 <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 @@
251 <dict>
251 <dict>
252 <key>hash2</key>
252 <key>hash2</key>
253 <data>
253 <data>
254− P6Dn2mj+cOkqtrabVP1bG8x1eg30NhlSjO1UYUi+eKc=
254+ /muL9+VQW3p0CxjUNmMSZit0d+SjXG5P0rlAa3orDmo=
255 </data>
255 </data>
256 </dict>
256 </dict>
257 <key>MonoBundle/Skrepka.pdb</key>
257 <key>MonoBundle/Skrepka.pdb</key>
258 <dict>
258 <dict>
259 <key>hash2</key>
259 <key>hash2</key>
260 <data>
260 <data>
261− 3XVw9RVc9dugW7Y8G78UD4MMYYewWf/pYJJpWHzCq/c=
261+ SGyPeKxWxldHdxyu67USmcmURx0ELpgiV7C4wpDifcU=
262 </data>
262 </data>
263 </dict>
263 </dict>
264 <key>MonoBundle/Sodium.Core.dll</key>
264 <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
10[<assembly: System.Reflection.AssemblyCompanyAttribute("Skrepka")>]
10[<assembly: System.Reflection.AssemblyCompanyAttribute("Skrepka")>]
11[<assembly: System.Reflection.AssemblyConfigurationAttribute("Debug")>]
11[<assembly: System.Reflection.AssemblyConfigurationAttribute("Debug")>]
12[<assembly: System.Reflection.AssemblyFileVersionAttribute("1.0.0.0")>]
12[<assembly: System.Reflection.AssemblyFileVersionAttribute("1.0.0.0")>]
13−[<assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0+50200f4e8cc7b94c2acf338b402efe56d4dce414")>]
13+[<assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0+7b4b58d8a12eba7ce8c31b169029ead687ed7426")>]
14[<assembly: System.Reflection.AssemblyProductAttribute("Skrepka")>]
14[<assembly: System.Reflection.AssemblyProductAttribute("Skrepka")>]
15[<assembly: System.Reflection.AssemblyTitleAttribute("Skrepka")>]
15[<assembly: System.Reflection.AssemblyTitleAttribute("Skrepka")>]
16[<assembly: System.Reflection.AssemblyVersionAttribute("1.0.0.0")>]
16[<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 @@
334/Users/iko/Developer/skrepka/client/obj/Debug/net10.0-ios/iossimulator-arm64/Skrepka.genruntimeconfig.cache
334/Users/iko/Developer/skrepka/client/obj/Debug/net10.0-ios/iossimulator-arm64/Skrepka.genruntimeconfig.cache
335/Users/iko/Developer/skrepka/client/obj/Debug/net10.0-ios/iossimulator-arm64/ref/Skrepka.dll
335/Users/iko/Developer/skrepka/client/obj/Debug/net10.0-ios/iossimulator-arm64/ref/Skrepka.dll
336/Users/iko/Developer/skrepka/client/bin/Debug/net10.0-ios/iossimulator-arm64/LiteDB.dll
336/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
10[<assembly: System.Reflection.AssemblyCompanyAttribute("Skrepka")>]
10[<assembly: System.Reflection.AssemblyCompanyAttribute("Skrepka")>]
11[<assembly: System.Reflection.AssemblyConfigurationAttribute("Debug")>]
11[<assembly: System.Reflection.AssemblyConfigurationAttribute("Debug")>]
12[<assembly: System.Reflection.AssemblyFileVersionAttribute("1.0.0.0")>]
12[<assembly: System.Reflection.AssemblyFileVersionAttribute("1.0.0.0")>]
13−[<assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0+26d71bfd7b4764c7381338d2d47a0b86c18d9a9b")>]
13+[<assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0+7b4b58d8a12eba7ce8c31b169029ead687ed7426")>]
14[<assembly: System.Reflection.AssemblyProductAttribute("Skrepka")>]
14[<assembly: System.Reflection.AssemblyProductAttribute("Skrepka")>]
15[<assembly: System.Reflection.AssemblyTitleAttribute("Skrepka")>]
15[<assembly: System.Reflection.AssemblyTitleAttribute("Skrepka")>]
16[<assembly: System.Reflection.AssemblyVersionAttribute("1.0.0.0")>]
16[<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>
43offlineGossipMinTtl = 60 -- minimum TTL for offline gossip events
43offlineGossipMinTtl = 60 -- minimum TTL for offline gossip events
44maxForwardRetriesPerServer = 100
44maxForwardRetriesPerServer = 100
45federationEnabled = federationSecret != ""
45federationEnabled = federationSecret != ""
46+federationAuth = "Bearer " ++ federationSecret
47+federationUrl = \server -> federationScheme ++ server
46
48
47-- ============================================
49-- ============================================
48-- Types
50-- Types
@@ -84,10 +86,11 @@ impl ToJSON MessageStatus where
84 MsUnauthorized {} -> "unauthorized"
86 MsUnauthorized {} -> "unauthorized"
85 MsRejected {} -> "rejected")
87 MsRejected {} -> "rejected")
86
88
89+isValidBoundedHex = \maxLen s -> isValidHex s && not (tooLong s maxLen) && s == normalizeHex s
87−type PubkeyHex = Text where \s -> isValidHex s && not (tooLong s maxPubkeyLen) && s == normalizeHex s
90+type PubkeyHex = Text where isValidBoundedHex maxPubkeyLen
88−type BlobHex = Text where \s -> isValidHex s && not (tooLong s maxBlobLen) && s == normalizeHex s
91+type BlobHex = Text where isValidBoundedHex maxBlobLen
89−type SignatureHex = Text where \s -> isValidHex s && not (tooLong s maxSignatureLen) && s == normalizeHex s
92+type SignatureHex = Text where isValidBoundedHex maxSignatureLen
90−isValidMessageId = \s -> isValidHex s && not (tooLong s maxMessageIdLen) && s == normalizeHex s
93+isValidMessageId = isValidBoundedHex maxMessageIdLen
91type MessageIdHex = Text where isValidMessageId
94type MessageIdHex = Text where isValidMessageId
92type OptionalMessageId = Text where \s -> s == "" || isValidMessageId s
95type OptionalMessageId = Text where \s -> s == "" || isValidMessageId s
93isValidServerName = \s -> not (tooLong s maxServerNameLen) && s == normalizeServerName s && not (isBadServerName s)
96isValidServerName = \s -> not (tooLong s maxServerNameLen) && s == normalizeServerName s && not (isBadServerName s)
@@ -235,44 +238,30 @@ isAllowedServerChar = \c -> contains c "0123456789abcdefghijklmnopqrstuvwxyz-.:"
235containsBadUrlChar = \s -> any (chars s) (\c -> not (isAllowedServerChar c))
238containsBadUrlChar = \s -> any (chars s) (\c -> not (isAllowedServerChar c))
236
239
237isCgnatRange = \s ->
240isCgnatRange = \s ->
238− if take 4 s != "100." then False {}
241+ take 4 s == "100."
239− else let d4 = take 1 (drop 4 s) in
242+ && let d4 = take 1 (drop 4 s) in
240 let d5 = take 1 (drop 5 s) in
243 let d5 = take 1 (drop 5 s) in
241 let d6 = take 1 (drop 6 s) in
244 let d6 = take 1 (drop 6 s) in
242 let d7 = take 1 (drop 7 s) in
245 let d7 = take 1 (drop 7 s) in
246+ let twoDigitOctet = d6 == "." && isAllDigitsOrDotsUntilColonOrEnd (drop 7 s) in
247+ let threeDigitOctet = d7 == "." && isAllDigitsOrDotsUntilColonOrEnd (drop 8 s) in
243− if contains d4 "789" && isDigitChar d5 && d6 == "."
248+ ((contains d4 "789" && isDigitChar d5 && twoDigitOctet)
244− && isAllDigitsOrDotsUntilColonOrEnd (drop 7 s)
245− then True {}
246− else if d4 == "6" && contains d5 "456789" && d6 == "."
249+ || (d4 == "6" && contains d5 "456789" && twoDigitOctet)
247− && isAllDigitsOrDotsUntilColonOrEnd (drop 7 s)
248− then True {}
249− else if d4 == "1" && contains d5 "01" && isDigitChar d6 && d7 == "."
250+ || (d4 == "1" && contains d5 "01" && isDigitChar d6 && threeDigitOctet)
250− && isAllDigitsOrDotsUntilColonOrEnd (drop 8 s)
251− then True {}
252− else if d4 == "1" && d5 == "2" && contains d6 "01234567" && d7 == "."
251+ || (d4 == "1" && d5 == "2" && contains d6 "01234567" && threeDigitOctet))
253− && isAllDigitsOrDotsUntilColonOrEnd (drop 8 s)
254− then True {}
255− else False {}
256
252
257containsMultipleColons = \s -> count (do c <- chars s; where c == ":"; yield c) > 1
253containsMultipleColons = \s -> count (do c <- chars s; where c == ":"; yield c) > 1
258
254
259−hasOctalOctet = \s ->
255+hasPatternAfterDot = \pred 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 ->
264 if s == "" then False {}
256 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 {}
257+ else if take 1 s == "." && pred (drop 1 s) then True {}
266− else hasOctalOctetAfterDot (drop 1 s)
258+ else hasPatternAfterDot pred (drop 1 s)
267
259
268−hasHexPrefix = \s ->
260+isOctalStart = \s -> take 1 s == "0" && isDigitChar (take 1 (drop 1 s)) && isAllDigitsOrDotsUntilColonOrEnd (drop 2 s)
269− if take 2 s == "0x" || take 2 s == "0X" then True {}
261+hasOctalOctet = \s -> isOctalStart s || hasPatternAfterDot isOctalStart s
270− else hasHexPrefixAfterDot s
271
262
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 {}
263+isHexStart = \s -> take 2 s == "0x" || take 2 s == "0X"
275− else hasHexPrefixAfterDot (drop 1 s)
264+hasHexPrefix = \s -> isHexStart s || hasPatternAfterDot isHexStart s
276
265
277isAllDigitsOrDotsUntilColonOrEnd = \s ->
266isAllDigitsOrDotsUntilColonOrEnd = \s ->
278 if s == "" then True {}
267 if s == "" then True {}
@@ -324,18 +313,18 @@ matchesHostname = \sn name ->
324 let nameLen = count (chars name) in
313 let nameLen = count (chars name) in
325 take nameLen sn == name && (drop nameLen sn == "" || take 1 (drop nameLen sn) == ":")
314 take nameLen sn == name && (drop nameLen sn == "" || take 1 (drop nameLen sn) == ":")
326
315
316+matchesHostnameOrSubdomain = \sn name ->
317+ matchesHostname sn name || containsSuffix ("." ++ name) sn
318+
327isBadServerName = \sn ->
319isBadServerName = \sn ->
328 sn == ""
320 sn == ""
329 || take 1 sn == "."
321 || take 1 sn == "."
330 || take 1 sn == ":"
322 || take 1 sn == ":"
331 || containsBadUrlChar sn
323 || containsBadUrlChar sn
332− || matchesHostname sn "localhost"
324+ || matchesHostnameOrSubdomain sn "localhost"
333 || matchesHostname sn "localhost.localdomain"
325 || matchesHostname sn "localhost.localdomain"
334− || containsSuffix ".localhost" sn
326+ || matchesHostnameOrSubdomain sn "local"
335− || matchesHostname sn "local"
327+ || matchesHostnameOrSubdomain sn "internal"
336− || containsSuffix ".local" sn
337− || matchesHostname sn "internal"
338− || containsSuffix ".internal" sn
339 || matchesHostname sn "metadata"
328 || matchesHostname sn "metadata"
340 || take 4 sn == "127." || take 3 sn == "10."
329 || take 4 sn == "127." || take 3 sn == "10."
341 || take 8 sn == "192.168." || take 8 sn == "169.254."
330 || take 8 sn == "192.168." || take 8 sn == "169.254."
@@ -594,8 +583,8 @@ forEachBatch = \items batchSize process ->
594sendBatchedGossipToServer : ServerName -> [GossipEvent] -> Int -> IO {network} {}
583sendBatchedGossipToServer : ServerName -> [GossipEvent] -> Int -> IO {network} {}
595sendBatchedGossipToServer = \server allEvents hops ->
584sendBatchedGossipToServer = \server allEvents hops ->
596 forEachBatch allEvents maxGossipEvents (\batch -> do
585 forEachBatch allEvents maxGossipEvents (\batch -> do
597− _ <- fetch (federationScheme ++ server) (RecvGossip {
586+ _ <- fetch (federationUrl server) (RecvGossip {
598− authorization: "Bearer " ++ federationSecret,
587+ authorization: federationAuth,
599 events: batch, hopsRemaining: hops, fromServer: serverHostN})
588 events: batch, hopsRemaining: hops, fromServer: serverHostN})
600 yield {})
589 yield {})
601
590
@@ -637,10 +626,10 @@ retryForwardsForServer = \entries remaining ->
637 Nothing {} -> yield {}
626 Nothing {} -> yield {}
638 Just {value: f} -> do
627 Just {value: f} -> do
639 let rest = do x <- entries; where x.messageId != f.messageId; yield x
628 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 {
641 toKey: f.toKey, fromKey: f.fromKey, encryptedBlob: f.encryptedBlob,
630 toKey: f.toKey, fromKey: f.fromKey, encryptedBlob: f.encryptedBlob,
642 timestamp: f.timestamp, originServer: serverHostN, messageId: f.messageId,
631 timestamp: f.timestamp, originServer: serverHostN, messageId: f.messageId,
643− authorization: "Bearer " ++ federationSecret})
632+ authorization: federationAuth})
644 whenOk result (removeForward f.messageId f.toServer)
633 whenOk result (removeForward f.messageId f.toServer)
645 retryForwardsForServer rest (remaining - 1)
634 retryForwardsForServer rest (remaining - 1)
646
635
@@ -654,8 +643,8 @@ retryPendingForwards = \pending -> do
654sendBatchedAcksToServer : [{messageId: MessageIdHex, toKey: PubkeyHex, originServer: ServerName, | a}] -> ServerName -> IO {network} {}
643sendBatchedAcksToServer : [{messageId: MessageIdHex, toKey: PubkeyHex, originServer: ServerName, | a}] -> ServerName -> IO {network} {}
655sendBatchedAcksToServer = \serverAcks server ->
644sendBatchedAcksToServer = \serverAcks server ->
656 forEachBatch serverAcks maxMessageIds (\batch -> do
645 forEachBatch serverAcks maxMessageIds (\batch -> do
657− result <- fetch (federationScheme ++ server) (FederationAck {
646+ result <- fetch (federationUrl server) (FederationAck {
658− authorization: "Bearer " ++ federationSecret,
647+ authorization: federationAuth,
659 messageIds: (do a <- batch; yield a.messageId),
648 messageIds: (do a <- batch; yield a.messageId),
660 originServer: server, fromServer: serverHostN})
649 originServer: server, fromServer: serverHostN})
661 whenOk result (atomic do
650 whenOk result (atomic do
@@ -679,8 +668,8 @@ retryEvictedAcks = \evictedEntries pendingAcks ->
679 in do
668 in do
680 let evictedOriginServers = collectPeerServers evictedToRetry (\a -> a.originServer)
669 let evictedOriginServers = collectPeerServers evictedToRetry (\a -> a.originServer)
681 sendToEach evictedOriginServers (\server -> do
670 sendToEach evictedOriginServers (\server -> do
682− fetchResult <- fetch (federationScheme ++ server) (FederationAck {
671+ fetchResult <- fetch (federationUrl server) (FederationAck {
683− authorization: "Bearer " ++ federationSecret,
672+ authorization: federationAuth,
684 messageIds: (do a <- evictedToRetry; where a.originServer == server; yield a.messageId),
673 messageIds: (do a <- evictedToRetry; where a.originServer == server; yield a.messageId),
685 originServer: server, fromServer: serverHostN})
674 originServer: server, fromServer: serverHostN})
686 case fetchResult of
675 case fetchResult of
@@ -737,6 +726,7 @@ pollOnce = \token cursor -> do
737 let lastReceivedAt = fold (\acc m -> max acc m.receivedAt) clampedCursor limitedMsgs
726 let lastReceivedAt = fold (\acc m -> max acc m.receivedAt) clampedCursor limitedMsgs
738 yield PollDone {events: evts, cursor: lastReceivedAt}
727 yield PollDone {events: evts, cursor: lastReceivedAt}
739
728
729+pollLoop : Text -> Int -> Int<Ms> -> IO {clock} PollResult
740pollLoop = \token cursor deadline -> do
730pollLoop = \token cursor deadline -> do
741 result <- pollOnce token cursor
731 result <- pollOnce token cursor
742 case result of
732 case result of
@@ -864,15 +854,16 @@ serve = \req -> case req of
864 SendDone {status, remoteServers, fromKey} ->
854 SendDone {status, remoteServers, fromKey} ->
865 when (count remoteServers > 0 && federationEnabled) (do
855 when (count remoteServers > 0 && federationEnabled) (do
866 sendToEach remoteServers (\server -> do
856 sendToEach remoteServers (\server -> do
867− fwdResult <- fetch (federationScheme ++ server) (ForwardMessage {
857+ fwdResult <- fetch (federationUrl server) (ForwardMessage {
868 toKey: to, fromKey: fromKey, encryptedBlob: encryptedBlob,
858 toKey: to, fromKey: fromKey, encryptedBlob: encryptedBlob,
869 timestamp: clampedTs, originServer: serverHostN, messageId: msgId,
859 timestamp: clampedTs, originServer: serverHostN, messageId: msgId,
870− authorization: "Bearer " ++ federationSecret})
860+ authorization: federationAuth})
871 whenOk fwdResult (removeForward msgId server))
861 whenOk fwdResult (removeForward msgId server))
872 case status of
862 case status of
873 MsQueued {} -> println ("sendMessage: forwards at capacity (" ++ show maxForwardsEntries ++ "), skipping forward tracking")
863 MsQueued {} -> println ("sendMessage: forwards at capacity (" ++ show maxForwardsEntries ++ "), skipping forward tracking")
874 _ -> yield {})
864 _ -> yield {})
865+ SendUnauthorized {} -> yield {}
875− _ -> yield {}
866+ SendRejected {} -> yield {}
876 broadcastOfflineGossip t result.pr.peerServers result.pr.expiredLocal)
867 broadcastOfflineGossip t result.pr.peerServers result.pr.expiredLocal)
877 yield (case result.send of
868 yield (case result.send of
878 SendUnauthorized {} -> respond {status: MsUnauthorized {}, messageId: ""}
869 SendUnauthorized {} -> respond {status: MsUnauthorized {}, messageId: ""}
@@ -912,9 +903,9 @@ serve = \req -> case req of
912 when federationEnabled (do
903 when federationEnabled (do
913 sendAcksAndClean pendingAcks
904 sendAcksAndClean pendingAcks
914 retryEvictedAcks evictedEntries pendingAcks)
905 retryEvictedAcks evictedEntries pendingAcks)
915− _ -> yield {}
906+ AckUnauthorized {} -> yield {}
916 broadcastOfflineGossip t result.pr.peerServers result.pr.expiredLocal)
907 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 {}})
918
909
919 LookupPresence {pubkey, authorization, respond} -> do
910 LookupPresence {pubkey, authorization, respond} -> do
920 t <- now
911 t <- now
@@ -990,8 +981,7 @@ serve = \req -> case req of
990 ForwardMessage {toKey, fromKey, encryptedBlob, timestamp, originServer, messageId, authorization, respond} -> do
981 ForwardMessage {toKey, fromKey, encryptedBlob, timestamp, originServer, messageId, authorization, respond} -> do
991 t <- now
982 t <- now
992 let authorized = verifyFederationAuth authorization
983 let authorized = verifyFederationAuth authorization
993− let validOrigin = originServer != serverHostN
994− if not authorized || not validOrigin
984+ if not authorized || originServer == serverHostN
995 then rejectFederation t respond
985 then rejectFederation t respond
996 else do
986 else do
997 let clampedTs = clampTimestamp timestamp t
987 let clampedTs = clampTimestamp timestamp t
@@ -1013,7 +1003,7 @@ serve = \req -> case req of
1013 encryptedBlob: encryptedBlob, timestamp: clampedTs, origin: FederatedOrigin {server: originServer}, receivedAt: seqTs}]
1003 encryptedBlob: encryptedBlob, timestamp: clampedTs, origin: FederatedOrigin {server: originServer}, receivedAt: seqTs}]
1014 yield {pr: pr, forward: ForwardStored {}}
1004 yield {pr: pr, forward: ForwardStored {}}
1015 forkBroadcastOffline t result.pr
1005 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 {}})
1017
1007
1018 FederationAck {messageIds, originServer, fromServer, authorization, respond} -> do
1008 FederationAck {messageIds, originServer, fromServer, authorization, respond} -> do
1019 t <- now
1009 t <- now