Compare commits

...

8 Commits

13 changed files with 870 additions and 97 deletions

View File

@ -1,4 +1,7 @@
module Internal.Api.Api exposing (..)
module Internal.Api.Api exposing
( TaskChain, request
, VersionControl, startWithVersion, startWithUnstableFeature, forVersion, sameForVersion, forUnstableFeature, versionChain
)
{-|
@ -7,35 +10,55 @@ module Internal.Api.Api exposing (..)
The API module is a front-end for implementing API endpoints according to spec.
This module is imported by various API endpoint implementations to keep the
implementation simple and understandable.
@docs TaskChain, request
## Spec versions
To respect spec versions, there is often a variety of ways to communicate with
the homeserver. For this reason, users can differentiate spec versions using
these functions.
@docs VersionControl, startWithVersion, startWithUnstableFeature, forVersion, sameForVersion, forUnstableFeature, versionChain
-}
import Internal.Api.Chain as C
import Internal.Api.Request as R
import Internal.Config.Log exposing (log)
import Internal.Config.Log exposing (Log, log)
import Internal.Tools.Json as Json
import Internal.Values.Context exposing (APIContext)
import Internal.Values.Context as Context exposing (APIContext, Versions)
import Internal.Values.Envelope as E
import Internal.Values.Vault as V
import Recursion
import Set
{-| A TaskChain helps create a chain of HTTP requests.
-}
type alias TaskChain ph1 ph2 =
C.TaskChain R.Error V.VaultUpdate { ph1 | baseUrl : () } { ph2 | baseUrl : () }
C.TaskChain R.Error (E.EnvelopeUpdate V.VaultUpdate) { ph1 | baseUrl : () } { ph2 | baseUrl : () }
{-| Make an HTTP request that adheres to the Matrix spec rules.
-}
request :
{ attributes : List (R.Attribute { ph1 | baseUrl : () })
, coder : Json.Coder V.VaultUpdate
, contextChange : V.VaultUpdate -> (APIContext { ph1 | baseUrl : () } -> APIContext { ph2 | baseUrl : () })
, coder : Json.Coder returnValue
, contextChange : returnValue -> (APIContext { ph1 | baseUrl : () } -> APIContext { ph2 | baseUrl : () })
, method : String
, path : List String
, toUpdate : returnValue -> ( E.EnvelopeUpdate V.VaultUpdate, List Log )
}
-> TaskChain ph1 ph2
request data =
R.toChain
{ logHttp =
\r ->
( V.HttpRequest r
( E.HttpRequest r
, String.concat
-- TODO: Move this to Internal.Config.Text module
[ "Matrix HTTP: "
@ -54,4 +77,120 @@ request data =
}
|> R.withAttributes data.attributes
, toContextChange = data.contextChange
, toUpdate = data.toUpdate
}
{-| This type allows different definitions for different spec versions,
allowing the Elm SDK to communicate differently to the server depending on
how up-to-date the server is.
-}
type VersionControl a ph1 ph2
= VC
{ name : VersionType
, chain : a -> TaskChain (WithV ph1) (WithV ph2)
, prev : Maybe (VersionControl a ph1 ph2)
}
type VersionType
= SpecVersion String
| UnstableFeature String
type alias WithV ph =
{ ph | versions : () }
{-| Start with a given spec version supporting a given API endpoint.
-}
startWithVersion : String -> (a -> TaskChain (WithV ph1) (WithV ph2)) -> VersionControl a ph1 ph2
startWithVersion name chain =
VC
{ name = SpecVersion name
, chain = chain
, prev = Nothing
}
{-| Start with a given unstable feature supporting a given API endpoint.
-}
startWithUnstableFeature : String -> (a -> TaskChain (WithV ph1) (WithV ph2)) -> VersionControl a ph1 ph2
startWithUnstableFeature name chain =
VC
{ name = UnstableFeature name
, chain = chain
, prev = Nothing
}
{-| Add a new unstable feature that supports a different version of the API endpoint.
-}
forUnstableFeature : String -> (a -> TaskChain (WithV ph1) (WithV ph2)) -> VersionControl a ph1 ph2 -> VersionControl a ph1 ph2
forUnstableFeature name chain prev =
VC
{ name = UnstableFeature name
, chain = chain
, prev = Just prev
}
{-| Add a new spec version that supports a different version of the API endpoint.
-}
forVersion : String -> (a -> TaskChain (WithV ph1) (WithV ph2)) -> VersionControl a ph1 ph2 -> VersionControl a ph1 ph2
forVersion name chain prev =
VC
{ name = SpecVersion name
, chain = chain
, prev = Just prev
}
{-| Add another spec version that has the API endpoint defined the same as the previous API endpoint.
-}
sameForVersion : String -> VersionControl a ph1 ph2 -> VersionControl a ph1 ph2
sameForVersion name (VC data) =
VC
{ name = SpecVersion name
, chain = data.chain
, prev = Just (VC data)
}
supportedVersion : Versions -> VersionType -> Bool
supportedVersion { versions, unstableFeatures } name =
case name of
SpecVersion n ->
List.member n versions
UnstableFeature n ->
Set.member n unstableFeatures
-- NOTE: Interesting detail! For some reason, I cannot add the `context`
-- NOTE: variable to the top line of the defined input values!
-- NOTE: Maybe this is a bug?
{-| Once you are done, turn a VersionControl type into a Task Chain.
-}
versionChain : VersionControl a ph1 ph2 -> a -> TaskChain (WithV ph1) (WithV ph2)
versionChain vc input =
\context ->
case Context.getVersions context of
versions ->
Recursion.runRecursion
(\mvc ->
case mvc of
Nothing ->
Recursion.base (C.fail R.NoSupportedVersion context)
Just (VC data) ->
if supportedVersion versions data.name then
Recursion.base (data.chain input context)
else
Recursion.recurse data.prev
)
(Just vc)

View File

@ -1,6 +1,7 @@
module Internal.Api.Chain exposing
( TaskChain, CompleteChain
, IdemChain, toTask
, fail, succeed, andThen
)
{-|
@ -23,10 +24,15 @@ avoid leaking values passing through the API in unexpected ways.
@docs IdemChain, toTask
## Operations
@docs fail, succeed, andThen
-}
import Internal.Config.Log exposing (Log)
import Internal.Values.Context as Context exposing (APIContext)
import Internal.Values.Context exposing (APIContext)
import Task

View File

@ -0,0 +1,246 @@
module Internal.Api.GetEvent.Api exposing (GetEventInput, getEvent)
{-|
# Get event
Get a single event based on `roomId/eventId`. You must have permission to
retrieve this event e.g. by being a member in the room for this event.
@docs GetEventInput, getEvent
-}
import Internal.Api.Api as A
import Internal.Api.Request as R
import Internal.Config.Log exposing (log)
import Internal.Tools.Json as Json
import Internal.Tools.Timestamp as Timestamp
import Internal.Values.Envelope as E
import Internal.Values.Event as Event exposing (Event)
import Internal.Values.Room as Room
import Internal.Values.User as User
import Internal.Values.Vault as V
{-| Input for getting an event.
-}
type alias GetEventInput =
{ eventId : String, roomId : String }
{-| Standard input for version 1 of the GetEvent API endpoint.
-}
type alias GetEventInputV1 a =
{ a | eventId : String, roomId : String }
{-| Universal phantom type encompassing all versions of this API endpoint.
-}
type alias Phantom a =
PhantomV1 { a | versions : () }
{-| Phantom values necessary for version 1 of the GetEvent API endpoint.
-}
type alias PhantomV1 a =
{ a | accessToken : (), baseUrl : () }
{-| Get an event based on a room id and event id.
-}
getEvent : GetEventInput -> A.TaskChain (Phantom a) (Phantom a)
getEvent =
A.startWithVersion "r0.5.0" getEventV1
|> A.sameForVersion "r0.6.0"
|> A.sameForVersion "r0.6.1"
|> A.forVersion "v1.1" getEventV2
|> 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.versionChain
{-| Version 1 of the GetEvent API endpoint
-}
getEventV1 : GetEventInputV1 input -> A.TaskChain (PhantomV1 a) (PhantomV1 a)
getEventV1 { eventId, roomId } =
A.request
{ attributes =
[ R.accessToken
, R.onStatusCode 404 "M_NOT_FOUND"
]
, coder = getEventCoderV1
, contextChange = always identity
, method = "GET"
, path = [ "_matrix", "client", "r0", "rooms", roomId, "event", eventId ]
, toUpdate =
\event ->
( E.ContentUpdate <| V.MapRoom roomId (Room.AddEvent event)
, event.eventId
|> (++) "Received event id "
|> log.debug
|> List.singleton
)
}
{-| Version 2 of the GetEvent API endpoint
-}
getEventV2 : GetEventInputV1 input -> A.TaskChain (PhantomV1 a) (PhantomV1 a)
getEventV2 { eventId, roomId } =
A.request
{ attributes =
[ R.accessToken
, R.onStatusCode 404 "M_NOT_FOUND"
]
, coder = getEventCoderV1
, contextChange = always identity
, method = "GET"
, path = [ "_matrix", "client", "v3", "rooms", roomId, "event", eventId ]
, toUpdate =
\event ->
( E.ContentUpdate <| V.MapRoom roomId (Room.AddEvent event)
, event.eventId
|> (++) "Received event id "
|> log.debug
|> List.singleton
)
}
getEventCoderV1 : Json.Coder Event
getEventCoderV1 =
Json.object8
{ name = "ClientEvent"
, description =
[ "ClientEvent as described by the Matrix spec"
, "https://spec.matrix.org/v1.10/client-server-api/#get_matrixclientv3roomsroomideventeventid"
]
, init = Event
}
(Json.field.required
{ fieldName = "content"
, toField = .content
, description =
[ "The body of this event, as created by the client which sent it."
]
, coder = Json.value
}
)
(Json.field.required
{ fieldName = "event_id"
, toField = .eventId
, description =
[ "The globally unique identifier for this event."
]
, coder = Json.string
}
)
(Json.field.required
{ fieldName = "origin_server_ts"
, toField = .originServerTs
, description =
[ "Timestamp (in milliseconds since the unix epoch) on originating homeserver when this event was sent."
]
, coder = Timestamp.coder
}
)
(Json.field.required
{ fieldName = "room_id"
, toField = .roomId
, description =
[ "The ID of the room associated with this event."
]
, coder = Json.string
}
)
(Json.field.required
{ fieldName = "sender"
, toField = .sender
, description =
[ "Contains the fully-qualified ID of the user who sent this event."
]
, coder = User.coder
}
)
(Json.field.optional.value
{ fieldName = "state_key"
, toField = .stateKey
, description =
[ "Present if, and only if, this event is a state event. The key making this piece of state unique in the room. Note that it is often an empty string."
, "State keys starting with an @ are reserved for referencing user IDs, such as room members. With the exception of a few events, state events set with a given users ID as the state key MUST only be set by that user."
]
, coder = Json.string
}
)
(Json.field.required
{ fieldName = "type"
, toField = .eventType
, description =
[ "The type of the event."
]
, coder = Json.string
}
)
(Json.field.optional.value
{ fieldName = "unsigned"
, toField = .unsigned
, description =
[ "Contains optional extra information about the event."
]
, coder =
Json.object4
{ name = "UnsignedData"
, description =
[ "UnsignedData as described by the Matrix spec"
, "https://spec.matrix.org/v1.10/client-server-api/#get_matrixclientv3roomsroomideventeventid"
]
, init = \a b c d -> Event.UnsignedData { age = a, prevContent = b, redactedBecause = c, transactionId = d }
}
(Json.field.optional.value
{ fieldName = "age"
, toField = \(Event.UnsignedData data) -> data.age
, description =
[ "The time in milliseconds that has elapsed since the event was sent. This field is generated by the local homeserver, and may be incorrect if the local time on at least one of the two servers is out of sync, which can cause the age to either be negative or greater than it actually is."
]
, coder = Json.int
}
)
(Json.field.optional.value
{ fieldName = "prev_content"
, toField = \(Event.UnsignedData data) -> data.prevContent
, description =
[ " The previous content for this event. This field is generated by the local homeserver, and is only returned if the event is a state event, and the client has permission to see the previous content."
, "Changed in v1.2: Previously, this field was specified at the top level of returned events rather than in unsigned (with the exception of the GET .../notifications endpoint), though in practice no known server implementations honoured this."
]
, coder = Json.value
}
)
(Json.field.optional.value
{ fieldName = "redacted_because"
, toField = \(Event.UnsignedData data) -> data.redactedBecause
, description =
[ "The event that redacted this event, if any."
]
, coder = Json.lazy (\() -> getEventCoderV1)
}
)
(Json.field.optional.value
{ fieldName = "transaction_id"
, toField = \(Event.UnsignedData data) -> data.transactionId
, description =
[ "The client-supplied transaction ID, for example, provided via PUT /_matrix/client/v3/rooms/{roomId}/send/{eventType}/{txnId}, if the client being given the event is the same one which sent it."
]
, coder = Json.string
}
)
}
)

View File

@ -0,0 +1,138 @@
module Internal.Api.Invite.Api exposing (InviteInput, Phantom, invite)
{-|
# Invite
This API invites a user to participate in a particular room. They do not start
participating in the room until they actually join the room.
Only users currently in a particular room can invite other users to join that
room.
If the user was invited to the room, the homeserver will append a m.room.member
event to the room.
@docs InviteInput, Phantom, invite
-}
import Internal.Api.Api as A
import Internal.Api.Request as R
import Internal.Config.Log exposing (log)
import Internal.Tools.Json as Json
import Internal.Values.Envelope as E
import Internal.Values.Room as Room
import Internal.Values.User as User exposing (User)
import Internal.Values.Vault as V
{-| Invite a user to a room.
-}
invite : InviteInput -> A.TaskChain (Phantom ph1) (Phantom ph1)
invite =
A.startWithVersion "r0.0.0" inviteV1
|> 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.sameForVersion "r0.6.1"
|> A.forVersion "v1.1" inviteV2
|> 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.versionChain
{-| Context needed for inviting a user.
-}
type alias Phantom a =
{ a | accessToken : (), versions : () }
type alias PhantomV1 a =
{ a | accessToken : () }
{-| Input for inviting a user.
-}
type alias InviteInput =
{ reason : Maybe String, roomId : String, user : User }
type alias InviteInputV1 a =
{ a | roomId : String, user : User }
type alias InviteInputV2 a =
{ a | roomId : String, user : User, reason : Maybe String }
inviteV1 : InviteInputV1 a -> A.TaskChain (PhantomV1 ph1) (PhantomV1 ph1)
inviteV1 { roomId, user } =
A.request
{ attributes =
[ R.accessToken
, R.bodyString "user_id" (User.toString user)
, R.onStatusCode 400 "M_UNKNOWN"
, R.onStatusCode 403 "M_FORBIDDEN"
, R.onStatusCode 429 "M_LIMIT_EXCEEDED"
]
, coder = Json.value
, contextChange = always identity
, method = "POST"
, path = [ "_matrix", "client", "r0", "rooms", roomId, "invite" ]
, toUpdate =
always
( E.ContentUpdate <| V.MapRoom roomId (Room.Invite user)
, String.concat
-- TODO: Move to Internal.Config.Text
[ "Invited user "
, User.toString user
, " to room "
, roomId
]
|> log.debug
|> List.singleton
)
}
inviteV2 : InviteInputV2 a -> A.TaskChain (PhantomV1 ph1) (PhantomV1 ph1)
inviteV2 { reason, roomId, user } =
A.request
{ attributes =
[ R.bodyOpString "reason" reason
, R.bodyString "user_id" (User.toString user)
, R.onStatusCode 400 "M_UNKNOWN"
, R.onStatusCode 403 "M_FORBIDDEN"
, R.onStatusCode 429 "M_LIMIT_EXCEEDED"
]
, coder = Json.value
, contextChange = always identity
, method = "POST"
, path = [ "_matrix", "client", "v3", "rooms", roomId, "invite" ]
, toUpdate =
always
( E.ContentUpdate <| V.MapRoom roomId (Room.Invite user)
, String.concat
-- TODO: Move to Internal.Config.Text
[ "Invited user "
, User.toString user
, " to room "
, roomId
]
|> log.debug
|> List.singleton
)
}

View File

@ -1,7 +1,7 @@
module Internal.Api.Request exposing
( ApiCall, ApiPlan, Attribute, callAPI, withAttributes, toChain
, Request, Error(..)
, accessToken, withTransactionId, timeout
, accessToken, withTransactionId, 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
@docs accessToken, withTransactionId, timeout, onStatusCode
### Body
@ -98,7 +98,9 @@ type ContextAttr
-}
type Error
= InternetException Http.Error
| NoSupportedVersion
| ServerReturnsBadJSON String
| ServerReturnsError String Json.Value
{-| Ordinary shape of an HTTP request.
@ -396,43 +398,28 @@ getUrl { attributes, baseUrl, path } =
(getQueryParams attributes)
{-| Resolve the response of a Matrix API call.
{-| When the HTTP request cannot be deciphered but the status code is known,
return with a given default error.
-}
rawApiCallResolver : Json.Coder a -> Dict.Dict Int ( Error, List Log ) -> Http.Resolver ( Error, List Log ) ( a, List Log )
rawApiCallResolver coder statusCodeErrors =
Http.stringResolver
(\response ->
case response of
Http.BadUrl_ s ->
Http.BadUrl s
|> InternetException
|> Tuple.pair
|> (|>) []
|> Err
Http.Timeout_ ->
Http.Timeout
|> InternetException
|> Tuple.pair
|> (|>) []
|> Err
Http.NetworkError_ ->
Http.NetworkError
|> InternetException
|> Tuple.pair
|> (|>) []
|> Err
Http.BadStatus_ metadata body ->
statusCodeErrors
|> Dict.get metadata.statusCode
|> decodeServerResponse (Json.decode coder) body
Http.GoodStatus_ metadata body ->
statusCodeErrors
|> Dict.get metadata.statusCode
|> decodeServerResponse (Json.decode coder) body
onStatusCode : Int -> String -> Attribute a
onStatusCode code err _ =
StatusCodeResponse code
( err
|> E.string
|> Tuple.pair "errcode"
|> List.singleton
|> E.object
|> ServerReturnsError err
, String.concat
-- TODO: Move to Internal.Config.Text
[ "Received an invalid HTTP response from Matrix server "
, "but managed to decode it using the status code "
, String.fromInt code
, ": Default to errcode "
, err
]
|> log.warn
|> List.singleton
)
@ -500,6 +487,48 @@ queryString key value _ =
QueryParam <| UrlBuilder.string key value
{-| Resolve the response of a Matrix API call.
-}
rawApiCallResolver : D.Decoder ( a, List Log ) -> Dict.Dict Int ( Error, List Log ) -> Http.Resolver ( Error, List Log ) ( a, List Log )
rawApiCallResolver decoder statusCodeErrors =
Http.stringResolver
(\response ->
case response of
Http.BadUrl_ s ->
Http.BadUrl s
|> InternetException
|> Tuple.pair
|> (|>) []
|> Err
Http.Timeout_ ->
Http.Timeout
|> InternetException
|> Tuple.pair
|> (|>) []
|> Err
Http.NetworkError_ ->
Http.NetworkError
|> InternetException
|> Tuple.pair
|> (|>) []
|> Err
Http.BadStatus_ metadata body ->
statusCodeErrors
|> Dict.get metadata.statusCode
|> decodeServerResponse decoder body
Http.GoodStatus_ metadata body ->
statusCodeErrors
|> Dict.get metadata.statusCode
|> decodeServerResponse decoder body
)
{-| Configure the HTTP request to time out after a given expiry time.
-}
timeout : Float -> Attribute a
timeout f _ =
Timeout f
@ -509,34 +538,57 @@ timeout f _ =
-}
toChain :
{ logHttp : Request ( Error, List Log ) ( update, List Log ) -> ( update, List Log )
, coder : Json.Coder update
, coder : Json.Coder httpOut
, request : ApiPlan ph1
, toContextChange : update -> (APIContext ph1 -> APIContext ph2)
, toContextChange : httpOut -> (APIContext ph1 -> APIContext ph2)
, toUpdate : httpOut -> ( update, List Log )
}
-> C.TaskChain Error update ph1 ph2
toChain data apiContext =
data.request apiContext
|> (\call ->
let
r : Request ( Error, List Log ) ( update, List Log )
r : Request ( Error, List Log ) ( httpOut, List Log )
r =
{ method = call.method
, headers = getHeaders call.attributes
, url = getUrl call
, body = Http.jsonBody (getBody call.attributes)
, resolver = rawApiCallResolver data.coder (getStatusCodes call.attributes)
, resolver = rawApiCallResolver (Json.decode data.coder) (getStatusCodes call.attributes)
, timeout = getTimeout call.attributes
}
logR : Request ( Error, List Log ) ( update, List Log )
logR =
{ method = call.method
, headers = getHeaders call.attributes
, url = getUrl call
, body = Http.jsonBody (getBody call.attributes)
, resolver =
rawApiCallResolver
(Json.decode data.coder
|> D.map
(\( out, logs ) ->
case data.toUpdate out of
( u, uLogs ) ->
( u, List.append logs uLogs )
)
)
(getStatusCodes call.attributes)
, timeout = getTimeout call.attributes
}
in
case data.logHttp r of
case data.logHttp logR of
( httpU, httpLogs ) ->
Http.task r
|> Task.map
(\( u, logs ) ->
{ contextChange = data.toContextChange u
, logs = List.append httpLogs logs
, messages = [ httpU, u ]
}
(\( httpO, logs ) ->
case data.toUpdate httpO of
( u, uLogs ) ->
{ contextChange = data.toContextChange httpO
, logs = List.concat [ httpLogs, logs, uLogs ]
, messages = [ httpU, u ]
}
)
|> Task.mapError
(\( err, logs ) ->

View File

@ -1,4 +1,23 @@
module Internal.Api.Task exposing (..)
module Internal.Api.Task exposing (Task, run)
{-|
# Task module
This module is used to define how API calls are made. These completed API tasks
can be directly converted to Cmd types that the end user of the SDK can access.
These tasks do not affect the `Vault` directly, but instead, return a
`VaultUpdate` type that the user can apply to keep their `Vault` type
up-to-date.
## Use
@docs Task, run
-}
import Internal.Api.Chain as C
import Internal.Api.Request as Request

View File

@ -0,0 +1,91 @@
module Internal.Api.Versions.Api exposing (versions, Phantom)
{-|
# Versions
Ask the Matrix API which versions it supports.
@docs versions, Phantom
-}
import Dict
import Internal.Api.Api as A
import Internal.Tools.Json as Json
import Internal.Values.Context as Context exposing (Versions)
import Internal.Values.Envelope as E
import Set
{-| Task chain to ask which spec versions the Matrix API supports.
-}
versions : A.TaskChain (Phantom ph) (Phantom { ph | versions : () })
versions =
A.request
{ attributes = []
, coder = versionsCoder
, contextChange = Context.setVersions
, method = "GET"
, path = [ "_matrix", "client", "versions" ]
, toUpdate = \v -> ( E.SetVersions v, [] )
}
{-| Context needed for asking the server's available spec versions
-}
type alias Phantom a =
{ a | baseUrl : () }
versionsCoder : Json.Coder Versions
versionsCoder =
Json.object2
{ name = "Versions"
, description =
[ "Gets the versions of the specification supported by the server."
, "Values will take the form vX.Y or rX.Y.Z in historical cases. See the Specification Versioning for more information."
, "The server may additionally advertise experimental features it supports through unstable_features. These features should be namespaced and may optionally include version information within their name if desired. Features listed here are not for optionally toggling parts of the Matrix specification and should only be used to advertise support for a feature which has not yet landed in the spec. For example, a feature currently undergoing the proposal process may appear here and eventually be taken off this list once the feature lands in the spec and the server deems it reasonable to do so. Servers can choose to enable some features only for some users, so clients should include authentication in the request to get all the features available for the logged-in user. If no authentication is provided, the server should only return the features available to all users. Servers may wish to keep advertising features here after theyve been released into the spec to give clients a chance to upgrade appropriately. Additionally, clients should avoid using unstable features in their stable releases."
]
, init = Versions
}
(Json.field.required
{ fieldName = "versions"
, toField = .versions
, description =
[ "The supported versions."
]
, coder = Json.list Json.string
}
)
(Json.field.optional.withDefault
{ fieldName = "unstable_features"
, toField = .unstableFeatures
, description =
[ "Experimental features the server supports. Features not listed here, or the lack of this property all together, indicate that a feature is not supported."
]
, coder =
Json.bool
|> Json.slowDict
|> Json.map
{ name = "Dict to set"
, description =
[ "Turn a dictionary of supported values into a set that contains only supported values"
]
, back = Set.foldl (\k d -> Dict.insert k True d) Dict.empty
, forth =
Dict.foldl
(\k v s ->
if v then
Set.insert k s
else
s
)
Set.empty
}
, default = ( Set.empty, [] )
, defaultToString = always "{}"
}
)

View File

@ -1,5 +1,5 @@
module Internal.Config.Leaks exposing
( accessToken, baseUrl, transaction, versions
( accessToken, baseUrl, field, transaction, versions
, allLeaks
)
@ -30,7 +30,7 @@ know 100% sure that the value isn't `Nothing`.
Just 5 |> Maybe.withDefault Leaks.number
@docs accessToken, baseUrl, transaction, versions
@docs accessToken, baseUrl, field, transaction, versions
For safety purposes, all leaking values are stored in the following value:
@ -52,14 +52,15 @@ accessToken =
-}
allLeaks : Set String
allLeaks =
Set.union
(Set.fromList versions)
(Set.fromList
[ accessToken
, baseUrl
, transaction
]
)
Set.fromList
[ accessToken
, baseUrl
, field
, transaction
, "elm-sdk-placeholder-versions-leaks" -- Old leaking value
]
|> Set.union (Set.fromList versions.versions)
|> Set.union versions.unstableFeatures
{-| Placeholder base URL.
@ -69,6 +70,13 @@ baseUrl =
"elm-sdk-placeholder-baseurl-leaks.example.org"
{-| Placeholder JSON field.
-}
field : String
field =
"elm-sdk-placeholder-json-field"
{-| Placeholder transaction id.
-}
transaction : String
@ -78,6 +86,8 @@ transaction =
{-| Placeholder versions list.
-}
versions : List String
versions : { versions : List String, unstableFeatures : Set String }
versions =
[ "elm-sdk-placeholder-versions-leaks" ]
{ versions = [ "elm-sdk-placeholder-versions-versions-leaks" ]
, unstableFeatures = Set.singleton "elm-sdk-placeholder-versions-unstableFeatures-leaks"
}

View File

@ -247,9 +247,11 @@ fields :
{ context :
{ accessToken : Desc
, baseUrl : Desc
, experimental : Desc
, password : Desc
, refreshToken : Desc
, username : Desc
, serverName : Desc
, transaction : Desc
, versions : Desc
}
@ -329,6 +331,9 @@ fields =
, baseUrl =
[ "The base URL of the Matrix server."
]
, experimental =
[ "Experimental features supported by the homeserver."
]
, password =
[ "The user's password for authentication purposes."
]
@ -338,6 +343,10 @@ fields =
, username =
[ "The username of the Matrix account."
]
, serverName =
[ "The homeserver that the user is trying to communicate with."
, "This name doesn't need to be the address. For example, the name might be `matrix.org` even though the homeserver is at a different location."
]
, transaction =
[ "A unique identifier for a transaction initiated by the user."
]

View File

@ -1,10 +1,10 @@
module Internal.Values.Context exposing
( Context, init, coder, encode, decoder
, APIContext, apiFormat
, APIContext, apiFormat, fromApiFormat
, setAccessToken, getAccessToken
, setBaseUrl, getBaseUrl
, setTransaction, getTransaction
, setVersions, getVersions
, Versions, setVersions, getVersions
)
{-| The Context is the set of variables that the user (mostly) cannot control.
@ -22,7 +22,7 @@ the Matrix API.
Once the API starts needing information, that's when we use the APIContext type
to build the right environment for the API communication to work with.
@docs APIContext, apiFormat
@docs APIContext, apiFormat, fromApiFormat
Once the APIContext is ready, there's helper functions for each piece of
information that can be inserted.
@ -45,13 +45,15 @@ information that can be inserted.
### Versions
@docs setVersions, getVersions
@docs Versions, setVersions, getVersions
-}
import Internal.Config.Leaks as L
import Internal.Config.Text as Text
import Internal.Tools.Json as Json
import Json.Encode as E
import Set exposing (Set)
{-| The Context type stores all the information in the Vault. This data type is
@ -62,9 +64,10 @@ type alias Context =
, baseUrl : Maybe String
, password : Maybe String
, refreshToken : Maybe String
, username : Maybe String
, serverName : String
, transaction : Maybe String
, versions : Maybe (List String)
, username : Maybe String
, versions : Maybe Versions
}
@ -78,10 +81,14 @@ type APIContext ph
, baseUrl : String
, context : Context
, transaction : String
, versions : List String
, versions : Versions
}
type alias Versions =
{ versions : List String, unstableFeatures : Set String }
{-| Create an unformatted APIContext type.
-}
apiFormat : Context -> APIContext {}
@ -95,11 +102,19 @@ apiFormat context =
}
{-| Get the original context that contains all values from before any were
gotten from the Matrix API.
-}
fromApiFormat : APIContext a -> Context
fromApiFormat (APIContext c) =
c.context
{-| Define how a Context can be encoded to and decoded from a JSON object.
-}
coder : Json.Coder Context
coder =
Json.object7
Json.object8
{ name = Text.docs.context.name
, description = Text.docs.context.description
, init = Context
@ -132,10 +147,10 @@ coder =
, coder = Json.string
}
)
(Json.field.optional.value
{ fieldName = "username"
, toField = .username
, description = Text.fields.context.username
(Json.field.required
{ fieldName = "serverName"
, toField = .serverName
, description = Text.fields.context.serverName
, coder = Json.string
}
)
@ -146,11 +161,18 @@ coder =
, coder = Json.string
}
)
(Json.field.optional.value
{ fieldName = "username"
, toField = .username
, description = Text.fields.context.username
, coder = Json.string
}
)
(Json.field.optional.value
{ fieldName = "versions"
, toField = .versions
, description = Text.fields.context.versions
, coder = Json.list Json.string
, coder = versionsCoder
}
)
@ -171,14 +193,15 @@ encode =
{-| A basic, untouched version of the Context, containing no information.
-}
init : Context
init =
init : String -> Context
init sn =
{ accessToken = Nothing
, baseUrl = Nothing
, refreshToken = Nothing
, password = Nothing
, username = Nothing
, serverName = sn
, transaction = Nothing
, username = Nothing
, versions = Nothing
}
@ -227,13 +250,38 @@ setTransaction value (APIContext c) =
{-| Get an inserted versions list.
-}
getVersions : APIContext { a | versions : () } -> List String
getVersions : APIContext { a | versions : () } -> Versions
getVersions (APIContext c) =
c.versions
{-| Insert a versions list into the APIContext.
-}
setVersions : List String -> APIContext a -> APIContext { a | versions : () }
setVersions : Versions -> APIContext a -> APIContext { a | versions : () }
setVersions value (APIContext c) =
APIContext { c | versions = value }
versionsCoder : Json.Coder Versions
versionsCoder =
Json.object2
{ name = Debug.todo "Add name" -- Text.docs.versions.name
, description = Debug.todo "Add description" -- Text.docs.versions.description
, init = Versions
}
(Json.field.required
{ fieldName = "versions"
, toField = .versions
, description = Debug.todo "Add description"
, coder = Json.list Json.string
}
)
(Json.field.optional.withDefault
{ fieldName = "unstableFeatures"
, toField = .unstableFeatures
, description = Debug.todo "Add description"
, coder = Json.set Json.string
, default = ( Set.empty, [] )
, defaultToString = Json.encode (Json.set Json.string) >> E.encode 0
}
)

View File

@ -48,9 +48,11 @@ 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.Json as Json
import Internal.Values.Context as Context exposing (Context)
import Internal.Values.Context as Context exposing (Context, Versions)
import Internal.Values.Settings as Settings
@ -70,10 +72,12 @@ type alias Envelope a =
-}
type EnvelopeUpdate a
= ContentUpdate a
| HttpRequest (Request.Request ( Request.Error, List Log ) ( EnvelopeUpdate a, List Log ))
| More (List (EnvelopeUpdate a))
| SetAccessToken String
| SetBaseUrl String
| SetRefreshToken String
| SetVersions (List String)
| SetVersions Versions
{-| Settings value from
@ -286,12 +290,18 @@ update updateContent eu ({ context } as data) =
ContentUpdate v ->
{ data | content = updateContent v data.content }
HttpRequest _ ->
data
More items ->
List.foldl (update updateContent) data items
SetAccessToken a ->
{ data | context = { context | accessToken = Just a } }
SetBaseUrl b ->
{ data | context = { context | baseUrl = Just b } }
SetRefreshToken r ->
{ data | context = { context | refreshToken = Just r } }

View File

@ -1,6 +1,6 @@
module Internal.Values.Room exposing
( Room, init
, RoomUpdate, update
, RoomUpdate(..), update
, Batch, addBatch, addSync, addEvents, mostRecentEvents
, getAccountData, setAccountData
, coder, encode, decode
@ -56,6 +56,7 @@ import Internal.Tools.Json as Json
import Internal.Values.Event as Event exposing (Event)
import Internal.Values.StateManager as StateManager exposing (StateManager)
import Internal.Values.Timeline as Timeline exposing (Timeline)
import Internal.Values.User as User exposing (User)
import Json.Encode as E
@ -81,7 +82,9 @@ type alias Room =
from the Matrix API.
-}
type RoomUpdate
= AddSync Batch
= AddEvent Event
| AddSync Batch
| Invite User
| More (List RoomUpdate)
| SetAccountData String Json.Value
@ -245,9 +248,17 @@ setAccountData key value room =
update : RoomUpdate -> Room -> Room
update ru room =
case ru of
AddEvent _ ->
-- TODO: Add event
room
AddSync batch ->
addSync batch room
Invite user ->
-- TODO: Invite user
room
More items ->
List.foldl update room items

View File

@ -54,7 +54,6 @@ based on new information provided by the Matrix API.
-}
type VaultUpdate
= CreateRoomIfNotExists String
| HttpRequest (Request.Request ( Request.Error, List Log ) ( VaultUpdate, List Log ))
| MapRoom String Room.RoomUpdate
| More (List VaultUpdate)
| SetAccountData String Json.Value
@ -129,11 +128,6 @@ update vu vault =
(Maybe.withDefault (Room.init roomId) >> Maybe.Just)
vault
-- The HTTP request currently isn't used anywhere other than for
-- auditing the requests that the Vault is making
HttpRequest _ ->
vault
MapRoom roomId ru ->
mapRoom roomId (Room.update ru) vault