on master help sign in
Simplify code and improve type safety: use EventType and EnvelopeType DUs, simplify array/option patterns, validate pubkey format
5a91ff1iko
client/ApiClient.fs+17−2
@@ -12,8 +12,10 @@ module ApiClient =
12 [<CLIMutable>]
12 [<CLIMutable>]
13 type EventPayload = { EncryptedBlob: string; Timestamp: int64 }
13 type EventPayload = { EncryptedBlob: string; Timestamp: int64 }
14
14
15+ type EventType = Message | UnknownEvent of string
16+
15 [<CLIMutable>]
17 [<CLIMutable>]
16− type PollEvent = { Id: string; EventType: string; Payload: EventPayload }
18+ type PollEvent = { Id: string; EventType: EventType; Payload: EventPayload }
17
19
18 type MessageStatus = Delivered | Federated | Queued | Rejected | Unauthorized
20 type MessageStatus = Delivered | Federated | Queued | Rejected | Unauthorized
19
21
@@ -58,9 +60,22 @@ module ApiClient =
58 | Rejected -> "rejected"
60 | Rejected -> "rejected"
59 | Unauthorized -> "unauthorized")
61 | Unauthorized -> "unauthorized")
60
62
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+
61 let private jsonOpts =
75 let private jsonOpts =
62 let opts = JsonSerializerOptions(PropertyNameCaseInsensitive = true, NumberHandling = JsonNumberHandling.AllowReadingFromString)
76 let opts = JsonSerializerOptions(PropertyNameCaseInsensitive = true, NumberHandling = JsonNumberHandling.AllowReadingFromString)
63 opts.Converters.Add(MessageStatusConverter())
77 opts.Converters.Add(MessageStatusConverter())
78+ opts.Converters.Add(EventTypeConverter())
64 opts
79 opts
65
80
66 let private client =
81 let private client =
@@ -114,7 +129,7 @@ module ApiClient =
114 let body = JsonSerializer.Serialize({| cursor = cursor; timeout = 30000 |})
129 let body = JsonSerializer.Serialize({| cursor = cursor; timeout = 30000 |})
115 async {
130 async {
116 let! response = postJson<PollResponse> pollClient $"{serverUrl}/poll" body (Some token)
131 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) }
118 }
133 }
119
134
120 let ackMessages (serverUrl: string) (token: string) (messageIds: string list) =
135 let ackMessages (serverUrl: string) (token: string) (messageIds: string list) =
client/App.fs+33−19
@@ -17,12 +17,31 @@ module App =
17
17
18 // ── Domain Types ──
18 // ── Domain Types ──
19
19
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+
20 [<CLIMutable>]
34 [<CLIMutable>]
21 type private TextEnvelope =
35 type private TextEnvelope =
22− { [<JsonPropertyName("type")>] Type: string
36+ { [<JsonPropertyName("type")>] Type: EnvelopeType
23 [<JsonPropertyName("id")>] Id: string
37 [<JsonPropertyName("id")>] Id: string
24 [<JsonPropertyName("body")>] Body: string }
38 [<JsonPropertyName("body")>] Body: string }
25
39
40+ let private envelopeJsonOpts =
41+ let opts = JsonSerializerOptions()
42+ opts.Converters.Add(EnvelopeTypeConverter())
43+ opts
44+
26 type Page =
45 type Page =
27 | Setup
46 | Setup
28 | Conversations
47 | Conversations
@@ -230,22 +249,17 @@ module App =
230 | DoSaveContact ->
249 | DoSaveContact ->
231 match model.Page with
250 match model.Page with
232 | AddContact(pk, nn) when pk <> "" && nn <> "" ->
251 | AddContact(pk, nn) when pk <> "" && nn <> "" ->
233− let pubkeyHex =
234− if pk.Contains("-") then
235− match Phonemic.fromOb pk with
252+ match Phonemic.fromOb pk |> Option.orElseWith (fun () -> Crypto.tryFromHex pk) with
236− | Some bytes -> Crypto.toHex bytes
253+ | Some bytes when bytes.Length = 32 ->
237− | None -> pk
238− else
239− pk
240−
241− let contact: Contact = { Pubkey = pubkeyHex; Nickname = nn }
254+ let contact: Contact = { Pubkey = Crypto.toHex bytes; Nickname = nn }
242
255
243− let model' =
256+ let model' =
244− { model with
257+ { model with
245− Contacts = model.Contacts |> upsertContact contact
258+ Contacts = model.Contacts |> upsertContact contact
246− Page = Conversations }
259+ Page = Conversations }
247
260
248− model', [ saveCmdMsg model' ]
261+ model', [ saveCmdMsg model' ]
262+ | _ -> { model with Error = Some "Invalid public key" }, []
249 | _ -> model, []
263 | _ -> model, []
250
264
251 | CopyPubKey ->
265 | CopyPubKey ->
@@ -287,16 +301,16 @@ module App =
287
301
288 match Crypto.decrypt privKey blob with
302 match Crypto.decrypt privKey blob with
289 | Some(plaintext, senderHex) ->
303 | Some(plaintext, senderHex) ->
290− let envelope = JsonSerializer.Deserialize<TextEnvelope>(plaintext)
304+ let envelope = JsonSerializer.Deserialize<TextEnvelope>(plaintext, envelopeJsonOpts)
291
305
292 match envelope.Type with
306 match envelope.Type with
293− | "text" ->
307+ | Text ->
294 Ok(senderHex,
308 Ok(senderHex,
295 { Id = envelope.Id
309 { Id = envelope.Id
296 Body = envelope.Body
310 Body = envelope.Body
297 Timestamp = DateTimeOffset.FromUnixTimeSeconds(payload.Timestamp)
311 Timestamp = DateTimeOffset.FromUnixTimeSeconds(payload.Timestamp)
298 IsOutgoing = false })
312 IsOutgoing = false })
299− | t -> Error $"unknown type: {t}"
313+ | UnknownEnvelope t -> Error $"unknown envelope type: {t}"
300 | None -> Error "decrypt failed"
314 | None -> Error "decrypt failed"
301 with ex ->
315 with ex ->
302 Error $"parse: {ex.Message}"
316 Error $"parse: {ex.Message}"
@@ -321,7 +335,7 @@ module App =
321 try
335 try
322 let recipPub = Crypto.fromHex recipientHex
336 let recipPub = Crypto.fromHex recipientHex
323 let ts = DateTimeOffset.UtcNow.ToUnixTimeSeconds()
337 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)
325 let blob = Crypto.encrypt session.Identity.PrivKey recipPub payload
339 let blob = Crypto.encrypt session.Identity.PrivKey recipPub payload
326 let! status = sendMessage session.Url session.Token recipientHex (Crypto.toHex blob) ts
340 let! status = sendMessage session.Url session.Token recipientHex (Crypto.toHex blob) ts
327
341
client/Crypto.fs+4
@@ -15,6 +15,10 @@ module Crypto =
15 let fromHex (hex: string) : byte[] =
15 let fromHex (hex: string) : byte[] =
16 Convert.FromHexString(hex)
16 Convert.FromHexString(hex)
17
17
18+ let tryFromHex (hex: string) : byte[] option =
19+ try Some(Convert.FromHexString(hex))
20+ with :? FormatException -> None
21+
18 let generateIdentity () : Identity =
22 let generateIdentity () : Identity =
19 let kp = PublicKeyAuth.GenerateKeyPair()
23 let kp = PublicKeyAuth.GenerateKeyPair()
20 { PrivKey = kp.PrivateKey; PubKeyHex = toHex kp.PublicKey }
24 { PrivKey = kp.PrivateKey; PubKeyHex = toHex kp.PublicKey }
client/Phonemic.fs+4−8
@@ -101,9 +101,9 @@ module Phonemic =
101 else
101 else
102 let pairs = s.Split('-') |> Array.filter (fun p -> p <> "")
102 let pairs = s.Split('-') |> Array.filter (fun p -> p <> "")
103
103
104− let results =
104+ let decoded =
105 pairs
105 pairs
106− |> Array.map (fun sp ->
106+ |> Array.choose (fun sp ->
107 if sp.Length = 6 then
107 if sp.Length = 6 then
108 match Map.tryFind sp.[0..2] prefixMap, Map.tryFind sp.[3..5] suffixMap with
108 match Map.tryFind sp.[0..2] prefixMap, Map.tryFind sp.[3..5] suffixMap with
109 | Some hi, Some lo -> Some(hi, lo)
109 | Some hi, Some lo -> Some(hi, lo)
@@ -111,11 +111,7 @@ module Phonemic =
111 else
111 else
112 None)
112 None)
113
113
114− if results |> Array.forall Option.isSome then
114+ if decoded.Length = pairs.Length then
115− results
116− |> Array.collect (fun r ->
115+ decoded |> Array.collect (fun (hi, lo) -> [| hi; lo |]) |> Some
117− let hi, lo = r.Value
118− [| hi; lo |])
119− |> Some
120 else
116 else
121 None
117 None
client/Store.fs+1−1
@@ -85,7 +85,7 @@ module Store =
85
85
86 { Contacts = contacts
86 { Contacts = contacts
87 Messages = messages
87 Messages = messages
88− ServerUrl = if String.IsNullOrEmpty settings.ServerUrl then None else Some settings.ServerUrl
88+ ServerUrl = settings.ServerUrl |> Option.ofObj |> Option.filter ((<>) "")
89 PollCursor = settings.PollCursor })
89 PollCursor = settings.PollCursor })
90 with _ ->
90 with _ ->
91 None
91 None