// 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], 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 { 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 { 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 }