on master help sign in
Simplify server.knot and improve type safety
commite4f77cce0bb7dbd14bf95a9270ed5c0ef7b378b4 authoriko <mail@iko.soy> dateMon 13 Apr 2026, 09:31 (6 months ago) parenta14779f
10 files changed+195−58
client/App.fs+10−3 client/Program.fs → client/Platform/Desktop/Program.fs client/Platform/iOS/AppDelegate.fs+26 client/Platform/iOS/Entitlements.plist+6 client/Platform/iOS/Info.plist+39 client/Platform/iOS/Resources/LaunchScreen.xib+32 client/Skrepka.fsproj+39−3 client/run-ios.sh+1−1 client/run-mac.sh+1−2 server.knot+41−49
client/App.fs+10−3
@@ -883,8 +883,8 @@ module App =
883883 .margin(20.)
884884 )
885885
886− let view model =
887− let content =
886+ let private pageContent model =
887+ let page =
888888 match model.Page with
889889 | Setup -> AnyView(viewSetup ())
890890 | Settings -> AnyView(viewSettings model)
@@ -893,12 +893,19 @@ module App =
893893 | AddContact(pk, nn) -> AnyView(viewAddContact model pk nn)
894894 | EditProfile(displayName, bio, photo) -> AnyView(viewEditProfile model displayName bio photo)
895895
896+ Border(page).background(SolidColorBrush(Colors.White))
897+
898+ let view model =
899+#if MOBILE
900+ SingleViewApplication(pageContent model)
901+#else
896902 DesktopApplication() {
897− Window(content)
903+ Window(pageContent model)
898904 .title("Skrepka")
899905 .width(450.)
900906 .height(750.)
901907 }
908+#endif
902909
903910 let program = Program.statefulWithCmdMsg init update mapCmd |> Program.withView view
904911
client/Program.fs → client/Platform/Desktop/Program.fsrenamed

The content is the same.

