Compare commits

...

5 Commits

23 changed files with 1345 additions and 69 deletions

View File

@ -19,7 +19,8 @@
"elm/http": "2.0.0 <= v < 3.0.0",
"elm/json": "1.0.0 <= v < 2.0.0",
"elm/time": "1.0.0 <= v < 2.0.0",
"elm/url": "1.0.0 <= v < 2.0.0"
"elm/url": "1.0.0 <= v < 2.0.0",
"miniBill/elm-fast-dict": "1.0.0 <= v < 2.0.0"
},
"test-dependencies": {}
}

View File

@ -31,30 +31,52 @@ type as a message to the Vault to update certain information.
-}
import Http
import Internal.Api.Helpers as Helpers
import Internal.Tools.Context as Context exposing (Context)
import Internal.Tools.Exceptions as X
import Task exposing (Task)
{-| The TaskChain is a piece in the long chain of tasks that need to be completed.
The type defines four variables:
- `err` value that may arise on an error
- `u` the update msg that should be returned
- `a` phantom type before executing the chain's context
- `b` phantom type after executing the chain's context
-}
type alias TaskChain err u a b =
Context a -> Task (FailedChainPiece err u) (TaskChainPiece u a b)
{-| An IdemChain is a TaskChain that does not influence the chain's context
- `err` value that may arise on an error
- `u` the update msg that should be executed
- `a` phantom type before, during and after the chain's context
-}
type alias IdemChain err u a =
TaskChain err u a a
{-| A CompleteChain is a complete snake that can be safely run and executed by
the Elm core.
-}
type alias CompleteChain u =
TaskChain () u {} {}
{-| A TaskChainPiece is a piece that updates the chain's context.
Once a chain is executed, the process will add the `messages` value to its list
of updates, and it will update its context according to the `contextChange`
function.
-}
type alias TaskChainPiece u a b =
{ contextChange : Context a -> Context b
, messages : List u
}
{-| A FailedChainPiece initiates an early breakdown of a chain. Unless caught,
this halts execution of the chain. The process will add the `messages` value to
its list of updates, and it will return the given `err` value for a direct
explanation of what went wrong.
-}
type alias FailedChainPiece err u =
{ error : err, messages : List u }

View File

@ -37,46 +37,3 @@ ratelimited task =
_ ->
Task.fail e
)
{-| Sometimes, you don't really care if something went wrong - you just want to try again.
This task will only return an error if it went wrong on the n'th attempt.
-}
retryTask : Int -> Task X.Error a -> Task X.Error a
retryTask n task =
if n <= 0 then
task
else
Task.onError
(\err ->
let
retry : Task X.Error a
retry =
retryTask (n - 1) task
in
case err of
X.InternetException (Http.BadUrl _) ->
Task.fail err
X.InternetException _ ->
retry
X.SDKException (X.ServerReturnsBadJSON _) ->
retry
X.SDKException _ ->
Task.fail err
X.ServerException _ ->
Task.fail err
X.ContextFailed _ ->
Task.fail err
X.UnsupportedSpecVersion ->
Task.fail err
)
task

View File

@ -20,9 +20,9 @@ Note that **under development** doesn't always mean that it _will be_ supported.
| **Spec version** | | Syncing | Redaction |
| ---------------- | - | ------- | --------- |
| v1.8 || | ✔️ |
| v1.7 || | ✔️ |
| v1.6 || | ✔️ |
| v1.8 || ✔️ | ✔️ |
| v1.7 || ✔️ | ✔️ |
| v1.6 || | ✔️ |
| v1.5 || ✔️ | ✔️ |
| v1.4 || ✔️ | ✔️ |
| v1.3 || ✔️ | ✔️ |
@ -64,8 +64,8 @@ Note that **under development** doesn't always mean that it _will be_ supported.
| **Spec version** | | Event | Joined members | Event at timestamp |
| ---------------- | - | ----- | -------------- | ------------------ |
| v1.8 || ✔️ | ✔️ | |
| v1.7 || ✔️ | ✔️ | |
| v1.8 || ✔️ | ✔️ | ⚠️ |
| v1.7 || ✔️ | ✔️ | ⚠️ |
| v1.6 || ✔️ | ✔️ | ⚠️ |
| v1.5 || ✔️ | ✔️ | ⛔ |
| v1.4 || ✔️ | ✔️ | ⛔ |

View File

@ -1,6 +1,7 @@
module Internal.Api.Snackbar exposing (..)
{-| The snackbar module helps wraps relevant credentials, access tokens, refresh tokens and more around internal types.
{-| The snackbar module helps wraps relevant credentials, access tokens,
refresh tokens and more around internal types.
Vault, Room and Event types don't need access to API tokens,
but a user may way to redact an event, leave a room or reject an invite.
@ -14,6 +15,7 @@ without needing to update every data type whenever any of the tokens change.
import Dict exposing (Dict)
import Internal.Api.Versions.V1.Versions as V
import Internal.Config.DefaultSettings as DS
import Internal.Tools.LoginValues as Login exposing (AccessToken(..))
import Task exposing (Task)
@ -27,9 +29,14 @@ type Snackbar a vu
, homeserver : String
, transactionOffset : Int
, vs : Maybe V.Versions
, settings : Settings
}
type alias Settings =
{ syncTimeout : Int }
accessToken : Snackbar a vu -> AccessToken
accessToken (Snackbar { access }) =
access
@ -92,6 +99,9 @@ init data =
, failedTasks = Dict.empty
, failedTasksOffset = 0
, homeserver = data.baseUrl
, settings =
{ syncTimeout = DS.syncTimeout
}
, transactionOffset = 0
, vs = Nothing
}
@ -105,6 +115,7 @@ map f (Snackbar data) =
, failedTasks = data.failedTasks
, failedTasksOffset = 0
, homeserver = data.homeserver
, settings = data.settings
, transactionOffset = data.transactionOffset
, vs = data.vs
}
@ -135,6 +146,11 @@ setTransactionOffset i (Snackbar data) =
Snackbar { data | transactionOffset = max (data.transactionOffset + 1) (i + 1) }
updateSettings : (Settings -> Settings) -> Snackbar a vu -> Snackbar a vu
updateSettings f (Snackbar ({ settings } as data)) =
Snackbar { data | settings = f settings }
userId : Snackbar a vu -> Maybe String
userId (Snackbar { access }) =
Login.getUserId access

