add vendoring

This commit is contained in:
Aine
2022-11-16 12:08:51 +02:00
parent 14751cbf3a
commit c1d33fe3cb
1104 changed files with 759066 additions and 0 deletions

158
vendor/maunium.net/go/mautrix/id/contenturi.go generated vendored Normal file
View File

@@ -0,0 +1,158 @@
// Copyright (c) 2020 Tulir Asokan
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
package id
import (
"bytes"
"database/sql/driver"
"encoding/json"
"errors"
"fmt"
"strings"
)
var (
InvalidContentURI = errors.New("invalid Matrix content URI")
InputNotJSONString = errors.New("input doesn't look like a JSON string")
)
// ContentURIString is a string that's expected to be a Matrix content URI.
// It's useful for delaying the parsing of the content URI to move errors from the event content
// JSON parsing step to a later step where more appropriate errors can be produced.
type ContentURIString string
func (uriString ContentURIString) Parse() (ContentURI, error) {
return ParseContentURI(string(uriString))
}
func (uriString ContentURIString) ParseOrIgnore() ContentURI {
parsed, _ := ParseContentURI(string(uriString))
return parsed
}
// ContentURI represents a Matrix content URI.
// https://spec.matrix.org/v1.2/client-server-api/#matrix-content-mxc-uris
type ContentURI struct {
Homeserver string
FileID string
}
func MustParseContentURI(uri string) ContentURI {
parsed, err := ParseContentURI(uri)
if err != nil {
panic(err)
}
return parsed
}
// ParseContentURI parses a Matrix content URI.
func ParseContentURI(uri string) (parsed ContentURI, err error) {
if len(uri) == 0 {
return
} else if !strings.HasPrefix(uri, "mxc://") {
err = InvalidContentURI
} else if index := strings.IndexRune(uri[6:], '/'); index == -1 || index == len(uri)-7 {
err = InvalidContentURI
} else {
parsed.Homeserver = uri[6 : 6+index]
parsed.FileID = uri[6+index+1:]
}
return
}
var mxcBytes = []byte("mxc://")
func ParseContentURIBytes(uri []byte) (parsed ContentURI, err error) {
if len(uri) == 0 {
return
} else if !bytes.HasPrefix(uri, mxcBytes) {
err = InvalidContentURI
} else if index := bytes.IndexRune(uri[6:], '/'); index == -1 || index == len(uri)-7 {
err = InvalidContentURI
} else {
parsed.Homeserver = string(uri[6 : 6+index])
parsed.FileID = string(uri[6+index+1:])
}
return
}
func (uri *ContentURI) UnmarshalJSON(raw []byte) (err error) {
if string(raw) == "null" {
*uri = ContentURI{}
return nil
} else if len(raw) < 2 || raw[0] != '"' || raw[len(raw)-1] != '"' {
return InputNotJSONString
}
parsed, err := ParseContentURIBytes(raw[1 : len(raw)-1])
if err != nil {
return err
}
*uri = parsed
return nil
}
func (uri *ContentURI) MarshalJSON() ([]byte, error) {
if uri == nil || uri.IsEmpty() {
return []byte("null"), nil
}
return json.Marshal(uri.String())
}
func (uri *ContentURI) UnmarshalText(raw []byte) (err error) {
parsed, err := ParseContentURIBytes(raw)
if err != nil {
return err
}
*uri = parsed
return nil
}
func (uri ContentURI) MarshalText() ([]byte, error) {
return []byte(uri.String()), nil
}
func (uri *ContentURI) Scan(i interface{}) error {
var parsed ContentURI
var err error
switch value := i.(type) {
case nil:
// don't do anything, set uri to empty
case []byte:
parsed, err = ParseContentURIBytes(value)
case string:
parsed, err = ParseContentURI(value)
default:
return fmt.Errorf("invalid type %T for ContentURI.Scan", i)
}
if err != nil {
return err
}
*uri = parsed
return nil
}
func (uri *ContentURI) Value() (driver.Value, error) {
if uri == nil {
return nil, nil
}
return uri.String(), nil
}
func (uri ContentURI) String() string {
if uri.IsEmpty() {
return ""
}
return fmt.Sprintf("mxc://%s/%s", uri.Homeserver, uri.FileID)
}
func (uri ContentURI) CUString() ContentURIString {
return ContentURIString(uri.String())
}
func (uri ContentURI) IsEmpty() bool {
return len(uri.Homeserver) == 0 || len(uri.FileID) == 0
}

