commit
8c73fbf9b1
5
elm.json
5
elm.json
|
@ -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"
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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 =
|
||||||
|
|
|
@ -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
|
|
@ -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)
|
|
@ -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)
|
||||||
|
|
|
@ -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
|
|
@ -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
|
||||||
|
|
|
@ -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
|
|
@ -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.
|
||||||
|
|
|
@ -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
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
|
)
|
||||||
|
]
|
||||||
|
]
|
|
@ -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)
|
||||||
|
)
|
||||||
|
]
|
||||||
|
]
|
|
@ -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))
|
||||||
)
|
)
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
|
|
|
@ -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))
|
||||||
)
|
)
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
|
|
|
@ -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)
|
||||||
|
|
Loading…
Reference in New Issue