on master help sign in
Simplify code and improve type safety: use EventType and EnvelopeType DUs, simplify array/option patterns, validate pubkey format
commit5a91ff165d267f1b48cd27804ddc67fbfbb1a12b authoriko <mail@iko.soy> dateFri 10 Apr 2026, 15:23 (6 months ago) parentc0ca8aa
5 files changed+59−30
client/ApiClient.fs+17−2 client/App.fs+33−19 client/Crypto.fs+4 client/Phonemic.fs+4−8 client/Store.fs+1−1
client/ApiClient.fs+17−2
@@ -12,8 +12,10 @@ module ApiClient =
1212 [<CLIMutable>]
1313 type EventPayload = { EncryptedBlob: string; Timestamp: int64 }
1414
15+ type EventType = Message | UnknownEvent of string
16+
1517 [<CLIMutable>]
16− type PollEvent = { Id: string; EventType: string; Payload: EventPayload }
18+ type PollEvent = { Id: string; EventType: EventType; Payload: EventPayload }
1719
1820 type MessageStatus = Delivered | Federated | Queued | Rejected | Unauthorized
1921
@@ -58,9 +60,22 @@ module ApiClient =
5860 | Rejected -> "rejected"
5961 | Unauthorized -> "unauthorized")
6062
63+ type private EventTypeConverter() =
64+ inherit JsonConverter<EventType>()
65+ override _.Read(reader, _, _) =
66+ match reader.GetString() with
67+ | "message" -> Message
68+ | s -> UnknownEvent s
69+ override _.Write(writer, value, _) =
70+ writer.WriteStringValue(
71+ match value with
72+ | Message -> "message"
73+ | UnknownEvent s -> s)
74+
6175 let private jsonOpts =
6276 let opts = JsonSerializerOptions(PropertyNameCaseInsensitive = true, NumberHandling = JsonNumberHandling.AllowReadingFromString)
6377 opts.Converters.Add(MessageStatusConverter())
78+ opts.Converters.Add(EventTypeConverter())
6479 opts
6580
6681 let private client =
@@ -114,7 +129,7 @@ module ApiClient =
114129 let body = JsonSerializer.Serialize({| cursor = cursor; timeout = 30000 |})
115130 async {
116131 let! response = postJson<PollResponse> pollClient $"{serverUrl}/poll" body (Some token)
117− return { response with Events = response.Events |> Array.filter (fun e -> e.EventType = "message") }
132+ return { response with Events = response.Events |> Array.filter (fun e -> e.EventType = Message) }
118133 }
119134
120135 let ackMessages (serverUrl: string) (token: string) (messageIds: string list) =
client/App.fs+33−19
@@ -17,12 +17,31 @@ module App =
1717
1818 // ── Domain Types ──
1919
20+ type private EnvelopeType = Text | UnknownEnvelope of string
21+
22+ type private EnvelopeTypeConverter() =
23+ inherit JsonConverter<EnvelopeType>()
24+ override _.Read(reader, _, _) =
25+ match reader.GetString() with
26+ | "text" -> Text
27+ | s -> UnknownEnvelope s
28+ override _.Write(writer, value, _) =
29+ writer.WriteStringValue(
30+ match value with
31+ | Text -> "text"
32+ | UnknownEnvelope s -> s)
33+
2034 [<CLIMutable>]
2135 type private TextEnvelope =
22− { [<JsonPropertyName("type")>] Type: string
36+ { [<JsonPropertyName("type")>] Type: EnvelopeType
2337 [<JsonPropertyName("id")>] Id: string
2438 [<JsonPropertyName("body")>] Body: string }
2539
40+ let private envelopeJsonOpts =
41+ let opts = JsonSerializerOptions()
42+ opts.Converters.Add(EnvelopeTypeConverter())
43+ opts
44+
2645 type Page =
2746 | Setup
2847 | Conversations
@@ -230,22 +249,17 @@ module App =
230249 | DoSaveContact ->
231250 match model.Page with
232251 | AddContact(pk, nn) when pk <> "" && nn <> "" ->
233− let pubkeyHex =
234− if pk.Contains("-") then
235− match Phonemic.fromOb pk with
236− | Some bytes -> Crypto.toHex bytes
237− | None -> pk
238− else
239− pk
240−
241− let contact: Contact = { Pubkey = pubkeyHex; Nickname = nn }
252+ match Phonemic.fromOb pk |> Option.orElseWith (fun () -> Crypto.tryFromHex pk) with
253+ | Some bytes when bytes.Length = 32 ->
254+ let contact: Contact = { Pubkey = Crypto.toHex bytes; Nickname = nn }
242255
243− let model' =
244− { model with
245− Contacts = model.Contacts |> upsertContact contact
246− Page = Conversations }
256+ let model' =
257+ { model with
258+ Contacts = model.Contacts |> upsertContact contact
259+ Page = Conversations }
247260
248− model', [ saveCmdMsg model' ]
261+ model', [ saveCmdMsg model' ]
262+ | _ -> { model with Error = Some "Invalid public key" }, []
249263 | _ -> model, []
250264
251265 | CopyPubKey ->
@@ -287,16 +301,16 @@ module App =
287301
288302 match Crypto.decrypt privKey blob with
289303 | Some(plaintext, senderHex) ->
290− let envelope = JsonSerializer.Deserialize<TextEnvelope>(plaintext)
304+ let envelope = JsonSerializer.Deserialize<TextEnvelope>(plaintext, envelopeJsonOpts)
291305
292306 match envelope.Type with
293− | "text" ->
307+ | Text ->
294308 Ok(senderHex,
295309 { Id = envelope.Id
296310 Body = envelope.Body
297311 Timestamp = DateTimeOffset.FromUnixTimeSeconds(payload.Timestamp)
298312 IsOutgoing = false })
299− | t -> Error $"unknown type: {t}"
313+ | UnknownEnvelope t -> Error $"unknown envelope type: {t}"
300314 | None -> Error "decrypt failed"
301315 with ex ->
302316 Error $"parse: {ex.Message}"
@@ -321,7 +335,7 @@ module App =
321335 try
322336 let recipPub = Crypto.fromHex recipientHex
323337 let ts = DateTimeOffset.UtcNow.ToUnixTimeSeconds()
324− let payload = JsonSerializer.Serialize({ Type = "text"; Id = messageId; Body = text }: TextEnvelope)
338+ let payload = JsonSerializer.Serialize<TextEnvelope>({ Type = Text; Id = messageId; Body = text }, envelopeJsonOpts)
325339 let blob = Crypto.encrypt session.Identity.PrivKey recipPub payload
326340 let! status = sendMessage session.Url session.Token recipientHex (Crypto.toHex blob) ts
327341
client/Crypto.fs+4
@@ -15,6 +15,10 @@ module Crypto =
1515 let fromHex (hex: string) : byte[] =
1616 Convert.FromHexString(hex)
1717
18+ let tryFromHex (hex: string) : byte[] option =
19+ try Some(Convert.FromHexString(hex))
20+ with :? FormatException -> None
21+
1822 let generateIdentity () : Identity =
1923 let kp = PublicKeyAuth.GenerateKeyPair()
2024 { PrivKey = kp.PrivateKey; PubKeyHex = toHex kp.PublicKey }
client/Phonemic.fs+4−8
@@ -101,9 +101,9 @@ module Phonemic =
101101 else
102102 let pairs = s.Split('-') |> Array.filter (fun p -> p <> "")
103103
104− let results =
104+ let decoded =
105105 pairs
106− |> Array.map (fun sp ->
106+ |> Array.choose (fun sp ->
107107 if sp.Length = 6 then
108108 match Map.tryFind sp.[0..2] prefixMap, Map.tryFind sp.[3..5] suffixMap with
109109 | Some hi, Some lo -> Some(hi, lo)
@@ -111,11 +111,7 @@ module Phonemic =
111111 else
112112 None)
113113
114− if results |> Array.forall Option.isSome then
115− results
116− |> Array.collect (fun r ->
117− let hi, lo = r.Value
118− [| hi; lo |])
119− |> Some
114+ if decoded.Length = pairs.Length then
115+ decoded |> Array.collect (fun (hi, lo) -> [| hi; lo |]) |> Some
120116 else
121117 None
client/Store.fs+1−1
@@ -85,7 +85,7 @@ module Store =
8585
8686 { Contacts = contacts
8787 Messages = messages
88− ServerUrl = if String.IsNullOrEmpty settings.ServerUrl then None else Some settings.ServerUrl
88+ ServerUrl = settings.ServerUrl |> Option.ofObj |> Option.filter ((<>) "")
8989 PollCursor = settings.PollCursor })
9090 with _ ->
9191 None