Add PreApi for getting unavailable values

pull/1/head
Bram van den Heuvel 2023-03-01 11:21:22 +01:00
parent c844c94564
commit 68d93180c1
14 changed files with 607 additions and 146 deletions

View File

@ -1,12 +1,18 @@
module Internal.Api.All exposing (..)
import Hash
import Internal.Api.GetEvent.Main as GetEvent
import Internal.Api.JoinedMembers.Main as JoinedMembers
import Internal.Api.PreApi.Main as PreApi
import Internal.Api.PreApi.Objects.Versions as V
import Internal.Api.SendMessageEvent.Main as SendMessageEvent
import Internal.Api.SendStateKey.Main as SendStateKey
import Internal.Api.Sync.Main as Sync
import Internal.Api.Versions.Main as Versions
import Internal.Tools.Exceptions as X
import Internal.Tools.LoginValues exposing (AccessToken)
import Internal.Tools.SpecEnums as Enums
import Internal.Tools.ValueGetter as VG
import Json.Encode as E
import Task exposing (Task)
@ -14,43 +20,161 @@ type alias Future a =
Task X.Error a
type alias GetEventInput =
{ accessToken : AccessToken
, baseUrl : String
, eventId : String
, roomId : String
, versions : Maybe V.Versions
}
{-| Get a specific event from the Matrix API.
-}
getEvent : List String -> Maybe (GetEvent.EventInput -> Future GetEvent.EventOutput)
getEvent =
GetEvent.getEvent
getEvent : GetEventInput -> Future GetEvent.EventOutput
getEvent data =
VG.withInfo2
(\accessToken versions ->
GetEvent.getEvent
versions.versions
{ accessToken = accessToken
, baseUrl = data.baseUrl
, eventId = data.eventId
, roomId = data.roomId
}
)
(PreApi.accessToken data.baseUrl data.accessToken)
(PreApi.versions data.baseUrl data.versions)
type alias JoinedMembersInput =
{ accessToken : AccessToken
, baseUrl : String
, roomId : String
, versions : Maybe V.Versions
}
{-| Get a list of members who are part of a Matrix room.
-}
joinedMembers : List String -> Maybe (JoinedMembers.JoinedMembersInput -> Future JoinedMembers.JoinedMembersOutput)
joinedMembers =
JoinedMembers.joinedMembers
joinedMembers : JoinedMembersInput -> Future JoinedMembers.JoinedMembersOutput
joinedMembers data =
VG.withInfo2
(\accessToken versions ->
JoinedMembers.joinedMembers
versions.versions
{ accessToken = accessToken
, baseUrl = data.baseUrl
, roomId = data.roomId
}
)
(PreApi.accessToken data.baseUrl data.accessToken)
(PreApi.versions data.baseUrl data.versions)
type alias SendMessageEventInput =
{ accessToken : AccessToken
, baseUrl : String
, content : E.Value
, eventType : String
, roomId : String
, versions : Maybe V.Versions
, extraTransactionNoise : String
}
{-| Send a message event into a Matrix room.
-}
sendMessageEvent : List String -> Maybe (SendMessageEvent.SendMessageEventInput -> Future SendMessageEvent.SendMessageEventOutput)
sendMessageEvent =
SendMessageEvent.sendMessageEvent
sendMessageEvent : SendMessageEventInput -> Future SendMessageEvent.SendMessageEventOutput
sendMessageEvent data =
VG.withInfo3
(\accessToken versions transactionId ->
SendMessageEvent.sendMessageEvent
versions.versions
{ accessToken = accessToken
, baseUrl = data.baseUrl
, content = data.content
, eventType = data.eventType
, roomId = data.roomId
, transactionId = transactionId
}
)
(PreApi.accessToken data.baseUrl data.accessToken)
(PreApi.versions data.baseUrl data.versions)
(PreApi.transactionId
(\timestamp ->
[ Hash.fromInt timestamp
, Hash.fromString data.baseUrl
, Hash.fromString data.eventType
, Hash.fromString data.roomId
, Hash.fromString data.extraTransactionNoise
]
|> List.foldl Hash.dependent (Hash.fromInt 0)
|> Hash.toString
|> (++) "elm"
)
)
type alias SendStateKeyInput =
{ accessToken : AccessToken
, baseUrl : String
, content : E.Value
, eventType : String
, roomId : String
, stateKey : String
, versions : Maybe V.Versions
}
{-| Send a state event into a Matrix room.
-}
sendStateEvent : List String -> Maybe (SendStateKey.SendStateKeyInput -> Future SendStateKey.SendStateKeyOutput)
sendStateEvent =
SendStateKey.sendStateKey
sendStateEvent : SendStateKeyInput -> Future SendStateKey.SendStateKeyOutput
sendStateEvent data =
VG.withInfo2
(\accessToken versions ->
SendStateKey.sendStateKey
versions.versions
{ accessToken = accessToken
, baseUrl = data.baseUrl
, content = data.content
, eventType = data.eventType
, roomId = data.roomId
, stateKey = data.stateKey
}
)
(PreApi.accessToken data.baseUrl data.accessToken)
(PreApi.versions data.baseUrl data.versions)
type alias SyncInput =
{ accessToken : AccessToken
, baseUrl : String
, filter : Maybe String
, fullState : Maybe Bool
, setPresence : Maybe Enums.UserPresence
, since : Maybe String
, timeout : Maybe Int
, versions : Maybe V.Versions
}
{-| Get the latest sync from the Matrix API.
-}
syncCredentials : List String -> Maybe (Sync.SyncInput -> Future Sync.SyncOutput)
syncCredentials =
Sync.sync
{-| Get all supported versions on the Matrix homeserver.
-}
versions : Versions.VersionsInput -> Versions.VersionsOutput
versions =
Versions.getVersions
syncCredentials : SyncInput -> Future Sync.SyncOutput
syncCredentials data =
VG.withInfo2
(\accessToken versions ->
Sync.sync
versions.versions
{ accessToken = accessToken
, baseUrl = data.baseUrl
, filter = data.filter
, fullState = data.fullState
, setPresence = data.setPresence
, since = data.since
, timeout = data.timeout
}
)
(PreApi.accessToken data.baseUrl data.accessToken)
(PreApi.versions data.baseUrl data.versions)

