on master help sign in
Add profile support: display names, bios, and photos shared via encrypted profile messages
commit4471c7ea728053f905aa06e4d51de8c7e2de1550 authoriko <mail@iko.soy> dateSat 11 Apr 2026, 13:22 (6 months ago) parentfec9263
28 files changed+284−100
client/App.fs+186−15 client/Store.fs+38−1 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+48−49
client/App.fs+186−15
@@ -1,6 +1,7 @@
11namespace Skrepka
22
33open System
4+open System.IO
45open System.Text.Json
56open System.Text.Json.Serialization
67open Fabulous
@@ -23,6 +24,7 @@ module App =
2324 | Chat of pubkey: string * compose: string
2425 | AddContact of pubkey: string * nickname: string
2526 | Settings
27+ | EditProfile of displayName: string * bio: string * photo: string option
2628
2729 type Session = { Url: string; Token: string; Identity: Identity }
2830
@@ -40,6 +42,7 @@ module App =
4042 type DecryptedEvent =
4143 | IncomingChat of IncomingMessage
4244 | IncomingDeliveryAck of senderPubkey: string * ackIds: string list
45+ | IncomingProfile of senderPubkey: string * displayName: string * bio: string * photo: string option
4346
4447 type PollReceived = { Events: int; Messages: int; Errors: int; FirstError: string option }
4548
@@ -59,13 +62,14 @@ module App =
5962 PollStatus: PollStatus
6063 Messages: Map<string, ChatMessage list>
6164 Error: string option
62− PollCursor: int64 }
65+ PollCursor: int64
66+ Profile: Profile option }
6367
6468 // ── Msg ──
6569
6670 type StateLoadResult =
67− | IdentityAndData of Identity * Data
68− | IdentityOnly of Identity
71+ | IdentityAndData of Identity * Data * Profile option
72+ | IdentityOnly of Identity * Profile option
6973 | NothingSaved
7074
7175 type Msg =
@@ -87,6 +91,11 @@ module App =
8791 | CopyPubKey
8892 | DismissError
8993 | StateLoaded of StateLoadResult
94+ | SetProfileDisplayName of string
95+ | SetProfileBio of string
96+ | DoPickPhoto
97+ | PhotoPicked of string option
98+ | DoSaveProfile
9099
91100 // ── CmdMsg ──
92101
@@ -98,6 +107,9 @@ module App =
98107 | CmdLoadState
99108 | CmdSaveIdentity of Identity
100109 | CmdSaveData of Data
110+ | CmdPickPhoto
111+ | CmdSaveProfile of Profile
112+ | CmdSendProfileToAll of session: Session * Profile * recipientHexes: string list
101113
102114 // ── Helpers ──
103115
@@ -121,10 +133,14 @@ module App =
121133 |> Option.filter (fun bytes -> bytes.Length = 32)
122134
123135 let private contactName (contacts: Contact list) (pk: string) =
136+ let fallback = truncKey pk
124137 contacts
125138 |> List.tryFind (fun c -> c.Pubkey = pk)
126− |> Option.map _.Nickname
127− |> Option.defaultValue (truncKey pk)
139+ |> Option.map (fun c ->
140+ if c.Nickname <> fallback then c.Nickname
141+ elif not (String.IsNullOrEmpty c.DisplayName) then c.DisplayName
142+ else c.Nickname)
143+ |> Option.defaultValue fallback
128144
129145 let private messagesFor (pk: string) (messages: Map<string, ChatMessage list>) =
130146 messages |> Map.tryFind pk |> Option.defaultValue []
@@ -179,7 +195,8 @@ module App =
179195 Contacts = []
180196 Messages = Map.empty
181197 Error = None
182− PollCursor = 0L },
198+ PollCursor = 0L
199+ Profile = None },
183200 [ CmdLoadState ]
184201
185202 // ── Update ──
@@ -237,12 +254,20 @@ module App =
237254 IsOutgoing = true
238255 Status = DeliveryStatus.Sent }
239256
257+ let isFirstInteraction = model.Messages |> messagesFor pk |> List.isEmpty
258+
240259 let model' =
241260 { model with
242261 Page = Chat(pk, "")
243262 Messages = model.Messages |> appendMessage pk outMsg }
244263
245− model', [ CmdSend(session, pk, compose, id); saveCmdMsg model' ]
264+ let cmds = [ CmdSend(session, pk, compose, id); saveCmdMsg model' ]
265+ let cmds =
266+ match model.Profile, isFirstInteraction with
267+ | Some profile, true -> cmds @ [ CmdSendProfileToAll(session, profile, [ pk ]) ]
268+ | _ -> cmds
269+
270+ model', cmds
246271 | _ -> model, []
247272
248273 | SendFailed err -> { model with Error = Some err }, []
@@ -256,10 +281,18 @@ module App =
256281 match evt with
257282 | IncomingChat msg ->
258283 { m with
259− Contacts = m.Contacts |> addContactIfNew { Pubkey = msg.SenderPubkey; Nickname = truncKey msg.SenderPubkey }
284+ Contacts = m.Contacts |> addContactIfNew { Pubkey = msg.SenderPubkey; Nickname = truncKey msg.SenderPubkey; DisplayName = null; Bio = null; PhotoBase64 = null }
260285 Messages = m.Messages |> appendMessage msg.SenderPubkey msg.Message }
261286 | IncomingDeliveryAck(senderPubkey, ackIds) ->
262− { m with Messages = m.Messages |> markDelivered senderPubkey ackIds })
287+ { m with Messages = m.Messages |> markDelivered senderPubkey ackIds }
288+ | IncomingProfile(senderPubkey, displayName, bio, photo) ->
289+ { m with
290+ Contacts =
291+ m.Contacts
292+ |> List.map (fun c ->
293+ if c.Pubkey = senderPubkey then
294+ { c with DisplayName = displayName; Bio = bio; PhotoBase64 = photo |> Option.defaultValue null }
295+ else c) })
263296 { model with PollStatus = status; PollCursor = newCursor }
264297
265298 model',
@@ -273,7 +306,12 @@ module App =
273306 | AddContact(cpk, cnn) when cpk <> "" && cnn <> "" ->
274307 match tryParsePubkey cpk with
275308 | Some bytes ->
276− let contact: Contact = { Pubkey = Crypto.toHex bytes; Nickname = cnn }
309+ let hex = Crypto.toHex bytes
310+ let existing = model.Contacts |> List.tryFind (fun c -> c.Pubkey = hex)
311+ let contact: Contact =
312+ match existing with
313+ | Some e -> { e with Nickname = cnn }
314+ | None -> { Pubkey = hex; Nickname = cnn; DisplayName = null; Bio = null; PhotoBase64 = null }
277315
278316 let model' =
279317 { model with
@@ -291,7 +329,37 @@ module App =
291329
292330 | DismissError -> { model with Error = None }, []
293331
294− | StateLoaded(IdentityAndData(identity, data)) ->
332+ | SetProfileDisplayName name ->
333+ match model.Page with
334+ | EditProfile(_, bio, photo) -> { model with Page = EditProfile(name, bio, photo) }, []
335+ | _ -> model, []
336+
337+ | SetProfileBio bio ->
338+ match model.Page with
339+ | EditProfile(name, _, photo) -> { model with Page = EditProfile(name, bio, photo) }, []
340+ | _ -> model, []
341+
342+ | DoPickPhoto -> model, [ CmdPickPhoto ]
343+
344+ | PhotoPicked photo ->
345+ match model.Page with
346+ | EditProfile(name, bio, _) -> { model with Page = EditProfile(name, bio, photo) }, []
347+ | _ -> model, []
348+
349+ | DoSaveProfile ->
350+ match model.Page with
351+ | EditProfile(name, bio, photo) ->
352+ let profile: Profile = { DisplayName = name; Bio = bio; PhotoBase64 = photo }
353+ let cmds = [ CmdSaveProfile profile ]
354+ let cmds =
355+ match trySession model with
356+ | Some session ->
357+ cmds @ [ CmdSendProfileToAll(session, profile, model.Contacts |> List.map _.Pubkey) ]
358+ | None -> cmds
359+ { model with Profile = Some profile; Page = Settings }, cmds
360+ | _ -> model, []
361+
362+ | StateLoaded(IdentityAndData(identity, data, profile)) ->
295363 let model' =
296364 { model with
297365 Auth = Identified(identity, Connecting)
@@ -299,10 +367,11 @@ module App =
299367 Messages = data.Messages
300368 ServerUrl = data.ServerUrl |> Option.defaultValue model.ServerUrl
301369 PollCursor = data.PollCursor
370+ Profile = profile
302371 Page = Conversations }
303372 model', [ CmdConnect(model'.ServerUrl, identity) ]
304− | StateLoaded(IdentityOnly identity) ->
305− { model with Auth = Identified(identity, Offline); Page = Settings }, []
373+ | StateLoaded(IdentityOnly(identity, profile)) ->
374+ { model with Auth = Identified(identity, Offline); Profile = profile; Page = Settings }, []
306375 | StateLoaded NothingSaved -> model, []
307376
308377 // ── mapCmd ──
@@ -318,12 +387,15 @@ module App =
318387 type private Envelope =
319388 | TextMessage of id: string * body: string
320389 | DeliveryAck of id: string * ackIds: string list
390+ | ProfileMessage of id: string * displayName: string * bio: string * photo: string option
321391
322392 let private serializeEnvelope = function
323393 | TextMessage(id, body) ->
324394 JsonSerializer.Serialize({| ``type`` = "text"; id = id; body = body |})
325395 | DeliveryAck(id, ackIds) ->
326396 JsonSerializer.Serialize({| ``type`` = "delivery.ack"; id = id; ack_ids = ackIds |})
397+ | ProfileMessage(id, displayName, bio, photo) ->
398+ JsonSerializer.Serialize({| ``type`` = "profile"; id = id; timestamp = DateTimeOffset.UtcNow.ToUnixTimeSeconds(); display_name = displayName; bio = bio; photo = (photo |> Option.defaultValue null) |})
327399
328400 let private parseEnvelope (json: string) =
329401 use doc = JsonDocument.Parse(json)
@@ -334,6 +406,17 @@ module App =
334406 | "delivery.ack" ->
335407 let ackIds = root.GetProperty("ack_ids").EnumerateArray() |> Seq.map _.GetString() |> Seq.toList
336408 Some(DeliveryAck(root.GetProperty("id").GetString(), ackIds))
409+ | "profile" ->
410+ let displayName = root.GetProperty("display_name").GetString()
411+ let bio =
412+ match root.TryGetProperty("bio") with
413+ | true, el when el.ValueKind <> JsonValueKind.Null -> el.GetString()
414+ | _ -> ""
415+ let photo =
416+ match root.TryGetProperty("photo") with
417+ | true, el when el.ValueKind <> JsonValueKind.Null -> Some(el.GetString())
418+ | _ -> None
419+ Some(ProfileMessage(root.GetProperty("id").GetString(), displayName, bio, photo))
337420 | _ -> None
338421
339422 let private decryptEvent (privKey: byte[]) (payload: EventPayload) =
@@ -353,6 +436,8 @@ module App =
353436 Status = DeliveryStatus.Delivered } })
354437 | Some(DeliveryAck(_, ackIds)) ->
355438 Ok(IncomingDeliveryAck(senderHex, ackIds))
439+ | Some(ProfileMessage(_, displayName, bio, photo)) ->
440+ Ok(IncomingProfile(senderHex, displayName, bio, photo))
356441 | None -> Error "unknown envelope type"
357442 | None -> Error "decrypt failed"
358443 with ex ->
@@ -445,10 +530,11 @@ module App =
445530 asyncCmd (async {
446531 match! Store.loadIdentity () with
447532 | Some id ->
533+ let profile = Store.loadProfile ()
448534 return StateLoaded(
449535 match Store.loadData () with
450− | Some data -> IdentityAndData(id, data)
451− | None -> IdentityOnly id)
536+ | Some data -> IdentityAndData(id, data, profile)
537+ | None -> IdentityOnly(id, profile))
452538 | None -> return StateLoaded NothingSaved
453539 })
454540
@@ -458,6 +544,36 @@ module App =
458544 | CmdSaveData data ->
459545 Cmd.ofEffect (fun _ -> Store.saveData data)
460546
547+ | CmdPickPhoto ->
548+ asyncCmd (async {
549+ try
550+ let! results = Microsoft.Maui.Media.MediaPicker.Default.PickPhotosAsync() |> Async.AwaitTask
551+ let file = results |> Seq.tryHead
552+ match file with
553+ | None -> return PhotoPicked None
554+ | Some file ->
555+ use! stream = file.OpenReadAsync() |> Async.AwaitTask
556+ use ms = new MemoryStream()
557+ do! stream.CopyToAsync(ms) |> Async.AwaitTask
558+ return PhotoPicked(Some(Convert.ToBase64String(ms.ToArray())))
559+ with _ ->
560+ return PhotoPicked None
561+ })
562+
563+ | CmdSaveProfile profile ->
564+ Cmd.ofEffect (fun _ -> Store.saveProfile profile)
565+
566+ | CmdSendProfileToAll(session, profile, recipientHexes) ->
567+ Cmd.ofEffect (fun _ ->
568+ async {
569+ for recipientHex in recipientHexes do
570+ try
571+ let payload = serializeEnvelope (ProfileMessage(Guid.NewGuid().ToString(), profile.DisplayName, profile.Bio, profile.PhotoBase64))
572+ let blob = Crypto.encrypt session.Identity.PrivKey (Crypto.fromHex recipientHex) payload
573+ do! sendMessage session.Url session.Token recipientHex (Crypto.toHex blob) (DateTimeOffset.UtcNow.ToUnixTimeSeconds()) |> Async.Ignore
574+ with _ -> ()
575+ } |> Async.Start)
576+
461577 // ── View ──
462578
463579 let private formatPollStatus = function
@@ -512,6 +628,21 @@ module App =
512628
513629 Button("Copy Public Key", CopyPubKey)
514630
631+ Label("Profile:").font(size = 18.)
632+ match model.Profile with
633+ | Some profile when not (String.IsNullOrEmpty profile.DisplayName) ->
634+ Label($"Name: {profile.DisplayName}").font(size = 14.)
635+ | _ -> ()
636+ match model.Profile with
637+ | Some profile when not (String.IsNullOrEmpty profile.Bio) ->
638+ Label($"Bio: {profile.Bio}").font(size = 14.).textColor(Colors.DimGray)
639+ | _ -> ()
640+ let profilePage =
641+ match model.Profile with
642+ | Some p -> EditProfile(p.DisplayName, p.Bio, p.PhotoBase64)
643+ | None -> EditProfile("", "", None)
644+ Button("Edit Profile", SetPage profilePage)
645+
515646 Label("Server:")
516647
517648 match conn with
@@ -630,6 +761,45 @@ module App =
630761 .padding(20.)
631762 )
632763
764+ let private viewEditProfile model displayName bio photo =
765+ ContentPage(
766+ ScrollView(
767+ (VStack(spacing = 16.) {
768+ HStack(spacing = 8.) {
769+ Button("< Back", SetPage Settings)
770+ Label("Edit Profile").font(size = 20.)
771+ }
772+
773+ viewErrorBanner model.Error
774+
775+ match photo with
776+ | Some p when not (String.IsNullOrEmpty p) ->
777+ Image(new MemoryStream(Convert.FromBase64String(p)) :> Stream)
778+ .width(120.)
779+ .height(120.)
780+ .centerHorizontal()
781+ | _ ->
782+ Label("No Photo")
783+ .font(size = 16.)
784+ .centerTextHorizontal()
785+ .textColor(Colors.Gray)
786+
787+ Button("Change Photo", DoPickPhoto).centerHorizontal()
788+
789+ Label("Display Name:")
790+ Entry(displayName, SetProfileDisplayName)
791+ .placeholder("Your name")
792+
793+ Label("Bio:")
794+ Entry(bio, SetProfileBio)
795+ .placeholder("Tell something about yourself")
796+
797+ Button("Save Profile", DoSaveProfile).centerHorizontal()
798+ })
799+ .padding(20.)
800+ )
801+ )
802+
633803 let view model =
634804 let page =
635805 match model.Page with
@@ -638,6 +808,7 @@ module App =
638808 | Conversations -> viewConversations model
639809 | Chat(pk, compose) -> viewChat model pk compose
640810 | AddContact(pk, nn) -> viewAddContact model pk nn
811+ | EditProfile(displayName, bio, photo) -> viewEditProfile model displayName bio photo
641812
642813 Application() { Window(page) }
643814
client/Store.fs+38−1
@@ -10,7 +10,15 @@ module Store =
1010 [<CLIMutable>]
1111 type Contact =
1212 { [<BsonId>] Pubkey: string
13− Nickname: string }
13+ Nickname: string
14+ DisplayName: string
15+ Bio: string
16+ PhotoBase64: string }
17+
18+ type Profile =
19+ { DisplayName: string
20+ Bio: string
21+ PhotoBase64: string option }
1422
1523 [<RequireQualifiedAccess>]
1624 type DeliveryStatus = Sent | Delivered
@@ -43,6 +51,13 @@ module Store =
4351 ServerUrl: string
4452 PollCursor: int64 }
4553
54+ [<CLIMutable>]
55+ type private ProfileDoc =
56+ { [<BsonId>] Id: string
57+ DisplayName: string
58+ Bio: string
59+ PhotoBase64: string }
60+
4661 let private toChatMessage (d: MessageDoc) : ChatMessage =
4762 { Id = d.Id; Body = d.Body; Timestamp = DateTimeOffset.FromUnixTimeSeconds d.TimestampUnix; IsOutgoing = d.IsOutgoing
4863 Status = match d.Status with 0 -> DeliveryStatus.Sent | 1 -> DeliveryStatus.Delivered | n -> failwith $"Unknown delivery status: {n}" }
@@ -70,6 +85,28 @@ module Store =
7085 SecureStorage.Default.SetAsync("identity_privkey", Convert.ToBase64String identity.PrivKey)
7186 |> Async.AwaitTask
7287
88+ let loadProfile () : Profile option =
89+ try
90+ use db = openDb ()
91+ db.GetCollection<ProfileDoc>("profile").FindById(BsonValue "me")
92+ |> Option.ofObj
93+ |> 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 })
97+ with _ -> None
98+
99+ let saveProfile (profile: Profile) =
100+ try
101+ use db = openDb ()
102+ db.GetCollection<ProfileDoc>("profile").Upsert
103+ { Id = "me"
104+ DisplayName = profile.DisplayName
105+ Bio = profile.Bio
106+ PhotoBase64 = profile.PhotoBase64 |> Option.defaultValue null }
107+ |> ignore
108+ with _ -> ()
109+
73110 let loadData () : Data option =
74111 try
75112 use db = openDb ()
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− ZSBquSe7af7XpQvv6oqXWEK9H74=
169+ u3/Jli23brn1acnURBDMHQ032EY=
170170 </data>
171171 <key>Skrepka.pdb</key>
172172 <data>
173− VivAVxtEMi2FHBPFF09uQi1418U=
173+ m9G0IzQJO1xJObAyLl6iRTGfI4Q=
174174 </data>
175175 <key>Sodium.Core.dll</key>
176176 <data>
@@ -1393,14 +1393,14 @@
13931393 <dict>
13941394 <key>hash2</key>
13951395 <data>
1396− Bd6/5/MU6L0SMz/GVfv2R6VSAXUbiQJI32j7+lnsJ4w=
1396+ N4S21qMsbcdZoPSka8wm/OYfeytcN6be2i+NeNshbkM=
13971397 </data>
13981398 </dict>
13991399 <key>Skrepka.pdb</key>
14001400 <dict>
14011401 <key>hash2</key>
14021402 <data>
1403− YuSpnbETdRksRCmKP9GMRFzwQi1n0Oc1JqlXdMOZiyE=
1403+ CZBydqM3QuDS2CPbFfJRWp0ybjMZr2cKEF/e4NllpY4=
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− F2ovogGs5gdnmuhl7I35TXBYBHvkWSc5l3Eb3dz5/Eo=
254+ bun2DyiQNygihIIRw+/9Fz9gKTvkJ8OKN160J0nU8T4=
255255 </data>
256256 </dict>
257257 <key>MonoBundle/Skrepka.pdb</key>
258258 <dict>
259259 <key>hash2</key>
260260 <data>
261− LL4NPXSPKwyqo6TCZ4Xmt3aHdsBLcNeTGcuqqNI6GIA=
261+ o5yVSDk2KiZkgGHYl08aary/xPSlP2KvgKUPeV1GPE0=
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+57cf03bb5cc8c778e01a2c8e1a8da7ec32e919da")>]
13+[<assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0+fec9263421fbac32ef5f434448cbad4cca707ffa")>]
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−07f51a22ce67924703904ac63354f57452f4a0f68bfe8fef4528a96892e114a1
1+14d65e38515f6fe609282787e3a83911d7f49a251ce309da041cf4a647d7c78c
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/57cf03bb5cc8c778e01a2c8e1a8da7ec32e919da/*"}}
1+{"documents":{"/Users/iko/Developer/skrepka/*":"https://raw.githubusercontent.com/ilyakooo0/skrepka/fec9263421fbac32ef5f434448cbad4cca707ffa/*"}}
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+57cf03bb5cc8c778e01a2c8e1a8da7ec32e919da")>]
13+[<assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0+fec9263421fbac32ef5f434448cbad4cca707ffa")>]
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−034a8eb90add363ed97907a65fdd8257ea50d7e41bd6c1fea1e566215afb5a16
1+ffbafc0ee7b3b70864bc0f4e57e320aaf92479c321cb705524064b7744b15e7c
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/57cf03bb5cc8c778e01a2c8e1a8da7ec32e919da/*"}}
1+{"documents":{"/Users/iko/Developer/skrepka/*":"https://raw.githubusercontent.com/ilyakooo0/skrepka/fec9263421fbac32ef5f434448cbad4cca707ffa/*"}}
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+48−49
@@ -67,20 +67,21 @@ impl FromJSON GossipEventType where
6767
6868isGossipOnline = \et -> case et of GossipOnline {} -> True {}; _ -> False {}
6969isGossipOffline = \et -> case et of GossipOffline {} -> True {}; _ -> False {}
70+isGossipUnknown = \et -> case et of GossipUnknown {} -> True {}; _ -> False {}
7071
7172type GossipEvent = {eventType: GossipEventType, pubkey: PubkeyHex, server: ServerName, timestamp: Timestamp, ttl: Int<S>}
7273
7374type ValidGossipEntry = {eventType: GossipEventType, pubkey: PubkeyHex, server: ServerName, expiresAt: Timestamp, timestamp: Timestamp, ttl: Int<S>}
7475
75−type MessageStatus = MsUnauthorized {} | MsRejected {} | MsDelivered {} | MsFederated {} | MsQueued {}
76+type MessageStatus = MsDelivered {} | MsFederated {} | MsQueued {}
77+
78+messageStatusText = \ms -> case ms of
79+ MsDelivered {} -> "delivered"
80+ MsFederated {} -> "federated"
81+ MsQueued {} -> "queued"
7682
7783impl ToJSON MessageStatus where
78− toJson ms = toJson (case ms of
79− MsUnauthorized {} -> "unauthorized"
80− MsRejected {} -> "rejected"
81− MsDelivered {} -> "delivered"
82− MsFederated {} -> "federated"
83− MsQueued {} -> "queued")
84+ toJson ms = toJson (messageStatusText ms)
8485
8586type PubkeyHex = Text where \s -> isValidHex s && not (tooLong s maxPubkeyLen) && s == normalizeHex s
8687type BlobHex = Text where \s -> isValidHex s && not (tooLong s maxBlobLen) && s == normalizeHex s
@@ -90,14 +91,14 @@ type MessageIdHex = Text where isValidMessageId
9091type OptionalMessageId = Text where \s -> s == "" || isValidMessageId s
9192isValidServerName = \s -> not (tooLong s maxServerNameLen) && s == normalizeServerName s && not (isBadServerName s)
9293type ServerName = Text where isValidServerName
93−type OptionalServerName = Text where \s -> s == "" || isValidServerName s
94+type MessageOrigin = LocalOrigin {} | FederatedOrigin {server: ServerName}
9495type AnyServerName = Text where \s -> not (tooLong s maxServerNameLen) && s == normalizeServerName s
9596
9697type Timestamp = Int<Ms>
9798
9899type Challenge = {pubkey: PubkeyHex, challenge: Text, expiresAt: Timestamp}
99100type Session = {token: Text, pubkey: PubkeyHex, expiresAt: Timestamp}
100−type Message = {messageId: MessageIdHex, toKey: PubkeyHex, fromKey: PubkeyHex, encryptedBlob: BlobHex, timestamp: Timestamp, originServer: OptionalServerName, receivedAt: Timestamp}
101+type Message = {messageId: MessageIdHex, toKey: PubkeyHex, fromKey: PubkeyHex, encryptedBlob: BlobHex, timestamp: Timestamp, origin: MessageOrigin, receivedAt: Timestamp}
101102type PresenceEntry = {pubkey: PubkeyHex, server: AnyServerName, expiresAt: Timestamp, timestamp: Timestamp}
102103type ForwardEntry = {messageId: MessageIdHex, toServer: ServerName, createdAt: Timestamp, toKey: PubkeyHex, fromKey: PubkeyHex, encryptedBlob: BlobHex, timestamp: Timestamp}
103104type AckedEntry = {messageId: MessageIdHex, toKey: PubkeyHex, ackedAt: Timestamp, originServer: ServerName}
@@ -149,13 +150,13 @@ route ClientApi where
149150 = Poll
150151 /messages
151152 POST {to: PubkeyHex, encryptedBlob: BlobHex, timestamp: Timestamp} / headers {authorization: Text}
152− -> {status: MessageStatus, messageId: OptionalMessageId}
153+ -> {status: Text, messageId: OptionalMessageId}
153154 = SendMessage
154155 POST {messageIds: [MessageIdHex]} /ack headers {authorization: Text}
155156 -> {ok: Bool}
156157 = AckMessages
157158 GET /lookup/{pubkey: PubkeyHex} headers {authorization: Text}
158− -> {online: Bool, server: AnyServerName, authorized: Bool}
159+ -> {server: AnyServerName, authorized: Bool}
159160 = LookupPresence
160161
161162route FederationApi where
@@ -385,7 +386,8 @@ clampTimestamp = \timestamp t -> clamp (t - messageMaxAge) t timestamp
385386
386387nextSeqTs = \t seqCounter -> if t > seqCounter then t else seqCounter + 1
387388
388−isFederatedOrigin = \originServer -> originServer != "" && originServer != serverHostN
389+isFederatedOrigin = \origin -> case origin of FederatedOrigin _ -> True {}; _ -> False {}
390+originServerName = \origin -> case origin of FederatedOrigin {server} -> server; LocalOrigin {} -> ""
389391
390392takeN = \items n -> (fold (\acc e -> if acc.n >= n then acc else {n: acc.n + 1, items: union acc.items [e]}) {n: 0, items: []} items).items
391393
@@ -493,19 +495,19 @@ removeOffline = \presenceSet trustedOffline t ->
493495 where samePubkeyServer p o && min o.timestamp t > p.timestamp
494496 yield p)
495497
496−capGossipPresence = \afterOffline appliedOnline trustedDeduped presence trustedOffline t ->
497− if count afterOffline <= maxPresenceEntries
498− then {finalPresence: afterOffline, appliedOnline: appliedOnline, withinCapacity: True {}}
498+capGossipPresence = \args ->
499+ if count args.afterOffline <= maxPresenceEntries
500+ then {finalPresence: args.afterOffline, appliedOnline: args.appliedOnline, withinCapacity: True {}}
499501 else
500− let updatesOnly = do e <- trustedDeduped; where any presence (\p -> samePubkeyServer p e); yield e in
501− let updatesOnlyMerged = mergeByPubkeyServer presence updatesOnly in
502− let updatesOnlyAfterOffline = removeOffline updatesOnlyMerged trustedOffline t in
502+ let updatesOnly = do e <- args.trustedDeduped; where any args.presence (\p -> samePubkeyServer p e); yield e in
503+ let updatesOnlyMerged = mergeByPubkeyServer args.presence updatesOnly in
504+ let updatesOnlyAfterOffline = removeOffline updatesOnlyMerged args.trustedOffline args.t in
503505 let reclaimSlots = maxPresenceEntries - count updatesOnlyAfterOffline in
504− let newEntries = do e <- trustedDeduped; where none presence (\p -> samePubkeyServer p e); yield e in
506+ let newEntries = do e <- args.trustedDeduped; where none args.presence (\p -> samePubkeyServer p e); yield e in
505507 let reclaimedEntries = takeN newEntries reclaimSlots in
506508 {finalPresence: union updatesOnlyAfterOffline reclaimedEntries,
507509 appliedOnline: union
508− (do e <- updatesOnly; where none presence (\p -> samePubkeyServer p e && p.expiresAt >= e.expiresAt); yield (pubkeyServer e))
510+ (do e <- updatesOnly; where none args.presence (\p -> samePubkeyServer p e && p.expiresAt >= e.expiresAt); yield (pubkeyServer e))
509511 (do e <- reclaimedEntries; yield (pubkeyServer e)),
510512 withinCapacity: False {}}
511513
@@ -516,6 +518,7 @@ countMessagesFor = \messages toKey t ->
516518validGossipEvents : [GossipEvent] -> Timestamp -> [ValidGossipEntry]
517519validGossipEvents = \events t -> do
518520 e <- events
521+ where not (isGossipUnknown e.eventType)
519522 where e.server != serverHostN && e.timestamp <= t + clockSkewTolerance
520523 let ttl = clampTtl e.ttl
521524 let expiresAt = e.timestamp + ttl * 1000
@@ -608,6 +611,10 @@ broadcastOfflineGossip = \t peerServers expiredLocal ->
608611forkBroadcastOffline = \t result ->
609612 fork (broadcastOfflineGossip t result.peerServers result.expiredLocal)
610613
614+rejectFederation = \t respond -> do
615+ maybePruneAndBroadcast t
616+ yield (respond {ok: False {}})
617+
611618maybePruneAndBroadcast = \t -> do
612619 pruneResult <- atomic do maybePrune t
613620 forkBroadcastOffline t pruneResult
@@ -693,7 +700,7 @@ pruneAndRetry = \t -> do
693700 pr <- pruneExpired t
694701 forwards <- *forwards
695702 ackedIds <- *ackedIds
696− let pendingAcks = do a <- ackedIds; where isFederatedOrigin a.originServer; yield a
703+ let pendingAcks = do a <- ackedIds; where a.originServer != serverHostN; yield a
697704 yield {expiredLocal: pr.expiredLocal, peerServers: pr.peerServers, pendingForwards: forwards, pendingAcks: pendingAcks}
698705 broadcastOfflineGossip t result.peerServers result.expiredLocal
699706 when federationEnabled (do
@@ -835,7 +842,7 @@ serve = \req -> case req of
835842 set *seqCounter = seqTs
836843 set *messages = union messages
837844 [{messageId: msgId, toKey: to, fromKey: fromKey,
838− encryptedBlob: encryptedBlob, timestamp: clampedTs, originServer: "", receivedAt: seqTs}]
845+ encryptedBlob: encryptedBlob, timestamp: clampedTs, origin: LocalOrigin {}, receivedAt: seqTs}]
839846 let toPresence = do p <- presence; where p.pubkey == to && p.expiresAt > t; yield p
840847 let localOnline = any toPresence (\p -> p.server == serverHostN)
841848 let remoteServers = do p <- toPresence; where p.server != serverHostN; yield p.server
@@ -865,9 +872,9 @@ serve = \req -> case req of
865872 _ -> yield {}
866873 broadcastOfflineGossip t result.pr.peerServers result.pr.expiredLocal)
867874 yield (case result.send of
868− SendUnauthorized {} -> respond {status: MsUnauthorized {}, messageId: ""}
869− SendRejected {} -> respond {status: MsRejected {}, messageId: ""}
870− SendDone {status} -> respond {status: status, messageId: msgId})
875+ SendUnauthorized {} -> respond {status: "unauthorized", messageId: ""}
876+ SendRejected {} -> respond {status: "rejected", messageId: ""}
877+ SendDone {status} -> respond {status: messageStatusText status, messageId: msgId})
871878
872879 AckMessages {messageIds, authorization, respond} -> do
873880 t <- now
@@ -887,26 +894,24 @@ serve = \req -> case req of
887894 set *messages = diff messages ackedMessages
888895 let ackedFederated = do
889896 m <- ackedMessages
890− where isFederatedOrigin m.originServer
891− yield {messageId: m.messageId, toKey: m.toKey, ackedAt: t, originServer: m.originServer}
897+ where isFederatedOrigin m.origin
898+ yield {messageId: m.messageId, toKey: m.toKey, ackedAt: t, originServer: originServerName m.origin}
892899 ackedIds <- *ackedIds
893900 let combinedAckedIds = union ackedIds ackedFederated
894901 let evictedEntries = let excessCount = count combinedAckedIds - maxAckedIds in
895902 if excessCount <= 0 then [] else boundedSmallest combinedAckedIds excessCount (\a -> a.ackedAt)
896903 set *ackedIds = diff combinedAckedIds evictedEntries
897904 yield {pr: pr, ack: AckDone {pendingAcks: ackedFederated, evictedEntries: evictedEntries}}
898− case result.ack of
899− AckDone {pendingAcks, evictedEntries} -> do
900− fork (do
905+ fork (do
906+ case result.ack of
907+ AckDone {pendingAcks, evictedEntries} ->
901908 when federationEnabled (do
902909 let originServers = collectPeerServers pendingAcks (\a -> a.originServer)
903910 sendAcksAndClean pendingAcks originServers
904911 retryEvictedAcks evictedEntries pendingAcks)
905− broadcastOfflineGossip t result.pr.peerServers result.pr.expiredLocal)
906− yield (respond {ok: True {}})
907− AckUnauthorized {} -> do
908− forkBroadcastOffline t result.pr
909− yield (respond {ok: False {}})
912+ _ -> yield {}
913+ broadcastOfflineGossip t result.pr.peerServers result.pr.expiredLocal)
914+ yield (respond {ok: case result.ack of AckDone _ -> True {}; _ -> False {}})
910915
911916 LookupPresence {pubkey, authorization, respond} -> do
912917 t <- now
@@ -929,17 +934,15 @@ serve = \req -> case req of
929934 yield {pr: pr, lookup: LookupDone {server: foundServer}}
930935 forkBroadcastOffline t result.pr
931936 yield (case result.lookup of
932− LookupUnauthorized {} -> respond {online: False {}, server: "", authorized: False {}}
933− LookupDone {server} -> respond {online: server != "", server: server, authorized: True {}})
937+ LookupUnauthorized {} -> respond {server: "", authorized: False {}}
938+ LookupDone {server} -> respond {server: server, authorized: True {}})
934939
935940 RecvGossip {events, hopsRemaining, fromServer, authorization, respond} -> do
936941 t <- now
937942 let authorized = verifyFederationAuth authorization
938943 let cappedHops = clamp 0 5 hopsRemaining
939944 if not authorized || cappedHops < 1 || fromServer == serverHostN || count events > maxGossipEvents
940− then do
941− maybePruneAndBroadcast t
942− yield (respond {ok: False {}})
945+ then rejectFederation t respond
943946 else do
944947 let allValidEntries = validGossipEvents events t
945948 let onlineEntries = do e <- allValidEntries; where isGossipOnline e.eventType; yield e
@@ -961,7 +964,7 @@ serve = \req -> case req of
961964 let merged = mergeByPubkeyServer presence trustedDeduped
962965 let afterOffline = removeOffline merged trustedOffline t
963966 let appliedOffline = do p <- diff merged afterOffline; yield (pubkeyServer p)
964− let capResult = capGossipPresence afterOffline appliedOnline trustedDeduped presence trustedOffline t
967+ let capResult = capGossipPresence {afterOffline: afterOffline, appliedOnline: appliedOnline, trustedDeduped: trustedDeduped, presence: presence, trustedOffline: trustedOffline, t: t}
965968 set *presence = capResult.finalPresence
966969 let gossipPeerServers = collectPeerServers capResult.finalPresence (\p -> p.server)
967970 yield {appliedKeys: union capResult.appliedOnline appliedOffline, pr: pr, gossipPeerServers: gossipPeerServers, withinCapacity: capResult.withinCapacity}
@@ -984,11 +987,9 @@ serve = \req -> case req of
984987 ForwardMessage {toKey, fromKey, encryptedBlob, timestamp, originServer, messageId, authorization, respond} -> do
985988 t <- now
986989 let authorized = verifyFederationAuth authorization
987− let validOrigin = isFederatedOrigin originServer
990+ let validOrigin = originServer != serverHostN
988991 if not authorized || not validOrigin
989− then do
990− maybePruneAndBroadcast t
991− yield (respond {ok: False {}})
992+ then rejectFederation t respond
992993 else do
993994 let clampedTs = clampTimestamp timestamp t
994995 result <- atomic do
@@ -1006,7 +1007,7 @@ serve = \req -> case req of
10061007 set *seqCounter = seqTs
10071008 set *messages = union messages
10081009 [{messageId: messageId, toKey: toKey, fromKey: fromKey,
1009− encryptedBlob: encryptedBlob, timestamp: clampedTs, originServer: originServer, receivedAt: seqTs}]
1010+ encryptedBlob: encryptedBlob, timestamp: clampedTs, origin: FederatedOrigin {server: originServer}, receivedAt: seqTs}]
10101011 yield {pr: pr, forward: ForwardStored {}}
10111012 forkBroadcastOffline t result.pr
10121013 yield (respond {ok: case result.forward of ForwardFull {} -> False {}; _ -> True {}})
@@ -1015,9 +1016,7 @@ serve = \req -> case req of
10151016 t <- now
10161017 let authorized = verifyFederationAuth authorization
10171018 if not authorized || originServer != serverHostN || fromServer == serverHostN
1018− then do
1019− maybePruneAndBroadcast t
1020− yield (respond {ok: False {}})
1019+ then rejectFederation t respond
10211020 else do
10221021 let dedupedIds = dedupMessageIds messageIds
10231022 pruneResult <- atomic do