View File

@ -33,3 +33,10 @@ supportedVersions =
defaultDeviceName : String
defaultDeviceName =
"Elm Matrix SDK (v" ++ currentVersion ++ ")"
{-| The amount of seconds that the Matrix Vault should wait for a response from the Matrix homeserver.
-}
syncTimeout : Int
syncTimeout =
10

View File

@ -16,7 +16,7 @@ import Internal.Values.Room as Internal
import Internal.Values.StateManager as StateManager
import Internal.Values.Timeline as Timeline
import Json.Encode as E
import Task exposing (Task)
import Task
{-| The `Room` type represents a Matrix Room. It contains context information

View File

@ -0,0 +1,56 @@
module Internal.Tools.DefaultDict exposing (..)
import FastDict as Dict exposing (Dict)
{-| A dictionary of keys and values that includes a default when a key doesn't exist.
-}
type DefaultDict k v
= DefaultDict
{ content : Dict k v
, default : v
}
{-| Create an empty dictionary that has a default value.
-}
empty : v -> DefaultDict k v
empty v =
DefaultDict
{ content = Dict.empty
, default = v
}
{-| Get the value associated with the key. Uses the default if not found. -}
get : comparable -> DefaultDict comparable v -> v
get k (DefaultDict data) =
Dict.get k data.content |> Maybe.withDefault data.default
{-| Insert a key-value pair into a dictionary with a default.
-}
insert : comparable -> v -> DefaultDict comparable v -> DefaultDict comparable v
insert k v (DefaultDict data) =
DefaultDict { data | content = Dict.insert k v data.content }
{-| "Remove" a value by making its value synchronize with the default value.
-}
remove : comparable -> DefaultDict comparable v -> DefaultDict comparable v
remove k (DefaultDict data) =
DefaultDict { data | content = Dict.remove k data.content }
{-| Update the default value of all unset keys.
-}
setDefault : v -> DefaultDict k v -> DefaultDict k v
setDefault v (DefaultDict data) =
DefaultDict { data | default = v }
{-| Update the value of a dictionary. The returned (or received) value is `Nothing`,
it means the key synchronizes with the default value.
-}
update : comparable -> (Maybe v -> Maybe v) -> DefaultDict comparable v -> DefaultDict comparable v
update k fv (DefaultDict data) =
DefaultDict { data | content = Dict.update k fv data.content }
{-| Update the default value.
-}
updateDefault : (v -> v) -> DefaultDict k v -> DefaultDict k v
updateDefault f (DefaultDict data) =
DefaultDict { data | default = f data.default }

View File

@ -0,0 +1,384 @@
module Internal.Tools.Filters.Filter exposing (..)
import Internal.Tools.Filters.SpecObjects as SO
import Internal.Tools.Filters.SimpleFilter as SF exposing (SimpleFilter)
import Internal.Tools.SpecEnums as Enums
{-| Event filters tell the API what events to look for,
but specifically for events that are unrelated to any room.
-}
type EventFilter
= EventFilter
{ limit : Maybe Int
, senders : SimpleFilter String
, types : SimpleFilter String
}
{-| The final type dictates how everything else behaves.
-}
type Filter =
Filter
{ accountData : EventFilter
, presence : EventFilter
, room : RoomFilter
}
{-| RoomFilter types tell the API what is considered relevant in a room,
and which rooms to include.
-}
type RoomFilter
= RoomFilter
{ accountData : RoomEventFilter
, ephemeral : RoomEventFilter
, rooms : SimpleFilter String
, timeline : RoomEventFilter
}
{-| RoomEventFilter types tell the API what events to look for,
and what ones to ignore.
-}
type RoomEventFilter
= RoomEventFilter
{ lazyLoadMembers : Bool
, limit : Maybe Int
, rooms : SimpleFilter String
, senders : SimpleFilter String
, types : SimpleFilter String
}
allEvents : EventFilter
allEvents =
EventFilter
{ limit = Nothing
, senders = SF.all
, types = SF.all
}
allFilters : Filter
allFilters =
Filter
{ accountData = allEvents
, presence = allEvents
, room = allRooms
}
allRooms : RoomFilter
allRooms =
RoomFilter
{ accountData = allRoomEvents
, ephemeral = allRoomEvents
, rooms = SF.all
, timeline = allRoomEvents
}
allRoomEvents : RoomEventFilter
allRoomEvents =
RoomEventFilter
{ lazyLoadMembers = False
, limit = Nothing
, rooms = SF.all
, senders = SF.all
, types = SF.all
}
decodeEventFilter : SO.EventFilter -> EventFilter
decodeEventFilter data =
EventFilter
{ limit = data.limit
, senders = SF.toSimpleFilter data.senders data.notSenders
, types = SF.toSimpleFilter data.types data.notTypes
}
decodeFilter : SO.Filter -> Filter
decodeFilter data =
Filter
{ accountData =
data.accountData
|> Maybe.map decodeEventFilter
|> Maybe.withDefault allEvents
, presence =
data.presence
|> Maybe.map decodeEventFilter
|> Maybe.withDefault allEvents
, room =
data.room
|> Maybe.map decodeRoomFilter
|> Maybe.withDefault allRooms
}
{-| Decode a RoomFilter from a spec-compliant format.
-}
decodeRoomFilter : SO.RoomFilter -> RoomFilter
decodeRoomFilter data =
let
decodeREF : Maybe SO.RoomEventFilter -> RoomEventFilter
decodeREF =
Maybe.map decodeRoomEventFilter >> Maybe.withDefault allRoomEvents
in
RoomFilter
{ accountData = decodeREF data.accountData
, ephemeral = decodeREF data.ephemeral
, rooms = SF.toSimpleFilter data.rooms data.notRooms
, timeline = decodeREF data.timeline
}
{-| Decode a RoomEventFilter from a spec-compliant format.
-}
decodeRoomEventFilter : SO.RoomEventFilter -> RoomEventFilter
decodeRoomEventFilter data =
RoomEventFilter
{ lazyLoadMembers = data.lazyLoadMembers
, limit = data.limit
, rooms = SF.toSimpleFilter data.rooms data.notRooms
, senders = SF.toSimpleFilter data.senders data.notSenders
, types = SF.toSimpleFilter data.types data.notTypes
}
{-| Encode an EventFilter into a spec-compliant format.
-}
encodeEventFilter : EventFilter -> SO.EventFilter
encodeEventFilter (EventFilter data) =
{ limit = data.limit
, notSenders = SF.toExclude data.senders
, notTypes = SF.toExclude data.types
, senders = SF.toInclude data.senders
, types = SF.toInclude data.types
}
{-| Encode a Filter into a spec-compliant format.
-}
encodeFilter : Filter -> SO.Filter
encodeFilter (Filter data) =
{ accountData = Just <| encodeEventFilter data.accountData
, eventFields = Nothing
, eventFormat = Enums.Client
, presence = Just <| encodeEventFilter data.presence
, room = Just <| encodeRoomFilter data.room
}
{-| Encode a RoomFilter into a spec-compliant format.
-}
encodeRoomFilter : RoomFilter -> SO.RoomFilter
encodeRoomFilter (RoomFilter data) =
{ accountData = Just <| encodeRoomEventFilter data.accountData
, ephemeral = Just <| encodeRoomEventFilter data.ephemeral
, includeLeave = False
, notRooms = SF.toExclude data.rooms
, rooms = SF.toInclude data.rooms
, state = Just <| encodeRoomEventFilter data.timeline
, timeline = Just <| encodeRoomEventFilter data.timeline
}
{-| Encode a RoomEventFilter into a spec-compliant format.
-}
encodeRoomEventFilter : RoomEventFilter -> SO.RoomEventFilter
encodeRoomEventFilter (RoomEventFilter data) =
{ containsUrl = Nothing
, includeRedundantMembers = False
, lazyLoadMembers = data.lazyLoadMembers
, limit = data.limit
, notRooms = SF.toExclude data.rooms
, notSenders = SF.toExclude data.senders
, notTypes = SF.toExclude data.types
, rooms = SF.toInclude data.rooms
, senders = SF.toInclude data.senders
, types = SF.toInclude data.types
, unreadThreadNotifications = True
}
{-| Flatten a filter.
-}
flattenFilter : Filter -> List (SimpleFilter String)
flattenFilter (Filter f) =
List.concat
[ flattenEventFilter f.accountData
, flattenEventFilter f.presence
, flattenRoomFilter f.room
]
{-| Flatten a EventFilter.
-}
flattenEventFilter : EventFilter -> List (SimpleFilter String)
flattenEventFilter (EventFilter f) = [ f.senders, f.types ]
{-| Flatten a RoomFilter.
-}
flattenRoomFilter : RoomFilter -> List (SimpleFilter String)
flattenRoomFilter (RoomFilter f) =
[ f.accountData, f.ephemeral, f.timeline ]
|> List.map flattenRoomEventFilter
|> List.concat
|> (::) f.rooms
{-| Flatten a RoomEventFilter.
-}
flattenRoomEventFilter : RoomEventFilter -> List (SimpleFilter String)
flattenRoomEventFilter (RoomEventFilter f) = [ f.rooms, f.senders, f.types ]
{-| Get an intersection of a Filter.
-}
intersectFilter : Filter -> Filter -> Filter
intersectFilter (Filter f1) (Filter f2) =
Filter
{ accountData = intersectEventFilter f1.accountData f2.accountData
, presence = intersectEventFilter f1.presence f2.presence
, room = intersectRoomFilter f1.room f2.room
}
{-| Get an intersection of a EventFilter.
-}
intersectEventFilter : EventFilter -> EventFilter -> EventFilter
intersectEventFilter (EventFilter f1) (EventFilter f2) =
EventFilter
{ limit =
case (f1.limit, f2.limit) of
(Just l1, Just l2) ->
Just (max l1 l2)
(Just _, Nothing) ->
f1.limit
(Nothing, Just _) ->
f2.limit
(Nothing, Nothing) ->
Nothing
, senders = SF.intersect f1.senders f2.senders
, types = SF.intersect f1.types f2.types
}
{-| Get an intersection of a RoomFilter.
-}
intersectRoomFilter : RoomFilter -> RoomFilter -> RoomFilter
intersectRoomFilter (RoomFilter f1) (RoomFilter f2) =
RoomFilter
{ accountData = intersectRoomEventFilter f1.accountData f2.accountData
, ephemeral = intersectRoomEventFilter f1.ephemeral f2.ephemeral
, rooms = SF.intersect f1.rooms f2.rooms
, timeline = intersectRoomEventFilter f1.timeline f2.timeline
}
{-| Get an intersection of a RoomEventFilter.
-}
intersectRoomEventFilter : RoomEventFilter -> RoomEventFilter -> RoomEventFilter
intersectRoomEventFilter (RoomEventFilter f1) (RoomEventFilter f2) =
RoomEventFilter
{ lazyLoadMembers = f1.lazyLoadMembers && f2.lazyLoadMembers
, limit =
case (f1.limit, f2.limit) of
(Just l1, Just l2) ->
Just (max l1 l2)
(Just _, Nothing) ->
f1.limit
(Nothing, Just _) ->
f2.limit
(Nothing, Nothing) ->
Nothing
, rooms = SF.intersect f1.rooms f2.rooms
, senders = SF.intersect f1.senders f2.senders
, types = SF.intersect f1.types f2.types
}
{-| Check whether a filter is a subset of another filter.
-}
isSubSet : Filter -> Filter -> Bool
isSubSet f1 f2 =
let
isSame : List (SimpleFilter String) -> List (SimpleFilter String) -> Bool
isSame l1 l2 =
case (l1, l2) of
(h1 :: t1, h2 :: t2) ->
SF.subset h1 h2 && isSame t1 t2
([], []) ->
True
_ ->
False
in
isSame (flattenFilter f1) (flattenFilter f2)
lazyLoadMembers : Bool -> RoomEventFilter -> RoomEventFilter
lazyLoadMembers b (RoomEventFilter data) =
RoomEventFilter { data | lazyLoadMembers = b }
{-| Determine a limit for the amount of events. If no limit is given, the homeserver decides this limit for itself.
-}
setEventLimit : Maybe Int -> RoomEventFilter -> RoomEventFilter
setEventLimit i (RoomEventFilter data) =
RoomEventFilter { data | limit = i }
{-| Include a specific event type.
-}
withEventType : String -> RoomEventFilter -> RoomEventFilter
withEventType x (RoomEventFilter ({ types } as data)) =
RoomEventFilter { data | types = SF.with x types }
{-| Include all event types that haven't been explicitly mentioned.
-}
withOtherEventTypes : RoomEventFilter -> RoomEventFilter
withOtherEventTypes (RoomEventFilter ({ types } as data)) =
RoomEventFilter { data | types = SF.withOthers types }
{-| Include all rooms that haven't been explicitly mentioned.
-}
withOtherRooms : RoomEventFilter -> RoomEventFilter
withOtherRooms (RoomEventFilter ({ rooms } as data)) =
RoomEventFilter { data | rooms = SF.withOthers rooms }
{-| Include all senders that haven't been explicitly mentioned.
-}
withOtherSenders : RoomEventFilter -> RoomEventFilter
withOtherSenders (RoomEventFilter ({ senders } as data)) =
RoomEventFilter { data | senders = SF.withOthers senders }
{-| Include a specific room.
-}
withRoom : String -> RoomEventFilter -> RoomEventFilter
withRoom x (RoomEventFilter ({ rooms } as data)) =
RoomEventFilter { data | rooms = SF.with x rooms }
{-| Include a specific sender.
-}
withSender : String -> RoomEventFilter -> RoomEventFilter
withSender x (RoomEventFilter ({ senders } as data)) =
RoomEventFilter { data | senders = SF.with x senders }
{-| Ignore a specific event type.
-}
withoutEventType : String -> RoomEventFilter -> RoomEventFilter
withoutEventType x (RoomEventFilter ({ types } as data)) =
RoomEventFilter { data | types = SF.without x types }
{-| Ignore all rooms that haven't been explicitly mentioned.
-}
withoutOtherEventTypes : RoomEventFilter -> RoomEventFilter
withoutOtherEventTypes (RoomEventFilter ({ types } as data)) =
RoomEventFilter { data | types = SF.withoutOthers types }
{-| Ignore all rooms that haven't been explicitly mentioned.
-}
withoutOtherRooms : RoomEventFilter -> RoomEventFilter
withoutOtherRooms (RoomEventFilter ({ rooms } as data)) =
RoomEventFilter { data | rooms = SF.withoutOthers rooms }
{-| Ignore all senders that haven't been explicitly mentioned.
-}
withoutOtherSenders : RoomEventFilter -> RoomEventFilter
withoutOtherSenders (RoomEventFilter ({ senders } as data)) =
RoomEventFilter { data | senders = SF.withoutOthers senders }
{-| Ignore a specific room.
-}
withoutRoom : String -> RoomEventFilter -> RoomEventFilter
withoutRoom x (RoomEventFilter ({ rooms } as data)) =
RoomEventFilter { data | rooms = SF.without x rooms }
{-| Ignore a specific sender.
-}
withoutSender : String -> RoomEventFilter -> RoomEventFilter
withoutSender x (RoomEventFilter ({ senders } as data)) =
RoomEventFilter { data | senders = SF.without x senders }

View File

@ -0,0 +1,85 @@
module Internal.Tools.Filters.Main exposing (..)
{-| This module contains the main functions used to get, manipulate and change
filters according to their needs.
-}
import Internal.Tools.Filters.Filter as F
import Internal.Tools.Filters.SimpleFilter as SF
type alias Filter = F.Filter
type alias SimpleFilter = SF.SimpleFilter String
{-| Filter that adds all occurrences by default, but leaves a few ones out.
When provided with an empty list, the filter allows all types.
-}
allExcept : List String -> SimpleFilter
allExcept =
List.foldl SF.without SF.all
{-| Filter that removes everything by default, but leaves a few ones in.
When provided with an empty list, the filter allows nothing.
-}
only : List String -> SimpleFilter
only =
List.foldl SF.with SF.none
fromSimpleFilter :
{ accountDataTypes : SimpleFilter
, presence : { limit : Maybe Int, senders : SimpleFilter, types : SimpleFilter }
, ephemeral : { limit : Maybe Int, senders : SimpleFilter, types : SimpleFilter }
, roomIds : SimpleFilter
, lazyLoadMembers : Bool
, roomEvents : { limit : Maybe Int, senders : SimpleFilter, types : SimpleFilter }
} -> Filter
fromSimpleFilter data =
F.Filter
{ accountData =
F.EventFilter
{ limit = Nothing
, senders = SF.all
, types = data.accountDataTypes
}
, presence =
F.EventFilter
{ limit = data.presence.limit
, senders = data.presence.senders
, types = data.presence.types
}
, room =
F.RoomFilter
{ accountData =
F.RoomEventFilter
{ lazyLoadMembers = data.lazyLoadMembers
, limit = Nothing
, rooms = data.roomIds
, senders = SF.all
, types = data.accountDataTypes
}
, ephemeral =
F.RoomEventFilter
{ lazyLoadMembers = data.lazyLoadMembers
, limit = data.ephemeral.limit
, rooms = data.roomIds
, senders = data.ephemeral.senders
, types = data.ephemeral.types
}
, rooms = data.roomIds
, timeline =
F.RoomEventFilter
{ lazyLoadMembers = data.lazyLoadMembers
, limit = data.roomEvents.limit
, rooms = data.roomIds
, senders = data.roomEvents.senders
, types = data.roomEvents.types
}
}
}
{-| Get the intersection of two filters.
-}
intersect : Filter -> Filter -> Filter
intersect =
F.intersectFilter

View File

@ -0,0 +1,172 @@
module Internal.Tools.Filters.SimpleFilter exposing (..)
{-| The SimpleFilter tracks values that should or should not be monitored.
-}
import Dict exposing (Dict)
{-| SimpleFilter type that tracks items to include or exclude.
-}
type alias SimpleFilter a =
{ specificOnes : Dict a Bool
, includeOthers : Bool
}
all : SimpleFilter a
all =
{ specificOnes = Dict.empty
, includeOthers = True
}
{-| Use filter ones that are only available in the first filter.
-}
diff : SimpleFilter comparable -> SimpleFilter comparable -> SimpleFilter comparable
diff f1 f2 =
{ specificOnes =
Dict.merge
(\k v1 -> Dict.insert k (v1 && not f2.includeOthers))
(\k v1 v2 -> Dict.insert k (v1 && not v2))
(\k v2 -> Dict.insert k (f1.includeOthers && not v2))
f1.specificOnes
f2.specificOnes
Dict.empty
, includeOthers = f1.includeOthers && not f2.includeOthers
}
{-| Form a filter that only shows the values that two filters have in common.
-}
intersect : SimpleFilter comparable -> SimpleFilter comparable -> SimpleFilter comparable
intersect f1 f2 =
{ specificOnes =
Dict.merge
(\key v1 -> Dict.insert key (v1 && f2.includeOthers))
(\key v1 v2 -> Dict.insert key (v1 && v2))
(\key v2 -> Dict.insert key (f1.includeOthers && v2))
f1.specificOnes
f2.specificOnes
Dict.empty
, includeOthers = f1.includeOthers && f2.includeOthers
}
{-| Start with a filter that includes none.
-}
none : SimpleFilter a
none =
{ specificOnes = Dict.empty
, includeOthers = False
}
{-| Check whether a SimpleFilter is a subset of another filter.
-}
subset : SimpleFilter comparable -> SimpleFilter comparable -> Bool
subset small large =
if small.includeOthers && not large.includeOthers then
False
else
-- All elements of small are in large
Dict.merge
(\_ s ->
if s && not large.includeOthers then
always False
else
identity
)
(\_ s l ->
if s && not l then
always False
else
identity
)
(\_ l ->
if small.includeOthers && not l then
always False
else
identity
)
small.specificOnes
large.specificOnes
True
{-| Encode a SimpleFilter into a list of items to exclude.
-}
toExclude : SimpleFilter comparable -> Maybe (List comparable)
toExclude f =
f.specificOnes
|> Dict.filter (always not)
|> Dict.keys
|> Just
{-| Encode a SimpleFilter into a list of items to include.
-}
toInclude : SimpleFilter comparable -> Maybe (List comparable)
toInclude f =
if f.includeOthers then
Nothing
else
f.specificOnes
|> Dict.filter (always identity)
|> Dict.keys
|> Just
{-| Create a SimpleFilter out of two optionally present lists.
-}
toSimpleFilter : Maybe (List comparable) -> Maybe (List comparable) -> SimpleFilter comparable
toSimpleFilter these notThese =
let
no : List comparable
no = Maybe.withDefault [] notThese
in
case these of
Just yes ->
{ specificOnes =
Dict.union
(Dict.fromList ( List.map (\x -> Tuple.pair x False) no ))
(Dict.fromList ( List.map (\x -> Tuple.pair x True) yes ))
, includeOthers = False
}
Nothing ->
{ specificOnes =
no
|> List.map (\x -> Tuple.pair x False)
|> Dict.fromList
, includeOthers = True
}
{-| Form a filter that includes values if it is included in either filters.
-}
union : SimpleFilter comparable -> SimpleFilter comparable -> SimpleFilter comparable
union f1 f2 =
{ specificOnes =
Dict.merge
(\key v1 -> Dict.insert key (v1 || f2.includeOthers))
(\key v1 v2 -> Dict.insert key (v1 || v2))
(\key v2 -> Dict.insert key (f1.includeOthers || v2))
f1.specificOnes
f2.specificOnes
Dict.empty
, includeOthers = f1.includeOthers && f2.includeOthers
}
{-| Add a value that should be included.
-}
with : comparable -> SimpleFilter comparable -> SimpleFilter comparable
with x f =
{ f | specificOnes = Dict.insert x True f.specificOnes }
{-| Include all values that haven't been mentioned.
-}
withOthers : SimpleFilter comparable -> SimpleFilter comparable
withOthers f =
{ f | includeOthers = True }
{-| Add a value that should be ignored.
-}
without : comparable -> SimpleFilter comparable -> SimpleFilter comparable
without x f =
{ f | specificOnes = Dict.insert x False f.specificOnes }
{-| Ignore all values that haven't been mentioned.
-}
withoutOthers : SimpleFilter comparable -> SimpleFilter comparable
withoutOthers f =
{ f | includeOthers = False }

View File

@ -0,0 +1,246 @@
module Internal.Tools.Filters.SpecObjects exposing
( EventFilter
, Filter
, RoomEventFilter
, RoomFilter
, StateFilter
, encodeEventFilter
, encodeFilter
, encodeRoomEventFilter
, encodeRoomFilter
, encodeStateFilter
, eventFilterDecoder
, filterDecoder
, roomEventFilterDecoder
, roomFilterDecoder
, stateFilterDecoder
)
{-| Automatically generated 'SpecObjects'
Last generated at Unix time 1681915222
-}
import Internal.Tools.DecodeExtra as D exposing (opField, opFieldWithDefault)
import Internal.Tools.EncodeExtra exposing (maybeObject)
import Internal.Tools.SpecEnums as Enums
import Json.Decode as D
import Json.Encode as E
{-| Filter that describes which events to include/exclude.
-}
type alias EventFilter =
{ limit : Maybe Int
, notSenders : Maybe (List String)
, notTypes : Maybe (List String)
, senders : Maybe (List String)
, types : Maybe (List String)
}
encodeEventFilter : EventFilter -> E.Value
encodeEventFilter data =
maybeObject
[ ( "limit", Maybe.map E.int data.limit )
, ( "not_senders", Maybe.map (E.list E.string) data.notSenders )
, ( "not_types", Maybe.map (E.list E.string) data.notTypes )
, ( "senders", Maybe.map (E.list E.string) data.senders )
, ( "types", Maybe.map (E.list E.string) data.types )
]
eventFilterDecoder : D.Decoder EventFilter
eventFilterDecoder =
D.map5
(\a b c d e ->
{ limit = a, notSenders = b, notTypes = c, senders = d, types = e }
)
(opField "limit" D.int)
(opField "not_senders" (D.list D.string))
(opField "not_types" (D.list D.string))
(opField "senders" (D.list D.string))
(opField "types" (D.list D.string))
{-| Main filter for filtering results
-}
type alias Filter =
{ accountData : Maybe EventFilter
, eventFields : Maybe (List String)
, eventFormat : Enums.EventFormat
, presence : Maybe EventFilter
, room : Maybe RoomFilter
}
encodeFilter : Filter -> E.Value
encodeFilter data =
maybeObject
[ ( "account_data", Maybe.map encodeEventFilter data.accountData )
, ( "event_fields", Maybe.map (E.list E.string) data.eventFields )
, ( "event_format", Just <| Enums.encodeEventFormat data.eventFormat )
, ( "presence", Maybe.map encodeEventFilter data.presence )
, ( "room", Maybe.map encodeRoomFilter data.room )
]
filterDecoder : D.Decoder Filter
filterDecoder =
D.map5
(\a b c d e ->
{ accountData = a, eventFields = b, eventFormat = c, presence = d, room = e }
)
(opField "account_data" eventFilterDecoder)
(opField "event_fields" (D.list D.string))
(opFieldWithDefault "event_format" Enums.Client Enums.eventFormatDecoder)
(opField "presence" eventFilterDecoder)
(opField "room" roomFilterDecoder)
{-| Filter that describes which events to include/exclude in a Matrix room.
-}
type alias RoomEventFilter =
{ containsUrl : Maybe Bool
, includeRedundantMembers : Bool
, lazyLoadMembers : Bool
, limit : Maybe Int
, notRooms : Maybe (List String)
, notSenders : Maybe (List String)
, notTypes : Maybe (List String)
, rooms : Maybe (List String)
, senders : Maybe (List String)
, types : Maybe (List String)
, unreadThreadNotifications : Bool
}
encodeRoomEventFilter : RoomEventFilter -> E.Value
encodeRoomEventFilter data =
maybeObject
[ ( "contains_url", Maybe.map E.bool data.containsUrl )
, ( "include_redundant_members", Just <| E.bool data.includeRedundantMembers )
, ( "lazy_load_members", Just <| E.bool data.lazyLoadMembers )
, ( "limit", Maybe.map E.int data.limit )
, ( "not_rooms", Maybe.map (E.list E.string) data.notRooms )
, ( "not_senders", Maybe.map (E.list E.string) data.notSenders )
, ( "not_types", Maybe.map (E.list E.string) data.notTypes )
, ( "rooms", Maybe.map (E.list E.string) data.rooms )
, ( "senders", Maybe.map (E.list E.string) data.senders )
, ( "types", Maybe.map (E.list E.string) data.types )
, ( "unread_thread_notifications", Just <| E.bool data.unreadThreadNotifications )
]
roomEventFilterDecoder : D.Decoder RoomEventFilter
roomEventFilterDecoder =
D.map11
(\a b c d e f g h i j k ->
{ containsUrl = a, includeRedundantMembers = b, lazyLoadMembers = c, limit = d, notRooms = e, notSenders = f, notTypes = g, rooms = h, senders = i, types = j, unreadThreadNotifications = k }
)
(opField "contains_url" D.bool)
(opFieldWithDefault "include_redundant_members" False D.bool)
(opFieldWithDefault "lazy_load_members" False D.bool)
(opField "limit" D.int)
(opField "not_rooms" (D.list D.string))
(opField "not_senders" (D.list D.string))
(opField "not_types" (D.list D.string))
(opField "rooms" (D.list D.string))
(opField "senders" (D.list D.string))
(opField "types" (D.list D.string))
(opFieldWithDefault "unread_thread_notifications" False D.bool)
{-| Filter that describes what should and shouldn't be included for rooms.
-}
type alias RoomFilter =
{ accountData : Maybe RoomEventFilter
, ephemeral : Maybe RoomEventFilter
, includeLeave : Bool
, notRooms : Maybe (List String)
, rooms : Maybe (List String)
, state : Maybe StateFilter
, timeline : Maybe RoomEventFilter
}
encodeRoomFilter : RoomFilter -> E.Value
encodeRoomFilter data =
maybeObject
[ ( "account_data", Maybe.map encodeRoomEventFilter data.accountData )
, ( "ephemeral", Maybe.map encodeRoomEventFilter data.ephemeral )
, ( "include_leave", Just <| E.bool data.includeLeave )
, ( "not_rooms", Maybe.map (E.list E.string) data.notRooms )
, ( "rooms", Maybe.map (E.list E.string) data.rooms )
, ( "state", Maybe.map encodeStateFilter data.state )
, ( "timeline", Maybe.map encodeRoomEventFilter data.timeline )
]
roomFilterDecoder : D.Decoder RoomFilter
roomFilterDecoder =
D.map7
(\a b c d e f g ->
{ accountData = a, ephemeral = b, includeLeave = c, notRooms = d, rooms = e, state = f, timeline = g }
)
(opField "account_data" roomEventFilterDecoder)
(opField "ephemeral" roomEventFilterDecoder)
(opFieldWithDefault "include_leave" False D.bool)
(opField "not_rooms" (D.list D.string))
(opField "rooms" (D.list D.string))
(opField "state" stateFilterDecoder)
(opField "timeline" roomEventFilterDecoder)
{-| Filter that describes which events to include/exclude in a Matrix room.
-}
type alias StateFilter =
{ containsUrl : Maybe Bool
, includeRedundantMembers : Bool
, lazyLoadMembers : Bool
, limit : Maybe Int
, notRooms : Maybe (List String)
, notSenders : Maybe (List String)
, notTypes : Maybe (List String)
, rooms : Maybe (List String)
, senders : Maybe (List String)
, types : Maybe (List String)
, unreadThreadNotifications : Bool
}
encodeStateFilter : StateFilter -> E.Value
encodeStateFilter data =
maybeObject
[ ( "contains_url", Maybe.map E.bool data.containsUrl )
, ( "include_redundant_members", Just <| E.bool data.includeRedundantMembers )
, ( "lazy_load_members", Just <| E.bool data.lazyLoadMembers )
, ( "limit", Maybe.map E.int data.limit )
, ( "not_rooms", Maybe.map (E.list E.string) data.notRooms )
, ( "not_senders", Maybe.map (E.list E.string) data.notSenders )
, ( "not_types", Maybe.map (E.list E.string) data.notTypes )
, ( "rooms", Maybe.map (E.list E.string) data.rooms )
, ( "senders", Maybe.map (E.list E.string) data.senders )
, ( "types", Maybe.map (E.list E.string) data.types )
, ( "unread_thread_notifications", Just <| E.bool data.unreadThreadNotifications )
]
stateFilterDecoder : D.Decoder StateFilter
stateFilterDecoder =
D.map11
(\a b c d e f g h i j k ->
{ containsUrl = a, includeRedundantMembers = b, lazyLoadMembers = c, limit = d, notRooms = e, notSenders = f, notTypes = g, rooms = h, senders = i, types = j, unreadThreadNotifications = k }
)
(opField "contains_url" D.bool)
(opFieldWithDefault "include_redundant_members" False D.bool)
(opFieldWithDefault "lazy_load_members" False D.bool)
(opField "limit" D.int)
(opField "not_rooms" (D.list D.string))
(opField "not_senders" (D.list D.string))
(opField "not_types" (D.list D.string))
(opField "rooms" (D.list D.string))
(opField "senders" (D.list D.string))
(opField "types" (D.list D.string))
(opFieldWithDefault "unread_thread_notifications" False D.bool)

View File

@ -0,0 +1,135 @@
version: v1
name: SpecObjects
objects:
Filter:
description: Main filter for filtering results
fields:
account_data:
type: EventFilter
required: false
event_fields:
type: '[string]'
required: false
event_format:
type: Enums.EventFormat
default: Enums.Client
presence:
type: EventFilter
required: false
room:
type: RoomFilter
required: false
EventFilter:
description: Filter that describes which events to include/exclude.
fields:
limit:
type: int
required: false
not_senders:
type: '[string]'
required: false
not_types:
type: '[string]'
required: false
senders:
type: '[string]'
required: false
types:
type: '[string]'
required: false
RoomFilter:
description: Filter that describes what should and shouldn't be included for rooms.
fields:
account_data:
type: RoomEventFilter
required: false
ephemeral:
type: RoomEventFilter
required: false
include_leave:
type: bool
default: 'False'
not_rooms:
type: '[string]'
required: false
rooms:
type: '[string]'
required: false
state:
type: StateFilter
required: false
timeline:
type: RoomEventFilter
required: false
RoomEventFilter:
description: Filter that describes which events to include/exclude in a Matrix room.
fields:
contains_url:
type: bool
required: false
include_redundant_members:
type: bool
default: 'False'
lazy_load_members:
type: bool
default: 'False'
limit:
type: int
required: false
not_rooms:
type: '[string]'
required: false
not_senders:
type: '[string]'
required: false
not_types:
type: '[string]'
required: false
rooms:
type: '[string]'
required: false
senders:
type: '[string]'
required: false
types:
type: '[string]'
required: false
unread_thread_notifications:
type: bool
default: 'False'
StateFilter:
description: Filter that describes which events to include/exclude in a Matrix room.
fields:
contains_url:
type: bool
required: false
include_redundant_members:
type: bool
default: 'False'
lazy_load_members:
type: bool
default: 'False'
limit:
type: int
required: false
not_rooms:
type: '[string]'
required: false
not_senders:
type: '[string]'
required: false
not_types:
type: '[string]'
required: false
rooms:
type: '[string]'
required: false
senders:
type: '[string]'
required: false
types:
type: '[string]'
required: false
unread_thread_notifications:
type: bool
default: 'False'

View File

@ -6,8 +6,10 @@ For example, this is used to store events by their event id, or store rooms by t
-}
import Dict exposing (Dict)
import FastDict as Dict exposing (Dict)
import Json.Decode as D
import Json.Encode as E
import Hash exposing (Hash)
type Hashdict a
= Hashdict
@ -20,6 +22,12 @@ empty : (a -> String) -> Hashdict a
empty hash =
Hashdict { hash = hash, values = Dict.empty }
encode : Hashdict E.Value -> E.Value
encode (Hashdict h) =
h.values
|> Dict.toList
|> E.object
fromList : (a -> String) -> List a -> Hashdict a
fromList hash xs =
@ -46,6 +54,9 @@ keys : Hashdict a -> List String
keys (Hashdict h) =
Dict.keys h.values
toList : Hashdict a -> List (String, a)
toList (Hashdict h) =
Dict.toList h.values
union : Hashdict a -> Hashdict a -> Hashdict a
union (Hashdict h1) (Hashdict h2) =