View File

@ -1,104 +0,0 @@
module Internal.Api.CredUpdate exposing (getEvent, joinedMembers, sendMessage, sendState, sync)
{-| Sometimes, the `Credentials` type needs to refresh its tokens, log in again,
change some state or adjust its values to be able to keep talking to the server.
That's what the `CredUpdate` type is for. It is a list of changes that the
`Credentials` type needs to make.
-}
import Internal.Api.GetEvent.Main as GetEvent
import Internal.Api.Helpers as H
import Internal.Api.JoinedMembers.Main as JoinedMembers
import Internal.Api.SendMessageEvent.Main as SendMessageEvent
import Internal.Api.SendStateKey.Main as SendStateKey
import Internal.Api.Sync.Main as Sync
import Internal.Api.Versions.Main as Versions
import Internal.Tools.Exceptions as X
import Task exposing (Task)
type CredUpdate
= MultipleChanges (List CredUpdate)
| EventDetails GetEvent.EventOutput
| RoomMemberList JoinedMembers.JoinedMembersOutput
| MessageEventSent SendMessageEvent.SendMessageEventOutput
| StateEventSent SendStateKey.SendStateKeyOutput
| SyncReceived Sync.SyncOutput
| VersionReceived Versions.VersionsOutput
type alias Updater = Task X.Error CredUpdate
getEvent : Maybe (List String) -> GetEvent.EventInput -> Updater
getEvent versions =
maybeWithVersions
{ maybeVersions = versions
, f = GetEvent.getEvent
, toUpdate = EventDetails
}
>> H.retryTask 2
joinedMembers : Maybe (List String) -> JoinedMembers.JoinedMembersInput -> Updater
joinedMembers versions =
maybeWithVersions
{ maybeVersions = versions
, f = JoinedMembers.joinedMembers
, toUpdate = RoomMemberList
}
sendMessage : Maybe (List String) -> SendMessageEvent.SendMessageEventInput -> Updater
sendMessage versions =
maybeWithVersions
{ maybeVersions = versions
, f = SendMessageEvent.sendMessageEvent
, toUpdate = MessageEventSent
}
>> H.retryTask 5
sendState : Maybe (List String) -> SendStateKey.SendStateKeyInput -> Updater
sendState versions =
maybeWithVersions
{ maybeVersions = versions
, f = SendStateKey.sendStateKey
, toUpdate = StateEventSent
}
>> H.retryTask 5
sync : Maybe (List String) -> Sync.SyncInput -> Updater
sync versions =
maybeWithVersions
{ maybeVersions = versions
, f = Sync.sync
, toUpdate = SyncReceived
}
>> H.retryTask 1
maybeWithVersions :
{ maybeVersions : Maybe (List String)
, f : (List String -> Maybe ({ in | baseUrl : String } -> Task X.Error out))
, toUpdate : (out -> CredUpdate)
} ->
{ in | baseUrl : String } -> Updater
maybeWithVersions {maybeVersions, f, toUpdate} params =
case maybeVersions of
Just versions ->
case f versions of
Just task ->
task params
|> Task.map toUpdate
Nothing ->
Task.fail X.UnsupportedSpecVersion
Nothing ->
Versions.getVersions params.baseUrl
|> Task.andThen
(\versions ->
maybeWithVersions (Just versions.supportedVersions) f toUpdate params
|> Task.map
(\update ->
MultipleChanges
[ update
, VersionReceived versions
]
)
)