149
vendor/maunium.net/go/mautrix/id/crypto.go generated vendored Normal file
View File

@@ -0,0 +1,149 @@
// Copyright (c) 2020 Tulir Asokan
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
package id
import (
"fmt"
"strings"
)
// OlmMsgType is an Olm message type
type OlmMsgType int
const (
OlmMsgTypePreKey OlmMsgType = 0
OlmMsgTypeMsg OlmMsgType = 1
)
// Algorithm is a Matrix message encryption algorithm.
// https://spec.matrix.org/v1.2/client-server-api/#messaging-algorithm-names
type Algorithm string
const (
AlgorithmOlmV1 Algorithm = "m.olm.v1.curve25519-aes-sha2"
AlgorithmMegolmV1 Algorithm = "m.megolm.v1.aes-sha2"
)
type KeyAlgorithm string
const (
KeyAlgorithmCurve25519 KeyAlgorithm = "curve25519"
KeyAlgorithmEd25519 KeyAlgorithm = "ed25519"
KeyAlgorithmSignedCurve25519 KeyAlgorithm = "signed_curve25519"
)
type CrossSigningUsage string
const (
XSUsageMaster CrossSigningUsage = "master"
XSUsageSelfSigning CrossSigningUsage = "self_signing"
XSUsageUserSigning CrossSigningUsage = "user_signing"
)
// A SessionID is an arbitrary string that identifies an Olm or Megolm session.
type SessionID string
func (sessionID SessionID) String() string {
return string(sessionID)
}
// Ed25519 is the base64 representation of an Ed25519 public key
type Ed25519 string
type SigningKey = Ed25519
func (ed25519 Ed25519) String() string {
return string(ed25519)
}
func (ed25519 Ed25519) Fingerprint() string {
spacedSigningKey := make([]byte, len(ed25519)+(len(ed25519)-1)/4)
var ptr = 0
for i, chr := range ed25519 {
spacedSigningKey[ptr] = byte(chr)
ptr++
if i%4 == 3 {
spacedSigningKey[ptr] = ' '
ptr++
}
}
return string(spacedSigningKey)
}
// Curve25519 is the base64 representation of an Curve25519 public key
type Curve25519 string
type SenderKey = Curve25519
type IdentityKey = Curve25519
func (curve25519 Curve25519) String() string {
return string(curve25519)
}
// A DeviceID is an arbitrary string that references a specific device.
type DeviceID string
func (deviceID DeviceID) String() string {
return string(deviceID)
}
// A DeviceKeyID is a string formatted as <algorithm>:<device_id> that is used as the key in deviceid-key mappings.
type DeviceKeyID string
func NewDeviceKeyID(algorithm KeyAlgorithm, deviceID DeviceID) DeviceKeyID {
return DeviceKeyID(fmt.Sprintf("%s:%s", algorithm, deviceID))
}
func (deviceKeyID DeviceKeyID) String() string {
return string(deviceKeyID)
}
func (deviceKeyID DeviceKeyID) Parse() (Algorithm, DeviceID) {
index := strings.IndexRune(string(deviceKeyID), ':')
if index < 0 || len(deviceKeyID) <= index+1 {
return "", ""
}
return Algorithm(deviceKeyID[:index]), DeviceID(deviceKeyID[index+1:])
}
// A KeyID a string formatted as <keyalgorithm>:<key_id> that is used as the key in one-time-key mappings.
type KeyID string
func NewKeyID(algorithm KeyAlgorithm, keyID string) KeyID {
return KeyID(fmt.Sprintf("%s:%s", algorithm, keyID))
}
func (keyID KeyID) String() string {
return string(keyID)
}
func (keyID KeyID) Parse() (KeyAlgorithm, string) {
index := strings.IndexRune(string(keyID), ':')
if index < 0 || len(keyID) <= index+1 {
return "", ""
}
return KeyAlgorithm(keyID[:index]), string(keyID[index+1:])
}
// Device contains the identity details of a device and some additional info.
type Device struct {
UserID UserID
DeviceID DeviceID
IdentityKey Curve25519
SigningKey Ed25519
Trust TrustState
Deleted bool
Name string
}
func (device *Device) Fingerprint() string {
return device.SigningKey.Fingerprint()
}
type CrossSigningKey struct {
Key Ed25519
First Ed25519
}

