summary refs log tree commit diff
path: root/vendor/maunium.net/go/mautrix
diff options
context:
space:
mode:
authorEmile <git@emile.space>2024-10-25 15:55:50 +0200
committerEmile <git@emile.space>2024-10-25 15:55:50 +0200
commitc90f36e3dd179d2de96f4f5fe38d8dc9a9de6dfe (patch)
tree89e9afb41c5bf76f48cfb09305a2d3db8d302b06 /vendor/maunium.net/go/mautrix
parent98bbb0f559a8883bc47bae80607dbe326a448e61 (diff)
vendor HEAD main
Diffstat (limited to 'vendor/maunium.net/go/mautrix')
-rw-r--r--vendor/maunium.net/go/mautrix/.editorconfig15
-rw-r--r--vendor/maunium.net/go/mautrix/.gitignore4
-rw-r--r--vendor/maunium.net/go/mautrix/.pre-commit-config.yaml27
-rw-r--r--vendor/maunium.net/go/mautrix/CHANGELOG.md924
-rw-r--r--vendor/maunium.net/go/mautrix/LICENSE373
-rw-r--r--vendor/maunium.net/go/mautrix/README.md22
-rw-r--r--vendor/maunium.net/go/mautrix/client.go2391
-rw-r--r--vendor/maunium.net/go/mautrix/crypto/aescbc/aes_cbc.go60
-rw-r--r--vendor/maunium.net/go/mautrix/crypto/aescbc/errors.go15
-rw-r--r--vendor/maunium.net/go/mautrix/crypto/attachment/attachments.go300
-rw-r--r--vendor/maunium.net/go/mautrix/crypto/backup/encryptedsessiondata.go131
-rw-r--r--vendor/maunium.net/go/mautrix/crypto/backup/ephemeralkey.go41
-rw-r--r--vendor/maunium.net/go/mautrix/crypto/backup/megolmbackup.go39
-rw-r--r--vendor/maunium.net/go/mautrix/crypto/backup/megolmbackupkey.go34
-rw-r--r--vendor/maunium.net/go/mautrix/crypto/canonicaljson/README.md6
-rw-r--r--vendor/maunium.net/go/mautrix/crypto/canonicaljson/json.go257
-rw-r--r--vendor/maunium.net/go/mautrix/crypto/ed25519/ed25519.go302
-rw-r--r--vendor/maunium.net/go/mautrix/crypto/goolm/crypto/curve25519.go186
-rw-r--r--vendor/maunium.net/go/mautrix/crypto/goolm/crypto/doc.go2
-rw-r--r--vendor/maunium.net/go/mautrix/crypto/goolm/crypto/ed25519.go184
-rw-r--r--vendor/maunium.net/go/mautrix/crypto/goolm/crypto/hmac.go29
-rw-r--r--vendor/maunium.net/go/mautrix/crypto/goolm/crypto/one_time_key.go95
-rw-r--r--vendor/maunium.net/go/mautrix/crypto/goolm/libolmpickle/pickle.go41
-rw-r--r--vendor/maunium.net/go/mautrix/crypto/goolm/libolmpickle/unpickle.go53
-rw-r--r--vendor/maunium.net/go/mautrix/crypto/olm/README.md4
-rw-r--r--vendor/maunium.net/go/mautrix/crypto/olm/account.go113
-rw-r--r--vendor/maunium.net/go/mautrix/crypto/olm/errors.go60
-rw-r--r--vendor/maunium.net/go/mautrix/crypto/olm/inboundgroupsession.go80
-rw-r--r--vendor/maunium.net/go/mautrix/crypto/olm/olm.go20
-rw-r--r--vendor/maunium.net/go/mautrix/crypto/olm/outboundgroupsession.go57
-rw-r--r--vendor/maunium.net/go/mautrix/crypto/olm/pk.go57
-rw-r--r--vendor/maunium.net/go/mautrix/crypto/olm/session.go83
-rw-r--r--vendor/maunium.net/go/mautrix/crypto/pkcs7/pkcs7.go30
-rw-r--r--vendor/maunium.net/go/mautrix/crypto/signatures/signatures.go94
-rw-r--r--vendor/maunium.net/go/mautrix/crypto/utils/utils.go132
-rw-r--r--vendor/maunium.net/go/mautrix/error.go184
-rw-r--r--vendor/maunium.net/go/mautrix/event/accountdata.go107
-rw-r--r--vendor/maunium.net/go/mautrix/event/audio.go21
-rw-r--r--vendor/maunium.net/go/mautrix/event/beeper.go106
-rw-r--r--vendor/maunium.net/go/mautrix/event/content.go609
-rw-r--r--vendor/maunium.net/go/mautrix/event/encryption.go202
-rw-r--r--vendor/maunium.net/go/mautrix/event/ephemeral.go140
-rw-r--r--vendor/maunium.net/go/mautrix/event/events.go156
-rw-r--r--vendor/maunium.net/go/mautrix/event/eventsource.go72
-rw-r--r--vendor/maunium.net/go/mautrix/event/member.go53
-rw-r--r--vendor/maunium.net/go/mautrix/event/message.go356
-rw-r--r--vendor/maunium.net/go/mautrix/event/poll.go67
-rw-r--r--vendor/maunium.net/go/mautrix/event/powerlevels.go199
-rw-r--r--vendor/maunium.net/go/mautrix/event/relations.go234
-rw-r--r--vendor/maunium.net/go/mautrix/event/reply.go98
-rw-r--r--vendor/maunium.net/go/mautrix/event/state.go212
-rw-r--r--vendor/maunium.net/go/mautrix/event/type.go290
-rw-r--r--vendor/maunium.net/go/mautrix/event/verification.go308
-rw-r--r--vendor/maunium.net/go/mautrix/event/voip.go116
-rw-r--r--vendor/maunium.net/go/mautrix/filter.go95
-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
-rw-r--r--vendor/maunium.net/go/mautrix/pushrules/action.go125
-rw-r--r--vendor/maunium.net/go/mautrix/pushrules/condition.go336
-rw-r--r--vendor/maunium.net/go/mautrix/pushrules/doc.go2
-rw-r--r--vendor/maunium.net/go/mautrix/pushrules/pushrules.go37
-rw-r--r--vendor/maunium.net/go/mautrix/pushrules/rule.go177
-rw-r--r--vendor/maunium.net/go/mautrix/pushrules/ruleset.go97
-rw-r--r--vendor/maunium.net/go/mautrix/requests.go475
-rw-r--r--vendor/maunium.net/go/mautrix/responses.go625
-rw-r--r--vendor/maunium.net/go/mautrix/room.go54
-rw-r--r--vendor/maunium.net/go/mautrix/statestore.go344
-rw-r--r--vendor/maunium.net/go/mautrix/sync.go284
-rw-r--r--vendor/maunium.net/go/mautrix/syncstore.go175
-rw-r--r--vendor/maunium.net/go/mautrix/url.go116
-rw-r--r--vendor/maunium.net/go/mautrix/version.go31
-rw-r--r--vendor/maunium.net/go/mautrix/versions.go196
76 files changed, 14069 insertions, 0 deletions
diff --git a/vendor/maunium.net/go/mautrix/.editorconfig b/vendor/maunium.net/go/mautrix/.editorconfig
new file mode 100644
index 0000000..1a167e7
--- /dev/null
+++ b/vendor/maunium.net/go/mautrix/.editorconfig
@@ -0,0 +1,15 @@
+root = true
+
+[*]
+indent_style = tab
+indent_size = 4
+end_of_line = lf
+charset = utf-8
+trim_trailing_whitespace = true
+insert_final_newline = true
+
+[*.{yaml,yml}]
+indent_style = space
+
+[provisioning.yaml]
+indent_size = 2
diff --git a/vendor/maunium.net/go/mautrix/.gitignore b/vendor/maunium.net/go/mautrix/.gitignore
new file mode 100644
index 0000000..c01f2f3
--- /dev/null
+++ b/vendor/maunium.net/go/mautrix/.gitignore
@@ -0,0 +1,4 @@
+.idea/
+.vscode/
+*.db*
+*.log
diff --git a/vendor/maunium.net/go/mautrix/.pre-commit-config.yaml b/vendor/maunium.net/go/mautrix/.pre-commit-config.yaml
new file mode 100644
index 0000000..c15d69d
--- /dev/null
+++ b/vendor/maunium.net/go/mautrix/.pre-commit-config.yaml
@@ -0,0 +1,27 @@
+repos:
+  - repo: https://github.com/pre-commit/pre-commit-hooks
+    rev: v4.6.0
+    hooks:
+      - id: trailing-whitespace
+        exclude_types: [markdown]
+      - id: end-of-file-fixer
+      - id: check-yaml
+      - id: check-added-large-files
+
+  - repo: https://github.com/tekwizely/pre-commit-golang
+    rev: v1.0.0-rc.1
+    hooks:
+      - id: go-imports-repo
+        args:
+          - "-local"
+          - "maunium.net/go/mautrix"
+          - "-w"
+      - id: go-vet-repo-mod
+      - id: go-mod-tidy
+      # TODO enable this
+      #- id: go-staticcheck-repo-mod
+
+  - repo: https://github.com/beeper/pre-commit-go
+    rev: v0.3.1
+    hooks:
+      - id: prevent-literal-http-methods
diff --git a/vendor/maunium.net/go/mautrix/CHANGELOG.md b/vendor/maunium.net/go/mautrix/CHANGELOG.md
new file mode 100644
index 0000000..e190431
--- /dev/null
+++ b/vendor/maunium.net/go/mautrix/CHANGELOG.md
@@ -0,0 +1,924 @@
+## v0.21.1 (2024-10-16)
+
+* *(bridgev2)* Added more features and fixed bugs.
+* *(hicli)* Added more features and fixed bugs.
+* *(appservice)* Removed TLS support. A reverse proxy should be used if TLS
+  is needed.
+* *(format/mdext)* Added goldmark extension to fix indented paragraphs when
+  disabling indented code block parser.
+* *(event)* Added `Has` method for `Mentions`.
+* *(event)* Added basic support for the unstable version of polls.
+
+## v0.21.0 (2024-09-16)
+
+* **Breaking change *(client)*** Dropped support for unauthenticated media.
+  Matrix v1.11 support is now required from the homeserver, although it's not
+  enforced using `/versions` as some servers don't advertise it.
+* *(bridgev2)* Added more features and fixed bugs.
+* *(appservice,crypto)* Added support for using MSC3202 for appservice
+  encryption.
+* *(crypto/olm)* Made everything into an interface to allow side-by-side
+  testing of libolm and goolm, as well as potentially support vodozemac
+  in the future.
+* *(client)* Fixed requests being retried even after context is canceled.
+* *(client)* Added option to move `/sync` request logs to trace level.
+* *(error)* Added `Write` and `WithMessage` helpers to `RespError` to make it
+  easier to use on servers.
+* *(event)* Fixed `org.matrix.msc1767.audio` field allowing omitting the
+  duration and waveform.
+* *(id)* Changed `MatrixURI` methods to not panic if the receiver is nil.
+* *(federation)* Added limit to response size when fetching `.well-known` files.
+
+## v0.20.0 (2024-08-16)
+
+* Bumped minimum Go version to 1.22.
+* *(bridgev2)* Added more features and fixed bugs.
+* *(event)* Added types for [MSC4144]: Per-message profiles.
+* *(federation)* Added implementation of server name resolution and a basic
+  client for making federation requests.
+* *(crypto/ssss)* Changed recovery key/passphrase verify functions to take the
+  key ID as a parameter to ensure it's correctly set even if the key metadata
+  wasn't fetched via `GetKeyData`.
+* *(format/mdext)* Added goldmark extensions for single-character bold, italic
+  and strikethrough parsing (as in `*foo*` -> **foo**, `_foo_` -> _foo_ and
+  `~foo~` -> ~~foo~~)
+* *(format)* Changed `RenderMarkdown` et al to always include `m.mentions` in
+  returned content. The mention list is filled with matrix.to URLs from the
+  input by default.
+
+[MSC4144]: https://github.com/matrix-org/matrix-spec-proposals/pull/4144
+
+## v0.19.0 (2024-07-16)
+
+* Renamed `master` branch to `main`.
+* *(bridgev2)* Added more features.
+* *(crypto)* Fixed bug with copying `m.relates_to` from wire content to
+  decrypted content.
+* *(mediaproxy)* Added module for implementing simple media repos that proxy
+  requests elsewhere.
+* *(client)* Changed `Members()` to automatically parse event content for all
+  returned events.
+* *(bridge)* Added `/register` call if `/versions` fails with `M_FORBIDDEN`.
+* *(crypto)* Fixed `DecryptMegolmEvent` sometimes calling database without
+  transaction by using the non-context version of `ResolveTrust`.
+* *(crypto/attachment)* Implemented `io.Seeker` in `EncryptStream` to allow
+  using it in retriable HTTP requests.
+* *(event)* Added helper method to add user ID to a `Mentions` object.
+* *(event)* Fixed default power level for invites
+  (thanks to [@rudis] in [#250]).
+* *(client)* Fixed incorrect warning log in `State()` when state store returns
+  no error (thanks to [@rudis] in [#249]).
+* *(crypto/verificationhelper)* Fixed deadlock when ignoring unknown
+  cancellation events (thanks to [@rudis] in [#247]).
+
+[@rudis]: https://github.com/rudis
+[#250]: https://github.com/mautrix/go/pull/250
+[#249]: https://github.com/mautrix/go/pull/249
+[#247]: https://github.com/mautrix/go/pull/247
+
+### beta.1 (2024-06-16)
+
+* *(bridgev2)* Added experimental high-level bridge framework.
+* *(hicli)* Added experimental high-level client framework.
+* **Slightly breaking changes**
+  * *(crypto)* Added room ID and first known index parameters to
+    `SessionReceived` callback.
+  * *(crypto)* Changed `ImportRoomKeyFromBackup` to return the imported
+    session.
+  * *(client)* Added `error` parameter to `ResponseHook`.
+  * *(client)* Changed `Download` to return entire response instead of just an
+    `io.Reader`.
+* *(crypto)* Changed initial olm device sharing to save keys before sharing to
+  ensure keys aren't accidentally regenerated in case the request fails.
+* *(crypto)* Changed `EncryptMegolmEvent` and `ShareGroupSession` to return
+  more errors instead of only logging and ignoring them.
+* *(crypto)* Added option to completely disable megolm ratchet tracking.
+  * The tracking is meant for bots and bridges which may want to delete old
+    keys, but for normal clients it's just unnecessary overhead.
+* *(crypto)* Changed Megolm session storage methods in `Store` to not take
+  sender key as parameter.
+  * This causes a breaking change to the layout of the `MemoryStore` struct.
+    Using MemoryStore in production is not recommended.
+* *(crypto)* Changed `DecryptMegolmEvent` to copy `m.relates_to` in the raw
+  content too instead of only in the parsed struct.
+* *(crypto)* Exported function to parse megolm message index from raw
+  ciphertext bytes.
+* *(crypto/sqlstore)* Fixed schema of `crypto_secrets` table to include
+  account ID.
+* *(crypto/verificationhelper)* Fixed more bugs.
+* *(client)* Added `UpdateRequestOnRetry` hook which is called immediately
+  before retrying a normal HTTP request.
+* *(client)* Added support for MSC3916 media download endpoint.
+  * Support is automatically detected from spec versions. The `SpecVersions`
+    property can either be filled manually, or `Versions` can be called to
+    automatically populate the field with the response.
+* *(event)* Added constants for known room versions.
+
+## v0.18.1 (2024-04-16)
+
+* *(format)* Added a `context.Context` field to HTMLParser's Context struct.
+* *(bridge)* Added support for handling join rules, knocks, invites and bans
+  (thanks to [@maltee1] in [#193] and [#204]).
+* *(crypto)* Changed forwarded room key handling to only accept keys with a
+  lower first known index than the existing session if there is one.
+* *(crypto)* Changed key backup restore to assume own device list is up to date
+  to avoid re-requesting device list for every deleted device that has signed
+  key backup.
+* *(crypto)* Fixed memory cache not being invalidated when storing own
+  cross-signing keys
+
+[@maltee1]: https://github.com/maltee1
+[#193]: https://github.com/mautrix/go/pull/193
+[#204]: https://github.com/mautrix/go/pull/204
+
+## v0.18.0 (2024-03-16)
+
+* **Breaking change *(client, bridge, appservice)*** Dropped support for
+  maulogger. Only zerolog loggers are now provided by default.
+* *(bridge)* Fixed upload size limit not having a default if the server
+  returned no value.
+* *(synapseadmin)* Added wrappers for some room and user admin APIs.
+  (thanks to [@grvn-ht] in [#181]).
+* *(crypto/verificationhelper)* Fixed bugs.
+* *(crypto)* Fixed key backup uploading doing too much base64.
+* *(crypto)* Changed `EncryptMegolmEvent` to return an error if persisting the
+  megolm session fails. This ensures that database errors won't cause messages
+  to be sent with duplicate indexes.
+* *(crypto)* Changed `GetOrRequestSecret` to use a callback instead of returning
+  the value directly. This allows validating the value in order to ignore
+  invalid secrets.
+* *(id)* Added `ParseCommonIdentifier` function to parse any Matrix identifier
+  in the [Common Identifier Format].
+* *(federation)* Added simple key server that passes the federation tester.
+
+[@grvn-ht]: https://github.com/grvn-ht
+[#181]: https://github.com/mautrix/go/pull/181
+[Common Identifier Format]: https://spec.matrix.org/v1.9/appendices/#common-identifier-format
+
+### beta.1 (2024-02-16)
+
+* Bumped minimum Go version to 1.21.
+* *(bridge)* Bumped minimum Matrix spec version to v1.4.
+* **Breaking change *(crypto)*** Deleted old half-broken interactive
+  verification code and replaced it with a new `verificationhelper`.
+  * The new verification helper is still experimental.
+  * Both QR and emoji verification are supported (in theory).
+* *(crypto)* Added support for server-side key backup.
+* *(crypto)* Added support for receiving and sending secrets like cross-signing
+  private keys via secret sharing.
+* *(crypto)* Added support for tracking which devices megolm sessions were
+  initially shared to, and allowing re-sharing the keys to those sessions.
+* *(client)* Changed cross-signing key upload method to accept a callback for
+  user-interactive auth instead of only hardcoding password support.
+* *(appservice)* Dropped support for legacy non-prefixed appservice paths
+  (e.g. `/transactions` instead of `/_matrix/app/v1/transactions`).
+* *(appservice)* Dropped support for legacy `access_token` authorization in
+  appservice endpoints.
+* *(bridge)* Fixed `RawArgs` field in command events of command state callbacks.
+* *(appservice)* Added `CreateFull` helper function for creating an `AppService`
+  instance with all the mandatory fields set.
+
+## v0.17.0 (2024-01-16)
+
+* **Breaking change *(bridge)*** Added raw event to portal membership handling
+  functions.
+* **Breaking change *(everything)*** Added context parameters to all functions
+  (started by [@recht] in [#144]).
+* **Breaking change *(client)*** Moved event source from sync event handler
+  function parameters to the `Mautrix.EventSource` field inside the event
+  struct.
+* **Breaking change *(client)*** Moved `EventSource` to `event.Source`.
+* *(client)* Removed deprecated `OldEventIgnorer`. The non-deprecated version
+  (`Client.DontProcessOldEvents`) is still available.
+* *(crypto)* Added experimental pure Go Olm implementation to replace libolm
+  (thanks to [@DerLukas15] in [#106]).
+  * You can use the `goolm` build tag to the new implementation.
+* *(bridge)* Added context parameter for bridge command events.
+* *(bridge)* Added method to allow custom validation for the entire config.
+* *(client)* Changed default syncer to not drop unknown events.
+  * The syncer will still drop known events if parsing the content fails.
+  * The behavior can be changed by changing the `ParseErrorHandler` function.
+* *(crypto)* Fixed some places using math/rand instead of crypto/rand.
+
+[@DerLukas15]: https://github.com/DerLukas15
+[@recht]: https://github.com/recht
+[#106]: https://github.com/mautrix/go/pull/106
+[#144]: https://github.com/mautrix/go/pull/144
+
+## v0.16.2 (2023-11-16)
+
+* *(event)* Added `Redacts` field to `RedactionEventContent` for room v11+.
+* *(event)* Added `ReverseTextToHTML` which reverses the changes made by
+  `TextToHTML` (i.e. unescapes HTML characters and replaces `<br/>` with `\n`).
+* *(bridge)* Added global zerologger to ensure all logs go through the bridge
+  logger.
+* *(bridge)* Changed encryption error messages to be sent in a thread if the
+  message that failed to decrypt was in a thread.
+
+## v0.16.1 (2023-09-16)
+
+* **Breaking change *(id)*** Updated user ID localpart encoding to not encode
+  `+` as per [MSC4009].
+* *(bridge)* Added bridge utility to handle double puppeting logins.
+  * The utility supports automatic logins with all three current methods
+    (shared secret, legacy appservice, new appservice).
+* *(appservice)* Added warning logs and timeout on appservice event handling.
+  * Defaults to warning after 30 seconds and timeout 15 minutes after that.
+  * Timeouts can be adjusted or disabled by setting `ExecSync` variables in the
+    `EventProcessor`.
+* *(crypto/olm)* Added `PkDecryption` wrapper.
+
+[MSC4009]: https://github.com/matrix-org/matrix-spec-proposals/pull/4009
+
+## v0.16.0 (2023-08-16)
+
+* Bumped minimum Go version to 1.20.
+* **Breaking change *(util)*** Moved package to [go.mau.fi/util](https://go.mau.fi/util/)
+* *(event)* Removed MSC2716 `historical` field in the `m.room.power_levels`
+  event content struct.
+* *(bridge)* Added `--version-json` flag to print bridge version info as JSON.
+* *(appservice)* Added option to use custom transaction handler for websocket mode.
+
+## v0.15.4 (2023-07-16)
+
+* *(client)* Deprecated MSC2716 methods and added new Beeper-specific batch
+  send methods, as upstream MSC2716 support has been abandoned.
+* *(client)* Added proper error handling and automatic retries to media
+  downloads.
+* *(crypto, bridge)* Added option to remove all keys that were received before
+  the automatic ratcheting was implemented (in v0.15.1).
+* *(dbutil)* Added `JSON` utility for writing/reading arbitrary JSON objects to
+  the db conveniently without manually de/serializing.
+
+## v0.15.3 (2023-06-16)
+
+* *(synapseadmin)* Added wrappers for some Synapse admin API endpoints.
+* *(pushrules)* Implemented new `event_property_is` and `event_property_contains`
+  push rule condition kinds as per MSC3758 and MSC3966.
+* *(bridge)* Moved websocket code from mautrix-imessage to enable all bridges
+  to use appservice websockets easily.
+* *(bridge)* Added retrying for appservice pings.
+* *(types)* Removed unstable field for MSC3952 (intentional mentions).
+* *(client)* Deprecated `OldEventIgnorer` and added `Client.DontProcessOldEvents`
+  to replace it.
+* *(client)* Added `MoveInviteState` sync handler for moving state events in
+  the invite section of sync inside the invite event itself.
+* *(crypto)* Added option to not rotate keys when devices change.
+* *(crypto)* Added additional duplicate message index check if decryption fails
+  because the keys had been ratcheted forward.
+* *(client)* Stabilized support for asynchronous uploads.
+  * `UnstableCreateMXC` and `UnstableUploadAsync` were renamed to `CreateMXC`
+    and `UploadAsync` respectively.
+* *(util/dbutil)* Added option to use a separate database connection pool for
+  read-only transactions.
+  * This is mostly meant for SQLite and it enables read-only transactions that
+    don't lock the database, even when normal transactions are configured to
+    acquire a write lock immediately.
+* *(util/dbutil)* Enabled caller info in zerolog by default.
+
+## v0.15.2 (2023-05-16)
+
+* *(client)* Changed member-fetching methods to clear existing member info in
+  state store.
+* *(client)* Added support for inserting mautrix-go commit hash into default
+  user agent at compile time.
+* *(bridge)* Fixed bridge bot intent not having state store set.
+* *(client)* Fixed `RespError` marshaling mutating the `ExtraData` map and
+  potentially causing panics.
+* *(util/dbutil)* Added `DoTxn` method for an easier way to manage database
+  transactions.
+* *(util)* Added a zerolog `CallerMarshalFunc` implementation that includes the
+  function name.
+* *(bridge)* Added error reply to encrypted messages if the bridge isn't
+  configured to do encryption.
+
+## v0.15.1 (2023-04-16)
+
+* *(crypto, bridge)* Added options to automatically ratchet/delete megolm
+  sessions to minimize access to old messages.
+* *(pushrules)* Added method to get entire push rule that matched (instead of
+  only the list of actions).
+* *(pushrules)* Deprecated `NotifySpecified` as there's no reason to read it.
+* *(crypto)* Changed `max_age` column in `crypto_megolm_inbound_session` table
+  to be milliseconds instead of nanoseconds.
+* *(util)* Added method for iterating `RingBuffer`.
+* *(crypto/cryptohelper)* Changed decryption errors to request session from all
+  own devices in addition to the sender, instead of only asking the sender.
+* *(sqlstatestore)* Fixed `FindSharedRooms` throwing an error when using from
+  a non-bridge context.
+* *(client)* Optimized `AccountDataSyncStore` to not resend save requests if
+  the sync token didn't change.
+* *(types)* Added `Clone()` method for `PowerLevelEventContent`.
+
+## v0.15.0 (2023-03-16)
+
+### beta.3 (2023-03-15)
+
+* **Breaking change *(appservice)*** Removed `Load()` and `AppService.Init()`
+  functions. The struct should just be created with `Create()` and the relevant
+  fields should be filled manually.
+* **Breaking change *(appservice)*** Removed public `HomeserverURL` field and
+  replaced it with a `SetHomeserverURL` method.
+* *(appservice)* Added support for unix sockets for homeserver URL and
+  appservice HTTP server.
+* *(client)* Changed request logging to log durations as floats instead of
+  strings (using zerolog's `Dur()`, so the exact output can be configured).
+* *(bridge)* Changed zerolog to use nanosecond precision timestamps.
+* *(crypto)* Added message index to log after encrypting/decrypting megolm
+  events, and when failing to decrypt due to duplicate index.
+* *(sqlstatestore)* Fixed warning log for rooms that don't have encryption
+  enabled.
+
+### beta.2 (2023-03-02)
+
+* *(bridge)* Fixed building with `nocrypto` tag.
+* *(bridge)* Fixed legacy logging config migration not disabling file writer
+  when `file_name_format` was empty.
+* *(bridge)* Added option to require room power level to run commands.
+* *(event)* Added structs for [MSC3952]: Intentional Mentions.
+* *(util/variationselector)* Added `FullyQualify` method to add necessary emoji
+  variation selectors without adding all possible ones.
+
+[MSC3952]: https://github.com/matrix-org/matrix-spec-proposals/pull/3952
+
+### beta.1 (2023-02-24)
+
+* Bumped minimum Go version to 1.19.
+* **Breaking changes**
+  * *(all)* Switched to zerolog for logging.
+    * The `Client` and `Bridge` structs still include a legacy logger for
+      backwards compatibility.
+  * *(client, appservice)* Moved `SQLStateStore` from appservice module to the
+    top-level (client) module.
+  * *(client, appservice)* Removed unused `Typing` map in `SQLStateStore`.
+  * *(client)* Removed unused `SaveRoom` and `LoadRoom` methods in `Storer`.
+  * *(client, appservice)* Removed deprecated `SendVideo` and `SendImage` methods.
+  * *(client)* Replaced `AppServiceUserID` field with `SetAppServiceUserID` boolean.
+    The `UserID` field is used as the value for the query param.
+  * *(crypto)* Renamed `GobStore` to `MemoryStore` and removed the file saving
+    features. The data can still be persisted, but the persistence part must be
+    implemented separately.
+  * *(crypto)* Removed deprecated `DeviceIdentity` alias
+    (renamed to `id.Device` long ago).
+  * *(client)* Removed `Stringifable` interface as it's the same as `fmt.Stringer`.
+* *(client)* Renamed `Storer` interface to `SyncStore`. A type alias exists for
+  backwards-compatibility.
+* *(crypto/cryptohelper)* Added package for a simplified crypto interface for clients.
+* *(example)* Added e2ee support to example using crypto helper.
+* *(client)* Changed default syncer to stop syncing on `M_UNKNOWN_TOKEN` errors.
+
+## v0.14.0 (2023-02-16)
+
+* **Breaking change *(format)*** Refactored the HTML parser `Context` to have
+  more data.
+* *(id)* Fixed escaping path components when forming matrix.to URLs
+  or `matrix:` URIs.
+* *(bridge)* Bumped default timeouts for decrypting incoming messages.
+* *(bridge)* Added `RawArgs` to commands to allow accessing non-split input.
+* *(bridge)* Added `ReplyAdvanced` to commands to allow setting markdown
+  settings.
+* *(event)* Added `notifications` key to `PowerLevelEventContent`.
+* *(event)* Changed `SetEdit` to cut off edit fallback if the message is long.
+* *(util)* Added `SyncMap` as a simple generic wrapper for a map with a mutex.
+* *(util)* Added `ReturnableOnce` as a wrapper for `sync.Once` with a return
+  value.
+
+## v0.13.0 (2023-01-16)
+
+* **Breaking change:** Removed `IsTyping` and `SetTyping` in `appservice.StateStore`
+  and removed the `TypingStateStore` struct implementing those methods.
+* **Breaking change:** Removed legacy fields in Beeper MSS events.
+* Added knocked rooms to sync response structs.
+* Added wrapper for `/timestamp_to_event` endpoint added in Matrix v1.6.
+* Fixed MSC3870 uploads not failing properly after using up the max retry count.
+* Fixed parsing non-positive ordered list start positions in HTML parser.
+
+## v0.12.4 (2022-12-16)
+
+* Added `SendReceipt` to support private read receipts and thread receipts in
+  the same function. `MarkReadWithContent` is now deprecated.
+* Changed media download methods to return errors if the server returns a
+  non-2xx status code.
+* Removed legacy `sql_store_upgrade.Upgrade` method. Using `store.DB.Upgrade()`
+  after `NewSQLCryptoStore(...)` is recommended instead (the bridge module does
+  this automatically).
+* Added missing `suggested` field to `m.space.child` content struct.
+* Added `device_unused_fallback_key_types` to `/sync` response and appservice
+  transaction structs.
+* Changed `ReqSetReadMarkers` to omit empty fields.
+* Changed bridge configs to force `sqlite3-fk-wal` instead of `sqlite3`.
+* Updated bridge helper to close database connection when stopping.
+* Fixed read receipt and account data endpoints sending `null` instead of an
+  empty object as the body when content isn't provided.
+
+## v0.12.3 (2022-11-16)
+
+* **Breaking change:** Added logging for row iteration in the dbutil package.
+  This changes the return type of `Query` methods from `*sql.Rows` to a new
+  `dbutil.Rows` interface.
+* Added flag to disable wrapping database upgrades in a transaction (e.g. to
+  allow setting `PRAGMA`s for advanced table mutations on SQLite).
+* Deprecated `MessageEventContent.GetReplyTo` in favor of directly using
+  `RelatesTo.GetReplyTo`. RelatesTo methods are nil-safe, so checking if
+  RelatesTo is nil is not necessary for using those methods.
+* Added wrapper for space hierarchyendpoint (thanks to [@mgcm] in [#100]).
+* Added bridge config option to handle transactions asynchronously.
+* Added separate channels for to-device events in appservice transaction
+  handler to avoid blocking to-device events behind normal events.
+* Added `RelatesTo.GetNonFallbackReplyTo` utility method to get the reply event
+  ID, unless the reply is a thread fallback.
+* Added `event.TextToHTML` as an utility method to HTML-escape a string and
+  replace newlines with `<br/>`.
+* Added check to bridge encryption helper to make sure the e2ee keys are still
+  on the server. Synapse is known to sometimes lose keys randomly.
+* Changed bridge crypto syncer to crash on `M_UNKNOWN_TOKEN` errors instead of
+  retrying forever pointlessly.
+* Fixed verifying signatures of fallback one-time keys.
+
+[@mgcm]: https://github.com/mgcm
+[#100]: https://github.com/mautrix/go/pull/100
+
+## v0.12.2 (2022-10-16)
+
+* Added utility method to redact bridge commands.
+* Added thread ID field to read receipts to match Matrix v1.4 changes.
+* Added automatic fetching of media repo config at bridge startup to make it
+  easier for bridges to check homeserver media size limits.
+* Added wrapper for the `/register/available` endpoint.
+* Added custom user agent to all requests mautrix-go makes. The value can be
+  customized by changing the `DefaultUserAgent` variable.
+* Implemented [MSC3664], [MSC3862] and [MSC3873] in the push rule evaluator.
+* Added workaround for potential race conditions in OTK uploads when using
+  appservice encryption ([MSC3202]).
+* Fixed generating registrations to use `.+` instead of `[0-9]+` in the
+  username regex.
+* Fixed panic in megolm session listing methods if the store contains withheld
+  key entries.
+* Fixed missing header in bridge command help messages.
+
+[MSC3664]: https://github.com/matrix-org/matrix-spec-proposals/pull/3664
+[MSC3862]: https://github.com/matrix-org/matrix-spec-proposals/pull/3862
+[MSC3873]: https://github.com/matrix-org/matrix-spec-proposals/pull/3873
+
+## v0.12.1 (2022-09-16)
+
+* Bumped minimum Go version to 1.18.
+* Added `omitempty` for a bunch of fields in response structs to make them more
+  usable for server implementations.
+* Added `util.RandomToken` to generate GitHub-style access tokens with checksums.
+* Added utilities to call the push gateway API.
+* Added `unread_notifications` and [MSC2654] `unread_count` fields to /sync
+  response structs.
+* Implemented [MSC3870] for uploading and downloading media directly to/from an
+  external media storage like S3.
+* Fixed dbutil database ownership checks on SQLite.
+* Fixed typo in unauthorized encryption key withheld code
+  (`m.unauthorized` -> `m.unauthorised`).
+* Fixed [MSC2409] support to have a separate field for to-device events.
+
+[MSC2654]: https://github.com/matrix-org/matrix-spec-proposals/pull/2654
+[MSC3870]: https://github.com/matrix-org/matrix-spec-proposals/pull/3870
+
+## v0.12.0 (2022-08-16)
+
+* **Breaking change:** Switched `Client.UserTyping` to take a `time.Duration`
+  instead of raw `int64` milliseconds.
+* **Breaking change:** Removed custom reply relation type and switched to using
+  the wire format (nesting in `m.in_reply_to`).
+* Added device ID to appservice OTK count map to match updated [MSC3202].
+  This is also a breaking change, but the previous incorrect behavior wasn't
+  implemented by anything other than mautrix-syncproxy/imessage.
+* (There are probably other breaking changes too).
+* Added database utility and schema upgrade framework
+  * Originally from mautrix-whatsapp, but usable for non-bridges too
+  * Includes connection wrapper to log query durations and mutate queries for
+    SQLite compatibility (replacing `$x` with `?x`).
+* Added bridge utilities similar to mautrix-python. Currently includes:
+  * Crypto helper
+  * Startup flow
+  * Command handling and some standard commands
+  * Double puppeting things
+  * Generic parts of config, basic config validation
+  * Appservice SQL state store
+* Added alternative markdown spoiler parsing extension that doesn't support
+  reasons, but works better otherwise.
+* Added Discord underline markdown parsing extension (`_foo_` -> <u>foo</u>).
+* Added support for parsing spoilers and color tags in the HTML parser.
+* Added support for mutating plain text nodes in the HTML parser.
+* Added room version field to the create room request struct.
+* Added empty JSON object as default request body for all non-GET requests.
+* Added wrapper for `/capabilities` endpoint.
+* Added `omitempty` markers for lots of structs to make the structs easier to
+  use on the server side too.
+* Added support for registering to-device event handlers via the default
+  Syncer's `OnEvent` and `OnEventType` methods.
+* Fixed `CreateEventContent` using the wrong field name for the room version
+  field.
+* Fixed `StopSync` not immediately cancelling the sync loop if it was sleeping
+  after a failed sync.
+* Fixed `GetAvatarURL` always returning the current user's avatar instead of
+  the specified user's avatar (thanks to [@nightmared] in [#83]).
+* Improved request logging and added new log when a request finishes.
+* Crypto store improvements:
+  * Deleted devices are now kept in the database.
+  * Made ValidateMessageIndex atomic.
+* Moved `appservice.RandomString` to the `util` package and made it use
+  `crypto/rand` instead of `math/rand`.
+* Significantly improved cross-signing validation code.
+  * There are now more options for required trust levels,
+    e.g. you can set `SendKeysMinTrust` to `id.TrustStateCrossSignedTOFU`
+    to trust the first cross-signing master key seen and require all devices
+    to be signed by that key.
+  * Trust state of incoming messages is automatically resolved and stored in
+    `evt.Mautrix.TrustState`. This can be used to reject incoming messages from
+    untrusted devices.
+
+[@nightmared]: https://github.com/nightmared
+[#83]: https://github.com/mautrix/go/pull/83
+
+## v0.11.1 (2023-01-15)
+
+* Fixed parsing non-positive ordered list start positions in HTML parser
+  (backport of the same fix in v0.13.0).
+
+## v0.11.0 (2022-05-16)
+
+* Bumped minimum Go version to 1.17.
+* Switched from `/r0` to `/v3` paths everywhere.
+  * The new `v3` paths are implemented since Synapse 1.48, Dendrite 0.6.5, and
+    Conduit 0.4.0. Servers older than these are no longer supported.
+* Switched from blackfriday to goldmark for markdown parsing in the `format`
+  module and added spoiler syntax.
+* Added `EncryptInPlace` and `DecryptInPlace` methods for attachment encryption.
+  In most cases the plain/ciphertext is not necessary after en/decryption, so
+  the old `Encrypt` and `Decrypt` are deprecated.
+* Added wrapper for `/rooms/.../aliases`.
+* Added utility for adding/removing emoji variation selectors to match
+  recommendations on reactions in Matrix.
+* Added support for async media uploads ([MSC2246]).
+* Added automatic sleep when receiving 429 error
+  (thanks to [@ownaginatious] in [#44]).
+* Added support for parsing spec version numbers from the `/versions` endpoint.
+* Removed unstable prefixed constant used for appservice login.
+* Fixed URL encoding not working correctly in some cases.
+
+[MSC2246]: https://github.com/matrix-org/matrix-spec-proposals/pull/2246
+[@ownaginatious]: https://github.com/ownaginatious
+[#44]: https://github.com/mautrix/go/pull/44
+
+## v0.10.12 (2022-03-16)
+
+* Added option to use a different `Client` to send invites in
+  `IntentAPI.EnsureJoined`.
+* Changed `MessageEventContent` struct to omit empty `msgtype`s in the output
+  JSON, as sticker events shouldn't have that field.
+* Fixed deserializing the `thumbnail_file` field in `FileInfo`.
+* Fixed bug that broke `SQLCryptoStore.FindDeviceByKey`.
+
+## v0.10.11 (2022-02-16)
+
+* Added automatic updating of state store from `IntentAPI` calls.
+* Added option to ignore cache in `IntentAPI.EnsureJoined`.
+* Added `GetURLPreview` as a wrapper for the `/preview_url` media repo endpoint.
+* Moved base58 module inline to avoid pulling in btcd as a dependency.
+
+## v0.10.10 (2022-01-16)
+
+* Added event types and content structs for server ACLs and moderation policy
+  lists (thanks to [@qua3k] in [#59] and [#60]).
+* Added optional parameter to `Client.LeaveRoom` to pass a `reason` field.
+
+[#59]: https://github.com/mautrix/go/pull/59
+[#60]: https://github.com/mautrix/go/pull/60
+
+## v0.10.9 (2022-01-04)
+
+* **Breaking change:** Changed `Messages()` to take a filter as a parameter
+  instead of using the syncer's filter (thanks to [@qua3k] in [#55] and [#56]).
+  * The previous filter behavior was completely broken, as it sent a whole
+    filter instead of just a RoomEventFilter.
+  * Passing `nil` as the filter is fine and will disable filtering
+    (which is equivalent to what it did before with the invalid filter).
+* Added `Context()` wrapper for the `/context` API (thanks to [@qua3k] in [#54]).
+* Added utility for converting media files with ffmpeg.
+
+[#54]: https://github.com/mautrix/go/pull/54
+[#55]: https://github.com/mautrix/go/pull/55
+[#56]: https://github.com/mautrix/go/pull/56
+[@qua3k]: https://github.com/qua3k
+
+## v0.10.8 (2021-12-30)
+
+* Added `OlmSession.Describe()` to wrap `olm_session_describe`.
+* Added trace logs to log olm session descriptions when encrypting/decrypting
+  to-device messages.
+* Added space event types and content structs.
+* Added support for power level content override field in `CreateRoom`.
+* Fixed ordering of olm sessions which would cause an old session to be used in
+  some cases even after a client created a new session.
+
+## v0.10.7 (2021-12-16)
+
+* Changed `Client.RedactEvent` to allow arbitrary fields in redaction request.
+
+## v0.10.5 (2021-12-06)
+
+* Fixed websocket disconnection not clearing all pending requests.
+* Added `OlmMachine.SendRoomKeyRequest` as a more direct way of sending room
+  key requests.
+* Added automatic Olm session recreation if an incoming message fails to decrypt.
+* Changed `Login` to only omit request content from logs if there's a password
+  or token (appservice logins don't have sensitive content).
+
+## v0.10.4 (2021-11-25)
+
+* Added `reason` field to invite and unban requests
+  (thanks to [@ptman] in [#48]).
+* Fixed `AppService.HasWebsocket()` returning `true` even after websocket has
+  disconnected.
+
+[@ptman]: https://github.com/ptman
+[#48]: https://github.com/mautrix/go/pull/48
+
+## v0.10.3 (2021-11-18)
+
+* Added logs about incoming appservice transactions.
+* Added support for message send checkpoints (as HTTP requests, similar to the
+  bridge state reporting system).
+
+## v0.10.2 (2021-11-15)
+
+* Added utility method for finding the first supported login flow matching any
+  of the given types.
+* Updated registering appservice ghosts to use `inhibit_login` flag to prevent
+  lots of unnecessary access tokens from being created.
+  * If you want to log in as an appservice ghost, you should use [MSC2778]'s
+    appservice login (e.g. like [mautrix-whatsapp does for e2be](https://github.com/mautrix/whatsapp/blob/v0.2.1/crypto.go#L143-L149)).
+
+## v0.10.1 (2021-11-05)
+
+* Removed direct dependency on `pq`
+  * In order to use some more efficient queries on postgres, you must set
+    `crypto.PostgresArrayWrapper = pq.Array` if you want to use both postgres
+    and e2ee.
+* Added temporary hack to ignore state events with the MSC2716 historical flag
+  (to be removed after [matrix-org/synapse#11265] is merged)
+* Added received transaction acknowledgements for websocket appservice
+  transactions.
+* Added automatic fallback to move `prev_content` from top level to the
+  standard location inside `unsigned`.
+
+[matrix-org/synapse#11265]: https://github.com/matrix-org/synapse/pull/11265
+
+## v0.9.31 (2021-10-27)
+
+* Added `SetEdit` utility function for `MessageEventContent`.
+
+## v0.9.30 (2021-10-26)
+
+* Added wrapper for [MSC2716]'s `/batch_send` endpoint.
+* Added `MarshalJSON` method for `Event` struct to prevent empty unsigned
+  structs from being included in the JSON.
+
+[MSC2716]: https://github.com/matrix-org/matrix-spec-proposals/pull/2716
+
+## v0.9.29 (2021-09-30)
+
+* Added `client.State` method to get full room state.
+* Added bridge info structs and event types ([MSC2346]).
+* Made response handling more customizable.
+* Fixed type of `AuthType` constants.
+
+[MSC2346]: https://github.com/matrix-org/matrix-spec-proposals/pull/2346
+
+## v0.9.28 (2021-09-30)
+
+* Added `X-Mautrix-Process-ID` to appservice websocket headers to help debug
+  issues where multiple instances are connecting to the server at the same time.
+
+## v0.9.27 (2021-09-23)
+
+* Fixed Go 1.14 compatibility (broken in v0.9.25).
+* Added GitHub actions CI to build, test and check formatting on Go 1.14-1.17.
+
+## v0.9.26 (2021-09-21)
+
+* Added default no-op logger to `Client` in order to prevent panic when the
+  application doesn't set a logger.
+
+## v0.9.25 (2021-09-19)
+
+* Disabled logging request JSON for sensitive requests like `/login`,
+  `/register` and other UIA endpoints. Logging can still be enabled by
+  setting `MAUTRIX_LOG_SENSITIVE_CONTENT` to `yes`.
+* Added option to store new homeserver URL from `/login` response well-known data.
+* Added option to stream big sync responses via disk to maybe reduce memory usage.
+* Fixed trailing slashes in homeserver URL breaking all requests.
+
+## v0.9.24 (2021-09-03)
+
+* Added write deadline for appservice websocket connection.
+
+## v0.9.23 (2021-08-31)
+
+* Fixed storing e2ee key withheld events in the SQL store.
+
+## v0.9.22 (2021-08-30)
+
+* Updated appservice handler to cache multiple recent transaction IDs
+  instead of only the most recent one.
+
+## v0.9.21 (2021-08-25)
+
+* Added liveness and readiness endpoints to appservices.
+  * The endpoints are the same as mautrix-python:
+    `/_matrix/mau/live` and `/_matrix/mau/ready`
+  * Liveness always returns 200 and an empty JSON object by default,
+    but it can be turned off by setting `appservice.Live` to `false`.
+  * Readiness defaults to returning 500, and it can be switched to 200
+    by setting `appservice.Ready` to `true`.
+
+## v0.9.20 (2021-08-19)
+
+* Added crypto store migration for converting all `VARCHAR(255)` columns
+  to `TEXT` in Postgres databases.
+
+## v0.9.19 (2021-08-17)
+
+* Fixed HTML parser outputting two newlines after paragraph tags.
+
+## v0.9.18 (2021-08-16)
+
+* Added new `BuildURL` method that does the same as `Client.BuildBaseURL`
+  but without requiring the `Client` instance.
+
+## v0.9.17 (2021-07-25)
+
+* Fixed handling OTK counts and device lists coming in through the appservice
+  transaction websocket.
+* Updated OlmMachine to ignore OTK counts intended for other devices.
+
+## v0.9.15 (2021-07-16)
+
+* Added support for [MSC3202] and the to-device part of [MSC2409] in the
+  appservice package.
+* Added support for sending commands through appservice websocket.
+* Changed error message JSON field name in appservice error responses to
+  conform with standard Matrix errors (`message` -> `error`).
+
+[MSC3202]: https://github.com/matrix-org/matrix-spec-proposals/pull/3202
+
+## v0.9.14 (2021-06-17)
+
+* Added default implementation of `PillConverter` in HTML parser utility.
+
+## v0.9.13 (2021-06-15)
+
+* Added support for parsing and generating encoded matrix.to URLs and `matrix:` URIs ([MSC2312](https://github.com/matrix-org/matrix-doc/pull/2312)).
+* Updated HTML parser to use new URI parser for parsing user/room pills.
+
+## v0.9.12 (2021-05-18)
+
+* Added new method for sending custom data with read receipts
+  (not currently a part of the spec).
+
+## v0.9.11 (2021-05-12)
+
+* Improved debug log for unsupported event types.
+* Added VoIP events to GuessClass.
+* Added support for parsing strings in VoIP event version field.
+
+## v0.9.10 (2021-04-29)
+
+* Fixed `format.RenderMarkdown()` still allowing HTML when both `allowHTML`
+  and `allowMarkdown` are `false`.
+
+## v0.9.9 (2021-04-26)
+
+* Updated appservice `StartWebsocket` to return websocket close info.
+
+## v0.9.8 (2021-04-20)
+
+* Added methods for getting room tags and account data.
+
+## v0.9.7 (2021-04-19)
+
+* **Breaking change (crypto):** `SendEncryptedToDevice` now requires an event
+  type parameter. Previously it only allowed sending events of type
+  `event.ToDeviceForwardedRoomKey`.
+* Added content structs for VoIP events.
+* Added global mutex for Olm decryption
+  (previously it was only used for encryption).
+
+## v0.9.6 (2021-04-15)
+
+* Added option to retry all HTTP requests when encountering a HTTP network
+  error or gateway error response (502/503/504)
+  * Disabled by default, you need to set the `DefaultHTTPRetries` field in
+    the `AppService` or `Client` struct to enable.
+  * Can also be enabled with `FullRequest`s `MaxAttempts` field.
+
+## v0.9.5 (2021-04-06)
+
+* Reverted update of `golang.org/x/sys` which broke Go 1.14 / darwin/arm.
+
+## v0.9.4 (2021-04-06)
+
+* Switched appservices to using shared `http.Client` instance with a in-memory
+  cookie jar.
+
+## v0.9.3 (2021-03-26)
+
+* Made user agent headers easier to configure.
+* Improved logging when receiving weird/unhandled to-device events.
+
+## v0.9.2 (2021-03-15)
+
+* Fixed type of presence state constants (thanks to [@babolivier] in [#30]).
+* Implemented presence state fetching methods (thanks to [@babolivier] in [#29]).
+* Added support for sending and receiving commands via appservice transaction websocket.
+
+[@babolivier]: https://github.com/babolivier
+[#29]: https://github.com/mautrix/go/pull/29
+[#30]: https://github.com/mautrix/go/pull/30
+
+## v0.9.1 (2021-03-11)
+
+* Fixed appservice register request hiding actual errors due to UIA error handling.
+
+## v0.9.0 (2021-03-04)
+
+* **Breaking change (manual API requests):** `MakeFullRequest` now takes a
+  `FullRequest` struct instead of individual parameters. `MakeRequest`'s
+  parameters are unchanged.
+* **Breaking change (manual /sync):** `SyncRequest` now requires a `Context`
+  parameter.
+* **Breaking change (end-to-bridge encryption):**
+  the `uk.half-shot.msc2778.login.application_service` constant used for
+  appservice login ([MSC2778]) was renamed from `AuthTypeAppservice`
+  to `AuthTypeHalfyAppservice`.
+  * The `AuthTypeAppservice` constant now contains `m.login.application_service`,
+    which is currently only used for registrations, but will also be used for
+    login once MSC2778 lands in the spec.
+* Fixed appservice registration requests to include `m.login.application_service`
+  as the `type` (re [matrix-org/synapse#9548]).
+* Added wrapper for `/logout/all`.
+
+[MSC2778]: https://github.com/matrix-org/matrix-spec-proposals/pull/2778
+[matrix-org/synapse#9548]: https://github.com/matrix-org/synapse/pull/9548
+
+## v0.8.6 (2021-03-02)
+
+* Added client-side timeout to `mautrix.Client`'s `http.Client`
+  (defaults to 3 minutes).
+* Updated maulogger to fix bug where plaintext file logs wouldn't have newlines.
+
+## v0.8.5 (2021-02-26)
+
+* Fixed potential concurrent map writes in appservice `Client` and `Intent`
+  methods.
+
+## v0.8.4 (2021-02-24)
+
+* Added option to output appservice logs as JSON.
+* Added new methods for validating user ID localparts.
+
+## v0.8.3 (2021-02-21)
+
+* Allowed empty content URIs in parser
+* Added functions for device management endpoints
+  (thanks to [@edwargix] in [#26]).
+
+[@edwargix]: https://github.com/edwargix
+[#26]: https://github.com/mautrix/go/pull/26
+
+## v0.8.2 (2021-02-09)
+
+* Fixed error when removing the user's avatar.
+
+## v0.8.1 (2021-02-09)
+
+* Added AccountDataStore to remove the need for persistent local storage other
+  than the access token (thanks to [@daenney] in [#23]).
+* Added support for receiving appservice transactions over websocket.
+  See <https://github.com/mautrix/wsproxy> for the server-side implementation.
+* Fixed error when removing the room avatar.
+
+[@daenney]: https://github.com/daenney
+[#23]: https://github.com/mautrix/go/pull/23
+
+## v0.8.0 (2020-12-24)
+
+* **Breaking change:** the `RateLimited` field in the `Registration` struct is
+  now a pointer, so that it can be omitted entirely.
+* Merged initial SSSS/cross-signing code by [@nikofil]. Interactive verification
+  doesn't work, but the other things mostly do.
+* Added support for authorization header auth in appservices ([MSC2832]).
+* Added support for receiving ephemeral events directly ([MSC2409]).
+* Fixed `SendReaction()` and other similar methods in the `Client` struct.
+* Fixed crypto cgo code panicking in Go 1.15.3+.
+* Fixed olm session locks sometime getting deadlocked.
+
+[MSC2832]: https://github.com/matrix-org/matrix-spec-proposals/pull/2832
+[MSC2409]: https://github.com/matrix-org/matrix-spec-proposals/pull/2409
+[@nikofil]: https://github.com/nikofil
diff --git a/vendor/maunium.net/go/mautrix/LICENSE b/vendor/maunium.net/go/mautrix/LICENSE
new file mode 100644
index 0000000..a612ad9
--- /dev/null
+++ b/vendor/maunium.net/go/mautrix/LICENSE
@@ -0,0 +1,373 @@
+Mozilla Public License Version 2.0
+==================================
+
+1. Definitions
+--------------
+
+1.1. "Contributor"
+    means each individual or legal entity that creates, contributes to
+    the creation of, or owns Covered Software.
+
+1.2. "Contributor Version"
+    means the combination of the Contributions of others (if any) used
+    by a Contributor and that particular Contributor's Contribution.
+
+1.3. "Contribution"
+    means Covered Software of a particular Contributor.
+
+1.4. "Covered Software"
+    means Source Code Form to which the initial Contributor has attached
+    the notice in Exhibit A, the Executable Form of such Source Code
+    Form, and Modifications of such Source Code Form, in each case
+    including portions thereof.
+
+1.5. "Incompatible With Secondary Licenses"
+    means
+
+    (a) that the initial Contributor has attached the notice described
+        in Exhibit B to the Covered Software; or
+
+    (b) that the Covered Software was made available under the terms of
+        version 1.1 or earlier of the License, but not also under the
+        terms of a Secondary License.
+
+1.6. "Executable Form"
+    means any form of the work other than Source Code Form.
+
+1.7. "Larger Work"
+    means a work that combines Covered Software with other material, in
+    a separate file or files, that is not Covered Software.
+
+1.8. "License"
+    means this document.
+
+1.9. "Licensable"
+    means having the right to grant, to the maximum extent possible,
+    whether at the time of the initial grant or subsequently, any and
+    all of the rights conveyed by this License.
+
+1.10. "Modifications"
+    means any of the following:
+
+    (a) any file in Source Code Form that results from an addition to,
+        deletion from, or modification of the contents of Covered
+        Software; or
+
+    (b) any new file in Source Code Form that contains any Covered
+        Software.
+
+1.11. "Patent Claims" of a Contributor
+    means any patent claim(s), including without limitation, method,
+    process, and apparatus claims, in any patent Licensable by such
+    Contributor that would be infringed, but for the grant of the
+    License, by the making, using, selling, offering for sale, having
+    made, import, or transfer of either its Contributions or its
+    Contributor Version.
+
+1.12. "Secondary License"
+    means either the GNU General Public License, Version 2.0, the GNU
+    Lesser General Public License, Version 2.1, the GNU Affero General
+    Public License, Version 3.0, or any later versions of those
+    licenses.
+
+1.13. "Source Code Form"
+    means the form of the work preferred for making modifications.
+
+1.14. "You" (or "Your")
+    means an individual or a legal entity exercising rights under this
+    License. For legal entities, "You" includes any entity that
+    controls, is controlled by, or is under common control with You. For
+    purposes of this definition, "control" means (a) the power, direct
+    or indirect, to cause the direction or management of such entity,
+    whether by contract or otherwise, or (b) ownership of more than
+    fifty percent (50%) of the outstanding shares or beneficial
+    ownership of such entity.
+
+2. License Grants and Conditions
+--------------------------------
+
+2.1. Grants
+
+Each Contributor hereby grants You a world-wide, royalty-free,
+non-exclusive license:
+
+(a) under intellectual property rights (other than patent or trademark)
+    Licensable by such Contributor to use, reproduce, make available,
+    modify, display, perform, distribute, and otherwise exploit its
+    Contributions, either on an unmodified basis, with Modifications, or
+    as part of a Larger Work; and
+
+(b) under Patent Claims of such Contributor to make, use, sell, offer
+    for sale, have made, import, and otherwise transfer either its
+    Contributions or its Contributor Version.
+
+2.2. Effective Date
+
+The licenses granted in Section 2.1 with respect to any Contribution
+become effective for each Contribution on the date the Contributor first
+distributes such Contribution.
+
+2.3. Limitations on Grant Scope
+
+The licenses granted in this Section 2 are the only rights granted under
+this License. No additional rights or licenses will be implied from the
+distribution or licensing of Covered Software under this License.
+Notwithstanding Section 2.1(b) above, no patent license is granted by a
+Contributor:
+
+(a) for any code that a Contributor has removed from Covered Software;
+    or
+
+(b) for infringements caused by: (i) Your and any other third party's
+    modifications of Covered Software, or (ii) the combination of its
+    Contributions with other software (except as part of its Contributor
+    Version); or
+
+(c) under Patent Claims infringed by Covered Software in the absence of
+    its Contributions.
+
+This License does not grant any rights in the trademarks, service marks,
+or logos of any Contributor (except as may be necessary to comply with
+the notice requirements in Section 3.4).
+
+2.4. Subsequent Licenses
+
+No Contributor makes additional grants as a result of Your choice to
+distribute the Covered Software under a subsequent version of this
+License (see Section 10.2) or under the terms of a Secondary License (if
+permitted under the terms of Section 3.3).
+
+2.5. Representation
+
+Each Contributor represents that the Contributor believes its
+Contributions are its original creation(s) or it has sufficient rights
+to grant the rights to its Contributions conveyed by this License.
+
+2.6. Fair Use
+
+This License is not intended to limit any rights You have under
+applicable copyright doctrines of fair use, fair dealing, or other
+equivalents.
+
+2.7. Conditions
+
+Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted
+in Section 2.1.
+
+3. Responsibilities
+-------------------
+
+3.1. Distribution of Source Form
+
+All distribution of Covered Software in Source Code Form, including any
+Modifications that You create or to which You contribute, must be under
+the terms of this License. You must inform recipients that the Source
+Code Form of the Covered Software is governed by the terms of this
+License, and how they can obtain a copy of this License. You may not
+attempt to alter or restrict the recipients' rights in the Source Code
+Form.
+
+3.2. Distribution of Executable Form
+
+If You distribute Covered Software in Executable Form then:
+
+(a) such Covered Software must also be made available in Source Code
+    Form, as described in Section 3.1, and You must inform recipients of
+    the Executable Form how they can obtain a copy of such Source Code
+    Form by reasonable means in a timely manner, at a charge no more
+    than the cost of distribution to the recipient; and
+
+(b) You may distribute such Executable Form under the terms of this
+    License, or sublicense it under different terms, provided that the
+    license for the Executable Form does not attempt to limit or alter
+    the recipients' rights in the Source Code Form under this License.
+
+3.3. Distribution of a Larger Work
+
+You may create and distribute a Larger Work under terms of Your choice,
+provided that You also comply with the requirements of this License for
+the Covered Software. If the Larger Work is a combination of Covered
+Software with a work governed by one or more Secondary Licenses, and the
+Covered Software is not Incompatible With Secondary Licenses, this
+License permits You to additionally distribute such Covered Software
+under the terms of such Secondary License(s), so that the recipient of
+the Larger Work may, at their option, further distribute the Covered
+Software under the terms of either this License or such Secondary
+License(s).
+
+3.4. Notices
+
+You may not remove or alter the substance of any license notices
+(including copyright notices, patent notices, disclaimers of warranty,
+or limitations of liability) contained within the Source Code Form of
+the Covered Software, except that You may alter any license notices to
+the extent required to remedy known factual inaccuracies.
+
+3.5. Application of Additional Terms
+
+You may choose to offer, and to charge a fee for, warranty, support,
+indemnity or liability obligations to one or more recipients of Covered
+Software. However, You may do so only on Your own behalf, and not on
+behalf of any Contributor. You must make it absolutely clear that any
+such warranty, support, indemnity, or liability obligation is offered by
+You alone, and You hereby agree to indemnify every Contributor for any
+liability incurred by such Contributor as a result of warranty, support,
+indemnity or liability terms You offer. You may include additional
+disclaimers of warranty and limitations of liability specific to any
+jurisdiction.
+
+4. Inability to Comply Due to Statute or Regulation
+---------------------------------------------------
+
+If it is impossible for You to comply with any of the terms of this
+License with respect to some or all of the Covered Software due to
+statute, judicial order, or regulation then You must: (a) comply with
+the terms of this License to the maximum extent possible; and (b)
+describe the limitations and the code they affect. Such description must
+be placed in a text file included with all distributions of the Covered
+Software under this License. Except to the extent prohibited by statute
+or regulation, such description must be sufficiently detailed for a
+recipient of ordinary skill to be able to understand it.
+
+5. Termination
+--------------
+
+5.1. The rights granted under this License will terminate automatically
+if You fail to comply with any of its terms. However, if You become
+compliant, then the rights granted under this License from a particular
+Contributor are reinstated (a) provisionally, unless and until such
+Contributor explicitly and finally terminates Your grants, and (b) on an
+ongoing basis, if such Contributor fails to notify You of the
+non-compliance by some reasonable means prior to 60 days after You have
+come back into compliance. Moreover, Your grants from a particular
+Contributor are reinstated on an ongoing basis if such Contributor
+notifies You of the non-compliance by some reasonable means, this is the
+first time You have received notice of non-compliance with this License
+from such Contributor, and You become compliant prior to 30 days after
+Your receipt of the notice.
+
+5.2. If You initiate litigation against any entity by asserting a patent
+infringement claim (excluding declaratory judgment actions,
+counter-claims, and cross-claims) alleging that a Contributor Version
+directly or indirectly infringes any patent, then the rights granted to
+You by any and all Contributors for the Covered Software under Section
+2.1 of this License shall terminate.
+
+5.3. In the event of termination under Sections 5.1 or 5.2 above, all
+end user license agreements (excluding distributors and resellers) which
+have been validly granted by You or Your distributors under this License
+prior to termination shall survive termination.
+
+************************************************************************
+*                                                                      *
+*  6. Disclaimer of Warranty                                           *
+*  -------------------------                                           *
+*                                                                      *
+*  Covered Software is provided under this License on an "as is"       *
+*  basis, without warranty of any kind, either expressed, implied, or  *
+*  statutory, including, without limitation, warranties that the       *
+*  Covered Software is free of defects, merchantable, fit for a        *
+*  particular purpose or non-infringing. The entire risk as to the     *
+*  quality and performance of the Covered Software is with You.        *
+*  Should any Covered Software prove defective in any respect, You     *
+*  (not any Contributor) assume the cost of any necessary servicing,   *
+*  repair, or correction. This disclaimer of warranty constitutes an   *
+*  essential part of this License. No use of any Covered Software is   *
+*  authorized under this License except under this disclaimer.         *
+*                                                                      *
+************************************************************************
+
+************************************************************************
+*                                                                      *
+*  7. Limitation of Liability                                          *
+*  --------------------------                                          *
+*                                                                      *
+*  Under no circumstances and under no legal theory, whether tort      *
+*  (including negligence), contract, or otherwise, shall any           *
+*  Contributor, or anyone who distributes Covered Software as          *
+*  permitted above, be liable to You for any direct, indirect,         *
+*  special, incidental, or consequential damages of any character      *
+*  including, without limitation, damages for lost profits, loss of    *
+*  goodwill, work stoppage, computer failure or malfunction, or any    *
+*  and all other commercial damages or losses, even if such party      *
+*  shall have been informed of the possibility of such damages. This   *
+*  limitation of liability shall not apply to liability for death or   *
+*  personal injury resulting from such party's negligence to the       *
+*  extent applicable law prohibits such limitation. Some               *
+*  jurisdictions do not allow the exclusion or limitation of           *
+*  incidental or consequential damages, so this exclusion and          *
+*  limitation may not apply to You.                                    *
+*                                                                      *
+************************************************************************
+
+8. Litigation
+-------------
+
+Any litigation relating to this License may be brought only in the
+courts of a jurisdiction where the defendant maintains its principal
+place of business and such litigation shall be governed by laws of that
+jurisdiction, without reference to its conflict-of-law provisions.
+Nothing in this Section shall prevent a party's ability to bring
+cross-claims or counter-claims.
+
+9. Miscellaneous
+----------------
+
+This License represents the complete agreement concerning the subject
+matter hereof. If any provision of this License is held to be
+unenforceable, such provision shall be reformed only to the extent
+necessary to make it enforceable. Any law or regulation which provides
+that the language of a contract shall be construed against the drafter
+shall not be used to construe this License against a Contributor.
+
+10. Versions of the License
+---------------------------
+
+10.1. New Versions
+
+Mozilla Foundation is the license steward. Except as provided in Section
+10.3, no one other than the license steward has the right to modify or
+publish new versions of this License. Each version will be given a
+distinguishing version number.
+
+10.2. Effect of New Versions
+
+You may distribute the Covered Software under the terms of the version
+of the License under which You originally received the Covered Software,
+or under the terms of any subsequent version published by the license
+steward.
+
+10.3. Modified Versions
+
+If you create software not governed by this License, and you want to
+create a new license for such software, you may create and use a
+modified version of this License if you rename the license and remove
+any references to the name of the license steward (except to note that
+such modified license differs from this License).
+
+10.4. Distributing Source Code Form that is Incompatible With Secondary
+Licenses
+
+If You choose to distribute Source Code Form that is Incompatible With
+Secondary Licenses under the terms of this version of the License, the
+notice described in Exhibit B of this License must be attached.
+
+Exhibit A - Source Code Form License Notice
+-------------------------------------------
+
+  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/.
+
+If it is not possible or desirable to put the notice in a particular
+file, then You may include the notice in a location (such as a LICENSE
+file in a relevant directory) where a recipient would be likely to look
+for such a notice.
+
+You may add additional accurate notices of copyright ownership.
+
+Exhibit B - "Incompatible With Secondary Licenses" Notice
+---------------------------------------------------------
+
+  This Source Code Form is "Incompatible With Secondary Licenses", as
+  defined by the Mozilla Public License, v. 2.0.
diff --git a/vendor/maunium.net/go/mautrix/README.md b/vendor/maunium.net/go/mautrix/README.md
new file mode 100644
index 0000000..ac41ca7
--- /dev/null
+++ b/vendor/maunium.net/go/mautrix/README.md
@@ -0,0 +1,22 @@
+# mautrix-go
+[![GoDoc](https://pkg.go.dev/badge/maunium.net/go/mautrix)](https://pkg.go.dev/maunium.net/go/mautrix)
+
+A Golang Matrix framework. Used by [gomuks](https://matrix.org/docs/projects/client/gomuks),
+[go-neb](https://github.com/matrix-org/go-neb), [mautrix-whatsapp](https://github.com/mautrix/whatsapp)
+and others.
+
+Matrix room: [`#go:maunium.net`](https://matrix.to/#/#go:maunium.net)
+
+This project is based on [matrix-org/gomatrix](https://github.com/matrix-org/gomatrix).
+The original project is licensed under [Apache 2.0](https://github.com/matrix-org/gomatrix/blob/master/LICENSE).
+
+In addition to the basic client API features the original project has, this framework also has:
+
+* Appservice support (Intent API like mautrix-python, room state storage, etc)
+* End-to-end encryption support (incl. interactive SAS verification)
+* High-level module for building puppeting bridges
+* High-level module for building chat clients
+* Wrapper functions for the Synapse admin API
+* Structs for parsing event content
+* Helpers for parsing and generating Matrix HTML
+* Helpers for handling push rules
diff --git a/vendor/maunium.net/go/mautrix/client.go b/vendor/maunium.net/go/mautrix/client.go
new file mode 100644
index 0000000..b85d86f
--- /dev/null
+++ b/vendor/maunium.net/go/mautrix/client.go
@@ -0,0 +1,2391 @@
+// Package mautrix implements the Matrix Client-Server API.
+//
+// Specification can be found at https://spec.matrix.org/v1.2/client-server-api/
+package mautrix
+
+import (
+	"bytes"
+	"context"
+	"encoding/json"
+	"errors"
+	"fmt"
+	"io"
+	"net/http"
+	"net/url"
+	"os"
+	"slices"
+	"strconv"
+	"strings"
+	"sync/atomic"
+	"time"
+
+	"github.com/rs/zerolog"
+	"go.mau.fi/util/ptr"
+	"go.mau.fi/util/retryafter"
+	"golang.org/x/exp/maps"
+
+	"maunium.net/go/mautrix/crypto/backup"
+	"maunium.net/go/mautrix/event"
+	"maunium.net/go/mautrix/id"
+	"maunium.net/go/mautrix/pushrules"
+)
+
+type CryptoHelper interface {
+	Encrypt(context.Context, id.RoomID, event.Type, any) (*event.EncryptedEventContent, error)
+	Decrypt(context.Context, *event.Event) (*event.Event, error)
+	WaitForSession(context.Context, id.RoomID, id.SenderKey, id.SessionID, time.Duration) bool
+	RequestSession(context.Context, id.RoomID, id.SenderKey, id.SessionID, id.UserID, id.DeviceID)
+	Init(context.Context) error
+}
+
+type VerificationHelper interface {
+	// Init initializes the helper. This should be called before any other
+	// methods.
+	Init(context.Context) error
+
+	// StartVerification starts an interactive verification flow with the given
+	// user via a to-device event.
+	StartVerification(ctx context.Context, to id.UserID) (id.VerificationTransactionID, error)
+	// StartInRoomVerification starts an interactive verification flow with the
+	// given user in the given room.
+	StartInRoomVerification(ctx context.Context, roomID id.RoomID, to id.UserID) (id.VerificationTransactionID, error)
+
+	// AcceptVerification accepts a verification request.
+	AcceptVerification(ctx context.Context, txnID id.VerificationTransactionID) error
+	// DismissVerification dismisses a verification request. This will not send
+	// a cancellation to the other device. This method should only be called
+	// *before* the request has been accepted and will error otherwise.
+	DismissVerification(ctx context.Context, txnID id.VerificationTransactionID) error
+	// CancelVerification cancels a verification request. This method should
+	// only be called *after* the request has been accepted, although it will
+	// not error if called beforehand.
+	CancelVerification(ctx context.Context, txnID id.VerificationTransactionID, code event.VerificationCancelCode, reason string) error
+
+	// HandleScannedQRData handles the data from a QR code scan.
+	HandleScannedQRData(ctx context.Context, data []byte) error
+	// ConfirmQRCodeScanned confirms that our QR code has been scanned.
+	ConfirmQRCodeScanned(ctx context.Context, txnID id.VerificationTransactionID) error
+
+	// StartSAS starts a SAS verification flow.
+	StartSAS(ctx context.Context, txnID id.VerificationTransactionID) error
+	// ConfirmSAS indicates that the user has confirmed that the SAS matches
+	// SAS shown on the other user's device.
+	ConfirmSAS(ctx context.Context, txnID id.VerificationTransactionID) error
+}
+
+// Client represents a Matrix client.
+type Client struct {
+	HomeserverURL *url.URL     // The base homeserver URL
+	UserID        id.UserID    // The user ID of the client. Used for forming HTTP paths which use the client's user ID.
+	DeviceID      id.DeviceID  // The device ID of the client.
+	AccessToken   string       // The access_token for the client.
+	UserAgent     string       // The value for the User-Agent header
+	Client        *http.Client // The underlying HTTP client which will be used to make HTTP requests.
+	Syncer        Syncer       // The thing which can process /sync responses
+	Store         SyncStore    // The thing which can store tokens/ids
+	StateStore    StateStore
+	Crypto        CryptoHelper
+	Verification  VerificationHelper
+	SpecVersions  *RespVersions
+
+	Log zerolog.Logger
+
+	RequestHook  func(req *http.Request)
+	ResponseHook func(req *http.Request, resp *http.Response, err error, duration time.Duration)
+
+	UpdateRequestOnRetry func(req *http.Request, cause error) *http.Request
+
+	SyncPresence event.Presence
+	SyncTraceLog bool
+
+	StreamSyncMinAge time.Duration
+
+	// Number of times that mautrix will retry any HTTP request
+	// if the request fails entirely or returns a HTTP gateway error (502-504)
+	DefaultHTTPRetries int
+	// Amount of time to wait between HTTP retries, defaults to 4 seconds
+	DefaultHTTPBackoff time.Duration
+	// Set to true to disable automatically sleeping on 429 errors.
+	IgnoreRateLimit bool
+
+	txnID int32
+
+	// Should the ?user_id= query parameter be set in requests?
+	// See https://spec.matrix.org/v1.6/application-service-api/#identity-assertion
+	SetAppServiceUserID bool
+	// Should the org.matrix.msc3202.device_id query parameter be set in requests?
+	// See https://github.com/matrix-org/matrix-spec-proposals/pull/3202
+	SetAppServiceDeviceID bool
+
+	syncingID uint32 // Identifies the current Sync. Only one Sync can be active at any given time.
+}
+
+type ClientWellKnown struct {
+	Homeserver     HomeserverInfo     `json:"m.homeserver"`
+	IdentityServer IdentityServerInfo `json:"m.identity_server"`
+}
+
+type HomeserverInfo struct {
+	BaseURL string `json:"base_url"`
+}
+
+type IdentityServerInfo struct {
+	BaseURL string `json:"base_url"`
+}
+
+// DiscoverClientAPI resolves the client API URL from a Matrix server name.
+// Use ParseUserID to extract the server name from a user ID.
+// https://spec.matrix.org/v1.2/client-server-api/#server-discovery
+func DiscoverClientAPI(ctx context.Context, serverName string) (*ClientWellKnown, error) {
+	wellKnownURL := url.URL{
+		Scheme: "https",
+		Host:   serverName,
+		Path:   "/.well-known/matrix/client",
+	}
+
+	req, err := http.NewRequestWithContext(ctx, http.MethodGet, wellKnownURL.String(), nil)
+	if err != nil {
+		return nil, err
+	}
+
+	req.Header.Set("Accept", "application/json")
+	req.Header.Set("User-Agent", DefaultUserAgent+" (.well-known fetcher)")
+
+	client := &http.Client{Timeout: 30 * time.Second}
+	resp, err := client.Do(req)
+	if err != nil {
+		return nil, err
+	}
+	defer resp.Body.Close()
+
+	if resp.StatusCode == http.StatusNotFound {
+		return nil, nil
+	}
+
+	data, err := io.ReadAll(resp.Body)
+	if err != nil {
+		return nil, err
+	}
+
+	var wellKnown ClientWellKnown
+	err = json.Unmarshal(data, &wellKnown)
+	if err != nil {
+		return nil, errors.New(".well-known response not JSON")
+	}
+
+	return &wellKnown, nil
+}
+
+// SetCredentials sets the user ID and access token on this client instance.
+//
+// Deprecated: use the StoreCredentials field in ReqLogin instead.
+func (cli *Client) SetCredentials(userID id.UserID, accessToken string) {
+	cli.AccessToken = accessToken
+	cli.UserID = userID
+}
+
+// ClearCredentials removes the user ID and access token on this client instance.
+func (cli *Client) ClearCredentials() {
+	cli.AccessToken = ""
+	cli.UserID = ""
+	cli.DeviceID = ""
+}
+
+// Sync starts syncing with the provided Homeserver. If Sync() is called twice then the first sync will be stopped and the
+// error will be nil.
+//
+// This function will block until a fatal /sync error occurs, so it should almost always be started as a new goroutine.
+// Fatal sync errors can be caused by:
+//   - The failure to create a filter.
+//   - Client.Syncer.OnFailedSync returning an error in response to a failed sync.
+//   - Client.Syncer.ProcessResponse returning an error.
+//
+// If you wish to continue retrying in spite of these fatal errors, call Sync() again.
+func (cli *Client) Sync() error {
+	return cli.SyncWithContext(context.Background())
+}
+
+func (cli *Client) SyncWithContext(ctx context.Context) error {
+	// Mark the client as syncing.
+	// We will keep syncing until the syncing state changes. Either because
+	// Sync is called or StopSync is called.
+	syncingID := cli.incrementSyncingID()
+	nextBatch, err := cli.Store.LoadNextBatch(ctx, cli.UserID)
+	if err != nil {
+		return err
+	}
+	filterID, err := cli.Store.LoadFilterID(ctx, cli.UserID)
+	if err != nil {
+		return err
+	}
+
+	if filterID == "" {
+		filterJSON := cli.Syncer.GetFilterJSON(cli.UserID)
+		resFilter, err := cli.CreateFilter(ctx, filterJSON)
+		if err != nil {
+			return err
+		}
+		filterID = resFilter.FilterID
+		if err := cli.Store.SaveFilterID(ctx, cli.UserID, filterID); err != nil {
+			return err
+		}
+	}
+	lastSuccessfulSync := time.Now().Add(-cli.StreamSyncMinAge - 1*time.Hour)
+	// Always do first sync with 0 timeout
+	isFailing := true
+	for {
+		streamResp := false
+		if cli.StreamSyncMinAge > 0 && time.Since(lastSuccessfulSync) > cli.StreamSyncMinAge {
+			cli.Log.Debug().Msg("Last sync is old, will stream next response")
+			streamResp = true
+		}
+		timeout := 30000
+		if isFailing {
+			timeout = 0
+		}
+		resSync, err := cli.FullSyncRequest(ctx, ReqSync{
+			Timeout:        timeout,
+			Since:          nextBatch,
+			FilterID:       filterID,
+			FullState:      false,
+			SetPresence:    cli.SyncPresence,
+			StreamResponse: streamResp,
+		})
+		if err != nil {
+			isFailing = true
+			if ctx.Err() != nil {
+				return ctx.Err()
+			}
+			duration, err2 := cli.Syncer.OnFailedSync(resSync, err)
+			if err2 != nil {
+				return err2
+			}
+			if duration <= 0 {
+				continue
+			}
+			select {
+			case <-ctx.Done():
+				return ctx.Err()
+			case <-time.After(duration):
+				continue
+			}
+		}
+		isFailing = false
+		lastSuccessfulSync = time.Now()
+
+		// Check that the syncing state hasn't changed
+		// Either because we've stopped syncing or another sync has been started.
+		// We discard the response from our sync.
+		if cli.getSyncingID() != syncingID {
+			return nil
+		}
+
+		// Save the token now *before* processing it. This means it's possible
+		// to not process some events, but it means that we won't get constantly stuck processing
+		// a malformed/buggy event which keeps making us panic.
+		err = cli.Store.SaveNextBatch(ctx, cli.UserID, resSync.NextBatch)
+		if err != nil {
+			return err
+		}
+		if err = cli.Syncer.ProcessResponse(ctx, resSync, nextBatch); err != nil {
+			return err
+		}
+
+		nextBatch = resSync.NextBatch
+	}
+}
+
+func (cli *Client) incrementSyncingID() uint32 {
+	return atomic.AddUint32(&cli.syncingID, 1)
+}
+
+func (cli *Client) getSyncingID() uint32 {
+	return atomic.LoadUint32(&cli.syncingID)
+}
+
+// StopSync stops the ongoing sync started by Sync.
+func (cli *Client) StopSync() {
+	// Advance the syncing state so that any running Syncs will terminate.
+	cli.incrementSyncingID()
+}
+
+type contextKey int
+
+const (
+	LogBodyContextKey contextKey = iota
+	LogRequestIDContextKey
+)
+
+func (cli *Client) RequestStart(req *http.Request) {
+	if cli.RequestHook != nil {
+		cli.RequestHook(req)
+	}
+}
+
+func (cli *Client) LogRequestDone(req *http.Request, resp *http.Response, err error, handlerErr error, contentLength int, duration time.Duration) {
+	var evt *zerolog.Event
+	if errors.Is(err, context.Canceled) {
+		evt = zerolog.Ctx(req.Context()).Warn()
+	} else if err != nil {
+		evt = zerolog.Ctx(req.Context()).Err(err)
+	} else if handlerErr != nil {
+		evt = zerolog.Ctx(req.Context()).Warn().
+			AnErr("body_parse_err", handlerErr)
+	} else if cli.SyncTraceLog && strings.HasSuffix(req.URL.Path, "/_matrix/client/v3/sync") {
+		evt = zerolog.Ctx(req.Context()).Trace()
+	} else {
+		evt = zerolog.Ctx(req.Context()).Debug()
+	}
+	evt = evt.
+		Str("method", req.Method).
+		Str("url", req.URL.String()).
+		Dur("duration", duration)
+	if cli.ResponseHook != nil {
+		cli.ResponseHook(req, resp, err, duration)
+	}
+	if resp != nil {
+		mime := resp.Header.Get("Content-Type")
+		length := resp.ContentLength
+		if length == -1 && contentLength > 0 {
+			length = int64(contentLength)
+		}
+		evt = evt.Int("status_code", resp.StatusCode).
+			Int64("response_length", length).
+			Str("response_mime", mime)
+		if serverRequestID := resp.Header.Get("X-Beeper-Request-ID"); serverRequestID != "" {
+			evt.Str("beeper_request_id", serverRequestID)
+		}
+	}
+	if body := req.Context().Value(LogBodyContextKey); body != nil {
+		evt.Interface("req_body", body)
+	}
+	if errors.Is(err, context.Canceled) {
+		evt.Msg("Request canceled")
+	} else if err != nil {
+		evt.Msg("Request failed")
+	} else if handlerErr != nil {
+		evt.Msg("Request parsing failed")
+	} else {
+		evt.Msg("Request completed")
+	}
+}
+
+func (cli *Client) MakeRequest(ctx context.Context, method string, httpURL string, reqBody any, resBody any) ([]byte, error) {
+	return cli.MakeFullRequest(ctx, FullRequest{Method: method, URL: httpURL, RequestJSON: reqBody, ResponseJSON: resBody})
+}
+
+type ClientResponseHandler = func(req *http.Request, res *http.Response, responseJSON interface{}) ([]byte, error)
+
+type FullRequest struct {
+	Method           string
+	URL              string
+	Headers          http.Header
+	RequestJSON      interface{}
+	RequestBytes     []byte
+	RequestBody      io.Reader
+	RequestLength    int64
+	ResponseJSON     interface{}
+	MaxAttempts      int
+	BackoffDuration  time.Duration
+	SensitiveContent bool
+	Handler          ClientResponseHandler
+	DontReadResponse bool
+	Logger           *zerolog.Logger
+	Client           *http.Client
+}
+
+var requestID int32
+var logSensitiveContent = os.Getenv("MAUTRIX_LOG_SENSITIVE_CONTENT") == "yes"
+
+func (params *FullRequest) compileRequest(ctx context.Context) (*http.Request, error) {
+	var logBody any
+	reqBody := params.RequestBody
+	if params.RequestJSON != nil {
+		jsonStr, err := json.Marshal(params.RequestJSON)
+		if err != nil {
+			return nil, HTTPError{
+				Message:      "failed to marshal JSON",
+				WrappedError: err,
+			}
+		}
+		if params.SensitiveContent && !logSensitiveContent {
+			logBody = "<sensitive content omitted>"
+		} else {
+			logBody = params.RequestJSON
+		}
+		reqBody = bytes.NewReader(jsonStr)
+	} else if params.RequestBytes != nil {
+		logBody = fmt.Sprintf("<%d bytes>", len(params.RequestBytes))
+		reqBody = bytes.NewReader(params.RequestBytes)
+		params.RequestLength = int64(len(params.RequestBytes))
+	} else if params.RequestLength > 0 && params.RequestBody != nil {
+		logBody = fmt.Sprintf("<%d bytes>", params.RequestLength)
+		if rsc, ok := params.RequestBody.(io.ReadSeekCloser); ok {
+			// Prevent HTTP from closing the request body, it might be needed for retries
+			reqBody = nopCloseSeeker{rsc}
+		}
+	} else if params.Method != http.MethodGet && params.Method != http.MethodHead {
+		params.RequestJSON = struct{}{}
+		logBody = params.RequestJSON
+		reqBody = bytes.NewReader([]byte("{}"))
+	}
+	reqID := atomic.AddInt32(&requestID, 1)
+	logger := zerolog.Ctx(ctx)
+	if logger.GetLevel() == zerolog.Disabled || logger == zerolog.DefaultContextLogger {
+		logger = params.Logger
+	}
+	ctx = logger.With().
+		Int32("req_id", reqID).
+		Logger().WithContext(ctx)
+	ctx = context.WithValue(ctx, LogBodyContextKey, logBody)
+	ctx = context.WithValue(ctx, LogRequestIDContextKey, int(reqID))
+	req, err := http.NewRequestWithContext(ctx, params.Method, params.URL, reqBody)
+	if err != nil {
+		return nil, HTTPError{
+			Message:      "failed to create request",
+			WrappedError: err,
+		}
+	}
+	if params.Headers != nil {
+		req.Header = params.Headers
+	}
+	if params.RequestJSON != nil {
+		req.Header.Set("Content-Type", "application/json")
+	}
+	if params.RequestLength > 0 && params.RequestBody != nil {
+		req.ContentLength = params.RequestLength
+	}
+	return req, nil
+}
+
+func (cli *Client) MakeFullRequest(ctx context.Context, params FullRequest) ([]byte, error) {
+	data, _, err := cli.MakeFullRequestWithResp(ctx, params)
+	return data, err
+}
+
+func (cli *Client) MakeFullRequestWithResp(ctx context.Context, params FullRequest) ([]byte, *http.Response, error) {
+	if params.MaxAttempts == 0 {
+		params.MaxAttempts = 1 + cli.DefaultHTTPRetries
+	}
+	if params.BackoffDuration == 0 {
+		if cli.DefaultHTTPBackoff == 0 {
+			params.BackoffDuration = 4 * time.Second
+		} else {
+			params.BackoffDuration = cli.DefaultHTTPBackoff
+		}
+	}
+	if params.Logger == nil {
+		params.Logger = &cli.Log
+	}
+	req, err := params.compileRequest(ctx)
+	if err != nil {
+		return nil, nil, err
+	}
+	if params.Handler == nil {
+		if params.DontReadResponse {
+			params.Handler = noopHandleResponse
+		} else {
+			params.Handler = handleNormalResponse
+		}
+	}
+	req.Header.Set("User-Agent", cli.UserAgent)
+	if len(cli.AccessToken) > 0 {
+		req.Header.Set("Authorization", "Bearer "+cli.AccessToken)
+	}
+	if params.Client == nil {
+		params.Client = cli.Client
+	}
+	return cli.executeCompiledRequest(req, params.MaxAttempts-1, params.BackoffDuration, params.ResponseJSON, params.Handler, params.DontReadResponse, params.Client)
+}
+
+func (cli *Client) cliOrContextLog(ctx context.Context) *zerolog.Logger {
+	log := zerolog.Ctx(ctx)
+	if log.GetLevel() == zerolog.Disabled || log == zerolog.DefaultContextLogger {
+		return &cli.Log
+	}
+	return log
+}
+
+func (cli *Client) doRetry(req *http.Request, cause error, retries int, backoff time.Duration, responseJSON any, handler ClientResponseHandler, dontReadResponse bool, client *http.Client) ([]byte, *http.Response, error) {
+	log := zerolog.Ctx(req.Context())
+	if req.Body != nil {
+		var err error
+		if req.GetBody != nil {
+			req.Body, err = req.GetBody()
+			if err != nil {
+				log.Warn().Err(err).Msg("Failed to get new body to retry request")
+				return nil, nil, cause
+			}
+		} else if bodySeeker, ok := req.Body.(io.ReadSeeker); ok {
+			_, err = bodySeeker.Seek(0, io.SeekStart)
+			if err != nil {
+				log.Warn().Err(err).Msg("Failed to seek to beginning of request body")
+				return nil, nil, cause
+			}
+		} else {
+			log.Warn().Msg("Failed to get new body to retry request: GetBody is nil and Body is not an io.ReadSeeker")
+			return nil, nil, cause
+		}
+	}
+	log.Warn().Err(cause).
+		Int("retry_in_seconds", int(backoff.Seconds())).
+		Msg("Request failed, retrying")
+	time.Sleep(backoff)
+	if cli.UpdateRequestOnRetry != nil {
+		req = cli.UpdateRequestOnRetry(req, cause)
+	}
+	return cli.executeCompiledRequest(req, retries-1, backoff*2, responseJSON, handler, dontReadResponse, client)
+}
+
+func readResponseBody(req *http.Request, res *http.Response) ([]byte, error) {
+	contents, err := io.ReadAll(res.Body)
+	if err != nil {
+		return nil, HTTPError{
+			Request:  req,
+			Response: res,
+
+			Message:      "failed to read response body",
+			WrappedError: err,
+		}
+	}
+	return contents, nil
+}
+
+func closeTemp(log *zerolog.Logger, file *os.File) {
+	_ = file.Close()
+	err := os.Remove(file.Name())
+	if err != nil {
+		log.Warn().Err(err).Str("file_name", file.Name()).Msg("Failed to remove response temp file")
+	}
+}
+
+func streamResponse(req *http.Request, res *http.Response, responseJSON interface{}) ([]byte, error) {
+	log := zerolog.Ctx(req.Context())
+	file, err := os.CreateTemp("", "mautrix-response-")
+	if err != nil {
+		log.Warn().Err(err).Msg("Failed to create temporary file for streaming response")
+		_, err = handleNormalResponse(req, res, responseJSON)
+		return nil, err
+	}
+	defer closeTemp(log, file)
+	if _, err = io.Copy(file, res.Body); err != nil {
+		return nil, fmt.Errorf("failed to copy response to file: %w", err)
+	} else if _, err = file.Seek(0, 0); err != nil {
+		return nil, fmt.Errorf("failed to seek to beginning of response file: %w", err)
+	} else if err = json.NewDecoder(file).Decode(responseJSON); err != nil {
+		return nil, fmt.Errorf("failed to unmarshal response body: %w", err)
+	} else {
+		return nil, nil
+	}
+}
+
+func noopHandleResponse(req *http.Request, res *http.Response, responseJSON interface{}) ([]byte, error) {
+	return nil, nil
+}
+
+func handleNormalResponse(req *http.Request, res *http.Response, responseJSON interface{}) ([]byte, error) {
+	if contents, err := readResponseBody(req, res); err != nil {
+		return nil, err
+	} else if responseJSON == nil {
+		return contents, nil
+	} else if err = json.Unmarshal(contents, &responseJSON); err != nil {
+		return nil, HTTPError{
+			Request:  req,
+			Response: res,
+
+			Message:      "failed to unmarshal response body",
+			ResponseBody: string(contents),
+			WrappedError: err,
+		}
+	} else {
+		return contents, nil
+	}
+}
+
+func ParseErrorResponse(req *http.Request, res *http.Response) ([]byte, error) {
+	contents, err := readResponseBody(req, res)
+	if err != nil {
+		return contents, err
+	}
+
+	respErr := &RespError{
+		StatusCode: res.StatusCode,
+	}
+	if _ = json.Unmarshal(contents, respErr); respErr.ErrCode == "" {
+		respErr = nil
+	}
+
+	return contents, HTTPError{
+		Request:   req,
+		Response:  res,
+		RespError: respErr,
+	}
+}
+
+func (cli *Client) executeCompiledRequest(req *http.Request, retries int, backoff time.Duration, responseJSON any, handler ClientResponseHandler, dontReadResponse bool, client *http.Client) ([]byte, *http.Response, error) {
+	cli.RequestStart(req)
+	startTime := time.Now()
+	res, err := client.Do(req)
+	duration := time.Now().Sub(startTime)
+	if res != nil && !dontReadResponse {
+		defer res.Body.Close()
+	}
+	if err != nil {
+		if retries > 0 && !errors.Is(err, context.Canceled) {
+			return cli.doRetry(req, err, retries, backoff, responseJSON, handler, dontReadResponse, client)
+		}
+		err = HTTPError{
+			Request:  req,
+			Response: res,
+
+			Message:      "request error",
+			WrappedError: err,
+		}
+		cli.LogRequestDone(req, res, err, nil, 0, duration)
+		return nil, res, err
+	}
+
+	if retries > 0 && retryafter.Should(res.StatusCode, !cli.IgnoreRateLimit) {
+		backoff = retryafter.Parse(res.Header.Get("Retry-After"), backoff)
+		return cli.doRetry(req, fmt.Errorf("HTTP %d", res.StatusCode), retries, backoff, responseJSON, handler, dontReadResponse, client)
+	}
+
+	var body []byte
+	if res.StatusCode < 200 || res.StatusCode >= 300 {
+		body, err = ParseErrorResponse(req, res)
+		cli.LogRequestDone(req, res, nil, nil, len(body), duration)
+	} else {
+		body, err = handler(req, res, responseJSON)
+		cli.LogRequestDone(req, res, nil, err, len(body), duration)
+	}
+	return body, res, err
+}
+
+// Whoami gets the user ID of the current user. See https://spec.matrix.org/v1.2/client-server-api/#get_matrixclientv3accountwhoami
+func (cli *Client) Whoami(ctx context.Context) (resp *RespWhoami, err error) {
+
+	urlPath := cli.BuildClientURL("v3", "account", "whoami")
+	_, err = cli.MakeRequest(ctx, http.MethodGet, urlPath, nil, &resp)
+	return
+}
+
+// CreateFilter makes an HTTP request according to https://spec.matrix.org/v1.2/client-server-api/#post_matrixclientv3useruseridfilter
+func (cli *Client) CreateFilter(ctx context.Context, filter *Filter) (resp *RespCreateFilter, err error) {
+	urlPath := cli.BuildClientURL("v3", "user", cli.UserID, "filter")
+	_, err = cli.MakeRequest(ctx, http.MethodPost, urlPath, filter, &resp)
+	return
+}
+
+// SyncRequest makes an HTTP request according to https://spec.matrix.org/v1.2/client-server-api/#get_matrixclientv3sync
+func (cli *Client) SyncRequest(ctx context.Context, timeout int, since, filterID string, fullState bool, setPresence event.Presence) (resp *RespSync, err error) {
+	return cli.FullSyncRequest(ctx, ReqSync{
+		Timeout:     timeout,
+		Since:       since,
+		FilterID:    filterID,
+		FullState:   fullState,
+		SetPresence: setPresence,
+	})
+}
+
+type ReqSync struct {
+	Timeout         int
+	Since           string
+	FilterID        string
+	FullState       bool
+	SetPresence     event.Presence
+	StreamResponse  bool
+	BeeperStreaming bool
+	Client          *http.Client
+}
+
+func (req *ReqSync) BuildQuery() map[string]string {
+	query := map[string]string{
+		"timeout": strconv.Itoa(req.Timeout),
+	}
+	if req.Since != "" {
+		query["since"] = req.Since
+	}
+	if req.FilterID != "" {
+		query["filter"] = req.FilterID
+	}
+	if req.SetPresence != "" {
+		query["set_presence"] = string(req.SetPresence)
+	}
+	if req.FullState {
+		query["full_state"] = "true"
+	}
+	if req.BeeperStreaming {
+		// TODO remove this
+		query["streaming"] = ""
+		query["com.beeper.streaming"] = "true"
+	}
+	return query
+}
+
+// FullSyncRequest makes an HTTP request according to https://spec.matrix.org/v1.2/client-server-api/#get_matrixclientv3sync
+func (cli *Client) FullSyncRequest(ctx context.Context, req ReqSync) (resp *RespSync, err error) {
+	urlPath := cli.BuildURLWithQuery(ClientURLPath{"v3", "sync"}, req.BuildQuery())
+	fullReq := FullRequest{
+		Method:       http.MethodGet,
+		URL:          urlPath,
+		ResponseJSON: &resp,
+		Client:       req.Client,
+		// We don't want automatic retries for SyncRequest, the Sync() wrapper handles those.
+		MaxAttempts: 1,
+	}
+	if req.StreamResponse {
+		fullReq.Handler = streamResponse
+	}
+	start := time.Now()
+	_, err = cli.MakeFullRequest(ctx, fullReq)
+	duration := time.Now().Sub(start)
+	timeout := time.Duration(req.Timeout) * time.Millisecond
+	buffer := 10 * time.Second
+	if req.Since == "" {
+		buffer = 1 * time.Minute
+	}
+	if err == nil && duration > timeout+buffer {
+		cli.cliOrContextLog(ctx).Warn().
+			Str("since", req.Since).
+			Dur("duration", duration).
+			Dur("timeout", timeout).
+			Msg("Sync request took unusually long")
+	}
+	return
+}
+
+// RegisterAvailable checks if a username is valid and available for registration on the server.
+//
+// See https://spec.matrix.org/v1.4/client-server-api/#get_matrixclientv3registeravailable for more details
+//
+// This will always return an error if the username isn't available, so checking the actual response struct is generally
+// not necessary. It is still returned for future-proofing. For a simple availability check, just check that the returned
+// error is nil. `errors.Is` can be used to find the exact reason why a username isn't available:
+//
+//	_, err := cli.RegisterAvailable("cat")
+//	if errors.Is(err, mautrix.MUserInUse) {
+//		// Username is taken
+//	} else if errors.Is(err, mautrix.MInvalidUsername) {
+//		// Username is not valid
+//	} else if errors.Is(err, mautrix.MExclusive) {
+//		// Username is reserved for an appservice
+//	} else if errors.Is(err, mautrix.MLimitExceeded) {
+//		// Too many requests
+//	} else if err != nil {
+//		// Unknown error
+//	} else {
+//		// Username is available
+//	}
+func (cli *Client) RegisterAvailable(ctx context.Context, username string) (resp *RespRegisterAvailable, err error) {
+	u := cli.BuildURLWithQuery(ClientURLPath{"v3", "register", "available"}, map[string]string{"username": username})
+	_, err = cli.MakeRequest(ctx, http.MethodGet, u, nil, &resp)
+	if err == nil && !resp.Available {
+		err = fmt.Errorf(`request returned OK status without "available": true`)
+	}
+	return
+}
+
+func (cli *Client) register(ctx context.Context, url string, req *ReqRegister) (resp *RespRegister, uiaResp *RespUserInteractive, err error) {
+	var bodyBytes []byte
+	bodyBytes, err = cli.MakeFullRequest(ctx, FullRequest{
+		Method:           http.MethodPost,
+		URL:              url,
+		RequestJSON:      req,
+		SensitiveContent: len(req.Password) > 0,
+	})
+	if err != nil {
+		httpErr, ok := err.(HTTPError)
+		// if response has a 401 status, but doesn't have the errcode field, it's probably a UIA response.
+		if ok && httpErr.IsStatus(http.StatusUnauthorized) && httpErr.RespError == nil {
+			err = json.Unmarshal(bodyBytes, &uiaResp)
+		}
+	} else {
+		// body should be RespRegister
+		err = json.Unmarshal(bodyBytes, &resp)
+	}
+	return
+}
+
+// Register makes an HTTP request according to https://spec.matrix.org/v1.2/client-server-api/#post_matrixclientv3register
+//
+// Registers with kind=user. For kind=guest, see RegisterGuest.
+func (cli *Client) Register(ctx context.Context, req *ReqRegister) (*RespRegister, *RespUserInteractive, error) {
+	u := cli.BuildClientURL("v3", "register")
+	return cli.register(ctx, u, req)
+}
+
+// RegisterGuest makes an HTTP request according to https://spec.matrix.org/v1.2/client-server-api/#post_matrixclientv3register
+// with kind=guest.
+//
+// For kind=user, see Register.
+func (cli *Client) RegisterGuest(ctx context.Context, req *ReqRegister) (*RespRegister, *RespUserInteractive, error) {
+	query := map[string]string{
+		"kind": "guest",
+	}
+	u := cli.BuildURLWithQuery(ClientURLPath{"v3", "register"}, query)
+	return cli.register(ctx, u, req)
+}
+
+// RegisterDummy performs m.login.dummy registration according to https://spec.matrix.org/v1.2/client-server-api/#dummy-auth
+//
+// Only a username and password need to be provided on the ReqRegister struct. Most local/developer homeservers will allow registration
+// this way. If the homeserver does not, an error is returned.
+//
+// This does not set credentials on the client instance. See SetCredentials() instead.
+//
+//	res, err := cli.RegisterDummy(&mautrix.ReqRegister{
+//		Username: "alice",
+//		Password: "wonderland",
+//	})
+//	if err != nil {
+//		panic(err)
+//	}
+//	token := res.AccessToken
+func (cli *Client) RegisterDummy(ctx context.Context, req *ReqRegister) (*RespRegister, error) {
+	res, uia, err := cli.Register(ctx, req)
+	if err != nil && uia == nil {
+		return nil, err
+	} else if uia == nil {
+		return nil, errors.New("server did not return user-interactive auth flows")
+	} else if !uia.HasSingleStageFlow(AuthTypeDummy) {
+		return nil, errors.New("server does not support m.login.dummy")
+	}
+	req.Auth = BaseAuthData{Type: AuthTypeDummy, Session: uia.Session}
+	res, _, err = cli.Register(ctx, req)
+	if err != nil {
+		return nil, err
+	}
+	return res, nil
+}
+
+// GetLoginFlows fetches the login flows that the homeserver supports using https://spec.matrix.org/v1.2/client-server-api/#get_matrixclientv3login
+func (cli *Client) GetLoginFlows(ctx context.Context) (resp *RespLoginFlows, err error) {
+	urlPath := cli.BuildClientURL("v3", "login")
+	_, err = cli.MakeRequest(ctx, http.MethodGet, urlPath, nil, &resp)
+	return
+}
+
+// Login a user to the homeserver according to https://spec.matrix.org/v1.2/client-server-api/#post_matrixclientv3login
+func (cli *Client) Login(ctx context.Context, req *ReqLogin) (resp *RespLogin, err error) {
+	_, err = cli.MakeFullRequest(ctx, FullRequest{
+		Method:           http.MethodPost,
+		URL:              cli.BuildClientURL("v3", "login"),
+		RequestJSON:      req,
+		ResponseJSON:     &resp,
+		SensitiveContent: len(req.Password) > 0 || len(req.Token) > 0,
+	})
+	if req.StoreCredentials && err == nil {
+		cli.DeviceID = resp.DeviceID
+		cli.AccessToken = resp.AccessToken
+		cli.UserID = resp.UserID
+
+		cli.Log.Debug().
+			Str("user_id", cli.UserID.String()).
+			Str("device_id", cli.DeviceID.String()).
+			Msg("Stored credentials after login")
+	}
+	if req.StoreHomeserverURL && err == nil && resp.WellKnown != nil && len(resp.WellKnown.Homeserver.BaseURL) > 0 {
+		var urlErr error
+		cli.HomeserverURL, urlErr = url.Parse(resp.WellKnown.Homeserver.BaseURL)
+		if urlErr != nil {
+			cli.Log.Warn().
+				Err(urlErr).
+				Str("homeserver_url", resp.WellKnown.Homeserver.BaseURL).
+				Msg("Failed to parse homeserver URL in login response")
+		} else {
+			cli.Log.Debug().
+				Str("homeserver_url", cli.HomeserverURL.String()).
+				Msg("Updated homeserver URL after login")
+		}
+	}
+	return
+}
+
+// Logout the current user. See https://spec.matrix.org/v1.2/client-server-api/#post_matrixclientv3logout
+// This does not clear the credentials from the client instance. See ClearCredentials() instead.
+func (cli *Client) Logout(ctx context.Context) (resp *RespLogout, err error) {
+	urlPath := cli.BuildClientURL("v3", "logout")
+	_, err = cli.MakeRequest(ctx, http.MethodPost, urlPath, nil, &resp)
+	return
+}
+
+// LogoutAll logs out all the devices of the current user. See https://spec.matrix.org/v1.2/client-server-api/#post_matrixclientv3logoutall
+// This does not clear the credentials from the client instance. See ClearCredentials() instead.
+func (cli *Client) LogoutAll(ctx context.Context) (resp *RespLogout, err error) {
+	urlPath := cli.BuildClientURL("v3", "logout", "all")
+	_, err = cli.MakeRequest(ctx, http.MethodPost, urlPath, nil, &resp)
+	return
+}
+
+// Versions returns the list of supported Matrix versions on this homeserver. See https://spec.matrix.org/v1.2/client-server-api/#get_matrixclientversions
+func (cli *Client) Versions(ctx context.Context) (resp *RespVersions, err error) {
+	urlPath := cli.BuildClientURL("versions")
+	_, err = cli.MakeRequest(ctx, http.MethodGet, urlPath, nil, &resp)
+	if resp != nil {
+		cli.SpecVersions = resp
+	}
+	return
+}
+
+// Capabilities returns capabilities on this homeserver. See https://spec.matrix.org/v1.3/client-server-api/#capabilities-negotiation
+func (cli *Client) Capabilities(ctx context.Context) (resp *RespCapabilities, err error) {
+	urlPath := cli.BuildClientURL("v3", "capabilities")
+	_, err = cli.MakeRequest(ctx, http.MethodGet, urlPath, nil, &resp)
+	return
+}
+
+// JoinRoom joins the client to a room ID or alias. See https://spec.matrix.org/v1.2/client-server-api/#post_matrixclientv3joinroomidoralias
+//
+// If serverName is specified, this will be added as a query param to instruct the homeserver to join via that server. If content is specified, it will
+// be JSON encoded and used as the request body.
+func (cli *Client) JoinRoom(ctx context.Context, roomIDorAlias, serverName string, content interface{}) (resp *RespJoinRoom, err error) {
+	var urlPath string
+	if serverName != "" {
+		urlPath = cli.BuildURLWithQuery(ClientURLPath{"v3", "join", roomIDorAlias}, map[string]string{
+			"server_name": serverName,
+		})
+	} else {
+		urlPath = cli.BuildClientURL("v3", "join", roomIDorAlias)
+	}
+	_, err = cli.MakeRequest(ctx, http.MethodPost, urlPath, content, &resp)
+	if err == nil && cli.StateStore != nil {
+		err = cli.StateStore.SetMembership(ctx, resp.RoomID, cli.UserID, event.MembershipJoin)
+		if err != nil {
+			err = fmt.Errorf("failed to update state store: %w", err)
+		}
+	}
+	return
+}
+
+// JoinRoomByID joins the client to a room ID. See https://spec.matrix.org/v1.2/client-server-api/#post_matrixclientv3roomsroomidjoin
+//
+// Unlike JoinRoom, this method can only be used to join rooms that the server already knows about.
+// It's mostly intended for bridges and other things where it's already certain that the server is in the room.
+func (cli *Client) JoinRoomByID(ctx context.Context, roomID id.RoomID) (resp *RespJoinRoom, err error) {
+	_, err = cli.MakeRequest(ctx, http.MethodPost, cli.BuildClientURL("v3", "rooms", roomID, "join"), nil, &resp)
+	if err == nil && cli.StateStore != nil {
+		err = cli.StateStore.SetMembership(ctx, resp.RoomID, cli.UserID, event.MembershipJoin)
+		if err != nil {
+			err = fmt.Errorf("failed to update state store: %w", err)
+		}
+	}
+	return
+}
+
+func (cli *Client) GetProfile(ctx context.Context, mxid id.UserID) (resp *RespUserProfile, err error) {
+	urlPath := cli.BuildClientURL("v3", "profile", mxid)
+	_, err = cli.MakeRequest(ctx, http.MethodGet, urlPath, nil, &resp)
+	return
+}
+
+// GetDisplayName returns the display name of the user with the specified MXID. See https://spec.matrix.org/v1.2/client-server-api/#get_matrixclientv3profileuseriddisplayname
+func (cli *Client) GetDisplayName(ctx context.Context, mxid id.UserID) (resp *RespUserDisplayName, err error) {
+	urlPath := cli.BuildClientURL("v3", "profile", mxid, "displayname")
+	_, err = cli.MakeRequest(ctx, http.MethodGet, urlPath, nil, &resp)
+	return
+}
+
+// GetOwnDisplayName returns the user's display name. See https://spec.matrix.org/v1.2/client-server-api/#get_matrixclientv3profileuseriddisplayname
+func (cli *Client) GetOwnDisplayName(ctx context.Context) (resp *RespUserDisplayName, err error) {
+	return cli.GetDisplayName(ctx, cli.UserID)
+}
+
+// SetDisplayName sets the user's profile display name. See https://spec.matrix.org/v1.2/client-server-api/#put_matrixclientv3profileuseriddisplayname
+func (cli *Client) SetDisplayName(ctx context.Context, displayName string) (err error) {
+	urlPath := cli.BuildClientURL("v3", "profile", cli.UserID, "displayname")
+	s := struct {
+		DisplayName string `json:"displayname"`
+	}{displayName}
+	_, err = cli.MakeRequest(ctx, http.MethodPut, urlPath, &s, nil)
+	return
+}
+
+// GetAvatarURL gets the avatar URL of the user with the specified MXID. See https://spec.matrix.org/v1.2/client-server-api/#get_matrixclientv3profileuseridavatar_url
+func (cli *Client) GetAvatarURL(ctx context.Context, mxid id.UserID) (url id.ContentURI, err error) {
+	urlPath := cli.BuildClientURL("v3", "profile", mxid, "avatar_url")
+	s := struct {
+		AvatarURL id.ContentURI `json:"avatar_url"`
+	}{}
+
+	_, err = cli.MakeRequest(ctx, http.MethodGet, urlPath, nil, &s)
+	if err != nil {
+		return
+	}
+	url = s.AvatarURL
+	return
+}
+
+// GetOwnAvatarURL gets the user's avatar URL. See https://spec.matrix.org/v1.2/client-server-api/#get_matrixclientv3profileuseridavatar_url
+func (cli *Client) GetOwnAvatarURL(ctx context.Context) (url id.ContentURI, err error) {
+	return cli.GetAvatarURL(ctx, cli.UserID)
+}
+
+// SetAvatarURL sets the user's avatar URL. See https://spec.matrix.org/v1.2/client-server-api/#put_matrixclientv3profileuseridavatar_url
+func (cli *Client) SetAvatarURL(ctx context.Context, url id.ContentURI) (err error) {
+	urlPath := cli.BuildClientURL("v3", "profile", cli.UserID, "avatar_url")
+	s := struct {
+		AvatarURL string `json:"avatar_url"`
+	}{url.String()}
+	_, err = cli.MakeRequest(ctx, http.MethodPut, urlPath, &s, nil)
+	if err != nil {
+		return err
+	}
+
+	return nil
+}
+
+// BeeperUpdateProfile sets custom fields in the user's profile.
+func (cli *Client) BeeperUpdateProfile(ctx context.Context, data any) (err error) {
+	urlPath := cli.BuildClientURL("v3", "profile", cli.UserID)
+	_, err = cli.MakeRequest(ctx, http.MethodPatch, urlPath, data, nil)
+	return
+}
+
+// GetAccountData gets the user's account data of this type. See https://spec.matrix.org/v1.2/client-server-api/#get_matrixclientv3useruseridaccount_datatype
+func (cli *Client) GetAccountData(ctx context.Context, name string, output interface{}) (err error) {
+	urlPath := cli.BuildClientURL("v3", "user", cli.UserID, "account_data", name)
+	_, err = cli.MakeRequest(ctx, http.MethodGet, urlPath, nil, output)
+	return
+}
+
+// SetAccountData sets the user's account data of this type. See https://spec.matrix.org/v1.2/client-server-api/#put_matrixclientv3useruseridaccount_datatype
+func (cli *Client) SetAccountData(ctx context.Context, name string, data interface{}) (err error) {
+	urlPath := cli.BuildClientURL("v3", "user", cli.UserID, "account_data", name)
+	_, err = cli.MakeRequest(ctx, http.MethodPut, urlPath, data, nil)
+	if err != nil {
+		return err
+	}
+
+	return nil
+}
+
+// GetRoomAccountData gets the user's account data of this type in a specific room. See https://spec.matrix.org/v1.2/client-server-api/#put_matrixclientv3useruseridaccount_datatype
+func (cli *Client) GetRoomAccountData(ctx context.Context, roomID id.RoomID, name string, output interface{}) (err error) {
+	urlPath := cli.BuildClientURL("v3", "user", cli.UserID, "rooms", roomID, "account_data", name)
+	_, err = cli.MakeRequest(ctx, http.MethodGet, urlPath, nil, output)
+	return
+}
+
+// SetRoomAccountData sets the user's account data of this type in a specific room. See https://spec.matrix.org/v1.2/client-server-api/#put_matrixclientv3useruseridroomsroomidaccount_datatype
+func (cli *Client) SetRoomAccountData(ctx context.Context, roomID id.RoomID, name string, data interface{}) (err error) {
+	urlPath := cli.BuildClientURL("v3", "user", cli.UserID, "rooms", roomID, "account_data", name)
+	_, err = cli.MakeRequest(ctx, http.MethodPut, urlPath, data, nil)
+	if err != nil {
+		return err
+	}
+
+	return nil
+}
+
+type ReqSendEvent struct {
+	Timestamp     int64
+	TransactionID string
+
+	DontEncrypt bool
+
+	MeowEventID id.EventID
+}
+
+// SendMessageEvent sends a message event into a room. See https://spec.matrix.org/v1.2/client-server-api/#put_matrixclientv3roomsroomidsendeventtypetxnid
+// contentJSON should be a pointer to something that can be encoded as JSON using json.Marshal.
+func (cli *Client) SendMessageEvent(ctx context.Context, roomID id.RoomID, eventType event.Type, contentJSON interface{}, extra ...ReqSendEvent) (resp *RespSendEvent, err error) {
+	var req ReqSendEvent
+	if len(extra) > 0 {
+		req = extra[0]
+	}
+
+	var txnID string
+	if len(req.TransactionID) > 0 {
+		txnID = req.TransactionID
+	} else {
+		txnID = cli.TxnID()
+	}
+
+	queryParams := map[string]string{}
+	if req.Timestamp > 0 {
+		queryParams["ts"] = strconv.FormatInt(req.Timestamp, 10)
+	}
+	if req.MeowEventID != "" {
+		queryParams["fi.mau.event_id"] = req.MeowEventID.String()
+	}
+
+	if !req.DontEncrypt && cli.Crypto != nil && eventType != event.EventReaction && eventType != event.EventEncrypted {
+		var isEncrypted bool
+		isEncrypted, err = cli.StateStore.IsEncrypted(ctx, roomID)
+		if err != nil {
+			err = fmt.Errorf("failed to check if room is encrypted: %w", err)
+			return
+		}
+		if isEncrypted {
+			if contentJSON, err = cli.Crypto.Encrypt(ctx, roomID, eventType, contentJSON); err != nil {
+				err = fmt.Errorf("failed to encrypt event: %w", err)
+				return
+			}
+			eventType = event.EventEncrypted
+		}
+	}
+
+	urlData := ClientURLPath{"v3", "rooms", roomID, "send", eventType.String(), txnID}
+	urlPath := cli.BuildURLWithQuery(urlData, queryParams)
+	_, err = cli.MakeRequest(ctx, http.MethodPut, urlPath, contentJSON, &resp)
+	return
+}
+
+// SendStateEvent sends a state event into a room. See https://spec.matrix.org/v1.2/client-server-api/#put_matrixclientv3roomsroomidstateeventtypestatekey
+// contentJSON should be a pointer to something that can be encoded as JSON using json.Marshal.
+func (cli *Client) SendStateEvent(ctx context.Context, roomID id.RoomID, eventType event.Type, stateKey string, contentJSON interface{}) (resp *RespSendEvent, err error) {
+	urlPath := cli.BuildClientURL("v3", "rooms", roomID, "state", eventType.String(), stateKey)
+	_, err = cli.MakeRequest(ctx, http.MethodPut, urlPath, contentJSON, &resp)
+	if err == nil && cli.StateStore != nil {
+		cli.updateStoreWithOutgoingEvent(ctx, roomID, eventType, stateKey, contentJSON)
+	}
+	return
+}
+
+// SendMassagedStateEvent sends a state event into a room with a custom timestamp. See https://spec.matrix.org/v1.2/client-server-api/#put_matrixclientv3roomsroomidstateeventtypestatekey
+// contentJSON should be a pointer to something that can be encoded as JSON using json.Marshal.
+func (cli *Client) SendMassagedStateEvent(ctx context.Context, roomID id.RoomID, eventType event.Type, stateKey string, contentJSON interface{}, ts int64) (resp *RespSendEvent, err error) {
+	urlPath := cli.BuildURLWithQuery(ClientURLPath{"v3", "rooms", roomID, "state", eventType.String(), stateKey}, map[string]string{
+		"ts": strconv.FormatInt(ts, 10),
+	})
+	_, err = cli.MakeRequest(ctx, http.MethodPut, urlPath, contentJSON, &resp)
+	if err == nil && cli.StateStore != nil {
+		cli.updateStoreWithOutgoingEvent(ctx, roomID, eventType, stateKey, contentJSON)
+	}
+	return
+}
+
+// SendText sends an m.room.message event into the given room with a msgtype of m.text
+// See https://spec.matrix.org/v1.2/client-server-api/#mtext
+func (cli *Client) SendText(ctx context.Context, roomID id.RoomID, text string) (*RespSendEvent, error) {
+	return cli.SendMessageEvent(ctx, roomID, event.EventMessage, &event.MessageEventContent{
+		MsgType: event.MsgText,
+		Body:    text,
+	})
+}
+
+// SendNotice sends an m.room.message event into the given room with a msgtype of m.notice
+// See https://spec.matrix.org/v1.2/client-server-api/#mnotice
+func (cli *Client) SendNotice(ctx context.Context, roomID id.RoomID, text string) (*RespSendEvent, error) {
+	return cli.SendMessageEvent(ctx, roomID, event.EventMessage, &event.MessageEventContent{
+		MsgType: event.MsgNotice,
+		Body:    text,
+	})
+}
+
+func (cli *Client) SendReaction(ctx context.Context, roomID id.RoomID, eventID id.EventID, reaction string) (*RespSendEvent, error) {
+	return cli.SendMessageEvent(ctx, roomID, event.EventReaction, &event.ReactionEventContent{
+		RelatesTo: event.RelatesTo{
+			EventID: eventID,
+			Type:    event.RelAnnotation,
+			Key:     reaction,
+		},
+	})
+}
+
+// RedactEvent redacts the given event. See https://spec.matrix.org/v1.2/client-server-api/#put_matrixclientv3roomsroomidredacteventidtxnid
+func (cli *Client) RedactEvent(ctx context.Context, roomID id.RoomID, eventID id.EventID, extra ...ReqRedact) (resp *RespSendEvent, err error) {
+	req := ReqRedact{}
+	if len(extra) > 0 {
+		req = extra[0]
+	}
+	if req.Extra == nil {
+		req.Extra = make(map[string]interface{})
+	}
+	if len(req.Reason) > 0 {
+		req.Extra["reason"] = req.Reason
+	}
+	var txnID string
+	if len(req.TxnID) > 0 {
+		txnID = req.TxnID
+	} else {
+		txnID = cli.TxnID()
+	}
+	urlPath := cli.BuildClientURL("v3", "rooms", roomID, "redact", eventID, txnID)
+	_, err = cli.MakeRequest(ctx, http.MethodPut, urlPath, req.Extra, &resp)
+	return
+}
+
+// CreateRoom creates a new Matrix room. See https://spec.matrix.org/v1.2/client-server-api/#post_matrixclientv3createroom
+//
+//	resp, err := cli.CreateRoom(&mautrix.ReqCreateRoom{
+//		Preset: "public_chat",
+//	})
+//	fmt.Println("Room:", resp.RoomID)
+func (cli *Client) CreateRoom(ctx context.Context, req *ReqCreateRoom) (resp *RespCreateRoom, err error) {
+	urlPath := cli.BuildClientURL("v3", "createRoom")
+	_, err = cli.MakeRequest(ctx, http.MethodPost, urlPath, req, &resp)
+	if err == nil && cli.StateStore != nil {
+		storeErr := cli.StateStore.SetMembership(ctx, resp.RoomID, cli.UserID, event.MembershipJoin)
+		if storeErr != nil {
+			cli.cliOrContextLog(ctx).Warn().Err(storeErr).
+				Stringer("creator_user_id", cli.UserID).
+				Msg("Failed to update creator membership in state store after creating room")
+		}
+		for _, evt := range req.InitialState {
+			UpdateStateStore(ctx, cli.StateStore, evt)
+		}
+		inviteMembership := event.MembershipInvite
+		if req.BeeperAutoJoinInvites {
+			inviteMembership = event.MembershipJoin
+		}
+		for _, invitee := range req.Invite {
+			storeErr = cli.StateStore.SetMembership(ctx, resp.RoomID, invitee, inviteMembership)
+			if storeErr != nil {
+				cli.cliOrContextLog(ctx).Warn().Err(storeErr).
+					Stringer("invitee_user_id", invitee).
+					Msg("Failed to update membership in state store after creating room")
+			}
+		}
+		for _, evt := range req.InitialState {
+			cli.updateStoreWithOutgoingEvent(ctx, resp.RoomID, evt.Type, evt.GetStateKey(), &evt.Content)
+		}
+	}
+	return
+}
+
+// LeaveRoom leaves the given room. See https://spec.matrix.org/v1.2/client-server-api/#post_matrixclientv3roomsroomidleave
+func (cli *Client) LeaveRoom(ctx context.Context, roomID id.RoomID, optionalReq ...*ReqLeave) (resp *RespLeaveRoom, err error) {
+	req := &ReqLeave{}
+	if len(optionalReq) == 1 {
+		req = optionalReq[0]
+	} else if len(optionalReq) > 1 {
+		panic("invalid number of arguments to LeaveRoom")
+	}
+	u := cli.BuildClientURL("v3", "rooms", roomID, "leave")
+	_, err = cli.MakeRequest(ctx, http.MethodPost, u, req, &resp)
+	if err == nil && cli.StateStore != nil {
+		err = cli.StateStore.SetMembership(ctx, roomID, cli.UserID, event.MembershipLeave)
+		if err != nil {
+			err = fmt.Errorf("failed to update membership in state store: %w", err)
+		}
+	}
+	return
+}
+
+// ForgetRoom forgets a room entirely. See https://spec.matrix.org/v1.2/client-server-api/#post_matrixclientv3roomsroomidforget
+func (cli *Client) ForgetRoom(ctx context.Context, roomID id.RoomID) (resp *RespForgetRoom, err error) {
+	u := cli.BuildClientURL("v3", "rooms", roomID, "forget")
+	_, err = cli.MakeRequest(ctx, http.MethodPost, u, struct{}{}, &resp)
+	return
+}
+
+// InviteUser invites a user to a room. See https://spec.matrix.org/v1.2/client-server-api/#post_matrixclientv3roomsroomidinvite
+func (cli *Client) InviteUser(ctx context.Context, roomID id.RoomID, req *ReqInviteUser) (resp *RespInviteUser, err error) {
+	u := cli.BuildClientURL("v3", "rooms", roomID, "invite")
+	_, err = cli.MakeRequest(ctx, http.MethodPost, u, req, &resp)
+	if err == nil && cli.StateStore != nil {
+		err = cli.StateStore.SetMembership(ctx, roomID, req.UserID, event.MembershipInvite)
+		if err != nil {
+			err = fmt.Errorf("failed to update membership in state store: %w", err)
+		}
+	}
+	return
+}
+
+// InviteUserByThirdParty invites a third-party identifier to a room. See https://spec.matrix.org/v1.2/client-server-api/#post_matrixclientv3roomsroomidinvite-1
+func (cli *Client) InviteUserByThirdParty(ctx context.Context, roomID id.RoomID, req *ReqInvite3PID) (resp *RespInviteUser, err error) {
+	u := cli.BuildClientURL("v3", "rooms", roomID, "invite")
+	_, err = cli.MakeRequest(ctx, http.MethodPost, u, req, &resp)
+	return
+}
+
+// KickUser kicks a user from a room. See https://spec.matrix.org/v1.2/client-server-api/#post_matrixclientv3roomsroomidkick
+func (cli *Client) KickUser(ctx context.Context, roomID id.RoomID, req *ReqKickUser) (resp *RespKickUser, err error) {
+	u := cli.BuildClientURL("v3", "rooms", roomID, "kick")
+	_, err = cli.MakeRequest(ctx, http.MethodPost, u, req, &resp)
+	if err == nil && cli.StateStore != nil {
+		err = cli.StateStore.SetMembership(ctx, roomID, req.UserID, event.MembershipLeave)
+		if err != nil {
+			err = fmt.Errorf("failed to update membership in state store: %w", err)
+		}
+	}
+	return
+}
+
+// BanUser bans a user from a room. See https://spec.matrix.org/v1.2/client-server-api/#post_matrixclientv3roomsroomidban
+func (cli *Client) BanUser(ctx context.Context, roomID id.RoomID, req *ReqBanUser) (resp *RespBanUser, err error) {
+	u := cli.BuildClientURL("v3", "rooms", roomID, "ban")
+	_, err = cli.MakeRequest(ctx, http.MethodPost, u, req, &resp)
+	if err == nil && cli.StateStore != nil {
+		err = cli.StateStore.SetMembership(ctx, roomID, req.UserID, event.MembershipBan)
+		if err != nil {
+			err = fmt.Errorf("failed to update membership in state store: %w", err)
+		}
+	}
+	return
+}
+
+// UnbanUser unbans a user from a room. See https://spec.matrix.org/v1.2/client-server-api/#post_matrixclientv3roomsroomidunban
+func (cli *Client) UnbanUser(ctx context.Context, roomID id.RoomID, req *ReqUnbanUser) (resp *RespUnbanUser, err error) {
+	u := cli.BuildClientURL("v3", "rooms", roomID, "unban")
+	_, err = cli.MakeRequest(ctx, http.MethodPost, u, req, &resp)
+	if err == nil && cli.StateStore != nil {
+		err = cli.StateStore.SetMembership(ctx, roomID, req.UserID, event.MembershipLeave)
+		if err != nil {
+			err = fmt.Errorf("failed to update membership in state store: %w", err)
+		}
+	}
+	return
+}
+
+// UserTyping sets the typing status of the user. See https://spec.matrix.org/v1.2/client-server-api/#put_matrixclientv3roomsroomidtypinguserid
+func (cli *Client) UserTyping(ctx context.Context, roomID id.RoomID, typing bool, timeout time.Duration) (resp *RespTyping, err error) {
+	req := ReqTyping{Typing: typing, Timeout: timeout.Milliseconds()}
+	u := cli.BuildClientURL("v3", "rooms", roomID, "typing", cli.UserID)
+	_, err = cli.MakeRequest(ctx, http.MethodPut, u, req, &resp)
+	return
+}
+
+// GetPresence gets the presence of the user with the specified MXID. See https://spec.matrix.org/v1.2/client-server-api/#get_matrixclientv3presenceuseridstatus
+func (cli *Client) GetPresence(ctx context.Context, userID id.UserID) (resp *RespPresence, err error) {
+	resp = new(RespPresence)
+	u := cli.BuildClientURL("v3", "presence", userID, "status")
+	_, err = cli.MakeRequest(ctx, http.MethodGet, u, nil, resp)
+	return
+}
+
+// GetOwnPresence gets the user's presence. See https://spec.matrix.org/v1.2/client-server-api/#get_matrixclientv3presenceuseridstatus
+func (cli *Client) GetOwnPresence(ctx context.Context) (resp *RespPresence, err error) {
+	return cli.GetPresence(ctx, cli.UserID)
+}
+
+func (cli *Client) SetPresence(ctx context.Context, status event.Presence) (err error) {
+	req := ReqPresence{Presence: status}
+	u := cli.BuildClientURL("v3", "presence", cli.UserID, "status")
+	_, err = cli.MakeRequest(ctx, http.MethodPut, u, req, nil)
+	return
+}
+
+func (cli *Client) updateStoreWithOutgoingEvent(ctx context.Context, roomID id.RoomID, eventType event.Type, stateKey string, contentJSON interface{}) {
+	if cli.StateStore == nil {
+		return
+	}
+	fakeEvt := &event.Event{
+		StateKey: &stateKey,
+		Type:     eventType,
+		RoomID:   roomID,
+	}
+	var err error
+	fakeEvt.Content.VeryRaw, err = json.Marshal(contentJSON)
+	if err != nil {
+		cli.Log.Warn().Err(err).Msg("Failed to marshal state event content to update state store")
+		return
+	}
+	err = json.Unmarshal(fakeEvt.Content.VeryRaw, &fakeEvt.Content.Raw)
+	if err != nil {
+		cli.Log.Warn().Err(err).Msg("Failed to unmarshal state event content to update state store")
+		return
+	}
+	err = fakeEvt.Content.ParseRaw(fakeEvt.Type)
+	if err != nil {
+		switch fakeEvt.Type {
+		case event.StateMember, event.StatePowerLevels, event.StateEncryption:
+			cli.Log.Warn().Err(err).Msg("Failed to parse state event content to update state store")
+		default:
+			cli.Log.Debug().Err(err).Msg("Failed to parse state event content to update state store")
+		}
+		return
+	}
+	UpdateStateStore(ctx, cli.StateStore, fakeEvt)
+}
+
+// StateEvent gets a single state event in a room. It will attempt to JSON unmarshal into the given "outContent" struct with
+// the HTTP response body, or return an error.
+// See https://spec.matrix.org/v1.2/client-server-api/#get_matrixclientv3roomsroomidstateeventtypestatekey
+func (cli *Client) StateEvent(ctx context.Context, roomID id.RoomID, eventType event.Type, stateKey string, outContent interface{}) (err error) {
+	u := cli.BuildClientURL("v3", "rooms", roomID, "state", eventType.String(), stateKey)
+	_, err = cli.MakeRequest(ctx, http.MethodGet, u, nil, outContent)
+	if err == nil && cli.StateStore != nil {
+		cli.updateStoreWithOutgoingEvent(ctx, roomID, eventType, stateKey, outContent)
+	}
+	return
+}
+
+// parseRoomStateArray parses a JSON array as a stream and stores the events inside it in a room state map.
+func parseRoomStateArray(_ *http.Request, res *http.Response, responseJSON interface{}) ([]byte, error) {
+	response := make(RoomStateMap)
+	responsePtr := responseJSON.(*map[event.Type]map[string]*event.Event)
+	*responsePtr = response
+	dec := json.NewDecoder(res.Body)
+
+	arrayStart, err := dec.Token()
+	if err != nil {
+		return nil, err
+	} else if arrayStart != json.Delim('[') {
+		return nil, fmt.Errorf("expected array start, got %+v", arrayStart)
+	}
+
+	for i := 1; dec.More(); i++ {
+		var evt *event.Event
+		err = dec.Decode(&evt)
+		if err != nil {
+			return nil, fmt.Errorf("failed to parse state array item #%d: %v", i, err)
+		}
+		evt.Type.Class = event.StateEventType
+		_ = evt.Content.ParseRaw(evt.Type)
+		subMap, ok := response[evt.Type]
+		if !ok {
+			subMap = make(map[string]*event.Event)
+			response[evt.Type] = subMap
+		}
+		subMap[*evt.StateKey] = evt
+	}
+
+	arrayEnd, err := dec.Token()
+	if err != nil {
+		return nil, err
+	} else if arrayEnd != json.Delim(']') {
+		return nil, fmt.Errorf("expected array end, got %+v", arrayStart)
+	}
+	return nil, nil
+}
+
+// State gets all state in a room.
+// See https://spec.matrix.org/v1.2/client-server-api/#get_matrixclientv3roomsroomidstate
+func (cli *Client) State(ctx context.Context, roomID id.RoomID) (stateMap RoomStateMap, err error) {
+	_, err = cli.MakeFullRequest(ctx, FullRequest{
+		Method:       http.MethodGet,
+		URL:          cli.BuildClientURL("v3", "rooms", roomID, "state"),
+		ResponseJSON: &stateMap,
+		Handler:      parseRoomStateArray,
+	})
+	if err == nil && cli.StateStore != nil {
+		for evtType, evts := range stateMap {
+			if evtType == event.StateMember {
+				continue
+			}
+			for _, evt := range evts {
+				UpdateStateStore(ctx, cli.StateStore, evt)
+			}
+		}
+		updateErr := cli.StateStore.ReplaceCachedMembers(ctx, roomID, maps.Values(stateMap[event.StateMember]))
+		if updateErr != nil {
+			cli.cliOrContextLog(ctx).Warn().Err(updateErr).
+				Stringer("room_id", roomID).
+				Msg("Failed to update members in state store after fetching members")
+		}
+	}
+	return
+}
+
+// StateAsArray gets all the state in a room as an array. It does not update the state store.
+// Use State to get the events as a map and also update the state store.
+func (cli *Client) StateAsArray(ctx context.Context, roomID id.RoomID) (state []*event.Event, err error) {
+	_, err = cli.MakeRequest(ctx, http.MethodGet, cli.BuildClientURL("v3", "rooms", roomID, "state"), nil, &state)
+	if err == nil {
+		for _, evt := range state {
+			evt.Type.Class = event.StateEventType
+		}
+	}
+	return
+}
+
+// GetMediaConfig fetches the configuration of the content repository, such as upload limitations.
+func (cli *Client) GetMediaConfig(ctx context.Context) (resp *RespMediaConfig, err error) {
+	_, err = cli.MakeRequest(ctx, http.MethodGet, cli.BuildClientURL("v1", "media", "config"), nil, &resp)
+	return
+}
+
+// UploadLink uploads an HTTP URL and then returns an MXC URI.
+func (cli *Client) UploadLink(ctx context.Context, link string) (*RespMediaUpload, error) {
+	req, err := http.NewRequestWithContext(ctx, http.MethodGet, link, nil)
+	if err != nil {
+		return nil, err
+	}
+
+	res, err := cli.Client.Do(req)
+	if res != nil {
+		defer res.Body.Close()
+	}
+	if err != nil {
+		return nil, err
+	}
+	return cli.Upload(ctx, res.Body, res.Header.Get("Content-Type"), res.ContentLength)
+}
+
+func (cli *Client) Download(ctx context.Context, mxcURL id.ContentURI) (*http.Response, error) {
+	_, resp, err := cli.MakeFullRequestWithResp(ctx, FullRequest{
+		Method:           http.MethodGet,
+		URL:              cli.BuildClientURL("v1", "media", "download", mxcURL.Homeserver, mxcURL.FileID),
+		DontReadResponse: true,
+	})
+	return resp, err
+}
+
+func (cli *Client) DownloadBytes(ctx context.Context, mxcURL id.ContentURI) ([]byte, error) {
+	resp, err := cli.Download(ctx, mxcURL)
+	if err != nil {
+		return nil, err
+	}
+	defer resp.Body.Close()
+	return io.ReadAll(resp.Body)
+}
+
+type ReqCreateMXC struct {
+	BeeperUniqueID string
+	BeeperRoomID   id.RoomID
+}
+
+// CreateMXC creates a blank Matrix content URI to allow uploading the content asynchronously later.
+//
+// See https://spec.matrix.org/v1.7/client-server-api/#post_matrixmediav1create
+func (cli *Client) CreateMXC(ctx context.Context, extra ...ReqCreateMXC) (*RespCreateMXC, error) {
+	var m RespCreateMXC
+	query := map[string]string{}
+	if len(extra) > 0 {
+		if extra[0].BeeperUniqueID != "" {
+			query["com.beeper.unique_id"] = extra[0].BeeperUniqueID
+		}
+		if extra[0].BeeperRoomID != "" {
+			query["com.beeper.room_id"] = string(extra[0].BeeperRoomID)
+		}
+	}
+	createURL := cli.BuildURLWithQuery(MediaURLPath{"v1", "create"}, query)
+	_, err := cli.MakeRequest(ctx, http.MethodPost, createURL, nil, &m)
+	return &m, err
+}
+
+// UploadAsync creates a blank content URI with CreateMXC, starts uploading the data in the background
+// and returns the created MXC immediately.
+//
+// See https://spec.matrix.org/v1.7/client-server-api/#post_matrixmediav1create
+// and https://spec.matrix.org/v1.7/client-server-api/#put_matrixmediav3uploadservernamemediaid
+func (cli *Client) UploadAsync(ctx context.Context, req ReqUploadMedia) (*RespCreateMXC, error) {
+	resp, err := cli.CreateMXC(ctx)
+	if err != nil {
+		req.DoneCallback()
+		return nil, err
+	}
+	req.MXC = resp.ContentURI
+	req.UnstableUploadURL = resp.UnstableUploadURL
+	go func() {
+		_, err = cli.UploadMedia(ctx, req)
+		if err != nil {
+			cli.Log.Error().Str("mxc", req.MXC.String()).Err(err).Msg("Async upload of media failed")
+		}
+	}()
+	return resp, nil
+}
+
+func (cli *Client) UploadBytes(ctx context.Context, data []byte, contentType string) (*RespMediaUpload, error) {
+	return cli.UploadBytesWithName(ctx, data, contentType, "")
+}
+
+func (cli *Client) UploadBytesWithName(ctx context.Context, data []byte, contentType, fileName string) (*RespMediaUpload, error) {
+	return cli.UploadMedia(ctx, ReqUploadMedia{
+		ContentBytes: data,
+		ContentType:  contentType,
+		FileName:     fileName,
+	})
+}
+
+// Upload uploads the given data to the content repository and returns an MXC URI.
+//
+// Deprecated: UploadMedia should be used instead.
+func (cli *Client) Upload(ctx context.Context, content io.Reader, contentType string, contentLength int64) (*RespMediaUpload, error) {
+	return cli.UploadMedia(ctx, ReqUploadMedia{
+		Content:       content,
+		ContentLength: contentLength,
+		ContentType:   contentType,
+	})
+}
+
+type ReqUploadMedia struct {
+	ContentBytes  []byte
+	Content       io.Reader
+	ContentLength int64
+	ContentType   string
+	FileName      string
+
+	DoneCallback func()
+
+	// MXC specifies an existing MXC URI which doesn't have content yet to upload into.
+	// See https://spec.matrix.org/unstable/client-server-api/#put_matrixmediav3uploadservernamemediaid
+	MXC id.ContentURI
+
+	// UnstableUploadURL specifies the URL to upload the content to. MXC must also be set.
+	// see https://github.com/matrix-org/matrix-spec-proposals/pull/3870 for more info
+	UnstableUploadURL string
+}
+
+func (cli *Client) tryUploadMediaToURL(ctx context.Context, url, contentType string, content io.Reader, contentLength int64) (*http.Response, error) {
+	cli.Log.Debug().Str("url", url).Msg("Uploading media to external URL")
+	req, err := http.NewRequestWithContext(ctx, http.MethodPut, url, content)
+	if err != nil {
+		return nil, err
+	}
+	req.ContentLength = contentLength
+	req.Header.Set("Content-Type", contentType)
+	req.Header.Set("User-Agent", cli.UserAgent+" (external media uploader)")
+
+	return http.DefaultClient.Do(req)
+}
+
+func (cli *Client) uploadMediaToURL(ctx context.Context, data ReqUploadMedia) (*RespMediaUpload, error) {
+	retries := cli.DefaultHTTPRetries
+	reader := data.Content
+	if data.ContentBytes != nil {
+		data.ContentLength = int64(len(data.ContentBytes))
+		reader = bytes.NewReader(data.ContentBytes)
+	} else if rsc, ok := reader.(io.ReadSeekCloser); ok {
+		// Prevent HTTP from closing the request body, it might be needed for retries
+		reader = nopCloseSeeker{rsc}
+	}
+	readerSeeker, canSeek := reader.(io.ReadSeeker)
+	if !canSeek {
+		retries = 0
+	}
+	for {
+		resp, err := cli.tryUploadMediaToURL(ctx, data.UnstableUploadURL, data.ContentType, reader, data.ContentLength)
+		if err == nil {
+			if resp.StatusCode >= 200 && resp.StatusCode < 300 {
+				// Everything is fine
+				break
+			}
+			err = fmt.Errorf("HTTP %d", resp.StatusCode)
+		}
+		if retries <= 0 {
+			cli.Log.Warn().Str("url", data.UnstableUploadURL).Err(err).
+				Msg("Error uploading media to external URL, not retrying")
+			return nil, err
+		}
+		cli.Log.Warn().Str("url", data.UnstableUploadURL).Err(err).
+			Msg("Error uploading media to external URL, retrying")
+		retries--
+		_, err = readerSeeker.Seek(0, io.SeekStart)
+		if err != nil {
+			return nil, fmt.Errorf("failed to seek back to start of reader: %w", err)
+		}
+	}
+
+	query := map[string]string{}
+	if len(data.FileName) > 0 {
+		query["filename"] = data.FileName
+	}
+
+	notifyURL := cli.BuildURLWithQuery(MediaURLPath{"unstable", "com.beeper.msc3870", "upload", data.MXC.Homeserver, data.MXC.FileID, "complete"}, query)
+
+	var m *RespMediaUpload
+	_, err := cli.MakeRequest(ctx, http.MethodPost, notifyURL, nil, &m)
+	if err != nil {
+		return nil, err
+	}
+
+	return m, nil
+}
+
+type nopCloseSeeker struct {
+	io.ReadSeeker
+}
+
+func (nopCloseSeeker) Close() error {
+	return nil
+}
+
+// UploadMedia uploads the given data to the content repository and returns an MXC URI.
+// See https://spec.matrix.org/v1.7/client-server-api/#post_matrixmediav3upload
+func (cli *Client) UploadMedia(ctx context.Context, data ReqUploadMedia) (*RespMediaUpload, error) {
+	if data.DoneCallback != nil {
+		defer data.DoneCallback()
+	}
+	if data.UnstableUploadURL != "" {
+		if data.MXC.IsEmpty() {
+			return nil, errors.New("MXC must also be set when uploading to external URL")
+		}
+		return cli.uploadMediaToURL(ctx, data)
+	}
+	u, _ := url.Parse(cli.BuildURL(MediaURLPath{"v3", "upload"}))
+	method := http.MethodPost
+	if !data.MXC.IsEmpty() {
+		u, _ = url.Parse(cli.BuildURL(MediaURLPath{"v3", "upload", data.MXC.Homeserver, data.MXC.FileID}))
+		method = http.MethodPut
+	}
+	if len(data.FileName) > 0 {
+		q := u.Query()
+		q.Set("filename", data.FileName)
+		u.RawQuery = q.Encode()
+	}
+
+	var headers http.Header
+	if len(data.ContentType) > 0 {
+		headers = http.Header{"Content-Type": []string{data.ContentType}}
+	}
+
+	var m RespMediaUpload
+	_, err := cli.MakeFullRequest(ctx, FullRequest{
+		Method:        method,
+		URL:           u.String(),
+		Headers:       headers,
+		RequestBytes:  data.ContentBytes,
+		RequestBody:   data.Content,
+		RequestLength: data.ContentLength,
+		ResponseJSON:  &m,
+	})
+	return &m, err
+}
+
+// GetURLPreview asks the homeserver to fetch a preview for a given URL.
+//
+// See https://spec.matrix.org/v1.2/client-server-api/#get_matrixmediav3preview_url
+func (cli *Client) GetURLPreview(ctx context.Context, url string) (*RespPreviewURL, error) {
+	reqURL := cli.BuildURLWithQuery(ClientURLPath{"v1", "media", "preview_url"}, map[string]string{
+		"url": url,
+	})
+	var output RespPreviewURL
+	_, err := cli.MakeRequest(ctx, http.MethodGet, reqURL, nil, &output)
+	return &output, err
+}
+
+// JoinedMembers returns a map of joined room members. See https://spec.matrix.org/v1.2/client-server-api/#get_matrixclientv3roomsroomidjoined_members
+//
+// In general, usage of this API is discouraged in favour of /sync, as calling this API can race with incoming membership changes.
+// This API is primarily designed for application services which may want to efficiently look up joined members in a room.
+func (cli *Client) JoinedMembers(ctx context.Context, roomID id.RoomID) (resp *RespJoinedMembers, err error) {
+	u := cli.BuildClientURL("v3", "rooms", roomID, "joined_members")
+	_, err = cli.MakeRequest(ctx, http.MethodGet, u, nil, &resp)
+	if err == nil && cli.StateStore != nil {
+		fakeEvents := make([]*event.Event, len(resp.Joined))
+		i := 0
+		for userID, member := range resp.Joined {
+			fakeEvents[i] = &event.Event{
+				StateKey: ptr.Ptr(userID.String()),
+				Type:     event.StateMember,
+				RoomID:   roomID,
+				Content: event.Content{Parsed: &event.MemberEventContent{
+					Membership:  event.MembershipJoin,
+					AvatarURL:   id.ContentURIString(member.AvatarURL),
+					Displayname: member.DisplayName,
+				}},
+			}
+			i++
+		}
+		updateErr := cli.StateStore.ReplaceCachedMembers(ctx, roomID, fakeEvents, event.MembershipJoin)
+		if updateErr != nil {
+			cli.cliOrContextLog(ctx).Warn().Err(updateErr).
+				Stringer("room_id", roomID).
+				Msg("Failed to update members in state store after fetching joined members")
+		}
+	}
+	return
+}
+
+func (cli *Client) Members(ctx context.Context, roomID id.RoomID, req ...ReqMembers) (resp *RespMembers, err error) {
+	var extra ReqMembers
+	if len(req) > 0 {
+		extra = req[0]
+	}
+	query := map[string]string{}
+	if len(extra.At) > 0 {
+		query["at"] = extra.At
+	}
+	if len(extra.Membership) > 0 {
+		query["membership"] = string(extra.Membership)
+	}
+	if len(extra.NotMembership) > 0 {
+		query["not_membership"] = string(extra.NotMembership)
+	}
+	u := cli.BuildURLWithQuery(ClientURLPath{"v3", "rooms", roomID, "members"}, query)
+	_, err = cli.MakeRequest(ctx, http.MethodGet, u, nil, &resp)
+	if err == nil {
+		for _, evt := range resp.Chunk {
+			_ = evt.Content.ParseRaw(evt.Type)
+		}
+	}
+	if err == nil && cli.StateStore != nil {
+		var onlyMemberships []event.Membership
+		if extra.Membership != "" {
+			onlyMemberships = []event.Membership{extra.Membership}
+		} else if extra.NotMembership != "" {
+			onlyMemberships = []event.Membership{event.MembershipJoin, event.MembershipLeave, event.MembershipInvite, event.MembershipBan, event.MembershipKnock}
+			onlyMemberships = slices.DeleteFunc(onlyMemberships, func(m event.Membership) bool {
+				return m == extra.NotMembership
+			})
+		}
+		updateErr := cli.StateStore.ReplaceCachedMembers(ctx, roomID, resp.Chunk, onlyMemberships...)
+		if updateErr != nil {
+			cli.cliOrContextLog(ctx).Warn().Err(updateErr).
+				Stringer("room_id", roomID).
+				Msg("Failed to update members in state store after fetching members")
+		}
+	}
+	return
+}
+
+// JoinedRooms returns a list of rooms which the client is joined to. See https://spec.matrix.org/v1.2/client-server-api/#get_matrixclientv3joined_rooms
+//
+// In general, usage of this API is discouraged in favour of /sync, as calling this API can race with incoming membership changes.
+// This API is primarily designed for application services which may want to efficiently look up joined rooms.
+func (cli *Client) JoinedRooms(ctx context.Context) (resp *RespJoinedRooms, err error) {
+	u := cli.BuildClientURL("v3", "joined_rooms")
+	_, err = cli.MakeRequest(ctx, http.MethodGet, u, nil, &resp)
+	return
+}
+
+// Hierarchy returns a list of rooms that are in the room's hierarchy. See https://spec.matrix.org/v1.4/client-server-api/#get_matrixclientv1roomsroomidhierarchy
+//
+// The hierarchy API is provided to walk the space tree and discover the rooms with their aesthetic details. works in a depth-first manner:
+// when it encounters another space as a child it recurses into that space before returning non-space children.
+//
+// The second function parameter specifies query parameters to limit the response. No query parameters will be added if it's nil.
+func (cli *Client) Hierarchy(ctx context.Context, roomID id.RoomID, req *ReqHierarchy) (resp *RespHierarchy, err error) {
+	urlPath := cli.BuildURLWithQuery(ClientURLPath{"v1", "rooms", roomID, "hierarchy"}, req.Query())
+	_, err = cli.MakeRequest(ctx, http.MethodGet, urlPath, nil, &resp)
+	return
+}
+
+// Messages returns a list of message and state events for a room. It uses
+// pagination query parameters to paginate history in the room.
+// See https://spec.matrix.org/v1.12/client-server-api/#get_matrixclientv3roomsroomidmessages
+func (cli *Client) Messages(ctx context.Context, roomID id.RoomID, from, to string, dir Direction, filter *FilterPart, limit int) (resp *RespMessages, err error) {
+	query := map[string]string{
+		"dir": string(dir),
+	}
+	if filter != nil {
+		filterJSON, err := json.Marshal(filter)
+		if err != nil {
+			return nil, err
+		}
+		query["filter"] = string(filterJSON)
+	}
+	if from != "" {
+		query["from"] = from
+	}
+	if to != "" {
+		query["to"] = to
+	}
+	if limit != 0 {
+		query["limit"] = strconv.Itoa(limit)
+	}
+
+	urlPath := cli.BuildURLWithQuery(ClientURLPath{"v3", "rooms", roomID, "messages"}, query)
+	_, err = cli.MakeRequest(ctx, http.MethodGet, urlPath, nil, &resp)
+	return
+}
+
+// TimestampToEvent finds the ID of the event closest to the given timestamp.
+//
+// See https://spec.matrix.org/v1.6/client-server-api/#get_matrixclientv1roomsroomidtimestamp_to_event
+func (cli *Client) TimestampToEvent(ctx context.Context, roomID id.RoomID, timestamp time.Time, dir Direction) (resp *RespTimestampToEvent, err error) {
+	query := map[string]string{
+		"ts":  strconv.FormatInt(timestamp.UnixMilli(), 10),
+		"dir": string(dir),
+	}
+	urlPath := cli.BuildURLWithQuery(ClientURLPath{"v1", "rooms", roomID, "timestamp_to_event"}, query)
+	_, err = cli.MakeRequest(ctx, http.MethodGet, urlPath, nil, &resp)
+	return
+}
+
+// Context returns a number of events that happened just before and after the
+// specified event. It use pagination query parameters to paginate history in
+// the room.
+// See https://spec.matrix.org/v1.2/client-server-api/#get_matrixclientv3roomsroomidcontexteventid
+func (cli *Client) Context(ctx context.Context, roomID id.RoomID, eventID id.EventID, filter *FilterPart, limit int) (resp *RespContext, err error) {
+	query := map[string]string{}
+	if filter != nil {
+		filterJSON, err := json.Marshal(filter)
+		if err != nil {
+			return nil, err
+		}
+		query["filter"] = string(filterJSON)
+	}
+	if limit != 0 {
+		query["limit"] = strconv.Itoa(limit)
+	}
+
+	urlPath := cli.BuildURLWithQuery(ClientURLPath{"v3", "rooms", roomID, "context", eventID}, query)
+	_, err = cli.MakeRequest(ctx, http.MethodGet, urlPath, nil, &resp)
+	return
+}
+
+func (cli *Client) GetEvent(ctx context.Context, roomID id.RoomID, eventID id.EventID) (resp *event.Event, err error) {
+	urlPath := cli.BuildClientURL("v3", "rooms", roomID, "event", eventID)
+	_, err = cli.MakeRequest(ctx, http.MethodGet, urlPath, nil, &resp)
+	return
+}
+
+func (cli *Client) MarkRead(ctx context.Context, roomID id.RoomID, eventID id.EventID) (err error) {
+	return cli.SendReceipt(ctx, roomID, eventID, event.ReceiptTypeRead, nil)
+}
+
+// MarkReadWithContent sends a read receipt including custom data.
+//
+// Deprecated: Use SendReceipt instead.
+func (cli *Client) MarkReadWithContent(ctx context.Context, roomID id.RoomID, eventID id.EventID, content interface{}) (err error) {
+	return cli.SendReceipt(ctx, roomID, eventID, event.ReceiptTypeRead, content)
+}
+
+// SendReceipt sends a receipt, usually specifically a read receipt.
+//
+// Passing nil as the content is safe, the library will automatically replace it with an empty JSON object.
+// To mark a message in a specific thread as read, use pass a ReqSendReceipt as the content.
+func (cli *Client) SendReceipt(ctx context.Context, roomID id.RoomID, eventID id.EventID, receiptType event.ReceiptType, content interface{}) (err error) {
+	urlPath := cli.BuildClientURL("v3", "rooms", roomID, "receipt", receiptType, eventID)
+	_, err = cli.MakeRequest(ctx, http.MethodPost, urlPath, content, nil)
+	return
+}
+
+func (cli *Client) SetReadMarkers(ctx context.Context, roomID id.RoomID, content interface{}) (err error) {
+	urlPath := cli.BuildClientURL("v3", "rooms", roomID, "read_markers")
+	_, err = cli.MakeRequest(ctx, http.MethodPost, urlPath, content, nil)
+	return
+}
+
+func (cli *Client) SetBeeperInboxState(ctx context.Context, roomID id.RoomID, content *ReqSetBeeperInboxState) (err error) {
+	urlPath := cli.BuildClientURL("unstable", "com.beeper.inbox", "user", cli.UserID, "rooms", roomID, "inbox_state")
+	_, err = cli.MakeRequest(ctx, http.MethodPut, urlPath, content, nil)
+	return
+}
+
+func (cli *Client) AddTag(ctx context.Context, roomID id.RoomID, tag event.RoomTag, order float64) error {
+	return cli.AddTagWithCustomData(ctx, roomID, tag, &event.TagMetadata{
+		Order: json.Number(strconv.FormatFloat(order, 'e', -1, 64)),
+	})
+}
+
+func (cli *Client) AddTagWithCustomData(ctx context.Context, roomID id.RoomID, tag event.RoomTag, data any) (err error) {
+	urlPath := cli.BuildClientURL("v3", "user", cli.UserID, "rooms", roomID, "tags", tag)
+	_, err = cli.MakeRequest(ctx, http.MethodPut, urlPath, data, nil)
+	return
+}
+
+func (cli *Client) GetTags(ctx context.Context, roomID id.RoomID) (tags event.TagEventContent, err error) {
+	err = cli.GetTagsWithCustomData(ctx, roomID, &tags)
+	return
+}
+
+func (cli *Client) GetTagsWithCustomData(ctx context.Context, roomID id.RoomID, resp any) (err error) {
+	urlPath := cli.BuildClientURL("v3", "user", cli.UserID, "rooms", roomID, "tags")
+	_, err = cli.MakeRequest(ctx, http.MethodGet, urlPath, nil, &resp)
+	return
+}
+
+func (cli *Client) RemoveTag(ctx context.Context, roomID id.RoomID, tag event.RoomTag) (err error) {
+	urlPath := cli.BuildClientURL("v3", "user", cli.UserID, "rooms", roomID, "tags", tag)
+	_, err = cli.MakeRequest(ctx, http.MethodDelete, urlPath, nil, nil)
+	return
+}
+
+// Deprecated: Synapse may not handle setting m.tag directly properly, so you should use the Add/RemoveTag methods instead.
+func (cli *Client) SetTags(ctx context.Context, roomID id.RoomID, tags event.Tags) (err error) {
+	return cli.SetRoomAccountData(ctx, roomID, "m.tag", map[string]event.Tags{
+		"tags": tags,
+	})
+}
+
+// TurnServer returns turn server details and credentials for the client to use when initiating calls.
+// See https://spec.matrix.org/v1.2/client-server-api/#get_matrixclientv3voipturnserver
+func (cli *Client) TurnServer(ctx context.Context) (resp *RespTurnServer, err error) {
+	urlPath := cli.BuildClientURL("v3", "voip", "turnServer")
+	_, err = cli.MakeRequest(ctx, http.MethodGet, urlPath, nil, &resp)
+	return
+}
+
+func (cli *Client) CreateAlias(ctx context.Context, alias id.RoomAlias, roomID id.RoomID) (resp *RespAliasCreate, err error) {
+	urlPath := cli.BuildClientURL("v3", "directory", "room", alias)
+	_, err = cli.MakeRequest(ctx, http.MethodPut, urlPath, &ReqAliasCreate{RoomID: roomID}, &resp)
+	return
+}
+
+func (cli *Client) ResolveAlias(ctx context.Context, alias id.RoomAlias) (resp *RespAliasResolve, err error) {
+	urlPath := cli.BuildClientURL("v3", "directory", "room", alias)
+	_, err = cli.MakeRequest(ctx, http.MethodGet, urlPath, nil, &resp)
+	return
+}
+
+func (cli *Client) DeleteAlias(ctx context.Context, alias id.RoomAlias) (resp *RespAliasDelete, err error) {
+	urlPath := cli.BuildClientURL("v3", "directory", "room", alias)
+	_, err = cli.MakeRequest(ctx, http.MethodDelete, urlPath, nil, &resp)
+	return
+}
+
+func (cli *Client) GetAliases(ctx context.Context, roomID id.RoomID) (resp *RespAliasList, err error) {
+	urlPath := cli.BuildClientURL("v3", "rooms", roomID, "aliases")
+	_, err = cli.MakeRequest(ctx, http.MethodGet, urlPath, nil, &resp)
+	return
+}
+
+func (cli *Client) UploadKeys(ctx context.Context, req *ReqUploadKeys) (resp *RespUploadKeys, err error) {
+	urlPath := cli.BuildClientURL("v3", "keys", "upload")
+	_, err = cli.MakeRequest(ctx, http.MethodPost, urlPath, req, &resp)
+	return
+}
+
+func (cli *Client) QueryKeys(ctx context.Context, req *ReqQueryKeys) (resp *RespQueryKeys, err error) {
+	urlPath := cli.BuildClientURL("v3", "keys", "query")
+	_, err = cli.MakeRequest(ctx, http.MethodPost, urlPath, req, &resp)
+	return
+}
+
+func (cli *Client) ClaimKeys(ctx context.Context, req *ReqClaimKeys) (resp *RespClaimKeys, err error) {
+	urlPath := cli.BuildClientURL("v3", "keys", "claim")
+	_, err = cli.MakeRequest(ctx, http.MethodPost, urlPath, req, &resp)
+	return
+}
+
+func (cli *Client) GetKeyChanges(ctx context.Context, from, to string) (resp *RespKeyChanges, err error) {
+	urlPath := cli.BuildURLWithQuery(ClientURLPath{"v3", "keys", "changes"}, map[string]string{
+		"from": from,
+		"to":   to,
+	})
+	_, err = cli.MakeRequest(ctx, http.MethodPost, urlPath, nil, &resp)
+	return
+}
+
+// GetKeyBackup retrieves the keys from the backup.
+//
+// See: https://spec.matrix.org/v1.9/client-server-api/#get_matrixclientv3room_keyskeys
+func (cli *Client) GetKeyBackup(ctx context.Context, version id.KeyBackupVersion) (resp *RespRoomKeys[backup.EncryptedSessionData[backup.MegolmSessionData]], err error) {
+	urlPath := cli.BuildURLWithQuery(ClientURLPath{"v3", "room_keys", "keys"}, map[string]string{
+		"version": string(version),
+	})
+	_, err = cli.MakeRequest(ctx, http.MethodGet, urlPath, nil, &resp)
+	return
+}
+
+// PutKeysInBackup stores several keys in the backup.
+//
+// See: https://spec.matrix.org/v1.9/client-server-api/#put_matrixclientv3room_keyskeys
+func (cli *Client) PutKeysInBackup(ctx context.Context, version id.KeyBackupVersion, req *ReqKeyBackup) (resp *RespRoomKeysUpdate, err error) {
+	urlPath := cli.BuildURLWithQuery(ClientURLPath{"v3", "room_keys", "keys"}, map[string]string{
+		"version": string(version),
+	})
+	_, err = cli.MakeRequest(ctx, http.MethodPut, urlPath, req, &resp)
+	return
+}
+
+// DeleteKeyBackup deletes all keys from the backup.
+//
+// See: https://spec.matrix.org/v1.9/client-server-api/#delete_matrixclientv3room_keyskeys
+func (cli *Client) DeleteKeyBackup(ctx context.Context, version id.KeyBackupVersion) (resp *RespRoomKeysUpdate, err error) {
+	urlPath := cli.BuildURLWithQuery(ClientURLPath{"v3", "room_keys", "keys"}, map[string]string{
+		"version": string(version),
+	})
+	_, err = cli.MakeRequest(ctx, http.MethodDelete, urlPath, nil, &resp)
+	return
+}
+
+// GetKeyBackupForRoom retrieves the keys from the backup for the given room.
+//
+// See: https://spec.matrix.org/v1.9/client-server-api/#get_matrixclientv3room_keyskeysroomid
+func (cli *Client) GetKeyBackupForRoom(
+	ctx context.Context, version id.KeyBackupVersion, roomID id.RoomID,
+) (resp *RespRoomKeyBackup[backup.EncryptedSessionData[backup.MegolmSessionData]], err error) {
+	urlPath := cli.BuildURLWithQuery(ClientURLPath{"v3", "room_keys", "keys", roomID.String()}, map[string]string{
+		"version": string(version),
+	})
+	_, err = cli.MakeRequest(ctx, http.MethodGet, urlPath, nil, &resp)
+	return
+}
+
+// PutKeysInBackupForRoom stores several keys in the backup for the given room.
+//
+// See: https://spec.matrix.org/v1.9/client-server-api/#put_matrixclientv3room_keyskeysroomid
+func (cli *Client) PutKeysInBackupForRoom(ctx context.Context, version id.KeyBackupVersion, roomID id.RoomID, req *ReqRoomKeyBackup) (resp *RespRoomKeysUpdate, err error) {
+	urlPath := cli.BuildURLWithQuery(ClientURLPath{"v3", "room_keys", "keys", roomID.String()}, map[string]string{
+		"version": string(version),
+	})
+	_, err = cli.MakeRequest(ctx, http.MethodPut, urlPath, req, &resp)
+	return
+}
+
+// DeleteKeysFromBackupForRoom deletes all the keys in the backup for the given
+// room.
+//
+// See: https://spec.matrix.org/v1.9/client-server-api/#delete_matrixclientv3room_keyskeysroomid
+func (cli *Client) DeleteKeysFromBackupForRoom(ctx context.Context, version id.KeyBackupVersion, roomID id.RoomID) (resp *RespRoomKeysUpdate, err error) {
+	urlPath := cli.BuildURLWithQuery(ClientURLPath{"v3", "room_keys", "keys", roomID.String()}, map[string]string{
+		"version": string(version),
+	})
+	_, err = cli.MakeRequest(ctx, http.MethodDelete, urlPath, nil, &resp)
+	return
+}
+
+// GetKeyBackupForRoomAndSession retrieves a key from the backup.
+//
+// See: https://spec.matrix.org/v1.9/client-server-api/#get_matrixclientv3room_keyskeysroomidsessionid
+func (cli *Client) GetKeyBackupForRoomAndSession(
+	ctx context.Context, version id.KeyBackupVersion, roomID id.RoomID, sessionID id.SessionID,
+) (resp *RespKeyBackupData[backup.EncryptedSessionData[backup.MegolmSessionData]], err error) {
+	urlPath := cli.BuildURLWithQuery(ClientURLPath{"v3", "room_keys", "keys", roomID.String(), sessionID.String()}, map[string]string{
+		"version": string(version),
+	})
+	_, err = cli.MakeRequest(ctx, http.MethodGet, urlPath, nil, &resp)
+	return
+}
+
+// PutKeysInBackupForRoomAndSession stores a key in the backup.
+//
+// See: https://spec.matrix.org/v1.9/client-server-api/#put_matrixclientv3room_keyskeysroomidsessionid
+func (cli *Client) PutKeysInBackupForRoomAndSession(ctx context.Context, version id.KeyBackupVersion, roomID id.RoomID, sessionID id.SessionID, req *ReqKeyBackupData) (resp *RespRoomKeysUpdate, err error) {
+	urlPath := cli.BuildURLWithQuery(ClientURLPath{"v3", "room_keys", "keys", roomID.String(), sessionID.String()}, map[string]string{
+		"version": string(version),
+	})
+	_, err = cli.MakeRequest(ctx, http.MethodPut, urlPath, req, &resp)
+	return
+}
+
+// DeleteKeysInBackupForRoomAndSession deletes a key from the backup.
+//
+// See: https://spec.matrix.org/v1.9/client-server-api/#delete_matrixclientv3room_keyskeysroomidsessionid
+func (cli *Client) DeleteKeysInBackupForRoomAndSession(ctx context.Context, version id.KeyBackupVersion, roomID id.RoomID, sessionID id.SessionID) (resp *RespRoomKeysUpdate, err error) {
+	urlPath := cli.BuildURLWithQuery(ClientURLPath{"v3", "room_keys", "keys", roomID.String(), sessionID.String()}, map[string]string{
+		"version": string(version),
+	})
+	_, err = cli.MakeRequest(ctx, http.MethodDelete, urlPath, nil, &resp)
+	return
+}
+
+// GetKeyBackupLatestVersion returns information about the latest backup version.
+//
+// See: https://spec.matrix.org/v1.9/client-server-api/#get_matrixclientv3room_keysversion
+func (cli *Client) GetKeyBackupLatestVersion(ctx context.Context) (resp *RespRoomKeysVersion[backup.MegolmAuthData], err error) {
+	urlPath := cli.BuildClientURL("v3", "room_keys", "version")
+	_, err = cli.MakeRequest(ctx, http.MethodGet, urlPath, nil, &resp)
+	return
+}
+
+// CreateKeyBackupVersion creates a new key backup.
+//
+// See: https://spec.matrix.org/v1.9/client-server-api/#post_matrixclientv3room_keysversion
+func (cli *Client) CreateKeyBackupVersion(ctx context.Context, req *ReqRoomKeysVersionCreate[backup.MegolmAuthData]) (resp *RespRoomKeysVersionCreate, err error) {
+	urlPath := cli.BuildClientURL("v3", "room_keys", "version")
+	_, err = cli.MakeRequest(ctx, http.MethodPost, urlPath, req, &resp)
+	return
+}
+
+// GetKeyBackupVersion returns information about an existing key backup.
+//
+// See: https://spec.matrix.org/v1.9/client-server-api/#get_matrixclientv3room_keysversionversion
+func (cli *Client) GetKeyBackupVersion(ctx context.Context, version id.KeyBackupVersion) (resp *RespRoomKeysVersion[backup.MegolmAuthData], err error) {
+	urlPath := cli.BuildClientURL("v3", "room_keys", "version", version)
+	_, err = cli.MakeRequest(ctx, http.MethodGet, urlPath, nil, &resp)
+	return
+}
+
+// UpdateKeyBackupVersion updates information about an existing key backup. Only
+// the auth_data can be modified.
+//
+// See: https://spec.matrix.org/v1.9/client-server-api/#put_matrixclientv3room_keysversionversion
+func (cli *Client) UpdateKeyBackupVersion(ctx context.Context, version id.KeyBackupVersion, req *ReqRoomKeysVersionUpdate[backup.MegolmAuthData]) error {
+	urlPath := cli.BuildClientURL("v3", "room_keys", "version", version)
+	_, err := cli.MakeRequest(ctx, http.MethodPut, urlPath, nil, nil)
+	return err
+}
+
+// DeleteKeyBackupVersion deletes an existing key backup. Both the information
+// about the backup, as well as all key data related to the backup will be
+// deleted.
+//
+// See: https://spec.matrix.org/v1.1/client-server-api/#delete_matrixclientv3room_keysversionversion
+func (cli *Client) DeleteKeyBackupVersion(ctx context.Context, version id.KeyBackupVersion) error {
+	urlPath := cli.BuildClientURL("v3", "room_keys", "version", version)
+	_, err := cli.MakeRequest(ctx, http.MethodDelete, urlPath, nil, nil)
+	return err
+}
+
+func (cli *Client) SendToDevice(ctx context.Context, eventType event.Type, req *ReqSendToDevice) (resp *RespSendToDevice, err error) {
+	urlPath := cli.BuildClientURL("v3", "sendToDevice", eventType.String(), cli.TxnID())
+	_, err = cli.MakeRequest(ctx, http.MethodPut, urlPath, req, &resp)
+	return
+}
+
+func (cli *Client) GetDevicesInfo(ctx context.Context) (resp *RespDevicesInfo, err error) {
+	urlPath := cli.BuildClientURL("v3", "devices")
+	_, err = cli.MakeRequest(ctx, http.MethodGet, urlPath, nil, &resp)
+	return
+}
+
+func (cli *Client) GetDeviceInfo(ctx context.Context, deviceID id.DeviceID) (resp *RespDeviceInfo, err error) {
+	urlPath := cli.BuildClientURL("v3", "devices", deviceID)
+	_, err = cli.MakeRequest(ctx, http.MethodGet, urlPath, nil, &resp)
+	return
+}
+
+func (cli *Client) SetDeviceInfo(ctx context.Context, deviceID id.DeviceID, req *ReqDeviceInfo) error {
+	urlPath := cli.BuildClientURL("v3", "devices", deviceID)
+	_, err := cli.MakeRequest(ctx, http.MethodPut, urlPath, req, nil)
+	return err
+}
+
+func (cli *Client) DeleteDevice(ctx context.Context, deviceID id.DeviceID, req *ReqDeleteDevice) error {
+	urlPath := cli.BuildClientURL("v3", "devices", deviceID)
+	_, err := cli.MakeRequest(ctx, http.MethodDelete, urlPath, req, nil)
+	return err
+}
+
+func (cli *Client) DeleteDevices(ctx context.Context, req *ReqDeleteDevices) error {
+	urlPath := cli.BuildClientURL("v3", "delete_devices")
+	_, err := cli.MakeRequest(ctx, http.MethodDelete, urlPath, req, nil)
+	return err
+}
+
+type UIACallback = func(*RespUserInteractive) interface{}
+
+// UploadCrossSigningKeys uploads the given cross-signing keys to the server.
+// Because the endpoint requires user-interactive authentication a callback must be provided that,
+// given the UI auth parameters, produces the required result (or nil to end the flow).
+func (cli *Client) UploadCrossSigningKeys(ctx context.Context, keys *UploadCrossSigningKeysReq, uiaCallback UIACallback) error {
+	content, err := cli.MakeFullRequest(ctx, FullRequest{
+		Method:           http.MethodPost,
+		URL:              cli.BuildClientURL("v3", "keys", "device_signing", "upload"),
+		RequestJSON:      keys,
+		SensitiveContent: keys.Auth != nil,
+	})
+	if respErr, ok := err.(HTTPError); ok && respErr.IsStatus(http.StatusUnauthorized) && uiaCallback != nil {
+		// try again with UI auth
+		var uiAuthResp RespUserInteractive
+		if err := json.Unmarshal(content, &uiAuthResp); err != nil {
+			return fmt.Errorf("failed to decode UIA response: %w", err)
+		}
+		auth := uiaCallback(&uiAuthResp)
+		if auth != nil {
+			keys.Auth = auth
+			return cli.UploadCrossSigningKeys(ctx, keys, nil)
+		}
+	}
+	return err
+}
+
+func (cli *Client) UploadSignatures(ctx context.Context, req *ReqUploadSignatures) (resp *RespUploadSignatures, err error) {
+	urlPath := cli.BuildClientURL("v3", "keys", "signatures", "upload")
+	_, err = cli.MakeRequest(ctx, http.MethodPost, urlPath, req, &resp)
+	return
+}
+
+// GetPushRules returns the push notification rules for the global scope.
+func (cli *Client) GetPushRules(ctx context.Context) (*pushrules.PushRuleset, error) {
+	return cli.GetScopedPushRules(ctx, "global")
+}
+
+// GetScopedPushRules returns the push notification rules for the given scope.
+func (cli *Client) GetScopedPushRules(ctx context.Context, scope string) (resp *pushrules.PushRuleset, err error) {
+	u, _ := url.Parse(cli.BuildClientURL("v3", "pushrules", scope))
+	// client.BuildURL returns the URL without a trailing slash, but the pushrules endpoint requires the slash.
+	u.Path += "/"
+	_, err = cli.MakeRequest(ctx, http.MethodGet, u.String(), nil, &resp)
+	return
+}
+
+func (cli *Client) GetPushRule(ctx context.Context, scope string, kind pushrules.PushRuleType, ruleID string) (resp *pushrules.PushRule, err error) {
+	urlPath := cli.BuildClientURL("v3", "pushrules", scope, kind, ruleID)
+	_, err = cli.MakeRequest(ctx, http.MethodGet, urlPath, nil, &resp)
+	if resp != nil {
+		resp.Type = kind
+	}
+	return
+}
+
+func (cli *Client) DeletePushRule(ctx context.Context, scope string, kind pushrules.PushRuleType, ruleID string) error {
+	urlPath := cli.BuildClientURL("v3", "pushrules", scope, kind, ruleID)
+	_, err := cli.MakeRequest(ctx, http.MethodDelete, urlPath, nil, nil)
+	return err
+}
+
+func (cli *Client) PutPushRule(ctx context.Context, scope string, kind pushrules.PushRuleType, ruleID string, req *ReqPutPushRule) error {
+	query := make(map[string]string)
+	if len(req.After) > 0 {
+		query["after"] = req.After
+	}
+	if len(req.Before) > 0 {
+		query["before"] = req.Before
+	}
+	urlPath := cli.BuildURLWithQuery(ClientURLPath{"v3", "pushrules", scope, kind, ruleID}, query)
+	_, err := cli.MakeRequest(ctx, http.MethodPut, urlPath, req, nil)
+	return err
+}
+
+func (cli *Client) ReportEvent(ctx context.Context, roomID id.RoomID, eventID id.EventID, reason string) error {
+	urlPath := cli.BuildClientURL("v3", "rooms", roomID, "report", eventID)
+	_, err := cli.MakeRequest(ctx, http.MethodPost, urlPath, &ReqReport{Reason: reason, Score: -100}, nil)
+	return err
+}
+
+func (cli *Client) ReportRoom(ctx context.Context, roomID id.RoomID, reason string) error {
+	urlPath := cli.BuildClientURL("v3", "rooms", roomID, "report")
+	_, err := cli.MakeRequest(ctx, http.MethodPost, urlPath, &ReqReport{Reason: reason, Score: -100}, nil)
+	return err
+}
+
+// BatchSend sends a batch of historical events into a room. This is only available for appservices.
+//
+// Deprecated: MSC2716 has been abandoned, so this is now Beeper-specific. BeeperBatchSend should be used instead.
+func (cli *Client) BatchSend(ctx context.Context, roomID id.RoomID, req *ReqBatchSend) (resp *RespBatchSend, err error) {
+	path := ClientURLPath{"unstable", "org.matrix.msc2716", "rooms", roomID, "batch_send"}
+	query := map[string]string{
+		"prev_event_id": req.PrevEventID.String(),
+	}
+	if req.BeeperNewMessages {
+		query["com.beeper.new_messages"] = "true"
+	}
+	if req.BeeperMarkReadBy != "" {
+		query["com.beeper.mark_read_by"] = req.BeeperMarkReadBy.String()
+	}
+	if len(req.BatchID) > 0 {
+		query["batch_id"] = req.BatchID.String()
+	}
+	_, err = cli.MakeRequest(ctx, http.MethodPost, cli.BuildURLWithQuery(path, query), req, &resp)
+	return
+}
+
+func (cli *Client) AppservicePing(ctx context.Context, id, txnID string) (resp *RespAppservicePing, err error) {
+	_, err = cli.MakeFullRequest(ctx, FullRequest{
+		Method:       http.MethodPost,
+		URL:          cli.BuildClientURL("v1", "appservice", id, "ping"),
+		RequestJSON:  &ReqAppservicePing{TxnID: txnID},
+		ResponseJSON: &resp,
+		// This endpoint intentionally returns 50x, so don't retry
+		MaxAttempts: 1,
+	})
+	return
+}
+
+func (cli *Client) BeeperBatchSend(ctx context.Context, roomID id.RoomID, req *ReqBeeperBatchSend) (resp *RespBeeperBatchSend, err error) {
+	u := cli.BuildClientURL("unstable", "com.beeper.backfill", "rooms", roomID, "batch_send")
+	_, err = cli.MakeRequest(ctx, http.MethodPost, u, req, &resp)
+	return
+}
+
+func (cli *Client) BeeperMergeRooms(ctx context.Context, req *ReqBeeperMergeRoom) (resp *RespBeeperMergeRoom, err error) {
+	urlPath := cli.BuildClientURL("unstable", "com.beeper.chatmerging", "merge")
+	_, err = cli.MakeRequest(ctx, http.MethodPost, urlPath, req, &resp)
+	return
+}
+
+func (cli *Client) BeeperSplitRoom(ctx context.Context, req *ReqBeeperSplitRoom) (resp *RespBeeperSplitRoom, err error) {
+	urlPath := cli.BuildClientURL("unstable", "com.beeper.chatmerging", "rooms", req.RoomID, "split")
+	_, err = cli.MakeRequest(ctx, http.MethodPost, urlPath, req, &resp)
+	return
+}
+func (cli *Client) BeeperDeleteRoom(ctx context.Context, roomID id.RoomID) (err error) {
+	urlPath := cli.BuildClientURL("unstable", "com.beeper.yeet", "rooms", roomID, "delete")
+	_, err = cli.MakeRequest(ctx, http.MethodPost, urlPath, nil, nil)
+	return
+}
+
+// TxnID returns the next transaction ID.
+func (cli *Client) TxnID() string {
+	txnID := atomic.AddInt32(&cli.txnID, 1)
+	return fmt.Sprintf("mautrix-go_%d_%d", time.Now().UnixNano(), txnID)
+}
+
+// NewClient creates a new Matrix Client ready for syncing
+func NewClient(homeserverURL string, userID id.UserID, accessToken string) (*Client, error) {
+	hsURL, err := ParseAndNormalizeBaseURL(homeserverURL)
+	if err != nil {
+		return nil, err
+	}
+	return &Client{
+		AccessToken:   accessToken,
+		UserAgent:     DefaultUserAgent,
+		HomeserverURL: hsURL,
+		UserID:        userID,
+		Client:        &http.Client{Timeout: 180 * time.Second},
+		Syncer:        NewDefaultSyncer(),
+		Log:           zerolog.Nop(),
+		// By default, use an in-memory store which will never save filter ids / next batch tokens to disk.
+		// The client will work with this storer: it just won't remember across restarts.
+		// In practice, a database backend should be used.
+		Store: NewMemorySyncStore(),
+	}, nil
+}
diff --git a/vendor/maunium.net/go/mautrix/crypto/aescbc/aes_cbc.go b/vendor/maunium.net/go/mautrix/crypto/aescbc/aes_cbc.go
new file mode 100644
index 0000000..d69a5f4
--- /dev/null
+++ b/vendor/maunium.net/go/mautrix/crypto/aescbc/aes_cbc.go
@@ -0,0 +1,60 @@
+// Copyright (c) 2024 Sumner Evans
+//
+// 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 aescbc
+
+import (
+	"crypto/aes"
+	"crypto/cipher"
+
+	"maunium.net/go/mautrix/crypto/pkcs7"
+)
+
+// Encrypt encrypts the plaintext with the key and IV. The IV length must be
+// equal to the AES block size.
+//
+// This function might mutate the plaintext.
+func Encrypt(key, iv, plaintext []byte) ([]byte, error) {
+	if len(key) == 0 {
+		return nil, ErrNoKeyProvided
+	}
+	if len(iv) != aes.BlockSize {
+		return nil, ErrIVNotBlockSize
+	}
+	plaintext = pkcs7.Pad(plaintext, aes.BlockSize)
+
+	block, err := aes.NewCipher(key)
+	if err != nil {
+		return nil, err
+	}
+
+	cipher.NewCBCEncrypter(block, iv).CryptBlocks(plaintext, plaintext)
+	return plaintext, nil
+}
+
+// Decrypt decrypts the ciphertext with the key and IV. The IV length must be
+// equal to the block size.
+//
+// This function mutates the ciphertext.
+func Decrypt(key, iv, ciphertext []byte) ([]byte, error) {
+	if len(key) == 0 {
+		return nil, ErrNoKeyProvided
+	}
+	if len(iv) != aes.BlockSize {
+		return nil, ErrIVNotBlockSize
+	}
+
+	block, err := aes.NewCipher(key)
+	if err != nil {
+		return nil, err
+	}
+	if len(ciphertext) < aes.BlockSize {
+		return nil, ErrNotMultipleBlockSize
+	}
+
+	cipher.NewCBCDecrypter(block, iv).CryptBlocks(ciphertext, ciphertext)
+	return pkcs7.Unpad(ciphertext), nil
+}
diff --git a/vendor/maunium.net/go/mautrix/crypto/aescbc/errors.go b/vendor/maunium.net/go/mautrix/crypto/aescbc/errors.go
new file mode 100644
index 0000000..f3d2d7c
--- /dev/null
+++ b/vendor/maunium.net/go/mautrix/crypto/aescbc/errors.go
@@ -0,0 +1,15 @@
+// Copyright (c) 2024 Sumner Evans
+//
+// 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 aescbc
+
+import "errors"
+
+var (
+	ErrNoKeyProvided        = errors.New("no key")
+	ErrIVNotBlockSize       = errors.New("IV length does not match AES block size")
+	ErrNotMultipleBlockSize = errors.New("ciphertext length is not a multiple of the AES block size")
+)
diff --git a/vendor/maunium.net/go/mautrix/crypto/attachment/attachments.go b/vendor/maunium.net/go/mautrix/crypto/attachment/attachments.go
new file mode 100644
index 0000000..cfa1c3e
--- /dev/null
+++ b/vendor/maunium.net/go/mautrix/crypto/attachment/attachments.go
@@ -0,0 +1,300 @@
+// 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 attachment
+
+import (
+	"crypto/aes"
+	"crypto/cipher"
+	"crypto/sha256"
+	"encoding/base64"
+	"errors"
+	"fmt"
+	"hash"
+	"io"
+
+	"maunium.net/go/mautrix/crypto/utils"
+)
+
+var (
+	HashMismatch         = errors.New("mismatching SHA-256 digest")
+	UnsupportedVersion   = errors.New("unsupported Matrix file encryption version")
+	UnsupportedAlgorithm = errors.New("unsupported JWK encryption algorithm")
+	InvalidKey           = errors.New("failed to decode key")
+	InvalidInitVector    = errors.New("failed to decode initialization vector")
+	InvalidHash          = errors.New("failed to decode SHA-256 hash")
+	ReaderClosed         = errors.New("encrypting reader was already closed")
+)
+
+var (
+	keyBase64Length  = base64.RawURLEncoding.EncodedLen(utils.AESCTRKeyLength)
+	ivBase64Length   = base64.RawStdEncoding.EncodedLen(utils.AESCTRIVLength)
+	hashBase64Length = base64.RawStdEncoding.EncodedLen(utils.SHAHashLength)
+)
+
+type JSONWebKey struct {
+	Key         string   `json:"k"`
+	Algorithm   string   `json:"alg"`
+	Extractable bool     `json:"ext"`
+	KeyType     string   `json:"kty"`
+	KeyOps      []string `json:"key_ops"`
+}
+
+type EncryptedFileHashes struct {
+	SHA256 string `json:"sha256"`
+}
+
+type decodedKeys struct {
+	key [utils.AESCTRKeyLength]byte
+	iv  [utils.AESCTRIVLength]byte
+
+	sha256 [utils.SHAHashLength]byte
+}
+
+type EncryptedFile struct {
+	Key        JSONWebKey          `json:"key"`
+	InitVector string              `json:"iv"`
+	Hashes     EncryptedFileHashes `json:"hashes"`
+	Version    string              `json:"v"`
+
+	decoded *decodedKeys
+}
+
+func NewEncryptedFile() *EncryptedFile {
+	key, iv := utils.GenAttachmentA256CTR()
+	return &EncryptedFile{
+		Key: JSONWebKey{
+			Key:         base64.RawURLEncoding.EncodeToString(key[:]),
+			Algorithm:   "A256CTR",
+			Extractable: true,
+			KeyType:     "oct",
+			KeyOps:      []string{"encrypt", "decrypt"},
+		},
+		InitVector: base64.RawStdEncoding.EncodeToString(iv[:]),
+		Version:    "v2",
+
+		decoded: &decodedKeys{key: key, iv: iv},
+	}
+}
+
+func (ef *EncryptedFile) decodeKeys(includeHash bool) error {
+	if ef.decoded != nil {
+		return nil
+	} else if len(ef.Key.Key) != keyBase64Length {
+		return InvalidKey
+	} else if len(ef.InitVector) != ivBase64Length {
+		return InvalidInitVector
+	} else if includeHash && len(ef.Hashes.SHA256) != hashBase64Length {
+		return InvalidHash
+	}
+	ef.decoded = &decodedKeys{}
+	_, err := base64.RawURLEncoding.Decode(ef.decoded.key[:], []byte(ef.Key.Key))
+	if err != nil {
+		return InvalidKey
+	}
+	_, err = base64.RawStdEncoding.Decode(ef.decoded.iv[:], []byte(ef.InitVector))
+	if err != nil {
+		return InvalidInitVector
+	}
+	if includeHash {
+		_, err = base64.RawStdEncoding.Decode(ef.decoded.sha256[:], []byte(ef.Hashes.SHA256))
+		if err != nil {
+			return InvalidHash
+		}
+	}
+	return nil
+}
+
+// Encrypt encrypts the given data, updates the SHA256 hash in the EncryptedFile struct and returns the ciphertext.
+//
+// Deprecated: this makes a copy for the ciphertext, which means 2x memory usage. EncryptInPlace is recommended.
+func (ef *EncryptedFile) Encrypt(plaintext []byte) []byte {
+	ciphertext := make([]byte, len(plaintext))
+	copy(ciphertext, plaintext)
+	ef.EncryptInPlace(ciphertext)
+	return ciphertext
+}
+
+// EncryptInPlace encrypts the given data in-place (i.e. the provided data is overridden with the ciphertext)
+// and updates the SHA256 hash in the EncryptedFile struct.
+func (ef *EncryptedFile) EncryptInPlace(data []byte) {
+	ef.decodeKeys(false)
+	utils.XorA256CTR(data, ef.decoded.key, ef.decoded.iv)
+	checksum := sha256.Sum256(data)
+	ef.Hashes.SHA256 = base64.RawStdEncoding.EncodeToString(checksum[:])
+}
+
+type ReadWriterAt interface {
+	io.WriterAt
+	io.Reader
+}
+
+// EncryptFile encrypts the given file in-place and updates the SHA256 hash in the EncryptedFile struct.
+func (ef *EncryptedFile) EncryptFile(file ReadWriterAt) error {
+	err := ef.decodeKeys(false)
+	if err != nil {
+		return err
+	}
+	block, _ := aes.NewCipher(ef.decoded.key[:])
+	stream := cipher.NewCTR(block, ef.decoded.iv[:])
+	hasher := sha256.New()
+	buf := make([]byte, 32*1024)
+	var writePtr int64
+	var n int
+	for {
+		n, err = file.Read(buf)
+		if err != nil && !errors.Is(err, io.EOF) {
+			return err
+		}
+		if n == 0 {
+			break
+		}
+		stream.XORKeyStream(buf[:n], buf[:n])
+		_, err = file.WriteAt(buf[:n], writePtr)
+		if err != nil {
+			return err
+		}
+		writePtr += int64(n)
+		hasher.Write(buf[:n])
+	}
+	ef.Hashes.SHA256 = base64.RawStdEncoding.EncodeToString(hasher.Sum(nil))
+	return nil
+}
+
+type encryptingReader struct {
+	stream cipher.Stream
+	hash   hash.Hash
+	source io.Reader
+	file   *EncryptedFile
+	closed bool
+
+	isDecrypting bool
+}
+
+var _ io.ReadSeekCloser = (*encryptingReader)(nil)
+
+func (r *encryptingReader) Seek(offset int64, whence int) (int64, error) {
+	if r.closed {
+		return 0, ReaderClosed
+	}
+	if offset != 0 || whence != io.SeekStart {
+		return 0, fmt.Errorf("attachments.EncryptStream: only seeking to the beginning is supported")
+	}
+	seeker, ok := r.source.(io.ReadSeeker)
+	if !ok {
+		return 0, fmt.Errorf("attachments.EncryptStream: source reader (%T) is not an io.ReadSeeker", r.source)
+	}
+	n, err := seeker.Seek(offset, whence)
+	if err != nil {
+		return 0, err
+	}
+	block, _ := aes.NewCipher(r.file.decoded.key[:])
+	r.stream = cipher.NewCTR(block, r.file.decoded.iv[:])
+	r.hash.Reset()
+	return n, nil
+}
+
+func (r *encryptingReader) Read(dst []byte) (n int, err error) {
+	if r.closed {
+		return 0, ReaderClosed
+	} else if r.isDecrypting && r.file.decoded == nil {
+		if err = r.file.PrepareForDecryption(); err != nil {
+			return
+		}
+	}
+	n, err = r.source.Read(dst)
+	r.stream.XORKeyStream(dst[:n], dst[:n])
+	r.hash.Write(dst[:n])
+	return
+}
+
+func (r *encryptingReader) Close() (err error) {
+	closer, ok := r.source.(io.ReadCloser)
+	if ok {
+		err = closer.Close()
+	}
+	if r.isDecrypting {
+		var downloadedChecksum [utils.SHAHashLength]byte
+		r.hash.Sum(downloadedChecksum[:])
+		if downloadedChecksum != r.file.decoded.sha256 {
+			return HashMismatch
+		}
+	} else {
+		r.file.Hashes.SHA256 = base64.RawStdEncoding.EncodeToString(r.hash.Sum(nil))
+	}
+	r.closed = true
+	return
+}
+
+// EncryptStream wraps the given io.Reader in order to encrypt the data.
+//
+// The Close() method of the returned io.ReadCloser must be called for the SHA256 hash
+// in the EncryptedFile struct to be updated. The metadata is not valid before the hash
+// is filled.
+func (ef *EncryptedFile) EncryptStream(reader io.Reader) io.ReadSeekCloser {
+	ef.decodeKeys(false)
+	block, _ := aes.NewCipher(ef.decoded.key[:])
+	return &encryptingReader{
+		stream: cipher.NewCTR(block, ef.decoded.iv[:]),
+		hash:   sha256.New(),
+		source: reader,
+		file:   ef,
+	}
+}
+
+// Decrypt decrypts the given data and returns the plaintext.
+//
+// Deprecated: this makes a copy for the plaintext data, which means 2x memory usage. DecryptInPlace is recommended.
+func (ef *EncryptedFile) Decrypt(ciphertext []byte) ([]byte, error) {
+	plaintext := make([]byte, len(ciphertext))
+	copy(plaintext, ciphertext)
+	return plaintext, ef.DecryptInPlace(plaintext)
+}
+
+// PrepareForDecryption checks that the version and algorithm are supported and decodes the base64 keys
+//
+// DecryptStream will call this with the first Read() call if this hasn't been called manually.
+//
+// DecryptInPlace will always call this automatically, so calling this manually is not necessary when using that function.
+func (ef *EncryptedFile) PrepareForDecryption() error {
+	if ef.Version != "v2" {
+		return UnsupportedVersion
+	} else if ef.Key.Algorithm != "A256CTR" {
+		return UnsupportedAlgorithm
+	} else if err := ef.decodeKeys(true); err != nil {
+		return err
+	}
+	return nil
+}
+
+// DecryptInPlace decrypts the given data in-place (i.e. the provided data is overridden with the plaintext).
+func (ef *EncryptedFile) DecryptInPlace(data []byte) error {
+	if err := ef.PrepareForDecryption(); err != nil {
+		return err
+	} else if ef.decoded.sha256 != sha256.Sum256(data) {
+		return HashMismatch
+	} else {
+		utils.XorA256CTR(data, ef.decoded.key, ef.decoded.iv)
+		return nil
+	}
+}
+
+// DecryptStream wraps the given io.Reader in order to decrypt the data.
+//
+// The first Read call will check the algorithm and decode keys, so it might return an error before actually reading anything.
+// If you want to validate the file before opening the stream, call PrepareForDecryption manually and check for errors.
+//
+// The Close call will validate the hash and return an error if it doesn't match.
+// In this case, the written data should be considered compromised and should not be used further.
+func (ef *EncryptedFile) DecryptStream(reader io.Reader) io.ReadSeekCloser {
+	block, _ := aes.NewCipher(ef.decoded.key[:])
+	return &encryptingReader{
+		stream: cipher.NewCTR(block, ef.decoded.iv[:]),
+		hash:   sha256.New(),
+		source: reader,
+		file:   ef,
+	}
+}
diff --git a/vendor/maunium.net/go/mautrix/crypto/backup/encryptedsessiondata.go b/vendor/maunium.net/go/mautrix/crypto/backup/encryptedsessiondata.go
new file mode 100644
index 0000000..ec551db
--- /dev/null
+++ b/vendor/maunium.net/go/mautrix/crypto/backup/encryptedsessiondata.go
@@ -0,0 +1,131 @@
+// Copyright (c) 2024 Sumner Evans
+//
+// 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 backup
+
+import (
+	"bytes"
+	"crypto/ecdh"
+	"crypto/hmac"
+	"crypto/rand"
+	"crypto/sha256"
+	"encoding/json"
+	"errors"
+
+	"go.mau.fi/util/jsonbytes"
+	"golang.org/x/crypto/hkdf"
+
+	"maunium.net/go/mautrix/crypto/aescbc"
+)
+
+var ErrInvalidMAC = errors.New("invalid MAC")
+
+// EncryptedSessionData is the encrypted session_data field of a key backup as
+// defined in [Section 11.12.3.2.2 of the Spec].
+//
+// The type parameter T represents the format of the session data contained in
+// the encrypted payload.
+//
+// [Section 11.12.3.2.2 of the Spec]: https://spec.matrix.org/v1.9/client-server-api/#backup-algorithm-mmegolm_backupv1curve25519-aes-sha2
+type EncryptedSessionData[T any] struct {
+	Ciphertext jsonbytes.UnpaddedBytes `json:"ciphertext"`
+	Ephemeral  EphemeralKey            `json:"ephemeral"`
+	MAC        jsonbytes.UnpaddedBytes `json:"mac"`
+}
+
+func calculateEncryptionParameters(sharedSecret []byte) (key, macKey, iv []byte, err error) {
+	hkdfReader := hkdf.New(sha256.New, sharedSecret, nil, nil)
+	encryptionParams := make([]byte, 80)
+	_, err = hkdfReader.Read(encryptionParams)
+	if err != nil {
+		return nil, nil, nil, err
+	}
+
+	return encryptionParams[:32], encryptionParams[32:64], encryptionParams[64:], nil
+}
+
+// calculateCompatMAC calculates the MAC as described in step 5 of according to
+// [Section 11.12.3.2.2] of the Spec which was updated in spec version 1.10 to
+// reflect what is actually implemented in libolm and Vodozemac.
+//
+// Libolm implemented the MAC functionality incorrectly. The MAC is computed
+// over an empty string rather than the ciphertext. Vodozemac implemented this
+// functionality the same way as libolm for compatibility. In version 1.10 of
+// the spec, the description of step 5 was updated to reflect the de-facto
+// standard of libolm and Vodozemac.
+//
+// [Section 11.12.3.2.2]: https://spec.matrix.org/v1.11/client-server-api/#backup-algorithm-mmegolm_backupv1curve25519-aes-sha2
+func calculateCompatMAC(macKey []byte) []byte {
+	hash := hmac.New(sha256.New, macKey)
+	return hash.Sum(nil)[:8]
+}
+
+// EncryptSessionData encrypts the given session data with the given recovery
+// key as defined in [Section 11.12.3.2.2 of the Spec].
+//
+// [Section 11.12.3.2.2 of the Spec]: https://spec.matrix.org/v1.9/client-server-api/#backup-algorithm-mmegolm_backupv1curve25519-aes-sha2
+func EncryptSessionData[T any](backupKey *MegolmBackupKey, sessionData T) (*EncryptedSessionData[T], error) {
+	sessionJSON, err := json.Marshal(sessionData)
+	if err != nil {
+		return nil, err
+	}
+
+	ephemeralKey, err := ecdh.X25519().GenerateKey(rand.Reader)
+	if err != nil {
+		return nil, err
+	}
+
+	sharedSecret, err := ephemeralKey.ECDH(backupKey.PublicKey())
+	if err != nil {
+		return nil, err
+	}
+
+	key, macKey, iv, err := calculateEncryptionParameters(sharedSecret)
+	if err != nil {
+		return nil, err
+	}
+
+	ciphertext, err := aescbc.Encrypt(key, iv, sessionJSON)
+	if err != nil {
+		return nil, err
+	}
+
+	return &EncryptedSessionData[T]{
+		Ciphertext: ciphertext,
+		Ephemeral:  EphemeralKey{ephemeralKey.PublicKey()},
+		MAC:        calculateCompatMAC(macKey),
+	}, nil
+}
+
+// Decrypt decrypts the [EncryptedSessionData] into a *T using the recovery key
+// by reversing the process described in [Section 11.12.3.2.2 of the Spec].
+//
+// [Section 11.12.3.2.2 of the Spec]: https://spec.matrix.org/v1.9/client-server-api/#backup-algorithm-mmegolm_backupv1curve25519-aes-sha2
+func (esd *EncryptedSessionData[T]) Decrypt(backupKey *MegolmBackupKey) (*T, error) {
+	sharedSecret, err := backupKey.ECDH(esd.Ephemeral.PublicKey)
+	if err != nil {
+		return nil, err
+	}
+
+	key, macKey, iv, err := calculateEncryptionParameters(sharedSecret)
+	if err != nil {
+		return nil, err
+	}
+
+	// Verify the MAC before decrypting.
+	if !bytes.Equal(calculateCompatMAC(macKey), esd.MAC) {
+		return nil, ErrInvalidMAC
+	}
+
+	plaintext, err := aescbc.Decrypt(key, iv, esd.Ciphertext)
+	if err != nil {
+		return nil, err
+	}
+
+	var sessionData T
+	err = json.Unmarshal(plaintext, &sessionData)
+	return &sessionData, err
+}
diff --git a/vendor/maunium.net/go/mautrix/crypto/backup/ephemeralkey.go b/vendor/maunium.net/go/mautrix/crypto/backup/ephemeralkey.go
new file mode 100644
index 0000000..e481e7a
--- /dev/null
+++ b/vendor/maunium.net/go/mautrix/crypto/backup/ephemeralkey.go
@@ -0,0 +1,41 @@
+// Copyright (c) 2024 Sumner Evans
+//
+// 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 backup
+
+import (
+	"crypto/ecdh"
+	"encoding/base64"
+	"encoding/json"
+)
+
+// EphemeralKey is a wrapper around an ECDH X25519 public key that implements
+// JSON marshalling and unmarshalling.
+type EphemeralKey struct {
+	*ecdh.PublicKey
+}
+
+func (k *EphemeralKey) MarshalJSON() ([]byte, error) {
+	if k == nil || k.PublicKey == nil {
+		return json.Marshal(nil)
+	}
+	return json.Marshal(base64.RawStdEncoding.EncodeToString(k.Bytes()))
+}
+
+func (k *EphemeralKey) UnmarshalJSON(data []byte) error {
+	var keyStr string
+	err := json.Unmarshal(data, &keyStr)
+	if err != nil {
+		return err
+	}
+
+	keyBytes, err := base64.RawStdEncoding.DecodeString(keyStr)
+	if err != nil {
+		return err
+	}
+	k.PublicKey, err = ecdh.X25519().NewPublicKey(keyBytes)
+	return err
+}
diff --git a/vendor/maunium.net/go/mautrix/crypto/backup/megolmbackup.go b/vendor/maunium.net/go/mautrix/crypto/backup/megolmbackup.go
new file mode 100644
index 0000000..71b8e88
--- /dev/null
+++ b/vendor/maunium.net/go/mautrix/crypto/backup/megolmbackup.go
@@ -0,0 +1,39 @@
+// Copyright (c) 2024 Sumner Evans
+//
+// 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 backup
+
+import (
+	"maunium.net/go/mautrix/crypto/signatures"
+	"maunium.net/go/mautrix/id"
+)
+
+// MegolmAuthData is the auth_data when the key backup is created with
+// the [id.KeyBackupAlgorithmMegolmBackupV1] algorithm as defined in
+// [Section 11.12.3.2.2 of the Spec].
+//
+// [Section 11.12.3.2.2 of the Spec]: https://spec.matrix.org/v1.9/client-server-api/#backup-algorithm-mmegolm_backupv1curve25519-aes-sha2
+type MegolmAuthData struct {
+	PublicKey  id.Ed25519            `json:"public_key"`
+	Signatures signatures.Signatures `json:"signatures"`
+}
+
+type SenderClaimedKeys struct {
+	Ed25519 id.Ed25519 `json:"ed25519"`
+}
+
+// MegolmSessionData is the decrypted session_data when the key backup is created
+// with the [id.KeyBackupAlgorithmMegolmBackupV1] algorithm as defined in
+// [Section 11.12.3.2.2 of the Spec].
+//
+// [Section 11.12.3.2.2 of the Spec]: https://spec.matrix.org/v1.9/client-server-api/#backup-algorithm-mmegolm_backupv1curve25519-aes-sha2
+type MegolmSessionData struct {
+	Algorithm          id.Algorithm      `json:"algorithm"`
+	ForwardingKeyChain []string          `json:"forwarding_curve25519_key_chain"`
+	SenderClaimedKeys  SenderClaimedKeys `json:"sender_claimed_keys"`
+	SenderKey          id.SenderKey      `json:"sender_key"`
+	SessionKey         string            `json:"session_key"`
+}
diff --git a/vendor/maunium.net/go/mautrix/crypto/backup/megolmbackupkey.go b/vendor/maunium.net/go/mautrix/crypto/backup/megolmbackupkey.go
new file mode 100644
index 0000000..8f23d10
--- /dev/null
+++ b/vendor/maunium.net/go/mautrix/crypto/backup/megolmbackupkey.go
@@ -0,0 +1,34 @@
+// Copyright (c) 2024 Sumner Evans
+//
+// 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 backup
+
+import (
+	"crypto/ecdh"
+	"crypto/rand"
+)
+
+// MegolmBackupKey is a wrapper around an ECDH X25519 private key that is used
+// to decrypt a megolm key backup.
+type MegolmBackupKey struct {
+	*ecdh.PrivateKey
+}
+
+func NewMegolmBackupKey() (*MegolmBackupKey, error) {
+	key, err := ecdh.X25519().GenerateKey(rand.Reader)
+	if err != nil {
+		return nil, err
+	}
+	return &MegolmBackupKey{key}, nil
+}
+
+func MegolmBackupKeyFromBytes(bytes []byte) (*MegolmBackupKey, error) {
+	key, err := ecdh.X25519().NewPrivateKey(bytes)
+	if err != nil {
+		return nil, err
+	}
+	return &MegolmBackupKey{key}, nil
+}
diff --git a/vendor/maunium.net/go/mautrix/crypto/canonicaljson/README.md b/vendor/maunium.net/go/mautrix/crypto/canonicaljson/README.md
new file mode 100644
index 0000000..da9d71f
--- /dev/null
+++ b/vendor/maunium.net/go/mautrix/crypto/canonicaljson/README.md
@@ -0,0 +1,6 @@
+# canonicaljson
+This is a Go package to produce Matrix [Canonical JSON](https://matrix.org/docs/spec/appendices#canonical-json).
+It is essentially just [json.go](https://github.com/matrix-org/gomatrixserverlib/blob/master/json.go)
+from gomatrixserverlib without all the other files that are completely useless for non-server use cases.
+
+The original project is licensed under the Apache 2.0 license.
diff --git a/vendor/maunium.net/go/mautrix/crypto/canonicaljson/json.go b/vendor/maunium.net/go/mautrix/crypto/canonicaljson/json.go
new file mode 100644
index 0000000..fd296e6
--- /dev/null
+++ b/vendor/maunium.net/go/mautrix/crypto/canonicaljson/json.go
@@ -0,0 +1,257 @@
+/* Copyright 2016-2017 Vector Creations Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package canonicaljson
+
+import (
+	"encoding/binary"
+	"fmt"
+	"sort"
+	"unicode/utf8"
+
+	"github.com/tidwall/gjson"
+)
+
+// CanonicalJSON re-encodes the JSON in a canonical encoding. The encoding is
+// the shortest possible encoding using integer values with sorted object keys.
+// https://matrix.org/docs/spec/appendices#canonical-json
+func CanonicalJSON(input []byte) ([]byte, error) {
+	if !gjson.Valid(string(input)) {
+		return nil, fmt.Errorf("invalid json")
+	}
+
+	return CanonicalJSONAssumeValid(input), nil
+}
+
+// CanonicalJSONAssumeValid is the same as CanonicalJSON, but assumes the
+// input is valid JSON
+func CanonicalJSONAssumeValid(input []byte) []byte {
+	input = CompactJSON(input, make([]byte, 0, len(input)))
+	return SortJSON(input, make([]byte, 0, len(input)))
+}
+
+// SortJSON reencodes the JSON with the object keys sorted by lexicographically
+// by codepoint. The input must be valid JSON.
+func SortJSON(input, output []byte) []byte {
+	result := gjson.ParseBytes(input)
+
+	return sortJSONValue(result, input, output)
+}
+
+// sortJSONValue takes a gjson.Result and sorts it. inputJSON must be the
+// raw JSON bytes that gjson.Result points to.
+func sortJSONValue(input gjson.Result, inputJSON, output []byte) []byte {
+	if input.IsArray() {
+		return sortJSONArray(input, inputJSON, output)
+	}
+
+	if input.IsObject() {
+		return sortJSONObject(input, inputJSON, output)
+	}
+
+	// If its neither an object nor an array then there is no sub structure
+	// to sort, so just append the raw bytes.
+	return append(output, input.Raw...)
+}
+
+// sortJSONArray takes a gjson.Result and sorts it, assuming its an array.
+// inputJSON must be the raw JSON bytes that gjson.Result points to.
+func sortJSONArray(input gjson.Result, inputJSON, output []byte) []byte {
+	sep := byte('[')
+
+	// Iterate over each value in the array and sort it.
+	input.ForEach(func(_, value gjson.Result) bool {
+		output = append(output, sep)
+		sep = ','
+		output = sortJSONValue(value, inputJSON, output)
+		return true // keep iterating
+	})
+
+	if sep == '[' {
+		// If sep is still '[' then the array was empty and we never wrote the
+		// initial '[', so we write it now along with the closing ']'.
+		output = append(output, '[', ']')
+	} else {
+		// Otherwise we end the array by writing a single ']'
+		output = append(output, ']')
+	}
+	return output
+}
+
+// sortJSONObject takes a gjson.Result and sorts it, assuming its an object.
+// inputJSON must be the raw JSON bytes that gjson.Result points to.
+func sortJSONObject(input gjson.Result, inputJSON, output []byte) []byte {
+	type entry struct {
+		key    string // The parsed key string
+		rawKey string // The raw, unparsed key JSON string
+		value  gjson.Result
+	}
+
+	var entries []entry
+
+	// Iterate over each key/value pair and add it to a slice
+	// that we can sort
+	input.ForEach(func(key, value gjson.Result) bool {
+		entries = append(entries, entry{
+			key:    key.String(),
+			rawKey: key.Raw,
+			value:  value,
+		})
+		return true // keep iterating
+	})
+
+	// Sort the slice based on the *parsed* key
+	sort.Slice(entries, func(a, b int) bool {
+		return entries[a].key < entries[b].key
+	})
+
+	sep := byte('{')
+
+	for _, entry := range entries {
+		output = append(output, sep)
+		sep = ','
+
+		// Append the raw unparsed JSON key, *not* the parsed key
+		output = append(output, entry.rawKey...)
+		output = append(output, ':')
+		output = sortJSONValue(entry.value, inputJSON, output)
+	}
+	if sep == '{' {
+		// If sep is still '{' then the object was empty and we never wrote the
+		// initial '{', so we write it now along with the closing '}'.
+		output = append(output, '{', '}')
+	} else {
+		// Otherwise we end the object by writing a single '}'
+		output = append(output, '}')
+	}
+	return output
+}
+
+// CompactJSON makes the encoded JSON as small as possible by removing
+// whitespace and unneeded unicode escapes
+func CompactJSON(input, output []byte) []byte {
+	var i int
+	for i < len(input) {
+		c := input[i]
+		i++
+		// The valid whitespace characters are all less than or equal to SPACE 0x20.
+		// The valid non-white characters are all greater than SPACE 0x20.
+		// So we can check for whitespace by comparing against SPACE 0x20.
+		if c <= ' ' {
+			// Skip over whitespace.
+			continue
+		}
+		// Add the non-whitespace character to the output.
+		output = append(output, c)
+		if c == '"' {
+			// We are inside a string.
+			for i < len(input) {
+				c = input[i]
+				i++
+				// Check if this is an escape sequence.
+				if c == '\\' {
+					escape := input[i]
+					i++
+					if escape == 'u' {
+						// If this is a unicode escape then we need to handle it specially
+						output, i = compactUnicodeEscape(input, output, i)
+					} else if escape == '/' {
+						// JSON does not require escaping '/', but allows encoders to escape it as a special case.
+						// Since the escape isn't required we remove it.
+						output = append(output, escape)
+					} else {
+						// All other permitted escapes are single charater escapes that are already in their shortest form.
+						output = append(output, '\\', escape)
+					}
+				} else {
+					output = append(output, c)
+				}
+				if c == '"' {
+					break
+				}
+			}
+		}
+	}
+	return output
+}
+
+// compactUnicodeEscape unpacks a 4 byte unicode escape starting at index.
+// If the escape is a surrogate pair then decode the 6 byte \uXXXX escape
+// that follows. Returns the output slice and a new input index.
+func compactUnicodeEscape(input, output []byte, index int) ([]byte, int) {
+	const (
+		ESCAPES = "uuuuuuuubtnufruuuuuuuuuuuuuuuuuu"
+		HEX     = "0123456789ABCDEF"
+	)
+	// If there aren't enough bytes to decode the hex escape then return.
+	if len(input)-index < 4 {
+		return output, len(input)
+	}
+	// Decode the 4 hex digits.
+	c := readHexDigits(input[index:])
+	index += 4
+	if c < ' ' {
+		// If the character is less than SPACE 0x20 then it will need escaping.
+		escape := ESCAPES[c]
+		output = append(output, '\\', escape)
+		if escape == 'u' {
+			output = append(output, '0', '0', byte('0'+(c>>4)), HEX[c&0xF])
+		}
+	} else if c == '\\' || c == '"' {
+		// Otherwise the character only needs escaping if it is a QUOTE '"' or BACKSLASH '\\'.
+		output = append(output, '\\', byte(c))
+	} else if c < 0xD800 || c >= 0xE000 {
+		// If the character isn't a surrogate pair then encoded it directly as UTF-8.
+		var buffer [4]byte
+		n := utf8.EncodeRune(buffer[:], rune(c))
+		output = append(output, buffer[:n]...)
+	} else {
+		// Otherwise the escaped character was the first part of a UTF-16 style surrogate pair.
+		// The next 6 bytes MUST be a '\uXXXX'.
+		// If there aren't enough bytes to decode the hex escape then return.
+		if len(input)-index < 6 {
+			return output, len(input)
+		}
+		// Decode the 4 hex digits from the '\uXXXX'.
+		surrogate := readHexDigits(input[index+2:])
+		index += 6
+		// Reconstruct the UCS4 codepoint from the surrogates.
+		codepoint := 0x10000 + (((c & 0x3FF) << 10) | (surrogate & 0x3FF))
+		// Encode the charater as UTF-8.
+		var buffer [4]byte
+		n := utf8.EncodeRune(buffer[:], rune(codepoint))
+		output = append(output, buffer[:n]...)
+	}
+	return output, index
+}
+
+// Read 4 hex digits from the input slice.
+// Taken from https://github.com/NegativeMjark/indolentjson-rust/blob/8b959791fe2656a88f189c5d60d153be05fe3deb/src/readhex.rs#L21
+func readHexDigits(input []byte) uint32 {
+	hex := binary.BigEndian.Uint32(input)
+	// subtract '0'
+	hex -= 0x30303030
+	// strip the higher bits, maps 'a' => 'A'
+	hex &= 0x1F1F1F1F
+	mask := hex & 0x10101010
+	// subtract 'A' - 10 - '9' - 9 = 7 from the letters.
+	hex -= mask >> 1
+	hex += mask >> 4
+	// collect the nibbles
+	hex |= hex >> 4
+	hex &= 0xFF00FF
+	hex |= hex >> 8
+	return hex & 0xFFFF
+}
diff --git a/vendor/maunium.net/go/mautrix/crypto/ed25519/ed25519.go b/vendor/maunium.net/go/mautrix/crypto/ed25519/ed25519.go
new file mode 100644
index 0000000..327cbb3
--- /dev/null
+++ b/vendor/maunium.net/go/mautrix/crypto/ed25519/ed25519.go
@@ -0,0 +1,302 @@
+// Copyright 2024 Sumner Evans.
+//
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// Package ed25519 implements the Ed25519 signature algorithm. See
+// https://ed25519.cr.yp.to/.
+//
+// This package stores the private key in the NaCl format, which is a different
+// format than that used by the [crypto/ed25519] package in the standard
+// library.
+//
+// This picture will help with the rest of the explanation:
+// https://blog.mozilla.org/warner/files/2011/11/key-formats.png
+//
+// The private key in the [crypto/ed25519] package is a 64-byte value where the
+// first 32-bytes are the seed and the last 32-bytes are the public key.
+//
+// The private key in this package is stored in the NaCl format. That is, the
+// left 32-bytes are the private scalar A and the right 32-bytes are the right
+// half of the SHA512 result.
+//
+// The contents of this package are mostly copied from the standard library,
+// and as such the source code is licensed under the BSD license of the
+// standard library implementation.
+//
+// Other notable changes from the standard library include:
+//
+//   - The Seed function of the standard library is not implemented in this
+//     package because there is no way to recover the seed after hashing it.
+package ed25519
+
+import (
+	"crypto"
+	"crypto/ed25519"
+	cryptorand "crypto/rand"
+	"crypto/sha512"
+	"crypto/subtle"
+	"errors"
+	"io"
+	"strconv"
+
+	"filippo.io/edwards25519"
+)
+
+const (
+	// PublicKeySize is the size, in bytes, of public keys as used in this package.
+	PublicKeySize = 32
+	// PrivateKeySize is the size, in bytes, of private keys as used in this package.
+	PrivateKeySize = 64
+	// SignatureSize is the size, in bytes, of signatures generated and verified by this package.
+	SignatureSize = 64
+	// SeedSize is the size, in bytes, of private key seeds. These are the private key representations used by RFC 8032.
+	SeedSize = 32
+)
+
+// PublicKey is the type of Ed25519 public keys.
+type PublicKey []byte
+
+// Any methods implemented on PublicKey might need to also be implemented on
+// PrivateKey, as the latter embeds the former and will expose its methods.
+
+// Equal reports whether pub and x have the same value.
+func (pub PublicKey) Equal(x crypto.PublicKey) bool {
+	switch x := x.(type) {
+	case PublicKey:
+		return subtle.ConstantTimeCompare(pub, x) == 1
+	case ed25519.PublicKey:
+		return subtle.ConstantTimeCompare(pub, x) == 1
+	default:
+		return false
+	}
+}
+
+// PrivateKey is the type of Ed25519 private keys. It implements [crypto.Signer].
+type PrivateKey []byte
+
+// Public returns the [PublicKey] corresponding to priv.
+//
+// This method differs from the standard library because it calculates the
+// public key instead of returning the right half of the private key (which
+// contains the public key in the standard library).
+func (priv PrivateKey) Public() crypto.PublicKey {
+	s, err := edwards25519.NewScalar().SetBytesWithClamping(priv[:32])
+	if err != nil {
+		panic("ed25519: internal error: setting scalar failed")
+	}
+	return (&edwards25519.Point{}).ScalarBaseMult(s).Bytes()
+}
+
+// Equal reports whether priv and x have the same value.
+func (priv PrivateKey) Equal(x crypto.PrivateKey) bool {
+	// TODO do we have any need to check equality with standard library ed25519
+	// private keys?
+	xx, ok := x.(PrivateKey)
+	if !ok {
+		return false
+	}
+	return subtle.ConstantTimeCompare(priv, xx) == 1
+}
+
+// Sign signs the given message with priv. rand is ignored and can be nil.
+//
+// If opts.HashFunc() is [crypto.SHA512], the pre-hashed variant Ed25519ph is used
+// and message is expected to be a SHA-512 hash, otherwise opts.HashFunc() must
+// be [crypto.Hash](0) and the message must not be hashed, as Ed25519 performs two
+// passes over messages to be signed.
+//
+// A value of type [Options] can be used as opts, or crypto.Hash(0) or
+// crypto.SHA512 directly to select plain Ed25519 or Ed25519ph, respectively.
+func (priv PrivateKey) Sign(rand io.Reader, message []byte, opts crypto.SignerOpts) (signature []byte, err error) {
+	hash := opts.HashFunc()
+	context := ""
+	if opts, ok := opts.(*Options); ok {
+		context = opts.Context
+	}
+	switch {
+	case hash == crypto.SHA512: // Ed25519ph
+		if l := len(message); l != sha512.Size {
+			return nil, errors.New("ed25519: bad Ed25519ph message hash length: " + strconv.Itoa(l))
+		}
+		if l := len(context); l > 255 {
+			return nil, errors.New("ed25519: bad Ed25519ph context length: " + strconv.Itoa(l))
+		}
+		signature := make([]byte, SignatureSize)
+		sign(signature, priv, message, domPrefixPh, context)
+		return signature, nil
+	case hash == crypto.Hash(0) && context != "": // Ed25519ctx
+		if l := len(context); l > 255 {
+			return nil, errors.New("ed25519: bad Ed25519ctx context length: " + strconv.Itoa(l))
+		}
+		signature := make([]byte, SignatureSize)
+		sign(signature, priv, message, domPrefixCtx, context)
+		return signature, nil
+	case hash == crypto.Hash(0): // Ed25519
+		return Sign(priv, message), nil
+	default:
+		return nil, errors.New("ed25519: expected opts.HashFunc() zero (unhashed message, for standard Ed25519) or SHA-512 (for Ed25519ph)")
+	}
+}
+
+// Options can be used with [PrivateKey.Sign] or [VerifyWithOptions]
+// to select Ed25519 variants.
+type Options struct {
+	// Hash can be zero for regular Ed25519, or crypto.SHA512 for Ed25519ph.
+	Hash crypto.Hash
+
+	// Context, if not empty, selects Ed25519ctx or provides the context string
+	// for Ed25519ph. It can be at most 255 bytes in length.
+	Context string
+}
+
+// HashFunc returns o.Hash.
+func (o *Options) HashFunc() crypto.Hash { return o.Hash }
+
+// GenerateKey generates a public/private key pair using entropy from rand.
+// If rand is nil, [crypto/rand.Reader] will be used.
+//
+// The output of this function is deterministic, and equivalent to reading
+// [SeedSize] bytes from rand, and passing them to [NewKeyFromSeed].
+func GenerateKey(rand io.Reader) (PublicKey, PrivateKey, error) {
+	if rand == nil {
+		rand = cryptorand.Reader
+	}
+
+	seed := make([]byte, SeedSize)
+	if _, err := io.ReadFull(rand, seed); err != nil {
+		return nil, nil, err
+	}
+
+	privateKey := NewKeyFromSeed(seed)
+	return PublicKey(privateKey.Public().([]byte)), privateKey, nil
+}
+
+// NewKeyFromSeed calculates a private key from a seed. It will panic if
+// len(seed) is not [SeedSize]. This function is provided for interoperability
+// with RFC 8032. RFC 8032's private keys correspond to seeds in this
+// package.
+func NewKeyFromSeed(seed []byte) PrivateKey {
+	// Outline the function body so that the returned key can be stack-allocated.
+	privateKey := make([]byte, PrivateKeySize)
+	newKeyFromSeed(privateKey, seed)
+	return privateKey
+}
+
+func newKeyFromSeed(privateKey, seed []byte) {
+	if l := len(seed); l != SeedSize {
+		panic("ed25519: bad seed length: " + strconv.Itoa(l))
+	}
+
+	h := sha512.Sum512(seed)
+
+	// Apply clamping to get A in the left half, and leave the right half
+	// as-is. This gets the private key into the NaCl format.
+	h[0] &= 248
+	h[31] &= 63
+	h[31] |= 64
+	copy(privateKey, h[:])
+}
+
+// Sign signs the message with privateKey and returns a signature. It will
+// panic if len(privateKey) is not [PrivateKeySize].
+func Sign(privateKey PrivateKey, message []byte) []byte {
+	// Outline the function body so that the returned signature can be
+	// stack-allocated.
+	signature := make([]byte, SignatureSize)
+	sign(signature, privateKey, message, domPrefixPure, "")
+	return signature
+}
+
+// Domain separation prefixes used to disambiguate Ed25519/Ed25519ph/Ed25519ctx.
+// See RFC 8032, Section 2 and Section 5.1.
+const (
+	// domPrefixPure is empty for pure Ed25519.
+	domPrefixPure = ""
+	// domPrefixPh is dom2(phflag=1) for Ed25519ph. It must be followed by the
+	// uint8-length prefixed context.
+	domPrefixPh = "SigEd25519 no Ed25519 collisions\x01"
+	// domPrefixCtx is dom2(phflag=0) for Ed25519ctx. It must be followed by the
+	// uint8-length prefixed context.
+	domPrefixCtx = "SigEd25519 no Ed25519 collisions\x00"
+)
+
+func sign(signature []byte, privateKey PrivateKey, message []byte, domPrefix, context string) {
+	if l := len(privateKey); l != PrivateKeySize {
+		panic("ed25519: bad private key length: " + strconv.Itoa(l))
+	}
+	// We have to extract the public key from the private key.
+	publicKey := privateKey.Public().([]byte)
+	// The private key is already the hashed value of the seed.
+	h := privateKey
+
+	s, err := edwards25519.NewScalar().SetBytesWithClamping(h[:32])
+	if err != nil {
+		panic("ed25519: internal error: setting scalar failed")
+	}
+	prefix := h[32:]
+
+	mh := sha512.New()
+	if domPrefix != domPrefixPure {
+		mh.Write([]byte(domPrefix))
+		mh.Write([]byte{byte(len(context))})
+		mh.Write([]byte(context))
+	}
+	mh.Write(prefix)
+	mh.Write(message)
+	messageDigest := make([]byte, 0, sha512.Size)
+	messageDigest = mh.Sum(messageDigest)
+	r, err := edwards25519.NewScalar().SetUniformBytes(messageDigest)
+	if err != nil {
+		panic("ed25519: internal error: setting scalar failed")
+	}
+
+	R := (&edwards25519.Point{}).ScalarBaseMult(r)
+
+	kh := sha512.New()
+	if domPrefix != domPrefixPure {
+		kh.Write([]byte(domPrefix))
+		kh.Write([]byte{byte(len(context))})
+		kh.Write([]byte(context))
+	}
+	kh.Write(R.Bytes())
+	kh.Write(publicKey)
+	kh.Write(message)
+	hramDigest := make([]byte, 0, sha512.Size)
+	hramDigest = kh.Sum(hramDigest)
+	k, err := edwards25519.NewScalar().SetUniformBytes(hramDigest)
+	if err != nil {
+		panic("ed25519: internal error: setting scalar failed")
+	}
+
+	S := edwards25519.NewScalar().MultiplyAdd(k, s, r)
+
+	copy(signature[:32], R.Bytes())
+	copy(signature[32:], S.Bytes())
+}
+
+// Verify reports whether sig is a valid signature of message by publicKey. It
+// will panic if len(publicKey) is not [PublicKeySize].
+//
+// This is just a wrapper around [ed25519.Verify] from the standard library.
+func Verify(publicKey PublicKey, message, sig []byte) bool {
+	return ed25519.Verify(ed25519.PublicKey(publicKey), message, sig)
+}
+
+// VerifyWithOptions reports whether sig is a valid signature of message by
+// publicKey. A valid signature is indicated by returning a nil error. It will
+// panic if len(publicKey) is not [PublicKeySize].
+//
+// If opts.Hash is [crypto.SHA512], the pre-hashed variant Ed25519ph is used and
+// message is expected to be a SHA-512 hash, otherwise opts.Hash must be
+// [crypto.Hash](0) and the message must not be hashed, as Ed25519 performs two
+// passes over messages to be signed.
+//
+// This is just a wrapper around [ed25519.VerifyWithOptions] from the standard
+// library.
+func VerifyWithOptions(publicKey PublicKey, message, sig []byte, opts *Options) error {
+	return ed25519.VerifyWithOptions(ed25519.PublicKey(publicKey), message, sig, &ed25519.Options{
+		Hash:    opts.Hash,
+		Context: opts.Context,
+	})
+}
diff --git a/vendor/maunium.net/go/mautrix/crypto/goolm/crypto/curve25519.go b/vendor/maunium.net/go/mautrix/crypto/goolm/crypto/curve25519.go
new file mode 100644
index 0000000..1c182ca
--- /dev/null
+++ b/vendor/maunium.net/go/mautrix/crypto/goolm/crypto/curve25519.go
@@ -0,0 +1,186 @@
+package crypto
+
+import (
+	"bytes"
+	"crypto/rand"
+	"encoding/base64"
+	"fmt"
+	"io"
+
+	"golang.org/x/crypto/curve25519"
+
+	"maunium.net/go/mautrix/crypto/goolm/libolmpickle"
+	"maunium.net/go/mautrix/crypto/olm"
+	"maunium.net/go/mautrix/id"
+)
+
+const (
+	Curve25519KeyLength    = curve25519.ScalarSize //The length of the private key.
+	curve25519PubKeyLength = 32
+)
+
+// Curve25519GenerateKey creates a new curve25519 key pair. If reader is nil, the random data is taken from crypto/rand.
+func Curve25519GenerateKey(reader io.Reader) (Curve25519KeyPair, error) {
+	privateKeyByte := make([]byte, Curve25519KeyLength)
+	if reader == nil {
+		_, err := rand.Read(privateKeyByte)
+		if err != nil {
+			return Curve25519KeyPair{}, err
+		}
+	} else {
+		_, err := reader.Read(privateKeyByte)
+		if err != nil {
+			return Curve25519KeyPair{}, err
+		}
+	}
+
+	privateKey := Curve25519PrivateKey(privateKeyByte)
+
+	publicKey, err := privateKey.PubKey()
+	if err != nil {
+		return Curve25519KeyPair{}, err
+	}
+	return Curve25519KeyPair{
+		PrivateKey: Curve25519PrivateKey(privateKey),
+		PublicKey:  Curve25519PublicKey(publicKey),
+	}, nil
+}
+
+// Curve25519GenerateFromPrivate creates a new curve25519 key pair with the private key given.
+func Curve25519GenerateFromPrivate(private Curve25519PrivateKey) (Curve25519KeyPair, error) {
+	publicKey, err := private.PubKey()
+	if err != nil {
+		return Curve25519KeyPair{}, err
+	}
+	return Curve25519KeyPair{
+		PrivateKey: private,
+		PublicKey:  Curve25519PublicKey(publicKey),
+	}, nil
+}
+
+// Curve25519KeyPair stores both parts of a curve25519 key.
+type Curve25519KeyPair struct {
+	PrivateKey Curve25519PrivateKey `json:"private,omitempty"`
+	PublicKey  Curve25519PublicKey  `json:"public,omitempty"`
+}
+
+// B64Encoded returns a base64 encoded string of the public key.
+func (c Curve25519KeyPair) B64Encoded() id.Curve25519 {
+	return c.PublicKey.B64Encoded()
+}
+
+// SharedSecret returns the shared secret between the key pair and the given public key.
+func (c Curve25519KeyPair) SharedSecret(pubKey Curve25519PublicKey) ([]byte, error) {
+	return c.PrivateKey.SharedSecret(pubKey)
+}
+
+// PickleLibOlm encodes the key pair into target. target has to have a size of at least PickleLen() and is written to from index 0.
+// It returns the number of bytes written.
+func (c Curve25519KeyPair) PickleLibOlm(target []byte) (int, error) {
+	if len(target) < c.PickleLen() {
+		return 0, fmt.Errorf("pickle curve25519 key pair: %w", olm.ErrValueTooShort)
+	}
+	written, err := c.PublicKey.PickleLibOlm(target)
+	if err != nil {
+		return 0, fmt.Errorf("pickle curve25519 key pair: %w", err)
+	}
+	if len(c.PrivateKey) != Curve25519KeyLength {
+		written += libolmpickle.PickleBytes(make([]byte, Curve25519KeyLength), target[written:])
+	} else {
+		written += libolmpickle.PickleBytes(c.PrivateKey, target[written:])
+	}
+	return written, nil
+}
+
+// UnpickleLibOlm decodes the unencryted value and populates the key pair accordingly. It returns the number of bytes read.
+func (c *Curve25519KeyPair) UnpickleLibOlm(value []byte) (int, error) {
+	//unpickle PubKey
+	read, err := c.PublicKey.UnpickleLibOlm(value)
+	if err != nil {
+		return 0, err
+	}
+	//unpickle PrivateKey
+	privKey, readPriv, err := libolmpickle.UnpickleBytes(value[read:], Curve25519KeyLength)
+	if err != nil {
+		return read, err
+	}
+	c.PrivateKey = privKey
+	return read + readPriv, nil
+}
+
+// PickleLen returns the number of bytes the pickled key pair will have.
+func (c Curve25519KeyPair) PickleLen() int {
+	lenPublic := c.PublicKey.PickleLen()
+	var lenPrivate int
+	if len(c.PrivateKey) != Curve25519KeyLength {
+		lenPrivate = libolmpickle.PickleBytesLen(make([]byte, Curve25519KeyLength))
+	} else {
+		lenPrivate = libolmpickle.PickleBytesLen(c.PrivateKey)
+	}
+	return lenPublic + lenPrivate
+}
+
+// Curve25519PrivateKey represents the private key for curve25519 usage
+type Curve25519PrivateKey []byte
+
+// Equal compares the private key to the given private key.
+func (c Curve25519PrivateKey) Equal(x Curve25519PrivateKey) bool {
+	return bytes.Equal(c, x)
+}
+
+// PubKey returns the public key derived from the private key.
+func (c Curve25519PrivateKey) PubKey() (Curve25519PublicKey, error) {
+	publicKey, err := curve25519.X25519(c, curve25519.Basepoint)
+	if err != nil {
+		return nil, err
+	}
+	return publicKey, nil
+}
+
+// SharedSecret returns the shared secret between the private key and the given public key.
+func (c Curve25519PrivateKey) SharedSecret(pubKey Curve25519PublicKey) ([]byte, error) {
+	return curve25519.X25519(c, pubKey)
+}
+
+// Curve25519PublicKey represents the public key for curve25519 usage
+type Curve25519PublicKey []byte
+
+// Equal compares the public key to the given public key.
+func (c Curve25519PublicKey) Equal(x Curve25519PublicKey) bool {
+	return bytes.Equal(c, x)
+}
+
+// B64Encoded returns a base64 encoded string of the public key.
+func (c Curve25519PublicKey) B64Encoded() id.Curve25519 {
+	return id.Curve25519(base64.RawStdEncoding.EncodeToString(c))
+}
+
+// PickleLibOlm encodes the public key into target. target has to have a size of at least PickleLen() and is written to from index 0.
+// It returns the number of bytes written.
+func (c Curve25519PublicKey) PickleLibOlm(target []byte) (int, error) {
+	if len(target) < c.PickleLen() {
+		return 0, fmt.Errorf("pickle curve25519 public key: %w", olm.ErrValueTooShort)
+	}
+	if len(c) != curve25519PubKeyLength {
+		return libolmpickle.PickleBytes(make([]byte, curve25519PubKeyLength), target), nil
+	}
+	return libolmpickle.PickleBytes(c, target), nil
+}
+
+// UnpickleLibOlm decodes the unencryted value and populates the public key accordingly. It returns the number of bytes read.
+func (c *Curve25519PublicKey) UnpickleLibOlm(value []byte) (int, error) {
+	unpickled, readBytes, err := libolmpickle.UnpickleBytes(value, curve25519PubKeyLength)
+	if err != nil {
+		return 0, err
+	}
+	*c = unpickled
+	return readBytes, nil
+}
+
+// PickleLen returns the number of bytes the pickled public key will have.
+func (c Curve25519PublicKey) PickleLen() int {
+	if len(c) != curve25519PubKeyLength {
+		return libolmpickle.PickleBytesLen(make([]byte, curve25519PubKeyLength))
+	}
+	return libolmpickle.PickleBytesLen(c)
+}
diff --git a/vendor/maunium.net/go/mautrix/crypto/goolm/crypto/doc.go b/vendor/maunium.net/go/mautrix/crypto/goolm/crypto/doc.go
new file mode 100644
index 0000000..5bdb01d
--- /dev/null
+++ b/vendor/maunium.net/go/mautrix/crypto/goolm/crypto/doc.go
@@ -0,0 +1,2 @@
+// Package crpyto provides the nessesary encryption methods for olm/megolm
+package crypto
diff --git a/vendor/maunium.net/go/mautrix/crypto/goolm/crypto/ed25519.go b/vendor/maunium.net/go/mautrix/crypto/goolm/crypto/ed25519.go
new file mode 100644
index 0000000..57fc25f
--- /dev/null
+++ b/vendor/maunium.net/go/mautrix/crypto/goolm/crypto/ed25519.go
@@ -0,0 +1,184 @@
+package crypto
+
+import (
+	"encoding/base64"
+	"fmt"
+	"io"
+
+	"maunium.net/go/mautrix/crypto/ed25519"
+	"maunium.net/go/mautrix/crypto/goolm/libolmpickle"
+	"maunium.net/go/mautrix/crypto/olm"
+	"maunium.net/go/mautrix/id"
+)
+
+const (
+	ED25519SignatureSize = ed25519.SignatureSize //The length of a signature
+)
+
+// Ed25519GenerateKey creates a new ed25519 key pair. If reader is nil, the random data is taken from crypto/rand.
+func Ed25519GenerateKey(reader io.Reader) (Ed25519KeyPair, error) {
+	publicKey, privateKey, err := ed25519.GenerateKey(reader)
+	if err != nil {
+		return Ed25519KeyPair{}, err
+	}
+	return Ed25519KeyPair{
+		PrivateKey: Ed25519PrivateKey(privateKey),
+		PublicKey:  Ed25519PublicKey(publicKey),
+	}, nil
+}
+
+// Ed25519GenerateFromPrivate creates a new ed25519 key pair with the private key given.
+func Ed25519GenerateFromPrivate(privKey Ed25519PrivateKey) Ed25519KeyPair {
+	return Ed25519KeyPair{
+		PrivateKey: privKey,
+		PublicKey:  privKey.PubKey(),
+	}
+}
+
+// Ed25519GenerateFromSeed creates a new ed25519 key pair with a given seed.
+func Ed25519GenerateFromSeed(seed []byte) Ed25519KeyPair {
+	privKey := Ed25519PrivateKey(ed25519.NewKeyFromSeed(seed))
+	return Ed25519KeyPair{
+		PrivateKey: privKey,
+		PublicKey:  privKey.PubKey(),
+	}
+}
+
+// Ed25519KeyPair stores both parts of a ed25519 key.
+type Ed25519KeyPair struct {
+	PrivateKey Ed25519PrivateKey `json:"private,omitempty"`
+	PublicKey  Ed25519PublicKey  `json:"public,omitempty"`
+}
+
+// B64Encoded returns a base64 encoded string of the public key.
+func (c Ed25519KeyPair) B64Encoded() id.Ed25519 {
+	return id.Ed25519(base64.RawStdEncoding.EncodeToString(c.PublicKey))
+}
+
+// Sign returns the signature for the message.
+func (c Ed25519KeyPair) Sign(message []byte) []byte {
+	return c.PrivateKey.Sign(message)
+}
+
+// Verify checks the signature of the message against the givenSignature
+func (c Ed25519KeyPair) Verify(message, givenSignature []byte) bool {
+	return c.PublicKey.Verify(message, givenSignature)
+}
+
+// PickleLibOlm encodes the key pair into target. target has to have a size of at least PickleLen() and is written to from index 0.
+// It returns the number of bytes written.
+func (c Ed25519KeyPair) PickleLibOlm(target []byte) (int, error) {
+	if len(target) < c.PickleLen() {
+		return 0, fmt.Errorf("pickle ed25519 key pair: %w", olm.ErrValueTooShort)
+	}
+	written, err := c.PublicKey.PickleLibOlm(target)
+	if err != nil {
+		return 0, fmt.Errorf("pickle ed25519 key pair: %w", err)
+	}
+
+	if len(c.PrivateKey) != ed25519.PrivateKeySize {
+		written += libolmpickle.PickleBytes(make([]byte, ed25519.PrivateKeySize), target[written:])
+	} else {
+		written += libolmpickle.PickleBytes(c.PrivateKey, target[written:])
+	}
+	return written, nil
+}
+
+// UnpickleLibOlm decodes the unencryted value and populates the key pair accordingly. It returns the number of bytes read.
+func (c *Ed25519KeyPair) UnpickleLibOlm(value []byte) (int, error) {
+	//unpickle PubKey
+	read, err := c.PublicKey.UnpickleLibOlm(value)
+	if err != nil {
+		return 0, err
+	}
+	//unpickle PrivateKey
+	privKey, readPriv, err := libolmpickle.UnpickleBytes(value[read:], ed25519.PrivateKeySize)
+	if err != nil {
+		return read, err
+	}
+	c.PrivateKey = privKey
+	return read + readPriv, nil
+}
+
+// PickleLen returns the number of bytes the pickled key pair will have.
+func (c Ed25519KeyPair) PickleLen() int {
+	lenPublic := c.PublicKey.PickleLen()
+	var lenPrivate int
+	if len(c.PrivateKey) != ed25519.PrivateKeySize {
+		lenPrivate = libolmpickle.PickleBytesLen(make([]byte, ed25519.PrivateKeySize))
+	} else {
+		lenPrivate = libolmpickle.PickleBytesLen(c.PrivateKey)
+	}
+	return lenPublic + lenPrivate
+}
+
+// Curve25519PrivateKey represents the private key for ed25519 usage. This is just a wrapper.
+type Ed25519PrivateKey ed25519.PrivateKey
+
+// Equal compares the private key to the given private key.
+func (c Ed25519PrivateKey) Equal(x Ed25519PrivateKey) bool {
+	return ed25519.PrivateKey(c).Equal(ed25519.PrivateKey(x))
+}
+
+// PubKey returns the public key derived from the private key.
+func (c Ed25519PrivateKey) PubKey() Ed25519PublicKey {
+	publicKey := ed25519.PrivateKey(c).Public()
+	return Ed25519PublicKey(publicKey.([]byte))
+}
+
+// Sign returns the signature for the message.
+func (c Ed25519PrivateKey) Sign(message []byte) []byte {
+	signature, err := ed25519.PrivateKey(c).Sign(nil, message, &ed25519.Options{})
+	if err != nil {
+		panic(err)
+	}
+	return signature
+}
+
+// Ed25519PublicKey represents the public key for ed25519 usage. This is just a wrapper.
+type Ed25519PublicKey ed25519.PublicKey
+
+// Equal compares the public key to the given public key.
+func (c Ed25519PublicKey) Equal(x Ed25519PublicKey) bool {
+	return ed25519.PublicKey(c).Equal(ed25519.PublicKey(x))
+}
+
+// B64Encoded returns a base64 encoded string of the public key.
+func (c Ed25519PublicKey) B64Encoded() id.Curve25519 {
+	return id.Curve25519(base64.RawStdEncoding.EncodeToString(c))
+}
+
+// Verify checks the signature of the message against the givenSignature
+func (c Ed25519PublicKey) Verify(message, givenSignature []byte) bool {
+	return ed25519.Verify(ed25519.PublicKey(c), message, givenSignature)
+}
+
+// PickleLibOlm encodes the public key into target. target has to have a size of at least PickleLen() and is written to from index 0.
+// It returns the number of bytes written.
+func (c Ed25519PublicKey) PickleLibOlm(target []byte) (int, error) {
+	if len(target) < c.PickleLen() {
+		return 0, fmt.Errorf("pickle ed25519 public key: %w", olm.ErrValueTooShort)
+	}
+	if len(c) != ed25519.PublicKeySize {
+		return libolmpickle.PickleBytes(make([]byte, ed25519.PublicKeySize), target), nil
+	}
+	return libolmpickle.PickleBytes(c, target), nil
+}
+
+// UnpickleLibOlm decodes the unencryted value and populates the public key accordingly. It returns the number of bytes read.
+func (c *Ed25519PublicKey) UnpickleLibOlm(value []byte) (int, error) {
+	unpickled, readBytes, err := libolmpickle.UnpickleBytes(value, ed25519.PublicKeySize)
+	if err != nil {
+		return 0, err
+	}
+	*c = unpickled
+	return readBytes, nil
+}
+
+// PickleLen returns the number of bytes the pickled public key will have.
+func (c Ed25519PublicKey) PickleLen() int {
+	if len(c) != ed25519.PublicKeySize {
+		return libolmpickle.PickleBytesLen(make([]byte, ed25519.PublicKeySize))
+	}
+	return libolmpickle.PickleBytesLen(c)
+}
diff --git a/vendor/maunium.net/go/mautrix/crypto/goolm/crypto/hmac.go b/vendor/maunium.net/go/mautrix/crypto/goolm/crypto/hmac.go
new file mode 100644
index 0000000..8542f7c
--- /dev/null
+++ b/vendor/maunium.net/go/mautrix/crypto/goolm/crypto/hmac.go
@@ -0,0 +1,29 @@
+package crypto
+
+import (
+	"crypto/hmac"
+	"crypto/sha256"
+	"io"
+
+	"golang.org/x/crypto/hkdf"
+)
+
+// HMACSHA256 returns the hash message authentication code with SHA-256 of the input with the key.
+func HMACSHA256(key, input []byte) []byte {
+	hash := hmac.New(sha256.New, key)
+	hash.Write(input)
+	return hash.Sum(nil)
+}
+
+// SHA256 return the SHA-256 of the value.
+func SHA256(value []byte) []byte {
+	hash := sha256.New()
+	hash.Write(value)
+	return hash.Sum(nil)
+}
+
+// HKDFSHA256 is the key deivation function based on HMAC and returns a reader based on input. salt and info can both be nil.
+// The reader can be used to read an arbitary length of bytes which are based on all parameters.
+func HKDFSHA256(input, salt, info []byte) io.Reader {
+	return hkdf.New(sha256.New, input, salt, info)
+}
diff --git a/vendor/maunium.net/go/mautrix/crypto/goolm/crypto/one_time_key.go b/vendor/maunium.net/go/mautrix/crypto/goolm/crypto/one_time_key.go
new file mode 100644
index 0000000..aaa253d
--- /dev/null
+++ b/vendor/maunium.net/go/mautrix/crypto/goolm/crypto/one_time_key.go
@@ -0,0 +1,95 @@
+package crypto
+
+import (
+	"encoding/base64"
+	"encoding/binary"
+	"fmt"
+
+	"maunium.net/go/mautrix/crypto/goolm/libolmpickle"
+	"maunium.net/go/mautrix/crypto/olm"
+	"maunium.net/go/mautrix/id"
+)
+
+// OneTimeKey stores the information about a one time key.
+type OneTimeKey struct {
+	ID        uint32            `json:"id"`
+	Published bool              `json:"published"`
+	Key       Curve25519KeyPair `json:"key,omitempty"`
+}
+
+// Equal compares the one time key to the given one.
+func (otk OneTimeKey) Equal(s OneTimeKey) bool {
+	if otk.ID != s.ID {
+		return false
+	}
+	if otk.Published != s.Published {
+		return false
+	}
+	if !otk.Key.PrivateKey.Equal(s.Key.PrivateKey) {
+		return false
+	}
+	if !otk.Key.PublicKey.Equal(s.Key.PublicKey) {
+		return false
+	}
+	return true
+}
+
+// PickleLibOlm encodes the key pair into target. target has to have a size of at least PickleLen() and is written to from index 0.
+// It returns the number of bytes written.
+func (c OneTimeKey) PickleLibOlm(target []byte) (int, error) {
+	if len(target) < c.PickleLen() {
+		return 0, fmt.Errorf("pickle one time key: %w", olm.ErrValueTooShort)
+	}
+	written := libolmpickle.PickleUInt32(uint32(c.ID), target)
+	written += libolmpickle.PickleBool(c.Published, target[written:])
+	writtenKey, err := c.Key.PickleLibOlm(target[written:])
+	if err != nil {
+		return 0, fmt.Errorf("pickle one time key: %w", err)
+	}
+	written += writtenKey
+	return written, nil
+}
+
+// UnpickleLibOlm decodes the unencryted value and populates the OneTimeKey accordingly. It returns the number of bytes read.
+func (c *OneTimeKey) UnpickleLibOlm(value []byte) (int, error) {
+	totalReadBytes := 0
+	id, readBytes, err := libolmpickle.UnpickleUInt32(value)
+	if err != nil {
+		return 0, err
+	}
+	totalReadBytes += readBytes
+	c.ID = id
+	published, readBytes, err := libolmpickle.UnpickleBool(value[totalReadBytes:])
+	if err != nil {
+		return 0, err
+	}
+	totalReadBytes += readBytes
+	c.Published = published
+	readBytes, err = c.Key.UnpickleLibOlm(value[totalReadBytes:])
+	if err != nil {
+		return 0, err
+	}
+	totalReadBytes += readBytes
+	return totalReadBytes, nil
+}
+
+// PickleLen returns the number of bytes the pickled OneTimeKey will have.
+func (c OneTimeKey) PickleLen() int {
+	length := 0
+	length += libolmpickle.PickleUInt32Len(c.ID)
+	length += libolmpickle.PickleBoolLen(c.Published)
+	length += c.Key.PickleLen()
+	return length
+}
+
+// KeyIDEncoded returns the base64 encoded id.
+func (c OneTimeKey) KeyIDEncoded() string {
+	resSlice := make([]byte, 4)
+	binary.BigEndian.PutUint32(resSlice, c.ID)
+	return base64.RawStdEncoding.EncodeToString(resSlice)
+}
+
+// PublicKeyEncoded returns the base64 encoded public key
+func (c OneTimeKey) PublicKeyEncoded() id.Curve25519 {
+	return c.Key.PublicKey.B64Encoded()
+}
diff --git a/vendor/maunium.net/go/mautrix/crypto/goolm/libolmpickle/pickle.go b/vendor/maunium.net/go/mautrix/crypto/goolm/libolmpickle/pickle.go
new file mode 100644
index 0000000..ec125a3
--- /dev/null
+++ b/vendor/maunium.net/go/mautrix/crypto/goolm/libolmpickle/pickle.go
@@ -0,0 +1,41 @@
+package libolmpickle
+
+import (
+	"encoding/binary"
+)
+
+func PickleUInt8(value uint8, target []byte) int {
+	target[0] = value
+	return 1
+}
+func PickleUInt8Len(value uint8) int {
+	return 1
+}
+
+func PickleBool(value bool, target []byte) int {
+	if value {
+		target[0] = 0x01
+	} else {
+		target[0] = 0x00
+	}
+	return 1
+}
+func PickleBoolLen(value bool) int {
+	return 1
+}
+
+func PickleBytes(value, target []byte) int {
+	return copy(target, value)
+}
+func PickleBytesLen(value []byte) int {
+	return len(value)
+}
+
+func PickleUInt32(value uint32, target []byte) int {
+	res := make([]byte, 4) //4 bytes for int32
+	binary.BigEndian.PutUint32(res, value)
+	return copy(target, res)
+}
+func PickleUInt32Len(value uint32) int {
+	return 4
+}
diff --git a/vendor/maunium.net/go/mautrix/crypto/goolm/libolmpickle/unpickle.go b/vendor/maunium.net/go/mautrix/crypto/goolm/libolmpickle/unpickle.go
new file mode 100644
index 0000000..dbd275a
--- /dev/null
+++ b/vendor/maunium.net/go/mautrix/crypto/goolm/libolmpickle/unpickle.go
@@ -0,0 +1,53 @@
+package libolmpickle
+
+import (
+	"fmt"
+
+	"maunium.net/go/mautrix/crypto/olm"
+)
+
+func isZeroByteSlice(bytes []byte) bool {
+	b := byte(0)
+	for _, s := range bytes {
+		b |= s
+	}
+	return b == 0
+}
+
+func UnpickleUInt8(value []byte) (uint8, int, error) {
+	if len(value) < 1 {
+		return 0, 0, fmt.Errorf("unpickle uint8: %w", olm.ErrValueTooShort)
+	}
+	return value[0], 1, nil
+}
+
+func UnpickleBool(value []byte) (bool, int, error) {
+	if len(value) < 1 {
+		return false, 0, fmt.Errorf("unpickle bool: %w", olm.ErrValueTooShort)
+	}
+	return value[0] != uint8(0x00), 1, nil
+}
+
+func UnpickleBytes(value []byte, length int) ([]byte, int, error) {
+	if len(value) < length {
+		return nil, 0, fmt.Errorf("unpickle bytes: %w", olm.ErrValueTooShort)
+	}
+	resp := value[:length]
+	if isZeroByteSlice(resp) {
+		return nil, length, nil
+	}
+	return resp, length, nil
+}
+
+func UnpickleUInt32(value []byte) (uint32, int, error) {
+	if len(value) < 4 {
+		return 0, 0, fmt.Errorf("unpickle uint32: %w", olm.ErrValueTooShort)
+	}
+	var res uint32
+	count := 0
+	for i := 3; i >= 0; i-- {
+		res |= uint32(value[count]) << (8 * i)
+		count++
+	}
+	return res, 4, nil
+}
diff --git a/vendor/maunium.net/go/mautrix/crypto/olm/README.md b/vendor/maunium.net/go/mautrix/crypto/olm/README.md
new file mode 100644
index 0000000..7d8086c
--- /dev/null
+++ b/vendor/maunium.net/go/mautrix/crypto/olm/README.md
@@ -0,0 +1,4 @@
+# Go olm bindings
+Based on [Dhole/go-olm](https://github.com/Dhole/go-olm)
+
+The original project is licensed under the Apache 2.0 license.
diff --git a/vendor/maunium.net/go/mautrix/crypto/olm/account.go b/vendor/maunium.net/go/mautrix/crypto/olm/account.go
new file mode 100644
index 0000000..3271b1c
--- /dev/null
+++ b/vendor/maunium.net/go/mautrix/crypto/olm/account.go
@@ -0,0 +1,113 @@
+// Copyright (c) 2024 Sumner Evans
+//
+// 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 olm
+
+import (
+	"io"
+
+	"maunium.net/go/mautrix/id"
+)
+
+type Account interface {
+	// Pickle returns an Account as a base64 string. Encrypts the Account using the
+	// supplied key.
+	Pickle(key []byte) ([]byte, error)
+
+	// Unpickle loads an Account from a pickled base64 string. Decrypts the
+	// Account using the supplied key. Returns error on failure.
+	Unpickle(pickled, key []byte) error
+
+	// IdentityKeysJSON returns the public parts of the identity keys for the Account.
+	IdentityKeysJSON() ([]byte, error)
+
+	// IdentityKeys returns the public parts of the Ed25519 and Curve25519 identity
+	// keys for the Account.
+	IdentityKeys() (id.Ed25519, id.Curve25519, error)
+
+	// Sign returns the signature of a message using the ed25519 key for this
+	// Account.
+	Sign(message []byte) ([]byte, error)
+
+	// OneTimeKeys returns the public parts of the unpublished one time keys for
+	// the Account.
+	//
+	// The returned data is a struct with the single value "Curve25519", which is
+	// itself an object mapping key id to base64-encoded Curve25519 key.  For
+	// example:
+	//
+	//	{
+	//	    Curve25519: {
+	//	        "AAAAAA": "wo76WcYtb0Vk/pBOdmduiGJ0wIEjW4IBMbbQn7aSnTo",
+	//	        "AAAAAB": "LRvjo46L1X2vx69sS9QNFD29HWulxrmW11Up5AfAjgU"
+	//	    }
+	//	}
+	OneTimeKeys() (map[string]id.Curve25519, error)
+
+	// MarkKeysAsPublished marks the current set of one time keys as being
+	// published.
+	MarkKeysAsPublished()
+
+	// MaxNumberOfOneTimeKeys returns the largest number of one time keys this
+	// Account can store.
+	MaxNumberOfOneTimeKeys() uint
+
+	// GenOneTimeKeys generates a number of new one time keys.  If the total
+	// number of keys stored by this Account exceeds MaxNumberOfOneTimeKeys
+	// then the old keys are discarded. Reads random data from the given
+	// reader, or if nil is passed, defaults to crypto/rand.
+	GenOneTimeKeys(reader io.Reader, num uint) error
+
+	// NewOutboundSession creates a new out-bound session for sending messages to a
+	// given curve25519 identityKey and oneTimeKey.  Returns error on failure.  If the
+	// keys couldn't be decoded as base64 then the error will be "INVALID_BASE64"
+	NewOutboundSession(theirIdentityKey, theirOneTimeKey id.Curve25519) (Session, error)
+
+	// NewInboundSession creates a new in-bound session for sending/receiving
+	// messages from an incoming PRE_KEY message.  Returns error on failure.  If
+	// the base64 couldn't be decoded then the error will be "INVALID_BASE64".  If
+	// the message was for an unsupported protocol version then the error will be
+	// "BAD_MESSAGE_VERSION".  If the message couldn't be decoded then then the
+	// error will be "BAD_MESSAGE_FORMAT".  If the message refers to an unknown one
+	// time key then the error will be "BAD_MESSAGE_KEY_ID".
+	NewInboundSession(oneTimeKeyMsg string) (Session, error)
+
+	// NewInboundSessionFrom creates a new in-bound session for sending/receiving
+	// messages from an incoming PRE_KEY message.  Returns error on failure.  If
+	// the base64 couldn't be decoded then the error will be "INVALID_BASE64".  If
+	// the message was for an unsupported protocol version then the error will be
+	// "BAD_MESSAGE_VERSION".  If the message couldn't be decoded then then the
+	// error will be "BAD_MESSAGE_FORMAT".  If the message refers to an unknown one
+	// time key then the error will be "BAD_MESSAGE_KEY_ID".
+	NewInboundSessionFrom(theirIdentityKey *id.Curve25519, oneTimeKeyMsg string) (Session, error)
+
+	// RemoveOneTimeKeys removes the one time keys that the session used from the
+	// Account.  Returns error on failure.  If the Account doesn't have any
+	// matching one time keys then the error will be "BAD_MESSAGE_KEY_ID".
+	RemoveOneTimeKeys(s Session) error
+}
+
+var InitBlankAccount func() Account
+var InitNewAccount func(io.Reader) (Account, error)
+var InitNewAccountFromPickled func(pickled, key []byte) (Account, error)
+
+// NewAccount creates a new Account.
+func NewAccount(r io.Reader) (Account, error) {
+	return InitNewAccount(r)
+}
+
+func NewBlankAccount() Account {
+	return InitBlankAccount()
+}
+
+// AccountFromPickled loads an Account from a pickled base64 string.  Decrypts
+// the Account using the supplied key.  Returns error on failure.  If the key
+// doesn't match the one used to encrypt the Account then the error will be
+// "BAD_ACCOUNT_KEY".  If the base64 couldn't be decoded then the error will be
+// "INVALID_BASE64".
+func AccountFromPickled(pickled, key []byte) (Account, error) {
+	return InitNewAccountFromPickled(pickled, key)
+}
diff --git a/vendor/maunium.net/go/mautrix/crypto/olm/errors.go b/vendor/maunium.net/go/mautrix/crypto/olm/errors.go
new file mode 100644
index 0000000..c80b82e
--- /dev/null
+++ b/vendor/maunium.net/go/mautrix/crypto/olm/errors.go
@@ -0,0 +1,60 @@
+// Copyright (c) 2024 Sumner Evans
+//
+// 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 olm
+
+import "errors"
+
+// Those are the most common used errors
+var (
+	ErrBadSignature         = errors.New("bad signature")
+	ErrBadMAC               = errors.New("bad mac")
+	ErrBadMessageFormat     = errors.New("bad message format")
+	ErrBadVerification      = errors.New("bad verification")
+	ErrWrongProtocolVersion = errors.New("wrong protocol version")
+	ErrEmptyInput           = errors.New("empty input")
+	ErrNoKeyProvided        = errors.New("no key")
+	ErrBadMessageKeyID      = errors.New("bad message key id")
+	ErrRatchetNotAvailable  = errors.New("ratchet not available: attempt to decode a message whose index is earlier than our earliest known session key")
+	ErrMsgIndexTooHigh      = errors.New("message index too high")
+	ErrProtocolViolation    = errors.New("not protocol message order")
+	ErrMessageKeyNotFound   = errors.New("message key not found")
+	ErrChainTooHigh         = errors.New("chain index too high")
+	ErrBadInput             = errors.New("bad input")
+	ErrBadVersion           = errors.New("wrong version")
+	ErrWrongPickleVersion   = errors.New("wrong pickle version")
+	ErrValueTooShort        = errors.New("value too short")
+	ErrInputToSmall         = errors.New("input too small (truncated?)")
+	ErrOverflow             = errors.New("overflow")
+)
+
+// Error codes from go-olm
+var (
+	EmptyInput         = errors.New("empty input")
+	NoKeyProvided      = errors.New("no pickle key provided")
+	NotEnoughGoRandom  = errors.New("couldn't get enough randomness from crypto/rand")
+	SignatureNotFound  = errors.New("input JSON doesn't contain signature from specified device")
+	InputNotJSONString = errors.New("input doesn't look like a JSON string")
+)
+
+// Error codes from olm code
+var (
+	NotEnoughRandom        = errors.New("not enough entropy was supplied")
+	OutputBufferTooSmall   = errors.New("supplied output buffer is too small")
+	BadMessageVersion      = errors.New("the message version is unsupported")
+	BadMessageFormat       = errors.New("the message couldn't be decoded")
+	BadMessageMAC          = errors.New("the message couldn't be decrypted")
+	BadMessageKeyID        = errors.New("the message references an unknown key ID")
+	InvalidBase64          = errors.New("the input base64 was invalid")
+	BadAccountKey          = errors.New("the supplied account key is invalid")
+	UnknownPickleVersion   = errors.New("the pickled object is too new")
+	CorruptedPickle        = errors.New("the pickled object couldn't be decoded")
+	BadSessionKey          = errors.New("attempt to initialise an inbound group session from an invalid session key")
+	UnknownMessageIndex    = errors.New("attempt to decode a message whose index is earlier than our earliest known session key")
+	BadLegacyAccountPickle = errors.New("attempt to unpickle an account which uses pickle version 1")
+	BadSignature           = errors.New("received message had a bad signature")
+	InputBufferTooSmall    = errors.New("the input data was too small to be valid")
+)
diff --git a/vendor/maunium.net/go/mautrix/crypto/olm/inboundgroupsession.go b/vendor/maunium.net/go/mautrix/crypto/olm/inboundgroupsession.go
new file mode 100644
index 0000000..8839b48
--- /dev/null
+++ b/vendor/maunium.net/go/mautrix/crypto/olm/inboundgroupsession.go
@@ -0,0 +1,80 @@
+// Copyright (c) 2024 Sumner Evans
+//
+// 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 olm
+
+import "maunium.net/go/mautrix/id"
+
+type InboundGroupSession interface {
+	// Pickle returns an InboundGroupSession as a base64 string.  Encrypts the
+	// InboundGroupSession using the supplied key.
+	Pickle(key []byte) ([]byte, error)
+
+	// Unpickle loads an [InboundGroupSession] from a pickled base64 string.
+	// Decrypts the [InboundGroupSession] using the supplied key.
+	Unpickle(pickled, key []byte) error
+
+	// Decrypt decrypts a message using the [InboundGroupSession]. Returns the
+	// plain-text and message index on success.  Returns error on failure.  If
+	// the base64 couldn't be decoded then the error will be "INVALID_BASE64".
+	// If the message is for an unsupported version of the protocol then the
+	// error will be "BAD_MESSAGE_VERSION".  If the message couldn't be decoded
+	// then the error will be BAD_MESSAGE_FORMAT".  If the MAC on the message
+	// was invalid then the error will be "BAD_MESSAGE_MAC".  If we do not have
+	// a session key corresponding to the message's index (ie, it was sent
+	// before the session key was shared with us) the error will be
+	// "OLM_UNKNOWN_MESSAGE_INDEX".
+	Decrypt(message []byte) ([]byte, uint, error)
+
+	// ID returns a base64-encoded identifier for this session.
+	ID() id.SessionID
+
+	// FirstKnownIndex returns the first message index we know how to decrypt.
+	FirstKnownIndex() uint32
+
+	// IsVerified check if the session has been verified as a valid session.
+	// (A session is verified either because the original session share was
+	// signed, or because we have subsequently successfully decrypted a
+	// message.)
+	IsVerified() bool
+
+	// Export returns the base64-encoded ratchet key for this session, at the
+	// given index, in a format which can be used by
+	// InboundGroupSession.InboundGroupSessionImport().  Encrypts the
+	// InboundGroupSession using the supplied key.  Returns error on failure.
+	// if we do not have a session key corresponding to the given index (ie, it
+	// was sent before the session key was shared with us) the error will be
+	// "OLM_UNKNOWN_MESSAGE_INDEX".
+	Export(messageIndex uint32) ([]byte, error)
+}
+
+var InitInboundGroupSessionFromPickled func(pickled, key []byte) (InboundGroupSession, error)
+var InitNewInboundGroupSession func(sessionKey []byte) (InboundGroupSession, error)
+var InitInboundGroupSessionImport func(sessionKey []byte) (InboundGroupSession, error)
+var InitBlankInboundGroupSession func() InboundGroupSession
+
+// InboundGroupSessionFromPickled loads an InboundGroupSession from a pickled
+// base64 string. Decrypts the InboundGroupSession using the supplied key.
+// Returns error on failure.
+func InboundGroupSessionFromPickled(pickled, key []byte) (InboundGroupSession, error) {
+	return InitInboundGroupSessionFromPickled(pickled, key)
+}
+
+// NewInboundGroupSession creates a new inbound group session from a key
+// exported from OutboundGroupSession.Key(). Returns error on failure.
+func NewInboundGroupSession(sessionKey []byte) (InboundGroupSession, error) {
+	return InitNewInboundGroupSession(sessionKey)
+}
+
+// InboundGroupSessionImport imports an inbound group session from a previous
+// export. Returns error on failure.
+func InboundGroupSessionImport(sessionKey []byte) (InboundGroupSession, error) {
+	return InitInboundGroupSessionImport(sessionKey)
+}
+
+func NewBlankInboundGroupSession() InboundGroupSession {
+	return InitBlankInboundGroupSession()
+}
diff --git a/vendor/maunium.net/go/mautrix/crypto/olm/olm.go b/vendor/maunium.net/go/mautrix/crypto/olm/olm.go
new file mode 100644
index 0000000..fa2345e
--- /dev/null
+++ b/vendor/maunium.net/go/mautrix/crypto/olm/olm.go
@@ -0,0 +1,20 @@
+// Copyright (c) 2024 Sumner Evans
+//
+// 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 olm
+
+var GetVersion func() (major, minor, patch uint8)
+var SetPickleKeyImpl func(key []byte)
+
+// Version returns the version number of the olm library.
+func Version() (major, minor, patch uint8) {
+	return GetVersion()
+}
+
+// SetPickleKey sets the global pickle key used when encoding structs with Gob or JSON.
+func SetPickleKey(key []byte) {
+	SetPickleKeyImpl(key)
+}
diff --git a/vendor/maunium.net/go/mautrix/crypto/olm/outboundgroupsession.go b/vendor/maunium.net/go/mautrix/crypto/olm/outboundgroupsession.go
new file mode 100644
index 0000000..c5b7bcb
--- /dev/null
+++ b/vendor/maunium.net/go/mautrix/crypto/olm/outboundgroupsession.go
@@ -0,0 +1,57 @@
+// Copyright (c) 2024 Sumner Evans
+//
+// 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 olm
+
+import "maunium.net/go/mautrix/id"
+
+type OutboundGroupSession interface {
+	// Pickle returns a Session as a base64 string. Encrypts the Session using
+	// the supplied key.
+	Pickle(key []byte) ([]byte, error)
+
+	// Unpickle loads an [OutboundGroupSession] from a pickled base64 string.
+	// Decrypts the [OutboundGroupSession] using the supplied key.
+	Unpickle(pickled, key []byte) error
+
+	// Encrypt encrypts a message using the [OutboundGroupSession]. Returns the
+	// encrypted message as base64.
+	Encrypt(plaintext []byte) ([]byte, error)
+
+	// ID returns a base64-encoded identifier for this session.
+	ID() id.SessionID
+
+	// MessageIndex returns the message index for this session.  Each message
+	// is sent with an increasing index; this returns the index for the next
+	// message.
+	MessageIndex() uint
+
+	// Key returns the base64-encoded current ratchet key for this session.
+	Key() string
+}
+
+var InitNewOutboundGroupSessionFromPickled func(pickled, key []byte) (OutboundGroupSession, error)
+var InitNewOutboundGroupSession func() OutboundGroupSession
+var InitNewBlankOutboundGroupSession func() OutboundGroupSession
+
+// OutboundGroupSessionFromPickled loads an OutboundGroupSession from a pickled
+// base64 string.  Decrypts the OutboundGroupSession using the supplied key.
+// Returns error on failure.  If the key doesn't match the one used to encrypt
+// the OutboundGroupSession then the error will be "BAD_SESSION_KEY".  If the
+// base64 couldn't be decoded then the error will be "INVALID_BASE64".
+func OutboundGroupSessionFromPickled(pickled, key []byte) (OutboundGroupSession, error) {
+	return InitNewOutboundGroupSessionFromPickled(pickled, key)
+}
+
+// NewOutboundGroupSession creates a new outbound group session.
+func NewOutboundGroupSession() OutboundGroupSession {
+	return InitNewOutboundGroupSession()
+}
+
+// NewBlankOutboundGroupSession initialises an empty [OutboundGroupSession].
+func NewBlankOutboundGroupSession() OutboundGroupSession {
+	return InitNewBlankOutboundGroupSession()
+}
diff --git a/vendor/maunium.net/go/mautrix/crypto/olm/pk.go b/vendor/maunium.net/go/mautrix/crypto/olm/pk.go
new file mode 100644
index 0000000..70ee452
--- /dev/null
+++ b/vendor/maunium.net/go/mautrix/crypto/olm/pk.go
@@ -0,0 +1,57 @@
+// Copyright (c) 2024 Sumner Evans
+//
+// 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 olm
+
+import (
+	"maunium.net/go/mautrix/id"
+)
+
+// PKSigning is an interface for signing messages.
+type PKSigning interface {
+	// Seed returns the seed of the key.
+	Seed() []byte
+
+	// PublicKey returns the public key.
+	PublicKey() id.Ed25519
+
+	// Sign creates a signature for the given message using this key.
+	Sign(message []byte) ([]byte, error)
+
+	// SignJSON creates a signature for the given object after encoding it to
+	// canonical JSON.
+	SignJSON(obj any) (string, error)
+}
+
+// PKDecryption is an interface for decrypting messages.
+type PKDecryption interface {
+	// PublicKey returns the public key.
+	PublicKey() id.Curve25519
+
+	// Decrypt verifies and decrypts the given message.
+	Decrypt(ephemeralKey, mac, ciphertext []byte) ([]byte, error)
+}
+
+var InitNewPKSigning func() (PKSigning, error)
+var InitNewPKSigningFromSeed func(seed []byte) (PKSigning, error)
+var InitNewPKDecryptionFromPrivateKey func(privateKey []byte) (PKDecryption, error)
+
+// NewPKSigning creates a new [PKSigning] object, containing a key pair for
+// signing messages.
+func NewPKSigning() (PKSigning, error) {
+	return InitNewPKSigning()
+}
+
+// NewPKSigningFromSeed creates a new PKSigning object using the given seed.
+func NewPKSigningFromSeed(seed []byte) (PKSigning, error) {
+	return InitNewPKSigningFromSeed(seed)
+}
+
+// NewPKDecryptionFromPrivateKey creates a new [PKDecryption] from a
+// base64-encoded private key.
+func NewPKDecryptionFromPrivateKey(privateKey []byte) (PKDecryption, error) {
+	return InitNewPKDecryptionFromPrivateKey(privateKey)
+}
diff --git a/vendor/maunium.net/go/mautrix/crypto/olm/session.go b/vendor/maunium.net/go/mautrix/crypto/olm/session.go
new file mode 100644
index 0000000..c4b91ff
--- /dev/null
+++ b/vendor/maunium.net/go/mautrix/crypto/olm/session.go
@@ -0,0 +1,83 @@
+// Copyright (c) 2024 Sumner Evans
+//
+// 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 olm
+
+import "maunium.net/go/mautrix/id"
+
+type Session interface {
+	// Pickle returns a Session as a base64 string. Encrypts the Session using
+	// the supplied key.
+	Pickle(key []byte) ([]byte, error)
+
+	// Unpickle loads a Session from a pickled base64 string. Decrypts the
+	// Session using the supplied key.
+	Unpickle(pickled, key []byte) error
+
+	// ID returns an identifier for this Session. Will be the same for both
+	// ends of the conversation.
+	ID() id.SessionID
+
+	// HasReceivedMessage returns true if this session has received any
+	// message.
+	HasReceivedMessage() bool
+
+	// MatchesInboundSession checks if the PRE_KEY message is for this in-bound
+	// Session. This can happen if multiple messages are sent to this Account
+	// before this Account sends a message in reply. Returns true if the
+	// session matches. Returns false if the session does not match. Returns
+	// error on failure. If the base64 couldn't be decoded then the error will
+	// be "INVALID_BASE64". If the message was for an unsupported protocol
+	// version then the error will be "BAD_MESSAGE_VERSION". If the message
+	// couldn't be decoded then then the error will be "BAD_MESSAGE_FORMAT".
+	MatchesInboundSession(oneTimeKeyMsg string) (bool, error)
+
+	// MatchesInboundSessionFrom checks if the PRE_KEY message is for this
+	// in-bound Session. This can happen if multiple messages are sent to this
+	// Account before this Account sends a message in reply. Returns true if
+	// the session matches. Returns false if the session does not match.
+	// Returns error on failure. If the base64 couldn't be decoded then the
+	// error will be "INVALID_BASE64". If the message was for an unsupported
+	// protocol version then the error will be "BAD_MESSAGE_VERSION". If the
+	// message couldn't be decoded then then the error will be
+	// "BAD_MESSAGE_FORMAT".
+	MatchesInboundSessionFrom(theirIdentityKey, oneTimeKeyMsg string) (bool, error)
+
+	// EncryptMsgType returns the type of the next message that Encrypt will
+	// return. Returns MsgTypePreKey if the message will be a PRE_KEY message.
+	// Returns MsgTypeMsg if the message will be a normal message.
+	EncryptMsgType() id.OlmMsgType
+
+	// Encrypt encrypts a message using the Session. Returns the encrypted
+	// message as base64.
+	Encrypt(plaintext []byte) (id.OlmMsgType, []byte, error)
+
+	// Decrypt decrypts a message using the Session. Returns the plain-text on
+	// success. Returns error on failure. If the base64 couldn't be decoded
+	// then the error will be "INVALID_BASE64". If the message is for an
+	// unsupported version of the protocol then the error will be
+	// "BAD_MESSAGE_VERSION". If the message couldn't be decoded then the error
+	// will be BAD_MESSAGE_FORMAT". If the MAC on the message was invalid then
+	// the error will be "BAD_MESSAGE_MAC".
+	Decrypt(message string, msgType id.OlmMsgType) ([]byte, error)
+
+	// Describe generates a string describing the internal state of an olm
+	// session for debugging and logging purposes.
+	Describe() string
+}
+
+var InitSessionFromPickled func(pickled, key []byte) (Session, error)
+var InitNewBlankSession func() Session
+
+// SessionFromPickled loads a Session from a pickled base64 string.  Decrypts
+// the Session using the supplied key.  Returns error on failure.
+func SessionFromPickled(pickled, key []byte) (Session, error) {
+	return InitSessionFromPickled(pickled, key)
+}
+
+func NewBlankSession() Session {
+	return InitNewBlankSession()
+}
diff --git a/vendor/maunium.net/go/mautrix/crypto/pkcs7/pkcs7.go b/vendor/maunium.net/go/mautrix/crypto/pkcs7/pkcs7.go
new file mode 100644
index 0000000..dc28ed6
--- /dev/null
+++ b/vendor/maunium.net/go/mautrix/crypto/pkcs7/pkcs7.go
@@ -0,0 +1,30 @@
+// Copyright (c) 2024 Sumner Evans
+//
+// 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 pkcs7
+
+import "bytes"
+
+// Pad implements PKCS#7 padding as defined in [RFC2315]. It pads the data to
+// the given blockSize in the range [1, 255]. This is normally used in AES-CBC
+// encryption.
+//
+// [RFC2315]: https://www.ietf.org/rfc/rfc2315.txt
+func Pad(data []byte, blockSize int) []byte {
+	padding := blockSize - len(data)%blockSize
+	return append(data, bytes.Repeat([]byte{byte(padding)}, padding)...)
+}
+
+// Unpad implements PKCS#7 unpadding as defined in [RFC2315]. It unpads the
+// data by reading the padding amount from the last byte of the data. This is
+// normally used in AES-CBC decryption.
+//
+// [RFC2315]: https://www.ietf.org/rfc/rfc2315.txt
+func Unpad(data []byte) []byte {
+	length := len(data)
+	unpadding := int(data[length-1])
+	return data[:length-unpadding]
+}
diff --git a/vendor/maunium.net/go/mautrix/crypto/signatures/signatures.go b/vendor/maunium.net/go/mautrix/crypto/signatures/signatures.go
new file mode 100644
index 0000000..0c4422f
--- /dev/null
+++ b/vendor/maunium.net/go/mautrix/crypto/signatures/signatures.go
@@ -0,0 +1,94 @@
+// Copyright (c) 2024 Sumner Evans
+//
+// 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 signatures
+
+import (
+	"encoding/base64"
+	"encoding/json"
+	"errors"
+	"fmt"
+
+	"github.com/tidwall/gjson"
+	"github.com/tidwall/sjson"
+	"go.mau.fi/util/exgjson"
+
+	"maunium.net/go/mautrix/crypto/canonicaljson"
+	"maunium.net/go/mautrix/crypto/goolm/crypto"
+	"maunium.net/go/mautrix/id"
+)
+
+var (
+	ErrEmptyInput        = errors.New("empty input")
+	ErrSignatureNotFound = errors.New("input JSON doesn't contain signature from specified device")
+)
+
+// Signatures represents a set of signatures for some data from multiple users
+// and keys.
+type Signatures map[id.UserID]map[id.KeyID]string
+
+// NewSingleSignature creates a new [Signatures] object with a single
+// signature.
+func NewSingleSignature(userID id.UserID, algorithm id.KeyAlgorithm, keyID string, signature string) Signatures {
+	return Signatures{
+		userID: {
+			id.NewKeyID(algorithm, keyID): signature,
+		},
+	}
+}
+
+// VerifySignature verifies an Ed25519 signature.
+func VerifySignature(message []byte, key id.Ed25519, signature []byte) (ok bool, err error) {
+	if len(message) == 0 || len(key) == 0 || len(signature) == 0 {
+		return false, ErrEmptyInput
+	}
+	keyDecoded, err := base64.RawStdEncoding.DecodeString(key.String())
+	if err != nil {
+		return false, err
+	}
+	publicKey := crypto.Ed25519PublicKey(keyDecoded)
+	return publicKey.Verify(message, signature), nil
+}
+
+// VerifySignatureJSON verifies the signature in the given JSON object "obj"
+// as described in [Appendix 3] of the Matrix Spec.
+//
+// This function is a wrapper over [Utility.VerifySignatureJSON] that creates
+// and destroys the [Utility] object transparently.
+//
+// If the "obj" is not already a [json.RawMessage], it will re-encoded as JSON
+// for the verification, so "json" tags will be honored.
+//
+// [Appendix 3]: https://spec.matrix.org/v1.9/appendices/#signing-json
+func VerifySignatureJSON(obj any, userID id.UserID, keyName string, key id.Ed25519) (bool, error) {
+	var err error
+	objJSON, ok := obj.(json.RawMessage)
+	if !ok {
+		objJSON, err = json.Marshal(obj)
+		if err != nil {
+			return false, err
+		}
+	}
+
+	sig := gjson.GetBytes(objJSON, exgjson.Path("signatures", string(userID), fmt.Sprintf("ed25519:%s", keyName)))
+	if !sig.Exists() || sig.Type != gjson.String {
+		return false, ErrSignatureNotFound
+	}
+	objJSON, err = sjson.DeleteBytes(objJSON, "unsigned")
+	if err != nil {
+		return false, err
+	}
+	objJSON, err = sjson.DeleteBytes(objJSON, "signatures")
+	if err != nil {
+		return false, err
+	}
+	objJSONString := canonicaljson.CanonicalJSONAssumeValid(objJSON)
+	sigBytes, err := base64.RawStdEncoding.DecodeString(sig.Str)
+	if err != nil {
+		return false, err
+	}
+	return VerifySignature(objJSONString, key, sigBytes)
+}
diff --git a/vendor/maunium.net/go/mautrix/crypto/utils/utils.go b/vendor/maunium.net/go/mautrix/crypto/utils/utils.go
new file mode 100644
index 0000000..e2f8a19
--- /dev/null
+++ b/vendor/maunium.net/go/mautrix/crypto/utils/utils.go
@@ -0,0 +1,132 @@
+// Copyright (c) 2020 Nikos Filippakis
+//
+// 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 utils
+
+import (
+	"crypto/aes"
+	"crypto/cipher"
+	"crypto/hmac"
+	"crypto/rand"
+	"crypto/sha256"
+	"crypto/sha512"
+	"encoding/base64"
+	"strings"
+
+	"go.mau.fi/util/base58"
+	"golang.org/x/crypto/hkdf"
+	"golang.org/x/crypto/pbkdf2"
+)
+
+const (
+	// AESCTRKeyLength is the length of the AES256-CTR key used.
+	AESCTRKeyLength = 32
+	// AESCTRIVLength is the length of the AES256-CTR IV used.
+	AESCTRIVLength = 16
+	// HMACKeyLength is the length of the HMAC key used.
+	HMACKeyLength = 32
+	// SHAHashLength is the length of the SHA hash used.
+	SHAHashLength = 32
+)
+
+// XorA256CTR encrypts the input with the keystream generated by the AES256-CTR algorithm with the given arguments.
+func XorA256CTR(source []byte, key [AESCTRKeyLength]byte, iv [AESCTRIVLength]byte) []byte {
+	block, _ := aes.NewCipher(key[:])
+	cipher.NewCTR(block, iv[:]).XORKeyStream(source, source)
+	return source
+}
+
+// GenAttachmentA256CTR generates a new random AES256-CTR key and IV suitable for encrypting attachments.
+func GenAttachmentA256CTR() (key [AESCTRKeyLength]byte, iv [AESCTRIVLength]byte) {
+	_, err := rand.Read(key[:])
+	if err != nil {
+		panic(err)
+	}
+
+	// The last 8 bytes of the IV act as the counter in AES-CTR, which means they're left empty here
+	_, err = rand.Read(iv[:8])
+	if err != nil {
+		panic(err)
+	}
+	return
+}
+
+// GenA256CTRIV generates a random IV for AES256-CTR with the last bit set to zero.
+func GenA256CTRIV() (iv [AESCTRIVLength]byte) {
+	_, err := rand.Read(iv[:])
+	if err != nil {
+		panic(err)
+	}
+	iv[8] &= 0x7F
+	return
+}
+
+// DeriveKeysSHA256 derives an AES and a HMAC key from the given recovery key.
+func DeriveKeysSHA256(key []byte, name string) ([AESCTRKeyLength]byte, [HMACKeyLength]byte) {
+	var zeroBytes [32]byte
+
+	derivedHkdf := hkdf.New(sha256.New, key[:], zeroBytes[:], []byte(name))
+
+	var aesKey [AESCTRKeyLength]byte
+	var hmacKey [HMACKeyLength]byte
+	derivedHkdf.Read(aesKey[:])
+	derivedHkdf.Read(hmacKey[:])
+
+	return aesKey, hmacKey
+}
+
+// PBKDF2SHA512 generates a key of the given bit-length using the given passphrase, salt and iteration count.
+func PBKDF2SHA512(password []byte, salt []byte, iters int, keyLenBits int) []byte {
+	return pbkdf2.Key(password, salt, iters, keyLenBits/8, sha512.New)
+}
+
+// DecodeBase58RecoveryKey recovers the secret storage from a recovery key.
+func DecodeBase58RecoveryKey(recoveryKey string) []byte {
+	noSpaces := strings.ReplaceAll(recoveryKey, " ", "")
+	decoded := base58.Decode(noSpaces)
+	if len(decoded) != AESCTRKeyLength+3 { // AESCTRKeyLength bytes key and 3 bytes prefix / parity
+		return nil
+	}
+	var parity byte
+	for _, b := range decoded[:34] {
+		parity ^= b
+	}
+	if parity != decoded[34] || decoded[0] != 0x8B || decoded[1] != 1 {
+		return nil
+	}
+	return decoded[2:34]
+}
+
+// EncodeBase58RecoveryKey recovers the secret storage from a recovery key.
+func EncodeBase58RecoveryKey(key []byte) string {
+	var inputBytes [35]byte
+	copy(inputBytes[2:34], key[:])
+	inputBytes[0] = 0x8B
+	inputBytes[1] = 1
+
+	var parity byte
+	for _, b := range inputBytes[:34] {
+		parity ^= b
+	}
+	inputBytes[34] = parity
+	recoveryKey := base58.Encode(inputBytes[:])
+
+	var spacedKey string
+	for i, c := range recoveryKey {
+		if i > 0 && i%4 == 0 {
+			spacedKey += " "
+		}
+		spacedKey += string(c)
+	}
+	return spacedKey
+}
+
+// HMACSHA256B64 calculates the unpadded base64 of the SHA256 hmac of the input with the given key.
+func HMACSHA256B64(input []byte, hmacKey [HMACKeyLength]byte) string {
+	h := hmac.New(sha256.New, hmacKey[:])
+	h.Write(input)
+	return base64.RawStdEncoding.EncodeToString(h.Sum(nil))
+}
diff --git a/vendor/maunium.net/go/mautrix/error.go b/vendor/maunium.net/go/mautrix/error.go
new file mode 100644
index 0000000..a4ba985
--- /dev/null
+++ b/vendor/maunium.net/go/mautrix/error.go
@@ -0,0 +1,184 @@
+// 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 mautrix
+
+import (
+	"encoding/json"
+	"errors"
+	"fmt"
+	"net/http"
+
+	"go.mau.fi/util/exhttp"
+	"golang.org/x/exp/maps"
+)
+
+// Common error codes from https://matrix.org/docs/spec/client_server/latest#api-standards
+//
+// Can be used with errors.Is() to check the response code without casting the error:
+//
+//	err := client.Sync()
+//	if errors.Is(err, MUnknownToken) {
+//		// logout
+//	}
+var (
+	// Generic error for when the server encounters an error and it does not have a more specific error code.
+	// Note that `errors.Is` will check the error message rather than code for M_UNKNOWNs.
+	MUnknown = RespError{ErrCode: "M_UNKNOWN", StatusCode: http.StatusInternalServerError}
+	// Forbidden access, e.g. joining a room without permission, failed login.
+	MForbidden = RespError{ErrCode: "M_FORBIDDEN", StatusCode: http.StatusForbidden}
+	// Unrecognized request, e.g. the endpoint does not exist or is not implemented.
+	MUnrecognized = RespError{ErrCode: "M_UNRECOGNIZED", StatusCode: http.StatusNotFound}
+	// The access token specified was not recognised.
+	MUnknownToken = RespError{ErrCode: "M_UNKNOWN_TOKEN", StatusCode: http.StatusUnauthorized}
+	// No access token was specified for the request.
+	MMissingToken = RespError{ErrCode: "M_MISSING_TOKEN", StatusCode: http.StatusUnauthorized}
+	// Request contained valid JSON, but it was malformed in some way, e.g. missing required keys, invalid values for keys.
+	MBadJSON = RespError{ErrCode: "M_BAD_JSON", StatusCode: http.StatusBadRequest}
+	// Request did not contain valid JSON.
+	MNotJSON = RespError{ErrCode: "M_NOT_JSON", StatusCode: http.StatusBadRequest}
+	// No resource was found for this request.
+	MNotFound = RespError{ErrCode: "M_NOT_FOUND", StatusCode: http.StatusNotFound}
+	// Too many requests have been sent in a short period of time. Wait a while then try again.
+	MLimitExceeded = RespError{ErrCode: "M_LIMIT_EXCEEDED", StatusCode: http.StatusTooManyRequests}
+	// The user ID associated with the request has been deactivated.
+	// Typically for endpoints that prove authentication, such as /login.
+	MUserDeactivated = RespError{ErrCode: "M_USER_DEACTIVATED"}
+	// Encountered when trying to register a user ID which has been taken.
+	MUserInUse = RespError{ErrCode: "M_USER_IN_USE", StatusCode: http.StatusBadRequest}
+	// Encountered when trying to register a user ID which is not valid.
+	MInvalidUsername = RespError{ErrCode: "M_INVALID_USERNAME", StatusCode: http.StatusBadRequest}
+	// Sent when the room alias given to the createRoom API is already in use.
+	MRoomInUse = RespError{ErrCode: "M_ROOM_IN_USE", StatusCode: http.StatusBadRequest}
+	// The state change requested cannot be performed, such as attempting to unban a user who is not banned.
+	MBadState = RespError{ErrCode: "M_BAD_STATE"}
+	// The request or entity was too large.
+	MTooLarge = RespError{ErrCode: "M_TOO_LARGE", StatusCode: http.StatusRequestEntityTooLarge}
+	// The resource being requested is reserved by an application service, or the application service making the request has not created the resource.
+	MExclusive = RespError{ErrCode: "M_EXCLUSIVE", StatusCode: http.StatusBadRequest}
+	// The client's request to create a room used a room version that the server does not support.
+	MUnsupportedRoomVersion = RespError{ErrCode: "M_UNSUPPORTED_ROOM_VERSION"}
+	// The client attempted to join a room that has a version the server does not support.
+	// Inspect the room_version property of the error response for the room's version.
+	MIncompatibleRoomVersion = RespError{ErrCode: "M_INCOMPATIBLE_ROOM_VERSION"}
+	// The client specified a parameter that has the wrong value.
+	MInvalidParam = RespError{ErrCode: "M_INVALID_PARAM", StatusCode: http.StatusBadRequest}
+
+	MURLNotSet         = RespError{ErrCode: "M_URL_NOT_SET"}
+	MBadStatus         = RespError{ErrCode: "M_BAD_STATUS"}
+	MConnectionTimeout = RespError{ErrCode: "M_CONNECTION_TIMEOUT"}
+	MConnectionFailed  = RespError{ErrCode: "M_CONNECTION_FAILED"}
+)
+
+// HTTPError An HTTP Error response, which may wrap an underlying native Go Error.
+type HTTPError struct {
+	Request      *http.Request
+	Response     *http.Response
+	ResponseBody string
+
+	WrappedError error
+	RespError    *RespError
+	Message      string
+}
+
+func (e HTTPError) Is(err error) bool {
+	return (e.RespError != nil && errors.Is(e.RespError, err)) || (e.WrappedError != nil && errors.Is(e.WrappedError, err))
+}
+
+func (e HTTPError) IsStatus(code int) bool {
+	return e.Response != nil && e.Response.StatusCode == code
+}
+
+func (e HTTPError) Error() string {
+	if e.WrappedError != nil {
+		return fmt.Sprintf("%s: %v", e.Message, e.WrappedError)
+	} else if e.RespError != nil {
+		return fmt.Sprintf("failed to %s %s: %s (HTTP %d): %s", e.Request.Method, e.Request.URL.Path,
+			e.RespError.ErrCode, e.Response.StatusCode, e.RespError.Err)
+	} else {
+		msg := fmt.Sprintf("failed to %s %s: HTTP %d", e.Request.Method, e.Request.URL.Path, e.Response.StatusCode)
+		if len(e.ResponseBody) > 0 {
+			msg = fmt.Sprintf("%s: %s", msg, e.ResponseBody)
+		}
+		return msg
+	}
+}
+
+func (e HTTPError) Unwrap() error {
+	if e.WrappedError != nil {
+		return e.WrappedError
+	} else if e.RespError != nil {
+		return *e.RespError
+	}
+	return nil
+}
+
+// RespError is the standard JSON error response from Homeservers. It also implements the Golang "error" interface.
+// See https://spec.matrix.org/v1.2/client-server-api/#api-standards
+type RespError struct {
+	ErrCode   string
+	Err       string
+	ExtraData map[string]any
+
+	StatusCode int
+}
+
+func (e *RespError) UnmarshalJSON(data []byte) error {
+	err := json.Unmarshal(data, &e.ExtraData)
+	if err != nil {
+		return err
+	}
+	e.ErrCode, _ = e.ExtraData["errcode"].(string)
+	e.Err, _ = e.ExtraData["error"].(string)
+	return nil
+}
+
+func (e *RespError) MarshalJSON() ([]byte, error) {
+	data := maps.Clone(e.ExtraData)
+	if data == nil {
+		data = make(map[string]any)
+	}
+	data["errcode"] = e.ErrCode
+	data["error"] = e.Err
+	return json.Marshal(data)
+}
+
+func (e RespError) Write(w http.ResponseWriter) {
+	statusCode := e.StatusCode
+	if statusCode == 0 {
+		statusCode = http.StatusInternalServerError
+	}
+	exhttp.WriteJSONResponse(w, statusCode, &e)
+}
+
+func (e RespError) WithMessage(msg string, args ...any) RespError {
+	if len(args) > 0 {
+		msg = fmt.Sprintf(msg, args...)
+	}
+	e.Err = msg
+	return e
+}
+
+func (e RespError) WithStatus(status int) RespError {
+	e.StatusCode = status
+	return e
+}
+
+// Error returns the errcode and error message.
+func (e RespError) Error() string {
+	return e.ErrCode + ": " + e.Err
+}
+
+func (e RespError) Is(err error) bool {
+	e2, ok := err.(RespError)
+	if !ok {
+		return false
+	}
+	if e.ErrCode == "M_UNKNOWN" && e2.ErrCode == "M_UNKNOWN" {
+		return e.Err == e2.Err
+	}
+	return e2.ErrCode == e.ErrCode
+}
diff --git a/vendor/maunium.net/go/mautrix/event/accountdata.go b/vendor/maunium.net/go/mautrix/event/accountdata.go
new file mode 100644
index 0000000..30ca35a
--- /dev/null
+++ b/vendor/maunium.net/go/mautrix/event/accountdata.go
@@ -0,0 +1,107 @@
+// 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 event
+
+import (
+	"encoding/json"
+	"strings"
+	"time"
+
+	"maunium.net/go/mautrix/id"
+)
+
+// TagEventContent represents the content of a m.tag room account data event.
+// https://spec.matrix.org/v1.2/client-server-api/#mtag
+type TagEventContent struct {
+	Tags Tags `json:"tags"`
+}
+
+type Tags map[RoomTag]TagMetadata
+
+type RoomTag string
+
+const (
+	RoomTagFavourite    RoomTag = "m.favourite"
+	RoomTagLowPriority  RoomTag = "m.lowpriority"
+	RoomTagServerNotice RoomTag = "m.server_notice"
+)
+
+func (rt RoomTag) IsUserDefined() bool {
+	return strings.HasPrefix(string(rt), "u.")
+}
+
+func (rt RoomTag) String() string {
+	return string(rt)
+}
+
+func (rt RoomTag) Name() string {
+	if rt.IsUserDefined() {
+		return string(rt[2:])
+	}
+	switch rt {
+	case RoomTagFavourite:
+		return "Favourite"
+	case RoomTagLowPriority:
+		return "Low priority"
+	case RoomTagServerNotice:
+		return "Server notice"
+	default:
+		return ""
+	}
+}
+
+// Deprecated: type alias
+type Tag = TagMetadata
+
+type TagMetadata struct {
+	Order json.Number `json:"order,omitempty"`
+
+	MauDoublePuppetSource string `json:"fi.mau.double_puppet_source,omitempty"`
+}
+
+// DirectChatsEventContent represents the content of a m.direct account data event.
+// https://spec.matrix.org/v1.2/client-server-api/#mdirect
+type DirectChatsEventContent map[id.UserID][]id.RoomID
+
+// FullyReadEventContent represents the content of a m.fully_read account data event.
+// https://spec.matrix.org/v1.2/client-server-api/#mfully_read
+type FullyReadEventContent struct {
+	EventID id.EventID `json:"event_id"`
+}
+
+// IgnoredUserListEventContent represents the content of a m.ignored_user_list account data event.
+// https://spec.matrix.org/v1.2/client-server-api/#mignored_user_list
+type IgnoredUserListEventContent struct {
+	IgnoredUsers map[id.UserID]IgnoredUser `json:"ignored_users"`
+}
+
+type IgnoredUser struct {
+	// This is an empty object
+}
+
+type MarkedUnreadEventContent struct {
+	Unread bool `json:"unread"`
+}
+
+type BeeperMuteEventContent struct {
+	MutedUntil int64 `json:"muted_until,omitempty"`
+}
+
+func (bmec *BeeperMuteEventContent) IsMuted() bool {
+	return bmec.MutedUntil < 0 || (bmec.MutedUntil > 0 && bmec.GetMutedUntilTime().After(time.Now()))
+}
+
+var MutedForever = time.Date(9999, 12, 31, 23, 59, 59, 999999999, time.UTC)
+
+func (bmec *BeeperMuteEventContent) GetMutedUntilTime() time.Time {
+	if bmec.MutedUntil < 0 {
+		return MutedForever
+	} else if bmec.MutedUntil > 0 {
+		return time.UnixMilli(bmec.MutedUntil)
+	}
+	return time.Time{}
+}
diff --git a/vendor/maunium.net/go/mautrix/event/audio.go b/vendor/maunium.net/go/mautrix/event/audio.go
new file mode 100644
index 0000000..9eeb8ed
--- /dev/null
+++ b/vendor/maunium.net/go/mautrix/event/audio.go
@@ -0,0 +1,21 @@
+package event
+
+import (
+	"encoding/json"
+)
+
+type MSC1767Audio struct {
+	Duration int   `json:"duration"`
+	Waveform []int `json:"waveform"`
+}
+
+type serializableMSC1767Audio MSC1767Audio
+
+func (ma *MSC1767Audio) MarshalJSON() ([]byte, error) {
+	if ma.Waveform == nil {
+		ma.Waveform = []int{}
+	}
+	return json.Marshal((*serializableMSC1767Audio)(ma))
+}
+
+type MSC3245Voice struct{}
diff --git a/vendor/maunium.net/go/mautrix/event/beeper.go b/vendor/maunium.net/go/mautrix/event/beeper.go
new file mode 100644
index 0000000..911bdfe
--- /dev/null
+++ b/vendor/maunium.net/go/mautrix/event/beeper.go
@@ -0,0 +1,106 @@
+// 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 event
+
+import (
+	"maunium.net/go/mautrix/id"
+)
+
+type MessageStatusReason string
+
+const (
+	MessageStatusGenericError      MessageStatusReason = "m.event_not_handled"
+	MessageStatusUnsupported       MessageStatusReason = "com.beeper.unsupported_event"
+	MessageStatusUndecryptable     MessageStatusReason = "com.beeper.undecryptable_event"
+	MessageStatusTooOld            MessageStatusReason = "m.event_too_old"
+	MessageStatusNetworkError      MessageStatusReason = "m.foreign_network_error"
+	MessageStatusNoPermission      MessageStatusReason = "m.no_permission"
+	MessageStatusBridgeUnavailable MessageStatusReason = "m.bridge_unavailable"
+)
+
+type MessageStatus string
+
+const (
+	MessageStatusSuccess   MessageStatus = "SUCCESS"
+	MessageStatusPending   MessageStatus = "PENDING"
+	MessageStatusRetriable MessageStatus = "FAIL_RETRIABLE"
+	MessageStatusFail      MessageStatus = "FAIL_PERMANENT"
+)
+
+type BeeperMessageStatusEventContent struct {
+	Network   string              `json:"network,omitempty"`
+	RelatesTo RelatesTo           `json:"m.relates_to"`
+	Status    MessageStatus       `json:"status"`
+	Reason    MessageStatusReason `json:"reason,omitempty"`
+	// Deprecated: clients were showing this to users even though they aren't supposed to.
+	// Use InternalError for error messages that should be included in bug reports, but not shown in the UI.
+	Error         string `json:"error,omitempty"`
+	InternalError string `json:"internal_error,omitempty"`
+	Message       string `json:"message,omitempty"`
+
+	LastRetry id.EventID `json:"last_retry,omitempty"`
+
+	MutateEventKey string `json:"mutate_event_key,omitempty"`
+
+	// Indicates the set of users to whom the event was delivered. If nil, then
+	// the client should not expect delivered status at any later point. If not
+	// nil (even if empty), this field indicates which users the event was
+	// delivered to.
+	DeliveredToUsers *[]id.UserID `json:"delivered_to_users,omitempty"`
+}
+
+type BeeperRetryMetadata struct {
+	OriginalEventID id.EventID `json:"original_event_id"`
+	RetryCount      int        `json:"retry_count"`
+	// last_retry is also present, but not used by bridges
+}
+
+type BeeperRoomKeyAckEventContent struct {
+	RoomID            id.RoomID    `json:"room_id"`
+	SessionID         id.SessionID `json:"session_id"`
+	FirstMessageIndex int          `json:"first_message_index"`
+}
+
+type LinkPreview struct {
+	CanonicalURL string `json:"og:url,omitempty"`
+	Title        string `json:"og:title,omitempty"`
+	Type         string `json:"og:type,omitempty"`
+	Description  string `json:"og:description,omitempty"`
+
+	ImageURL id.ContentURIString `json:"og:image,omitempty"`
+
+	ImageSize   int    `json:"matrix:image:size,omitempty"`
+	ImageWidth  int    `json:"og:image:width,omitempty"`
+	ImageHeight int    `json:"og:image:height,omitempty"`
+	ImageType   string `json:"og:image:type,omitempty"`
+}
+
+// BeeperLinkPreview contains the data for a bundled URL preview as specified in MSC4095
+//
+// https://github.com/matrix-org/matrix-spec-proposals/pull/4095
+type BeeperLinkPreview struct {
+	LinkPreview
+
+	MatchedURL      string             `json:"matched_url,omitempty"`
+	ImageEncryption *EncryptedFileInfo `json:"beeper:image:encryption,omitempty"`
+}
+
+type BeeperProfileExtra struct {
+	RemoteID     string   `json:"com.beeper.bridge.remote_id,omitempty"`
+	Identifiers  []string `json:"com.beeper.bridge.identifiers,omitempty"`
+	Service      string   `json:"com.beeper.bridge.service,omitempty"`
+	Network      string   `json:"com.beeper.bridge.network,omitempty"`
+	IsBridgeBot  bool     `json:"com.beeper.bridge.is_bridge_bot,omitempty"`
+	IsNetworkBot bool     `json:"com.beeper.bridge.is_network_bot,omitempty"`
+}
+
+type BeeperPerMessageProfile struct {
+	ID          string               `json:"id"`
+	Displayname string               `json:"displayname,omitempty"`
+	AvatarURL   *id.ContentURIString `json:"avatar_url,omitempty"`
+	AvatarFile  *EncryptedFileInfo   `json:"avatar_file,omitempty"`
+}
diff --git a/vendor/maunium.net/go/mautrix/event/content.go b/vendor/maunium.net/go/mautrix/event/content.go
new file mode 100644
index 0000000..882d336
--- /dev/null
+++ b/vendor/maunium.net/go/mautrix/event/content.go
@@ -0,0 +1,609 @@
+// 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 event
+
+import (
+	"encoding/gob"
+	"encoding/json"
+	"errors"
+	"fmt"
+	"reflect"
+)
+
+// TypeMap is a mapping from event type to the content struct type.
+// This is used by Content.ParseRaw() for creating the correct type of struct.
+var TypeMap = map[Type]reflect.Type{
+	StateMember:            reflect.TypeOf(MemberEventContent{}),
+	StatePowerLevels:       reflect.TypeOf(PowerLevelsEventContent{}),
+	StateCanonicalAlias:    reflect.TypeOf(CanonicalAliasEventContent{}),
+	StateRoomName:          reflect.TypeOf(RoomNameEventContent{}),
+	StateRoomAvatar:        reflect.TypeOf(RoomAvatarEventContent{}),
+	StateServerACL:         reflect.TypeOf(ServerACLEventContent{}),
+	StateTopic:             reflect.TypeOf(TopicEventContent{}),
+	StateTombstone:         reflect.TypeOf(TombstoneEventContent{}),
+	StateCreate:            reflect.TypeOf(CreateEventContent{}),
+	StateJoinRules:         reflect.TypeOf(JoinRulesEventContent{}),
+	StateHistoryVisibility: reflect.TypeOf(HistoryVisibilityEventContent{}),
+	StateGuestAccess:       reflect.TypeOf(GuestAccessEventContent{}),
+	StatePinnedEvents:      reflect.TypeOf(PinnedEventsEventContent{}),
+	StatePolicyRoom:        reflect.TypeOf(ModPolicyContent{}),
+	StatePolicyServer:      reflect.TypeOf(ModPolicyContent{}),
+	StatePolicyUser:        reflect.TypeOf(ModPolicyContent{}),
+	StateEncryption:        reflect.TypeOf(EncryptionEventContent{}),
+	StateBridge:            reflect.TypeOf(BridgeEventContent{}),
+	StateHalfShotBridge:    reflect.TypeOf(BridgeEventContent{}),
+	StateSpaceParent:       reflect.TypeOf(SpaceParentEventContent{}),
+	StateSpaceChild:        reflect.TypeOf(SpaceChildEventContent{}),
+	StateInsertionMarker:   reflect.TypeOf(InsertionMarkerContent{}),
+
+	StateLegacyPolicyRoom:     reflect.TypeOf(ModPolicyContent{}),
+	StateLegacyPolicyServer:   reflect.TypeOf(ModPolicyContent{}),
+	StateLegacyPolicyUser:     reflect.TypeOf(ModPolicyContent{}),
+	StateUnstablePolicyRoom:   reflect.TypeOf(ModPolicyContent{}),
+	StateUnstablePolicyServer: reflect.TypeOf(ModPolicyContent{}),
+	StateUnstablePolicyUser:   reflect.TypeOf(ModPolicyContent{}),
+
+	StateElementFunctionalMembers: reflect.TypeOf(ElementFunctionalMembersContent{}),
+
+	EventMessage:   reflect.TypeOf(MessageEventContent{}),
+	EventSticker:   reflect.TypeOf(MessageEventContent{}),
+	EventEncrypted: reflect.TypeOf(EncryptedEventContent{}),
+	EventRedaction: reflect.TypeOf(RedactionEventContent{}),
+	EventReaction:  reflect.TypeOf(ReactionEventContent{}),
+
+	EventUnstablePollStart:    reflect.TypeOf(PollStartEventContent{}),
+	EventUnstablePollResponse: reflect.TypeOf(PollResponseEventContent{}),
+
+	BeeperMessageStatus: reflect.TypeOf(BeeperMessageStatusEventContent{}),
+
+	AccountDataRoomTags:        reflect.TypeOf(TagEventContent{}),
+	AccountDataDirectChats:     reflect.TypeOf(DirectChatsEventContent{}),
+	AccountDataFullyRead:       reflect.TypeOf(FullyReadEventContent{}),
+	AccountDataIgnoredUserList: reflect.TypeOf(IgnoredUserListEventContent{}),
+	AccountDataMarkedUnread:    reflect.TypeOf(MarkedUnreadEventContent{}),
+	AccountDataBeeperMute:      reflect.TypeOf(BeeperMuteEventContent{}),
+
+	EphemeralEventTyping:   reflect.TypeOf(TypingEventContent{}),
+	EphemeralEventReceipt:  reflect.TypeOf(ReceiptEventContent{}),
+	EphemeralEventPresence: reflect.TypeOf(PresenceEventContent{}),
+
+	InRoomVerificationReady:  reflect.TypeOf(VerificationReadyEventContent{}),
+	InRoomVerificationStart:  reflect.TypeOf(VerificationStartEventContent{}),
+	InRoomVerificationDone:   reflect.TypeOf(VerificationDoneEventContent{}),
+	InRoomVerificationCancel: reflect.TypeOf(VerificationCancelEventContent{}),
+
+	InRoomVerificationAccept: reflect.TypeOf(VerificationAcceptEventContent{}),
+	InRoomVerificationKey:    reflect.TypeOf(VerificationKeyEventContent{}),
+	InRoomVerificationMAC:    reflect.TypeOf(VerificationMACEventContent{}),
+
+	ToDeviceRoomKey:          reflect.TypeOf(RoomKeyEventContent{}),
+	ToDeviceForwardedRoomKey: reflect.TypeOf(ForwardedRoomKeyEventContent{}),
+	ToDeviceRoomKeyRequest:   reflect.TypeOf(RoomKeyRequestEventContent{}),
+	ToDeviceEncrypted:        reflect.TypeOf(EncryptedEventContent{}),
+	ToDeviceRoomKeyWithheld:  reflect.TypeOf(RoomKeyWithheldEventContent{}),
+	ToDeviceSecretRequest:    reflect.TypeOf(SecretRequestEventContent{}),
+	ToDeviceSecretSend:       reflect.TypeOf(SecretSendEventContent{}),
+	ToDeviceDummy:            reflect.TypeOf(DummyEventContent{}),
+
+	ToDeviceVerificationRequest: reflect.TypeOf(VerificationRequestEventContent{}),
+	ToDeviceVerificationReady:   reflect.TypeOf(VerificationReadyEventContent{}),
+	ToDeviceVerificationStart:   reflect.TypeOf(VerificationStartEventContent{}),
+	ToDeviceVerificationDone:    reflect.TypeOf(VerificationDoneEventContent{}),
+	ToDeviceVerificationCancel:  reflect.TypeOf(VerificationCancelEventContent{}),
+
+	ToDeviceVerificationAccept: reflect.TypeOf(VerificationAcceptEventContent{}),
+	ToDeviceVerificationKey:    reflect.TypeOf(VerificationKeyEventContent{}),
+	ToDeviceVerificationMAC:    reflect.TypeOf(VerificationMACEventContent{}),
+
+	ToDeviceOrgMatrixRoomKeyWithheld: reflect.TypeOf(RoomKeyWithheldEventContent{}),
+
+	ToDeviceBeeperRoomKeyAck: reflect.TypeOf(BeeperRoomKeyAckEventContent{}),
+
+	CallInvite:       reflect.TypeOf(CallInviteEventContent{}),
+	CallCandidates:   reflect.TypeOf(CallCandidatesEventContent{}),
+	CallAnswer:       reflect.TypeOf(CallAnswerEventContent{}),
+	CallReject:       reflect.TypeOf(CallRejectEventContent{}),
+	CallSelectAnswer: reflect.TypeOf(CallSelectAnswerEventContent{}),
+	CallNegotiate:    reflect.TypeOf(CallNegotiateEventContent{}),
+	CallHangup:       reflect.TypeOf(CallHangupEventContent{}),
+}
+
+// Content stores the content of a Matrix event.
+//
+// By default, the raw JSON bytes are stored in VeryRaw and parsed into a map[string]interface{} in the Raw field.
+// Additionally, you can call ParseRaw with the correct event type to parse the (VeryRaw) content into a nicer struct,
+// which you can then access from Parsed or via the helper functions.
+//
+// When being marshaled into JSON, the data in Parsed will be marshaled first and then recursively merged
+// with the data in Raw. Values in Raw are preferred, but nested objects will be recursed into before merging,
+// rather than overriding the whole object with the one in Raw).
+// If one of them is nil, the only the other is used. If both (Parsed and Raw) are nil, VeryRaw is used instead.
+type Content struct {
+	VeryRaw json.RawMessage
+	Raw     map[string]interface{}
+	Parsed  interface{}
+}
+
+type Relatable interface {
+	GetRelatesTo() *RelatesTo
+	OptionalGetRelatesTo() *RelatesTo
+	SetRelatesTo(rel *RelatesTo)
+}
+
+func (content *Content) UnmarshalJSON(data []byte) error {
+	content.VeryRaw = data
+	err := json.Unmarshal(data, &content.Raw)
+	return err
+}
+
+func (content *Content) MarshalJSON() ([]byte, error) {
+	if content.Raw == nil {
+		if content.Parsed == nil {
+			if content.VeryRaw == nil {
+				return []byte("{}"), nil
+			}
+			return content.VeryRaw, nil
+		}
+		return json.Marshal(content.Parsed)
+	} else if content.Parsed != nil {
+		// TODO this whole thing is incredibly hacky
+		// It needs to produce JSON, where:
+		// * content.Parsed is applied after content.Raw
+		// * MarshalJSON() is respected inside content.Parsed
+		// * Custom field inside nested objects of content.Raw are preserved,
+		//   even if content.Parsed contains the higher-level objects.
+		// * content.Raw is not modified
+
+		unparsed, err := json.Marshal(content.Parsed)
+		if err != nil {
+			return nil, err
+		}
+
+		var rawParsed map[string]interface{}
+		err = json.Unmarshal(unparsed, &rawParsed)
+		if err != nil {
+			return nil, err
+		}
+
+		output := make(map[string]interface{})
+		for key, value := range content.Raw {
+			output[key] = value
+		}
+
+		mergeMaps(output, rawParsed)
+		return json.Marshal(output)
+	}
+	return json.Marshal(content.Raw)
+}
+
+// Deprecated: use errors.Is directly
+func IsUnsupportedContentType(err error) bool {
+	return errors.Is(err, ErrUnsupportedContentType)
+}
+
+var ErrContentAlreadyParsed = errors.New("content is already parsed")
+var ErrUnsupportedContentType = errors.New("unsupported event type")
+
+func (content *Content) ParseRaw(evtType Type) error {
+	if content.Parsed != nil {
+		return ErrContentAlreadyParsed
+	}
+	structType, ok := TypeMap[evtType]
+	if !ok {
+		return fmt.Errorf("%w %s", ErrUnsupportedContentType, evtType.Repr())
+	}
+	content.Parsed = reflect.New(structType).Interface()
+	return json.Unmarshal(content.VeryRaw, &content.Parsed)
+}
+
+func mergeMaps(into, from map[string]interface{}) {
+	for key, newValue := range from {
+		existingValue, ok := into[key]
+		if !ok {
+			into[key] = newValue
+			continue
+		}
+		existingValueMap, okEx := existingValue.(map[string]interface{})
+		newValueMap, okNew := newValue.(map[string]interface{})
+		if okEx && okNew {
+			mergeMaps(existingValueMap, newValueMap)
+		} else {
+			into[key] = newValue
+		}
+	}
+}
+
+func init() {
+	gob.Register(&MemberEventContent{})
+	gob.Register(&PowerLevelsEventContent{})
+	gob.Register(&CanonicalAliasEventContent{})
+	gob.Register(&EncryptionEventContent{})
+	gob.Register(&BridgeEventContent{})
+	gob.Register(&SpaceChildEventContent{})
+	gob.Register(&SpaceParentEventContent{})
+	gob.Register(&ElementFunctionalMembersContent{})
+	gob.Register(&RoomNameEventContent{})
+	gob.Register(&RoomAvatarEventContent{})
+	gob.Register(&TopicEventContent{})
+	gob.Register(&TombstoneEventContent{})
+	gob.Register(&CreateEventContent{})
+	gob.Register(&JoinRulesEventContent{})
+	gob.Register(&HistoryVisibilityEventContent{})
+	gob.Register(&GuestAccessEventContent{})
+	gob.Register(&PinnedEventsEventContent{})
+	gob.Register(&MessageEventContent{})
+	gob.Register(&MessageEventContent{})
+	gob.Register(&EncryptedEventContent{})
+	gob.Register(&RedactionEventContent{})
+	gob.Register(&ReactionEventContent{})
+	gob.Register(&TagEventContent{})
+	gob.Register(&DirectChatsEventContent{})
+	gob.Register(&FullyReadEventContent{})
+	gob.Register(&IgnoredUserListEventContent{})
+	gob.Register(&TypingEventContent{})
+	gob.Register(&ReceiptEventContent{})
+	gob.Register(&PresenceEventContent{})
+	gob.Register(&RoomKeyEventContent{})
+	gob.Register(&ForwardedRoomKeyEventContent{})
+	gob.Register(&RoomKeyRequestEventContent{})
+	gob.Register(&RoomKeyWithheldEventContent{})
+}
+
+func CastOrDefault[T any](content *Content) *T {
+	casted, ok := content.Parsed.(*T)
+	if ok {
+		return casted
+	}
+	casted2, _ := content.Parsed.(T)
+	return &casted2
+}
+
+// Helper cast functions below
+
+func (content *Content) AsMember() *MemberEventContent {
+	casted, ok := content.Parsed.(*MemberEventContent)
+	if !ok {
+		return &MemberEventContent{}
+	}
+	return casted
+}
+func (content *Content) AsPowerLevels() *PowerLevelsEventContent {
+	casted, ok := content.Parsed.(*PowerLevelsEventContent)
+	if !ok {
+		return &PowerLevelsEventContent{}
+	}
+	return casted
+}
+func (content *Content) AsCanonicalAlias() *CanonicalAliasEventContent {
+	casted, ok := content.Parsed.(*CanonicalAliasEventContent)
+	if !ok {
+		return &CanonicalAliasEventContent{}
+	}
+	return casted
+}
+func (content *Content) AsRoomName() *RoomNameEventContent {
+	casted, ok := content.Parsed.(*RoomNameEventContent)
+	if !ok {
+		return &RoomNameEventContent{}
+	}
+	return casted
+}
+func (content *Content) AsRoomAvatar() *RoomAvatarEventContent {
+	casted, ok := content.Parsed.(*RoomAvatarEventContent)
+	if !ok {
+		return &RoomAvatarEventContent{}
+	}
+	return casted
+}
+func (content *Content) AsTopic() *TopicEventContent {
+	casted, ok := content.Parsed.(*TopicEventContent)
+	if !ok {
+		return &TopicEventContent{}
+	}
+	return casted
+}
+func (content *Content) AsTombstone() *TombstoneEventContent {
+	casted, ok := content.Parsed.(*TombstoneEventContent)
+	if !ok {
+		return &TombstoneEventContent{}
+	}
+	return casted
+}
+func (content *Content) AsCreate() *CreateEventContent {
+	casted, ok := content.Parsed.(*CreateEventContent)
+	if !ok {
+		return &CreateEventContent{}
+	}
+	return casted
+}
+func (content *Content) AsJoinRules() *JoinRulesEventContent {
+	casted, ok := content.Parsed.(*JoinRulesEventContent)
+	if !ok {
+		return &JoinRulesEventContent{}
+	}
+	return casted
+}
+func (content *Content) AsHistoryVisibility() *HistoryVisibilityEventContent {
+	casted, ok := content.Parsed.(*HistoryVisibilityEventContent)
+	if !ok {
+		return &HistoryVisibilityEventContent{}
+	}
+	return casted
+}
+func (content *Content) AsGuestAccess() *GuestAccessEventContent {
+	casted, ok := content.Parsed.(*GuestAccessEventContent)
+	if !ok {
+		return &GuestAccessEventContent{}
+	}
+	return casted
+}
+func (content *Content) AsPinnedEvents() *PinnedEventsEventContent {
+	casted, ok := content.Parsed.(*PinnedEventsEventContent)
+	if !ok {
+		return &PinnedEventsEventContent{}
+	}
+	return casted
+}
+func (content *Content) AsEncryption() *EncryptionEventContent {
+	casted, ok := content.Parsed.(*EncryptionEventContent)
+	if !ok {
+		return &EncryptionEventContent{}
+	}
+	return casted
+}
+func (content *Content) AsBridge() *BridgeEventContent {
+	casted, ok := content.Parsed.(*BridgeEventContent)
+	if !ok {
+		return &BridgeEventContent{}
+	}
+	return casted
+}
+func (content *Content) AsSpaceChild() *SpaceChildEventContent {
+	casted, ok := content.Parsed.(*SpaceChildEventContent)
+	if !ok {
+		return &SpaceChildEventContent{}
+	}
+	return casted
+}
+func (content *Content) AsSpaceParent() *SpaceParentEventContent {
+	casted, ok := content.Parsed.(*SpaceParentEventContent)
+	if !ok {
+		return &SpaceParentEventContent{}
+	}
+	return casted
+}
+func (content *Content) AsElementFunctionalMembers() *ElementFunctionalMembersContent {
+	casted, ok := content.Parsed.(*ElementFunctionalMembersContent)
+	if !ok {
+		return &ElementFunctionalMembersContent{}
+	}
+	return casted
+}
+func (content *Content) AsMessage() *MessageEventContent {
+	casted, ok := content.Parsed.(*MessageEventContent)
+	if !ok {
+		return &MessageEventContent{}
+	}
+	return casted
+}
+func (content *Content) AsEncrypted() *EncryptedEventContent {
+	casted, ok := content.Parsed.(*EncryptedEventContent)
+	if !ok {
+		return &EncryptedEventContent{}
+	}
+	return casted
+}
+func (content *Content) AsRedaction() *RedactionEventContent {
+	casted, ok := content.Parsed.(*RedactionEventContent)
+	if !ok {
+		return &RedactionEventContent{}
+	}
+	return casted
+}
+func (content *Content) AsReaction() *ReactionEventContent {
+	casted, ok := content.Parsed.(*ReactionEventContent)
+	if !ok {
+		return &ReactionEventContent{}
+	}
+	return casted
+}
+func (content *Content) AsTag() *TagEventContent {
+	casted, ok := content.Parsed.(*TagEventContent)
+	if !ok {
+		return &TagEventContent{}
+	}
+	return casted
+}
+func (content *Content) AsDirectChats() *DirectChatsEventContent {
+	casted, ok := content.Parsed.(*DirectChatsEventContent)
+	if !ok {
+		return &DirectChatsEventContent{}
+	}
+	return casted
+}
+func (content *Content) AsFullyRead() *FullyReadEventContent {
+	casted, ok := content.Parsed.(*FullyReadEventContent)
+	if !ok {
+		return &FullyReadEventContent{}
+	}
+	return casted
+}
+func (content *Content) AsIgnoredUserList() *IgnoredUserListEventContent {
+	casted, ok := content.Parsed.(*IgnoredUserListEventContent)
+	if !ok {
+		return &IgnoredUserListEventContent{}
+	}
+	return casted
+}
+func (content *Content) AsMarkedUnread() *MarkedUnreadEventContent {
+	casted, ok := content.Parsed.(*MarkedUnreadEventContent)
+	if !ok {
+		return &MarkedUnreadEventContent{}
+	}
+	return casted
+}
+func (content *Content) AsTyping() *TypingEventContent {
+	casted, ok := content.Parsed.(*TypingEventContent)
+	if !ok {
+		return &TypingEventContent{}
+	}
+	return casted
+}
+func (content *Content) AsReceipt() *ReceiptEventContent {
+	casted, ok := content.Parsed.(*ReceiptEventContent)
+	if !ok {
+		return &ReceiptEventContent{}
+	}
+	return casted
+}
+func (content *Content) AsPresence() *PresenceEventContent {
+	casted, ok := content.Parsed.(*PresenceEventContent)
+	if !ok {
+		return &PresenceEventContent{}
+	}
+	return casted
+}
+func (content *Content) AsRoomKey() *RoomKeyEventContent {
+	casted, ok := content.Parsed.(*RoomKeyEventContent)
+	if !ok {
+		return &RoomKeyEventContent{}
+	}
+	return casted
+}
+func (content *Content) AsForwardedRoomKey() *ForwardedRoomKeyEventContent {
+	casted, ok := content.Parsed.(*ForwardedRoomKeyEventContent)
+	if !ok {
+		return &ForwardedRoomKeyEventContent{}
+	}
+	return casted
+}
+func (content *Content) AsRoomKeyRequest() *RoomKeyRequestEventContent {
+	casted, ok := content.Parsed.(*RoomKeyRequestEventContent)
+	if !ok {
+		return &RoomKeyRequestEventContent{}
+	}
+	return casted
+}
+func (content *Content) AsRoomKeyWithheld() *RoomKeyWithheldEventContent {
+	casted, ok := content.Parsed.(*RoomKeyWithheldEventContent)
+	if !ok {
+		return &RoomKeyWithheldEventContent{}
+	}
+	return casted
+}
+func (content *Content) AsCallInvite() *CallInviteEventContent {
+	casted, ok := content.Parsed.(*CallInviteEventContent)
+	if !ok {
+		return &CallInviteEventContent{}
+	}
+	return casted
+}
+func (content *Content) AsCallCandidates() *CallCandidatesEventContent {
+	casted, ok := content.Parsed.(*CallCandidatesEventContent)
+	if !ok {
+		return &CallCandidatesEventContent{}
+	}
+	return casted
+}
+func (content *Content) AsCallAnswer() *CallAnswerEventContent {
+	casted, ok := content.Parsed.(*CallAnswerEventContent)
+	if !ok {
+		return &CallAnswerEventContent{}
+	}
+	return casted
+}
+func (content *Content) AsCallReject() *CallRejectEventContent {
+	casted, ok := content.Parsed.(*CallRejectEventContent)
+	if !ok {
+		return &CallRejectEventContent{}
+	}
+	return casted
+}
+func (content *Content) AsCallSelectAnswer() *CallSelectAnswerEventContent {
+	casted, ok := content.Parsed.(*CallSelectAnswerEventContent)
+	if !ok {
+		return &CallSelectAnswerEventContent{}
+	}
+	return casted
+}
+func (content *Content) AsCallNegotiate() *CallNegotiateEventContent {
+	casted, ok := content.Parsed.(*CallNegotiateEventContent)
+	if !ok {
+		return &CallNegotiateEventContent{}
+	}
+	return casted
+}
+func (content *Content) AsCallHangup() *CallHangupEventContent {
+	casted, ok := content.Parsed.(*CallHangupEventContent)
+	if !ok {
+		return &CallHangupEventContent{}
+	}
+	return casted
+}
+func (content *Content) AsModPolicy() *ModPolicyContent {
+	casted, ok := content.Parsed.(*ModPolicyContent)
+	if !ok {
+		return &ModPolicyContent{}
+	}
+	return casted
+}
+func (content *Content) AsVerificationRequest() *VerificationRequestEventContent {
+	casted, ok := content.Parsed.(*VerificationRequestEventContent)
+	if !ok {
+		return &VerificationRequestEventContent{}
+	}
+	return casted
+}
+func (content *Content) AsVerificationReady() *VerificationReadyEventContent {
+	casted, ok := content.Parsed.(*VerificationReadyEventContent)
+	if !ok {
+		return &VerificationReadyEventContent{}
+	}
+	return casted
+}
+func (content *Content) AsVerificationStart() *VerificationStartEventContent {
+	casted, ok := content.Parsed.(*VerificationStartEventContent)
+	if !ok {
+		return &VerificationStartEventContent{}
+	}
+	return casted
+}
+func (content *Content) AsVerificationDone() *VerificationDoneEventContent {
+	casted, ok := content.Parsed.(*VerificationDoneEventContent)
+	if !ok {
+		return &VerificationDoneEventContent{}
+	}
+	return casted
+}
+func (content *Content) AsVerificationCancel() *VerificationCancelEventContent {
+	casted, ok := content.Parsed.(*VerificationCancelEventContent)
+	if !ok {
+		return &VerificationCancelEventContent{}
+	}
+	return casted
+}
+func (content *Content) AsVerificationAccept() *VerificationAcceptEventContent {
+	casted, ok := content.Parsed.(*VerificationAcceptEventContent)
+	if !ok {
+		return &VerificationAcceptEventContent{}
+	}
+	return casted
+}
+func (content *Content) AsVerificationKey() *VerificationKeyEventContent {
+	casted, ok := content.Parsed.(*VerificationKeyEventContent)
+	if !ok {
+		return &VerificationKeyEventContent{}
+	}
+	return casted
+}
+func (content *Content) AsVerificationMAC() *VerificationMACEventContent {
+	casted, ok := content.Parsed.(*VerificationMACEventContent)
+	if !ok {
+		return &VerificationMACEventContent{}
+	}
+	return casted
+}
diff --git a/vendor/maunium.net/go/mautrix/event/encryption.go b/vendor/maunium.net/go/mautrix/event/encryption.go
new file mode 100644
index 0000000..cf9c281
--- /dev/null
+++ b/vendor/maunium.net/go/mautrix/event/encryption.go
@@ -0,0 +1,202 @@
+// 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 event
+
+import (
+	"encoding/json"
+	"fmt"
+
+	"maunium.net/go/mautrix/id"
+)
+
+// EncryptionEventContent represents the content of a m.room.encryption state event.
+// https://spec.matrix.org/v1.2/client-server-api/#mroomencryption
+type EncryptionEventContent struct {
+	// The encryption algorithm to be used to encrypt messages sent in this room. Must be 'm.megolm.v1.aes-sha2'.
+	Algorithm id.Algorithm `json:"algorithm"`
+	// How long the session should be used before changing it. 604800000 (a week) is the recommended default.
+	RotationPeriodMillis int64 `json:"rotation_period_ms,omitempty"`
+	// How many messages should be sent before changing the session. 100 is the recommended default.
+	RotationPeriodMessages int `json:"rotation_period_msgs,omitempty"`
+}
+
+// EncryptedEventContent represents the content of a m.room.encrypted message event.
+// https://spec.matrix.org/v1.2/client-server-api/#mroomencrypted
+//
+// Note that sender_key and device_id are deprecated in Megolm events as of https://github.com/matrix-org/matrix-spec-proposals/pull/3700
+type EncryptedEventContent struct {
+	Algorithm id.Algorithm `json:"algorithm"`
+	SenderKey id.SenderKey `json:"sender_key,omitempty"`
+	// Deprecated: Matrix v1.3
+	DeviceID id.DeviceID `json:"device_id,omitempty"`
+	// Only present for Megolm events
+	SessionID id.SessionID `json:"session_id,omitempty"`
+
+	Ciphertext json.RawMessage `json:"ciphertext"`
+
+	MegolmCiphertext []byte         `json:"-"`
+	OlmCiphertext    OlmCiphertexts `json:"-"`
+
+	RelatesTo *RelatesTo `json:"m.relates_to,omitempty"`
+	Mentions  *Mentions  `json:"m.mentions,omitempty"`
+}
+
+type OlmCiphertexts map[id.Curve25519]struct {
+	Body string        `json:"body"`
+	Type id.OlmMsgType `json:"type"`
+}
+
+type serializableEncryptedEventContent EncryptedEventContent
+
+func (content *EncryptedEventContent) UnmarshalJSON(data []byte) error {
+	err := json.Unmarshal(data, (*serializableEncryptedEventContent)(content))
+	if err != nil {
+		return err
+	}
+	switch content.Algorithm {
+	case id.AlgorithmOlmV1:
+		content.OlmCiphertext = make(OlmCiphertexts)
+		return json.Unmarshal(content.Ciphertext, &content.OlmCiphertext)
+	case id.AlgorithmMegolmV1:
+		if len(content.Ciphertext) == 0 || content.Ciphertext[0] != '"' || content.Ciphertext[len(content.Ciphertext)-1] != '"' {
+			return id.InputNotJSONString
+		}
+		content.MegolmCiphertext = content.Ciphertext[1 : len(content.Ciphertext)-1]
+	}
+	return nil
+}
+
+func (content *EncryptedEventContent) MarshalJSON() ([]byte, error) {
+	var err error
+	switch content.Algorithm {
+	case id.AlgorithmOlmV1:
+		content.Ciphertext, err = json.Marshal(content.OlmCiphertext)
+	case id.AlgorithmMegolmV1:
+		content.Ciphertext = make([]byte, len(content.MegolmCiphertext)+2)
+		content.Ciphertext[0] = '"'
+		content.Ciphertext[len(content.Ciphertext)-1] = '"'
+		copy(content.Ciphertext[1:len(content.Ciphertext)-1], content.MegolmCiphertext)
+	}
+	if err != nil {
+		return nil, err
+	}
+	return json.Marshal((*serializableEncryptedEventContent)(content))
+}
+
+// RoomKeyEventContent represents the content of a m.room_key to_device event.
+// https://spec.matrix.org/v1.2/client-server-api/#mroom_key
+type RoomKeyEventContent struct {
+	Algorithm  id.Algorithm `json:"algorithm"`
+	RoomID     id.RoomID    `json:"room_id"`
+	SessionID  id.SessionID `json:"session_id"`
+	SessionKey string       `json:"session_key"`
+
+	MaxAge      int64 `json:"com.beeper.max_age_ms"`
+	MaxMessages int   `json:"com.beeper.max_messages"`
+	IsScheduled bool  `json:"com.beeper.is_scheduled"`
+}
+
+// ForwardedRoomKeyEventContent represents the content of a m.forwarded_room_key to_device event.
+// https://spec.matrix.org/v1.2/client-server-api/#mforwarded_room_key
+type ForwardedRoomKeyEventContent struct {
+	RoomKeyEventContent
+	SenderKey          id.SenderKey `json:"sender_key"`
+	SenderClaimedKey   id.Ed25519   `json:"sender_claimed_ed25519_key"`
+	ForwardingKeyChain []string     `json:"forwarding_curve25519_key_chain"`
+
+	MaxAge      int64 `json:"com.beeper.max_age_ms"`
+	MaxMessages int   `json:"com.beeper.max_messages"`
+	IsScheduled bool  `json:"com.beeper.is_scheduled"`
+}
+
+type KeyRequestAction string
+
+const (
+	KeyRequestActionRequest = "request"
+	KeyRequestActionCancel  = "request_cancellation"
+)
+
+// RoomKeyRequestEventContent represents the content of a m.room_key_request to_device event.
+// https://spec.matrix.org/v1.2/client-server-api/#mroom_key_request
+type RoomKeyRequestEventContent struct {
+	Body               RequestedKeyInfo `json:"body"`
+	Action             KeyRequestAction `json:"action"`
+	RequestingDeviceID id.DeviceID      `json:"requesting_device_id"`
+	RequestID          string           `json:"request_id"`
+}
+
+type RequestedKeyInfo struct {
+	Algorithm id.Algorithm `json:"algorithm"`
+	RoomID    id.RoomID    `json:"room_id"`
+	SenderKey id.SenderKey `json:"sender_key"`
+	SessionID id.SessionID `json:"session_id"`
+}
+
+type RoomKeyWithheldCode string
+
+const (
+	RoomKeyWithheldBlacklisted  RoomKeyWithheldCode = "m.blacklisted"
+	RoomKeyWithheldUnverified   RoomKeyWithheldCode = "m.unverified"
+	RoomKeyWithheldUnauthorized RoomKeyWithheldCode = "m.unauthorised"
+	RoomKeyWithheldUnavailable  RoomKeyWithheldCode = "m.unavailable"
+	RoomKeyWithheldNoOlmSession RoomKeyWithheldCode = "m.no_olm"
+
+	RoomKeyWithheldBeeperRedacted RoomKeyWithheldCode = "com.beeper.redacted"
+)
+
+type RoomKeyWithheldEventContent struct {
+	RoomID    id.RoomID           `json:"room_id,omitempty"`
+	Algorithm id.Algorithm        `json:"algorithm"`
+	SessionID id.SessionID        `json:"session_id,omitempty"`
+	SenderKey id.SenderKey        `json:"sender_key"`
+	Code      RoomKeyWithheldCode `json:"code"`
+	Reason    string              `json:"reason,omitempty"`
+}
+
+const groupSessionWithheldMsg = "group session has been withheld: %s"
+
+func (withheld *RoomKeyWithheldEventContent) Error() string {
+	switch withheld.Code {
+	case RoomKeyWithheldBlacklisted, RoomKeyWithheldUnverified, RoomKeyWithheldUnauthorized, RoomKeyWithheldUnavailable, RoomKeyWithheldNoOlmSession:
+		return fmt.Sprintf(groupSessionWithheldMsg, withheld.Code)
+	default:
+		return fmt.Sprintf(groupSessionWithheldMsg+" (%s)", withheld.Code, withheld.Reason)
+	}
+}
+
+func (withheld *RoomKeyWithheldEventContent) Is(other error) bool {
+	otherWithheld, ok := other.(*RoomKeyWithheldEventContent)
+	if !ok {
+		return false
+	}
+	return withheld.Code == "" || otherWithheld.Code == "" || withheld.Code == otherWithheld.Code
+}
+
+type SecretRequestAction string
+
+func (a SecretRequestAction) String() string {
+	return string(a)
+}
+
+const (
+	SecretRequestRequest      = "request"
+	SecretRequestCancellation = "request_cancellation"
+)
+
+type SecretRequestEventContent struct {
+	Name               id.Secret           `json:"name,omitempty"`
+	Action             SecretRequestAction `json:"action"`
+	RequestingDeviceID id.DeviceID         `json:"requesting_device_id"`
+	RequestID          string              `json:"request_id"`
+}
+
+type SecretSendEventContent struct {
+	RequestID string `json:"request_id"`
+	Secret    string `json:"secret"`
+}
+
+type DummyEventContent struct{}
diff --git a/vendor/maunium.net/go/mautrix/event/ephemeral.go b/vendor/maunium.net/go/mautrix/event/ephemeral.go
new file mode 100644
index 0000000..f447404
--- /dev/null
+++ b/vendor/maunium.net/go/mautrix/event/ephemeral.go
@@ -0,0 +1,140 @@
+// 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 event
+
+import (
+	"encoding/json"
+	"time"
+
+	"maunium.net/go/mautrix/id"
+)
+
+// TypingEventContent represents the content of a m.typing ephemeral event.
+// https://spec.matrix.org/v1.2/client-server-api/#mtyping
+type TypingEventContent struct {
+	UserIDs []id.UserID `json:"user_ids"`
+}
+
+// ReceiptEventContent represents the content of a m.receipt ephemeral event.
+// https://spec.matrix.org/v1.2/client-server-api/#mreceipt
+type ReceiptEventContent map[id.EventID]Receipts
+
+func (rec ReceiptEventContent) Set(evtID id.EventID, receiptType ReceiptType, userID id.UserID, receipt ReadReceipt) {
+	rec.GetOrCreate(evtID).GetOrCreate(receiptType).Set(userID, receipt)
+}
+
+func (rec ReceiptEventContent) GetOrCreate(evt id.EventID) Receipts {
+	receipts, ok := rec[evt]
+	if !ok {
+		receipts = make(Receipts)
+		rec[evt] = receipts
+	}
+	return receipts
+}
+
+type ReceiptType string
+
+const (
+	ReceiptTypeRead        ReceiptType = "m.read"
+	ReceiptTypeReadPrivate ReceiptType = "m.read.private"
+)
+
+type Receipts map[ReceiptType]UserReceipts
+
+func (rps Receipts) GetOrCreate(receiptType ReceiptType) UserReceipts {
+	read, ok := rps[receiptType]
+	if !ok {
+		read = make(UserReceipts)
+		rps[receiptType] = read
+	}
+	return read
+}
+
+type UserReceipts map[id.UserID]ReadReceipt
+
+func (ur UserReceipts) Set(userID id.UserID, receipt ReadReceipt) {
+	ur[userID] = receipt
+}
+
+type ThreadID = id.EventID
+
+const ReadReceiptThreadMain ThreadID = "main"
+
+type ReadReceipt struct {
+	Timestamp time.Time
+
+	// Thread ID for thread-specific read receipts from MSC3771
+	ThreadID ThreadID
+
+	// Extra contains any unknown fields in the read receipt event.
+	// Most servers don't allow clients to set them, so this will be empty in most cases.
+	Extra map[string]interface{}
+}
+
+func (rr *ReadReceipt) UnmarshalJSON(data []byte) error {
+	// Hacky compatibility hack against crappy clients that send double-encoded read receipts.
+	// TODO is this actually needed? clients can't currently set custom content in receipts 🤔
+	if data[0] == '"' && data[len(data)-1] == '"' {
+		var strData string
+		err := json.Unmarshal(data, &strData)
+		if err != nil {
+			return err
+		}
+		data = []byte(strData)
+	}
+
+	var parsed map[string]interface{}
+	err := json.Unmarshal(data, &parsed)
+	if err != nil {
+		return err
+	}
+	threadID, _ := parsed["thread_id"].(string)
+	ts, tsOK := parsed["ts"].(float64)
+	delete(parsed, "thread_id")
+	delete(parsed, "ts")
+	*rr = ReadReceipt{
+		ThreadID: ThreadID(threadID),
+		Extra:    parsed,
+	}
+	if tsOK {
+		rr.Timestamp = time.UnixMilli(int64(ts))
+	}
+	return nil
+}
+
+func (rr ReadReceipt) MarshalJSON() ([]byte, error) {
+	data := rr.Extra
+	if data == nil {
+		data = make(map[string]interface{})
+	}
+	if rr.ThreadID != "" {
+		data["thread_id"] = rr.ThreadID
+	}
+	if !rr.Timestamp.IsZero() {
+		data["ts"] = rr.Timestamp.UnixMilli()
+	}
+	return json.Marshal(data)
+}
+
+type Presence string
+
+const (
+	PresenceOnline      Presence = "online"
+	PresenceOffline     Presence = "offline"
+	PresenceUnavailable Presence = "unavailable"
+)
+
+// PresenceEventContent represents the content of a m.presence ephemeral event.
+// https://spec.matrix.org/v1.2/client-server-api/#mpresence
+type PresenceEventContent struct {
+	Presence        Presence            `json:"presence"`
+	Displayname     string              `json:"displayname,omitempty"`
+	AvatarURL       id.ContentURIString `json:"avatar_url,omitempty"`
+	LastActiveAgo   int64               `json:"last_active_ago,omitempty"`
+	CurrentlyActive bool                `json:"currently_active,omitempty"`
+	StatusMessage   string              `json:"status_msg,omitempty"`
+}
diff --git a/vendor/maunium.net/go/mautrix/event/events.go b/vendor/maunium.net/go/mautrix/event/events.go
new file mode 100644
index 0000000..23769ae
--- /dev/null
+++ b/vendor/maunium.net/go/mautrix/event/events.go
@@ -0,0 +1,156 @@
+// 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 event
+
+import (
+	"encoding/json"
+	"time"
+
+	"maunium.net/go/mautrix/id"
+)
+
+// Event represents a single Matrix event.
+type Event struct {
+	StateKey  *string    `json:"state_key,omitempty"`        // The state key for the event. Only present on State Events.
+	Sender    id.UserID  `json:"sender,omitempty"`           // The user ID of the sender of the event
+	Type      Type       `json:"type"`                       // The event type
+	Timestamp int64      `json:"origin_server_ts,omitempty"` // The unix timestamp when this message was sent by the origin server
+	ID        id.EventID `json:"event_id,omitempty"`         // The unique ID of this event
+	RoomID    id.RoomID  `json:"room_id,omitempty"`          // The room the event was sent to. May be nil (e.g. for presence)
+	Content   Content    `json:"content"`                    // The JSON content of the event.
+	Redacts   id.EventID `json:"redacts,omitempty"`          // The event ID that was redacted if a m.room.redaction event
+	Unsigned  Unsigned   `json:"unsigned,omitempty"`         // Unsigned content set by own homeserver.
+
+	Mautrix MautrixInfo `json:"-"`
+
+	ToUserID   id.UserID   `json:"to_user_id,omitempty"`   // The user ID that the to-device event was sent to. Only present in MSC2409 appservice transactions.
+	ToDeviceID id.DeviceID `json:"to_device_id,omitempty"` // The device ID that the to-device event was sent to. Only present in MSC2409 appservice transactions.
+}
+
+type eventForMarshaling struct {
+	StateKey  *string    `json:"state_key,omitempty"`
+	Sender    id.UserID  `json:"sender,omitempty"`
+	Type      Type       `json:"type"`
+	Timestamp int64      `json:"origin_server_ts,omitempty"`
+	ID        id.EventID `json:"event_id,omitempty"`
+	RoomID    id.RoomID  `json:"room_id,omitempty"`
+	Content   Content    `json:"content"`
+	Redacts   id.EventID `json:"redacts,omitempty"`
+	Unsigned  *Unsigned  `json:"unsigned,omitempty"`
+
+	PrevContent   *Content    `json:"prev_content,omitempty"`
+	ReplacesState *id.EventID `json:"replaces_state,omitempty"`
+
+	ToUserID   id.UserID   `json:"to_user_id,omitempty"`
+	ToDeviceID id.DeviceID `json:"to_device_id,omitempty"`
+}
+
+// UnmarshalJSON unmarshals the event, including moving prev_content from the top level to inside unsigned.
+func (evt *Event) UnmarshalJSON(data []byte) error {
+	var efm eventForMarshaling
+	err := json.Unmarshal(data, &efm)
+	if err != nil {
+		return err
+	}
+	evt.StateKey = efm.StateKey
+	evt.Sender = efm.Sender
+	evt.Type = efm.Type
+	evt.Timestamp = efm.Timestamp
+	evt.ID = efm.ID
+	evt.RoomID = efm.RoomID
+	evt.Content = efm.Content
+	evt.Redacts = efm.Redacts
+	if efm.Unsigned != nil {
+		evt.Unsigned = *efm.Unsigned
+	}
+	if efm.PrevContent != nil && evt.Unsigned.PrevContent == nil {
+		evt.Unsigned.PrevContent = efm.PrevContent
+	}
+	if efm.ReplacesState != nil && *efm.ReplacesState != "" && evt.Unsigned.ReplacesState == "" {
+		evt.Unsigned.ReplacesState = *efm.ReplacesState
+	}
+	evt.ToUserID = efm.ToUserID
+	evt.ToDeviceID = efm.ToDeviceID
+	return nil
+}
+
+// MarshalJSON marshals the event, including omitting the unsigned field if it's empty.
+//
+// This is necessary because Unsigned is not a pointer (for convenience reasons),
+// and encoding/json doesn't know how to check if a non-pointer struct is empty.
+//
+// TODO(tulir): maybe it makes more sense to make Unsigned a pointer and make an easy and safe way to access it?
+func (evt *Event) MarshalJSON() ([]byte, error) {
+	unsigned := &evt.Unsigned
+	if unsigned.IsEmpty() {
+		unsigned = nil
+	}
+	return json.Marshal(&eventForMarshaling{
+		StateKey:   evt.StateKey,
+		Sender:     evt.Sender,
+		Type:       evt.Type,
+		Timestamp:  evt.Timestamp,
+		ID:         evt.ID,
+		RoomID:     evt.RoomID,
+		Content:    evt.Content,
+		Redacts:    evt.Redacts,
+		Unsigned:   unsigned,
+		ToUserID:   evt.ToUserID,
+		ToDeviceID: evt.ToDeviceID,
+	})
+}
+
+type MautrixInfo struct {
+	EventSource Source
+
+	TrustState    id.TrustState
+	ForwardedKeys bool
+	WasEncrypted  bool
+	TrustSource   *id.Device
+
+	ReceivedAt         time.Time
+	EditedAt           time.Time
+	LastEditID         id.EventID
+	DecryptionDuration time.Duration
+
+	CheckpointSent bool
+}
+
+func (evt *Event) GetStateKey() string {
+	if evt.StateKey != nil {
+		return *evt.StateKey
+	}
+	return ""
+}
+
+type StrippedState struct {
+	Content  Content   `json:"content"`
+	Type     Type      `json:"type"`
+	StateKey string    `json:"state_key"`
+	Sender   id.UserID `json:"sender"`
+}
+
+type Unsigned struct {
+	PrevContent     *Content        `json:"prev_content,omitempty"`
+	PrevSender      id.UserID       `json:"prev_sender,omitempty"`
+	ReplacesState   id.EventID      `json:"replaces_state,omitempty"`
+	Age             int64           `json:"age,omitempty"`
+	TransactionID   string          `json:"transaction_id,omitempty"`
+	Relations       *Relations      `json:"m.relations,omitempty"`
+	RedactedBecause *Event          `json:"redacted_because,omitempty"`
+	InviteRoomState []StrippedState `json:"invite_room_state,omitempty"`
+
+	BeeperHSOrder    int64 `json:"com.beeper.hs.order,omitempty"`
+	BeeperHSSuborder int64 `json:"com.beeper.hs.suborder,omitempty"`
+	BeeperFromBackup bool  `json:"com.beeper.from_backup,omitempty"`
+}
+
+func (us *Unsigned) IsEmpty() bool {
+	return us.PrevContent == nil && us.PrevSender == "" && us.ReplacesState == "" && us.Age == 0 &&
+		us.TransactionID == "" && us.RedactedBecause == nil && us.InviteRoomState == nil && us.Relations == nil &&
+		us.BeeperHSOrder == 0 && us.BeeperHSSuborder == 0
+}
diff --git a/vendor/maunium.net/go/mautrix/event/eventsource.go b/vendor/maunium.net/go/mautrix/event/eventsource.go
new file mode 100644
index 0000000..86c1ceb
--- /dev/null
+++ b/vendor/maunium.net/go/mautrix/event/eventsource.go
@@ -0,0 +1,72 @@
+// Copyright (c) 2024 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 event
+
+import (
+	"fmt"
+)
+
+// Source represents the part of the sync response that an event came from.
+type Source int
+
+const (
+	SourcePresence Source = 1 << iota
+	SourceJoin
+	SourceInvite
+	SourceLeave
+	SourceAccountData
+	SourceTimeline
+	SourceState
+	SourceEphemeral
+	SourceToDevice
+	SourceDecrypted
+)
+
+const primaryTypes = SourcePresence | SourceAccountData | SourceToDevice | SourceTimeline | SourceState
+const roomSections = SourceJoin | SourceInvite | SourceLeave
+const roomableTypes = SourceAccountData | SourceTimeline | SourceState
+const encryptableTypes = roomableTypes | SourceToDevice
+
+func (es Source) String() string {
+	var typeName string
+	switch es & primaryTypes {
+	case SourcePresence:
+		typeName = "presence"
+	case SourceAccountData:
+		typeName = "account data"
+	case SourceToDevice:
+		typeName = "to-device"
+	case SourceTimeline:
+		typeName = "timeline"
+	case SourceState:
+		typeName = "state"
+	default:
+		return fmt.Sprintf("unknown (%d)", es)
+	}
+	if es&roomableTypes != 0 {
+		switch es & roomSections {
+		case SourceJoin:
+			typeName = "joined room " + typeName
+		case SourceInvite:
+			typeName = "invited room " + typeName
+		case SourceLeave:
+			typeName = "left room " + typeName
+		default:
+			return fmt.Sprintf("unknown (%s+%d)", typeName, es)
+		}
+		es &^= roomSections
+	}
+	if es&encryptableTypes != 0 && es&SourceDecrypted != 0 {
+		typeName += " (decrypted)"
+		es &^= SourceDecrypted
+	}
+	es &^= primaryTypes
+	if es != 0 {
+		return fmt.Sprintf("unknown (%s+%d)", typeName, es)
+	}
+	return typeName
+}
diff --git a/vendor/maunium.net/go/mautrix/event/member.go b/vendor/maunium.net/go/mautrix/event/member.go
new file mode 100644
index 0000000..ebafdcb
--- /dev/null
+++ b/vendor/maunium.net/go/mautrix/event/member.go
@@ -0,0 +1,53 @@
+// 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 event
+
+import (
+	"encoding/json"
+
+	"maunium.net/go/mautrix/id"
+)
+
+// Membership is an enum specifying the membership state of a room member.
+type Membership string
+
+func (ms Membership) IsInviteOrJoin() bool {
+	return ms == MembershipJoin || ms == MembershipInvite
+}
+
+func (ms Membership) IsLeaveOrBan() bool {
+	return ms == MembershipLeave || ms == MembershipBan
+}
+
+// The allowed membership states as specified in spec section 10.5.5.
+const (
+	MembershipJoin   Membership = "join"
+	MembershipLeave  Membership = "leave"
+	MembershipInvite Membership = "invite"
+	MembershipBan    Membership = "ban"
+	MembershipKnock  Membership = "knock"
+)
+
+// MemberEventContent represents the content of a m.room.member state event.
+// https://spec.matrix.org/v1.2/client-server-api/#mroommember
+type MemberEventContent struct {
+	Membership       Membership          `json:"membership"`
+	AvatarURL        id.ContentURIString `json:"avatar_url,omitempty"`
+	Displayname      string              `json:"displayname,omitempty"`
+	IsDirect         bool                `json:"is_direct,omitempty"`
+	ThirdPartyInvite *ThirdPartyInvite   `json:"third_party_invite,omitempty"`
+	Reason           string              `json:"reason,omitempty"`
+}
+
+type ThirdPartyInvite struct {
+	DisplayName string `json:"display_name"`
+	Signed      struct {
+		Token      string          `json:"token"`
+		Signatures json.RawMessage `json:"signatures"`
+		MXID       string          `json:"mxid"`
+	}
+}
diff --git a/vendor/maunium.net/go/mautrix/event/message.go b/vendor/maunium.net/go/mautrix/event/message.go
new file mode 100644
index 0000000..9badd9a
--- /dev/null
+++ b/vendor/maunium.net/go/mautrix/event/message.go
@@ -0,0 +1,356 @@
+// Copyright (c) 2023 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 event
+
+import (
+	"encoding/json"
+	"slices"
+	"strconv"
+	"strings"
+
+	"golang.org/x/net/html"
+
+	"maunium.net/go/mautrix/crypto/attachment"
+	"maunium.net/go/mautrix/id"
+)
+
+// MessageType is the sub-type of a m.room.message event.
+// https://spec.matrix.org/v1.2/client-server-api/#mroommessage-msgtypes
+type MessageType string
+
+func (mt MessageType) IsText() bool {
+	switch mt {
+	case MsgText, MsgNotice, MsgEmote:
+		return true
+	default:
+		return false
+	}
+}
+
+func (mt MessageType) IsMedia() bool {
+	switch mt {
+	case MsgImage, MsgVideo, MsgAudio, MsgFile, MessageType(EventSticker.Type):
+		return true
+	default:
+		return false
+	}
+}
+
+// Msgtypes
+const (
+	MsgText     MessageType = "m.text"
+	MsgEmote    MessageType = "m.emote"
+	MsgNotice   MessageType = "m.notice"
+	MsgImage    MessageType = "m.image"
+	MsgLocation MessageType = "m.location"
+	MsgVideo    MessageType = "m.video"
+	MsgAudio    MessageType = "m.audio"
+	MsgFile     MessageType = "m.file"
+
+	MsgVerificationRequest MessageType = "m.key.verification.request"
+
+	MsgBeeperGallery MessageType = "com.beeper.gallery"
+)
+
+// Format specifies the format of the formatted_body in m.room.message events.
+// https://spec.matrix.org/v1.2/client-server-api/#mroommessage-msgtypes
+type Format string
+
+// Message formats
+const (
+	FormatHTML Format = "org.matrix.custom.html"
+)
+
+// RedactionEventContent represents the content of a m.room.redaction message event.
+//
+// https://spec.matrix.org/v1.8/client-server-api/#mroomredaction
+type RedactionEventContent struct {
+	Reason string `json:"reason,omitempty"`
+
+	// The event ID is here as of room v11. In old servers it may only be at the top level.
+	Redacts id.EventID `json:"redacts,omitempty"`
+}
+
+// ReactionEventContent represents the content of a m.reaction message event.
+// This is not yet in a spec release, see https://github.com/matrix-org/matrix-doc/pull/1849
+type ReactionEventContent struct {
+	RelatesTo RelatesTo `json:"m.relates_to"`
+}
+
+func (content *ReactionEventContent) GetRelatesTo() *RelatesTo {
+	return &content.RelatesTo
+}
+
+func (content *ReactionEventContent) OptionalGetRelatesTo() *RelatesTo {
+	return &content.RelatesTo
+}
+
+func (content *ReactionEventContent) SetRelatesTo(rel *RelatesTo) {
+	content.RelatesTo = *rel
+}
+
+// MessageEventContent represents the content of a m.room.message event.
+//
+// It is also used to represent m.sticker events, as they are equivalent to m.room.message
+// with the exception of the msgtype field.
+//
+// https://spec.matrix.org/v1.2/client-server-api/#mroommessage
+type MessageEventContent struct {
+	// Base m.room.message fields
+	MsgType MessageType `json:"msgtype,omitempty"`
+	Body    string      `json:"body"`
+
+	// Extra fields for text types
+	Format        Format `json:"format,omitempty"`
+	FormattedBody string `json:"formatted_body,omitempty"`
+
+	// Extra field for m.location
+	GeoURI string `json:"geo_uri,omitempty"`
+
+	// Extra fields for media types
+	URL  id.ContentURIString `json:"url,omitempty"`
+	Info *FileInfo           `json:"info,omitempty"`
+	File *EncryptedFileInfo  `json:"file,omitempty"`
+
+	FileName string `json:"filename,omitempty"`
+
+	Mentions *Mentions `json:"m.mentions,omitempty"`
+
+	// Edits and relations
+	NewContent *MessageEventContent `json:"m.new_content,omitempty"`
+	RelatesTo  *RelatesTo           `json:"m.relates_to,omitempty"`
+
+	// In-room verification
+	To         id.UserID            `json:"to,omitempty"`
+	FromDevice id.DeviceID          `json:"from_device,omitempty"`
+	Methods    []VerificationMethod `json:"methods,omitempty"`
+
+	replyFallbackRemoved bool
+
+	MessageSendRetry         *BeeperRetryMetadata     `json:"com.beeper.message_send_retry,omitempty"`
+	BeeperGalleryImages      []*MessageEventContent   `json:"com.beeper.gallery.images,omitempty"`
+	BeeperGalleryCaption     string                   `json:"com.beeper.gallery.caption,omitempty"`
+	BeeperGalleryCaptionHTML string                   `json:"com.beeper.gallery.caption_html,omitempty"`
+	BeeperPerMessageProfile  *BeeperPerMessageProfile `json:"com.beeper.per_message_profile,omitempty"`
+
+	BeeperLinkPreviews []*BeeperLinkPreview `json:"com.beeper.linkpreviews,omitempty"`
+
+	MSC1767Audio *MSC1767Audio `json:"org.matrix.msc1767.audio,omitempty"`
+	MSC3245Voice *MSC3245Voice `json:"org.matrix.msc3245.voice,omitempty"`
+}
+
+func (content *MessageEventContent) GetFileName() string {
+	if content.FileName != "" {
+		return content.FileName
+	}
+	return content.Body
+}
+
+func (content *MessageEventContent) GetCaption() string {
+	if content.FileName != "" && content.Body != "" && content.Body != content.FileName {
+		return content.Body
+	}
+	return ""
+}
+
+func (content *MessageEventContent) GetFormattedCaption() string {
+	if content.Format == FormatHTML && content.FormattedBody != "" {
+		return content.FormattedBody
+	}
+	return ""
+}
+
+func (content *MessageEventContent) GetRelatesTo() *RelatesTo {
+	if content.RelatesTo == nil {
+		content.RelatesTo = &RelatesTo{}
+	}
+	return content.RelatesTo
+}
+
+func (content *MessageEventContent) OptionalGetRelatesTo() *RelatesTo {
+	return content.RelatesTo
+}
+
+func (content *MessageEventContent) SetRelatesTo(rel *RelatesTo) {
+	content.RelatesTo = rel
+}
+
+func (content *MessageEventContent) SetEdit(original id.EventID) {
+	newContent := *content
+	content.NewContent = &newContent
+	content.RelatesTo = (&RelatesTo{}).SetReplace(original)
+	if content.MsgType == MsgText || content.MsgType == MsgNotice {
+		content.Body = "* " + content.Body
+		if content.Format == FormatHTML && len(content.FormattedBody) > 0 {
+			content.FormattedBody = "* " + content.FormattedBody
+		}
+		// If the message is long, remove most of the useless edit fallback to avoid event size issues.
+		if len(content.Body) > 10000 {
+			content.FormattedBody = ""
+			content.Format = ""
+			content.Body = content.Body[:50] + "[edit fallback cut…]"
+		}
+	}
+}
+
+// TextToHTML converts the given text to a HTML-safe representation by escaping HTML characters
+// and replacing newlines with <br/> tags.
+func TextToHTML(text string) string {
+	return strings.ReplaceAll(html.EscapeString(text), "\n", "<br/>")
+}
+
+// ReverseTextToHTML reverses the modifications made by TextToHTML, i.e. replaces <br/> tags with newlines
+// and unescapes HTML escape codes. For actually parsing HTML, use the format package instead.
+func ReverseTextToHTML(input string) string {
+	return html.UnescapeString(strings.ReplaceAll(input, "<br/>", "\n"))
+}
+
+func (content *MessageEventContent) EnsureHasHTML() {
+	if len(content.FormattedBody) == 0 || content.Format != FormatHTML {
+		content.FormattedBody = TextToHTML(content.Body)
+		content.Format = FormatHTML
+	}
+}
+
+func (content *MessageEventContent) GetFile() *EncryptedFileInfo {
+	if content.File == nil {
+		content.File = &EncryptedFileInfo{}
+	}
+	return content.File
+}
+
+func (content *MessageEventContent) GetInfo() *FileInfo {
+	if content.Info == nil {
+		content.Info = &FileInfo{}
+	}
+	return content.Info
+}
+
+type Mentions struct {
+	UserIDs []id.UserID `json:"user_ids,omitempty"`
+	Room    bool        `json:"room,omitempty"`
+}
+
+func (m *Mentions) Add(userID id.UserID) {
+	if userID != "" && !slices.Contains(m.UserIDs, userID) {
+		m.UserIDs = append(m.UserIDs, userID)
+	}
+}
+
+func (m *Mentions) Has(userID id.UserID) bool {
+	return m != nil && slices.Contains(m.UserIDs, userID)
+}
+
+type EncryptedFileInfo struct {
+	attachment.EncryptedFile
+	URL id.ContentURIString `json:"url"`
+}
+
+type FileInfo struct {
+	MimeType      string              `json:"mimetype,omitempty"`
+	ThumbnailInfo *FileInfo           `json:"thumbnail_info,omitempty"`
+	ThumbnailURL  id.ContentURIString `json:"thumbnail_url,omitempty"`
+	ThumbnailFile *EncryptedFileInfo  `json:"thumbnail_file,omitempty"`
+
+	Blurhash     string `json:"blurhash,omitempty"`
+	AnoaBlurhash string `json:"xyz.amorgan.blurhash,omitempty"`
+
+	Width    int `json:"-"`
+	Height   int `json:"-"`
+	Duration int `json:"-"`
+	Size     int `json:"-"`
+}
+
+type serializableFileInfo struct {
+	MimeType      string                `json:"mimetype,omitempty"`
+	ThumbnailInfo *serializableFileInfo `json:"thumbnail_info,omitempty"`
+	ThumbnailURL  id.ContentURIString   `json:"thumbnail_url,omitempty"`
+	ThumbnailFile *EncryptedFileInfo    `json:"thumbnail_file,omitempty"`
+
+	Blurhash     string `json:"blurhash,omitempty"`
+	AnoaBlurhash string `json:"xyz.amorgan.blurhash,omitempty"`
+
+	Width    json.Number `json:"w,omitempty"`
+	Height   json.Number `json:"h,omitempty"`
+	Duration json.Number `json:"duration,omitempty"`
+	Size     json.Number `json:"size,omitempty"`
+}
+
+func (sfi *serializableFileInfo) CopyFrom(fileInfo *FileInfo) *serializableFileInfo {
+	if fileInfo == nil {
+		return nil
+	}
+	*sfi = serializableFileInfo{
+		MimeType:      fileInfo.MimeType,
+		ThumbnailURL:  fileInfo.ThumbnailURL,
+		ThumbnailInfo: (&serializableFileInfo{}).CopyFrom(fileInfo.ThumbnailInfo),
+		ThumbnailFile: fileInfo.ThumbnailFile,
+
+		Blurhash:     fileInfo.Blurhash,
+		AnoaBlurhash: fileInfo.AnoaBlurhash,
+	}
+	if fileInfo.Width > 0 {
+		sfi.Width = json.Number(strconv.Itoa(fileInfo.Width))
+	}
+	if fileInfo.Height > 0 {
+		sfi.Height = json.Number(strconv.Itoa(fileInfo.Height))
+	}
+	if fileInfo.Size > 0 {
+		sfi.Size = json.Number(strconv.Itoa(fileInfo.Size))
+
+	}
+	if fileInfo.Duration > 0 {
+		sfi.Duration = json.Number(strconv.Itoa(int(fileInfo.Duration)))
+	}
+	return sfi
+}
+
+func (sfi *serializableFileInfo) CopyTo(fileInfo *FileInfo) {
+	*fileInfo = FileInfo{
+		Width:         numberToInt(sfi.Width),
+		Height:        numberToInt(sfi.Height),
+		Size:          numberToInt(sfi.Size),
+		Duration:      numberToInt(sfi.Duration),
+		MimeType:      sfi.MimeType,
+		ThumbnailURL:  sfi.ThumbnailURL,
+		ThumbnailFile: sfi.ThumbnailFile,
+		Blurhash:      sfi.Blurhash,
+		AnoaBlurhash:  sfi.AnoaBlurhash,
+	}
+	if sfi.ThumbnailInfo != nil {
+		fileInfo.ThumbnailInfo = &FileInfo{}
+		sfi.ThumbnailInfo.CopyTo(fileInfo.ThumbnailInfo)
+	}
+}
+
+func (fileInfo *FileInfo) UnmarshalJSON(data []byte) error {
+	sfi := &serializableFileInfo{}
+	if err := json.Unmarshal(data, sfi); err != nil {
+		return err
+	}
+	sfi.CopyTo(fileInfo)
+	return nil
+}
+
+func (fileInfo *FileInfo) MarshalJSON() ([]byte, error) {
+	return json.Marshal((&serializableFileInfo{}).CopyFrom(fileInfo))
+}
+
+func numberToInt(val json.Number) int {
+	f64, _ := val.Float64()
+	if f64 > 0 {
+		return int(f64)
+	}
+	return 0
+}
+
+func (fileInfo *FileInfo) GetThumbnailInfo() *FileInfo {
+	if fileInfo.ThumbnailInfo == nil {
+		fileInfo.ThumbnailInfo = &FileInfo{}
+	}
+	return fileInfo.ThumbnailInfo
+}
diff --git a/vendor/maunium.net/go/mautrix/event/poll.go b/vendor/maunium.net/go/mautrix/event/poll.go
new file mode 100644
index 0000000..3733301
--- /dev/null
+++ b/vendor/maunium.net/go/mautrix/event/poll.go
@@ -0,0 +1,67 @@
+// Copyright (c) 2024 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 event
+
+type PollResponseEventContent struct {
+	RelatesTo RelatesTo `json:"m.relates_to"`
+	Response  struct {
+		Answers []string `json:"answers"`
+	} `json:"org.matrix.msc3381.poll.response"`
+}
+
+func (content *PollResponseEventContent) GetRelatesTo() *RelatesTo {
+	return &content.RelatesTo
+}
+
+func (content *PollResponseEventContent) OptionalGetRelatesTo() *RelatesTo {
+	if content.RelatesTo.Type == "" {
+		return nil
+	}
+	return &content.RelatesTo
+}
+
+func (content *PollResponseEventContent) SetRelatesTo(rel *RelatesTo) {
+	content.RelatesTo = *rel
+}
+
+type MSC1767Message struct {
+	Text    string `json:"org.matrix.msc1767.text,omitempty"`
+	HTML    string `json:"org.matrix.msc1767.html,omitempty"`
+	Message []struct {
+		MimeType string `json:"mimetype"`
+		Body     string `json:"body"`
+	} `json:"org.matrix.msc1767.message,omitempty"`
+}
+
+type PollStartEventContent struct {
+	RelatesTo *RelatesTo `json:"m.relates_to"`
+	Mentions  *Mentions  `json:"m.mentions,omitempty"`
+	PollStart struct {
+		Kind          string         `json:"kind"`
+		MaxSelections int            `json:"max_selections"`
+		Question      MSC1767Message `json:"question"`
+		Answers       []struct {
+			ID string `json:"id"`
+			MSC1767Message
+		} `json:"answers"`
+	} `json:"org.matrix.msc3381.poll.start"`
+}
+
+func (content *PollStartEventContent) GetRelatesTo() *RelatesTo {
+	if content.RelatesTo == nil {
+		content.RelatesTo = &RelatesTo{}
+	}
+	return content.RelatesTo
+}
+
+func (content *PollStartEventContent) OptionalGetRelatesTo() *RelatesTo {
+	return content.RelatesTo
+}
+
+func (content *PollStartEventContent) SetRelatesTo(rel *RelatesTo) {
+	content.RelatesTo = rel
+}
diff --git a/vendor/maunium.net/go/mautrix/event/powerlevels.go b/vendor/maunium.net/go/mautrix/event/powerlevels.go
new file mode 100644
index 0000000..2f4d457
--- /dev/null
+++ b/vendor/maunium.net/go/mautrix/event/powerlevels.go
@@ -0,0 +1,199 @@
+// 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 event
+
+import (
+	"sync"
+
+	"go.mau.fi/util/ptr"
+	"golang.org/x/exp/maps"
+
+	"maunium.net/go/mautrix/id"
+)
+
+// PowerLevelsEventContent represents the content of a m.room.power_levels state event content.
+// https://spec.matrix.org/v1.5/client-server-api/#mroompower_levels
+type PowerLevelsEventContent struct {
+	usersLock    sync.RWMutex
+	Users        map[id.UserID]int `json:"users,omitempty"`
+	UsersDefault int               `json:"users_default,omitempty"`
+
+	eventsLock    sync.RWMutex
+	Events        map[string]int `json:"events,omitempty"`
+	EventsDefault int            `json:"events_default,omitempty"`
+
+	Notifications *NotificationPowerLevels `json:"notifications,omitempty"`
+
+	StateDefaultPtr *int `json:"state_default,omitempty"`
+
+	InvitePtr *int `json:"invite,omitempty"`
+	KickPtr   *int `json:"kick,omitempty"`
+	BanPtr    *int `json:"ban,omitempty"`
+	RedactPtr *int `json:"redact,omitempty"`
+}
+
+func (pl *PowerLevelsEventContent) Clone() *PowerLevelsEventContent {
+	if pl == nil {
+		return nil
+	}
+	return &PowerLevelsEventContent{
+		Users:           maps.Clone(pl.Users),
+		UsersDefault:    pl.UsersDefault,
+		Events:          maps.Clone(pl.Events),
+		EventsDefault:   pl.EventsDefault,
+		StateDefaultPtr: ptr.Clone(pl.StateDefaultPtr),
+
+		Notifications: pl.Notifications.Clone(),
+
+		InvitePtr: ptr.Clone(pl.InvitePtr),
+		KickPtr:   ptr.Clone(pl.KickPtr),
+		BanPtr:    ptr.Clone(pl.BanPtr),
+		RedactPtr: ptr.Clone(pl.RedactPtr),
+	}
+}
+
+type NotificationPowerLevels struct {
+	RoomPtr *int `json:"room,omitempty"`
+}
+
+func (npl *NotificationPowerLevels) Clone() *NotificationPowerLevels {
+	if npl == nil {
+		return nil
+	}
+	return &NotificationPowerLevels{
+		RoomPtr: ptr.Clone(npl.RoomPtr),
+	}
+}
+
+func (npl *NotificationPowerLevels) Room() int {
+	if npl != nil && npl.RoomPtr != nil {
+		return *npl.RoomPtr
+	}
+	return 50
+}
+
+func (pl *PowerLevelsEventContent) Invite() int {
+	if pl.InvitePtr != nil {
+		return *pl.InvitePtr
+	}
+	return 0
+}
+
+func (pl *PowerLevelsEventContent) Kick() int {
+	if pl.KickPtr != nil {
+		return *pl.KickPtr
+	}
+	return 50
+}
+
+func (pl *PowerLevelsEventContent) Ban() int {
+	if pl.BanPtr != nil {
+		return *pl.BanPtr
+	}
+	return 50
+}
+
+func (pl *PowerLevelsEventContent) Redact() int {
+	if pl.RedactPtr != nil {
+		return *pl.RedactPtr
+	}
+	return 50
+}
+
+func (pl *PowerLevelsEventContent) StateDefault() int {
+	if pl.StateDefaultPtr != nil {
+		return *pl.StateDefaultPtr
+	}
+	return 50
+}
+
+func (pl *PowerLevelsEventContent) GetUserLevel(userID id.UserID) int {
+	pl.usersLock.RLock()
+	defer pl.usersLock.RUnlock()
+	level, ok := pl.Users[userID]
+	if !ok {
+		return pl.UsersDefault
+	}
+	return level
+}
+
+func (pl *PowerLevelsEventContent) SetUserLevel(userID id.UserID, level int) {
+	pl.usersLock.Lock()
+	defer pl.usersLock.Unlock()
+	if level == pl.UsersDefault {
+		delete(pl.Users, userID)
+	} else {
+		if pl.Users == nil {
+			pl.Users = make(map[id.UserID]int)
+		}
+		pl.Users[userID] = level
+	}
+}
+
+func (pl *PowerLevelsEventContent) EnsureUserLevel(target id.UserID, level int) bool {
+	return pl.EnsureUserLevelAs("", target, level)
+}
+
+func (pl *PowerLevelsEventContent) EnsureUserLevelAs(actor, target id.UserID, level int) bool {
+	existingLevel := pl.GetUserLevel(target)
+	if actor != "" {
+		actorLevel := pl.GetUserLevel(actor)
+		if actorLevel <= existingLevel || actorLevel < level {
+			return false
+		}
+	}
+	if existingLevel != level {
+		pl.SetUserLevel(target, level)
+		return true
+	}
+	return false
+}
+
+func (pl *PowerLevelsEventContent) GetEventLevel(eventType Type) int {
+	pl.eventsLock.RLock()
+	defer pl.eventsLock.RUnlock()
+	level, ok := pl.Events[eventType.String()]
+	if !ok {
+		if eventType.IsState() {
+			return pl.StateDefault()
+		}
+		return pl.EventsDefault
+	}
+	return level
+}
+
+func (pl *PowerLevelsEventContent) SetEventLevel(eventType Type, level int) {
+	pl.eventsLock.Lock()
+	defer pl.eventsLock.Unlock()
+	if (eventType.IsState() && level == pl.StateDefault()) || (!eventType.IsState() && level == pl.EventsDefault) {
+		delete(pl.Events, eventType.String())
+	} else {
+		if pl.Events == nil {
+			pl.Events = make(map[string]int)
+		}
+		pl.Events[eventType.String()] = level
+	}
+}
+
+func (pl *PowerLevelsEventContent) EnsureEventLevel(eventType Type, level int) bool {
+	return pl.EnsureEventLevelAs("", eventType, level)
+}
+
+func (pl *PowerLevelsEventContent) EnsureEventLevelAs(actor id.UserID, eventType Type, level int) bool {
+	existingLevel := pl.GetEventLevel(eventType)
+	if actor != "" {
+		actorLevel := pl.GetUserLevel(actor)
+		if existingLevel > actorLevel || level > actorLevel {
+			return false
+		}
+	}
+	if existingLevel != level {
+		pl.SetEventLevel(eventType, level)
+		return true
+	}
+	return false
+}
diff --git a/vendor/maunium.net/go/mautrix/event/relations.go b/vendor/maunium.net/go/mautrix/event/relations.go
new file mode 100644
index 0000000..ea40cc0
--- /dev/null
+++ b/vendor/maunium.net/go/mautrix/event/relations.go
@@ -0,0 +1,234 @@
+// 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 event
+
+import (
+	"encoding/json"
+
+	"maunium.net/go/mautrix/id"
+)
+
+type RelationType string
+
+const (
+	RelReplace    RelationType = "m.replace"
+	RelReference  RelationType = "m.reference"
+	RelAnnotation RelationType = "m.annotation"
+	RelThread     RelationType = "m.thread"
+)
+
+type RelatesTo struct {
+	Type    RelationType `json:"rel_type,omitempty"`
+	EventID id.EventID   `json:"event_id,omitempty"`
+	Key     string       `json:"key,omitempty"`
+
+	InReplyTo     *InReplyTo `json:"m.in_reply_to,omitempty"`
+	IsFallingBack bool       `json:"is_falling_back,omitempty"`
+}
+
+type InReplyTo struct {
+	EventID id.EventID `json:"event_id,omitempty"`
+
+	UnstableRoomID id.RoomID `json:"room_id,omitempty"`
+}
+
+func (rel *RelatesTo) Copy() *RelatesTo {
+	if rel == nil {
+		return nil
+	}
+	cp := *rel
+	return &cp
+}
+
+func (rel *RelatesTo) GetReplaceID() id.EventID {
+	if rel != nil && rel.Type == RelReplace {
+		return rel.EventID
+	}
+	return ""
+}
+
+func (rel *RelatesTo) GetReferenceID() id.EventID {
+	if rel != nil && rel.Type == RelReference {
+		return rel.EventID
+	}
+	return ""
+}
+
+func (rel *RelatesTo) GetThreadParent() id.EventID {
+	if rel != nil && rel.Type == RelThread {
+		return rel.EventID
+	}
+	return ""
+}
+
+func (rel *RelatesTo) GetReplyTo() id.EventID {
+	if rel != nil && rel.InReplyTo != nil {
+		return rel.InReplyTo.EventID
+	}
+	return ""
+}
+
+func (rel *RelatesTo) GetNonFallbackReplyTo() id.EventID {
+	if rel != nil && rel.InReplyTo != nil && (rel.Type != RelThread || !rel.IsFallingBack) {
+		return rel.InReplyTo.EventID
+	}
+	return ""
+}
+
+func (rel *RelatesTo) GetAnnotationID() id.EventID {
+	if rel != nil && rel.Type == RelAnnotation {
+		return rel.EventID
+	}
+	return ""
+}
+
+func (rel *RelatesTo) GetAnnotationKey() string {
+	if rel != nil && rel.Type == RelAnnotation {
+		return rel.Key
+	}
+	return ""
+}
+
+func (rel *RelatesTo) SetReplace(mxid id.EventID) *RelatesTo {
+	rel.Type = RelReplace
+	rel.EventID = mxid
+	return rel
+}
+
+func (rel *RelatesTo) SetReplyTo(mxid id.EventID) *RelatesTo {
+	rel.InReplyTo = &InReplyTo{EventID: mxid}
+	rel.IsFallingBack = false
+	return rel
+}
+
+func (rel *RelatesTo) SetThread(mxid, fallback id.EventID) *RelatesTo {
+	rel.Type = RelThread
+	rel.EventID = mxid
+	if fallback != "" && rel.GetReplyTo() == "" {
+		rel.SetReplyTo(fallback)
+		rel.IsFallingBack = true
+	}
+	return rel
+}
+
+func (rel *RelatesTo) SetAnnotation(mxid id.EventID, key string) *RelatesTo {
+	rel.Type = RelAnnotation
+	rel.EventID = mxid
+	rel.Key = key
+	return rel
+}
+
+type RelationChunkItem struct {
+	Type    RelationType `json:"type"`
+	EventID string       `json:"event_id,omitempty"`
+	Key     string       `json:"key,omitempty"`
+	Count   int          `json:"count,omitempty"`
+}
+
+type RelationChunk struct {
+	Chunk []RelationChunkItem `json:"chunk"`
+
+	Limited bool `json:"limited"`
+	Count   int  `json:"count"`
+}
+
+type AnnotationChunk struct {
+	RelationChunk
+	Map map[string]int `json:"-"`
+}
+
+type serializableAnnotationChunk AnnotationChunk
+
+func (ac *AnnotationChunk) UnmarshalJSON(data []byte) error {
+	if err := json.Unmarshal(data, (*serializableAnnotationChunk)(ac)); err != nil {
+		return err
+	}
+	ac.Map = make(map[string]int)
+	for _, item := range ac.Chunk {
+		if item.Key != "" {
+			ac.Map[item.Key] += item.Count
+		}
+	}
+	return nil
+}
+
+func (ac *AnnotationChunk) Serialize() RelationChunk {
+	ac.Chunk = make([]RelationChunkItem, len(ac.Map))
+	i := 0
+	for key, count := range ac.Map {
+		ac.Chunk[i] = RelationChunkItem{
+			Type:  RelAnnotation,
+			Key:   key,
+			Count: count,
+		}
+		i++
+	}
+	return ac.RelationChunk
+}
+
+type EventIDChunk struct {
+	RelationChunk
+	List []string `json:"-"`
+}
+
+type serializableEventIDChunk EventIDChunk
+
+func (ec *EventIDChunk) UnmarshalJSON(data []byte) error {
+	if err := json.Unmarshal(data, (*serializableEventIDChunk)(ec)); err != nil {
+		return err
+	}
+	for _, item := range ec.Chunk {
+		ec.List = append(ec.List, item.EventID)
+	}
+	return nil
+}
+
+func (ec *EventIDChunk) Serialize(typ RelationType) RelationChunk {
+	ec.Chunk = make([]RelationChunkItem, len(ec.List))
+	for i, eventID := range ec.List {
+		ec.Chunk[i] = RelationChunkItem{
+			Type:    typ,
+			EventID: eventID,
+		}
+	}
+	return ec.RelationChunk
+}
+
+type Relations struct {
+	Raw map[RelationType]RelationChunk `json:"-"`
+
+	Annotations AnnotationChunk `json:"m.annotation,omitempty"`
+	References  EventIDChunk    `json:"m.reference,omitempty"`
+	Replaces    EventIDChunk    `json:"m.replace,omitempty"`
+}
+
+type serializableRelations Relations
+
+func (relations *Relations) UnmarshalJSON(data []byte) error {
+	if err := json.Unmarshal(data, &relations.Raw); err != nil {
+		return err
+	}
+	return json.Unmarshal(data, (*serializableRelations)(relations))
+}
+
+func (relations *Relations) MarshalJSON() ([]byte, error) {
+	if relations.Raw == nil {
+		relations.Raw = make(map[RelationType]RelationChunk)
+	}
+	relations.Raw[RelAnnotation] = relations.Annotations.Serialize()
+	relations.Raw[RelReference] = relations.References.Serialize(RelReference)
+	relations.Raw[RelReplace] = relations.Replaces.Serialize(RelReplace)
+	for key, item := range relations.Raw {
+		if !item.Limited {
+			item.Count = len(item.Chunk)
+		}
+		if item.Count == 0 {
+			delete(relations.Raw, key)
+		}
+	}
+	return json.Marshal(relations.Raw)
+}
diff --git a/vendor/maunium.net/go/mautrix/event/reply.go b/vendor/maunium.net/go/mautrix/event/reply.go
new file mode 100644
index 0000000..73f8cfc
--- /dev/null
+++ b/vendor/maunium.net/go/mautrix/event/reply.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 event
+
+import (
+	"fmt"
+	"regexp"
+	"strings"
+
+	"maunium.net/go/mautrix/id"
+)
+
+var HTMLReplyFallbackRegex = regexp.MustCompile(`^<mx-reply>[\s\S]+?</mx-reply>`)
+
+func TrimReplyFallbackHTML(html string) string {
+	return HTMLReplyFallbackRegex.ReplaceAllString(html, "")
+}
+
+func TrimReplyFallbackText(text string) string {
+	if (!strings.HasPrefix(text, "> <") && !strings.HasPrefix(text, "> * <")) || !strings.Contains(text, "\n") {
+		return text
+	}
+
+	lines := strings.Split(text, "\n")
+	for len(lines) > 0 && strings.HasPrefix(lines[0], "> ") {
+		lines = lines[1:]
+	}
+	return strings.TrimSpace(strings.Join(lines, "\n"))
+}
+
+func (content *MessageEventContent) RemoveReplyFallback() {
+	if len(content.RelatesTo.GetReplyTo()) > 0 && !content.replyFallbackRemoved {
+		if content.Format == FormatHTML {
+			content.FormattedBody = TrimReplyFallbackHTML(content.FormattedBody)
+		}
+		content.Body = TrimReplyFallbackText(content.Body)
+		content.replyFallbackRemoved = true
+	}
+}
+
+// Deprecated: RelatesTo methods are nil-safe, so RelatesTo.GetReplyTo can be used directly
+func (content *MessageEventContent) GetReplyTo() id.EventID {
+	return content.RelatesTo.GetReplyTo()
+}
+
+const ReplyFormat = `<mx-reply><blockquote><a href="https://matrix.to/#/%s/%s">In reply to</a> <a href="https://matrix.to/#/%s">%s</a><br>%s</blockquote></mx-reply>`
+
+func (evt *Event) GenerateReplyFallbackHTML() string {
+	parsedContent, ok := evt.Content.Parsed.(*MessageEventContent)
+	if !ok {
+		return ""
+	}
+	parsedContent.RemoveReplyFallback()
+	body := parsedContent.FormattedBody
+	if len(body) == 0 {
+		body = TextToHTML(parsedContent.Body)
+	}
+
+	senderDisplayName := evt.Sender
+
+	return fmt.Sprintf(ReplyFormat, evt.RoomID, evt.ID, evt.Sender, senderDisplayName, body)
+}
+
+func (evt *Event) GenerateReplyFallbackText() string {
+	parsedContent, ok := evt.Content.Parsed.(*MessageEventContent)
+	if !ok {
+		return ""
+	}
+	parsedContent.RemoveReplyFallback()
+	body := parsedContent.Body
+	lines := strings.Split(strings.TrimSpace(body), "\n")
+	firstLine, lines := lines[0], lines[1:]
+
+	senderDisplayName := evt.Sender
+
+	var fallbackText strings.Builder
+	_, _ = fmt.Fprintf(&fallbackText, "> <%s> %s", senderDisplayName, firstLine)
+	for _, line := range lines {
+		_, _ = fmt.Fprintf(&fallbackText, "\n> %s", line)
+	}
+	fallbackText.WriteString("\n\n")
+	return fallbackText.String()
+}
+
+func (content *MessageEventContent) SetReply(inReplyTo *Event) {
+	content.RelatesTo = (&RelatesTo{}).SetReplyTo(inReplyTo.ID)
+
+	if content.MsgType == MsgText || content.MsgType == MsgNotice {
+		content.EnsureHasHTML()
+		content.FormattedBody = inReplyTo.GenerateReplyFallbackHTML() + content.FormattedBody
+		content.Body = inReplyTo.GenerateReplyFallbackText() + content.Body
+		content.replyFallbackRemoved = false
+	}
+}
diff --git a/vendor/maunium.net/go/mautrix/event/state.go b/vendor/maunium.net/go/mautrix/event/state.go
new file mode 100644
index 0000000..1597289
--- /dev/null
+++ b/vendor/maunium.net/go/mautrix/event/state.go
@@ -0,0 +1,212 @@
+// 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 event
+
+import (
+	"maunium.net/go/mautrix/id"
+)
+
+// CanonicalAliasEventContent represents the content of a m.room.canonical_alias state event.
+// https://spec.matrix.org/v1.2/client-server-api/#mroomcanonical_alias
+type CanonicalAliasEventContent struct {
+	Alias      id.RoomAlias   `json:"alias"`
+	AltAliases []id.RoomAlias `json:"alt_aliases,omitempty"`
+}
+
+// RoomNameEventContent represents the content of a m.room.name state event.
+// https://spec.matrix.org/v1.2/client-server-api/#mroomname
+type RoomNameEventContent struct {
+	Name string `json:"name"`
+}
+
+// RoomAvatarEventContent represents the content of a m.room.avatar state event.
+// https://spec.matrix.org/v1.2/client-server-api/#mroomavatar
+type RoomAvatarEventContent struct {
+	URL         id.ContentURIString `json:"url,omitempty"`
+	Info        *FileInfo           `json:"info,omitempty"`
+	MSC3414File *EncryptedFileInfo  `json:"org.matrix.msc3414.file,omitempty"`
+}
+
+// ServerACLEventContent represents the content of a m.room.server_acl state event.
+// https://spec.matrix.org/v1.2/client-server-api/#server-access-control-lists-acls-for-rooms
+type ServerACLEventContent struct {
+	Allow           []string `json:"allow,omitempty"`
+	AllowIPLiterals bool     `json:"allow_ip_literals"`
+	Deny            []string `json:"deny,omitempty"`
+}
+
+// TopicEventContent represents the content of a m.room.topic state event.
+// https://spec.matrix.org/v1.2/client-server-api/#mroomtopic
+type TopicEventContent struct {
+	Topic string `json:"topic"`
+}
+
+// TombstoneEventContent represents the content of a m.room.tombstone state event.
+// https://spec.matrix.org/v1.2/client-server-api/#mroomtombstone
+type TombstoneEventContent struct {
+	Body            string    `json:"body"`
+	ReplacementRoom id.RoomID `json:"replacement_room"`
+}
+
+type Predecessor struct {
+	RoomID  id.RoomID  `json:"room_id"`
+	EventID id.EventID `json:"event_id"`
+}
+
+type RoomVersion string
+
+const (
+	RoomV1  RoomVersion = "1"
+	RoomV2  RoomVersion = "2"
+	RoomV3  RoomVersion = "3"
+	RoomV4  RoomVersion = "4"
+	RoomV5  RoomVersion = "5"
+	RoomV6  RoomVersion = "6"
+	RoomV7  RoomVersion = "7"
+	RoomV8  RoomVersion = "8"
+	RoomV9  RoomVersion = "9"
+	RoomV10 RoomVersion = "10"
+	RoomV11 RoomVersion = "11"
+)
+
+// CreateEventContent represents the content of a m.room.create state event.
+// https://spec.matrix.org/v1.2/client-server-api/#mroomcreate
+type CreateEventContent struct {
+	Type        RoomType     `json:"type,omitempty"`
+	Creator     id.UserID    `json:"creator,omitempty"`
+	Federate    bool         `json:"m.federate,omitempty"`
+	RoomVersion RoomVersion  `json:"room_version,omitempty"`
+	Predecessor *Predecessor `json:"predecessor,omitempty"`
+}
+
+// JoinRule specifies how open a room is to new members.
+// https://spec.matrix.org/v1.2/client-server-api/#mroomjoin_rules
+type JoinRule string
+
+const (
+	JoinRulePublic          JoinRule = "public"
+	JoinRuleKnock           JoinRule = "knock"
+	JoinRuleInvite          JoinRule = "invite"
+	JoinRuleRestricted      JoinRule = "restricted"
+	JoinRuleKnockRestricted JoinRule = "knock_restricted"
+	JoinRulePrivate         JoinRule = "private"
+)
+
+// JoinRulesEventContent represents the content of a m.room.join_rules state event.
+// https://spec.matrix.org/v1.2/client-server-api/#mroomjoin_rules
+type JoinRulesEventContent struct {
+	JoinRule JoinRule        `json:"join_rule"`
+	Allow    []JoinRuleAllow `json:"allow,omitempty"`
+}
+
+type JoinRuleAllowType string
+
+const (
+	JoinRuleAllowRoomMembership JoinRuleAllowType = "m.room_membership"
+)
+
+type JoinRuleAllow struct {
+	RoomID id.RoomID         `json:"room_id"`
+	Type   JoinRuleAllowType `json:"type"`
+}
+
+// PinnedEventsEventContent represents the content of a m.room.pinned_events state event.
+// https://spec.matrix.org/v1.2/client-server-api/#mroompinned_events
+type PinnedEventsEventContent struct {
+	Pinned []id.EventID `json:"pinned"`
+}
+
+// HistoryVisibility specifies who can see new messages.
+// https://spec.matrix.org/v1.2/client-server-api/#mroomhistory_visibility
+type HistoryVisibility string
+
+const (
+	HistoryVisibilityInvited       HistoryVisibility = "invited"
+	HistoryVisibilityJoined        HistoryVisibility = "joined"
+	HistoryVisibilityShared        HistoryVisibility = "shared"
+	HistoryVisibilityWorldReadable HistoryVisibility = "world_readable"
+)
+
+// HistoryVisibilityEventContent represents the content of a m.room.history_visibility state event.
+// https://spec.matrix.org/v1.2/client-server-api/#mroomhistory_visibility
+type HistoryVisibilityEventContent struct {
+	HistoryVisibility HistoryVisibility `json:"history_visibility"`
+}
+
+// GuestAccess specifies whether or not guest accounts can join.
+// https://spec.matrix.org/v1.2/client-server-api/#mroomguest_access
+type GuestAccess string
+
+const (
+	GuestAccessCanJoin   GuestAccess = "can_join"
+	GuestAccessForbidden GuestAccess = "forbidden"
+)
+
+// GuestAccessEventContent represents the content of a m.room.guest_access state event.
+// https://spec.matrix.org/v1.2/client-server-api/#mroomguest_access
+type GuestAccessEventContent struct {
+	GuestAccess GuestAccess `json:"guest_access"`
+}
+
+type BridgeInfoSection struct {
+	ID          string              `json:"id"`
+	DisplayName string              `json:"displayname,omitempty"`
+	AvatarURL   id.ContentURIString `json:"avatar_url,omitempty"`
+	ExternalURL string              `json:"external_url,omitempty"`
+
+	Receiver string `json:"fi.mau.receiver,omitempty"`
+}
+
+// BridgeEventContent represents the content of a m.bridge state event.
+// https://github.com/matrix-org/matrix-doc/pull/2346
+type BridgeEventContent struct {
+	BridgeBot id.UserID          `json:"bridgebot"`
+	Creator   id.UserID          `json:"creator,omitempty"`
+	Protocol  BridgeInfoSection  `json:"protocol"`
+	Network   *BridgeInfoSection `json:"network,omitempty"`
+	Channel   BridgeInfoSection  `json:"channel"`
+
+	BeeperRoomType   string `json:"com.beeper.room_type,omitempty"`
+	BeeperRoomTypeV2 string `json:"com.beeper.room_type.v2,omitempty"`
+}
+
+type SpaceChildEventContent struct {
+	Via       []string `json:"via,omitempty"`
+	Order     string   `json:"order,omitempty"`
+	Suggested bool     `json:"suggested,omitempty"`
+}
+
+type SpaceParentEventContent struct {
+	Via       []string `json:"via,omitempty"`
+	Canonical bool     `json:"canonical,omitempty"`
+}
+
+type PolicyRecommendation string
+
+const (
+	PolicyRecommendationBan         PolicyRecommendation = "m.ban"
+	PolicyRecommendationUnstableBan PolicyRecommendation = "org.matrix.mjolnir.ban"
+	PolicyRecommendationUnban       PolicyRecommendation = "fi.mau.meowlnir.unban"
+)
+
+// ModPolicyContent represents the content of a m.room.rule.user, m.room.rule.room, and m.room.rule.server state event.
+// https://spec.matrix.org/v1.2/client-server-api/#moderation-policy-lists
+type ModPolicyContent struct {
+	Entity         string               `json:"entity"`
+	Reason         string               `json:"reason"`
+	Recommendation PolicyRecommendation `json:"recommendation"`
+}
+
+// Deprecated: MSC2716 has been abandoned
+type InsertionMarkerContent struct {
+	InsertionID id.EventID `json:"org.matrix.msc2716.marker.insertion"`
+	Timestamp   int64      `json:"com.beeper.timestamp,omitempty"`
+}
+
+type ElementFunctionalMembersContent struct {
+	ServiceMembers []id.UserID `json:"service_members"`
+}
diff --git a/vendor/maunium.net/go/mautrix/event/type.go b/vendor/maunium.net/go/mautrix/event/type.go
new file mode 100644
index 0000000..4396c9c
--- /dev/null
+++ b/vendor/maunium.net/go/mautrix/event/type.go
@@ -0,0 +1,290 @@
+// 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 event
+
+import (
+	"encoding/json"
+	"fmt"
+	"strings"
+
+	"maunium.net/go/mautrix/id"
+)
+
+type RoomType string
+
+const (
+	RoomTypeDefault RoomType = ""
+	RoomTypeSpace   RoomType = "m.space"
+)
+
+type TypeClass int
+
+func (tc TypeClass) Name() string {
+	switch tc {
+	case MessageEventType:
+		return "message"
+	case StateEventType:
+		return "state"
+	case EphemeralEventType:
+		return "ephemeral"
+	case AccountDataEventType:
+		return "account data"
+	case ToDeviceEventType:
+		return "to-device"
+	default:
+		return "unknown"
+	}
+}
+
+const (
+	// Unknown events
+	UnknownEventType TypeClass = iota
+	// Normal message events
+	MessageEventType
+	// State events
+	StateEventType
+	// Ephemeral events
+	EphemeralEventType
+	// Account data events
+	AccountDataEventType
+	// Device-to-device events
+	ToDeviceEventType
+)
+
+type Type struct {
+	Type  string
+	Class TypeClass
+}
+
+func NewEventType(name string) Type {
+	evtType := Type{Type: name}
+	evtType.Class = evtType.GuessClass()
+	return evtType
+}
+
+func (et *Type) IsState() bool {
+	return et.Class == StateEventType
+}
+
+func (et *Type) IsEphemeral() bool {
+	return et.Class == EphemeralEventType
+}
+
+func (et *Type) IsAccountData() bool {
+	return et.Class == AccountDataEventType
+}
+
+func (et *Type) IsToDevice() bool {
+	return et.Class == ToDeviceEventType
+}
+
+func (et *Type) IsInRoomVerification() bool {
+	switch et.Type {
+	case InRoomVerificationStart.Type, InRoomVerificationReady.Type, InRoomVerificationAccept.Type,
+		InRoomVerificationKey.Type, InRoomVerificationMAC.Type, InRoomVerificationCancel.Type:
+		return true
+	default:
+		return false
+	}
+}
+
+func (et *Type) IsCall() bool {
+	switch et.Type {
+	case CallInvite.Type, CallCandidates.Type, CallAnswer.Type, CallReject.Type, CallSelectAnswer.Type,
+		CallNegotiate.Type, CallHangup.Type:
+		return true
+	default:
+		return false
+	}
+}
+
+func (et *Type) IsCustom() bool {
+	return !strings.HasPrefix(et.Type, "m.")
+}
+
+func (et *Type) GuessClass() TypeClass {
+	switch et.Type {
+	case StateAliases.Type, StateCanonicalAlias.Type, StateCreate.Type, StateJoinRules.Type, StateMember.Type,
+		StatePowerLevels.Type, StateRoomName.Type, StateRoomAvatar.Type, StateServerACL.Type, StateTopic.Type,
+		StatePinnedEvents.Type, StateTombstone.Type, StateEncryption.Type, StateBridge.Type, StateHalfShotBridge.Type,
+		StateSpaceParent.Type, StateSpaceChild.Type, StatePolicyRoom.Type, StatePolicyServer.Type, StatePolicyUser.Type,
+		StateInsertionMarker.Type, StateElementFunctionalMembers.Type:
+		return StateEventType
+	case EphemeralEventReceipt.Type, EphemeralEventTyping.Type, EphemeralEventPresence.Type:
+		return EphemeralEventType
+	case AccountDataDirectChats.Type, AccountDataPushRules.Type, AccountDataRoomTags.Type,
+		AccountDataFullyRead.Type, AccountDataIgnoredUserList.Type, AccountDataMarkedUnread.Type,
+		AccountDataSecretStorageKey.Type, AccountDataSecretStorageDefaultKey.Type,
+		AccountDataCrossSigningMaster.Type, AccountDataCrossSigningSelf.Type, AccountDataCrossSigningUser.Type,
+		AccountDataFullyRead.Type, AccountDataMegolmBackupKey.Type:
+		return AccountDataEventType
+	case EventRedaction.Type, EventMessage.Type, EventEncrypted.Type, EventReaction.Type, EventSticker.Type,
+		InRoomVerificationStart.Type, InRoomVerificationReady.Type, InRoomVerificationAccept.Type,
+		InRoomVerificationKey.Type, InRoomVerificationMAC.Type, InRoomVerificationCancel.Type,
+		CallInvite.Type, CallCandidates.Type, CallAnswer.Type, CallReject.Type, CallSelectAnswer.Type,
+		CallNegotiate.Type, CallHangup.Type, BeeperMessageStatus.Type, EventUnstablePollStart.Type, EventUnstablePollResponse.Type:
+		return MessageEventType
+	case ToDeviceRoomKey.Type, ToDeviceRoomKeyRequest.Type, ToDeviceForwardedRoomKey.Type, ToDeviceRoomKeyWithheld.Type,
+		ToDeviceBeeperRoomKeyAck.Type:
+		return ToDeviceEventType
+	default:
+		return UnknownEventType
+	}
+}
+
+func (et *Type) UnmarshalJSON(data []byte) error {
+	err := json.Unmarshal(data, &et.Type)
+	if err != nil {
+		return err
+	}
+	et.Class = et.GuessClass()
+	return nil
+}
+
+func (et *Type) MarshalJSON() ([]byte, error) {
+	return json.Marshal(&et.Type)
+}
+
+func (et Type) UnmarshalText(data []byte) error {
+	et.Type = string(data)
+	et.Class = et.GuessClass()
+	return nil
+}
+
+func (et Type) MarshalText() ([]byte, error) {
+	return []byte(et.Type), nil
+}
+
+func (et *Type) String() string {
+	return et.Type
+}
+
+func (et *Type) Repr() string {
+	return fmt.Sprintf("%s (%s)", et.Type, et.Class.Name())
+}
+
+// State events
+var (
+	StateAliases           = Type{"m.room.aliases", StateEventType}
+	StateCanonicalAlias    = Type{"m.room.canonical_alias", StateEventType}
+	StateCreate            = Type{"m.room.create", StateEventType}
+	StateJoinRules         = Type{"m.room.join_rules", StateEventType}
+	StateHistoryVisibility = Type{"m.room.history_visibility", StateEventType}
+	StateGuestAccess       = Type{"m.room.guest_access", StateEventType}
+	StateMember            = Type{"m.room.member", StateEventType}
+	StatePowerLevels       = Type{"m.room.power_levels", StateEventType}
+	StateRoomName          = Type{"m.room.name", StateEventType}
+	StateTopic             = Type{"m.room.topic", StateEventType}
+	StateRoomAvatar        = Type{"m.room.avatar", StateEventType}
+	StatePinnedEvents      = Type{"m.room.pinned_events", StateEventType}
+	StateServerACL         = Type{"m.room.server_acl", StateEventType}
+	StateTombstone         = Type{"m.room.tombstone", StateEventType}
+	StatePolicyRoom        = Type{"m.policy.rule.room", StateEventType}
+	StatePolicyServer      = Type{"m.policy.rule.server", StateEventType}
+	StatePolicyUser        = Type{"m.policy.rule.user", StateEventType}
+	StateEncryption        = Type{"m.room.encryption", StateEventType}
+	StateBridge            = Type{"m.bridge", StateEventType}
+	StateHalfShotBridge    = Type{"uk.half-shot.bridge", StateEventType}
+	StateSpaceChild        = Type{"m.space.child", StateEventType}
+	StateSpaceParent       = Type{"m.space.parent", StateEventType}
+
+	StateLegacyPolicyRoom     = Type{"m.room.rule.room", StateEventType}
+	StateLegacyPolicyServer   = Type{"m.room.rule.server", StateEventType}
+	StateLegacyPolicyUser     = Type{"m.room.rule.user", StateEventType}
+	StateUnstablePolicyRoom   = Type{"org.matrix.mjolnir.rule.room", StateEventType}
+	StateUnstablePolicyServer = Type{"org.matrix.mjolnir.rule.server", StateEventType}
+	StateUnstablePolicyUser   = Type{"org.matrix.mjolnir.rule.user", StateEventType}
+
+	// Deprecated: MSC2716 has been abandoned
+	StateInsertionMarker = Type{"org.matrix.msc2716.marker", StateEventType}
+
+	StateElementFunctionalMembers = Type{"io.element.functional_members", StateEventType}
+)
+
+// Message events
+var (
+	EventRedaction = Type{"m.room.redaction", MessageEventType}
+	EventMessage   = Type{"m.room.message", MessageEventType}
+	EventEncrypted = Type{"m.room.encrypted", MessageEventType}
+	EventReaction  = Type{"m.reaction", MessageEventType}
+	EventSticker   = Type{"m.sticker", MessageEventType}
+
+	InRoomVerificationReady  = Type{"m.key.verification.ready", MessageEventType}
+	InRoomVerificationStart  = Type{"m.key.verification.start", MessageEventType}
+	InRoomVerificationDone   = Type{"m.key.verification.done", MessageEventType}
+	InRoomVerificationCancel = Type{"m.key.verification.cancel", MessageEventType}
+
+	// SAS Verification Events
+	InRoomVerificationAccept = Type{"m.key.verification.accept", MessageEventType}
+	InRoomVerificationKey    = Type{"m.key.verification.key", MessageEventType}
+	InRoomVerificationMAC    = Type{"m.key.verification.mac", MessageEventType}
+
+	CallInvite       = Type{"m.call.invite", MessageEventType}
+	CallCandidates   = Type{"m.call.candidates", MessageEventType}
+	CallAnswer       = Type{"m.call.answer", MessageEventType}
+	CallReject       = Type{"m.call.reject", MessageEventType}
+	CallSelectAnswer = Type{"m.call.select_answer", MessageEventType}
+	CallNegotiate    = Type{"m.call.negotiate", MessageEventType}
+	CallHangup       = Type{"m.call.hangup", MessageEventType}
+
+	BeeperMessageStatus = Type{"com.beeper.message_send_status", MessageEventType}
+
+	EventUnstablePollStart    = Type{Type: "org.matrix.msc3381.poll.start", Class: MessageEventType}
+	EventUnstablePollResponse = Type{Type: "org.matrix.msc3381.poll.response", Class: MessageEventType}
+)
+
+// Ephemeral events
+var (
+	EphemeralEventReceipt  = Type{"m.receipt", EphemeralEventType}
+	EphemeralEventTyping   = Type{"m.typing", EphemeralEventType}
+	EphemeralEventPresence = Type{"m.presence", EphemeralEventType}
+)
+
+// Account data events
+var (
+	AccountDataDirectChats     = Type{"m.direct", AccountDataEventType}
+	AccountDataPushRules       = Type{"m.push_rules", AccountDataEventType}
+	AccountDataRoomTags        = Type{"m.tag", AccountDataEventType}
+	AccountDataFullyRead       = Type{"m.fully_read", AccountDataEventType}
+	AccountDataIgnoredUserList = Type{"m.ignored_user_list", AccountDataEventType}
+	AccountDataMarkedUnread    = Type{"m.marked_unread", AccountDataEventType}
+	AccountDataBeeperMute      = Type{"com.beeper.mute", AccountDataEventType}
+
+	AccountDataSecretStorageDefaultKey = Type{"m.secret_storage.default_key", AccountDataEventType}
+	AccountDataSecretStorageKey        = Type{"m.secret_storage.key", AccountDataEventType}
+	AccountDataCrossSigningMaster      = Type{string(id.SecretXSMaster), AccountDataEventType}
+	AccountDataCrossSigningUser        = Type{string(id.SecretXSUserSigning), AccountDataEventType}
+	AccountDataCrossSigningSelf        = Type{string(id.SecretXSSelfSigning), AccountDataEventType}
+	AccountDataMegolmBackupKey         = Type{string(id.SecretMegolmBackupV1), AccountDataEventType}
+)
+
+// Device-to-device events
+var (
+	ToDeviceRoomKey          = Type{"m.room_key", ToDeviceEventType}
+	ToDeviceRoomKeyRequest   = Type{"m.room_key_request", ToDeviceEventType}
+	ToDeviceForwardedRoomKey = Type{"m.forwarded_room_key", ToDeviceEventType}
+	ToDeviceEncrypted        = Type{"m.room.encrypted", ToDeviceEventType}
+	ToDeviceRoomKeyWithheld  = Type{"m.room_key.withheld", ToDeviceEventType}
+	ToDeviceSecretRequest    = Type{"m.secret.request", ToDeviceEventType}
+	ToDeviceSecretSend       = Type{"m.secret.send", ToDeviceEventType}
+	ToDeviceDummy            = Type{"m.dummy", ToDeviceEventType}
+
+	ToDeviceVerificationRequest = Type{"m.key.verification.request", ToDeviceEventType}
+	ToDeviceVerificationReady   = Type{"m.key.verification.ready", ToDeviceEventType}
+	ToDeviceVerificationStart   = Type{"m.key.verification.start", ToDeviceEventType}
+	ToDeviceVerificationDone    = Type{"m.key.verification.done", ToDeviceEventType}
+	ToDeviceVerificationCancel  = Type{"m.key.verification.cancel", ToDeviceEventType}
+
+	// SAS Verification Events
+	ToDeviceVerificationAccept = Type{"m.key.verification.accept", ToDeviceEventType}
+	ToDeviceVerificationKey    = Type{"m.key.verification.key", ToDeviceEventType}
+	ToDeviceVerificationMAC    = Type{"m.key.verification.mac", ToDeviceEventType}
+
+	ToDeviceOrgMatrixRoomKeyWithheld = Type{"org.matrix.room_key.withheld", ToDeviceEventType}
+
+	ToDeviceBeeperRoomKeyAck = Type{"com.beeper.room_key.ack", ToDeviceEventType}
+)
diff --git a/vendor/maunium.net/go/mautrix/event/verification.go b/vendor/maunium.net/go/mautrix/event/verification.go
new file mode 100644
index 0000000..6101896
--- /dev/null
+++ b/vendor/maunium.net/go/mautrix/event/verification.go
@@ -0,0 +1,308 @@
+// Copyright (c) 2020 Nikos Filippakis
+//
+// 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 event
+
+import (
+	"go.mau.fi/util/jsonbytes"
+	"go.mau.fi/util/jsontime"
+
+	"maunium.net/go/mautrix/id"
+)
+
+type VerificationMethod string
+
+const (
+	VerificationMethodSAS VerificationMethod = "m.sas.v1"
+
+	VerificationMethodReciprocate VerificationMethod = "m.reciprocate.v1"
+	VerificationMethodQRCodeShow  VerificationMethod = "m.qr_code.show.v1"
+	VerificationMethodQRCodeScan  VerificationMethod = "m.qr_code.scan.v1"
+)
+
+type VerificationTransactionable interface {
+	GetTransactionID() id.VerificationTransactionID
+	SetTransactionID(id.VerificationTransactionID)
+}
+
+// ToDeviceVerificationEvent contains the fields common to all to-device
+// verification events.
+type ToDeviceVerificationEvent struct {
+	// TransactionID is an opaque identifier for the verification request. Must
+	// be unique with respect to the devices involved.
+	TransactionID id.VerificationTransactionID `json:"transaction_id,omitempty"`
+}
+
+var _ VerificationTransactionable = (*ToDeviceVerificationEvent)(nil)
+
+func (ve *ToDeviceVerificationEvent) GetTransactionID() id.VerificationTransactionID {
+	return ve.TransactionID
+}
+
+func (ve *ToDeviceVerificationEvent) SetTransactionID(id id.VerificationTransactionID) {
+	ve.TransactionID = id
+}
+
+// InRoomVerificationEvent contains the fields common to all in-room
+// verification events.
+type InRoomVerificationEvent struct {
+	// RelatesTo indicates the m.key.verification.request that this message is
+	// related to. Note that for encrypted messages, this property should be in
+	// the unencrypted portion of the event.
+	RelatesTo *RelatesTo `json:"m.relates_to,omitempty"`
+}
+
+var _ Relatable = (*InRoomVerificationEvent)(nil)
+
+func (ve *InRoomVerificationEvent) GetRelatesTo() *RelatesTo {
+	if ve.RelatesTo == nil {
+		ve.RelatesTo = &RelatesTo{}
+	}
+	return ve.RelatesTo
+}
+
+func (ve *InRoomVerificationEvent) OptionalGetRelatesTo() *RelatesTo {
+	return ve.RelatesTo
+}
+
+func (ve *InRoomVerificationEvent) SetRelatesTo(rel *RelatesTo) {
+	ve.RelatesTo = rel
+}
+
+// VerificationRequestEventContent represents the content of an
+// [m.key.verification.request] to-device event as described in [Section
+// 11.12.2.1] of the Spec.
+//
+// For the in-room version, use a standard [MessageEventContent] struct.
+//
+// [m.key.verification.request]: https://spec.matrix.org/v1.9/client-server-api/#mkeyverificationrequest
+// [Section 11.12.2.1]: https://spec.matrix.org/v1.9/client-server-api/#key-verification-framework
+type VerificationRequestEventContent struct {
+	ToDeviceVerificationEvent
+	// FromDevice is the device ID which is initiating the request.
+	FromDevice id.DeviceID `json:"from_device"`
+	// Methods is a list of the verification methods supported by the sender.
+	Methods []VerificationMethod `json:"methods"`
+	// Timestamp is the time at which the request was made.
+	Timestamp jsontime.UnixMilli `json:"timestamp,omitempty"`
+}
+
+// VerificationRequestEventContentFromMessage converts an in-room verification
+// request message event to a [VerificationRequestEventContent].
+func VerificationRequestEventContentFromMessage(evt *Event) *VerificationRequestEventContent {
+	content := evt.Content.AsMessage()
+	return &VerificationRequestEventContent{
+		ToDeviceVerificationEvent: ToDeviceVerificationEvent{
+			TransactionID: id.VerificationTransactionID(evt.ID),
+		},
+		Timestamp:  jsontime.UMInt(evt.Timestamp),
+		FromDevice: content.FromDevice,
+		Methods:    content.Methods,
+	}
+}
+
+// VerificationReadyEventContent represents the content of an
+// [m.key.verification.ready] event (both the to-device and the in-room
+// version) as described in [Section 11.12.2.1] of the Spec.
+//
+// [m.key.verification.ready]: https://spec.matrix.org/v1.9/client-server-api/#mkeyverificationready
+// [Section 11.12.2.1]: https://spec.matrix.org/v1.9/client-server-api/#key-verification-framework
+type VerificationReadyEventContent struct {
+	ToDeviceVerificationEvent
+	InRoomVerificationEvent
+
+	// FromDevice is the device ID which is initiating the request.
+	FromDevice id.DeviceID `json:"from_device"`
+	// Methods is a list of the verification methods supported by the sender.
+	Methods []VerificationMethod `json:"methods"`
+}
+
+type KeyAgreementProtocol string
+
+const (
+	KeyAgreementProtocolCurve25519           KeyAgreementProtocol = "curve25519"
+	KeyAgreementProtocolCurve25519HKDFSHA256 KeyAgreementProtocol = "curve25519-hkdf-sha256"
+)
+
+type VerificationHashMethod string
+
+const VerificationHashMethodSHA256 VerificationHashMethod = "sha256"
+
+type MACMethod string
+
+const (
+	MACMethodHKDFHMACSHA256   MACMethod = "hkdf-hmac-sha256"
+	MACMethodHKDFHMACSHA256V2 MACMethod = "hkdf-hmac-sha256.v2"
+)
+
+type SASMethod string
+
+const (
+	SASMethodDecimal SASMethod = "decimal"
+	SASMethodEmoji   SASMethod = "emoji"
+)
+
+// VerificationStartEventContent represents the content of an
+// [m.key.verification.start] event (both the to-device and the in-room
+// version) as described in [Section 11.12.2.1] of the Spec.
+//
+// This struct also contains the fields for an [m.key.verification.start] event
+// using the [VerificationMethodSAS] method as described in [Section
+// 11.12.2.2.2] and an [m.key.verification.start] using
+// [VerificationMethodReciprocate] as described in [Section 11.12.2.4.2].
+//
+// [m.key.verification.start]: https://spec.matrix.org/v1.9/client-server-api/#mkeyverificationstart
+// [Section 11.12.2.1]: https://spec.matrix.org/v1.9/client-server-api/#key-verification-framework
+// [Section 11.12.2.2.2]: https://spec.matrix.org/v1.9/client-server-api/#verification-messages-specific-to-sas
+// [Section 11.12.2.4.2]: https://spec.matrix.org/v1.9/client-server-api/#verification-messages-specific-to-qr-codes
+type VerificationStartEventContent struct {
+	ToDeviceVerificationEvent
+	InRoomVerificationEvent
+
+	// FromDevice is the device ID which is initiating the request.
+	FromDevice id.DeviceID `json:"from_device"`
+	// Method is the verification method to use.
+	Method VerificationMethod `json:"method"`
+	// NextMethod is an optional method to use to verify the other user's key.
+	// Applicable when the method chosen only verifies one user’s key. This
+	// field will never be present if the method verifies keys both ways.
+	NextMethod VerificationMethod `json:"next_method,omitempty"`
+
+	// Hashes are the hash methods the sending device understands. This field
+	// is only applicable when the method is m.sas.v1.
+	Hashes []VerificationHashMethod `json:"hashes,omitempty"`
+	// KeyAgreementProtocols is the list of key agreement protocols the sending
+	// device understands. This field is only applicable when the method is
+	// m.sas.v1.
+	KeyAgreementProtocols []KeyAgreementProtocol `json:"key_agreement_protocols,omitempty"`
+	// MessageAuthenticationCodes is a list of the MAC methods that the sending
+	// device understands. This field is only applicable when the method is
+	// m.sas.v1.
+	MessageAuthenticationCodes []MACMethod `json:"message_authentication_codes"`
+	// ShortAuthenticationString is a list of SAS methods the sending device
+	// (and the sending device's user) understands. This field is only
+	// applicable when the method is m.sas.v1.
+	ShortAuthenticationString []SASMethod `json:"short_authentication_string"`
+
+	// Secret is the shared secret from the QR code. This field is only
+	// applicable when the method is m.reciprocate.v1.
+	Secret jsonbytes.UnpaddedBytes `json:"secret,omitempty"`
+}
+
+// VerificationDoneEventContent represents the content of an
+// [m.key.verification.done] event (both the to-device and the in-room version)
+// as described in [Section 11.12.2.1] of the Spec.
+//
+// This type is an alias for [VerificationRelatable] since there are no
+// additional fields defined by the spec.
+//
+// [m.key.verification.done]: https://spec.matrix.org/v1.9/client-server-api/#mkeyverificationdone
+// [Section 11.12.2.1]: https://spec.matrix.org/v1.9/client-server-api/#mkeyverificationdone
+type VerificationDoneEventContent struct {
+	ToDeviceVerificationEvent
+	InRoomVerificationEvent
+}
+
+type VerificationCancelCode string
+
+const (
+	VerificationCancelCodeUser               VerificationCancelCode = "m.user"
+	VerificationCancelCodeTimeout            VerificationCancelCode = "m.timeout"
+	VerificationCancelCodeUnknownTransaction VerificationCancelCode = "m.unknown_transaction"
+	VerificationCancelCodeUnknownMethod      VerificationCancelCode = "m.unknown_method"
+	VerificationCancelCodeUnexpectedMessage  VerificationCancelCode = "m.unexpected_message"
+	VerificationCancelCodeKeyMismatch        VerificationCancelCode = "m.key_mismatch"
+	VerificationCancelCodeUserMismatch       VerificationCancelCode = "m.user_mismatch"
+	VerificationCancelCodeInvalidMessage     VerificationCancelCode = "m.invalid_message"
+	VerificationCancelCodeAccepted           VerificationCancelCode = "m.accepted"
+	VerificationCancelCodeSASMismatch        VerificationCancelCode = "m.mismatched_sas"
+	VerificationCancelCodeCommitmentMismatch VerificationCancelCode = "m.mismatched_commitment"
+
+	// Non-spec codes
+	VerificationCancelCodeInternalError       VerificationCancelCode = "com.beeper.internal_error"
+	VerificationCancelCodeMasterKeyNotTrusted VerificationCancelCode = "com.beeper.master_key_not_trusted" // the master key is not trusted by this device, but the QR code that was scanned was from a device that doesn't trust the master key
+)
+
+// VerificationCancelEventContent represents the content of an
+// [m.key.verification.cancel] event (both the to-device and the in-room
+// version) as described in [Section 11.12.2.1] of the Spec.
+//
+// [m.key.verification.cancel]: https://spec.matrix.org/v1.9/client-server-api/#mkeyverificationcancel
+// [Section 11.12.2.1]: https://spec.matrix.org/v1.9/client-server-api/#mkeyverificationdone
+type VerificationCancelEventContent struct {
+	ToDeviceVerificationEvent
+	InRoomVerificationEvent
+
+	// Code is the error code for why the process/request was cancelled by the
+	// user.
+	Code VerificationCancelCode `json:"code"`
+	// Reason is a human readable description of the code. The client should
+	// only rely on this string if it does not understand the code.
+	Reason string `json:"reason"`
+}
+
+// VerificationAcceptEventContent represents the content of an
+// [m.key.verification.accept] event (both the to-device and the in-room
+// version) as described in [Section 11.12.2.2.2] of the Spec.
+//
+// [m.key.verification.accept]: https://spec.matrix.org/v1.9/client-server-api/#mkeyverificationaccept
+// [Section 11.12.2.2.2]: https://spec.matrix.org/v1.9/client-server-api/#verification-messages-specific-to-sas
+type VerificationAcceptEventContent struct {
+	ToDeviceVerificationEvent
+	InRoomVerificationEvent
+
+	// Commitment is the hash of the concatenation of the device's ephemeral
+	// public key (encoded as unpadded base64) and the canonical JSON
+	// representation of the m.key.verification.start message.
+	Commitment jsonbytes.UnpaddedBytes `json:"commitment"`
+	// Hash is the hash method the device is choosing to use, out of the
+	// options in the m.key.verification.start message.
+	Hash VerificationHashMethod `json:"hash"`
+	// KeyAgreementProtocol is the key agreement protocol the device is
+	// choosing to use, out of the options in the m.key.verification.start
+	// message.
+	KeyAgreementProtocol KeyAgreementProtocol `json:"key_agreement_protocol"`
+	// MessageAuthenticationCode is the message authentication code the device
+	// is choosing to use, out of the options in the m.key.verification.start
+	// message.
+	MessageAuthenticationCode MACMethod `json:"message_authentication_code"`
+	// ShortAuthenticationString is a list of SAS methods both devices involved
+	// in the verification process understand. Must be a subset of the options
+	// in the m.key.verification.start message.
+	ShortAuthenticationString []SASMethod `json:"short_authentication_string"`
+}
+
+// VerificationKeyEventContent represents the content of an
+// [m.key.verification.key] event (both the to-device and the in-room version)
+// as described in [Section 11.12.2.2.2] of the Spec.
+//
+// [m.key.verification.key]: https://spec.matrix.org/v1.9/client-server-api/#mkeyverificationkey
+// [Section 11.12.2.2.2]: https://spec.matrix.org/v1.9/client-server-api/#verification-messages-specific-to-sas
+type VerificationKeyEventContent struct {
+	ToDeviceVerificationEvent
+	InRoomVerificationEvent
+
+	// Key is the device’s ephemeral public key.
+	Key jsonbytes.UnpaddedBytes `json:"key"`
+}
+
+// VerificationMACEventContent represents the content of an
+// [m.key.verification.mac] event (both the to-device and the in-room version)
+// as described in [Section 11.12.2.2.2] of the Spec.
+//
+// [m.key.verification.mac]: https://spec.matrix.org/v1.9/client-server-api/#mkeyverificationmac
+// [Section 11.12.2.2.2]: https://spec.matrix.org/v1.9/client-server-api/#verification-messages-specific-to-sas
+type VerificationMACEventContent struct {
+	ToDeviceVerificationEvent
+	InRoomVerificationEvent
+
+	// Keys is the MAC of the comma-separated, sorted, list of key IDs given in
+	// the MAC property.
+	Keys jsonbytes.UnpaddedBytes `json:"keys"`
+	// MAC is a map of the key ID to the MAC of the key, using the algorithm in
+	// the verification process.
+	MAC map[id.KeyID]jsonbytes.UnpaddedBytes `json:"mac"`
+}
diff --git a/vendor/maunium.net/go/mautrix/event/voip.go b/vendor/maunium.net/go/mautrix/event/voip.go
new file mode 100644
index 0000000..28f56c9
--- /dev/null
+++ b/vendor/maunium.net/go/mautrix/event/voip.go
@@ -0,0 +1,116 @@
+// 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 event
+
+import (
+	"encoding/json"
+	"fmt"
+	"strconv"
+)
+
+type CallHangupReason string
+
+const (
+	CallHangupICEFailed       CallHangupReason = "ice_failed"
+	CallHangupInviteTimeout   CallHangupReason = "invite_timeout"
+	CallHangupUserHangup      CallHangupReason = "user_hangup"
+	CallHangupUserMediaFailed CallHangupReason = "user_media_failed"
+	CallHangupUnknownError    CallHangupReason = "unknown_error"
+)
+
+type CallDataType string
+
+const (
+	CallDataTypeOffer  CallDataType = "offer"
+	CallDataTypeAnswer CallDataType = "answer"
+)
+
+type CallData struct {
+	SDP  string       `json:"sdp"`
+	Type CallDataType `json:"type"`
+}
+
+type CallCandidate struct {
+	Candidate     string `json:"candidate"`
+	SDPMLineIndex int    `json:"sdpMLineIndex"`
+	SDPMID        string `json:"sdpMid"`
+}
+
+type CallVersion string
+
+func (cv *CallVersion) UnmarshalJSON(raw []byte) error {
+	var numberVersion int
+	err := json.Unmarshal(raw, &numberVersion)
+	if err != nil {
+		var stringVersion string
+		err = json.Unmarshal(raw, &stringVersion)
+		if err != nil {
+			return fmt.Errorf("failed to parse CallVersion: %w", err)
+		}
+		*cv = CallVersion(stringVersion)
+	} else {
+		*cv = CallVersion(strconv.Itoa(numberVersion))
+	}
+	return nil
+}
+
+func (cv *CallVersion) MarshalJSON() ([]byte, error) {
+	for _, char := range *cv {
+		if char < '0' || char > '9' {
+			// The version contains weird characters, return as string.
+			return json.Marshal(string(*cv))
+		}
+	}
+	// The version consists of only ASCII digits, return as an integer.
+	return []byte(*cv), nil
+}
+
+func (cv *CallVersion) Int() (int, error) {
+	return strconv.Atoi(string(*cv))
+}
+
+type BaseCallEventContent struct {
+	CallID  string      `json:"call_id"`
+	PartyID string      `json:"party_id"`
+	Version CallVersion `json:"version"`
+}
+
+type CallInviteEventContent struct {
+	BaseCallEventContent
+	Lifetime int      `json:"lifetime"`
+	Offer    CallData `json:"offer"`
+}
+
+type CallCandidatesEventContent struct {
+	BaseCallEventContent
+	Candidates []CallCandidate `json:"candidates"`
+}
+
+type CallRejectEventContent struct {
+	BaseCallEventContent
+}
+
+type CallAnswerEventContent struct {
+	BaseCallEventContent
+	Answer CallData `json:"answer"`
+}
+
+type CallSelectAnswerEventContent struct {
+	BaseCallEventContent
+	SelectedPartyID string `json:"selected_party_id"`
+}
+
+type CallNegotiateEventContent struct {
+	BaseCallEventContent
+	Lifetime    int      `json:"lifetime"`
+	Description CallData `json:"description"`
+}
+
+type CallHangupEventContent struct {
+	BaseCallEventContent
+	Reason CallHangupReason `json:"reason"`
+}
diff --git a/vendor/maunium.net/go/mautrix/filter.go b/vendor/maunium.net/go/mautrix/filter.go
new file mode 100644
index 0000000..2603bfb
--- /dev/null
+++ b/vendor/maunium.net/go/mautrix/filter.go
@@ -0,0 +1,95 @@
+// Copyright 2017 Jan Christian Grünhage
+
+package mautrix
+
+import (
+	"errors"
+
+	"maunium.net/go/mautrix/event"
+	"maunium.net/go/mautrix/id"
+)
+
+type EventFormat string
+
+const (
+	EventFormatClient     EventFormat = "client"
+	EventFormatFederation EventFormat = "federation"
+)
+
+// Filter is used by clients to specify how the server should filter responses to e.g. sync requests
+// Specified by: https://spec.matrix.org/v1.2/client-server-api/#filtering
+type Filter struct {
+	AccountData FilterPart  `json:"account_data,omitempty"`
+	EventFields []string    `json:"event_fields,omitempty"`
+	EventFormat EventFormat `json:"event_format,omitempty"`
+	Presence    FilterPart  `json:"presence,omitempty"`
+	Room        RoomFilter  `json:"room,omitempty"`
+
+	BeeperToDevice *FilterPart `json:"com.beeper.to_device,omitempty"`
+}
+
+// RoomFilter is used to define filtering rules for room events
+type RoomFilter struct {
+	AccountData  FilterPart  `json:"account_data,omitempty"`
+	Ephemeral    FilterPart  `json:"ephemeral,omitempty"`
+	IncludeLeave bool        `json:"include_leave,omitempty"`
+	NotRooms     []id.RoomID `json:"not_rooms,omitempty"`
+	Rooms        []id.RoomID `json:"rooms,omitempty"`
+	State        FilterPart  `json:"state,omitempty"`
+	Timeline     FilterPart  `json:"timeline,omitempty"`
+}
+
+// FilterPart is used to define filtering rules for specific categories of events
+type FilterPart struct {
+	NotRooms    []id.RoomID  `json:"not_rooms,omitempty"`
+	Rooms       []id.RoomID  `json:"rooms,omitempty"`
+	Limit       int          `json:"limit,omitempty"`
+	NotSenders  []id.UserID  `json:"not_senders,omitempty"`
+	NotTypes    []event.Type `json:"not_types,omitempty"`
+	Senders     []id.UserID  `json:"senders,omitempty"`
+	Types       []event.Type `json:"types,omitempty"`
+	ContainsURL *bool        `json:"contains_url,omitempty"`
+
+	LazyLoadMembers         bool `json:"lazy_load_members,omitempty"`
+	IncludeRedundantMembers bool `json:"include_redundant_members,omitempty"`
+}
+
+// Validate checks if the filter contains valid property values
+func (filter *Filter) Validate() error {
+	if filter.EventFormat != EventFormatClient && filter.EventFormat != EventFormatFederation {
+		return errors.New("Bad event_format value. Must be one of [\"client\", \"federation\"]")
+	}
+	return nil
+}
+
+// DefaultFilter returns the default filter used by the Matrix server if no filter is provided in the request
+func DefaultFilter() Filter {
+	return Filter{
+		AccountData: DefaultFilterPart(),
+		EventFields: nil,
+		EventFormat: "client",
+		Presence:    DefaultFilterPart(),
+		Room: RoomFilter{
+			AccountData:  DefaultFilterPart(),
+			Ephemeral:    DefaultFilterPart(),
+			IncludeLeave: false,
+			NotRooms:     nil,
+			Rooms:        nil,
+			State:        DefaultFilterPart(),
+			Timeline:     DefaultFilterPart(),
+		},
+	}
+}
+
+// DefaultFilterPart returns the default filter part used by the Matrix server if no filter is provided in the request
+func DefaultFilterPart() FilterPart {
+	return FilterPart{
+		NotRooms:   nil,
+		Rooms:      nil,
+		Limit:      20,
+		NotSenders: nil,
+		NotTypes:   nil,
+		Senders:    nil,
+		Types:      nil,
+	}
+}
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
+}
diff --git a/vendor/maunium.net/go/mautrix/pushrules/action.go b/vendor/maunium.net/go/mautrix/pushrules/action.go
new file mode 100644
index 0000000..9838e88
--- /dev/null
+++ b/vendor/maunium.net/go/mautrix/pushrules/action.go
@@ -0,0 +1,125 @@
+// 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 pushrules
+
+import "encoding/json"
+
+// PushActionType is the type of a PushAction
+type PushActionType string
+
+// The allowed push action types as specified in spec section 11.12.1.4.1.
+const (
+	ActionNotify     PushActionType = "notify"
+	ActionDontNotify PushActionType = "dont_notify"
+	ActionCoalesce   PushActionType = "coalesce"
+	ActionSetTweak   PushActionType = "set_tweak"
+)
+
+// PushActionTweak is the type of the tweak in SetTweak push actions.
+type PushActionTweak string
+
+// The allowed tweak types as specified in spec section 11.12.1.4.1.1.
+const (
+	TweakSound     PushActionTweak = "sound"
+	TweakHighlight PushActionTweak = "highlight"
+)
+
+// PushActionArray is an array of PushActions.
+type PushActionArray []*PushAction
+
+// PushActionArrayShould contains the important information parsed from a PushActionArray.
+type PushActionArrayShould struct {
+	// Whether the array contained a Notify, DontNotify or Coalesce action type.
+	// Deprecated: an empty array should be treated as no notification, so there's no reason to check this field.
+	NotifySpecified bool
+	// Whether the event in question should trigger a notification.
+	Notify bool
+	// Whether the event in question should be highlighted.
+	Highlight bool
+
+	// Whether the event in question should trigger a sound alert.
+	PlaySound bool
+	// The name of the sound to play if PlaySound is true.
+	SoundName string
+}
+
+// Should parses this push action array and returns the relevant details wrapped in a PushActionArrayShould struct.
+func (actions PushActionArray) Should() (should PushActionArrayShould) {
+	for _, action := range actions {
+		switch action.Action {
+		case ActionNotify, ActionCoalesce:
+			should.Notify = true
+			should.NotifySpecified = true
+		case ActionDontNotify:
+			should.Notify = false
+			should.NotifySpecified = true
+		case ActionSetTweak:
+			switch action.Tweak {
+			case TweakHighlight:
+				var ok bool
+				should.Highlight, ok = action.Value.(bool)
+				if !ok {
+					// Highlight value not specified, so assume true since the tweak is set.
+					should.Highlight = true
+				}
+			case TweakSound:
+				should.SoundName = action.Value.(string)
+				should.PlaySound = len(should.SoundName) > 0
+			}
+		}
+	}
+	return
+}
+
+// PushAction is a single action that should be triggered when receiving a message.
+type PushAction struct {
+	Action PushActionType
+	Tweak  PushActionTweak
+	Value  interface{}
+}
+
+// UnmarshalJSON parses JSON into this PushAction.
+//
+//   - If the JSON is a single string, the value is stored in the Action field.
+//   - If the JSON is an object with the set_tweak field, Action will be set to
+//     "set_tweak", Tweak will be set to the value of the set_tweak field and
+//     and Value will be set to the value of the value field.
+//   - In any other case, the function does nothing.
+func (action *PushAction) UnmarshalJSON(raw []byte) error {
+	var data interface{}
+
+	err := json.Unmarshal(raw, &data)
+	if err != nil {
+		return err
+	}
+
+	switch val := data.(type) {
+	case string:
+		action.Action = PushActionType(val)
+	case map[string]interface{}:
+		tweak, ok := val["set_tweak"].(string)
+		if ok {
+			action.Action = ActionSetTweak
+			action.Tweak = PushActionTweak(tweak)
+			action.Value, _ = val["value"]
+		}
+	}
+	return nil
+}
+
+// MarshalJSON is the reverse of UnmarshalJSON()
+func (action *PushAction) MarshalJSON() (raw []byte, err error) {
+	if action.Action == ActionSetTweak {
+		data := map[string]interface{}{
+			"set_tweak": action.Tweak,
+			"value":     action.Value,
+		}
+		return json.Marshal(&data)
+	}
+	data := string(action.Action)
+	return json.Marshal(&data)
+}
diff --git a/vendor/maunium.net/go/mautrix/pushrules/condition.go b/vendor/maunium.net/go/mautrix/pushrules/condition.go
new file mode 100644
index 0000000..dbe83a6
--- /dev/null
+++ b/vendor/maunium.net/go/mautrix/pushrules/condition.go
@@ -0,0 +1,336 @@
+// Copyright (c) 2023 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 pushrules
+
+import (
+	"encoding/json"
+	"fmt"
+	"regexp"
+	"strconv"
+	"strings"
+	"unicode"
+
+	"github.com/tidwall/gjson"
+	"go.mau.fi/util/glob"
+
+	"maunium.net/go/mautrix/event"
+	"maunium.net/go/mautrix/id"
+)
+
+// Room is an interface with the functions that are needed for processing room-specific push conditions
+type Room interface {
+	GetOwnDisplayname() string
+	GetMemberCount() int
+}
+
+// EventfulRoom is an extension of Room to support MSC3664.
+type EventfulRoom interface {
+	Room
+	GetEvent(id.EventID) *event.Event
+}
+
+// PushCondKind is the type of a push condition.
+type PushCondKind string
+
+// The allowed push condition kinds as specified in https://spec.matrix.org/v1.2/client-server-api/#conditions-1
+const (
+	KindEventMatch            PushCondKind = "event_match"
+	KindContainsDisplayName   PushCondKind = "contains_display_name"
+	KindRoomMemberCount       PushCondKind = "room_member_count"
+	KindEventPropertyIs       PushCondKind = "event_property_is"
+	KindEventPropertyContains PushCondKind = "event_property_contains"
+
+	// MSC3664: https://github.com/matrix-org/matrix-spec-proposals/pull/3664
+
+	KindRelatedEventMatch         PushCondKind = "related_event_match"
+	KindUnstableRelatedEventMatch PushCondKind = "im.nheko.msc3664.related_event_match"
+)
+
+// PushCondition wraps a condition that is required for a specific PushRule to be used.
+type PushCondition struct {
+	// The type of the condition.
+	Kind PushCondKind `json:"kind"`
+	// The dot-separated field of the event to match. Only applicable if kind is EventMatch.
+	Key string `json:"key,omitempty"`
+	// The glob-style pattern to match the field against. Only applicable if kind is EventMatch.
+	Pattern string `json:"pattern,omitempty"`
+	// The exact value to match the field against. Only applicable if kind is EventPropertyIs or EventPropertyContains.
+	Value any `json:"value,omitempty"`
+	// The condition that needs to be fulfilled for RoomMemberCount-type conditions.
+	// A decimal integer optionally prefixed by ==, <, >, >= or <=. Prefix "==" is assumed if no prefix found.
+	MemberCountCondition string `json:"is,omitempty"`
+
+	// The relation type for related_event_match from MSC3664
+	RelType event.RelationType `json:"rel_type,omitempty"`
+}
+
+// MemberCountFilterRegex is the regular expression to parse the MemberCountCondition of PushConditions.
+var MemberCountFilterRegex = regexp.MustCompile("^(==|[<>]=?)?([0-9]+)$")
+
+// Match checks if this condition is fulfilled for the given event in the given room.
+func (cond *PushCondition) Match(room Room, evt *event.Event) bool {
+	switch cond.Kind {
+	case KindEventMatch, KindEventPropertyIs, KindEventPropertyContains:
+		return cond.matchValue(evt)
+	case KindRelatedEventMatch, KindUnstableRelatedEventMatch:
+		return cond.matchRelatedEvent(room, evt)
+	case KindContainsDisplayName:
+		return cond.matchDisplayName(room, evt)
+	case KindRoomMemberCount:
+		return cond.matchMemberCount(room)
+	default:
+		return false
+	}
+}
+
+func splitWithEscaping(s string, separator, escape byte) []string {
+	var token []byte
+	var tokens []string
+	for i := 0; i < len(s); i++ {
+		if s[i] == separator {
+			tokens = append(tokens, string(token))
+			token = token[:0]
+		} else if s[i] == escape && i+1 < len(s) {
+			i++
+			token = append(token, s[i])
+		} else {
+			token = append(token, s[i])
+		}
+	}
+	tokens = append(tokens, string(token))
+	return tokens
+}
+
+func hackyNestedGet(data map[string]any, path []string) (any, bool) {
+	val, ok := data[path[0]]
+	if len(path) == 1 {
+		// We don't have any more path parts, return the value regardless of whether it exists or not.
+		return val, ok
+	} else if ok {
+		if mapVal, ok := val.(map[string]any); ok {
+			val, ok = hackyNestedGet(mapVal, path[1:])
+			if ok {
+				return val, true
+			}
+		}
+	}
+	// If we don't find the key, try to combine the first two parts.
+	// e.g. if the key is content.m.relates_to.rel_type, we'll first try data["m"], which will fail,
+	//      then combine m and relates_to to get data["m.relates_to"], which should succeed.
+	path[1] = path[0] + "." + path[1]
+	return hackyNestedGet(data, path[1:])
+}
+
+func stringifyForPushCondition(val interface{}) string {
+	// Implement MSC3862 to allow matching any type of field
+	// https://github.com/matrix-org/matrix-spec-proposals/pull/3862
+	switch typedVal := val.(type) {
+	case string:
+		return typedVal
+	case nil:
+		return "null"
+	case float64:
+		// Floats aren't allowed in Matrix events, but the JSON parser always stores numbers as floats,
+		// so just handle that and convert to int
+		return strconv.FormatInt(int64(typedVal), 10)
+	default:
+		return fmt.Sprint(val)
+	}
+}
+
+func (cond *PushCondition) getValue(evt *event.Event) (any, bool) {
+	key, subkey, _ := strings.Cut(cond.Key, ".")
+
+	switch key {
+	case "type":
+		return evt.Type.Type, true
+	case "sender":
+		return evt.Sender.String(), true
+	case "room_id":
+		return evt.RoomID.String(), true
+	case "state_key":
+		if evt.StateKey == nil {
+			return nil, false
+		}
+		return *evt.StateKey, true
+	case "content":
+		// Split the match key with escaping to implement https://github.com/matrix-org/matrix-spec-proposals/pull/3873
+		splitKey := splitWithEscaping(subkey, '.', '\\')
+		// Then do a hacky nested get that supports combining parts for the backwards-compat part of MSC3873
+		return hackyNestedGet(evt.Content.Raw, splitKey)
+	default:
+		return nil, false
+	}
+}
+
+func numberToInt64(a any) int64 {
+	switch typed := a.(type) {
+	case float64:
+		return int64(typed)
+	case float32:
+		return int64(typed)
+	case int:
+		return int64(typed)
+	case int8:
+		return int64(typed)
+	case int16:
+		return int64(typed)
+	case int32:
+		return int64(typed)
+	case int64:
+		return typed
+	case uint:
+		return int64(typed)
+	case uint8:
+		return int64(typed)
+	case uint16:
+		return int64(typed)
+	case uint32:
+		return int64(typed)
+	case uint64:
+		return int64(typed)
+	default:
+		return 0
+	}
+}
+
+func valueEquals(a, b any) bool {
+	// Convert floats to ints when comparing numbers (the JSON parser generates floats, but Matrix only allows integers)
+	// Also allow other numeric types in case something generates events manually without json
+	switch a.(type) {
+	case float64, float32, int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64:
+		switch b.(type) {
+		case float64, float32, int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64:
+			return numberToInt64(a) == numberToInt64(b)
+		}
+	}
+	return a == b
+}
+
+func (cond *PushCondition) matchValue(evt *event.Event) bool {
+	val, ok := cond.getValue(evt)
+	if !ok {
+		return false
+	}
+
+	switch cond.Kind {
+	case KindEventMatch, KindRelatedEventMatch, KindUnstableRelatedEventMatch:
+		pattern := glob.CompileWithImplicitContains(cond.Pattern)
+		if pattern == nil {
+			return false
+		}
+		return pattern.Match(stringifyForPushCondition(val))
+	case KindEventPropertyIs:
+		return valueEquals(val, cond.Value)
+	case KindEventPropertyContains:
+		valArr, ok := val.([]any)
+		if !ok {
+			return false
+		}
+		for _, item := range valArr {
+			if valueEquals(item, cond.Value) {
+				return true
+			}
+		}
+		return false
+	default:
+		panic(fmt.Errorf("matchValue called for unknown condition kind %s", cond.Kind))
+	}
+}
+
+func (cond *PushCondition) getRelationEventID(relatesTo *event.RelatesTo) id.EventID {
+	if relatesTo == nil {
+		return ""
+	}
+	switch cond.RelType {
+	case "":
+		return relatesTo.EventID
+	case "m.in_reply_to":
+		if relatesTo.IsFallingBack || relatesTo.InReplyTo == nil {
+			return ""
+		}
+		return relatesTo.InReplyTo.EventID
+	default:
+		if relatesTo.Type != cond.RelType {
+			return ""
+		}
+		return relatesTo.EventID
+	}
+}
+
+func (cond *PushCondition) matchRelatedEvent(room Room, evt *event.Event) bool {
+	var relatesTo *event.RelatesTo
+	if relatable, ok := evt.Content.Parsed.(event.Relatable); ok {
+		relatesTo = relatable.OptionalGetRelatesTo()
+	} else {
+		res := gjson.GetBytes(evt.Content.VeryRaw, `m\.relates_to`)
+		if res.Exists() && res.IsObject() {
+			_ = json.Unmarshal([]byte(res.Raw), &relatesTo)
+		}
+	}
+	if evtID := cond.getRelationEventID(relatesTo); evtID == "" {
+		return false
+	} else if eventfulRoom, ok := room.(EventfulRoom); !ok {
+		return false
+	} else if evt = eventfulRoom.GetEvent(relatesTo.EventID); evt == nil {
+		return false
+	} else {
+		return cond.matchValue(evt)
+	}
+}
+
+func (cond *PushCondition) matchDisplayName(room Room, evt *event.Event) bool {
+	displayname := room.GetOwnDisplayname()
+	if len(displayname) == 0 {
+		return false
+	}
+
+	msg, ok := evt.Content.Raw["body"].(string)
+	if !ok {
+		return false
+	}
+
+	isAcceptable := func(r uint8) bool {
+		return unicode.IsSpace(rune(r)) || unicode.IsPunct(rune(r))
+	}
+	length := len(displayname)
+	for index := strings.Index(msg, displayname); index != -1; index = strings.Index(msg, displayname) {
+		if (index <= 0 || isAcceptable(msg[index-1])) && (index+length >= len(msg) || isAcceptable(msg[index+length])) {
+			return true
+		}
+		msg = msg[index+len(displayname):]
+	}
+	return false
+}
+
+func (cond *PushCondition) matchMemberCount(room Room) bool {
+	group := MemberCountFilterRegex.FindStringSubmatch(cond.MemberCountCondition)
+	if len(group) != 3 {
+		return false
+	}
+
+	operator := group[1]
+	wantedMemberCount, _ := strconv.Atoi(group[2])
+
+	memberCount := room.GetMemberCount()
+
+	switch operator {
+	case "==", "":
+		return memberCount == wantedMemberCount
+	case ">":
+		return memberCount > wantedMemberCount
+	case ">=":
+		return memberCount >= wantedMemberCount
+	case "<":
+		return memberCount < wantedMemberCount
+	case "<=":
+		return memberCount <= wantedMemberCount
+	default:
+		// Should be impossible due to regex.
+		return false
+	}
+}
diff --git a/vendor/maunium.net/go/mautrix/pushrules/doc.go b/vendor/maunium.net/go/mautrix/pushrules/doc.go
new file mode 100644
index 0000000..19cd774
--- /dev/null
+++ b/vendor/maunium.net/go/mautrix/pushrules/doc.go
@@ -0,0 +1,2 @@
+// Package pushrules contains utilities to parse push notification rules.
+package pushrules
diff --git a/vendor/maunium.net/go/mautrix/pushrules/pushrules.go b/vendor/maunium.net/go/mautrix/pushrules/pushrules.go
new file mode 100644
index 0000000..7944299
--- /dev/null
+++ b/vendor/maunium.net/go/mautrix/pushrules/pushrules.go
@@ -0,0 +1,37 @@
+// 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 pushrules
+
+import (
+	"encoding/gob"
+	"encoding/json"
+	"reflect"
+
+	"maunium.net/go/mautrix/event"
+)
+
+// EventContent represents the content of a m.push_rules account data event.
+// https://spec.matrix.org/v1.2/client-server-api/#mpush_rules
+type EventContent struct {
+	Ruleset *PushRuleset `json:"global"`
+}
+
+func init() {
+	event.TypeMap[event.AccountDataPushRules] = reflect.TypeOf(EventContent{})
+	gob.Register(&EventContent{})
+}
+
+// EventToPushRules converts a m.push_rules event to a PushRuleset by passing the data through JSON.
+func EventToPushRules(evt *event.Event) (*PushRuleset, error) {
+	content := &EventContent{}
+	err := json.Unmarshal(evt.Content.VeryRaw, content)
+	if err != nil {
+		return nil, err
+	}
+
+	return content.Ruleset, nil
+}
diff --git a/vendor/maunium.net/go/mautrix/pushrules/rule.go b/vendor/maunium.net/go/mautrix/pushrules/rule.go
new file mode 100644
index 0000000..ee6d33c
--- /dev/null
+++ b/vendor/maunium.net/go/mautrix/pushrules/rule.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 pushrules
+
+import (
+	"encoding/gob"
+
+	"go.mau.fi/util/glob"
+
+	"maunium.net/go/mautrix/event"
+	"maunium.net/go/mautrix/id"
+)
+
+func init() {
+	gob.Register(PushRuleArray{})
+	gob.Register(PushRuleMap{})
+}
+
+type PushRuleCollection interface {
+	GetMatchingRule(room Room, evt *event.Event) *PushRule
+	GetActions(room Room, evt *event.Event) PushActionArray
+}
+
+type PushRuleArray []*PushRule
+
+func (rules PushRuleArray) SetType(typ PushRuleType) PushRuleArray {
+	for _, rule := range rules {
+		rule.Type = typ
+	}
+	return rules
+}
+
+func (rules PushRuleArray) GetMatchingRule(room Room, evt *event.Event) *PushRule {
+	for _, rule := range rules {
+		if !rule.Match(room, evt) {
+			continue
+		}
+		return rule
+	}
+	return nil
+}
+
+func (rules PushRuleArray) GetActions(room Room, evt *event.Event) PushActionArray {
+	return rules.GetMatchingRule(room, evt).GetActions()
+}
+
+type PushRuleMap struct {
+	Map  map[string]*PushRule
+	Type PushRuleType
+}
+
+func (rules PushRuleArray) SetTypeAndMap(typ PushRuleType) PushRuleMap {
+	data := PushRuleMap{
+		Map:  make(map[string]*PushRule),
+		Type: typ,
+	}
+	for _, rule := range rules {
+		rule.Type = typ
+		data.Map[rule.RuleID] = rule
+	}
+	return data
+}
+
+func (ruleMap PushRuleMap) GetMatchingRule(room Room, evt *event.Event) *PushRule {
+	var rule *PushRule
+	var found bool
+	switch ruleMap.Type {
+	case RoomRule:
+		rule, found = ruleMap.Map[string(evt.RoomID)]
+	case SenderRule:
+		rule, found = ruleMap.Map[string(evt.Sender)]
+	}
+	if found && rule.Match(room, evt) {
+		return rule
+	}
+	return nil
+}
+
+func (ruleMap PushRuleMap) GetActions(room Room, evt *event.Event) PushActionArray {
+	return ruleMap.GetMatchingRule(room, evt).GetActions()
+}
+
+func (ruleMap PushRuleMap) Unmap() PushRuleArray {
+	array := make(PushRuleArray, len(ruleMap.Map))
+	index := 0
+	for _, rule := range ruleMap.Map {
+		array[index] = rule
+		index++
+	}
+	return array
+}
+
+type PushRuleType string
+
+const (
+	OverrideRule  PushRuleType = "override"
+	ContentRule   PushRuleType = "content"
+	RoomRule      PushRuleType = "room"
+	SenderRule    PushRuleType = "sender"
+	UnderrideRule PushRuleType = "underride"
+)
+
+type PushRule struct {
+	// The type of this rule.
+	Type PushRuleType `json:"-"`
+	// The ID of this rule.
+	// For room-specific rules and user-specific rules, this is the room or user ID (respectively)
+	// For other types of rules, this doesn't affect anything.
+	RuleID string `json:"rule_id"`
+	// The actions this rule should trigger when matched.
+	Actions PushActionArray `json:"actions"`
+	// Whether this is a default rule, or has been set explicitly.
+	Default bool `json:"default"`
+	// Whether or not this push rule is enabled.
+	Enabled bool `json:"enabled"`
+	// The conditions to match in order to trigger this rule.
+	// Only applicable to generic underride/override rules.
+	Conditions []*PushCondition `json:"conditions,omitempty"`
+	// Pattern for content-specific push rules
+	Pattern string `json:"pattern,omitempty"`
+}
+
+func (rule *PushRule) GetActions() PushActionArray {
+	if rule == nil {
+		return nil
+	}
+	return rule.Actions
+}
+
+func (rule *PushRule) Match(room Room, evt *event.Event) bool {
+	if rule == nil || !rule.Enabled {
+		return false
+	}
+	if rule.RuleID == ".m.rule.contains_display_name" || rule.RuleID == ".m.rule.contains_user_name" || rule.RuleID == ".m.rule.roomnotif" {
+		if _, containsMentions := evt.Content.Raw["m.mentions"]; containsMentions {
+			// Disable legacy mention push rules when the event contains the new mentions key
+			return false
+		}
+	}
+	switch rule.Type {
+	case OverrideRule, UnderrideRule:
+		return rule.matchConditions(room, evt)
+	case ContentRule:
+		return rule.matchPattern(room, evt)
+	case RoomRule:
+		return id.RoomID(rule.RuleID) == evt.RoomID
+	case SenderRule:
+		return id.UserID(rule.RuleID) == evt.Sender
+	default:
+		return false
+	}
+}
+
+func (rule *PushRule) matchConditions(room Room, evt *event.Event) bool {
+	for _, cond := range rule.Conditions {
+		if !cond.Match(room, evt) {
+			return false
+		}
+	}
+	return true
+}
+
+func (rule *PushRule) matchPattern(room Room, evt *event.Event) bool {
+	pattern := glob.CompileWithImplicitContains(rule.Pattern)
+	if pattern == nil {
+		return false
+	}
+	msg, ok := evt.Content.Raw["body"].(string)
+	if !ok {
+		return false
+	}
+	return pattern.Match(msg)
+}
diff --git a/vendor/maunium.net/go/mautrix/pushrules/ruleset.go b/vendor/maunium.net/go/mautrix/pushrules/ruleset.go
new file mode 100644
index 0000000..609997b
--- /dev/null
+++ b/vendor/maunium.net/go/mautrix/pushrules/ruleset.go
@@ -0,0 +1,97 @@
+// 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 pushrules
+
+import (
+	"encoding/json"
+
+	"maunium.net/go/mautrix/event"
+)
+
+type PushRuleset struct {
+	Override  PushRuleArray
+	Content   PushRuleArray
+	Room      PushRuleMap
+	Sender    PushRuleMap
+	Underride PushRuleArray
+}
+
+type rawPushRuleset struct {
+	Override  PushRuleArray `json:"override"`
+	Content   PushRuleArray `json:"content"`
+	Room      PushRuleArray `json:"room"`
+	Sender    PushRuleArray `json:"sender"`
+	Underride PushRuleArray `json:"underride"`
+}
+
+// UnmarshalJSON parses JSON into this PushRuleset.
+//
+// For override, sender and underride push rule arrays, the type is added
+// to each PushRule and the array is used as-is.
+//
+// For room and sender push rule arrays, the type is added to each PushRule
+// and the array is converted to a map with the rule ID as the key and the
+// PushRule as the value.
+func (rs *PushRuleset) UnmarshalJSON(raw []byte) (err error) {
+	data := rawPushRuleset{}
+	err = json.Unmarshal(raw, &data)
+	if err != nil {
+		return
+	}
+
+	rs.Override = data.Override.SetType(OverrideRule)
+	rs.Content = data.Content.SetType(ContentRule)
+	rs.Room = data.Room.SetTypeAndMap(RoomRule)
+	rs.Sender = data.Sender.SetTypeAndMap(SenderRule)
+	rs.Underride = data.Underride.SetType(UnderrideRule)
+	return
+}
+
+// MarshalJSON is the reverse of UnmarshalJSON()
+func (rs *PushRuleset) MarshalJSON() ([]byte, error) {
+	data := rawPushRuleset{
+		Override:  rs.Override,
+		Content:   rs.Content,
+		Room:      rs.Room.Unmap(),
+		Sender:    rs.Sender.Unmap(),
+		Underride: rs.Underride,
+	}
+	return json.Marshal(&data)
+}
+
+// DefaultPushActions is the value returned if none of the rule
+// collections in a Ruleset match the event given to GetActions()
+var DefaultPushActions = PushActionArray{&PushAction{Action: ActionDontNotify}}
+
+func (rs *PushRuleset) GetMatchingRule(room Room, evt *event.Event) (rule *PushRule) {
+	// Add push rule collections to array in priority order
+	arrays := []PushRuleCollection{rs.Override, rs.Content, rs.Room, rs.Sender, rs.Underride}
+	// Loop until one of the push rule collections matches the room/event combo.
+	for _, pra := range arrays {
+		if pra == nil {
+			continue
+		}
+		if rule = pra.GetMatchingRule(room, evt); rule != nil {
+			// Match found, return it.
+			return
+		}
+	}
+	// No match found
+	return nil
+}
+
+// GetActions matches the given event against all of the push rule
+// collections in this push ruleset in the order of priority as
+// specified in spec section 11.12.1.4.
+func (rs *PushRuleset) GetActions(room Room, evt *event.Event) (match PushActionArray) {
+	actions := rs.GetMatchingRule(room, evt).GetActions()
+	if actions == nil {
+		// No match found, return default actions.
+		return DefaultPushActions
+	}
+	return actions
+}
diff --git a/vendor/maunium.net/go/mautrix/requests.go b/vendor/maunium.net/go/mautrix/requests.go
new file mode 100644
index 0000000..a6b0ea8
--- /dev/null
+++ b/vendor/maunium.net/go/mautrix/requests.go
@@ -0,0 +1,475 @@
+package mautrix
+
+import (
+	"encoding/json"
+	"strconv"
+
+	"maunium.net/go/mautrix/crypto/signatures"
+	"maunium.net/go/mautrix/event"
+	"maunium.net/go/mautrix/id"
+	"maunium.net/go/mautrix/pushrules"
+)
+
+type AuthType string
+
+const (
+	AuthTypePassword   AuthType = "m.login.password"
+	AuthTypeReCAPTCHA  AuthType = "m.login.recaptcha"
+	AuthTypeOAuth2     AuthType = "m.login.oauth2"
+	AuthTypeSSO        AuthType = "m.login.sso"
+	AuthTypeEmail      AuthType = "m.login.email.identity"
+	AuthTypeMSISDN     AuthType = "m.login.msisdn"
+	AuthTypeToken      AuthType = "m.login.token"
+	AuthTypeDummy      AuthType = "m.login.dummy"
+	AuthTypeAppservice AuthType = "m.login.application_service"
+
+	AuthTypeSynapseJWT AuthType = "org.matrix.login.jwt"
+
+	AuthTypeDevtureSharedSecret AuthType = "com.devture.shared_secret_auth"
+)
+
+type IdentifierType string
+
+const (
+	IdentifierTypeUser       = "m.id.user"
+	IdentifierTypeThirdParty = "m.id.thirdparty"
+	IdentifierTypePhone      = "m.id.phone"
+)
+
+type Direction rune
+
+const (
+	DirectionForward  Direction = 'f'
+	DirectionBackward Direction = 'b'
+)
+
+// ReqRegister is the JSON request for https://spec.matrix.org/v1.2/client-server-api/#post_matrixclientv3register
+type ReqRegister struct {
+	Username                 string      `json:"username,omitempty"`
+	Password                 string      `json:"password,omitempty"`
+	DeviceID                 id.DeviceID `json:"device_id,omitempty"`
+	InitialDeviceDisplayName string      `json:"initial_device_display_name,omitempty"`
+	InhibitLogin             bool        `json:"inhibit_login,omitempty"`
+	RefreshToken             bool        `json:"refresh_token,omitempty"`
+	Auth                     interface{} `json:"auth,omitempty"`
+
+	// Type for registration, only used for appservice user registrations
+	// https://spec.matrix.org/v1.2/application-service-api/#server-admin-style-permissions
+	Type AuthType `json:"type,omitempty"`
+}
+
+type BaseAuthData struct {
+	Type    AuthType `json:"type"`
+	Session string   `json:"session,omitempty"`
+}
+
+type UserIdentifier struct {
+	Type IdentifierType `json:"type"`
+
+	User string `json:"user,omitempty"`
+
+	Medium  string `json:"medium,omitempty"`
+	Address string `json:"address,omitempty"`
+
+	Country string `json:"country,omitempty"`
+	Phone   string `json:"phone,omitempty"`
+}
+
+// ReqLogin is the JSON request for https://spec.matrix.org/v1.2/client-server-api/#post_matrixclientv3login
+type ReqLogin struct {
+	Type                     AuthType       `json:"type"`
+	Identifier               UserIdentifier `json:"identifier"`
+	Password                 string         `json:"password,omitempty"`
+	Token                    string         `json:"token,omitempty"`
+	DeviceID                 id.DeviceID    `json:"device_id,omitempty"`
+	InitialDeviceDisplayName string         `json:"initial_device_display_name,omitempty"`
+	RefreshToken             bool           `json:"refresh_token,omitempty"`
+
+	// Whether or not the returned credentials should be stored in the Client
+	StoreCredentials bool `json:"-"`
+	// Whether or not the returned .well-known data should update the homeserver URL in the Client
+	StoreHomeserverURL bool `json:"-"`
+}
+
+type ReqUIAuthFallback struct {
+	Session string `json:"session"`
+	User    string `json:"user"`
+}
+
+type ReqUIAuthLogin struct {
+	BaseAuthData
+	User     string `json:"user,omitempty"`
+	Password string `json:"password,omitempty"`
+	Token    string `json:"token,omitempty"`
+}
+
+// ReqCreateRoom is the JSON request for https://spec.matrix.org/v1.2/client-server-api/#post_matrixclientv3createroom
+type ReqCreateRoom struct {
+	Visibility      string                 `json:"visibility,omitempty"`
+	RoomAliasName   string                 `json:"room_alias_name,omitempty"`
+	Name            string                 `json:"name,omitempty"`
+	Topic           string                 `json:"topic,omitempty"`
+	Invite          []id.UserID            `json:"invite,omitempty"`
+	Invite3PID      []ReqInvite3PID        `json:"invite_3pid,omitempty"`
+	CreationContent map[string]interface{} `json:"creation_content,omitempty"`
+	InitialState    []*event.Event         `json:"initial_state,omitempty"`
+	Preset          string                 `json:"preset,omitempty"`
+	IsDirect        bool                   `json:"is_direct,omitempty"`
+	RoomVersion     string                 `json:"room_version,omitempty"`
+
+	PowerLevelOverride *event.PowerLevelsEventContent `json:"power_level_content_override,omitempty"`
+
+	MeowRoomID            id.RoomID   `json:"fi.mau.room_id,omitempty"`
+	BeeperInitialMembers  []id.UserID `json:"com.beeper.initial_members,omitempty"`
+	BeeperAutoJoinInvites bool        `json:"com.beeper.auto_join_invites,omitempty"`
+	BeeperLocalRoomID     id.RoomID   `json:"com.beeper.local_room_id,omitempty"`
+}
+
+// ReqRedact is the JSON request for https://spec.matrix.org/v1.2/client-server-api/#put_matrixclientv3roomsroomidredacteventidtxnid
+type ReqRedact struct {
+	Reason string
+	TxnID  string
+	Extra  map[string]interface{}
+}
+
+type ReqMembers struct {
+	At            string           `json:"at"`
+	Membership    event.Membership `json:"membership,omitempty"`
+	NotMembership event.Membership `json:"not_membership,omitempty"`
+}
+
+// ReqInvite3PID is the JSON request for https://spec.matrix.org/v1.2/client-server-api/#post_matrixclientv3roomsroomidinvite-1
+// It is also a JSON object used in https://spec.matrix.org/v1.2/client-server-api/#post_matrixclientv3createroom
+type ReqInvite3PID struct {
+	IDServer string `json:"id_server"`
+	Medium   string `json:"medium"`
+	Address  string `json:"address"`
+}
+
+type ReqLeave struct {
+	Reason string `json:"reason,omitempty"`
+}
+
+// ReqInviteUser is the JSON request for https://spec.matrix.org/v1.2/client-server-api/#post_matrixclientv3roomsroomidinvite
+type ReqInviteUser struct {
+	Reason string    `json:"reason,omitempty"`
+	UserID id.UserID `json:"user_id"`
+}
+
+// ReqKickUser is the JSON request for https://spec.matrix.org/v1.2/client-server-api/#post_matrixclientv3roomsroomidkick
+type ReqKickUser struct {
+	Reason string    `json:"reason,omitempty"`
+	UserID id.UserID `json:"user_id"`
+}
+
+// ReqBanUser is the JSON request for https://spec.matrix.org/v1.2/client-server-api/#post_matrixclientv3roomsroomidban
+type ReqBanUser struct {
+	Reason string    `json:"reason,omitempty"`
+	UserID id.UserID `json:"user_id"`
+}
+
+// ReqUnbanUser is the JSON request for https://spec.matrix.org/v1.2/client-server-api/#post_matrixclientv3roomsroomidunban
+type ReqUnbanUser struct {
+	Reason string    `json:"reason,omitempty"`
+	UserID id.UserID `json:"user_id"`
+}
+
+// ReqTyping is the JSON request for https://spec.matrix.org/v1.2/client-server-api/#put_matrixclientv3roomsroomidtypinguserid
+type ReqTyping struct {
+	Typing  bool  `json:"typing"`
+	Timeout int64 `json:"timeout,omitempty"`
+}
+
+type ReqPresence struct {
+	Presence event.Presence `json:"presence"`
+}
+
+type ReqAliasCreate struct {
+	RoomID id.RoomID `json:"room_id"`
+}
+
+type OneTimeKey struct {
+	Key        id.Curve25519         `json:"key"`
+	Fallback   bool                  `json:"fallback,omitempty"`
+	Signatures signatures.Signatures `json:"signatures,omitempty"`
+	Unsigned   map[string]any        `json:"unsigned,omitempty"`
+	IsSigned   bool                  `json:"-"`
+
+	// Raw data in the one-time key. This must be used for signature verification to ensure unrecognized fields
+	// aren't thrown away (because that would invalidate the signature).
+	RawData json.RawMessage `json:"-"`
+}
+
+type serializableOTK OneTimeKey
+
+func (otk *OneTimeKey) UnmarshalJSON(data []byte) (err error) {
+	if len(data) > 0 && data[0] == '"' && data[len(data)-1] == '"' {
+		err = json.Unmarshal(data, &otk.Key)
+		otk.Signatures = nil
+		otk.Unsigned = nil
+		otk.IsSigned = false
+	} else {
+		err = json.Unmarshal(data, (*serializableOTK)(otk))
+		otk.RawData = data
+		otk.IsSigned = true
+	}
+	return err
+}
+
+func (otk *OneTimeKey) MarshalJSON() ([]byte, error) {
+	if !otk.IsSigned {
+		return json.Marshal(otk.Key)
+	} else {
+		return json.Marshal((*serializableOTK)(otk))
+	}
+}
+
+type ReqUploadKeys struct {
+	DeviceKeys  *DeviceKeys             `json:"device_keys,omitempty"`
+	OneTimeKeys map[id.KeyID]OneTimeKey `json:"one_time_keys,omitempty"`
+}
+
+type ReqKeysSignatures struct {
+	UserID     id.UserID              `json:"user_id"`
+	DeviceID   id.DeviceID            `json:"device_id,omitempty"`
+	Algorithms []id.Algorithm         `json:"algorithms,omitempty"`
+	Usage      []id.CrossSigningUsage `json:"usage,omitempty"`
+	Keys       map[id.KeyID]string    `json:"keys"`
+	Signatures signatures.Signatures  `json:"signatures"`
+}
+
+type ReqUploadSignatures map[id.UserID]map[string]ReqKeysSignatures
+
+type DeviceKeys struct {
+	UserID     id.UserID              `json:"user_id"`
+	DeviceID   id.DeviceID            `json:"device_id"`
+	Algorithms []id.Algorithm         `json:"algorithms"`
+	Keys       KeyMap                 `json:"keys"`
+	Signatures signatures.Signatures  `json:"signatures"`
+	Unsigned   map[string]interface{} `json:"unsigned,omitempty"`
+}
+
+type CrossSigningKeys struct {
+	UserID     id.UserID               `json:"user_id"`
+	Usage      []id.CrossSigningUsage  `json:"usage"`
+	Keys       map[id.KeyID]id.Ed25519 `json:"keys"`
+	Signatures signatures.Signatures   `json:"signatures,omitempty"`
+}
+
+func (csk *CrossSigningKeys) FirstKey() id.Ed25519 {
+	for _, key := range csk.Keys {
+		return key
+	}
+	return ""
+}
+
+type UploadCrossSigningKeysReq struct {
+	Master      CrossSigningKeys `json:"master_key"`
+	SelfSigning CrossSigningKeys `json:"self_signing_key"`
+	UserSigning CrossSigningKeys `json:"user_signing_key"`
+	Auth        interface{}      `json:"auth,omitempty"`
+}
+
+type KeyMap map[id.DeviceKeyID]string
+
+func (km KeyMap) GetEd25519(deviceID id.DeviceID) id.Ed25519 {
+	val, ok := km[id.NewDeviceKeyID(id.KeyAlgorithmEd25519, deviceID)]
+	if !ok {
+		return ""
+	}
+	return id.Ed25519(val)
+}
+
+func (km KeyMap) GetCurve25519(deviceID id.DeviceID) id.Curve25519 {
+	val, ok := km[id.NewDeviceKeyID(id.KeyAlgorithmCurve25519, deviceID)]
+	if !ok {
+		return ""
+	}
+	return id.Curve25519(val)
+}
+
+type ReqQueryKeys struct {
+	DeviceKeys DeviceKeysRequest `json:"device_keys"`
+	Timeout    int64             `json:"timeout,omitempty"`
+}
+
+type DeviceKeysRequest map[id.UserID]DeviceIDList
+
+type DeviceIDList []id.DeviceID
+
+type ReqClaimKeys struct {
+	OneTimeKeys OneTimeKeysRequest `json:"one_time_keys"`
+
+	Timeout int64 `json:"timeout,omitempty"`
+}
+
+type OneTimeKeysRequest map[id.UserID]map[id.DeviceID]id.KeyAlgorithm
+
+type ReqSendToDevice struct {
+	Messages map[id.UserID]map[id.DeviceID]*event.Content `json:"messages"`
+}
+
+// ReqDeviceInfo is the JSON request for https://spec.matrix.org/v1.2/client-server-api/#put_matrixclientv3devicesdeviceid
+type ReqDeviceInfo struct {
+	DisplayName string `json:"display_name,omitempty"`
+}
+
+// ReqDeleteDevice is the JSON request for https://spec.matrix.org/v1.2/client-server-api/#delete_matrixclientv3devicesdeviceid
+type ReqDeleteDevice struct {
+	Auth interface{} `json:"auth,omitempty"`
+}
+
+// ReqDeleteDevices is the JSON request for https://spec.matrix.org/v1.2/client-server-api/#post_matrixclientv3delete_devices
+type ReqDeleteDevices struct {
+	Devices []id.DeviceID `json:"devices"`
+	Auth    interface{}   `json:"auth,omitempty"`
+}
+
+type ReqPutPushRule struct {
+	Before string `json:"-"`
+	After  string `json:"-"`
+
+	Actions    []pushrules.PushActionType `json:"actions"`
+	Conditions []pushrules.PushCondition  `json:"conditions"`
+	Pattern    string                     `json:"pattern"`
+}
+
+// Deprecated: MSC2716 was abandoned
+type ReqBatchSend struct {
+	PrevEventID id.EventID `json:"-"`
+	BatchID     id.BatchID `json:"-"`
+
+	BeeperNewMessages bool      `json:"-"`
+	BeeperMarkReadBy  id.UserID `json:"-"`
+
+	StateEventsAtStart []*event.Event `json:"state_events_at_start"`
+	Events             []*event.Event `json:"events"`
+}
+
+type ReqBeeperBatchSend struct {
+	// ForwardIfNoMessages should be set to true if the batch should be forward
+	// backfilled if there are no messages currently in the room.
+	ForwardIfNoMessages bool           `json:"forward_if_no_messages"`
+	Forward             bool           `json:"forward"`
+	SendNotification    bool           `json:"send_notification"`
+	MarkReadBy          id.UserID      `json:"mark_read_by,omitempty"`
+	Events              []*event.Event `json:"events"`
+}
+
+type ReqSetReadMarkers struct {
+	Read        id.EventID `json:"m.read,omitempty"`
+	ReadPrivate id.EventID `json:"m.read.private,omitempty"`
+	FullyRead   id.EventID `json:"m.fully_read,omitempty"`
+
+	BeeperReadExtra        interface{} `json:"com.beeper.read.extra,omitempty"`
+	BeeperReadPrivateExtra interface{} `json:"com.beeper.read.private.extra,omitempty"`
+	BeeperFullyReadExtra   interface{} `json:"com.beeper.fully_read.extra,omitempty"`
+}
+
+type BeeperInboxDone struct {
+	Delta   int64 `json:"at_delta"`
+	AtOrder int64 `json:"at_order"`
+}
+
+type ReqSetBeeperInboxState struct {
+	MarkedUnread *bool              `json:"marked_unread,omitempty"`
+	Done         *BeeperInboxDone   `json:"done,omitempty"`
+	ReadMarkers  *ReqSetReadMarkers `json:"read_markers,omitempty"`
+}
+
+type ReqSendReceipt struct {
+	ThreadID string `json:"thread_id,omitempty"`
+}
+
+// ReqHierarchy contains the parameters for https://spec.matrix.org/v1.4/client-server-api/#get_matrixclientv1roomsroomidhierarchy
+//
+// As it's a GET method, there is no JSON body, so this is only query parameters.
+type ReqHierarchy struct {
+	// A pagination token from a previous Hierarchy call.
+	// If specified, max_depth and suggested_only cannot be changed from the first request.
+	From string
+	// Limit for the maximum number of rooms to include per response.
+	// The server will apply a default value if a limit isn't provided.
+	Limit int
+	// Limit for how far to go into the space. When reached, no further child rooms will be returned.
+	// The server will apply a default value if a max depth isn't provided.
+	MaxDepth *int
+	// Flag to indicate whether the server should only consider suggested rooms.
+	// Suggested rooms are annotated in their m.space.child event contents.
+	SuggestedOnly bool
+}
+
+func (req *ReqHierarchy) Query() map[string]string {
+	query := map[string]string{}
+	if req == nil {
+		return query
+	}
+	if req.From != "" {
+		query["from"] = req.From
+	}
+	if req.Limit > 0 {
+		query["limit"] = strconv.Itoa(req.Limit)
+	}
+	if req.MaxDepth != nil {
+		query["max_depth"] = strconv.Itoa(*req.MaxDepth)
+	}
+	if req.SuggestedOnly {
+		query["suggested_only"] = "true"
+	}
+	return query
+}
+
+type ReqAppservicePing struct {
+	TxnID string `json:"transaction_id,omitempty"`
+}
+
+type ReqBeeperMergeRoom struct {
+	NewRoom ReqCreateRoom `json:"create"`
+	Key     string        `json:"key"`
+	Rooms   []id.RoomID   `json:"rooms"`
+	User    id.UserID     `json:"user_id"`
+}
+
+type BeeperSplitRoomPart struct {
+	UserID  id.UserID     `json:"user_id"`
+	Values  []string      `json:"values"`
+	NewRoom ReqCreateRoom `json:"create"`
+}
+
+type ReqBeeperSplitRoom struct {
+	RoomID id.RoomID `json:"-"`
+
+	Key   string                `json:"key"`
+	Parts []BeeperSplitRoomPart `json:"parts"`
+}
+
+type ReqRoomKeysVersionCreate[A any] struct {
+	Algorithm id.KeyBackupAlgorithm `json:"algorithm"`
+	AuthData  A                     `json:"auth_data"`
+}
+
+type ReqRoomKeysVersionUpdate[A any] struct {
+	Algorithm id.KeyBackupAlgorithm `json:"algorithm"`
+	AuthData  A                     `json:"auth_data"`
+	Version   id.KeyBackupVersion   `json:"version,omitempty"`
+}
+
+type ReqKeyBackup struct {
+	Rooms map[id.RoomID]ReqRoomKeyBackup `json:"rooms"`
+}
+
+type ReqRoomKeyBackup struct {
+	Sessions map[id.SessionID]ReqKeyBackupData `json:"sessions"`
+}
+
+type ReqKeyBackupData struct {
+	FirstMessageIndex int             `json:"first_message_index"`
+	ForwardedCount    int             `json:"forwarded_count"`
+	IsVerified        bool            `json:"is_verified"`
+	SessionData       json.RawMessage `json:"session_data"`
+}
+
+type ReqReport struct {
+	Reason string `json:"reason,omitempty"`
+	Score  int    `json:"score,omitempty"`
+}
diff --git a/vendor/maunium.net/go/mautrix/responses.go b/vendor/maunium.net/go/mautrix/responses.go
new file mode 100644
index 0000000..26aaac7
--- /dev/null
+++ b/vendor/maunium.net/go/mautrix/responses.go
@@ -0,0 +1,625 @@
+package mautrix
+
+import (
+	"bytes"
+	"encoding/json"
+	"fmt"
+	"reflect"
+	"strconv"
+	"strings"
+
+	"github.com/tidwall/gjson"
+	"github.com/tidwall/sjson"
+	"go.mau.fi/util/jsontime"
+
+	"maunium.net/go/mautrix/event"
+	"maunium.net/go/mautrix/id"
+)
+
+// RespWhoami is the JSON response for https://spec.matrix.org/v1.2/client-server-api/#get_matrixclientv3accountwhoami
+type RespWhoami struct {
+	UserID   id.UserID   `json:"user_id"`
+	DeviceID id.DeviceID `json:"device_id"`
+}
+
+// RespCreateFilter is the JSON response for https://spec.matrix.org/v1.2/client-server-api/#post_matrixclientv3useruseridfilter
+type RespCreateFilter struct {
+	FilterID string `json:"filter_id"`
+}
+
+// RespJoinRoom is the JSON response for https://spec.matrix.org/v1.2/client-server-api/#post_matrixclientv3roomsroomidjoin
+type RespJoinRoom struct {
+	RoomID id.RoomID `json:"room_id"`
+}
+
+// RespLeaveRoom is the JSON response for https://spec.matrix.org/v1.2/client-server-api/#post_matrixclientv3roomsroomidleave
+type RespLeaveRoom struct{}
+
+// RespForgetRoom is the JSON response for https://spec.matrix.org/v1.2/client-server-api/#post_matrixclientv3roomsroomidforget
+type RespForgetRoom struct{}
+
+// RespInviteUser is the JSON response for https://spec.matrix.org/v1.2/client-server-api/#post_matrixclientv3roomsroomidinvite
+type RespInviteUser struct{}
+
+// RespKickUser is the JSON response for https://spec.matrix.org/v1.2/client-server-api/#post_matrixclientv3roomsroomidkick
+type RespKickUser struct{}
+
+// RespBanUser is the JSON response for https://spec.matrix.org/v1.2/client-server-api/#post_matrixclientv3roomsroomidban
+type RespBanUser struct{}
+
+// RespUnbanUser is the JSON response for https://spec.matrix.org/v1.2/client-server-api/#post_matrixclientv3roomsroomidunban
+type RespUnbanUser struct{}
+
+// RespTyping is the JSON response for https://spec.matrix.org/v1.2/client-server-api/#put_matrixclientv3roomsroomidtypinguserid
+type RespTyping struct{}
+
+// RespPresence is the JSON response for https://spec.matrix.org/v1.2/client-server-api/#get_matrixclientv3presenceuseridstatus
+type RespPresence struct {
+	Presence        event.Presence `json:"presence"`
+	LastActiveAgo   int            `json:"last_active_ago"`
+	StatusMsg       string         `json:"status_msg"`
+	CurrentlyActive bool           `json:"currently_active"`
+}
+
+// RespJoinedRooms is the JSON response for https://spec.matrix.org/v1.2/client-server-api/#get_matrixclientv3joined_rooms
+type RespJoinedRooms struct {
+	JoinedRooms []id.RoomID `json:"joined_rooms"`
+}
+
+// RespJoinedMembers is the JSON response for https://spec.matrix.org/v1.2/client-server-api/#get_matrixclientv3roomsroomidjoined_members
+type RespJoinedMembers struct {
+	Joined map[id.UserID]JoinedMember `json:"joined"`
+}
+
+type JoinedMember struct {
+	DisplayName string `json:"display_name,omitempty"`
+	AvatarURL   string `json:"avatar_url,omitempty"`
+}
+
+// RespMessages is the JSON response for https://spec.matrix.org/v1.2/client-server-api/#get_matrixclientv3roomsroomidmessages
+type RespMessages struct {
+	Start string         `json:"start"`
+	Chunk []*event.Event `json:"chunk"`
+	State []*event.Event `json:"state"`
+	End   string         `json:"end,omitempty"`
+}
+
+// RespContext is the JSON response for https://spec.matrix.org/v1.2/client-server-api/#get_matrixclientv3roomsroomidcontexteventid
+type RespContext struct {
+	End          string         `json:"end"`
+	Event        *event.Event   `json:"event"`
+	EventsAfter  []*event.Event `json:"events_after"`
+	EventsBefore []*event.Event `json:"events_before"`
+	Start        string         `json:"start"`
+	State        []*event.Event `json:"state"`
+}
+
+// RespSendEvent is the JSON response for https://spec.matrix.org/v1.2/client-server-api/#put_matrixclientv3roomsroomidsendeventtypetxnid
+type RespSendEvent struct {
+	EventID id.EventID `json:"event_id"`
+}
+
+// RespMediaConfig is the JSON response for https://spec.matrix.org/v1.4/client-server-api/#get_matrixmediav3config
+type RespMediaConfig struct {
+	UploadSize int64 `json:"m.upload.size,omitempty"`
+}
+
+// RespMediaUpload is the JSON response for https://spec.matrix.org/v1.2/client-server-api/#post_matrixmediav3upload
+type RespMediaUpload struct {
+	ContentURI id.ContentURI `json:"content_uri"`
+}
+
+// RespCreateMXC is the JSON response for https://spec.matrix.org/v1.7/client-server-api/#post_matrixmediav1create
+type RespCreateMXC struct {
+	ContentURI      id.ContentURI      `json:"content_uri"`
+	UnusedExpiresAt jsontime.UnixMilli `json:"unused_expires_at,omitempty"`
+
+	UnstableUploadURL string `json:"com.beeper.msc3870.upload_url,omitempty"`
+
+	// Beeper extensions for uploading unique media only once
+	BeeperUniqueID    string             `json:"com.beeper.unique_id,omitempty"`
+	BeeperCompletedAt jsontime.UnixMilli `json:"com.beeper.completed_at,omitempty"`
+}
+
+// RespPreviewURL is the JSON response for https://spec.matrix.org/v1.2/client-server-api/#get_matrixmediav3preview_url
+type RespPreviewURL = event.LinkPreview
+
+// RespUserInteractive is the JSON response for https://spec.matrix.org/v1.2/client-server-api/#user-interactive-authentication-api
+type RespUserInteractive struct {
+	Flows     []UIAFlow                `json:"flows,omitempty"`
+	Params    map[AuthType]interface{} `json:"params,omitempty"`
+	Session   string                   `json:"session,omitempty"`
+	Completed []string                 `json:"completed,omitempty"`
+
+	ErrCode string `json:"errcode,omitempty"`
+	Error   string `json:"error,omitempty"`
+}
+
+type UIAFlow struct {
+	Stages []AuthType `json:"stages,omitempty"`
+}
+
+// HasSingleStageFlow returns true if there exists at least 1 Flow with a single stage of stageName.
+func (r RespUserInteractive) HasSingleStageFlow(stageName AuthType) bool {
+	for _, f := range r.Flows {
+		if len(f.Stages) == 1 && f.Stages[0] == stageName {
+			return true
+		}
+	}
+	return false
+}
+
+// RespUserDisplayName is the JSON response for https://spec.matrix.org/v1.2/client-server-api/#get_matrixclientv3profileuseriddisplayname
+type RespUserDisplayName struct {
+	DisplayName string `json:"displayname"`
+}
+
+type RespUserProfile struct {
+	DisplayName string        `json:"displayname"`
+	AvatarURL   id.ContentURI `json:"avatar_url"`
+}
+
+// RespRegisterAvailable is the JSON response for https://spec.matrix.org/v1.4/client-server-api/#get_matrixclientv3registeravailable
+type RespRegisterAvailable struct {
+	Available bool `json:"available"`
+}
+
+// RespRegister is the JSON response for https://spec.matrix.org/v1.2/client-server-api/#post_matrixclientv3register
+type RespRegister struct {
+	AccessToken string      `json:"access_token,omitempty"`
+	DeviceID    id.DeviceID `json:"device_id,omitempty"`
+	UserID      id.UserID   `json:"user_id"`
+
+	RefreshToken string `json:"refresh_token,omitempty"`
+	ExpiresInMS  int64  `json:"expires_in_ms,omitempty"`
+
+	// Deprecated: homeserver should be parsed from the user ID
+	HomeServer string `json:"home_server,omitempty"`
+}
+
+type LoginFlow struct {
+	Type AuthType `json:"type"`
+}
+
+// RespLoginFlows is the JSON response for https://spec.matrix.org/v1.2/client-server-api/#get_matrixclientv3login
+type RespLoginFlows struct {
+	Flows []LoginFlow `json:"flows"`
+}
+
+func (rlf *RespLoginFlows) FirstFlowOfType(flowTypes ...AuthType) *LoginFlow {
+	for _, flow := range rlf.Flows {
+		for _, flowType := range flowTypes {
+			if flow.Type == flowType {
+				return &flow
+			}
+		}
+	}
+	return nil
+}
+
+func (rlf *RespLoginFlows) HasFlow(flowType ...AuthType) bool {
+	return rlf.FirstFlowOfType(flowType...) != nil
+}
+
+// RespLogin is the JSON response for https://spec.matrix.org/v1.2/client-server-api/#post_matrixclientv3login
+type RespLogin struct {
+	AccessToken string           `json:"access_token"`
+	DeviceID    id.DeviceID      `json:"device_id"`
+	UserID      id.UserID        `json:"user_id"`
+	WellKnown   *ClientWellKnown `json:"well_known,omitempty"`
+}
+
+// RespLogout is the JSON response for https://spec.matrix.org/v1.2/client-server-api/#post_matrixclientv3logout
+type RespLogout struct{}
+
+// RespCreateRoom is the JSON response for https://spec.matrix.org/v1.2/client-server-api/#post_matrixclientv3createroom
+type RespCreateRoom struct {
+	RoomID id.RoomID `json:"room_id"`
+}
+
+type RespMembers struct {
+	Chunk []*event.Event `json:"chunk"`
+}
+
+type LazyLoadSummary struct {
+	Heroes             []id.UserID `json:"m.heroes,omitempty"`
+	JoinedMemberCount  *int        `json:"m.joined_member_count,omitempty"`
+	InvitedMemberCount *int        `json:"m.invited_member_count,omitempty"`
+}
+
+type SyncEventsList struct {
+	Events []*event.Event `json:"events,omitempty"`
+}
+
+type SyncTimeline struct {
+	SyncEventsList
+	Limited   bool   `json:"limited,omitempty"`
+	PrevBatch string `json:"prev_batch,omitempty"`
+}
+
+// RespSync is the JSON response for https://spec.matrix.org/v1.2/client-server-api/#get_matrixclientv3sync
+type RespSync struct {
+	NextBatch string `json:"next_batch"`
+
+	AccountData SyncEventsList `json:"account_data"`
+	Presence    SyncEventsList `json:"presence"`
+	ToDevice    SyncEventsList `json:"to_device"`
+
+	DeviceLists    DeviceLists       `json:"device_lists"`
+	DeviceOTKCount OTKCount          `json:"device_one_time_keys_count"`
+	FallbackKeys   []id.KeyAlgorithm `json:"device_unused_fallback_key_types"`
+
+	Rooms RespSyncRooms `json:"rooms"`
+}
+
+type RespSyncRooms struct {
+	Leave  map[id.RoomID]*SyncLeftRoom    `json:"leave,omitempty"`
+	Join   map[id.RoomID]*SyncJoinedRoom  `json:"join,omitempty"`
+	Invite map[id.RoomID]*SyncInvitedRoom `json:"invite,omitempty"`
+	Knock  map[id.RoomID]*SyncKnockedRoom `json:"knock,omitempty"`
+}
+
+type marshalableRespSync RespSync
+
+var syncPathsToDelete = []string{"account_data", "presence", "to_device", "device_lists", "device_one_time_keys_count", "rooms"}
+
+// marshalAndDeleteEmpty marshals a JSON object, then uses gjson to delete empty objects at the given gjson paths.
+func marshalAndDeleteEmpty(marshalable interface{}, paths []string) ([]byte, error) {
+	data, err := json.Marshal(marshalable)
+	if err != nil {
+		return nil, err
+	}
+	for _, path := range paths {
+		res := gjson.GetBytes(data, path)
+		if res.IsObject() && len(res.Raw) == 2 {
+			data, err = sjson.DeleteBytes(data, path)
+			if err != nil {
+				return nil, fmt.Errorf("failed to delete empty %s: %w", path, err)
+			}
+		}
+	}
+	return data, nil
+}
+
+func (rs *RespSync) MarshalJSON() ([]byte, error) {
+	return marshalAndDeleteEmpty((*marshalableRespSync)(rs), syncPathsToDelete)
+}
+
+type DeviceLists struct {
+	Changed []id.UserID `json:"changed,omitempty"`
+	Left    []id.UserID `json:"left,omitempty"`
+}
+
+type OTKCount struct {
+	Curve25519       int `json:"curve25519,omitempty"`
+	SignedCurve25519 int `json:"signed_curve25519,omitempty"`
+
+	// For appservice OTK counts only: the user ID in question
+	UserID   id.UserID   `json:"-"`
+	DeviceID id.DeviceID `json:"-"`
+}
+
+type SyncLeftRoom struct {
+	Summary  LazyLoadSummary `json:"summary"`
+	State    SyncEventsList  `json:"state"`
+	Timeline SyncTimeline    `json:"timeline"`
+}
+
+type marshalableSyncLeftRoom SyncLeftRoom
+
+var syncLeftRoomPathsToDelete = []string{"summary", "state", "timeline"}
+
+func (slr SyncLeftRoom) MarshalJSON() ([]byte, error) {
+	return marshalAndDeleteEmpty((marshalableSyncLeftRoom)(slr), syncLeftRoomPathsToDelete)
+}
+
+type BeeperInboxPreviewEvent struct {
+	EventID   id.EventID         `json:"event_id"`
+	Timestamp jsontime.UnixMilli `json:"origin_server_ts"`
+	Event     *event.Event       `json:"event,omitempty"`
+}
+
+type SyncJoinedRoom struct {
+	Summary     LazyLoadSummary `json:"summary"`
+	State       SyncEventsList  `json:"state"`
+	Timeline    SyncTimeline    `json:"timeline"`
+	Ephemeral   SyncEventsList  `json:"ephemeral"`
+	AccountData SyncEventsList  `json:"account_data"`
+
+	UnreadNotifications *UnreadNotificationCounts `json:"unread_notifications,omitempty"`
+	// https://github.com/matrix-org/matrix-spec-proposals/pull/2654
+	MSC2654UnreadCount *int `json:"org.matrix.msc2654.unread_count,omitempty"`
+	// Beeper extension
+	BeeperInboxPreview *BeeperInboxPreviewEvent `json:"com.beeper.inbox.preview,omitempty"`
+}
+
+type UnreadNotificationCounts struct {
+	HighlightCount    int `json:"highlight_count"`
+	NotificationCount int `json:"notification_count"`
+}
+
+type marshalableSyncJoinedRoom SyncJoinedRoom
+
+var syncJoinedRoomPathsToDelete = []string{"summary", "state", "timeline", "ephemeral", "account_data"}
+
+func (sjr SyncJoinedRoom) MarshalJSON() ([]byte, error) {
+	return marshalAndDeleteEmpty((marshalableSyncJoinedRoom)(sjr), syncJoinedRoomPathsToDelete)
+}
+
+type SyncInvitedRoom struct {
+	Summary LazyLoadSummary `json:"summary"`
+	State   SyncEventsList  `json:"invite_state"`
+}
+
+type marshalableSyncInvitedRoom SyncInvitedRoom
+
+var syncInvitedRoomPathsToDelete = []string{"summary"}
+
+func (sir SyncInvitedRoom) MarshalJSON() ([]byte, error) {
+	return marshalAndDeleteEmpty((marshalableSyncInvitedRoom)(sir), syncInvitedRoomPathsToDelete)
+}
+
+type SyncKnockedRoom struct {
+	State SyncEventsList `json:"knock_state"`
+}
+
+type RespTurnServer struct {
+	Username string   `json:"username"`
+	Password string   `json:"password"`
+	TTL      int      `json:"ttl"`
+	URIs     []string `json:"uris"`
+}
+
+type RespAliasCreate struct{}
+type RespAliasDelete struct{}
+type RespAliasResolve struct {
+	RoomID  id.RoomID `json:"room_id"`
+	Servers []string  `json:"servers"`
+}
+type RespAliasList struct {
+	Aliases []id.RoomAlias `json:"aliases"`
+}
+
+type RespUploadKeys struct {
+	OneTimeKeyCounts OTKCount `json:"one_time_key_counts"`
+}
+
+type RespQueryKeys struct {
+	Failures        map[string]interface{}                   `json:"failures,omitempty"`
+	DeviceKeys      map[id.UserID]map[id.DeviceID]DeviceKeys `json:"device_keys"`
+	MasterKeys      map[id.UserID]CrossSigningKeys           `json:"master_keys"`
+	SelfSigningKeys map[id.UserID]CrossSigningKeys           `json:"self_signing_keys"`
+	UserSigningKeys map[id.UserID]CrossSigningKeys           `json:"user_signing_keys"`
+}
+
+type RespClaimKeys struct {
+	Failures    map[string]interface{}                                `json:"failures,omitempty"`
+	OneTimeKeys map[id.UserID]map[id.DeviceID]map[id.KeyID]OneTimeKey `json:"one_time_keys"`
+}
+
+type RespUploadSignatures struct {
+	Failures map[string]interface{} `json:"failures,omitempty"`
+}
+
+type RespKeyChanges struct {
+	Changed []id.UserID `json:"changed"`
+	Left    []id.UserID `json:"left"`
+}
+
+type RespSendToDevice struct{}
+
+// RespDevicesInfo is the JSON response for https://spec.matrix.org/v1.2/client-server-api/#get_matrixclientv3devices
+type RespDevicesInfo struct {
+	Devices []RespDeviceInfo `json:"devices"`
+}
+
+// RespDeviceInfo is the JSON response for https://spec.matrix.org/v1.2/client-server-api/#get_matrixclientv3devicesdeviceid
+type RespDeviceInfo struct {
+	DeviceID    id.DeviceID `json:"device_id"`
+	DisplayName string      `json:"display_name"`
+	LastSeenIP  string      `json:"last_seen_ip"`
+	LastSeenTS  int64       `json:"last_seen_ts"`
+}
+
+// Deprecated: MSC2716 was abandoned
+type RespBatchSend struct {
+	StateEventIDs []id.EventID `json:"state_event_ids"`
+	EventIDs      []id.EventID `json:"event_ids"`
+
+	InsertionEventID     id.EventID `json:"insertion_event_id"`
+	BatchEventID         id.EventID `json:"batch_event_id"`
+	BaseInsertionEventID id.EventID `json:"base_insertion_event_id"`
+
+	NextBatchID id.BatchID `json:"next_batch_id"`
+}
+
+type RespBeeperBatchSend struct {
+	EventIDs []id.EventID `json:"event_ids"`
+}
+
+// RespCapabilities is the JSON response for https://spec.matrix.org/v1.3/client-server-api/#get_matrixclientv3capabilities
+type RespCapabilities struct {
+	RoomVersions    *CapRoomVersions `json:"m.room_versions,omitempty"`
+	ChangePassword  *CapBooleanTrue  `json:"m.change_password,omitempty"`
+	SetDisplayname  *CapBooleanTrue  `json:"m.set_displayname,omitempty"`
+	SetAvatarURL    *CapBooleanTrue  `json:"m.set_avatar_url,omitempty"`
+	ThreePIDChanges *CapBooleanTrue  `json:"m.3pid_changes,omitempty"`
+
+	Custom map[string]interface{} `json:"-"`
+}
+
+type serializableRespCapabilities RespCapabilities
+
+func (rc *RespCapabilities) UnmarshalJSON(data []byte) error {
+	res := gjson.GetBytes(data, "capabilities")
+	if !res.Exists() || !res.IsObject() {
+		return nil
+	}
+	if res.Index > 0 {
+		data = data[res.Index : res.Index+len(res.Raw)]
+	} else {
+		data = []byte(res.Raw)
+	}
+	err := json.Unmarshal(data, (*serializableRespCapabilities)(rc))
+	if err != nil {
+		return err
+	}
+	err = json.Unmarshal(data, &rc.Custom)
+	if err != nil {
+		return err
+	}
+	// Remove non-custom capabilities from the custom map so that they don't get overridden when serializing back
+	for _, field := range reflect.VisibleFields(reflect.TypeOf(rc).Elem()) {
+		jsonTag := strings.Split(field.Tag.Get("json"), ",")[0]
+		if jsonTag != "-" && jsonTag != "" {
+			delete(rc.Custom, jsonTag)
+		}
+	}
+	return nil
+}
+
+func (rc *RespCapabilities) MarshalJSON() ([]byte, error) {
+	marshalableCopy := make(map[string]interface{}, len(rc.Custom))
+	val := reflect.ValueOf(rc).Elem()
+	for _, field := range reflect.VisibleFields(val.Type()) {
+		jsonTag := strings.Split(field.Tag.Get("json"), ",")[0]
+		if jsonTag != "-" && jsonTag != "" {
+			fieldVal := val.FieldByIndex(field.Index)
+			if !fieldVal.IsNil() {
+				marshalableCopy[jsonTag] = fieldVal.Interface()
+			}
+		}
+	}
+	if rc.Custom != nil {
+		for key, value := range rc.Custom {
+			marshalableCopy[key] = value
+		}
+	}
+	var buf bytes.Buffer
+	buf.WriteString(`{"capabilities":`)
+	err := json.NewEncoder(&buf).Encode(marshalableCopy)
+	if err != nil {
+		return nil, err
+	}
+	buf.WriteByte('}')
+	return buf.Bytes(), nil
+}
+
+type CapBoolean struct {
+	Enabled bool `json:"enabled"`
+}
+
+type CapBooleanTrue CapBoolean
+
+// IsEnabled returns true if the capability is either enabled explicitly or not specified (nil)
+func (cb *CapBooleanTrue) IsEnabled() bool {
+	// Default to true when
+	return cb == nil || cb.Enabled
+}
+
+type CapBooleanFalse CapBoolean
+
+// IsEnabled returns true if the capability is enabled explicitly. If it's not specified, this returns false.
+func (cb *CapBooleanFalse) IsEnabled() bool {
+	return cb != nil && cb.Enabled
+}
+
+type CapRoomVersionStability string
+
+const (
+	CapRoomVersionStable   CapRoomVersionStability = "stable"
+	CapRoomVersionUnstable CapRoomVersionStability = "unstable"
+)
+
+type CapRoomVersions struct {
+	Default   string                             `json:"default"`
+	Available map[string]CapRoomVersionStability `json:"available"`
+}
+
+func (vers *CapRoomVersions) IsStable(version string) bool {
+	if vers == nil || vers.Available == nil {
+		val, err := strconv.Atoi(version)
+		return err == nil && val > 0
+	}
+	return vers.Available[version] == CapRoomVersionStable
+}
+
+func (vers *CapRoomVersions) IsAvailable(version string) bool {
+	if vers == nil || vers.Available == nil {
+		return false
+	}
+	_, available := vers.Available[version]
+	return available
+}
+
+// RespHierarchy is the JSON response for https://spec.matrix.org/v1.4/client-server-api/#get_matrixclientv1roomsroomidhierarchy
+type RespHierarchy struct {
+	NextBatch string            `json:"next_batch,omitempty"`
+	Rooms     []ChildRoomsChunk `json:"rooms"`
+}
+
+type ChildRoomsChunk struct {
+	AvatarURL        id.ContentURI           `json:"avatar_url,omitempty"`
+	CanonicalAlias   id.RoomAlias            `json:"canonical_alias,omitempty"`
+	ChildrenState    []StrippedStateWithTime `json:"children_state"`
+	GuestCanJoin     bool                    `json:"guest_can_join"`
+	JoinRule         event.JoinRule          `json:"join_rule,omitempty"`
+	Name             string                  `json:"name,omitempty"`
+	NumJoinedMembers int                     `json:"num_joined_members"`
+	RoomID           id.RoomID               `json:"room_id"`
+	RoomType         event.RoomType          `json:"room_type"`
+	Topic            string                  `json:"topic,omitempty"`
+	WorldReadble     bool                    `json:"world_readable"`
+}
+
+type StrippedStateWithTime struct {
+	event.StrippedState
+	Timestamp jsontime.UnixMilli `json:"origin_server_ts"`
+}
+
+type RespAppservicePing struct {
+	DurationMS int64 `json:"duration_ms"`
+}
+
+type RespBeeperMergeRoom RespCreateRoom
+
+type RespBeeperSplitRoom struct {
+	RoomIDs map[string]id.RoomID `json:"room_ids"`
+}
+
+type RespTimestampToEvent struct {
+	EventID   id.EventID         `json:"event_id"`
+	Timestamp jsontime.UnixMilli `json:"origin_server_ts"`
+}
+
+type RespRoomKeysVersionCreate struct {
+	Version id.KeyBackupVersion `json:"version"`
+}
+
+type RespRoomKeysVersion[A any] struct {
+	Algorithm id.KeyBackupAlgorithm `json:"algorithm"`
+	AuthData  A                     `json:"auth_data"`
+	Count     int                   `json:"count"`
+	ETag      string                `json:"etag"`
+	Version   id.KeyBackupVersion   `json:"version"`
+}
+
+type RespRoomKeys[S any] struct {
+	Rooms map[id.RoomID]RespRoomKeyBackup[S] `json:"rooms"`
+}
+
+type RespRoomKeyBackup[S any] struct {
+	Sessions map[id.SessionID]RespKeyBackupData[S] `json:"sessions"`
+}
+
+type RespKeyBackupData[S any] struct {
+	FirstMessageIndex int  `json:"first_message_index"`
+	ForwardedCount    int  `json:"forwarded_count"`
+	IsVerified        bool `json:"is_verified"`
+	SessionData       S    `json:"session_data"`
+}
+
+type RespRoomKeysUpdate struct {
+	Count int    `json:"count"`
+	ETag  string `json:"etag"`
+}
diff --git a/vendor/maunium.net/go/mautrix/room.go b/vendor/maunium.net/go/mautrix/room.go
new file mode 100644
index 0000000..c3ddb7e
--- /dev/null
+++ b/vendor/maunium.net/go/mautrix/room.go
@@ -0,0 +1,54 @@
+package mautrix
+
+import (
+	"maunium.net/go/mautrix/event"
+	"maunium.net/go/mautrix/id"
+)
+
+type RoomStateMap = map[event.Type]map[string]*event.Event
+
+// Room represents a single Matrix room.
+type Room struct {
+	ID    id.RoomID
+	State RoomStateMap
+}
+
+// UpdateState updates the room's current state with the given Event. This will clobber events based
+// on the type/state_key combination.
+func (room Room) UpdateState(evt *event.Event) {
+	_, exists := room.State[evt.Type]
+	if !exists {
+		room.State[evt.Type] = make(map[string]*event.Event)
+	}
+	room.State[evt.Type][*evt.StateKey] = evt
+}
+
+// GetStateEvent returns the state event for the given type/state_key combo, or nil.
+func (room Room) GetStateEvent(eventType event.Type, stateKey string) *event.Event {
+	stateEventMap, _ := room.State[eventType]
+	evt, _ := stateEventMap[stateKey]
+	return evt
+}
+
+// GetMembershipState returns the membership state of the given user ID in this room. If there is
+// no entry for this member, 'leave' is returned for consistency with left users.
+func (room Room) GetMembershipState(userID id.UserID) event.Membership {
+	state := event.MembershipLeave
+	evt := room.GetStateEvent(event.StateMember, string(userID))
+	if evt != nil {
+		membership, ok := evt.Content.Raw["membership"].(string)
+		if ok {
+			state = event.Membership(membership)
+		}
+	}
+	return state
+}
+
+// NewRoom creates a new Room with the given ID
+func NewRoom(roomID id.RoomID) *Room {
+	// Init the State map and return a pointer to the Room
+	return &Room{
+		ID:    roomID,
+		State: make(RoomStateMap),
+	}
+}
diff --git a/vendor/maunium.net/go/mautrix/statestore.go b/vendor/maunium.net/go/mautrix/statestore.go
new file mode 100644
index 0000000..e728b88
--- /dev/null
+++ b/vendor/maunium.net/go/mautrix/statestore.go
@@ -0,0 +1,344 @@
+// Copyright (c) 2023 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 mautrix
+
+import (
+	"context"
+	"maps"
+	"sync"
+
+	"github.com/rs/zerolog"
+	"go.mau.fi/util/exerrors"
+
+	"maunium.net/go/mautrix/event"
+	"maunium.net/go/mautrix/id"
+)
+
+// StateStore is an interface for storing basic room state information.
+type StateStore interface {
+	IsInRoom(ctx context.Context, roomID id.RoomID, userID id.UserID) bool
+	IsInvited(ctx context.Context, roomID id.RoomID, userID id.UserID) bool
+	IsMembership(ctx context.Context, roomID id.RoomID, userID id.UserID, allowedMemberships ...event.Membership) bool
+	GetMember(ctx context.Context, roomID id.RoomID, userID id.UserID) (*event.MemberEventContent, error)
+	TryGetMember(ctx context.Context, roomID id.RoomID, userID id.UserID) (*event.MemberEventContent, error)
+	SetMembership(ctx context.Context, roomID id.RoomID, userID id.UserID, membership event.Membership) error
+	SetMember(ctx context.Context, roomID id.RoomID, userID id.UserID, member *event.MemberEventContent) error
+	IsConfusableName(ctx context.Context, roomID id.RoomID, currentUser id.UserID, name string) ([]id.UserID, error)
+	ClearCachedMembers(ctx context.Context, roomID id.RoomID, memberships ...event.Membership) error
+	ReplaceCachedMembers(ctx context.Context, roomID id.RoomID, evts []*event.Event, onlyMemberships ...event.Membership) error
+
+	SetPowerLevels(ctx context.Context, roomID id.RoomID, levels *event.PowerLevelsEventContent) error
+	GetPowerLevels(ctx context.Context, roomID id.RoomID) (*event.PowerLevelsEventContent, error)
+
+	HasFetchedMembers(ctx context.Context, roomID id.RoomID) (bool, error)
+	MarkMembersFetched(ctx context.Context, roomID id.RoomID) error
+	GetAllMembers(ctx context.Context, roomID id.RoomID) (map[id.UserID]*event.MemberEventContent, error)
+
+	SetEncryptionEvent(ctx context.Context, roomID id.RoomID, content *event.EncryptionEventContent) error
+	IsEncrypted(ctx context.Context, roomID id.RoomID) (bool, error)
+
+	GetRoomJoinedOrInvitedMembers(ctx context.Context, roomID id.RoomID) ([]id.UserID, error)
+}
+
+type StateStoreUpdater interface {
+	UpdateState(ctx context.Context, evt *event.Event)
+}
+
+func UpdateStateStore(ctx context.Context, store StateStore, evt *event.Event) {
+	if store == nil || evt == nil || evt.StateKey == nil {
+		return
+	}
+	if directUpdater, ok := store.(StateStoreUpdater); ok {
+		directUpdater.UpdateState(ctx, evt)
+		return
+	}
+	// We only care about events without a state key (power levels, encryption) or member events with state key
+	if evt.Type != event.StateMember && evt.GetStateKey() != "" {
+		return
+	}
+	var err error
+	switch content := evt.Content.Parsed.(type) {
+	case *event.MemberEventContent:
+		err = store.SetMember(ctx, evt.RoomID, id.UserID(evt.GetStateKey()), content)
+	case *event.PowerLevelsEventContent:
+		err = store.SetPowerLevels(ctx, evt.RoomID, content)
+	case *event.EncryptionEventContent:
+		err = store.SetEncryptionEvent(ctx, evt.RoomID, content)
+	default:
+		switch evt.Type {
+		case event.StateMember, event.StatePowerLevels, event.StateEncryption:
+			zerolog.Ctx(ctx).Warn().
+				Stringer("event_id", evt.ID).
+				Str("event_type", evt.Type.Type).
+				Type("content_type", evt.Content.Parsed).
+				Msg("Got known event type with unknown content type in UpdateStateStore")
+		}
+	}
+	if err != nil {
+		zerolog.Ctx(ctx).Warn().Err(err).
+			Stringer("event_id", evt.ID).
+			Str("event_type", evt.Type.Type).
+			Msg("Failed to update state store")
+	}
+}
+
+// StateStoreSyncHandler can be added as an event handler in the syncer to update the state store automatically.
+//
+//	client.Syncer.(mautrix.ExtensibleSyncer).OnEvent(client.StateStoreSyncHandler)
+//
+// DefaultSyncer.ParseEventContent must also be true for this to work (which it is by default).
+func (cli *Client) StateStoreSyncHandler(ctx context.Context, evt *event.Event) {
+	UpdateStateStore(ctx, cli.StateStore, evt)
+}
+
+type MemoryStateStore struct {
+	Registrations  map[id.UserID]bool                                    `json:"registrations"`
+	Members        map[id.RoomID]map[id.UserID]*event.MemberEventContent `json:"memberships"`
+	MembersFetched map[id.RoomID]bool                                    `json:"members_fetched"`
+	PowerLevels    map[id.RoomID]*event.PowerLevelsEventContent          `json:"power_levels"`
+	Encryption     map[id.RoomID]*event.EncryptionEventContent           `json:"encryption"`
+
+	registrationsLock sync.RWMutex
+	membersLock       sync.RWMutex
+	powerLevelsLock   sync.RWMutex
+	encryptionLock    sync.RWMutex
+}
+
+func NewMemoryStateStore() StateStore {
+	return &MemoryStateStore{
+		Registrations:  make(map[id.UserID]bool),
+		Members:        make(map[id.RoomID]map[id.UserID]*event.MemberEventContent),
+		MembersFetched: make(map[id.RoomID]bool),
+		PowerLevels:    make(map[id.RoomID]*event.PowerLevelsEventContent),
+		Encryption:     make(map[id.RoomID]*event.EncryptionEventContent),
+	}
+}
+
+func (store *MemoryStateStore) IsRegistered(_ context.Context, userID id.UserID) (bool, error) {
+	store.registrationsLock.RLock()
+	defer store.registrationsLock.RUnlock()
+	registered, ok := store.Registrations[userID]
+	return ok && registered, nil
+}
+
+func (store *MemoryStateStore) MarkRegistered(_ context.Context, userID id.UserID) error {
+	store.registrationsLock.Lock()
+	defer store.registrationsLock.Unlock()
+	store.Registrations[userID] = true
+	return nil
+}
+
+func (store *MemoryStateStore) GetRoomMembers(_ context.Context, roomID id.RoomID) (map[id.UserID]*event.MemberEventContent, error) {
+	store.membersLock.RLock()
+	members, ok := store.Members[roomID]
+	store.membersLock.RUnlock()
+	if !ok {
+		members = make(map[id.UserID]*event.MemberEventContent)
+		store.membersLock.Lock()
+		store.Members[roomID] = members
+		store.membersLock.Unlock()
+	}
+	return members, nil
+}
+
+func (store *MemoryStateStore) GetRoomJoinedOrInvitedMembers(ctx context.Context, roomID id.RoomID) ([]id.UserID, error) {
+	members, err := store.GetRoomMembers(ctx, roomID)
+	if err != nil {
+		return nil, err
+	}
+	ids := make([]id.UserID, 0, len(members))
+	for id := range members {
+		ids = append(ids, id)
+	}
+	return ids, nil
+}
+
+func (store *MemoryStateStore) GetMembership(ctx context.Context, roomID id.RoomID, userID id.UserID) (event.Membership, error) {
+	return exerrors.Must(store.GetMember(ctx, roomID, userID)).Membership, nil
+}
+
+func (store *MemoryStateStore) GetMember(ctx context.Context, roomID id.RoomID, userID id.UserID) (*event.MemberEventContent, error) {
+	member, err := store.TryGetMember(ctx, roomID, userID)
+	if member == nil && err == nil {
+		member = &event.MemberEventContent{Membership: event.MembershipLeave}
+	}
+	return member, err
+}
+
+func (store *MemoryStateStore) IsConfusableName(ctx context.Context, roomID id.RoomID, currentUser id.UserID, name string) ([]id.UserID, error) {
+	// TODO implement?
+	return nil, nil
+}
+
+func (store *MemoryStateStore) TryGetMember(_ context.Context, roomID id.RoomID, userID id.UserID) (member *event.MemberEventContent, err error) {
+	store.membersLock.RLock()
+	defer store.membersLock.RUnlock()
+	members, membersOk := store.Members[roomID]
+	if !membersOk {
+		return
+	}
+	member = members[userID]
+	return
+}
+
+func (store *MemoryStateStore) IsInRoom(ctx context.Context, roomID id.RoomID, userID id.UserID) bool {
+	return store.IsMembership(ctx, roomID, userID, "join")
+}
+
+func (store *MemoryStateStore) IsInvited(ctx context.Context, roomID id.RoomID, userID id.UserID) bool {
+	return store.IsMembership(ctx, roomID, userID, "join", "invite")
+}
+
+func (store *MemoryStateStore) IsMembership(ctx context.Context, roomID id.RoomID, userID id.UserID, allowedMemberships ...event.Membership) bool {
+	membership := exerrors.Must(store.GetMembership(ctx, roomID, userID))
+	for _, allowedMembership := range allowedMemberships {
+		if allowedMembership == membership {
+			return true
+		}
+	}
+	return false
+}
+
+func (store *MemoryStateStore) SetMembership(_ context.Context, roomID id.RoomID, userID id.UserID, membership event.Membership) error {
+	store.membersLock.Lock()
+	members, ok := store.Members[roomID]
+	if !ok {
+		members = map[id.UserID]*event.MemberEventContent{
+			userID: {Membership: membership},
+		}
+	} else {
+		member, ok := members[userID]
+		if !ok {
+			members[userID] = &event.MemberEventContent{Membership: membership}
+		} else {
+			member.Membership = membership
+			members[userID] = member
+		}
+	}
+	store.Members[roomID] = members
+	store.membersLock.Unlock()
+	return nil
+}
+
+func (store *MemoryStateStore) SetMember(_ context.Context, roomID id.RoomID, userID id.UserID, member *event.MemberEventContent) error {
+	store.membersLock.Lock()
+	members, ok := store.Members[roomID]
+	if !ok {
+		members = map[id.UserID]*event.MemberEventContent{
+			userID: member,
+		}
+	} else {
+		members[userID] = member
+	}
+	store.Members[roomID] = members
+	store.membersLock.Unlock()
+	return nil
+}
+
+func (store *MemoryStateStore) ClearCachedMembers(_ context.Context, roomID id.RoomID, memberships ...event.Membership) error {
+	store.membersLock.Lock()
+	defer store.membersLock.Unlock()
+	members, ok := store.Members[roomID]
+	if !ok {
+		return nil
+	}
+	for userID, member := range members {
+		for _, membership := range memberships {
+			if membership == member.Membership {
+				delete(members, userID)
+				break
+			}
+		}
+	}
+	store.MembersFetched[roomID] = false
+	return nil
+}
+
+func (store *MemoryStateStore) HasFetchedMembers(ctx context.Context, roomID id.RoomID) (bool, error) {
+	store.membersLock.RLock()
+	defer store.membersLock.RUnlock()
+	return store.MembersFetched[roomID], nil
+}
+
+func (store *MemoryStateStore) MarkMembersFetched(ctx context.Context, roomID id.RoomID) error {
+	store.membersLock.Lock()
+	defer store.membersLock.Unlock()
+	store.MembersFetched[roomID] = true
+	return nil
+}
+
+func (store *MemoryStateStore) ReplaceCachedMembers(ctx context.Context, roomID id.RoomID, evts []*event.Event, onlyMemberships ...event.Membership) error {
+	_ = store.ClearCachedMembers(ctx, roomID, onlyMemberships...)
+	for _, evt := range evts {
+		UpdateStateStore(ctx, store, evt)
+	}
+	if len(onlyMemberships) == 0 {
+		_ = store.MarkMembersFetched(ctx, roomID)
+	}
+	return nil
+}
+
+func (store *MemoryStateStore) GetAllMembers(ctx context.Context, roomID id.RoomID) (map[id.UserID]*event.MemberEventContent, error) {
+	store.membersLock.RLock()
+	defer store.membersLock.RUnlock()
+	return maps.Clone(store.Members[roomID]), nil
+}
+
+func (store *MemoryStateStore) SetPowerLevels(_ context.Context, roomID id.RoomID, levels *event.PowerLevelsEventContent) error {
+	store.powerLevelsLock.Lock()
+	store.PowerLevels[roomID] = levels
+	store.powerLevelsLock.Unlock()
+	return nil
+}
+
+func (store *MemoryStateStore) GetPowerLevels(_ context.Context, roomID id.RoomID) (levels *event.PowerLevelsEventContent, err error) {
+	store.powerLevelsLock.RLock()
+	levels = store.PowerLevels[roomID]
+	store.powerLevelsLock.RUnlock()
+	return
+}
+
+func (store *MemoryStateStore) GetPowerLevel(ctx context.Context, roomID id.RoomID, userID id.UserID) (int, error) {
+	return exerrors.Must(store.GetPowerLevels(ctx, roomID)).GetUserLevel(userID), nil
+}
+
+func (store *MemoryStateStore) GetPowerLevelRequirement(ctx context.Context, roomID id.RoomID, eventType event.Type) (int, error) {
+	return exerrors.Must(store.GetPowerLevels(ctx, roomID)).GetEventLevel(eventType), nil
+}
+
+func (store *MemoryStateStore) HasPowerLevel(ctx context.Context, roomID id.RoomID, userID id.UserID, eventType event.Type) (bool, error) {
+	return exerrors.Must(store.GetPowerLevel(ctx, roomID, userID)) >= exerrors.Must(store.GetPowerLevelRequirement(ctx, roomID, eventType)), nil
+}
+
+func (store *MemoryStateStore) SetEncryptionEvent(_ context.Context, roomID id.RoomID, content *event.EncryptionEventContent) error {
+	store.encryptionLock.Lock()
+	store.Encryption[roomID] = content
+	store.encryptionLock.Unlock()
+	return nil
+}
+
+func (store *MemoryStateStore) GetEncryptionEvent(_ context.Context, roomID id.RoomID) (*event.EncryptionEventContent, error) {
+	store.encryptionLock.RLock()
+	defer store.encryptionLock.RUnlock()
+	return store.Encryption[roomID], nil
+}
+
+func (store *MemoryStateStore) IsEncrypted(ctx context.Context, roomID id.RoomID) (bool, error) {
+	cfg, err := store.GetEncryptionEvent(ctx, roomID)
+	return cfg != nil && cfg.Algorithm == id.AlgorithmMegolmV1, err
+}
+
+func (store *MemoryStateStore) FindSharedRooms(ctx context.Context, userID id.UserID) (rooms []id.RoomID, err error) {
+	store.membersLock.RLock()
+	defer store.membersLock.RUnlock()
+	for roomID, members := range store.Members {
+		if _, ok := members[userID]; ok {
+			rooms = append(rooms, roomID)
+		}
+	}
+	return rooms, nil
+}
diff --git a/vendor/maunium.net/go/mautrix/sync.go b/vendor/maunium.net/go/mautrix/sync.go
new file mode 100644
index 0000000..d420840
--- /dev/null
+++ b/vendor/maunium.net/go/mautrix/sync.go
@@ -0,0 +1,284 @@
+// Copyright (c) 2024 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 mautrix
+
+import (
+	"context"
+	"errors"
+	"fmt"
+	"runtime/debug"
+	"time"
+
+	"maunium.net/go/mautrix/event"
+	"maunium.net/go/mautrix/id"
+)
+
+// EventHandler handles a single event from a sync response.
+type EventHandler func(ctx context.Context, evt *event.Event)
+
+// SyncHandler handles a whole sync response. If the return value is false, handling will be stopped completely.
+type SyncHandler func(ctx context.Context, resp *RespSync, since string) bool
+
+// Syncer is an interface that must be satisfied in order to do /sync requests on a client.
+type Syncer interface {
+	// ProcessResponse processes the /sync response. The since parameter is the since= value that was used to produce the response.
+	// This is useful for detecting the very first sync (since=""). If an error is return, Syncing will be stopped permanently.
+	ProcessResponse(ctx context.Context, resp *RespSync, since string) error
+	// OnFailedSync returns either the time to wait before retrying or an error to stop syncing permanently.
+	OnFailedSync(res *RespSync, err error) (time.Duration, error)
+	// GetFilterJSON for the given user ID. NOT the filter ID.
+	GetFilterJSON(userID id.UserID) *Filter
+}
+
+type ExtensibleSyncer interface {
+	OnSync(callback SyncHandler)
+	OnEvent(callback EventHandler)
+	OnEventType(eventType event.Type, callback EventHandler)
+}
+
+type DispatchableSyncer interface {
+	Dispatch(ctx context.Context, evt *event.Event)
+}
+
+// DefaultSyncer is the default syncing implementation. You can either write your own syncer, or selectively
+// replace parts of this default syncer (e.g. the ProcessResponse method). The default syncer uses the observer
+// pattern to notify callers about incoming events. See DefaultSyncer.OnEventType for more information.
+type DefaultSyncer struct {
+	// syncListeners want the whole sync response, e.g. the crypto machine
+	syncListeners []SyncHandler
+	// globalListeners want all events
+	globalListeners []EventHandler
+	// listeners want a specific event type
+	listeners map[event.Type][]EventHandler
+	// ParseEventContent determines whether or not event content should be parsed before passing to handlers.
+	ParseEventContent bool
+	// ParseErrorHandler is called when event.Content.ParseRaw returns an error.
+	// If it returns false, the event will not be forwarded to listeners.
+	ParseErrorHandler func(evt *event.Event, err error) bool
+	// FilterJSON is used when the client starts syncing and doesn't get an existing filter ID from SyncStore's LoadFilterID.
+	FilterJSON *Filter
+}
+
+var _ Syncer = (*DefaultSyncer)(nil)
+var _ ExtensibleSyncer = (*DefaultSyncer)(nil)
+
+// NewDefaultSyncer returns an instantiated DefaultSyncer
+func NewDefaultSyncer() *DefaultSyncer {
+	return &DefaultSyncer{
+		listeners:         make(map[event.Type][]EventHandler),
+		syncListeners:     []SyncHandler{},
+		globalListeners:   []EventHandler{},
+		ParseEventContent: true,
+		ParseErrorHandler: func(evt *event.Event, err error) bool {
+			// By default, drop known events that can't be parsed, but let unknown events through
+			return errors.Is(err, event.ErrUnsupportedContentType) ||
+				// Also allow events that had their content already parsed by some other function
+				errors.Is(err, event.ErrContentAlreadyParsed)
+		},
+	}
+}
+
+// ProcessResponse processes the /sync response in a way suitable for bots. "Suitable for bots" means a stream of
+// unrepeating events. Returns a fatal error if a listener panics.
+func (s *DefaultSyncer) ProcessResponse(ctx context.Context, res *RespSync, since string) (err error) {
+	defer func() {
+		if r := recover(); r != nil {
+			err = fmt.Errorf("ProcessResponse panicked! since=%s panic=%s\n%s", since, r, debug.Stack())
+		}
+	}()
+
+	for _, listener := range s.syncListeners {
+		if !listener(ctx, res, since) {
+			return
+		}
+	}
+
+	s.processSyncEvents(ctx, "", res.ToDevice.Events, event.SourceToDevice)
+	s.processSyncEvents(ctx, "", res.Presence.Events, event.SourcePresence)
+	s.processSyncEvents(ctx, "", res.AccountData.Events, event.SourceAccountData)
+
+	for roomID, roomData := range res.Rooms.Join {
+		s.processSyncEvents(ctx, roomID, roomData.State.Events, event.SourceJoin|event.SourceState)
+		s.processSyncEvents(ctx, roomID, roomData.Timeline.Events, event.SourceJoin|event.SourceTimeline)
+		s.processSyncEvents(ctx, roomID, roomData.Ephemeral.Events, event.SourceJoin|event.SourceEphemeral)
+		s.processSyncEvents(ctx, roomID, roomData.AccountData.Events, event.SourceJoin|event.SourceAccountData)
+	}
+	for roomID, roomData := range res.Rooms.Invite {
+		s.processSyncEvents(ctx, roomID, roomData.State.Events, event.SourceInvite|event.SourceState)
+	}
+	for roomID, roomData := range res.Rooms.Leave {
+		s.processSyncEvents(ctx, roomID, roomData.State.Events, event.SourceLeave|event.SourceState)
+		s.processSyncEvents(ctx, roomID, roomData.Timeline.Events, event.SourceLeave|event.SourceTimeline)
+	}
+	return
+}
+
+func (s *DefaultSyncer) processSyncEvents(ctx context.Context, roomID id.RoomID, events []*event.Event, source event.Source) {
+	for _, evt := range events {
+		s.processSyncEvent(ctx, roomID, evt, source)
+	}
+}
+
+func (s *DefaultSyncer) processSyncEvent(ctx context.Context, roomID id.RoomID, evt *event.Event, source event.Source) {
+	evt.RoomID = roomID
+
+	// Ensure the type class is correct. It's safe to mutate the class since the event type is not a pointer.
+	// Listeners are keyed by type structs, which means only the correct class will pass.
+	switch {
+	case evt.StateKey != nil:
+		evt.Type.Class = event.StateEventType
+	case source == event.SourcePresence, source&event.SourceEphemeral != 0:
+		evt.Type.Class = event.EphemeralEventType
+	case source&event.SourceAccountData != 0:
+		evt.Type.Class = event.AccountDataEventType
+	case source == event.SourceToDevice:
+		evt.Type.Class = event.ToDeviceEventType
+	default:
+		evt.Type.Class = event.MessageEventType
+	}
+
+	if s.ParseEventContent {
+		err := evt.Content.ParseRaw(evt.Type)
+		if err != nil && !s.ParseErrorHandler(evt, err) {
+			return
+		}
+	}
+
+	evt.Mautrix.EventSource = source
+	s.Dispatch(ctx, evt)
+}
+
+func (s *DefaultSyncer) Dispatch(ctx context.Context, evt *event.Event) {
+	for _, fn := range s.globalListeners {
+		fn(ctx, evt)
+	}
+	listeners, exists := s.listeners[evt.Type]
+	if exists {
+		for _, fn := range listeners {
+			fn(ctx, evt)
+		}
+	}
+}
+
+// OnEventType allows callers to be notified when there are new events for the given event type.
+// There are no duplicate checks.
+func (s *DefaultSyncer) OnEventType(eventType event.Type, callback EventHandler) {
+	_, exists := s.listeners[eventType]
+	if !exists {
+		s.listeners[eventType] = []EventHandler{}
+	}
+	s.listeners[eventType] = append(s.listeners[eventType], callback)
+}
+
+func (s *DefaultSyncer) OnSync(callback SyncHandler) {
+	s.syncListeners = append(s.syncListeners, callback)
+}
+
+func (s *DefaultSyncer) OnEvent(callback EventHandler) {
+	s.globalListeners = append(s.globalListeners, callback)
+}
+
+// OnFailedSync always returns a 10 second wait period between failed /syncs, never a fatal error.
+func (s *DefaultSyncer) OnFailedSync(res *RespSync, err error) (time.Duration, error) {
+	if errors.Is(err, MUnknownToken) {
+		return 0, err
+	}
+	return 10 * time.Second, nil
+}
+
+var defaultFilter = Filter{
+	Room: RoomFilter{
+		Timeline: FilterPart{
+			Limit: 50,
+		},
+	},
+}
+
+// GetFilterJSON returns a filter with a timeline limit of 50.
+func (s *DefaultSyncer) GetFilterJSON(userID id.UserID) *Filter {
+	if s.FilterJSON == nil {
+		defaultFilterCopy := defaultFilter
+		s.FilterJSON = &defaultFilterCopy
+	}
+	return s.FilterJSON
+}
+
+// DontProcessOldEvents is a sync handler that removes rooms that the user just joined.
+// It's meant for bots to ignore events from before the bot joined the room.
+//
+// To use it, register it with your Syncer, e.g.:
+//
+//	cli.Syncer.(mautrix.ExtensibleSyncer).OnSync(cli.DontProcessOldEvents)
+func (cli *Client) DontProcessOldEvents(_ context.Context, resp *RespSync, since string) bool {
+	return dontProcessOldEvents(cli.UserID, resp, since)
+}
+
+var _ SyncHandler = (*Client)(nil).DontProcessOldEvents
+
+func dontProcessOldEvents(userID id.UserID, resp *RespSync, since string) bool {
+	if since == "" {
+		return false
+	}
+	// This is a horrible hack because /sync will return the most recent messages for a room
+	// as soon as you /join it. We do NOT want to process those events in that particular room
+	// because they may have already been processed (if you toggle the bot in/out of the room).
+	//
+	// Work around this by inspecting each room's timeline and seeing if an m.room.member event for us
+	// exists and is "join" and then discard processing that room entirely if so.
+	// TODO: We probably want to process messages from after the last join event in the timeline.
+	for roomID, roomData := range resp.Rooms.Join {
+		for i := len(roomData.Timeline.Events) - 1; i >= 0; i-- {
+			evt := roomData.Timeline.Events[i]
+			if evt.Type == event.StateMember && evt.GetStateKey() == string(userID) {
+				membership, _ := evt.Content.Raw["membership"].(string)
+				if membership == "join" {
+					_, ok := resp.Rooms.Join[roomID]
+					if !ok {
+						continue
+					}
+					delete(resp.Rooms.Join, roomID)   // don't re-process messages
+					delete(resp.Rooms.Invite, roomID) // don't re-process invites
+					break
+				}
+			}
+		}
+	}
+	return true
+}
+
+// MoveInviteState is a sync handler that moves events from the state event list to the InviteRoomState in the invite event.
+//
+// To use it, register it with your Syncer, e.g.:
+//
+//	cli.Syncer.(mautrix.ExtensibleSyncer).OnSync(cli.MoveInviteState)
+func (cli *Client) MoveInviteState(ctx context.Context, resp *RespSync, _ string) bool {
+	for _, meta := range resp.Rooms.Invite {
+		var inviteState []event.StrippedState
+		var inviteEvt *event.Event
+		for _, evt := range meta.State.Events {
+			if evt.Type == event.StateMember && evt.GetStateKey() == cli.UserID.String() {
+				inviteEvt = evt
+			} else {
+				evt.Type.Class = event.StateEventType
+				_ = evt.Content.ParseRaw(evt.Type)
+				inviteState = append(inviteState, event.StrippedState{
+					Content:  evt.Content,
+					Type:     evt.Type,
+					StateKey: evt.GetStateKey(),
+					Sender:   evt.Sender,
+				})
+			}
+		}
+		if inviteEvt != nil {
+			inviteEvt.Unsigned.InviteRoomState = inviteState
+			meta.State.Events = []*event.Event{inviteEvt}
+		}
+	}
+	return true
+}
+
+var _ SyncHandler = (*Client)(nil).MoveInviteState
diff --git a/vendor/maunium.net/go/mautrix/syncstore.go b/vendor/maunium.net/go/mautrix/syncstore.go
new file mode 100644
index 0000000..6c7fc9e
--- /dev/null
+++ b/vendor/maunium.net/go/mautrix/syncstore.go
@@ -0,0 +1,175 @@
+package mautrix
+
+import (
+	"context"
+	"errors"
+	"fmt"
+
+	"maunium.net/go/mautrix/id"
+)
+
+var _ SyncStore = (*MemorySyncStore)(nil)
+var _ SyncStore = (*AccountDataStore)(nil)
+
+// SyncStore is an interface which must be satisfied to store client data.
+//
+// You can either write a struct which persists this data to disk, or you can use the
+// provided "MemorySyncStore" which just keeps data around in-memory which is lost on
+// restarts.
+type SyncStore interface {
+	SaveFilterID(ctx context.Context, userID id.UserID, filterID string) error
+	LoadFilterID(ctx context.Context, userID id.UserID) (string, error)
+	SaveNextBatch(ctx context.Context, userID id.UserID, nextBatchToken string) error
+	LoadNextBatch(ctx context.Context, userID id.UserID) (string, error)
+}
+
+// Deprecated: renamed to SyncStore
+type Storer = SyncStore
+
+// MemorySyncStore implements the Storer interface.
+//
+// Everything is persisted in-memory as maps. It is not safe to load/save filter IDs
+// or next batch tokens on any goroutine other than the syncing goroutine: the one
+// which called Client.Sync().
+type MemorySyncStore struct {
+	Filters   map[id.UserID]string
+	NextBatch map[id.UserID]string
+}
+
+// SaveFilterID to memory.
+func (s *MemorySyncStore) SaveFilterID(ctx context.Context, userID id.UserID, filterID string) error {
+	s.Filters[userID] = filterID
+	return nil
+}
+
+// LoadFilterID from memory.
+func (s *MemorySyncStore) LoadFilterID(ctx context.Context, userID id.UserID) (string, error) {
+	return s.Filters[userID], nil
+}
+
+// SaveNextBatch to memory.
+func (s *MemorySyncStore) SaveNextBatch(ctx context.Context, userID id.UserID, nextBatchToken string) error {
+	s.NextBatch[userID] = nextBatchToken
+	return nil
+}
+
+// LoadNextBatch from memory.
+func (s *MemorySyncStore) LoadNextBatch(ctx context.Context, userID id.UserID) (string, error) {
+	return s.NextBatch[userID], nil
+}
+
+// NewMemorySyncStore constructs a new MemorySyncStore.
+func NewMemorySyncStore() *MemorySyncStore {
+	return &MemorySyncStore{
+		Filters:   make(map[id.UserID]string),
+		NextBatch: make(map[id.UserID]string),
+	}
+}
+
+// AccountDataStore uses account data to store the next batch token, and stores the filter ID in memory
+// (as filters can be safely recreated every startup).
+type AccountDataStore struct {
+	FilterID  string
+	EventType string
+	client    *Client
+	nextBatch string
+}
+
+type accountData struct {
+	NextBatch string `json:"next_batch"`
+}
+
+func (s *AccountDataStore) SaveFilterID(ctx context.Context, userID id.UserID, filterID string) error {
+	if userID.String() != s.client.UserID.String() {
+		panic("AccountDataStore must only be used with a single account")
+	}
+	s.FilterID = filterID
+	return nil
+}
+
+func (s *AccountDataStore) LoadFilterID(ctx context.Context, userID id.UserID) (string, error) {
+	if userID.String() != s.client.UserID.String() {
+		panic("AccountDataStore must only be used with a single account")
+	}
+	return s.FilterID, nil
+}
+
+func (s *AccountDataStore) SaveNextBatch(ctx context.Context, userID id.UserID, nextBatchToken string) error {
+	if userID.String() != s.client.UserID.String() {
+		panic("AccountDataStore must only be used with a single account")
+	} else if nextBatchToken == s.nextBatch {
+		return nil
+	}
+
+	data := accountData{
+		NextBatch: nextBatchToken,
+	}
+
+	err := s.client.SetAccountData(ctx, s.EventType, data)
+	if err != nil {
+		return fmt.Errorf("failed to save next batch token to account data: %w", err)
+	} else {
+		s.client.Log.Debug().
+			Str("old_token", s.nextBatch).
+			Str("new_token", nextBatchToken).
+			Msg("Saved next batch token")
+		s.nextBatch = nextBatchToken
+	}
+	return nil
+}
+
+func (s *AccountDataStore) LoadNextBatch(ctx context.Context, userID id.UserID) (string, error) {
+	if userID.String() != s.client.UserID.String() {
+		panic("AccountDataStore must only be used with a single account")
+	}
+
+	data := &accountData{}
+
+	err := s.client.GetAccountData(ctx, s.EventType, data)
+	if err != nil {
+		if errors.Is(err, MNotFound) {
+			s.client.Log.Debug().Msg("No next batch token found in account data")
+			return "", nil
+		} else {
+			return "", fmt.Errorf("failed to load next batch token from account data: %w", err)
+		}
+	}
+	s.nextBatch = data.NextBatch
+	s.client.Log.Debug().Str("next_batch", data.NextBatch).Msg("Loaded next batch token from account data")
+
+	return s.nextBatch, nil
+}
+
+// NewAccountDataStore returns a new AccountDataStore, which stores
+// the next_batch token as a custom event in account data in the
+// homeserver.
+//
+// AccountDataStore is only appropriate for bots, not appservices.
+//
+// The event type should be a reversed DNS name like tld.domain.sub.internal and
+// must be unique for a client. The data stored in it is considered internal
+// and must not be modified through outside means. You should also add a filter
+// for account data changes of this event type, to avoid ending up in a sync
+// loop:
+//
+//	filter := mautrix.Filter{
+//		AccountData: mautrix.FilterPart{
+//			Limit: 20,
+//			NotTypes: []event.Type{
+//				event.NewEventType(eventType),
+//			},
+//		},
+//	}
+//	// If you use a custom Syncer, set the filter there, not like this
+//	client.Syncer.(*mautrix.DefaultSyncer).FilterJSON = &filter
+//	client.Store = mautrix.NewAccountDataStore("com.example.mybot.store", client)
+//	go func() {
+//		err := client.Sync()
+//		// don't forget to check err
+//	}()
+func NewAccountDataStore(eventType string, client *Client) *AccountDataStore {
+	return &AccountDataStore{
+		EventType: eventType,
+		client:    client,
+	}
+}
diff --git a/vendor/maunium.net/go/mautrix/url.go b/vendor/maunium.net/go/mautrix/url.go
new file mode 100644
index 0000000..f35ae5e
--- /dev/null
+++ b/vendor/maunium.net/go/mautrix/url.go
@@ -0,0 +1,116 @@
+// 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 mautrix
+
+import (
+	"fmt"
+	"net/url"
+	"strconv"
+	"strings"
+)
+
+func ParseAndNormalizeBaseURL(homeserverURL string) (*url.URL, error) {
+	hsURL, err := url.Parse(homeserverURL)
+	if err != nil {
+		return nil, err
+	}
+	if hsURL.Scheme == "" {
+		hsURL.Scheme = "https"
+		fixedURL := hsURL.String()
+		hsURL, err = url.Parse(fixedURL)
+		if err != nil {
+			return nil, fmt.Errorf("failed to parse fixed URL '%s': %v", fixedURL, err)
+		}
+	}
+	hsURL.RawPath = hsURL.EscapedPath()
+	return hsURL, nil
+}
+
+// BuildURL builds a URL with the given path parts
+func BuildURL(baseURL *url.URL, path ...any) *url.URL {
+	createdURL := *baseURL
+	rawParts := make([]string, len(path)+1)
+	rawParts[0] = strings.TrimSuffix(createdURL.RawPath, "/")
+	parts := make([]string, len(path)+1)
+	parts[0] = strings.TrimSuffix(createdURL.Path, "/")
+	for i, part := range path {
+		switch casted := part.(type) {
+		case string:
+			parts[i+1] = casted
+		case int:
+			parts[i+1] = strconv.Itoa(casted)
+		case fmt.Stringer:
+			parts[i+1] = casted.String()
+		default:
+			parts[i+1] = fmt.Sprint(casted)
+		}
+		rawParts[i+1] = url.PathEscape(parts[i+1])
+	}
+	createdURL.Path = strings.Join(parts, "/")
+	createdURL.RawPath = strings.Join(rawParts, "/")
+	return &createdURL
+}
+
+// BuildURL builds a URL with the Client's homeserver and appservice user ID set already.
+func (cli *Client) BuildURL(urlPath PrefixableURLPath) string {
+	return cli.BuildURLWithQuery(urlPath, nil)
+}
+
+// BuildClientURL builds a URL with the Client's homeserver and appservice user ID set already.
+// This method also automatically prepends the client API prefix (/_matrix/client).
+func (cli *Client) BuildClientURL(urlPath ...any) string {
+	return cli.BuildURLWithQuery(ClientURLPath(urlPath), nil)
+}
+
+type PrefixableURLPath interface {
+	FullPath() []any
+}
+
+type BaseURLPath []any
+
+func (bup BaseURLPath) FullPath() []any {
+	return bup
+}
+
+type ClientURLPath []any
+
+func (cup ClientURLPath) FullPath() []any {
+	return append([]any{"_matrix", "client"}, []any(cup)...)
+}
+
+type MediaURLPath []any
+
+func (mup MediaURLPath) FullPath() []any {
+	return append([]any{"_matrix", "media"}, []any(mup)...)
+}
+
+type SynapseAdminURLPath []any
+
+func (saup SynapseAdminURLPath) FullPath() []any {
+	return append([]any{"_synapse", "admin"}, []any(saup)...)
+}
+
+// BuildURLWithQuery builds a URL with query parameters in addition to the Client's homeserver
+// and appservice user ID set already.
+func (cli *Client) BuildURLWithQuery(urlPath PrefixableURLPath, urlQuery map[string]string) string {
+	hsURL := *BuildURL(cli.HomeserverURL, urlPath.FullPath()...)
+	query := hsURL.Query()
+	if cli.SetAppServiceUserID {
+		query.Set("user_id", string(cli.UserID))
+	}
+	if cli.SetAppServiceDeviceID && cli.DeviceID != "" {
+		query.Set("device_id", string(cli.DeviceID))
+		query.Set("org.matrix.msc3202.device_id", string(cli.DeviceID))
+	}
+	if urlQuery != nil {
+		for k, v := range urlQuery {
+			query.Set(k, v)
+		}
+	}
+	hsURL.RawQuery = query.Encode()
+	return hsURL.String()
+}
diff --git a/vendor/maunium.net/go/mautrix/version.go b/vendor/maunium.net/go/mautrix/version.go
new file mode 100644
index 0000000..2936857
--- /dev/null
+++ b/vendor/maunium.net/go/mautrix/version.go
@@ -0,0 +1,31 @@
+package mautrix
+
+import (
+	"fmt"
+	"regexp"
+	"runtime"
+	"strings"
+)
+
+const Version = "v0.21.1"
+
+var GoModVersion = ""
+var Commit = ""
+var VersionWithCommit = Version
+
+var DefaultUserAgent = "mautrix-go/" + Version + " go/" + strings.TrimPrefix(runtime.Version(), "go")
+
+var goModVersionRegex = regexp.MustCompile(`v.+\d{14}-([0-9a-f]{12})`)
+
+func init() {
+	if GoModVersion != "" {
+		match := goModVersionRegex.FindStringSubmatch(GoModVersion)
+		if match != nil {
+			Commit = match[1]
+		}
+	}
+	if Commit != "" {
+		VersionWithCommit = fmt.Sprintf("%s+dev.%s", Version, Commit[:8])
+		DefaultUserAgent = strings.Replace(DefaultUserAgent, "mautrix-go/"+Version, "mautrix-go/"+VersionWithCommit, 1)
+	}
+}
diff --git a/vendor/maunium.net/go/mautrix/versions.go b/vendor/maunium.net/go/mautrix/versions.go
new file mode 100644
index 0000000..672018f
--- /dev/null
+++ b/vendor/maunium.net/go/mautrix/versions.go
@@ -0,0 +1,196 @@
+// 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 mautrix
+
+import (
+	"fmt"
+	"regexp"
+	"strconv"
+)
+
+// RespVersions is the JSON response for https://spec.matrix.org/v1.2/client-server-api/#get_matrixclientversions
+type RespVersions struct {
+	Versions         []SpecVersion   `json:"versions"`
+	UnstableFeatures map[string]bool `json:"unstable_features"`
+}
+
+func (versions *RespVersions) ContainsFunc(match func(found SpecVersion) bool) bool {
+	if versions == nil {
+		return false
+	}
+	for _, found := range versions.Versions {
+		if match(found) {
+			return true
+		}
+	}
+	return false
+}
+
+func (versions *RespVersions) Contains(version SpecVersion) bool {
+	return versions.ContainsFunc(func(found SpecVersion) bool {
+		return found == version
+	})
+}
+
+func (versions *RespVersions) ContainsGreaterOrEqual(version SpecVersion) bool {
+	return versions.ContainsFunc(func(found SpecVersion) bool {
+		return found.GreaterThan(version) || found == version
+	})
+}
+
+func (versions *RespVersions) GetLatest() (latest SpecVersion) {
+	if versions == nil {
+		return
+	}
+	for _, ver := range versions.Versions {
+		if ver.GreaterThan(latest) {
+			latest = ver
+		}
+	}
+	return
+}
+
+type UnstableFeature struct {
+	UnstableFlag string
+	SpecVersion  SpecVersion
+}
+
+var (
+	FeatureAsyncUploads       = UnstableFeature{UnstableFlag: "fi.mau.msc2246.stable", SpecVersion: SpecV17}
+	FeatureAppservicePing     = UnstableFeature{UnstableFlag: "fi.mau.msc2659.stable", SpecVersion: SpecV17}
+	FeatureAuthenticatedMedia = UnstableFeature{UnstableFlag: "org.matrix.msc3916.stable", SpecVersion: SpecV111}
+
+	BeeperFeatureHungry               = UnstableFeature{UnstableFlag: "com.beeper.hungry"}
+	BeeperFeatureBatchSending         = UnstableFeature{UnstableFlag: "com.beeper.batch_sending"}
+	BeeperFeatureRoomYeeting          = UnstableFeature{UnstableFlag: "com.beeper.room_yeeting"}
+	BeeperFeatureAutojoinInvites      = UnstableFeature{UnstableFlag: "com.beeper.room_create_autojoin_invites"}
+	BeeperFeatureArbitraryProfileMeta = UnstableFeature{UnstableFlag: "com.beeper.arbitrary_profile_meta"}
+	BeeperFeatureAccountDataMute      = UnstableFeature{UnstableFlag: "com.beeper.account_data_mute"}
+	BeeperFeatureInboxState           = UnstableFeature{UnstableFlag: "com.beeper.inbox_state"}
+)
+
+func (versions *RespVersions) Supports(feature UnstableFeature) bool {
+	if versions == nil {
+		return false
+	}
+	return versions.UnstableFeatures[feature.UnstableFlag] ||
+		(!feature.SpecVersion.IsEmpty() && versions.ContainsGreaterOrEqual(feature.SpecVersion))
+}
+
+type SpecVersionFormat int
+
+const (
+	SpecVersionFormatUnknown SpecVersionFormat = iota
+	SpecVersionFormatR
+	SpecVersionFormatV
+)
+
+var (
+	SpecR000 = MustParseSpecVersion("r0.0.0")
+	SpecR001 = MustParseSpecVersion("r0.0.1")
+	SpecR010 = MustParseSpecVersion("r0.1.0")
+	SpecR020 = MustParseSpecVersion("r0.2.0")
+	SpecR030 = MustParseSpecVersion("r0.3.0")
+	SpecR040 = MustParseSpecVersion("r0.4.0")
+	SpecR050 = MustParseSpecVersion("r0.5.0")
+	SpecR060 = MustParseSpecVersion("r0.6.0")
+	SpecR061 = MustParseSpecVersion("r0.6.1")
+	SpecV11  = MustParseSpecVersion("v1.1")
+	SpecV12  = MustParseSpecVersion("v1.2")
+	SpecV13  = MustParseSpecVersion("v1.3")
+	SpecV14  = MustParseSpecVersion("v1.4")
+	SpecV15  = MustParseSpecVersion("v1.5")
+	SpecV16  = MustParseSpecVersion("v1.6")
+	SpecV17  = MustParseSpecVersion("v1.7")
+	SpecV18  = MustParseSpecVersion("v1.8")
+	SpecV19  = MustParseSpecVersion("v1.9")
+	SpecV110 = MustParseSpecVersion("v1.10")
+	SpecV111 = MustParseSpecVersion("v1.11")
+)
+
+func (svf SpecVersionFormat) String() string {
+	switch svf {
+	case SpecVersionFormatR:
+		return "r"
+	case SpecVersionFormatV:
+		return "v"
+	default:
+		return ""
+	}
+}
+
+type SpecVersion struct {
+	Format SpecVersionFormat
+	Major  int
+	Minor  int
+	Patch  int
+
+	Raw string
+}
+
+var legacyVersionRegex = regexp.MustCompile(`^r(\d+)\.(\d+)\.(\d+)$`)
+var modernVersionRegex = regexp.MustCompile(`^v(\d+)\.(\d+)$`)
+
+func MustParseSpecVersion(version string) SpecVersion {
+	sv, err := ParseSpecVersion(version)
+	if err != nil {
+		panic(err)
+	}
+	return sv
+}
+
+func ParseSpecVersion(version string) (sv SpecVersion, err error) {
+	sv.Raw = version
+	if parts := modernVersionRegex.FindStringSubmatch(version); parts != nil {
+		sv.Major, _ = strconv.Atoi(parts[1])
+		sv.Minor, _ = strconv.Atoi(parts[2])
+		sv.Format = SpecVersionFormatV
+	} else if parts = legacyVersionRegex.FindStringSubmatch(version); parts != nil {
+		sv.Major, _ = strconv.Atoi(parts[1])
+		sv.Minor, _ = strconv.Atoi(parts[2])
+		sv.Patch, _ = strconv.Atoi(parts[3])
+		sv.Format = SpecVersionFormatR
+	} else {
+		err = fmt.Errorf("version '%s' doesn't match either known syntax", version)
+	}
+	return
+}
+
+func (sv *SpecVersion) UnmarshalText(version []byte) error {
+	*sv, _ = ParseSpecVersion(string(version))
+	return nil
+}
+
+func (sv *SpecVersion) MarshalText() ([]byte, error) {
+	return []byte(sv.String()), nil
+}
+
+func (sv SpecVersion) String() string {
+	switch sv.Format {
+	case SpecVersionFormatR:
+		return fmt.Sprintf("r%d.%d.%d", sv.Major, sv.Minor, sv.Patch)
+	case SpecVersionFormatV:
+		return fmt.Sprintf("v%d.%d", sv.Major, sv.Minor)
+	default:
+		return sv.Raw
+	}
+}
+
+func (sv SpecVersion) IsEmpty() bool {
+	return sv.Format == SpecVersionFormatUnknown && sv.Raw == ""
+}
+
+func (sv SpecVersion) LessThan(other SpecVersion) bool {
+	return sv != other && !sv.GreaterThan(other)
+}
+
+func (sv SpecVersion) GreaterThan(other SpecVersion) bool {
+	return sv.Format > other.Format ||
+		(sv.Format == other.Format && sv.Major > other.Major) ||
+		(sv.Format == other.Format && sv.Major == other.Major && sv.Minor > other.Minor) ||
+		(sv.Format == other.Format && sv.Major == other.Major && sv.Minor == other.Minor && sv.Patch > other.Patch)
+}