View File

@ -6,7 +6,7 @@ import Internal.Tools.VersionControl as VC
import Task exposing (Task)
getEvent : List String -> Maybe (EventInput -> Task X.Error EventOutput)
getEvent : List String -> EventInput -> Task X.Error EventOutput
getEvent versions =
VC.withBottomLayer
{ current = Api.getEventInputV1
@ -20,6 +20,7 @@ getEvent versions =
|> VC.sameForVersion "v1.4"
|> VC.sameForVersion "v1.5"
|> VC.mostRecentFromVersionList versions
|> Maybe.withDefault (always <| Task.fail X.UnsupportedSpecVersion)
type alias EventOutput =

View File

@ -6,7 +6,7 @@ import Internal.Tools.VersionControl as VC
import Task exposing (Task)
joinedMembers : List String -> Maybe (JoinedMembersInput -> Task X.Error JoinedMembersOutput)
joinedMembers : List String -> JoinedMembersInput -> Task X.Error JoinedMembersOutput
joinedMembers versions =
VC.withBottomLayer
{ current = Api.joinedMembersV1
@ -31,6 +31,7 @@ joinedMembers versions =
|> VC.sameForVersion "v1.4"
|> VC.sameForVersion "v1.5"
|> VC.mostRecentFromVersionList versions
|> Maybe.withDefault (always <| Task.fail X.UnsupportedSpecVersion)
type alias JoinedMembersInput =

View File

@ -0,0 +1,65 @@
module Internal.Api.PreApi.Main exposing (..)
{-| Certain values are required knowledge for (almost) every endpoint.
Some values aren't known right away, however.
This module takes care of values like access tokens, transaction ids and spec version lists
that the credentials type needs to know about before it can make a request.
-}
import Internal.Api.PreApi.Objects.Versions as V
import Internal.Api.Request as R
import Internal.Tools.Exceptions as X
import Internal.Tools.LoginValues exposing (AccessToken(..))
import Internal.Tools.ValueGetter exposing (ValueGetter)
import Task
import Time
accessToken : String -> AccessToken -> ValueGetter String
accessToken baseUrl t =
{ value =
case t of
NoAccess ->
Nothing
AccessToken token ->
Just token
UsernameAndPassword { token } ->
token
, getValue =
"Automated login yet needs to be implemented."
|> X.NotSupportedYet
|> X.SDKException
|> Task.fail
}
transactionId : (Int -> String) -> ValueGetter String
transactionId seeder =
{ value = Nothing
, getValue =
Time.now
|> Task.map Time.posixToMillis
|> Task.map seeder
}
versions : String -> Maybe V.Versions -> ValueGetter V.Versions
versions baseUrl mVersions =
{ value = mVersions
, getValue =
R.rawApiCall
{ headers = R.NoHeaders
, method = "GET"
, baseUrl = baseUrl
, path = "/_matrix/client/versions"
, pathParams = []
, queryParams = []
, bodyParams = []
, timeout = Nothing
, decoder = \_ -> V.versionsDecoder
}
}

View File

@ -0,0 +1,43 @@
module Internal.Api.PreApi.Objects.Versions exposing
( Versions
, encodeVersions
, versionsDecoder
)
{-| Automatically generated 'Versions'
Last generated at Unix time 1677064309
-}
import Dict exposing (Dict)
import Internal.Tools.DecodeExtra exposing (opField, opFieldWithDefault)
import Internal.Tools.EncodeExtra exposing (maybeObject)
import Json.Decode as D
import Json.Encode as E
{-| Information on what the homeserver supports.
-}
type alias Versions =
{ unstableFeatures : Dict String Bool
, versions : List String
}
encodeVersions : Versions -> E.Value
encodeVersions data =
maybeObject
[ ( "unstable_features", Just <| E.dict identity E.bool data.unstableFeatures )
, ( "versions", Just <| E.list E.string data.versions )
]
versionsDecoder : D.Decoder Versions
versionsDecoder =
D.map2
(\a b ->
{ unstableFeatures = a, versions = b }
)
(opFieldWithDefault "unstable_features" Dict.empty (D.dict D.bool))
(D.field "versions" (D.list D.string))

View File

@ -0,0 +1,12 @@
version: V_all
name: Versions
objects:
Versions:
description: Information on what the homeserver supports.
fields:
unstable_features:
type: "{bool}"
default: Dict.empty
versions:
type: "[string]"
required: true

View File

@ -6,7 +6,7 @@ import Internal.Tools.VersionControl as VC
import Task exposing (Task)
sendMessageEvent : List String -> Maybe (SendMessageEventInput -> Task X.Error SendMessageEventOutput)
sendMessageEvent : List String -> SendMessageEventInput -> Task X.Error SendMessageEventOutput
sendMessageEvent versions =
VC.withBottomLayer
{ current = Api.sendMessageEventV1
@ -31,6 +31,7 @@ sendMessageEvent versions =
|> VC.sameForVersion "v1.4"
|> VC.sameForVersion "v1.5"
|> VC.mostRecentFromVersionList versions
|> Maybe.withDefault (always <| Task.fail X.UnsupportedSpecVersion)
type alias SendMessageEventInput =

View File

@ -6,7 +6,7 @@ import Internal.Tools.VersionControl as VC
import Task exposing (Task)
sendStateKey : List String -> Maybe (SendStateKeyInput -> Task X.Error SendStateKeyOutput)
sendStateKey : List String -> SendStateKeyInput -> Task X.Error SendStateKeyOutput
sendStateKey versions =
VC.withBottomLayer
{ current = Api.sendStateKeyV1
@ -31,6 +31,7 @@ sendStateKey versions =
|> VC.sameForVersion "v1.4"
|> VC.sameForVersion "v1.5"
|> VC.mostRecentFromVersionList versions
|> Maybe.withDefault (always <| Task.fail X.UnsupportedSpecVersion)
type alias SendStateKeyInput =

View File

@ -45,7 +45,7 @@ syncV1 data =
, bodyParams = []
, timeout =
data.timeout
|> Maybe.map ((+) 10000)
|> Maybe.map ((*) 1000)
|> Maybe.map toFloat
, decoder = \_ -> SO1.syncDecoder
}
@ -69,7 +69,7 @@ syncV2 data =
, bodyParams = []
, timeout =
data.timeout
|> Maybe.map ((+) 10000)
|> Maybe.map ((*) 1000)
|> Maybe.map toFloat
, decoder = \_ -> SO2.syncDecoder
}

View File

@ -7,7 +7,7 @@ import Internal.Tools.VersionControl as VC
import Task exposing (Task)
sync : List String -> Maybe (SyncInput -> Task X.Error SyncOutput)
sync : List String -> SyncInput -> Task X.Error SyncOutput
sync versions =
VC.withBottomLayer
{ current = Api.syncV1
@ -22,6 +22,7 @@ sync versions =
}
|> VC.sameForVersion "v1.5"
|> VC.mostRecentFromVersionList versions
|> Maybe.withDefault (always <| Task.fail X.UnsupportedSpecVersion)
type alias SyncInput =

View File

@ -0,0 +1,56 @@
module Internal.Tools.LoginValues exposing (..)
type AccessToken
= NoAccess
| AccessToken String
| UsernameAndPassword { username : String, password : String, token : Maybe String }
defaultAccessToken : AccessToken
defaultAccessToken =
NoAccess
fromAccessToken : String -> AccessToken
fromAccessToken =
AccessToken
fromUsernameAndPassword : String -> String -> AccessToken
fromUsernameAndPassword username password =
UsernameAndPassword
{ username = username
, password = password
, token = Nothing
}
getToken : AccessToken -> Maybe String
getToken t =
case t of
NoAccess ->
Nothing
AccessToken token ->
Just token
UsernameAndPassword { token } ->
token
addToken : String -> AccessToken -> AccessToken
addToken s t =
case t of
NoAccess ->
AccessToken s
AccessToken _ ->
AccessToken s
UsernameAndPassword { username, password } ->
UsernameAndPassword
{ username = username
, password = password
, token = Just s
}

View File

@ -1,8 +1,7 @@
module Internal.Tools.Timestamp exposing (Timestamp, encodeTimestamp, generateTransactionId, timestampDecoder)
module Internal.Tools.Timestamp exposing (Timestamp, encodeTimestamp, timestampDecoder)
import Json.Decode as D
import Json.Encode as E
import Task exposing (Task)
import Time
@ -22,13 +21,3 @@ encodeTimestamp =
timestampDecoder : D.Decoder Timestamp
timestampDecoder =
D.map Time.millisToPosix D.int
{-| Generate a transaction id from the current Unix timestamp
-}
generateTransactionId : Task x String
generateTransactionId =
Time.now
|> Task.map Time.posixToMillis
|> Task.map String.fromInt
|> Task.map ((++) "elm")

View File

@ -0,0 +1,271 @@
module Internal.Tools.ValueGetter exposing (..)
{-| This module creates task pipelines that help gather information
in the Matrix API.
For example, it might happen that you need to make multiple API calls:
- Authenticate
- Log in
- Get a list of channels
- Send a message in every room
For each of these API requests, you might need certain information like
which spec version the homeserver supports.
This module takes care of this. That way, functions can be written simply by
saying "I need THESE values" and you will then be able to assign them to each
HTTP call that needs that value.
-}
import Task exposing (Task)
{-| A ValueGetter type takes care of values that MIGHT be available.
If a value is not available, then the task can be used to get a new value.
-}
type alias ValueGetter a =
{ value : Maybe a, getValue : Task x a }
{-| Convert a `ValueGetter` type to a task. If a previous value has already been given,
then use that value. Otherwise, use the `getValue` task to get a new value.
-}
toTask : ValueGetter a -> Task x a
toTask { value, getValue } =
Maybe.map Task.succeed value
|> Maybe.withDefault getValue
withInfo : (a -> Task x result) -> ValueGetter a -> Task x result
withInfo task info1 =
Task.andThen
(\a ->
task a
)
(toTask info1)
withInfo2 :
(a -> b -> Task x result)
-> ValueGetter a
-> ValueGetter b
-> Task x result
withInfo2 task info1 info2 =
Task.andThen
(\a ->
Task.andThen
(\b ->
task a b
)
(toTask info2)
)
(toTask info1)
withInfo3 :
(a -> b -> c -> Task x result)
-> ValueGetter a
-> ValueGetter b
-> ValueGetter c
-> Task x result
withInfo3 task info1 info2 info3 =
Task.andThen
(\a ->
Task.andThen
(\b ->
Task.andThen
(\c ->
task a b c
)
(toTask info3)
)
(toTask info2)
)
(toTask info1)
withInfo4 :
(a -> b -> c -> d -> Task x result)
-> ValueGetter a
-> ValueGetter b
-> ValueGetter c
-> ValueGetter d
-> Task x result
withInfo4 task info1 info2 info3 info4 =
Task.andThen
(\a ->
Task.andThen
(\b ->
Task.andThen
(\c ->
Task.andThen
(\d ->
task a b c d
)
(toTask info4)
)
(toTask info3)
)
(toTask info2)
)
(toTask info1)
withInfo5 :
(a -> b -> c -> d -> e -> Task x result)
-> ValueGetter a
-> ValueGetter b
-> ValueGetter c
-> ValueGetter d
-> ValueGetter e
-> Task x result
withInfo5 task info1 info2 info3 info4 info5 =
Task.andThen
(\a ->
Task.andThen
(\b ->
Task.andThen
(\c ->
Task.andThen
(\d ->
Task.andThen
(\e ->
task a b c d e
)
(toTask info5)
)
(toTask info4)
)
(toTask info3)
)
(toTask info2)
)
(toTask info1)
withInfo6 :
(a -> b -> c -> d -> e -> f -> Task x result)
-> ValueGetter a
-> ValueGetter b
-> ValueGetter c
-> ValueGetter d
-> ValueGetter e
-> ValueGetter f
-> Task x result
withInfo6 task info1 info2 info3 info4 info5 info6 =
Task.andThen
(\a ->
Task.andThen
(\b ->
Task.andThen
(\c ->
Task.andThen
(\d ->
Task.andThen
(\e ->
Task.andThen
(\f ->
task a b c d e f
)
(toTask info6)
)
(toTask info5)
)
(toTask info4)
)
(toTask info3)
)
(toTask info2)
)
(toTask info1)
withInfo7 :
(a -> b -> c -> d -> e -> f -> g -> Task x result)
-> ValueGetter a
-> ValueGetter b
-> ValueGetter c
-> ValueGetter d
-> ValueGetter e
-> ValueGetter f
-> ValueGetter g
-> Task x result
withInfo7 task info1 info2 info3 info4 info5 info6 info7 =
Task.andThen
(\a ->
Task.andThen
(\b ->
Task.andThen
(\c ->
Task.andThen
(\d ->
Task.andThen
(\e ->
Task.andThen
(\f ->
Task.andThen
(\g ->
task a b c d e f g
)
(toTask info7)
)
(toTask info6)
)
(toTask info5)
)
(toTask info4)
)
(toTask info3)
)
(toTask info2)
)
(toTask info1)
withInfo8 :
(a -> b -> c -> d -> e -> f -> g -> h -> Task x result)
-> ValueGetter a
-> ValueGetter b
-> ValueGetter c
-> ValueGetter d
-> ValueGetter e
-> ValueGetter f
-> ValueGetter g
-> ValueGetter h
-> Task x result
withInfo8 task info1 info2 info3 info4 info5 info6 info7 info8 =
Task.andThen
(\a ->
Task.andThen
(\b ->
Task.andThen
(\c ->
Task.andThen
(\d ->
Task.andThen
(\e ->
Task.andThen
(\f ->
Task.andThen
(\g ->
Task.andThen
(\h ->
task a b c d e f g h
)
(toTask info8)
)
(toTask info7)
)
(toTask info6)
)
(toTask info5)
)
(toTask info4)
)
(toTask info3)
)
(toTask info2)
)
(toTask info1)