summary refs log tree commit diff
path: root/vendor/maunium.net/go/mautrix/id
diff options
context:
space:
mode:
Diffstat (limited to 'vendor/maunium.net/go/mautrix/id')
-rw-r--r--vendor/maunium.net/go/mautrix/id/contenturi.go177
-rw-r--r--vendor/maunium.net/go/mautrix/id/crypto.go203
-rw-r--r--vendor/maunium.net/go/mautrix/id/matrixuri.go302
-rw-r--r--vendor/maunium.net/go/mautrix/id/opaque.go98
-rw-r--r--vendor/maunium.net/go/mautrix/id/trust.go87
-rw-r--r--vendor/maunium.net/go/mautrix/id/userid.go242
6 files changed, 1109 insertions, 0 deletions
diff --git a/vendor/maunium.net/go/mautrix/id/contenturi.go b/vendor/maunium.net/go/mautrix/id/contenturi.go
new file mode 100644
index 0000000..e6a313f
--- /dev/null
+++ b/vendor/maunium.net/go/mautrix/id/contenturi.go
@@ -0,0 +1,177 @@
+// 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"
+	"regexp"
+	"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
+}
+
+var simpleHomeserverRegex = regexp.MustCompile(`^[a-zA-Z0-9.:-]+$`)
+
+func (uri ContentURI) IsValid() bool {
+	return IsValidMediaID(uri.FileID) && uri.Homeserver != "" && simpleHomeserverRegex.MatchString(uri.Homeserver)
+}
+
+func IsValidMediaID(mediaID string) bool {
+	if len(mediaID) == 0 {
+		return false
+	}
+	for _, char := range mediaID {
+		if (char < 'A' || char > 'Z') && (char < 'a' || char > 'z') && (char < '0' || char > '9') && char != '-' && char != '_' {
+			return false
+		}
+	}
+	return true
+}
diff --git a/vendor/maunium.net/go/mautrix/id/crypto.go b/vendor/maunium.net/go/mautrix/id/crypto.go
new file mode 100644
index 0000000..355a84a
--- /dev/null
+++ b/vendor/maunium.net/go/mautrix/id/crypto.go
@@ -0,0 +1,203 @@
+// 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 (
+	"encoding/base64"
+	"fmt"
+	"strings"
+
+	"go.mau.fi/util/random"
+)
+
+// 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"
+)
+
+type KeyBackupAlgorithm string
+
+const (
+	KeyBackupAlgorithmMegolmBackupV1 KeyBackupAlgorithm = "m.megolm_backup.v1.curve25519-aes-sha2"
+)
+
+// BackupVersion is an arbitrary string that identifies a server side key backup.
+type KeyBackupVersion string
+
+func (version KeyBackupVersion) String() string {
+	return string(version)
+}
+
+// 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) Bytes() []byte {
+	val, _ := base64.RawStdEncoding.DecodeString(string(ed25519))
+	// TODO handle errors
+	return val
+}
+
+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)
+}
+
+func (curve25519 Curve25519) Bytes() []byte {
+	val, _ := base64.RawStdEncoding.DecodeString(string(curve25519))
+	// TODO handle errors
+	return val
+}
+
+// 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
+}
+
+// Secret storage keys
+type Secret string
+
+func (s Secret) String() string {
+	return string(s)
+}
+
+const (
+	SecretXSMaster       Secret = "m.cross_signing.master"
+	SecretXSSelfSigning  Secret = "m.cross_signing.self_signing"
+	SecretXSUserSigning  Secret = "m.cross_signing.user_signing"
+	SecretMegolmBackupV1 Secret = "m.megolm_backup.v1"
+)
+
+// VerificationTransactionID is a unique identifier for a verification
+// transaction.
+type VerificationTransactionID string
+
+func NewVerificationTransactionID() VerificationTransactionID {
+	return VerificationTransactionID(random.String(32))
+}
+
+func (t VerificationTransactionID) String() string {
+	return string(t)
+}
diff --git a/vendor/maunium.net/go/mautrix/id/matrixuri.go b/vendor/maunium.net/go/mautrix/id/matrixuri.go
new file mode 100644
index 0000000..acd8e0c
--- /dev/null
+++ b/vendor/maunium.net/go/mautrix/id/matrixuri.go
@@ -0,0 +1,302 @@
+// 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 {
+	if uri == nil {
+		return ""
+	}
+	parts := []string{
+		SigilToPathSegment[uri.Sigil1],
+		url.PathEscape(uri.MXID1),
+	}
+	if uri.Sigil2 != 0 {
+		parts = append(parts, SigilToPathSegment[uri.Sigil2], url.PathEscape(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 {
+	if uri == nil {
+		return ""
+	}
+	fragment := fmt.Sprintf("#/%s", url.PathEscape(uri.PrimaryIdentifier()))
+	if uri.Sigil2 != 0 {
+		fragment = fmt.Sprintf("%s/%s", fragment, url.PathEscape(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 {
+	if uri == nil {
+		return ""
+	}
+	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 == nil || 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 != nil && 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 != nil && 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 != nil && 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 != nil && (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
+}
diff --git a/vendor/maunium.net/go/mautrix/id/opaque.go b/vendor/maunium.net/go/mautrix/id/opaque.go
new file mode 100644
index 0000000..1d9f0dc
--- /dev/null
+++ b/vendor/maunium.net/go/mautrix/id/opaque.go
@@ -0,0 +1,98 @@
+// 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 {
+	if roomID == "" {
+		return nil
+	}
+	return &MatrixURI{
+		Sigil1: '!',
+		MXID1:  string(roomID)[1:],
+		Via:    via,
+	}
+}
+
+func (roomID RoomID) EventURI(eventID EventID, via ...string) *MatrixURI {
+	if roomID == "" {
+		return nil
+	} else if eventID == "" {
+		return roomID.URI(via...)
+	}
+	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 {
+	if roomAlias == "" {
+		return nil
+	}
+	return &MatrixURI{
+		Sigil1: '#',
+		MXID1:  string(roomAlias)[1:],
+	}
+}
+
+// Deprecated: room alias event links should not be used. Use room IDs instead.
+func (roomAlias RoomAlias) EventURI(eventID EventID) *MatrixURI {
+	if roomAlias == "" {
+		return nil
+	}
+	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)
+}
diff --git a/vendor/maunium.net/go/mautrix/id/trust.go b/vendor/maunium.net/go/mautrix/id/trust.go
new file mode 100644
index 0000000..04f6e36
--- /dev/null
+++ b/vendor/maunium.net/go/mautrix/id/trust.go
@@ -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"
+	}
+}
diff --git a/vendor/maunium.net/go/mautrix/id/userid.go b/vendor/maunium.net/go/mautrix/id/userid.go
new file mode 100644
index 0000000..1e1f3b2
--- /dev/null
+++ b/vendor/maunium.net/go/mautrix/id/userid.go
@@ -0,0 +1,242 @@
+// 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")
+)
+
+// ParseCommonIdentifier parses a common identifier according to https://spec.matrix.org/v1.9/appendices/#common-identifier-format
+func ParseCommonIdentifier[Stringish ~string](identifier Stringish) (sigil byte, localpart, homeserver string) {
+	if len(identifier) == 0 {
+		return
+	}
+	sigil = identifier[0]
+	strIdentifier := string(identifier)
+	if strings.ContainsRune(strIdentifier, ':') {
+		parts := strings.SplitN(strIdentifier, ":", 2)
+		localpart = parts[0][1:]
+		homeserver = parts[1]
+	} else {
+		localpart = strIdentifier[1:]
+	}
+	return
+}
+
+// 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) {
+	var sigil byte
+	sigil, localpart, homeserver = ParseCommonIdentifier(userID)
+	if sigil != '@' || homeserver == "" {
+		err = fmt.Errorf("'%s' %w", userID, ErrInvalidUserID)
+	}
+	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 {
+	if userID == "" {
+		return nil
+	}
+	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 != '+' && !(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 == '-' || 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
+}