Merge pull request #19 from noordstar/parser

Expose Matrix.User
pull/18/head
BramvdnHeuvel 2024-04-12 14:49:48 +02:00 committed by GitHub
commit 8c73fbf9b1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
18 changed files with 1140 additions and 28 deletions

View File

@ -11,6 +11,8 @@
"Internal.Config.Phantom", "Internal.Config.Phantom",
"Internal.Config.Text", "Internal.Config.Text",
"Internal.Filter.Timeline", "Internal.Filter.Timeline",
"Internal.Grammar.ServerName",
"Internal.Grammar.UserId",
"Internal.Tools.DecodeExtra", "Internal.Tools.DecodeExtra",
"Internal.Tools.EncodeExtra", "Internal.Tools.EncodeExtra",
"Internal.Tools.Hashdict", "Internal.Tools.Hashdict",
@ -25,16 +27,19 @@
"Internal.Values.Settings", "Internal.Values.Settings",
"Internal.Values.StateManager", "Internal.Values.StateManager",
"Internal.Values.Timeline", "Internal.Values.Timeline",
"Internal.Values.User",
"Internal.Values.Vault", "Internal.Values.Vault",
"Matrix", "Matrix",
"Matrix.Event", "Matrix.Event",
"Matrix.Settings", "Matrix.Settings",
"Matrix.User",
"Types" "Types"
], ],
"elm-version": "0.19.0 <= v < 0.20.0", "elm-version": "0.19.0 <= v < 0.20.0",
"dependencies": { "dependencies": {
"elm/core": "1.0.0 <= v < 2.0.0", "elm/core": "1.0.0 <= v < 2.0.0",
"elm/json": "1.0.0 <= v < 2.0.0", "elm/json": "1.0.0 <= v < 2.0.0",
"elm/parser": "1.0.0 <= v < 2.0.0",
"elm/time": "1.0.0 <= v < 2.0.0", "elm/time": "1.0.0 <= v < 2.0.0",
"micahhahn/elm-safe-recursion": "2.0.0 <= v < 3.0.0", "micahhahn/elm-safe-recursion": "2.0.0 <= v < 3.0.0",
"miniBill/elm-fast-dict": "1.0.0 <= v < 2.0.0" "miniBill/elm-fast-dict": "1.0.0 <= v < 2.0.0"

View File

@ -1,5 +1,5 @@
module Internal.Config.Text exposing module Internal.Config.Text exposing
( docs, failures, fields, mappings, logs ( docs, failures, fields, mappings, logs, parses
, accessTokenFoundLocally, accessTokenExpired, accessTokenInvalid , accessTokenFoundLocally, accessTokenExpired, accessTokenInvalid
, versionsFoundLocally, versionsReceived, versionsFailedToDecode , versionsFoundLocally, versionsReceived, versionsFailedToDecode
, unsupportedVersionForEndpoint , unsupportedVersionForEndpoint
@ -27,7 +27,7 @@ You should only do this if you know what you're doing.
## Type documentation ## Type documentation
@docs docs, failures, fields, mappings, logs @docs docs, failures, fields, mappings, logs, parses
## API Authentication ## API Authentication
@ -515,6 +515,28 @@ mappings =
} }
{-| Logs for issues that might be found while parsing strings into meaningful data.
-}
parses :
{ historicalUserId : String -> String
, reservedIPs :
{ ipv6Toipv4 : String
, multicast : String
, futureUse : String
, unspecified : String
}
}
parses =
{ historicalUserId = \name -> "Found a historical username `" ++ name ++ "`."
, reservedIPs =
{ ipv6Toipv4 = "Detected a reserved ip address that is formerly used as an IPv6 to IPv4 relay. It is unlikely that this IP Address is real."
, multicast = "Detected a reserved ip address that is used for multicasting. It is unlikely that this IP Address is real."
, futureUse = "Detected a reserves ip address that is reserved for future use. It is unlikely that this IP Address is real if you're running a recent version of the Elm SDK."
, unspecified = "This is an unspecified ip address. It is unlikely that this IP Address is real and someone might try to break something."
}
}
{-| The Matrix homeserver can specify how it wishes to communicate, and the Elm {-| The Matrix homeserver can specify how it wishes to communicate, and the Elm
SDK aims to communicate accordingly. This may fail in some scenarios, however, SDK aims to communicate accordingly. This may fail in some scenarios, however,
in which case it will throw this error. in which case it will throw this error.

View File

@ -48,6 +48,7 @@ for interacting with the Matrix API.
-} -}
import Internal.Config.Text as Text import Internal.Config.Text as Text
import Internal.Grammar.UserId as U
import Internal.Tools.Json as Json import Internal.Tools.Json as Json
import Json.Decode as D import Json.Decode as D
import Json.Encode as E import Json.Encode as E
@ -57,7 +58,7 @@ import Set exposing (Set)
{-| Placeholder Event type so the real Event doesn't need to be imported. {-| Placeholder Event type so the real Event doesn't need to be imported.
-} -}
type alias Event a = type alias Event a =
{ a | eventType : String, sender : String } { a | eventType : String, sender : U.UserID }
{-| The Timeline Filter filters events out of a timeline, guaranteeing that only {-| The Timeline Filter filters events out of a timeline, guaranteeing that only
@ -246,7 +247,7 @@ match (Filter f) { eventType, sender } =
let let
mentionedSender : Bool mentionedSender : Bool
mentionedSender = mentionedSender =
Set.member sender f.senders Set.member (U.toString sender) f.senders
mentionedType : Bool mentionedType : Bool
mentionedType = mentionedType =

View File

@ -0,0 +1,279 @@
module Internal.Grammar.ServerName exposing
( ServerName, toString, fromString
, serverNameParser
, HostName(..)
)
{-|
# Server name
A homeserver is uniquely identified by its server name. The server name
represents the address at which the homeserver in question can be reached by
other homeservers.
@docs ServerName, toString, fromString
## Parser
@docs serverNameParser
## Debug
@docs HostName
-}
import Internal.Tools.ParserExtra as PE
import Parser as P exposing ((|.), (|=), Parser)
{-| The hostname is the location where the server can be found.
Notice how the Matrix spec specifies that the hostname can either be a DNS name,
an IPv4Address or an IPv6Address. Since the IPv4Address is compatible with the
specification of DNS names, however, and RFC1123 (section 2.1) does not require
a client to distinguish them, we treat IPv4Addresses like DNS names.
-}
type HostName
= DNS String
| IPv6 IPv6Address
{-| The IPv6Address is represented by a list of items BEFORE and AFTER the
double colons (::).
-}
type alias IPv6Address =
{ front : List String, back : List String }
{-| The server name is a combination of a hostname and an optional port.
-}
type alias ServerName =
{ host : HostName, port_ : Maybe Int }
{-| Parser for the DNS name record. The Matrix spec bases its grammar on the
standard for internet host names, as specified by RFC1123, section 2.1, with an
extension IPv6 literals.
[RFC-1123 §2.2]
The syntax of a legal Internet host name was specified in RFC-952
[DNS:4]. One aspect of host name syntax is hereby changed: the
restriction on the first character is relaxed to allow either a
letter or a digit. Host software MUST support this more liberal
syntax.
Host software MUST handle host names of up to 63 characters and
SHOULD handle host names of up to 255 characters.
[RFC-952 §Assumptions-1]
A "name" (Net, Host, Gateway, or Domain name) is a text string up
to 24 characters drawn from the alphabet (A-Z), digits (0-9), minus
sign (-), and period (.). Note that periods are only allowed when
they serve to delimit components of "domain style names". (See
RFC-921, "Domain Name System Implementation Schedule", for
background).
-}
dnsNameParser : Parser String
dnsNameParser =
P.chompIf Char.isAlphaNum
|. P.chompWhile (\c -> Char.isAlphaNum c || c == '-' || c == '.')
|> P.getChompedString
{-| Convert a string to a server name.
-}
fromString : String -> Maybe ServerName
fromString s =
P.run (serverNameParser |. P.end) s
|> (\out ->
case out of
Ok _ ->
out
Err e ->
Debug.log "No parse" e
|> always (Debug.log "original" s)
|> always out
)
|> Result.toMaybe
{-| Parse a Hostname.
-}
hostnameParser : Parser HostName
hostnameParser =
P.oneOf
[ P.succeed IPv6
|. P.symbol "["
|= ipv6Parser
|. P.symbol "]"
, P.succeed DNS
|= dnsNameParser
]
{-| Parse all values to the left of the double colon (::)
-}
ipv6LeftParser : Parser (List String)
ipv6LeftParser =
P.oneOf
[ P.succeed []
|. P.symbol ":"
, P.succeed (|>)
|= PE.times 1 7 (ipv6NumParser |. P.symbol ":")
|= P.oneOf
[ P.succeed (\bottom tail -> tail ++ [ bottom ])
|= ipv6NumParser
, P.succeed identity
]
]
{-| Parse an ordinary IPv6 number
-}
ipv6NumParser : Parser String
ipv6NumParser =
P.chompIf Char.isHexDigit
|> P.getChompedString
|> PE.times 1 4
|> P.map String.concat
{-| Parse an IPv6 Address
-}
ipv6Parser : Parser IPv6Address
ipv6Parser =
ipv6LeftParser
|> P.andThen
(\front ->
if List.length front < 8 then
P.succeed (IPv6Address front)
|= ipv6RightParser (8 - 1 - List.length front)
-- The -1 is because :: implies one or more zeroes
else
P.succeed (IPv6Address front [])
)
{-| Parse all values to the right of the double colon (::)
-}
ipv6RightParser : Int -> Parser (List String)
ipv6RightParser n =
if n > 0 then
P.succeed identity
|. P.symbol ":"
|= P.oneOf
[ P.succeed (::)
|= ipv6NumParser
|= PE.times 0
(n - 1)
(P.succeed identity
|. P.symbol ":"
|= ipv6NumParser
)
, P.succeed []
]
else
P.succeed []
|. P.symbol ":"
{-| Convert an IPv6 address to a readable string format
-}
ipv6ToString : IPv6Address -> String
ipv6ToString { front, back } =
(if List.length front == 8 then
front
else if List.length back == 8 then
back
else
List.concat [ front, [ "" ], back ]
)
|> List.intersperse ":"
|> String.concat
portParser : Parser Int
portParser =
P.chompIf Char.isDigit
|. P.chompWhile Char.isDigit
|> P.getChompedString
|> P.andThen
(\v ->
case String.toInt v of
Just i ->
if 0 <= i && i <= 2 ^ 16 - 1 then
P.succeed i
else
P.problem ("Port out of range: " ++ v)
Nothing ->
P.problem "Not a port number"
)
{-| Parse a server name. Generally used by other identifiers that have a server
name as one of its parts.
-}
serverNameParser : Parser ServerName
serverNameParser =
P.succeed ServerName
|= hostnameParser
|= P.oneOf
[ P.succeed Just
|. P.symbol ":"
|= portParser
, P.succeed Nothing
]
{-| Convert a parsed server name back to a string.
-}
toString : ServerName -> String
toString { host, port_ } =
let
hostString : String
hostString =
case host of
DNS name ->
name
IPv6 { front, back } ->
(if List.length front == 8 then
List.intersperse ":" front
else if List.length back == 8 then
List.intersperse ":" back
else
List.concat
[ List.intersperse ":" front
, [ "::" ]
, List.intersperse ":" back
]
)
|> String.concat
|> (\i -> "[" ++ i ++ "]")
portString : String
portString =
port_
|> Maybe.map String.fromInt
|> Maybe.map ((++) ":")
|> Maybe.withDefault ""
in
hostString ++ portString

View File

@ -0,0 +1,128 @@
module Internal.Grammar.UserId exposing
( UserID, toString, fromString
, userIdParser, isHistorical
)
{-|
# User ids
Users within Matrix are uniquely identified by their Matrix user ID. The user
ID is namespaced to the homeserver which allocated the account and has the form:
@localpart:domain
The localpart of a user ID is an opaque identifier for that user. It MUST NOT
be empty, and MUST contain only the characters a-z, 0-9, ., \_, =, -, /, and +.
The domain of a user ID is the server name of the homeserver which allocated
the account.
The length of a user ID, including the @ sigil and the domain, MUST NOT exceed
255 characters.
The complete grammar for a legal user ID is:
user_id = "@" user_id_localpart ":" server_name
user_id_localpart = 1*user_id_char
user_id_char = DIGIT
/ %x61-7A ; a-z
/ "-" / "." / "=" / "_" / "/" / "+"
Older versions of this specification were more tolerant of the characters
permitted in user ID localparts. There are currently active users whose user
IDs do not conform to the permitted character set, and a number of rooms whose
history includes events with a sender which does not conform. In order to
handle these rooms successfully, clients and servers MUST accept user IDs with
localparts from the expanded character set:
extended_user_id_char = %x21-39 / %x3B-7E ; all ASCII printing chars except :
## User ID
@docs UserID, toString, fromString
## Extra
@docs userIdParser, isHistorical
-}
import Internal.Grammar.ServerName as ServerName exposing (ServerName)
import Internal.Tools.ParserExtra as PE
import Parser as P exposing ((|.), (|=), Parser)
{-| The User ID type defining a user.
-}
type alias UserID =
{ localpart : String, domain : ServerName }
{-| Convert a Matrix User ID back into its uniquely identifying string.
-}
fromString : String -> Maybe UserID
fromString =
P.run (userIdParser |. P.end) >> Result.toMaybe
{-| Return a boolean on whether a Matrix user has a historical user ID.
Since this user ID is not SUPPOSED to be legal but clients are nevertheless
forced to support them due to backwards compatibility, clients may occasionally
attempt to break the rules in an attempt to find undefined behaviour.
As a result, an explicit method to spot historical users is added to the SDK.
-}
isHistorical : UserID -> Bool
isHistorical { localpart } =
String.any
(\c ->
let
i : Int
i =
Char.toCode c
in
not ((0x61 <= i && i <= 0x7A) || Char.isAlpha c)
)
localpart
localpartParser : Parser String
localpartParser =
P.chompIf validHistoricalUsernameChar
|> P.getChompedString
|> PE.times 1 255
|> P.map String.concat
{-| Convert a parsed User ID to a string.
-}
toString : UserID -> String
toString { localpart, domain } =
String.concat [ "@", localpart, ":", ServerName.toString domain ]
{-| Parse a UserID from a string.
-}
userIdParser : Parser UserID
userIdParser =
P.succeed UserID
|. P.symbol "@"
|= localpartParser
|. P.symbol ":"
|= ServerName.serverNameParser
|> PE.maxLength 255
validHistoricalUsernameChar : Char -> Bool
validHistoricalUsernameChar c =
let
i : Int
i =
Char.toCode c
in
(0x21 <= i && i <= 0x39) || (0x3B <= i && i <= 0x7E)

View File

@ -4,7 +4,7 @@ module Internal.Tools.Json exposing
, succeed, fail, andThen, lazy, map , succeed, fail, andThen, lazy, map
, Docs(..), RequiredField(..), toDocs , Docs(..), RequiredField(..), toDocs
, list, listWithOne, slowDict, fastDict, fastIntDict, set, maybe , list, listWithOne, slowDict, fastDict, fastIntDict, set, maybe
, Field, field , Field, field, parser
, object2, object3, object4, object5, object6, object7, object8, object9, object10, object11 , object2, object3, object4, object5, object6, object7, object8, object9, object10, object11
) )
@ -58,7 +58,7 @@ This section creates objects that can be (re)used in the library's JSON
specification. For this, the user needs to construct fields for the object specification. For this, the user needs to construct fields for the object
first. first.
@docs Field, field @docs Field, field, parser
Once all fields are constructed, the user can create JSON objects. Once all fields are constructed, the user can create JSON objects.
@ -74,6 +74,7 @@ import Internal.Tools.DecodeExtra as D
import Internal.Tools.EncodeExtra as E import Internal.Tools.EncodeExtra as E
import Json.Decode as D import Json.Decode as D
import Json.Encode as E import Json.Encode as E
import Parser as P
import Set exposing (Set) import Set exposing (Set)
@ -158,6 +159,7 @@ type Docs
} }
) )
| DocsOptional Docs | DocsOptional Docs
| DocsParser String
| DocsRiskyMap (Descriptive { content : Docs, failure : List String }) | DocsRiskyMap (Descriptive { content : Docs, failure : List String })
| DocsSet Docs | DocsSet Docs
| DocsString | DocsString
@ -1152,6 +1154,27 @@ object11 { name, description, init } fa fb fc fd fe ff fg fh fi fj fk =
} }
{-| Define a parser that converts a string into a custom Elm type.
-}
parser : { name : String, p : P.Parser ( a, List Log ), toString : a -> String } -> Coder a
parser { name, p, toString } =
Coder
{ encoder = toString >> E.string
, decoder =
D.string
|> D.andThen
(\v ->
case P.run p v of
Err _ ->
D.fail ("Failed to parse " ++ name ++ "!")
Ok o ->
D.succeed o
)
, docs = DocsParser name
}
{-| Define a set. {-| Define a set.
-} -}
set : Coder comparable -> Coder (Set comparable) set : Coder comparable -> Coder (Set comparable)