293
vendor/maunium.net/go/mautrix/id/matrixuri.go generated vendored Normal file
View File

@@ -0,0 +1,293 @@
// Copyright (c) 2021 Tulir Asokan
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
package id
import (
"errors"
"fmt"
"net/url"
"strings"
)
// Errors that can happen when parsing matrix: URIs
var (
ErrInvalidScheme = errors.New("matrix URI scheme must be exactly 'matrix'")
ErrInvalidPartCount = errors.New("matrix URIs must have exactly 2 or 4 segments")
ErrInvalidFirstSegment = errors.New("invalid identifier in first segment of matrix URI")
ErrEmptySecondSegment = errors.New("the second segment of the matrix URI must not be empty")
ErrInvalidThirdSegment = errors.New("invalid identifier in third segment of matrix URI")
ErrEmptyFourthSegment = errors.New("the fourth segment of the matrix URI must not be empty when the third segment is present")
)
// Errors that can happen when parsing matrix.to URLs
var (
ErrNotMatrixTo = errors.New("that URL is not a matrix.to URL")
ErrInvalidMatrixToPartCount = errors.New("matrix.to URLs must have exactly 1 or 2 segments")
ErrEmptyMatrixToPrimaryIdentifier = errors.New("the primary identifier in the matrix.to URL is empty")
ErrInvalidMatrixToPrimaryIdentifier = errors.New("the primary identifier in the matrix.to URL has an invalid sigil")
ErrInvalidMatrixToSecondaryIdentifier = errors.New("the secondary identifier in the matrix.to URL has an invalid sigil")
)
var ErrNotMatrixToOrMatrixURI = errors.New("that URL is not a matrix.to URL nor matrix: URI")
// MatrixURI contains the result of parsing a matrix: URI using ParseMatrixURI
type MatrixURI struct {
Sigil1 rune
Sigil2 rune
MXID1 string
MXID2 string
Via []string
Action string
}
// SigilToPathSegment contains a mapping from Matrix identifier sigils to matrix: URI path segments.
var SigilToPathSegment = map[rune]string{
'$': "e",
'#': "r",
'!': "roomid",
'@': "u",
}
func (uri *MatrixURI) getQuery() url.Values {
q := make(url.Values)
if uri.Via != nil && len(uri.Via) > 0 {
q["via"] = uri.Via
}
if len(uri.Action) > 0 {
q.Set("action", uri.Action)
}
return q
}
// String converts the parsed matrix: URI back into the string representation.
func (uri *MatrixURI) String() string {
parts := []string{
SigilToPathSegment[uri.Sigil1],
uri.MXID1,
}
if uri.Sigil2 != 0 {
parts = append(parts, SigilToPathSegment[uri.Sigil2], uri.MXID2)
}
return (&url.URL{
Scheme: "matrix",
Opaque: strings.Join(parts, "/"),
RawQuery: uri.getQuery().Encode(),
}).String()
}
// MatrixToURL converts to parsed matrix: URI into a matrix.to URL
func (uri *MatrixURI) MatrixToURL() string {
fragment := fmt.Sprintf("#/%s", url.QueryEscape(uri.PrimaryIdentifier()))
if uri.Sigil2 != 0 {
fragment = fmt.Sprintf("%s/%s", fragment, url.QueryEscape(uri.SecondaryIdentifier()))
}
query := uri.getQuery().Encode()
if len(query) > 0 {
fragment = fmt.Sprintf("%s?%s", fragment, query)
}
// It would be nice to use URL{...}.String() here, but figuring out the Fragment vs RawFragment stuff is a pain
return fmt.Sprintf("https://matrix.to/%s", fragment)
}
// PrimaryIdentifier returns the first Matrix identifier in the URI.
// Currently room IDs, room aliases and user IDs can be in the primary identifier slot.
func (uri *MatrixURI) PrimaryIdentifier() string {
return fmt.Sprintf("%c%s", uri.Sigil1, uri.MXID1)
}
// SecondaryIdentifier returns the second Matrix identifier in the URI.
// Currently only event IDs can be in the secondary identifier slot.
func (uri *MatrixURI) SecondaryIdentifier() string {
if uri.Sigil2 == 0 {
return ""
}
return fmt.Sprintf("%c%s", uri.Sigil2, uri.MXID2)
}
// UserID returns the user ID from the URI if the primary identifier is a user ID.
func (uri *MatrixURI) UserID() UserID {
if uri.Sigil1 == '@' {
return UserID(uri.PrimaryIdentifier())
}
return ""
}
// RoomID returns the room ID from the URI if the primary identifier is a room ID.
func (uri *MatrixURI) RoomID() RoomID {
if uri.Sigil1 == '!' {
return RoomID(uri.PrimaryIdentifier())
}
return ""
}
// RoomAlias returns the room alias from the URI if the primary identifier is a room alias.
func (uri *MatrixURI) RoomAlias() RoomAlias {
if uri.Sigil1 == '#' {
return RoomAlias(uri.PrimaryIdentifier())
}
return ""
}
// EventID returns the event ID from the URI if the primary identifier is a room ID or alias and the secondary identifier is an event ID.
func (uri *MatrixURI) EventID() EventID {
if (uri.Sigil1 == '!' || uri.Sigil1 == '#') && uri.Sigil2 == '$' {
return EventID(uri.SecondaryIdentifier())
}
return ""
}
// ParseMatrixURIOrMatrixToURL parses the given matrix.to URL or matrix: URI into a unified representation.
func ParseMatrixURIOrMatrixToURL(uri string) (*MatrixURI, error) {
parsed, err := url.Parse(uri)
if err != nil {
return nil, fmt.Errorf("failed to parse URI: %w", err)
}
if parsed.Scheme == "matrix" {
return ProcessMatrixURI(parsed)
} else if strings.HasSuffix(parsed.Hostname(), "matrix.to") {
return ProcessMatrixToURL(parsed)
} else {
return nil, ErrNotMatrixToOrMatrixURI
}
}
// ParseMatrixURI implements the matrix: URI parsing algorithm.
//
// Currently specified in https://github.com/matrix-org/matrix-doc/blob/master/proposals/2312-matrix-uri.md#uri-parsing-algorithm
func ParseMatrixURI(uri string) (*MatrixURI, error) {
// Step 1: parse the URI according to RFC 3986
parsed, err := url.Parse(uri)
if err != nil {
return nil, fmt.Errorf("failed to parse URI: %w", err)
}
return ProcessMatrixURI(parsed)
}
// ProcessMatrixURI implements steps 2-7 of the matrix: URI parsing algorithm
// (i.e. everything except parsing the URI itself, which is done with url.Parse or ParseMatrixURI)
func ProcessMatrixURI(uri *url.URL) (*MatrixURI, error) {
// Step 2: check that scheme is exactly `matrix`
if uri.Scheme != "matrix" {
return nil, ErrInvalidScheme
}
// Step 3: split the path into segments separated by /
parts := strings.Split(uri.Opaque, "/")
// Step 4: Check that the URI contains either 2 or 4 segments
if len(parts) != 2 && len(parts) != 4 {
return nil, ErrInvalidPartCount
}
var parsed MatrixURI
// Step 5: Construct the top-level Matrix identifier
// a: find the sigil from the first segment
switch parts[0] {
case "u", "user":
parsed.Sigil1 = '@'
case "r", "room":
parsed.Sigil1 = '#'
case "roomid":
parsed.Sigil1 = '!'
default:
return nil, fmt.Errorf("%w: '%s'", ErrInvalidFirstSegment, parts[0])
}
// b: find the identifier from the second segment
if len(parts[1]) == 0 {
return nil, ErrEmptySecondSegment
}
parsed.MXID1 = parts[1]
// Step 6: if the first part is a room and the URI has 4 segments, construct a second level identifier
if (parsed.Sigil1 == '!' || parsed.Sigil1 == '#') && len(parts) == 4 {
// a: find the sigil from the third segment
switch parts[2] {
case "e", "event":
parsed.Sigil2 = '$'
default:
return nil, fmt.Errorf("%w: '%s'", ErrInvalidThirdSegment, parts[0])
}
// b: find the identifier from the fourth segment
if len(parts[3]) == 0 {
return nil, ErrEmptyFourthSegment
}
parsed.MXID2 = parts[3]
}
// Step 7: parse the query and extract via and action items
via, ok := uri.Query()["via"]
if ok && len(via) > 0 {
parsed.Via = via
}
action, ok := uri.Query()["action"]
if ok && len(action) > 0 {
parsed.Action = action[len(action)-1]
}
return &parsed, nil
}
// ParseMatrixToURL parses a matrix.to URL into the same container as ParseMatrixURI parses matrix: URIs.
func ParseMatrixToURL(uri string) (*MatrixURI, error) {
parsed, err := url.Parse(uri)
if err != nil {
return nil, fmt.Errorf("failed to parse URL: %w", err)
}
return ProcessMatrixToURL(parsed)
}
// ProcessMatrixToURL is the equivalent of ProcessMatrixURI for matrix.to URLs.
func ProcessMatrixToURL(uri *url.URL) (*MatrixURI, error) {
if !strings.HasSuffix(uri.Hostname(), "matrix.to") {
return nil, ErrNotMatrixTo
}
initialSplit := strings.SplitN(uri.Fragment, "?", 2)
parts := strings.Split(initialSplit[0], "/")
if len(initialSplit) > 1 {
uri.RawQuery = initialSplit[1]
}
if len(parts) < 2 || len(parts) > 3 {
return nil, ErrInvalidMatrixToPartCount
}
if len(parts[1]) == 0 {
return nil, ErrEmptyMatrixToPrimaryIdentifier
}
var parsed MatrixURI
parsed.Sigil1 = rune(parts[1][0])
parsed.MXID1 = parts[1][1:]
_, isKnown := SigilToPathSegment[parsed.Sigil1]
if !isKnown {
return nil, ErrInvalidMatrixToPrimaryIdentifier
}
if len(parts) == 3 && len(parts[2]) > 0 {
parsed.Sigil2 = rune(parts[2][0])
parsed.MXID2 = parts[2][1:]
_, isKnown = SigilToPathSegment[parsed.Sigil2]
if !isKnown {
return nil, ErrInvalidMatrixToSecondaryIdentifier
}
}
via, ok := uri.Query()["via"]
if ok && len(via) > 0 {
parsed.Via = via
}
action, ok := uri.Query()["action"]
if ok && len(action) > 0 {
parsed.Action = action[len(action)-1]
}
return &parsed, nil
}