View File

@ -0,0 +1,40 @@
module Internal.Tools.Iddict exposing (..)
{-| The id-dict stores values and gives them a unique id.
-}
import FastDict as Dict exposing (Dict)
type Iddict a
= Iddict
{ cursor : Int
, dict : Dict Int a
}
empty : Iddict a
empty =
Iddict
{ cursor = 0
, dict = Dict.empty
}
get : Int -> Iddict a -> Maybe a
get k (Iddict { dict }) =
Dict.get k dict
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 }
)
keys : Iddict a -> List Int
keys (Iddict { dict }) =
Dict.keys dict
remove : Int -> Iddict a -> Iddict a
remove k (Iddict d) =
Iddict { d | dict = Dict.remove k d.dict }
values : Iddict a -> List a
values (Iddict { dict }) =
Dict.values dict

View File

@ -0,0 +1,14 @@
module Internal.Tools.VaultResult exposing (..)
import Internal.Tools.Exceptions as X
import Task exposing (Task)
type Info b a
= Info a
| NoInfo
| InfoFailed { status : LoadingError, retry : Task X.Error b }
type LoadingError
= NeverRequested

View File

@ -1,23 +1,113 @@
module Internal.Values.Timeline exposing (..)
{-| This module shapes the Timeline type used to keep track of timelines in Matrix rooms.
{-| The Timeline can be very complex, and it can be represented in surprisingly
complex manners. This module aims to provide one single Timeline type that
accepts the complex pieces of information from the API and contain it all in
a simple way to view events.
-}
import FastDict as Dict exposing (Dict)
import Internal.Config.Leaking as Leaking
import Internal.Tools.Fold as Fold
import Internal.Values.Event as Event exposing (IEvent)
import Internal.Values.StateManager as StateManager exposing (StateManager)
import Internal.Tools.DefaultDict as DefaultDict exposing (DefaultDict)
import Internal.Tools.Hashdict as Hashdict exposing (Hashdict)
import Internal.Tools.Iddict as Iddict exposing (Iddict)
import Internal.Tools.Filters.Main as Filter exposing (Filter)
import Internal.Config.Leaking exposing (nextBatch)
{-| The Timeline is a comprehensive object describing a timeline in a room.
Any Timeline type contains the following pieces of information:
- `events` Comprehensive dictionary containing all locally stored timeline events
- `batches` Comprehensive dictionary containing all batches. Batches are pieces
of the timeline that have been sent by the homeserver.
- `token` Dictionary that maps for each batch token which batches it borders
- `mostRecentSync` Id of the most "recent" batch in the timeline
-}
type Timeline
= Timeline
{ prevBatch : String
, nextBatch : String
, events : List IEvent
, stateAtStart : StateManager
, previous : BeforeTimeline
{ events : Hashdict IEvent
, batches : Iddict TimelineBatch
, token : DefaultDict String (List Int)
, mostRecentSync : Maybe Int
}
{-| A BatchToken is a token that has been handed out by the server to mark the end of a -}
type alias BatchToken = String
type alias TimelineBatch =
{ prevBatch : List Batch
, nextBatch : List Batch
, filter : Filter
, events : List String
, stateDelta : StateManager
}
type Batch
= Token BatchToken
| Batch Int
addNewSync :
{ events : List IEvent
, filter : Filter
, limited : Bool
, nextBatch : String
, prevBatch : String
, stateDelta : Maybe StateManager
} -> Timeline -> Timeline
addNewSync data (Timeline timeline) =
let
batchToInsert : TimelineBatch
batchToInsert =
{ prevBatch =
[ Just <| Token data.prevBatch
, Maybe.map Batch timeline.mostRecentSync
]
|> List.filterMap identity
, nextBatch =
[ Token data.nextBatch ]
, filter = data.filter
, events = List.map Event.eventId data.events
, stateDelta = Maybe.withDefault StateManager.empty data.stateDelta
}
in
case Iddict.insert batchToInsert timeline.batches of
( batchId, batches ) ->
Timeline
{ events = List.foldl Hashdict.insert timeline.events data.events
, batches = batches
, mostRecentSync = Just batchId
, token =
timeline.token
|> DefaultDict.update data.prevBatch
(\value ->
case value of
Just v ->
Just (batchId :: v)
Nothing ->
Just [ batchId ]
)
|> DefaultDict.update data.nextBatch
(\value ->
case value of
Just v ->
Just (batchId :: v)
Nothing ->
Just [ batchId ]
)
}
-- type Timeline
-- = Timeline
-- { prevBatch : String
-- , nextBatch : String
-- , events : List IEvent
-- , stateAtStart : StateManager
-- , previous : BeforeTimeline
-- }
type BeforeTimeline
= Endless String

View File

@ -432,3 +432,8 @@ sync vault onResponse =
rooms : Vault -> List Room.Room
rooms =
Snackbar.mapList Internal.getRooms
settings : (Snackbar.Settings -> Snackbar.Settings) -> Vault -> Vault
settings =
Snackbar.updateSettings

View File

@ -33,10 +33,8 @@ interact with the API.
import Internal.Api.VaultUpdate as Api
import Internal.Invite exposing (RoomInvite)
import Internal.Room exposing (Room)
import Internal.Tools.Exceptions as X
import Internal.Vault
import Json.Encode as E
import Task exposing (Task)
{-| The Matrix API requires you to keep track of a lot of tokens, keys, values and more.
@ -58,6 +56,13 @@ translate those instructions to a `VaultUpdate` that you can feed to your `Vault
type alias VaultUpdate =
Api.VaultUpdate
{-| After evaluating an update, the `Status` type tells you how the Vault.
-}
type Status
= Good
| Warning String ((VaultUpdate -> msg) -> Cmd msg)
| Break String ((VaultUpdate -> msg) -> Cmd msg)
{-| Create a new vault based on an access token.
Keep in mind that access tokens might eventually be revoked or expire,

View File

@ -68,9 +68,7 @@ allowed for every room admin.
import Internal.Api.VaultUpdate exposing (VaultUpdate)
import Internal.Event as Event
import Internal.Room as Internal
import Internal.Tools.Exceptions as X
import Json.Decode as D
import Task exposing (Task)
{-| A room represents a channel of communication within a Matrix home server.

View File

@ -32,10 +32,8 @@ Once you have the event you want, you can explore it with the following function
import Internal.Api.VaultUpdate exposing (VaultUpdate)
import Internal.Invite as Internal
import Internal.Tools.Exceptions as X
import Internal.Values.RoomInvite as IR
import Json.Encode as E
import Task exposing (Task)
{-| The `RoomInvite` type serves as an invite to a given room.

27
src/Matrix/Settings.elm Normal file
View File

@ -0,0 +1,27 @@
module Matrix.Settings exposing (..)
{-| There are a lot of settings that you can change!
These settings change how the Vault interacts with the Matrix API.
You can adjust these values for performance reasons, for customizability, benchmarking,
or maybe just because you like it. :)
It is common to set all settings in the `init` function, but you can adjust all settings on the fly.
-}
import Internal.Vault exposing (Vault)
{-| When your Matrix client synchronizes with the homeserver, the homeserver often
responds quite quickly, giving all the information that you need.
Sometimes, the homeserver has nothing new to report, and instead makes you wait for a response.
This is called long-polling, and it's the homeserver waiting for an update to give to you.
Long-polling is very useful!
This setting sets a limit on how long the long-polling should last. It is smart
to make this equal to the interval at which you run the `sync` function.
**Default:** 10 (seconds)
-}
syncTimeout : Int -> Vault -> Vault
syncTimeout timeout =
Internal.Vault.settings \data -> { data | syncTimeout = timeout }

7
src/refactors.md Normal file
View File

@ -0,0 +1,7 @@
# Design refactors to be made
The following refactors are to be made:
- We need an `Info` type that informs the user whether data exists, and if not, why it doesn't exist.
- We need a `Timeline` type that users can use to go through threads in the global room timeline.