View File

@ -0,0 +1,105 @@
module Internal.Tools.ParserExtra exposing (..)
import Parser as P exposing ((|.), (|=), Parser)
zeroOrMore : Parser a -> Parser (List a)
zeroOrMore parser =
P.loop []
(\tail ->
P.oneOf
[ P.succeed (\head -> P.Loop (head :: tail))
|= parser
, P.succeed (P.Done (List.reverse tail))
]
)
oneOrMore : Parser a -> Parser (List a)
oneOrMore parser =
P.succeed (::)
|= parser
|= zeroOrMore parser
atLeast : Int -> Parser a -> Parser (List a)
atLeast n parser =
P.loop []
(\tail ->
if List.length tail < n then
P.succeed (\head -> P.Loop (head :: tail))
|= parser
else
P.oneOf
[ P.succeed (\head -> P.Loop (head :: tail))
|= parser
, P.succeed (P.Done (List.reverse tail))
]
)
atMost : Int -> Parser a -> Parser (List a)
atMost n parser =
P.loop []
(\tail ->
if List.length tail < n then
P.oneOf
[ P.succeed (\head -> P.Loop (head :: tail))
|= parser
, P.succeed (P.Done (List.reverse tail))
]
else
P.succeed (P.Done (List.reverse tail))
)
times : Int -> Int -> Parser a -> Parser (List a)
times inf sup parser =
let
low : Int
low =
max 0 (min inf sup)
high : Int
high =
max 0 sup
in
P.loop []
(\tail ->
if List.length tail < low then
P.succeed (\head -> P.Loop (head :: tail))
|= parser
else if List.length tail < high then
P.oneOf
[ P.succeed (\head -> P.Loop (head :: tail))
|= parser
, P.succeed (P.Done (List.reverse tail))
]
else
P.succeed (P.Done (List.reverse tail))
)
exactly : Int -> Parser a -> Parser (List a)
exactly n =
times n n
maxLength : Int -> Parser a -> Parser a
maxLength n parser =
P.succeed
(\start value end ->
if abs (end - start) > n then
P.problem "Parsed too much text!"
else
P.succeed value
)
|= P.getOffset
|= parser
|= P.getOffset
|> P.andThen identity