83
vendor/maunium.net/go/mautrix/id/opaque.go generated vendored Normal file
View File

@@ -0,0 +1,83 @@
// Copyright (c) 2020 Tulir Asokan
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
package id
import (
"fmt"
)
// A RoomID is a string starting with ! that references a specific room.
// https://matrix.org/docs/spec/appendices#room-ids-and-event-ids
type RoomID string
// A RoomAlias is a string starting with # that can be resolved into.
// https://matrix.org/docs/spec/appendices#room-aliases
type RoomAlias string
func NewRoomAlias(localpart, server string) RoomAlias {
return RoomAlias(fmt.Sprintf("#%s:%s", localpart, server))
}
// An EventID is a string starting with $ that references a specific event.
//
// https://matrix.org/docs/spec/appendices#room-ids-and-event-ids
// https://matrix.org/docs/spec/rooms/v4#event-ids
type EventID string
// A BatchID is a string identifying a batch of events being backfilled to a room.
// https://github.com/matrix-org/matrix-doc/pull/2716
type BatchID string
func (roomID RoomID) String() string {
return string(roomID)
}
func (roomID RoomID) URI(via ...string) *MatrixURI {
return &MatrixURI{
Sigil1: '!',
MXID1: string(roomID)[1:],
Via: via,
}
}
func (roomID RoomID) EventURI(eventID EventID, via ...string) *MatrixURI {
return &MatrixURI{
Sigil1: '!',
MXID1: string(roomID)[1:],
Sigil2: '$',
MXID2: string(eventID)[1:],
Via: via,
}
}
func (roomAlias RoomAlias) String() string {
return string(roomAlias)
}
func (roomAlias RoomAlias) URI() *MatrixURI {
return &MatrixURI{
Sigil1: '#',
MXID1: string(roomAlias)[1:],
}
}
func (roomAlias RoomAlias) EventURI(eventID EventID) *MatrixURI {
return &MatrixURI{
Sigil1: '#',
MXID1: string(roomAlias)[1:],
Sigil2: '$',
MXID2: string(eventID)[1:],
}
}
func (eventID EventID) String() string {
return string(eventID)
}
func (batchID BatchID) String() string {
return string(batchID)
}

