From c5d07f0a94c270d185c5241a3a76ed9e6b0b1225 Mon Sep 17 00:00:00 2001 From: Bram Date: Tue, 9 Jul 2024 13:29:45 +0200 Subject: [PATCH] Add toUpdate function for sync V1 - V3 --- src/Internal/Api/Sync/V1.elm | 120 ++++++++++++++++---- src/Internal/Api/Sync/V2.elm | 212 +++++++++++++++++++++++++++++++++-- src/Internal/Api/Sync/V3.elm | 147 +++++++++++++++++++++++- src/Internal/Config/Text.elm | 3 + 4 files changed, 449 insertions(+), 33 deletions(-) diff --git a/src/Internal/Api/Sync/V1.elm b/src/Internal/Api/Sync/V1.elm index 88f0dd0..cd71914 100644 --- a/src/Internal/Api/Sync/V1.elm +++ b/src/Internal/Api/Sync/V1.elm @@ -12,12 +12,16 @@ This API module represents the /sync endpoint on Matrix spec version v1.1. -} import FastDict as Dict exposing (Dict) -import Internal.Config.Log exposing (Log) +import Internal.Config.Log exposing (Log, log) +import Internal.Config.Text as Text +import Internal.Filter.Timeline exposing (Filter) import Internal.Tools.Json as Json -import Internal.Tools.StrippedEvent as StrippedEvent exposing (StrippedEvent) +import Internal.Tools.StrippedEvent as StrippedEvent import Internal.Tools.Timestamp as Timestamp exposing (Timestamp) import Internal.Values.Envelope as E +import Internal.Values.Event as Event import Internal.Values.Room as R +import Internal.Values.User as User exposing (User) import Internal.Values.Vault as V @@ -64,7 +68,7 @@ type alias InviteState = type alias StrippedState = { content : Json.Value - , sender : String + , sender : User , stateKey : String , eventType : String } @@ -93,7 +97,7 @@ type alias SyncStateEvent = , eventId : String , originServerTs : Timestamp , prevContent : Maybe Json.Value - , sender : String + , sender : User , stateKey : String , eventType : String , unsigned : Maybe UnsignedData @@ -125,7 +129,7 @@ type alias SyncRoomEvent = { content : Json.Value , eventId : String , originServerTs : Timestamp - , sender : String + , sender : User , eventType : String , unsigned : Maybe UnsignedData } @@ -164,7 +168,7 @@ type alias ToDevice = type alias ToDeviceEvent = { content : Maybe Json.Value - , sender : Maybe String + , sender : Maybe User , eventType : Maybe String } @@ -351,7 +355,7 @@ coderStrippedState = { fieldName = "sender" , toField = .sender , description = [ "The sender for the event." ] - , coder = Json.string + , coder = User.coder } ) (Json.field.required @@ -492,7 +496,7 @@ coderSyncStateEvent = { fieldName = "sender" , toField = .sender , description = [ "Contains the fully-qualified ID of the user who sent this event." ] - , coder = Json.string + , coder = User.coder } ) (Json.field.required @@ -640,7 +644,7 @@ coderSyncRoomEvent = { fieldName = "sender" , toField = .sender , description = [ "Contains the fully-qualified ID of the user who sent this event." ] - , coder = Json.string + , coder = User.coder } ) (Json.field.required @@ -801,7 +805,7 @@ coderToDeviceEvent = { fieldName = "sender" , toField = .sender , description = [ "The Matrix user ID of the user who sent this event." ] - , coder = Json.string + , coder = User.coder } ) (Json.field.optional.value @@ -813,16 +817,37 @@ coderToDeviceEvent = ) -updateSyncResponse : SyncResponse -> ( E.EnvelopeUpdate V.VaultUpdate, List Log ) -updateSyncResponse response = - -- TODO: Add account data +updateSyncResponse : { filter : Filter, since : Maybe String } -> SyncResponse -> ( E.EnvelopeUpdate V.VaultUpdate, List Log ) +updateSyncResponse { filter, since } response = + -- Account data + [ response.accountData + |> Maybe.andThen .events + |> Maybe.map (List.map (\e -> V.SetAccountData e.eventType e.content)) + |> Maybe.map + (\x -> + ( E.ContentUpdate <| V.More x + , if List.length x > 0 then + List.length x + |> Text.logs.syncAccountDataFound + |> log.debug + |> List.singleton + + else + [] + ) + ) + -- TODO: Add device lists -- Next batch - [ Just ( E.SetNextBatch response.nextBatch, [] ) + , Just ( E.SetNextBatch response.nextBatch, [] ) -- TODO: Add presence -- Rooms - , Maybe.map (updateRooms >> Tuple.mapFirst E.ContentUpdate) response.rooms + , Maybe.map + (updateRooms { filter = filter, nextBatch = response.nextBatch, since = since } + >> Tuple.mapFirst E.ContentUpdate + ) + response.rooms -- TODO: Add to_device ] @@ -832,8 +857,8 @@ updateSyncResponse response = |> Tuple.mapSecond List.concat -updateRooms : Rooms -> ( V.VaultUpdate, List Log ) -updateRooms rooms = +updateRooms : { filter : Filter, nextBatch : String, since : Maybe String } -> Rooms -> ( V.VaultUpdate, List Log ) +updateRooms { filter, nextBatch, since } rooms = let ( roomUpdate, roomLogs ) = rooms.join @@ -843,7 +868,13 @@ updateRooms rooms = (\( roomId, room ) -> let ( u, l ) = - updateJoinedRoom room + updateJoinedRoom + { filter = filter + , nextBatch = nextBatch + , roomId = roomId + , since = since + } + room in ( V.MapRoom roomId u, l ) ) @@ -869,8 +900,8 @@ updateRooms rooms = ) -updateJoinedRoom : JoinedRoom -> ( R.RoomUpdate, List Log ) -updateJoinedRoom room = +updateJoinedRoom : { filter : Filter, nextBatch : String, roomId : String, since : Maybe String } -> JoinedRoom -> ( R.RoomUpdate, List Log ) +updateJoinedRoom data room = ( R.More [ room.accountData |> Maybe.andThen .events @@ -888,8 +919,55 @@ updateJoinedRoom room = -- TODO: Add state -- TODO: Add RoomSummary - -- TODO: Add timeline + , room.timeline + |> Maybe.andThen + (updateTimeline data) + |> R.Optional + -- TODO: Add unread notifications ] , [] ) + + +updateTimeline : { filter : Filter, nextBatch : String, roomId : String, since : Maybe String } -> Timeline -> Maybe R.RoomUpdate +updateTimeline { filter, nextBatch, roomId, since } timeline = + timeline.events + |> Maybe.map + (\events -> + R.AddSync + { events = List.map (toEvent roomId) events + , filter = filter + , start = + case timeline.prevBatch of + Just _ -> + timeline.prevBatch + + Nothing -> + since + , end = nextBatch + } + ) + + +toEvent : String -> SyncRoomEvent -> Event.Event +toEvent roomId event = + { content = event.content + , eventId = event.eventId + , originServerTs = event.originServerTs + , roomId = roomId + , sender = event.sender + , stateKey = Nothing + , eventType = event.eventType + , unsigned = Maybe.map toUnsigned event.unsigned + } + + +toUnsigned : UnsignedData -> Event.UnsignedData +toUnsigned u = + Event.UnsignedData + { age = u.age + , prevContent = Nothing + , redactedBecause = Nothing + , transactionId = u.transactionId + } diff --git a/src/Internal/Api/Sync/V2.elm b/src/Internal/Api/Sync/V2.elm index cfbf29c..01b6387 100644 --- a/src/Internal/Api/Sync/V2.elm +++ b/src/Internal/Api/Sync/V2.elm @@ -13,9 +13,19 @@ v1.3. -} -import FastDict exposing (Dict) +import FastDict as Dict exposing (Dict) import Internal.Api.Sync.V1 as PV +import Internal.Config.Log exposing (Log, log) +import Internal.Config.Text as Text +import Internal.Filter.Timeline exposing (Filter) import Internal.Tools.Json as Json +import Internal.Tools.Timestamp as Timestamp exposing (Timestamp) +import Internal.Values.Envelope as E +import Internal.Values.Event as Event +import Internal.Values.Room as R +import Internal.Values.User as User exposing (User) +import Internal.Values.Vault as V +import Recursion type alias SyncResponse = @@ -62,7 +72,7 @@ type alias InviteState = type alias StrippedStateEvent = { content : Json.Value - , sender : String + , sender : User , stateKey : String , eventType : String } @@ -89,8 +99,8 @@ type alias State = type alias ClientEventWithoutRoomID = { content : Json.Value , eventId : String - , originServerTs : Int - , sender : String + , originServerTs : Timestamp + , sender : User , stateKey : Maybe String , eventType : String , unsigned : Maybe UnsignedData @@ -153,7 +163,7 @@ type alias ToDevice = type alias ToDeviceEvent = { content : Maybe Json.Value - , sender : Maybe String + , sender : Maybe User , eventType : Maybe String } @@ -387,14 +397,14 @@ coderClientEventWithoutRoomID = { fieldName = "origin_server_ts" , toField = .originServerTs , description = [ "Required: Timestamp (in milliseconds since the unix epoch) on originating homeserver when this event was sent." ] - , coder = Json.int + , coder = Timestamp.coder } ) (Json.field.required { fieldName = "sender" , toField = .sender , description = [ "Required: Contains the fully-qualified ID of the user who sent this event." ] - , coder = Json.string + , coder = User.coder } ) (Json.field.optional.value @@ -557,3 +567,191 @@ coderToDevice = coderToDeviceEvent : Json.Coder ToDeviceEvent coderToDeviceEvent = PV.coderToDeviceEvent + + +updateSyncResponse : { filter : Filter, since : Maybe String } -> SyncResponse -> ( E.EnvelopeUpdate V.VaultUpdate, List Log ) +updateSyncResponse { filter, since } response = + -- Account data + [ response.accountData + |> Maybe.andThen .events + |> Maybe.map (List.map (\e -> V.SetAccountData e.eventType e.content)) + |> Maybe.map + (\x -> + ( E.ContentUpdate <| V.More x + , if List.length x > 0 then + List.length x + |> Text.logs.syncAccountDataFound + |> log.debug + |> List.singleton + + else + [] + ) + ) + + -- TODO: Add device lists + -- Next batch + , Just ( E.SetNextBatch response.nextBatch, [] ) + + -- TODO: Add presence + -- Rooms + , Maybe.map + (updateRooms { filter = filter, nextBatch = response.nextBatch, since = since } + >> Tuple.mapFirst E.ContentUpdate + ) + response.rooms + + -- TODO: Add to_device + ] + |> List.filterMap identity + |> List.unzip + |> Tuple.mapFirst E.More + |> Tuple.mapSecond List.concat + + +updateRooms : { filter : Filter, nextBatch : String, since : Maybe String } -> Rooms -> ( V.VaultUpdate, List Log ) +updateRooms { filter, nextBatch, since } rooms = + let + ( roomUpdate, roomLogs ) = + rooms.join + |> Maybe.withDefault Dict.empty + |> Dict.toList + |> List.map + (\( roomId, room ) -> + let + ( u, l ) = + updateJoinedRoom + { filter = filter + , nextBatch = nextBatch + , roomId = roomId + , since = since + } + room + in + ( V.MapRoom roomId u, l ) + ) + |> List.unzip + |> Tuple.mapBoth V.More List.concat + in + ( V.More + -- Add rooms + [ rooms.join + |> Maybe.withDefault Dict.empty + |> Dict.keys + |> List.map V.CreateRoomIfNotExists + |> V.More + + -- Update rooms + , roomUpdate + + -- TODO: Add invited rooms + -- TODO: Add knocked rooms + -- TODO: Add left rooms + ] + , roomLogs + ) + + +updateJoinedRoom : { filter : Filter, nextBatch : String, roomId : String, since : Maybe String } -> JoinedRoom -> ( R.RoomUpdate, List Log ) +updateJoinedRoom data room = + ( R.More + [ room.accountData + |> Maybe.andThen .events + |> Maybe.map + (\events -> + events + |> List.map (\e -> R.SetAccountData e.eventType e.content) + |> R.More + ) + |> R.Optional + , room.ephemeral + |> Maybe.andThen .events + |> Maybe.map R.SetEphemeral + |> R.Optional + + -- TODO: Add state + -- TODO: Add RoomSummary + , room.timeline + |> Maybe.map (updateTimeline data) + |> R.Optional + + -- TODO: Add unread notifications + ] + , [] + ) + + +updateTimeline : { filter : Filter, nextBatch : String, roomId : String, since : Maybe String } -> Timeline -> R.RoomUpdate +updateTimeline { filter, nextBatch, roomId, since } timeline = + R.AddSync + { events = List.map (toEvent roomId) timeline.events + , filter = filter + , start = + case timeline.prevBatch of + Just _ -> + timeline.prevBatch + + Nothing -> + since + , end = nextBatch + } + + +toEvent : String -> ClientEventWithoutRoomID -> Event.Event +toEvent roomId event = + Recursion.runRecursion + (\ev -> + case Maybe.andThen (\(UnsignedData u) -> u.redactedBecause) ev.unsigned of + Just e -> + Recursion.recurseThen e + (\eo -> + Recursion.base + { content = ev.content + , eventId = ev.eventId + , originServerTs = ev.originServerTs + , roomId = roomId + , sender = ev.sender + , stateKey = ev.stateKey + , eventType = ev.eventType + , unsigned = toUnsigned (Just eo) ev.unsigned + } + ) + + Nothing -> + Recursion.base + { content = ev.content + , eventId = ev.eventId + , originServerTs = ev.originServerTs + , roomId = roomId + , sender = ev.sender + , stateKey = ev.stateKey + , eventType = ev.eventType + , unsigned = toUnsigned Nothing ev.unsigned + } + ) + event + + +toUnsigned : Maybe Event.Event -> Maybe UnsignedData -> Maybe Event.UnsignedData +toUnsigned ev unsigned = + case ( ev, unsigned ) of + ( Nothing, Nothing ) -> + Nothing + + ( Just e, Nothing ) -> + { age = Nothing + , prevContent = Nothing + , redactedBecause = Just e + , transactionId = Nothing + } + |> Event.UnsignedData + |> Just + + ( _, Just (UnsignedData u) ) -> + { age = u.age + , prevContent = u.prevContent + , redactedBecause = ev + , transactionId = u.transactionId + } + |> Event.UnsignedData + |> Just diff --git a/src/Internal/Api/Sync/V3.elm b/src/Internal/Api/Sync/V3.elm index 676a1f1..743a589 100644 --- a/src/Internal/Api/Sync/V3.elm +++ b/src/Internal/Api/Sync/V3.elm @@ -18,9 +18,18 @@ versions: -} -import FastDict exposing (Dict) +import FastDict as Dict exposing (Dict) import Internal.Api.Sync.V2 as PV +import Internal.Config.Log exposing (Log, log) +import Internal.Config.Text as Text +import Internal.Filter.Timeline exposing (Filter) import Internal.Tools.Json as Json +import Internal.Tools.Timestamp exposing (Timestamp) +import Internal.Values.Envelope as E +import Internal.Values.Event as Event +import Internal.Values.Room as R +import Internal.Values.User exposing (User) +import Internal.Values.Vault as V type alias SyncResponse = @@ -67,7 +76,7 @@ type alias InviteState = type alias StrippedStateEvent = { content : Json.Value - , sender : String + , sender : User , stateKey : String , eventType : String } @@ -95,8 +104,8 @@ type alias State = type alias ClientEventWithoutRoomID = { content : Json.Value , eventId : String - , originServerTs : Int - , sender : String + , originServerTs : Timestamp + , sender : User , stateKey : Maybe String , eventType : String , unsigned : Maybe UnsignedData @@ -160,7 +169,7 @@ type alias ToDevice = type alias ToDeviceEvent = { content : Maybe Json.Value - , sender : Maybe String + , sender : Maybe User , eventType : Maybe String } @@ -441,3 +450,131 @@ coderToDevice = coderToDeviceEvent : Json.Coder ToDeviceEvent coderToDeviceEvent = PV.coderToDeviceEvent + + +updateSyncResponse : { filter : Filter, since : Maybe String } -> SyncResponse -> ( E.EnvelopeUpdate V.VaultUpdate, List Log ) +updateSyncResponse { filter, since } response = + -- Account data + [ response.accountData + |> Maybe.andThen .events + |> Maybe.map (List.map (\e -> V.SetAccountData e.eventType e.content)) + |> Maybe.map + (\x -> + ( E.ContentUpdate <| V.More x + , if List.length x > 0 then + List.length x + |> Text.logs.syncAccountDataFound + |> log.debug + |> List.singleton + + else + [] + ) + ) + + -- TODO: Add device lists + -- Next batch + , Just ( E.SetNextBatch response.nextBatch, [] ) + + -- TODO: Add presence + -- Rooms + , Maybe.map + (updateRooms { filter = filter, nextBatch = response.nextBatch, since = since } + >> Tuple.mapFirst E.ContentUpdate + ) + response.rooms + + -- TODO: Add to_device + ] + |> List.filterMap identity + |> List.unzip + |> Tuple.mapFirst E.More + |> Tuple.mapSecond List.concat + + +updateRooms : { filter : Filter, nextBatch : String, since : Maybe String } -> Rooms -> ( V.VaultUpdate, List Log ) +updateRooms { filter, nextBatch, since } rooms = + let + ( roomUpdate, roomLogs ) = + rooms.join + |> Maybe.withDefault Dict.empty + |> Dict.toList + |> List.map + (\( roomId, room ) -> + let + ( u, l ) = + updateJoinedRoom + { filter = filter + , nextBatch = nextBatch + , roomId = roomId + , since = since + } + room + in + ( V.MapRoom roomId u, l ) + ) + |> List.unzip + |> Tuple.mapBoth V.More List.concat + in + ( V.More + -- Add rooms + [ rooms.join + |> Maybe.withDefault Dict.empty + |> Dict.keys + |> List.map V.CreateRoomIfNotExists + |> V.More + + -- Update rooms + , roomUpdate + + -- TODO: Add invited rooms + -- TODO: Add knocked rooms + -- TODO: Add left rooms + ] + , roomLogs + ) + + +updateJoinedRoom : { filter : Filter, nextBatch : String, roomId : String, since : Maybe String } -> JoinedRoom -> ( R.RoomUpdate, List Log ) +updateJoinedRoom data room = + ( R.More + [ room.accountData + |> Maybe.andThen .events + |> Maybe.map + (\events -> + events + |> List.map (\e -> R.SetAccountData e.eventType e.content) + |> R.More + ) + |> R.Optional + , room.ephemeral + |> Maybe.andThen .events + |> Maybe.map R.SetEphemeral + |> R.Optional + + -- TODO: Add state + -- TODO: Add RoomSummary + , room.timeline + |> Maybe.map (updateTimeline data) + |> R.Optional + + -- TODO: Add unread notifications + -- TODO: Add unread thread notifications + ] + , [] + ) + + +updateTimeline : { filter : Filter, nextBatch : String, roomId : String, since : Maybe String } -> Timeline -> R.RoomUpdate +updateTimeline = + PV.updateTimeline + + +toEvent : String -> ClientEventWithoutRoomID -> Event.Event +toEvent = + PV.toEvent + + +toUnsigned : Maybe Event.Event -> Maybe UnsignedData -> Maybe Event.UnsignedData +toUnsigned = + PV.toUnsigned diff --git a/src/Internal/Config/Text.elm b/src/Internal/Config/Text.elm index 3550be2..122a37a 100644 --- a/src/Internal/Config/Text.elm +++ b/src/Internal/Config/Text.elm @@ -638,6 +638,7 @@ logs : , sendEvent : Maybe String -> String , serverReturnedInvalidJSON : String -> String , serverReturnedUnknownJSON : String -> String + , syncAccountDataFound : Int -> String } logs = { baseUrlFound = @@ -675,6 +676,8 @@ logs = "Sent event, event id not known - make sure to check transaction id" , serverReturnedInvalidJSON = (++) "The server returned invalid JSON: " , serverReturnedUnknownJSON = (++) "The server returned JSON that doesn't seem to live up to spec rules: " + , syncAccountDataFound = + \n -> String.concat [ "Found ", String.fromInt n, " account data updates" ] }