diff --git a/lib/src/utils/matrix_id_string_extension.dart b/lib/src/utils/matrix_id_string_extension.dart index 8e91d2cd..3cfb6372 100644 --- a/lib/src/utils/matrix_id_string_extension.dart +++ b/lib/src/utils/matrix_id_string_extension.dart @@ -38,4 +38,115 @@ extension MatrixIdExtension on String { String get domain => isValidMatrixId ? _getParts().last : null; bool equals(String other) => toLowerCase() == other?.toLowerCase(); + + /// Separate a matrix identifier string into a primary indentifier, a secondary identifier, + /// a query string and already parsed `via` parameters. A matrix identifier string + /// can be an mxid, a matrix.to-url or a matrix-uri. + MatrixIdentifierStringExtensionResults parseIdentifierIntoParts() { + const matrixUriPrefix = 'matrix:'; + + final via = {}; + String action; + final parseQueryString = (qs) { + if (qs != null) { + // as there might be multiple "via" tags we can't just use Uri.splitQueryString, we need to do our own thing + for (final parameterStr in qs.split('&')) { + final index = parameterStr.indexOf('='); + if (index == -1) { + continue; + } + final parameter = + Uri.decodeQueryComponent(parameterStr.substring(0, index)); + final value = + Uri.decodeQueryComponent(parameterStr.substring(index + 1)); + if (parameter == 'via') { + via.add(value); + } + if (parameter == 'action') { + action = value; + } + } + } + }; + + // check if we have a "matrix:" uri + if (toLowerCase().startsWith(matrixUriPrefix)) { + final uri = Uri.tryParse(this); + if (uri == null) { + return null; + } + final pathSegments = uri.pathSegments; + final identifiers = []; + for (var i = 0; i < pathSegments.length - 1; i += 2) { + final thisSigil = { + 'user': '@', + 'roomid': '!', + 'room': '#', + 'group': '+', + 'event': '\$', + }[pathSegments[i].toLowerCase()]; + if (thisSigil == null) { + break; + } + final identifier = thisSigil + pathSegments[i + 1]; + if (!identifier.isValidMatrixId) { + return null; + } + identifiers.add(identifier); + } + if (identifiers.isEmpty) { + return null; + } + final queryString = uri.query.isNotEmpty ? uri.query : null; + parseQueryString(queryString); + return MatrixIdentifierStringExtensionResults( + primaryIdentifier: identifiers.first, + secondaryIdentifier: identifiers.length > 1 ? identifiers[1] : null, + queryString: queryString, + via: via, + action: action, + ); + } + + const matrixToPrefix = 'https://matrix.to/#/'; + // matrix identifiers and matrix.to URLs are parsed similarly, so we do them here + var s = this; + if (toLowerCase().startsWith(matrixToPrefix)) { + // as we decode a component we may only call it on the url part *before* the "query" part + final parts = substring(matrixToPrefix.length).split('?'); + s = Uri.decodeComponent(parts.removeAt(0)) + '?' + parts.join('?'); + } + final match = RegExp(r'^([#!@+][^:]*:[^\/?]*)(?:\/(\$[^?]*))?(?:\?(.*))?$') + .firstMatch(s); + if (match == null || + !match.group(1).isValidMatrixId || + !(match.group(2)?.isValidMatrixId ?? true)) { + return null; + } + final queryString = + match.group(3)?.isNotEmpty ?? false ? match.group(3) : null; + parseQueryString(queryString); + return MatrixIdentifierStringExtensionResults( + primaryIdentifier: match.group(1), + secondaryIdentifier: match.group(2), + queryString: queryString, + via: via, + action: action, + ); + } +} + +class MatrixIdentifierStringExtensionResults { + final String primaryIdentifier; + final String secondaryIdentifier; + final String queryString; + final Set via; + final String action; + + MatrixIdentifierStringExtensionResults( + {this.primaryIdentifier, + this.secondaryIdentifier, + this.queryString, + this.via, + this.action}); } diff --git a/test/matrix_id_string_extension_test.dart b/test/matrix_id_string_extension_test.dart index 01d59c86..50c02a27 100644 --- a/test/matrix_id_string_extension_test.dart +++ b/test/matrix_id_string_extension_test.dart @@ -46,5 +46,77 @@ void main() { expect('@user:domain:8448'.localpart, 'user'); expect('@user:domain:8448'.domain, 'domain:8448'); }); + test('parseIdentifierIntoParts', () { + var res = '#alias:beep'.parseIdentifierIntoParts(); + expect(res.primaryIdentifier, '#alias:beep'); + expect(res.secondaryIdentifier, null); + expect(res.queryString, null); + res = 'blha'.parseIdentifierIntoParts(); + expect(res, null); + res = '#alias:beep/\$event'.parseIdentifierIntoParts(); + expect(res.primaryIdentifier, '#alias:beep'); + expect(res.secondaryIdentifier, '\$event'); + expect(res.queryString, null); + res = '#alias:beep?blubb'.parseIdentifierIntoParts(); + expect(res.primaryIdentifier, '#alias:beep'); + expect(res.secondaryIdentifier, null); + expect(res.queryString, 'blubb'); + res = '#alias:beep/\$event?blubb'.parseIdentifierIntoParts(); + expect(res.primaryIdentifier, '#alias:beep'); + expect(res.secondaryIdentifier, '\$event'); + expect(res.queryString, 'blubb'); + res = '#/\$?:beep/\$event?blubb?b'.parseIdentifierIntoParts(); + expect(res.primaryIdentifier, '#/\$?:beep'); + expect(res.secondaryIdentifier, '\$event'); + expect(res.queryString, 'blubb?b'); + + res = 'https://matrix.to/#/#alias:beep'.parseIdentifierIntoParts(); + expect(res.primaryIdentifier, '#alias:beep'); + expect(res.secondaryIdentifier, null); + expect(res.queryString, null); + res = 'https://matrix.to/#/%23alias%3abeep'.parseIdentifierIntoParts(); + expect(res.primaryIdentifier, '#alias:beep'); + expect(res.secondaryIdentifier, null); + expect(res.queryString, null); + res = 'https://matrix.to/#/%23alias%3abeep?boop%F0%9F%A7%A1%F0%9F%A6%8A' + .parseIdentifierIntoParts(); + expect(res.primaryIdentifier, '#alias:beep'); + expect(res.secondaryIdentifier, null); + expect(res.queryString, 'boop%F0%9F%A7%A1%F0%9F%A6%8A'); + + res = 'https://matrix.to/#/#alias:beep?via=fox.com&via=fox.org' + .parseIdentifierIntoParts(); + expect(res.via, {'fox.com', 'fox.org'}); + + res = 'matrix:user/her:example.org'.parseIdentifierIntoParts(); + expect(res.primaryIdentifier, '@her:example.org'); + expect(res.secondaryIdentifier, null); + res = 'matrix:user/bad'.parseIdentifierIntoParts(); + expect(res, null); + res = 'matrix:roomid/rid:example.org'.parseIdentifierIntoParts(); + expect(res.primaryIdentifier, '!rid:example.org'); + expect(res.secondaryIdentifier, null); + expect(res.action, null); + res = 'matrix:room/us:example.org?action=chat'.parseIdentifierIntoParts(); + expect(res.primaryIdentifier, '#us:example.org'); + expect(res.secondaryIdentifier, null); + expect(res.action, 'chat'); + res = 'matrix:room/us:example.org/event/lol823y4bcp3qo4' + .parseIdentifierIntoParts(); + expect(res.primaryIdentifier, '#us:example.org'); + expect(res.secondaryIdentifier, '\$lol823y4bcp3qo4'); + res = 'matrix:group/them:example.org'.parseIdentifierIntoParts(); + expect(res.primaryIdentifier, '+them:example.org'); + expect(res.secondaryIdentifier, null); + res = 'matrix:roomid/rid:example.org?via=fox.com&via=fox.org' + .parseIdentifierIntoParts(); + expect(res.primaryIdentifier, '!rid:example.org'); + expect(res.secondaryIdentifier, null); + expect(res.via, {'fox.com', 'fox.org'}); + res = 'matrix:beep/boop:example.org'.parseIdentifierIntoParts(); + expect(res, null); + res = 'matrix:boop'.parseIdentifierIntoParts(); + expect(res, null); + }); }); }