client/Platform/iOS/AppDelegate.fsnew file+26
@@ -0,0 +1,26 @@
1+namespace Skrepka.iOS
2+
3+open Foundation
4+open UIKit
5+open Avalonia
6+open Avalonia.iOS
7+open Fabulous.Avalonia
8+open Skrepka
9+
10+[<Register(nameof AppDelegate)>]
11+type AppDelegate() =
12+ inherit AvaloniaAppDelegate<FabApplication>()
13+ override this.CreateAppBuilder() = App.create().UseiOS()
14+
15+ [<Export("application:didFinishLaunchingWithOptions:")>]
16+ member this.FinishedLaunching(app: UIApplication, options: NSDictionary) : bool =
17+ let result = base.FinishedLaunching(app, options)
18+ if this.Window <> null then
19+ this.Window.BackgroundColor <- UIColor.White
20+ result
21+
22+module Main =
23+ [<EntryPoint>]
24+ let main (args: string array) =
25+ UIApplication.Main(args, null, typeof<AppDelegate>)
26+ 0
client/Platform/iOS/Entitlements.plistnew file+6
@@ -0,0 +1,6 @@
1+<?xml version="1.0" encoding="UTF-8"?>
2+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
3+<plist version="1.0">
4+<dict>
5+</dict>
6+</plist>
client/Platform/iOS/Info.plistnew file+39
@@ -0,0 +1,39 @@
1+<?xml version="1.0" encoding="UTF-8"?>
2+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
3+<plist version="1.0">
4+<dict>
5+ <key>CFBundleDisplayName</key>
6+ <string>Skrepka</string>
7+ <key>CFBundleIdentifier</key>
8+ <string>com.skrepka.client</string>
9+ <key>CFBundleShortVersionString</key>
10+ <string>1.0</string>
11+ <key>CFBundleVersion</key>
12+ <string>1.0</string>
13+ <key>LSRequiresIPhoneOS</key>
14+ <true/>
15+ <key>MinimumOSVersion</key>
16+ <string>15.0</string>
17+ <key>UIDeviceFamily</key>
18+ <array>
19+ <integer>1</integer>
20+ <integer>2</integer>
21+ </array>
22+ <key>UILaunchStoryboardName</key>
23+ <string>LaunchScreen</string>
24+ <key>UIRequiredDeviceCapabilities</key>
25+ <array>
26+ <string>armv7</string>
27+ </array>
28+ <key>UISupportedInterfaceOrientations</key>
29+ <array>
30+ <string>UIInterfaceOrientationPortrait</string>
31+ <string>UIInterfaceOrientationLandscapeLeft</string>
32+ <string>UIInterfaceOrientationLandscapeRight</string>
33+ </array>
34+ <key>UIStatusBarHidden</key>
35+ <true/>
36+ <key>UIViewControllerBasedStatusBarAppearance</key>
37+ <false/>
38+</dict>
39+</plist>
client/Platform/iOS/Resources/LaunchScreen.xibnew file+32
@@ -0,0 +1,32 @@
1+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
2+<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="6214" systemVersion="14A314h" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" useTraitCollections="YES">
3+ <dependencies>
4+ <plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="6207" />
5+ <capability name="Constraints with non-1.0 multipliers" minToolsVersion="5.1" />
6+ </dependencies>
7+ <objects>
8+ <placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner" />
9+ <placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder" />
10+ <view contentMode="scaleToFill" id="iN0-l3-epB">
11+ <rect key="frame" x="0.0" y="0.0" width="480" height="480" />
12+ <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES" />
13+ <subviews>
14+ <label opaque="NO" clipsSubviews="YES" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Skrepka" textAlignment="center" lineBreakMode="middleTruncation" baselineAdjustment="alignBaselines" minimumFontSize="18" translatesAutoresizingMaskIntoConstraints="NO" id="kId-c2-rCX">
15+ <rect key="frame" x="20" y="140" width="441" height="43" />
16+ <fontDescription key="fontDescription" type="boldSystem" pointSize="36" />
17+ <color key="textColor" cocoaTouchSystemColor="darkTextColor" />
18+ <nil key="highlightedColor" />
19+ </label>
20+ </subviews>
21+ <color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="calibratedWhite" />
22+ <constraints>
23+ <constraint firstItem="kId-c2-rCX" firstAttribute="centerY" secondItem="iN0-l3-epB" secondAttribute="bottom" multiplier="1/3" constant="1" id="5cJ-9S-tgC" />
24+ <constraint firstAttribute="centerX" secondItem="kId-c2-rCX" secondAttribute="centerX" id="Koa-jz-hwk" />
25+ <constraint firstItem="kId-c2-rCX" firstAttribute="leading" secondItem="iN0-l3-epB" secondAttribute="leading" constant="20" symbolic="YES" id="fvb-Df-36g" />
26+ </constraints>
27+ <nil key="simulatedStatusBarMetrics" />
28+ <freeformSimulatedSizeMetrics key="simulatedDestinationMetrics" />
29+ <point key="canvasLocation" x="548" y="455" />
30+ </view>
31+ </objects>
32+</document>
client/Skrepka.fsproj+39−3
@@ -2,9 +2,27 @@
22
33 <PropertyGroup>
44 <OutputType>Exe</OutputType>
5− <TargetFramework>net10.0</TargetFramework>
5+ <TargetFrameworks>net10.0</TargetFrameworks>
6+ <TargetFrameworks Condition="$([MSBuild]::IsOSPlatform('osx'))">$(TargetFrameworks);net10.0-ios</TargetFrameworks>
67 <RootNamespace>Skrepka</RootNamespace>
78 <DisableImplicitFSharpCoreReference>true</DisableImplicitFSharpCoreReference>
9+ <NoWarn>MT7091;NETSDK1206</NoWarn>
10+ </PropertyGroup>
11+
12+ <PropertyGroup>
13+ <AvaloniaPlatform>$([MSBuild]::GetTargetPlatformIdentifier($(TargetFramework)))</AvaloniaPlatform>
14+ <AvaloniaPlatform Condition="'$(AvaloniaPlatform)' == ''">Desktop</AvaloniaPlatform>
15+ <AvaloniaPlatform Condition="'$(AvaloniaPlatform)' == 'ios'">iOS</AvaloniaPlatform>
16+
17+ <iOSPlatformFolder>Platform/iOS/</iOSPlatformFolder>
18+ <DesktopPlatformFolder>Platform/Desktop/</DesktopPlatformFolder>
19+ </PropertyGroup>
20+
21+ <PropertyGroup Condition="'$(AvaloniaPlatform)' == 'iOS'">
22+ <DefineConstants>$(DefineConstants);MOBILE</DefineConstants>
23+ <SupportedOSPlatformVersion>15.0</SupportedOSPlatformVersion>
24+ <RuntimeIdentifier>iossimulator-arm64</RuntimeIdentifier>
25+ <ValidateXcodeVersion>false</ValidateXcodeVersion>
826 </PropertyGroup>
927
1028 <ItemGroup>
@@ -15,7 +33,6 @@
1533 <Compile Include="Store.fs" />
1634 <Compile Include="Buttons.fs" />
1735 <Compile Include="App.fs" />
18− <Compile Include="Program.fs" />
1936 </ItemGroup>
2037
2138 <ItemGroup>
@@ -23,8 +40,27 @@
2340 <AvaloniaResource Include="Assets\Images\*" />
2441 </ItemGroup>
2542
26− <ItemGroup>
43+ <ItemGroup Condition="$(AvaloniaPlatform) == 'Desktop'">
44+ <Compile Include="$(DesktopPlatformFolder)Program.fs" />
2745 <PackageReference Include="Avalonia.Desktop" Version="11.3.0" />
46+ </ItemGroup>
47+
48+ <ItemGroup Condition="$(AvaloniaPlatform) == 'iOS'">
49+ <None Include="$(iOSPlatformFolder)Entitlements.plist" />
50+ <None Include="$(iOSPlatformFolder)Info.plist" LogicalName="Info.plist" />
51+ <InterfaceDefinition Include="$(iOSPlatformFolder)Resources\LaunchScreen.xib" />
52+ <Compile Include="$(iOSPlatformFolder)AppDelegate.fs" />
53+ <PackageReference Include="Avalonia.iOS" Version="11.3.0" />
54+ </ItemGroup>
55+
56+ <ItemGroup Condition="$(AvaloniaPlatform) == 'iOS' And $(RuntimeIdentifier.StartsWith('iossimulator'))">
57+ <NativeReference Include="native/iossimulator-arm64/libsodium.a">
58+ <Kind>Static</Kind>
59+ <ForceLoad>true</ForceLoad>
60+ </NativeReference>
61+ </ItemGroup>
62+
63+ <ItemGroup>
2864 <PackageReference Include="Avalonia.Themes.Fluent" Version="11.3.0" />
2965 <PackageReference Include="FSharp.Core" Version="9.0.100" />
3066 <PackageReference Include="Fabulous" Version="3.0.0-pre22" />
client/run-ios.sh+1−1
@@ -6,7 +6,7 @@ DEVICE="${1:-iPhone 17 Pro}"
66BUNDLE_ID="com.skrepka.client"
77APP="bin/Debug/net10.0-ios/iossimulator-arm64/Skrepka.app"
88
9−dotnet build Skrepka.fsproj -f net10.0-ios -r iossimulator-arm64
9+dotnet build Skrepka.fsproj -f net10.0-ios
1010
1111UDID=$(xcrun simctl list devices available -j | python3 -c "
1212import json, sys
client/run-mac.sh+1−2
@@ -1,5 +1,4 @@
11#!/bin/bash
22set -e
33cd "$(dirname "$0")"
4−dotnet build Skrepka.fsproj -f net10.0-maccatalyst
5−open bin/Debug/net10.0-maccatalyst/maccatalyst-arm64/Skrepka.app
4+dotnet run -f net10.0
server.knot+41−49
@@ -360,18 +360,21 @@ authenticateSession = \sessions token t ->
360360extractToken = \authHeader ->
361361 let token = drop 7 authHeader in
362362 if toLower (take 7 authHeader) == "bearer " && not (tooLong token maxTokenLen)
363− then token
364− else ""
363+ then Just {value: token}
364+ else Nothing {}
365365
366366verifySecret = \token secret ->
367367 secret != "" && safeCompare token secret
368368
369369verifyFederationAuth = \authorization ->
370− verifySecret (extractToken authorization) federationSecret
370+ case extractToken authorization of
371+ Nothing {} -> False {}
372+ Just {value: token} -> verifySecret token federationSecret
371373
372374verifyMaintenanceAuth = \authorization ->
373− let token = extractToken authorization in
374− verifySecret token federationSecret || verifySecret token maintenanceSecret
375+ case extractToken authorization of
376+ Nothing {} -> False {}
377+ Just {value: token} -> verifySecret token federationSecret || verifySecret token maintenanceSecret
375378
376379clampTtl = clamp 1 maxPresenceTtl
377380
@@ -435,17 +438,7 @@ safeCompare = \a b ->
435438 safeCompareLoop a b maxTokenLen 0 + (if a == "" then 1 else 0) + (if b == "" then 1 else 0) == 0
436439
437440evictOneOldest = \items getKey ->
438− let oldest = fold (\acc x -> let k = getKey x in Just {value: case acc of
439− Nothing {} -> k
440− Just {value: v} -> min k v
441− }) Nothing {} items in
442− case oldest of
443− Nothing {} -> items
444− Just {value: oldestKey} ->
445− (fold (\acc x ->
446− if not acc.removed && getKey x == oldestKey
447− then {removed: True {}, items: acc.items}
448− else {removed: acc.removed, items: union acc.items [x]}) {removed: False {}, items: []} items).items
441+ diff items (boundedSmallest items 1 getKey)
449442
450443capPerKey = \items matchKey maxPerKey getExpiry t ->
451444 let active = do x <- items; where getExpiry x > t; yield x in
@@ -638,8 +631,8 @@ rejectFederation = \t respond -> do
638631 maybePruneAndBroadcast t
639632 yield (respond {ok: False {}})
640633
641−withFederationAuth = \authorization t respond extraCheck action ->
642− if not (verifyFederationAuth authorization) || extraCheck
634+withFederationAuth = \authorization t respond precondition action ->
635+ if not (verifyFederationAuth authorization) || not precondition
643636 then rejectFederation t respond
644637 else action
645638
@@ -647,6 +640,15 @@ maybePruneAndBroadcast = \t -> do
647640 pruneResult <- atomic do maybePrune t
648641 forkBroadcastOffline t pruneResult
649642
643+withSessionAuth = \authorization t onUnauth onAuth -> do
644+ sessions <- *sessions
645+ case extractToken authorization of
646+ Nothing {} -> onUnauth
647+ Just {value: token} ->
648+ case authenticateSession sessions token t of
649+ Unauthorized {} -> onUnauth
650+ Authorized {pubkey} -> onAuth pubkey
651+
650652removeForward = \mid srv -> atomic do
651653 forwards <- *forwards
652654 set *forwards = do
@@ -842,8 +844,9 @@ serve = \req -> case req of
842844 t <- now
843845 let clampedTimeout = clamp 0 maxPollTimeout timeout
844846 let deadline = t + clampedTimeout
845− let token = extractToken authorization
846− result <- pollLoop token cursor deadline
847+ result <- case extractToken authorization of
848+ Nothing {} -> yield (PollUnauthorized {})
849+ Just {value: token} -> pollLoop token cursor deadline
847850 tAfterPoll <- now
848851 maybePruneAndBroadcast tAfterPoll
849852 yield (respond (case result of
@@ -856,12 +859,9 @@ serve = \req -> case req of
856859 let clampedTs = clampTimestamp timestamp t
857860 result <- atomic do
858861 pr <- maybePrune t
859− sessions <- *sessions
860− let auth = authenticateSession sessions (extractToken authorization) t
861− case auth of
862− Unauthorized {} ->
863− yield {pr: pr, send: SendFailed {status: MsUnauthorized {}}}
864− Authorized {pubkey: fromKey} -> do
862+ withSessionAuth authorization t
863+ (yield {pr: pr, send: SendFailed {status: MsUnauthorized {}}})
864+ (\fromKey -> do
865865 messages <- *messages
866866 if countMessagesFor messages to t >= maxMessagesPerRecipient
867867 then yield {pr: pr, send: SendFailed {status: MsRejected {}}}
@@ -885,7 +885,7 @@ serve = \req -> case req of
885885 set *forwards = if shouldForward
886886 then union forwards (do s <- forwardServers; yield {messageId: msgId, toServer: s, createdAt: t, toKey: to, fromKey: fromKey, encryptedBlob: encryptedBlob, timestamp: clampedTs})
887887 else forwards
888− yield {pr: pr, send: SendDone {delivery: delivery, remoteServers: remoteServers, forwardServers: forwardServers, fromKey: fromKey}}
888+ yield {pr: pr, send: SendDone {delivery: delivery, remoteServers: remoteServers, forwardServers: forwardServers, fromKey: fromKey}})
889889 case result.send of
890890 SendDone {delivery, remoteServers, forwardServers, fromKey} ->
891891 fork (do
@@ -897,9 +897,8 @@ serve = \req -> case req of
897897 authorization: federationAuth})
898898 when (any forwardServers (\s -> s == server))
899899 (whenOk fwdResult (removeForward msgId server)))
900− case delivery of
901− DsQueued {} -> println ("sendMessage: forwards at capacity (" ++ show maxForwardsEntries ++ "), skipping forward tracking")
902− _ -> yield {})
900+ when (case delivery of DsQueued {} -> True {}; _ -> False {})
901+ (println ("sendMessage: forwards at capacity (" ++ show maxForwardsEntries ++ "), skipping forward tracking")))
903902 broadcastOfflineGossip t result.pr.peerServers result.pr.expiredLocal)
904903 _ -> forkBroadcastOffline t result.pr
905904 yield (case result.send of
@@ -911,12 +910,9 @@ serve = \req -> case req of
911910 let dedupedIds = dedupMessageIds messageIds
912911 result <- atomic do
913912 pr <- maybePrune t
914− sessions <- *sessions
915− let auth = authenticateSession sessions (extractToken authorization) t
916− case auth of
917− Unauthorized {} ->
918− yield {pr: pr, ack: AckUnauthorized {}}
919− Authorized {pubkey} -> do
913+ withSessionAuth authorization t
914+ (yield {pr: pr, ack: AckUnauthorized {}})
915+ (\pubkey -> do
920916 messages <- *messages
921917 let ackedMessages = do
922918 m <- messages
@@ -933,7 +929,7 @@ serve = \req -> case req of
933929 let evictedEntries = let excessCount = count combinedAckedIds - maxAckedIds in
934930 if excessCount <= 0 then [] else boundedSmallest combinedAckedIds excessCount (\a -> a.ackedAt)
935931 set *ackedIds = diff combinedAckedIds evictedEntries
936− yield {pr: pr, ack: AckDone {pendingAcks: ackedFederated, evictedEntries: evictedEntries}}
932+ yield {pr: pr, ack: AckDone {pendingAcks: ackedFederated, evictedEntries: evictedEntries}})
937933 fork (do
938934 case result.ack of
939935 AckDone {pendingAcks, evictedEntries} ->
@@ -948,12 +944,9 @@ serve = \req -> case req of
948944 t <- now
949945 result <- atomic do
950946 pr <- maybePrune t
951− sessions <- *sessions
952− let auth = authenticateSession sessions (extractToken authorization) t
953− case auth of
954− Unauthorized {} ->
955− yield {pr: pr, server: "", authorized: False {}}
956− Authorized _ -> do
947+ withSessionAuth authorization t
948+ (yield {pr: pr, server: "", authorized: False {}})
949+ (\_ -> do
957950 presence <- *presence
958951 let valid = do
959952 p <- presence
@@ -964,14 +957,14 @@ serve = \req -> case req of
964957 let bestRemote = maxBy remoteValid (\p -> p.expiresAt)
965958 let foundServer = if localOnline then serverHostN
966959 else case bestRemote of Just {value: p} -> p.server; Nothing {} -> ""
967− yield {pr: pr, server: foundServer, authorized: True {}}
960+ yield {pr: pr, server: foundServer, authorized: True {}})
968961 forkBroadcastOffline t result.pr
969962 yield (respond {server: result.server, authorized: result.authorized})
970963
971964 RecvGossip {events, hopsRemaining, fromServer, authorization, respond} -> do
972965 t <- now
973966 let cappedHops = clamp 0 maxGossipHops hopsRemaining
974− withFederationAuth authorization t respond (cappedHops < 1 || isLocalServer fromServer || count events > maxGossipEvents) (do
967+ withFederationAuth authorization t respond (cappedHops >= 1 && not (isLocalServer fromServer) && count events <= maxGossipEvents) (do
975968 let validated = validGossipEvents events t
976969 let onlineEntries = validated.online
977970 let trustedOffline = do e <- validated.offline; where e.server == fromServer; yield e
@@ -999,7 +992,7 @@ serve = \req -> case req of
999992
1000993 ForwardMessage {toKey, fromKey, encryptedBlob, timestamp, originServer, messageId, authorization, respond} -> do
1001994 t <- now
1002− withFederationAuth authorization t respond (isLocalServer originServer) (do
995+ withFederationAuth authorization t respond (not (isLocalServer originServer)) (do
1003996 let clampedTs = clampTimestamp timestamp t
1004997 result <- atomic do
1005998 pr <- maybePrune t
@@ -1020,12 +1013,11 @@ serve = \req -> case req of
10201013 forkBroadcastOffline t result.pr
10211014 yield (respond {ok: case result.forward of
10221015 ForwardRejected {} -> False {}
1023− ForwardDuplicate {} -> True {}
1024− ForwardStored {} -> True {}}))
1016+ _ -> True {}}))
10251017
10261018 FederationAck {messageIds, originServer, fromServer, authorization, respond} -> do
10271019 t <- now
1028− withFederationAuth authorization t respond (not (isLocalServer originServer) || isLocalServer fromServer) (do
1020+ withFederationAuth authorization t respond (isLocalServer originServer && not (isLocalServer fromServer)) (do
10291021 let dedupedIds = dedupMessageIds messageIds
10301022 pruneResult <- atomic do
10311023 pr <- maybePrune t