Compare commits

...

9 Commits

13 changed files with 1742 additions and 58 deletions

View File

@ -0,0 +1,163 @@
module Internal.Api.BaseUrl.Api exposing (..)
{-|
# Base URL
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.Tools.Json as Json
import Internal.Values.Context as Context
import Internal.Values.Envelope as E
import Internal.Values.Vault as V
baseUrl : BaseUrlInput -> C.TaskChain R.Error (E.EnvelopeUpdate V.VaultUpdate) ph { ph | baseUrl : () }
baseUrl data =
R.toChain
{ logHttp =
\r ->
( E.HttpRequest r
, String.concat
-- TODO: Move this to Internal.Config.Text module
[ "Matrix HTTP: "
, r.method
, " "
, r.url
]
|> log.info
|> List.singleton
)
, coder = coder
, request =
\context ->
{ attributes = []
, baseUrl = data.url
, context = context
, method = "GET"
, path = [ ".well-known", "matrix", "client" ]
}
, toContextChange = \info -> Context.setBaseUrl info.homeserver.baseUrl
, toUpdate =
\info ->
( E.SetBaseUrl info.homeserver.baseUrl
, String.concat
[ "Found baseURL of "
, data.url
, " at address "
, info.homeserver.baseUrl
]
|> log.debug
|> List.singleton
)
}
type alias BaseUrlInput =
{ url : String }
type alias DiscoveryInformation =
{ homeserver : HomeserverInformation
, identityServer : Maybe IdentityServerInformation
}
type alias HomeserverInformation =
{ baseUrl : String }
type alias IdentityServerInformation =
{ baseUrl : String }
coder : Json.Coder DiscoveryInformation
coder =
Json.object2
{ name = "Discovery Information"
, description =
[ "Gets discovery information about the domain. The file may include additional keys, which MUST follow the Java package naming convention, e.g. com.example.myapp.property. This ensures property names are suitably namespaced for each application and reduces the risk of clashes."
, "Note that this endpoint is not necessarily handled by the homeserver, but by another webserver, to be used for discovering the homeserver URL."
, "https://spec.matrix.org/v1.10/client-server-api/#getwell-knownmatrixclient"
]
, init = DiscoveryInformation
}
(Json.field.required
{ fieldName = "m.homeserver"
, toField = .homeserver
, coder =
Json.object2
{ name = "Homeserver Information"
, description =
[ "Used by clients to discover homeserver information."
]
, init = \a _ -> { baseUrl = a }
}
(Json.field.required
{ fieldName = "base_url"
, toField = .baseUrl
, description =
[ "The base URL for the homeserver for client-server connections."
]
, 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."
]
}
)
(Json.field.optional.value
{ fieldName = "m.identity_server"
, toField = .identityServer
, coder =
Json.object2
{ name = "Homeserver Information"
, description =
[ "Used by clients to discover homeserver information."
]
, init = \a _ -> { baseUrl = a }
}
(Json.field.required
{ fieldName = "base_url"
, toField = .baseUrl
, description =
[ "The base URL for the homeserver for client-server connections."
]
, 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."
]
}
)

View File

@ -1,7 +1,7 @@
module Internal.Api.Chain exposing
( TaskChain, CompleteChain
, IdemChain, toTask
, fail, succeed, andThen
, fail, succeed, andThen, catchWith
)
{-|
@ -27,7 +27,7 @@ avoid leaking values passing through the API in unexpected ways.
## Operations
@docs fail, succeed, andThen
@docs fail, succeed, andThen, catchWith
-}
@ -127,7 +127,7 @@ andThen f2 f1 =
{-| When an error has occurred, "fix" it with an artificial task chain result.
-}
catchWith : (err -> TaskChainPiece u a b) -> TaskChain err u a b -> TaskChain err u a b
catchWith : (err -> TaskChainPiece u a b) -> TaskChain err u a b -> TaskChain err2 u a b
catchWith onErr f =
onError (\e -> succeed <| onErr e) f
@ -173,7 +173,7 @@ onError onErr f =
|> Task.onError
(\old ->
{ contextChange = identity
, logs = old.logs
, logs = old.logs -- TODO: Log caught errors
, messages = old.messages
}
|> succeed

View File

@ -0,0 +1,924 @@
module Internal.Api.LoginWithUsernameAndPassword.Api exposing (Phantom, loginWithUsernameAndPassword)
{-|
# Login
This module allows the user to log in using a username and password.
@docs Phantom, loginWithUsernameAndPassword
-}
import Internal.Api.Api as A
import Internal.Api.Request as R
import Internal.Config.Leaks as L
import Internal.Tools.Json as Json
import Internal.Values.Context as Context
import Internal.Values.Envelope as E
import Internal.Values.User as User exposing (User)
import Internal.Values.Vault as V
import Json.Encode as E
loginWithUsernameAndPassword : LoginWithUsernameAndPasswordInput -> A.TaskChain (Phantom a) (Phantom { a | accessToken : () })
loginWithUsernameAndPassword =
A.startWithVersion "r0.0.0" loginWithUsernameAndPasswordV1
|> A.sameForVersion "r0.0.1"
|> A.sameForVersion "r0.1.0"
|> A.sameForVersion "r0.2.0"
|> A.forVersion "r0.3.0" loginWithUsernameAndPasswordV2
|> A.forVersion "r0.4.0" loginWithUsernameAndPasswordV3
|> A.forVersion "r0.5.0" loginWithUsernameAndPasswordV4
|> A.sameForVersion "r0.6.0"
|> A.sameForVersion "r0.6.1"
|> A.forVersion "v1.1" loginWithUsernameAndPasswordV5
|> A.sameForVersion "v1.2"
|> A.forVersion "v1.3" loginWithUsernameAndPasswordV6
|> A.forVersion "v1.4" loginWithUsernameAndPasswordV7
|> 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.versionChain
type alias Phantom a =
{ a | baseUrl : (), versions : () }
type alias LoginWithUsernameAndPasswordInput =
{ deviceId : Maybe String
, enableRefreshToken : Maybe Bool
, initialDeviceDisplayName : Maybe String
, password : String
, username : String
}
type alias LoginWithUsernameAndPasswordInputV1 a =
{ a
| password : String
, username : String
}
type alias LoginWithUsernameAndPasswordInputV2 a =
{ a
| deviceId : Maybe String
, initialDeviceDisplayName : Maybe String
, password : String
, username : String
}
type alias LoginWithUsernameAndPasswordInputV3 a =
{ a
| deviceId : Maybe String
, enableRefreshToken : Maybe Bool
, initialDeviceDisplayName : Maybe String
, password : String
, username : String
}
type alias LoginWithUsernameAndPasswordOutputV1 =
{ accessToken : String -- Even though it is not required, we do not want it to be omitted.
, homeserver : String
, refreshToken : Maybe String
, user : Maybe User
}
type alias LoginWithUsernameAndPasswordOutputV2 =
{ accessToken : String -- Even though it is not required, we do not want it to be omitted.
, deviceId : Maybe String
, homeserver : String
, user : Maybe User
}
type alias LoginWithUsernameAndPasswordOutputV3 =
{ accessToken : String -- Even though it is not required, we do not want it to be omitted.
, deviceId : Maybe String
, homeserver : Maybe String
, user : Maybe User
}
type alias LoginWithUsernameAndPasswordOutputV4 =
{ accessToken : String -- Even though it is not required, we do not want it to be omitted.
, deviceId : Maybe String
, homeserver : Maybe String
, user : Maybe User
, wellKnown : Maybe DiscoveryInformationV1
}
type alias LoginWithUsernameAndPasswordOutputV5 =
{ accessToken : String -- Even though it is not required, we do not want it to be omitted.
, deviceId : Maybe String
, expiresInMs : Maybe Int
, homeserver : Maybe String
, refreshToken : Maybe String
, user : Maybe User
, wellKnown : Maybe DiscoveryInformationV1
}
type alias LoginWithUsernameAndPasswordOutputV6 =
{ accessToken : String
, deviceId : String
, expiresInMs : Maybe Int
, homeserver : Maybe String
, refreshToken : Maybe String
, user : User
, wellKnown : Maybe DiscoveryInformationV1
}
type alias DiscoveryInformationV1 =
{ homeserver : HomeserverInformation
, identityServer : Maybe IdentityServerInformation
}
type alias HomeserverInformation =
{ baseUrl : String }
type alias IdentityServerInformation =
{ baseUrl : String }
type alias PhantomV1 a =
{ a | baseUrl : (), now : () }
loginWithUsernameAndPasswordV1 : LoginWithUsernameAndPasswordInputV1 i -> A.TaskChain (PhantomV1 a) (PhantomV1 { a | accessToken : () })
loginWithUsernameAndPasswordV1 { username, password } context =
A.request
{ attributes =
[ R.bodyString "password" password
, R.bodyString "type" "m.login.password"
, R.bodyString "user" username
, R.onStatusCode 400 "M_UNKNOWN"
, R.onStatusCode 403 "M_FORBIDDEN"
]
, coder = coderV1
, method = "POST"
, path = [ "_matrix", "client", "r0", "login" ]
, contextChange =
\out -> Context.setAccessToken out.accessToken
, toUpdate =
\out ->
( E.More
[ E.SetAccessToken
{ created = Context.getNow context
, expiryMs = Nothing
, lastUsed = Context.getNow context
, refresh = out.refreshToken
, value = out.accessToken
}
, out.user
|> Maybe.map (V.SetUser >> E.ContentUpdate)
|> E.Optional
]
, []
)
}
context
loginWithUsernameAndPasswordV2 : LoginWithUsernameAndPasswordInputV2 i -> A.TaskChain (PhantomV1 a) (PhantomV1 { a | accessToken : () })
loginWithUsernameAndPasswordV2 { deviceId, initialDeviceDisplayName, username, password } context =
A.request
{ attributes =
[ R.bodyOpString "device_id" deviceId
, R.bodyOpString "initial_device_display_name" initialDeviceDisplayName
, R.bodyString "password" password
, R.bodyString "type" "m.login.password"
, R.bodyString "user" username
, R.onStatusCode 400 "M_UNKNOWN"
, R.onStatusCode 403 "M_FORBIDDEN"
, R.onStatusCode 429 "string" -- Yup. That's what it says.
]
, coder = coderV2
, method = "POST"
, path = [ "_matrix", "client", "r0", "login" ]
, contextChange =
\out -> Context.setAccessToken out.accessToken
, toUpdate =
\out ->
( E.More
[ E.SetAccessToken
{ created = Context.getNow context
, expiryMs = Nothing
, lastUsed = Context.getNow context
, refresh = Nothing
, value = out.accessToken
}
, out.user
|> Maybe.map (V.SetUser >> E.ContentUpdate)
|> E.Optional
, out.deviceId
|> Maybe.map E.SetDeviceId
|> E.Optional
]
, []
)
}
context
loginWithUsernameAndPasswordV3 : LoginWithUsernameAndPasswordInputV2 i -> A.TaskChain (PhantomV1 a) (PhantomV1 { a | accessToken : () })
loginWithUsernameAndPasswordV3 { deviceId, initialDeviceDisplayName, username, password } context =
A.request
{ attributes =
[ R.bodyOpString "address" Nothing
, R.bodyOpString "device_id" deviceId
, R.bodyValue "identifier"
(E.object
[ ( "type", E.string "m.id.user" )
, ( "user", E.string username )
]
)
, R.bodyOpString "initial_device_display_name" initialDeviceDisplayName
, R.bodyString "password" password
, R.bodyString "type" "m.login.password"
, R.bodyString "user" username
, R.onStatusCode 400 "M_UNKNOWN"
, R.onStatusCode 403 "M_FORBIDDEN"
, R.onStatusCode 429 "M_LIMIT_EXCEEDED"
]
, coder = coderV3
, method = "POST"
, path = [ "_matrix", "client", "r0", "login" ]
, contextChange =
\out -> Context.setAccessToken out.accessToken
, toUpdate =
\out ->
( E.More
[ E.SetAccessToken
{ created = Context.getNow context
, expiryMs = Nothing
, lastUsed = Context.getNow context
, refresh = Nothing
, value = out.accessToken
}
, out.user
|> Maybe.map (V.SetUser >> E.ContentUpdate)
|> E.Optional
, out.deviceId
|> Maybe.map E.SetDeviceId
|> E.Optional
]
, []
)
}
context
loginWithUsernameAndPasswordV4 : LoginWithUsernameAndPasswordInputV2 i -> A.TaskChain (PhantomV1 a) (PhantomV1 { a | accessToken : () })
loginWithUsernameAndPasswordV4 { deviceId, initialDeviceDisplayName, username, password } context =
A.request
{ attributes =
[ R.bodyOpString "address" Nothing
, R.bodyOpString "device_id" deviceId
, R.bodyValue "identifier"
(E.object
[ ( "type", E.string "m.id.user" )
, ( "user", E.string username )
]
)
, R.bodyOpString "initial_device_display_name" initialDeviceDisplayName
, R.bodyString "password" password
, R.bodyString "type" "m.login.password"
, R.bodyString "user" username
, R.onStatusCode 400 "M_UNKNOWN"
, R.onStatusCode 403 "M_FORBIDDEN"
, R.onStatusCode 429 "M_LIMIT_EXCEEDED"
]
, coder = coderV4
, method = "POST"
, path = [ "_matrix", "client", "r0", "login" ]
, contextChange =
\out -> Context.setAccessToken out.accessToken
, toUpdate =
\out ->
( E.More
[ E.SetAccessToken
{ created = Context.getNow context
, expiryMs = Nothing
, lastUsed = Context.getNow context
, refresh = Nothing
, value = out.accessToken
}
, out.user
|> Maybe.map (V.SetUser >> E.ContentUpdate)
|> E.Optional
, out.wellKnown
|> Maybe.map (.homeserver >> .baseUrl)
|> Maybe.map E.SetBaseUrl
|> E.Optional
, out.deviceId
|> Maybe.map E.SetDeviceId
|> E.Optional
]
, []
)
}
context
loginWithUsernameAndPasswordV5 : LoginWithUsernameAndPasswordInputV2 i -> A.TaskChain (PhantomV1 a) (PhantomV1 { a | accessToken : () })
loginWithUsernameAndPasswordV5 { deviceId, initialDeviceDisplayName, username, password } context =
A.request
{ attributes =
[ R.bodyOpString "address" Nothing
, R.bodyOpString "device_id" deviceId
, R.bodyValue "identifier"
(E.object
[ ( "type", E.string "m.id.user" )
, ( "user", E.string username )
]
)
, R.bodyOpString "initial_device_display_name" initialDeviceDisplayName
, R.bodyString "password" password
, R.bodyString "type" "m.login.password"
, R.bodyString "user" username
, R.onStatusCode 400 "M_UNKNOWN"
, R.onStatusCode 403 "M_FORBIDDEN"
, R.onStatusCode 429 "M_LIMIT_EXCEEDED"
]
, coder = coderV4
, method = "POST"
, path = [ "_matrix", "client", "v3", "login" ]
, contextChange =
\out -> Context.setAccessToken out.accessToken
, toUpdate =
\out ->
( E.More
[ E.SetAccessToken
{ created = Context.getNow context
, expiryMs = Nothing
, lastUsed = Context.getNow context
, refresh = Nothing
, value = out.accessToken
}
, out.user
|> Maybe.map (V.SetUser >> E.ContentUpdate)
|> E.Optional
, out.wellKnown
|> Maybe.map (.homeserver >> .baseUrl)
|> Maybe.map E.SetBaseUrl
|> E.Optional
, out.deviceId
|> Maybe.map E.SetDeviceId
|> E.Optional
]
, []
)
}
context
loginWithUsernameAndPasswordV6 : LoginWithUsernameAndPasswordInputV3 i -> A.TaskChain (PhantomV1 a) (PhantomV1 { a | accessToken : () })
loginWithUsernameAndPasswordV6 { deviceId, enableRefreshToken, initialDeviceDisplayName, username, password } context =
A.request
{ attributes =
[ R.bodyOpString "address" Nothing
, R.bodyOpString "device_id" deviceId
, R.bodyValue "identifier"
(E.object
[ ( "type", E.string "m.id.user" )
, ( "user", E.string username )
]
)
, R.bodyOpString "initial_device_display_name" initialDeviceDisplayName
, R.bodyString "password" password
, R.bodyOpBool "refresh_token" enableRefreshToken
, R.bodyString "type" "m.login.password"
, R.bodyString "user" username
, R.onStatusCode 400 "M_UNKNOWN"
, R.onStatusCode 403 "M_FORBIDDEN"
, R.onStatusCode 429 "M_LIMIT_EXCEEDED"
]
, coder = coderV5
, method = "POST"
, path = [ "_matrix", "client", "v3", "login" ]
, contextChange =
\out -> Context.setAccessToken out.accessToken
, toUpdate =
\out ->
( E.More
[ E.SetAccessToken
{ created = Context.getNow context
, expiryMs = out.expiresInMs
, lastUsed = Context.getNow context
, refresh = out.refreshToken
, value = out.accessToken
}
, out.user
|> Maybe.map (V.SetUser >> E.ContentUpdate)
|> E.Optional
, out.wellKnown
|> Maybe.map (.homeserver >> .baseUrl)
|> Maybe.map E.SetBaseUrl
|> E.Optional
, out.deviceId
|> Maybe.map E.SetDeviceId
|> E.Optional
]
, []
)
}
context
loginWithUsernameAndPasswordV7 : LoginWithUsernameAndPasswordInputV3 i -> A.TaskChain (PhantomV1 a) (PhantomV1 { a | accessToken : () })
loginWithUsernameAndPasswordV7 { deviceId, enableRefreshToken, initialDeviceDisplayName, username, password } context =
A.request
{ attributes =
[ R.bodyOpString "address" Nothing
, R.bodyOpString "device_id" deviceId
, R.bodyValue "identifier"
(E.object
[ ( "type", E.string "m.id.user" )
, ( "user", E.string username )
]
)
, R.bodyOpString "initial_device_display_name" initialDeviceDisplayName
, R.bodyString "password" password
, R.bodyOpBool "refresh_token" enableRefreshToken
, R.bodyString "type" "m.login.password"
, R.bodyString "user" username
, R.onStatusCode 400 "M_UNKNOWN"
, R.onStatusCode 403 "M_FORBIDDEN"
, R.onStatusCode 429 "M_LIMIT_EXCEEDED"
]
, coder = coderV6
, method = "POST"
, path = [ "_matrix", "client", "v3", "login" ]
, contextChange =
\out -> Context.setAccessToken out.accessToken
, toUpdate =
\out ->
( E.More
[ E.SetAccessToken
{ created = Context.getNow context
, expiryMs = out.expiresInMs
, lastUsed = Context.getNow context
, refresh = out.refreshToken
, value = out.accessToken
}
, E.ContentUpdate (V.SetUser out.user)
, out.wellKnown
|> Maybe.map (.homeserver >> .baseUrl)
|> Maybe.map E.SetBaseUrl
|> E.Optional
, E.SetDeviceId out.deviceId
]
, []
)
}
context
coderV1 : Json.Coder LoginWithUsernameAndPasswordOutputV1
coderV1 =
Json.object4
{ name = "Login Response"
, description =
[ "Authenticates the user by password, and issues an access token they can use to authorize themself in subsequent requests."
, "https://spec.matrix.org/legacy/r0.0.0/client_server.html#post-matrix-client-r0-login"
]
, init = LoginWithUsernameAndPasswordOutputV1
}
(Json.field.required
{ fieldName = "access_token"
, toField = .accessToken
, description =
[ "An access token for the account. This access token can then be used to authorize other requests. The access token may expire at some point, and if so, it SHOULD come with a refresh_token. There is no specific error message to indicate that a request has failed because an access token has expired; instead, if a client has reason to believe its access token is valid, and it receives an auth error, they should attempt to refresh for a new token on failure, and retry the request with the new token."
]
, coder = Json.string
}
)
(Json.field.required
{ fieldName = "home_server"
, toField = .homeserver
, description =
[ "The hostname of the homeserver on which the account has been registered."
]
, coder = Json.string
}
)
(Json.field.optional.value
{ fieldName = "refresh_token"
, toField = .refreshToken
, description =
[ "A refresh_token may be exchanged for a new access_token using the /tokenrefresh API endpoint."
]
, coder = Json.string
}
)
(Json.field.optional.value
{ fieldName = "user_id"
, toField = .user
, description =
[ "The fully-qualified Matrix ID that has been registered."
]
, coder = User.coder
}
)
coderV2 : Json.Coder LoginWithUsernameAndPasswordOutputV2
coderV2 =
Json.object4
{ name = "Login Response"
, description =
[ "Authenticates the user by password, and issues an access token they can use to authorize themself in subsequent requests."
, "https://spec.matrix.org/legacy/client_server/r0.3.0.html#post-matrix-client-r0-login"
]
, init = LoginWithUsernameAndPasswordOutputV2
}
(Json.field.required
{ fieldName = "access_token"
, toField = .accessToken
, description =
[ "An access token for the account. This access token can then be used to authorize other requests."
]
, coder = Json.string
}
)
(Json.field.optional.value
{ fieldName = "device_id"
, toField = .deviceId
, description =
[ "ID of the logged-in device. Will be the same as the corresponding parameter in the request, if one was specified."
]
, coder = Json.string
}
)
(Json.field.required
{ fieldName = "home_server"
, toField = .homeserver
, description =
[ "The hostname of the homeserver on which the account has been registered."
]
, coder = Json.string
}
)
(Json.field.optional.value
{ fieldName = "user_id"
, toField = .user
, description =
[ "The fully-qualified Matrix ID that has been registered."
]
, coder = User.coder
}
)
coderV3 : Json.Coder LoginWithUsernameAndPasswordOutputV3
coderV3 =
Json.object4
{ name = "Login Response"
, description =
[ "Authenticates the user by password, and issues an access token they can use to authorize themself in subsequent requests."
, "https://spec.matrix.org/legacy/client_server/r0.3.0.html#post-matrix-client-r0-login"
]
, init = LoginWithUsernameAndPasswordOutputV3
}
(Json.field.required
{ fieldName = "access_token"
, toField = .accessToken
, description =
[ "An access token for the account. This access token can then be used to authorize other requests."
]
, coder = Json.string
}
)
(Json.field.optional.value
{ fieldName = "device_id"
, toField = .deviceId
, description =
[ "ID of the logged-in device. Will be the same as the corresponding parameter in the request, if one was specified."
]
, coder = Json.string
}
)
(Json.field.optional.value
{ fieldName = "home_server"
, toField = .homeserver
, description =
[ "The hostname of the homeserver on which the account has been registered."
]
, coder = Json.string
}
)
(Json.field.optional.value
{ fieldName = "user_id"
, toField = .user
, description =
[ "The fully-qualified Matrix ID that has been registered."
]
, coder = User.coder
}
)
coderV4 : Json.Coder LoginWithUsernameAndPasswordOutputV4
coderV4 =
Json.object5
{ name = "Login Response"
, description =
[ "Authenticates the user by password, and issues an access token they can use to authorize themself in subsequent requests."
, "https://spec.matrix.org/legacy/client_server/r0.5.0.html#post-matrix-client-r0-login"
]
, init = LoginWithUsernameAndPasswordOutputV4
}
(Json.field.required
{ fieldName = "access_token"
, toField = .accessToken
, description =
[ "An access token for the account. This access token can then be used to authorize other requests."
]
, coder = Json.string
}
)
(Json.field.optional.value
{ fieldName = "device_id"
, toField = .deviceId
, description =
[ "ID of the logged-in device. Will be the same as the corresponding parameter in the request, if one was specified."
]
, coder = Json.string
}
)
(Json.field.optional.value
{ fieldName = "home_server"
, toField = .homeserver
, description =
[ "The hostname of the homeserver on which the account has been registered."
]
, coder = Json.string
}
)
(Json.field.optional.value
{ fieldName = "user_id"
, toField = .user
, description =
[ "The fully-qualified Matrix ID that has been registered."
]
, coder = User.coder
}
)
(Json.field.optional.value
{ fieldName = "well_known"
, toField = .wellKnown
, description =
[ "Optional client configuration provided by the server. If present, clients SHOULD use the provided object to reconfigure themselves, optionally validating the URLs within. This object takes the same form as the one returned from .well-known autodiscovery."
]
, coder = disoveryInformationCoderV1
}
)
coderV5 : Json.Coder LoginWithUsernameAndPasswordOutputV5
coderV5 =
Json.object7
{ name = "Login Response"
, description =
[ "Authenticates the user by password, and issues an access token they can use to authorize themself in subsequent requests."
, "https://spec.matrix.org/v1.3/client-server-api/#post_matrixclientv3login"
]
, init = LoginWithUsernameAndPasswordOutputV5
}
(Json.field.required
{ fieldName = "access_token"
, toField = .accessToken
, description =
[ "An access token for the account. This access token can then be used to authorize other requests."
]
, coder = Json.string
}
)
(Json.field.optional.value
{ fieldName = "device_id"
, toField = .deviceId
, description =
[ "ID of the logged-in device. Will be the same as the corresponding parameter in the request, if one was specified."
]
, coder = Json.string
}
)
(Json.field.optional.value
{ fieldName = "expires_in_ms"
, toField = .expiresInMs
, description =
[ "The lifetime of the access token, in milliseconds. Once the access token has expired a new access token can be obtained by using the provided refresh token. If no refresh token is provided, the client will need to re-log in to obtain a new access token. If not given, the client can assume that the access token will not expire. "
]
, coder = Json.int
}
)
(Json.field.optional.value
{ fieldName = "home_server"
, toField = .homeserver
, description =
[ "The hostname of the homeserver on which the account has been registered."
]
, coder = Json.string
}
)
(Json.field.optional.value
{ fieldName = "refresh_token"
, toField = .refreshToken
, description =
[ "A refresh token for the account. This token can be used to obtain a new access token when it expires by calling the /refresh endpoint."
]
, coder = Json.string
}
)
(Json.field.optional.value
{ fieldName = "user_id"
, toField = .user
, description =
[ "The fully-qualified Matrix ID that has been registered."
]
, coder = User.coder
}
)
(Json.field.optional.value
{ fieldName = "well_known"
, toField = .wellKnown
, description =
[ "Optional client configuration provided by the server. If present, clients SHOULD use the provided object to reconfigure themselves, optionally validating the URLs within. This object takes the same form as the one returned from .well-known autodiscovery."
]
, coder = disoveryInformationCoderV1
}
)
coderV6 : Json.Coder LoginWithUsernameAndPasswordOutputV6
coderV6 =
Json.object7
{ name = "Login Response"
, description =
[ "Authenticates the user by password, and issues an access token they can use to authorize themself in subsequent requests."
, "https://spec.matrix.org/v1.3/client-server-api/#post_matrixclientv3login"
]
, init = LoginWithUsernameAndPasswordOutputV6
}
(Json.field.required
{ fieldName = "access_token"
, toField = .accessToken
, description =
[ "An access token for the account. This access token can then be used to authorize other requests."
]
, coder = Json.string
}
)
(Json.field.required
{ fieldName = "device_id"
, toField = .deviceId
, description =
[ "ID of the logged-in device. Will be the same as the corresponding parameter in the request, if one was specified."
]
, coder = Json.string
}
)
(Json.field.optional.value
{ fieldName = "expires_in_ms"
, toField = .expiresInMs
, description =
[ "The lifetime of the access token, in milliseconds. Once the access token has expired a new access token can be obtained by using the provided refresh token. If no refresh token is provided, the client will need to re-log in to obtain a new access token. If not given, the client can assume that the access token will not expire. "
]
, coder = Json.int
}
)
(Json.field.optional.value
{ fieldName = "home_server"
, toField = .homeserver
, description =
[ "The hostname of the homeserver on which the account has been registered."
]
, coder = Json.string
}
)
(Json.field.optional.value
{ fieldName = "refresh_token"
, toField = .refreshToken
, description =
[ "A refresh token for the account. This token can be used to obtain a new access token when it expires by calling the /refresh endpoint."
]
, coder = Json.string
}
)
(Json.field.required
{ fieldName = "user_id"
, toField = .user
, description =
[ "The fully-qualified Matrix ID that has been registered."
]
, coder = User.coder
}
)
(Json.field.optional.value
{ fieldName = "well_known"
, toField = .wellKnown
, description =
[ "Optional client configuration provided by the server. If present, clients SHOULD use the provided object to reconfigure themselves, optionally validating the URLs within. This object takes the same form as the one returned from .well-known autodiscovery."
]
, coder = disoveryInformationCoderV1
}
)
disoveryInformationCoderV1 : Json.Coder DiscoveryInformationV1
disoveryInformationCoderV1 =
Json.object2
{ name = "Discovery Information"
, description =
[ "Gets discovery information about the domain. The file may include additional keys, which MUST follow the Java package naming convention, e.g. com.example.myapp.property. This ensures property names are suitably namespaced for each application and reduces the risk of clashes."
, "Note that this endpoint is not necessarily handled by the homeserver, but by another webserver, to be used for discovering the homeserver URL."
, "https://spec.matrix.org/v1.10/client-server-api/#getwell-knownmatrixclient"
]
, init = DiscoveryInformationV1
}
(Json.field.required
{ fieldName = "m.homeserver"
, toField = .homeserver
, coder =
Json.object2
{ name = "Homeserver Information"
, description =
[ "Used by clients to discover homeserver information."
]
, init = \a _ -> { baseUrl = a }
}
(Json.field.required
{ fieldName = "base_url"
, toField = .baseUrl
, description =
[ "The base URL for the homeserver for client-server connections."
]
, 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."
]
}
)
(Json.field.optional.value
{ fieldName = "m.identity_server"
, toField = .identityServer
, coder =
Json.object2
{ name = "Homeserver Information"
, description =
[ "Used by clients to discover homeserver information."
]
, init = \a _ -> { baseUrl = a }
}
(Json.field.required
{ fieldName = "base_url"
, toField = .baseUrl
, description =
[ "The base URL for the homeserver for client-server connections."
]
, 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."
]
}
)

57
src/Internal/Api/Main.elm Normal file
View File

@ -0,0 +1,57 @@
module Internal.Api.Main exposing
( Msg
, sendMessageEvent
)
{-|
# Main API module
This module is used as reference for getting
## VaultUpdate
@docs Msg
## Actions
@docs sendMessageEvent
-}
import Internal.Api.Task as ITask exposing (Backpack)
import Internal.Tools.Json as Json
import Internal.Values.Context as Context
import Internal.Values.Envelope as E
type alias Msg =
Backpack
{-| Send a message event.
-}
sendMessageEvent :
E.Envelope a
->
{ content : Json.Value
, eventType : String
, roomId : String
, toMsg : Msg -> msg
, transactionId : String
}
-> Cmd msg
sendMessageEvent env data =
ITask.run
data.toMsg
(ITask.sendMessageEvent
{ content = data.content
, eventType = data.eventType
, roomId = data.roomId
, transactionId = data.transactionId
}
)
(Context.apiFormat env.context)

View File

@ -0,0 +1,37 @@
module Internal.Api.Now.Api exposing (getNow)
{-|
# Now
Get the current time.
@docs getNow
-}
import Internal.Api.Api as A
import Internal.Config.Log exposing (log)
import Internal.Values.Context as Context
import Internal.Values.Envelope as E
import Task
import Time
getNow : A.TaskChain a { a | now : () }
getNow _ =
Task.map
(\now ->
{ messages = [ E.SetNow now ]
, logs =
[ "Identified current time at Unix time "
, now |> Time.posixToMillis |> String.fromInt
]
|> String.concat
|> log.debug
|> List.singleton
, contextChange = Context.setNow now
}
)
Time.now

View File

@ -1,7 +1,7 @@
module Internal.Api.Request exposing
( ApiCall, ApiPlan, Attribute, callAPI, withAttributes, toChain
, Request, Error(..)
, accessToken, withTransactionId, timeout, onStatusCode
, accessToken, timeout, onStatusCode
, fullBody, bodyBool, bodyInt, bodyString, bodyValue, bodyOpBool, bodyOpInt, bodyOpString, bodyOpValue
, queryBool, queryInt, queryString, queryOpBool, queryOpInt, queryOpString
)
@ -28,7 +28,7 @@ Sometimes, APIs might fail. As a result, you may receive an error.
### General attributes
@docs accessToken, withTransactionId, timeout, onStatusCode
@docs accessToken, timeout, onStatusCode
### Body
@ -89,7 +89,6 @@ type ContextAttr
| Header Http.Header
| NoAttr
| QueryParam UrlBuilder.QueryParameter
| ReplaceInUrl String String
| StatusCodeResponse Int ( Error, List Log )
| Timeout Float
@ -98,6 +97,8 @@ type ContextAttr
-}
type Error
= InternetException Http.Error
| MissingUsername
| MissingPassword
| NoSupportedVersion
| ServerReturnsBadJSON String
| ServerReturnsError String Json.Value
@ -374,27 +375,7 @@ getUrl : ApiCall a -> String
getUrl { attributes, baseUrl, path } =
UrlBuilder.crossOrigin
baseUrl
(path
|> List.map
(\p ->
List.foldl
(\attr cp ->
case attr of
ReplaceInUrl from to ->
if from == cp then
to
else
cp
_ ->
cp
)
p
attributes
)
|> List.map Url.percentEncode
)
(List.map Url.percentEncode path)
(getQueryParams attributes)
@ -613,10 +594,3 @@ withAttributes attrs f context =
|> List.append data.attributes
}
)
{-| Attribute that requires a transaction id to be present.
-}
withTransactionId : Attribute { a | transaction : () }
withTransactionId =
Context.getTransaction >> ReplaceInUrl "txnId"

View File

@ -0,0 +1,198 @@
module Internal.Api.SendMessageEvent.Api exposing (..)
{-|
# Send message event
This module helps send message events to rooms on the Matrix API.
@docs Phantom
-}
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.Tools.Json as Json
import Internal.Values.Envelope as E
sendMessageEvent : SendMessageEventInput -> A.TaskChain (Phantom a) (Phantom a)
sendMessageEvent =
A.startWithVersion "r0.0.0" sendMessageEventV1
|> A.sameForVersion "r0.0.1"
|> A.sameForVersion "r0.1.0"
|> A.sameForVersion "r0.2.0"
|> A.sameForVersion "r0.3.0"
|> A.sameForVersion "r0.4.0"
|> A.sameForVersion "r0.5.0"
|> A.sameForVersion "r0.6.0"
|> A.forVersion "r0.6.1" sendMessageEventV2
|> A.forVersion "v1.1" sendMessageEventV3
|> A.sameForVersion "v1.2"
|> A.sameForVersion "v1.3"
|> A.sameForVersion "v1.4"
|> 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.sameForVersion "v1.11"
|> A.versionChain
type alias Phantom a =
a
type alias PhantomV1 a =
{ a | accessToken : (), baseUrl : () }
type alias SendMessageEventInput =
{ content : Json.Value
, eventType : String
, roomId : String
, transactionId : String
}
type alias SendMessageEventInputV1 a =
{ a
| content : Json.Value
, eventType : String
, roomId : String
, transactionId : String
}
type alias SendMessageEventOutputV1 =
{ eventId : Maybe String }
type alias SendMessageEventOutputV2 =
{ eventId : String }
sendMessageEventV1 : SendMessageEventInputV1 i -> A.TaskChain (PhantomV1 a) (PhantomV1 a)
sendMessageEventV1 { content, eventType, roomId, transactionId } =
A.request
{ attributes = [ R.fullBody content ]
, coder = coderV1
, contextChange = always identity
, method = "PUT"
, path = [ "_matrix", "client", "r0", "rooms", roomId, "send", eventType, transactionId ]
, toUpdate =
\out ->
( E.More []
, out.eventId
|> Maybe.map ((++) ", received event id ")
|> Maybe.withDefault ""
|> (++) "Sent event"
|> log.debug
|> List.singleton
)
}
sendMessageEventV2 : SendMessageEventInputV1 i -> A.TaskChain (PhantomV1 a) (PhantomV1 a)
sendMessageEventV2 { content, eventType, roomId, transactionId } =
A.request
{ attributes = [ R.fullBody content ]
, coder = coderV2
, contextChange = always identity
, method = "PUT"
, path = [ "_matrix", "client", "r0", "rooms", roomId, "send", eventType, transactionId ]
, toUpdate =
\out ->
( E.More []
, out.eventId
|> (++) "Sent event, received event id "
|> log.debug
|> List.singleton
)
}
sendMessageEventV3 : SendMessageEventInputV1 i -> A.TaskChain (PhantomV1 a) (PhantomV1 a)
sendMessageEventV3 { content, eventType, roomId, transactionId } =
A.request
{ attributes = [ R.fullBody content ]
, coder = coderV2
, contextChange = always identity
, method = "PUT"
, path = [ "_matrix", "client", "v3", "rooms", roomId, "send", eventType, transactionId ]
, toUpdate =
\out ->
( E.More []
, out.eventId
|> (++) "Sent event, received event id "
|> log.debug
|> List.singleton
)
}
coderV1 : Json.Coder SendMessageEventOutputV1
coderV1 =
Json.object2
{ 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
}
(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
, description = Debug.todo "Needs docs"
, coder = Json.string
}
)
coderV2 : Json.Coder SendMessageEventOutputV2
coderV2 =
Json.object2
{ 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 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
, description = Debug.todo "Needs docs"
, coder = Json.string
}
)

View File

@ -1,4 +1,7 @@
module Internal.Api.Task exposing (Task, run)
module Internal.Api.Task exposing
( Task, run, Backpack
, sendMessageEvent
)
{-|
@ -15,14 +18,25 @@ up-to-date.
## Use
@docs Task, run
@docs Task, run, Backpack
## Tasks
@docs sendMessageEvent
-}
import Internal.Api.BaseUrl.Api
import Internal.Api.Chain as C
import Internal.Api.LoginWithUsernameAndPassword.Api
import Internal.Api.Now.Api
import Internal.Api.Request as Request
import Internal.Config.Log exposing (Log)
import Internal.Values.Context exposing (APIContext)
import Internal.Api.SendMessageEvent.Api
import Internal.Api.Versions.Api
import Internal.Config.Log exposing (Log, log)
import Internal.Tools.Json as Json
import Internal.Values.Context as Context exposing (APIContext)
import Internal.Values.Envelope exposing (EnvelopeUpdate(..))
import Internal.Values.Room exposing (RoomUpdate(..))
import Internal.Values.Vault exposing (VaultUpdate(..))
@ -49,6 +63,128 @@ type alias UFTask a b =
C.TaskChain Request.Error (EnvelopeUpdate VaultUpdate) a b
{-| Get an access token to talk to the Matrix API
-}
getAccessToken : UFTask { a | now : () } { a | accessToken : (), now : () }
getAccessToken c =
case Context.fromApiFormat c of
context ->
case ( Context.mostPopularToken context, context.username, context.password ) of
( Just a, _, _ ) ->
C.succeed
{ messages = []
, logs = [ log.debug "Using cached access token from Vault" ]
, contextChange = Context.setAccessToken a
}
c
( Nothing, Just u, Just p ) ->
Internal.Api.LoginWithUsernameAndPassword.Api.loginWithUsernameAndPassword
{ deviceId = Context.fromApiFormat c |> .deviceId
, enableRefreshToken = Just True -- TODO: Turn this into a setting
, initialDeviceDisplayName = Nothing -- TODO: Turn this into a setting
, password = p
, username = u
}
c
( Nothing, Nothing, _ ) ->
C.fail Request.MissingUsername c
( Nothing, Just _, Nothing ) ->
C.fail Request.MissingPassword c
{-| Get the base URL where the Matrix API can be accessed
-}
getBaseUrl : UFTask a { a | baseUrl : () }
getBaseUrl c =
case Context.fromApiFormat c |> .baseUrl of
Just b ->
C.succeed
{ messages = []
, logs = [ log.debug "Using cached baseURL from Vault" ]
, contextChange = Context.setBaseUrl b
}
c
Nothing ->
Internal.Api.BaseUrl.Api.baseUrl
{ url = Context.fromApiFormat c |> .serverName }
c
{-| Get the current timestamp
-}
getNow : UFTask { a | baseUrl : () } { a | baseUrl : (), now : () }
getNow =
Internal.Api.Now.Api.getNow
{-| Get the versions that are potentially supported by the Matrix API
-}
getVersions : UFTask { a | baseUrl : () } { a | baseUrl : (), versions : () }
getVersions c =
case Context.fromApiFormat c |> .versions of
Just v ->
C.succeed
{ messages = []
, logs = [ log.debug "Using cached versions from Vault" ]
, contextChange = Context.setVersions v
}
c
Nothing ->
Internal.Api.Versions.Api.versions c
finishTask : UFTask {} b -> Task
finishTask uftask =
uftask
|> C.andThen
(C.succeed
{ messages = []
, logs = []
, contextChange = Context.reset
}
)
|> C.catchWith
(\_ ->
{ messages = [] -- TODO: Maybe categorize errors?
, logs = [ log.warn "Encountered unhandled error" ]
, contextChange = Context.reset
}
)
{-| Establish a Task Chain context where the base URL and supported list of
versions are known.
-}
makeVB : UFTask a { a | baseUrl : (), versions : () }
makeVB =
C.andThen getVersions getBaseUrl
{-| Establish a Task Chain context where the base URL and supported list of
versions are known, and where an access token is available to make an
authenticated API call.
-}
makeVBA : UFTask a { a | accessToken : (), baseUrl : (), now : (), versions : () }
makeVBA =
makeVB
|> C.andThen getNow
|> C.andThen getAccessToken
{-| Send a message event to a room.
-}
sendMessageEvent : { content : Json.Value, eventType : String, roomId : String, transactionId : String } -> Task
sendMessageEvent input =
makeVBA
|> C.andThen (Internal.Api.SendMessageEvent.Api.sendMessageEvent input)
|> finishTask
{-| Transform a completed task into a Cmd.
-}
run : (Backpack -> msg) -> Task -> APIContext {} -> Cmd msg

View File

@ -1,5 +1,6 @@
module Internal.Tools.Timestamp exposing
( Timestamp
, add, toMs
, coder, encode, decoder
)
@ -12,6 +13,11 @@ elm/time. This module offers ways to work with the timestamp in meaningful ways.
@docs Timestamp
## Calculate
@docs add, toMs
## JSON coders
@docs coder, encode, decoder
@ -28,6 +34,15 @@ type alias Timestamp =
Time.Posix
{-| Add a given number of miliseconds to a given Timestamp.
-}
add : Int -> Timestamp -> Timestamp
add m =
Time.posixToMillis
>> (+) m
>> Time.millisToPosix
{-| Create a Json coder
-}
coder : Json.Coder Timestamp
@ -55,3 +70,10 @@ encode =
decoder : Json.Decoder Timestamp
decoder =
Json.decode coder
{-| Turn a Timestamp into a number of miliseconds
-}
toMs : Timestamp -> Int
toMs =
Time.posixToMillis

View File

@ -1,10 +1,12 @@
module Internal.Values.Context exposing
( Context, init, coder, encode, decoder
( Context, AccessToken, init, coder, encode, decoder, mostPopularToken
, APIContext, apiFormat, fromApiFormat
, setAccessToken, getAccessToken
, setBaseUrl, getBaseUrl
, setNow, getNow
, setTransaction, getTransaction
, Versions, setVersions, getVersions
, reset
)
{-| The Context is the set of variables that the user (mostly) cannot control.
@ -14,7 +16,7 @@ the Matrix API.
## Context
@docs Context, init, coder, encode, decoder
@docs Context, AccessToken, init, coder, encode, decoder, mostPopularToken
## APIContext
@ -38,6 +40,11 @@ information that can be inserted.
@docs setBaseUrl, getBaseUrl
### Timestamp
@docs setNow, getNow
### Transaction id
@docs setTransaction, getTransaction
@ -47,21 +54,43 @@ information that can be inserted.
@docs Versions, setVersions, getVersions
### Reset
@docs reset
-}
import Internal.Config.Leaks as L
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
{-| The Access Token is a combination of access tokens, values and refresh
tokens that contain and summarizes all properties of a known access token.
-}
type alias AccessToken =
{ created : Timestamp
, expiryMs : Maybe Int
, lastUsed : Timestamp
, refresh : Maybe String
, value : String
}
{-| The Context type stores all the information in the Vault. This data type is
static and hence can be passed on easily.
-}
type alias Context =
{ accessToken : Maybe String
{ accessTokens : Hashdict AccessToken
, baseUrl : Maybe String
, deviceId : Maybe String
, now : Maybe Timestamp
, password : Maybe String
, refreshToken : Maybe String
, serverName : String
@ -80,6 +109,7 @@ type APIContext ph
{ accessToken : String
, baseUrl : String
, context : Context
, now : Timestamp
, transaction : String
, versions : Versions
}
@ -94,9 +124,11 @@ type alias Versions =
apiFormat : Context -> APIContext {}
apiFormat context =
APIContext
{ accessToken = context.accessToken |> Maybe.withDefault L.accessToken
{ accessToken =
mostPopularToken context |> Maybe.withDefault L.accessToken
, baseUrl = context.baseUrl |> Maybe.withDefault L.baseUrl
, context = context
, now = context.now |> Maybe.withDefault (Time.millisToPosix 0)
, transaction = context.transaction |> Maybe.withDefault L.transaction
, versions = context.versions |> Maybe.withDefault L.versions
}
@ -114,16 +146,16 @@ fromApiFormat (APIContext c) =
-}
coder : Json.Coder Context
coder =
Json.object8
Json.object10
{ name = Text.docs.context.name
, description = Text.docs.context.description
, init = Context
}
(Json.field.optional.value
{ fieldName = "accessToken"
, toField = .accessToken
(Json.field.required
{ fieldName = "accessTokens"
, toField = .accessTokens
, description = Text.fields.context.accessToken
, coder = Json.string
, coder = Hashdict.coder .value coderAccessToken
}
)
(Json.field.optional.value
@ -133,6 +165,20 @@ coder =
, coder = Json.string
}
)
(Json.field.optional.value
{ fieldName = "deviceId"
, toField = .deviceId
, description = Debug.todo "Needs docs"
, coder = Json.string
}
)
(Json.field.optional.value
{ fieldName = "now"
, toField = .now
, description = Debug.todo "Needs docs"
, coder = Timestamp.coder
}
)
(Json.field.optional.value
{ fieldName = "password"
, toField = .password
@ -177,6 +223,52 @@ coder =
)
{-| JSON coder for an Access Token.
-}
coderAccessToken : Json.Coder AccessToken
coderAccessToken =
Json.object5
{ name = Debug.todo "Needs docs"
, description = Debug.todo "Needs docs"
, init = AccessToken
}
(Json.field.required
{ fieldName = "created"
, toField = .created
, description = Debug.todo "Needs docs"
, coder = Timestamp.coder
}
)
(Json.field.optional.value
{ fieldName = "expiryMs"
, toField = .expiryMs
, description = Debug.todo "Needs docs"
, coder = Json.int
}
)
(Json.field.required
{ fieldName = "lastUsed"
, toField = .lastUsed
, description = Debug.todo "Needs docs"
, coder = Timestamp.coder
}
)
(Json.field.optional.value
{ fieldName = "refresh"
, toField = .refresh
, description = Debug.todo "Needs docs"
, coder = Json.string
}
)
(Json.field.required
{ fieldName = "value"
, toField = .value
, description = Debug.todo "Needs docs"
, coder = Json.string
}
)
{-| Decode a Context type from a JSON value.
-}
decoder : Json.Decoder Context
@ -195,8 +287,10 @@ encode =
-}
init : String -> Context
init sn =
{ accessToken = Nothing
{ accessTokens = Hashdict.empty .value
, baseUrl = Nothing
, deviceId = Nothing
, now = Nothing
, refreshToken = Nothing
, password = Nothing
, serverName = sn
@ -206,6 +300,36 @@ init sn =
}
{-| Get the most popular access token available, if any.
-}
mostPopularToken : Context -> Maybe String
mostPopularToken c =
c.accessTokens
|> Hashdict.values
|> List.sortBy
(\token ->
case token.expiryMs of
Nothing ->
( 0, Timestamp.toMs token.created )
Just e ->
( 1
, token.created
|> Timestamp.add e
|> Timestamp.toMs
)
)
|> List.head
|> Maybe.map .value
{-| Reset the phantom type of the Context, effectively forgetting all values.
-}
reset : APIContext a -> APIContext {}
reset (APIContext c) =
APIContext c
{-| Get an inserted access token.
-}
getAccessToken : APIContext { a | accessToken : () } -> String
@ -234,6 +358,20 @@ setBaseUrl value (APIContext c) =
APIContext { c | baseUrl = value }
{-| Get an inserted timestamp.
-}
getNow : APIContext { a | now : () } -> Timestamp
getNow (APIContext c) =
c.now
{-| Insert a Timestamp into the APIContext.
-}
setNow : Timestamp -> APIContext a -> APIContext { a | now : () }
setNow t (APIContext c) =
APIContext { c | now = t }
{-| Get an inserted transaction id.
-}
getTransaction : APIContext { a | transaction : () } -> String

View File

@ -51,8 +51,10 @@ settings that can be adjusted manually.
import Internal.Api.Request as Request
import Internal.Config.Log exposing (Log)
import Internal.Config.Text as Text
import Internal.Tools.Hashdict as Hashdict
import Internal.Tools.Json as Json
import Internal.Values.Context as Context exposing (Context, Versions)
import Internal.Tools.Timestamp exposing (Timestamp)
import Internal.Values.Context as Context exposing (AccessToken, Context, Versions)
import Internal.Values.Settings as Settings
@ -74,8 +76,12 @@ type EnvelopeUpdate a
= ContentUpdate a
| HttpRequest (Request.Request ( Request.Error, List Log ) ( EnvelopeUpdate a, List Log ))
| More (List (EnvelopeUpdate a))
| SetAccessToken String
| Optional (Maybe (EnvelopeUpdate a))
| RemoveAccessToken String
| SetAccessToken AccessToken
| SetBaseUrl String
| SetDeviceId String
| SetNow Timestamp
| SetRefreshToken String
| SetVersions Versions
@ -179,10 +185,10 @@ getContent =
{-| Create a new enveloped data type. All settings are set to default values
from the [Internal.Config.Default](Internal-Config-Default) module.
-}
init : a -> Envelope a
init x =
{ content = x
, context = Context.init
init : { serverName : String, content : a } -> Envelope a
init data =
{ content = data.content
, context = Context.init data.serverName
, settings = Settings.init
}
@ -296,12 +302,27 @@ update updateContent eu ({ context } as data) =
More items ->
List.foldl (update updateContent) data items
Optional (Just u) ->
update updateContent u data
Optional Nothing ->
data
RemoveAccessToken token ->
{ data | context = { context | accessTokens = Hashdict.removeKey token context.accessTokens } }
SetAccessToken a ->
{ data | context = { context | accessToken = Just a } }
{ data | context = { context | accessTokens = Hashdict.insert a context.accessTokens } }
SetBaseUrl b ->
{ data | context = { context | baseUrl = Just b } }
SetDeviceId d ->
{ data | context = { context | deviceId = Just d } }
SetNow n ->
{ data | context = { context | now = Just n } }
SetRefreshToken r ->
{ data | context = { context | refreshToken = Just r } }

View File

@ -39,6 +39,7 @@ import Internal.Config.Text as Text
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)
{-| This is the Vault type.
@ -46,6 +47,7 @@ import Internal.Values.Room as Room exposing (Room)
type alias Vault =
{ accountData : Dict String Json.Value
, rooms : Hashdict Room
, user : User
}
@ -57,11 +59,12 @@ type VaultUpdate
| MapRoom String Room.RoomUpdate
| More (List VaultUpdate)
| SetAccountData String Json.Value
| SetUser User
coder : Json.Coder Vault
coder =
Json.object2
Json.object3
{ name = Text.docs.vault.name
, description = Text.docs.vault.description
, init = Vault
@ -80,6 +83,13 @@ coder =
, coder = Hashdict.coder .roomId Room.coder
}
)
(Json.field.required
{ fieldName = "user"
, toField = .user
, description = Debug.todo "Needs description"
, coder = User.coder
}
)
{-| Get a given room by its room id.
@ -136,3 +146,6 @@ update vu vault =
SetAccountData key value ->
setAccountData key value vault
SetUser user ->
{ vault | user = user }

View File

@ -16,6 +16,7 @@ safely access all exposed data types without risking to create circular imports.
-}
import Internal.Api.Main as Api
import Internal.Values.Envelope as Envelope
import Internal.Values.Event as Event
import Internal.Values.Room as Room
@ -50,4 +51,4 @@ type Vault
{-| Opaque type for Matrix VaultUpdate
-}
type VaultUpdate
= VaultUpdate (Envelope.EnvelopeUpdate Vault.VaultUpdate)
= VaultUpdate Api.Msg