on master help sign in
Simplify code and improve type safety: separate form input state from Page DU, add dedicated input messages for compose and contact form fields
e7abeeaiko
client/App.fs+37−18
@@ -45,8 +45,8 @@ module App =
45 type Page =
45 type Page =
46 | Setup
46 | Setup
47 | Conversations
47 | Conversations
48− | Chat of pubkey: string * compose: string
48+ | Chat of pubkey: string
49− | AddContact of pubkey: string * nickname: string
49+ | AddContact
50 | Settings
50 | Settings
51
51
52 type Session = { Url: string; Token: string; Identity: Identity }
52 type Session = { Url: string; Token: string; Identity: Identity }
@@ -70,12 +70,18 @@ module App =
70 PollStatus: string
70 PollStatus: string
71 Messages: Map<string, ChatMessage list>
71 Messages: Map<string, ChatMessage list>
72 Error: string option
72 Error: string option
73− PollCursor: int64 }
73+ PollCursor: int64
74+ Compose: string
75+ ContactPubkey: string
76+ ContactNickname: string }
74
77
75 // ── Msg ──
78 // ── Msg ──
76
79
77 type Msg =
80 type Msg =
78 | SetPage of Page
81 | SetPage of Page
82+ | SetCompose of string
83+ | SetContactPubkey of string
84+ | SetContactNickname of string
79 | GenIdentity
85 | GenIdentity
80 | IdentityReady of Identity
86 | IdentityReady of Identity
81 | SetServerUrl of string
87 | SetServerUrl of string
@@ -175,15 +181,26 @@ module App =
175 Contacts = []
181 Contacts = []
176 Messages = Map.empty
182 Messages = Map.empty
177 Error = None
183 Error = None
178− PollCursor = 0L },
184+ PollCursor = 0L
185+ Compose = ""
186+ ContactPubkey = ""
187+ ContactNickname = "" },
179 [ CmdLoadState ]
188 [ CmdLoadState ]
180
189
181 // ── Update ──
190 // ── Update ──
182
191
183 let update msg model =
192 let update msg model =
184 match msg with
193 match msg with
194+ | SetPage(Chat pk) ->
195+ { model with Page = Chat pk; Compose = "" }, []
196+ | SetPage AddContact ->
197+ { model with Page = AddContact; ContactPubkey = ""; ContactNickname = "" }, []
185 | SetPage page -> { model with Page = page }, []
198 | SetPage page -> { model with Page = page }, []
186
199
200+ | SetCompose text -> { model with Compose = text }, []
201+ | SetContactPubkey pk -> { model with ContactPubkey = pk }, []
202+ | SetContactNickname nn -> { model with ContactNickname = nn }, []
203+
187 | GenIdentity -> model, [ CmdGenIdentity ]
204 | GenIdentity -> model, [ CmdGenIdentity ]
188
205
189 | IdentityReady identity ->
206 | IdentityReady identity ->
@@ -212,20 +229,21 @@ module App =
212
229
213 | DoSend ->
230 | DoSend ->
214 match model.Page, trySession model with
231 match model.Page, trySession model with
215− | Chat(pk, compose), Some session when compose <> "" ->
232+ | Chat pk, Some session when model.Compose <> "" ->
233+ let text = model.Compose
216 let id = Guid.NewGuid().ToString()
234 let id = Guid.NewGuid().ToString()
217 let outMsg =
235 let outMsg =
218 { Id = id
236 { Id = id
219− Body = compose
237+ Body = text
220 Timestamp = DateTimeOffset.UtcNow
238 Timestamp = DateTimeOffset.UtcNow
221 IsOutgoing = true }
239 IsOutgoing = true }
222
240
223 let model' =
241 let model' =
224 { model with
242 { model with
225− Page = Chat(pk, "")
243+ Compose = ""
226 Messages = model.Messages |> appendMessage pk outMsg }
244 Messages = model.Messages |> appendMessage pk outMsg }
227
245
228− model', [ CmdSend(session, pk, compose, id); saveCmdMsg model' ]
246+ model', [ CmdSend(session, pk, text, id); saveCmdMsg model' ]
229 | _ -> model, []
247 | _ -> model, []
230
248
231 | SendFailed err -> { model with Error = Some err }, []
249 | SendFailed err -> { model with Error = Some err }, []
@@ -247,8 +265,9 @@ module App =
247 if not incoming.IsEmpty then saveCmdMsg model' ]
265 if not incoming.IsEmpty then saveCmdMsg model' ]
248
266
249 | DoSaveContact ->
267 | DoSaveContact ->
268+ let pk, nn = model.ContactPubkey, model.ContactNickname
250 match model.Page with
269 match model.Page with
251− | AddContact(pk, nn) when pk <> "" && nn <> "" ->
270+ | AddContact when pk <> "" && nn <> "" ->
252 match Phonemic.fromOb pk |> Option.orElseWith (fun () -> Crypto.tryFromHex pk) with
271 match Phonemic.fromOb pk |> Option.orElseWith (fun () -> Crypto.tryFromHex pk) with
253 | Some bytes when bytes.Length = 32 ->
272 | Some bytes when bytes.Length = 32 ->
254 let contact: Contact = { Pubkey = Crypto.toHex bytes; Nickname = nn }
273 let contact: Contact = { Pubkey = Crypto.toHex bytes; Nickname = nn }
@@ -482,7 +501,7 @@ module App =
482 Label("Conversations")
501 Label("Conversations")
483 .font(size = 24.)
502 .font(size = 24.)
484
503
485− Button("+", SetPage(AddContact("", "")))
504+ Button("+", SetPage AddContact)
486 Button("Settings", SetPage Settings)
505 Button("Settings", SetPage Settings)
487 }
506 }
488
507
@@ -511,13 +530,13 @@ module App =
511 |> Option.map (fun m -> truncate 40 m.Body)
530 |> Option.map (fun m -> truncate 40 m.Body)
512 |> Option.defaultValue ""
531 |> Option.defaultValue ""
513
532
514− Button($"{c.Nickname}\n{preview}", SetPage(Chat(c.Pubkey, "")))
533+ Button($"{c.Nickname}\n{preview}", SetPage(Chat c.Pubkey))
515 })
534 })
516 .padding(20.)
535 .padding(20.)
517 )
536 )
518 )
537 )
519
538
520− let private viewChat model pk compose =
539+ let private viewChat model pk =
521 let name = contactName model.Contacts pk
540 let name = contactName model.Contacts pk
522 let msgs = messagesFor pk model.Messages
541 let msgs = messagesFor pk model.Messages
523
542
@@ -549,7 +568,7 @@ module App =
549 .font(size = 14.)
568 .font(size = 14.)
550
569
551 HStack(spacing = 8.) {
570 HStack(spacing = 8.) {
552− Entry(compose, fun t -> SetPage(Chat(pk, t)))
571+ Entry(model.Compose, SetCompose)
553 .placeholder("Message...")
572 .placeholder("Message...")
554
573
555 Button("Send", DoSend)
574 Button("Send", DoSend)
@@ -558,7 +577,7 @@ module App =
558 .padding(12.)
577 .padding(12.)
559 )
578 )
560
579
561− let private viewAddContact pk nn =
580+ let private viewAddContact model =
562 ContentPage(
581 ContentPage(
563 (VStack(spacing = 16.) {
582 (VStack(spacing = 16.) {
564 HStack(spacing = 8.) {
583 HStack(spacing = 8.) {
@@ -569,11 +588,11 @@ module App =
569 }
588 }
570
589
571 Label("Public Key:")
590 Label("Public Key:")
572− Entry(pk, fun t -> SetPage(AddContact(t, nn)))
591+ Entry(model.ContactPubkey, SetContactPubkey)
573 .placeholder("sampel-palnet-...")
592 .placeholder("sampel-palnet-...")
574
593
575 Label("Nickname:")
594 Label("Nickname:")
576− Entry(nn, fun t -> SetPage(AddContact(pk, t)))
595+ Entry(model.ContactNickname, SetContactNickname)
577
596
578 Button("Save Contact", DoSaveContact)
597 Button("Save Contact", DoSaveContact)
579 .centerHorizontal()
598 .centerHorizontal()
@@ -587,8 +606,8 @@ module App =
587 | Setup -> viewSetup ()
606 | Setup -> viewSetup ()
588 | Settings -> viewSettings model
607 | Settings -> viewSettings model
589 | Conversations -> viewConversations model
608 | Conversations -> viewConversations model
590− | Chat(pk, compose) -> viewChat model pk compose
609+ | Chat pk -> viewChat model pk
591− | AddContact(pk, nn) -> viewAddContact pk nn
610+ | AddContact -> viewAddContact model
592
611
593 Application() { Window(page) }
612 Application() { Window(page) }
594
613
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− lezBPlMGgvvXPLrij5BGxw+mSWU=
169+ odqTbduEhGM/hvZvmJSmPAW/DxQ=
170 </data>
170 </data>
171 <key>Skrepka.pdb</key>
171 <key>Skrepka.pdb</key>
172 <data>
172 <data>
173− GEoIFmupvmKvjt37aNX0Nqhf4Mw=
173+ 6ZCzYSeKwCxHL9bsITOsUC1FbSY=
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− lOwwpy7OvL/VnkdUcEg8WtWpvQYej7RHx+vgMXJ9cIk=
1396+ 2y12o1alIca/RdSLuaoUEAbdYM1HImAE66/FLxG9NZ0=
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− jNIcynqsu5mNKMdxE4ue6lbnHnYvri+aDBLPMWQYh38=
1403+ jCeuDQPnhCUmzAqkbCUw8pzbweZFd3m0luVDHGR5lBs=
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− mCT+MjgJCiZB3ntslUAZlKSRDaSra+agyPLYQVKRysE=
254+ km4jJeyq8/Komz5joA4/Gcm8bUYB7zg5nUoDcC4qySM=
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− RjcWvTe0sRpmh7W0SPeKoUI1aVen35HuEM/X+//ONNk=
261+ iSV2W540FfQSaG7HhcVDuQi7W3VeL2x8ALd8CmMz+BE=
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+5c34fab63923cdb1b780a7e5a5ce5fae72adf78e")>]
13+[<assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0+5a91ff165d267f1b48cd27804ddc67fbfbb1a12b")>]
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−5ce28345d4f1ab17ca4f1637108170a533a9e7a3ae603bd386f96f4a7115bf28
1+a6658304228f3f3637147ff877110d1fb4cb441db24a871da0543c9b5d997966
client/obj/Debug/net10.0-ios/iossimulator-arm64/Skrepka.dll

Binary file not shown.

client/obj/Debug/net10.0-ios/iossimulator-arm64/Skrepka.fsproj.CoreCompileInputs.cache+1−1
@@ -1 +1 @@
1−69642e04cc9acf8f904dc028dba9bb2fb594ca3a355cba1325fba75dad676d69
1+0182c29e4e78ed8ffdff7ee936a192ffc3599905f475905b18569649ce05748a
client/obj/Debug/net10.0-ios/iossimulator-arm64/Skrepka.fsproj.FileListAbsolute.txt+23
@@ -334,3 +334,26 @@
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/5c34fab63923cdb1b780a7e5a5ce5fae72adf78e/*"}}
1+{"documents":{"/Users/iko/Developer/skrepka/*":"https://raw.githubusercontent.com/ilyakooo0/skrepka/5a91ff165d267f1b48cd27804ddc67fbfbb1a12b/*"}}
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+f4f8a00e43e943aaf1ab17647f6d1e4f055c7d3a")>]
13+[<assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0+5a91ff165d267f1b48cd27804ddc67fbfbb1a12b")>]
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−d1e3dd8bf6923e3c33b8e7c46138a01be4bdfd2c1de6dfe8e4da64444b301b79
1+bab902372413363aadc7dd6fd03839ec75cc7bfbd2431aa615e69476a889187f
client/obj/Debug/net10.0-maccatalyst/maccatalyst-arm64/Skrepka.dll

Binary file not shown.

client/obj/Debug/net10.0-maccatalyst/maccatalyst-arm64/Skrepka.fsproj.CoreCompileInputs.cache+1−1
@@ -1 +1 @@
1−6202acc233295b7d078b1e7cc174d976ebb69fcd5075adc57d9dd3761c9e9f75
1+e72de62331c5600bbb3125cfe2f57f32602258ae53f7d082ac6aaf123c600e47
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/c0cce11a1754909d3aa01c81e84df143a9bcd275/*"}}
1+{"documents":{"/Users/iko/Developer/skrepka/*":"https://raw.githubusercontent.com/ilyakooo0/skrepka/5a91ff165d267f1b48cd27804ddc67fbfbb1a12b/*"}}
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+39−35
@@ -48,9 +48,7 @@ federationEnabled = federationSecret != ""
48-- Types
48-- Types
49-- ============================================
49-- ============================================
50
50
51−type PollEventType = Text where \s -> s == "message"
52−
53−type PollEvent = {id: MessageIdHex, eventType: PollEventType, payload: {encryptedBlob: BlobHex, fromKey: PubkeyHex, timestamp: Timestamp, toKey: PubkeyHex}}
51+type PollEvent = {id: MessageIdHex, eventType: Text, payload: {encryptedBlob: BlobHex, fromKey: PubkeyHex, timestamp: Timestamp, toKey: PubkeyHex}}
54
52
55type GossipEventType = Text where \s -> s == "online" || s == "offline"
53type GossipEventType = Text where \s -> s == "online" || s == "offline"
56
54
@@ -58,26 +56,29 @@ type GossipEvent = {eventType: GossipEventType, pubkey: PubkeyHex, server: Serve
58
56
59type ValidGossipEntry = {eventType: GossipEventType, pubkey: PubkeyHex, server: ServerName, expiresAt: Timestamp, timestamp: Timestamp, ttl: Int<S>}
57type ValidGossipEntry = {eventType: GossipEventType, pubkey: PubkeyHex, server: ServerName, expiresAt: Timestamp, timestamp: Timestamp, ttl: Int<S>}
60
58
59+isDeliveryStatus = \s -> s == "delivered" || s == "federated" || s == "queued"
61−type MessageStatus = Text where \s -> s == "unauthorized" || s == "rejected" || s == "delivered" || s == "federated" || s == "queued"
60+type MessageStatus = Text where \s -> s == "unauthorized" || s == "rejected" || isDeliveryStatus s
62
61
63type PubkeyHex = Text where \s -> isValidHex s && not (tooLong s maxPubkeyLen) && s == normalizeHex s
62type PubkeyHex = Text where \s -> isValidHex s && not (tooLong s maxPubkeyLen) && s == normalizeHex s
64type BlobHex = Text where \s -> isValidHex s && not (tooLong s maxBlobLen) && s == normalizeHex s
63type BlobHex = Text where \s -> isValidHex s && not (tooLong s maxBlobLen) && s == normalizeHex s
65type SignatureHex = Text where \s -> isValidHex s && not (tooLong s maxSignatureLen) && s == normalizeHex s
64type SignatureHex = Text where \s -> isValidHex s && not (tooLong s maxSignatureLen) && s == normalizeHex s
66type MessageIdHex = Text where \s -> isValidHex s && not (tooLong s maxMessageIdLen) && s == normalizeHex s
65type MessageIdHex = Text where \s -> isValidHex s && not (tooLong s maxMessageIdLen) && s == normalizeHex s
67−type ServerName = Text where \s -> not (tooLong s maxServerNameLen) && s == normalizeServerName s && not (isBadServerName s)
66+isValidServerName = \s -> not (tooLong s maxServerNameLen) && s == normalizeServerName s && not (isBadServerName s)
67+type ServerName = Text where isValidServerName
68+type OptionalServerName = Text where \s -> s == "" || isValidServerName s
68−type OptionalServerName = Text where \s -> s == "" || (not (tooLong s maxServerNameLen) && s == normalizeServerName s && not (isBadServerName s))
69+type AnyServerName = Text where \s -> not (tooLong s maxServerNameLen) && s == normalizeServerName s
69
70
70type Timestamp = Int<Ms>
71type Timestamp = Int<Ms>
71
72
72type Challenge = {pubkey: PubkeyHex, challenge: Text, expiresAt: Timestamp}
73type Challenge = {pubkey: PubkeyHex, challenge: Text, expiresAt: Timestamp}
73type Session = {token: Text, pubkey: PubkeyHex, expiresAt: Timestamp}
74type Session = {token: Text, pubkey: PubkeyHex, expiresAt: Timestamp}
74type Message = {messageId: MessageIdHex, toKey: PubkeyHex, fromKey: PubkeyHex, encryptedBlob: BlobHex, timestamp: Timestamp, originServer: OptionalServerName, receivedAt: Timestamp}
75type Message = {messageId: MessageIdHex, toKey: PubkeyHex, fromKey: PubkeyHex, encryptedBlob: BlobHex, timestamp: Timestamp, originServer: OptionalServerName, receivedAt: Timestamp}
75−type PresenceEntry = {pubkey: PubkeyHex, server: Text, expiresAt: Timestamp, timestamp: Timestamp}
76+type PresenceEntry = {pubkey: PubkeyHex, server: AnyServerName, expiresAt: Timestamp, timestamp: Timestamp}
76type ForwardEntry = {messageId: MessageIdHex, toServer: ServerName, createdAt: Timestamp, toKey: PubkeyHex, fromKey: PubkeyHex, encryptedBlob: BlobHex, timestamp: Timestamp}
77type ForwardEntry = {messageId: MessageIdHex, toServer: ServerName, createdAt: Timestamp, toKey: PubkeyHex, fromKey: PubkeyHex, encryptedBlob: BlobHex, timestamp: Timestamp}
77type AckedEntry = {messageId: MessageIdHex, toKey: PubkeyHex, ackedAt: Timestamp, originServer: ServerName}
78type AckedEntry = {messageId: MessageIdHex, toKey: PubkeyHex, ackedAt: Timestamp, originServer: ServerName}
78type AuthResult = Authorized {pubkey: PubkeyHex} | Unauthorized {}
79type AuthResult = Authorized {pubkey: PubkeyHex} | Unauthorized {}
79type PruneResult = {expiredLocal: [PresenceEntry], peerServers: [ServerName]}
80type PruneResult = {expiredLocal: [PresenceEntry], peerServers: [ServerName]}
80−type DeliveryStatus = Text where \s -> s == "delivered" || s == "federated" || s == "queued"
81+type DeliveryStatus = Text where isDeliveryStatus
81type SendResult = SendUnauthorized {} | SendRejected {} | SendDone {status: DeliveryStatus, remoteServers: [ServerName], fromKey: PubkeyHex}
82type SendResult = SendUnauthorized {} | SendRejected {} | SendDone {status: DeliveryStatus, remoteServers: [ServerName], fromKey: PubkeyHex}
82
83
83
84
@@ -265,14 +266,16 @@ normalizeServerName = \s -> toLower (stripTrailingDot s)
265
266
266serverHostN = normalizeServerName serverHost
267serverHostN = normalizeServerName serverHost
267
268
268−containsSuffix = \suffix suffixLen s ->
269+containsSuffixN = \suffix suffixLen s ->
269 if s == "" then False {}
270 if s == "" then False {}
270 else if take suffixLen s == suffix && (drop suffixLen s == "" || take 1 (drop suffixLen s) == ":") then True {}
271 else if take suffixLen s == suffix && (drop suffixLen s == "" || take 1 (drop suffixLen s) == ":") then True {}
271− else containsSuffix suffix suffixLen (drop 1 s)
272+ else containsSuffixN suffix suffixLen (drop 1 s)
273+
274+containsSuffix = \suffix s -> containsSuffixN suffix (count (chars suffix)) s
272
275
273−containsLocalhostSuffix = \s -> containsSuffix ".localhost" 10 s
276+containsLocalhostSuffix = \s -> containsSuffix ".localhost" s
274−containsLocalSuffix = \s -> containsSuffix ".local" 6 s
277+containsLocalSuffix = \s -> containsSuffix ".local" s
275−containsInternalSuffix = \s -> containsSuffix ".internal" 9 s
278+containsInternalSuffix = \s -> containsSuffix ".internal" s
276
279
277is172Private = \sn ->
280is172Private = \sn ->
278 take 4 sn == "172."
281 take 4 sn == "172."
@@ -463,6 +466,22 @@ removeOffline = \presenceSet offlineOnlyKeys trustedOffline t ->
463 where samePubkeyServer p k && any trustedOffline (\o -> samePubkeyServer o k && (if o.timestamp > t then t else o.timestamp) > p.timestamp)
466 where samePubkeyServer p k && any trustedOffline (\o -> samePubkeyServer o k && (if o.timestamp > t then t else o.timestamp) > p.timestamp)
464 yield p)
467 yield p)
465
468
469+capGossipPresence = \afterOffline appliedOnline trustedDeduped presence offlineOnlyKeys trustedOffline t ->
470+ if count afterOffline <= maxPresenceEntries
471+ then {finalPresence: afterOffline, appliedOnline: appliedOnline, withinCapacity: True {}}
472+ else
473+ let updatesOnly = do e <- trustedDeduped; where any presence (\p -> samePubkeyServer p e); yield e in
474+ let updatesOnlyMerged = mergeByPubkeyServer presence updatesOnly in
475+ let updatesOnlyAfterOffline = removeOffline updatesOnlyMerged offlineOnlyKeys trustedOffline t in
476+ let reclaimSlots = maxPresenceEntries - count updatesOnlyAfterOffline in
477+ let newEntries = do e <- trustedDeduped; where none presence (\p -> samePubkeyServer p e); yield e in
478+ let reclaimedEntries = takeN newEntries reclaimSlots in
479+ {finalPresence: union updatesOnlyAfterOffline reclaimedEntries,
480+ appliedOnline: union
481+ (do e <- updatesOnly; where none presence (\p -> samePubkeyServer p e && p.expiresAt >= e.expiresAt); yield {pubkey: e.pubkey, server: e.server})
482+ (do e <- reclaimedEntries; yield {pubkey: e.pubkey, server: e.server}),
483+ withinCapacity: False {}}
484+
466countMessagesFor = \messages toKey t ->
485countMessagesFor = \messages toKey t ->
467 count (do
486 count (do
468 m <- messages
487 m <- messages
@@ -617,7 +636,7 @@ retryEvictedAcks = \evictedEntries pendingAcks ->
617 a <- evictedEntries
636 a <- evictedEntries
618 where none pendingAcks (\a2 -> a2.messageId == a.messageId && a2.toKey == a.toKey)
637 where none pendingAcks (\a2 -> a2.messageId == a.messageId && a2.toKey == a.toKey)
619 yield a
638 yield a
620− in when (count evictedToRetry > 0) (do
639+ in do
621 let evictedOriginServers = collectPeerServers evictedToRetry (\a -> a.originServer)
640 let evictedOriginServers = collectPeerServers evictedToRetry (\a -> a.originServer)
622 sendToEach evictedOriginServers (\server -> do
641 sendToEach evictedOriginServers (\server -> do
623 fetchResult <- fetch (federationScheme ++ server) (FederationAck {
642 fetchResult <- fetch (federationScheme ++ server) (FederationAck {
@@ -637,7 +656,7 @@ retryEvictedAcks = \evictedEntries pendingAcks ->
637 yield excess
656 yield excess
638 when (dropped > 0)
657 when (dropped > 0)
639 (println ("ackMessages: ackedIds at capacity (" ++ show maxAckedIds ++ "), " ++ show dropped ++ " ack entries permanently lost for " ++ server))
658 (println ("ackMessages: ackedIds at capacity (" ++ show maxAckedIds ++ "), " ++ show dropped ++ " ack entries permanently lost for " ++ server))
640− _ -> yield {}))
659+ _ -> yield {})
641
660
642pruneAndRetry = \t -> do
661pruneAndRetry = \t -> do
643 result <- atomic do
662 result <- atomic do
@@ -649,11 +668,9 @@ pruneAndRetry = \t -> do
649 yield {expiredLocal: pr.expiredLocal, peerServers: pr.peerServers, pendingForwards: forwards, pendingAcks: pendingAcks}
668 yield {expiredLocal: pr.expiredLocal, peerServers: pr.peerServers, pendingForwards: forwards, pendingAcks: pendingAcks}
650 broadcastOfflineGossip t result.peerServers result.expiredLocal
669 broadcastOfflineGossip t result.peerServers result.expiredLocal
651 when federationEnabled (do
670 when federationEnabled (do
652− when (count result.pendingForwards > 0)
653− (retryPendingForwards result.pendingForwards)
671+ retryPendingForwards result.pendingForwards
654− when (count result.pendingAcks > 0) (do
655− let ackOriginServers = collectPeerServers result.pendingAcks (\a -> a.originServer)
672+ let ackOriginServers = collectPeerServers result.pendingAcks (\a -> a.originServer)
656− sendAcksAndClean result.pendingAcks ackOriginServers))
673+ sendAcksAndClean result.pendingAcks ackOriginServers)
657
674
658pollOnce = \token cursor -> do
675pollOnce = \token cursor -> do
659 t <- now
676 t <- now
@@ -920,23 +937,10 @@ serve = \req -> case req of
920 where any presence (\p -> samePubkeyServer p k && any trustedOffline (\o -> samePubkeyServer o k && (if o.timestamp > t then t else o.timestamp) > p.timestamp))
937 where any presence (\p -> samePubkeyServer p k && any trustedOffline (\o -> samePubkeyServer o k && (if o.timestamp > t then t else o.timestamp) > p.timestamp))
921 yield {pubkey: k.pubkey, server: k.server}
938 yield {pubkey: k.pubkey, server: k.server}
922 let afterOffline = removeOffline merged offlineOnlyKeys trustedOffline t
939 let afterOffline = removeOffline merged offlineOnlyKeys trustedOffline t
923− let withinCapacity = count afterOffline <= maxPresenceEntries
940+ let capResult = capGossipPresence afterOffline appliedOnline trustedDeduped presence offlineOnlyKeys trustedOffline t
924− let capResult = if withinCapacity
925− then {finalPresence: afterOffline, appliedOnline: appliedOnline}
926− else
927− let updatesOnly = do e <- trustedDeduped; where any presence (\p -> samePubkeyServer p e); yield e in
928− let updatesOnlyMerged = mergeByPubkeyServer presence updatesOnly in
929− let updatesOnlyAfterOffline = removeOffline updatesOnlyMerged offlineOnlyKeys trustedOffline t in
930− let reclaimSlots = maxPresenceEntries - count updatesOnlyAfterOffline in
931− let newEntries = do e <- trustedDeduped; where none presence (\p -> samePubkeyServer p e); yield e in
932− let reclaimedEntries = takeN newEntries reclaimSlots in
933− {finalPresence: union updatesOnlyAfterOffline reclaimedEntries,
934− appliedOnline: union
935− (do e <- updatesOnly; where none presence (\p -> samePubkeyServer p e && p.expiresAt >= e.expiresAt); yield {pubkey: e.pubkey, server: e.server})
936− (do e <- reclaimedEntries; yield {pubkey: e.pubkey, server: e.server})}
937 set *presence = capResult.finalPresence
941 set *presence = capResult.finalPresence
938 let gossipPeerServers = collectPeerServers capResult.finalPresence (\p -> p.server)
942 let gossipPeerServers = collectPeerServers capResult.finalPresence (\p -> p.server)
939− yield {appliedKeys: union capResult.appliedOnline appliedOffline, pr: pr, gossipPeerServers: gossipPeerServers, withinCapacity: withinCapacity}
943+ yield {appliedKeys: union capResult.appliedOnline appliedOffline, pr: pr, gossipPeerServers: gossipPeerServers, withinCapacity: capResult.withinCapacity}
940 let effectiveEvents = do
944 let effectiveEvents = do
941 e <- union filteredOnline filteredOffline
945 e <- union filteredOnline filteredOffline
942 where any gossipResult.appliedKeys (\a -> samePubkeyServer a e)
946 where any gossipResult.appliedKeys (\a -> samePubkeyServer a e)