87
vendor/maunium.net/go/mautrix/id/trust.go generated vendored Normal file
View File

@@ -0,0 +1,87 @@
// Copyright (c) 2022 Tulir Asokan
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
package id
import (
"fmt"
"strings"
)
// TrustState determines how trusted a device is.
type TrustState int
const (
TrustStateBlacklisted TrustState = -100
TrustStateUnset TrustState = 0
TrustStateUnknownDevice TrustState = 10
TrustStateForwarded TrustState = 20
TrustStateCrossSignedUntrusted TrustState = 50
TrustStateCrossSignedTOFU TrustState = 100
TrustStateCrossSignedVerified TrustState = 200
TrustStateVerified TrustState = 300
TrustStateInvalid TrustState = (1 << 31) - 1
)
func (ts *TrustState) UnmarshalText(data []byte) error {
strData := string(data)
state := ParseTrustState(strData)
if state == TrustStateInvalid {
return fmt.Errorf("invalid trust state %q", strData)
}
*ts = state
return nil
}
func (ts *TrustState) MarshalText() ([]byte, error) {
return []byte(ts.String()), nil
}
func ParseTrustState(val string) TrustState {
switch strings.ToLower(val) {
case "blacklisted":
return TrustStateBlacklisted
case "unverified":
return TrustStateUnset
case "cross-signed-untrusted":
return TrustStateCrossSignedUntrusted
case "unknown-device":
return TrustStateUnknownDevice
case "forwarded":
return TrustStateForwarded
case "cross-signed-tofu", "cross-signed":
return TrustStateCrossSignedTOFU
case "cross-signed-verified", "cross-signed-trusted":
return TrustStateCrossSignedVerified
case "verified":
return TrustStateVerified
default:
return TrustStateInvalid
}
}
func (ts TrustState) String() string {
switch ts {
case TrustStateBlacklisted:
return "blacklisted"
case TrustStateUnset:
return "unverified"
case TrustStateCrossSignedUntrusted:
return "cross-signed-untrusted"
case TrustStateUnknownDevice:
return "unknown-device"
case TrustStateForwarded:
return "forwarded"
case TrustStateCrossSignedTOFU:
return "cross-signed-tofu"
case TrustStateCrossSignedVerified:
return "cross-signed-verified"
case TrustStateVerified:
return "verified"
default:
return "invalid"
}
}

