Add new Elm types

I have had a few helpful Elm ideas and lessons while at FOSDEM, which may help the project in positive ways. For example, the fact that the file `Leaking.elm` exists, suggests that the code has a few downsides that may shine through when a refactor is made. For this reason, some features will be reworked and reprogrammed.

Luckily, Elm's homepage claims that this language is easy to refactor. :)
pull/1/head
Bram van den Heuvel 2023-02-08 16:52:54 +01:00
parent 3bf84a2a57
commit 4797ba2f93
8 changed files with 481 additions and 25 deletions

View File

@ -31,6 +31,11 @@ originServerTs =
Time.millisToPosix 0 Time.millisToPosix 0
prevBatch : String
prevBatch =
"this_previous_batch_does_not_exist"
roomId : String roomId : String
roomId = roomId =
"!unknown-room:example.org" "!unknown-room:example.org"

View File

@ -0,0 +1,49 @@
module Internal.Tools.Fold exposing (..)
{-| This module allows users to iterate over lists in more intelligent ways.
-}
type FoldingState a
= Calculating a
| AnswerFound a
type FoldingResponse a
= ContinueWith a
| AnswerWith a
| AnswerWithPrevious
{-| Fold until a given condition is met.
The first argument is a function that returns a `Maybe b`. As soon as that value is `Nothing`, the function will ignore the rest of the list and return the most recent value.
-}
untilCompleted : ((a -> FoldingState b -> FoldingState b) -> FoldingState b -> List a -> FoldingState b) -> (a -> b -> FoldingResponse b) -> b -> List a -> b
untilCompleted folder updater startValue items =
folder
(\piece oldValue ->
case oldValue of
AnswerFound x ->
AnswerFound x
Calculating x ->
case updater piece x of
ContinueWith y ->
Calculating y
AnswerWith y ->
AnswerFound y
AnswerWithPrevious ->
AnswerFound x
)
(Calculating startValue)
items
|> (\resp ->
case resp of
Calculating x ->
x
AnswerFound x ->
x
)

View File

@ -0,0 +1,41 @@
module Internal.Tools.Hashdict exposing (..)
{-| This module abstracts the `Dict` type with one function that chooses the unique identifier for each type.
For example, this is used to store events by their event id, or store rooms by their room id.
-}
import Dict exposing (Dict)
type Hashdict a
= Hashdict
{ hash : a -> String
, values : Dict String a
}
empty : (a -> String) -> Hashdict a
empty hash =
Hashdict { hash = hash, values = Dict.empty }
get : String -> Hashdict a -> Maybe a
get k (Hashdict h) =
Dict.get k h.values
insert : a -> Hashdict a -> Hashdict a
insert v (Hashdict h) =
Hashdict { h | values = Dict.insert (h.hash v) v h.values }
keys : Hashdict a -> List String
keys (Hashdict h) =
Dict.keys h.values
values : Hashdict a -> List a
values (Hashdict h) =
Dict.values h.values

View File

@ -0,0 +1,45 @@
module Internal.Values.Credentials exposing (..)
import Dict exposing (Dict)
import Internal.Tools.Hashdict as Hashdict exposing (Hashdict)
import Internal.Values.Room as Room exposing (Room)
type Credentials
= Credentials { access : AccessToken, homeserver : String, rooms : Hashdict Room }
type AccessToken
= AccessToken String
| NoAccess
| UsernameAndPassword { username : String, password : String, accessToken : Maybe String }
defaultCredentials : String -> Credentials
defaultCredentials homeserver =
Credentials
{ access = NoAccess
, homeserver = homeserver
, rooms = Hashdict.empty Room.roomId
}
fromAccessToken : { accessToken : String, homeserver : String } -> Credentials
fromAccessToken { accessToken, homeserver } =
case defaultCredentials homeserver of
Credentials c ->
Credentials { c | access = AccessToken accessToken }
fromLoginCredentials : { username : String, password : String, homeserver : String } -> Credentials
fromLoginCredentials { username, password, homeserver } =
case defaultCredentials homeserver of
Credentials c ->
Credentials { c | access = UsernameAndPassword { username = username, password = password, accessToken = Nothing } }
getRoomById : String -> Credentials -> Maybe Room
getRoomById roomId (Credentials cred) =
Hashdict.get roomId cred.rooms
insertRoom : Room -> Credentials -> Credentials
insertRoom room (Credentials cred) =
Credentials
{ cred | rooms = Hashdict.insert room cred.rooms }

View File