View File

@ -35,6 +35,7 @@ of a room.
import Internal.Config.Text as Text import Internal.Config.Text as Text
import Internal.Tools.Json as Json import Internal.Tools.Json as Json
import Internal.Tools.Timestamp as Timestamp exposing (Timestamp) import Internal.Tools.Timestamp as Timestamp exposing (Timestamp)
import Internal.Values.User as User exposing (User)
import Json.Encode as E import Json.Encode as E
@ -45,7 +46,7 @@ type alias Event =
, eventId : String , eventId : String
, originServerTs : Timestamp , originServerTs : Timestamp
, roomId : String , roomId : String
, sender : String , sender : User
, stateKey : Maybe String , stateKey : Maybe String
, eventType : String , eventType : String
, unsigned : Maybe UnsignedData , unsigned : Maybe UnsignedData
@ -112,7 +113,7 @@ coder =
{ fieldName = "sender" { fieldName = "sender"
, toField = .sender , toField = .sender
, description = Text.fields.event.sender , description = Text.fields.event.sender
, coder = Json.string , coder = User.coder
} }
) )
(Json.field.optional.value (Json.field.optional.value

View File

@ -0,0 +1,106 @@
module Internal.Values.User exposing
( User, toString, fromString
, localpart, domain
, coder
)
{-| The Matrix user is uniquely identified by their identifier. This User type
helps identify and safely handle these strings to transform them into meaningful
data types.
## User
@docs User, toString, fromString
## Divide
Matrix users are identified by their unique ID. In the Matrix API, this is a
string that looks as follows:
@alice:example.org
\---/ \---------/
| |
| |
localpart domain
Since the username is safely parsed, one can get these parts of the username.
@docs localpart, domain
## JSON
@docs coder
-}
import Internal.Config.Log as Log exposing (log)
import Internal.Grammar.ServerName as ServerName
import Internal.Grammar.UserId as UserId
import Internal.Tools.Json as Json
import Parser as P
{-| The Matrix user represents a user across multiple Matrix rooms.
-}
type alias User =
UserId.UserID
{-| Define a method to encode/decode Matrix users.
-}
coder : Json.Coder User
coder =
Json.parser
{ name = "Username"
, p =
P.andThen
(\name ->
P.succeed
( name
, if UserId.isHistorical name then
[ log.warn "Historical user found"
]
else
[]
)
)
UserId.userIdParser
, toString = UserId.toString
}
{-| The domain represents the Matrix homeserver controlling this user. It also
offers other Matrix homeservers an indication of where to look if you wish to
send a message to this user.
-}
domain : User -> String
domain =
.domain >> ServerName.toString
{-| Parse a string and convert it into a User, if formatted properly.
-}
fromString : String -> Maybe User
fromString =
UserId.fromString
{-| The localpart is similar to a username, in the sense that every user has
their own localpart. The localpart is not unique across multiple servers,
however! There can be a user @alice:example.com and a user @alice:example.org in
a room at the same time.
-}
localpart : User -> String
localpart =
.localpart
{-| Convert a user into its unique identifier string value.
-}
toString : User -> String
toString =
UserId.toString

View File

@ -122,9 +122,10 @@ roomId (Event event) =
{-| Determine the fully-qualified ID of the user who sent an event. {-| Determine the fully-qualified ID of the user who sent an event.
-} -}
sender : Event -> String sender : Event -> Types.User
sender (Event event) = sender (Event event) =
Envelope.extract .sender event Envelope.map .sender event
|> Types.User
{-| Determine an event's state key. {-| Determine an event's state key.

147
src/Matrix/User.elm Normal file
View File

@ -0,0 +1,147 @@
module Matrix.User exposing
( User, toString
, localpart, domain
, get
)
{-| Matrix users are identified by their unique ID. In the Matrix API, this is a
string that looks as follows:
@alice:example.org
\---/ \---------/
| |
| |
localpart domain
Since it is very easy to abuse Matrix user IDs to sneak in arbitrary values,
the Elm SDK parses them and makes sure they are safe. As a result, you might
need this module to get the right information from a user!
## User
@docs User, toString
## Info
Sometimes, you are more interested in the username itself. These functions can
help you decipher, disambiguate and categorize users based on their username.
@docs localpart, domain
## Manipulate
@docs get
-}
import Internal.Values.Envelope as Envelope
import Internal.Values.User as Internal
import Types exposing (User(..))
{-| The User type represents a Matrix user.
It contains information like:
- Their username on Matrix
- The server that hosts their account
- Access tokens needed to talk to the server
It does **NOT** contain information like:
- Their nickname
- Their profile picture
- Your private room with them
You can get all that information by looking it up in the [Vault](Matrix#Vault).
**Note:** Please do not store this user type as a variable in your model! You
should always maintain a single source of truth in Elm, and the User type
contains various credentials and API tokens that might expire if you don't
update them from the Vault.
If you need to remember specific users, you can best compare their identifying
string using [toString](Matrix-User#toString) or you can use
[get](Matrix-User#get) with the Vault to get the user type.
-}
type alias User =
Types.User
{-| The domain is the name of the server that the user connects to. Server names
are case-sensitive, so if the strings are equal, the users are on the same
server!
As a result, you can use the user domain for:
- When multiple users in a room have the same localpart on different servers
- Finding other users from a potentially malicious homeserver
- Counting homeservers in a room
See the following examples:
domain (get vault "@alice:example.org") -- "example.org"
domain (get vault "@bob:127.0.0.1") -- "127.0.0.1"
domain (get vault "@charlie:[2001:db8::]") -- "[2001:db8::]"
-}
domain : User -> String
domain (User user) =
Envelope.extract Internal.domain user
{-| Get a specific user by their unique identifier.
The Vault is needed as an input because the `User` type also stores various
credentials needed to talk to the Matrix API.
get vault "@alice:example.org" -- Just (User "alice" "example.org")
get vault "@bob:127.0.0.1" -- Just (User "bob" "127.0.0.1")
get vault "@charlie:[2001:db8::]" -- Just (User "charlie" "2001:db8::")
get vault "@evil:#mp#ss#bl#.c#m" -- Nothing
get vault "" -- Nothing
-}
get : Types.Vault -> String -> Maybe User
get (Types.Vault vault) username =
Envelope.mapMaybe (\_ -> Internal.fromString username) vault
|> Maybe.map Types.User
{-| The localpart is the user's unique username. Every homeserver has their own
username registry, so you might occasionally find distinct users with the same
localpart.
The localpart is often used as a user's name in a room if they haven't set up
a custom name.
See the following examples:
localpart (get vault "@alice:example.org") -- "alice"
localpart (get vault "@bob:127.0.0.1") -- "bob"
localpart (get vault "@charlie:[2001:db8::]") -- "charlie"
-}
localpart : User -> String
localpart (User user) =
Envelope.extract Internal.localpart user
{-| Get the uniquely identifying string for this user. Since the strings are
case-sensitive, you can run a simple string comparison to compare usernames.
-}
toString : User -> String
toString (User user) =
Envelope.extract Internal.toString user

View File

@ -1,4 +1,4 @@
module Types exposing (Vault(..), Event(..)) module Types exposing (Vault(..), Event(..), User(..))
{-| The Elm SDK uses a lot of records and values that are easy to manipulate. {-| The Elm SDK uses a lot of records and values that are easy to manipulate.
Yet, the [Elm design guidelines](https://package.elm-lang.org/help/design-guidelines#keep-tags-and-record-constructors-secret) Yet, the [Elm design guidelines](https://package.elm-lang.org/help/design-guidelines#keep-tags-and-record-constructors-secret)
@ -12,12 +12,13 @@ access their content directly.
The opaque types are placed in a central module so all exposed modules can The opaque types are placed in a central module so all exposed modules can
safely access all exposed data types without risking to create circular imports. safely access all exposed data types without risking to create circular imports.
@docs Vault, Event @docs Vault, Event, User
-} -}
import Internal.Values.Envelope as Envelope import Internal.Values.Envelope as Envelope
import Internal.Values.Event as Event import Internal.Values.Event as Event
import Internal.Values.User as User
import Internal.Values.Vault as Vault import Internal.Values.Vault as Vault
@ -27,6 +28,12 @@ type Event
= Event (Envelope.Envelope Event.Event) = Event (Envelope.Envelope Event.Event)
{-| Opaque type for Matrix User
-}
type User
= User (Envelope.Envelope User.User)
{-| Opaque type for Matrix Vault {-| Opaque type for Matrix Vault
-} -}
type Vault type Vault

View File

@ -3,6 +3,7 @@ module Test.Filter.Timeline exposing (..)
import Expect import Expect
import Fuzz exposing (Fuzzer) import Fuzz exposing (Fuzzer)
import Internal.Filter.Timeline as Filter exposing (Filter) import Internal.Filter.Timeline as Filter exposing (Filter)
import Internal.Grammar.UserId as U
import Internal.Values.Event as Event import Internal.Values.Event as Event
import Json.Decode as D import Json.Decode as D
import Json.Encode as E import Json.Encode as E
@ -86,7 +87,7 @@ suite =
"Only event sender filter matches" "Only event sender filter matches"
(\event -> (\event ->
event event
|> Filter.match (Filter.onlySenders [ event.sender ]) |> Filter.match (Filter.onlySenders [ U.toString event.sender ])
|> Expect.equal True |> Expect.equal True
) )
, fuzz TestEvent.fuzzer , fuzz TestEvent.fuzzer
@ -100,7 +101,7 @@ suite =
"Not event sender filter doesn't match" "Not event sender filter doesn't match"
(\event -> (\event ->
event event
|> Filter.match (Filter.allSendersExcept [ event.sender ]) |> Filter.match (Filter.allSendersExcept [ U.toString event.sender ])
|> Expect.equal False |> Expect.equal False
) )
, fuzz2 TestEvent.fuzzer , fuzz2 TestEvent.fuzzer
@ -109,7 +110,7 @@ suite =
(\event senders -> (\event senders ->
event event
|> Filter.match (Filter.onlySenders senders) |> Filter.match (Filter.onlySenders senders)
|> Expect.equal (List.member event.sender senders) |> Expect.equal (List.member (U.toString event.sender) senders)
) )
, fuzz2 TestEvent.fuzzer , fuzz2 TestEvent.fuzzer
(Fuzz.list Fuzz.string) (Fuzz.list Fuzz.string)
@ -125,7 +126,7 @@ suite =
(\event senders -> (\event senders ->
event event
|> Filter.match (Filter.allSendersExcept senders) |> Filter.match (Filter.allSendersExcept senders)
|> Expect.notEqual (List.member event.sender senders) |> Expect.notEqual (List.member (U.toString event.sender) senders)
) )
, fuzz2 TestEvent.fuzzer , fuzz2 TestEvent.fuzzer
(Fuzz.list Fuzz.string) (Fuzz.list Fuzz.string)
@ -302,7 +303,7 @@ suite =
l2 = l2 =
List.filter List.filter
(\e -> (\e ->
List.member e.sender senders List.member (U.toString e.sender) senders
&& List.member e.eventType types && List.member e.eventType types
) )
events events
@ -336,8 +337,8 @@ suite =
l2 = l2 =
List.filter List.filter
(\e -> (\e ->
List.member e.sender senders List.member (U.toString e.sender) senders
&& (not <| List.member e.eventType types) && (not <| List.member (U.toString e.sender) types)
) )
events events
in in
@ -370,7 +371,7 @@ suite =
l2 = l2 =
List.filter List.filter
(\e -> (\e ->
(not <| List.member e.sender senders) (not <| List.member (U.toString e.sender) senders)
&& List.member e.eventType types && List.member e.eventType types
) )
events events
@ -404,7 +405,7 @@ suite =
l2 = l2 =
List.filter List.filter
(\e -> (\e ->
(not <| List.member e.sender senders) (not <| List.member (U.toString e.sender) senders)
&& (not <| List.member e.eventType types) && (not <| List.member e.eventType types)
) )
events events

View File

@ -0,0 +1,126 @@
module Test.Grammar.ServerName exposing (..)
import Expect
import Fuzz exposing (Fuzzer)
import Internal.Grammar.ServerName as SN
import Test exposing (..)
dnsFuzzer : Fuzzer String
dnsFuzzer =
Fuzz.map2
(\head tail ->
String.fromList (head :: tail)
)
("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
|> String.toList
|> Fuzz.oneOfValues
)
("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-."
|> String.toList
|> Fuzz.oneOfValues
|> Fuzz.listOfLengthBetween 0 (255 - 1)
)
hostnameFuzzer : Fuzzer String
hostnameFuzzer =
Fuzz.oneOf
[ dnsFuzzer
, ipv4Fuzzer
, Fuzz.map (\x -> "[" ++ x ++ "]") ipv6Fuzzer
]
ipv4Fuzzer : Fuzzer String
ipv4Fuzzer =
Fuzz.intRange 0 255
|> Fuzz.listOfLength 4
|> Fuzz.map
(List.map String.fromInt
>> List.intersperse "."
>> String.concat
)
ipv6Fuzzer : Fuzzer String
ipv6Fuzzer =
let
num : Fuzzer String
num =
"0123456789abcdefABCDEF"
|> String.toList
|> Fuzz.oneOfValues
|> Fuzz.listOfLength 4
|> Fuzz.map String.fromList
in
Fuzz.oneOf
[ Fuzz.listOfLength 8 num
|> Fuzz.map (List.intersperse ":")
|> Fuzz.map String.concat
, Fuzz.listOfLengthBetween 0 7 num
|> Fuzz.andThen
(\front ->
num
|> Fuzz.listOfLengthBetween 0 (8 - 1 - List.length front)
|> Fuzz.map
(\back ->
[ front
|> List.intersperse ":"
, [ "::" ]
, back
|> List.intersperse ":"
]
|> List.concat
|> String.concat
)
)
]
portFuzzer : Fuzzer String
portFuzzer =
Fuzz.oneOf
[ Fuzz.constant ""
, Fuzz.intRange 0 65535
|> Fuzz.map (\p -> ":" ++ String.fromInt p)
]
serverNameFuzzer : Fuzzer String
serverNameFuzzer =
Fuzz.map2 (++) hostnameFuzzer portFuzzer
suite : Test
suite =
describe "Server name tests"
[ describe "Checking correct values"
[ fuzz serverNameFuzzer
"Correct server names validate"
(\server ->
SN.fromString server
|> Maybe.map SN.toString
|> Expect.equal (Just server)
)
, test "Checking spec examples"
(\() ->
let
examples : List String
examples =
[ "matrix.org"
, "matrix.org:8888"
, "1.2.3.4"
, "1.2.3.4:1234"
, "[1234:5678::abcd]"
, "[1234:5678::abcd]:5678"
]
in
examples
|> List.map SN.fromString
|> List.map ((/=) Nothing)
|> Expect.equalLists
(List.repeat (List.length examples) True)
)
]
]

View File

@ -0,0 +1,159 @@
module Test.Grammar.UserId exposing (..)
import Expect
import Fuzz exposing (Fuzzer)
import Internal.Grammar.ServerName as SN
import Internal.Grammar.UserId as U
import Test exposing (..)
import Test.Grammar.ServerName as ServerName
modernUserCharFuzzer : Fuzzer Char
modernUserCharFuzzer =
Fuzz.oneOf
[ Fuzz.intRange 0x61 0x7A
|> Fuzz.map Char.fromCode
, "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
|> String.toList
|> Fuzz.oneOfValues
]
historicalUserCharFuzzer : Fuzzer Char
historicalUserCharFuzzer =
[ ( 0x21, 0x39 ), ( 0x3B, 0x7E ) ]
|> List.map (\( low, high ) -> Fuzz.intRange low high)
|> Fuzz.oneOf
|> Fuzz.map Char.fromCode
modernUserFuzzer : Fuzzer String
modernUserFuzzer =
Fuzz.map2
(\localpart domain ->
let
maxLocalSize : Int
maxLocalSize =
255 - String.length domain - 2
in
localpart
|> List.take maxLocalSize
|> String.fromList
|> (\l -> "@" ++ l ++ ":" ++ domain)
)
(Fuzz.listOfLengthBetween 1 255 modernUserCharFuzzer)
(ServerName.serverNameFuzzer
|> Fuzz.filter
(\name ->
String.length name < 255 - 2
)
)
historicalUserFuzzer : Fuzzer String
historicalUserFuzzer =
Fuzz.map2
(\localpart domain ->
let
maxLocalSize : Int
maxLocalSize =
255 - String.length domain - 2
in
localpart
|> List.take maxLocalSize
|> String.fromList
|> (\l -> "@" ++ l ++ ":" ++ domain)
)
(Fuzz.listOfLengthBetween 1 255 historicalUserCharFuzzer)
(ServerName.serverNameFuzzer
|> Fuzz.filter
(\name ->
String.length name < 255 - 2
)
)
userFuzzer : Fuzzer String
userFuzzer =
Fuzz.oneOf [ modernUserFuzzer, historicalUserFuzzer ]
fullUserFuzzer : Fuzzer U.UserID
fullUserFuzzer =
userFuzzer
|> Fuzz.map U.fromString
|> Fuzz.map (Maybe.withDefault { localpart = "a", domain = { host = SN.DNS "a", port_ = Nothing } })
suite : Test
suite =
describe "UserId"
[ describe "Size"
[ fuzz ServerName.serverNameFuzzer
"Username cannot be length 0"
(\domain ->
"@"
++ ":"
++ domain
|> U.fromString
|> Expect.equal Nothing
)
, fuzz2 (Fuzz.listOfLengthBetween 1 255 historicalUserCharFuzzer)
ServerName.serverNameFuzzer
"Username length cannot exceed 255"
(\localpart domain ->
let
username : String
username =
"@"
++ String.fromList localpart
++ ":"
++ domain
in
Expect.equal
(U.fromString username == Nothing)
(String.length username > 255)
)
, fuzz modernUserFuzzer
"Modern fuzzer has appropriate size"
(String.length >> Expect.lessThan 256)
, fuzz historicalUserFuzzer
"Historical fuzzer has appropriate size"
(String.length >> Expect.lessThan 256)
, fuzz userFuzzer
"User fuzzers have appropriate size"
(String.length >> Expect.lessThan 256)
]
, describe "From string evaluation"
[ fuzz userFuzzer
"fromString always returns a value on fuzzer"
(U.fromString >> Expect.notEqual Nothing)
, fuzz userFuzzer
"fromString -> toString returns the same value"
(\username ->
username
|> U.fromString
|> Maybe.map U.toString
|> Expect.equal (Just username)
)
-- Not always True
-- TODO: Define a fitting fuzzer for this test
-- , fuzz historicalUserFuzzer
-- "Historical users are historical"
-- (\username ->
-- username
-- |> U.fromString
-- |> Maybe.map U.isHistorical
-- |> Expect.equal (Just True)
-- )
, fuzz modernUserFuzzer
"Modern users are not historical"
(\username ->
username
|> U.fromString
|> Maybe.map U.isHistorical
|> Expect.equal (Just False)
)
]
]

View File

@ -115,7 +115,7 @@ suite =
(\event -> (\event ->
Hashdict.singleton .eventId event Hashdict.singleton .eventId event
|> Hashdict.remove event |> Hashdict.remove event
|> Hashdict.isEqual (Hashdict.empty .sender) |> Hashdict.isEqual (Hashdict.empty .roomId)
|> Expect.equal True |> Expect.equal True
) )
, fuzz TestEvent.fuzzer , fuzz TestEvent.fuzzer
@ -123,7 +123,7 @@ suite =
(\event -> (\event ->
Hashdict.singleton .eventId event Hashdict.singleton .eventId event
|> Hashdict.removeKey event.eventId |> Hashdict.removeKey event.eventId
|> Hashdict.isEqual (Hashdict.empty .sender) |> Hashdict.isEqual (Hashdict.empty .roomId)
|> Expect.equal True |> Expect.equal True
) )
, fuzz TestEvent.fuzzer , fuzz TestEvent.fuzzer
@ -168,8 +168,8 @@ suite =
|> Json.encode (Hashdict.coder .eventId Event.coder) |> Json.encode (Hashdict.coder .eventId Event.coder)
|> E.encode indent |> E.encode indent
|> D.decodeString (Json.decode <| Hashdict.coder .eventId Event.coder) |> D.decodeString (Json.decode <| Hashdict.coder .eventId Event.coder)
|> Result.map (Tuple.mapFirst Hashdict.toList) |> Result.map (Tuple.first >> Hashdict.toList)
|> Expect.equal (Ok ( Hashdict.toList hashdict, [] )) |> Expect.equal (Ok (Hashdict.toList hashdict))
) )
] ]
] ]

View File

@ -198,8 +198,8 @@ suite =
|> Json.encode (Mashdict.coder .stateKey Event.coder) |> Json.encode (Mashdict.coder .stateKey Event.coder)
|> E.encode indent |> E.encode indent
|> D.decodeString (Json.decode <| Mashdict.coder .stateKey Event.coder) |> D.decodeString (Json.decode <| Mashdict.coder .stateKey Event.coder)
|> Result.map (Tuple.mapFirst Mashdict.toList) |> Result.map (Tuple.first >> Mashdict.toList)
|> Expect.equal (Ok ( Mashdict.toList hashdict, [] )) |> Expect.equal (Ok (Mashdict.toList hashdict))
) )
] ]
] ]

View File

@ -5,6 +5,7 @@ import Fuzz exposing (Fuzzer)
import Internal.Values.Event as Event exposing (Event) import Internal.Values.Event as Event exposing (Event)
import Json.Encode as E import Json.Encode as E
import Test exposing (..) import Test exposing (..)
import Test.Grammar.UserId as UserId
import Test.Tools.Timestamp as TestTimestamp import Test.Tools.Timestamp as TestTimestamp
@ -15,7 +16,7 @@ fuzzer =
Fuzz.string Fuzz.string
TestTimestamp.fuzzer TestTimestamp.fuzzer
Fuzz.string Fuzz.string
Fuzz.string UserId.fullUserFuzzer
(Fuzz.maybe Fuzz.string) (Fuzz.maybe Fuzz.string)
Fuzz.string Fuzz.string
(Fuzz.maybe unsignedDataFuzzer) (Fuzz.maybe unsignedDataFuzzer)