224
vendor/maunium.net/go/mautrix/id/userid.go generated vendored Normal file
View File

@@ -0,0 +1,224 @@
// Copyright (c) 2021 Tulir Asokan
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
package id
import (
"bytes"
"encoding/hex"
"errors"
"fmt"
"regexp"
"strings"
)
// UserID represents a Matrix user ID.
// https://matrix.org/docs/spec/appendices#user-identifiers
type UserID string
const UserIDMaxLength = 255
func NewUserID(localpart, homeserver string) UserID {
return UserID(fmt.Sprintf("@%s:%s", localpart, homeserver))
}
func NewEncodedUserID(localpart, homeserver string) UserID {
return NewUserID(EncodeUserLocalpart(localpart), homeserver)
}
var (
ErrInvalidUserID = errors.New("is not a valid user ID")
ErrNoncompliantLocalpart = errors.New("contains characters that are not allowed")
ErrUserIDTooLong = errors.New("the given user ID is longer than 255 characters")
ErrEmptyLocalpart = errors.New("empty localparts are not allowed")
)
// Parse parses the user ID into the localpart and server name.
//
// Note that this only enforces very basic user ID formatting requirements: user IDs start with
// a @, and contain a : after the @. If you want to enforce localpart validity, see the
// ParseAndValidate and ValidateUserLocalpart functions.
func (userID UserID) Parse() (localpart, homeserver string, err error) {
if len(userID) == 0 || userID[0] != '@' || !strings.ContainsRune(string(userID), ':') {
// This error wrapping lets you use errors.Is() nicely even though the message contains the user ID
err = fmt.Errorf("'%s' %w", userID, ErrInvalidUserID)
return
}
parts := strings.SplitN(string(userID), ":", 2)
localpart, homeserver = strings.TrimPrefix(parts[0], "@"), parts[1]
return
}
func (userID UserID) Localpart() string {
localpart, _, _ := userID.Parse()
return localpart
}
func (userID UserID) Homeserver() string {
_, homeserver, _ := userID.Parse()
return homeserver
}
// URI returns the user ID as a MatrixURI struct, which can then be stringified into a matrix: URI or a matrix.to URL.
//
// This does not parse or validate the user ID. Use the ParseAndValidate method if you want to ensure the user ID is valid first.
func (userID UserID) URI() *MatrixURI {
return &MatrixURI{
Sigil1: '@',
MXID1: string(userID)[1:],
}
}
var ValidLocalpartRegex = regexp.MustCompile("^[0-9a-z-.=_/]+$")
// ValidateUserLocalpart validates a Matrix user ID localpart using the grammar
// in https://matrix.org/docs/spec/appendices#user-identifier
func ValidateUserLocalpart(localpart string) error {
if len(localpart) == 0 {
return ErrEmptyLocalpart
} else if !ValidLocalpartRegex.MatchString(localpart) {
return fmt.Errorf("'%s' %w", localpart, ErrNoncompliantLocalpart)
}
return nil
}
// ParseAndValidate parses the user ID into the localpart and server name like Parse,
// and also validates that the localpart is allowed according to the user identifiers spec.
func (userID UserID) ParseAndValidate() (localpart, homeserver string, err error) {
localpart, homeserver, err = userID.Parse()
if err == nil {
err = ValidateUserLocalpart(localpart)
}
if err == nil && len(userID) > UserIDMaxLength {
err = ErrUserIDTooLong
}
return
}
func (userID UserID) ParseAndDecode() (localpart, homeserver string, err error) {
localpart, homeserver, err = userID.ParseAndValidate()
if err == nil {
localpart, err = DecodeUserLocalpart(localpart)
}
return
}
func (userID UserID) String() string {
return string(userID)
}
const lowerhex = "0123456789abcdef"
// encode the given byte using quoted-printable encoding (e.g "=2f")
// and writes it to the buffer
// See https://golang.org/src/mime/quotedprintable/writer.go
func encode(buf *bytes.Buffer, b byte) {
buf.WriteByte('=')
buf.WriteByte(lowerhex[b>>4])
buf.WriteByte(lowerhex[b&0x0f])
}
// escape the given alpha character and writes it to the buffer
func escape(buf *bytes.Buffer, b byte) {
buf.WriteByte('_')
if b == '_' {
buf.WriteByte('_') // another _
} else {
buf.WriteByte(b + 0x20) // ASCII shift A-Z to a-z
}
}
func shouldEncode(b byte) bool {
return b != '-' && b != '.' && b != '_' && !(b >= '0' && b <= '9') && !(b >= 'a' && b <= 'z') && !(b >= 'A' && b <= 'Z')
}
func shouldEscape(b byte) bool {
return (b >= 'A' && b <= 'Z') || b == '_'
}
func isValidByte(b byte) bool {
return isValidEscapedChar(b) || (b >= '0' && b <= '9') || b == '.' || b == '=' || b == '-'
}
func isValidEscapedChar(b byte) bool {
return b == '_' || (b >= 'a' && b <= 'z')
}
// EncodeUserLocalpart encodes the given string into Matrix-compliant user ID localpart form.
// See https://spec.matrix.org/v1.2/appendices/#mapping-from-other-character-sets
//
// This returns a string with only the characters "a-z0-9._=-". The uppercase range A-Z
// are encoded using leading underscores ("_"). Characters outside the aforementioned ranges
// (including literal underscores ("_") and equals ("=")) are encoded as UTF8 code points (NOT NCRs)
// and converted to lower-case hex with a leading "=". For example:
//
// Alph@Bet_50up => _alph=40_bet=5f50up
func EncodeUserLocalpart(str string) string {
strBytes := []byte(str)
var outputBuffer bytes.Buffer
for _, b := range strBytes {
if shouldEncode(b) {
encode(&outputBuffer, b)
} else if shouldEscape(b) {
escape(&outputBuffer, b)
} else {
outputBuffer.WriteByte(b)
}
}
return outputBuffer.String()
}
// DecodeUserLocalpart decodes the given string back into the original input string.
// Returns an error if the given string is not a valid user ID localpart encoding.
// See https://spec.matrix.org/v1.2/appendices/#mapping-from-other-character-sets
//
// This decodes quoted-printable bytes back into UTF8, and unescapes casing. For
// example:
//
// _alph=40_bet=5f50up => Alph@Bet_50up
//
// Returns an error if the input string contains characters outside the
// range "a-z0-9._=-", has an invalid quote-printable byte (e.g. not hex), or has
// an invalid _ escaped byte (e.g. "_5").
func DecodeUserLocalpart(str string) (string, error) {
strBytes := []byte(str)
var outputBuffer bytes.Buffer
for i := 0; i < len(strBytes); i++ {
b := strBytes[i]
if !isValidByte(b) {
return "", fmt.Errorf("Byte pos %d: Invalid byte", i)
}
if b == '_' { // next byte is a-z and should be upper-case or is another _ and should be a literal _
if i+1 >= len(strBytes) {
return "", fmt.Errorf("Byte pos %d: expected _[a-z_] encoding but ran out of string", i)
}
if !isValidEscapedChar(strBytes[i+1]) { // invalid escaping
return "", fmt.Errorf("Byte pos %d: expected _[a-z_] encoding", i)
}
if strBytes[i+1] == '_' {
outputBuffer.WriteByte('_')
} else {
outputBuffer.WriteByte(strBytes[i+1] - 0x20) // ASCII shift a-z to A-Z
}
i++ // skip next byte since we just handled it
} else if b == '=' { // next 2 bytes are hex and should be buffered ready to be read as utf8
if i+2 >= len(strBytes) {
return "", fmt.Errorf("Byte pos: %d: expected quote-printable encoding but ran out of string", i)
}
dst := make([]byte, 1)
_, err := hex.Decode(dst, strBytes[i+1:i+3])
if err != nil {
return "", err
}
outputBuffer.WriteByte(dst[0])
i += 2 // skip next 2 bytes since we just handled it
} else { // pass through
outputBuffer.WriteByte(b)
}
}
return outputBuffer.String(), nil
}