@ -3,8 +3,9 @@ module Internal.Values.Event exposing (..)
import Internal.Tools.Timestamp exposing (Timestamp) import Internal.Tools.Timestamp exposing (Timestamp)
import Json.Encode as E import Json.Encode as E
type Event =
Event type Event
= Event
{ content : E.Value { content : E.Value
, eventId : String , eventId : String
, originServerTs : Timestamp , originServerTs : Timestamp
@ -12,48 +13,61 @@ type Event =
, sender : String , sender : String
, stateKey : Maybe String , stateKey : Maybe String
, contentType : String , contentType : String
, unsigned : Maybe { age : Maybe Int , unsigned :
Maybe
{ age : Maybe Int
, prevContent : Maybe E.Value , prevContent : Maybe E.Value
, redactedBecause : Maybe Event , redactedBecause : Maybe Event
, transactionId : Maybe String , transactionId : Maybe String
} }
} }
{- GETTER FUNCTIONS -} {- GETTER FUNCTIONS -}
content : Event -> E.Value content : Event -> E.Value
content (Event e) = content (Event e) =
e.content e.content
eventId : Event -> String eventId : Event -> String
eventId (Event e) = eventId (Event e) =
e.eventId e.eventId
originServerTs : Event -> Timestamp originServerTs : Event -> Timestamp
originServerTs (Event e) = originServerTs (Event e) =
e.originServerTs e.originServerTs
roomId : Event -> String roomId : Event -> String
roomId (Event e) = roomId (Event e) =
e.roomId e.roomId
sender : Event -> String sender : Event -> String
sender (Event e) = sender (Event e) =
e.sender e.sender
stateKey : Event -> Maybe String stateKey : Event -> Maybe String
stateKey (Event e) = stateKey (Event e) =
e.stateKey e.stateKey
contentType : Event -> String contentType : Event -> String
contentType (Event e) = contentType (Event e) =
e.contentType e.contentType
age : Event -> Maybe Int age : Event -> Maybe Int
age (Event e) = age (Event e) =
e.unsigned e.unsigned
|> Maybe.andThen .age |> Maybe.andThen .age
redactedBecause : Event -> Maybe Event redactedBecause : Event -> Maybe Event
redactedBecause (Event e) = redactedBecause (Event e) =
e.unsigned e.unsigned
@ -65,17 +79,22 @@ age (Event e) =
e.unsigned e.unsigned
|> Maybe.andThen .age |> Maybe.andThen .age
transactionId : Event -> Maybe String transactionId : Event -> Maybe String
transactionId (Event e) = transactionId (Event e) =
e.unsigned e.unsigned
|> Maybe.andThen .transactionId |> Maybe.andThen .transactionId
type BlindEvent = BlindEvent { contentType : String, content : E.Value }
type BlindEvent
= BlindEvent { contentType : String, content : E.Value }
blindContent : BlindEvent -> E.Value blindContent : BlindEvent -> E.Value
blindContent (BlindEvent be) = blindContent (BlindEvent be) =
be.content be.content
blindContentType : BlindEvent -> String blindContentType : BlindEvent -> String
blindContentType (BlindEvent be) = blindContentType (BlindEvent be) =
be.contentType be.contentType

View File

@ -0,0 +1,55 @@
module Internal.Values.Room exposing (..)
import Dict exposing (Dict)
import Internal.Tools.Fold as Fold
import Internal.Tools.SpecEnums exposing (SessionDescriptionType(..))
import Internal.Values.Event as Event exposing (BlindEvent, Event)
import Internal.Values.StateManager as StateManager exposing (StateManager)
import Internal.Values.Timeline as Timeline exposing (Timeline)
import Json.Encode as E
type Room
= Room
{ accountData : Dict String E.Value
, ephemeral : List BlindEvent
, events : Dict String Event
, roomId : String
, timeline : Timeline
}
{-| Add new events as the most recent events.
-}
addEvents :
{ events : List Event
, nextBatch : String
, prevBatch : String
, stateDelta : Maybe StateManager
}
-> Room
-> Room
addEvents ({ events } as data) (Room room) =
Room
{ room
| events =
events
|> List.map (\e -> ( Event.eventId e, e ))
|> Dict.fromList
|> (\x -> Dict.union x room.events)
, timeline = Timeline.addNewEvents data room.timeline
}
{-| Get an event by its id.
-}
getEventById : String -> Room -> Maybe Event
getEventById eventId (Room room) =
Dict.get eventId room.events
{-| Get the room's id.
-}
roomId : Room -> String
roomId (Room room) =
room.roomId

View File

@ -3,26 +3,42 @@ module Internal.Values.StateManager exposing (..)
import Dict exposing (Dict) import Dict exposing (Dict)
import Internal.Values.Event as Event exposing (Event) import Internal.Values.Event as Event exposing (Event)
type alias StateManager = Dict (String, String) Event
type alias StateManager =
Dict ( String, String ) Event
addEvent : Event -> StateManager -> StateManager
addEvent event oldManager =
case Event.stateKey event of
Just key ->
Dict.insert ( Event.contentType event, key ) event oldManager
Nothing ->
oldManager
getStateEvent : String -> String -> StateManager -> Maybe Event getStateEvent : String -> String -> StateManager -> Maybe Event
getStateEvent eventType stateKey = getStateEvent eventType stateKey =
Dict.get ( eventType, stateKey ) Dict.get ( eventType, stateKey )
updateRoomStateWith : StateManager -> StateManager -> StateManager updateRoomStateWith : StateManager -> StateManager -> StateManager
updateRoomStateWith = Dict.union updateRoomStateWith =
Dict.union
fromEvent : Event -> StateManager
fromEvent event =
Dict.empty
|> addEvent event
fromEventList : List Event -> StateManager fromEventList : List Event -> StateManager
fromEventList = fromEventList =
List.filterMap List.foldl addEvent Dict.empty
(\event ->
event
|> Event.stateKey
|> Maybe.map
(\key ->
( ( Event.contentType event, key ), event )
)
)
>> Dict.fromList
empty : StateManager
empty =
Dict.empty

