diff --git a/src/Internal/Api/Task.elm b/src/Internal/Api/Task.elm index 70d8cbf..a7d7710 100644 --- a/src/Internal/Api/Task.elm +++ b/src/Internal/Api/Task.elm @@ -7,6 +7,7 @@ import Hash import Internal.Api.Chain as Chain import Internal.Api.Credentials as Cred exposing (Credentials) import Internal.Api.GetEvent.Main exposing (EventInput) +import Internal.Api.GetMessages.Main exposing (GetMessagesInput) import Internal.Api.Invite.Main exposing (InviteInput) import Internal.Api.JoinRoomById.Main exposing (JoinRoomByIdInput) import Internal.Api.JoinedMembers.Main exposing (JoinedMembersInput) @@ -35,6 +36,13 @@ getEvent { eventId, roomId } cred = |> C.toTask +getMessages : GetMessagesInput -> Credentials -> FutureTask +getMessages data cred = + C.makeVBA cred + |> Chain.andThen (C.getMessages data) + |> C.toTask + + invite : InviteInput -> Credentials -> FutureTask invite data cred = C.makeVBA cred diff --git a/src/Internal/Event.elm b/src/Internal/Event.elm index f1bb8ae..3df1122 100644 --- a/src/Internal/Event.elm +++ b/src/Internal/Event.elm @@ -10,6 +10,7 @@ resend other events or forward them elsewhere. import Internal.Api.Credentials exposing (Credentials) import Internal.Api.GetEvent.Main as GetEvent import Internal.Api.GetEvent.V1.SpecObjects as GetEventSO +import Internal.Api.GetMessages.V4.SpecObjects as GetMessagesSO import Internal.Api.Sync.V2.SpecObjects as SyncSO import Internal.Tools.Timestamp exposing (Timestamp) import Internal.Values.Event as Internal @@ -62,6 +63,32 @@ initFromGetEvent output = } +{-| Create an internal event type from an API endpoint event object. +This function is placed in this file to respect file hierarchy and avoid circular imports. +-} +initFromGetMessages : GetMessagesSO.ClientEvent -> Internal.IEvent +initFromGetMessages output = + Internal.init + { content = output.content + , eventId = output.eventId + , originServerTs = output.originServerTs + , roomId = output.roomId + , sender = output.sender + , stateKey = output.stateKey + , contentType = output.contentType + , unsigned = + output.unsigned + |> Maybe.map + (\(GetMessagesSO.UnsignedData data) -> + { age = data.age + , prevContent = data.prevContent + , redactedBecause = Maybe.map initFromGetMessages data.redactedBecause + , transactionId = data.transactionId + } + ) + } + + {-| Create an internal event type from an API endpoint event object. This function is placed in this file to respect file hierarchy and avoid circular imports. -} diff --git a/src/Internal/Room.elm b/src/Internal/Room.elm index 52d56f6..58694a0 100644 --- a/src/Internal/Room.elm +++ b/src/Internal/Room.elm @@ -7,10 +7,11 @@ import Dict import Internal.Api.Credentials exposing (Credentials) import Internal.Api.Sync.V2.SpecObjects as Sync import Internal.Api.Task as Api -import Internal.Api.VaultUpdate exposing (VaultUpdate) +import Internal.Api.VaultUpdate exposing (VaultUpdate(..)) import Internal.Event as Event exposing (Event) import Internal.Tools.Exceptions as X import Internal.Tools.Hashdict as Hashdict +import Internal.Tools.SpecEnums as Enums import Internal.Values.Event as IEvent import Internal.Values.Room as Internal import Internal.Values.StateManager as StateManager @@ -118,6 +119,26 @@ withoutCredentials (Room { room }) = room +{-| Get older events from the Matrix API. +-} +getOlderEvents : { limit : Maybe Int } -> Room -> Task X.Error VaultUpdate +getOlderEvents { limit } (Room { context, room }) = + case Internal.latestGap room of + Nothing -> + Task.succeed (MultipleUpdates []) + + Just { from, to } -> + Api.getMessages + { direction = Enums.ReverseChronological + , filter = Nothing + , from = Just to + , limit = limit + , roomId = Internal.roomId room + , to = from + } + context + + {-| Get the most recent events. -} mostRecentEvents : Room -> List Event diff --git a/src/Internal/Values/Room.elm b/src/Internal/Values/Room.elm index 334c829..8564ddd 100644 --- a/src/Internal/Values/Room.elm +++ b/src/Internal/Values/Room.elm @@ -59,11 +59,28 @@ getStateEvent data (IRoom room) = |> StateManager.getStateEvent data -{-| Get the room's id. +{-| Insert a chunk of events into a room. -} -roomId : IRoom -> String -roomId (IRoom room) = - room.roomId +insertEvents : + { events : List IEvent + , nextBatch : String + , prevBatch : Maybe String + , stateDelta : Maybe StateManager + } + -> IRoom + -> IRoom +insertEvents data (IRoom ({ timeline } as room)) = + IRoom + { room | timeline = Timeline.insertEvents data timeline } + |> List.foldl addEvent + |> (|>) data.events + + +{-| Get the latest gap. +-} +latestGap : IRoom -> Maybe { from : Maybe String, to : String } +latestGap (IRoom room) = + Timeline.latestGap room.timeline {-| Get the most recent events. @@ -71,3 +88,10 @@ roomId (IRoom room) = mostRecentEvents : IRoom -> List IEvent mostRecentEvents (IRoom room) = Timeline.mostRecentEvents room.timeline + + +{-| Get the room's id. +-} +roomId : IRoom -> String +roomId (IRoom room) = + room.roomId diff --git a/src/Internal/Values/Timeline.elm b/src/Internal/Values/Timeline.elm index ad044ca..628527a 100644 --- a/src/Internal/Values/Timeline.elm +++ b/src/Internal/Values/Timeline.elm @@ -92,63 +92,119 @@ newFromEvents { events, nextBatch, prevBatch, stateDelta } = insertEvents : { events : List IEvent , nextBatch : String - , prevBatch : String + , prevBatch : Maybe String , stateDelta : Maybe StateManager } -> Timeline -> Timeline insertEvents ({ events, nextBatch, prevBatch, stateDelta } as data) (Timeline t) = Timeline - (if t.nextBatch == prevBatch then - { t - | events = t.events ++ events - , nextBatch = nextBatch - } - - else if nextBatch == t.prevBatch then - case t.previous of - Gap (Timeline prevT) -> - if prevT.nextBatch == prevBatch then - { events = prevT.events ++ events ++ t.events - , nextBatch = t.nextBatch - , prevBatch = prevT.prevBatch - , stateAtStart = prevT.stateAtStart - , previous = prevT.previous - } - - else + (case prevBatch of + -- No prevbatch suggests the start of the timeline. + -- This means that we must recurse until we've hit the bottom, + -- and then mark the bottom of the timeline. + Nothing -> + case t.previous of + Gap prevT -> { t - | events = events ++ t.events - , prevBatch = prevBatch - , stateAtStart = - stateDelta - |> Maybe.withDefault StateManager.empty + | previous = + prevT + |> insertEvents data + |> Gap } - _ -> - { t - | events = events ++ t.events - , prevBatch = prevBatch - , stateAtStart = - stateDelta - |> Maybe.withDefault StateManager.empty - } + _ -> + if nextBatch == t.prevBatch then + { t | previous = StartOfTimeline, events = events ++ t.events, stateAtStart = StateManager.empty } - else - case t.previous of - Gap prevT -> - { t - | previous = - prevT - |> insertEvents data - |> Gap - } + else + { t | previous = Gap <| newFromEvents data } - _ -> - t + -- If there is a prevbatch, it is not the start of the timeline + -- and could be located anywhere. + -- Starting at the front, look for a way to match it with the existing timeline. + Just p -> + -- Piece connects to the front of the timeline. + if t.nextBatch == p then + { t + | events = t.events ++ events + , nextBatch = nextBatch + } + -- Piece connects to the back of the timeline. + + else if nextBatch == t.prevBatch then + case t.previous of + Gap (Timeline prevT) -> + -- Piece also connects to the timeline in the back, + -- allowing the two timelines to merge. + if prevT.nextBatch == p then + { events = prevT.events ++ events ++ t.events + , nextBatch = t.nextBatch + , prevBatch = prevT.prevBatch + , stateAtStart = prevT.stateAtStart + , previous = prevT.previous + } + + else + { t + | events = events ++ t.events + , prevBatch = p + , stateAtStart = + stateDelta + |> Maybe.withDefault StateManager.empty + } + + Endless _ -> + { t + | events = events ++ t.events + , prevBatch = p + , stateAtStart = + stateDelta + |> Maybe.withDefault StateManager.empty + , previous = Endless p + } + + _ -> + { t + | events = events ++ t.events + , prevBatch = p + , stateAtStart = + stateDelta + |> Maybe.withDefault StateManager.empty + } + -- Piece doesn't connect to this piece of the timeline. + -- Consequently, look for previous parts of the timeline to see if it connects. + + else + case t.previous of + Gap prevT -> + { t + | previous = + prevT + |> insertEvents data + |> Gap + } + + _ -> + t ) +{-| Get the width of the latest gap. This data is usually accessed when trying to get more messages. +-} +latestGap : Timeline -> Maybe { from : Maybe String, to : String } +latestGap (Timeline t) = + case t.previous of + StartOfTimeline -> + Nothing + + Endless prevBatch -> + Just { from = Nothing, to = prevBatch } + + Gap (Timeline pt) -> + Just { from = Just pt.nextBatch, to = t.prevBatch } + + {-| Get the longest uninterrupted length of most recent events. -} localSize : Timeline -> Int diff --git a/src/Internal/Vault.elm b/src/Internal/Vault.elm index e910222..cb53c89 100644 --- a/src/Internal/Vault.elm +++ b/src/Internal/Vault.elm @@ -16,6 +16,7 @@ import Internal.Event as Event import Internal.Invite as Invite import Internal.Room as Room import Internal.Tools.Exceptions as X +import Internal.Tools.SpecEnums as Enums import Internal.Values.Room as IRoom import Internal.Values.RoomInvite exposing (IRoomInvite) import Internal.Values.StateManager as StateManager @@ -125,8 +126,62 @@ updateWith vaultUpdate ((Vault ({ cred, context } as data)) as vault) = vault -- TODO - GetMessages _ _ -> - vault + GetMessages input output -> + let + prevBatch : Maybe String + prevBatch = + case input.direction of + Enums.Chronological -> + Just output.start + + Enums.ReverseChronological -> + case output.end of + Just end -> + Just end + + Nothing -> + input.to + + nextBatch : Maybe String + nextBatch = + case input.direction of + Enums.Chronological -> + case output.end of + Just end -> + Just end + + Nothing -> + input.to + + Enums.ReverseChronological -> + Just output.start + in + case ( getRoomById input.roomId vault, nextBatch ) of + ( Just room, Just nb ) -> + room + |> Room.withoutCredentials + |> IRoom.insertEvents + { events = + output.chunk + |> List.map Event.initFromGetMessages + |> (\x -> + case input.direction of + Enums.Chronological -> + x + + Enums.ReverseChronological -> + List.reverse x + ) + , prevBatch = prevBatch + , nextBatch = nb + , stateDelta = Just <| StateManager.fromEventList (List.map Event.initFromGetMessages output.state) + } + |> Internal.insertRoom + |> (|>) cred + |> (\v -> Vault { cred = v, context = context }) + + _ -> + vault -- TODO InviteSent _ _ -> diff --git a/src/Matrix/Room.elm b/src/Matrix/Room.elm index da88437..fa1287f 100644 --- a/src/Matrix/Room.elm +++ b/src/Matrix/Room.elm @@ -1,5 +1,5 @@ module Matrix.Room exposing - ( Room, roomId, mostRecentEvents + ( Room, roomId, mostRecentEvents, findOlderEvents , sendMessage, sendMessages, sendOneEvent, sendMultipleEvents ) @@ -10,7 +10,7 @@ module Matrix.Room exposing A room represents a channel of communication within a Matrix home server. -@docs Room, roomId, mostRecentEvents +@docs Room, roomId, mostRecentEvents, findOlderEvents # Sending events @@ -35,6 +35,13 @@ type alias Room = Internal.Room +{-| If you want more events as part of the most recent events, you can run this task to get more. +-} +findOlderEvents : { limit : Maybe Int, room : Room } -> Task X.Error VaultUpdate +findOlderEvents { limit, room } = + Internal.getOlderEvents { limit = limit } room + + {-| Get the most recent events from this room. -} mostRecentEvents : Room -> List Event.Event