From d360c561f9d3390745bdee9c49e5a7089c7e1829 Mon Sep 17 00:00:00 2001 From: Bram Date: Thu, 30 May 2024 16:46:44 +0200 Subject: [PATCH 01/34] Hide custom Elm configurations --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index ec38022..016b8d9 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,6 @@ repl-temp-* # Elm output index.html elm.js + +# Elm configurations +elm-*.json From a51449740640dd63ec06f5a9a6655688f400fb05 Mon Sep 17 00:00:00 2001 From: Bram Date: Mon, 3 Jun 2024 11:04:34 +0200 Subject: [PATCH 02/34] Improve documentation of sendMessageEvent --- src/Matrix.elm | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/Matrix.elm b/src/Matrix.elm index f59136a..92d5876 100644 --- a/src/Matrix.elm +++ b/src/Matrix.elm @@ -119,6 +119,18 @@ the client, or is unable to. This function doesn't check whether the given room exists and the user is able to send a message to, and instead just sends the request to the Matrix API. +The fields stand for the following: + + - `content` is the JSON object that is sent to the Matrix room. + - `eventType` is the event type that is sent to the Matrix room. + - `roomId` is the Matrix room ID. + - `toMsg` is the `msg` type that is returned after the message has been sent. + - `transactionId` is a unique identifier that helps the Matrix server + distringuish messages. If you send the same message with the same transactionId, + the server promises to register it only once. + - `vault` is the Matrix Vault that contains all the latest and most relevant + information. + -} sendMessageEvent : { content : E.Value From f714438dd4439ddb96a56d4725734fd09dbd2e8b Mon Sep 17 00:00:00 2001 From: Bram Date: Mon, 3 Jun 2024 11:05:06 +0200 Subject: [PATCH 03/34] Add logs function for better runtime documentation --- src/Matrix.elm | 26 ++++++++++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/src/Matrix.elm b/src/Matrix.elm index 92d5876..379530b 100644 --- a/src/Matrix.elm +++ b/src/Matrix.elm @@ -1,6 +1,6 @@ module Matrix exposing ( Vault, fromUserId, fromUsername - , VaultUpdate, update + , VaultUpdate, update, logs , addAccessToken, sendMessageEvent ) @@ -24,7 +24,7 @@ support a monolithic public registry. (: ## Keeping the Vault up-to-date -@docs VaultUpdate, update +@docs VaultUpdate, update, logs ## Debugging @@ -112,6 +112,28 @@ fromUsername { username, host, port_ } = |> Vault +{-| The VaultUpdate is a complex type that helps update the Vault. However, +it also contains a human output! + +Using this function, you can get a human output that describes everything that +the VaultUpdate has to tell the Vault. + +The `channel` field describes the context of the log, allowing you to filter +further. For example: + + - `debug` is a comprehensive channel describing everything the Elm runtime has + executed. + - `warn` contains warnings that aren't breaking, but relevant. + - `securityWarn` warns about potential security issues or potential attacks. + - `error` has errors that were encountered. + - `caughtError` has errors that were dealt with successfully. + +-} +logs : VaultUpdate -> List { channel : String, content : String } +logs (VaultUpdate vu) = + vu.logs + + {-| Send a message event to a room. This function can be used in a scenario where the user does not want to sync From 7b00a46ffa83e672669bab30a62c37e6ca9a15c0 Mon Sep 17 00:00:00 2001 From: Bram Date: Mon, 3 Jun 2024 15:02:35 +0200 Subject: [PATCH 04/34] Add object1 --- src/Internal/Api/BaseUrl/Api.elm | 30 +++---------------- .../Api/LoginWithUsernameAndPassword/Api.elm | 30 +++---------------- src/Internal/Api/SendMessageEvent/Api.elm | 30 +++---------------- src/Internal/Tools/Json.elm | 21 +++++++++++-- 4 files changed, 31 insertions(+), 80 deletions(-) diff --git a/src/Internal/Api/BaseUrl/Api.elm b/src/Internal/Api/BaseUrl/Api.elm index 0e9a930..5e98ed3 100644 --- a/src/Internal/Api/BaseUrl/Api.elm +++ b/src/Internal/Api/BaseUrl/Api.elm @@ -87,12 +87,12 @@ coder = { fieldName = "m.homeserver" , toField = .homeserver , coder = - Json.object2 + Json.object1 { name = "Homeserver Information" , description = [ "Used by clients to discover homeserver information." ] - , init = \a _ -> { baseUrl = a } + , init = HomeserverInformation } (Json.field.required { fieldName = "base_url" @@ -103,17 +103,6 @@ coder = , coder = Json.string } ) - (Json.field.optional.value - { fieldName = L.field - , toField = always Nothing - , description = - [ "The Elm SDK always expects objects to have at least two fields." - , "Otherwise, what's the point of hiding the value in an object?" - , "For this reason, this empty placeholder key will always be ignored." - ] - , coder = Json.value - } - ) , description = [ "Used by clients to discover homeserver information." ] @@ -123,12 +112,12 @@ coder = { fieldName = "m.identity_server" , toField = .identityServer , coder = - Json.object2 + Json.object1 { name = "Homeserver Information" , description = [ "Used by clients to discover homeserver information." ] - , init = \a _ -> { baseUrl = a } + , init = IdentityServerInformation } (Json.field.required { fieldName = "base_url" @@ -139,17 +128,6 @@ coder = , coder = Json.string } ) - (Json.field.optional.value - { fieldName = L.field - , toField = always Nothing - , description = - [ "The Elm SDK always expects objects to have at least two fields." - , "Otherwise, what's the point of hiding the value in an object?" - , "For this reason, this empty placeholder key will always be ignored." - ] - , coder = Json.value - } - ) , description = [ "Used by clients to discover identity server information." ] diff --git a/src/Internal/Api/LoginWithUsernameAndPassword/Api.elm b/src/Internal/Api/LoginWithUsernameAndPassword/Api.elm index 17d121d..9242ba2 100644 --- a/src/Internal/Api/LoginWithUsernameAndPassword/Api.elm +++ b/src/Internal/Api/LoginWithUsernameAndPassword/Api.elm @@ -888,12 +888,12 @@ disoveryInformationCoderV1 = { fieldName = "m.homeserver" , toField = .homeserver , coder = - Json.object2 + Json.object1 { name = "Homeserver Information" , description = [ "Used by clients to discover homeserver information." ] - , init = \a _ -> { baseUrl = a } + , init = HomeserverInformation } (Json.field.required { fieldName = "base_url" @@ -904,17 +904,6 @@ disoveryInformationCoderV1 = , coder = Json.string } ) - (Json.field.optional.value - { fieldName = L.field - , toField = always Nothing - , description = - [ "The Elm SDK always expects objects to have at least two fields." - , "Otherwise, what's the point of hiding the value in an object?" - , "For this reason, this empty placeholder key will always be ignored." - ] - , coder = Json.value - } - ) , description = [ "Used by clients to discover homeserver information." ] @@ -924,12 +913,12 @@ disoveryInformationCoderV1 = { fieldName = "m.identity_server" , toField = .identityServer , coder = - Json.object2 + Json.object1 { name = "Homeserver Information" , description = [ "Used by clients to discover homeserver information." ] - , init = \a _ -> { baseUrl = a } + , init = HomeserverInformation } (Json.field.required { fieldName = "base_url" @@ -940,17 +929,6 @@ disoveryInformationCoderV1 = , coder = Json.string } ) - (Json.field.optional.value - { fieldName = L.field - , toField = always Nothing - , description = - [ "The Elm SDK always expects objects to have at least two fields." - , "Otherwise, what's the point of hiding the value in an object?" - , "For this reason, this empty placeholder key will always be ignored." - ] - , coder = Json.value - } - ) , description = [ "Used by clients to discover identity server information." ] diff --git a/src/Internal/Api/SendMessageEvent/Api.elm b/src/Internal/Api/SendMessageEvent/Api.elm index a16491f..abccf41 100644 --- a/src/Internal/Api/SendMessageEvent/Api.elm +++ b/src/Internal/Api/SendMessageEvent/Api.elm @@ -143,26 +143,15 @@ sendMessageEventV3 { content, eventType, roomId, transactionId } = coderV1 : Json.Coder SendMessageEventOutputV1 coderV1 = - Json.object2 + Json.object1 { name = "EventResponse" , description = [ "This endpoint is used to send a message event to a room. Message events allow access to historical events and pagination, making them suited for \"once-off\" activity in a room." , "The body of the request should be the content object of the event; the fields in this object will vary depending on the type of event." , "https://spec.matrix.org/legacy/r0.0.0/client_server.html#put-matrix-client-r0-rooms-roomid-send-eventtype-txnid" ] - , init = always SendMessageEventOutputV1 + , init = SendMessageEventOutputV1 } - (Json.field.optional.value - { fieldName = L.field - , toField = always Nothing - , description = - [ "The Elm SDK always expects objects to have at least two fields." - , "Otherwise, what's the point of hiding the value in an object?" - , "For this reason, this empty placeholder key will always be ignored." - ] - , coder = Json.value - } - ) (Json.field.optional.value { fieldName = "event_id" , toField = .eventId @@ -174,26 +163,15 @@ coderV1 = coderV2 : Json.Coder SendMessageEventOutputV2 coderV2 = - Json.object2 + Json.object1 { name = "EventResponse" , description = [ "This endpoint is used to send a message event to a room. Message events allow access to historical events and pagination, making them suited for \"once-off\" activity in a room." , "The body of the request should be the content object of the event; the fields in this object will vary depending on the type of event." , "https://spec.matrix.org/legacy/client_server/r0.6.1.html#put-matrix-client-r0-rooms-roomid-send-eventtype-txnid" ] - , init = always SendMessageEventOutputV2 + , init = SendMessageEventOutputV2 } - (Json.field.optional.value - { fieldName = L.field - , toField = always Nothing - , description = - [ "The Elm SDK always expects objects to have at least two fields." - , "Otherwise, what's the point of hiding the value in an object?" - , "For this reason, this empty placeholder key will always be ignored." - ] - , coder = Json.value - } - ) (Json.field.required { fieldName = "event_id" , toField = .eventId diff --git a/src/Internal/Tools/Json.elm b/src/Internal/Tools/Json.elm index 28ccd8c..4962d45 100644 --- a/src/Internal/Tools/Json.elm +++ b/src/Internal/Tools/Json.elm @@ -5,7 +5,7 @@ module Internal.Tools.Json exposing , Docs(..), RequiredField(..), toDocs , list, listWithOne, slowDict, fastDict, fastIntDict, set, maybe , Field, field, parser - , object2, object3, object4, object5, object6, object7, object8, object9, object10, object11 + , object1, object2, object3, object4, object5, object6, object7, object8, object9, object10, object11 ) {-| @@ -62,7 +62,7 @@ first. Once all fields are constructed, the user can create JSON objects. -@docs object2, object3, object4, object5, object6, object7, object8, object9, object10, object11 +@docs object1, object2, object3, object4, object5, object6, object7, object8, object9, object10, object11 -} @@ -596,6 +596,23 @@ objectEncoder items object = |> E.maybeObject +object1 : + Descriptive { init : a -> object } + -> Field a object + -> Coder object +object1 { name, description, init } fa = + Coder + { encoder = objectEncoder [ toEncodeField fa ] + , decoder = D.map (Tuple.mapFirst init) (toDecoderField fa) + , docs = + DocsObject + { name = name + , description = description + , keys = [ toDocsField fa ] + } + } + + {-| Define an object with 2 keys type alias Human = From f6a6bb535e57835b828076413f62994f8280112e Mon Sep 17 00:00:00 2001 From: Bram Date: Sat, 8 Jun 2024 15:10:58 +0200 Subject: [PATCH 05/34] Add StrippedEvent module --- src/Internal/Tools/StrippedEvent.elm | 50 ++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 src/Internal/Tools/StrippedEvent.elm diff --git a/src/Internal/Tools/StrippedEvent.elm b/src/Internal/Tools/StrippedEvent.elm new file mode 100644 index 0000000..f5112ea --- /dev/null +++ b/src/Internal/Tools/StrippedEvent.elm @@ -0,0 +1,50 @@ +module Internal.Tools.StrippedEvent exposing (StrippedEvent, coder, strip) + +{-| + + +# Stripped event + +The stripped event is a simple Matrix event that does not contain any metadata. + +@docs StrippedEvent, coder, strip + +-} + +import Internal.Tools.Json as Json + + +type alias StrippedEvent = + { content : Json.Value, eventType : String } + + +coder : Json.Coder StrippedEvent +coder = + Json.object2 + { name = Debug.todo "Needs name" + , description = Debug.todo "Needs description" + , init = StrippedEvent + } + (Json.field.required + { fieldName = "content" + , toField = .content + , description = + [ "Event content" + ] + , coder = Json.value + } + ) + (Json.field.required + { fieldName = "eventType" + , toField = .eventType + , description = + [ "Event type, generally namespaced using the Java package naming convention." + ] + , coder = Json.string + } + ) + + +strip : { a | content : Json.Value, eventType : String } -> StrippedEvent +strip { content, eventType } = + { content = content, eventType = eventType } From 0ded7ab6bd4a6df457464b06aecb8eb94c6e0732 Mon Sep 17 00:00:00 2001 From: Bram Date: Sat, 8 Jun 2024 15:19:55 +0200 Subject: [PATCH 06/34] Add ephemeral events to Room --- src/Internal/Config/Text.elm | 4 ++++ src/Internal/Values/Room.elm | 25 ++++++++++++++++++++++++- 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/src/Internal/Config/Text.elm b/src/Internal/Config/Text.elm index a63e936..0c0d4ad 100644 --- a/src/Internal/Config/Text.elm +++ b/src/Internal/Config/Text.elm @@ -313,6 +313,7 @@ fields : } , room : { accountData : Desc + , ephemeral : Desc , events : Desc , roomId : Desc , state : Desc @@ -486,6 +487,9 @@ fields = , room = { accountData = [ "Room account data tracking the user's private storage about this room." ] + , ephemeral = + [ "Ephemeral events that were sent recently in this room." + ] , events = [ "Database containing events that were sent in this room." ] , roomId = diff --git a/src/Internal/Values/Room.elm b/src/Internal/Values/Room.elm index 1db7bf1..6f5c198 100644 --- a/src/Internal/Values/Room.elm +++ b/src/Internal/Values/Room.elm @@ -53,6 +53,7 @@ import Internal.Config.Text as Text import Internal.Filter.Timeline as Filter exposing (Filter) import Internal.Tools.Hashdict as Hashdict exposing (Hashdict) import Internal.Tools.Json as Json +import Internal.Tools.StrippedEvent as StrippedEvent exposing (StrippedEvent) import Internal.Values.Event as Event exposing (Event) import Internal.Values.StateManager as StateManager exposing (StateManager) import Internal.Values.Timeline as Timeline exposing (Timeline) @@ -71,6 +72,7 @@ homeserver. -} type alias Room = { accountData : Dict String Json.Value + , ephemeral : List StrippedEvent , events : Hashdict Event , roomId : String , state : StateManager @@ -86,7 +88,9 @@ type RoomUpdate | AddSync Batch | Invite User | More (List RoomUpdate) + | Optional (Maybe RoomUpdate) | SetAccountData String Json.Value + | SetEphemeral (List { eventType : String, content : Json.Value }) {-| Add new events to the Room's event directory + Room's timeline. @@ -140,7 +144,7 @@ addSync = -} coder : Json.Coder Room coder = - Json.object5 + Json.object6 { name = Text.docs.room.name , description = Text.docs.room.description , init = Room @@ -154,6 +158,15 @@ coder = , defaultToString = Json.encode (Json.fastDict Json.value) >> E.encode 0 } ) + (Json.field.optional.withDefault + { fieldName = "ephemeral" + , toField = .ephemeral + , description = Text.fields.room.ephemeral + , coder = Json.list StrippedEvent.coder + , default = ( [], [] ) + , defaultToString = Json.encode (Json.list StrippedEvent.coder) >> E.encode 0 + } + ) (Json.field.optional.withDefault { fieldName = "events" , toField = .events @@ -216,6 +229,7 @@ getAccountData key room = init : String -> Room init roomId = { accountData = Dict.empty + , ephemeral = [] , events = Hashdict.empty .eventId , roomId = roomId , state = StateManager.empty @@ -262,5 +276,14 @@ update ru room = More items -> List.foldl update room items + Optional (Just u) -> + update u room + + Optional Nothing -> + room + SetAccountData key value -> setAccountData key value room + + SetEphemeral eph -> + { room | ephemeral = eph } From 6783186c18d68b012bf28318e9e1f406facd5795 Mon Sep 17 00:00:00 2001 From: Bram Date: Sat, 8 Jun 2024 15:25:49 +0200 Subject: [PATCH 07/34] Add nextBatch value in Vault --- src/Internal/Config/Text.elm | 4 ++++ src/Internal/Values/Vault.elm | 22 +++++++++++++++++++++- tests/Test/Values/Vault.elm | 3 ++- 3 files changed, 27 insertions(+), 2 deletions(-) diff --git a/src/Internal/Config/Text.elm b/src/Internal/Config/Text.elm index 0c0d4ad..c1c2060 100644 --- a/src/Internal/Config/Text.elm +++ b/src/Internal/Config/Text.elm @@ -346,6 +346,7 @@ fields : } , vault : { accountData : Desc + , nextBatch : Desc , rooms : Desc , user : Desc } @@ -565,6 +566,9 @@ fields = { accountData = [ "The account's global private data." ] + , nextBatch = + [ "The next batch that can be used to sync with the Matrix API." + ] , rooms = [ "Directory of joined rooms that the user is a member of." ] diff --git a/src/Internal/Values/Vault.elm b/src/Internal/Values/Vault.elm index 2725744..1c4112f 100644 --- a/src/Internal/Values/Vault.elm +++ b/src/Internal/Values/Vault.elm @@ -44,6 +44,7 @@ import Internal.Values.User as User exposing (User) -} type alias Vault = { accountData : Dict String Json.Value + , nextBatch : Maybe String , rooms : Hashdict Room , user : Maybe User } @@ -56,13 +57,15 @@ type VaultUpdate = CreateRoomIfNotExists String | MapRoom String Room.RoomUpdate | More (List VaultUpdate) + | Optional (Maybe VaultUpdate) | SetAccountData String Json.Value + | SetNextBatch String | SetUser User coder : Json.Coder Vault coder = - Json.object3 + Json.object4 { name = Text.docs.vault.name , description = Text.docs.vault.description , init = Vault @@ -74,6 +77,13 @@ coder = , coder = Json.fastDict Json.value } ) + (Json.field.optional.value + { fieldName = "nextBatch" + , toField = .nextBatch + , description = Text.fields.vault.nextBatch + , coder = Json.string + } + ) (Json.field.required { fieldName = "rooms" , toField = .rooms @@ -109,6 +119,7 @@ getAccountData key vault = init : Maybe User -> Vault init mUser = { accountData = Dict.empty + , nextBatch = Nothing , rooms = Hashdict.empty .roomId , user = mUser } @@ -152,8 +163,17 @@ update vu vault = More items -> List.foldl update vault items + Optional (Just u) -> + update u vault + + Optional Nothing -> + vault + SetAccountData key value -> setAccountData key value vault + SetNextBatch nb -> + { vault | nextBatch = Just nb } + SetUser user -> { vault | user = Just user } diff --git a/tests/Test/Values/Vault.elm b/tests/Test/Values/Vault.elm index 3982791..f1c90a0 100644 --- a/tests/Test/Values/Vault.elm +++ b/tests/Test/Values/Vault.elm @@ -12,11 +12,12 @@ import Test.Values.User as TestUser vault : Fuzzer Vault vault = - Fuzz.map3 Vault + Fuzz.map4 Vault (Fuzz.string |> Fuzz.map (\k -> ( k, Json.encode Json.int 0 )) |> Fuzz.list |> Fuzz.map Dict.fromList ) + (Fuzz.maybe Fuzz.string) (TestHashdict.fuzzer .roomId TestRoom.fuzzer) (Fuzz.maybe TestUser.fuzzer) From 693124aa15e5e3e91459c57288d9094c707e73f7 Mon Sep 17 00:00:00 2001 From: Bram Date: Sat, 8 Jun 2024 15:32:17 +0200 Subject: [PATCH 08/34] Add documentation for StrippedEvent --- src/Internal/Config/Text.elm | 7 +++++++ src/Internal/Tools/StrippedEvent.elm | 5 +++-- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/src/Internal/Config/Text.elm b/src/Internal/Config/Text.elm index c1c2060..c40dd9a 100644 --- a/src/Internal/Config/Text.elm +++ b/src/Internal/Config/Text.elm @@ -124,6 +124,7 @@ docs : , room : TypeDocs , settings : TypeDocs , stateManager : TypeDocs + , strippedEvent : TypeDocs , timeline : TypeDocs , timelineFilter : TypeDocs , unsigned : TypeDocs @@ -206,6 +207,12 @@ docs = , "Instead of making the user loop through the room's timeline of events, the StateManager offers the user a dictionary-like experience to navigate through the Matrix room state." ] } + , strippedEvent = + { name = "StrippedEvent" + , description = + [ "The StrippedEvent is a simplified Matrix event that contains no metadata." + ] + } , timeline = { name = "Timeline" , description = diff --git a/src/Internal/Tools/StrippedEvent.elm b/src/Internal/Tools/StrippedEvent.elm index f5112ea..8465cba 100644 --- a/src/Internal/Tools/StrippedEvent.elm +++ b/src/Internal/Tools/StrippedEvent.elm @@ -11,6 +11,7 @@ The stripped event is a simple Matrix event that does not contain any metadata. -} +import Internal.Config.Text as Text import Internal.Tools.Json as Json @@ -21,8 +22,8 @@ type alias StrippedEvent = coder : Json.Coder StrippedEvent coder = Json.object2 - { name = Debug.todo "Needs name" - , description = Debug.todo "Needs description" + { name = Text.docs.strippedEvent.name + , description = Text.docs.strippedEvent.description , init = StrippedEvent } (Json.field.required From 6300d15edf6dfb7da552e0d60011ab5e491c13c2 Mon Sep 17 00:00:00 2001 From: Bram Date: Thu, 13 Jun 2024 19:47:39 +0200 Subject: [PATCH 09/34] Move Iddict to noordstar/elm-iddict --- elm.json | 3 +- src/Internal/Config/Text.elm | 7 - src/Internal/Tools/Iddict.elm | 198 ---------------------- src/Internal/Tools/Json.elm | 25 ++- src/Internal/Values/Timeline.elm | 12 +- tests/Test/Tools/Iddict.elm | 280 ------------------------------- 6 files changed, 31 insertions(+), 494 deletions(-) delete mode 100644 src/Internal/Tools/Iddict.elm delete mode 100644 tests/Test/Tools/Iddict.elm diff --git a/elm.json b/elm.json index aad9344..7c3b668 100644 --- a/elm.json +++ b/elm.json @@ -20,7 +20,8 @@ "elm/time": "1.0.0 <= v < 2.0.0", "elm/url": "1.0.0 <= v < 2.0.0", "micahhahn/elm-safe-recursion": "2.0.0 <= v < 3.0.0", - "miniBill/elm-fast-dict": "1.0.0 <= v < 2.0.0" + "miniBill/elm-fast-dict": "1.0.0 <= v < 2.0.0", + "noordstar/elm-iddict": "1.0.1 <= v < 2.0.0" }, "test-dependencies": { "elm-explorations/test": "2.1.2 <= v < 3.0.0" diff --git a/src/Internal/Config/Text.elm b/src/Internal/Config/Text.elm index 806ac16..2bfe125 100644 --- a/src/Internal/Config/Text.elm +++ b/src/Internal/Config/Text.elm @@ -118,7 +118,6 @@ docs : , event : TypeDocs , hashdict : TypeDocs , ibatch : TypeDocs - , iddict : TypeDocs , itoken : TypeDocs , mashdict : TypeDocs , room : TypeDocs @@ -169,12 +168,6 @@ docs = [ "The internal batch tracks a patch of events on the Matrix timeline." ] } - , iddict = - { name = "Iddict" - , description = - [ "An iddict automatically handles creating appropriate keys by incrementally assiging a new key to new values." - ] - } , itoken = { name = "IToken" , description = diff --git a/src/Internal/Tools/Iddict.elm b/src/Internal/Tools/Iddict.elm deleted file mode 100644 index da718f2..0000000 --- a/src/Internal/Tools/Iddict.elm +++ /dev/null @@ -1,198 +0,0 @@ -module Internal.Tools.Iddict exposing - ( Iddict - , empty, singleton, insert, map, remove - , isEmpty, member, get, size - , keys, values - , coder, encode, decoder - ) - -{-| The id-dict is a data type that lets us store values in a dictionary using -unique identifiers. This can be used as a dictionary where the keys do not -matter. - -The benefit of the iddict is that it generates the keys FOR you. This way, you -do not need to generate identifiers yourself. - - -## Id-dict - -@docs Iddict - - -## Build - -@docs empty, singleton, insert, map, remove - - -## Query - -@docs isEmpty, member, get, size - - -## Lists - -@docs keys, values - - -## JSON coders - -@docs coder, encode, decoder - --} - -import FastDict as Dict exposing (Dict) -import Internal.Config.Text as Text -import Internal.Tools.Json as Json - - -{-| The Iddict data type. --} -type Iddict a - = Iddict - { cursor : Int - , dict : Dict Int a - } - - -{-| Define how an Iddict can be encoded and decoded to and from a JSON value. --} -coder : Json.Coder a -> Json.Coder (Iddict a) -coder x = - Json.object2 - { name = Text.docs.iddict.name - , description = Text.docs.iddict.description - , init = - \c d -> - Iddict - { cursor = - Dict.keys d - |> List.maximum - |> Maybe.map ((+) 1) - |> Maybe.withDefault 0 - |> max (Dict.size d) - |> max c - , dict = d - } - } - (Json.field.optional.withDefault - { fieldName = "cursor" - , toField = \(Iddict i) -> i.cursor - , description = Text.fields.iddict.cursor - , coder = Json.int - , default = ( 0, [] ) - , defaultToString = String.fromInt - } - ) - (Json.field.required - { fieldName = "dict" - , toField = \(Iddict i) -> i.dict - , description = Text.fields.iddict.dict - , coder = Json.fastIntDict x - } - ) - - -{-| Decode an id-dict from a JSON value. --} -decoder : Json.Coder a -> Json.Decoder (Iddict a) -decoder x = - Json.decode (coder x) - - -{-| Create an empty id-dict. --} -empty : Iddict a -empty = - Iddict - { cursor = 0 - , dict = Dict.empty - } - - -{-| Encode an id-dict to a JSON value. --} -encode : Json.Coder a -> Json.Encoder (Iddict a) -encode x = - Json.encode (coder x) - - -{-| Get a value from the id-dict using its key. --} -get : Int -> Iddict a -> Maybe a -get k (Iddict { dict }) = - Dict.get k dict - - -{-| Insert a new value into the id-dict. Given that the id-dict generates its -key, the function returns both the updated id-dict as the newly generated key. - - x = empty |> insert "hello" -- ( 0, ) - - case x of - ( _, iddict ) -> - get 0 iddict -- Just "hello" - --} -insert : a -> Iddict a -> ( Int, Iddict a ) -insert v (Iddict d) = - ( d.cursor - , Iddict { cursor = d.cursor + 1, dict = Dict.insert d.cursor v d.dict } - ) - - -{-| Determine if an id-dict is empty. --} -isEmpty : Iddict a -> Bool -isEmpty (Iddict d) = - Dict.isEmpty d.dict - - -{-| Get all of the keys from the id-dict, sorted from lowest to highest. --} -keys : Iddict a -> List Int -keys (Iddict { dict }) = - Dict.keys dict - - -{-| Map an existing value at a given key, if it exists. If it does not exist, -the operation does nothing. --} -map : Int -> (a -> a) -> Iddict a -> Iddict a -map k f (Iddict d) = - Iddict { d | dict = Dict.update k (Maybe.map f) d.dict } - - -{-| Determine if a key is in an id-dict. --} -member : Int -> Iddict a -> Bool -member k (Iddict d) = - k < d.cursor && Dict.member k d.dict - - -{-| Remove a key-value pair from the id-dict. If the key is not found, no -changes are made. --} -remove : Int -> Iddict a -> Iddict a -remove k (Iddict d) = - Iddict { d | dict = Dict.remove k d.dict } - - -{-| Create an id-dict with a single value. --} -singleton : a -> ( Int, Iddict a ) -singleton v = - insert v empty - - -{-| Determine the number of key-value pairs in the id-dict. --} -size : Iddict a -> Int -size (Iddict d) = - Dict.size d.dict - - -{-| Get all of the values from an id-dict, in the order of their keys. --} -values : Iddict a -> List a -values (Iddict { dict }) = - Dict.values dict diff --git a/src/Internal/Tools/Json.elm b/src/Internal/Tools/Json.elm index 28ccd8c..1a3116f 100644 --- a/src/Internal/Tools/Json.elm +++ b/src/Internal/Tools/Json.elm @@ -3,7 +3,7 @@ module Internal.Tools.Json exposing , Encoder, encode, Decoder, decode, Value , succeed, fail, andThen, lazy, map , Docs(..), RequiredField(..), toDocs - , list, listWithOne, slowDict, fastDict, fastIntDict, set, maybe + , list, listWithOne, slowDict, fastDict, fastIntDict, set, iddict, maybe , Field, field, parser , object2, object3, object4, object5, object6, object7, object8, object9, object10, object11 ) @@ -49,7 +49,7 @@ module to build its encoders and decoders. ## Data types -@docs list, listWithOne, slowDict, fastDict, fastIntDict, set, maybe +@docs list, listWithOne, slowDict, fastDict, fastIntDict, set, iddict, maybe ## Objects @@ -68,6 +68,7 @@ Once all fields are constructed, the user can create JSON objects. import Dict as SlowDict import FastDict +import Iddict exposing (Iddict) import Internal.Config.Log as Log exposing (Log) import Internal.Config.Text as Text import Internal.Tools.DecodeExtra as D @@ -141,6 +142,7 @@ type Docs = DocsBool | DocsDict Docs | DocsFloat + | DocsIddict Docs | DocsInt | DocsIntDict Docs | DocsLazy (() -> Docs) @@ -467,6 +469,25 @@ float = } +{-| Define an Iddict as defined in +[noordstar/elm-iddict](https://package.elm-lang.org/packages/noordstar/elm-iddict/latest/). +-} +iddict : Coder a -> Coder (Iddict a) +iddict (Coder old) = + Coder + { encoder = Iddict.encode old.encoder + , decoder = + D.andThen + (\( out, logs ) -> + D.succeed out + |> Iddict.decoder + |> D.map (\o -> ( o, logs )) + ) + old.decoder + , docs = DocsIddict old.docs + } + + {-| Define an int value. -} int : Coder Int diff --git a/src/Internal/Values/Timeline.elm b/src/Internal/Values/Timeline.elm index 9bded76..ea038a1 100644 --- a/src/Internal/Values/Timeline.elm +++ b/src/Internal/Values/Timeline.elm @@ -67,10 +67,10 @@ events! -} import FastDict as Dict exposing (Dict) +import Iddict exposing (Iddict) import Internal.Config.Text as Text import Internal.Filter.Timeline as Filter exposing (Filter) import Internal.Tools.Hashdict as Hashdict exposing (Hashdict) -import Internal.Tools.Iddict as Iddict exposing (Iddict) import Internal.Tools.Json as Json import Recursion import Recursion.Traverse @@ -210,7 +210,7 @@ coder = { fieldName = "batches" , toField = \(Timeline t) -> t.batches , description = Text.fields.timeline.batches - , coder = Iddict.coder coderIBatch + , coder = Json.iddict coderIBatch } ) (Json.field.required @@ -411,8 +411,8 @@ connectIBatchToIToken (IBatchPTR bptr) pointer (Timeline tl) = Timeline { tl | batches = - Iddict.map bptr - (\batch -> { batch | end = pointer }) + Iddict.update bptr + (Maybe.map (\batch -> { batch | end = pointer })) tl.batches , tokens = Hashdict.map tptr @@ -437,8 +437,8 @@ connectITokenToIBatch pointer (IBatchPTR bptr) (Timeline tl) = (\token -> { token | starts = Set.insert bptr token.starts }) tl.tokens , batches = - Iddict.map bptr - (\batch -> { batch | start = pointer }) + Iddict.update bptr + (Maybe.map (\batch -> { batch | start = pointer })) tl.batches } diff --git a/tests/Test/Tools/Iddict.elm b/tests/Test/Tools/Iddict.elm deleted file mode 100644 index 708ecb7..0000000 --- a/tests/Test/Tools/Iddict.elm +++ /dev/null @@ -1,280 +0,0 @@ -module Test.Tools.Iddict exposing (..) - -import Expect -import Fuzz exposing (Fuzzer) -import Internal.Tools.Iddict as Iddict exposing (Iddict) -import Internal.Tools.Json as Json -import Json.Decode as D -import Json.Encode as E -import Test exposing (..) - - -fuzzer : Fuzzer a -> Fuzzer (Iddict a) -fuzzer fuz = - fuz - |> Fuzz.pair Fuzz.bool - |> Fuzz.list - |> Fuzz.map - (\items -> - List.foldl - (\( rm, item ) dict -> - case Iddict.insert item dict of - ( key, d ) -> - if rm then - Iddict.remove key d - - else - d - ) - Iddict.empty - items - ) - - -empty : Test -empty = - describe "empty" - [ test "isEmpty" - (Iddict.empty - |> Iddict.isEmpty - |> Expect.equal True - |> always - ) - , fuzz Fuzz.int - "No members" - (\i -> - Iddict.empty - |> Iddict.member i - |> Expect.equal False - ) - , fuzz Fuzz.int - "Get gets Nothing" - (\i -> - Iddict.empty - |> Iddict.get i - |> Expect.equal Nothing - ) - , test "Size = 0" - (Iddict.empty - |> Iddict.size - |> Expect.equal 0 - |> always - ) - , test "No keys" - (Iddict.empty - |> Iddict.keys - |> Expect.equal [] - |> always - ) - , test "No values" - (Iddict.empty - |> Iddict.values - |> Expect.equal [] - |> always - ) - , test "JSON encode -> decode -> empty" - (Iddict.empty - |> Iddict.encode Json.value - |> D.decodeValue (Iddict.decoder Json.value) - |> Result.map Tuple.first - |> Expect.equal (Ok Iddict.empty) - |> always - ) - , test "JSON encode" - (Iddict.empty - |> Iddict.encode Json.value - |> E.encode 0 - |> Expect.equal "{\"dict\":{}}" - |> always - ) - , test "JSON decode" - ("{\"dict\":{}}" - |> D.decodeString (Iddict.decoder Json.value) - |> Result.map Tuple.first - |> Expect.equal (Ok Iddict.empty) - |> always - ) - ] - - -singleton : Test -singleton = - let - singleFuzzer : Fuzzer (Iddict Int) - singleFuzzer = - Fuzz.map - (\i -> - Iddict.singleton i - |> Tuple.second - ) - Fuzz.int - in - describe "singleton" - [ fuzz singleFuzzer - "not isEmpty" - (\single -> - single - |> Iddict.isEmpty - |> Expect.equal False - ) - , fuzz Fuzz.int - "singleton == insert empty" - (\i -> - Iddict.empty - |> Iddict.insert i - |> Expect.equal (Iddict.singleton i) - ) - , fuzz Fuzz.int - "First item is key 0" - (\i -> - Iddict.singleton i - |> Tuple.first - |> Expect.equal 0 - ) - , fuzz singleFuzzer - "Key 0 is member" - (\single -> - single - |> Iddict.member 0 - |> Expect.equal True - ) - , fuzz Fuzz.int - "Key 0 get returns Just value" - (\i -> - Iddict.singleton i - |> Tuple.second - |> Iddict.get 0 - |> Expect.equal (Just i) - ) - , fuzz singleFuzzer - "Size == 1" - (\single -> - single - |> Iddict.size - |> Expect.equal 1 - ) - , fuzz Fuzz.int - "Only key 0" - (\i -> - Iddict.singleton i - |> Tuple.second - |> Iddict.keys - |> Expect.equal [ 0 ] - ) - , fuzz Fuzz.int - "Only value value" - (\i -> - Iddict.singleton i - |> Tuple.second - |> Iddict.values - |> Expect.equal [ i ] - ) - , fuzz singleFuzzer - "JSON encode -> decode -> singleton" - (\single -> - single - |> Iddict.encode Json.int - |> D.decodeValue (Iddict.decoder Json.int) - |> Result.map Tuple.first - |> Expect.equal (Ok single) - ) - , fuzz Fuzz.int - "JSON encode" - (\i -> - Iddict.singleton i - |> Tuple.second - |> Iddict.encode Json.int - |> E.encode 0 - |> Expect.equal ("{\"cursor\":1,\"dict\":{\"0\":" ++ String.fromInt i ++ "}}") - ) - , fuzz Fuzz.int - "JSON decode" - (\i -> - ("{\"cursor\":1,\"dict\":{\"0\":" ++ String.fromInt i ++ "}}") - |> D.decodeString (Iddict.decoder Json.int) - |> Result.map Tuple.first - |> Tuple.pair 0 - |> Expect.equal (Iddict.singleton i |> Tuple.mapSecond Ok) - ) - ] - - -insert : Test -insert = - describe "insert" - [ fuzz2 (fuzzer Fuzz.int) - Fuzz.int - "Add something" - (\d i -> - case Iddict.insert i d of - ( key, dict ) -> - dict - |> Iddict.get key - |> Expect.equal (Just i) - ) - , fuzz2 (fuzzer Fuzz.int) - Fuzz.int - "Never isEmpty" - (\d i -> - Iddict.insert i d - |> Tuple.second - |> Iddict.isEmpty - |> Expect.equal False - ) - , fuzz2 (fuzzer Fuzz.int) - Fuzz.int - "New key" - (\d i -> - case Iddict.insert i d of - ( key, dict ) -> - dict - |> Iddict.remove key - |> Iddict.insert i - |> (\( newKey, _ ) -> - Expect.notEqual key newKey - ) - ) - , fuzz2 (fuzzer Fuzz.int) - Fuzz.int - "New dict" - (\d i -> - case Iddict.insert i d of - ( key, dict ) -> - dict - |> Iddict.remove key - |> Iddict.insert i - |> (\( _, newDict ) -> - Expect.notEqual dict newDict - ) - ) - , fuzz2 (fuzzer Fuzz.int) - Fuzz.int - "Inserted value is member" - (\d i -> - case Iddict.insert i d of - ( key, dict ) -> - dict - |> Iddict.member key - |> Expect.equal True - ) - , fuzz2 (fuzzer Fuzz.int) - Fuzz.int - "Get inserted value" - (\d i -> - case Iddict.insert i d of - ( key, dict ) -> - dict - |> Iddict.get key - |> Expect.equal (Just i) - ) - , fuzz2 (fuzzer Fuzz.int) - Fuzz.int - "size = size + 1" - (\d i -> - case Iddict.insert i d of - ( _, dict ) -> - Expect.equal - (Iddict.size dict) - (Iddict.size d + 1) - ) - ] From 0092f96a255e8aaae665a318632f815f7afc3c21 Mon Sep 17 00:00:00 2001 From: Bram Date: Thu, 13 Jun 2024 22:27:12 +0200 Subject: [PATCH 10/34] Add /sync at r0.3.0 --- src/Internal/Api/Sync/Api.elm | 94 ++++ src/Internal/Api/Sync/V1.elm | 801 ++++++++++++++++++++++++++++++++++ 2 files changed, 895 insertions(+) create mode 100644 src/Internal/Api/Sync/Api.elm create mode 100644 src/Internal/Api/Sync/V1.elm diff --git a/src/Internal/Api/Sync/Api.elm b/src/Internal/Api/Sync/Api.elm new file mode 100644 index 0000000..e51d35f --- /dev/null +++ b/src/Internal/Api/Sync/Api.elm @@ -0,0 +1,94 @@ +module Internal.Api.Sync.Api exposing (..) + +{-| + + +# Sync + +The sync module might be one of the most crucial parts of the Elm SDK. It offers +users the guarantee that the `Vault` type remains up-to-date, and it helps +communicate with the Matrix server about the Vault's needs. + +@docs Phantom + +-} + +import Internal.Api.Api as A +import Internal.Api.Request as R +import Internal.Api.Sync.V1 as V1 +import Internal.Filter.Timeline as Filter + + + +-- For simplicity, we will not use a filter for now +-- and assume that every client always wants to receive all events. +-- type FilterV1 +-- = FilterV1 Filter +-- | FilterIdV1 String Filter +-- | NoFilter + + +type alias Phantom a = + { a | accessToken : (), baseUrl : (), versions : () } + + +type alias PhantomV1 a = + { a | accessToken : (), baseUrl : () } + + +type PresenceV1 + = OfflineV1 + + +type alias SyncInput = + { -- filter : FilterV1, + fullState : Maybe Bool + , presenceV1 : Maybe PresenceV1 + , since : Maybe String + , timeout : Maybe Int + } + + +type alias SyncInputV1 a = + { a + | -- filter : FilterV1 , + since : Maybe String + , fullState : Maybe Bool + , presenceV1 : Maybe PresenceV1 + , timeout : Maybe Int + } + + +sync : SyncInput -> A.TaskChain (Phantom a) (Phantom a) +sync = + A.startWithVersion "r0.0.0" syncV1 + |> A.versionChain + + +syncV1 : SyncInputV1 i -> A.TaskChain (PhantomV1 a) (PhantomV1 a) +syncV1 data = + A.request + { attributes = + [ R.accessToken + , R.queryOpString "filter" Nothing -- FILTER HERE + , R.queryOpString "since" data.since + , R.queryOpBool "full_state" data.fullState + , data.presenceV1 + |> Maybe.map (always "offline") + |> R.queryOpString "set_presence" + , R.queryOpInt "timeout" data.timeout + ] + , coder = V1.syncResponseCoder + , contextChange = always identity + , method = "GET" + , path = [ "_matrix", "client", "r0", "sync" ] + , toUpdate = + \out -> + ( V1.syncResponseToUpdate + { filter = Filter.pass -- FILTER HERE + , since = data.since + } + out + , [] + ) + } diff --git a/src/Internal/Api/Sync/V1.elm b/src/Internal/Api/Sync/V1.elm new file mode 100644 index 0000000..3685c63 --- /dev/null +++ b/src/Internal/Api/Sync/V1.elm @@ -0,0 +1,801 @@ +module Internal.Api.Sync.V1 exposing (..) + +{-| + + +# Sync V1 + +Given the complexity of the /sync endpoint, the JSON coders have been placed +in separate modules. Version 1 provides a valid JSON coder for the following +spec versions: + + - r0.3.0 : + +-- @docs coder + +-} + +import FastDict as Dict exposing (Dict) +import Internal.Config.Log exposing (Log) +import Internal.Filter.Timeline exposing (Filter) +import Internal.Tools.Json as Json +import Internal.Tools.StrippedEvent as StrippedEvent +import Internal.Tools.Timestamp as Timestamp exposing (Timestamp) +import Internal.Values.Envelope as E exposing (EnvelopeUpdate(..)) +import Internal.Values.Event as Event +import Internal.Values.Room as R exposing (RoomUpdate(..)) +import Internal.Values.User as User exposing (User) +import Internal.Values.Vault as V exposing (VaultUpdate(..)) + + +type alias SyncResponse = + { accountData : Maybe AccountData + , deviceLists : Maybe DeviceLists + , nextBatch : Maybe String + , presence : Maybe Presence + , rooms : Maybe Rooms + , toDevice : Maybe ToDevice + } + + +syncResponseCoder : Json.Coder SyncResponse +syncResponseCoder = + Json.object6 + { name = "SyncResponse" + , description = + [ "Response from the /sync endpoint" + ] + , init = SyncResponse + } + (Json.field.optional.value + { fieldName = "account_data" + , toField = .accountData + , description = + [ "The global private data created by this user." + ] + , coder = accountDataCoder + } + ) + (Json.field.optional.value + { fieldName = "device_lists" + , toField = .deviceLists + , description = + [ "Information on end-to-end device updates, as specified in End-to-end encryption." + ] + , coder = deviceListsCoder + } + ) + (Json.field.optional.value + { fieldName = "next_batch" + , toField = .nextBatch + , description = + [ "The batch token to supply in the since param of the next /sync request." + ] + , coder = Json.string + } + ) + (Json.field.optional.value + { fieldName = "presence" + , toField = .presence + , description = + [ "The updates to the presence status of other users." + ] + , coder = presenceCoder + } + ) + (Json.field.optional.value + { fieldName = "rooms" + , toField = .rooms + , description = + [ "Updates to rooms." + ] + , coder = roomsCoder + } + ) + (Json.field.optional.value + { fieldName = "to_device" + , toField = .toDevice + , description = + [ "Information on the send-to-device messages for the client device, as defined in Send-to-Device messaging." + ] + , coder = toDeviceCoder + } + ) + + +syncResponseToUpdate : { filter : Filter, since : Maybe String } -> SyncResponse -> EnvelopeUpdate VaultUpdate +syncResponseToUpdate data response = + E.More + [ E.ContentUpdate + (V.More + -- global account data + [ response.accountData + |> Maybe.map + (\acd -> + acd.events + |> List.map (\e -> V.SetAccountData e.eventType e.content) + |> V.More + ) + |> V.Optional + + -- rooms to update + , Maybe.map2 + (\rooms nextBatch -> + roomsToUpdate + { filter = data.filter + , nextBatch = nextBatch + , since = data.since + } + rooms + ) + response.rooms + response.nextBatch + |> V.Optional + + -- next batch + , response.nextBatch + |> Maybe.map V.SetNextBatch + |> V.Optional + ] + ) + + -- Add more updates here + ] + + +type alias ToDevice = + { events : List DeviceEvent } + + +toDeviceCoder : Json.Coder ToDevice +toDeviceCoder = + Json.object1 + { name = "ToDevice" + , description = + [ "Events indicating users' to_device changes, usually involving cryptography" + ] + , init = ToDevice + } + (Json.field.required + { fieldName = "events" + , toField = .events + , description = + [ "List of events" + ] + , coder = Json.list deviceEventCoder + } + ) + + +type alias DeviceEvent = + { content : EventContent, sender : User, eventType : String } + + +deviceEventCoder : Json.Coder DeviceEvent +deviceEventCoder = + Json.object3 + { name = "DeviceEvent" + , description = + [ "Partially stripped event used for sending to_device events." + ] + , init = DeviceEvent + } + (Json.field.required + { fieldName = "content" + , toField = .content + , description = + [ "The content of this event. The fields in this object will vary depending on the type of event." + ] + , coder = eventContentCoder + } + ) + (Json.field.required + { fieldName = "sender" + , toField = .sender + , description = + [ "The Matrix user ID of the user who sent this event." + ] + , coder = User.coder + } + ) + (Json.field.required + { fieldName = "type" + , toField = .eventType + , description = + [ "The type of event." + ] + , coder = Json.string + } + ) + + +type alias DeviceLists = + { changed : List String } + + +deviceListsCoder : Json.Coder DeviceLists +deviceListsCoder = + Json.object1 + { name = "DeviceLists" + , description = + [ "This module adds an optional device_lists property to the /sync response, as specified below. The server need only populate this property for an incremental /sync (ie, one where the since parameter was specified). The client is expected to use /keys/query or /keys/changes for the equivalent functionality after an initial sync, as documented in \"Tracking the device list for a user.\"" + ] + , init = DeviceLists + } + (Json.field.required + { fieldName = "changed" + , toField = .changed + , description = + [ "List of users who have updated their device identity keys since the previous sync response." + ] + , coder = Json.list Json.string + } + ) + + +type alias Rooms = + { invite : Dict String InvitedRoom + , join : Dict String JoinedRoom + , leave : Dict String LeftRoom + } + + +roomsCoder : Json.Coder Rooms +roomsCoder = + Json.object3 + { name = "Rooms" + , description = + [ "Summary of all relevant updates to all rooms." + ] + , init = Rooms + } + (Json.field.optional.withDefault + { fieldName = "invite" + , toField = .invite + , description = + [ "The rooms that the user has been invited to." + ] + , coder = Json.fastDict invitedRoomCoder + , default = ( Dict.empty, [] ) + , defaultToString = always "{}" + } + ) + (Json.field.optional.withDefault + { fieldName = "join" + , toField = .join + , description = + [ "The rooms that the user is a member of." + ] + , coder = Json.fastDict joinedRoomCoder + , default = ( Dict.empty, [] ) + , defaultToString = always "{}" + } + ) + (Json.field.optional.withDefault + { fieldName = "leave" + , toField = .leave + , description = + [ "The rooms that the user has left." + ] + , coder = Json.fastDict leftRoomCoder + , default = ( Dict.empty, [] ) + , defaultToString = always "{}" + } + ) + + +roomsToUpdate : { filter : Filter, nextBatch : String, since : Maybe String } -> Rooms -> VaultUpdate +roomsToUpdate data rooms = + rooms.join + |> Dict.foldl + (\roomId joinedRoom items -> + List.append items + [ V.CreateRoomIfNotExists roomId + , V.MapRoom roomId + (joinedRoomToUpdate + { filter = data.filter + , nextBatch = data.nextBatch + , roomId = roomId + , since = data.since + } + joinedRoom + ) + ] + ) + [] + |> V.More + + +type alias InvitedRoom = + { inviteState : Maybe InviteState } + + +invitedRoomCoder : Json.Coder InvitedRoom +invitedRoomCoder = + Json.object1 + { name = "InvitedRoom" + , description = + [ "Room that the user was invited to." + ] + , init = InvitedRoom + } + (Json.field.optional.value + { fieldName = "invite_state" + , toField = .inviteState + , description = + [ "The state of a room that the user has been invited to. These state events may only have the sender, type, state_key and content keys present. These events do not replace any state that the client already has for the room, for example if the client has archived the room. Instead the client should keep two separate copies of the state: the one from the invite_state and one from the archived state. If the client joins the room then the current state will be given as a delta against the archived state not the invite_state." + ] + , coder = inviteStateCoder + } + ) + + +type alias InviteState = + { events : List Event } + + +inviteStateCoder : Json.Coder InviteState +inviteStateCoder = + Json.object1 + { name = "InviteState" + , description = + [ "Invite (state) events describing the state of an invited room." + ] + , init = Ephemeral + } + (Json.field.required + { fieldName = "events" + , toField = .events + , description = + [ "List of events" + ] + , coder = Json.list eventCoder + } + ) + + +type alias JoinedRoom = + { accountData : Maybe AccountData + , ephemeral : Maybe Ephemeral + , state : Maybe State + , timeline : Maybe Timeline + } + + +joinedRoomCoder : Json.Coder JoinedRoom +joinedRoomCoder = + Json.object4 + { name = "JoinedRoom" + , description = + [ "Matrix room that the user is a member of." + ] + , init = JoinedRoom + } + (Json.field.optional.value + { fieldName = "account_data" + , toField = .accountData + , description = + [ "The private data that this user has attached to this room." + ] + , coder = accountDataCoder + } + ) + (Json.field.optional.value + { fieldName = "ephemeral" + , toField = .ephemeral + , description = + [ "The ephemeral events in the room that aren't recorded in the timeline or state of the room. e.g. typing." + ] + , coder = ephemeralCoder + } + ) + (Json.field.optional.value + { fieldName = "state" + , toField = .state + , description = + [ "Updates to the state, between the time indicated by the since parameter, and the start of the timeline (or all state up to the start of the timeline, if since is not given, or full_state is true)." + ] + , coder = stateCoder + } + ) + (Json.field.optional.value + { fieldName = "timeline" + , toField = .timeline + , description = + [ "The timeline of messages and state changes in the room." + ] + , coder = timelineCoder + } + ) + + +joinedRoomToUpdate : { filter : Filter, nextBatch : String, roomId : String, since : Maybe String } -> JoinedRoom -> RoomUpdate +joinedRoomToUpdate data room = + R.More + [ room.accountData + |> Maybe.map + (\acd -> + acd.events + |> List.map (\event -> R.SetAccountData event.eventType event.content) + |> R.More + ) + |> R.Optional + , room.ephemeral + |> Maybe.map ephemeralToUpdate + |> R.Optional + , room.timeline + |> Maybe.map (timelineToUpdate data) + |> R.Optional + ] + + +type alias Ephemeral = + { events : List Event } + + +ephemeralCoder : Json.Coder Ephemeral +ephemeralCoder = + Json.object1 + { name = "Ephemeral" + , description = + [ "Ephemeral events sent to the user from a room." + ] + , init = Ephemeral + } + (Json.field.required + { fieldName = "events" + , toField = .events + , description = + [ "List of events" + ] + , coder = Json.list eventCoder + } + ) + + +ephemeralToUpdate : Ephemeral -> RoomUpdate +ephemeralToUpdate { events } = + events + |> List.map StrippedEvent.strip + |> R.SetEphemeral + + +type alias LeftRoom = + { state : Maybe State, timeline : Maybe Timeline } + + +leftRoomCoder : Json.Coder LeftRoom +leftRoomCoder = + Json.object2 + { name = "LeftRoom" + , description = + [ "Room that the user is no longer a member of." + ] + , init = LeftRoom + } + (Json.field.optional.value + { fieldName = "state" + , toField = .state + , description = + [ "The state updates for the room up to the start of the timeline." + ] + , coder = stateCoder + } + ) + (Json.field.optional.value + { fieldName = "timeline" + , toField = .timeline + , description = + [ "The timeline of messages and state changes in the room up to the point when the user left." + ] + , coder = timelineCoder + } + ) + + +type alias State = + { events : List Event } + + +stateCoder : Json.Coder State +stateCoder = + Json.object1 + { name = "State" + , description = + [ "List of (state) events describing the room's state." + ] + , init = State + } + (Json.field.required + { fieldName = "events" + , toField = .events + , description = [ "List of events" ] + , coder = Json.list eventCoder + } + ) + + +type alias Timeline = + { events : List Event + , limited : Bool + , prevBatch : Maybe String + } + + +timelineCoder : Json.Coder Timeline +timelineCoder = + Json.object3 + { name = "Timeline" + , description = + [ "Timeline type describing a timeline in a room." + ] + , init = Timeline + } + (Json.field.required + { fieldName = "events" + , toField = .events + , description = + [ "List of events" + ] + , coder = Json.list eventCoder + } + ) + (Json.field.optional.withDefault + { fieldName = "limited" + , toField = .limited + , description = + [ "True if the number of events returned was limited by the limit on the filter" + ] + , coder = Json.bool + , default = ( False, [] ) + , defaultToString = + \b -> + if b then + "true" + + else + "false" + } + ) + (Json.field.optional.value + { fieldName = "prev_batch" + , toField = .prevBatch + , description = + [ "A token that can be supplied to to the from parameter of the rooms/{roomId}/messages endpoint" + , "If the batch was limited then this is a token that can be supplied to the server to retrieve earlier events" + ] + , coder = Json.string + } + ) + + +timelineToUpdate : { filter : Filter, nextBatch : String, roomId : String, since : Maybe String } -> Timeline -> RoomUpdate +timelineToUpdate { filter, nextBatch, roomId, since } t = + R.AddSync + { events = List.map (eventToUpdate roomId) t.events + , filter = filter + , start = + case t.prevBatch of + Just _ -> + t.prevBatch + + Nothing -> + since + , end = nextBatch + } + + +type alias Presence = + { events : List Event } + + +presenceCoder : Json.Coder Presence +presenceCoder = + Json.object1 + { name = "Presence" + , description = + [ "Events indicating users' presence" + ] + , init = Ephemeral + } + (Json.field.required + { fieldName = "events" + , toField = .events + , description = + [ "List of events" + ] + , coder = Json.list eventCoder + } + ) + + +type alias AccountData = + { events : List Event } + + +accountDataCoder : Json.Coder AccountData +accountDataCoder = + Json.object1 + { name = "AccountData" + , description = + [ "Account data events sent by the user in a given room using a different client." + ] + , init = Ephemeral + } + (Json.field.required + { fieldName = "events" + , toField = .events + , description = + [ "List of events" + ] + , coder = Json.list eventCoder + } + ) + + +type alias Event = + { content : EventContent + , eventId : String + , originServerTs : Timestamp + , sender : User + , stateKey : Maybe String + , eventType : String + , unsigned : Maybe Unsigned + } + + +eventCoder : Json.Coder Event +eventCoder = + Json.object7 + { name = "Event" + , description = + [ "Event describing a JSON object sent in a room." + ] + , init = Event + } + (Json.field.required + { fieldName = "content" + , toField = .content + , description = + [ "The content of this event. The fields in this object will vary depending on the type of event." + ] + , coder = eventContentCoder + } + ) + (Json.field.required + { fieldName = "event_id" + , toField = .eventId + , description = + [ "The ID of this event, if applicable." + ] + , coder = Json.string + } + ) + (Json.field.required + { fieldName = "origin_server_ts" + , toField = .originServerTs + , description = + [ "Timestamp in milliseconds on originating homeserver when this event was sent." + ] + , coder = Timestamp.coder + } + ) + (Json.field.required + { fieldName = "sender" + , toField = .sender + , description = + [ "The MXID of the user who sent this event." + ] + , coder = User.coder + } + ) + (Json.field.optional.value + { fieldName = "state_key" + , toField = .stateKey + , description = + [ "This key will only be present for state events. A unique key which defines the overwriting semantics for this piece of room state." + ] + , coder = Json.string + } + ) + (Json.field.required + { fieldName = "type" + , toField = .eventType + , description = + [ "The type of event." + ] + , coder = Json.string + } + ) + (Json.field.optional.value + { fieldName = "unsigned" + , toField = .unsigned + , description = + [ "Information about this event which was not sent by the originating homeserver" + ] + , coder = unsignedCoder + } + ) + + +eventToUpdate : String -> Event -> Event.Event +eventToUpdate roomId e = + -- TODO: Use Recursion module for call stack safety + { content = e.content + , eventId = e.eventId + , originServerTs = e.originServerTs + , roomId = roomId + , sender = e.sender + , stateKey = e.stateKey + , eventType = e.eventType + , unsigned = + Maybe.map + (\(Unsigned u) -> + Event.UnsignedData + { age = Just u.age + , transactionId = u.transactionId + , redactedBecause = Maybe.map (eventToUpdate roomId) u.redactedBecause + , prevContent = u.prevContent + } + ) + e.unsigned + } + + +type Unsigned + = Unsigned { age : Int, prevContent : Maybe EventContent, redactedBecause : Maybe Event, transactionId : Maybe String } + + +unsignedCoder : Json.Coder Unsigned +unsignedCoder = + Json.object4 + { name = "Unsigned" + , description = + [ "Information about the event that doesn't originate from the original homeserver." + ] + , init = \a b c d -> Unsigned { age = a, prevContent = b, redactedBecause = c, transactionId = d } + } + (Json.field.required + { fieldName = "age" + , toField = \(Unsigned u) -> u.age + , description = + [ "Time in milliseconds since the event was sent." ] + , coder = Json.int + } + ) + (Json.field.optional.value + { fieldName = "prev_content" + , toField = \(Unsigned u) -> u.prevContent + , description = + [ "The previous content for this state. This will be present only for state events appearing in the timeline. If this is not a state event, or there is no previous content, this key will be missing." + ] + , coder = eventContentCoder + } + ) + (Json.field.optional.value + { fieldName = "redacted_because" + , toField = \(Unsigned u) -> u.redactedBecause + , description = + [ "The event that redacted this event, if any." + ] + , coder = Json.lazy (\() -> eventCoder) + } + ) + (Json.field.optional.value + { fieldName = "transaction_id" + , toField = \(Unsigned u) -> u.transactionId + , description = + [ "The transaction ID set when this message was sent. This key will only be present for message events sent by the device calling this API." + ] + , coder = Json.string + } + ) + + +type alias EventContent = + Json.Value + + +eventContentCoder : Json.Coder EventContent +eventContentCoder = + Json.value From 7ab21b43144d80d71e49b8198c5744918e8ef660 Mon Sep 17 00:00:00 2001 From: Bram Date: Thu, 13 Jun 2024 22:42:36 +0200 Subject: [PATCH 11/34] Add /sync at r0.4.0 (partially) --- src/Internal/Api/Sync/V1.elm | 4 +- src/Internal/Api/Sync/V2.elm | 226 +++++++++++++++++++++++++++++++++++ 2 files changed, 228 insertions(+), 2 deletions(-) create mode 100644 src/Internal/Api/Sync/V2.elm diff --git a/src/Internal/Api/Sync/V1.elm b/src/Internal/Api/Sync/V1.elm index 3685c63..4f26038 100644 --- a/src/Internal/Api/Sync/V1.elm +++ b/src/Internal/Api/Sync/V1.elm @@ -596,7 +596,7 @@ presenceCoder = , description = [ "Events indicating users' presence" ] - , init = Ephemeral + , init = Presence } (Json.field.required { fieldName = "events" @@ -620,7 +620,7 @@ accountDataCoder = , description = [ "Account data events sent by the user in a given room using a different client." ] - , init = Ephemeral + , init = AccountData } (Json.field.required { fieldName = "events" diff --git a/src/Internal/Api/Sync/V2.elm b/src/Internal/Api/Sync/V2.elm new file mode 100644 index 0000000..6c55a29 --- /dev/null +++ b/src/Internal/Api/Sync/V2.elm @@ -0,0 +1,226 @@ +module Internal.Api.Sync.V2 exposing (..) + +{-| + + +# Sync V1 + +Given the complexity of the /sync endpoint, the JSON coders have been placed +in separate modules. Version 2 provides a valid JSON coder for the following +spec versions: + + - r0.4.0 : + +-- @docs coder + +-} +import Internal.Tools.StrippedEvent as StrippedEvent exposing (StrippedEvent) +import Internal.Api.Sync.V1 as V1 +import Internal.Tools.Json as Json +import Internal.Tools.Timestamp as Timestamp exposing (Timestamp) +import Internal.Values.User as User exposing (User) +import Internal.Values.Event as Event + +type alias RoomEvent = + { content : Json.Value + , eventId : String + , originServerTs : Timestamp + , sender : User + , stateKey : Maybe String + , eventType : String + , unsigned : Maybe Unsigned + } + + +roomEventCoder : Json.Coder RoomEvent +roomEventCoder = + Json.object7 + { name = "Event" + , description = + [ "Event describing a JSON object sent in a room." + ] + , init = RoomEvent + } + (Json.field.required + { fieldName = "content" + , toField = .content + , description = + [ "The content of this event. The fields in this object will vary depending on the type of event." + ] + , coder = Json.value + } + ) + (Json.field.required + { fieldName = "event_id" + , toField = .eventId + , description = + [ "The ID of this event, if applicable." + ] + , coder = Json.string + } + ) + (Json.field.required + { fieldName = "origin_server_ts" + , toField = .originServerTs + , description = + [ "Timestamp in milliseconds on originating homeserver when this event was sent." + ] + , coder = Timestamp.coder + } + ) + (Json.field.required + { fieldName = "sender" + , toField = .sender + , description = + [ "The MXID of the user who sent this event." + ] + , coder = User.coder + } + ) + (Json.field.optional.value + { fieldName = "state_key" + , toField = .stateKey + , description = + [ "This key will only be present for state events. A unique key which defines the overwriting semantics for this piece of room state." + ] + , coder = Json.string + } + ) + (Json.field.required + { fieldName = "type" + , toField = .eventType + , description = + [ "The type of event." + ] + , coder = Json.string + } + ) + (Json.field.optional.value + { fieldName = "unsigned" + , toField = .unsigned + , description = + [ "Information about this event which was not sent by the originating homeserver" + ] + , coder = unsignedCoder + } + ) + + +roomEventToUpdate : String -> RoomEvent -> Event.Event +roomEventToUpdate roomId e = + -- TODO: Use Recursion module for call stack safety + { content = e.content + , eventId = e.eventId + , originServerTs = e.originServerTs + , roomId = roomId + , sender = e.sender + , stateKey = e.stateKey + , eventType = e.eventType + , unsigned = + Maybe.map + (\(Unsigned u) -> + Event.UnsignedData + { age = Just u.age + , transactionId = u.transactionId + , redactedBecause = Maybe.map (roomEventToUpdate roomId) u.redactedBecause + , prevContent = u.prevContent + } + ) + e.unsigned + } + +type Unsigned + = Unsigned { age : Int, prevContent : Maybe Json.Value, redactedBecause : Maybe RoomEvent, transactionId : Maybe String } + + +unsignedCoder : Json.Coder Unsigned +unsignedCoder = + Json.object4 + { name = "Unsigned" + , description = + [ "Information about the event that doesn't originate from the original homeserver." + ] + , init = \a b c d -> Unsigned { age = a, prevContent = b, redactedBecause = c, transactionId = d } + } + (Json.field.required + { fieldName = "age" + , toField = \(Unsigned u) -> u.age + , description = + [ "Time in milliseconds since the event was sent." ] + , coder = Json.int + } + ) + (Json.field.optional.value -- Officially not supported in this spec version, but hey, what gives to try and look for it? + { fieldName = "prev_content" + , toField = \(Unsigned u) -> u.prevContent + , description = + [ "The previous content for this state. This will be present only for state events appearing in the timeline. If this is not a state event, or there is no previous content, this key will be missing." + ] + , coder = Json.value + } + ) + (Json.field.optional.value + { fieldName = "redacted_because" + , toField = \(Unsigned u) -> u.redactedBecause + , description = + [ "The event that redacted this event, if any." + ] + , coder = Json.lazy (\() -> roomEventCoder) + } + ) + (Json.field.optional.value + { fieldName = "transaction_id" + , toField = \(Unsigned u) -> u.transactionId + , description = + [ "The transaction ID set when this message was sent. This key will only be present for message events sent by the device calling this API." + ] + , coder = Json.string + } + ) + +type alias Presence = { events : List Event } + +presenceCoder : Json.Coder Presence +presenceCoder = + Json.object1 + { name = "Presence" + , description = + [ "Events indicating users' presence" + ] + , init = Presence + } + (Json.field.required + { fieldName = "events" + , toField = .events + , description = + [ "List of events" + ] + , coder = Json.list eventCoder + } + ) + +type alias AccountData = { events : List Event } + +accountDataCoder : Json.Coder AccountData +accountDataCoder = + Json.object1 + { name = "AccountData" + , description = + [ "Account data events sent by the user in a given room using a different client." + ] + , init = AccountData + } + (Json.field.required + { fieldName = "events" + , toField = .events + , description = + [ "List of events" + ] + , coder = Json.list eventCoder + } + ) + +type alias Event = StrippedEvent + +eventCoder : Json.Coder Event +eventCoder = StrippedEvent.coder From 95f0aa2934c788338b887d1777072f06e5710bd4 Mon Sep 17 00:00:00 2001 From: Bram Date: Thu, 13 Jun 2024 22:43:25 +0200 Subject: [PATCH 12/34] Remove deprecated spec version To improve backwards compatibility, we'll start with v1.1 and go from there. Then, if the spec versions seem fit, we can work backwards. --- src/Internal/Api/Sync/V1.elm | 801 ----------------------------------- src/Internal/Api/Sync/V2.elm | 226 ---------- 2 files changed, 1027 deletions(-) delete mode 100644 src/Internal/Api/Sync/V1.elm delete mode 100644 src/Internal/Api/Sync/V2.elm diff --git a/src/Internal/Api/Sync/V1.elm b/src/Internal/Api/Sync/V1.elm deleted file mode 100644 index 4f26038..0000000 --- a/src/Internal/Api/Sync/V1.elm +++ /dev/null @@ -1,801 +0,0 @@ -module Internal.Api.Sync.V1 exposing (..) - -{-| - - -# Sync V1 - -Given the complexity of the /sync endpoint, the JSON coders have been placed -in separate modules. Version 1 provides a valid JSON coder for the following -spec versions: - - - r0.3.0 : - --- @docs coder - --} - -import FastDict as Dict exposing (Dict) -import Internal.Config.Log exposing (Log) -import Internal.Filter.Timeline exposing (Filter) -import Internal.Tools.Json as Json -import Internal.Tools.StrippedEvent as StrippedEvent -import Internal.Tools.Timestamp as Timestamp exposing (Timestamp) -import Internal.Values.Envelope as E exposing (EnvelopeUpdate(..)) -import Internal.Values.Event as Event -import Internal.Values.Room as R exposing (RoomUpdate(..)) -import Internal.Values.User as User exposing (User) -import Internal.Values.Vault as V exposing (VaultUpdate(..)) - - -type alias SyncResponse = - { accountData : Maybe AccountData - , deviceLists : Maybe DeviceLists - , nextBatch : Maybe String - , presence : Maybe Presence - , rooms : Maybe Rooms - , toDevice : Maybe ToDevice - } - - -syncResponseCoder : Json.Coder SyncResponse -syncResponseCoder = - Json.object6 - { name = "SyncResponse" - , description = - [ "Response from the /sync endpoint" - ] - , init = SyncResponse - } - (Json.field.optional.value - { fieldName = "account_data" - , toField = .accountData - , description = - [ "The global private data created by this user." - ] - , coder = accountDataCoder - } - ) - (Json.field.optional.value - { fieldName = "device_lists" - , toField = .deviceLists - , description = - [ "Information on end-to-end device updates, as specified in End-to-end encryption." - ] - , coder = deviceListsCoder - } - ) - (Json.field.optional.value - { fieldName = "next_batch" - , toField = .nextBatch - , description = - [ "The batch token to supply in the since param of the next /sync request." - ] - , coder = Json.string - } - ) - (Json.field.optional.value - { fieldName = "presence" - , toField = .presence - , description = - [ "The updates to the presence status of other users." - ] - , coder = presenceCoder - } - ) - (Json.field.optional.value - { fieldName = "rooms" - , toField = .rooms - , description = - [ "Updates to rooms." - ] - , coder = roomsCoder - } - ) - (Json.field.optional.value - { fieldName = "to_device" - , toField = .toDevice - , description = - [ "Information on the send-to-device messages for the client device, as defined in Send-to-Device messaging." - ] - , coder = toDeviceCoder - } - ) - - -syncResponseToUpdate : { filter : Filter, since : Maybe String } -> SyncResponse -> EnvelopeUpdate VaultUpdate -syncResponseToUpdate data response = - E.More - [ E.ContentUpdate - (V.More - -- global account data - [ response.accountData - |> Maybe.map - (\acd -> - acd.events - |> List.map (\e -> V.SetAccountData e.eventType e.content) - |> V.More - ) - |> V.Optional - - -- rooms to update - , Maybe.map2 - (\rooms nextBatch -> - roomsToUpdate - { filter = data.filter - , nextBatch = nextBatch - , since = data.since - } - rooms - ) - response.rooms - response.nextBatch - |> V.Optional - - -- next batch - , response.nextBatch - |> Maybe.map V.SetNextBatch - |> V.Optional - ] - ) - - -- Add more updates here - ] - - -type alias ToDevice = - { events : List DeviceEvent } - - -toDeviceCoder : Json.Coder ToDevice -toDeviceCoder = - Json.object1 - { name = "ToDevice" - , description = - [ "Events indicating users' to_device changes, usually involving cryptography" - ] - , init = ToDevice - } - (Json.field.required - { fieldName = "events" - , toField = .events - , description = - [ "List of events" - ] - , coder = Json.list deviceEventCoder - } - ) - - -type alias DeviceEvent = - { content : EventContent, sender : User, eventType : String } - - -deviceEventCoder : Json.Coder DeviceEvent -deviceEventCoder = - Json.object3 - { name = "DeviceEvent" - , description = - [ "Partially stripped event used for sending to_device events." - ] - , init = DeviceEvent - } - (Json.field.required - { fieldName = "content" - , toField = .content - , description = - [ "The content of this event. The fields in this object will vary depending on the type of event." - ] - , coder = eventContentCoder - } - ) - (Json.field.required - { fieldName = "sender" - , toField = .sender - , description = - [ "The Matrix user ID of the user who sent this event." - ] - , coder = User.coder - } - ) - (Json.field.required - { fieldName = "type" - , toField = .eventType - , description = - [ "The type of event." - ] - , coder = Json.string - } - ) - - -type alias DeviceLists = - { changed : List String } - - -deviceListsCoder : Json.Coder DeviceLists -deviceListsCoder = - Json.object1 - { name = "DeviceLists" - , description = - [ "This module adds an optional device_lists property to the /sync response, as specified below. The server need only populate this property for an incremental /sync (ie, one where the since parameter was specified). The client is expected to use /keys/query or /keys/changes for the equivalent functionality after an initial sync, as documented in \"Tracking the device list for a user.\"" - ] - , init = DeviceLists - } - (Json.field.required - { fieldName = "changed" - , toField = .changed - , description = - [ "List of users who have updated their device identity keys since the previous sync response." - ] - , coder = Json.list Json.string - } - ) - - -type alias Rooms = - { invite : Dict String InvitedRoom - , join : Dict String JoinedRoom - , leave : Dict String LeftRoom - } - - -roomsCoder : Json.Coder Rooms -roomsCoder = - Json.object3 - { name = "Rooms" - , description = - [ "Summary of all relevant updates to all rooms." - ] - , init = Rooms - } - (Json.field.optional.withDefault - { fieldName = "invite" - , toField = .invite - , description = - [ "The rooms that the user has been invited to." - ] - , coder = Json.fastDict invitedRoomCoder - , default = ( Dict.empty, [] ) - , defaultToString = always "{}" - } - ) - (Json.field.optional.withDefault - { fieldName = "join" - , toField = .join - , description = - [ "The rooms that the user is a member of." - ] - , coder = Json.fastDict joinedRoomCoder - , default = ( Dict.empty, [] ) - , defaultToString = always "{}" - } - ) - (Json.field.optional.withDefault - { fieldName = "leave" - , toField = .leave - , description = - [ "The rooms that the user has left." - ] - , coder = Json.fastDict leftRoomCoder - , default = ( Dict.empty, [] ) - , defaultToString = always "{}" - } - ) - - -roomsToUpdate : { filter : Filter, nextBatch : String, since : Maybe String } -> Rooms -> VaultUpdate -roomsToUpdate data rooms = - rooms.join - |> Dict.foldl - (\roomId joinedRoom items -> - List.append items - [ V.CreateRoomIfNotExists roomId - , V.MapRoom roomId - (joinedRoomToUpdate - { filter = data.filter - , nextBatch = data.nextBatch - , roomId = roomId - , since = data.since - } - joinedRoom - ) - ] - ) - [] - |> V.More - - -type alias InvitedRoom = - { inviteState : Maybe InviteState } - - -invitedRoomCoder : Json.Coder InvitedRoom -invitedRoomCoder = - Json.object1 - { name = "InvitedRoom" - , description = - [ "Room that the user was invited to." - ] - , init = InvitedRoom - } - (Json.field.optional.value - { fieldName = "invite_state" - , toField = .inviteState - , description = - [ "The state of a room that the user has been invited to. These state events may only have the sender, type, state_key and content keys present. These events do not replace any state that the client already has for the room, for example if the client has archived the room. Instead the client should keep two separate copies of the state: the one from the invite_state and one from the archived state. If the client joins the room then the current state will be given as a delta against the archived state not the invite_state." - ] - , coder = inviteStateCoder - } - ) - - -type alias InviteState = - { events : List Event } - - -inviteStateCoder : Json.Coder InviteState -inviteStateCoder = - Json.object1 - { name = "InviteState" - , description = - [ "Invite (state) events describing the state of an invited room." - ] - , init = Ephemeral - } - (Json.field.required - { fieldName = "events" - , toField = .events - , description = - [ "List of events" - ] - , coder = Json.list eventCoder - } - ) - - -type alias JoinedRoom = - { accountData : Maybe AccountData - , ephemeral : Maybe Ephemeral - , state : Maybe State - , timeline : Maybe Timeline - } - - -joinedRoomCoder : Json.Coder JoinedRoom -joinedRoomCoder = - Json.object4 - { name = "JoinedRoom" - , description = - [ "Matrix room that the user is a member of." - ] - , init = JoinedRoom - } - (Json.field.optional.value - { fieldName = "account_data" - , toField = .accountData - , description = - [ "The private data that this user has attached to this room." - ] - , coder = accountDataCoder - } - ) - (Json.field.optional.value - { fieldName = "ephemeral" - , toField = .ephemeral - , description = - [ "The ephemeral events in the room that aren't recorded in the timeline or state of the room. e.g. typing." - ] - , coder = ephemeralCoder - } - ) - (Json.field.optional.value - { fieldName = "state" - , toField = .state - , description = - [ "Updates to the state, between the time indicated by the since parameter, and the start of the timeline (or all state up to the start of the timeline, if since is not given, or full_state is true)." - ] - , coder = stateCoder - } - ) - (Json.field.optional.value - { fieldName = "timeline" - , toField = .timeline - , description = - [ "The timeline of messages and state changes in the room." - ] - , coder = timelineCoder - } - ) - - -joinedRoomToUpdate : { filter : Filter, nextBatch : String, roomId : String, since : Maybe String } -> JoinedRoom -> RoomUpdate -joinedRoomToUpdate data room = - R.More - [ room.accountData - |> Maybe.map - (\acd -> - acd.events - |> List.map (\event -> R.SetAccountData event.eventType event.content) - |> R.More - ) - |> R.Optional - , room.ephemeral - |> Maybe.map ephemeralToUpdate - |> R.Optional - , room.timeline - |> Maybe.map (timelineToUpdate data) - |> R.Optional - ] - - -type alias Ephemeral = - { events : List Event } - - -ephemeralCoder : Json.Coder Ephemeral -ephemeralCoder = - Json.object1 - { name = "Ephemeral" - , description = - [ "Ephemeral events sent to the user from a room." - ] - , init = Ephemeral - } - (Json.field.required - { fieldName = "events" - , toField = .events - , description = - [ "List of events" - ] - , coder = Json.list eventCoder - } - ) - - -ephemeralToUpdate : Ephemeral -> RoomUpdate -ephemeralToUpdate { events } = - events - |> List.map StrippedEvent.strip - |> R.SetEphemeral - - -type alias LeftRoom = - { state : Maybe State, timeline : Maybe Timeline } - - -leftRoomCoder : Json.Coder LeftRoom -leftRoomCoder = - Json.object2 - { name = "LeftRoom" - , description = - [ "Room that the user is no longer a member of." - ] - , init = LeftRoom - } - (Json.field.optional.value - { fieldName = "state" - , toField = .state - , description = - [ "The state updates for the room up to the start of the timeline." - ] - , coder = stateCoder - } - ) - (Json.field.optional.value - { fieldName = "timeline" - , toField = .timeline - , description = - [ "The timeline of messages and state changes in the room up to the point when the user left." - ] - , coder = timelineCoder - } - ) - - -type alias State = - { events : List Event } - - -stateCoder : Json.Coder State -stateCoder = - Json.object1 - { name = "State" - , description = - [ "List of (state) events describing the room's state." - ] - , init = State - } - (Json.field.required - { fieldName = "events" - , toField = .events - , description = [ "List of events" ] - , coder = Json.list eventCoder - } - ) - - -type alias Timeline = - { events : List Event - , limited : Bool - , prevBatch : Maybe String - } - - -timelineCoder : Json.Coder Timeline -timelineCoder = - Json.object3 - { name = "Timeline" - , description = - [ "Timeline type describing a timeline in a room." - ] - , init = Timeline - } - (Json.field.required - { fieldName = "events" - , toField = .events - , description = - [ "List of events" - ] - , coder = Json.list eventCoder - } - ) - (Json.field.optional.withDefault - { fieldName = "limited" - , toField = .limited - , description = - [ "True if the number of events returned was limited by the limit on the filter" - ] - , coder = Json.bool - , default = ( False, [] ) - , defaultToString = - \b -> - if b then - "true" - - else - "false" - } - ) - (Json.field.optional.value - { fieldName = "prev_batch" - , toField = .prevBatch - , description = - [ "A token that can be supplied to to the from parameter of the rooms/{roomId}/messages endpoint" - , "If the batch was limited then this is a token that can be supplied to the server to retrieve earlier events" - ] - , coder = Json.string - } - ) - - -timelineToUpdate : { filter : Filter, nextBatch : String, roomId : String, since : Maybe String } -> Timeline -> RoomUpdate -timelineToUpdate { filter, nextBatch, roomId, since } t = - R.AddSync - { events = List.map (eventToUpdate roomId) t.events - , filter = filter - , start = - case t.prevBatch of - Just _ -> - t.prevBatch - - Nothing -> - since - , end = nextBatch - } - - -type alias Presence = - { events : List Event } - - -presenceCoder : Json.Coder Presence -presenceCoder = - Json.object1 - { name = "Presence" - , description = - [ "Events indicating users' presence" - ] - , init = Presence - } - (Json.field.required - { fieldName = "events" - , toField = .events - , description = - [ "List of events" - ] - , coder = Json.list eventCoder - } - ) - - -type alias AccountData = - { events : List Event } - - -accountDataCoder : Json.Coder AccountData -accountDataCoder = - Json.object1 - { name = "AccountData" - , description = - [ "Account data events sent by the user in a given room using a different client." - ] - , init = AccountData - } - (Json.field.required - { fieldName = "events" - , toField = .events - , description = - [ "List of events" - ] - , coder = Json.list eventCoder - } - ) - - -type alias Event = - { content : EventContent - , eventId : String - , originServerTs : Timestamp - , sender : User - , stateKey : Maybe String - , eventType : String - , unsigned : Maybe Unsigned - } - - -eventCoder : Json.Coder Event -eventCoder = - Json.object7 - { name = "Event" - , description = - [ "Event describing a JSON object sent in a room." - ] - , init = Event - } - (Json.field.required - { fieldName = "content" - , toField = .content - , description = - [ "The content of this event. The fields in this object will vary depending on the type of event." - ] - , coder = eventContentCoder - } - ) - (Json.field.required - { fieldName = "event_id" - , toField = .eventId - , description = - [ "The ID of this event, if applicable." - ] - , coder = Json.string - } - ) - (Json.field.required - { fieldName = "origin_server_ts" - , toField = .originServerTs - , description = - [ "Timestamp in milliseconds on originating homeserver when this event was sent." - ] - , coder = Timestamp.coder - } - ) - (Json.field.required - { fieldName = "sender" - , toField = .sender - , description = - [ "The MXID of the user who sent this event." - ] - , coder = User.coder - } - ) - (Json.field.optional.value - { fieldName = "state_key" - , toField = .stateKey - , description = - [ "This key will only be present for state events. A unique key which defines the overwriting semantics for this piece of room state." - ] - , coder = Json.string - } - ) - (Json.field.required - { fieldName = "type" - , toField = .eventType - , description = - [ "The type of event." - ] - , coder = Json.string - } - ) - (Json.field.optional.value - { fieldName = "unsigned" - , toField = .unsigned - , description = - [ "Information about this event which was not sent by the originating homeserver" - ] - , coder = unsignedCoder - } - ) - - -eventToUpdate : String -> Event -> Event.Event -eventToUpdate roomId e = - -- TODO: Use Recursion module for call stack safety - { content = e.content - , eventId = e.eventId - , originServerTs = e.originServerTs - , roomId = roomId - , sender = e.sender - , stateKey = e.stateKey - , eventType = e.eventType - , unsigned = - Maybe.map - (\(Unsigned u) -> - Event.UnsignedData - { age = Just u.age - , transactionId = u.transactionId - , redactedBecause = Maybe.map (eventToUpdate roomId) u.redactedBecause - , prevContent = u.prevContent - } - ) - e.unsigned - } - - -type Unsigned - = Unsigned { age : Int, prevContent : Maybe EventContent, redactedBecause : Maybe Event, transactionId : Maybe String } - - -unsignedCoder : Json.Coder Unsigned -unsignedCoder = - Json.object4 - { name = "Unsigned" - , description = - [ "Information about the event that doesn't originate from the original homeserver." - ] - , init = \a b c d -> Unsigned { age = a, prevContent = b, redactedBecause = c, transactionId = d } - } - (Json.field.required - { fieldName = "age" - , toField = \(Unsigned u) -> u.age - , description = - [ "Time in milliseconds since the event was sent." ] - , coder = Json.int - } - ) - (Json.field.optional.value - { fieldName = "prev_content" - , toField = \(Unsigned u) -> u.prevContent - , description = - [ "The previous content for this state. This will be present only for state events appearing in the timeline. If this is not a state event, or there is no previous content, this key will be missing." - ] - , coder = eventContentCoder - } - ) - (Json.field.optional.value - { fieldName = "redacted_because" - , toField = \(Unsigned u) -> u.redactedBecause - , description = - [ "The event that redacted this event, if any." - ] - , coder = Json.lazy (\() -> eventCoder) - } - ) - (Json.field.optional.value - { fieldName = "transaction_id" - , toField = \(Unsigned u) -> u.transactionId - , description = - [ "The transaction ID set when this message was sent. This key will only be present for message events sent by the device calling this API." - ] - , coder = Json.string - } - ) - - -type alias EventContent = - Json.Value - - -eventContentCoder : Json.Coder EventContent -eventContentCoder = - Json.value diff --git a/src/Internal/Api/Sync/V2.elm b/src/Internal/Api/Sync/V2.elm deleted file mode 100644 index 6c55a29..0000000 --- a/src/Internal/Api/Sync/V2.elm +++ /dev/null @@ -1,226 +0,0 @@ -module Internal.Api.Sync.V2 exposing (..) - -{-| - - -# Sync V1 - -Given the complexity of the /sync endpoint, the JSON coders have been placed -in separate modules. Version 2 provides a valid JSON coder for the following -spec versions: - - - r0.4.0 : - --- @docs coder - --} -import Internal.Tools.StrippedEvent as StrippedEvent exposing (StrippedEvent) -import Internal.Api.Sync.V1 as V1 -import Internal.Tools.Json as Json -import Internal.Tools.Timestamp as Timestamp exposing (Timestamp) -import Internal.Values.User as User exposing (User) -import Internal.Values.Event as Event - -type alias RoomEvent = - { content : Json.Value - , eventId : String - , originServerTs : Timestamp - , sender : User - , stateKey : Maybe String - , eventType : String - , unsigned : Maybe Unsigned - } - - -roomEventCoder : Json.Coder RoomEvent -roomEventCoder = - Json.object7 - { name = "Event" - , description = - [ "Event describing a JSON object sent in a room." - ] - , init = RoomEvent - } - (Json.field.required - { fieldName = "content" - , toField = .content - , description = - [ "The content of this event. The fields in this object will vary depending on the type of event." - ] - , coder = Json.value - } - ) - (Json.field.required - { fieldName = "event_id" - , toField = .eventId - , description = - [ "The ID of this event, if applicable." - ] - , coder = Json.string - } - ) - (Json.field.required - { fieldName = "origin_server_ts" - , toField = .originServerTs - , description = - [ "Timestamp in milliseconds on originating homeserver when this event was sent." - ] - , coder = Timestamp.coder - } - ) - (Json.field.required - { fieldName = "sender" - , toField = .sender - , description = - [ "The MXID of the user who sent this event." - ] - , coder = User.coder - } - ) - (Json.field.optional.value - { fieldName = "state_key" - , toField = .stateKey - , description = - [ "This key will only be present for state events. A unique key which defines the overwriting semantics for this piece of room state." - ] - , coder = Json.string - } - ) - (Json.field.required - { fieldName = "type" - , toField = .eventType - , description = - [ "The type of event." - ] - , coder = Json.string - } - ) - (Json.field.optional.value - { fieldName = "unsigned" - , toField = .unsigned - , description = - [ "Information about this event which was not sent by the originating homeserver" - ] - , coder = unsignedCoder - } - ) - - -roomEventToUpdate : String -> RoomEvent -> Event.Event -roomEventToUpdate roomId e = - -- TODO: Use Recursion module for call stack safety - { content = e.content - , eventId = e.eventId - , originServerTs = e.originServerTs - , roomId = roomId - , sender = e.sender - , stateKey = e.stateKey - , eventType = e.eventType - , unsigned = - Maybe.map - (\(Unsigned u) -> - Event.UnsignedData - { age = Just u.age - , transactionId = u.transactionId - , redactedBecause = Maybe.map (roomEventToUpdate roomId) u.redactedBecause - , prevContent = u.prevContent - } - ) - e.unsigned - } - -type Unsigned - = Unsigned { age : Int, prevContent : Maybe Json.Value, redactedBecause : Maybe RoomEvent, transactionId : Maybe String } - - -unsignedCoder : Json.Coder Unsigned -unsignedCoder = - Json.object4 - { name = "Unsigned" - , description = - [ "Information about the event that doesn't originate from the original homeserver." - ] - , init = \a b c d -> Unsigned { age = a, prevContent = b, redactedBecause = c, transactionId = d } - } - (Json.field.required - { fieldName = "age" - , toField = \(Unsigned u) -> u.age - , description = - [ "Time in milliseconds since the event was sent." ] - , coder = Json.int - } - ) - (Json.field.optional.value -- Officially not supported in this spec version, but hey, what gives to try and look for it? - { fieldName = "prev_content" - , toField = \(Unsigned u) -> u.prevContent - , description = - [ "The previous content for this state. This will be present only for state events appearing in the timeline. If this is not a state event, or there is no previous content, this key will be missing." - ] - , coder = Json.value - } - ) - (Json.field.optional.value - { fieldName = "redacted_because" - , toField = \(Unsigned u) -> u.redactedBecause - , description = - [ "The event that redacted this event, if any." - ] - , coder = Json.lazy (\() -> roomEventCoder) - } - ) - (Json.field.optional.value - { fieldName = "transaction_id" - , toField = \(Unsigned u) -> u.transactionId - , description = - [ "The transaction ID set when this message was sent. This key will only be present for message events sent by the device calling this API." - ] - , coder = Json.string - } - ) - -type alias Presence = { events : List Event } - -presenceCoder : Json.Coder Presence -presenceCoder = - Json.object1 - { name = "Presence" - , description = - [ "Events indicating users' presence" - ] - , init = Presence - } - (Json.field.required - { fieldName = "events" - , toField = .events - , description = - [ "List of events" - ] - , coder = Json.list eventCoder - } - ) - -type alias AccountData = { events : List Event } - -accountDataCoder : Json.Coder AccountData -accountDataCoder = - Json.object1 - { name = "AccountData" - , description = - [ "Account data events sent by the user in a given room using a different client." - ] - , init = AccountData - } - (Json.field.required - { fieldName = "events" - , toField = .events - , description = - [ "List of events" - ] - , coder = Json.list eventCoder - } - ) - -type alias Event = StrippedEvent - -eventCoder : Json.Coder Event -eventCoder = StrippedEvent.coder From 1d0a9de7da98bd1614c727be1de4227e11b774ec Mon Sep 17 00:00:00 2001 From: Bram Date: Mon, 8 Jul 2024 18:24:41 +0200 Subject: [PATCH 13/34] Automate defaultToString behaviour --- src/Internal/Filter/Timeline.elm | 2 -- src/Internal/Tools/Iddict.elm | 1 - src/Internal/Tools/Json.elm | 12 ++++++------ src/Internal/Values/Context.elm | 1 - src/Internal/Values/Envelope.elm | 1 - src/Internal/Values/Room.elm | 5 ----- src/Internal/Values/Settings.elm | 10 ---------- src/Internal/Values/Timeline.elm | 5 ----- tests/Test/Tools/Json.elm | 9 --------- 9 files changed, 6 insertions(+), 40 deletions(-) diff --git a/src/Internal/Filter/Timeline.elm b/src/Internal/Filter/Timeline.elm index 71e59d8..9b9d6c5 100644 --- a/src/Internal/Filter/Timeline.elm +++ b/src/Internal/Filter/Timeline.elm @@ -183,7 +183,6 @@ coder = , description = Text.fields.timelineFilter.senders , coder = Json.set Json.string , default = ( Set.empty, [] ) - , defaultToString = always "[]" } ) (Json.field.required @@ -199,7 +198,6 @@ coder = , description = Text.fields.timelineFilter.types , coder = Json.set Json.string , default = ( Set.empty, [] ) - , defaultToString = always "[]" } ) (Json.field.required diff --git a/src/Internal/Tools/Iddict.elm b/src/Internal/Tools/Iddict.elm index da718f2..6c99363 100644 --- a/src/Internal/Tools/Iddict.elm +++ b/src/Internal/Tools/Iddict.elm @@ -80,7 +80,6 @@ coder x = , description = Text.fields.iddict.cursor , coder = Json.int , default = ( 0, [] ) - , defaultToString = String.fromInt } ) (Json.field.required diff --git a/src/Internal/Tools/Json.elm b/src/Internal/Tools/Json.elm index 4962d45..3ab93b3 100644 --- a/src/Internal/Tools/Json.elm +++ b/src/Internal/Tools/Json.elm @@ -362,7 +362,7 @@ then the following field type would be used: , coder = string } -Suppose the JSO isn't obligated to provide a list of hobbies, and the list would +Suppose the JSON isn't obligated to provide a list of hobbies, and the list would by default be overriden with an empty list, then we would use the following field type: @@ -373,8 +373,7 @@ field type: [ "The hobbies of the person. Can be omitted." ] , coder = list string - , default = ( [], [] ) -- The `List Log` can be inserted in case you wish to insert a message when relying on a default - , defaultToString = always "[]" -- Default converted to a string + , default = ( [ "football" ], [] ) -- The `List Log` can be inserted in case you wish to insert a message when relying on a default } -} @@ -382,7 +381,7 @@ field : { required : { fieldName : String, toField : object -> a, description : List String, coder : Coder a } -> Field a object , optional : { value : { fieldName : String, toField : object -> Maybe a, description : List String, coder : Coder a } -> Field (Maybe a) object - , withDefault : { fieldName : String, toField : object -> a, description : List String, coder : Coder a, default : ( a, List Log ), defaultToString : a -> String } -> Field a object + , withDefault : { fieldName : String, toField : object -> a, description : List String, coder : Coder a, default : ( a, List Log ) } -> Field a object } } field = @@ -425,7 +424,7 @@ field = , requiredness = OptionalField } , withDefault = - \{ fieldName, toField, description, coder, default, defaultToString } -> + \{ fieldName, toField, description, coder, default } -> case coder of Coder { encoder, decoder, docs } -> Field @@ -449,7 +448,8 @@ field = , requiredness = default |> Tuple.first - |> defaultToString + |> encoder + |> E.encode 0 |> OptionalFieldWithDefault } } diff --git a/src/Internal/Values/Context.elm b/src/Internal/Values/Context.elm index 0449e7c..ca6a8a1 100644 --- a/src/Internal/Values/Context.elm +++ b/src/Internal/Values/Context.elm @@ -439,6 +439,5 @@ versionsCoder = , description = Text.fields.versions.unstableFeatures , coder = Json.set Json.string , default = ( Set.empty, [] ) - , defaultToString = Json.encode (Json.set Json.string) >> E.encode 0 } ) diff --git a/src/Internal/Values/Envelope.elm b/src/Internal/Values/Envelope.elm index 7823e62..7ae8ceb 100644 --- a/src/Internal/Values/Envelope.elm +++ b/src/Internal/Values/Envelope.elm @@ -124,7 +124,6 @@ coder c1 = , description = Text.fields.envelope.settings , coder = Settings.coder , default = Tuple.pair Settings.init [] - , defaultToString = always "" } ) diff --git a/src/Internal/Values/Room.elm b/src/Internal/Values/Room.elm index 6f5c198..2dd93b9 100644 --- a/src/Internal/Values/Room.elm +++ b/src/Internal/Values/Room.elm @@ -155,7 +155,6 @@ coder = , description = Text.fields.room.accountData , coder = Json.fastDict Json.value , default = ( Dict.empty, [] ) - , defaultToString = Json.encode (Json.fastDict Json.value) >> E.encode 0 } ) (Json.field.optional.withDefault @@ -164,7 +163,6 @@ coder = , description = Text.fields.room.ephemeral , coder = Json.list StrippedEvent.coder , default = ( [], [] ) - , defaultToString = Json.encode (Json.list StrippedEvent.coder) >> E.encode 0 } ) (Json.field.optional.withDefault @@ -173,7 +171,6 @@ coder = , description = Text.fields.room.events , coder = Hashdict.coder .eventId Event.coder , default = ( Hashdict.empty .eventId, [ log.warn "Found a room with no known events! Is it empty?" ] ) - , defaultToString = Json.encode (Hashdict.coder .eventId Event.coder) >> E.encode 0 } ) (Json.field.required @@ -189,7 +186,6 @@ coder = , description = Text.fields.room.state , coder = StateManager.coder , default = ( StateManager.empty, [] ) - , defaultToString = Json.encode StateManager.coder >> E.encode 0 } ) (Json.field.optional.withDefault @@ -198,7 +194,6 @@ coder = , description = Text.fields.room.timeline , coder = Timeline.coder , default = ( Timeline.empty, [] ) - , defaultToString = Json.encode Timeline.coder >> E.encode 0 } ) diff --git a/src/Internal/Values/Settings.elm b/src/Internal/Values/Settings.elm index eed039a..d0f40c4 100644 --- a/src/Internal/Values/Settings.elm +++ b/src/Internal/Values/Settings.elm @@ -55,7 +55,6 @@ coder = , description = Text.fields.settings.currentVersion , coder = Json.string , default = Tuple.pair Default.currentVersion [] - , defaultToString = identity } ) (Json.field.optional.withDefault @@ -64,7 +63,6 @@ coder = , description = Text.fields.settings.deviceName , coder = Json.string , default = Tuple.pair Default.deviceName [] - , defaultToString = identity } ) (Json.field.optional.withDefault @@ -73,13 +71,6 @@ coder = , description = Text.fields.settings.removePasswordOnLogin , coder = Json.bool , default = Tuple.pair Default.removePasswordOnLogin [] - , defaultToString = - \b -> - if b then - "true" - - else - "false" } ) (Json.field.optional.withDefault @@ -88,7 +79,6 @@ coder = , description = Text.fields.settings.syncTime , coder = Json.int , default = Tuple.pair Default.syncTime [] - , defaultToString = String.fromInt } ) diff --git a/src/Internal/Values/Timeline.elm b/src/Internal/Values/Timeline.elm index 9bded76..548b1a1 100644 --- a/src/Internal/Values/Timeline.elm +++ b/src/Internal/Values/Timeline.elm @@ -226,7 +226,6 @@ coder = , description = Text.fields.timeline.filledBatches , coder = Json.int , default = ( 0, [] ) - , defaultToString = String.fromInt } ) (Json.field.required @@ -326,7 +325,6 @@ coderIToken = , description = Text.fields.itoken.starts , coder = Json.set coderIBatchPTRValue , default = ( Set.empty, [] ) - , defaultToString = always "[]" } ) (Json.field.optional.withDefault @@ -335,7 +333,6 @@ coderIToken = , description = Text.fields.itoken.ends , coder = Json.set coderIBatchPTRValue , default = ( Set.empty, [] ) - , defaultToString = always "[]" } ) (Json.field.optional.withDefault @@ -344,7 +341,6 @@ coderIToken = , description = Text.fields.itoken.inFrontOf , coder = Json.set coderITokenPTRValue , default = ( Set.empty, [] ) - , defaultToString = always "[]" } ) (Json.field.optional.withDefault @@ -353,7 +349,6 @@ coderIToken = , description = Text.fields.itoken.behind , coder = Json.set coderITokenPTRValue , default = ( Set.empty, [] ) - , defaultToString = always "[]" } ) diff --git a/tests/Test/Tools/Json.elm b/tests/Test/Tools/Json.elm index 6124615..13d4c3a 100644 --- a/tests/Test/Tools/Json.elm +++ b/tests/Test/Tools/Json.elm @@ -100,7 +100,6 @@ gridField = , description = [] , coder = Json.list (Json.list Json.int) , default = ( [], [] ) - , defaultToString = always "[]" } @@ -132,7 +131,6 @@ hobbiesField = , description = [] , coder = Json.list Json.string , default = ( [], [] ) - , defaultToString = always "[]" } @@ -149,13 +147,6 @@ invitedToPartyField = , description = [] , coder = Json.bool , default = ( False, [] ) - , defaultToString = - \b -> - if b then - "True" - - else - "False" } From 80bb05fd30a7bf49f9b5e5244bf7512db51e7f42 Mon Sep 17 00:00:00 2001 From: Bram Date: Mon, 8 Jul 2024 20:08:32 +0200 Subject: [PATCH 14/34] Add /sync from v1.1 to v1.5 --- src/Internal/Api/Sync/V1.elm | 809 +++++++++++++++++++++++++++++++++++ src/Internal/Api/Sync/V2.elm | 557 ++++++++++++++++++++++++ src/Internal/Api/Sync/V3.elm | 261 +++++++++++ src/Internal/Api/Sync/V4.elm | 436 +++++++++++++++++++ src/Internal/Api/Sync/V5.elm | 249 +++++++++++ 5 files changed, 2312 insertions(+) create mode 100644 src/Internal/Api/Sync/V1.elm create mode 100644 src/Internal/Api/Sync/V2.elm create mode 100644 src/Internal/Api/Sync/V3.elm create mode 100644 src/Internal/Api/Sync/V4.elm create mode 100644 src/Internal/Api/Sync/V5.elm diff --git a/src/Internal/Api/Sync/V1.elm b/src/Internal/Api/Sync/V1.elm new file mode 100644 index 0000000..8260d66 --- /dev/null +++ b/src/Internal/Api/Sync/V1.elm @@ -0,0 +1,809 @@ +module Internal.Api.Sync.V1 exposing (..) + +{-| + + +# Sync response + +This API module represents the /sync endpoint on Matrix spec version v1.1. + + + +-} + +import FastDict exposing (Dict) +import Internal.Tools.Json as Json +import Internal.Tools.StrippedEvent as StrippedEvent exposing (StrippedEvent) +import Internal.Tools.Timestamp as Timestamp exposing (Timestamp) + + +type alias SyncResponse = + { accountData : Maybe AccountData + , deviceLists : Maybe DeviceLists + , deviceOneTimeKeysCount : Maybe (Dict String Int) + , nextBatch : String + , presence : Maybe Presence + , rooms : Maybe Rooms + , toDevice : Maybe ToDevice + } + + +type alias AccountData = + { events : Maybe (List Event) } + + +type alias Event = + { content : Json.Value + , eventType : String + } + + +type alias Presence = + { events : Maybe (List Event) } + + +type alias Rooms = + { invite : Maybe (Dict String InvitedRoom) + , join : Maybe (Dict String JoinedRoom) + , knock : Maybe (Dict String KnockedRoom) + , leave : Maybe (Dict String LeftRoom) + } + + +type alias InvitedRoom = + { inviteState : Maybe InviteState } + + +type alias InviteState = + { events : Maybe (List StrippedState) } + + +type alias StrippedState = + { content : Json.Value + , sender : String + , stateKey : String + , eventType : String + } + + +type alias JoinedRoom = + { accountData : Maybe AccountData + , ephemeral : Maybe Ephemeral + , state : Maybe State + , summary : Maybe RoomSummary + , timeline : Maybe Timeline + , unreadNotifications : Maybe UnreadNotificationCounts + } + + +type alias Ephemeral = + { events : Maybe (List Event) } + + +type alias State = + { events : Maybe (List SyncStateEvent) } + + +type alias SyncStateEvent = + { content : Json.Value + , eventId : String + , originServerTs : Timestamp + , prevContent : Maybe Json.Value + , sender : String + , stateKey : String + , eventType : String + , unsigned : Maybe UnsignedData + } + + +type alias UnsignedData = + { age : Maybe Int + , redactedBecause : Maybe Event + , transactionId : Maybe String + } + + +type alias RoomSummary = + { mHeroes : Maybe (List String) + , mInvitedMemberCount : Maybe Int + , mJoinedMemberCount : Maybe Int + } + + +type alias Timeline = + { events : Maybe (List SyncRoomEvent) + , limited : Maybe Bool + , prevBatch : Maybe String + } + + +type alias SyncRoomEvent = + { content : Json.Value + , eventId : String + , originServerTs : Timestamp + , sender : String + , eventType : String + , unsigned : Maybe UnsignedData + } + + +type alias UnreadNotificationCounts = + { highlightCount : Maybe Int + , notificationCount : Maybe Int + } + + +type alias KnockedRoom = + { knockState : Maybe KnockState } + + +type alias KnockState = + { events : Maybe (List StrippedState) } + + +type alias LeftRoom = + { accountData : Maybe AccountData + , state : Maybe State + , timeline : Maybe Timeline + } + + +type alias DeviceLists = + { changed : Maybe (List String) + , left : Maybe (List String) + } + + +type alias ToDevice = + { events : Maybe (List ToDeviceEvent) } + + +type alias ToDeviceEvent = + { content : Maybe Json.Value + , sender : Maybe String + , eventType : Maybe String + } + + +coderSyncResponse : Json.Coder SyncResponse +coderSyncResponse = + Json.object7 + { name = "SyncResponse" + , description = [ "The event that is returned on a 200 response." ] + , init = SyncResponse + } + (Json.field.optional.value + { fieldName = "account_data" + , toField = .accountData + , description = [ "The global private data created by this user." ] + , coder = coderAccountData + } + ) + (Json.field.optional.value + { fieldName = "device_lists" + , toField = .deviceLists + , description = [ "Information on end-to-end device updates, as specified in End-to-end encryption." ] + , coder = coderDeviceLists + } + ) + (Json.field.optional.value + { fieldName = "device_one_time_keys_count" + , toField = .deviceOneTimeKeysCount + , description = [ "Information on end-to-end encryption keys, as specified in End-to-end encryption." ] + , coder = Json.fastDict Json.int + } + ) + (Json.field.required + { fieldName = "next_batch" + , toField = .nextBatch + , description = [ "Required: The batch token to supply in the since param of the next /sync request." ] + , coder = Json.string + } + ) + (Json.field.optional.value + { fieldName = "presence" + , toField = .presence + , description = [ "The updates to the presence status of other users." ] + , coder = coderPresence + } + ) + (Json.field.optional.value + { fieldName = "rooms" + , toField = .rooms + , description = [ "Updates to rooms." ] + , coder = coderRooms + } + ) + (Json.field.optional.value + { fieldName = "to_device" + , toField = .toDevice + , description = [ "Information on the send-to-device messages for the client device, as defined in Send-to-Device messaging." ] + , coder = coderToDevice + } + ) + + +coderAccountData : Json.Coder AccountData +coderAccountData = + Json.object1 + { name = "AccountData" + , description = [ "The global private data created by this user." ] + , init = AccountData + } + (Json.field.optional.value + { fieldName = "events" + , toField = .events + , description = [ "List of events." ] + , coder = Json.list coderEvent + } + ) + + +coderEvent : Json.Coder Event +coderEvent = + StrippedEvent.coder + + +coderPresence : Json.Coder Presence +coderPresence = + Json.object1 + { name = "Presence" + , description = [ "The updates to the presence status of other users." ] + , init = Presence + } + (Json.field.optional.value + { fieldName = "events" + , toField = .events + , description = [ "List of events." ] + , coder = Json.list coderEvent + } + ) + + +coderRooms : Json.Coder Rooms +coderRooms = + Json.object4 + { name = "Rooms" + , description = [ "Updates to rooms." ] + , init = Rooms + } + (Json.field.optional.value + { fieldName = "invite" + , toField = .invite + , description = [ "The rooms that the user has been invited to, mapped as room ID to room information." ] + , coder = Json.fastDict coderInvitedRoom + } + ) + (Json.field.optional.value + { fieldName = "join" + , toField = .join + , description = [ "The rooms that the user has joined, mapped as room ID to room information." ] + , coder = Json.fastDict coderJoinedRoom + } + ) + (Json.field.optional.value + { fieldName = "knock" + , toField = .knock + , description = [ "The rooms that the user has knocked upon, mapped as room ID to room information." ] + , coder = Json.fastDict coderKnockedRoom + } + ) + (Json.field.optional.value + { fieldName = "leave" + , toField = .leave + , description = [ "The rooms that the user has left or been banned from, mapped as room ID to room information." ] + , coder = Json.fastDict coderLeftRoom + } + ) + + +coderInvitedRoom : Json.Coder InvitedRoom +coderInvitedRoom = + Json.object1 + { name = "InvitedRoom" + , description = [ "The rooms that the user has been invited to, mapped as room ID to room information." ] + , init = InvitedRoom + } + (Json.field.optional.value + { fieldName = "invite_state" + , toField = .inviteState + , description = [ "The state of a room that the user has been invited to.", "These state events may only have the sender, type, state_key and content keys present.", "These events do not replace any state that the client already has for the room, for example if the client has archived the room.", "Instead the client should keep two separate copies of the state: the one from the invite_state and one from the archived state.", "If the client joins the room then the current state will be given as a delta against the archived state not the invite_state." ] + , coder = coderInviteState + } + ) + + +coderInviteState : Json.Coder InviteState +coderInviteState = + Json.object1 + { name = "InviteState" + , description = [ "The state of a room that the user has been invited to." ] + , init = InviteState + } + (Json.field.optional.value + { fieldName = "events" + , toField = .events + , description = [ "The StrippedState events that form the invite state." ] + , coder = Json.list coderStrippedState + } + ) + + +coderStrippedState : Json.Coder StrippedState +coderStrippedState = + Json.object4 + { name = "StrippedState" + , description = [ "The StrippedState events that form the invite state." ] + , init = StrippedState + } + (Json.field.required + { fieldName = "content" + , toField = .content + , description = [ "The content for the event." ] + , coder = Json.value + } + ) + (Json.field.required + { fieldName = "sender" + , toField = .sender + , description = [ "The sender for the event." ] + , coder = Json.string + } + ) + (Json.field.required + { fieldName = "state_key" + , toField = .stateKey + , description = [ "The state_key for the event." ] + , coder = Json.string + } + ) + (Json.field.required + { fieldName = "type" + , toField = .eventType + , description = [ "The type for the event." ] + , coder = Json.string + } + ) + + +coderJoinedRoom : Json.Coder JoinedRoom +coderJoinedRoom = + Json.object6 + { name = "JoinedRoom" + , description = [ "The rooms that the user has joined, mapped as room ID to room information." ] + , init = JoinedRoom + } + (Json.field.optional.value + { fieldName = "account_data" + , toField = .accountData + , description = [ "The private data that this user has attached to this room." ] + , coder = coderAccountData + } + ) + (Json.field.optional.value + { fieldName = "ephemeral" + , toField = .ephemeral + , description = [ "The ephemeral events in the room that aren’t recorded in the timeline or state of the room. e.g. typing." ] + , coder = coderEphemeral + } + ) + (Json.field.optional.value + { fieldName = "state" + , toField = .state + , description = [ "Updates to the state, between the time indicated by the since parameter, and the start of the timeline (or all state up to the start of the timeline, if since is not given, or full_state is true).", "N.B. state updates for m.room.member events will be incomplete if lazy_load_members is enabled in the /sync filter, and only return the member events required to display the senders of the timeline events in this response." ] + , coder = coderState + } + ) + (Json.field.optional.value + { fieldName = "summary" + , toField = .summary + , description = [ "Information about the room which clients may need to correctly render it to users." ] + , coder = coderRoomSummary + } + ) + (Json.field.optional.value + { fieldName = "timeline" + , toField = .timeline + , description = [ "The timeline of messages and state changes in the room." ] + , coder = coderTimeline + } + ) + (Json.field.optional.value + { fieldName = "unread_notifications" + , toField = .unreadNotifications + , description = [ "Counts of unread notifications for this room. See the Receiving notifications section for more information on how these are calculated." ] + , coder = coderUnreadNotificationCounts + } + ) + + +coderEphemeral : Json.Coder Ephemeral +coderEphemeral = + Json.object1 + { name = "Ephemeral" + , description = [ "The ephemeral events in the room that aren’t recorded in the timeline or state of the room. e.g. typing." ] + , init = Ephemeral + } + (Json.field.optional.value + { fieldName = "events" + , toField = .events + , description = [ "List of events." ] + , coder = Json.list coderEvent + } + ) + + +coderState : Json.Coder State +coderState = + Json.object1 + { name = "State" + , description = [ "Updates to the state, between the time indicated by the since parameter, and the start of the timeline (or all state up to the start of the timeline, if since is not given, or full_state is true)." ] + , init = State + } + (Json.field.optional.value + { fieldName = "events" + , toField = .events + , description = [ "List of events." ] + , coder = Json.list coderSyncStateEvent + } + ) + + +coderSyncStateEvent : Json.Coder SyncStateEvent +coderSyncStateEvent = + Json.object8 + { name = "SyncStateEvent" + , description = [ "Represents a state event within a sync response." ] + , init = SyncStateEvent + } + (Json.field.required + { fieldName = "content" + , toField = .content + , description = [ "The fields in this object will vary depending on the type of event. When interacting with the REST API, this is the HTTP body." ] + , coder = Json.value + } + ) + (Json.field.required + { fieldName = "event_id" + , toField = .eventId + , description = [ "The globally unique event identifier." ] + , coder = Json.string + } + ) + (Json.field.required + { fieldName = "origin_server_ts" + , toField = .originServerTs + , description = [ "Timestamp in milliseconds on originating homeserver when this event was sent." ] + , coder = Timestamp.coder + } + ) + (Json.field.optional.value + { fieldName = "prev_content" + , toField = .prevContent + , description = [ "Optional. The previous content for this event. If there is no previous content, this key will be missing." ] + , coder = Json.value + } + ) + (Json.field.required + { fieldName = "sender" + , toField = .sender + , description = [ "Contains the fully-qualified ID of the user who sent this event." ] + , coder = Json.string + } + ) + (Json.field.required + { fieldName = "state_key" + , toField = .stateKey + , description = [ "A unique key which defines the overwriting semantics for this piece of room state.", "This value is often a zero-length string. The presence of this key makes this event a State Event.", "State keys starting with an @ are reserved for referencing user IDs, such as room members.", "With the exception of a few events, state events set with a given user’s ID as the state key MUST only be set by that user." ] + , coder = Json.string + } + ) + (Json.field.required + { fieldName = "type" + , toField = .eventType + , description = [ "The type of event. This SHOULD be namespaced similar to Java package naming conventions e.g. ‘com.example.subdomain.event.type’" ] + , coder = Json.string + } + ) + (Json.field.optional.value + { fieldName = "unsigned" + , toField = .unsigned + , description = [ "Contains optional extra information about the event." ] + , coder = coderUnsignedData + } + ) + + +coderUnsignedData : Json.Coder UnsignedData +coderUnsignedData = + Json.object3 + { name = "UnsignedData" + , description = [ "Contains optional extra information about the event." ] + , init = UnsignedData + } + (Json.field.optional.value + { fieldName = "age" + , toField = .age + , description = [ "The time in milliseconds that has elapsed since the event was sent.", "This field is generated by the local homeserver, and may be incorrect if the local time on at least one of the two servers is out of sync, which can cause the age to either be negative or greater than it actually is." ] + , coder = Json.int + } + ) + (Json.field.optional.value + { fieldName = "redacted_because" + , toField = .redactedBecause + , description = [ "The event that redacted this event, if any." ] + , coder = coderEvent + } + ) + (Json.field.optional.value + { fieldName = "transaction_id" + , toField = .transactionId + , description = [ "The client-supplied transaction ID, for example, provided via PUT /_matrix/client/r0/rooms/{roomId}/send/{eventType}/{txnId}, if the client being given the event is the same one which sent it." ] + , coder = Json.string + } + ) + + +coderRoomSummary : Json.Coder RoomSummary +coderRoomSummary = + Json.object3 + { name = "RoomSummary" + , description = [ "Information about the room which clients may need to correctly render it to users." ] + , init = RoomSummary + } + (Json.field.optional.value + { fieldName = "m.heroes" + , toField = .mHeroes + , description = [ "The users which can be used to generate a room name if the room does not have one. Required if the room’s m.room.name or m.room.canonical_alias state events are unset or empty.", "This should be the first 5 members of the room, ordered by stream ordering, which are joined or invited.", "The list must never include the client’s own user ID.", "When no joined or invited members are available, this should consist of the banned and left users.", "More than 5 members may be provided, however less than 5 should only be provided when there are less than 5 members to represent.", "When lazy-loading room members is enabled, the membership events for the heroes MUST be included in the state, unless they are redundant.", "When the list of users changes, the server notifies the client by sending a fresh list of heroes.", "If there are no changes since the last sync, this field may be omitted." ] + , coder = Json.list Json.string + } + ) + (Json.field.optional.value + { fieldName = "m.invited_member_count" + , toField = .mInvitedMemberCount + , description = [ "The number of users with membership of invite.", "If this field has not changed since the last sync, it may be omitted. Required otherwise." ] + , coder = Json.int + } + ) + (Json.field.optional.value + { fieldName = "m.joined_member_count" + , toField = .mJoinedMemberCount + , description = [ "The number of users with membership of join, including the client’s own user ID.", "If this field has not changed since the last sync, it may be omitted. Required otherwise." ] + , coder = Json.int + } + ) + + +coderTimeline : Json.Coder Timeline +coderTimeline = + Json.object3 + { name = "Timeline" + , description = [ "The timeline of messages and state changes in the room." ] + , init = Timeline + } + (Json.field.optional.value + { fieldName = "events" + , toField = .events + , description = [ "List of events." ] + , coder = Json.list coderSyncRoomEvent + } + ) + (Json.field.optional.value + { fieldName = "limited" + , toField = .limited + , description = [ "True if the number of events returned was limited by the limit on the filter." ] + , coder = Json.bool + } + ) + (Json.field.optional.value + { fieldName = "prev_batch" + , toField = .prevBatch + , description = [ "A token that can be supplied to the from parameter of the /rooms//messages endpoint in order to retrieve earlier events.", "If no earlier events are available, this property may be omitted from the response." ] + , coder = Json.string + } + ) + + +coderSyncRoomEvent : Json.Coder SyncRoomEvent +coderSyncRoomEvent = + Json.object6 + { name = "SyncRoomEvent" + , description = [ "Represents a room event within a sync response." ] + , init = SyncRoomEvent + } + (Json.field.required + { fieldName = "content" + , toField = .content + , description = [ "The fields in this object will vary depending on the type of event. When interacting with the REST API, this is the HTTP body." ] + , coder = Json.value + } + ) + (Json.field.required + { fieldName = "event_id" + , toField = .eventId + , description = [ "The globally unique event identifier." ] + , coder = Json.string + } + ) + (Json.field.required + { fieldName = "origin_server_ts" + , toField = .originServerTs + , description = [ "Timestamp in milliseconds on originating homeserver when this event was sent." ] + , coder = Timestamp.coder + } + ) + (Json.field.required + { fieldName = "sender" + , toField = .sender + , description = [ "Contains the fully-qualified ID of the user who sent this event." ] + , coder = Json.string + } + ) + (Json.field.required + { fieldName = "type" + , toField = .eventType + , description = [ "The type of event. This SHOULD be namespaced similar to Java package naming conventions e.g. ‘com.example.subdomain.event.type’" ] + , coder = Json.string + } + ) + (Json.field.optional.value + { fieldName = "unsigned" + , toField = .unsigned + , description = [ "Contains optional extra information about the event." ] + , coder = coderUnsignedData + } + ) + + +coderUnreadNotificationCounts : Json.Coder UnreadNotificationCounts +coderUnreadNotificationCounts = + Json.object2 + { name = "UnreadNotificationCounts" + , description = [ "Counts of unread notifications for this room." ] + , init = UnreadNotificationCounts + } + (Json.field.optional.value + { fieldName = "highlight_count" + , toField = .highlightCount + , description = [ "The number of unread notifications for this room with the highlight flag set." ] + , coder = Json.int + } + ) + (Json.field.optional.value + { fieldName = "notification_count" + , toField = .notificationCount + , description = [ "The total number of unread notifications for this room." ] + , coder = Json.int + } + ) + + +coderKnockedRoom : Json.Coder KnockedRoom +coderKnockedRoom = + Json.object1 + { name = "KnockedRoom" + , description = [ "The rooms that the user has knocked upon, mapped as room ID to room information." ] + , init = KnockedRoom + } + (Json.field.optional.value + { fieldName = "knock_state" + , toField = .knockState + , description = [ "The state of a room that the user has knocked upon.", "The state events contained here have the same restrictions as InviteState above." ] + , coder = coderKnockState + } + ) + + +coderKnockState : Json.Coder KnockState +coderKnockState = + Json.object1 + { name = "KnockState" + , description = [ "The state of a room that the user has knocked upon." ] + , init = KnockState + } + (Json.field.optional.value + { fieldName = "events" + , toField = .events + , description = [ "The StrippedState events that form the knock state." ] + , coder = Json.list coderStrippedState + } + ) + + +coderLeftRoom : Json.Coder LeftRoom +coderLeftRoom = + Json.object3 + { name = "LeftRoom" + , description = [ "The rooms that the user has left or been banned from, mapped as room ID to room information." ] + , init = LeftRoom + } + (Json.field.optional.value + { fieldName = "account_data" + , toField = .accountData + , description = [ "The private data that this user has attached to this room." ] + , coder = coderAccountData + } + ) + (Json.field.optional.value + { fieldName = "state" + , toField = .state + , description = [ "The state updates for the room up to the start of the timeline." ] + , coder = coderState + } + ) + (Json.field.optional.value + { fieldName = "timeline" + , toField = .timeline + , description = [ "The timeline of messages and state changes in the room up to the point when the user left." ] + , coder = coderTimeline + } + ) + + +coderDeviceLists : Json.Coder DeviceLists +coderDeviceLists = + Json.object2 + { name = "DeviceLists" + , description = [ "Information on end-to-end device updates, as specified in End-to-end encryption." ] + , init = DeviceLists + } + (Json.field.optional.value + { fieldName = "changed" + , toField = .changed + , description = [ "List of users who have updated their device identity or cross-signing keys, or who now share an encrypted room with the client since the previous sync response." ] + , coder = Json.list Json.string + } + ) + (Json.field.optional.value + { fieldName = "left" + , toField = .left + , description = [ "List of users with whom we do not share any encrypted rooms anymore since the previous sync response." ] + , coder = Json.list Json.string + } + ) + + +coderToDevice : Json.Coder ToDevice +coderToDevice = + Json.object1 + { name = "ToDevice" + , description = [ "Information on the send-to-device messages for the client device, as defined in Send-to-Device messaging." ] + , init = ToDevice + } + (Json.field.optional.value + { fieldName = "events" + , toField = .events + , description = [ "List of send-to-device messages." ] + , coder = Json.list coderToDeviceEvent + } + ) + + +coderToDeviceEvent : Json.Coder ToDeviceEvent +coderToDeviceEvent = + Json.object3 + { name = "ToDeviceEvent" + , description = [ "An event." ] + , init = ToDeviceEvent + } + (Json.field.optional.value + { fieldName = "content" + , toField = .content + , description = [ "The content of this event. The fields in this object will vary depending on the type of event." ] + , coder = Json.value + } + ) + (Json.field.optional.value + { fieldName = "sender" + , toField = .sender + , description = [ "The Matrix user ID of the user who sent this event." ] + , coder = Json.string + } + ) + (Json.field.optional.value + { fieldName = "type" + , toField = .eventType + , description = [ "The type of event." ] + , coder = Json.string + } + ) diff --git a/src/Internal/Api/Sync/V2.elm b/src/Internal/Api/Sync/V2.elm new file mode 100644 index 0000000..ee34599 --- /dev/null +++ b/src/Internal/Api/Sync/V2.elm @@ -0,0 +1,557 @@ +module Internal.Api.Sync.V2 exposing (..) + +{-| + + +# Sync response + +This API module represents the /sync endpoint on Matrix spec version v1.1. + + + +-} + +import FastDict exposing (Dict) +import Internal.Api.Sync.V1 as PV +import Internal.Tools.Json as Json + + +type alias SyncResponse = + { accountData : Maybe AccountData + , deviceLists : Maybe DeviceLists + , deviceOneTimeKeysCount : Maybe (Dict String Int) + , deviceUnusedFallbackKeyTypes : List String + , nextBatch : String + , presence : Maybe Presence + , rooms : Maybe Rooms + , toDevice : Maybe ToDevice + } + + +type alias AccountData = + { events : Maybe (List Event) } + + +type alias Event = + { content : Json.Value + , eventType : String + } + + +type alias Presence = + { events : Maybe (List Event) } + + +type alias Rooms = + { invite : Maybe (Dict String InvitedRoom) + , join : Maybe (Dict String JoinedRoom) + , knock : Maybe (Dict String KnockedRoom) + , leave : Maybe (Dict String LeftRoom) + } + + +type alias InvitedRoom = + { inviteState : Maybe InviteState } + + +type alias InviteState = + { events : Maybe (List StrippedStateEvent) } + + +type alias StrippedStateEvent = + { content : Json.Value + , sender : String + , stateKey : String + , eventType : String + } + + +type alias JoinedRoom = + { accountData : Maybe AccountData + , ephemeral : Maybe Ephemeral + , state : Maybe State + , summary : Maybe RoomSummary + , timeline : Maybe Timeline + , unreadNotifications : Maybe UnreadNotificationCounts + } + + +type alias Ephemeral = + { events : Maybe (List Event) } + + +type alias State = + { events : Maybe (List ClientEventWithoutRoomID) } + + +type alias ClientEventWithoutRoomID = + { content : Json.Value + , eventId : String + , originServerTs : Int + , sender : String + , stateKey : Maybe String + , eventType : String + , unsigned : Maybe UnsignedData + } + + +type UnsignedData + = UnsignedData + { age : Maybe Int + , prevContent : Maybe Json.Value + , redactedBecause : Maybe ClientEventWithoutRoomID + , transactionId : Maybe String + } + + +type alias RoomSummary = + { mHeroes : Maybe (List String) + , mInvitedMemberCount : Maybe Int + , mJoinedMemberCount : Maybe Int + } + + +type alias Timeline = + { events : List ClientEventWithoutRoomID + , limited : Maybe Bool + , prevBatch : Maybe String + } + + +type alias UnreadNotificationCounts = + { highlightCount : Maybe Int + , notificationCount : Maybe Int + } + + +type alias KnockedRoom = + { knockState : Maybe KnockState } + + +type alias KnockState = + { events : Maybe (List StrippedStateEvent) } + + +type alias LeftRoom = + { accountData : Maybe AccountData + , state : Maybe State + , timeline : Maybe Timeline + } + + +type alias DeviceLists = + { changed : Maybe (List String) + , left : Maybe (List String) + } + + +type alias ToDevice = + { events : Maybe (List ToDeviceEvent) } + + +type alias ToDeviceEvent = + { content : Maybe Json.Value + , sender : Maybe String + , eventType : Maybe String + } + + +coderSyncResponse : Json.Coder SyncResponse +coderSyncResponse = + Json.object8 + { name = "SyncResponse" + , description = [ "An event that is part of a response." ] + , init = SyncResponse + } + (Json.field.optional.value + { fieldName = "account_data" + , toField = .accountData + , description = [ "The global private data created by this user." ] + , coder = coderAccountData + } + ) + (Json.field.optional.value + { fieldName = "device_lists" + , toField = .deviceLists + , description = [ "Information on end-to-end device updates, as specified in End-to-end encryption." ] + , coder = coderDeviceLists + } + ) + (Json.field.optional.value + { fieldName = "device_one_time_keys_count" + , toField = .deviceOneTimeKeysCount + , description = [ "Information on end-to-end encryption keys, as specified in End-to-end encryption." ] + , coder = Json.fastDict Json.int + } + ) + (Json.field.required + { fieldName = "device_unused_fallback_key_types" + , toField = .deviceUnusedFallbackKeyTypes + , description = [ "The unused fallback key algorithms." ] + , coder = Json.list Json.string + } + ) + (Json.field.required + { fieldName = "next_batch" + , toField = .nextBatch + , description = [ "Required: The batch token to supply in the since param of the next /sync request." ] + , coder = Json.string + } + ) + (Json.field.optional.value + { fieldName = "presence" + , toField = .presence + , description = [ "The updates to the presence status of other users." ] + , coder = coderPresence + } + ) + (Json.field.optional.value + { fieldName = "rooms" + , toField = .rooms + , description = [ "Updates to rooms." ] + , coder = coderRooms + } + ) + (Json.field.optional.value + { fieldName = "to_device" + , toField = .toDevice + , description = [ "Information on the send-to-device messages for the client device, as defined in Send-to-Device messaging." ] + , coder = coderToDevice + } + ) + + +coderAccountData : Json.Coder AccountData +coderAccountData = + PV.coderAccountData + + +coderEvent : Json.Coder Event +coderEvent = + PV.coderEvent + + +coderPresence : Json.Coder Presence +coderPresence = + PV.coderPresence + + +coderRooms : Json.Coder Rooms +coderRooms = + Json.object4 + { name = "Rooms" + , description = [ "Updates to rooms." ] + , init = Rooms + } + (Json.field.optional.value + { fieldName = "invite" + , toField = .invite + , description = [ "The rooms that the user has been invited to, mapped as room ID to room information." ] + , coder = Json.fastDict coderInvitedRoom + } + ) + (Json.field.optional.value + { fieldName = "join" + , toField = .join + , description = [ "The rooms that the user has joined, mapped as room ID to room information." ] + , coder = Json.fastDict coderJoinedRoom + } + ) + (Json.field.optional.value + { fieldName = "knock" + , toField = .knock + , description = [ "The rooms that the user has knocked upon, mapped as room ID to room information." ] + , coder = Json.fastDict coderKnockedRoom + } + ) + (Json.field.optional.value + { fieldName = "leave" + , toField = .leave + , description = [ "The rooms that the user has left or been banned from, mapped as room ID to room information." ] + , coder = Json.fastDict coderLeftRoom + } + ) + + +coderInvitedRoom : Json.Coder InvitedRoom +coderInvitedRoom = + PV.coderInvitedRoom + + +coderInviteState : Json.Coder InviteState +coderInviteState = + PV.coderInviteState + + +coderStrippedStateEvent : Json.Coder StrippedStateEvent +coderStrippedStateEvent = + PV.coderStrippedState + + +coderJoinedRoom : Json.Coder JoinedRoom +coderJoinedRoom = + Json.object6 + { name = "JoinedRoom" + , description = [ "The rooms that the user has joined." ] + , init = JoinedRoom + } + (Json.field.optional.value + { fieldName = "account_data" + , toField = .accountData + , description = [ "The private data that this user has attached to this room." ] + , coder = coderAccountData + } + ) + (Json.field.optional.value + { fieldName = "ephemeral" + , toField = .ephemeral + , description = [ "The ephemeral events in the room that aren’t recorded in the timeline or state of the room. e.g. typing." ] + , coder = coderEphemeral + } + ) + (Json.field.optional.value + { fieldName = "state" + , toField = .state + , description = [ "Updates to the state, between the time indicated by the since parameter, and the start of the timeline (or all state up to the start of the timeline, if since is not given, or full_state is true).", "N.B. state updates for m.room.member events will be incomplete if lazy_load_members is enabled in the /sync filter, and only return the member events required to display the senders of the timeline events in this response." ] + , coder = coderState + } + ) + (Json.field.optional.value + { fieldName = "summary" + , toField = .summary + , description = [ "Information about the room which clients may need to correctly render it to users." ] + , coder = coderRoomSummary + } + ) + (Json.field.optional.value + { fieldName = "timeline" + , toField = .timeline + , description = [ "The timeline of messages and state changes in the room." ] + , coder = coderTimeline + } + ) + (Json.field.optional.value + { fieldName = "unread_notifications" + , toField = .unreadNotifications + , description = [ "Counts of unread notifications for this room. See the Receiving notifications section for more information on how these are calculated." ] + , coder = coderUnreadNotificationCounts + } + ) + + +coderEphemeral : Json.Coder Ephemeral +coderEphemeral = + PV.coderEphemeral + + +coderState : Json.Coder State +coderState = + Json.object1 + { name = "State" + , description = [ "Updates to the state." ] + , init = State + } + (Json.field.optional.value + { fieldName = "events" + , toField = .events + , description = [ "List of events." ] + , coder = Json.list coderClientEventWithoutRoomID + } + ) + + +coderClientEventWithoutRoomID : Json.Coder ClientEventWithoutRoomID +coderClientEventWithoutRoomID = + Json.object7 + { name = "ClientEventWithoutRoomID" + , description = [ "An event without a room ID." ] + , init = ClientEventWithoutRoomID + } + (Json.field.required + { fieldName = "content" + , toField = .content + , description = [ "Required: The body of this event, as created by the client which sent it." ] + , coder = Json.value + } + ) + (Json.field.required + { fieldName = "event_id" + , toField = .eventId + , description = [ "Required: The globally unique identifier for this event." ] + , coder = Json.string + } + ) + (Json.field.required + { 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 + } + ) + (Json.field.required + { fieldName = "sender" + , toField = .sender + , description = [ "Required: Contains the fully-qualified ID of the user who sent this event." ] + , coder = Json.string + } + ) + (Json.field.optional.value + { fieldName = "state_key" + , toField = .stateKey + , description = [ "Present if, and only if, this event is a state event. The key making this piece of state unique in the room. Note that it is often an empty string." ] + , coder = Json.string + } + ) + (Json.field.required + { fieldName = "type" + , toField = .eventType + , description = [ "Required: The type of the event." ] + , coder = Json.string + } + ) + (Json.field.optional.value + { fieldName = "unsigned" + , toField = .unsigned + , description = [ "Contains optional extra information about the event." ] + , coder = coderUnsignedData + } + ) + + +coderUnsignedData : Json.Coder UnsignedData +coderUnsignedData = + Json.object4 + { name = "UnsignedData" + , description = [ "Contains optional extra information about the event." ] + , init = + \a b c d -> + UnsignedData + { age = a + , prevContent = b + , redactedBecause = c + , transactionId = d + } + } + (Json.field.optional.value + { fieldName = "age" + , toField = \(UnsignedData u) -> u.age + , description = [ "The time in milliseconds that has elapsed since the event was sent. This field is generated by the local homeserver, and may be incorrect if the local time on at least one of the two servers is out of sync, which can cause the age to either be negative or greater than it actually is." ] + , coder = Json.int + } + ) + (Json.field.optional.value + { fieldName = "prev_content" + , toField = \(UnsignedData u) -> u.prevContent + , description = [ "The previous content for this event. This field is generated by the local homeserver, and is only returned if the event is a state event, and the client has permission to see the previous content.", "Changed in v1.2: Previously, this field was specified at the top level of returned events rather than in unsigned (with the exception of the GET .../notifications endpoint), though in practice no known server implementations honoured this." ] + , coder = Json.value + } + ) + (Json.field.optional.value + { fieldName = "redacted_because" + , toField = \(UnsignedData u) -> u.redactedBecause + , description = [ "The event that redacted this event, if any." ] + , coder = Json.lazy (\_ -> coderClientEventWithoutRoomID) + } + ) + (Json.field.optional.value + { fieldName = "transaction_id" + , toField = \(UnsignedData u) -> u.transactionId + , description = [ "The client-supplied transaction ID, for example, provided via PUT /_matrix/client/v3/rooms/{roomId}/send/{eventType}/{txnId}, if the client being given the event is the same one which sent it." ] + , coder = Json.string + } + ) + + +coderRoomSummary : Json.Coder RoomSummary +coderRoomSummary = + PV.coderRoomSummary + + +coderTimeline : Json.Coder Timeline +coderTimeline = + Json.object3 + { name = "Timeline" + , description = [ "The timeline of messages and state changes in the room." ] + , init = Timeline + } + (Json.field.required + { fieldName = "events" + , toField = .events + , description = [ "Required: List of events." ] + , coder = Json.list coderClientEventWithoutRoomID + } + ) + (Json.field.optional.value + { fieldName = "limited" + , toField = .limited + , description = [ "True if the number of events returned was limited by the limit on the filter." ] + , coder = Json.bool + } + ) + (Json.field.optional.value + { fieldName = "prev_batch" + , toField = .prevBatch + , description = [ "A token that can be supplied to the from parameter of the /rooms//messages endpoint in order to retrieve earlier events. If no earlier events are available, this property may be omitted from the response." ] + , coder = Json.string + } + ) + + +coderUnreadNotificationCounts : Json.Coder UnreadNotificationCounts +coderUnreadNotificationCounts = + PV.coderUnreadNotificationCounts + + +coderKnockedRoom : Json.Coder KnockedRoom +coderKnockedRoom = + PV.coderKnockedRoom + + +coderKnockState : Json.Coder KnockState +coderKnockState = + PV.coderKnockState + + +coderLeftRoom : Json.Coder LeftRoom +coderLeftRoom = + Json.object3 + { name = "LeftRoom" + , description = [ "The rooms that the user has left or been banned from." ] + , init = LeftRoom + } + (Json.field.optional.value + { fieldName = "account_data" + , toField = .accountData + , description = [ "The private data that this user has attached to this room." ] + , coder = coderAccountData + } + ) + (Json.field.optional.value + { fieldName = "state" + , toField = .state + , description = [ "The state updates for the room up to the start of the timeline." ] + , coder = coderState + } + ) + (Json.field.optional.value + { fieldName = "timeline" + , toField = .timeline + , description = [ "The timeline of messages and state changes in the room up to the point when the user left." ] + , coder = coderTimeline + } + ) + + +coderDeviceLists : Json.Coder DeviceLists +coderDeviceLists = + PV.coderDeviceLists + + +coderToDevice : Json.Coder ToDevice +coderToDevice = + PV.coderToDevice + + +coderToDeviceEvent : Json.Coder ToDeviceEvent +coderToDeviceEvent = + PV.coderToDeviceEvent diff --git a/src/Internal/Api/Sync/V3.elm b/src/Internal/Api/Sync/V3.elm new file mode 100644 index 0000000..2f7bafb --- /dev/null +++ b/src/Internal/Api/Sync/V3.elm @@ -0,0 +1,261 @@ +module Internal.Api.Sync.V3 exposing (..) + +{-| + + +# Sync response + +This API module represents the /sync endpoint on Matrix spec version v1.1. + + + +-} + +import FastDict exposing (Dict) +import Internal.Api.Sync.V2 as PV +import Internal.Tools.Json as Json + + +type alias SyncResponse = + { accountData : Maybe AccountData + , deviceLists : Maybe DeviceLists + , deviceOneTimeKeysCount : Maybe (Dict String Int) + , deviceUnusedFallbackKeyTypes : List String + , nextBatch : String + , presence : Maybe Presence + , rooms : Maybe Rooms + , toDevice : Maybe ToDevice + } + + +type alias AccountData = + { events : Maybe (List Event) } + + +type alias Event = + { content : Json.Value + , eventType : String + } + + +type alias Presence = + { events : Maybe (List Event) } + + +type alias Rooms = + { invite : Maybe (Dict String InvitedRoom) + , join : Maybe (Dict String JoinedRoom) + , knock : Maybe (Dict String KnockedRoom) + , leave : Maybe (Dict String LeftRoom) + } + + +type alias InvitedRoom = + { inviteState : Maybe InviteState } + + +type alias InviteState = + { events : Maybe (List StrippedStateEvent) } + + +type alias StrippedStateEvent = + { content : Json.Value + , sender : String + , stateKey : String + , eventType : String + } + + +type alias JoinedRoom = + { accountData : Maybe AccountData + , ephemeral : Maybe Ephemeral + , state : Maybe State + , summary : Maybe RoomSummary + , timeline : Maybe Timeline + , unreadNotifications : Maybe UnreadNotificationCounts + } + + +type alias Ephemeral = + { events : Maybe (List Event) } + + +type alias State = + { events : Maybe (List ClientEventWithoutRoomID) } + + +type alias ClientEventWithoutRoomID = + { content : Json.Value + , eventId : String + , originServerTs : Int + , sender : String + , stateKey : Maybe String + , eventType : String + , unsigned : Maybe UnsignedData + } + + +type alias UnsignedData = + PV.UnsignedData + + +type alias RoomSummary = + { mHeroes : Maybe (List String) + , mInvitedMemberCount : Maybe Int + , mJoinedMemberCount : Maybe Int + } + + +type alias Timeline = + { events : List ClientEventWithoutRoomID + , limited : Maybe Bool + , prevBatch : Maybe String + } + + +type alias UnreadNotificationCounts = + { highlightCount : Maybe Int + , notificationCount : Maybe Int + } + + +type alias KnockedRoom = + { knockState : Maybe KnockState } + + +type alias KnockState = + { events : Maybe (List StrippedStateEvent) } + + +type alias LeftRoom = + { accountData : Maybe AccountData + , state : Maybe State + , timeline : Maybe Timeline + } + + +type alias DeviceLists = + { changed : Maybe (List String) + , left : Maybe (List String) + } + + +type alias ToDevice = + { events : Maybe (List ToDeviceEvent) } + + +type alias ToDeviceEvent = + { content : Maybe Json.Value + , sender : Maybe String + , eventType : Maybe String + } + + +coderSyncResponse : Json.Coder SyncResponse +coderSyncResponse = + PV.coderSyncResponse + + +coderAccountData : Json.Coder AccountData +coderAccountData = + PV.coderAccountData + + +coderEvent : Json.Coder Event +coderEvent = + PV.coderEvent + + +coderPresence : Json.Coder Presence +coderPresence = + PV.coderPresence + + +coderRooms : Json.Coder Rooms +coderRooms = + PV.coderRooms + + +coderInvitedRoom : Json.Coder InvitedRoom +coderInvitedRoom = + PV.coderInvitedRoom + + +coderInviteState : Json.Coder InviteState +coderInviteState = + PV.coderInviteState + + +coderStrippedStateEvent : Json.Coder StrippedStateEvent +coderStrippedStateEvent = + PV.coderStrippedStateEvent + + +coderJoinedRoom : Json.Coder JoinedRoom +coderJoinedRoom = + PV.coderJoinedRoom + + +coderEphemeral : Json.Coder Ephemeral +coderEphemeral = + PV.coderEphemeral + + +coderState : Json.Coder State +coderState = + PV.coderState + + +coderClientEventWithoutRoomID : Json.Coder ClientEventWithoutRoomID +coderClientEventWithoutRoomID = + PV.coderClientEventWithoutRoomID + + +coderUnsignedData : Json.Coder UnsignedData +coderUnsignedData = + PV.coderUnsignedData + + +coderRoomSummary : Json.Coder RoomSummary +coderRoomSummary = + PV.coderRoomSummary + + +coderTimeline : Json.Coder Timeline +coderTimeline = + PV.coderTimeline + + +coderUnreadNotificationCounts : Json.Coder UnreadNotificationCounts +coderUnreadNotificationCounts = + PV.coderUnreadNotificationCounts + + +coderKnockedRoom : Json.Coder KnockedRoom +coderKnockedRoom = + PV.coderKnockedRoom + + +coderKnockState : Json.Coder KnockState +coderKnockState = + PV.coderKnockState + + +coderLeftRoom : Json.Coder LeftRoom +coderLeftRoom = + PV.coderLeftRoom + + +coderDeviceLists : Json.Coder DeviceLists +coderDeviceLists = + PV.coderDeviceLists + + +coderToDevice : Json.Coder ToDevice +coderToDevice = + PV.coderToDevice + + +coderToDeviceEvent : Json.Coder ToDeviceEvent +coderToDeviceEvent = + PV.coderToDeviceEvent diff --git a/src/Internal/Api/Sync/V4.elm b/src/Internal/Api/Sync/V4.elm new file mode 100644 index 0000000..d65b5a5 --- /dev/null +++ b/src/Internal/Api/Sync/V4.elm @@ -0,0 +1,436 @@ +module Internal.Api.Sync.V4 exposing (..) + +{-| + + +# Sync response + +This API module represents the /sync endpoint on Matrix spec version v1.1. + + + +-} + +import FastDict exposing (Dict) +import Internal.Api.Sync.V3 as PV +import Internal.Tools.Json as Json + + +type alias SyncResponse = + { accountData : Maybe AccountData + , deviceLists : Maybe DeviceLists + , deviceOneTimeKeysCount : Maybe (Dict String Int) + , deviceUnusedFallbackKeyTypes : List String + , nextBatch : String + , presence : Maybe Presence + , rooms : Maybe Rooms + , toDevice : Maybe ToDevice + } + + +type alias AccountData = + { events : Maybe (List Event) } + + +type alias Event = + { content : Json.Value + , eventType : String + } + + +type alias Presence = + { events : Maybe (List Event) } + + +type alias Rooms = + { invite : Maybe (Dict String InvitedRoom) + , join : Maybe (Dict String JoinedRoom) + , knock : Maybe (Dict String KnockedRoom) + , leave : Maybe (Dict String LeftRoom) + } + + +type alias InvitedRoom = + { inviteState : Maybe InviteState } + + +type alias InviteState = + { events : Maybe (List StrippedStateEvent) } + + +type alias StrippedStateEvent = + { content : Json.Value + , sender : String + , stateKey : String + , eventType : String + } + + +type alias JoinedRoom = + { accountData : Maybe AccountData + , ephemeral : Maybe Ephemeral + , state : Maybe State + , summary : Maybe RoomSummary + , timeline : Maybe Timeline + , unreadNotifications : Maybe UnreadNotificationCounts + , unreadThreadNotifications : Maybe (Dict String ThreadNotificationCounts) + } + + +type alias Ephemeral = + { events : Maybe (List Event) } + + +type alias State = + { events : Maybe (List ClientEventWithoutRoomID) } + + +type alias ClientEventWithoutRoomID = + { content : Json.Value + , eventId : String + , originServerTs : Int + , sender : String + , stateKey : Maybe String + , eventType : String + , unsigned : Maybe UnsignedData + } + + +type alias UnsignedData = + PV.UnsignedData + + +type alias RoomSummary = + { mHeroes : Maybe (List String) + , mInvitedMemberCount : Maybe Int + , mJoinedMemberCount : Maybe Int + } + + +type alias Timeline = + { events : List ClientEventWithoutRoomID + , limited : Maybe Bool + , prevBatch : Maybe String + } + + +type alias UnreadNotificationCounts = + { highlightCount : Maybe Int + , notificationCount : Maybe Int + } + + +type alias ThreadNotificationCounts = + { highlightCount : Maybe Int + , notificationCount : Maybe Int + } + + +type alias KnockedRoom = + { knockState : Maybe KnockState } + + +type alias KnockState = + { events : Maybe (List StrippedStateEvent) } + + +type alias LeftRoom = + { accountData : Maybe AccountData + , state : Maybe State + , timeline : Maybe Timeline + } + + +type alias DeviceLists = + { changed : Maybe (List String) + , left : Maybe (List String) + } + + +type alias ToDevice = + { events : Maybe (List ToDeviceEvent) } + + +type alias ToDeviceEvent = + { content : Maybe Json.Value + , sender : Maybe String + , eventType : Maybe String + } + + +coderSyncResponse : Json.Coder SyncResponse +coderSyncResponse = + Json.object8 + { name = "SyncResponse" + , description = [ "The response for a sync request." ] + , init = SyncResponse + } + (Json.field.optional.value + { fieldName = "account_data" + , toField = .accountData + , description = [ "The global private data created by this user." ] + , coder = coderAccountData + } + ) + (Json.field.optional.value + { fieldName = "device_lists" + , toField = .deviceLists + , description = [ "Information on end-to-end device updates, as specified in End-to-end encryption." ] + , coder = coderDeviceLists + } + ) + (Json.field.optional.value + { fieldName = "device_one_time_keys_count" + , toField = .deviceOneTimeKeysCount + , description = [ "Information on end-to-end encryption keys, as specified in End-to-end encryption." ] + , coder = Json.fastDict Json.int + } + ) + (Json.field.required + { fieldName = "device_unused_fallback_key_types" + , toField = .deviceUnusedFallbackKeyTypes + , description = [ "The unused fallback key algorithms." ] + , coder = Json.list Json.string + } + ) + (Json.field.required + { fieldName = "next_batch" + , toField = .nextBatch + , description = [ "The batch token to supply in the since param of the next /sync request." ] + , coder = Json.string + } + ) + (Json.field.optional.value + { fieldName = "presence" + , toField = .presence + , description = [ "The updates to the presence status of other users." ] + , coder = coderPresence + } + ) + (Json.field.optional.value + { fieldName = "rooms" + , toField = .rooms + , description = [ "Updates to rooms." ] + , coder = coderRooms + } + ) + (Json.field.optional.value + { fieldName = "to_device" + , toField = .toDevice + , description = [ "Information on the send-to-device messages for the client device, as defined in Send-to-Device messaging." ] + , coder = coderToDevice + } + ) + + +coderAccountData : Json.Coder AccountData +coderAccountData = + PV.coderAccountData + + +coderEvent : Json.Coder Event +coderEvent = + PV.coderEvent + + +coderPresence : Json.Coder Presence +coderPresence = + PV.coderPresence + + +coderRooms : Json.Coder Rooms +coderRooms = + Json.object4 + { name = "Rooms" + , description = [ "Updates to rooms." ] + , init = Rooms + } + (Json.field.optional.value + { fieldName = "invite" + , toField = .invite + , description = [ "The rooms that the user has been invited to, mapped as room ID to room information." ] + , coder = Json.fastDict coderInvitedRoom + } + ) + (Json.field.optional.value + { fieldName = "join" + , toField = .join + , description = [ "The rooms that the user has joined, mapped as room ID to room information." ] + , coder = Json.fastDict coderJoinedRoom + } + ) + (Json.field.optional.value + { fieldName = "knock" + , toField = .knock + , description = [ "The rooms that the user has knocked upon, mapped as room ID to room information." ] + , coder = Json.fastDict coderKnockedRoom + } + ) + (Json.field.optional.value + { fieldName = "leave" + , toField = .leave + , description = [ "The rooms that the user has left or been banned from, mapped as room ID to room information." ] + , coder = Json.fastDict coderLeftRoom + } + ) + + +coderInvitedRoom : Json.Coder InvitedRoom +coderInvitedRoom = + PV.coderInvitedRoom + + +coderInviteState : Json.Coder InviteState +coderInviteState = + PV.coderInviteState + + +coderStrippedStateEvent : Json.Coder StrippedStateEvent +coderStrippedStateEvent = + PV.coderStrippedStateEvent + + +coderJoinedRoom : Json.Coder JoinedRoom +coderJoinedRoom = + Json.object7 + { name = "JoinedRoom" + , description = [ "Information about a room the user has joined." ] + , init = JoinedRoom + } + (Json.field.optional.value + { fieldName = "account_data" + , toField = .accountData + , description = [ "The private data that this user has attached to this room." ] + , coder = coderAccountData + } + ) + (Json.field.optional.value + { fieldName = "ephemeral" + , toField = .ephemeral + , description = [ "The ephemeral events in the room that aren’t recorded in the timeline or state of the room. e.g. typing." ] + , coder = coderEphemeral + } + ) + (Json.field.optional.value + { fieldName = "state" + , toField = .state + , description = [ "Updates to the state, between the time indicated by the since parameter, and the start of the timeline (or all state up to the start of the timeline, if since is not given, or full_state is true).", "N.B. state updates for m.room.member events will be incomplete if lazy_load_members is enabled in the /sync filter, and only return the member events required to display the senders of the timeline events in this response." ] + , coder = coderState + } + ) + (Json.field.optional.value + { fieldName = "summary" + , toField = .summary + , description = [ "Information about the room which clients may need to correctly render it to users." ] + , coder = coderRoomSummary + } + ) + (Json.field.optional.value + { fieldName = "timeline" + , toField = .timeline + , description = [ "The timeline of messages and state changes in the room." ] + , coder = coderTimeline + } + ) + (Json.field.optional.value + { fieldName = "unread_notifications" + , toField = .unreadNotifications + , description = [ "Counts of unread notifications for this room. See the Receiving notifications section for more information on how these are calculated.", "If unread_thread_notifications was specified as true on the RoomEventFilter, these counts will only be for the main timeline rather than all events in the room. See the threading module for more information.", "Changed in v1.4: Updated to reflect behaviour of having unread_thread_notifications as true in the RoomEventFilter for /sync." ] + , coder = coderUnreadNotificationCounts + } + ) + (Json.field.optional.value + { fieldName = "unread_thread_notifications" + , toField = .unreadThreadNotifications + , description = [ "If unread_thread_notifications was specified as true on the RoomEventFilter, the notification counts for each thread in this room. The object is keyed by thread root ID, with values matching unread_notifications.", "If a thread does not have any notifications it can be omitted from this object. If no threads have notification counts, this whole object can be omitted.", "Added in v1.4" ] + , coder = Json.fastDict coderThreadNotificationCounts + } + ) + + +coderEphemeral : Json.Coder Ephemeral +coderEphemeral = + PV.coderEphemeral + + +coderState : Json.Coder State +coderState = + PV.coderState + + +coderClientEventWithoutRoomID : Json.Coder ClientEventWithoutRoomID +coderClientEventWithoutRoomID = + PV.coderClientEventWithoutRoomID + + +coderUnsignedData : Json.Coder UnsignedData +coderUnsignedData = + PV.coderUnsignedData + + +coderRoomSummary : Json.Coder RoomSummary +coderRoomSummary = + PV.coderRoomSummary + + +coderTimeline : Json.Coder Timeline +coderTimeline = + PV.coderTimeline + + +coderUnreadNotificationCounts : Json.Coder UnreadNotificationCounts +coderUnreadNotificationCounts = + PV.coderUnreadNotificationCounts + + +coderThreadNotificationCounts : Json.Coder ThreadNotificationCounts +coderThreadNotificationCounts = + Json.object2 + { name = "ThreadNotificationCounts" + , description = [ "The notification counts for each thread in this room." ] + , init = ThreadNotificationCounts + } + (Json.field.optional.value + { fieldName = "highlight_count" + , toField = .highlightCount + , description = [ "The number of unread notifications for this thread with the highlight flag set." ] + , coder = Json.int + } + ) + (Json.field.optional.value + { fieldName = "notification_count" + , toField = .notificationCount + , description = [ "The total number of unread notifications for this thread." ] + , coder = Json.int + } + ) + + +coderKnockedRoom : Json.Coder KnockedRoom +coderKnockedRoom = + PV.coderKnockedRoom + + +coderKnockState : Json.Coder KnockState +coderKnockState = + PV.coderKnockState + + +coderLeftRoom : Json.Coder LeftRoom +coderLeftRoom = + PV.coderLeftRoom + + +coderDeviceLists : Json.Coder DeviceLists +coderDeviceLists = + PV.coderDeviceLists + + +coderToDevice : Json.Coder ToDevice +coderToDevice = + PV.coderToDevice + + +coderToDeviceEvent : Json.Coder ToDeviceEvent +coderToDeviceEvent = + PV.coderToDeviceEvent diff --git a/src/Internal/Api/Sync/V5.elm b/src/Internal/Api/Sync/V5.elm new file mode 100644 index 0000000..0e85d1c --- /dev/null +++ b/src/Internal/Api/Sync/V5.elm @@ -0,0 +1,249 @@ +module Internal.Api.Sync.V5 exposing (..) + +{-| + + +# Sync response + +This API module represents the /sync endpoint on Matrix spec version v1.1. + + + +-} + +import FastDict exposing (Dict) +import Internal.Api.Sync.V4 as PV +import Internal.Tools.Json as Json + + +type alias SyncResponse = + { accountData : Maybe AccountData + , deviceLists : Maybe DeviceLists + , deviceOneTimeKeysCount : Maybe (Dict String Int) + , deviceUnusedFallbackKeyTypes : List String + , nextBatch : String + , presence : Maybe Presence + , rooms : Maybe Rooms + , toDevice : Maybe ToDevice + } + + +type alias AccountData = + { events : Maybe (List Event) } + + +type alias Event = + { content : Json.Value + , eventType : String + } + + +type alias Presence = + { events : Maybe (List Event) } + + +type alias Rooms = + { invite : Maybe (Dict String InvitedRoom) + , join : Maybe (Dict String JoinedRoom) + , knock : Maybe (Dict String KnockedRoom) + , leave : Maybe (Dict String LeftRoom) + } + + +type alias InvitedRoom = + { inviteState : Maybe InviteState } + + +type alias InviteState = + { events : Maybe (List StrippedStateEvent) } + + +type alias StrippedStateEvent = + { content : Json.Value + , sender : String + , stateKey : String + , eventType : String + } + + +type alias JoinedRoom = + { accountData : Maybe AccountData + , ephemeral : Maybe Ephemeral + , state : Maybe State + , summary : Maybe RoomSummary + , timeline : Maybe Timeline + , unreadNotifications : Maybe UnreadNotificationCounts + , unreadThreadNotifications : Maybe (Dict String ThreadNotificationCounts) + } + + +type alias Ephemeral = + { events : Maybe (List Event) } + + +type alias State = + { events : Maybe (List ClientEventWithoutRoomID) } + + +type alias ClientEventWithoutRoomID = + { content : Json.Value + , eventId : String + , originServerTs : Int + , sender : String + , stateKey : Maybe String + , eventType : String + , unsigned : Maybe UnsignedData + } + + +type alias UnsignedData = PV.UnsignedData + + +type alias RoomSummary = + { mHeroes : Maybe (List String) + , mInvitedMemberCount : Maybe Int + , mJoinedMemberCount : Maybe Int + } + + +type alias Timeline = + { events : List ClientEventWithoutRoomID + , limited : Maybe Bool + , prevBatch : Maybe String + } + + +type alias UnreadNotificationCounts = + { highlightCount : Maybe Int + , notificationCount : Maybe Int + } + + +type alias ThreadNotificationCounts = + { highlightCount : Maybe Int + , notificationCount : Maybe Int + } + + +type alias KnockedRoom = + { knockState : Maybe KnockState } + + +type alias KnockState = + { events : Maybe (List StrippedStateEvent) } + + +type alias LeftRoom = + { accountData : Maybe AccountData + , state : Maybe State + , timeline : Maybe Timeline + } + + +type alias DeviceLists = + { changed : Maybe (List String) + , left : Maybe (List String) + } + + +type alias ToDevice = + { events : Maybe (List ToDeviceEvent) } + + +type alias ToDeviceEvent = + { content : Maybe Json.Value + , sender : Maybe String + , eventType : Maybe String + } + + +coderSyncResponse : Json.Coder SyncResponse +coderSyncResponse = PV.coderSyncResponse + + +coderAccountData : Json.Coder AccountData +coderAccountData = PV.coderAccountData + + +coderEvent : Json.Coder Event +coderEvent = PV.coderEvent + + +coderPresence : Json.Coder Presence +coderPresence = PV.coderPresence + + +coderRooms : Json.Coder Rooms +coderRooms = PV.coderRooms + + +coderInvitedRoom : Json.Coder InvitedRoom +coderInvitedRoom = PV.coderInvitedRoom + + +coderInviteState : Json.Coder InviteState +coderInviteState = PV.coderInviteState + + +coderStrippedStateEvent : Json.Coder StrippedStateEvent +coderStrippedStateEvent = PV.coderStrippedStateEvent + + +coderJoinedRoom : Json.Coder JoinedRoom +coderJoinedRoom = PV.coderJoinedRoom + + +coderEphemeral : Json.Coder Ephemeral +coderEphemeral = PV.coderEphemeral + + +coderState : Json.Coder State +coderState = PV.coderState + + +coderClientEventWithoutRoomID : Json.Coder ClientEventWithoutRoomID +coderClientEventWithoutRoomID = PV.coderClientEventWithoutRoomID + + +coderUnsignedData : Json.Coder UnsignedData +coderUnsignedData = PV.coderUnsignedData + + +coderRoomSummary : Json.Coder RoomSummary +coderRoomSummary = PV.coderRoomSummary + + +coderTimeline : Json.Coder Timeline +coderTimeline = PV.coderTimeline + + +coderUnreadNotificationCounts : Json.Coder UnreadNotificationCounts +coderUnreadNotificationCounts = PV.coderUnreadNotificationCounts + + +coderThreadNotificationCounts : Json.Coder ThreadNotificationCounts +coderThreadNotificationCounts = PV.coderThreadNotificationCounts + + +coderKnockedRoom : Json.Coder KnockedRoom +coderKnockedRoom = PV.coderKnockedRoom + + +coderKnockState : Json.Coder KnockState +coderKnockState = PV.coderKnockState + + +coderLeftRoom : Json.Coder LeftRoom +coderLeftRoom = PV.coderLeftRoom + + +coderDeviceLists : Json.Coder DeviceLists +coderDeviceLists = PV.coderDeviceLists + + +coderToDevice : Json.Coder ToDevice +coderToDevice = PV.coderToDevice + + +coderToDeviceEvent : Json.Coder ToDeviceEvent +coderToDeviceEvent = PV.coderToDeviceEvent From 29906ff976655bba78b10153fcaba8a390eaf655 Mon Sep 17 00:00:00 2001 From: Bram Date: Mon, 8 Jul 2024 22:10:29 +0200 Subject: [PATCH 15/34] Remove redundant /sync versions --- src/Internal/Api/Sync/V2.elm | 1 + src/Internal/Api/Sync/V3.elm | 261 ------------------------------ src/Internal/Api/Sync/V4.elm | 4 +- src/Internal/Api/Sync/V5.elm | 249 ---------------------------- src/Internal/Api/Versions/Api.elm | 1 - 5 files changed, 4 insertions(+), 512 deletions(-) delete mode 100644 src/Internal/Api/Sync/V3.elm delete mode 100644 src/Internal/Api/Sync/V5.elm diff --git a/src/Internal/Api/Sync/V2.elm b/src/Internal/Api/Sync/V2.elm index ee34599..d47491d 100644 --- a/src/Internal/Api/Sync/V2.elm +++ b/src/Internal/Api/Sync/V2.elm @@ -8,6 +8,7 @@ module Internal.Api.Sync.V2 exposing (..) This API module represents the /sync endpoint on Matrix spec version v1.1. + -} diff --git a/src/Internal/Api/Sync/V3.elm b/src/Internal/Api/Sync/V3.elm deleted file mode 100644 index 2f7bafb..0000000 --- a/src/Internal/Api/Sync/V3.elm +++ /dev/null @@ -1,261 +0,0 @@ -module Internal.Api.Sync.V3 exposing (..) - -{-| - - -# Sync response - -This API module represents the /sync endpoint on Matrix spec version v1.1. - - - --} - -import FastDict exposing (Dict) -import Internal.Api.Sync.V2 as PV -import Internal.Tools.Json as Json - - -type alias SyncResponse = - { accountData : Maybe AccountData - , deviceLists : Maybe DeviceLists - , deviceOneTimeKeysCount : Maybe (Dict String Int) - , deviceUnusedFallbackKeyTypes : List String - , nextBatch : String - , presence : Maybe Presence - , rooms : Maybe Rooms - , toDevice : Maybe ToDevice - } - - -type alias AccountData = - { events : Maybe (List Event) } - - -type alias Event = - { content : Json.Value - , eventType : String - } - - -type alias Presence = - { events : Maybe (List Event) } - - -type alias Rooms = - { invite : Maybe (Dict String InvitedRoom) - , join : Maybe (Dict String JoinedRoom) - , knock : Maybe (Dict String KnockedRoom) - , leave : Maybe (Dict String LeftRoom) - } - - -type alias InvitedRoom = - { inviteState : Maybe InviteState } - - -type alias InviteState = - { events : Maybe (List StrippedStateEvent) } - - -type alias StrippedStateEvent = - { content : Json.Value - , sender : String - , stateKey : String - , eventType : String - } - - -type alias JoinedRoom = - { accountData : Maybe AccountData - , ephemeral : Maybe Ephemeral - , state : Maybe State - , summary : Maybe RoomSummary - , timeline : Maybe Timeline - , unreadNotifications : Maybe UnreadNotificationCounts - } - - -type alias Ephemeral = - { events : Maybe (List Event) } - - -type alias State = - { events : Maybe (List ClientEventWithoutRoomID) } - - -type alias ClientEventWithoutRoomID = - { content : Json.Value - , eventId : String - , originServerTs : Int - , sender : String - , stateKey : Maybe String - , eventType : String - , unsigned : Maybe UnsignedData - } - - -type alias UnsignedData = - PV.UnsignedData - - -type alias RoomSummary = - { mHeroes : Maybe (List String) - , mInvitedMemberCount : Maybe Int - , mJoinedMemberCount : Maybe Int - } - - -type alias Timeline = - { events : List ClientEventWithoutRoomID - , limited : Maybe Bool - , prevBatch : Maybe String - } - - -type alias UnreadNotificationCounts = - { highlightCount : Maybe Int - , notificationCount : Maybe Int - } - - -type alias KnockedRoom = - { knockState : Maybe KnockState } - - -type alias KnockState = - { events : Maybe (List StrippedStateEvent) } - - -type alias LeftRoom = - { accountData : Maybe AccountData - , state : Maybe State - , timeline : Maybe Timeline - } - - -type alias DeviceLists = - { changed : Maybe (List String) - , left : Maybe (List String) - } - - -type alias ToDevice = - { events : Maybe (List ToDeviceEvent) } - - -type alias ToDeviceEvent = - { content : Maybe Json.Value - , sender : Maybe String - , eventType : Maybe String - } - - -coderSyncResponse : Json.Coder SyncResponse -coderSyncResponse = - PV.coderSyncResponse - - -coderAccountData : Json.Coder AccountData -coderAccountData = - PV.coderAccountData - - -coderEvent : Json.Coder Event -coderEvent = - PV.coderEvent - - -coderPresence : Json.Coder Presence -coderPresence = - PV.coderPresence - - -coderRooms : Json.Coder Rooms -coderRooms = - PV.coderRooms - - -coderInvitedRoom : Json.Coder InvitedRoom -coderInvitedRoom = - PV.coderInvitedRoom - - -coderInviteState : Json.Coder InviteState -coderInviteState = - PV.coderInviteState - - -coderStrippedStateEvent : Json.Coder StrippedStateEvent -coderStrippedStateEvent = - PV.coderStrippedStateEvent - - -coderJoinedRoom : Json.Coder JoinedRoom -coderJoinedRoom = - PV.coderJoinedRoom - - -coderEphemeral : Json.Coder Ephemeral -coderEphemeral = - PV.coderEphemeral - - -coderState : Json.Coder State -coderState = - PV.coderState - - -coderClientEventWithoutRoomID : Json.Coder ClientEventWithoutRoomID -coderClientEventWithoutRoomID = - PV.coderClientEventWithoutRoomID - - -coderUnsignedData : Json.Coder UnsignedData -coderUnsignedData = - PV.coderUnsignedData - - -coderRoomSummary : Json.Coder RoomSummary -coderRoomSummary = - PV.coderRoomSummary - - -coderTimeline : Json.Coder Timeline -coderTimeline = - PV.coderTimeline - - -coderUnreadNotificationCounts : Json.Coder UnreadNotificationCounts -coderUnreadNotificationCounts = - PV.coderUnreadNotificationCounts - - -coderKnockedRoom : Json.Coder KnockedRoom -coderKnockedRoom = - PV.coderKnockedRoom - - -coderKnockState : Json.Coder KnockState -coderKnockState = - PV.coderKnockState - - -coderLeftRoom : Json.Coder LeftRoom -coderLeftRoom = - PV.coderLeftRoom - - -coderDeviceLists : Json.Coder DeviceLists -coderDeviceLists = - PV.coderDeviceLists - - -coderToDevice : Json.Coder ToDevice -coderToDevice = - PV.coderToDevice - - -coderToDeviceEvent : Json.Coder ToDeviceEvent -coderToDeviceEvent = - PV.coderToDeviceEvent diff --git a/src/Internal/Api/Sync/V4.elm b/src/Internal/Api/Sync/V4.elm index d65b5a5..83054ed 100644 --- a/src/Internal/Api/Sync/V4.elm +++ b/src/Internal/Api/Sync/V4.elm @@ -8,11 +8,13 @@ module Internal.Api.Sync.V4 exposing (..) This API module represents the /sync endpoint on Matrix spec version v1.1. + + -} import FastDict exposing (Dict) -import Internal.Api.Sync.V3 as PV +import Internal.Api.Sync.V2 as PV import Internal.Tools.Json as Json diff --git a/src/Internal/Api/Sync/V5.elm b/src/Internal/Api/Sync/V5.elm deleted file mode 100644 index 0e85d1c..0000000 --- a/src/Internal/Api/Sync/V5.elm +++ /dev/null @@ -1,249 +0,0 @@ -module Internal.Api.Sync.V5 exposing (..) - -{-| - - -# Sync response - -This API module represents the /sync endpoint on Matrix spec version v1.1. - - - --} - -import FastDict exposing (Dict) -import Internal.Api.Sync.V4 as PV -import Internal.Tools.Json as Json - - -type alias SyncResponse = - { accountData : Maybe AccountData - , deviceLists : Maybe DeviceLists - , deviceOneTimeKeysCount : Maybe (Dict String Int) - , deviceUnusedFallbackKeyTypes : List String - , nextBatch : String - , presence : Maybe Presence - , rooms : Maybe Rooms - , toDevice : Maybe ToDevice - } - - -type alias AccountData = - { events : Maybe (List Event) } - - -type alias Event = - { content : Json.Value - , eventType : String - } - - -type alias Presence = - { events : Maybe (List Event) } - - -type alias Rooms = - { invite : Maybe (Dict String InvitedRoom) - , join : Maybe (Dict String JoinedRoom) - , knock : Maybe (Dict String KnockedRoom) - , leave : Maybe (Dict String LeftRoom) - } - - -type alias InvitedRoom = - { inviteState : Maybe InviteState } - - -type alias InviteState = - { events : Maybe (List StrippedStateEvent) } - - -type alias StrippedStateEvent = - { content : Json.Value - , sender : String - , stateKey : String - , eventType : String - } - - -type alias JoinedRoom = - { accountData : Maybe AccountData - , ephemeral : Maybe Ephemeral - , state : Maybe State - , summary : Maybe RoomSummary - , timeline : Maybe Timeline - , unreadNotifications : Maybe UnreadNotificationCounts - , unreadThreadNotifications : Maybe (Dict String ThreadNotificationCounts) - } - - -type alias Ephemeral = - { events : Maybe (List Event) } - - -type alias State = - { events : Maybe (List ClientEventWithoutRoomID) } - - -type alias ClientEventWithoutRoomID = - { content : Json.Value - , eventId : String - , originServerTs : Int - , sender : String - , stateKey : Maybe String - , eventType : String - , unsigned : Maybe UnsignedData - } - - -type alias UnsignedData = PV.UnsignedData - - -type alias RoomSummary = - { mHeroes : Maybe (List String) - , mInvitedMemberCount : Maybe Int - , mJoinedMemberCount : Maybe Int - } - - -type alias Timeline = - { events : List ClientEventWithoutRoomID - , limited : Maybe Bool - , prevBatch : Maybe String - } - - -type alias UnreadNotificationCounts = - { highlightCount : Maybe Int - , notificationCount : Maybe Int - } - - -type alias ThreadNotificationCounts = - { highlightCount : Maybe Int - , notificationCount : Maybe Int - } - - -type alias KnockedRoom = - { knockState : Maybe KnockState } - - -type alias KnockState = - { events : Maybe (List StrippedStateEvent) } - - -type alias LeftRoom = - { accountData : Maybe AccountData - , state : Maybe State - , timeline : Maybe Timeline - } - - -type alias DeviceLists = - { changed : Maybe (List String) - , left : Maybe (List String) - } - - -type alias ToDevice = - { events : Maybe (List ToDeviceEvent) } - - -type alias ToDeviceEvent = - { content : Maybe Json.Value - , sender : Maybe String - , eventType : Maybe String - } - - -coderSyncResponse : Json.Coder SyncResponse -coderSyncResponse = PV.coderSyncResponse - - -coderAccountData : Json.Coder AccountData -coderAccountData = PV.coderAccountData - - -coderEvent : Json.Coder Event -coderEvent = PV.coderEvent - - -coderPresence : Json.Coder Presence -coderPresence = PV.coderPresence - - -coderRooms : Json.Coder Rooms -coderRooms = PV.coderRooms - - -coderInvitedRoom : Json.Coder InvitedRoom -coderInvitedRoom = PV.coderInvitedRoom - - -coderInviteState : Json.Coder InviteState -coderInviteState = PV.coderInviteState - - -coderStrippedStateEvent : Json.Coder StrippedStateEvent -coderStrippedStateEvent = PV.coderStrippedStateEvent - - -coderJoinedRoom : Json.Coder JoinedRoom -coderJoinedRoom = PV.coderJoinedRoom - - -coderEphemeral : Json.Coder Ephemeral -coderEphemeral = PV.coderEphemeral - - -coderState : Json.Coder State -coderState = PV.coderState - - -coderClientEventWithoutRoomID : Json.Coder ClientEventWithoutRoomID -coderClientEventWithoutRoomID = PV.coderClientEventWithoutRoomID - - -coderUnsignedData : Json.Coder UnsignedData -coderUnsignedData = PV.coderUnsignedData - - -coderRoomSummary : Json.Coder RoomSummary -coderRoomSummary = PV.coderRoomSummary - - -coderTimeline : Json.Coder Timeline -coderTimeline = PV.coderTimeline - - -coderUnreadNotificationCounts : Json.Coder UnreadNotificationCounts -coderUnreadNotificationCounts = PV.coderUnreadNotificationCounts - - -coderThreadNotificationCounts : Json.Coder ThreadNotificationCounts -coderThreadNotificationCounts = PV.coderThreadNotificationCounts - - -coderKnockedRoom : Json.Coder KnockedRoom -coderKnockedRoom = PV.coderKnockedRoom - - -coderKnockState : Json.Coder KnockState -coderKnockState = PV.coderKnockState - - -coderLeftRoom : Json.Coder LeftRoom -coderLeftRoom = PV.coderLeftRoom - - -coderDeviceLists : Json.Coder DeviceLists -coderDeviceLists = PV.coderDeviceLists - - -coderToDevice : Json.Coder ToDevice -coderToDevice = PV.coderToDevice - - -coderToDeviceEvent : Json.Coder ToDeviceEvent -coderToDeviceEvent = PV.coderToDeviceEvent diff --git a/src/Internal/Api/Versions/Api.elm b/src/Internal/Api/Versions/Api.elm index a617ab3..835d0ef 100644 --- a/src/Internal/Api/Versions/Api.elm +++ b/src/Internal/Api/Versions/Api.elm @@ -86,6 +86,5 @@ versionsCoder = Set.empty } , default = ( Set.empty, [] ) - , defaultToString = always "{}" } ) From e122a7b262bedc3b404a75e04f7e81ea6d4a6dc4 Mon Sep 17 00:00:00 2001 From: Bram Date: Mon, 8 Jul 2024 22:11:04 +0200 Subject: [PATCH 16/34] Rename /sync v4 to v3 --- src/Internal/Api/Sync/{V4.elm => V3.elm} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename src/Internal/Api/Sync/{V4.elm => V3.elm} (99%) diff --git a/src/Internal/Api/Sync/V4.elm b/src/Internal/Api/Sync/V3.elm similarity index 99% rename from src/Internal/Api/Sync/V4.elm rename to src/Internal/Api/Sync/V3.elm index 83054ed..c1c43bf 100644 --- a/src/Internal/Api/Sync/V4.elm +++ b/src/Internal/Api/Sync/V3.elm @@ -1,4 +1,4 @@ -module Internal.Api.Sync.V4 exposing (..) +module Internal.Api.Sync.V3 exposing (..) {-| From e7d3a129b1b2147d53043066a86268d57851d423 Mon Sep 17 00:00:00 2001 From: Bram Date: Mon, 8 Jul 2024 23:07:50 +0200 Subject: [PATCH 17/34] Finish /sync for all spec versions v1.1 to v1.11 --- src/Internal/Api/Sync/V2.elm | 3 +- src/Internal/Api/Sync/V3.elm | 7 +- src/Internal/Api/Sync/V4.elm | 827 +++++++++++++++++++++++++++++++++++ 3 files changed, 835 insertions(+), 2 deletions(-) create mode 100644 src/Internal/Api/Sync/V4.elm diff --git a/src/Internal/Api/Sync/V2.elm b/src/Internal/Api/Sync/V2.elm index d47491d..cfbf29c 100644 --- a/src/Internal/Api/Sync/V2.elm +++ b/src/Internal/Api/Sync/V2.elm @@ -5,7 +5,8 @@ module Internal.Api.Sync.V2 exposing (..) # Sync response -This API module represents the /sync endpoint on Matrix spec version v1.1. +This API module represents the /sync endpoint on Matrix spec version v1.2 and +v1.3. diff --git a/src/Internal/Api/Sync/V3.elm b/src/Internal/Api/Sync/V3.elm index c1c43bf..676a1f1 100644 --- a/src/Internal/Api/Sync/V3.elm +++ b/src/Internal/Api/Sync/V3.elm @@ -5,11 +5,16 @@ module Internal.Api.Sync.V3 exposing (..) # Sync response -This API module represents the /sync endpoint on Matrix spec version v1.1. +This API module represents the /sync endpoint on the following Matrix spec +versions: + + + + -} diff --git a/src/Internal/Api/Sync/V4.elm b/src/Internal/Api/Sync/V4.elm new file mode 100644 index 0000000..7d67b2f --- /dev/null +++ b/src/Internal/Api/Sync/V4.elm @@ -0,0 +1,827 @@ +module Internal.Api.Sync.V4 exposing (..) + +{-| + + +# Sync response + +This API module represents the /sync endpoint on Matrix spec version v1.11. + + + +-} + +import FastDict exposing (Dict) +import Internal.Api.Sync.V3 as PV +import Internal.Tools.Json as Json + + +type alias SyncResponse = + { accountData : Maybe AccountData + , deviceLists : Maybe DeviceLists + , deviceOneTimeKeysCount : Maybe (Dict String Int) + , deviceUnusedFallbackKeyTypes : List String + , nextBatch : String + , presence : Maybe Presence + , rooms : Maybe Rooms + , toDevice : Maybe ToDevice + } + + +type alias AccountData = + { events : Maybe (List Event) } + + +type alias Event = + { content : Json.Value + , eventType : String + } + + +type alias Presence = + { events : Maybe (List Event) } + + +type alias Rooms = + { invite : Maybe (Dict String InvitedRoom) + , join : Maybe (Dict String JoinedRoom) + , knock : Maybe (Dict String KnockedRoom) + , leave : Maybe (Dict String LeftRoom) + } + + +type alias InvitedRoom = + { inviteState : Maybe InviteState } + + +type alias InviteState = + { events : Maybe (List StrippedStateEvent) } + + +type alias StrippedStateEvent = + { content : Json.Value + , sender : String + , stateKey : String + , eventType : String + } + + +type alias JoinedRoom = + { accountData : Maybe AccountData + , ephemeral : Maybe Ephemeral + , state : Maybe State + , summary : Maybe RoomSummary + , timeline : Maybe Timeline + , unreadNotifications : Maybe UnreadNotificationCounts + , unreadThreadNotifications : Maybe (Dict String ThreadNotificationCounts) + } + + +type alias Ephemeral = + { events : Maybe (List Event) } + + +type alias State = + { events : Maybe (List ClientEventWithoutRoomID) } + + +type alias ClientEventWithoutRoomID = + { content : Json.Value + , eventId : String + , originServerTs : Int + , sender : String + , stateKey : Maybe String + , eventType : String + , unsigned : Maybe UnsignedData + } + + +type UnsignedData + = UnsignedData + { age : Maybe Int + , membership : Maybe String + , prevContent : Maybe Json.Value + , redactedBecause : Maybe ClientEventWithoutRoomID + , transactionId : Maybe String + } + + +type alias RoomSummary = + { mHeroes : Maybe (List String) + , mInvitedMemberCount : Maybe Int + , mJoinedMemberCount : Maybe Int + } + + +type alias Timeline = + { events : List ClientEventWithoutRoomID + , limited : Maybe Bool + , prevBatch : Maybe String + } + + +type alias UnreadNotificationCounts = + { highlightCount : Maybe Int + , notificationCount : Maybe Int + } + + +type alias ThreadNotificationCounts = + { highlightCount : Maybe Int + , notificationCount : Maybe Int + } + + +type alias KnockedRoom = + { knockState : Maybe KnockState } + + +type alias KnockState = + { events : Maybe (List StrippedStateEvent) } + + +type alias LeftRoom = + { accountData : Maybe AccountData + , state : Maybe State + , timeline : Maybe Timeline + } + + +type alias DeviceLists = + { changed : Maybe (List String) + , left : Maybe (List String) + } + + +type alias ToDevice = + { events : Maybe (List ToDeviceEvent) } + + +type alias ToDeviceEvent = + { content : Maybe Json.Value + , sender : Maybe String + , eventType : Maybe String + } + + +coderSyncResponse : Json.Coder SyncResponse +coderSyncResponse = + Json.object8 + { name = "SyncResponse" + , description = [ "The response received when the server successfully processes the request." ] + , init = SyncResponse + } + (Json.field.optional.value + { fieldName = "account_data" + , toField = .accountData + , description = [ "The global private data created by this user." ] + , coder = coderAccountData + } + ) + (Json.field.optional.value + { fieldName = "device_lists" + , toField = .deviceLists + , description = [ "Information on end-to-end device updates, as specified in End-to-end encryption." ] + , coder = coderDeviceLists + } + ) + (Json.field.optional.value + { fieldName = "device_one_time_keys_count" + , toField = .deviceOneTimeKeysCount + , description = [ "Information on end-to-end encryption keys, as specified in End-to-end encryption." ] + , coder = Json.fastDict Json.int + } + ) + (Json.field.required + { fieldName = "device_unused_fallback_key_types" + , toField = .deviceUnusedFallbackKeyTypes + , description = [ "The unused fallback key algorithms." ] + , coder = Json.list Json.string + } + ) + (Json.field.required + { fieldName = "next_batch" + , toField = .nextBatch + , description = [ "The batch token to supply in the since param of the next /sync request." ] + , coder = Json.string + } + ) + (Json.field.optional.value + { fieldName = "presence" + , toField = .presence + , description = [ "The updates to the presence status of other users." ] + , coder = coderPresence + } + ) + (Json.field.optional.value + { fieldName = "rooms" + , toField = .rooms + , description = [ "Updates to rooms." ] + , coder = coderRooms + } + ) + (Json.field.optional.value + { fieldName = "to_device" + , toField = .toDevice + , description = [ "Information on the send-to-device messages for the client device, as defined in Send-to-Device messaging." ] + , coder = coderToDevice + } + ) + + +coderAccountData : Json.Coder AccountData +coderAccountData = + Json.object1 + { name = "AccountData" + , description = [ "The global private data created by this user." ] + , init = AccountData + } + (Json.field.optional.value + { fieldName = "events" + , toField = .events + , description = [ "List of events." ] + , coder = Json.list coderEvent + } + ) + + +coderEvent : Json.Coder Event +coderEvent = + Json.object2 + { name = "Event" + , description = [ "Details of an event." ] + , init = Event + } + (Json.field.required + { fieldName = "content" + , toField = .content + , description = [ "The fields in this object will vary depending on the type of event. When interacting with the REST API, this is the HTTP body." ] + , coder = Json.value + } + ) + (Json.field.required + { fieldName = "type" + , toField = .eventType + , description = [ "The type of event. This SHOULD be namespaced similar to Java package naming conventions e.g. ‘com.example.subdomain.event.type’" ] + , coder = Json.string + } + ) + + +coderPresence : Json.Coder Presence +coderPresence = + Json.object1 + { name = "Presence" + , description = [ "The updates to the presence status of other users." ] + , init = Presence + } + (Json.field.optional.value + { fieldName = "events" + , toField = .events + , description = [ "List of events." ] + , coder = Json.list coderEvent + } + ) + + +coderRooms : Json.Coder Rooms +coderRooms = + Json.object4 + { name = "Rooms" + , description = [ "Updates to rooms." ] + , init = Rooms + } + (Json.field.optional.value + { fieldName = "invite" + , toField = .invite + , description = [ "The rooms that the user has been invited to, mapped as room ID to room information." ] + , coder = Json.fastDict coderInvitedRoom + } + ) + (Json.field.optional.value + { fieldName = "join" + , toField = .join + , description = [ "The rooms that the user has joined, mapped as room ID to room information." ] + , coder = Json.fastDict coderJoinedRoom + } + ) + (Json.field.optional.value + { fieldName = "knock" + , toField = .knock + , description = [ "The rooms that the user has knocked upon, mapped as room ID to room information." ] + , coder = Json.fastDict coderKnockedRoom + } + ) + (Json.field.optional.value + { fieldName = "leave" + , toField = .leave + , description = [ "The rooms that the user has left or been banned from, mapped as room ID to room information." ] + , coder = Json.fastDict coderLeftRoom + } + ) + + +coderInvitedRoom : Json.Coder InvitedRoom +coderInvitedRoom = + Json.object1 + { name = "InvitedRoom" + , description = [ "The room that the user has been invited to." ] + , init = InvitedRoom + } + (Json.field.optional.value + { fieldName = "invite_state" + , toField = .inviteState + , description = [ "The stripped state of a room that the user has been invited to." ] + , coder = coderInviteState + } + ) + + +coderInviteState : Json.Coder InviteState +coderInviteState = + Json.object1 + { name = "InviteState" + , description = [ "The stripped state of a room that the user has been invited to." ] + , init = InviteState + } + (Json.field.optional.value + { fieldName = "events" + , toField = .events + , description = [ "The stripped state events that form the invite state." ] + , coder = Json.list coderStrippedStateEvent + } + ) + + +coderStrippedStateEvent : Json.Coder StrippedStateEvent +coderStrippedStateEvent = + Json.object4 + { name = "StrippedStateEvent" + , description = [ "A stripped state event that forms part of the invite state." ] + , init = StrippedStateEvent + } + (Json.field.required + { fieldName = "content" + , toField = .content + , description = [ "The content for the event." ] + , coder = Json.value + } + ) + (Json.field.required + { fieldName = "sender" + , toField = .sender + , description = [ "The sender for the event." ] + , coder = Json.string + } + ) + (Json.field.required + { fieldName = "state_key" + , toField = .stateKey + , description = [ "The state_key for the event." ] + , coder = Json.string + } + ) + (Json.field.required + { fieldName = "type" + , toField = .eventType + , description = [ "The type for the event." ] + , coder = Json.string + } + ) + + +coderJoinedRoom : Json.Coder JoinedRoom +coderJoinedRoom = + Json.object7 + { name = "JoinedRoom" + , description = [ "The room that the user has joined." ] + , init = JoinedRoom + } + (Json.field.optional.value + { fieldName = "account_data" + , toField = .accountData + , description = [ "The private data that this user has attached to this room." ] + , coder = coderAccountData + } + ) + (Json.field.optional.value + { fieldName = "ephemeral" + , toField = .ephemeral + , description = [ "The new ephemeral events in the room (events that aren’t recorded in the timeline or state of the room). In this version of the spec, these are typing notification and read receipt events." ] + , coder = coderEphemeral + } + ) + (Json.field.optional.value + { fieldName = "state" + , toField = .state + , description = [ "Updates to the state, between the time indicated by the since parameter, and the start of the timeline (or all state up to the start of the timeline, if since is not given, or full_state is true).", "N.B. state updates for m.room.member events will be incomplete if lazy_load_members is enabled in the /sync filter, and only return the member events required to display the senders of the timeline events in this response." ] + , coder = coderState + } + ) + (Json.field.optional.value + { fieldName = "summary" + , toField = .summary + , description = [ "Information about the room which clients may need to correctly render it to users." ] + , coder = coderRoomSummary + } + ) + (Json.field.optional.value + { fieldName = "timeline" + , toField = .timeline + , description = [ "The timeline of messages and state changes in the room." ] + , coder = coderTimeline + } + ) + (Json.field.optional.value + { fieldName = "unread_notifications" + , toField = .unreadNotifications + , description = [ "Counts of unread notifications for this room. See the Receiving notifications section for more information on how these are calculated.", "If unread_thread_notifications was specified as true on the RoomEventFilter, these counts will only be for the main timeline rather than all events in the room. See the threading module for more information.", "Changed in v1.4: Updated to reflect behaviour of having unread_thread_notifications as true in the RoomEventFilter for /sync." ] + , coder = coderUnreadNotificationCounts + } + ) + (Json.field.optional.value + { fieldName = "unread_thread_notifications" + , toField = .unreadThreadNotifications + , description = [ "If unread_thread_notifications was specified as true on the RoomEventFilter, the notification counts for each thread in this room. The object is keyed by thread root ID, with values matching unread_notifications.", "If a thread does not have any notifications it can be omitted from this object. If no threads have notification counts, this whole object can be omitted.", "Added in v1.4" ] + , coder = Json.fastDict coderThreadNotificationCounts + } + ) + + +coderEphemeral : Json.Coder Ephemeral +coderEphemeral = + Json.object1 + { name = "Ephemeral" + , description = [ "The new ephemeral events in the room." ] + , init = Ephemeral + } + (Json.field.optional.value + { fieldName = "events" + , toField = .events + , description = [ "List of events." ] + , coder = Json.list coderEvent + } + ) + + +coderState : Json.Coder State +coderState = + Json.object1 + { name = "State" + , description = [ "Updates to the state of the room." ] + , init = State + } + (Json.field.optional.value + { fieldName = "events" + , toField = .events + , description = [ "List of events." ] + , coder = Json.list coderClientEventWithoutRoomID + } + ) + + +coderClientEventWithoutRoomID : Json.Coder ClientEventWithoutRoomID +coderClientEventWithoutRoomID = + Json.object7 + { name = "ClientEventWithoutRoomID" + , description = [ "An event without the room ID." ] + , init = ClientEventWithoutRoomID + } + (Json.field.required + { fieldName = "content" + , toField = .content + , description = [ "The body of this event, as created by the client which sent it." ] + , coder = Json.value + } + ) + (Json.field.required + { fieldName = "event_id" + , toField = .eventId + , description = [ "The globally unique identifier for this event." ] + , coder = Json.string + } + ) + (Json.field.required + { fieldName = "origin_server_ts" + , toField = .originServerTs + , description = [ "Timestamp (in milliseconds since the unix epoch) on originating homeserver when this event was sent." ] + , coder = Json.int + } + ) + (Json.field.required + { fieldName = "sender" + , toField = .sender + , description = [ "Contains the fully-qualified ID of the user who sent this event." ] + , coder = Json.string + } + ) + (Json.field.optional.value + { fieldName = "state_key" + , toField = .stateKey + , description = [ "Present if, and only if, this event is a state event. The key making this piece of state unique in the room. Note that it is often an empty string.", "State keys starting with an @ are reserved for referencing user IDs, such as room members. With the exception of a few events, state events set with a given user’s ID as the state key MUST only be set by that user." ] + , coder = Json.string + } + ) + (Json.field.required + { fieldName = "type" + , toField = .eventType + , description = [ "The type of the event." ] + , coder = Json.string + } + ) + (Json.field.optional.value + { fieldName = "unsigned" + , toField = .unsigned + , description = [ "Contains optional extra information about the event." ] + , coder = coderUnsignedData + } + ) + + +coderUnsignedData : Json.Coder UnsignedData +coderUnsignedData = + Json.object5 + { name = "UnsignedData" + , description = [ "Contains optional extra information about the event." ] + , init = + \a b c d e -> + UnsignedData + { age = a + , membership = b + , prevContent = c + , redactedBecause = d + , transactionId = e + } + } + (Json.field.optional.value + { fieldName = "age" + , toField = \(UnsignedData u) -> u.age + , description = [ "The time in milliseconds that has elapsed since the event was sent. This field is generated by the local homeserver, and may be incorrect if the local time on at least one of the two servers is out of sync, which can cause the age to either be negative or greater than it actually is." ] + , coder = Json.int + } + ) + (Json.field.optional.value + { fieldName = "membership" + , toField = \(UnsignedData u) -> u.membership + , description = [ "The room membership of the user making the request, at the time of the event.", "This property is the value of the membership property of the requesting user’s m.room.member state at the point of the event, including any changes caused by the event. If the user had yet to join the room at the time of the event (i.e, they have no m.room.member state), this property is set to leave.", "Homeservers SHOULD populate this property wherever practical, but they MAY omit it if necessary (for example, if calculating the value is expensive, servers might choose to only implement it in encrypted rooms). The property is not normally populated in events pushed to application services via the application service transaction API (where there is no clear definition of “requesting user”).", "Added in v1.11" ] + , coder = Json.string + } + ) + (Json.field.optional.value + { fieldName = "prev_content" + , toField = \(UnsignedData u) -> u.prevContent + , description = [ "The previous content for this event. This field is generated by the local homeserver, and is only returned if the event is a state event, and the client has permission to see the previous content.", "Changed in v1.2: Previously, this field was specified at the top level of returned events rather than in unsigned (with the exception of the GET .../notifications endpoint), though in practice no known server implementations honoured this." ] + , coder = Json.value + } + ) + (Json.field.optional.value + { fieldName = "redacted_because" + , toField = \(UnsignedData u) -> u.redactedBecause + , description = [ "The event that redacted this event, if any." ] + , coder = Json.lazy (\_ -> coderClientEventWithoutRoomID) + } + ) + (Json.field.optional.value + { fieldName = "transaction_id" + , toField = \(UnsignedData u) -> u.transactionId + , description = [ "The client-supplied transaction ID, for example, provided via PUT /_matrix/client/v3/rooms/{roomId}/send/{eventType}/{txnId}, if the client being given the event is the same one which sent it." ] + , coder = Json.string + } + ) + + +coderRoomSummary : Json.Coder RoomSummary +coderRoomSummary = + Json.object3 + { name = "RoomSummary" + , description = [ "Information about the room which clients may need to correctly render it to users." ] + , init = RoomSummary + } + (Json.field.optional.value + { fieldName = "m.heroes" + , toField = .mHeroes + , description = [ "The users which can be used to generate a room name if the room does not have one. Required if the room’s m.room.name or m.room.canonical_alias state events are unset or empty.", "This should be the first 5 members of the room, ordered by stream ordering, which are joined or invited. The list must never include the client’s own user ID. When no joined or invited members are available, this should consist of the banned and left users. More than 5 members may be provided, however less than 5 should only be provided when there are less than 5 members to represent.", "When lazy-loading room members is enabled, the membership events for the heroes MUST be included in the state, unless they are redundant. When the list of users changes, the server notifies the client by sending a fresh list of heroes. If there are no changes since the last sync, this field may be omitted." ] + , coder = Json.list Json.string + } + ) + (Json.field.optional.value + { fieldName = "m.invited_member_count" + , toField = .mInvitedMemberCount + , description = [ "The number of users with membership of invite. If this field has not changed since the last sync, it may be omitted. Required otherwise." ] + , coder = Json.int + } + ) + (Json.field.optional.value + { fieldName = "m.joined_member_count" + , toField = .mJoinedMemberCount + , description = [ "The number of users with membership of join, including the client’s own user ID. If this field has not changed since the last sync, it may be omitted. Required otherwise." ] + , coder = Json.int + } + ) + + +coderTimeline : Json.Coder Timeline +coderTimeline = + Json.object3 + { name = "Timeline" + , description = [ "The timeline of messages and state changes in the room." ] + , init = Timeline + } + (Json.field.required + { fieldName = "events" + , toField = .events + , description = [ "List of events." ] + , coder = Json.list coderClientEventWithoutRoomID + } + ) + (Json.field.optional.value + { fieldName = "limited" + , toField = .limited + , description = [ "True if the number of events returned was limited by the limit on the filter." ] + , coder = Json.bool + } + ) + (Json.field.optional.value + { fieldName = "prev_batch" + , toField = .prevBatch + , description = [ "A token that can be supplied to the from parameter of the /rooms//messages endpoint in order to retrieve earlier events. If no earlier events are available, this property may be omitted from the response." ] + , coder = Json.string + } + ) + + +coderUnreadNotificationCounts : Json.Coder UnreadNotificationCounts +coderUnreadNotificationCounts = + Json.object2 + { name = "UnreadNotificationCounts" + , description = [ "Counts of unread notifications for this room." ] + , init = UnreadNotificationCounts + } + (Json.field.optional.value + { fieldName = "highlight_count" + , toField = .highlightCount + , description = [ "The number of unread notifications for this room with the highlight flag set." ] + , coder = Json.int + } + ) + (Json.field.optional.value + { fieldName = "notification_count" + , toField = .notificationCount + , description = [ "The total number of unread notifications for this room." ] + , coder = Json.int + } + ) + + +coderThreadNotificationCounts : Json.Coder ThreadNotificationCounts +coderThreadNotificationCounts = + Json.object2 + { name = "ThreadNotificationCounts" + , description = [ "The notification counts for each thread in this room." ] + , init = ThreadNotificationCounts + } + (Json.field.optional.value + { fieldName = "highlight_count" + , toField = .highlightCount + , description = [ "The number of unread notifications for this thread with the highlight flag set." ] + , coder = Json.int + } + ) + (Json.field.optional.value + { fieldName = "notification_count" + , toField = .notificationCount + , description = [ "The total number of unread notifications for this thread." ] + , coder = Json.int + } + ) + + +coderKnockedRoom : Json.Coder KnockedRoom +coderKnockedRoom = + Json.object1 + { name = "KnockedRoom" + , description = [ "The room that the user has knocked upon." ] + , init = KnockedRoom + } + (Json.field.optional.value + { fieldName = "knock_state" + , toField = .knockState + , description = [ "The stripped state of a room that the user has knocked upon." ] + , coder = coderKnockState + } + ) + + +coderKnockState : Json.Coder KnockState +coderKnockState = + Json.object1 + { name = "KnockState" + , description = [ "The stripped state of a room that the user has knocked upon." ] + , init = KnockState + } + (Json.field.optional.value + { fieldName = "events" + , toField = .events + , description = [ "The stripped state events that form the knock state." ] + , coder = Json.list coderStrippedStateEvent + } + ) + + +coderLeftRoom : Json.Coder LeftRoom +coderLeftRoom = + Json.object3 + { name = "LeftRoom" + , description = [ "The room that the user has left or been banned from." ] + , init = LeftRoom + } + (Json.field.optional.value + { fieldName = "account_data" + , toField = .accountData + , description = [ "The private data that this user has attached to this room." ] + , coder = coderAccountData + } + ) + (Json.field.optional.value + { fieldName = "state" + , toField = .state + , description = [ "The state updates for the room up to the start of the timeline." ] + , coder = coderState + } + ) + (Json.field.optional.value + { fieldName = "timeline" + , toField = .timeline + , description = [ "The timeline of messages and state changes in the room up to the point when the user left." ] + , coder = coderTimeline + } + ) + + +coderDeviceLists : Json.Coder DeviceLists +coderDeviceLists = + Json.object2 + { name = "DeviceLists" + , description = [ "Information on end-to-end device updates, as specified in End-to-end encryption." ] + , init = DeviceLists + } + (Json.field.optional.value + { fieldName = "changed" + , toField = .changed + , description = [ "List of users who have updated their device identity or cross-signing keys, or who now share an encrypted room with the client since the previous sync response." ] + , coder = Json.list Json.string + } + ) + (Json.field.optional.value + { fieldName = "left" + , toField = .left + , description = [ "List of users with whom we do not share any encrypted rooms anymore since the previous sync response." ] + , coder = Json.list Json.string + } + ) + + +coderToDevice : Json.Coder ToDevice +coderToDevice = + Json.object1 + { name = "ToDevice" + , description = [ "Information on the send-to-device messages for the client device, as defined in Send-to-Device messaging." ] + , init = ToDevice + } + (Json.field.optional.value + { fieldName = "events" + , toField = .events + , description = [ "List of send-to-device messages." ] + , coder = Json.list coderToDeviceEvent + } + ) + + +coderToDeviceEvent : Json.Coder ToDeviceEvent +coderToDeviceEvent = + Json.object3 + { name = "ToDeviceEvent" + , description = [ "A send-to-device event." ] + , init = ToDeviceEvent + } + (Json.field.optional.value + { fieldName = "content" + , toField = .content + , description = [ "The content of this event. The fields in this object will vary depending on the type of event." ] + , coder = Json.value + } + ) + (Json.field.optional.value + { fieldName = "sender" + , toField = .sender + , description = [ "The Matrix user ID of the user who sent this event." ] + , coder = Json.string + } + ) + (Json.field.optional.value + { fieldName = "type" + , toField = .eventType + , description = [ "The type of event." ] + , coder = Json.string + } + ) From b239eecc6bf3529c12caae70c3dec144882be76c Mon Sep 17 00:00:00 2001 From: Bram Date: Tue, 9 Jul 2024 00:08:46 +0200 Subject: [PATCH 18/34] Add toUpdate function for /sync v1 --- src/Internal/Api/Sync/V1.elm | 88 +++++++++++++++++++++++++++++- src/Internal/Config/Text.elm | 4 ++ src/Internal/Tools/DecodeExtra.elm | 36 +++++++++++- src/Internal/Tools/Json.elm | 79 ++++++++++++++++++++++++++- src/Internal/Values/Context.elm | 11 +++- src/Internal/Values/Envelope.elm | 4 ++ 6 files changed, 216 insertions(+), 6 deletions(-) diff --git a/src/Internal/Api/Sync/V1.elm b/src/Internal/Api/Sync/V1.elm index 8260d66..88f0dd0 100644 --- a/src/Internal/Api/Sync/V1.elm +++ b/src/Internal/Api/Sync/V1.elm @@ -11,10 +11,14 @@ This API module represents the /sync endpoint on Matrix spec version v1.1. -} -import FastDict exposing (Dict) +import FastDict as Dict exposing (Dict) +import Internal.Config.Log exposing (Log) import Internal.Tools.Json as Json import Internal.Tools.StrippedEvent as StrippedEvent exposing (StrippedEvent) import Internal.Tools.Timestamp as Timestamp exposing (Timestamp) +import Internal.Values.Envelope as E +import Internal.Values.Room as R +import Internal.Values.Vault as V type alias SyncResponse = @@ -807,3 +811,85 @@ coderToDeviceEvent = , coder = Json.string } ) + + +updateSyncResponse : SyncResponse -> ( E.EnvelopeUpdate V.VaultUpdate, List Log ) +updateSyncResponse response = + -- TODO: Add account data + -- TODO: Add device lists + -- Next batch + [ Just ( E.SetNextBatch response.nextBatch, [] ) + + -- TODO: Add presence + -- Rooms + , Maybe.map (updateRooms >> Tuple.mapFirst E.ContentUpdate) response.rooms + + -- TODO: Add to_device + ] + |> List.filterMap identity + |> List.unzip + |> Tuple.mapFirst E.More + |> Tuple.mapSecond List.concat + + +updateRooms : Rooms -> ( V.VaultUpdate, List Log ) +updateRooms rooms = + let + ( roomUpdate, roomLogs ) = + rooms.join + |> Maybe.withDefault Dict.empty + |> Dict.toList + |> List.map + (\( roomId, room ) -> + let + ( u, l ) = + updateJoinedRoom 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 : JoinedRoom -> ( R.RoomUpdate, List Log ) +updateJoinedRoom 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 + -- TODO: Add timeline + -- TODO: Add unread notifications + ] + , [] + ) diff --git a/src/Internal/Config/Text.elm b/src/Internal/Config/Text.elm index c40dd9a..3550be2 100644 --- a/src/Internal/Config/Text.elm +++ b/src/Internal/Config/Text.elm @@ -277,6 +277,7 @@ fields : , baseUrl : Desc , deviceId : Desc , experimental : Desc + , nextBatch : Desc , now : Desc , password : Desc , refreshToken : Desc @@ -388,6 +389,9 @@ fields = , experimental = [ "Experimental features supported by the homeserver." ] + , nextBatch = + [ "The batch token to supply in the since param of the next /sync request." + ] , now = [ "The most recently found timestamp." ] diff --git a/src/Internal/Tools/DecodeExtra.elm b/src/Internal/Tools/DecodeExtra.elm index 6460233..b7a0ae8 100644 --- a/src/Internal/Tools/DecodeExtra.elm +++ b/src/Internal/Tools/DecodeExtra.elm @@ -1,6 +1,6 @@ module Internal.Tools.DecodeExtra exposing ( opField, opFieldWithDefault - , map9, map10, map11 + , map9, map10, map11, map12 ) {-| @@ -18,7 +18,7 @@ This module contains helper functions that help decode JSON. ## Extended map functions -@docs map9, map10, map11 +@docs map9, map10, map11, map12 -} @@ -153,3 +153,35 @@ map11 func da db dc dd de df dg dh di dj dk = (D.map2 Tuple.pair df dg) (D.map2 Tuple.pair dh di) (D.map2 Tuple.pair dj dk) + + +{-| Try 12 decoders and combine the result. +-} +map12 : + (a -> b -> c -> d -> e -> f -> g -> h -> i -> j -> k -> l -> value) + -> D.Decoder a + -> D.Decoder b + -> D.Decoder c + -> D.Decoder d + -> D.Decoder e + -> D.Decoder f + -> D.Decoder g + -> D.Decoder h + -> D.Decoder i + -> D.Decoder j + -> D.Decoder k + -> D.Decoder l + -> D.Decoder value +map12 func da db dc dd de df dg dh di dj dk dl = + D.map8 + (\a b c d ( e, f ) ( g, h ) ( i, j ) ( k, l ) -> + func a b c d e f g h i j k l + ) + da + db + dc + dd + (D.map2 Tuple.pair de df) + (D.map2 Tuple.pair dg dh) + (D.map2 Tuple.pair di dj) + (D.map2 Tuple.pair dk dl) diff --git a/src/Internal/Tools/Json.elm b/src/Internal/Tools/Json.elm index 3ab93b3..2be4c1d 100644 --- a/src/Internal/Tools/Json.elm +++ b/src/Internal/Tools/Json.elm @@ -5,7 +5,7 @@ module Internal.Tools.Json exposing , Docs(..), RequiredField(..), toDocs , list, listWithOne, slowDict, fastDict, fastIntDict, set, maybe , Field, field, parser - , object1, object2, object3, object4, object5, object6, object7, object8, object9, object10, object11 + , object1, object2, object3, object4, object5, object6, object7, object8, object9, object10, object11, object12 ) {-| @@ -62,7 +62,7 @@ first. Once all fields are constructed, the user can create JSON objects. -@docs object1, object2, object3, object4, object5, object6, object7, object8, object9, object10, object11 +@docs object1, object2, object3, object4, object5, object6, object7, object8, object9, object10, object11, object12 -} @@ -1175,6 +1175,81 @@ object11 { name, description, init } fa fb fc fd fe ff fg fh fi fj fk = } +{-| Define an object with 12 keys +-} +object12 : + Descriptive { init : a -> b -> c -> d -> e -> f -> g -> h -> i -> j -> k -> l -> object } + -> Field a object + -> Field b object + -> Field c object + -> Field d object + -> Field e object + -> Field f object + -> Field g object + -> Field h object + -> Field i object + -> Field j object + -> Field k object + -> Field l object + -> Coder object +object12 { name, description, init } fa fb fc fd fe ff fg fh fi fj fk fl = + Coder + { encoder = + objectEncoder + [ toEncodeField fa + , toEncodeField fb + , toEncodeField fc + , toEncodeField fd + , toEncodeField fe + , toEncodeField ff + , toEncodeField fg + , toEncodeField fh + , toEncodeField fi + , toEncodeField fj + , toEncodeField fk + , toEncodeField fl + ] + , decoder = + D.map12 + (\( a, la ) ( b, lb ) ( c, lc ) ( d, ld ) ( e, le ) ( f, lf ) ( g, lg ) ( h, lh ) ( i, li ) ( j, lj ) ( k, lk ) ( l, ll ) -> + ( init a b c d e f g h i j k l + , List.concat [ la, lb, lc, ld, le, lf, lg, lh, li, lj, lk, ll ] + ) + ) + (toDecoderField fa) + (toDecoderField fb) + (toDecoderField fc) + (toDecoderField fd) + (toDecoderField fe) + (toDecoderField ff) + (toDecoderField fg) + (toDecoderField fh) + (toDecoderField fi) + (toDecoderField fj) + (toDecoderField fk) + (toDecoderField fl) + , docs = + DocsObject + { name = name + , description = description + , keys = + [ toDocsField fa + , toDocsField fb + , toDocsField fc + , toDocsField fd + , toDocsField fe + , toDocsField ff + , toDocsField fg + , toDocsField fh + , toDocsField fi + , toDocsField fj + , toDocsField fk + , toDocsField fl + ] + } + } + + {-| Define a parser that converts a string into a custom Elm type. -} parser : { name : String, p : P.Parser ( a, List Log ), toString : a -> String } -> Coder a diff --git a/src/Internal/Values/Context.elm b/src/Internal/Values/Context.elm index ca6a8a1..5fc358d 100644 --- a/src/Internal/Values/Context.elm +++ b/src/Internal/Values/Context.elm @@ -95,6 +95,7 @@ type alias Context = { accessTokens : Hashdict AccessToken , baseUrl : Maybe String , deviceId : Maybe String + , nextBatch : Maybe String , now : Maybe Timestamp , password : Maybe String , refreshToken : Maybe String @@ -152,7 +153,7 @@ fromApiFormat (APIContext c) = -} coder : Json.Coder Context coder = - Json.object11 + Json.object12 { name = Text.docs.context.name , description = Text.docs.context.description , init = Context @@ -178,6 +179,13 @@ coder = , coder = Json.string } ) + (Json.field.optional.value + { fieldName = "nextBatch" + , toField = .nextBatch + , description = Text.fields.context.nextBatch + , coder = Json.string + } + ) (Json.field.optional.value { fieldName = "now" , toField = .now @@ -303,6 +311,7 @@ init sn = { accessTokens = Hashdict.empty .value , baseUrl = Nothing , deviceId = Nothing + , nextBatch = Nothing , now = Nothing , refreshToken = Nothing , password = Nothing diff --git a/src/Internal/Values/Envelope.elm b/src/Internal/Values/Envelope.elm index 7ae8ceb..c9cc126 100644 --- a/src/Internal/Values/Envelope.elm +++ b/src/Internal/Values/Envelope.elm @@ -82,6 +82,7 @@ type EnvelopeUpdate a | SetAccessToken AccessToken | SetBaseUrl String | SetDeviceId String + | SetNextBatch String | SetNow Timestamp | SetRefreshToken String | SetVersions Versions @@ -327,6 +328,9 @@ update updateContent eu ({ context } as data) = SetDeviceId d -> { data | context = { context | deviceId = Just d } } + SetNextBatch nextBatch -> + { data | context = { context | nextBatch = Just nextBatch } } + SetNow n -> { data | context = { context | now = Just n } } From c5d07f0a94c270d185c5241a3a76ed9e6b0b1225 Mon Sep 17 00:00:00 2001 From: Bram Date: Tue, 9 Jul 2024 13:29:45 +0200 Subject: [PATCH 19/34] 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" ] } From 632158f3099e9b2e3f949140116232dead9c58e1 Mon Sep 17 00:00:00 2001 From: Bram Date: Tue, 9 Jul 2024 16:48:53 +0200 Subject: [PATCH 20/34] Add toUpdate function for sync V4 --- src/Internal/Api/Sync/V1.elm | 1 + src/Internal/Api/Sync/V2.elm | 2 + src/Internal/Api/Sync/V4.elm | 219 ++++++++++++++++++++++++++++++++-- src/Internal/Config/Text.elm | 4 + src/Internal/Values/Event.elm | 12 +- 5 files changed, 227 insertions(+), 11 deletions(-) diff --git a/src/Internal/Api/Sync/V1.elm b/src/Internal/Api/Sync/V1.elm index cd71914..628f727 100644 --- a/src/Internal/Api/Sync/V1.elm +++ b/src/Internal/Api/Sync/V1.elm @@ -967,6 +967,7 @@ toUnsigned : UnsignedData -> Event.UnsignedData toUnsigned u = Event.UnsignedData { age = u.age + , membership = Nothing , prevContent = Nothing , redactedBecause = Nothing , transactionId = u.transactionId diff --git a/src/Internal/Api/Sync/V2.elm b/src/Internal/Api/Sync/V2.elm index 01b6387..304a079 100644 --- a/src/Internal/Api/Sync/V2.elm +++ b/src/Internal/Api/Sync/V2.elm @@ -740,6 +740,7 @@ toUnsigned ev unsigned = ( Just e, Nothing ) -> { age = Nothing + , membership = Nothing , prevContent = Nothing , redactedBecause = Just e , transactionId = Nothing @@ -749,6 +750,7 @@ toUnsigned ev unsigned = ( _, Just (UnsignedData u) ) -> { age = u.age + , membership = Nothing , prevContent = u.prevContent , redactedBecause = ev , transactionId = u.transactionId diff --git a/src/Internal/Api/Sync/V4.elm b/src/Internal/Api/Sync/V4.elm index 7d67b2f..f254401 100644 --- a/src/Internal/Api/Sync/V4.elm +++ b/src/Internal/Api/Sync/V4.elm @@ -11,9 +11,19 @@ This API module represents the /sync endpoint on Matrix spec version v1.11. -} -import FastDict exposing (Dict) +import FastDict as Dict exposing (Dict) import Internal.Api.Sync.V3 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 = @@ -60,7 +70,7 @@ type alias InviteState = type alias StrippedStateEvent = { content : Json.Value - , sender : String + , sender : User , stateKey : String , eventType : String } @@ -88,8 +98,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 @@ -159,7 +169,7 @@ type alias ToDevice = type alias ToDeviceEvent = { content : Maybe Json.Value - , sender : Maybe String + , sender : Maybe User , eventType : Maybe String } @@ -371,7 +381,7 @@ coderStrippedStateEvent = { fieldName = "sender" , toField = .sender , description = [ "The sender for the event." ] - , coder = Json.string + , coder = User.coder } ) (Json.field.required @@ -505,14 +515,14 @@ coderClientEventWithoutRoomID = { fieldName = "origin_server_ts" , toField = .originServerTs , description = [ "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 = [ "Contains the fully-qualified ID of the user who sent this event." ] - , coder = Json.string + , coder = User.coder } ) (Json.field.optional.value @@ -815,7 +825,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 @@ -825,3 +835,194 @@ coderToDeviceEvent = , coder = Json.string } ) + + +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 { 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 + , membership = Nothing + , prevContent = Nothing + , redactedBecause = Just e + , transactionId = Nothing + } + |> Event.UnsignedData + |> Just + + ( _, Just (UnsignedData u) ) -> + { age = u.age + , membership = Nothing + , prevContent = u.prevContent + , redactedBecause = ev + , transactionId = u.transactionId + } + |> Event.UnsignedData + |> Just diff --git a/src/Internal/Config/Text.elm b/src/Internal/Config/Text.elm index 122a37a..7a0326e 100644 --- a/src/Internal/Config/Text.elm +++ b/src/Internal/Config/Text.elm @@ -348,6 +348,7 @@ fields : } , unsigned : { age : Desc + , membership : Desc , prevContent : Desc , redactedBecause : Desc , transactionId : Desc @@ -563,6 +564,9 @@ fields = { age = [ "The time in milliseconds that has elapsed since the event was sent. This field is generated by the local homeserver, and may be incorrect if the local time on at least one of the two servers is out of sync, which can cause the age to either be negative or greater than it actually is." ] + , membership = + [ "The room membership of the user making the request, at the time of the event." + ] , prevContent = [ "The previous content for this event. This field is generated by the local homeserver, and is only returned if the event is a state event, and the client has permission to see the previous content." ] diff --git a/src/Internal/Values/Event.elm b/src/Internal/Values/Event.elm index 2ee528a..2f54a13 100644 --- a/src/Internal/Values/Event.elm +++ b/src/Internal/Values/Event.elm @@ -59,6 +59,7 @@ helper functions. type UnsignedData = UnsignedData { age : Maybe Int + , membership : Maybe String , prevContent : Maybe Json.Value , redactedBecause : Maybe Event , transactionId : Maybe String @@ -242,10 +243,10 @@ transactionId event = unsignedCoder : Json.Coder UnsignedData unsignedCoder = - Json.object4 + Json.object5 { name = Text.docs.unsigned.name , description = Text.docs.unsigned.description - , init = \a b c d -> UnsignedData { age = a, prevContent = b, redactedBecause = c, transactionId = d } + , init = \a b c d e -> UnsignedData { age = a, membership = b, prevContent = c, redactedBecause = d, transactionId = e } } (Json.field.optional.value { fieldName = "age" @@ -254,6 +255,13 @@ unsignedCoder = , coder = Json.int } ) + (Json.field.optional.value + { fieldName = "membership" + , toField = \(UnsignedData data) -> data.membership + , description = Text.fields.unsigned.membership + , coder = Json.string + } + ) (Json.field.optional.value { fieldName = "prevContent" , toField = \(UnsignedData data) -> data.prevContent From 0978e43fc0c253739ff8239e9b90b55a1f439da6 Mon Sep 17 00:00:00 2001 From: Bram Date: Tue, 9 Jul 2024 16:55:19 +0200 Subject: [PATCH 21/34] Finish /sync API definition --- src/Internal/Api/Sync/Api.elm | 119 +++++++++++++++++++++++++++++----- 1 file changed, 104 insertions(+), 15 deletions(-) diff --git a/src/Internal/Api/Sync/Api.elm b/src/Internal/Api/Sync/Api.elm index e51d35f..e18092e 100644 --- a/src/Internal/Api/Sync/Api.elm +++ b/src/Internal/Api/Sync/Api.elm @@ -16,9 +16,30 @@ communicate with the Matrix server about the Vault's needs. import Internal.Api.Api as A import Internal.Api.Request as R import Internal.Api.Sync.V1 as V1 +import Internal.Api.Sync.V2 as V2 +import Internal.Api.Sync.V3 as V3 +import Internal.Api.Sync.V4 as V4 import Internal.Filter.Timeline as Filter +{-| Sync with the Matrix API. +-} +sync : SyncInput -> A.TaskChain (Phantom a) (Phantom a) +sync = + A.startWithVersion "v1.1" syncV1 + |> A.forVersion "v1.2" syncV2 + |> A.sameForVersion "v1.3" + |> A.forVersion "v1.4" syncV3 + |> A.sameForVersion "v1.5" + |> A.sameForVersion "v1.6" + |> A.sameForVersion "v1.7" + |> A.sameForVersion "v1.8" + |> A.sameForVersion "v1.9" + |> A.sameForVersion "v1.10" + |> A.forVersion "v1.11" syncV4 + |> A.versionChain + + -- For simplicity, we will not use a filter for now -- and assume that every client always wants to receive all events. @@ -38,6 +59,8 @@ type alias PhantomV1 a = type PresenceV1 = OfflineV1 + | OnlineV1 + | UnavailableV1 type alias SyncInput = @@ -59,10 +82,17 @@ type alias SyncInputV1 a = } -sync : SyncInput -> A.TaskChain (Phantom a) (Phantom a) -sync = - A.startWithVersion "r0.0.0" syncV1 - |> A.versionChain +presenceV1ToString : PresenceV1 -> String +presenceV1ToString p = + case p of + OfflineV1 -> + "offline" + + OnlineV1 -> + "online" + + UnavailableV1 -> + "unavailable" syncV1 : SyncInputV1 i -> A.TaskChain (PhantomV1 a) (PhantomV1 a) @@ -71,24 +101,83 @@ syncV1 data = { attributes = [ R.accessToken , R.queryOpString "filter" Nothing -- FILTER HERE - , R.queryOpString "since" data.since , R.queryOpBool "full_state" data.fullState , data.presenceV1 - |> Maybe.map (always "offline") + |> Maybe.map presenceV1ToString |> R.queryOpString "set_presence" + , R.queryOpString "since" data.since , R.queryOpInt "timeout" data.timeout ] - , coder = V1.syncResponseCoder + , coder = V1.coderSyncResponse , contextChange = always identity , method = "GET" , path = [ "_matrix", "client", "r0", "sync" ] , toUpdate = - \out -> - ( V1.syncResponseToUpdate - { filter = Filter.pass -- FILTER HERE - , since = data.since - } - out - , [] - ) + V1.updateSyncResponse { filter = Filter.pass, since = data.since } + } + + +syncV2 : SyncInputV1 i -> A.TaskChain (PhantomV1 a) (PhantomV1 a) +syncV2 data = + A.request + { attributes = + [ R.accessToken + , R.queryOpString "filter" Nothing + , R.queryOpBool "full_state" data.fullState + , data.presenceV1 + |> Maybe.map presenceV1ToString + |> R.queryOpString "set_presence" + , R.queryOpString "since" data.since + , R.queryOpInt "timeout" data.timeout + ] + , coder = V2.coderSyncResponse + , contextChange = always identity + , method = "GET" + , path = [ "_matrix", "client", "r0", "sync" ] + , toUpdate = + V2.updateSyncResponse { filter = Filter.pass, since = data.since } + } + + +syncV3 : SyncInputV1 i -> A.TaskChain (PhantomV1 a) (PhantomV1 a) +syncV3 data = + A.request + { attributes = + [ R.accessToken + , R.queryOpString "filter" Nothing + , R.queryOpBool "full_state" data.fullState + , data.presenceV1 + |> Maybe.map presenceV1ToString + |> R.queryOpString "set_presence" + , R.queryOpString "since" data.since + , R.queryOpInt "timeout" data.timeout + ] + , coder = V3.coderSyncResponse + , contextChange = always identity + , method = "GET" + , path = [ "_matrix", "client", "r0", "sync" ] + , toUpdate = + V3.updateSyncResponse { filter = Filter.pass, since = data.since } + } + + +syncV4 : SyncInputV1 i -> A.TaskChain (PhantomV1 a) (PhantomV1 a) +syncV4 data = + A.request + { attributes = + [ R.accessToken + , R.queryOpString "filter" Nothing + , R.queryOpBool "full_state" data.fullState + , data.presenceV1 + |> Maybe.map presenceV1ToString + |> R.queryOpString "set_presence" + , R.queryOpString "since" data.since + , R.queryOpInt "timeout" data.timeout + ] + , coder = V4.coderSyncResponse + , contextChange = always identity + , method = "GET" + , path = [ "_matrix", "client", "r0", "sync" ] + , toUpdate = + V4.updateSyncResponse { filter = Filter.pass, since = data.since } } From 4e378a5f500519c2e0cce2a360a1d2ced68d9b1b Mon Sep 17 00:00:00 2001 From: Bram Date: Tue, 9 Jul 2024 17:33:41 +0200 Subject: [PATCH 22/34] Add /sync as task --- src/Internal/Api/Main.elm | 23 +++++++++++++-- src/Internal/Api/Sync/Api.elm | 49 ++++++++++++++------------------ src/Internal/Api/Task.elm | 14 +++++++-- src/Internal/Config/Text.elm | 4 +++ src/Internal/Values/Settings.elm | 11 ++++++- 5 files changed, 68 insertions(+), 33 deletions(-) diff --git a/src/Internal/Api/Main.elm b/src/Internal/Api/Main.elm index 9807572..04011d0 100644 --- a/src/Internal/Api/Main.elm +++ b/src/Internal/Api/Main.elm @@ -1,6 +1,6 @@ module Internal.Api.Main exposing ( Msg - , sendMessageEvent + , sendMessageEvent, sync ) {-| @@ -18,7 +18,7 @@ This module is used as reference for getting ## Actions -@docs sendMessageEvent +@docs sendMessageEvent, sync -} @@ -57,3 +57,22 @@ sendMessageEvent env data = } ) (Context.apiFormat env.context) + + +{-| Sync with the Matrix API to stay up-to-date. +-} +sync : + E.Envelope a + -> { timeout : Int, toMsg : Msg -> msg } + -> Cmd msg +sync env data = + ITask.run + data.toMsg + (ITask.sync + { fullState = Nothing + , presence = env.settings.presence + , since = env.context.nextBatch + , timeout = Just data.timeout + } + ) + (Context.apiFormat env.context) diff --git a/src/Internal/Api/Sync/Api.elm b/src/Internal/Api/Sync/Api.elm index e18092e..59a622a 100644 --- a/src/Internal/Api/Sync/Api.elm +++ b/src/Internal/Api/Sync/Api.elm @@ -1,4 +1,4 @@ -module Internal.Api.Sync.Api exposing (..) +module Internal.Api.Sync.Api exposing (sync, Phantom) {-| @@ -9,7 +9,7 @@ The sync module might be one of the most crucial parts of the Elm SDK. It offers users the guarantee that the `Vault` type remains up-to-date, and it helps communicate with the Matrix server about the Vault's needs. -@docs Phantom +@docs sync, Phantom -} @@ -57,16 +57,10 @@ type alias PhantomV1 a = { a | accessToken : (), baseUrl : () } -type PresenceV1 - = OfflineV1 - | OnlineV1 - | UnavailableV1 - - type alias SyncInput = { -- filter : FilterV1, fullState : Maybe Bool - , presenceV1 : Maybe PresenceV1 + , presence : Maybe String , since : Maybe String , timeout : Maybe Int } @@ -77,22 +71,21 @@ type alias SyncInputV1 a = | -- filter : FilterV1 , since : Maybe String , fullState : Maybe Bool - , presenceV1 : Maybe PresenceV1 + , presence : Maybe String , timeout : Maybe Int } -presenceV1ToString : PresenceV1 -> String -presenceV1ToString p = - case p of - OfflineV1 -> - "offline" +presenceFromOptions : List String -> Maybe String -> Maybe String +presenceFromOptions options = + Maybe.andThen + (\v -> + if List.member v options then + Just v - OnlineV1 -> - "online" - - UnavailableV1 -> - "unavailable" + else + Nothing + ) syncV1 : SyncInputV1 i -> A.TaskChain (PhantomV1 a) (PhantomV1 a) @@ -102,8 +95,8 @@ syncV1 data = [ R.accessToken , R.queryOpString "filter" Nothing -- FILTER HERE , R.queryOpBool "full_state" data.fullState - , data.presenceV1 - |> Maybe.map presenceV1ToString + , data.presence + |> presenceFromOptions [ "offline", "online", "unavailable" ] |> R.queryOpString "set_presence" , R.queryOpString "since" data.since , R.queryOpInt "timeout" data.timeout @@ -124,8 +117,8 @@ syncV2 data = [ R.accessToken , R.queryOpString "filter" Nothing , R.queryOpBool "full_state" data.fullState - , data.presenceV1 - |> Maybe.map presenceV1ToString + , data.presence + |> presenceFromOptions [ "offline", "online", "unavailable" ] |> R.queryOpString "set_presence" , R.queryOpString "since" data.since , R.queryOpInt "timeout" data.timeout @@ -146,8 +139,8 @@ syncV3 data = [ R.accessToken , R.queryOpString "filter" Nothing , R.queryOpBool "full_state" data.fullState - , data.presenceV1 - |> Maybe.map presenceV1ToString + , data.presence + |> presenceFromOptions [ "offline", "online", "unavailable" ] |> R.queryOpString "set_presence" , R.queryOpString "since" data.since , R.queryOpInt "timeout" data.timeout @@ -168,8 +161,8 @@ syncV4 data = [ R.accessToken , R.queryOpString "filter" Nothing , R.queryOpBool "full_state" data.fullState - , data.presenceV1 - |> Maybe.map presenceV1ToString + , data.presence + |> presenceFromOptions [ "offline", "online", "unavailable" ] |> R.queryOpString "set_presence" , R.queryOpString "since" data.since , R.queryOpInt "timeout" data.timeout diff --git a/src/Internal/Api/Task.elm b/src/Internal/Api/Task.elm index 744b1d2..81205da 100644 --- a/src/Internal/Api/Task.elm +++ b/src/Internal/Api/Task.elm @@ -1,6 +1,6 @@ module Internal.Api.Task exposing ( Task, run, Backpack - , sendMessageEvent + , sendMessageEvent, sync ) {-| @@ -23,7 +23,7 @@ up-to-date. ## Tasks -@docs sendMessageEvent +@docs sendMessageEvent, sync -} @@ -33,6 +33,7 @@ import Internal.Api.LoginWithUsernameAndPassword.Api import Internal.Api.Now.Api import Internal.Api.Request as Request import Internal.Api.SendMessageEvent.Api +import Internal.Api.Sync.Api import Internal.Api.Versions.Api import Internal.Config.Log exposing (Log, log) import Internal.Tools.Json as Json @@ -217,6 +218,15 @@ sendMessageEvent input = |> finishTask +{-| Sync with the Matrix API to stay up-to-date. +-} +sync : { fullState : Maybe Bool, presence : Maybe String, since : Maybe String, timeout : Maybe Int } -> Task +sync input = + makeVBA + |> C.andThen (Internal.Api.Sync.Api.sync input) + |> finishTask + + {-| Transform a completed task into a Cmd. -} run : (Backpack -> msg) -> Task -> APIContext {} -> Cmd msg diff --git a/src/Internal/Config/Text.elm b/src/Internal/Config/Text.elm index 7a0326e..08d5d1b 100644 --- a/src/Internal/Config/Text.elm +++ b/src/Internal/Config/Text.elm @@ -330,6 +330,7 @@ fields : , settings : { currentVersion : Desc , deviceName : Desc + , presence : Desc , removePasswordOnLogin : Desc , syncTime : Desc } @@ -519,6 +520,9 @@ fields = , deviceName = [ "Indicates the device name that is communicated to the Matrix API." ] + , presence = + [ "Controls whether the client is automatically marked as online. The value is passed on to the Matrix API." + ] , removePasswordOnLogin = [ "Remove the password as soon as a valid access token has been received." ] diff --git a/src/Internal/Values/Settings.elm b/src/Internal/Values/Settings.elm index d0f40c4..69e1e70 100644 --- a/src/Internal/Values/Settings.elm +++ b/src/Internal/Values/Settings.elm @@ -35,6 +35,7 @@ behave under the user's preferred settings. type alias Settings = { currentVersion : String , deviceName : String + , presence : Maybe String , removePasswordOnLogin : Bool , syncTime : Int } @@ -44,7 +45,7 @@ type alias Settings = -} coder : Json.Coder Settings coder = - Json.object4 + Json.object5 { name = Text.docs.settings.name , description = Text.docs.settings.description , init = Settings @@ -65,6 +66,13 @@ coder = , default = Tuple.pair Default.deviceName [] } ) + (Json.field.optional.value + { fieldName = "presence" + , toField = .presence + , description = Text.fields.settings.presence + , coder = Json.string + } + ) (Json.field.optional.withDefault { fieldName = "removePasswordOnLogin" , toField = .removePasswordOnLogin @@ -103,6 +111,7 @@ init : Settings init = { currentVersion = Default.currentVersion , deviceName = Default.deviceName + , presence = Nothing , removePasswordOnLogin = Default.removePasswordOnLogin , syncTime = Default.syncTime } From 005e103389e9dbf9d7bc5de03e0edbb4007c4515 Mon Sep 17 00:00:00 2001 From: Bram Date: Tue, 9 Jul 2024 17:50:48 +0200 Subject: [PATCH 23/34] Expose sync function --- src/Internal/Api/Main.elm | 4 ++-- src/Matrix.elm | 15 +++++++++++++-- 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/src/Internal/Api/Main.elm b/src/Internal/Api/Main.elm index 04011d0..bf9b1ea 100644 --- a/src/Internal/Api/Main.elm +++ b/src/Internal/Api/Main.elm @@ -63,7 +63,7 @@ sendMessageEvent env data = -} sync : E.Envelope a - -> { timeout : Int, toMsg : Msg -> msg } + -> { toMsg : Msg -> msg } -> Cmd msg sync env data = ITask.run @@ -72,7 +72,7 @@ sync env data = { fullState = Nothing , presence = env.settings.presence , since = env.context.nextBatch - , timeout = Just data.timeout + , timeout = Just env.settings.syncTime } ) (Context.apiFormat env.context) diff --git a/src/Matrix.elm b/src/Matrix.elm index f59136a..64f411f 100644 --- a/src/Matrix.elm +++ b/src/Matrix.elm @@ -1,6 +1,6 @@ module Matrix exposing ( Vault, fromUserId, fromUsername - , VaultUpdate, update + , VaultUpdate, update, sync , addAccessToken, sendMessageEvent ) @@ -24,7 +24,7 @@ support a monolithic public registry. (: ## Keeping the Vault up-to-date -@docs VaultUpdate, update +@docs VaultUpdate, update, sync ## Debugging @@ -141,6 +141,17 @@ sendMessageEvent data = } +{-| Synchronize the Vault with the Matrix API. + +Effectively, this task asks the Matrix API to provide the latest information, +which will be returned as your VaultUpdate. + +-} +sync : (VaultUpdate -> msg) -> Vault -> Cmd msg +sync toMsg (Vault vault) = + Api.sync vault { toMsg = Types.VaultUpdate >> toMsg } + + {-| Using new VaultUpdate information, update the Vault accordingly. This allows us to change our perception of the Matrix environment: has anyone From 39f8021a8f9a6daa108cf60c76bfac5bf1bbf554 Mon Sep 17 00:00:00 2001 From: Bram Date: Sat, 13 Jul 2024 09:38:57 +0200 Subject: [PATCH 24/34] Fix spec misconception Apparently, the field is also meant for other reasons than the cputime unlimited filesize unlimited datasize unlimited stacksize 8MB coredumpsize unlimited resident unlimited maxproc 62741 descriptors 524288 memorylocked 8MB addressspace unlimited maxfilelocks unlimited sigpending 62741 msgqueue 819200 nice 0 rt_priority 0 rt_time unlimited field on the filter, despite the description suggesting otherwise. --- src/Internal/Api/Sync/Api.elm | 16 +++++----- src/Internal/Api/Sync/V1.elm | 60 ++++++++++++++++++++++++++++------- src/Internal/Api/Sync/V2.elm | 60 ++++++++++++++++++++++++++++------- src/Internal/Api/Sync/V4.elm | 60 ++++++++++++++++++++++++++++------- 4 files changed, 155 insertions(+), 41 deletions(-) diff --git a/src/Internal/Api/Sync/Api.elm b/src/Internal/Api/Sync/Api.elm index 59a622a..ed094eb 100644 --- a/src/Internal/Api/Sync/Api.elm +++ b/src/Internal/Api/Sync/Api.elm @@ -104,9 +104,9 @@ syncV1 data = , coder = V1.coderSyncResponse , contextChange = always identity , method = "GET" - , path = [ "_matrix", "client", "r0", "sync" ] + , path = [ "_matrix", "client", "v3", "sync" ] , toUpdate = - V1.updateSyncResponse { filter = Filter.pass, since = data.since } + Debug.log "Handling output v1" >> V1.updateSyncResponse { filter = Filter.pass, since = data.since } >> Debug.log "Received" } @@ -126,9 +126,9 @@ syncV2 data = , coder = V2.coderSyncResponse , contextChange = always identity , method = "GET" - , path = [ "_matrix", "client", "r0", "sync" ] + , path = [ "_matrix", "client", "v3", "sync" ] , toUpdate = - V2.updateSyncResponse { filter = Filter.pass, since = data.since } + Debug.log "Handling output v2" >> V2.updateSyncResponse { filter = Filter.pass, since = data.since } >> Debug.log "Received" } @@ -148,9 +148,9 @@ syncV3 data = , coder = V3.coderSyncResponse , contextChange = always identity , method = "GET" - , path = [ "_matrix", "client", "r0", "sync" ] + , path = [ "_matrix", "client", "v3", "sync" ] , toUpdate = - V3.updateSyncResponse { filter = Filter.pass, since = data.since } + Debug.log "Handling output v3" >> V3.updateSyncResponse { filter = Filter.pass, since = data.since } >> Debug.log "Received" } @@ -170,7 +170,7 @@ syncV4 data = , coder = V4.coderSyncResponse , contextChange = always identity , method = "GET" - , path = [ "_matrix", "client", "r0", "sync" ] + , path = [ "_matrix", "client", "v3", "sync" ] , toUpdate = - V4.updateSyncResponse { filter = Filter.pass, since = data.since } + Debug.log "Handling output v4" >> V4.updateSyncResponse { filter = Filter.pass, since = data.since } >> Debug.log "Received" } diff --git a/src/Internal/Api/Sync/V1.elm b/src/Internal/Api/Sync/V1.elm index 628f727..de62369 100644 --- a/src/Internal/Api/Sync/V1.elm +++ b/src/Internal/Api/Sync/V1.elm @@ -935,18 +935,56 @@ 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 + let + limited : Bool + limited = + Maybe.withDefault False timeline.limited - Nothing -> - since - , end = nextBatch - } + newEvents : List Event.Event + newEvents = + List.map (toEvent roomId) events + in + case ( limited, timeline.prevBatch ) of + ( False, Just p ) -> + if timeline.prevBatch == since then + R.AddSync + { events = newEvents + , filter = filter + , start = Just p + , end = nextBatch + } + + else + R.More + [ R.AddSync + { events = [] + , filter = filter + , start = since + , end = p + } + , R.AddSync + { events = newEvents + , filter = filter + , start = Just p + , end = nextBatch + } + ] + + ( False, Nothing ) -> + R.AddSync + { events = newEvents + , filter = filter + , start = since + , end = nextBatch + } + + ( True, _ ) -> + R.AddSync + { events = newEvents + , filter = filter + , start = timeline.prevBatch + , end = nextBatch + } ) diff --git a/src/Internal/Api/Sync/V2.elm b/src/Internal/Api/Sync/V2.elm index 304a079..85c0d40 100644 --- a/src/Internal/Api/Sync/V2.elm +++ b/src/Internal/Api/Sync/V2.elm @@ -683,18 +683,56 @@ updateJoinedRoom data room = 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 + let + limited : Bool + limited = + Maybe.withDefault False timeline.limited - Nothing -> - since - , end = nextBatch - } + newEvents : List Event.Event + newEvents = + List.map (toEvent roomId) timeline.events + in + case ( limited, timeline.prevBatch ) of + ( False, Just p ) -> + if timeline.prevBatch == since then + R.AddSync + { events = newEvents + , filter = filter + , start = Just p + , end = nextBatch + } + + else + R.More + [ R.AddSync + { events = [] + , filter = filter + , start = since + , end = p + } + , R.AddSync + { events = newEvents + , filter = filter + , start = Just p + , end = nextBatch + } + ] + + ( False, Nothing ) -> + R.AddSync + { events = newEvents + , filter = filter + , start = since + , end = nextBatch + } + + ( True, _ ) -> + R.AddSync + { events = newEvents + , filter = filter + , start = timeline.prevBatch + , end = nextBatch + } toEvent : String -> ClientEventWithoutRoomID -> Event.Event diff --git a/src/Internal/Api/Sync/V4.elm b/src/Internal/Api/Sync/V4.elm index f254401..aa03724 100644 --- a/src/Internal/Api/Sync/V4.elm +++ b/src/Internal/Api/Sync/V4.elm @@ -952,18 +952,56 @@ updateJoinedRoom data room = 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 + let + limited : Bool + limited = + Maybe.withDefault False timeline.limited - Nothing -> - since - , end = nextBatch - } + newEvents : List Event.Event + newEvents = + List.map (toEvent roomId) timeline.events + in + case ( limited, timeline.prevBatch ) of + ( False, Just p ) -> + if timeline.prevBatch == since then + R.AddSync + { events = newEvents + , filter = filter + , start = Just p + , end = nextBatch + } + + else + R.More + [ R.AddSync + { events = [] + , filter = filter + , start = since + , end = p + } + , R.AddSync + { events = newEvents + , filter = filter + , start = Just p + , end = nextBatch + } + ] + + ( False, Nothing ) -> + R.AddSync + { events = newEvents + , filter = filter + , start = since + , end = nextBatch + } + + ( True, _ ) -> + R.AddSync + { events = newEvents + , filter = filter + , start = timeline.prevBatch + , end = nextBatch + } toEvent : String -> ClientEventWithoutRoomID -> Event.Event From 1eb07377fde4c8b48113217552146310d200e5a6 Mon Sep 17 00:00:00 2001 From: Bram Date: Sat, 13 Jul 2024 09:39:14 +0200 Subject: [PATCH 25/34] Fix StrippedEvent JSON coder --- src/Internal/Tools/StrippedEvent.elm | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Internal/Tools/StrippedEvent.elm b/src/Internal/Tools/StrippedEvent.elm index 8465cba..ee64f6f 100644 --- a/src/Internal/Tools/StrippedEvent.elm +++ b/src/Internal/Tools/StrippedEvent.elm @@ -36,7 +36,7 @@ coder = } ) (Json.field.required - { fieldName = "eventType" + { fieldName = "type" , toField = .eventType , description = [ "Event type, generally namespaced using the Java package naming convention." From 458ea594255aaba3e86b720af9b7894a00dab6e8 Mon Sep 17 00:00:00 2001 From: Bram Date: Sat, 13 Jul 2024 09:50:39 +0200 Subject: [PATCH 26/34] Add ways to navigate through rooms --- src/Internal/Values/Vault.elm | 11 +++++++++-- src/Matrix.elm | 22 ++++++++++++++++++++++ src/Matrix/Room.elm | 12 ++++++++++-- 3 files changed, 41 insertions(+), 4 deletions(-) diff --git a/src/Internal/Values/Vault.elm b/src/Internal/Values/Vault.elm index 2725744..8ed3362 100644 --- a/src/Internal/Values/Vault.elm +++ b/src/Internal/Values/Vault.elm @@ -1,7 +1,7 @@ module Internal.Values.Vault exposing ( Vault, init , VaultUpdate(..), update - , fromRoomId, mapRoom, updateRoom + , rooms, fromRoomId, mapRoom, updateRoom , getAccountData, setAccountData ) @@ -23,7 +23,7 @@ To update the Vault, one uses VaultUpdate types. Rooms are environments where people can have a conversation with each other. -@docs fromRoomId, mapRoom, updateRoom +@docs rooms, fromRoomId, mapRoom, updateRoom ## Account data @@ -122,6 +122,13 @@ mapRoom roomId f vault = { vault | rooms = Hashdict.map roomId f vault.rooms } +{-| Get a list of all joined rooms present in the vault. +-} +rooms : Vault -> List Room +rooms vault = + Hashdict.values vault.rooms + + {-| Set a piece of account data as information in the global vault data. -} setAccountData : String -> Json.Value -> Vault -> Vault diff --git a/src/Matrix.elm b/src/Matrix.elm index f59136a..1e60a17 100644 --- a/src/Matrix.elm +++ b/src/Matrix.elm @@ -1,6 +1,7 @@ module Matrix exposing ( Vault, fromUserId, fromUsername , VaultUpdate, update + , rooms, fromRoomId , addAccessToken, sendMessageEvent ) @@ -27,6 +28,11 @@ support a monolithic public registry. (: @docs VaultUpdate, update +## Exploring the Vault + +@docs rooms, fromRoomId + + ## Debugging @docs addAccessToken, sendMessageEvent @@ -66,6 +72,14 @@ addAccessToken token (Vault vault) = |> Vault +{-| Get a room based on its room ID, if the user is a member of that room. +-} +fromRoomId : String -> Vault -> Maybe Types.Room +fromRoomId roomId (Vault vault) = + Envelope.mapMaybe (Internal.fromRoomId roomId) vault + |> Maybe.map Types.Room + + {-| Use a fully-fledged Matrix ID to connect. case Matrix.fromUserId "@alice:example.org" of @@ -112,6 +126,14 @@ fromUsername { username, host, port_ } = |> Vault +{-| Get a list of all the rooms that the user has joined. +-} +rooms : Vault -> List Types.Room +rooms (Vault vault) = + Envelope.mapList Internal.rooms vault + |> List.map Types.Room + + {-| Send a message event to a room. This function can be used in a scenario where the user does not want to sync diff --git a/src/Matrix/Room.elm b/src/Matrix/Room.elm index 9ebc08e..a3e24ae 100644 --- a/src/Matrix/Room.elm +++ b/src/Matrix/Room.elm @@ -1,5 +1,5 @@ module Matrix.Room exposing - ( Room, mostRecentEvents + ( Room, mostRecentEvents, roomId , getAccountData ) @@ -12,7 +12,7 @@ What is usually called a chat, a channel, a conversation or a group chat on other platforms, the term used in Matrix is a "room". A room is a conversation where a group of users talk to each other. -@docs Room, mostRecentEvents +@docs Room, mostRecentEvents, roomId This module exposes various functions that help you inspect various aspects of a room. @@ -56,6 +56,14 @@ getAccountData key (Room room) = Envelope.extract (Internal.getAccountData key) room +{-| Get a room's room id. This is an opaque string that distinguishes rooms from +each other. +-} +roomId : Room -> String +roomId (Room room) = + Envelope.extract .roomId room + + {-| Get a list of the most recent events sent in the room. -} mostRecentEvents : Room -> List Types.Event From d7a7fa385ccdf525fcdf3fd1399faf4a4dbbdef9 Mon Sep 17 00:00:00 2001 From: Bram Date: Mon, 15 Jul 2024 15:50:32 +0200 Subject: [PATCH 27/34] Enable safe recursion in VaultUpdate type --- src/Internal/Values/Envelope.elm | 102 +++++++++++++++++++++---------- src/Internal/Values/Room.elm | 35 ++++++----- src/Internal/Values/Vault.elm | 38 +++++++----- 3 files changed, 116 insertions(+), 59 deletions(-) diff --git a/src/Internal/Values/Envelope.elm b/src/Internal/Values/Envelope.elm index 7823e62..1ef986a 100644 --- a/src/Internal/Values/Envelope.elm +++ b/src/Internal/Values/Envelope.elm @@ -56,6 +56,8 @@ import Internal.Tools.Json as Json import Internal.Tools.Timestamp exposing (Timestamp) import Internal.Values.Context as Context exposing (AccessToken, Context, Versions) import Internal.Values.Settings as Settings +import Recursion +import Recursion.Fold {-| There are lots of different data types in the Elm SDK, and many of them @@ -292,47 +294,85 @@ toMaybe data = {-| Updates the Envelope with a given EnvelopeUpdate value. -} update : (au -> a -> a) -> EnvelopeUpdate au -> Envelope a -> Envelope a -update updateContent eu ({ context } as data) = - case eu of - ContentUpdate v -> - { data | content = updateContent v data.content } +update updateContent eu startData = + Recursion.runRecursion + (\updt -> + case updt of + ContentUpdate v -> + Recursion.base + (\data -> + { data | content = updateContent v data.content } + ) - HttpRequest _ -> - data + HttpRequest _ -> + Recursion.base identity - More items -> - List.foldl (update updateContent) data items + More items -> + Recursion.Fold.foldList (<<) identity items - Optional (Just u) -> - update updateContent u data + Optional (Just u) -> + Recursion.recurse u - Optional Nothing -> - data + Optional Nothing -> + Recursion.base identity - RemoveAccessToken token -> - { data | context = { context | accessTokens = Hashdict.removeKey token context.accessTokens } } + RemoveAccessToken token -> + Recursion.base + (\({context} as data) -> + { data + | context = + { context + | accessTokens = + Hashdict.removeKey token context.accessTokens + } + } + ) - RemovePasswordIfNecessary -> - if data.settings.removePasswordOnLogin then - { data | context = { context | password = Nothing } } + RemovePasswordIfNecessary -> + Recursion.base + (\({context} as data) -> + if data.settings.removePasswordOnLogin then + { data | context = { context | password = Nothing } } - else - data + else + data + ) - SetAccessToken a -> - { data | context = { context | accessTokens = Hashdict.insert a context.accessTokens } } + SetAccessToken a -> + Recursion.base + (\({context} as data) -> + { data | context = { context | accessTokens = Hashdict.insert a context.accessTokens } } + ) - SetBaseUrl b -> - { data | context = { context | baseUrl = Just b } } + SetBaseUrl b -> + Recursion.base + (\({ context } as data) -> + { data | context = { context | baseUrl = Just b } } + ) - SetDeviceId d -> - { data | context = { context | deviceId = Just d } } + SetDeviceId d -> + Recursion.base + (\({ context } as data) -> + { data | context = { context | deviceId = Just d } } + ) - SetNow n -> - { data | context = { context | now = Just n } } + SetNow n -> + Recursion.base + (\({ context } as data) -> + { data | context = { context | now = Just n } } + ) - SetRefreshToken r -> - { data | context = { context | refreshToken = Just r } } + SetRefreshToken r -> + Recursion.base + (\({ context } as data) -> + { data | context = { context | refreshToken = Just r } } + ) - SetVersions vs -> - { data | context = { context | versions = Just vs } } + SetVersions vs -> + Recursion.base + (\({ context } as data) -> + { data | context = { context | versions = Just vs } } + ) + ) + eu + startData diff --git a/src/Internal/Values/Room.elm b/src/Internal/Values/Room.elm index 1db7bf1..ca1f817 100644 --- a/src/Internal/Values/Room.elm +++ b/src/Internal/Values/Room.elm @@ -58,6 +58,8 @@ import Internal.Values.StateManager as StateManager exposing (StateManager) import Internal.Values.Timeline as Timeline exposing (Timeline) import Internal.Values.User exposing (User) import Json.Encode as E +import Recursion +import Recursion.Fold {-| The Batch is a group of new events from somewhere in the timeline. @@ -246,21 +248,26 @@ setAccountData key value room = {-| Update the Room based on given instructions. -} update : RoomUpdate -> Room -> Room -update ru room = - case ru of - AddEvent _ -> - -- TODO: Add event - room +update roomUpdate startRoom = + Recursion.runRecursion + (\ru -> + case ru of + AddEvent _ -> + -- TODO: Add event + Recursion.base identity - AddSync batch -> - addSync batch room + AddSync batch -> + Recursion.base (addSync batch) - Invite _ -> - -- TODO: Invite user - room + Invite _ -> + -- TODO: Invite user + Recursion.base identity - More items -> - List.foldl update room items + More items -> + Recursion.Fold.foldList (<<) identity items - SetAccountData key value -> - setAccountData key value room + SetAccountData key value -> + Recursion.base (setAccountData key value) + ) + roomUpdate + startRoom diff --git a/src/Internal/Values/Vault.elm b/src/Internal/Values/Vault.elm index 2725744..27bc43b 100644 --- a/src/Internal/Values/Vault.elm +++ b/src/Internal/Values/Vault.elm @@ -38,6 +38,8 @@ import Internal.Tools.Hashdict as Hashdict exposing (Hashdict) import Internal.Tools.Json as Json import Internal.Values.Room as Room exposing (Room) import Internal.Values.User as User exposing (User) +import Recursion +import Recursion.Fold {-| This is the Vault type. @@ -139,21 +141,29 @@ updateRoom roomId f vault = {-| Update the Vault using a VaultUpdate type. -} update : VaultUpdate -> Vault -> Vault -update vu vault = - case vu of - CreateRoomIfNotExists roomId -> - updateRoom roomId - (Maybe.withDefault (Room.init roomId) >> Maybe.Just) - vault +update vaultUpdate startVault = + Recursion.runRecursion + (\vu -> + case vu of + CreateRoomIfNotExists roomId -> + (Maybe.withDefault (Room.init roomId) >> Maybe.Just) + |> updateRoom roomId + |> Recursion.base - MapRoom roomId ru -> - mapRoom roomId (Room.update ru) vault + MapRoom roomId ru -> + Recursion.base (mapRoom roomId (Room.update ru)) - More items -> - List.foldl update vault items + More items -> + Recursion.Fold.foldList (<<) identity items - SetAccountData key value -> - setAccountData key value vault + SetAccountData key value -> + Recursion.base (setAccountData key value) - SetUser user -> - { vault | user = Just user } + SetUser user -> + Recursion.base + (\vault -> + { vault | user = Just user } + ) + ) + vaultUpdate + startVault From 90eb06f3a19d0d02b373b5975de0613b44f8cccf Mon Sep 17 00:00:00 2001 From: Bram Date: Mon, 15 Jul 2024 15:57:08 +0200 Subject: [PATCH 28/34] elm-format --- src/Internal/Values/Envelope.elm | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/Internal/Values/Envelope.elm b/src/Internal/Values/Envelope.elm index 1ef986a..7c0266d 100644 --- a/src/Internal/Values/Envelope.elm +++ b/src/Internal/Values/Envelope.elm @@ -317,20 +317,20 @@ update updateContent eu startData = Recursion.base identity RemoveAccessToken token -> - Recursion.base - (\({context} as data) -> + Recursion.base + (\({ context } as data) -> { data - | context = - { context - | accessTokens = - Hashdict.removeKey token context.accessTokens - } + | context = + { context + | accessTokens = + Hashdict.removeKey token context.accessTokens + } } ) RemovePasswordIfNecessary -> Recursion.base - (\({context} as data) -> + (\({ context } as data) -> if data.settings.removePasswordOnLogin then { data | context = { context | password = Nothing } } @@ -340,7 +340,7 @@ update updateContent eu startData = SetAccessToken a -> Recursion.base - (\({context} as data) -> + (\({ context } as data) -> { data | context = { context | accessTokens = Hashdict.insert a context.accessTokens } } ) From 1736679e0f974c0ee8ab83a64a4c787f8ce1e34d Mon Sep 17 00:00:00 2001 From: Bram Date: Mon, 15 Jul 2024 16:07:28 +0200 Subject: [PATCH 29/34] elm-format --- src/Internal/Values/Room.elm | 7 +++---- src/Internal/Values/Vault.elm | 4 ++-- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/src/Internal/Values/Room.elm b/src/Internal/Values/Room.elm index f00db1d..5d9e2cd 100644 --- a/src/Internal/Values/Room.elm +++ b/src/Internal/Values/Room.elm @@ -277,16 +277,15 @@ update roomUpdate startRoom = Optional (Just u) -> Recursion.recurse u - + Optional Nothing -> Recursion.base identity - + SetAccountData key value -> Recursion.base (setAccountData key value) - + SetEphemeral eph -> Recursion.base (\room -> { room | ephemeral = eph }) ) roomUpdate startRoom - diff --git a/src/Internal/Values/Vault.elm b/src/Internal/Values/Vault.elm index c331662..eff1766 100644 --- a/src/Internal/Values/Vault.elm +++ b/src/Internal/Values/Vault.elm @@ -176,10 +176,10 @@ update vaultUpdate startVault = Optional (Just u) -> Recursion.recurse u - + Optional Nothing -> Recursion.base identity - + SetAccountData key value -> Recursion.base (setAccountData key value) From f3799add8798cb593f657d62c8fbd1b7481bbf08 Mon Sep 17 00:00:00 2001 From: Bram Date: Mon, 15 Jul 2024 16:13:11 +0200 Subject: [PATCH 30/34] HOTFIX: Fix syntax error --- src/Internal/Values/Envelope.elm | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Internal/Values/Envelope.elm b/src/Internal/Values/Envelope.elm index 90ce910..82c8d81 100644 --- a/src/Internal/Values/Envelope.elm +++ b/src/Internal/Values/Envelope.elm @@ -358,10 +358,10 @@ update updateContent eu startData = SetNextBatch nextBatch -> Recursion.base - (\{ context } as data -> + (\({ context } as data) -> { data | context = { context | nextBatch = Just nextBatch } } ) - + SetNow n -> Recursion.base (\({ context } as data) -> From a401c25a470ecfab492fb213d645161c412b6e0e Mon Sep 17 00:00:00 2001 From: Bram Date: Tue, 16 Jul 2024 10:10:42 +0200 Subject: [PATCH 31/34] Remove issues & warnings --- src/Internal/Api/BaseUrl/Api.elm | 1 - src/Internal/Api/GetEvent/Api.elm | 2 +- .../Api/LoginWithUsernameAndPassword/Api.elm | 1 - src/Internal/Api/SendMessageEvent/Api.elm | 1 - src/Internal/Api/Sync/V4.elm | 1 - src/Internal/Grammar/ServerName.elm | 27 +++++++++---------- 6 files changed, 14 insertions(+), 19 deletions(-) diff --git a/src/Internal/Api/BaseUrl/Api.elm b/src/Internal/Api/BaseUrl/Api.elm index 5e98ed3..21333dc 100644 --- a/src/Internal/Api/BaseUrl/Api.elm +++ b/src/Internal/Api/BaseUrl/Api.elm @@ -13,7 +13,6 @@ This module looks for the right homeserver address. import Internal.Api.Chain as C import Internal.Api.Request as R -import Internal.Config.Leaks as L import Internal.Config.Log exposing (log) import Internal.Config.Text as Text import Internal.Tools.Json as Json diff --git a/src/Internal/Api/GetEvent/Api.elm b/src/Internal/Api/GetEvent/Api.elm index dd10f0d..e81c599 100644 --- a/src/Internal/Api/GetEvent/Api.elm +++ b/src/Internal/Api/GetEvent/Api.elm @@ -204,7 +204,7 @@ getEventCoderV1 = [ "UnsignedData as described by the Matrix spec" , "https://spec.matrix.org/v1.10/client-server-api/#get_matrixclientv3roomsroomideventeventid" ] - , init = \a b c d -> Event.UnsignedData { age = a, prevContent = b, redactedBecause = c, transactionId = d } + , init = \a b c d -> Event.UnsignedData { age = a, membership = Nothing, prevContent = b, redactedBecause = c, transactionId = d } } (Json.field.optional.value { fieldName = "age" diff --git a/src/Internal/Api/LoginWithUsernameAndPassword/Api.elm b/src/Internal/Api/LoginWithUsernameAndPassword/Api.elm index 9242ba2..b5cddcd 100644 --- a/src/Internal/Api/LoginWithUsernameAndPassword/Api.elm +++ b/src/Internal/Api/LoginWithUsernameAndPassword/Api.elm @@ -13,7 +13,6 @@ This module allows the user to log in using a username and password. import Internal.Api.Api as A import Internal.Api.Request as R -import Internal.Config.Leaks as L import Internal.Config.Log exposing (log) import Internal.Config.Text as Text import Internal.Tools.Json as Json diff --git a/src/Internal/Api/SendMessageEvent/Api.elm b/src/Internal/Api/SendMessageEvent/Api.elm index abccf41..9e298f0 100644 --- a/src/Internal/Api/SendMessageEvent/Api.elm +++ b/src/Internal/Api/SendMessageEvent/Api.elm @@ -13,7 +13,6 @@ This module helps send message events to rooms on the Matrix API. import Internal.Api.Api as A import Internal.Api.Request as R -import Internal.Config.Leaks as L import Internal.Config.Log exposing (log) import Internal.Config.Text as Text import Internal.Tools.Json as Json diff --git a/src/Internal/Api/Sync/V4.elm b/src/Internal/Api/Sync/V4.elm index aa03724..b574997 100644 --- a/src/Internal/Api/Sync/V4.elm +++ b/src/Internal/Api/Sync/V4.elm @@ -12,7 +12,6 @@ This API module represents the /sync endpoint on Matrix spec version v1.11. -} import FastDict as Dict exposing (Dict) -import Internal.Api.Sync.V3 as PV import Internal.Config.Log exposing (Log, log) import Internal.Config.Text as Text import Internal.Filter.Timeline exposing (Filter) diff --git a/src/Internal/Grammar/ServerName.elm b/src/Internal/Grammar/ServerName.elm index 4b9f1b6..4899881 100644 --- a/src/Internal/Grammar/ServerName.elm +++ b/src/Internal/Grammar/ServerName.elm @@ -189,21 +189,20 @@ ipv6RightParser n = |. P.symbol ":" -{-| Convert an IPv6 address to a readable string format --} -ipv6ToString : IPv6Address -> String -ipv6ToString { front, back } = - (if List.length front == 8 then - front - else if List.length back == 8 then - back - - else - List.concat [ front, [ "" ], back ] - ) - |> List.intersperse ":" - |> String.concat +-- {-| Convert an IPv6 address to a readable string format +-- -} +-- ipv6ToString : IPv6Address -> String +-- ipv6ToString { front, back } = +-- (if List.length front == 8 then +-- front +-- else if List.length back == 8 then +-- back +-- else +-- List.concat [ front, [ "" ], back ] +-- ) +-- |> List.intersperse ":" +-- |> String.concat portParser : Parser Int From 20504d4a8b85528e5f55b482f9726be0aece860f Mon Sep 17 00:00:00 2001 From: Bram Date: Tue, 16 Jul 2024 10:20:38 +0200 Subject: [PATCH 32/34] Remove test issues & warnings --- src/Internal/Values/Context.elm | 1 - src/Internal/Values/Room.elm | 1 - src/Internal/Values/Timeline.elm | 29 +++++++++++++++-------------- src/Internal/Values/Vault.elm | 8 ++++++++ tests/Test/Values/Context.elm | 9 +++++---- tests/Test/Values/Envelope.elm | 3 --- tests/Test/Values/Event.elm | 6 ++++-- tests/Test/Values/Room.elm | 2 -- tests/Test/Values/Settings.elm | 3 ++- 9 files changed, 34 insertions(+), 28 deletions(-) diff --git a/src/Internal/Values/Context.elm b/src/Internal/Values/Context.elm index 5fc358d..d53783d 100644 --- a/src/Internal/Values/Context.elm +++ b/src/Internal/Values/Context.elm @@ -71,7 +71,6 @@ import Internal.Config.Text as Text import Internal.Tools.Hashdict as Hashdict exposing (Hashdict) import Internal.Tools.Json as Json import Internal.Tools.Timestamp as Timestamp exposing (Timestamp) -import Json.Encode as E import Set exposing (Set) import Time diff --git a/src/Internal/Values/Room.elm b/src/Internal/Values/Room.elm index 5d9e2cd..8602c11 100644 --- a/src/Internal/Values/Room.elm +++ b/src/Internal/Values/Room.elm @@ -58,7 +58,6 @@ import Internal.Values.Event as Event exposing (Event) import Internal.Values.StateManager as StateManager exposing (StateManager) import Internal.Values.Timeline as Timeline exposing (Timeline) import Internal.Values.User exposing (User) -import Json.Encode as E import Recursion import Recursion.Fold diff --git a/src/Internal/Values/Timeline.elm b/src/Internal/Values/Timeline.elm index 1e0459a..513baa0 100644 --- a/src/Internal/Values/Timeline.elm +++ b/src/Internal/Values/Timeline.elm @@ -678,20 +678,21 @@ mostRecentFrom filter timeline ptr = { ptr = ptr, visited = Set.empty } -{-| Recount the Timeline's amount of filled batches. Since the Timeline -automatically tracks the count on itself, this is generally exclusively used in -specific scenarios like decoding JSON values. --} -recountFilledBatches : Timeline -> Timeline -recountFilledBatches (Timeline tl) = - Timeline - { tl - | filledBatches = - tl.batches - |> Iddict.values - |> List.filter (\v -> v.events /= []) - |> List.length - } + +-- {-| Recount the Timeline's amount of filled batches. Since the Timeline +-- automatically tracks the count on itself, this is generally exclusively used in +-- specific scenarios like decoding JSON values. +-- -} +-- recountFilledBatches : Timeline -> Timeline +-- recountFilledBatches (Timeline tl) = +-- Timeline +-- { tl +-- | filledBatches = +-- tl.batches +-- |> Iddict.values +-- |> List.filter (\v -> v.events /= []) +-- |> List.length +-- } {-| Create a timeline with a single batch inserted. This batch is considered the diff --git a/src/Internal/Values/Vault.elm b/src/Internal/Values/Vault.elm index eff1766..3e048d3 100644 --- a/src/Internal/Values/Vault.elm +++ b/src/Internal/Values/Vault.elm @@ -3,6 +3,7 @@ module Internal.Values.Vault exposing , VaultUpdate(..), update , rooms, fromRoomId, mapRoom, updateRoom , getAccountData, setAccountData + , coder ) {-| This module hosts the Vault module. The Vault is the data type storing all @@ -30,6 +31,11 @@ Rooms are environments where people can have a conversation with each other. @docs getAccountData, setAccountData + +## JSON + +@docs coder + -} import FastDict as Dict exposing (Dict) @@ -65,6 +71,8 @@ type VaultUpdate | SetUser User +{-| Convert a Vault to and from a JSON object. +-} coder : Json.Coder Vault coder = Json.object4 diff --git a/tests/Test/Values/Context.elm b/tests/Test/Values/Context.elm index 2994de8..888287d 100644 --- a/tests/Test/Values/Context.elm +++ b/tests/Test/Values/Context.elm @@ -5,8 +5,6 @@ import Fuzz exposing (Fuzzer) import Internal.Config.Leaks as Leaks import Internal.Tools.Hashdict as Hashdict import Internal.Values.Context as Context exposing (Context, Versions) -import Json.Decode as D -import Json.Encode as E import Set import Test exposing (..) import Test.Tools.Timestamp as TestTimestamp @@ -19,12 +17,15 @@ fuzzer = maybeString = Fuzz.maybe Fuzz.string in - Fuzz.map8 (\a b c d e ( f, g ) ( h, i ) ( j, k ) -> Context a b c d e f g h i j k) + Fuzz.map8 (\a b c d ( e, f ) ( g, h ) ( i, j ) ( k, l ) -> Context a b c d e f g h i j k l) (Fuzz.constant <| Hashdict.empty .value) maybeString maybeString - (Fuzz.maybe TestTimestamp.fuzzer) maybeString + (Fuzz.pair + (Fuzz.maybe TestTimestamp.fuzzer) + maybeString + ) (Fuzz.pair maybeString Fuzz.string diff --git a/tests/Test/Values/Envelope.elm b/tests/Test/Values/Envelope.elm index f86bf7f..81bb569 100644 --- a/tests/Test/Values/Envelope.elm +++ b/tests/Test/Values/Envelope.elm @@ -3,10 +3,7 @@ module Test.Values.Envelope exposing (..) import Expect import Fuzz exposing (Fuzzer) import Internal.Config.Default as Default -import Internal.Tools.Json as Json import Internal.Values.Envelope as Envelope exposing (Envelope) -import Json.Decode as D -import Json.Encode as E import Test exposing (..) import Test.Values.Context as TestContext import Test.Values.Settings as TestSettings diff --git a/tests/Test/Values/Event.elm b/tests/Test/Values/Event.elm index 35ba18e..3731cb1 100644 --- a/tests/Test/Values/Event.elm +++ b/tests/Test/Values/Event.elm @@ -41,16 +41,18 @@ fuzzerState = unsignedDataFuzzer : Fuzzer Event.UnsignedData unsignedDataFuzzer = - Fuzz.map4 - (\age prev redact trans -> + Fuzz.map5 + (\age memb prev redact trans -> Event.UnsignedData { age = age + , membership = memb , prevContent = prev , redactedBecause = redact , transactionId = trans } ) (Fuzz.maybe Fuzz.int) + (Fuzz.maybe Fuzz.string) (Fuzz.maybe valueFuzzer) (Fuzz.maybe <| Fuzz.lazy (\_ -> fuzzer)) (Fuzz.maybe Fuzz.string) diff --git a/tests/Test/Values/Room.elm b/tests/Test/Values/Room.elm index d2aed8d..d3fec18 100644 --- a/tests/Test/Values/Room.elm +++ b/tests/Test/Values/Room.elm @@ -4,8 +4,6 @@ import Fuzz exposing (Fuzzer) import Internal.Values.Room as Room exposing (Room) import Json.Encode as E import Test exposing (..) -import Test.Filter.Timeline as TestFilter -import Test.Values.Event as TestEvent placeholderValue : E.Value diff --git a/tests/Test/Values/Settings.elm b/tests/Test/Values/Settings.elm index 55aff8b..d011d92 100644 --- a/tests/Test/Values/Settings.elm +++ b/tests/Test/Values/Settings.elm @@ -11,7 +11,7 @@ import Test exposing (..) fuzzer : Fuzzer Settings fuzzer = - Fuzz.map4 Settings + Fuzz.map5 Settings (Fuzz.oneOf [ Fuzz.constant Default.currentVersion , Fuzz.string @@ -22,6 +22,7 @@ fuzzer = , Fuzz.string ] ) + (Fuzz.maybe Fuzz.string) (Fuzz.oneOf [ Fuzz.constant Default.removePasswordOnLogin , Fuzz.bool From cacb876a95074c1ee752ac118f3826e09cc41de1 Mon Sep 17 00:00:00 2001 From: Bram Date: Tue, 16 Jul 2024 12:05:23 +0200 Subject: [PATCH 33/34] Fix test errors elm-test --fuzz 1000 --seed 156536263253947 --- src/Internal/Tools/Json.elm | 15 ++++++++------- tests/Test/Values/Timeline.elm | 4 +++- 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/src/Internal/Tools/Json.elm b/src/Internal/Tools/Json.elm index ab609fa..799298a 100644 --- a/src/Internal/Tools/Json.elm +++ b/src/Internal/Tools/Json.elm @@ -477,13 +477,14 @@ iddict (Coder old) = Coder { encoder = Iddict.encode old.encoder , decoder = - D.andThen - (\( out, logs ) -> - D.succeed out - |> Iddict.decoder - |> D.map (\o -> ( o, logs )) - ) - old.decoder + Iddict.decoder old.decoder + |> D.map + (\out -> + ( Iddict.map (always Tuple.first) out + , Iddict.values out + |> List.concatMap Tuple.second + ) + ) , docs = DocsIddict old.docs } diff --git a/tests/Test/Values/Timeline.elm b/tests/Test/Values/Timeline.elm index 83a15dd..2097ea0 100644 --- a/tests/Test/Values/Timeline.elm +++ b/tests/Test/Values/Timeline.elm @@ -6,6 +6,7 @@ import Internal.Filter.Timeline as Filter import Internal.Tools.Json as Json import Internal.Values.Timeline as Timeline exposing (Batch, Timeline) import Json.Decode as D +import Json.Encode as E import Test exposing (..) import Test.Filter.Timeline as TestFilter @@ -250,7 +251,8 @@ suite = (\timeline -> timeline |> Json.encode Timeline.coder - |> D.decodeValue (Json.decode Timeline.coder) + |> E.encode 0 + |> D.decodeString (Json.decode Timeline.coder) |> Result.map Tuple.first |> Result.map (Timeline.mostRecentEvents Filter.pass) |> Expect.equal (Ok <| Timeline.mostRecentEvents Filter.pass timeline) From 1ed9fa7d22a04ad231a16a0880945d191f977665 Mon Sep 17 00:00:00 2001 From: Bram Date: Tue, 16 Jul 2024 12:06:35 +0200 Subject: [PATCH 34/34] Prepare develop for master elm-test --fuzz 1000 --seed 156536263253947 --- elm.json | 2 +- src/Internal/Config/Default.elm | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/elm.json b/elm.json index 7c3b668..0eed841 100644 --- a/elm.json +++ b/elm.json @@ -3,7 +3,7 @@ "name": "noordstar/elm-matrix-sdk-beta", "summary": "Matrix SDK for instant communication. Unstable beta version for testing only.", "license": "EUPL-1.1", - "version": "3.3.1", + "version": "3.4.0", "exposed-modules": [ "Matrix", "Matrix.Event", diff --git a/src/Internal/Config/Default.elm b/src/Internal/Config/Default.elm index 4b56824..606468e 100644 --- a/src/Internal/Config/Default.elm +++ b/src/Internal/Config/Default.elm @@ -29,7 +29,7 @@ will assume until overriden by the user. -} currentVersion : String currentVersion = - "beta 3.3.1" + "beta 3.4.0" {-| The default device name that is being communicated with the Matrix API.