View File

@ -0,0 +1,226 @@
module Internal.Values.Timeline exposing (..)
{-| This module shapes the Timeline type used to keep track of timelines in Matrix rooms.
-}
import Internal.Config.Leaking as Leaking
import Internal.Tools.Fold as Fold
import Internal.Values.Event as Event exposing (Event)
import Internal.Values.Room exposing (stateAtEvent)
import Internal.Values.StateManager as StateManager exposing (StateManager)
type Timeline
= Timeline
{ prevBatch : String
, nextBatch : String
, events : List Event
, stateAtStart : StateManager
, previous : BeforeTimeline
}
type BeforeTimeline
= Endless String
| Gap Timeline
| StartOfTimeline
{-| Add a new batch of events to the front of the timeline.
-}
addNewEvents :
{ events : List Event
, nextBatch : String
, prevBatch : String
, stateDelta : Maybe StateManager
}
-> Timeline
-> Timeline
addNewEvents { events, nextBatch, prevBatch, stateDelta } (Timeline t) =
Timeline
(if prevBatch == t.nextBatch then
{ t
| events = t.events ++ events
, nextBatch = nextBatch
}
else
{ prevBatch = prevBatch
, nextBatch = nextBatch
, events = events
, stateAtStart =
t
|> Timeline
|> mostRecentState
|> StateManager.updateRoomStateWith
(stateDelta
|> Maybe.withDefault StateManager.empty
)
, previous = Gap (Timeline t)
}
)
{-| Create a new timeline.
-}
newFromEvents :
{ events : List Event
, nextBatch : String
, prevBatch : Maybe String
, stateDelta : Maybe StateManager
}
-> Timeline
newFromEvents { events, nextBatch, prevBatch, stateDelta } =
Timeline
{ events = events
, nextBatch = nextBatch
, prevBatch =
prevBatch
|> Maybe.withDefault Leaking.prevBatch
, previous =
prevBatch
|> Maybe.map Endless
|> Maybe.withDefault StartOfTimeline
, stateAtStart =
stateDelta
|> Maybe.withDefault StateManager.empty
}
{-| Insert events starting from a known batch token.
-}
insertEvents :
{ events : List Event
, nextBatch : String
, prevBatch : 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
{ t
| events = events ++ t.events
, prevBatch = prevBatch
, stateAtStart =
stateDelta
|> Maybe.withDefault StateManager.empty
}
_ ->
{ t
| events = events ++ t.events
, prevBatch = prevBatch
, stateAtStart =
stateDelta
|> Maybe.withDefault StateManager.empty
}
else
case t.previous of
Gap prevT ->
{ t
| previous =
prevT
|> insertEvents data
|> Gap
}
_ ->
t
)
{-| Get the longest uninterrupted length of most recent events.
-}
localSize : Timeline -> Int
localSize =
mostRecentEvents >> List.length
{-| Get a list of the most recent events recorded.
-}
mostRecentEvents : Timeline -> List Event
mostRecentEvents (Timeline t) =
t.events
{-| Get the needed `since` parameter to get the latest events.
-}
nextSyncToken : Timeline -> String
nextSyncToken (Timeline t) =
t.nextBatch
{-| Get the state of the room after the most recent event.
-}
mostRecentState : Timeline -> StateManager
mostRecentState (Timeline t) =
t.stateAtStart
|> StateManager.updateRoomStateWith
(StateManager.fromEventList t.events)
{-| Get the timeline's room state at any given event. The function returns `Nothing` if the event is not found in the timeline.
-}
stateAtEvent : Event -> Timeline -> Maybe StateManager
stateAtEvent event (Timeline t) =
if
t.events
|> List.map Event.eventId
|> List.member (Event.eventId event)
then
Fold.untilCompleted
List.foldl
(\e ->
StateManager.addEvent e
>> (if Event.eventId e == Event.eventId event then
Fold.AnswerWith
else
Fold.ContinueWith
)
)
t.stateAtStart
t.events
|> Just
else
case t.previous of
Gap prevT ->
stateAtEvent event prevT
_ ->
Nothing
{-| Count how many events the current timeline is storing.
-}
size : Timeline -> Int
size (Timeline t) =
(case t.previous of
Gap prev ->
size prev
_ ->
0
)
+ List.length t.events