Compare commits

...

1,587 commits

Author SHA1 Message Date
Tulir Asokan
ef6de851a2 format/htmlparser: fix generating markdown for code blocks with backticks
Some checks are pending
Go / Lint (latest) (push) Waiting to run
Go / Build (old, libolm) (push) Waiting to run
Go / Build (latest, libolm) (push) Waiting to run
Go / Build (old, goolm) (push) Waiting to run
Go / Build (latest, goolm) (push) Waiting to run
2026-03-13 18:33:22 +02:00
Tulir Asokan
b42ac0e83d bridgev2/status: make RemoteProfile a non-pointer
Closes #468
2026-03-13 16:28:07 +02:00
Tulir Asokan
92cfc0095d
bridgev2: add support for custom profile fields for ghosts (#462) 2026-03-13 16:24:31 +02:00
Tulir Asokan
8fb92239dc bridgev2: fix bugs with threads
Some checks failed
Go / Lint (latest) (push) Has been cancelled
Go / Build (old, libolm) (push) Has been cancelled
Go / Build (latest, libolm) (push) Has been cancelled
Go / Build (old, goolm) (push) Has been cancelled
Go / Build (latest, goolm) (push) Has been cancelled
2026-03-10 13:00:00 +02:00
Tulir Asokan
c243dad24a bridgev2/portal: include portal receiver in logs
Some checks are pending
Go / Lint (latest) (push) Waiting to run
Go / Build (old, libolm) (push) Waiting to run
Go / Build (latest, libolm) (push) Waiting to run
Go / Build (old, goolm) (push) Waiting to run
Go / Build (latest, goolm) (push) Waiting to run
2026-03-09 14:27:28 +02:00
timedout
c107c25d07
client: add type parameter to UIA request bodies (#469)
Some checks failed
Go / Lint (latest) (push) Has been cancelled
Go / Build (old, libolm) (push) Has been cancelled
Go / Build (latest, libolm) (push) Has been cancelled
Go / Build (old, goolm) (push) Has been cancelled
Go / Build (latest, goolm) (push) Has been cancelled
2026-03-07 14:26:42 +00:00
Tulir Asokan
df24fb96e2 client: update MSC2666 implementation
Some checks are pending
Go / Lint (latest) (push) Waiting to run
Go / Build (old, libolm) (push) Waiting to run
Go / Build (latest, libolm) (push) Waiting to run
Go / Build (old, goolm) (push) Waiting to run
Go / Build (latest, goolm) (push) Waiting to run
2026-03-06 20:58:18 +02:00
Tulir Asokan
531822f6dc bridgev2/config: add limit for unknown error auto-reconnects
Some checks are pending
Go / Lint (latest) (push) Waiting to run
Go / Build (old, libolm) (push) Waiting to run
Go / Build (latest, libolm) (push) Waiting to run
Go / Build (old, goolm) (push) Waiting to run
Go / Build (latest, goolm) (push) Waiting to run
2026-03-06 16:08:28 +02:00
Tulir Asokan
7a53f3928a bridgev2/portal: redact conflicting reactions before sending MSS success 2026-03-06 14:37:36 +02:00
Tulir Asokan
7836f35a1a bridgev2/portal: fix third matrix reaction not removing previous one on single-reaction networks
Some checks are pending
Go / Lint (latest) (push) Waiting to run
Go / Build (old, libolm) (push) Waiting to run
Go / Build (latest, libolm) (push) Waiting to run
Go / Build (old, goolm) (push) Waiting to run
Go / Build (latest, goolm) (push) Waiting to run
2026-03-05 23:57:35 +02:00
Tulir Asokan
0f6a779dd2 readme: update
Some checks are pending
Go / Lint (latest) (push) Waiting to run
Go / Build (old, libolm) (push) Waiting to run
Go / Build (latest, libolm) (push) Waiting to run
Go / Build (old, goolm) (push) Waiting to run
Go / Build (latest, goolm) (push) Waiting to run
2026-03-05 11:59:11 +02:00
Tulir Asokan
ed6dbcaaee client: log content length when uploading to external url
Some checks are pending
Go / Lint (latest) (push) Waiting to run
Go / Build (old, libolm) (push) Waiting to run
Go / Build (latest, libolm) (push) Waiting to run
Go / Build (old, goolm) (push) Waiting to run
Go / Build (latest, goolm) (push) Waiting to run
2026-03-04 22:50:43 +02:00
Tulir Asokan
ed9820356e bridgev2/portalreid: try to fix deadlock when racing with room creation
Some checks are pending
Go / Lint (latest) (push) Waiting to run
Go / Build (old, libolm) (push) Waiting to run
Go / Build (latest, libolm) (push) Waiting to run
Go / Build (old, goolm) (push) Waiting to run
Go / Build (latest, goolm) (push) Waiting to run
2026-03-04 13:58:24 +02:00
batuhan içöz
fef4326fbc
client,event,bridgev2: add support for Beeper's custom ephemeral events and AI stream events (#457)
Some checks are pending
Go / Lint (latest) (push) Waiting to run
Go / Build (old, libolm) (push) Waiting to run
Go / Build (latest, libolm) (push) Waiting to run
Go / Build (old, goolm) (push) Waiting to run
Go / Build (latest, goolm) (push) Waiting to run
2026-03-04 01:38:50 +01:00
Tulir Asokan
77f0658365 bridgev2/{commands,provisioning}: log full login step data
Some checks are pending
Go / Lint (latest) (push) Waiting to run
Go / Build (old, libolm) (push) Waiting to run
Go / Build (latest, libolm) (push) Waiting to run
Go / Build (old, goolm) (push) Waiting to run
Go / Build (latest, goolm) (push) Waiting to run
2026-03-03 17:33:51 +02:00
Tulir Asokan
e1529f9616 bridgev2/provisioning: log when returning login steps in provisioning API 2026-03-03 17:28:19 +02:00
Tulir Asokan
26a62a7eec event: add missing omitempty
Some checks failed
Go / Lint (latest) (push) Has been cancelled
Go / Build (old, libolm) (push) Has been cancelled
Go / Build (latest, libolm) (push) Has been cancelled
Go / Build (old, goolm) (push) Has been cancelled
Go / Build (latest, goolm) (push) Has been cancelled
2026-03-01 13:49:04 +02:00
Tulir Asokan
f8234ecf85 event: add m.room.policy event type 2026-03-01 13:23:32 +02:00
Tulir Asokan
36c353abc7 federation/pdu: add AddSignature helper method 2026-03-01 12:37:13 +02:00
Tulir Asokan
dd51c562ab crypto: log destination map when sharing megolm sessions
Some checks failed
Go / Lint (latest) (push) Has been cancelled
Go / Build (old, libolm) (push) Has been cancelled
Go / Build (latest, libolm) (push) Has been cancelled
Go / Build (old, goolm) (push) Has been cancelled
Go / Build (latest, goolm) (push) Has been cancelled
2026-02-26 17:40:15 +02:00
Tulir Asokan
98c830181b client: omit large request bodies from logs 2026-02-26 17:40:15 +02:00
Radon Rosborough
7f24c78002
bridgev2/login: add attachments option to user input step type (#465)
Some checks are pending
Go / Lint (latest) (push) Waiting to run
Go / Build (old, libolm) (push) Waiting to run
Go / Build (latest, libolm) (push) Waiting to run
Go / Build (old, goolm) (push) Waiting to run
Go / Build (latest, goolm) (push) Waiting to run
2026-02-25 08:52:29 -08:00
Tulir Asokan
3efa3ef73a bridgev2/portal: log remote event timestamps by default
Some checks failed
Go / Lint (latest) (push) Has been cancelled
Go / Build (old, libolm) (push) Has been cancelled
Go / Build (latest, libolm) (push) Has been cancelled
Go / Build (old, goolm) (push) Has been cancelled
Go / Build (latest, goolm) (push) Has been cancelled
2026-02-23 22:14:23 +02:00
timedout
28b7bf7e56
federation/eventauth: Fix inverted membership check for 5.6.1 (#464)
Some checks are pending
Go / Lint (latest) (push) Waiting to run
Go / Build (old, libolm) (push) Waiting to run
Go / Build (latest, libolm) (push) Waiting to run
Go / Build (old, goolm) (push) Waiting to run
Go / Build (latest, goolm) (push) Waiting to run
2026-02-22 19:37:19 +00:00
Tulir Asokan
5779871f1b bridgev2/commands: add file info for QR codes
Some checks are pending
Go / Lint (latest) (push) Waiting to run
Go / Build (old, libolm) (push) Waiting to run
Go / Build (latest, libolm) (push) Waiting to run
Go / Build (old, goolm) (push) Waiting to run
Go / Build (latest, goolm) (push) Waiting to run
2026-02-21 14:10:55 +02:00
Tulir Asokan
bc79822eab crypto: save source of megolm sessions
Some checks are pending
Go / Lint (latest) (push) Waiting to run
Go / Build (old, libolm) (push) Waiting to run
Go / Build (latest, libolm) (push) Waiting to run
Go / Build (old, goolm) (push) Waiting to run
Go / Build (latest, goolm) (push) Waiting to run
2026-02-21 01:06:12 +02:00
Tulir Asokan
67d30e054c dependencies: update
Some checks are pending
Go / Lint (latest) (push) Waiting to run
Go / Build (old, libolm) (push) Waiting to run
Go / Build (latest, libolm) (push) Waiting to run
Go / Build (old, goolm) (push) Waiting to run
Go / Build (latest, goolm) (push) Waiting to run
2026-02-19 22:51:37 +02:00
Tulir Asokan
974f7dc544 crypto/decryptmegolm: allow device key mismatches, but mark as untrusted
Some checks are pending
Go / Lint (latest) (push) Waiting to run
Go / Build (old, libolm) (push) Waiting to run
Go / Build (latest, libolm) (push) Waiting to run
Go / Build (old, goolm) (push) Waiting to run
Go / Build (latest, goolm) (push) Waiting to run
2026-02-19 14:10:20 +02:00
Tulir Asokan
ae58161412 bridgev2/provisioning: log group create params 2026-02-19 14:09:59 +02:00
Tulir Asokan
de0d12e26a goolm/crypto: add test to ensure shared secrets can't be zero
Some checks are pending
Go / Lint (latest) (push) Waiting to run
Go / Build (old, libolm) (push) Waiting to run
Go / Build (latest, libolm) (push) Waiting to run
Go / Build (old, goolm) (push) Waiting to run
Go / Build (latest, goolm) (push) Waiting to run
2026-02-18 12:53:37 +02:00
Tulir Asokan
9cd7258764 Bump version to v0.26.3
Some checks failed
Go / Lint (latest) (push) Has been cancelled
Go / Build (old, libolm) (push) Has been cancelled
Go / Build (latest, libolm) (push) Has been cancelled
Go / Build (old, goolm) (push) Has been cancelled
Go / Build (latest, goolm) (push) Has been cancelled
2026-02-16 14:33:21 +02:00
Tulir Asokan
0b9471e190 dependencies: update 2026-02-16 14:31:01 +02:00
Tulir Asokan
53ed8526c6 federation/eventauth: disable underscore support in string power levels 2026-02-16 14:29:09 +02:00
Tulir Asokan
c52d87b6ea mediaproxy: handle federation thumbnail requests
Some checks are pending
Go / Lint (latest) (push) Waiting to run
Go / Build (old, libolm) (push) Waiting to run
Go / Build (latest, libolm) (push) Waiting to run
Go / Build (old, goolm) (push) Waiting to run
Go / Build (latest, goolm) (push) Waiting to run
2026-02-15 21:47:10 +02:00
Tulir Asokan
bafba9b227 federation/eventauth: make expected success a part of test name 2026-02-14 23:49:14 +02:00
Tulir Asokan
b97f989032 federation/eventauth: add support for underscores in string power levels
Some checks are pending
Go / Lint (latest) (push) Waiting to run
Go / Build (old, libolm) (push) Waiting to run
Go / Build (latest, libolm) (push) Waiting to run
Go / Build (old, goolm) (push) Waiting to run
Go / Build (latest, goolm) (push) Waiting to run
2026-02-14 23:37:20 +02:00
Tulir Asokan
7dbc4dd16a appservice: fix building websocket url
Some checks failed
Go / Lint (latest) (push) Has been cancelled
Go / Build (old, libolm) (push) Has been cancelled
Go / Build (latest, libolm) (push) Has been cancelled
Go / Build (old, goolm) (push) Has been cancelled
Go / Build (latest, goolm) (push) Has been cancelled
2026-02-12 17:34:40 +02:00
Tulir Asokan
fe541df217 main: bump minimum Go version to 1.25
Some checks are pending
Go / Lint (latest) (push) Waiting to run
Go / Build (old, libolm) (push) Waiting to run
Go / Build (latest, libolm) (push) Waiting to run
Go / Build (old, goolm) (push) Waiting to run
Go / Build (latest, goolm) (push) Waiting to run
2026-02-11 21:34:47 +02:00
Tulir Asokan
d2364b3822 bridgev2/portal: allow delivery receipts even if portal has no other user ID
Some checks failed
Go / Lint (latest) (push) Has been cancelled
Go / Build (old, libolm) (push) Has been cancelled
Go / Build (latest, libolm) (push) Has been cancelled
Go / Build (old, goolm) (push) Has been cancelled
Go / Build (latest, goolm) (push) Has been cancelled
2026-01-29 19:47:19 +02:00
Nick Mills-Barrett
4b387c305b
error: add RespError.CanRetry field (#456)
Some checks are pending
Go / Lint (latest) (push) Waiting to run
Go / Build (old, libolm) (push) Waiting to run
Go / Build (latest, libolm) (push) Waiting to run
Go / Build (old, goolm) (push) Waiting to run
Go / Build (latest, goolm) (push) Waiting to run
2026-01-29 15:01:48 +00:00
Tulir Asokan
60742c4b61 crypto: update test
Some checks are pending
Go / Lint (latest) (push) Waiting to run
Go / Build (old, libolm) (push) Waiting to run
Go / Build (latest, libolm) (push) Waiting to run
Go / Build (old, goolm) (push) Waiting to run
Go / Build (latest, goolm) (push) Waiting to run
2026-01-28 21:37:23 +02:00
Tulir Asokan
2423716f83 crypto/keysharing: don't send withheld response to some key requests 2026-01-28 21:34:07 +02:00
Tulir Asokan
b613f4d676 crypto/sessions: add missing field in export 2026-01-28 21:32:48 +02:00
Tulir Asokan
2c0d51ee7d crypto/ssss: handle slightly broken key metadata better 2026-01-28 14:43:02 +02:00
Tulir Asokan
c4ce008c8e crypto/ssss: skip verifying recovery key if MAC or IV are missing
Some checks are pending
Go / Lint (latest) (push) Waiting to run
Go / Build (old, libolm) (push) Waiting to run
Go / Build (latest, libolm) (push) Waiting to run
Go / Build (old, goolm) (push) Waiting to run
Go / Build (latest, goolm) (push) Waiting to run
2026-01-28 12:51:46 +02:00
Tulir Asokan
9d30203f6b bridgev2/userlogin: add todo 2026-01-26 13:42:33 +02:00
Tulir Asokan
074a2d8d4d crypto/keysharing: fix including sender key in forwards
Some checks failed
Go / Lint (latest) (push) Has been cancelled
Go / Build (old, libolm) (push) Has been cancelled
Go / Build (latest, libolm) (push) Has been cancelled
Go / Build (old, goolm) (push) Has been cancelled
Go / Build (latest, goolm) (push) Has been cancelled
2026-01-26 01:39:44 +02:00
Tulir Asokan
b041eb924e error: allow storing extra headers in RespError 2026-01-26 01:21:20 +02:00
Tulir Asokan
8b04430d84 event: switch url preview image blurhash to use MSC2448 field
Some checks failed
Go / Lint (latest) (push) Has been cancelled
Go / Build (old, libolm) (push) Has been cancelled
Go / Build (latest, libolm) (push) Has been cancelled
Go / Build (old, goolm) (push) Has been cancelled
Go / Build (latest, goolm) (push) Has been cancelled
2026-01-23 19:38:09 +02:00
SpiritCroc
d057f1c673
event: add action message content for rich call notifications (#454) 2026-01-23 15:38:17 +01:00
Tulir Asokan
a1236b65be crypto/keyimport: call session received callback for all sessions in import
Some checks failed
Go / Lint (latest) (push) Has been cancelled
Go / Build (old, libolm) (push) Has been cancelled
Go / Build (latest, libolm) (push) Has been cancelled
Go / Build (old, goolm) (push) Has been cancelled
Go / Build (latest, goolm) (push) Has been cancelled
2026-01-20 14:28:21 +02:00
Tulir Asokan
a55693bbd7 client,bridgev2/matrix: fix context used for async uploads
Some checks are pending
Go / Lint (latest) (push) Waiting to run
Go / Build (old, libolm) (push) Waiting to run
Go / Build (latest, libolm) (push) Waiting to run
Go / Build (old, goolm) (push) Waiting to run
Go / Build (latest, goolm) (push) Waiting to run
2026-01-20 12:09:01 +02:00
Nick Mills-Barrett
f32af79d20
bridgev2/ghost: consider avatar being set in Ghost.UpdateInfoIfNecessary (#453)
Some checks are pending
Go / Lint (latest) (push) Waiting to run
Go / Build (old, libolm) (push) Waiting to run
Go / Build (latest, libolm) (push) Waiting to run
Go / Build (old, goolm) (push) Waiting to run
Go / Build (latest, goolm) (push) Waiting to run
Co-authored-by: Tulir Asokan <tulir@maunium.net>
2026-01-19 14:26:22 +00:00
Tulir Asokan
e28f7170bc
bridgev2/portal: auto-accept message requests on message (#451) 2026-01-19 14:58:18 +02:00
Tulir Asokan
28bcc356db client: add MemberCount helper method for lazy load summary
Some checks are pending
Go / Lint (latest) (push) Waiting to run
Go / Build (old, libolm) (push) Waiting to run
Go / Build (latest, libolm) (push) Waiting to run
Go / Build (old, goolm) (push) Waiting to run
Go / Build (latest, goolm) (push) Waiting to run
2026-01-18 22:41:34 +02:00
Tulir Asokan
0b6fa137ce client: add support for sending MSC4354 sticky events
Some checks are pending
Go / Lint (latest) (push) Waiting to run
Go / Build (old, libolm) (push) Waiting to run
Go / Build (latest, libolm) (push) Waiting to run
Go / Build (old, goolm) (push) Waiting to run
Go / Build (latest, goolm) (push) Waiting to run
2026-01-18 14:49:06 +02:00
Tulir Asokan
b2b58f3a29 bridgev2/provisioning: cancel logins on error and delete completed logins from map
Some checks failed
Go / Lint (latest) (push) Has been cancelled
Go / Build (old, libolm) (push) Has been cancelled
Go / Build (latest, libolm) (push) Has been cancelled
Go / Build (old, goolm) (push) Has been cancelled
Go / Build (latest, goolm) (push) Has been cancelled
2026-01-17 01:36:36 +02:00
Tulir Asokan
ec3cf5fbdd crypto/decryptmegolm: add additional checks for megolm decryption 2026-01-17 01:02:39 +02:00
Tulir Asokan
b226c03277 crypto: add length check to hacky megolm message index parser 2026-01-17 00:55:16 +02:00
Tulir Asokan
0e4b074b57 event: add detail to not json string parse error 2026-01-17 00:43:41 +02:00
Tulir Asokan
65d708f1b7 Bump version to v0.26.2
Some checks are pending
Go / Lint (latest) (push) Waiting to run
Go / Build (old, libolm) (push) Waiting to run
Go / Build (latest, libolm) (push) Waiting to run
Go / Build (old, goolm) (push) Waiting to run
Go / Build (latest, goolm) (push) Waiting to run
2026-01-16 14:50:43 +02:00
Tulir Asokan
34bcd027e5 bridgev2/commands: add debug command for resetting connections
Some checks are pending
Go / Lint (latest) (push) Waiting to run
Go / Build (old, libolm) (push) Waiting to run
Go / Build (latest, libolm) (push) Waiting to run
Go / Build (old, goolm) (push) Waiting to run
Go / Build (latest, goolm) (push) Waiting to run
2026-01-15 14:02:00 +02:00
Tulir Asokan
75f9cb369b bridgev2: add helper method for getting HTTP settings from matrix connector
Some checks are pending
Go / Lint (latest) (push) Waiting to run
Go / Build (old, libolm) (push) Waiting to run
Go / Build (latest, libolm) (push) Waiting to run
Go / Build (old, goolm) (push) Waiting to run
Go / Build (latest, goolm) (push) Waiting to run
2026-01-14 17:06:32 +02:00
Tulir Asokan
38799be3ca bridgev2/networkinterface: let matrix connector reset remote network connections
Some checks are pending
Go / Lint (latest) (push) Waiting to run
Go / Build (old, libolm) (push) Waiting to run
Go / Build (latest, libolm) (push) Waiting to run
Go / Build (old, goolm) (push) Waiting to run
Go / Build (latest, goolm) (push) Waiting to run
2026-01-13 23:23:31 +02:00
Tulir Asokan
d77cb628ff bridgev2/matrixinterface: let matrix connector suggest HTTP client settings 2026-01-13 23:11:50 +02:00
Tulir Asokan
3d5de4ed2f bridgev2/matrixinterface: add parent interface to MatrixConnector subinterfaces 2026-01-13 23:11:18 +02:00
Tulir Asokan
9d70b2b845 bridgev2/matrixinterface: properly expose GetProvisioning
Some checks failed
Go / Lint (latest) (push) Has been cancelled
Go / Build (old, libolm) (push) Has been cancelled
Go / Build (latest, libolm) (push) Has been cancelled
Go / Build (old, goolm) (push) Has been cancelled
Go / Build (latest, goolm) (push) Has been cancelled
2026-01-12 12:33:55 +02:00
Tulir Asokan
650f9c3139 event/cmdschema: adjust handling of unterminated quotes
Some checks are pending
Go / Lint (latest) (push) Waiting to run
Go / Build (old, libolm) (push) Waiting to run
Go / Build (latest, libolm) (push) Waiting to run
Go / Build (old, goolm) (push) Waiting to run
Go / Build (latest, goolm) (push) Waiting to run
2026-01-12 00:57:12 +02:00
Tulir Asokan
4c0b511c01 event/cmdschema: add JSON schemas for test data 2026-01-12 00:52:24 +02:00
Tulir Asokan
e034c16753 event/cmdschema: don't allow flags after tail parameter 2026-01-12 00:09:05 +02:00
Tulir Asokan
4cd376cd90 event/cmdschema: disallow positional optional parameters and add tail parameters 2026-01-11 23:42:24 +02:00
Tulir Asokan
60be954407 event/cmdschema: make boolean parsing stricter 2026-01-11 23:42:16 +02:00
Tulir Asokan
d63a008ec6 commands: add MSC4391 support
Some checks are pending
Go / Lint (latest) (push) Waiting to run
Go / Build (old, libolm) (push) Waiting to run
Go / Build (latest, libolm) (push) Waiting to run
Go / Build (old, goolm) (push) Waiting to run
Go / Build (latest, goolm) (push) Waiting to run
2026-01-10 20:55:11 +02:00
Tulir Asokan
5ac73563b0 event/cmdschema: add MSC4391 types, parser and stringifier 2026-01-10 20:55:11 +02:00
Tulir Asokan
be22286000 event: drop MSC4332 support 2026-01-10 20:55:11 +02:00
Tulir Asokan
c69518ab3c bridgev2/login: add default_value for user input fields 2026-01-10 20:53:44 +02:00
Tulir Asokan
6da5f6b5d0 federation: change serverauth test domains
Some checks are pending
Go / Lint (latest) (push) Waiting to run
Go / Build (old, libolm) (push) Waiting to run
Go / Build (latest, libolm) (push) Waiting to run
Go / Build (old, goolm) (push) Waiting to run
Go / Build (latest, goolm) (push) Waiting to run
2026-01-10 14:18:57 +02:00
Tulir Asokan
32da107299 bridgev2/matrix: fix decrypting events in GetEvent
Some checks failed
Go / Lint (latest) (push) Has been cancelled
Go / Build (old, libolm) (push) Has been cancelled
Go / Build (latest, libolm) (push) Has been cancelled
Go / Build (old, goolm) (push) Has been cancelled
Go / Build (latest, goolm) (push) Has been cancelled
2026-01-08 22:52:25 +02:00
Tulir Asokan
9f327602f6 event/beeper: add blurhash for link previews
Some checks failed
Go / Lint (latest) (push) Has been cancelled
Go / Build (old, libolm) (push) Has been cancelled
Go / Build (latest, libolm) (push) Has been cancelled
Go / Build (old, goolm) (push) Has been cancelled
Go / Build (latest, goolm) (push) Has been cancelled
2026-01-07 20:05:59 +02:00
Tulir Asokan
f4434b33c6
crypto,bridgev2: add option to encrypt reactions and replies (#445) 2026-01-07 19:22:32 +02:00
Tulir Asokan
3a2c6ae865 client: stabilize MSC4323
Some checks failed
Go / Lint (latest) (push) Has been cancelled
Go / Build (old, libolm) (push) Has been cancelled
Go / Build (latest, libolm) (push) Has been cancelled
Go / Build (old, goolm) (push) Has been cancelled
Go / Build (latest, goolm) (push) Has been cancelled
2026-01-05 14:58:29 +02:00
Tulir Asokan
788151bc50 client: error if Download parameter is empty
Some checks failed
Go / Lint (latest) (push) Has been cancelled
Go / Build (old, libolm) (push) Has been cancelled
Go / Build (latest, libolm) (push) Has been cancelled
Go / Build (old, goolm) (push) Has been cancelled
Go / Build (latest, goolm) (push) Has been cancelled
2025-12-30 22:53:27 +02:00
Tulir Asokan
59ec890dcb changelog: add missing link
Some checks failed
Go / Lint (latest) (push) Has been cancelled
Go / Build (old, libolm) (push) Has been cancelled
Go / Build (latest, libolm) (push) Has been cancelled
Go / Build (old, goolm) (push) Has been cancelled
Go / Build (latest, goolm) (push) Has been cancelled
2025-12-19 15:15:23 +02:00
Tulir Asokan
4825e41d5c bridgev2/portalreid: try to cancel room creation
Some checks are pending
Go / Lint (latest) (push) Waiting to run
Go / Build (old, libolm) (push) Waiting to run
Go / Build (latest, libolm) (push) Waiting to run
Go / Build (old, goolm) (push) Waiting to run
Go / Build (latest, goolm) (push) Waiting to run
2025-12-19 13:32:55 +02:00
Tulir Asokan
af06098723 bridgev2/simplevent: add method to merge log contexts 2025-12-19 13:06:34 +02:00
Tulir Asokan
80b4201ff1 bridgev2/portalreid: add more logs 2025-12-19 13:03:19 +02:00
Tulir Asokan
33eb00fde0 bridgev2/database: reduce limit for using chunked deletion
Some checks failed
Go / Lint (latest) (push) Has been cancelled
Go / Build (old, libolm) (push) Has been cancelled
Go / Build (latest, libolm) (push) Has been cancelled
Go / Build (old, goolm) (push) Has been cancelled
Go / Build (latest, goolm) (push) Has been cancelled
2025-12-16 19:29:26 +02:00
Tulir Asokan
b44f81d114 bridgev2/database: only allow one chunked portal deletion at a time 2025-12-16 18:57:39 +02:00
Tulir Asokan
e38d758a52 bridgev2/database: delete messages in chunks if portal has too many 2025-12-16 16:59:54 +02:00
Tulir Asokan
e9b262e671 bridgev2/database: add index for disappearing messages and portal parents 2025-12-16 16:23:44 +02:00
Tulir Asokan
b9635964a5 Bump version to v0.26.1
Some checks are pending
Go / Lint (latest) (push) Waiting to run
Go / Build (old, libolm) (push) Waiting to run
Go / Build (latest, libolm) (push) Waiting to run
Go / Build (old, goolm) (push) Waiting to run
Go / Build (latest, goolm) (push) Waiting to run
2025-12-16 12:20:42 +02:00
Tulir Asokan
950ce6636e crypto/goolm: include version number in version mismatches 2025-12-15 15:18:40 +02:00
Tulir Asokan
4be2562297 changelog: update
Some checks failed
Go / Lint (latest) (push) Has been cancelled
Go / Build (old, libolm) (push) Has been cancelled
Go / Build (latest, libolm) (push) Has been cancelled
Go / Build (old, goolm) (push) Has been cancelled
Go / Build (latest, goolm) (push) Has been cancelled
2025-12-14 14:37:57 +02:00
Tulir Asokan
cb6f673e7a bridgev2/portal: fix event loop not stopping
Some checks failed
Go / Lint (latest) (push) Has been cancelled
Go / Build (old, libolm) (push) Has been cancelled
Go / Build (latest, libolm) (push) Has been cancelled
Go / Build (old, goolm) (push) Has been cancelled
Go / Build (latest, goolm) (push) Has been cancelled
2025-12-13 11:09:09 +02:00
Tulir Asokan
9dc3772c47 ci: update actions and pre-commit hooks 2025-12-13 10:54:58 +02:00
Tulir Asokan
de52a753be bridgev2: remove hardcoded room version 2025-12-13 10:47:37 +02:00
Tulir Asokan
9e3fa96fb4 bridgev2/portal: handle portal deletion edge cases
Some checks are pending
Go / Lint (latest) (push) Waiting to run
Go / Build (old, libolm) (push) Waiting to run
Go / Build (latest, libolm) (push) Waiting to run
Go / Build (old, goolm) (push) Waiting to run
Go / Build (latest, goolm) (push) Waiting to run
2025-12-12 17:31:56 +02:00
Tulir Asokan
efd4136c7a dependencies: update
Some checks are pending
Go / Lint (latest) (push) Waiting to run
Go / Build (old, libolm) (push) Waiting to run
Go / Build (latest, libolm) (push) Waiting to run
Go / Build (old, goolm) (push) Waiting to run
Go / Build (latest, goolm) (push) Waiting to run
2025-12-11 14:17:45 +02:00
Tulir Asokan
2c62641c73 bridgev2/portal: make queueEvent slightly safer when deleting portals
Some checks are pending
Go / Lint (latest) (push) Waiting to run
Go / Build (old, libolm) (push) Waiting to run
Go / Build (latest, libolm) (push) Waiting to run
Go / Build (old, goolm) (push) Waiting to run
Go / Build (latest, goolm) (push) Waiting to run
2025-12-10 13:15:33 +02:00
Tulir Asokan
31579be20a bridgev2,event: add interface for message requests
Some checks are pending
Go / Lint (latest) (push) Waiting to run
Go / Build (old, libolm) (push) Waiting to run
Go / Build (latest, libolm) (push) Waiting to run
Go / Build (old, goolm) (push) Waiting to run
Go / Build (latest, goolm) (push) Waiting to run
2025-12-09 16:41:56 +02:00
Nick Mills-Barrett
e7a95b7f97
client: backoff before retrying external upload requests
Some checks are pending
Go / Lint (latest) (push) Waiting to run
Go / Build (old, libolm) (push) Waiting to run
Go / Build (latest, libolm) (push) Waiting to run
Go / Build (old, goolm) (push) Waiting to run
Go / Build (latest, goolm) (push) Waiting to run
2025-12-08 14:33:02 +00:00
Tulir Asokan
315d2ab17d all: fix staticcheck issues
Some checks are pending
Go / Lint (latest) (push) Waiting to run
Go / Build (old, libolm) (push) Waiting to run
Go / Build (latest, libolm) (push) Waiting to run
Go / Build (old, goolm) (push) Waiting to run
Go / Build (latest, goolm) (push) Waiting to run
2025-12-08 00:07:25 +02:00
Tulir Asokan
6017612c55 bridgev2/portal: only delete old reactions if new one is successful 2025-12-07 23:21:05 +02:00
Tulir Asokan
00c58efc59 bridgev2/portal: don't try to update functional members if portal doesn't exist 2025-12-07 19:52:22 +02:00
Tulir Asokan
0584fd0c0d bridgev2/portal: don't forward backfill without CanBackfill flag 2025-12-07 19:52:08 +02:00
Tulir Asokan
a2522192ff bridgev2/config: fix warning log for null env_config_prefix 2025-12-07 19:34:29 +02:00
Tulir Asokan
3e07631f9e bridgev2/mxmain: add better error for pre-megabridge dbs
Some checks are pending
Go / Lint (latest) (push) Waiting to run
Go / Build (old, libolm) (push) Waiting to run
Go / Build (latest, libolm) (push) Waiting to run
Go / Build (old, goolm) (push) Waiting to run
Go / Build (latest, goolm) (push) Waiting to run
2025-12-06 22:58:11 +02:00
Tulir Asokan
4efa4bdac5 bridgev2/config: allow multiple prioritized backfill limit override keys
Some checks are pending
Go / Lint (latest) (push) Waiting to run
Go / Build (old, libolm) (push) Waiting to run
Go / Build (latest, libolm) (push) Waiting to run
Go / Build (old, goolm) (push) Waiting to run
Go / Build (latest, goolm) (push) Waiting to run
2025-12-06 12:51:12 +02:00
Nick Mills-Barrett
f6d8362278
client: add missing retry cancel check while backing off requests
Some checks are pending
Go / Lint (latest) (push) Waiting to run
Go / Build (old, libolm) (push) Waiting to run
Go / Build (latest, libolm) (push) Waiting to run
Go / Build (old, goolm) (push) Waiting to run
Go / Build (latest, goolm) (push) Waiting to run
2025-12-05 11:36:43 +00:00
Tulir Asokan
02ce6ff918 mediaproxy: allow delayed mime type and redirects for file responses
Some checks failed
Go / Lint (latest) (push) Has been cancelled
Go / Build (old, libolm) (push) Has been cancelled
Go / Build (latest, libolm) (push) Has been cancelled
Go / Build (old, goolm) (push) Has been cancelled
Go / Build (latest, goolm) (push) Has been cancelled
2025-12-03 21:59:41 +02:00
Tulir Asokan
7d54edbfda bridgev2/mxmain: add support for reading env vars from config
Some checks are pending
Go / Lint (latest) (push) Waiting to run
Go / Build (old, libolm) (push) Waiting to run
Go / Build (latest, libolm) (push) Waiting to run
Go / Build (old, goolm) (push) Waiting to run
Go / Build (latest, goolm) (push) Waiting to run
2025-12-02 19:04:04 +02:00
Tulir Asokan
2eeece6942 bridgev2/networkinterface: allow HandleMatrixMembership to redirect invites to another user ID 2025-12-02 15:22:04 +02:00
Tulir Asokan
dfd5485a0d bridgev2/networkinterface: remove deprecated fields in MatrixMembershipChange 2025-12-02 14:17:29 +02:00
Tulir Asokan
5206439b83 bridgev2/portal: pass is state request flag to event handlers 2025-12-02 13:52:48 +02:00
Tulir Asokan
e22802b9bb bridgev2/database: improve missing parents when migrating to split portals
Some checks are pending
Go / Lint (latest) (push) Waiting to run
Go / Build (old, libolm) (push) Waiting to run
Go / Build (latest, libolm) (push) Waiting to run
Go / Build (old, goolm) (push) Waiting to run
Go / Build (latest, goolm) (push) Waiting to run
2025-12-01 17:07:54 +02:00
Tulir Asokan
09052986b2 bridgev2/commands: add command for muting chat on remote network 2025-12-01 15:28:56 +02:00
Tulir Asokan
6e402e8fd2 bridgev2/backfill: don't try to backfill empty threads 2025-12-01 00:10:29 +02:00
Tulir Asokan
1d1ecb2286 federation/eventauth: fix sender membership check when kicking
Some checks failed
Go / Lint (latest) (push) Has been cancelled
Go / Build (old, libolm) (push) Has been cancelled
Go / Build (latest, libolm) (push) Has been cancelled
Go / Build (old, goolm) (push) Has been cancelled
Go / Build (latest, goolm) (push) Has been cancelled
2025-11-28 13:40:54 +02:00
Tulir Asokan
3293e2f8ff dependencies: update 2025-11-28 13:38:05 +02:00
Nick Mills-Barrett
c3b85e8e3c
client: add special error that indicates to retry canceled contexts
Some checks failed
Go / Lint (latest) (push) Has been cancelled
Go / Build (old, libolm) (push) Has been cancelled
Go / Build (latest, libolm) (push) Has been cancelled
Go / Build (old, goolm) (push) Has been cancelled
Go / Build (latest, goolm) (push) Has been cancelled
On it's own this is useless since the retries would all immediately
fail with the canceled context error. The caller is expected to also
set a `UpdateRequestOnRetry` on the client which is used to swap out
the context.
2025-11-26 10:55:36 +00:00
Nick Mills-Barrett
016637ebf8
bridgev2/bridgestate: add var to disable catching bridge state queue panics 2025-11-26 10:54:18 +00:00
Nick Mills-Barrett
dc38165473
crypto: allow storing arbitrary metadata alongside encrypted account data
For example, the creation time of a key.
2025-11-26 10:42:32 +00:00
Tulir Asokan
0f2ff4a090 bridgev2/portal: improve error messages in FindPreferredLogin when portal has receiver
Some checks are pending
Go / Lint (latest) (push) Waiting to run
Go / Build (old, libolm) (push) Waiting to run
Go / Build (latest, libolm) (push) Waiting to run
Go / Build (old, goolm) (push) Waiting to run
Go / Build (latest, goolm) (push) Waiting to run
2025-11-25 14:23:30 +02:00
Tulir Asokan
eaa4e07eae bridgev2/portal: only allow setting receiver as relay in split portals 2025-11-25 14:23:09 +02:00
Tulir Asokan
41b1dfc8c1 bridgev2/provisionutil: check for orphaned DMs in resolve identifier
Some checks failed
Go / Lint (latest) (push) Has been cancelled
Go / Build (old, libolm) (push) Has been cancelled
Go / Build (latest, libolm) (push) Has been cancelled
Go / Build (old, goolm) (push) Has been cancelled
Go / Build (latest, goolm) (push) Has been cancelled
2025-11-23 15:51:15 +02:00
Tulir Asokan
75d54132ae bridgev2/portal: fix getting state events in roomIsPublic
Some checks failed
Go / Lint (latest) (push) Has been cancelled
Go / Build (old, libolm) (push) Has been cancelled
Go / Build (latest, libolm) (push) Has been cancelled
Go / Build (old, goolm) (push) Has been cancelled
Go / Build (latest, goolm) (push) Has been cancelled
2025-11-21 16:07:16 +02:00
Tulir Asokan
1fac8ceb66 bridgev2/matrix: fix GetStateEvent not passing state key through
Some checks failed
Go / Lint (latest) (push) Has been cancelled
Go / Build (old, libolm) (push) Has been cancelled
Go / Build (latest, libolm) (push) Has been cancelled
Go / Build (old, goolm) (push) Has been cancelled
Go / Build (latest, goolm) (push) Has been cancelled
2025-11-19 23:21:56 +02:00
Tulir Asokan
fa56255a06 bridgev2/portal: ignore not found errors when fetching prev state 2025-11-19 23:13:41 +02:00
Tulir Asokan
57657d54ee
bridgev2: add custom event for requesting state change (#428)
Some checks are pending
Go / Lint (latest) (push) Waiting to run
Go / Build (old, libolm) (push) Waiting to run
Go / Build (latest, libolm) (push) Waiting to run
Go / Build (old, goolm) (push) Waiting to run
Go / Build (latest, goolm) (push) Waiting to run
2025-11-19 13:15:38 +02:00
Tulir Asokan
8a59112eb1 client: move some room summary fields to public room info 2025-11-19 12:51:08 +02:00
Tulir Asokan
606b627d48 changelog: fix link 2025-11-19 12:51:08 +02:00
Finn
346100cfd4
statestore: fix missing JoinRules map when initializing MemoryStateStore (#432)
Some checks failed
Go / Lint (latest) (push) Has been cancelled
Go / Build (old, libolm) (push) Has been cancelled
Go / Build (latest, libolm) (push) Has been cancelled
Go / Build (old, goolm) (push) Has been cancelled
Go / Build (latest, goolm) (push) Has been cancelled
2025-11-17 20:18:46 +02:00
timedout
14b85e98a6
federation: Implement federated membership functions (make/send join/knock/leave) (#422) 2025-11-17 16:35:46 +00:00
Tulir Asokan
36029b7622 Bump version to v0.26.0
Some checks failed
Go / Lint (latest) (push) Has been cancelled
Go / Build (old, libolm) (push) Has been cancelled
Go / Build (latest, libolm) (push) Has been cancelled
Go / Build (old, goolm) (push) Has been cancelled
Go / Build (latest, goolm) (push) Has been cancelled
2025-11-16 12:51:14 +02:00
Tulir Asokan
202c7f1176 dependencies: update 2025-11-16 12:43:52 +02:00
Tulir Asokan
a0cb5c6129 bridgev2/backfill: ignore nil reactions 2025-11-13 18:10:27 +02:00
Tulir Asokan
a61e4d05f8 bridgev2/matrix: use MSC4169 to send redactions when available
Some checks failed
Go / Lint (latest) (push) Has been cancelled
Go / Build (old, libolm) (push) Has been cancelled
Go / Build (latest, libolm) (push) Has been cancelled
Go / Build (old, goolm) (push) Has been cancelled
Go / Build (latest, goolm) (push) Has been cancelled
2025-11-13 17:39:27 +02:00
Tulir Asokan
0b73e9e7be client,appservice: deprecate SendMassagedStateEvent in favor of SendStateEvent params 2025-11-13 17:38:45 +02:00
Tulir Asokan
eb2fb84009 appservice/intent: don't EnsureJoined when sending massaged own join event 2025-11-13 17:32:14 +02:00
Tulir Asokan
151d945685 event/capabilities: add docstrings for state and member_actions 2025-11-13 01:29:45 +02:00
Tulir Asokan
828ba3cec1 bridgev2/portal: add capability to disable formatting relayed messages
Some checks are pending
Go / Lint (latest) (push) Waiting to run
Go / Build (old, libolm) (push) Waiting to run
Go / Build (latest, libolm) (push) Waiting to run
Go / Build (old, goolm) (push) Waiting to run
Go / Build (latest, goolm) (push) Waiting to run
2025-11-12 23:14:37 +02:00
Tulir Asokan
85e25748a8 bridgev2/portal: ensure join is sent using target intent 2025-11-12 23:09:49 +02:00
Tulir Asokan
e9bfa0c519 bridgev2/portal: treat spam checker join rule as public 2025-11-12 22:04:29 +02:00
Tulir Asokan
6c7828afe3 bridgev2/portal: skip invite step if room is public 2025-11-12 21:46:23 +02:00
Tulir Asokan
e31d186dc8 statestore: save join rules for rooms 2025-11-12 21:46:23 +02:00
Tulir Asokan
981addddc9 bridgev2/config: add option to disable kicking matrix users
Some checks are pending
Go / Lint (latest) (push) Waiting to run
Go / Build (old, libolm) (push) Waiting to run
Go / Build (latest, libolm) (push) Waiting to run
Go / Build (old, goolm) (push) Waiting to run
Go / Build (latest, goolm) (push) Waiting to run
2025-11-12 19:38:08 +02:00
Tulir Asokan
8b70baa336 bridgev2/commands: add support for ResolveIdentifierTryNext in pm command 2025-11-12 15:34:31 +02:00
Tulir Asokan
4913b123f1 bridgev2/space: let network connector customize personal filtering space 2025-11-12 14:57:18 +02:00
Tulir Asokan
7b33248d3d bridgev2: add flag to indicate when bridge is stopping
Some checks are pending
Go / Lint (latest) (push) Waiting to run
Go / Build (old, libolm) (push) Waiting to run
Go / Build (latest, libolm) (push) Waiting to run
Go / Build (old, goolm) (push) Waiting to run
Go / Build (latest, goolm) (push) Waiting to run
2025-11-12 01:54:29 +02:00
Tulir Asokan
19ed3ac40b changelog: update 2025-11-11 01:32:27 +02:00
Tulir Asokan
bb0b26a58b bridgev2/database: fix latest version
Some checks are pending
Go / Lint (latest) (push) Waiting to run
Go / Build (old, libolm) (push) Waiting to run
Go / Build (latest, libolm) (push) Waiting to run
Go / Build (old, goolm) (push) Waiting to run
Go / Build (latest, goolm) (push) Waiting to run
2025-11-11 01:07:40 +02:00
Tulir Asokan
77519b6de7 bridgev2/errors: send notice for public media errors 2025-11-11 01:07:40 +02:00
Nick Mills-Barrett
913a28fdce
bridgev2: pass back event ID and stream order in send results
Some checks are pending
Go / Lint (latest) (push) Waiting to run
Go / Build (old, libolm) (push) Waiting to run
Go / Build (latest, libolm) (push) Waiting to run
Go / Build (old, goolm) (push) Waiting to run
Go / Build (latest, goolm) (push) Waiting to run
2025-11-10 13:44:04 +00:00
Nick Mills-Barrett
1779c72316
bridgev2: pass back event ID and stream order in send results 2025-11-10 13:44:04 +00:00
Tulir Asokan
aa53cbc528 bridgev2/publicmedia: add support for encrypted files
Some checks are pending
Go / Lint (latest) (push) Waiting to run
Go / Build (old, libolm) (push) Waiting to run
Go / Build (latest, libolm) (push) Waiting to run
Go / Build (old, goolm) (push) Waiting to run
Go / Build (latest, goolm) (push) Waiting to run
2025-11-10 00:11:40 +02:00
Tulir Asokan
2eea2e7412 bridgev2/publicmedia: add support for file name in content disposition 2025-11-09 23:02:23 +02:00
Tulir Asokan
60cbe66e2f bridgev2/publicmedia: add support for custom path prefixes 2025-11-09 22:44:02 +02:00
Tulir Asokan
14e16a3a81 bridgev2/matrix: drop events from users without permission earlier
Some checks are pending
Go / Lint (latest) (push) Waiting to run
Go / Build (old, libolm) (push) Waiting to run
Go / Build (latest, libolm) (push) Waiting to run
Go / Build (old, goolm) (push) Waiting to run
Go / Build (latest, goolm) (push) Waiting to run
2025-11-09 11:40:10 +02:00
Tulir Asokan
fdd7632e53 bridgev2/matrix: avoid sending message status notices for m.notice events 2025-11-09 11:33:39 +02:00
Tulir Asokan
a973e5dc94 event/reply: only remove plaintext reply fallback if there is one in HTML
Some checks are pending
Go / Lint (latest) (push) Waiting to run
Go / Build (old, libolm) (push) Waiting to run
Go / Build (latest, libolm) (push) Waiting to run
Go / Build (old, goolm) (push) Waiting to run
Go / Build (latest, goolm) (push) Waiting to run
2025-11-08 09:49:15 +01:00
Tulir Asokan
bade596e49 bridgev2/portal: allow chaining ChatMembermap.Set calls
Some checks are pending
Go / Lint (latest) (push) Waiting to run
Go / Build (old, libolm) (push) Waiting to run
Go / Build (latest, libolm) (push) Waiting to run
Go / Build (old, goolm) (push) Waiting to run
Go / Build (latest, goolm) (push) Waiting to run
2025-11-07 14:33:00 +01:00
Tulir Asokan
3014bf966c bridgev2/commands: include options in user input prompt
Some checks are pending
Go / Lint (latest) (push) Waiting to run
Go / Build (old, libolm) (push) Waiting to run
Go / Build (latest, libolm) (push) Waiting to run
Go / Build (old, goolm) (push) Waiting to run
Go / Build (latest, goolm) (push) Waiting to run
2025-11-06 16:38:22 +01:00
Tulir Asokan
36d4e1f99c federation: don't close body when not reading it
Closes #431
2025-11-06 16:38:10 +01:00
Tulir Asokan
cfa47299df bridgev2/provisioning: add select type for login user input
Some checks are pending
Go / Lint (latest) (push) Waiting to run
Go / Build (old, libolm) (push) Waiting to run
Go / Build (latest, libolm) (push) Waiting to run
Go / Build (old, goolm) (push) Waiting to run
Go / Build (latest, goolm) (push) Waiting to run
2025-11-06 09:26:28 +01:00
Tulir Asokan
6e7b692098 federation/eventauth: fix restricted joins typo
Some checks failed
Go / Lint (latest) (push) Has been cancelled
Go / Build (old, libolm) (push) Has been cancelled
Go / Build (latest, libolm) (push) Has been cancelled
Go / Build (old, goolm) (push) Has been cancelled
Go / Build (latest, goolm) (push) Has been cancelled
2025-11-01 22:19:57 +01:00
Tulir Asokan
4ec3fbb4ab crypto/goolm: fix var bytes read overflow 2025-11-01 22:10:43 +01:00
Tulir Asokan
175f5a1c61 federation/serverauth: fix request uri
Some checks failed
Go / Lint (latest) (push) Has been cancelled
Go / Build (old, libolm) (push) Has been cancelled
Go / Build (latest, libolm) (push) Has been cancelled
Go / Build (old, goolm) (push) Has been cancelled
Go / Build (latest, goolm) (push) Has been cancelled
2025-10-31 21:11:24 +01:00
Nick Mills-Barrett
8e23192a7d
client: support sending custom txn ID query param with state events
Some checks are pending
Go / Lint (latest) (push) Waiting to run
Go / Build (old, libolm) (push) Waiting to run
Go / Build (latest, libolm) (push) Waiting to run
Go / Build (old, goolm) (push) Waiting to run
Go / Build (latest, goolm) (push) Waiting to run
2025-10-31 10:01:49 +00:00
Tulir Asokan
2ece053b2b
bridgev2: roll back failed room metadata changes (#425)
Some checks are pending
Go / Lint (latest) (push) Waiting to run
Go / Build (old, libolm) (push) Waiting to run
Go / Build (latest, libolm) (push) Waiting to run
Go / Build (old, goolm) (push) Waiting to run
Go / Build (latest, goolm) (push) Waiting to run
2025-10-31 00:07:24 +02:00
Tulir Asokan
be9bbf8d09 bridgev2/provisioning: fix max length checks in group creation
Some checks are pending
Go / Lint (latest) (push) Waiting to run
Go / Build (old, libolm) (push) Waiting to run
Go / Build (latest, libolm) (push) Waiting to run
Go / Build (old, goolm) (push) Waiting to run
Go / Build (latest, goolm) (push) Waiting to run
2025-10-29 22:50:02 +02:00
Tulir Asokan
0da0175157 bridgev2: add new flag for slack remote ID migration
Some checks are pending
Go / Lint (latest) (push) Waiting to run
Go / Build (old, libolm) (push) Waiting to run
Go / Build (latest, libolm) (push) Waiting to run
Go / Build (old, goolm) (push) Waiting to run
Go / Build (latest, goolm) (push) Waiting to run
2025-10-29 20:58:46 +02:00
timedout
1edfccb4e2
federation/client: Use PUT instead of POST to send transactions (#426) 2025-10-29 17:55:12 +00:00
Tulir Asokan
76cb8ee7d3 bridgev2/provisioning: add option to skip identifier validation in create group
Some checks are pending
Go / Lint (latest) (push) Waiting to run
Go / Build (old, libolm) (push) Waiting to run
Go / Build (latest, libolm) (push) Waiting to run
Go / Build (old, goolm) (push) Waiting to run
Go / Build (latest, goolm) (push) Waiting to run
2025-10-28 22:46:29 +02:00
Tulir Asokan
bea28c1381 bridgev2/portal: log mismatching disappearing timers in events
Some checks are pending
Go / Lint (latest) (push) Waiting to run
Go / Build (old, libolm) (push) Waiting to run
Go / Build (latest, libolm) (push) Waiting to run
Go / Build (old, goolm) (push) Waiting to run
Go / Build (latest, goolm) (push) Waiting to run
2025-10-28 15:06:46 +02:00
Tulir Asokan
adc035b6a5
event: add state and member action maps to room features (#424)
Some checks are pending
Go / Lint (latest) (push) Waiting to run
Go / Build (old, libolm) (push) Waiting to run
Go / Build (latest, libolm) (push) Waiting to run
Go / Build (old, goolm) (push) Waiting to run
Go / Build (latest, goolm) (push) Waiting to run
2025-10-27 18:39:10 +02:00
Tulir Asokan
d486dba927 event: add some getters for state content
Some checks failed
Go / Lint (latest) (push) Has been cancelled
Go / Build (old, libolm) (push) Has been cancelled
Go / Build (latest, libolm) (push) Has been cancelled
Go / Build (old, goolm) (push) Has been cancelled
Go / Build (latest, goolm) (push) Has been cancelled
2025-10-25 16:59:36 +03:00
Tulir Asokan
364ae39fef responses: add Equal method for LazyLoadSummary 2025-10-25 15:34:48 +03:00
Tulir Asokan
02a0aad583 bridgev2/portal: add event for waiting for room creation
Some checks failed
Go / Lint (latest) (push) Has been cancelled
Go / Build (old, libolm) (push) Has been cancelled
Go / Build (latest, libolm) (push) Has been cancelled
Go / Build (old, goolm) (push) Has been cancelled
Go / Build (latest, goolm) (push) Has been cancelled
2025-10-24 15:14:31 +03:00
Tulir Asokan
ee1e05c3e8 event: fix 32-bit compatibility 2025-10-24 13:15:46 +03:00
Tulir Asokan
5d87d14b88 event/powerlevels: fix some set user level calls in v12 rooms 2025-10-24 12:42:09 +03:00
Tulir Asokan
75ad1961d5 bridgev2/errors: add special-cased message for too long voice messages
Some checks are pending
Go / Lint (latest) (push) Waiting to run
Go / Build (old, libolm) (push) Waiting to run
Go / Build (latest, libolm) (push) Waiting to run
Go / Build (old, goolm) (push) Waiting to run
Go / Build (latest, goolm) (push) Waiting to run
2025-10-23 17:35:08 +03:00
Tulir Asokan
1be49d53e4 bridgev2/config: add option to limit maximum number of logins 2025-10-23 15:49:11 +03:00
Tulir Asokan
756196ad4f
bridgev2/disappear: only start timers for read messages rather than all pending ones (#415) 2025-10-23 15:12:42 +03:00
Tulir Asokan
33d8d658fe bridgev2/commands: fix panic when creating group with no arguments
Some checks are pending
Go / Lint (latest) (push) Waiting to run
Go / Build (old, libolm) (push) Waiting to run
Go / Build (latest, libolm) (push) Waiting to run
Go / Build (old, goolm) (push) Waiting to run
Go / Build (latest, goolm) (push) Waiting to run
2025-10-22 21:25:46 +03:00
Tulir Asokan
34a65d3087 bridgev2/commands: enable create group command 2025-10-22 21:24:14 +03:00
Tulir Asokan
bae61f955f bridgev2/matrixinvite: fix bugs in DM creation 2025-10-22 20:54:53 +03:00
Tulir Asokan
9fd1e0f87c bridgev2/networkinterface: allow deleting children in chat delete event 2025-10-22 18:56:41 +03:00
Tulir Asokan
7f0f51ecf3 bridgev2/commands: add command to sync single chat 2025-10-22 18:13:21 +03:00
Tulir Asokan
2a01535030 bridgev2/portal: add helpers for chat member map
Some checks are pending
Go / Lint (latest) (push) Waiting to run
Go / Build (old, libolm) (push) Waiting to run
Go / Build (latest, libolm) (push) Waiting to run
Go / Build (old, goolm) (push) Waiting to run
Go / Build (latest, goolm) (push) Waiting to run
2025-10-22 16:50:27 +03:00
Tulir Asokan
1cd285dee0 bridgev2/matrixinvite: allow redirecting created DM to no ghost 2025-10-22 16:50:16 +03:00
Tulir Asokan
e805815e41 bridgev2/commands: add account data debug command 2025-10-22 13:03:32 +03:00
Tulir Asokan
237499fdf5 client: fix admin whois response body
Some checks are pending
Go / Lint (latest) (push) Waiting to run
Go / Build (old, libolm) (push) Waiting to run
Go / Build (latest, libolm) (push) Waiting to run
Go / Build (old, goolm) (push) Waiting to run
Go / Build (latest, goolm) (push) Waiting to run
2025-10-21 22:53:18 +03:00
Tulir Asokan
ef31dae082 bridgev2/provisioning: include user and DM room MXID in failed participants 2025-10-21 18:55:49 +03:00
Tulir Asokan
1aacf6e987 bridgev2/commands: include failed participants in group create response
Some checks are pending
Go / Lint (latest) (push) Waiting to run
Go / Build (old, libolm) (push) Waiting to run
Go / Build (latest, libolm) (push) Waiting to run
Go / Build (old, goolm) (push) Waiting to run
Go / Build (latest, goolm) (push) Waiting to run
2025-10-21 17:40:18 +03:00
Tulir Asokan
8ee8fb1a20 bridgev2/provisioning: allow group creation to signal failed participants 2025-10-21 17:31:10 +03:00
Tulir Asokan
36edccf61a bridgev2/provisionutil: allow mxids as participants in CreateGroup 2025-10-21 16:59:18 +03:00
Tulir Asokan
56b182f85d bridgev2/bridgestate: only send one delayed transient disconnect notice
Some checks are pending
Go / Lint (latest) (push) Waiting to run
Go / Build (old, libolm) (push) Waiting to run
Go / Build (latest, libolm) (push) Waiting to run
Go / Build (old, goolm) (push) Waiting to run
Go / Build (latest, goolm) (push) Waiting to run
2025-10-20 11:48:45 +03:00
Tulir Asokan
7b70ec6d52 bridgev2/bridgestate: send transient disconnect notices if they persist 2025-10-20 11:45:35 +03:00
Tulir Asokan
a661641bcb bridgev2/matrix: don't sleep after registering bot on versions error
Some checks are pending
Go / Lint (latest) (push) Waiting to run
Go / Build (old, libolm) (push) Waiting to run
Go / Build (latest, libolm) (push) Waiting to run
Go / Build (old, goolm) (push) Waiting to run
Go / Build (latest, goolm) (push) Waiting to run
2025-10-19 23:36:07 +03:00
timedout
2fd9e799d2
synapseadmin: Add force_purge option (#420)
Some checks are pending
Go / Lint (latest) (push) Waiting to run
Go / Build (old, libolm) (push) Waiting to run
Go / Build (latest, libolm) (push) Waiting to run
Go / Build (old, goolm) (push) Waiting to run
Go / Build (latest, goolm) (push) Waiting to run
2025-10-18 21:27:08 +01:00
timedout
e61c7b3f1e
client: Add AdminWhoIs func (#411) 2025-10-18 20:30:43 +01:00
Tulir Asokan
c50460cd6e client: add response size limits
Some checks are pending
Go / Lint (latest) (push) Waiting to run
Go / Build (old, libolm) (push) Waiting to run
Go / Build (latest, libolm) (push) Waiting to run
Go / Build (old, goolm) (push) Waiting to run
Go / Build (latest, goolm) (push) Waiting to run
2025-10-18 13:54:14 +02:00
Tulir Asokan
827bb4c621 federation: add response size limit 2025-10-18 13:33:45 +02:00
Tulir Asokan
df957301be federation: don't allow redirects 2025-10-18 13:33:45 +02:00
Tulir Asokan
a214af5bab federation: fix server key query test 2025-10-18 13:33:45 +02:00
Brad Murray
572a704b04
errors: Add M_WRONG_ROOM_KEYS_VERSION (#419) 2025-10-18 05:42:01 -04:00
Tulir Asokan
50a49e01f3 Bump version to v0.25.2
Some checks failed
Go / Lint (latest) (push) Has been cancelled
Go / Build (old, libolm) (push) Has been cancelled
Go / Build (latest, libolm) (push) Has been cancelled
Go / Build (old, goolm) (push) Has been cancelled
Go / Build (latest, goolm) (push) Has been cancelled
2025-10-16 11:26:46 +02:00
Toni Spets
22ea75db96 client,event: MSC4140: Delayed events
Some checks failed
Go / Lint (latest) (push) Has been cancelled
Go / Build (old, libolm) (push) Has been cancelled
Go / Build (latest, libolm) (push) Has been cancelled
Go / Build (old, goolm) (push) Has been cancelled
Go / Build (latest, goolm) (push) Has been cancelled
Includes transparent migration from deprecated MSC fields still used
in Synapse to later revision.
2025-10-14 14:22:47 +03:00
Toni Spets
080ad4c0a0 crypto: Allow decrypting message content without event id or ts
Replay attack prevention shouldn't store empty event id or ts to
database if we're decrypting without them. This may happen if we are
looking into a future delayed event for example as it doesn't yet have
those.

We still prevent doing that if we already know them meaning we have
gotten the actual event through sync as that's also when a delayed event
would move from scheduled to finalised and then it also contains those
fields.
2025-10-14 14:22:42 +03:00
Tulir Asokan
ab4a7852d6 bridgev2/provisionutil: don't allow self in create group participants 2025-10-14 13:01:21 +03:00
Tulir Asokan
097813c9b2 bridgev2/provisionutil: validate user IDs in CreateGroup if network supports it
Some checks are pending
Go / Lint (latest) (push) Waiting to run
Go / Build (old, libolm) (push) Waiting to run
Go / Build (latest, libolm) (push) Waiting to run
Go / Build (old, goolm) (push) Waiting to run
Go / Build (latest, goolm) (push) Waiting to run
2025-10-14 00:20:04 +03:00
Tulir Asokan
5593d8afcd changelog: update
Some checks are pending
Go / Lint (latest) (push) Waiting to run
Go / Build (old, libolm) (push) Waiting to run
Go / Build (latest, libolm) (push) Waiting to run
Go / Build (old, goolm) (push) Waiting to run
Go / Build (latest, goolm) (push) Waiting to run
2025-10-13 15:30:12 +03:00
Tulir Asokan
91ea77b4d4 bridgev2/portal: don't send implicit read receipts for account data
Some checks failed
Go / Lint (latest) (push) Has been cancelled
Go / Build (old, libolm) (push) Has been cancelled
Go / Build (latest, libolm) (push) Has been cancelled
Go / Build (old, goolm) (push) Has been cancelled
Go / Build (latest, goolm) (push) Has been cancelled
2025-10-08 19:16:00 +03:00
Tulir Asokan
9654a0b01e bridgev2/portal: enforce media duration and size limits
Some checks failed
Go / Lint (latest) (push) Has been cancelled
Go / Build (old, libolm) (push) Has been cancelled
Go / Build (latest, libolm) (push) Has been cancelled
Go / Build (old, goolm) (push) Has been cancelled
Go / Build (latest, goolm) (push) Has been cancelled
2025-10-08 18:47:55 +03:00
Tulir Asokan
d18142c794 bridgev2/errors: add reason for unsupported errors 2025-10-08 18:33:57 +03:00
Tulir Asokan
3a300246ac id/userid: split validation into 2 functions
Some checks failed
Go / Lint (latest) (push) Has been cancelled
Go / Build (old, libolm) (push) Has been cancelled
Go / Build (latest, libolm) (push) Has been cancelled
Go / Build (old, goolm) (push) Has been cancelled
Go / Build (latest, goolm) (push) Has been cancelled
2025-10-06 23:10:04 +03:00
Tulir Asokan
51edfc27c0 bridgev2: add omitempty for group create params struct 2025-10-06 23:00:04 +03:00
Tulir Asokan
548970fd0f event: add Clone for other capability types too 2025-10-06 17:05:46 +03:00
Tulir Asokan
344b04c407 event: add Clone method for file features 2025-10-06 17:03:30 +03:00
Tulir Asokan
07bc756971 changelog: update 2025-10-06 16:51:41 +03:00
Tulir Asokan
13f251fe60 crypto/helper: don't block on decryption
Some checks failed
Go / Lint (latest) (push) Has been cancelled
Go / Build (old, libolm) (push) Has been cancelled
Go / Build (latest, libolm) (push) Has been cancelled
Go / Build (old, goolm) (push) Has been cancelled
Go / Build (latest, goolm) (push) Has been cancelled
2025-10-05 12:30:54 +03:00
Tulir Asokan
8a72af9f6b federation/eventauth: require that join authorizer is in the room
Some checks failed
Go / Lint (latest) (push) Has been cancelled
Go / Build (old, libolm) (push) Has been cancelled
Go / Build (latest, libolm) (push) Has been cancelled
Go / Build (old, goolm) (push) Has been cancelled
Go / Build (latest, goolm) (push) Has been cancelled
2025-10-03 22:51:38 +03:00
Tulir Asokan
4be60a0021 bridgev2/simplevent: allow upserts with PreConvertedMessage
Some checks are pending
Go / Lint (latest) (push) Waiting to run
Go / Build (old, libolm) (push) Waiting to run
Go / Build (latest, libolm) (push) Waiting to run
Go / Build (old, goolm) (push) Waiting to run
Go / Build (latest, goolm) (push) Waiting to run
2025-10-03 03:14:51 +03:00
Tulir Asokan
ce667a65e5 bridgev2/simplevent: add simpler form of message event 2025-10-03 03:10:29 +03:00
Tulir Asokan
8e668586f9 appservice/intent: add room ID to fake join response
Some checks are pending
Go / Lint (latest) (push) Waiting to run
Go / Build (old, libolm) (push) Waiting to run
Go / Build (latest, libolm) (push) Waiting to run
Go / Build (old, goolm) (push) Waiting to run
Go / Build (latest, goolm) (push) Waiting to run
2025-10-02 22:10:22 +03:00
Tulir Asokan
9fc5d98774 bridgev2/mxmain: fix --version flag output 2025-10-02 21:57:25 +03:00
Tulir Asokan
5d69963ab5 bridgev2/portal: add exclude from timeline flag for not in chat leaves 2025-10-02 17:19:45 +03:00
Tulir Asokan
97da8eb44d event: add helper to get remaining mute duration
Some checks are pending
Go / Lint (latest) (push) Waiting to run
Go / Build (old, libolm) (push) Waiting to run
Go / Build (latest, libolm) (push) Waiting to run
Go / Build (old, goolm) (push) Waiting to run
Go / Build (latest, goolm) (push) Waiting to run
2025-10-02 14:45:46 +03:00
Tulir Asokan
dd778ae0cd bridgev2/portal: add option to exclude metadata changes from timeline
Some checks are pending
Go / Lint (latest) (push) Waiting to run
Go / Build (old, libolm) (push) Waiting to run
Go / Build (latest, libolm) (push) Waiting to run
Go / Build (old, goolm) (push) Waiting to run
Go / Build (latest, goolm) (push) Waiting to run
2025-10-01 14:55:35 +03:00
Tulir Asokan
9ee13d1363 bridgev2/portal: add option to exclude member changes from timeline by default 2025-10-01 14:48:28 +03:00
Tulir Asokan
77682fb292 bridgev2,error: use NonNilClone instead of creating map manually 2025-10-01 14:48:11 +03:00
Tulir Asokan
329da10584 bridgev2/database: fix split portal parent migration query
Some checks are pending
Go / Lint (latest) (push) Waiting to run
Go / Build (old, libolm) (push) Waiting to run
Go / Build (latest, libolm) (push) Waiting to run
Go / Build (old, goolm) (push) Waiting to run
Go / Build (latest, goolm) (push) Waiting to run
2025-09-30 15:35:25 +03:00
Tulir Asokan
b597f149b7 version: initialize go.mod version regex lazily 2025-09-28 20:39:07 +03:00
Tulir Asokan
f2b77f0433 version: find from build info if unset
Some checks failed
Go / Lint (latest) (push) Has been cancelled
Go / Build (old, libolm) (push) Has been cancelled
Go / Build (latest, libolm) (push) Has been cancelled
Go / Build (old, goolm) (push) Has been cancelled
Go / Build (latest, goolm) (push) Has been cancelled
2025-09-28 20:35:41 +03:00
Tulir Asokan
d146b6caf8 bridgev2/mxmain: move version calculation to go-util
Some checks are pending
Go / Lint (latest) (push) Waiting to run
Go / Build (old, libolm) (push) Waiting to run
Go / Build (latest, libolm) (push) Waiting to run
Go / Build (old, goolm) (push) Waiting to run
Go / Build (latest, goolm) (push) Waiting to run
2025-09-27 17:09:44 +03:00
Tulir Asokan
743cbb5f2c bridgev2/mxmain: add option to mix calendar and semantic versioning 2025-09-27 16:26:15 +03:00
Tulir Asokan
9878c3d675 federation/eventauth: change error message for users-specific power level check
Some checks are pending
Go / Lint (latest) (push) Waiting to run
Go / Build (old, libolm) (push) Waiting to run
Go / Build (latest, libolm) (push) Waiting to run
Go / Build (old, goolm) (push) Waiting to run
Go / Build (latest, goolm) (push) Waiting to run
2025-09-26 23:36:58 +03:00
Tulir Asokan
6e231a45e4 federation/eventauth: fix gjson path construction in new power level check 2025-09-26 23:36:03 +03:00
Tulir Asokan
ae6a0b4f51 federation/eventauth: fix checking user power level changes 2025-09-26 23:26:17 +03:00
Tulir Asokan
a3c6832c48 federation/eventauth: fix default power levels in pre-v12 rooms 2025-09-26 23:18:05 +03:00
Tulir Asokan
acc449daf4 crypto: add basic group session sharing benchmark 2025-09-26 20:37:58 +03:00
Tulir Asokan
fa90bba820 crypto: don't check otk count if sharing new keys 2025-09-26 19:48:22 +03:00
Tulir Asokan
caca057b23 crypto/helper: always share keys when creating new device 2025-09-26 19:17:16 +03:00
Tulir Asokan
0685bd7786 crypto/verificationhelper: extract mockserver to new package 2025-09-26 16:56:48 +03:00
Tulir Asokan
b0481d4b43 client: re-add support for unstable profile fields
Some checks are pending
Go / Lint (latest) (push) Waiting to run
Go / Build (old, libolm) (push) Waiting to run
Go / Build (latest, libolm) (push) Waiting to run
Go / Build (old, goolm) (push) Waiting to run
Go / Build (latest, goolm) (push) Waiting to run
2025-09-26 12:55:36 +03:00
Tulir Asokan
cf29b07f32 appservice/websocket: use io.ReadAll instead of json decoder
Some checks failed
Go / Lint (latest) (push) Has been cancelled
Go / Build (old, libolm) (push) Has been cancelled
Go / Build (latest, libolm) (push) Has been cancelled
Go / Build (old, goolm) (push) Has been cancelled
Go / Build (latest, goolm) (push) Has been cancelled
2025-09-24 20:29:49 +03:00
Tulir Asokan
5c580a7859 crypto/sqlstore: fix query used for olm unwedging
Some checks failed
Go / Lint (latest) (push) Has been cancelled
Go / Build (old, libolm) (push) Has been cancelled
Go / Build (latest, libolm) (push) Has been cancelled
Go / Build (old, goolm) (push) Has been cancelled
Go / Build (latest, goolm) (push) Has been cancelled
2025-09-22 20:28:49 +03:00
Tulir Asokan
4635590fca bridgev2/portal: add temporary flag to slack bridge info
To let clients detect that 952806ea52 is done
2025-09-22 18:24:49 +03:00
Tulir Asokan
a8b5fa9156 client: fix some footguns in compileRequest
Some checks are pending
Go / Lint (latest) (push) Waiting to run
Go / Build (old, libolm) (push) Waiting to run
Go / Build (latest, libolm) (push) Waiting to run
Go / Build (old, goolm) (push) Waiting to run
Go / Build (latest, goolm) (push) Waiting to run
* add warning log if RequestBody is used without length instead of
  silently discarding the body
* fix wrapping RequestBody in nopcloser
* always set content length
2025-09-22 16:32:29 +03:00
Tulir Asokan
d5c6393f23 bridgev2/portal: don't process any more events if portal is deleted 2025-09-22 16:11:21 +03:00
Tulir Asokan
a9ff1443f7 bridgev2: add interface for deleting chats from Matrix
Closes #408
2025-09-22 16:05:53 +03:00
Tulir Asokan
b3c883bc7f event: add beeper chat delete event 2025-09-22 16:05:28 +03:00
Tulir Asokan
23b18aa0ca bridgev2/provisioning: fix login_id query param name 2025-09-22 14:46:47 +03:00
Tulir Asokan
c4701ba06c responses: fix RespSearchUserDirectory type 2025-09-22 14:30:41 +03:00
Tulir Asokan
f9fb77d6aa client: add user directory search method 2025-09-22 13:46:46 +03:00
Toni Spets
cf814a5aaa
error: Add RespError WithExtraData convenience function (#416)
To dynamically build errors with extra keys like returning `max_delay`
for `M_MAX_DELAY_EXCEEDED`.
2025-09-22 13:30:08 +03:00
Tulir Asokan
0198ef315c changelog: update
Some checks are pending
Go / Lint (latest) (push) Waiting to run
Go / Build (old, libolm) (push) Waiting to run
Go / Build (latest, libolm) (push) Waiting to run
Go / Build (old, goolm) (push) Waiting to run
Go / Build (latest, goolm) (push) Waiting to run
2025-09-21 20:51:51 +03:00
Tulir Asokan
658b2e1d1d bridgev2/matrix: share device keys as part of e2ee init 2025-09-21 20:34:04 +03:00
Tulir Asokan
6c37f2b21f bridgev2/matrix: add config option to self-sign bot device 2025-09-21 20:34:04 +03:00
Tulir Asokan
0a84c052dd crypto: add utilities for cross-signing 2025-09-21 20:10:59 +03:00
Tulir Asokan
0012a23d85 bridgev2/portal: don't allow queuing events into uninitialized portals
Some checks failed
Go / Lint (latest) (push) Has been cancelled
Go / Build (old, libolm) (push) Has been cancelled
Go / Build (latest, libolm) (push) Has been cancelled
Go / Build (old, goolm) (push) Has been cancelled
Go / Build (latest, goolm) (push) Has been cancelled
2025-09-19 21:21:25 +03:00
Tulir Asokan
fbf8718e22 bridgev2: also fix portal parent receivers in split portal migration 2025-09-19 21:19:55 +03:00
Tulir Asokan
54c0e5c2f6 bridgev2/portal: remove portal from cache if loading parent/relay fails 2025-09-19 21:19:01 +03:00
Tulir Asokan
820d0ee66b bridgev2: only delete rooms in split portal migration after starting connectors 2025-09-19 21:01:42 +03:00
Tulir Asokan
f7bfa885c9 bridgev2: improve split portal migration 2025-09-19 20:45:17 +03:00
Tulir Asokan
9fbf1b8598 bridgev2: make split portal migration errors fatal 2025-09-19 20:26:55 +03:00
Tulir Asokan
b42fb5096a bridgev2/portal: also log long events when using async events 2025-09-19 19:53:22 +03:00
Tulir Asokan
2240aa0267 bridgev2/portal: log if room create event is taking long 2025-09-19 19:50:41 +03:00
Tulir Asokan
6acb04aa1e federation/pdu: use option to trust internal metadata for GetEventID 2025-09-19 19:15:02 +03:00
Tulir Asokan
b760023dca bridgev2/portal: add support for implicit read receipts to network
Some checks are pending
Go / Lint (latest) (push) Waiting to run
Go / Build (old, libolm) (push) Waiting to run
Go / Build (latest, libolm) (push) Waiting to run
Go / Build (old, goolm) (push) Waiting to run
Go / Build (latest, goolm) (push) Waiting to run
2025-09-19 14:30:47 +03:00
Tulir Asokan
8780c2eb44 bridgev2/portal: set exclude from timeline flag for creation state 2025-09-19 13:23:15 +03:00
Tiago Loureiro
e19d009d59
event: add EventUnstablePollEnd to GuessClass() (#414)
Some checks are pending
Go / Lint (latest) (push) Waiting to run
Go / Build (old, libolm) (push) Waiting to run
Go / Build (latest, libolm) (push) Waiting to run
Go / Build (old, goolm) (push) Waiting to run
Go / Build (latest, goolm) (push) Waiting to run
2025-09-18 11:07:13 -03:00
Tulir Asokan
e932aff209 crypto/ssss: use constant time comparison when decrypting account data
Some checks are pending
Go / Lint (latest) (push) Waiting to run
Go / Build (old, libolm) (push) Waiting to run
Go / Build (latest, libolm) (push) Waiting to run
Go / Build (old, goolm) (push) Waiting to run
Go / Build (latest, goolm) (push) Waiting to run
2025-09-17 22:30:32 +03:00
Tulir Asokan
5b860f8bfb responses: fix marshaling RespUserProfile 2025-09-17 22:30:16 +03:00
Tulir Asokan
35ac4fcb8d bridgev2/matrix: don't encrypt reactions in batch sends 2025-09-17 21:45:43 +03:00
Tulir Asokan
e6a1fa6fd7
bridgev2/provisioning: sync ghost info when searching (#413)
Some checks are pending
Go / Lint (latest) (push) Waiting to run
Go / Build (old, libolm) (push) Waiting to run
Go / Build (latest, libolm) (push) Waiting to run
Go / Build (old, goolm) (push) Waiting to run
Go / Build (latest, goolm) (push) Waiting to run
2025-09-17 15:18:43 +03:00
Tulir Asokan
af2e6c7ce0 bridgev2/portal: ensure state key is set when handling state events 2025-09-17 14:47:09 +03:00
Tulir Asokan
5af25d2eb7 event/poll: add missing omitempty
Some checks are pending
Go / Lint (latest) (push) Waiting to run
Go / Build (old, libolm) (push) Waiting to run
Go / Build (latest, libolm) (push) Waiting to run
Go / Build (old, goolm) (push) Waiting to run
Go / Build (latest, goolm) (push) Waiting to run
2025-09-16 18:02:14 +03:00
Tulir Asokan
c37ddcc3a5 Bump version to v0.25.1
Some checks are pending
Go / Lint (latest) (push) Waiting to run
Go / Build (old, libolm) (push) Waiting to run
Go / Build (latest, libolm) (push) Waiting to run
Go / Build (old, goolm) (push) Waiting to run
Go / Build (latest, goolm) (push) Waiting to run
2025-09-16 14:45:37 +03:00
Tulir Asokan
b5bec2e96c client: stabilize support for state_after
Some checks are pending
Go / Lint (latest) (push) Waiting to run
Go / Build (old, libolm) (push) Waiting to run
Go / Build (latest, libolm) (push) Waiting to run
Go / Build (old, goolm) (push) Waiting to run
Go / Build (latest, goolm) (push) Waiting to run
2025-09-13 13:44:46 +03:00
Tulir Asokan
717c8c3092 bridgev2/database: normalize disappearing settings before insert
Some checks failed
Go / Lint (latest) (push) Has been cancelled
Go / Build (old, libolm) (push) Has been cancelled
Go / Build (latest, libolm) (push) Has been cancelled
Go / Build (old, goolm) (push) Has been cancelled
Go / Build (latest, goolm) (push) Has been cancelled
2025-09-13 01:38:06 +03:00
Tulir Asokan
3a6f20bb62 crypto/sqlstore: ignore unused sessions in olm unwedging
Some checks are pending
Go / Lint (latest) (push) Waiting to run
Go / Build (old, libolm) (push) Waiting to run
Go / Build (latest, libolm) (push) Waiting to run
Go / Build (old, goolm) (push) Waiting to run
Go / Build (latest, goolm) (push) Waiting to run
2025-09-12 19:30:05 +03:00
Tiago Loureiro
4603a344ce
event: add org.matrix.msc3381.poll.end type (#412)
Some checks are pending
Go / Lint (latest) (push) Waiting to run
Go / Build (old, libolm) (push) Waiting to run
Go / Build (latest, libolm) (push) Waiting to run
Go / Build (old, goolm) (push) Waiting to run
Go / Build (latest, goolm) (push) Waiting to run
2025-09-11 15:10:14 -03:00
Tulir Asokan
5dbab3ae99 crypto/machine: don't clear account on Destroy()
Some checks are pending
Go / Lint (latest) (push) Waiting to run
Go / Build (old, libolm) (push) Waiting to run
Go / Build (latest, libolm) (push) Waiting to run
Go / Build (old, goolm) (push) Waiting to run
Go / Build (latest, goolm) (push) Waiting to run
2025-09-11 14:46:21 +03:00
Tulir Asokan
87fe127414 crypto/decryptolm: retry prekey decryption with goolm 2025-09-11 14:17:24 +03:00
Tulir Asokan
c716f30959 crypto/register: don't use init in *olm packages 2025-09-11 14:14:15 +03:00
Tulir Asokan
84e5d6bda1 crypto/machine: allow canceling background context 2025-09-11 14:13:18 +03:00
Tulir Asokan
69869f7cb5 crypto: log active driver 2025-09-11 14:12:35 +03:00
Tulir Asokan
bdb9e22a43 crypto/libolm: clean up pointer management 2025-09-11 13:22:45 +03:00
Tulir Asokan
faa1c5ff8d crypto/machine: log when loading olm account
Some checks are pending
Go / Lint (latest) (push) Waiting to run
Go / Build (old, libolm) (push) Waiting to run
Go / Build (latest, libolm) (push) Waiting to run
Go / Build (old, goolm) (push) Waiting to run
Go / Build (latest, goolm) (push) Waiting to run
2025-09-10 16:46:05 +03:00
Tulir Asokan
22a908d8d6 crypto/decryptolm: add debug logs for failing to decrypt with new session 2025-09-10 16:24:43 +03:00
Tulir Asokan
e295028ffd client: stabilize arbitrary profile field support
Some checks are pending
Go / Lint (latest) (push) Waiting to run
Go / Build (old, libolm) (push) Waiting to run
Go / Build (latest, libolm) (push) Waiting to run
Go / Build (old, goolm) (push) Waiting to run
Go / Build (latest, goolm) (push) Waiting to run
2025-09-09 19:10:07 +03:00
Tulir Asokan
41bbe4ace4 bridgev2/portal: add action message metadata to disappearing notices 2025-09-09 16:24:18 +03:00
Tulir Asokan
30ab68f7f1 appservice: maybe fix url template raw path for unix sockets
Some checks failed
Go / Lint (latest) (push) Has been cancelled
Go / Build (old, libolm) (push) Has been cancelled
Go / Build (latest, libolm) (push) Has been cancelled
Go / Build (old, goolm) (push) Has been cancelled
Go / Build (latest, goolm) (push) Has been cancelled
2025-09-04 18:18:53 +03:00
Tulir Asokan
709f48f2b3 bridgev2/provisioning: remove unused structs
Some checks failed
Go / Lint (latest) (push) Has been cancelled
Go / Build (old, libolm) (push) Has been cancelled
Go / Build (latest, libolm) (push) Has been cancelled
Go / Build (old, goolm) (push) Has been cancelled
Go / Build (latest, goolm) (push) Has been cancelled
2025-09-02 18:24:24 +03:00
Tulir Asokan
8f8b26d815 event: add is_animated flag from MSC4230 2025-09-02 10:33:49 +03:00
Tulir Asokan
bcd0a70bdf appservice/websocket: override read limit
Some checks are pending
Go / Lint (latest) (push) Waiting to run
Go / Build (old, libolm) (push) Waiting to run
Go / Build (latest, libolm) (push) Waiting to run
Go / Build (old, goolm) (push) Waiting to run
Go / Build (latest, goolm) (push) Waiting to run
2025-09-02 00:31:15 +03:00
Tulir Asokan
f8c3a95de7
bridgev2: add support for creating groups (#405)
Some checks are pending
Go / Lint (latest) (push) Waiting to run
Go / Build (old, libolm) (push) Waiting to run
Go / Build (latest, libolm) (push) Waiting to run
Go / Build (old, goolm) (push) Waiting to run
Go / Build (latest, goolm) (push) Waiting to run
2025-09-01 18:01:20 +03:00
timedout (aka nexy7574)
0627c42270
client: implement MSC4323 (#407) 2025-09-01 16:01:05 +01:00
Tulir Asokan
61a90da145 event: use RawMessage instead of map for bot command arguments
Some checks are pending
Go / Lint (latest) (push) Waiting to run
Go / Build (old, libolm) (push) Waiting to run
Go / Build (latest, libolm) (push) Waiting to run
Go / Build (old, goolm) (push) Waiting to run
Go / Build (latest, goolm) (push) Waiting to run
2025-09-01 00:45:32 +03:00
Tulir Asokan
cd927c2796 event: add types for MSC4332
Some checks failed
Go / Lint (latest) (push) Has been cancelled
Go / Build (old, libolm) (push) Has been cancelled
Go / Build (latest, libolm) (push) Has been cancelled
Go / Build (old, goolm) (push) Has been cancelled
Go / Build (latest, goolm) (push) Has been cancelled
2025-08-30 19:54:58 +03:00
fmseals
1d6bea5fe3
client: fix v3/delete_devices method (#393)
Some checks are pending
Go / Lint (latest) (push) Waiting to run
Go / Build (old, libolm) (push) Waiting to run
Go / Build (latest, libolm) (push) Waiting to run
Go / Build (old, goolm) (push) Waiting to run
Go / Build (latest, goolm) (push) Waiting to run
2025-08-29 19:34:06 +03:00
Tulir Asokan
050fbbd466 bridgev2/status: change RemoteID to a UserLoginID 2025-08-29 18:32:04 +03:00
Tulir Asokan
f9e3e8a30f bridgev2/provisionutil: allow passing mxids to ResolveIdentifier
Closes #398
2025-08-29 18:32:04 +03:00
Tulir Asokan
8f464b5b76 bridgev2: move shared SNC code to provisionutil 2025-08-29 16:45:54 +03:00
Ping Chen
c18d2e2565
bridgev2/matrixinterface: add GetEvent interface for linkedin reply (#406)
Some checks are pending
Go / Lint (latest) (push) Waiting to run
Go / Build (old, libolm) (push) Waiting to run
Go / Build (latest, libolm) (push) Waiting to run
Go / Build (old, goolm) (push) Waiting to run
Go / Build (latest, goolm) (push) Waiting to run
Co-authored-by: Tulir Asokan <tulir@maunium.net>
2025-08-29 17:20:11 +09:00
Tulir Asokan
19f3b2179c pre-commit: ban log.Str(x.String()) 2025-08-29 11:07:16 +03:00
Tulir Asokan
3048d2edab bridgev2/provisioning: add minimum length for shared secret
Some checks failed
Go / Lint (latest) (push) Has been cancelled
Go / Build (old, libolm) (push) Has been cancelled
Go / Build (latest, libolm) (push) Has been cancelled
Go / Build (old, goolm) (push) Has been cancelled
Go / Build (latest, goolm) (push) Has been cancelled
2025-08-28 02:20:41 +03:00
Tulir Asokan
359afbea2b bridgev2/matrix: remove provisioning API prefix option
Reverse proxy configuration should be used instead when adding prefixes
to the path. Changing the path entirely is not recommended even with
reverse proxies.

Fixes #403
2025-08-28 02:19:27 +03:00
Tulir Asokan
febca20dd7 bridgev2/status: use _file pattern for avatar instead of splitting url and keys
Some checks are pending
Go / Lint (latest) (push) Waiting to run
Go / Build (old, libolm) (push) Waiting to run
Go / Build (latest, libolm) (push) Waiting to run
Go / Build (old, goolm) (push) Waiting to run
Go / Build (latest, goolm) (push) Waiting to run
2025-08-27 17:12:00 +03:00
Tulir Asokan
9f693702b0 federation/pdu: add extra field to internal metadata 2025-08-27 12:25:08 +03:00
Tulir Asokan
f131ae5aa4 federation/pdu: add cached event ID to internal metadata
Some checks are pending
Go / Lint (latest) (push) Waiting to run
Go / Build (old, libolm) (push) Waiting to run
Go / Build (latest, libolm) (push) Waiting to run
Go / Build (old, goolm) (push) Waiting to run
Go / Build (latest, goolm) (push) Waiting to run
2025-08-27 12:24:15 +03:00
Tulir Asokan
ba16c30a8c
federation/eventauth: add v3-v12 event auth rules (#401)
Some checks are pending
Go / Lint (latest) (push) Waiting to run
Go / Build (old, libolm) (push) Waiting to run
Go / Build (latest, libolm) (push) Waiting to run
Go / Build (old, goolm) (push) Waiting to run
Go / Build (latest, goolm) (push) Waiting to run
2025-08-27 01:45:33 +03:00
Tulir Asokan
0345a5356d bridgev2/database: don't set disappearing timer content to nil
Some checks are pending
Go / Lint (latest) (push) Waiting to run
Go / Build (old, libolm) (push) Waiting to run
Go / Build (latest, libolm) (push) Waiting to run
Go / Build (old, goolm) (push) Waiting to run
Go / Build (latest, goolm) (push) Waiting to run
2025-08-26 17:07:16 +03:00
Tulir Asokan
7b3a60742e event: allow omitting timers from disappearing timer capability 2025-08-26 15:57:10 +03:00
Tulir Asokan
e9d4eeb332 bridgev2/status: add avatar_keys to remote profile 2025-08-26 15:56:27 +03:00
Tulir Asokan
63b654187d event: marshal zero disappearing timers as empty object
Some checks are pending
Go / Lint (latest) (push) Waiting to run
Go / Build (old, libolm) (push) Waiting to run
Go / Build (latest, libolm) (push) Waiting to run
Go / Build (old, goolm) (push) Waiting to run
Go / Build (latest, goolm) (push) Waiting to run
2025-08-25 19:03:07 +03:00
Tulir Asokan
c3a422347c bridgev2/portal: validate capabilities when updating disappearing timer 2025-08-25 18:37:15 +03:00
Tulir Asokan
bca8b0528c sqlstatestore: fix GetPowerLevels returning non-nil even if power levels weren't found 2025-08-25 18:27:49 +03:00
Tulir Asokan
4f7c7dafdc bridgev2/matrix: fix encryption error notice not being redacted after retry success 2025-08-25 17:42:20 +03:00
Tulir Asokan
a6bbe978bd bridgev2/networkinterface: add interface for handling disappearing timer changes from Matrix 2025-08-25 17:35:57 +03:00
Tulir Asokan
f860b0e238 bridgev2/portal: fix send notice option when updating disappearing message timer 2025-08-25 17:23:25 +03:00
Tulir Asokan
8e703410f4 bridgev2/portal: always set timestamp for disappearing message timer update 2025-08-25 17:21:55 +03:00
Tulir Asokan
5ac8a888a3 bridgev2/portal: make UpdateDisappearingSetting more versatile 2025-08-25 17:16:18 +03:00
Tulir Asokan
0fab92dbc1 event: add third party invite state event content 2025-08-25 17:16:18 +03:00
Tulir Asokan
c04d0b6681 bridgev2: merge mentions and url previews when merging caption 2025-08-25 17:16:18 +03:00
Brad Murray
fa7c1ae2bc
crypto/sqlstore: add index to make finding megolm sessions to backup faster (#402)
```
2025-08-24T22:23:19Z debug    [MatrixBridgeV2]           {"level":"warn","component":"matrix","component":"client_loop","subcomponent":"sync_key_backup_loop","rows":0,"duration_seconds":1.046191042,"method":"EndRows","query":"SELECT room_id, sender_key, signing_key, session, forwarding_chains, ratchet_safety, received_at, max_age, max_messages, is_scheduled, key_backup_version FROM crypto_megolm_inbound_session WHERE account_id=?1 AND session IS NOT NULL AND key_backup_version != ?2","time":"2025-08-24T22:23:19.22077Z","message":"Query took long"} 
```

before:
```
sqlite> EXPLAIN SELECT room_id, sender_key, signing_key, session, forwarding_chains, ratchet_safety, received_at, max_age, max_messages, is_scheduled, key_backup_version FROM crypto_megolm_inbound_session WHERE account_id='@brad:beeper.com/CHNWOJWEUC' AND sessi
addr  opcode         p1    p2    p3    p4             p5  comment
----  -------------  ----  ----  ----  -------------  --  -------------
0     Init           0     25    0                    0   Start at 25
1     OpenRead       0     48    0     15             0   root=48 iDb=0; crypto_megolm_inbound_session
2     OpenRead       1     49    0     k(3,,,)        2   root=49 iDb=0; sqlite_autoindex_crypto_megolm_inbound_session_1
3     String8        0     1     0     @brad:beeper.com/CHNWOJWEUC 0   r[1]='@brad:beeper.com/CHNWOJWEUC'
4     SeekGE         1     24    1     1              0   key=r[1]
5       IdxGT          1     24    1     1              0   key=r[1]
6       DeferredSeek   1     0     0                    0   Move 0 to 1.rowid if needed
7       Column         0     5     2                    128 r[2]= cursor 0 column 5
8       IsNull         2     23    0                    0   if r[2]==NULL goto 23
9       Column         0     14    2                    0   r[2]=crypto_megolm_inbound_session.key_backup_version
10      Eq             3     23    2     BINARY-8       82  if r[2]==r[3] goto 23
11      Column         0     4     4                    0   r[4]= cursor 0 column 4
12      Column         0     2     5                    0   r[5]= cursor 0 column 2
13      Column         0     3     6                    0   r[6]= cursor 0 column 3
14      Column         0     5     7                    0   r[7]= cursor 0 column 5
15      Column         0     6     8                    0   r[8]= cursor 0 column 6
16      Column         0     9     9                    0   r[9]= cursor 0 column 9
17      Column         0     10    10                   0   r[10]= cursor 0 column 10
18      Column         0     11    11                   0   r[11]= cursor 0 column 11
19      Column         0     12    12                   0   r[12]= cursor 0 column 12
20      Column         0     13    13    0              0   r[13]=crypto_megolm_inbound_session.is_scheduled
21      Column         0     14    14                   0   r[14]=crypto_megolm_inbound_session.key_backup_version
22      ResultRow      4     11    0                    0   output=r[4..14]
23    Next           1     5     0                    0
24    Halt           0     0     0                    0
25    Transaction    0     0     55    0              1   usesStmtJournal=0
26    Integer        1     3     0                    0   r[3]=1
27    Goto           0     1     0                    0
sqlite> SELECT COUNT(*) FROM crypto_megolm_inbound_session ;
+----------+
| COUNT(*) |
+----------+
| 168792   |
+----------+
sqlite> SELECT COUNT(*) FROM crypto_megolm_inbound_session WHERE session IS NULL;
+----------+
| COUNT(*) |
+----------+
| 39       |
+----------+
sqlite> SELECT COUNT(*) FROM crypto_megolm_inbound_session WHERE key_backup_version != 1;
+----------+
| COUNT(*) |
+----------+
| 39       |
+----------+
```

after:
```
sqlite> CREATE INDEX idx_megolm_filtered
   ...> ON crypto_megolm_inbound_session(account_id, key_backup_version, session);
sqlite> EXPLAIN SELECT room_id, sender_key, signing_key, session, forwarding_chains, ratchet_safety, received_at, max_age, max_messages, is_scheduled, key_backup_version FROM crypto_megolm_inbound_session WHERE account_id='@brad:beeper.com/CHNWOJWEUC' AND session IS NOT NULL AND key_backup_version != 1;
addr  opcode         p1    p2    p3    p4             p5  comment
----  -------------  ----  ----  ----  -------------  --  -------------
0     Init           0     25    0                    0   Start at 25
1     OpenRead       0     48    0     15             0   root=48 iDb=0; crypto_megolm_inbound_session
2     OpenRead       1     91264 0     k(4,,,,)       2   root=91264 iDb=0; idx_megolm_filtered
3     String8        0     1     0     @brad:beeper.com/CHNWOJWEUC 0   r[1]='@brad:beeper.com/CHNWOJWEUC'
4     SeekGE         1     24    1     1              0   key=r[1]
5       IdxGT          1     24    1     1              0   key=r[1]
6       DeferredSeek   1     0     0                    0   Move 0 to 1.rowid if needed
7       Column         1     2     2                    128 r[2]= cursor 1 column 2
8       IsNull         2     23    0                    0   if r[2]==NULL goto 23
9       Column         1     1     2                    0   r[2]=crypto_megolm_inbound_session.key_backup_version
10      Eq             3     23    2     BINARY-8       82  if r[2]==r[3] goto 23
11      Column         0     4     4                    0   r[4]= cursor 0 column 4
12      Column         0     2     5                    0   r[5]= cursor 0 column 2
13      Column         0     3     6                    0   r[6]= cursor 0 column 3
14      Column         1     2     7                    0   r[7]= cursor 1 column 2
15      Column         0     6     8                    0   r[8]= cursor 0 column 6
16      Column         0     9     9                    0   r[9]= cursor 0 column 9
17      Column         0     10    10                   0   r[10]= cursor 0 column 10
18      Column         0     11    11                   0   r[11]= cursor 0 column 11
19      Column         0     12    12                   0   r[12]= cursor 0 column 12
20      Column         0     13    13    0              0   r[13]=crypto_megolm_inbound_session.is_scheduled
21      Column         1     1     14                   0   r[14]=crypto_megolm_inbound_session.key_backup_version
22      ResultRow      4     11    0                    0   output=r[4..14]
23    Next           1     5     0                    0
24    Halt           0     0     0                    0
25    Transaction    0     0     56    0              1   usesStmtJournal=0
26    Integer        1     3     0                    0   r[3]=1
27    Goto           0     1     0                    0
sqlite>
```
2025-08-25 08:03:13 -04:00
Tulir Asokan
7e07700a69 format: add MarkdownMentionRoomID helper
Some checks failed
Go / Lint (latest) (push) Has been cancelled
Go / Build (old, libolm) (push) Has been cancelled
Go / Build (latest, libolm) (push) Has been cancelled
Go / Build (old, goolm) (push) Has been cancelled
Go / Build (latest, goolm) (push) Has been cancelled
2025-08-24 00:47:55 +03:00
Tulir Asokan
d2cad8c57e format: add MarkdownMentionWithName helper 2025-08-24 00:44:50 +03:00
Tulir Asokan
71bbbdb3c3 federation/pdu: use jsontext.Value instead of any for deprecated fields 2025-08-23 23:22:43 +03:00
Tulir Asokan
363aa94389 federation/pdu: add server name parameter to GetKeyFunc
Some checks are pending
Go / Lint (latest) (push) Waiting to run
Go / Build (old, libolm) (push) Waiting to run
Go / Build (latest, libolm) (push) Waiting to run
Go / Build (old, goolm) (push) Waiting to run
Go / Build (latest, goolm) (push) Waiting to run
2025-08-23 03:13:10 +03:00
Tulir Asokan
fd20a61d87 event: add json struct tag to third party signed object 2025-08-23 03:08:44 +03:00
Tulir Asokan
35b805440f federation/pdu: add auth event selection 2025-08-22 19:37:53 +03:00
Tulir Asokan
206071ec03 federation/pdu: add redacted member event 2025-08-22 18:42:28 +03:00
Kishan Bagaria
1d484e01d0
event: implement disappearing timer types (#399)
Some checks are pending
Go / Lint (latest) (push) Waiting to run
Go / Build (old, libolm) (push) Waiting to run
Go / Build (latest, libolm) (push) Waiting to run
Go / Build (old, goolm) (push) Waiting to run
Go / Build (latest, goolm) (push) Waiting to run
Co-authored-by: Tulir Asokan <tulir@maunium.net>
2025-08-22 02:16:56 -07:00
Tulir Asokan
a547c0636c event,pushrules: replace assert.Nil with assert.NoError
Some checks failed
Go / Lint (latest) (push) Has been cancelled
Go / Build (old, libolm) (push) Has been cancelled
Go / Build (latest, libolm) (push) Has been cancelled
Go / Build (old, goolm) (push) Has been cancelled
Go / Build (latest, goolm) (push) Has been cancelled
2025-08-21 13:19:11 +03:00
Tulir Asokan
29780ffb18 federation/pdu: refactor redaction to allow reuse of RedactContent 2025-08-21 13:18:11 +03:00
Tulir Asokan
baf54f57b6 crypto/encryptmegolm: add fallback for copying m.relates_to
Some checks failed
Go / Lint (latest) (push) Has been cancelled
Go / Build (old, libolm) (push) Has been cancelled
Go / Build (latest, libolm) (push) Has been cancelled
Go / Build (old, goolm) (push) Has been cancelled
Go / Build (latest, goolm) (push) Has been cancelled
2025-08-19 19:44:53 +03:00
Tulir Asokan
05b711d181 federation/pdu: add more tests for signature checks
Some checks failed
Go / Lint (latest) (push) Has been cancelled
Go / Build (old, libolm) (push) Has been cancelled
Go / Build (latest, libolm) (push) Has been cancelled
Go / Build (old, goolm) (push) Has been cancelled
Go / Build (latest, goolm) (push) Has been cancelled
2025-08-18 00:53:51 +03:00
Tulir Asokan
d1004d42b0 client: add method to download media thumbnail 2025-08-18 00:24:57 +03:00
Tulir Asokan
ca4ca62249 federation/pdu: add docs for GetKeyFunc 2025-08-17 20:24:18 +03:00
Tulir Asokan
ec663b53d4 federation/pdu: reorganize code and add methods to v1 struct 2025-08-17 20:12:38 +03:00
Tulir Asokan
cc80be1500 federation/pdu: add method to convert to client event
Some checks are pending
Go / Lint (latest) (push) Waiting to run
Go / Build (old, libolm) (push) Waiting to run
Go / Build (latest, libolm) (push) Waiting to run
Go / Build (old, goolm) (push) Waiting to run
Go / Build (latest, goolm) (push) Waiting to run
2025-08-17 13:45:34 +03:00
Tulir Asokan
6eced49860 client,event: remove deprecated MSC2716 structs 2025-08-17 13:32:07 +03:00
Tulir Asokan
9b075f8bb9 ci: disable tests on goolm again 2025-08-17 13:15:53 +03:00
Tulir Asokan
0f177058c1 ci: move tags to correct place 2025-08-17 13:13:24 +03:00
Tulir Asokan
0dc957fa30 ci: fix more things 2025-08-17 13:11:26 +03:00
Tulir Asokan
31178e9f42 federation/pdu: fail on any signature check error 2025-08-17 13:02:47 +03:00
Tulir Asokan
86802be0f7 federation/pdu: gate signing key validity check by room version 2025-08-17 13:00:42 +03:00
Tulir Asokan
e85276fc0b ci: disable gotestfmt in goolm
It explodes with `panic: BUG: Empty package name encountered.`
2025-08-17 12:59:18 +03:00
Tulir Asokan
d2e7302dae ci: test goolm and jsonv2 2025-08-17 12:53:21 +03:00
Tulir Asokan
80c0b950dc federation/pdu: add utilities for PDU generation and validation 2025-08-17 12:52:58 +03:00
Tulir Asokan
2d4850a188 changelog: fix date
Some checks are pending
Go / Lint (latest) (push) Waiting to run
Go / Build (old, libolm) (push) Waiting to run
Go / Build (latest, libolm) (push) Waiting to run
Go / Build (old, goolm) (push) Waiting to run
Go / Build (latest, goolm) (push) Waiting to run
2025-08-16 13:21:06 +03:00
Tulir Asokan
0bbfafe02f Bump version to v0.25.0 2025-08-16 13:13:55 +03:00
Tulir Asokan
cd022c9010 client: don't set user-agent header on wasm
Some checks are pending
Go / Lint (latest) (push) Waiting to run
Go / Build (old, libolm) (push) Waiting to run
Go / Build (latest, libolm) (push) Waiting to run
Go / Build (old, goolm) (push) Waiting to run
Go / Build (latest, goolm) (push) Waiting to run
2025-08-15 16:45:18 +03:00
Tulir Asokan
ee869b97e6 dependencies: update
Some checks failed
Go / Lint (latest) (push) Has been cancelled
Go / Build (old, libolm) (push) Has been cancelled
Go / Build (latest, libolm) (push) Has been cancelled
Go / Build (old, goolm) (push) Has been cancelled
Go / Build (latest, goolm) (push) Has been cancelled
2025-08-13 20:33:04 +03:00
V02460
809333fcc5
verificationhelper: use static format strings (#390) 2025-08-13 20:32:21 +03:00
Tulir Asokan
7dcd45eba2 changelog: update
Some checks are pending
Go / Lint (latest) (push) Waiting to run
Go / Build (old, libolm) (push) Waiting to run
Go / Build (latest, libolm) (push) Waiting to run
Go / Build (old, goolm) (push) Waiting to run
Go / Build (latest, goolm) (push) Waiting to run
2025-08-12 23:50:50 +03:00
Tulir Asokan
5d84bddc62 crypto/attachments: hash correct data while decrypting
Some checks failed
Go / Lint (latest) (push) Has been cancelled
Go / Build (old, libolm) (push) Has been cancelled
Go / Build (latest, libolm) (push) Has been cancelled
Go / Build (old, goolm) (push) Has been cancelled
Go / Build (latest, goolm) (push) Has been cancelled
2025-08-11 10:58:24 +03:00
Tulir Asokan
23df81f1cc crypto/attachments: fix hash check when decrypting 2025-08-11 10:46:22 +03:00
Tulir Asokan
78aea00999 format/htmlparser: collapse spaces when parsing html
Some checks are pending
Go / Lint (latest) (push) Waiting to run
Go / Build (old, libolm) (push) Waiting to run
Go / Build (latest, libolm) (push) Waiting to run
Go / Build (old, goolm) (push) Waiting to run
Go / Build (latest, goolm) (push) Waiting to run
2025-08-10 23:23:15 +03:00
Tulir Asokan
6ea2337283 event: add policy server spammy flag to unsigned 2025-08-10 23:22:25 +03:00
Tulir Asokan
87d599c491 crypto: remove group session already shared error
Some checks are pending
Go / Lint (latest) (push) Waiting to run
Go / Build (old, libolm) (push) Waiting to run
Go / Build (latest, libolm) (push) Waiting to run
Go / Build (old, goolm) (push) Waiting to run
Go / Build (latest, goolm) (push) Waiting to run
2025-08-09 17:42:34 +03:00
Tulir Asokan
135cffc7c1 requests: add json un/marshaler for Direction rune 2025-08-09 13:10:31 +03:00
Tulir Asokan
3865abb3b8 dependencies: update go-util and use new UnsafeString helper 2025-08-09 13:10:18 +03:00
Tulir Asokan
90e3427ac5 bridgev2: check that avatar mxc is set before ignoring update
Some checks failed
Go / Lint (latest) (push) Has been cancelled
Go / Build (old, libolm) (push) Has been cancelled
Go / Build (latest, libolm) (push) Has been cancelled
Go / Build (old, goolm) (push) Has been cancelled
Go / Build (latest, goolm) (push) Has been cancelled
2025-08-07 12:08:25 +03:00
Tulir Asokan
7a791e908c federation: extract VerifyJSON into subpackage
Some checks failed
Go / Lint (latest) (push) Has been cancelled
Go / Build (old, libolm) (push) Has been cancelled
Go / Build (latest, libolm) (push) Has been cancelled
Go / Build (old, goolm) (push) Has been cancelled
Go / Build (latest, goolm) (push) Has been cancelled
2025-08-03 20:37:11 +03:00
Tulir Asokan
1215f6237e event: fix json tag in power levels
Some checks are pending
Go / Lint (latest) (push) Waiting to run
Go / Build (old, libolm) (push) Waiting to run
Go / Build (latest, libolm) (push) Waiting to run
Go / Build (old, goolm) (push) Waiting to run
Go / Build (latest, goolm) (push) Waiting to run
2025-08-03 15:16:42 +03:00
Tulir Asokan
e27e00b391 id: move room version from event package and add flags 2025-08-03 15:16:42 +03:00
Sumner Evans
654b6b1d45
crypto: replace t.Fatal and t.Error with require and assert
Some checks are pending
Go / Lint (latest) (push) Waiting to run
Go / Build (old, libolm) (push) Waiting to run
Go / Build (latest, libolm) (push) Waiting to run
Go / Build (old, goolm) (push) Waiting to run
Go / Build (latest, goolm) (push) Waiting to run
Signed-off-by: Sumner Evans <me@sumnerevans.com>
2025-08-02 12:22:24 -06:00
Tulir Asokan
09e4706fdb crypto/backup: allow encrypting session without private key
Some checks failed
Go / Lint (latest) (push) Has been cancelled
Go / Build (old, libolm) (push) Has been cancelled
Go / Build (latest, libolm) (push) Has been cancelled
Go / Build (old, goolm) (push) Has been cancelled
Go / Build (latest, goolm) (push) Has been cancelled
2025-08-01 14:13:55 +03:00
Tulir Asokan
aeeea09549 sqlstatestore: ensure empty room/user ids aren't stored in db 2025-08-01 12:19:51 +03:00
Tulir Asokan
0a804c58a1 bridgev2/matrix: don't ensure joined for state resync 2025-08-01 12:15:47 +03:00
timedout (aka nexy7574)
196164ed67
event: add join_authorised_via_users_server to MemberEventContent (#395)
Adds `JoinAuthorisedViaUsersServer` (`join_authorised_via_users_server`)
to `MemberEventContent`, introduced in room version 8
2025-08-01 09:47:53 +01:00
Tulir Asokan
66ec881a74 bridgev2/matrix: add hack for resyncing encryption state cache 2025-08-01 11:00:37 +03:00
Tulir Asokan
190c0de94f bridgev2/matrix: always clear mx_user_profile when deleting room 2025-08-01 10:51:00 +03:00
Tulir Asokan
10b26b507d client: fix updating state store in CreateRoom 2025-08-01 10:38:02 +03:00
Tulir Asokan
94f53c5853 bridgev2/cryptostore: add missing escape clause to not like
Some checks are pending
Go / Lint (latest) (push) Waiting to run
Go / Build (old, libolm) (push) Waiting to run
Go / Build (latest, libolm) (push) Waiting to run
Go / Build (old, goolm) (push) Waiting to run
Go / Build (latest, goolm) (push) Waiting to run
2025-07-31 14:00:00 +03:00
Tulir Asokan
66e0ed47c0 bridgev2/portal: include error in event handling results 2025-07-31 13:40:18 +03:00
Tulir Asokan
91b2bcdb9f bridgev2/matrix: don't send connecting bridge states to cloud 2025-07-31 13:01:08 +03:00
Tulir Asokan
bcf92ba0e8 appservice/intent: don't download avatar before setting on hungry
Some checks failed
Go / Lint (latest) (push) Has been cancelled
Go / Build (old, libolm) (push) Has been cancelled
Go / Build (latest, libolm) (push) Has been cancelled
Go / Build (old, goolm) (push) Has been cancelled
Go / Build (latest, goolm) (push) Has been cancelled
2025-07-29 17:42:04 +03:00
Tulir Asokan
3a28151780 client: log method/url when retrying requests 2025-07-29 17:41:51 +03:00
Tulir Asokan
7bd136196d format/htmlparser: don't add link suffix if plaintext is only missing protocol
Auto-linkification will add a protocol in the `href`, but usually won't touch
the text part. We want to undo the linkification here since it doesn't carry
any additional information.
2025-07-29 17:24:01 +03:00
Tulir Asokan
b4c7abd62b bridgev2,federation,mediaproxy: enable http access logging
Some checks are pending
Go / Lint (latest) (push) Waiting to run
Go / Build (old, libolm) (push) Waiting to run
Go / Build (latest, libolm) (push) Waiting to run
Go / Build (old, goolm) (push) Waiting to run
Go / Build (latest, goolm) (push) Waiting to run
2025-07-29 17:10:50 +03:00
Tulir Asokan
26e66f293e bridgev2/portal: return event ignored result for type unknown 2025-07-29 16:15:36 +03:00
Tulir Asokan
f1da44490c bridgev2/provisioning: move login step checks into handler 2025-07-29 16:15:16 +03:00
Tulir Asokan
2e7ff3fedd all: fix trailing slash in subrouters
Some checks are pending
Go / Lint (latest) (push) Waiting to run
Go / Build (old, libolm) (push) Waiting to run
Go / Build (latest, libolm) (push) Waiting to run
Go / Build (old, goolm) (push) Waiting to run
Go / Build (latest, goolm) (push) Waiting to run
2025-07-28 22:03:43 +03:00
Tulir Asokan
ae2c07fb86 appservice/websocket: close writer after sending 2025-07-28 17:34:28 +03:00
Tulir Asokan
74ab3b118e bridgev2/portal: add todo
Some checks are pending
Go / Lint (latest) (push) Waiting to run
Go / Build (old, libolm) (push) Waiting to run
Go / Build (latest, libolm) (push) Waiting to run
Go / Build (old, goolm) (push) Waiting to run
Go / Build (latest, goolm) (push) Waiting to run
2025-07-28 15:53:17 +03:00
Tulir Asokan
83b4b71a16 appservice/websocket: switch from gorilla to coder 2025-07-28 14:56:09 +03:00
Tulir Asokan
62c03d093a bridgev2/status: take context and http client in checkpoint SendHTTP 2025-07-28 14:56:09 +03:00
Tulir Asokan
d5223cdc8f all: replace gorilla/mux with standard library 2025-07-28 14:56:09 +03:00
Tulir Asokan
5b55330b85 bridgev2: run PostStart in background
Some checks failed
Go / Lint (latest) (push) Has been cancelled
Go / Build (old, libolm) (push) Has been cancelled
Go / Build (latest, libolm) (push) Has been cancelled
Go / Build (old, goolm) (push) Has been cancelled
Go / Build (latest, goolm) (push) Has been cancelled
2025-07-23 14:37:57 +03:00
Tulir Asokan
463d2ea6d0 bridgev2/portal: add bots to functional members in DMs
Some checks are pending
Go / Lint (latest) (push) Waiting to run
Go / Build (old, libolm) (push) Waiting to run
Go / Build (latest, libolm) (push) Waiting to run
Go / Build (old, goolm) (push) Waiting to run
Go / Build (latest, goolm) (push) Waiting to run
2025-07-22 23:35:58 +03:00
Tulir Asokan
69a3d27c1c bridgev2: add interface for getting arbitrary state event 2025-07-22 22:50:26 +03:00
Tulir Asokan
cb80e5c63f bridgev2/portal: fix adding rooms to personal space on create
Some checks are pending
Go / Lint (latest) (push) Waiting to run
Go / Build (old, libolm) (push) Waiting to run
Go / Build (latest, libolm) (push) Waiting to run
Go / Build (old, goolm) (push) Waiting to run
Go / Build (latest, goolm) (push) Waiting to run
2025-07-22 20:31:31 +03:00
Tulir Asokan
fcd7d9a525 bridgev2/commands: allow canceling qr login 2025-07-22 19:20:32 +03:00
Tulir Asokan
3fe5a7badc event: replace soft failed field in unsigned 2025-07-22 17:19:47 +03:00
Tulir Asokan
3ecdb886bf bridgev2/database: add method to mark backfill task as not done 2025-07-22 16:18:25 +03:00
Tulir Asokan
ea72271bad bridgev2/queue: run command handlers in background
Some checks failed
Go / Lint (latest) (push) Has been cancelled
Go / Build (old, libolm) (push) Has been cancelled
Go / Build (latest, libolm) (push) Has been cancelled
Go / Build (old, goolm) (push) Has been cancelled
Go / Build (latest, goolm) (push) Has been cancelled
2025-07-21 11:15:23 +03:00
Tulir Asokan
65a64c8044 client: allow using custom http client for .well-known resolution
Some checks are pending
Go / Lint (latest) (push) Waiting to run
Go / Build (old, libolm) (push) Waiting to run
Go / Build (latest, libolm) (push) Waiting to run
Go / Build (old, goolm) (push) Waiting to run
Go / Build (latest, goolm) (push) Waiting to run
2025-07-20 14:23:20 +03:00
Tulir Asokan
4866da5200 client: add custom room create ts field
Some checks failed
Go / Lint (latest) (push) Has been cancelled
Go / Build (old, libolm) (push) Has been cancelled
Go / Build (latest, libolm) (push) Has been cancelled
Go / Build (old, goolm) (push) Has been cancelled
Go / Build (latest, goolm) (push) Has been cancelled
2025-07-18 23:59:28 +03:00
Tulir Asokan
96b07ad724 event: use full event type for stripped state for MSC4311 2025-07-18 23:59:28 +03:00
Tulir Asokan
0b62253d3b all: add support for creator power 2025-07-18 23:59:28 +03:00
Tulir Asokan
237ce1c64c client: remove redundant state store update in room create 2025-07-18 22:32:25 +03:00
Tulir Asokan
9a170d2669 bridgev2,appservice: add via to EnsureJoined and use it for tombstone handling
Some checks are pending
Go / Lint (latest) (push) Waiting to run
Go / Build (old, libolm) (push) Waiting to run
Go / Build (latest, libolm) (push) Waiting to run
Go / Build (old, goolm) (push) Waiting to run
Go / Build (latest, goolm) (push) Waiting to run
2025-07-18 17:55:27 +03:00
Tulir Asokan
c7263bab40 bridgev2/portal: add support for following tombstones 2025-07-18 17:37:45 +03:00
Tulir Asokan
90a7dc3c75 bridgev2/portal: ignore delete for me in multi-user portals 2025-07-18 16:05:04 +03:00
Tulir Asokan
0508f02a9e bridgev2/disappear: make next check field atomic
Some checks failed
Go / Lint (latest) (push) Has been cancelled
Go / Build (old, libolm) (push) Has been cancelled
Go / Build (latest, libolm) (push) Has been cancelled
Go / Build (old, goolm) (push) Has been cancelled
Go / Build (latest, goolm) (push) Has been cancelled
2025-07-17 17:36:16 +03:00
Tulir Asokan
5a9e20e451 bridgev2/disappear: always delete synchronously if limit is reached 2025-07-17 17:27:48 +03:00
Tulir Asokan
8efdbc029b bridgev2/disappear: reduce disappear loop interval when there are lots of messages 2025-07-17 17:20:28 +03:00
Tulir Asokan
7ffdbe8bfc bridgev2/disappear: add limit to getting messages from the db 2025-07-17 16:54:55 +03:00
Tulir Asokan
81a807a6c9 Bump version to v0.24.2
Some checks are pending
Go / Lint (latest) (push) Waiting to run
Go / Build (old, libolm) (push) Waiting to run
Go / Build (latest, libolm) (push) Waiting to run
Go / Build (old, goolm) (push) Waiting to run
Go / Build (latest, goolm) (push) Waiting to run
2025-07-16 11:32:09 +03:00
Tulir Asokan
fcc72dc54b dependencies: update 2025-07-16 11:06:39 +03:00
Tulir Asokan
095c63a97e bridgev2/portal: add missing return
Some checks are pending
Go / Lint (latest) (push) Waiting to run
Go / Build (old, libolm) (push) Waiting to run
Go / Build (latest, libolm) (push) Waiting to run
Go / Build (old, goolm) (push) Waiting to run
Go / Build (latest, goolm) (push) Waiting to run
2025-07-15 14:57:52 +03:00
Tulir Asokan
1ee29a47b6
bridgev2: add option to auto-reconnect after unknown error (#394) 2025-07-15 14:37:07 +03:00
Tulir Asokan
1d37430204 bridgev2/portal: block in queueEvent if buffer is full 2025-07-15 14:31:44 +03:00
Tulir Asokan
687717bd73 bridgev2: hardcode room v11 for new rooms
Upcoming breaking changes in room v12 prevent safely using the default
room version and security embargoes prevent fixing them ahead of time.
2025-07-15 14:19:38 +03:00
Tulir Asokan
b74368ac23 commands: add safety to type check 2025-07-15 13:19:44 +03:00
Tulir Asokan
5e29bac3dd bridgev2/portal: adjust handleMatrixMessage return value for pending messages
Some checks failed
Go / Lint (latest) (push) Has been cancelled
Go / Build (old, libolm) (push) Has been cancelled
Go / Build (latest, libolm) (push) Has been cancelled
Go / Build (old, goolm) (push) Has been cancelled
Go / Build (latest, goolm) (push) Has been cancelled
2025-07-10 16:19:37 +03:00
Tulir Asokan
4f8ff2a350 bridgev2/portal: merge MSS errors with handling result
Some checks are pending
Go / Lint (latest) (push) Waiting to run
Go / Build (old, libolm) (push) Waiting to run
Go / Build (latest, libolm) (push) Waiting to run
Go / Build (old, goolm) (push) Waiting to run
Go / Build (latest, goolm) (push) Waiting to run
2025-07-10 15:04:57 +03:00
Tulir Asokan
40bb9637cd bridgev2/queue: add event handling result for matrix events 2025-07-10 14:48:54 +03:00
Tulir Asokan
22587e9159 bridgev2/portal: track event handler panics 2025-07-10 13:45:23 +03:00
Tulir Asokan
c80808439d bridgev2: add logger to background context 2025-07-10 13:45:11 +03:00
Tulir Asokan
0777c10028 bridgev2/networkinterface: add extra fields to reply metadata to allow unknown cross-room replies
Some checks are pending
Go / Lint (latest) (push) Waiting to run
Go / Build (old, libolm) (push) Waiting to run
Go / Build (latest, libolm) (push) Waiting to run
Go / Build (old, goolm) (push) Waiting to run
Go / Build (latest, goolm) (push) Waiting to run
2025-07-09 16:35:14 +03:00
Tulir Asokan
44515616d4 bridgev2/portal: don't assume unknown reply events are cross-room 2025-07-09 16:28:02 +03:00
Tulir Asokan
b62535edaa bridgev2/portal: fix disappearing message notice for implicitly turning off timer
Some checks failed
Go / Lint (latest) (push) Has been cancelled
Go / Build (old, libolm) (push) Has been cancelled
Go / Build (latest, libolm) (push) Has been cancelled
Go / Build (old, goolm) (push) Has been cancelled
Go / Build (latest, goolm) (push) Has been cancelled
2025-07-03 21:22:19 +03:00
Tulir Asokan
71b994b3fd appservice: remove unnecessary parameter in ping
Some checks failed
Go / Lint (latest) (push) Has been cancelled
Go / Build (old, libolm) (push) Has been cancelled
Go / Build (latest, libolm) (push) Has been cancelled
Go / Build (old, goolm) (push) Has been cancelled
Go / Build (latest, goolm) (push) Has been cancelled
2025-07-01 23:29:43 +03:00
Tulir Asokan
6f370cc3bb bridgev2,appservice: move appservice ping loop to appservice package 2025-07-01 23:28:59 +03:00
Tulir Asokan
4f6d4d7c63 bridgev2/portal: add support for per-message profiles in relay mode
Some checks are pending
Go / Lint (latest) (push) Waiting to run
Go / Build (old, libolm) (push) Waiting to run
Go / Build (latest, libolm) (push) Waiting to run
Go / Build (old, goolm) (push) Waiting to run
Go / Build (latest, goolm) (push) Waiting to run
2025-07-01 01:34:42 +03:00
Tulir Asokan
94950585c9 event: fix removing per-message profile fallback in edits 2025-07-01 01:15:24 +03:00
Tulir Asokan
7a7d7f70ef federation: fix base64 in generated signatures
Some checks are pending
Go / Lint (latest) (push) Waiting to run
Go / Build (old, libolm) (push) Waiting to run
Go / Build (latest, libolm) (push) Waiting to run
Go / Build (old, goolm) (push) Waiting to run
Go / Build (latest, goolm) (push) Waiting to run
2025-06-29 19:11:27 +03:00
Matthias Kesler
3a135b6b15
id: fix ServerNameRegex not matching port correctly (#392)
Some checks failed
Go / Lint (latest) (push) Has been cancelled
Go / Build (old, libolm) (push) Has been cancelled
Go / Build (latest, libolm) (push) Has been cancelled
Go / Build (old, goolm) (push) Has been cancelled
Go / Build (latest, goolm) (push) Has been cancelled
fixes #391
2025-06-25 12:35:18 +02:00
Tulir Asokan
324be4ecb9 mediaproxy: fix closing data response readers
Some checks failed
Go / Lint (latest) (push) Has been cancelled
Go / Build (old, libolm) (push) Has been cancelled
Go / Build (latest, libolm) (push) Has been cancelled
Go / Build (old, goolm) (push) Has been cancelled
Go / Build (latest, goolm) (push) Has been cancelled
2025-06-19 17:55:09 +02:00
Tulir Asokan
f3722ca31f mediaproxy: validate media IDs 2025-06-19 17:17:27 +02:00
Tulir Asokan
26da46dbbf
bridgev2/portal: return result of handling remote events (#389)
Some checks failed
Go / Lint (latest) (push) Has been cancelled
Go / Build (old, libolm) (push) Has been cancelled
Go / Build (latest, libolm) (push) Has been cancelled
Go / Build (old, goolm) (push) Has been cancelled
Go / Build (latest, goolm) (push) Has been cancelled
2025-06-17 19:38:29 +03:00
Tulir Asokan
1878700a9d Bump version to v0.24.1
Some checks are pending
Go / Lint (latest) (push) Waiting to run
Go / Build (old, libolm) (push) Waiting to run
Go / Build (latest, libolm) (push) Waiting to run
Go / Build (old, goolm) (push) Waiting to run
Go / Build (latest, goolm) (push) Waiting to run
2025-06-16 16:42:04 +03:00
Tulir Asokan
1143cfaa85 event: implement fallbacks for per-message profiles
Some checks failed
Go / Lint (latest) (push) Has been cancelled
Go / Build (old, libolm) (push) Has been cancelled
Go / Build (latest, libolm) (push) Has been cancelled
Go / Build (old, goolm) (push) Has been cancelled
Go / Build (latest, goolm) (push) Has been cancelled
2025-06-14 18:19:42 +03:00
Tulir Asokan
c836dbafdf bridgev2/matrixinvite: clean up old portal room if user is not a member 2025-06-14 12:31:03 +03:00
Tulir Asokan
79969306e7 bridgev2/matrix: check stream upload size after writing file 2025-06-14 12:23:36 +03:00
Tulir Asokan
c888801751 bridgev2/matrixinvite: allow redirecting DM creations to another user 2025-06-14 12:21:05 +03:00
Tulir Asokan
c540f30ef9 dependencies: update
Some checks failed
Go / Lint (latest) (push) Has been cancelled
Go / Build (old, libolm) (push) Has been cancelled
Go / Build (latest, libolm) (push) Has been cancelled
Go / Build (old, goolm) (push) Has been cancelled
Go / Build (latest, goolm) (push) Has been cancelled
2025-06-12 13:32:00 +03:00
Tulir Asokan
b8921397b8 event,requests: add MSC4293 redact events field to member events
Some checks are pending
Go / Lint (latest) (push) Waiting to run
Go / Build (old, libolm) (push) Waiting to run
Go / Build (latest, libolm) (push) Waiting to run
Go / Build (old, goolm) (push) Waiting to run
Go / Build (latest, goolm) (push) Waiting to run
2025-06-11 19:10:19 +03:00
Tulir Asokan
15d0b63eb6 bridgev2/provisioning: check for nil steps in submit and wait calls
Some checks are pending
Go / Lint (latest) (push) Waiting to run
Go / Build (old, libolm) (push) Waiting to run
Go / Build (latest, libolm) (push) Waiting to run
Go / Build (old, goolm) (push) Waiting to run
Go / Build (latest, goolm) (push) Waiting to run
2025-06-11 15:34:34 +03:00
Tulir Asokan
1038f6a73c bridgev2: fix more background contexts 2025-06-10 19:33:02 +03:00
Tulir Asokan
9c67d238d7 bridgev2/portal: check only for me flag in delete chat events 2025-06-10 18:53:56 +03:00
Tulir Asokan
12502e213a bridgev2/userlogin: never set client to nil
Some checks are pending
Go / Lint (latest) (push) Waiting to run
Go / Build (old, libolm) (push) Waiting to run
Go / Build (latest, libolm) (push) Waiting to run
Go / Build (old, goolm) (push) Waiting to run
Go / Build (latest, goolm) (push) Waiting to run
2025-06-10 17:46:23 +03:00
Tulir Asokan
99cfa0b53a bridgev2/matrixinvite: save portal after setting mxid
Some checks are pending
Go / Lint (latest) (push) Waiting to run
Go / Build (old, libolm) (push) Waiting to run
Go / Build (latest, libolm) (push) Waiting to run
Go / Build (old, goolm) (push) Waiting to run
Go / Build (latest, goolm) (push) Waiting to run
2025-06-10 15:07:25 +03:00
Tulir Asokan
72bacbb666 appservice/intent: ensure registered when sending own member state event
Some checks are pending
Go / Lint (latest) (push) Waiting to run
Go / Build (old, libolm) (push) Waiting to run
Go / Build (latest, libolm) (push) Waiting to run
Go / Build (old, goolm) (push) Waiting to run
Go / Build (latest, goolm) (push) Waiting to run
2025-06-09 19:30:37 +03:00
Tulir Asokan
a154718b5d
bridgev2/portal: allow specifying extra fields for portal members (#386)
Some checks are pending
Go / Lint (latest) (push) Waiting to run
Go / Build (old, libolm) (push) Waiting to run
Go / Build (latest, libolm) (push) Waiting to run
Go / Build (old, goolm) (push) Waiting to run
Go / Build (latest, goolm) (push) Waiting to run
2025-06-09 14:54:46 +03:00
Tulir Asokan
05f371a480 event: add membership field to unsigned 2025-06-09 14:00:13 +03:00
Tulir Asokan
8fb41765e2 event: add custom soft fail fields 2025-06-09 13:52:24 +03:00
Tulir Asokan
07567f6f96 bridgev2/portal: include room id in cross-room replies
Some checks failed
Go / Lint (latest) (push) Has been cancelled
Go / Build (old, libolm) (push) Has been cancelled
Go / Build (latest, libolm) (push) Has been cancelled
Go / Build (old, goolm) (push) Has been cancelled
Go / Build (latest, goolm) (push) Has been cancelled
2025-06-08 00:12:53 +03:00
Tulir Asokan
40fd8dfcbd event/relations: use unstable prefix for reply room ID field 2025-06-08 00:05:59 +03:00
Tulir Asokan
d296f7b660 bridgev2/provisioning: ensure that Start returns a non-nil first step
Some checks failed
Go / Lint (latest) (push) Has been cancelled
Go / Build (old, libolm) (push) Has been cancelled
Go / Build (latest, libolm) (push) Has been cancelled
Go / Build (old, goolm) (push) Has been cancelled
Go / Build (latest, goolm) (push) Has been cancelled
2025-06-06 14:08:19 +03:00
Brad Murray
d04d524209
crypto/verificationhelper: add method to verification done callback (#385)
Some checks are pending
Go / Lint (latest) (push) Waiting to run
Go / Build (old, libolm) (push) Waiting to run
Go / Build (latest, libolm) (push) Waiting to run
Go / Build (old, goolm) (push) Waiting to run
Go / Build (latest, goolm) (push) Waiting to run
2025-06-05 13:38:19 -04:00
Toni Spets
d228995d71
bridgev2: Configurable disconnect timeout (#383)
Some checks are pending
Go / Lint (latest) (push) Waiting to run
Go / Build (old, libolm) (push) Waiting to run
Go / Build (latest, libolm) (push) Waiting to run
Go / Build (old, goolm) (push) Waiting to run
Go / Build (latest, goolm) (push) Waiting to run
Let the caller decide if they want to have a timeout or not.  For
standalone bridges using the Bridge struct the behavior is kept the same
by waiting for five seconds when UserLogin DisconnectWithTimeout() is
called.
2025-06-05 07:25:48 +03:00
Brad Murray
1e10d9460a
bridgev2/status: add RESTART UserAction (#384)
Some checks are pending
Go / Lint (latest) (push) Waiting to run
Go / Build (old, libolm) (push) Waiting to run
Go / Build (latest, libolm) (push) Waiting to run
Go / Build (old, goolm) (push) Waiting to run
Go / Build (latest, goolm) (push) Waiting to run
2025-06-04 11:37:00 -04:00
Tulir Asokan
baf4cc3ee4 bridgev2/portal: log start time when event handling takes long 2025-06-04 16:13:16 +03:00
Tulir Asokan
d804b5d961 client: add support for stable version of room summary endpoint 2025-06-04 14:48:22 +03:00
Tulir Asokan
522a373c68 id: validate server names in UserID.ParseAndValidate
Some checks are pending
Go / Lint (latest) (push) Waiting to run
Go / Build (old, libolm) (push) Waiting to run
Go / Build (latest, libolm) (push) Waiting to run
Go / Build (old, goolm) (push) Waiting to run
Go / Build (latest, goolm) (push) Waiting to run
2025-06-03 00:32:07 +03:00
Tulir Asokan
8fb04d1806 id/matrixuri: fix parsing url-encoded matrix URIs
Some checks are pending
Go / Lint (latest) (push) Waiting to run
Go / Build (old, libolm) (push) Waiting to run
Go / Build (latest, libolm) (push) Waiting to run
Go / Build (old, goolm) (push) Waiting to run
Go / Build (latest, goolm) (push) Waiting to run
2025-06-02 20:06:52 +03:00
Tulir Asokan
788621f7e0 bridgev2/crypto: fix ghost ID format in db queries
Some checks failed
Go / Lint (latest) (push) Has been cancelled
Go / Build (old, libolm) (push) Has been cancelled
Go / Build (latest, libolm) (push) Has been cancelled
Go / Build (old, goolm) (push) Has been cancelled
Go / Build (latest, goolm) (push) Has been cancelled
2025-05-31 18:03:47 +03:00
Tulir Asokan
1b1b83298c client,bridgev2: use time.After instead of sleep 2025-05-31 18:03:47 +03:00
Nick Mills-Barrett
e859fd8333
bridgev2/bridgeconfig: add missing copy for session transfer config
Some checks are pending
Go / Lint (latest) (push) Waiting to run
Go / Build (old, libolm) (push) Waiting to run
Go / Build (latest, libolm) (push) Waiting to run
Go / Build (old, goolm) (push) Waiting to run
Go / Build (latest, goolm) (push) Waiting to run
2025-05-30 15:09:43 +01:00
Tulir Asokan
3473f91864 bridgev2/portal: add some default log context fields for remote events
Some checks are pending
Go / Lint (latest) (push) Waiting to run
Go / Build (old, libolm) (push) Waiting to run
Go / Build (latest, libolm) (push) Waiting to run
Go / Build (old, goolm) (push) Waiting to run
Go / Build (latest, goolm) (push) Waiting to run
2025-05-29 13:58:05 +03:00
Tulir Asokan
842f21b24f bridgev2/provisioning: add log when explicitly specified login ID is not found
Some checks are pending
Go / Lint (latest) (push) Waiting to run
Go / Build (old, libolm) (push) Waiting to run
Go / Build (latest, libolm) (push) Waiting to run
Go / Build (old, goolm) (push) Waiting to run
Go / Build (latest, goolm) (push) Waiting to run
2025-05-28 22:25:17 +03:00
Tulir Asokan
f73480446c mediaproxy: remove deprecated custom ResponseError struct 2025-05-28 21:39:34 +03:00
Tulir Asokan
53d027c06f appservice: replace custom response utilities with RespError and exhttp 2025-05-28 21:34:46 +03:00
Tulir Asokan
64f55ac3a7 bridgev2/provisioning: use exhttp utilities for writing responses 2025-05-28 21:24:15 +03:00
Tulir Asokan
d89130ba76 bridgev2/provisioning: fix returning wait errors
Closes #382
2025-05-28 21:12:50 +03:00
Tulir Asokan
f5746ee0f6 event: add omitempty for mod policy entity
Some checks failed
Go / Lint (latest) (push) Has been cancelled
Go / Build (old, libolm) (push) Has been cancelled
Go / Build (latest, libolm) (push) Has been cancelled
Go / Build (old, goolm) (push) Has been cancelled
Go / Build (latest, goolm) (push) Has been cancelled
Only one of hash and entity should be set
2025-05-27 18:04:56 +03:00
Tulir Asokan
a3092e5195 bridgev2/portal: don't do initial backfill in background 2025-05-27 16:45:39 +03:00
Tulir Asokan
50cc3d4d47 bridgev2/queue: fix context used for queueing remote events 2025-05-27 16:37:51 +03:00
Tulir Asokan
0589b8757b synapseadmin: fix response structs again 2025-05-27 15:57:40 +03:00
Tulir Asokan
5c8ea2c269 synapseadmin: add wrapper for room delete status 2025-05-27 15:54:46 +03:00
Tulir Asokan
8a745c0d03 bridgev2/portal: allow always using deterministic ids for replies 2025-05-27 11:37:57 +03:00
Tulir Asokan
cdb99239d3
bridgev2: add interfaces for reading up to stream order (#379) 2025-05-27 11:35:40 +03:00
Tulir Asokan
140b20cab9 id: add utilities for validating server names
Some checks are pending
Go / Lint (latest) (push) Waiting to run
Go / Build (old, libolm) (push) Waiting to run
Go / Build (latest, libolm) (push) Waiting to run
Go / Build (old, goolm) (push) Waiting to run
Go / Build (latest, goolm) (push) Waiting to run
2025-05-27 12:04:13 +05:30
Tulir Asokan
34afb98ef0 event: fix parsing some url preview responses 2025-05-27 12:04:13 +05:30
Tulir Asokan
e7322f04b8 bridgev2: fix handling some cases of context cancellation 2025-05-27 09:14:25 +03:00
nexy7574
c7fbfd150f
federation/serverauth: fix URI passed to signableRequest (#381)
Some checks are pending
Go / Lint (latest) (push) Waiting to run
Go / Build (old, libolm) (push) Waiting to run
Go / Build (latest, libolm) (push) Waiting to run
Go / Build (old, goolm) (push) Waiting to run
Go / Build (latest, goolm) (push) Waiting to run
2025-05-26 20:37:28 +01:00
Tulir Asokan
a3d5da315f federation: use errors in signature verification 2025-05-26 21:58:35 +03:00
Tulir Asokan
92311e5c98 federation/client: fix QueryKeys return format 2025-05-26 21:14:47 +03:00
Tulir Asokan
6ed660557b federation/signingkey: store raw response for validation 2025-05-26 21:14:37 +03:00
Tulir Asokan
c5ef0f9d90 bridgev2/userlogin: ensure Client is filled in NewLogin 2025-05-26 20:24:33 +03:00
Tulir Asokan
306b48bd68 bridgev2/ghost: ensure GetGhostByID can't return nil 2025-05-26 19:34:51 +03:00
Tulir Asokan
89fad2f462 commands: add reaction button system
Some checks failed
Go / Lint (latest) (push) Has been cancelled
Go / Build (old, libolm) (push) Has been cancelled
Go / Build (latest, libolm) (push) Has been cancelled
Go / Build (old, goolm) (push) Has been cancelled
Go / Build (latest, goolm) (push) Has been cancelled
2025-05-24 16:33:36 +03:00
Tulir Asokan
da9e72e616 commands: add separate field for logger in event 2025-05-24 16:29:37 +03:00
Tulir Asokan
ec15b79493 commands: add event id to logger 2025-05-24 16:29:37 +03:00
Tulir Asokan
e9dfee45c0 event: add missing letter to docstring 2025-05-24 16:29:37 +03:00
Tulir Asokan
68565a1f18 client: add wrapper for /relations endpoints 2025-05-24 16:29:37 +03:00
Tulir Asokan
50f0b5fa7d synapseadmin: add support for synchronous room delete 2025-05-24 14:43:28 +03:00
Tulir Asokan
49d2f39183 format: add markdown link utilities 2025-05-24 14:35:00 +03:00
Tulir Asokan
ad8145c43b synapseadmin: don't embed mautrix.Client in admin client struct 2025-05-24 14:23:48 +03:00
Nick Mills-Barrett
203e402ebf
bridgev2/provisioning: correct field name
Some checks failed
Go / Lint (latest) (push) Has been cancelled
Go / Build (old, libolm) (push) Has been cancelled
Go / Build (latest, libolm) (push) Has been cancelled
Go / Build (old, goolm) (push) Has been cancelled
Go / Build (latest, goolm) (push) Has been cancelled
2025-05-22 15:22:13 +01:00
Nick Mills-Barrett
a3efaa3632
bridgev2/provisioning: disconnect login before exporting credentials 2025-05-22 15:20:36 +01:00
Nick Mills-Barrett
487fc699fe
bridgev2/provisioning: add session transfer support
Some checks are pending
Go / Lint (latest) (push) Waiting to run
Go / Build (old, libolm) (push) Waiting to run
Go / Build (latest, libolm) (push) Waiting to run
Go / Build (old, goolm) (push) Waiting to run
Go / Build (latest, goolm) (push) Waiting to run
For connector logins that support it this will expose an API to transfer
credentials between bridge instances.

Currently does not do any extra validation beyond the usual provisioning
API request validation (so shared secret or matrix token). One future
improvement would be to require clients to sign incoming requests, and
to then validate a) the signature and b) the device is verified.
2025-05-22 11:28:58 +01:00
Nick Mills-Barrett
a205a77db4
bridgev2: add CredentialExportingNetworkAPI interface 2025-05-22 11:28:57 +01:00
Tulir Asokan
0a8e823016 Bump version to v0.24.0
Some checks failed
Go / Lint (latest) (push) Has been cancelled
Go / Build (old, libolm) (push) Has been cancelled
Go / Build (latest, libolm) (push) Has been cancelled
Go / Build (old, goolm) (push) Has been cancelled
Go / Build (latest, goolm) (push) Has been cancelled
2025-05-16 08:13:16 +03:00
Tulir Asokan
978e0983ea dependencies: update
Some checks are pending
Go / Lint (latest) (push) Waiting to run
Go / Build (old, libolm) (push) Waiting to run
Go / Build (latest, libolm) (push) Waiting to run
Go / Build (old, goolm) (push) Waiting to run
Go / Build (latest, goolm) (push) Waiting to run
2025-05-15 14:15:34 +03:00
Tulir Asokan
f23fc99ef4 crypto/cross_signing: allow json marshaling cross-signing key seeds
Some checks are pending
Go / Lint (latest) (push) Waiting to run
Go / Build (old, libolm) (push) Waiting to run
Go / Build (latest, libolm) (push) Waiting to run
Go / Build (old, goolm) (push) Waiting to run
Go / Build (latest, goolm) (push) Waiting to run
2025-05-10 11:35:06 +03:00
Tulir Asokan
a0191c8f58 bridgev2: expose background context
Some checks failed
Go / Lint (latest) (push) Has been cancelled
Go / Build (old, libolm) (push) Has been cancelled
Go / Build (latest, libolm) (push) Has been cancelled
Go / Build (old, goolm) (push) Has been cancelled
Go / Build (latest, goolm) (push) Has been cancelled
2025-05-09 15:16:14 +03:00
Tulir Asokan
23d91b64cb bridgev2: fall back to remote ID for state update notices 2025-05-09 14:25:43 +03:00
Tulir Asokan
376fa1f368 bridgev2: fix initializing background context
Some checks are pending
Go / Lint (latest) (push) Waiting to run
Go / Build (old, libolm) (push) Waiting to run
Go / Build (latest, libolm) (push) Waiting to run
Go / Build (old, goolm) (push) Waiting to run
Go / Build (latest, goolm) (push) Waiting to run
2025-05-08 15:25:20 +03:00
Nick Mills-Barrett
27769dfc98
bridgev2: add shared event handling context
Some checks are pending
Go / Lint (latest) (push) Waiting to run
Go / Build (old, libolm) (push) Waiting to run
Go / Build (latest, libolm) (push) Waiting to run
Go / Build (old, goolm) (push) Waiting to run
Go / Build (latest, goolm) (push) Waiting to run
This context is then passed into the network connectors handlers and
message conversion functions which may require making network requests,
which before this would not be canceled on bridge stop.
2025-05-07 17:30:50 +01:00
Nick Mills-Barrett
4ffe1d23e9
client: don't attempt to make requests if the homeserver URL isn't set (#376)
Some checks are pending
Go / Lint (latest) (push) Waiting to run
Go / Build (old, libolm) (push) Waiting to run
Go / Build (latest, libolm) (push) Waiting to run
Go / Build (old, goolm) (push) Waiting to run
Go / Build (latest, goolm) (push) Waiting to run
Quick guard for where the client is created without using the
`NewClient` method.
2025-05-07 14:19:01 +01:00
Tulir Asokan
c93d30a83c bridgev2: add option to deduplicate Matrix messages by event or transaction ID 2025-05-07 14:47:05 +03:00
Tulir Asokan
72f6229f40 crypto: fix key export test
Some checks are pending
Go / Lint (latest) (push) Waiting to run
Go / Build (old, libolm) (push) Waiting to run
Go / Build (latest, libolm) (push) Waiting to run
Go / Build (old, goolm) (push) Waiting to run
Go / Build (latest, goolm) (push) Waiting to run
2025-05-06 23:18:23 +03:00
Tulir Asokan
0ffe3524f6 crypto/sql_store: ensure forwarding chains is always set instead of having fallback in getter 2025-05-06 22:55:23 +03:00
Tulir Asokan
bef23edaea crypto/keysharing: ensure forwarding chains is always set 2025-05-06 22:50:46 +03:00
Tulir Asokan
a7faac33c8 bridgev2/portal: add fallback if last receipt target is fake 2025-05-06 20:55:26 +03:00
Tulir Asokan
37d486dfcd bridgev2/portal: ignore fake mxids when bridging read receipts 2025-05-06 20:53:00 +03:00
Tulir Asokan
ba43e615f8 bridgev2/login: add wait_for_url_pattern field to cookie logins 2025-05-06 18:49:54 +03:00
Tulir Asokan
6eb4c7b17f crypto/keybackup: allow importing room keys without saving
Some checks failed
Go / Lint (latest) (push) Has been cancelled
Go / Build (old, libolm) (push) Has been cancelled
Go / Build (latest, libolm) (push) Has been cancelled
Go / Build (old, goolm) (push) Has been cancelled
Go / Build (latest, goolm) (push) Has been cancelled
2025-05-04 14:09:06 +03:00
Tulir Asokan
5cd8ba8887 federation/serverauth: fix go 1.23 compatibility
Some checks are pending
Go / Lint (latest) (push) Waiting to run
Go / Build (old, libolm) (push) Waiting to run
Go / Build (latest, libolm) (push) Waiting to run
Go / Build (old, goolm) (push) Waiting to run
Go / Build (latest, goolm) (push) Waiting to run
2025-05-04 01:14:44 +03:00
Tulir Asokan
b45dcd42fc federation/serverauth: fix get requests 2025-05-04 01:09:12 +03:00
Tulir Asokan
5c2bc3b1cf mediaproxy: add option to enforce federation auth 2025-05-04 01:04:40 +03:00
Tulir Asokan
63f35754c6 federation/serverauth: store verified origin in request context 2025-05-04 01:04:12 +03:00
Tulir Asokan
0a33bde865 federation/cache: expose noop cache as variable instead of type 2025-05-04 01:00:08 +03:00
Tulir Asokan
dec68fb4d7 federation/serverauth: don't unnecessarily export errors 2025-05-04 00:49:34 +03:00
Tulir Asokan
d145f00863 federation/serverauth: cache key querying errors 2025-05-04 00:39:43 +03:00
Tulir Asokan
9a02b6428d federation/serverauth: implement server side of request authentication 2025-05-04 00:08:09 +03:00
Tulir Asokan
2d1620ded3 federation/keyserver: add support for returning other servers keys 2025-05-04 00:07:55 +03:00
Tulir Asokan
9c3e1b5904 federation/signingkey: add support for roundtripping ServerKeyResponses 2025-05-04 00:07:55 +03:00
Tulir Asokan
b1f0b1732f federation/cache: add noop cache 2025-05-04 00:07:55 +03:00
Tulir Asokan
44de13a7de federation/keyserver: use shared utilities for writing responses 2025-05-04 00:07:52 +03:00
Tulir Asokan
66e7d834cc federation/resolution: parse cache-control headers for .well-known 2025-05-04 00:07:42 +03:00
Tulir Asokan
36781e7de4 federation: move server name cache to separate type 2025-05-04 00:07:02 +03:00
Tulir Asokan
441349efac synapseadmin: add SuspendAccount method
Some checks are pending
Go / Lint (latest) (push) Waiting to run
Go / Build (old, libolm) (push) Waiting to run
Go / Build (latest, libolm) (push) Waiting to run
Go / Build (old, goolm) (push) Waiting to run
Go / Build (latest, goolm) (push) Waiting to run
2025-05-03 03:00:31 +03:00
Tulir Asokan
2b973cac00 commands: include handler chain in command events
Some checks failed
Go / Lint (latest) (push) Has been cancelled
Go / Build (old, libolm) (push) Has been cancelled
Go / Build (latest, libolm) (push) Has been cancelled
Go / Build (old, goolm) (push) Has been cancelled
Go / Build (latest, goolm) (push) Has been cancelled
2025-05-02 02:05:11 +03:00
Tulir Asokan
e491e87309 commands: panic on duplicate registration 2025-05-02 01:56:46 +03:00
Tulir Asokan
5094eea718 bridgev2/networkinterface: allow clients to generate transaction IDs
Some checks are pending
Go / Lint (latest) (push) Waiting to run
Go / Build (old, libolm) (push) Waiting to run
Go / Build (latest, libolm) (push) Waiting to run
Go / Build (old, goolm) (push) Waiting to run
Go / Build (latest, goolm) (push) Waiting to run
2025-05-01 16:28:21 +03:00
Tulir Asokan
5c9529606e crypto/keybackup: return wrapped errors in ImportRoomKeyFromBackup 2025-05-01 15:23:31 +03:00
Tulir Asokan
69a17c6a59 bridgev2/networkinterface: remove timeout from ViewingChat 2025-05-01 15:22:47 +03:00
Tulir Asokan
58e4d0f2cc bridgev2: stop disappearing message loop on shutdown
Some checks are pending
Go / Lint (latest) (push) Waiting to run
Go / Build (old, libolm) (push) Waiting to run
Go / Build (latest, libolm) (push) Waiting to run
Go / Build (old, goolm) (push) Waiting to run
Go / Build (latest, goolm) (push) Waiting to run
2025-04-30 15:33:33 +03:00
Tulir Asokan
e0b1e9b0d3 commands/event: allow overriding mentions when replying
Some checks are pending
Go / Lint (latest) (push) Waiting to run
Go / Build (old, libolm) (push) Waiting to run
Go / Build (latest, libolm) (push) Waiting to run
Go / Build (old, goolm) (push) Waiting to run
Go / Build (latest, goolm) (push) Waiting to run
2025-04-29 19:59:31 +03:00
Tulir Asokan
da25a87fc1 event: clear mentions in SetEdit 2025-04-29 19:58:47 +03:00
Tulir Asokan
6c9cd6da6b commands: return event ID to allow edits 2025-04-29 18:41:37 +03:00
Tulir Asokan
771424f86b commands: stop looking for subcommands if not found 2025-04-29 17:31:27 +03:00
Tulir Asokan
db62b9a1d8 commands: ignore notices
Some checks are pending
Go / Lint (latest) (push) Waiting to run
Go / Build (old, libolm) (push) Waiting to run
Go / Build (latest, libolm) (push) Waiting to run
Go / Build (old, goolm) (push) Waiting to run
Go / Build (latest, goolm) (push) Waiting to run
2025-04-29 02:27:03 +03:00
Tulir Asokan
06a292e1cc commands: add pre func for subcommand parameters 2025-04-29 02:27:03 +03:00
Nick Mills-Barrett
bf33889eab
bridgev2/userlogin: delete disappearing messages when deleting portals (#374)
Some checks are pending
Go / Lint (latest) (push) Waiting to run
Go / Build (old, libolm) (push) Waiting to run
Go / Build (latest, libolm) (push) Waiting to run
Go / Build (old, goolm) (push) Waiting to run
Go / Build (latest, goolm) (push) Waiting to run
2025-04-28 14:10:57 +01:00
Tulir Asokan
a121a6101c format: accept any string-like type in SafeMarkdownCode
Some checks are pending
Go / Lint (latest) (push) Waiting to run
Go / Build (old, libolm) (push) Waiting to run
Go / Build (latest, libolm) (push) Waiting to run
Go / Build (old, goolm) (push) Waiting to run
Go / Build (latest, goolm) (push) Waiting to run
2025-04-28 01:00:25 +03:00
Tulir Asokan
287899435d format: add method to quote string in markdown inline code 2025-04-28 00:59:06 +03:00
Tulir Asokan
9dc0b3cddf commands: make unknown command handler more generic 2025-04-28 00:34:21 +03:00
Tulir Asokan
3badb9b332 commands: add subcommand system 2025-04-28 00:34:21 +03:00
Nick Mills-Barrett
33f3ccd6ae
crypto/verification: add missing lock in AcceptVerification method
Some checks failed
Go / Lint (latest) (push) Has been cancelled
Go / Build (old, libolm) (push) Has been cancelled
Go / Build (latest, libolm) (push) Has been cancelled
Go / Build (old, goolm) (push) Has been cancelled
Go / Build (latest, goolm) (push) Has been cancelled
2025-04-23 16:46:58 +01:00
Nick Mills-Barrett
de171e38d5
crypto/verification: use consistent action log 2025-04-23 16:46:46 +01:00
Nick Mills-Barrett
931f89202b
crypto/verification: include the incorrect state in non-ready error message
Some checks are pending
Go / Lint (latest) (push) Waiting to run
Go / Build (old, libolm) (push) Waiting to run
Go / Build (latest, libolm) (push) Waiting to run
Go / Build (old, goolm) (push) Waiting to run
Go / Build (latest, goolm) (push) Waiting to run
2025-04-23 16:30:46 +01:00
Nick Mills-Barrett
19153e3638
client: return immediately if context canceled on external upload 2025-04-23 16:30:46 +01:00
Tulir Asokan
5f4bd44baa event/voip: omit empty version field in call events 2025-04-23 15:57:35 +03:00
Tulir Asokan
3698f139b6 crypto/helper: always update crypto store device ID 2025-04-23 15:47:22 +03:00
Tulir Asokan
f931c9972d crypto/decryptolm: don't try to parse content if there is none 2025-04-23 15:29:45 +03:00
Tulir Asokan
87ca9bef1c bridgev2/networkinterface: add viewing chat callback 2025-04-23 11:29:07 +03:00
Tulir Asokan
953334a0a0 client,federation: add wrappers for /publicRooms 2025-04-21 23:43:44 +03:00
Tulir Asokan
d3d20cbcf2 client: add context parameter for setting max retries 2025-04-21 23:43:23 +03:00
Tulir Asokan
7165d3fa58 Bump version to v0.23.3
Some checks failed
Go / Lint (latest) (push) Has been cancelled
Go / Build (old, libolm) (push) Has been cancelled
Go / Build (latest, libolm) (push) Has been cancelled
Go / Build (old, goolm) (push) Has been cancelled
Go / Build (latest, goolm) (push) Has been cancelled
2025-04-16 11:59:48 +03:00
Tulir Asokan
7cb13f8fd3 bridgev2/status: add user_action field for bridge states
Some checks are pending
Go / Lint (latest) (push) Waiting to run
Go / Build (old, libolm) (push) Waiting to run
Go / Build (latest, libolm) (push) Waiting to run
Go / Build (old, goolm) (push) Waiting to run
Go / Build (latest, goolm) (push) Waiting to run
2025-04-15 14:43:47 +03:00
Tulir Asokan
89b41900e4 bridgev2/userlogin: stop using deprecated alias 2025-04-15 14:35:47 +03:00
Tulir Asokan
aae91f67b4 bridgev2: split stopping matrix connector
Also fix stopping the websocket in the default Matrix connector
2025-04-15 12:11:18 +03:00
Tulir Asokan
95a7e940d5 bridgev2: don't keep cache lock while waiting for stop 2025-04-15 11:06:02 +03:00
Tulir Asokan
99ff0c0964 crypto/decryptmegolm: add option to ignore failing to parse content after decryption
Some checks are pending
Go / Lint (latest) (push) Waiting to run
Go / Build (old, libolm) (push) Waiting to run
Go / Build (latest, libolm) (push) Waiting to run
Go / Build (old, goolm) (push) Waiting to run
Go / Build (latest, goolm) (push) Waiting to run
2025-04-14 23:08:11 +03:00
Tulir Asokan
60e14d7dff format: parse task list html
Some checks are pending
Go / Lint (latest) (push) Waiting to run
Go / Build (old, libolm) (push) Waiting to run
Go / Build (latest, libolm) (push) Waiting to run
Go / Build (old, goolm) (push) Waiting to run
Go / Build (latest, goolm) (push) Waiting to run
2025-04-13 22:45:51 +03:00
Tulir Asokan
56e2adbf83 commands: add generic command processing framework for bots
Some checks are pending
Go / Lint (latest) (push) Waiting to run
Go / Build (old, libolm) (push) Waiting to run
Go / Build (latest, libolm) (push) Waiting to run
Go / Build (old, goolm) (push) Waiting to run
Go / Build (latest, goolm) (push) Waiting to run
2025-04-13 21:34:21 +03:00
Tulir Asokan
7c1b0c5968 format: add EscapeMarkdown 2025-04-13 02:59:35 +03:00
Tulir Asokan
0f06c9ce31 event/content: add SetThread method 2025-04-13 02:59:26 +03:00
Tulir Asokan
cf801729af bridgev2/commands: implement MarkRead 2025-04-13 02:59:13 +03:00
Tulir Asokan
826089e020 id: make user id parsing more efficient
Some checks failed
Go / Lint (latest) (push) Has been cancelled
Go / Build (old, libolm) (push) Has been cancelled
Go / Build (latest, libolm) (push) Has been cancelled
Go / Build (old, goolm) (push) Has been cancelled
Go / Build (latest, goolm) (push) Has been cancelled
2025-04-11 17:12:06 +03:00
Adam Van Ymeren
0fcb552c27
bridgev2: make Bridge.Start() take a context (#368)
Some checks failed
Go / Lint (latest) (push) Has been cancelled
Go / Build (old, libolm) (push) Has been cancelled
Go / Build (latest, libolm) (push) Has been cancelled
Go / Build (old, goolm) (push) Has been cancelled
Go / Build (latest, goolm) (push) Has been cancelled
2025-04-09 07:46:03 -07:00
nexy7574
e675a3c09c
client: add allowed_room_ids to room summary response (#367)
Some checks failed
Go / Lint (latest) (push) Has been cancelled
Go / Build (old, libolm) (push) Has been cancelled
Go / Build (latest, libolm) (push) Has been cancelled
Go / Build (old, goolm) (push) Has been cancelled
Go / Build (latest, goolm) (push) Has been cancelled
2025-04-06 00:41:16 +01:00
Sumner Evans
4964889787
bridgev2/legacymigrate: don't error if post migrate hook fails
Some checks failed
Go / Lint (latest) (push) Has been cancelled
Go / Build (old, libolm) (push) Has been cancelled
Go / Build (latest, libolm) (push) Has been cancelled
Go / Build (old, goolm) (push) Has been cancelled
Go / Build (latest, goolm) (push) Has been cancelled
Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2025-04-04 08:37:49 -06:00
Sumner Evans
74a02366d7
bridgev2/legacymigrate: add post-migrate hook
Some checks failed
Go / Lint (latest) (push) Has been cancelled
Go / Build (old, libolm) (push) Has been cancelled
Go / Build (latest, libolm) (push) Has been cancelled
Go / Build (old, goolm) (push) Has been cancelled
Go / Build (latest, goolm) (push) Has been cancelled
Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2025-04-02 14:29:56 -06:00
Tulir Asokan
93b9509135 bridgev2/portal: send typing stop after message
Some checks are pending
Go / Lint (latest) (push) Waiting to run
Go / Build (old, libolm) (push) Waiting to run
Go / Build (latest, libolm) (push) Waiting to run
Go / Build (old, goolm) (push) Waiting to run
Go / Build (latest, goolm) (push) Waiting to run
2025-04-02 12:50:00 +03:00
SpiritCroc
d3ca9472cb
event: add Beeper transcription event definitions (#364)
Some checks failed
Go / Lint (latest) (push) Has been cancelled
Go / Build (old, libolm) (push) Has been cancelled
Go / Build (latest, libolm) (push) Has been cancelled
Go / Build (old, goolm) (push) Has been cancelled
Go / Build (latest, goolm) (push) Has been cancelled
2025-03-31 09:53:01 +02:00
Tulir Asokan
06f200da0d bridgev2: clear management room on leave. Fixes #355
Some checks failed
Go / Lint (latest) (push) Has been cancelled
Go / Build (old, libolm) (push) Has been cancelled
Go / Build (latest, libolm) (push) Has been cancelled
Go / Build (old, goolm) (push) Has been cancelled
Go / Build (latest, goolm) (push) Has been cancelled
2025-03-20 15:41:09 +02:00
Tulir Asokan
4a0aed30e8 brdigev2/bridgestate: add option to send status updates to management room 2025-03-20 15:34:40 +02:00
Tulir Asokan
03618fcc89 bridgev2: add support for timeouts on pending messages 2025-03-20 14:24:51 +02:00
Tulir Asokan
1c2898870c bridge: remove fallback status package 2025-03-18 14:23:37 +02:00
Tulir Asokan
11f9374003 Bump version to v0.23.2
Some checks failed
Go / Lint (latest) (push) Has been cancelled
Go / Build (old, libolm) (push) Has been cancelled
Go / Build (latest, libolm) (push) Has been cancelled
Go / Build (old, goolm) (push) Has been cancelled
Go / Build (latest, goolm) (push) Has been cancelled
2025-03-16 16:46:02 +02:00
Tulir Asokan
c0d1df18b4 appservice/http: use constant time comparisons for access tokens
Some checks are pending
Go / Lint (latest) (push) Waiting to run
Go / Build (old, libolm) (push) Waiting to run
Go / Build (latest, libolm) (push) Waiting to run
Go / Build (old, goolm) (push) Waiting to run
Go / Build (latest, goolm) (push) Waiting to run
2025-03-16 15:05:55 +02:00
Tulir Asokan
e1938c5159 bridge: remove package
Some checks are pending
Go / Lint (latest) (push) Waiting to run
Go / Build (old, libolm) (push) Waiting to run
Go / Build (latest, libolm) (push) Waiting to run
Go / Build (old, goolm) (push) Waiting to run
Go / Build (latest, goolm) (push) Waiting to run
2025-03-15 22:28:16 +02:00
Tulir Asokan
df7e02616d dependencies: update
Some checks failed
Go / Lint (latest) (push) Has been cancelled
Go / Build (old, libolm) (push) Has been cancelled
Go / Build (latest, libolm) (push) Has been cancelled
Go / Build (old, goolm) (push) Has been cancelled
Go / Build (latest, goolm) (push) Has been cancelled
2025-03-14 00:25:42 +02:00
Tulir Asokan
de5bee328b event: add utility functions for hashed policy list entities
Some checks are pending
Go / Lint (latest) (push) Waiting to run
Go / Build (old, libolm) (push) Waiting to run
Go / Build (latest, libolm) (push) Waiting to run
Go / Build (old, goolm) (push) Waiting to run
Go / Build (latest, goolm) (push) Waiting to run
2025-03-13 02:08:07 +02:00
Tulir Asokan
1b77ce1d3d event: add fields for MSC4204 and MSC4205 2025-03-13 01:08:29 +02:00
Tulir Asokan
f33b0506d0 client: add some nil safety
Some checks are pending
Go / Lint (latest) (push) Waiting to run
Go / Build (old, libolm) (push) Waiting to run
Go / Build (latest, libolm) (push) Waiting to run
Go / Build (old, goolm) (push) Waiting to run
Go / Build (latest, goolm) (push) Waiting to run
2025-03-12 19:48:05 +02:00
Tulir Asokan
6bba74ecb6 client: add refresh token to login response
Some checks are pending
Go / Lint (latest) (push) Waiting to run
Go / Build (old, libolm) (push) Waiting to run
Go / Build (latest, libolm) (push) Waiting to run
Go / Build (old, goolm) (push) Waiting to run
Go / Build (latest, goolm) (push) Waiting to run
Closes #294
2025-03-12 12:30:17 +02:00
Sumner Evans
eed1ffe107
verificationhelper/sas: fix error when both sides send StartSAS
Some checks are pending
Go / Lint (latest) (push) Waiting to run
Go / Build (old, libolm) (push) Waiting to run
Go / Build (latest, libolm) (push) Waiting to run
Go / Build (old, goolm) (push) Waiting to run
Go / Build (latest, goolm) (push) Waiting to run
Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2025-03-11 14:15:14 -06:00
Nick Mills-Barrett
a6d948f7c2
bridgev2/config: add BackfillConfig.WillPaginateManually
Some checks are pending
Go / Lint (latest) (push) Waiting to run
Go / Build (old, libolm) (push) Waiting to run
Go / Build (latest, libolm) (push) Waiting to run
Go / Build (old, goolm) (push) Waiting to run
Go / Build (latest, goolm) (push) Waiting to run
2025-03-11 10:55:00 +00:00
Tulir Asokan
a01edae1c3 bridgev2/portal: don't bridge remote edits by different users
Some checks are pending
Go / Lint (latest) (push) Waiting to run
Go / Build (old, libolm) (push) Waiting to run
Go / Build (latest, libolm) (push) Waiting to run
Go / Build (old, goolm) (push) Waiting to run
Go / Build (latest, goolm) (push) Waiting to run
2025-03-10 22:53:34 +02:00
Brad Murray
7492e6e308
crypto/verificationhelper: add supports SAS parameter (#361)
Some checks are pending
Go / Lint (latest) (push) Waiting to run
Go / Build (old, libolm) (push) Waiting to run
Go / Build (latest, libolm) (push) Waiting to run
Go / Build (old, goolm) (push) Waiting to run
Go / Build (latest, goolm) (push) Waiting to run
2025-03-10 14:51:26 -04:00
Tulir Asokan
d4975cbffd crypto/sql_store: fix MarkTrackedUsersOutdated for big lists
Some checks are pending
Go / Lint (latest) (push) Waiting to run
Go / Build (old, libolm) (push) Waiting to run
Go / Build (latest, libolm) (push) Waiting to run
Go / Build (old, goolm) (push) Waiting to run
Go / Build (latest, goolm) (push) Waiting to run
2025-03-10 01:25:17 +02:00
Tulir Asokan
52c8a2e1de sync: add support for MSC4222 2025-03-09 21:04:50 +02:00
Tulir Asokan
7f04ae7a9f client: remove unprefixed beeper streaming sync flag 2025-03-09 21:04:50 +02:00
nexy7574
d83b63aeaf
client: add wrapper for /knock endpoint (#359)
Some checks are pending
Go / Lint (latest) (push) Waiting to run
Go / Build (old, libolm) (push) Waiting to run
Go / Build (latest, libolm) (push) Waiting to run
Go / Build (old, goolm) (push) Waiting to run
Go / Build (latest, goolm) (push) Waiting to run
2025-03-09 19:16:31 +02:00
Tulir Asokan
0f4c560bd6 bridgev2/userlogin: log error if client isn't loaded
Some checks are pending
Go / Lint (latest) (push) Waiting to run
Go / Build (old, libolm) (push) Waiting to run
Go / Build (latest, libolm) (push) Waiting to run
Go / Build (old, goolm) (push) Waiting to run
Go / Build (latest, goolm) (push) Waiting to run
2025-03-08 19:27:57 +02:00
Tulir Asokan
c10d4eb80b event: make m.federate a pointer 2025-03-08 19:27:44 +02:00
Tulir Asokan
e306c2817e client: fix update delayed event method
Some checks failed
Go / Lint (latest) (push) Has been cancelled
Go / Build (old, libolm) (push) Has been cancelled
Go / Build (latest, libolm) (push) Has been cancelled
Go / Build (old, goolm) (push) Has been cancelled
Go / Build (latest, goolm) (push) Has been cancelled
2025-03-06 01:29:06 +02:00
Tulir Asokan
01d1e9d69c client: fix msc4104 unstable prefix 2025-03-06 01:27:34 +02:00
Tulir Asokan
8c4920a6c4 client: add wrappers for sending MSC4140 delayed events 2025-03-06 00:58:53 +02:00
Nick Mills-Barrett
ef5eb3c9cf
bridgev2/database: add BackfillTaskQuery.GetNextForPortal method
Some checks are pending
Go / Lint (latest) (push) Waiting to run
Go / Build (old, libolm) (push) Waiting to run
Go / Build (latest, libolm) (push) Waiting to run
Go / Build (old, goolm) (push) Waiting to run
Go / Build (latest, goolm) (push) Waiting to run
2025-03-05 14:33:04 +00:00
Nick Mills-Barrett
07f0d8836a
bridgev2/backfillqueue: expose DoBackfillTask method 2025-03-05 10:46:06 +00:00
Tulir Asokan
32b2376409 crypto/ssss: fix panic if key metadata is corrupted
Some checks are pending
Go / Lint (latest) (push) Waiting to run
Go / Build (old, libolm) (push) Waiting to run
Go / Build (latest, libolm) (push) Waiting to run
Go / Build (old, goolm) (push) Waiting to run
Go / Build (latest, goolm) (push) Waiting to run
Fixes tulir/gomuks#601
2025-03-04 12:42:50 +02:00
Tulir Asokan
7d3791ace3 crypto/encryptolm: add generic method to encrypt to-device events
Some checks are pending
Go / Lint (latest) (push) Waiting to run
Go / Build (old, libolm) (push) Waiting to run
Go / Build (latest, libolm) (push) Waiting to run
Go / Build (old, goolm) (push) Waiting to run
Go / Build (latest, goolm) (push) Waiting to run
2025-03-04 02:47:36 +02:00
Tulir Asokan
0c1fc68ec3 crypto/machine: return unhandled to-device events in HandleEncryptedEvent 2025-03-04 02:37:08 +02:00
Tulir Asokan
006bbe2806 crypto/helper: use sqlite3-fk-wal by default
Some checks failed
Go / Lint (latest) (push) Has been cancelled
Go / Build (old, libolm) (push) Has been cancelled
Go / Build (latest, libolm) (push) Has been cancelled
Go / Build (old, goolm) (push) Has been cancelled
Go / Build (latest, goolm) (push) Has been cancelled
2025-03-02 18:41:27 +02:00
Brad Murray
c7cb9ff2a3
crypto/keybackup: log mismatching public keys when verifiying megolm backups (#356)
Some checks failed
Go / Lint (latest) (push) Has been cancelled
Go / Build (old, libolm) (push) Has been cancelled
Go / Build (latest, libolm) (push) Has been cancelled
Go / Build (old, goolm) (push) Has been cancelled
Go / Build (latest, goolm) (push) Has been cancelled
Co-authored-by: Sumner Evans <sumner@beeper.com>
2025-02-28 09:45:28 -05:00
Sumner Evans
02733b5775
deps/go-util: upgrade to support -b in curl parsing
Some checks failed
Go / Lint (latest) (push) Has been cancelled
Go / Build (old, libolm) (push) Has been cancelled
Go / Build (latest, libolm) (push) Has been cancelled
Go / Build (old, goolm) (push) Has been cancelled
Go / Build (latest, goolm) (push) Has been cancelled
Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2025-02-27 11:59:13 -07:00
Tulir Asokan
2e7bdbc7a2 bridgev2/portal: delete old reaction before sending new one 2025-02-27 16:49:18 +02:00
Tulir Asokan
b72caa948c format/htmlparser: keep <> when converting links without text
Some checks are pending
Go / Lint (latest) (push) Waiting to run
Go / Build (old, libolm) (push) Waiting to run
Go / Build (latest, libolm) (push) Waiting to run
Go / Build (old, goolm) (push) Waiting to run
Go / Build (latest, goolm) (push) Waiting to run
2025-02-26 22:56:39 +02:00
Sumner Evans
0115ba0258
event: add support for MSC3765 rich text room topics
Some checks failed
Go / Lint (latest) (push) Has been cancelled
Go / Build (old, libolm) (push) Has been cancelled
Go / Build (latest, libolm) (push) Has been cancelled
Go / Build (old, goolm) (push) Has been cancelled
Go / Build (latest, goolm) (push) Has been cancelled
Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2025-02-24 11:47:59 -07:00
Tulir Asokan
1cc073cde6 client: add wrapper method for MSC2815
Some checks are pending
Go / Lint (latest) (push) Waiting to run
Go / Build (old, libolm) (push) Waiting to run
Go / Build (latest, libolm) (push) Waiting to run
Go / Build (old, goolm) (push) Waiting to run
Go / Build (latest, goolm) (push) Waiting to run
2025-02-23 18:13:09 +02:00
Tulir Asokan
e879ad19cc crypto/decryptmegolm: hide state event decryption behind flag 2025-02-23 16:24:03 +02:00
Tulir Asokan
43dbbb1ff8 bridgev2/portal: add m.mentions for disappearing message timer notice
Some checks are pending
Go / Lint (latest) (push) Waiting to run
Go / Build (old, libolm) (push) Waiting to run
Go / Build (latest, libolm) (push) Waiting to run
Go / Build (old, goolm) (push) Waiting to run
Go / Build (latest, goolm) (push) Waiting to run
2025-02-22 23:01:14 +02:00
Tulir Asokan
83f81ea67e bridgev2/messagestatus: send errors as m.notice 2025-02-22 23:01:14 +02:00
Tulir Asokan
af84927e31 bridgev2: add option to disable bridging m.notice messages 2025-02-22 23:01:14 +02:00
Tulir Asokan
fcdf7fd193 bridgev2/commands: add command to set new management room 2025-02-22 23:01:14 +02:00
Sumner Evans
4c58b82813
verificationhelper/sas: don't trust keys until both MAC events are sent
Some checks failed
Go / Lint (latest) (push) Has been cancelled
Go / Build (old, libolm) (push) Has been cancelled
Go / Build (latest, libolm) (push) Has been cancelled
Go / Build (old, goolm) (push) Has been cancelled
Go / Build (latest, goolm) (push) Has been cancelled
Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2025-02-20 14:13:42 -07:00
Tulir Asokan
12db97adb3 Bump version to v0.23.1
Some checks failed
Go / Lint (latest) (push) Has been cancelled
Go / Build (old, libolm) (push) Has been cancelled
Go / Build (latest, libolm) (push) Has been cancelled
Go / Build (old, goolm) (push) Has been cancelled
Go / Build (latest, goolm) (push) Has been cancelled
2025-02-16 17:17:34 +02:00
Tulir Asokan
b6c225c343 dependencies: update 2025-02-16 17:17:12 +02:00
Sumner Evans
5600dd4054
verificationhelper: add VerificationReady callback for when verification is accepted
Some checks failed
Go / Lint (latest) (push) Has been cancelled
Go / Build (old, libolm) (push) Has been cancelled
Go / Build (latest, libolm) (push) Has been cancelled
Go / Build (old, goolm) (push) Has been cancelled
Go / Build (latest, goolm) (push) Has been cancelled
This callback supersedes the ScanQRCode and ShowQRCode callbacks.

Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2025-02-13 14:09:14 -07:00
Brad Murray
14008caaa4
crypto/ssss: only accept secret shares from verified devices (#352)
Co-authored-by: Tulir Asokan <tulir@maunium.net>
2025-02-13 15:52:34 -05:00
Tulir Asokan
041784441f crypto: add context to IsDeviceTrusted and deprecate ResolveTrust
Some checks are pending
Go / Lint (latest) (push) Waiting to run
Go / Build (old, libolm) (push) Waiting to run
Go / Build (latest, libolm) (push) Waiting to run
Go / Build (old, goolm) (push) Waiting to run
Go / Build (latest, goolm) (push) Waiting to run
2025-02-13 14:07:49 +02:00
Brad Murray
100d945d39
Trust key backups if the public key matches (#351)
Some checks are pending
Go / Lint (latest) (push) Waiting to run
Go / Build (old, libolm) (push) Waiting to run
Go / Build (latest, libolm) (push) Waiting to run
Go / Build (old, goolm) (push) Waiting to run
Go / Build (latest, goolm) (push) Waiting to run
2025-02-12 16:58:04 -05:00
Tulir Asokan
aaad5119e0 dependencies: update go
Some checks are pending
Go / Lint (latest) (push) Waiting to run
Go / Build (old, libolm) (push) Waiting to run
Go / Build (latest, libolm) (push) Waiting to run
Go / Build (old, goolm) (push) Waiting to run
Go / Build (latest, goolm) (push) Waiting to run
2025-02-12 13:44:24 +02:00
Tulir Asokan
29319ccfd5 pushrules: fix word boundary matching and case sensitivity
Some checks failed
Go / Lint (latest) (push) Has been cancelled
Go / Build (old, libolm) (push) Has been cancelled
Go / Build (latest, libolm) (push) Has been cancelled
Go / Build (old, goolm) (push) Has been cancelled
Go / Build (latest, goolm) (push) Has been cancelled
2025-02-08 16:18:51 +02:00
Tulir Asokan
4c652f5200 bridgev2: add FormattedTitle to direct notification data
Some checks failed
Go / Lint (latest) (push) Has been cancelled
Go / Build (old, libolm) (push) Has been cancelled
Go / Build (latest, libolm) (push) Has been cancelled
Go / Build (old, goolm) (push) Has been cancelled
Go / Build (latest, goolm) (push) Has been cancelled
2025-02-06 15:02:45 +02:00
Sumner Evans
890db20d8e
verificationhelper: don't request QR scan if not enabled
Some checks are pending
Go / Lint (latest) (push) Waiting to run
Go / Build (old, libolm) (push) Waiting to run
Go / Build (latest, libolm) (push) Waiting to run
Go / Build (old, goolm) (push) Waiting to run
Go / Build (latest, goolm) (push) Waiting to run
Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2025-02-05 12:48:35 -07:00
Tulir Asokan
475c4bf39d crypto: fix key exports
Some checks failed
Go / Lint (latest) (push) Has been cancelled
Go / Build (old, libolm) (push) Has been cancelled
Go / Build (latest, libolm) (push) Has been cancelled
Go / Build (old, goolm) (push) Has been cancelled
Go / Build (latest, goolm) (push) Has been cancelled
2025-02-04 00:24:56 +02:00
Tulir Asokan
cf10041598 bridgev2/portal: fix handling edits if max age is undefined
Some checks are pending
Go / Lint (latest) (push) Waiting to run
Go / Build (old, libolm) (push) Waiting to run
Go / Build (latest, libolm) (push) Waiting to run
Go / Build (old, goolm) (push) Waiting to run
Go / Build (latest, goolm) (push) Waiting to run
2025-02-03 17:33:32 +02:00
Tulir Asokan
642e17f2ae client: add request body for user redact
Some checks failed
Go / Lint (latest) (push) Has been cancelled
Go / Build (old, libolm) (push) Has been cancelled
Go / Build (latest, libolm) (push) Has been cancelled
Go / Build (old, goolm) (push) Has been cancelled
Go / Build (latest, goolm) (push) Has been cancelled
2025-01-29 21:52:05 +02:00
Tulir Asokan
990519c29f versions: add constant for MSC4194 feature flag 2025-01-29 21:49:37 +02:00
Tulir Asokan
f915ba2671 client: add wrapper for MSC4194 2025-01-29 21:48:36 +02:00
Tulir Asokan
36942121f4 crypto/helper: add support for MSC4190 2025-01-29 21:35:32 +02:00
Tulir Asokan
4d1cd8432c crypto,sqlstatestore: fix more deprecated NewRowIter uses
Some checks are pending
Go / Lint (latest) (push) Waiting to run
Go / Build (old, libolm) (push) Waiting to run
Go / Build (latest, libolm) (push) Waiting to run
Go / Build (old, goolm) (push) Waiting to run
Go / Build (latest, goolm) (push) Waiting to run
2025-01-29 15:16:53 +02:00
Tulir Asokan
7c0ed06e43 bridge,crypto: fix uses of deprecated NewRowIter 2025-01-29 15:11:06 +02:00
Tulir Asokan
30ad8a99a8 bridgev2: make restarting bridges safer 2025-01-29 14:50:04 +02:00
Tulir Asokan
f2966bc55a dependencies: update 2025-01-29 14:48:48 +02:00
Tulir Asokan
7f20932607 client: add method to get full state event
Some checks are pending
Go / Lint (latest) (push) Waiting to run
Go / Build (old, libolm) (push) Waiting to run
Go / Build (latest, libolm) (push) Waiting to run
Go / Build (old, goolm) (push) Waiting to run
Go / Build (latest, goolm) (push) Waiting to run
2025-01-29 00:35:20 +02:00
Brad Murray
625dbc6de3
Add local bridge state types (#348)
Some checks failed
Go / Lint (latest) (push) Has been cancelled
Go / Build (old, libolm) (push) Has been cancelled
Go / Build (latest, libolm) (push) Has been cancelled
Go / Build (old, goolm) (push) Has been cancelled
Go / Build (latest, goolm) (push) Has been cancelled
2025-01-27 14:40:10 -05:00
Tulir Asokan
873d34ff5d bridgev2/matrixinterface: add message ID field to notification data
Some checks failed
Go / Lint (latest) (push) Has been cancelled
Go / Build (old, libolm) (push) Has been cancelled
Go / Build (latest, libolm) (push) Has been cancelled
Go / Build (old, goolm) (push) Has been cancelled
Go / Build (latest, goolm) (push) Has been cancelled
2025-01-24 18:05:41 +02:00
Tulir Asokan
4cde40cfb9 bridgev2/matrixinterface: add interface for displaying raw notifications 2025-01-24 18:01:28 +02:00
Tulir Asokan
2d79ce4eed bridgev2: allow passing extra data in ConnectBackground
Some checks are pending
Go / Lint (latest) (push) Waiting to run
Go / Build (old, libolm) (push) Waiting to run
Go / Build (latest, libolm) (push) Waiting to run
Go / Build (old, goolm) (push) Waiting to run
Go / Build (latest, goolm) (push) Waiting to run
2025-01-23 15:06:50 +02:00
Tulir Asokan
524379bdb3 bridgev2/networkinterface: add PushParsingNetwork
Some checks are pending
Go / Lint (latest) (push) Waiting to run
Go / Build (old, libolm) (push) Waiting to run
Go / Build (latest, libolm) (push) Waiting to run
Go / Build (old, goolm) (push) Waiting to run
Go / Build (latest, goolm) (push) Waiting to run
2025-01-22 15:18:41 +02:00
Tulir Asokan
9fa8272991 bridgev2: add fallback for RunOnce
Some checks are pending
Go / Lint (latest) (push) Waiting to run
Go / Build (old, libolm) (push) Waiting to run
Go / Build (latest, libolm) (push) Waiting to run
Go / Build (old, goolm) (push) Waiting to run
Go / Build (latest, goolm) (push) Waiting to run
2025-01-21 12:53:07 +02:00
Tulir Asokan
2c1aa218ae bridgev2/backfill: call complete callback if forward backfill has no messages 2025-01-21 12:53:07 +02:00
Tulir Asokan
21c059184b bridgev2/networkinterface: add some comments 2025-01-21 12:53:07 +02:00
Tulir Asokan
71d7d1e097 bridgev2/portal: fix manual CreateMatrixRoom calls when buffer is disabled 2025-01-21 12:53:07 +02:00
Sumner Evans
20db7f86ec
crypto/goolm: reorganize pickle code
Some checks failed
Go / Lint (latest) (push) Has been cancelled
Go / Build (old, libolm) (push) Has been cancelled
Go / Build (latest, libolm) (push) Has been cancelled
Go / Build (old, goolm) (push) Has been cancelled
Go / Build (latest, goolm) (push) Has been cancelled
Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2025-01-17 11:31:49 -07:00
Sumner Evans
976e11ad11
crypto/goolm/message: use buffers for encode/decode functions
Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2025-01-17 11:31:39 -07:00
Sumner Evans
d60d8d4744
crypto/aessha2: extract AES SHA2 functionality from crypto/goolm/cipher
This also refactors it to not recompute the keys via HKDF repeatedly.

Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2025-01-17 11:23:45 -07:00
Tulir Asokan
250d3356a4 Bump version to v0.23.0
Some checks failed
Go / Lint (latest) (push) Has been cancelled
Go / Build (old, libolm) (push) Has been cancelled
Go / Build (latest, libolm) (push) Has been cancelled
Go / Build (old, goolm) (push) Has been cancelled
Go / Build (latest, goolm) (push) Has been cancelled
2025-01-16 12:36:11 +02:00
Tulir Asokan
d579e450c6 dependencies: update 2025-01-16 12:04:13 +02:00
Tulir Asokan
757cdc7563 bridgev2/config: update MSC reference for appservice e2ee 2025-01-16 12:03:53 +02:00
Tulir Asokan
b17a8cd74c bridgev2: add RunOnce method to backfill a single user login and disconnect 2025-01-15 15:05:29 +02:00
Tulir Asokan
27ac910b65 bridgev2/portal: only use event loop when buffer is enabled
When buffer is disabled, queueEvent will instead acquire a lock and call
the handler directly. Hopefully the queueEvent callers are already in a
queue and will block so that queueEvent itself doesn't need to be strictly
FIFO (if callers aren't in a queue, even the buffered channel writes could
race each other).
2025-01-14 21:34:38 +02:00
Tulir Asokan
53a56684d3 event: remove struct tags from FileInfo
Some checks failed
Go / Lint (latest) (push) Has been cancelled
Go / Build (old, libolm) (push) Has been cancelled
Go / Build (latest, libolm) (push) Has been cancelled
Go / Build (old, goolm) (push) Has been cancelled
Go / Build (latest, goolm) (push) Has been cancelled
They're lies, only `serializableFileInfo` is actually used
2025-01-13 22:09:49 +02:00
Tulir Asokan
c05be16a52 event: fix de/serializing fi.mau.gif file info field 2025-01-13 22:09:38 +02:00
Tulir Asokan
bbcb1904e2 event/capabilities: add max text length field
Some checks failed
Go / Lint (latest) (push) Has been cancelled
Go / Build (old, libolm) (push) Has been cancelled
Go / Build (latest, libolm) (push) Has been cancelled
Go / Build (old, goolm) (push) Has been cancelled
Go / Build (latest, goolm) (push) Has been cancelled
2025-01-10 17:41:03 +02:00
Tulir Asokan
2851065869
bridgev2: send room capabilities as a state event (#344) 2025-01-10 16:55:18 +02:00
Tulir Asokan
59645cdf73
bridgev2/matrixinterface: let connector generate deterministic room IDs (#343) 2025-01-10 16:54:46 +02:00
Tulir Asokan
fc696eaa47 bridgev2/database: fix bugs with double puppeted column 2025-01-10 16:48:09 +02:00
Brad Murray
ac1ff66e3b
bridgev2/messagestatus: prevent checkpoints for double puppeted events (#342)
Some checks are pending
Go / Lint (latest) (push) Waiting to run
Go / Build (old, libolm) (push) Waiting to run
Go / Build (latest, libolm) (push) Waiting to run
Go / Build (old, goolm) (push) Waiting to run
Go / Build (latest, goolm) (push) Waiting to run
Co-authored-by: Tulir Asokan <tulir@maunium.net>
2025-01-10 15:19:44 +02:00
Sumner Evans
9748015309
bridgev2/portal: add function to get per-message profile for sender
Some checks are pending
Go / Lint (latest) (push) Waiting to run
Go / Build (old, libolm) (push) Waiting to run
Go / Build (latest, libolm) (push) Waiting to run
Go / Build (old, goolm) (push) Waiting to run
Go / Build (latest, goolm) (push) Waiting to run
Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2025-01-09 12:24:15 -07:00
Nick Mills-Barrett
e571946e82
client: add optional media HTTP client
Some checks failed
Go / Lint (latest) (push) Has been cancelled
Go / Build (old, libolm) (push) Has been cancelled
Go / Build (latest, libolm) (push) Has been cancelled
Go / Build (old, goolm) (push) Has been cancelled
Go / Build (latest, goolm) (push) Has been cancelled
2025-01-08 13:30:59 +00:00
Nick Mills-Barrett
6c5e4d8476
bridgev2/portal: using blocking portal queue push if buffer disabled
Some checks are pending
Go / Lint (latest) (push) Waiting to run
Go / Build (old, libolm) (push) Waiting to run
Go / Build (latest, libolm) (push) Waiting to run
Go / Build (old, goolm) (push) Waiting to run
Go / Build (latest, goolm) (push) Waiting to run
2025-01-07 13:51:53 +00:00
Tulir Asokan
ceb9c7b866 bridgev2/portal: fix reaction sync replacing all emojis
Some checks are pending
Go / Lint (latest) (push) Waiting to run
Go / Build (old, libolm) (push) Waiting to run
Go / Build (latest, libolm) (push) Waiting to run
Go / Build (old, goolm) (push) Waiting to run
Go / Build (latest, goolm) (push) Waiting to run
2025-01-07 13:44:37 +02:00
Tulir Asokan
68eaa9d1df dependencies: update
Some checks are pending
Go / Lint (latest) (push) Waiting to run
Go / Build (old, libolm) (push) Waiting to run
Go / Build (latest, libolm) (push) Waiting to run
Go / Build (old, goolm) (push) Waiting to run
Go / Build (latest, goolm) (push) Waiting to run
2025-01-06 17:24:26 +02:00
Tulir Asokan
012c246061 bridgev2/matrixinvite: fix setting service members when creating DM via invite
Some checks failed
Go / Lint (latest) (push) Has been cancelled
Go / Build (old, libolm) (push) Has been cancelled
Go / Build (latest, libolm) (push) Has been cancelled
Go / Build (old, goolm) (push) Has been cancelled
Go / Build (latest, goolm) (push) Has been cancelled
2025-01-04 12:48:14 +02:00
Tulir Asokan
5227c77012 bridgev2/commands: hide commands based on network interface implementations 2025-01-04 12:48:14 +02:00
Sumner Evans
dbd04afd41
verificationhelper/sas: include emoji descriptions in callback
Some checks are pending
Go / Lint (latest) (push) Waiting to run
Go / Build (old, libolm) (push) Waiting to run
Go / Build (latest, libolm) (push) Waiting to run
Go / Build (old, goolm) (push) Waiting to run
Go / Build (latest, goolm) (push) Waiting to run
Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2025-01-03 14:14:31 -07:00
Tulir Asokan
077716a4ec client: add wrapper for /openid/request_token
Some checks failed
Go / Lint (latest) (push) Has been cancelled
Go / Build (old, libolm) (push) Has been cancelled
Go / Build (latest, libolm) (push) Has been cancelled
Go / Build (old, goolm) (push) Has been cancelled
Go / Build (latest, goolm) (push) Has been cancelled
2025-01-01 18:00:25 +02:00
nexy7574
2cd6183f30
client: add support for arbitrary fields in /profile (#337)
Some checks failed
Go / Lint (latest) (push) Has been cancelled
Go / Build (old, libolm) (push) Has been cancelled
Go / Build (latest, libolm) (push) Has been cancelled
Go / Build (old, goolm) (push) Has been cancelled
Go / Build (latest, goolm) (push) Has been cancelled
Co-authored-by: Tulir Asokan <tulir@maunium.net>
2024-12-29 21:37:41 +02:00
Tulir Asokan
ba210a16b9 event: add site_name to link previews
Some checks failed
Go / Lint (latest) (push) Has been cancelled
Go / Build (old, libolm) (push) Has been cancelled
Go / Build (latest, libolm) (push) Has been cancelled
Go / Build (old, goolm) (push) Has been cancelled
Go / Build (latest, goolm) (push) Has been cancelled
2024-12-23 13:46:59 +02:00
nexy7574
5c4474ae70
client: support setting status message in SetPresence (#336)
Some checks are pending
Go / Lint (latest) (push) Waiting to run
Go / Build (old, libolm) (push) Waiting to run
Go / Build (latest, libolm) (push) Waiting to run
Go / Build (old, goolm) (push) Waiting to run
Go / Build (latest, goolm) (push) Waiting to run
2024-12-22 16:17:22 +02:00
Tulir Asokan
33b4e823c5 crypto/sqlstore: fix mistakes in olm hash methods 2024-12-22 14:10:30 +02:00
Tulir Asokan
fb7958b293 Merge branch 'tulir/olm-unwedging-improvements' 2024-12-20 15:21:18 +02:00
Tulir Asokan
1b66266b15 responses: remove non-existent summary field in invites 2024-12-20 15:21:05 +02:00
Tulir Asokan
049990cd7b crypto/decryptolm: check last olm session creation ts before unwedging 2024-12-20 14:48:53 +02:00
Tulir Asokan
e844153658 crypto/decryptolm: store olm hashes to prevent errors if they're repeated 2024-12-20 14:48:50 +02:00
Tulir Asokan
918ed4bf23 error: don't include path in HTTP errors
Some checks failed
Go / Lint (latest) (push) Has been cancelled
Go / Build (old, libolm) (push) Has been cancelled
Go / Build (latest, libolm) (push) Has been cancelled
Go / Build (old, goolm) (push) Has been cancelled
Go / Build (latest, goolm) (push) Has been cancelled
The request data is logged anyway, so it's nicer if the error string
only has the important part.
2024-12-19 23:34:02 +02:00
Tulir Asokan
6c9a29d25a client: add GetRoomSummary to implement MSC3266 2024-12-19 23:23:48 +02:00
Tulir Asokan
fbee4248a1 client: allow multiple vias in JoinRoom 2024-12-19 22:15:43 +02:00
Sumner Evans
7cc19d9720
filter: add unread_thread_notifications to FilterPart
Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2024-12-19 13:13:27 -07:00
Sumner Evans
4b4599d4ab
filter: make sub-structs properly nullable so omitempty works
Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2024-12-19 12:56:15 -07:00
Tulir Asokan
ddcb5fa6c5 versions: add new spec version constants
Some checks are pending
Go / Lint (latest) (push) Waiting to run
Go / Build (old, libolm) (push) Waiting to run
Go / Build (latest, libolm) (push) Waiting to run
Go / Build (old, goolm) (push) Waiting to run
Go / Build (latest, goolm) (push) Waiting to run
2024-12-19 19:05:00 +02:00
Tulir Asokan
1b78c83989 bridgev2/space: ensure adding portal to space isn't cancelled 2024-12-19 19:04:25 +02:00
Sumner Evans
6f47d6abfc
responses: add m.get_login_token to capabilities
See https://github.com/matrix-org/matrix-spec-proposals/pull/3882

Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2024-12-19 09:57:40 -07:00
SpiritCroc
a1a8791860
crypto/verificationhelper: add cross-signing to SAS verification path (#332)
Some checks are pending
Go / Lint (latest) (push) Waiting to run
Go / Build (old, libolm) (push) Waiting to run
Go / Build (latest, libolm) (push) Waiting to run
Go / Build (old, goolm) (push) Waiting to run
Go / Build (latest, goolm) (push) Waiting to run
When verifying a new device via SAS / emoji verification, we never cross-sign the new device, which thus never fully finishes verification.

This commit refactors a bunch of the key signing code and makes the SAS and QR methods behave more similarly.

---------

Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
Co-authored-by: Sumner Evans <sumner.evans@automattic.com>
2024-12-18 09:46:39 -07:00
Tulir Asokan
513bae79b8 bridgev2/config: fix missing database config error message
Some checks are pending
Go / Lint (latest) (push) Waiting to run
Go / Build (old, libolm) (push) Waiting to run
Go / Build (latest, libolm) (push) Waiting to run
Go / Build (old, goolm) (push) Waiting to run
Go / Build (latest, goolm) (push) Waiting to run
2024-12-17 21:02:21 +02:00
Tulir Asokan
6bf21f1019 bridgev2/config: move msc4190 flag to encryption section 2024-12-17 21:02:10 +02:00
Sumner Evans
742af7f70b
status/bridgestate: add IsValid function
Some checks are pending
Go / Lint (latest) (push) Waiting to run
Go / Build (old, libolm) (push) Waiting to run
Go / Build (latest, libolm) (push) Waiting to run
Go / Build (old, goolm) (push) Waiting to run
Go / Build (latest, goolm) (push) Waiting to run
Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2024-12-17 09:50:52 -07:00
Nick Mills-Barrett
15ab545e72
crypto: add background context to olm machine
Some checks are pending
Go / Lint (latest) (push) Waiting to run
Go / Build (old, libolm) (push) Waiting to run
Go / Build (latest, libolm) (push) Waiting to run
Go / Build (old, goolm) (push) Waiting to run
Go / Build (latest, goolm) (push) Waiting to run
Defaults to `context.Background()` but can be passed any context to
support cancelling background jobs the olm instance might be executing.
2024-12-16 15:45:57 +00:00
Tulir Asokan
2dcf2f9244 Bump version to v0.22.1
Some checks are pending
Go / Lint (latest) (push) Waiting to run
Go / Build (old, libolm) (push) Waiting to run
Go / Build (latest, libolm) (push) Waiting to run
Go / Build (old, goolm) (push) Waiting to run
Go / Build (latest, goolm) (push) Waiting to run
2024-12-16 15:59:29 +02:00
Tulir Asokan
351b49f8a8 bridgev2/matrixinvite: add separate interface for creating DM with ghost
Some checks are pending
Go / Lint (latest) (push) Waiting to run
Go / Build (old, libolm) (push) Waiting to run
Go / Build (latest, libolm) (push) Waiting to run
Go / Build (old, goolm) (push) Waiting to run
Go / Build (latest, goolm) (push) Waiting to run
2024-12-15 12:31:54 +02:00
Tulir Asokan
bfdd0efd0e dependencies: update
Some checks failed
Go / Lint (latest) (push) Has been cancelled
Go / Build (old, libolm) (push) Has been cancelled
Go / Build (latest, libolm) (push) Has been cancelled
Go / Build (old, goolm) (push) Has been cancelled
Go / Build (latest, goolm) (push) Has been cancelled
2024-12-12 02:46:32 +02:00
Tulir Asokan
48b7b3aca5 bridgev2/portal: don't try to backfill spaces
Some checks are pending
Go / Lint (latest) (push) Waiting to run
Go / Build (old, libolm) (push) Waiting to run
Go / Build (latest, libolm) (push) Waiting to run
Go / Build (old, goolm) (push) Waiting to run
Go / Build (latest, goolm) (push) Waiting to run
2024-12-11 22:57:09 +02:00
Tulir Asokan
fb0563e5ca bridgev2/login: add reauth hook in login process 2024-12-11 22:57:09 +02:00
Tulir Asokan
58a30b2958 bridgev2/commands: redact login commands with direct parameters 2024-12-11 22:57:09 +02:00
Scott Weber
c15e0dba93
event/beeper: change suborder field to int16 (from int64) (#328)
Some checks failed
Go / Lint (latest) (push) Has been cancelled
Go / Build (old, libolm) (push) Has been cancelled
Go / Build (latest, libolm) (push) Has been cancelled
Go / Build (old, goolm) (push) Has been cancelled
Go / Build (latest, goolm) (push) Has been cancelled
2024-12-09 14:41:36 -05:00
Tulir Asokan
3312a58161 bridgev2/matrix: log type of interrupt
Some checks are pending
Go / Lint (latest) (push) Waiting to run
Go / Build (old, libolm) (push) Waiting to run
Go / Build (latest, libolm) (push) Waiting to run
Go / Build (old, goolm) (push) Waiting to run
Go / Build (latest, goolm) (push) Waiting to run
2024-12-08 23:08:45 +02:00
Tulir Asokan
8d3c208bda crypto/verificationhelper: add from device parameter to requested callback 2024-12-08 22:04:08 +02:00
onestacked
3cb79ba7b5 client,bridgev2: add support for MSC4190
Some checks are pending
Go / Lint (latest) (push) Waiting to run
Go / Build (old, libolm) (push) Waiting to run
Go / Build (latest, libolm) (push) Waiting to run
Go / Build (old, goolm) (push) Waiting to run
Go / Build (latest, goolm) (push) Waiting to run
Closes #288
2024-12-07 23:34:04 +02:00
Tulir Asokan
421bd5c4c8 crypto/devicelist: remove unnecessary parameter
Some checks are pending
Go / Lint (latest) (push) Waiting to run
Go / Build (old, libolm) (push) Waiting to run
Go / Build (latest, libolm) (push) Waiting to run
Go / Build (old, goolm) (push) Waiting to run
Go / Build (latest, goolm) (push) Waiting to run
2024-12-07 15:04:33 +02:00
Tulir Asokan
3a9061e69c crypto/devicelist: add helper for getting cached device list 2024-12-07 14:59:45 +02:00
Tulir Asokan
933daead3b bridgev2/commands: fix pm command not starting chat
Some checks failed
Go / Lint (latest) (push) Has been cancelled
Go / Build (old, libolm) (push) Has been cancelled
Go / Build (latest, libolm) (push) Has been cancelled
Go / Build (old, goolm) (push) Has been cancelled
Go / Build (latest, goolm) (push) Has been cancelled
2024-12-05 14:25:04 +02:00
Tulir Asokan
bfa32f375f client: add support for MSC2666 2024-12-05 14:25:04 +02:00
Nick Mills-Barrett
9593f72d1b
event: add encrypted file info for m.room.member
Some checks failed
Go / Lint (latest) (push) Has been cancelled
Go / Build (old, libolm) (push) Has been cancelled
Go / Build (latest, libolm) (push) Has been cancelled
Go / Build (old, goolm) (push) Has been cancelled
Go / Build (latest, goolm) (push) Has been cancelled
2024-12-03 15:30:24 +00:00
Nick Mills-Barrett
166ba04aae
verificationhelper: Add missing verification txns unlock
Some checks are pending
Go / Lint (latest) (push) Waiting to run
Go / Build (old, libolm) (push) Waiting to run
Go / Build (latest, libolm) (push) Waiting to run
Go / Build (old, goolm) (push) Waiting to run
Go / Build (latest, goolm) (push) Waiting to run
2024-12-02 13:11:10 +00:00
Tulir Asokan
6032adb113 bridgev2/legacymigrate: fix database type for legacy go configs too
Some checks failed
Go / Lint (latest) (push) Has been cancelled
Go / Build (old, libolm) (push) Has been cancelled
Go / Build (latest, libolm) (push) Has been cancelled
Go / Build (old, goolm) (push) Has been cancelled
Go / Build (latest, goolm) (push) Has been cancelled
2024-11-30 23:02:26 +02:00
Tulir Asokan
e3d5267485 bridgev2/networkinterface: don't allow returning errors in Connect
Some checks failed
Go / Lint (latest) (push) Has been cancelled
Go / Build (old, libolm) (push) Has been cancelled
Go / Build (latest, libolm) (push) Has been cancelled
Go / Build (old, goolm) (push) Has been cancelled
Go / Build (latest, goolm) (push) Has been cancelled
2024-11-29 18:21:54 +02:00
Nick Mills-Barrett
b32def2b14
bridgev2/userlogin: add blocking cleanup delete option
Some checks failed
Go / Lint (latest) (push) Has been cancelled
Go / Build (old, libolm) (push) Has been cancelled
Go / Build (latest, libolm) (push) Has been cancelled
Go / Build (old, goolm) (push) Has been cancelled
Go / Build (latest, goolm) (push) Has been cancelled
This allows the caller to block until the cleanup actions are complete.
2024-11-27 17:04:35 +00:00
Brad Murray
4b970e0ea7
crypto/sqlstore: add index to crypto_olm_sessions table to speed up lookups by sender_key (#323)
Some checks are pending
Go / Lint (latest) (push) Waiting to run
Go / Build (old, libolm) (push) Waiting to run
Go / Build (latest, libolm) (push) Waiting to run
Go / Build (old, goolm) (push) Waiting to run
Go / Build (latest, goolm) (push) Waiting to run
Co-authored-by: Tulir Asokan <tulir@maunium.net>
2024-11-26 15:29:18 -05:00
Sumner Evans
f7e5f0a3b6
verificationhelper: add tests for using SQLite store for verification
Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2024-11-26 10:08:52 -07:00
Sumner Evans
2a8e6fba65
verificationhelper: set the expiration time correctly
Some checks failed
Go / Lint (latest) (push) Has been cancelled
Go / Build (old, libolm) (push) Has been cancelled
Go / Build (latest, libolm) (push) Has been cancelled
Go / Build (old, goolm) (push) Has been cancelled
Go / Build (latest, goolm) (push) Has been cancelled
Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2024-11-26 08:15:48 -07:00
Sumner Evans
3f23a752e6
verificationhelper: set the context logger more aggressively
Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2024-11-26 08:15:48 -07:00
Sumner Evans
15da5bed52
verificationhelper: save verification status in store
Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2024-11-26 08:15:48 -07:00
Tulir Asokan
0384e800fd bridgev2/login: add url and domain user input types
Some checks are pending
Go / Lint (latest) (push) Waiting to run
Go / Build (old, libolm) (push) Waiting to run
Go / Build (latest, libolm) (push) Waiting to run
Go / Build (old, goolm) (push) Waiting to run
Go / Build (latest, goolm) (push) Waiting to run
2024-11-26 14:50:02 +02:00
Tulir Asokan
2353d323a4 bridgev2/legacymigrate: log portal info in post-migration 2024-11-26 13:28:26 +02:00
Tulir Asokan
4820d4da48 client: use timeout 0 for initial sync
Some checks are pending
Go / Lint (latest) (push) Waiting to run
Go / Build (old, libolm) (push) Waiting to run
Go / Build (latest, libolm) (push) Waiting to run
Go / Build (old, goolm) (push) Waiting to run
Go / Build (latest, goolm) (push) Waiting to run
2024-11-25 23:51:46 +02:00
Sumner Evans
249bc1b14e
Revert "verificationhelper: save verification status in store" (#319)
Reverts commit 40dbe7535d

Signed-off-by: Sumner Evans <sumner.evans@automattic.com>

Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2024-11-25 13:06:33 -05:00
Tulir Asokan
b4551fc3da crypto/decryptolm: don't use deleted sessions for decrypting
Some checks failed
Go / Lint (latest) (push) Has been cancelled
Go / Build (old, libolm) (push) Has been cancelled
Go / Build (latest, libolm) (push) Has been cancelled
Go / Build (old, goolm) (push) Has been cancelled
Go / Build (latest, goolm) (push) Has been cancelled
2024-11-22 10:20:34 +02:00
Tulir Asokan
1170825b09 crypto: fix key share count log
Some checks are pending
Go / Lint (latest) (push) Waiting to run
Go / Build (old, libolm) (push) Waiting to run
Go / Build (latest, libolm) (push) Waiting to run
Go / Build (old, goolm) (push) Waiting to run
Go / Build (latest, goolm) (push) Waiting to run
2024-11-21 18:23:04 +02:00
Sumner Evans
4cd2bb62ff
verificationhelper: improve logging on ready and start event handlers
Some checks are pending
Go / Lint (latest) (push) Waiting to run
Go / Build (old, libolm) (push) Waiting to run
Go / Build (latest, libolm) (push) Waiting to run
Go / Build (old, goolm) (push) Waiting to run
Go / Build (latest, goolm) (push) Waiting to run
Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2024-11-20 10:16:18 -07:00
Sumner Evans
d89912cfcb
verificationhelper: fix hard-coded username
Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2024-11-20 10:08:10 -07:00
Sumner Evans
40dbe7535d
verificationhelper: save verification status in store
Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2024-11-20 08:36:00 -07:00
Tulir Asokan
ed709621d4 bridgev2/portal: ensure ghost row exists even when overriding sender id 2024-11-20 15:25:37 +02:00
Tulir Asokan
9373794606
crypto: delete old olm sessions if there are too many (#315)
Some checks are pending
Go / Lint (latest) (push) Waiting to run
Go / Build (old, libolm) (push) Waiting to run
Go / Build (latest, libolm) (push) Waiting to run
Go / Build (old, goolm) (push) Waiting to run
Go / Build (latest, goolm) (push) Waiting to run
2024-11-20 15:03:21 +02:00
Tulir Asokan
c8e197a4f9 bridgev2/errors: add shared error for unknown login flow ID 2024-11-20 13:22:53 +02:00
Sumner Evans
d575cc79ef
Revert "verificationhelper: add callback for when other user reports done"
Some checks are pending
Go / Lint (latest) (push) Waiting to run
Go / Build (old, libolm) (push) Waiting to run
Go / Build (latest, libolm) (push) Waiting to run
Go / Build (old, goolm) (push) Waiting to run
Go / Build (latest, goolm) (push) Waiting to run
This reverts commit 363fdfa3b2.
2024-11-19 16:09:18 -07:00
Tulir Asokan
4bc4bc0046 bridgev2/networkinterface: add native flag for push config
Some checks are pending
Go / Lint (latest) (push) Waiting to run
Go / Build (old, libolm) (push) Waiting to run
Go / Build (latest, libolm) (push) Waiting to run
Go / Build (old, goolm) (push) Waiting to run
Go / Build (latest, goolm) (push) Waiting to run
2024-11-19 16:00:28 +02:00
Tulir Asokan
039f7335e4 client: switch to via in /join calls as per MSC4156
Some checks are pending
Go / Lint (latest) (push) Waiting to run
Go / Build (old, libolm) (push) Waiting to run
Go / Build (latest, libolm) (push) Waiting to run
Go / Build (old, goolm) (push) Waiting to run
Go / Build (latest, goolm) (push) Waiting to run
2024-11-19 01:35:30 +02:00
Sumner Evans
363fdfa3b2
verificationhelper: add callback for when other user reports done
Some checks are pending
Go / Lint (latest) (push) Waiting to run
Go / Build (old, libolm) (push) Waiting to run
Go / Build (latest, libolm) (push) Waiting to run
Go / Build (old, goolm) (push) Waiting to run
Go / Build (latest, goolm) (push) Waiting to run
Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2024-11-18 11:11:22 -07:00
Tulir Asokan
88dd813d67 events/beeper: make order_string a pointer 2024-11-18 19:52:36 +02:00
Tulir Asokan
c13ec82f6d Bump version to v0.22.0
Some checks failed
Go / Lint (latest) (push) Has been cancelled
Go / Build (old, libolm) (push) Has been cancelled
Go / Build (latest, libolm) (push) Has been cancelled
Go / Build (old, goolm) (push) Has been cancelled
Go / Build (latest, goolm) (push) Has been cancelled
2024-11-16 16:03:52 +02:00
Tulir Asokan
5a3dd8d45c imports: use html instead of x/net/html for escaping
Some checks are pending
Go / Lint (latest) (push) Waiting to run
Go / Build (old, libolm) (push) Waiting to run
Go / Build (latest, libolm) (push) Waiting to run
Go / Build (old, goolm) (push) Waiting to run
Go / Build (latest, goolm) (push) Waiting to run
2024-11-15 15:34:31 +02:00
Tulir Asokan
21aa3291f3 bridgev2/database: include portal receiver in reaction queries
Some checks are pending
Go / Lint (latest) (push) Waiting to run
Go / Build (old, libolm) (push) Waiting to run
Go / Build (latest, libolm) (push) Waiting to run
Go / Build (old, goolm) (push) Waiting to run
Go / Build (latest, goolm) (push) Waiting to run
2024-11-14 14:58:08 +02:00
Tulir Asokan
3f9a63784e bridgev2/config: add more granular control over room tags
Some checks are pending
Go / Lint (latest) (push) Waiting to run
Go / Build (old, libolm) (push) Waiting to run
Go / Build (latest, libolm) (push) Waiting to run
Go / Build (old, goolm) (push) Waiting to run
Go / Build (latest, goolm) (push) Waiting to run
2024-11-13 18:31:41 +02:00
Tulir Asokan
b22764aa17 dependencies: update 2024-11-13 15:56:38 +02:00
Tulir Asokan
8fbf245e97 bridgev2/commands: include state event in list-logins 2024-11-13 15:26:01 +02:00
Tulir Asokan
702a0e047c bridgev2/config: add option to disable tag bridging
Some checks failed
Go / Lint (latest) (push) Has been cancelled
Go / Build (old, libolm) (push) Has been cancelled
Go / Build (latest, libolm) (push) Has been cancelled
Go / Build (old, goolm) (push) Has been cancelled
Go / Build (latest, goolm) (push) Has been cancelled
2024-11-08 10:25:12 +01:00
Scott Weber
22a4c50e0d
event: add GetRaw() to initialize Raw if necessary (#313)
Some checks failed
Go / Lint (latest) (push) Has been cancelled
Go / Build (old, libolm) (push) Has been cancelled
Go / Build (latest, libolm) (push) Has been cancelled
Go / Build (old, goolm) (push) Has been cancelled
Go / Build (latest, goolm) (push) Has been cancelled
2024-11-06 11:23:37 -05:00
Tulir Asokan
449de115ff bridgev2/portal: run post handle even if chat resync is short-circuited 2024-11-06 15:58:56 +01:00
Tulir Asokan
5967fe7b0f bridgev2/simplevent: add pre/post handle support to EventMeta
Some checks are pending
Go / Lint (latest) (push) Waiting to run
Go / Build (old, libolm) (push) Waiting to run
Go / Build (latest, libolm) (push) Waiting to run
Go / Build (old, goolm) (push) Waiting to run
Go / Build (latest, goolm) (push) Waiting to run
2024-11-06 14:50:41 +01:00
Tulir Asokan
f588c35d8b mediaproxy: pass through query parameters 2024-11-06 13:11:31 +01:00
Tulir Asokan
8c3056a447 mediaproxy: add content disposition for proxied downloads 2024-11-06 12:14:29 +01:00
Tulir Asokan
87651815a9 mediaproxy: drop support for proxying urls 2024-11-06 12:14:29 +01:00
Tulir Asokan
49310b6ec1 mediaproxy: refactor error responses 2024-11-06 12:14:29 +01:00
Tulir Asokan
39bddeb7d3 mediaproxy: add support for temp files 2024-11-06 12:14:29 +01:00
Tulir Asokan
56aadb232f mediaproxy: add support for writing directly to http response 2024-11-06 12:14:29 +01:00
Nick Mills-Barrett
3b93df0702
Remove unused InviteUser in the MatrixAPI interface 2024-11-06 11:24:00 +01:00
Scott Weber
7a4e5e549e
Use BeeperEncodedOrder to encode BeeperHSOrderString (#311) 2024-11-06 04:56:20 -05:00
Tulir Asokan
05a970d370 changelog: update 2024-11-06 09:33:45 +01:00
Tulir Asokan
ff907f4033 event/reply: implement MSC2781 2024-11-06 09:26:59 +01:00
Sumner Evans
fff009b5fa
bridgev2/portal: check if client is logged in before handling read receipt
Some checks failed
Go / Lint (latest) (push) Has been cancelled
Go / Build (old, libolm) (push) Has been cancelled
Go / Build (latest, libolm) (push) Has been cancelled
Go / Build (old, goolm) (push) Has been cancelled
Go / Build (latest, goolm) (push) Has been cancelled
Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2024-11-04 10:00:05 -07:00
Tulir Asokan
83e60efa15 format: add markdown renderer for custom emojis
Some checks failed
Go / Lint (latest) (push) Has been cancelled
Go / Build (old, libolm) (push) Has been cancelled
Go / Build (latest, libolm) (push) Has been cancelled
Go / Build (old, goolm) (push) Has been cancelled
Go / Build (latest, goolm) (push) Has been cancelled
2024-11-02 13:44:51 +02:00
Tulir Asokan
013afd06d3 format/htmlparser: convert math to just latex 2024-11-02 12:37:50 +02:00
Tulir Asokan
0f73c83196 format/mdext: maybe improve math parser 2024-11-02 12:31:23 +02:00
Tulir Asokan
f0c46cf629 format/mdext: add math parser 2024-11-02 11:55:42 +02:00
Tulir Asokan
9f1a8f2cc4 format: add TextToContent helper 2024-11-02 11:55:42 +02:00
Nick Mills-Barrett
f606129e73
Add Beeper local bridge fields to create room struct
Some checks are pending
Go / Lint (latest) (push) Waiting to run
Go / Build (old, libolm) (push) Waiting to run
Go / Build (latest, libolm) (push) Waiting to run
Go / Build (old, goolm) (push) Waiting to run
Go / Build (latest, goolm) (push) Waiting to run
2024-11-01 16:26:20 +00:00
Scott Weber
7e8d435aef
event: add BeeperHSOrderString to unsigned (#308)
Some checks are pending
Go / Lint (latest) (push) Waiting to run
Go / Build (old, libolm) (push) Waiting to run
Go / Build (latest, libolm) (push) Waiting to run
Go / Build (old, goolm) (push) Waiting to run
Go / Build (latest, goolm) (push) Waiting to run
2024-10-31 14:35:48 -04:00
Tulir Asokan
34d551085c crypto/encryptmegolm: log target identity key when encrypting session
Some checks are pending
Go / Lint (latest) (push) Waiting to run
Go / Build (old, libolm) (push) Waiting to run
Go / Build (latest, libolm) (push) Waiting to run
Go / Build (old, goolm) (push) Waiting to run
Go / Build (latest, goolm) (push) Waiting to run
2024-10-30 21:06:11 +02:00
Sumner Evans
7c227e175d
pre-commit: update and ban Msgf on zerolog logs
Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2024-10-30 10:15:37 -06:00
Sumner Evans
40927f4b12
pre-commit: update and ban use of global zerolog logger
Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2024-10-30 09:58:09 -06:00
Sumner Evans
2a20aa3232
verificationhelper: add better logging when sending cancellation due to unknown transaction ID
Some checks are pending
Go / Lint (latest) (push) Waiting to run
Go / Build (old, libolm) (push) Waiting to run
Go / Build (latest, libolm) (push) Waiting to run
Go / Build (old, goolm) (push) Waiting to run
Go / Build (latest, goolm) (push) Waiting to run
Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2024-10-28 23:04:00 -06:00
Sumner Evans
7066beb946
event: fix receivers for event.Type
Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2024-10-28 23:02:40 -06:00
Sumner Evans
48aa04889c
simplevent/meta: make builder pattern methods non-pointer receivers
Some checks are pending
Go / Lint (latest) (push) Waiting to run
Go / Build (old, libolm) (push) Waiting to run
Go / Build (latest, libolm) (push) Waiting to run
Go / Build (old, goolm) (push) Waiting to run
Go / Build (latest, goolm) (push) Waiting to run
Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2024-10-28 13:46:14 -06:00
Sumner Evans
0f31a2fb8e
simplevent/meta: add builder pattern methods
Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2024-10-28 13:35:33 -06:00
Tulir Asokan
0f3c599888 appservice/registration: update stable ephemeral event field
Some checks are pending
Go / Lint (latest) (push) Waiting to run
Go / Build (old, libolm) (push) Waiting to run
Go / Build (latest, libolm) (push) Waiting to run
Go / Build (old, goolm) (push) Waiting to run
Go / Build (latest, goolm) (push) Waiting to run
2024-10-28 14:24:00 +02:00
Tulir Asokan
a59d4d7867 format: add support for img tags in HTML parser
Some checks failed
Go / Lint (latest) (push) Has been cancelled
Go / Build (old, libolm) (push) Has been cancelled
Go / Build (latest, libolm) (push) Has been cancelled
Go / Build (old, goolm) (push) Has been cancelled
Go / Build (latest, goolm) (push) Has been cancelled
2024-10-26 14:51:59 +03:00
Sumner Evans
4a2557ed15
crypto: propagate more errors
Some checks are pending
Go / Lint (latest) (push) Waiting to run
Go / Build (old, libolm) (push) Waiting to run
Go / Build (latest, libolm) (push) Waiting to run
Go / Build (old, goolm) (push) Waiting to run
Go / Build (latest, goolm) (push) Waiting to run
Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2024-10-25 09:39:19 -06:00
Sumner Evans
9f74b58d84
crypto/goolm/crypto: use stdlib for HKDF and HMAC operations
Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2024-10-25 09:08:34 -06:00
Sumner Evans
c09eae39d0
crypto: always read from crypto/rand
Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2024-10-25 09:08:32 -06:00
Sumner Evans
d2aaa2dc5c
goolm/libolmpickle: add Decoder for easier pickling API
Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2024-10-25 09:06:46 -06:00
Sumner Evans
e525e151e1
goolm/libolmpickle: add Encoder for easier pickling API
Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2024-10-25 09:06:45 -06:00
Sumner Evans
bc1f09086f
goolm: use constants for pickle lengths when possible
Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2024-10-25 08:54:48 -06:00
Sumner Evans
fbea2a067c
goolm: simplify return statements
Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2024-10-25 08:53:43 -06:00
Sumner Evans
93a57f5378
goolm/cipher: remove unnecessary function
Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2024-10-25 08:47:58 -06:00
Sumner Evans
95e562b2fe
goolm/cipher: make deriveAESKeys call io.ReadFull less
Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2024-10-25 08:47:58 -06:00
Sumner Evans
b1af9f4941
goolm/cipher: inline keys and KDF info in test
Before this commit, it didn't properly check that changing only the key
changes the output.

Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2024-10-25 08:47:58 -06:00
Sumner Evans
eb632a9994
goolm: simplify tests using testify
Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2024-10-25 08:47:56 -06:00
Sumner Evans
7cc46f1ff3
crypto: always read from crypto/rand
Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2024-10-25 08:17:13 -06:00
Sumner Evans
6fd4b8a213
bridgev2/database: add function to get last N messages in portal
Some checks are pending
Go / Lint (latest) (push) Waiting to run
Go / Build (old, libolm) (push) Waiting to run
Go / Build (latest, libolm) (push) Waiting to run
Go / Build (old, goolm) (push) Waiting to run
Go / Build (latest, goolm) (push) Waiting to run
Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2024-10-23 14:40:42 -06:00
Tulir Asokan
e7811488dd bridgev2/portal: only clear name if it's set to a custom one
Some checks failed
Go / Lint (latest) (push) Has been cancelled
Go / Build (old, libolm) (push) Has been cancelled
Go / Build (latest, libolm) (push) Has been cancelled
Go / Build (old, goolm) (push) Has been cancelled
Go / Build (latest, goolm) (push) Has been cancelled
2024-10-23 17:31:31 +03:00
Tulir Asokan
07d7b3cfc2 bridgev2/portal: add special constant to reset portal name 2024-10-23 17:29:25 +03:00
Nick Mills-Barrett
ab6c2ed9a2
Add custom field to set state event ID when sending 2024-10-23 14:56:23 +01:00
Tulir Asokan
eead5937ea bridgev2/provisioning: include HTTP request in login contexts 2024-10-23 12:30:59 +03:00
Tulir Asokan
d316a6b55f bridgev2/commands: don't validate cookies before url decoding 2024-10-23 12:12:49 +03:00
Tulir Asokan
9b8244269b bridgev2/portal: re-id outgoing reactions too
Some checks are pending
Go / Lint (latest) (push) Waiting to run
Go / Build (old, libolm) (push) Waiting to run
Go / Build (latest, libolm) (push) Waiting to run
Go / Build (old, goolm) (push) Waiting to run
Go / Build (latest, goolm) (push) Waiting to run
2024-10-22 19:01:26 +03:00
Tulir Asokan
8a8163106d sqlstatestore: handle nulls in members_fetched
Some checks are pending
Go / Lint (latest) (push) Waiting to run
Go / Build (old, libolm) (push) Waiting to run
Go / Build (latest, libolm) (push) Waiting to run
Go / Build (old, goolm) (push) Waiting to run
Go / Build (latest, goolm) (push) Waiting to run
2024-10-22 12:50:53 +03:00
Tulir Asokan
e17cb83855 error: ignore RespError.Write calls with nil writer
Some checks failed
Go / Lint (latest) (push) Has been cancelled
Go / Build (old, libolm) (push) Has been cancelled
Go / Build (latest, libolm) (push) Has been cancelled
Go / Build (old, goolm) (push) Has been cancelled
Go / Build (latest, goolm) (push) Has been cancelled
2024-10-20 19:44:13 +03:00
Sumner Evans
3f08ef0d57
verificationhelper/request: check txn ID is different before sending cancellations
Some checks are pending
Go / Lint (latest) (push) Waiting to run
Go / Build (old, libolm) (push) Waiting to run
Go / Build (latest, libolm) (push) Waiting to run
Go / Build (old, goolm) (push) Waiting to run
Go / Build (latest, goolm) (push) Waiting to run
This will allow it to fallthrought to the correct error which is that we
received a new verification request for the same transaction ID.

Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2024-10-19 18:01:09 -06:00
Tulir Asokan
3277c529a2 crypto: add full support for json.RawMessage in EncryptMegolmEvent
Some checks are pending
Go / Lint (latest) (push) Waiting to run
Go / Build (old, libolm) (push) Waiting to run
Go / Build (latest, libolm) (push) Waiting to run
Go / Build (old, goolm) (push) Waiting to run
Go / Build (latest, goolm) (push) Waiting to run
2024-10-18 20:49:05 +03:00
Tulir Asokan
6c07832ed7 pushrules: add support for sender_notification_permission condition kind
Some checks are pending
Go / Lint (latest) (push) Waiting to run
Go / Build (old, libolm) (push) Waiting to run
Go / Build (latest, libolm) (push) Waiting to run
Go / Build (old, goolm) (push) Waiting to run
Go / Build (latest, goolm) (push) Waiting to run
2024-10-18 14:07:25 +03:00
Tulir Asokan
3678284292 id: remove outdated URI tests
Some checks are pending
Go / Lint (latest) (push) Waiting to run
Go / Build (old, libolm) (push) Waiting to run
Go / Build (latest, libolm) (push) Waiting to run
Go / Build (old, goolm) (push) Waiting to run
Go / Build (latest, goolm) (push) Waiting to run
2024-10-17 20:30:32 +03:00
Tulir Asokan
2a2a576bf4 format/rainbow: move into gomuks 2024-10-17 20:15:32 +03:00
Tulir Asokan
32e9a2f6e3 hicli: move into gomuks 2024-10-17 20:14:32 +03:00
Tulir Asokan
af360cd534 id: drop support for room alias + event ID links 2024-10-17 20:02:23 +03:00
Tulir Asokan
915167f459 bridgev2/commands: use PathUnescape instead of QueryUnescape for cookies
Some checks are pending
Go / Lint (latest) (push) Waiting to run
Go / Build (old, libolm) (push) Waiting to run
Go / Build (latest, libolm) (push) Waiting to run
Go / Build (old, goolm) (push) Waiting to run
Go / Build (latest, goolm) (push) Waiting to run
`+` should not be decoded into a space
2024-10-17 14:04:19 +03:00
Tulir Asokan
758e80a5f0 hicli: add html sanitization and push rule evaluation 2024-10-17 14:04:19 +03:00
Tulir Asokan
1d4c2d2554 hicli/database: ignore duplicate timeline inserts
Some checks are pending
Go / Lint (latest) (push) Waiting to run
Go / Build (old, libolm) (push) Waiting to run
Go / Build (latest, libolm) (push) Waiting to run
Go / Build (old, goolm) (push) Waiting to run
Go / Build (latest, goolm) (push) Waiting to run
2024-10-16 17:33:40 +03:00
Tulir Asokan
eed7bc66a0 ci: use pre-commit action instead of running manually 2024-10-16 17:32:04 +03:00
Tulir Asokan
cc4170475b Bump version to v0.21.1
Some checks are pending
Go / Lint (latest) (push) Waiting to run
Go / Build (old, libolm) (push) Waiting to run
Go / Build (latest, libolm) (push) Waiting to run
Go / Build (old, goolm) (push) Waiting to run
Go / Build (latest, goolm) (push) Waiting to run
2024-10-16 10:31:14 +03:00
Tulir Asokan
dc697ecd64 bridgev2/portal: include receiver in deterministic room IDs
Some checks are pending
Go / Lint (latest) (push) Waiting to run
Go / Build (old, libolm) (push) Waiting to run
Go / Build (latest, libolm) (push) Waiting to run
Go / Build (old, goolm) (push) Waiting to run
Go / Build (latest, goolm) (push) Waiting to run
2024-10-15 20:36:12 +03:00
Tulir Asokan
df65202dac dependencies: update 2024-10-15 17:04:51 +03:00
Tulir Asokan
21eaeeaecf hicli/sync: always set sorting timestamp for new rooms
Some checks are pending
Go / Lint (latest) (push) Waiting to run
Go / Build (old, libolm) (push) Waiting to run
Go / Build (latest, libolm) (push) Waiting to run
Go / Build (old, goolm) (push) Waiting to run
Go / Build (latest, goolm) (push) Waiting to run
2024-10-15 02:19:04 +03:00
Tulir Asokan
948c9b0f39 hicli/sync: don't fail event parsing if it's already parsed 2024-10-15 01:52:58 +03:00
Tulir Asokan
89f78e907d hicli: use user avatar as room avatar in DMs 2024-10-15 01:39:05 +03:00
Tulir Asokan
68f1ff3e69 hicli/sync: fix calculating room name if member event is not found 2024-10-15 01:21:13 +03:00
Tulir Asokan
e2c6980988 hicli/send: add set typing method 2024-10-15 00:24:13 +03:00
Tulir Asokan
965008e846 bridgev2: add optional stop method for network connectors
Some checks are pending
Go / Lint (latest) (push) Waiting to run
Go / Build (old, libolm) (push) Waiting to run
Go / Build (latest, libolm) (push) Waiting to run
Go / Build (old, goolm) (push) Waiting to run
Go / Build (latest, goolm) (push) Waiting to run
2024-10-14 19:47:22 +03:00
Tulir Asokan
efc532bfb2 hicli/processEvent: save session request manually if decryption queue is not provided 2024-10-14 17:23:15 +03:00
Tulir Asokan
67b1c97f14 hicli/database: fix edit triggers
Some checks are pending
Go / Lint (latest) (push) Waiting to run
Go / Build (old, libolm) (push) Waiting to run
Go / Build (latest, libolm) (push) Waiting to run
Go / Build (old, goolm) (push) Waiting to run
Go / Build (latest, goolm) (push) Waiting to run
2024-10-14 01:54:29 +03:00
Tulir Asokan
6f1c516baa hicli/database: ignore edits to edits and annotations 2024-10-14 00:49:45 +03:00
Tulir Asokan
8d9caf0d55 hicli/send: add support for sending replies 2024-10-13 22:33:25 +03:00
Tulir Asokan
5cccf93cdc hicli/sync: cache push rules in memory
Some checks are pending
Go / Lint (latest) (push) Waiting to run
Go / Build (old, libolm) (push) Waiting to run
Go / Build (latest, libolm) (push) Waiting to run
Go / Build (old, goolm) (push) Waiting to run
Go / Build (latest, goolm) (push) Waiting to run
2024-10-13 17:14:33 +03:00
Tulir Asokan
e48f081942 hicli/send: add mark read method 2024-10-13 17:14:18 +03:00
Tulir Asokan
226144ca9f hicli/sync: fix handling redactions to unknown events
Some checks are pending
Go / Lint (latest) (push) Waiting to run
Go / Build (old, libolm) (push) Waiting to run
Go / Build (latest, libolm) (push) Waiting to run
Go / Build (old, goolm) (push) Waiting to run
Go / Build (latest, goolm) (push) Waiting to run
2024-10-12 18:14:22 +03:00
Tulir Asokan
974fab0e0f hicli: use unix timestamps for events 2024-10-12 15:45:36 +03:00
Tulir Asokan
190760cd65 hicli: add support for sending markdown and rainbows 2024-10-12 14:52:21 +03:00
Tulir Asokan
8f6dec74c7 hicli/database: add more checks for edit triggers 2024-10-12 14:08:45 +03:00
Tulir Asokan
9e796dd66c hicli/json: handle ping commands
Some checks are pending
Go / Lint (latest) (push) Waiting to run
Go / Build (old, libolm) (push) Waiting to run
Go / Build (latest, libolm) (push) Waiting to run
Go / Build (old, goolm) (push) Waiting to run
Go / Build (latest, goolm) (push) Waiting to run
2024-10-12 12:14:19 +03:00
Tulir Asokan
99ce4618c6 hicli/sync: eagerly include related events in sync events 2024-10-12 11:52:14 +03:00
Tulir Asokan
8b3828c764 hicli/sync: remove reply fallback from stickers 2024-10-11 00:30:16 +03:00
Tulir Asokan
50f4a2eec1 client: omit from parameter in /messages if empty
Some checks failed
Go / Lint (latest) (push) Has been cancelled
Go / Build (old, libolm) (push) Has been cancelled
Go / Build (latest, libolm) (push) Has been cancelled
Go / Build (old, goolm) (push) Has been cancelled
Go / Build (latest, goolm) (push) Has been cancelled
2024-10-10 20:28:18 +03:00
Tulir Asokan
3d84843ec4 hicli/paginate: add special value for pagination complete 2024-10-10 20:28:18 +03:00
Tulir Asokan
5e4a7b56d8 hicli/sync: include empty list as timeline if there are no new events 2024-10-10 20:17:13 +03:00
Tulir Asokan
a7e8a2ce05 hicli/sync: update prev batch on limited timelines 2024-10-10 20:09:24 +03:00
Tulir Asokan
38610d681d bridgev2/backfill: don't try to backfill if client is logged out
Some checks are pending
Go / Lint (latest) (push) Waiting to run
Go / Build (old, libolm) (push) Waiting to run
Go / Build (latest, libolm) (push) Waiting to run
Go / Build (old, goolm) (push) Waiting to run
Go / Build (latest, goolm) (push) Waiting to run
2024-10-10 17:05:10 +03:00
Tulir Asokan
691c834144
bridgev2: add option to use deterministic ID for outgoing messages (#292) 2024-10-10 16:51:20 +03:00
Tulir Asokan
33834b1b2c hicli/send: ignore context cancellation inside goroutine
Some checks are pending
Go / Lint (latest) (push) Waiting to run
Go / Build (old, libolm) (push) Waiting to run
Go / Build (latest, libolm) (push) Waiting to run
Go / Build (old, goolm) (push) Waiting to run
Go / Build (latest, goolm) (push) Waiting to run
2024-10-09 21:23:23 +03:00
Tulir Asokan
29b0d9b95c crypto: move more cross-signing logs to trace level 2024-10-09 21:23:23 +03:00
Tulir Asokan
c068fd7bd7 hicli/database: use exslices for chunking
Some checks are pending
Go / Lint (latest) (push) Waiting to run
Go / Build (old, libolm) (push) Waiting to run
Go / Build (latest, libolm) (push) Waiting to run
Go / Build (old, goolm) (push) Waiting to run
Go / Build (latest, goolm) (push) Waiting to run
2024-10-09 02:02:53 +03:00
Tulir Asokan
463fc41125 hicli/database: add json tags for account data and receipts 2024-10-09 01:58:54 +03:00
Tulir Asokan
0e1ff4e10a hicli: don't dispatch event on empty sync 2024-10-09 01:28:14 +03:00
Tulir Asokan
cb3a7ce87a hicli: include state in sync and add method to get state 2024-10-09 01:22:13 +03:00
Tulir Asokan
38127b85b2 hicli: include next retry ts in cached errors 2024-10-08 23:50:39 +03:00
Tulir Asokan
1f7f489fa9 hicli: allow storing errors in media cache 2024-10-08 23:44:06 +03:00
Tulir Asokan
e192932af9 bridgev2/provisioning: add description of entire login flow 2024-10-08 19:43:33 +03:00
Tulir Asokan
1d8891fdb4 hicli: fix pagination bugs
Some checks are pending
Go / Lint (latest) (push) Waiting to run
Go / Build (old, libolm) (push) Waiting to run
Go / Build (latest, libolm) (push) Waiting to run
Go / Build (old, goolm) (push) Waiting to run
Go / Build (latest, goolm) (push) Waiting to run
2024-10-07 22:13:46 +03:00
Tulir Asokan
9dbd433363 bridgev2/backfill: do complete callback if messages are cut off 2024-10-07 19:49:00 +03:00
Tulir Asokan
7a9269e8ff bridgev2/networkinterface: add post-save callback for matrix messages
Some checks are pending
Go / Lint (latest) (push) Waiting to run
Go / Build (old, libolm) (push) Waiting to run
Go / Build (latest, libolm) (push) Waiting to run
Go / Build (old, goolm) (push) Waiting to run
Go / Build (latest, goolm) (push) Waiting to run
2024-10-07 16:12:43 +03:00
Tulir Asokan
092ba65cad bridgev2,event: add basic support for polls 2024-10-07 16:07:02 +03:00
Tulir Asokan
c4d8189d47 bridgev2/matrix: enable DM portal meta and list syncing by default 2024-10-07 14:09:56 +03:00
Tulir Asokan
cb361e1f59 pre-commit: add todo for staticcheck 2024-10-07 12:53:46 +03:00
Tulir Asokan
2621417bf0 client: don't let http close request body reader 2024-10-07 12:53:35 +03:00
Tulir Asokan
64692eb06e hicli: add method to get rooms by sort order
Some checks are pending
Go / Lint (latest) (push) Waiting to run
Go / Build (old, libolm) (push) Waiting to run
Go / Build (latest, libolm) (push) Waiting to run
Go / Build (old, goolm) (push) Waiting to run
Go / Build (latest, goolm) (push) Waiting to run
2024-10-06 21:17:05 +03:00
Tulir Asokan
014ea70762 hicli: add media cache entries when receiving events 2024-10-06 16:50:08 +03:00
Tulir Asokan
bb6aaf79a9 event: add helpers for getting caption and file name 2024-10-06 16:50:08 +03:00
Tulir Asokan
381c8780e0 hicli: add media cache table 2024-10-06 16:50:08 +03:00
Tulir Asokan
a284650568 hicli: scope EventsDecrypted event to single room 2024-10-06 14:50:07 +03:00
Tulir Asokan
e2329e8430 hicli: save event send error to database 2024-10-06 14:50:07 +03:00
Tulir Asokan
0e05a6b866 crypto: reduce logs when verifying cross-signign 2024-10-06 02:09:30 +03:00
Tulir Asokan
6e2a54d2b0 hicli: parse raw before removing reply fallback
Some checks are pending
Go / Lint (latest) (push) Waiting to run
Go / Build (old, libolm) (push) Waiting to run
Go / Build (latest, libolm) (push) Waiting to run
Go / Build (old, goolm) (push) Waiting to run
Go / Build (latest, goolm) (push) Waiting to run
2024-10-06 00:12:20 +03:00
Tulir Asokan
1e7196ed34 hicli: add helpers for using hicli over RPC 2024-10-05 23:52:43 +03:00
Tulir Asokan
144a995951 bridgev2/commands: add pm as alias to start-chat
Some checks failed
Go / Lint (latest) (push) Has been cancelled
Go / Build (old, libolm) (push) Has been cancelled
Go / Build (latest, libolm) (push) Has been cancelled
Go / Build (old, goolm) (push) Has been cancelled
Go / Build (latest, goolm) (push) Has been cancelled
2024-10-04 16:25:43 +03:00
Tulir Asokan
d48a5ca615 bridgev2/portal: assume newly created rooms are unmuted and untagged 2024-10-04 14:41:10 +03:00
Tulir Asokan
7e041c6e76 dependencies: update
Some checks are pending
Go / Lint (latest) (push) Waiting to run
Go / Build (old, libolm) (push) Waiting to run
Go / Build (latest, libolm) (push) Waiting to run
Go / Build (old, goolm) (push) Waiting to run
Go / Build (latest, goolm) (push) Waiting to run
2024-10-03 12:33:00 +03:00
Tulir Asokan
b9fdcd0dce bridgev2/config: add support for deprecated integer permissions
Some checks are pending
Go / Lint (latest) (push) Waiting to run
Go / Build (old, libolm) (push) Waiting to run
Go / Build (latest, libolm) (push) Waiting to run
Go / Build (old, goolm) (push) Waiting to run
Go / Build (latest, goolm) (push) Waiting to run
2024-10-03 00:36:55 +03:00
Tulir Asokan
eea038ea6b bridgev2/config: mark async_events as unsafe 2024-10-02 23:01:41 +03:00
Tulir Asokan
3cda734447 bridgev2/portal: don't handle unsaved pending message as upsert 2024-10-02 22:41:48 +03:00
Tulir Asokan
260f642bf0 bridgev2/legacymigrate: fix extra parameters
Some checks are pending
Go / Lint (latest) (push) Waiting to run
Go / Build (old, libolm) (push) Waiting to run
Go / Build (latest, libolm) (push) Waiting to run
Go / Build (old, goolm) (push) Waiting to run
Go / Build (latest, goolm) (push) Waiting to run
2024-10-01 20:21:03 +03:00
Sumner Evans
c27b62aa24
bridgev2/provisioning: add request ID middleware
Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2024-10-01 10:41:51 -06:00
Tulir Asokan
e0961922e5 bridgev2/legacymigrate: fix error message for other db upgrade 2024-10-01 17:52:28 +03:00
Tulir Asokan
31a68cbcea bridgev2/portal: don't try to send disappearing notice before room is created 2024-10-01 17:27:29 +03:00
Tulir Asokan
c259682a7c bridgev2/backfill: catch panics in backfill queue 2024-10-01 16:55:37 +03:00
Tulir Asokan
37af19a01a bridgev2/portal: allow remote events to have post handlers
Some checks are pending
Go / Lint (latest) (push) Waiting to run
Go / Build (old, libolm) (push) Waiting to run
Go / Build (latest, libolm) (push) Waiting to run
Go / Build (old, goolm) (push) Waiting to run
Go / Build (latest, goolm) (push) Waiting to run
2024-10-01 13:52:37 +03:00
Tulir Asokan
1b7a78b811 bridgev2/commands: url-decode cookie parameters 2024-09-30 21:28:34 +03:00
Tulir Asokan
741b4e823f bridgev2/commands: set missing fields in sudo/doin
Some checks are pending
Go / Lint (latest) (push) Waiting to run
Go / Build (old, libolm) (push) Waiting to run
Go / Build (latest, libolm) (push) Waiting to run
Go / Build (old, goolm) (push) Waiting to run
Go / Build (latest, goolm) (push) Waiting to run
2024-09-30 17:21:22 +03:00
Tulir Asokan
ed074ba657 changelog: update 2024-09-30 17:18:26 +03:00
Tulir Asokan
919834e6bf bridgev2/provisioning: add simpler auth for pprof endpoints 2024-09-30 16:16:05 +03:00
Tulir Asokan
59251f83de bridgev2/example-config: be more explicit about securing the appservice address 2024-09-30 12:41:43 +03:00
Tulir Asokan
3b878b4bcd bridgev2/commands: change how log context is applied 2024-09-30 12:05:58 +03:00
Tulir Asokan
c8d19e8e18 bridgev2/commands: add sudo and doin 2024-09-30 11:11:26 +03:00
Tulir Asokan
0cf0b48a96 bridgev2/provisioning: use RespError.Write in RespondWithError 2024-09-30 11:11:26 +03:00
Tulir Asokan
cc179f8ff7 appservice: remove TLS support
Some checks are pending
Go / Lint (latest) (push) Waiting to run
Go / Build (old, libolm) (push) Waiting to run
Go / Build (latest, libolm) (push) Waiting to run
Go / Build (old, goolm) (push) Waiting to run
Go / Build (latest, goolm) (push) Waiting to run
2024-09-30 11:11:26 +03:00
Tulir Asokan
f48a66c31c ci: change cron schedule
Some checks are pending
Go / Lint (latest) (push) Waiting to run
Go / Build (old, libolm) (push) Waiting to run
Go / Build (latest, libolm) (push) Waiting to run
Go / Build (old, goolm) (push) Waiting to run
Go / Build (latest, goolm) (push) Waiting to run
2024-09-28 18:18:29 +03:00
Tulir Asokan
cf80de9f1a bridgev2: don't include weeks in disappearing timer notices
Some checks failed
Go / Lint (latest) (push) Has been cancelled
Go / Build (old, libolm) (push) Has been cancelled
Go / Build (latest, libolm) (push) Has been cancelled
Go / Build (old, goolm) (push) Has been cancelled
Go / Build (latest, goolm) (push) Has been cancelled
2024-09-27 20:45:47 +03:00
Tulir Asokan
d1e5b09d97 bridgev2: add UserLogin.TrackAnalytics shortcut 2024-09-27 14:36:33 +03:00
Tulir Asokan
7a5f15b03c bridgev2/networkinterface: add DeleteOnlyForMe field to message remove events
Some checks failed
Go / Lint (latest) (push) Has been cancelled
Go / Build (old, libolm) (push) Has been cancelled
Go / Build (latest, libolm) (push) Has been cancelled
Go / Build (old, goolm) (push) Has been cancelled
Go / Build (latest, goolm) (push) Has been cancelled
2024-09-26 12:16:54 +03:00
Tulir Asokan
5d916e0e9a bridgev2/queue: add shortcut for QueueRemoteEvent 2024-09-26 12:12:02 +03:00
Tulir Asokan
edae08383b event: add Has method for Mentions
Some checks are pending
Go / Lint (latest) (push) Waiting to run
Go / Build (old, libolm) (push) Waiting to run
Go / Build (latest, libolm) (push) Waiting to run
Go / Build (old, goolm) (push) Waiting to run
Go / Build (latest, goolm) (push) Waiting to run
2024-09-26 00:23:05 +03:00
Tulir Asokan
4e180ee36b format/mdext: add indented paragraph fixer
Some checks are pending
Go / Lint (latest) (push) Waiting to run
Go / Build (old, libolm) (push) Waiting to run
Go / Build (latest, libolm) (push) Waiting to run
Go / Build (old, goolm) (push) Waiting to run
Go / Build (latest, goolm) (push) Waiting to run
2024-09-25 16:45:09 +03:00
Tulir Asokan
0c7f701828 bridgev2/portal: add function for formatting disappearing timer change 2024-09-25 16:14:09 +03:00
Tulir Asokan
9a14949d9a bridgev2/matrixinterface: add method for getting URL preview 2024-09-25 14:54:08 +03:00
Tulir Asokan
b3452db038 ci: reduce issue lock interval
[skip ci]
2024-09-25 00:14:49 +03:00
Tulir Asokan
dff2edec78 bridgev2/legacymigrate: clear version table to be safe
Fixes mautrix/signal#557
2024-09-24 22:14:50 +03:00
Tulir Asokan
cb64bbcff4 ci: lock closed issues automatically after 90 days
[skip ci]
2024-09-24 20:44:55 +03:00
Tulir Asokan
a834fa8431 portal: plumb stream order from send response to MSS event
Some checks are pending
Go / Lint (latest) (push) Waiting to run
Go / Build (old, libolm) (push) Waiting to run
Go / Build (latest, libolm) (push) Waiting to run
Go / Build (old, goolm) (push) Waiting to run
Go / Build (latest, goolm) (push) Waiting to run
2024-09-24 19:26:06 +03:00
Tulir Asokan
5a8f566c3c bridgev2/portal: log when thread or reply message is not found 2024-09-24 13:55:36 +03:00
Tulir Asokan
7324f6edec bridgev2/matrix: handle token errors in /versions properly
Some checks failed
Go / Lint (latest) (push) Has been cancelled
Go / Build (old, libolm) (push) Has been cancelled
Go / Build (latest, libolm) (push) Has been cancelled
Go / Build (old, goolm) (push) Has been cancelled
Go / Build (latest, goolm) (push) Has been cancelled
Fixes mautrix/signal#554
2024-09-21 17:53:00 +03:00
Tulir Asokan
a95101ea7f bridgev2/backfill: add optional done callback to fetch response
Some checks failed
Go / Lint (latest) (push) Has been cancelled
Go / Build (old, libolm) (push) Has been cancelled
Go / Build (latest, libolm) (push) Has been cancelled
Go / Build (old, goolm) (push) Has been cancelled
Go / Build (latest, goolm) (push) Has been cancelled
2024-09-17 16:58:25 +03:00
Tulir Asokan
830136b49d crypto: avoid data race in HandleOTKCounts
Some checks are pending
Go / Lint (latest) (push) Waiting to run
Go / Build (old, libolm) (push) Waiting to run
Go / Build (latest, libolm) (push) Waiting to run
Go / Build (old, goolm) (push) Waiting to run
Go / Build (latest, goolm) (push) Waiting to run
2024-09-16 17:15:19 +03:00
Tulir Asokan
1e3493188f Bump version to v0.21.0
Some checks are pending
Go / Lint (latest) (push) Waiting to run
Go / Build (old, libolm) (push) Waiting to run
Go / Build (latest, libolm) (push) Waiting to run
Go / Build (old, goolm) (push) Waiting to run
Go / Build (latest, goolm) (push) Waiting to run
2024-09-16 13:48:59 +03:00
Tulir Asokan
b5602fd4fe appservice: increase OTK count channel size
Some checks failed
Go / Lint (latest) (push) Has been cancelled
Go / Build (old, libolm) (push) Has been cancelled
Go / Build (latest, libolm) (push) Has been cancelled
Go / Build (old, goolm) (push) Has been cancelled
Go / Build (latest, goolm) (push) Has been cancelled
2024-09-15 01:20:50 +03:00
Tulir Asokan
6f9927c399 crypto: make OTK count for other user log less noisy 2024-09-15 01:12:21 +03:00
Tulir Asokan
d86913bd5c bridgev2/legacymigrate: drop *_mxid_unique constraints before migration
Some checks are pending
Go / Lint (latest) (push) Waiting to run
Go / Build (old, libolm) (push) Waiting to run
Go / Build (latest, libolm) (push) Waiting to run
Go / Build (old, goolm) (push) Waiting to run
Go / Build (latest, goolm) (push) Waiting to run
2024-09-14 14:32:11 +03:00
Tulir Asokan
cb4204ceb5 bridgev2/networkid: update receiver docs 2024-09-14 12:54:05 +03:00
Tulir Asokan
d89dac594d bridgev2: automatically update old portals when enabling split portals 2024-09-14 12:45:16 +03:00
Tulir Asokan
a5c4446a22 bridgev2: add option to split all portals by user login 2024-09-13 23:36:47 +03:00
Tulir Asokan
ff4126b5d0 bridgev2/database: delete duplicate mxids in migration
Some checks are pending
Go / Lint (latest) (push) Waiting to run
Go / Build (old, libolm) (push) Waiting to run
Go / Build (latest, libolm) (push) Waiting to run
Go / Build (old, goolm) (push) Waiting to run
Go / Build (latest, goolm) (push) Waiting to run
2024-09-13 21:20:28 +03:00
Tulir Asokan
41213c6230 bridgev2/matrix: add separators to data when generating deterministic event ids 2024-09-13 21:16:25 +03:00
Tulir Asokan
c2d0f4cf5d ci: don't allow go to update itself 2024-09-13 13:27:37 +03:00
Tulir Asokan
e12ecbe82d bridgev2/matrix: allow key sharing for bridge admins
Some checks are pending
Go / Lint (latest) (push) Waiting to run
Go / Build (old, libolm) (push) Waiting to run
Go / Build (latest, libolm) (push) Waiting to run
Go / Build (old, goolm) (push) Waiting to run
Go / Build (latest, goolm) (push) Waiting to run
2024-09-13 12:55:32 +03:00
Tulir Asokan
96e68fb485 dependencies: update 2024-09-13 12:16:47 +03:00
Tulir Asokan
e51e36ac99 client: drop support for unauthenticated media
Some checks are pending
Go / Lint (latest) (push) Waiting to run
Go / Build (old, libolm) (push) Waiting to run
Go / Build (latest, libolm) (push) Waiting to run
Go / Build (old, goolm) (push) Waiting to run
Go / Build (latest, goolm) (push) Waiting to run
2024-09-13 01:55:02 +03:00
Tulir Asokan
65364d0133 error: add MUnknown 2024-09-13 01:38:18 +03:00
Tulir Asokan
36ef69bf7f error: remove pointer receiver from Write
Otherwise it can't be chained after WithMessage
2024-09-13 01:36:38 +03:00
Tulir Asokan
012aed97e3 error: add WithMessage and Write helpers 2024-09-13 01:33:34 +03:00
Tulir Asokan
e16b681c10 crypto/helper: allow overriding post-decrypt function 2024-09-13 00:46:39 +03:00
Scott Weber
d472be3412
event: add BeeperHSSuborder to unsigned (#287)
Some checks are pending
Go / Lint (latest) (push) Waiting to run
Go / Build (old, libolm) (push) Waiting to run
Go / Build (latest, libolm) (push) Waiting to run
Go / Build (old, goolm) (push) Waiting to run
Go / Build (latest, goolm) (push) Waiting to run
2024-09-12 11:09:02 -04:00
Tulir Asokan
08d58d4d2a bridgev2/analytics: rename method 2024-09-12 17:08:16 +03:00
Tulir Asokan
bc22852f06 bridgev2: add analytics sending method 2024-09-12 16:53:50 +03:00
Tulir Asokan
0328ed1c9f bridgev2/backfill: add log before deduplicating messages 2024-09-12 15:59:01 +03:00
Tulir Asokan
6b4ff8b60e bridgev2/portal: fix event handlin panic log message 2024-09-12 14:23:17 +03:00
Tulir Asokan
c62757ab15 client: add wrappers for event and room reporting endpoints
Some checks are pending
Go / Lint (latest) (push) Waiting to run
Go / Build (old, libolm) (push) Waiting to run
Go / Build (latest, libolm) (push) Waiting to run
Go / Build (old, goolm) (push) Waiting to run
Go / Build (latest, goolm) (push) Waiting to run
2024-09-12 02:33:28 +03:00
Tulir Asokan
1d428be6d9 crypto/helper: allow using LoginAs with unmanaged crypto store 2024-09-12 02:19:50 +03:00
Tulir Asokan
288a94ec14 crypto/sqlstore: allow initializing manually 2024-09-11 23:59:16 +03:00
Tulir Asokan
2bf53fce92 crypto/helper: don't require setting ASEventProcessor 2024-09-11 23:59:02 +03:00
Tulir Asokan
328bab41a3 bridgev2/portal: run portal create background tasks without context cancellation
Some checks are pending
Go / Lint (latest) (push) Waiting to run
Go / Build (old, libolm) (push) Waiting to run
Go / Build (latest, libolm) (push) Waiting to run
Go / Build (old, goolm) (push) Waiting to run
Go / Build (latest, goolm) (push) Waiting to run
2024-09-11 17:07:23 +03:00
Tulir Asokan
0088368066 bridgev2/provisioning: export responding with wrapped errors
Some checks are pending
Go / Lint (latest) (push) Waiting to run
Go / Build (old, libolm) (push) Waiting to run
Go / Build (latest, libolm) (push) Waiting to run
Go / Build (old, goolm) (push) Waiting to run
Go / Build (latest, goolm) (push) Waiting to run
2024-09-11 13:53:42 +03:00
Tulir Asokan
a95cb9adb3 bridgev2/matrixinterface: use callback for DownloadMediaToFile
The caller doesn't know whether the file should be removed, so let the
Matrix interface deal with it.
2024-09-11 12:46:21 +03:00
Tulir Asokan
c9a9cb6957 bridgev2/matrixinterface: add download to file method
Some checks are pending
Go / Lint (latest) (push) Waiting to run
Go / Build (old, libolm) (push) Waiting to run
Go / Build (latest, libolm) (push) Waiting to run
Go / Build (old, goolm) (push) Waiting to run
Go / Build (latest, goolm) (push) Waiting to run
2024-09-11 02:07:39 +03:00
Tulir Asokan
ffdb1d575e bridgev2/portal: maybe fix check for adding log context
Some checks are pending
Go / Lint (latest) (push) Waiting to run
Go / Build (old, libolm) (push) Waiting to run
Go / Build (latest, libolm) (push) Waiting to run
Go / Build (old, goolm) (push) Waiting to run
Go / Build (latest, goolm) (push) Waiting to run
2024-09-10 14:29:32 +03:00
Tulir Asokan
cbc307b311 bridgev2/database: add unique constraint on message mxids 2024-09-10 14:29:32 +03:00
Toni Spets
4fd082aba9
event: add Beeper backup flag to unsigned (#284)
Some checks are pending
Go / Lint (latest) (push) Waiting to run
Go / Build (old, libolm) (push) Waiting to run
Go / Build (latest, libolm) (push) Waiting to run
Go / Build (old, goolm) (push) Waiting to run
Go / Build (latest, goolm) (push) Waiting to run
2024-09-09 16:56:40 +03:00
Tulir Asokan
4098a3726e event: ensure MSC1767 audio has empty waveform
Some checks are pending
Go / Lint (latest) (push) Waiting to run
Go / Build (old, libolm) (push) Waiting to run
Go / Build (latest, libolm) (push) Waiting to run
Go / Build (old, goolm) (push) Waiting to run
Go / Build (latest, goolm) (push) Waiting to run
2024-09-08 13:47:49 +03:00
Tulir Asokan
6b055b1475 bridgev2: include portal receiver in m.bridge events
Some checks failed
Go / Lint (latest) (push) Has been cancelled
Go / Build (old, libolm) (push) Has been cancelled
Go / Build (latest, libolm) (push) Has been cancelled
Go / Build (old, goolm) (push) Has been cancelled
Go / Build (latest, goolm) (push) Has been cancelled
2024-09-06 17:51:30 +03:00
Toni Spets
33d724bf4c
event: add encrypted file info for m.room.avatar (#283)
Some checks are pending
Go / Lint (latest) (push) Waiting to run
Go / Build (old, libolm) (push) Waiting to run
Go / Build (latest, libolm) (push) Waiting to run
Go / Build (old, goolm) (push) Waiting to run
Go / Build (latest, goolm) (push) Waiting to run
2024-09-06 13:54:54 +03:00
Tulir Asokan
059d9a36e5 bridgev2: fix issues in event loop debug logger
Some checks are pending
Go / Lint (latest) (push) Waiting to run
Go / Build (old, libolm) (push) Waiting to run
Go / Build (latest, libolm) (push) Waiting to run
Go / Build (old, goolm) (push) Waiting to run
Go / Build (latest, goolm) (push) Waiting to run
2024-09-06 00:15:28 +03:00
Tulir Asokan
e750881a4a bridgev2/backfill: respect DontBridge flag
Some checks are pending
Go / Lint (latest) (push) Waiting to run
Go / Build (old, libolm) (push) Waiting to run
Go / Build (latest, libolm) (push) Waiting to run
Go / Build (old, goolm) (push) Waiting to run
Go / Build (latest, goolm) (push) Waiting to run
2024-09-05 00:41:12 +03:00
Tulir Asokan
6c8519d39e bridgev2: add timeouts for event handling 2024-09-04 23:49:44 +03:00
Tulir Asokan
e5ea10d64c bridgev2: allow adding pending message before returning from handler 2024-09-04 22:28:52 +03:00
Tulir Asokan
a62bdb6250 dependencies: update
Some checks are pending
Go / Lint (latest) (push) Waiting to run
Go / Build (old, libolm) (push) Waiting to run
Go / Build (latest, libolm) (push) Waiting to run
Go / Build (old, goolm) (push) Waiting to run
Go / Build (latest, goolm) (push) Waiting to run
2024-09-04 20:36:41 +03:00
Nick Mills-Barrett
da2780bcbe
Use a warning log when request context canceled (#279)
Some checks are pending
Go / Lint (latest) (push) Waiting to run
Go / Build (old, libolm) (push) Waiting to run
Go / Build (latest, libolm) (push) Waiting to run
Go / Build (old, goolm) (push) Waiting to run
Go / Build (latest, goolm) (push) Waiting to run
2024-09-03 13:54:10 +01:00
Nick Mills-Barrett
ab110b4425
Add refresh token field to login request (#278) 2024-09-03 13:54:05 +01:00
Sumner Evans
a17dc5867e
bridgev2/provisioning: allow custom user ID retrieval
Some checks are pending
Go / Lint (latest) (push) Waiting to run
Go / Build (old, libolm) (push) Waiting to run
Go / Build (latest, libolm) (push) Waiting to run
Go / Build (old, goolm) (push) Waiting to run
Go / Build (latest, goolm) (push) Waiting to run
Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2024-09-02 16:53:57 -06:00
Tulir Asokan
4e8156519e federation: limit .well-known file size 2024-09-03 01:49:06 +03:00
Tulir Asokan
55770a4a15 bridgev2/provisioning: check user permissions 2024-09-03 01:48:53 +03:00
Tulir Asokan
db8f2433a1 statestore: mass insert members on refetch 2024-09-02 23:49:06 +03:00
Tulir Asokan
0c14ad0f0c event: add unban mod policy recommendation 2024-09-02 23:17:30 +03:00
Tulir Asokan
6f1a3878c4 dependencies: update
Some checks are pending
Go / Lint (latest) (push) Waiting to run
Go / Build (old, libolm) (push) Waiting to run
Go / Build (latest, libolm) (push) Waiting to run
Go / Build (old, goolm) (push) Waiting to run
Go / Build (latest, goolm) (push) Waiting to run
2024-09-02 12:23:46 +03:00
Tulir Asokan
fd15ca4a68 bridgev2/matrix: add missing return in UploadMediaStream 2024-09-02 12:02:14 +03:00
Tulir Asokan
a0d427e4df crypto: add hack to avoid logging about OTK counts for cross-signing keys
Some checks are pending
Go / Lint (latest) (push) Waiting to run
Go / Build (old, libolm) (push) Waiting to run
Go / Build (latest, libolm) (push) Waiting to run
Go / Build (old, goolm) (push) Waiting to run
Go / Build (latest, goolm) (push) Waiting to run
2024-09-02 01:20:22 +03:00
Tulir Asokan
e7bc21d463 client: add support for feature flag for authenticated media
Some checks are pending
Go / Lint (latest) (push) Waiting to run
Go / Build (old, libolm) (push) Waiting to run
Go / Build (latest, libolm) (push) Waiting to run
Go / Build (old, goolm) (push) Waiting to run
Go / Build (latest, goolm) (push) Waiting to run
2024-09-01 12:54:13 +03:00
Tulir Asokan
79391515ed event: add unstable prefixes for policy events
Some checks failed
Go / Lint (latest) (push) Has been cancelled
Go / Build (old, libolm) (push) Has been cancelled
Go / Build (latest, libolm) (push) Has been cancelled
Go / Build (old, goolm) (push) Has been cancelled
Go / Build (latest, goolm) (push) Has been cancelled
2024-08-31 02:41:28 +03:00
Tulir Asokan
87ca6a9ba2 bridgev2/portal: add event ID to handle matrix event log 2024-08-30 21:00:38 +03:00
Tulir Asokan
7a86cb26ff pushrules: fix glob matching
Some checks are pending
Go / Lint (latest) (push) Waiting to run
Go / Build (old, libolm) (push) Waiting to run
Go / Build (latest, libolm) (push) Waiting to run
Go / Build (old, goolm) (push) Waiting to run
Go / Build (latest, goolm) (push) Waiting to run
2024-08-30 18:10:22 +03:00
Tulir Asokan
67703cf96f pushrules: use glob package from util 2024-08-30 17:27:12 +03:00
Tulir Asokan
f46572f058 event: add type for policy rule recommendations 2024-08-30 17:27:12 +03:00
Tulir Asokan
fe20235578 bridgev2/backfill: remove www. prefix in deterministic event IDs 2024-08-30 17:27:12 +03:00
Tulir Asokan
238cacf2d5 client,crypto,appservice: add MSC3202 features 2024-08-30 17:27:12 +03:00
Sumner Evans
5f49ca683a
bridgestate: deduplicate on remote name
Some checks failed
Go / Lint (latest) (push) Has been cancelled
Go / Build (old, libolm) (push) Has been cancelled
Go / Build (latest, libolm) (push) Has been cancelled
Go / Build (old, goolm) (push) Has been cancelled
Go / Build (latest, goolm) (push) Has been cancelled
Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2024-08-28 12:14:22 -06:00
Tulir Asokan
fd89457be8 bridgev2/backfill: add option for aggressive deduplication
Some checks are pending
Go / Lint (latest) (push) Waiting to run
Go / Build (old, libolm) (push) Waiting to run
Go / Build (latest, libolm) (push) Waiting to run
Go / Build (old, goolm) (push) Waiting to run
Go / Build (latest, goolm) (push) Waiting to run
2024-08-28 16:18:27 +03:00
Tulir Asokan
838237da73 bridgev2/provisioning: add search users endpoint 2024-08-28 13:41:21 +03:00
Tulir Asokan
a224ed019d bridgev2: stop using GetCachedUserLogins 2024-08-28 13:41:21 +03:00
Tulir Asokan
eef37c3295 client: add option to log syncs at trace level 2024-08-28 13:41:21 +03:00
Sumner Evans
892e5cf01f
bridgev2/provisioning: allow custom auth token retrieval
Some checks are pending
Go / Lint (latest) (push) Waiting to run
Go / Build (old, libolm) (push) Waiting to run
Go / Build (latest, libolm) (push) Waiting to run
Go / Build (old, goolm) (push) Waiting to run
Go / Build (latest, goolm) (push) Waiting to run
Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2024-08-27 16:12:52 -06:00
Tulir Asokan
f56905a276 bridgev2/portal: fix panic in FindPreferredLogin if receiver login doesn't exist 2024-08-27 22:10:23 +03:00
Tulir Asokan
e3eb2953dd bridgev2/legacymigrate: fix version table values
Some checks are pending
Go / Lint (latest) (push) Waiting to run
Go / Build (old, libolm) (push) Waiting to run
Go / Build (latest, libolm) (push) Waiting to run
Go / Build (old, goolm) (push) Waiting to run
Go / Build (latest, goolm) (push) Waiting to run
2024-08-27 16:49:50 +03:00
Tulir Asokan
09e9cac5e8 bridgev2/commands: reply with login URL when doing cookie login 2024-08-27 16:13:51 +03:00
Tulir Asokan
7276f10fcf bridgev2/legacymigrate: clear database owner on upgrade 2024-08-27 14:54:26 +03:00
Tulir Asokan
3eeca239f1 bridgev2/legacymigrate: add txlock when migrating python sqlite db config 2024-08-27 14:51:37 +03:00
Tulir Asokan
ae306b3efa bridgev2/legacymigrate: add support for python configs 2024-08-27 14:50:19 +03:00
Tulir Asokan
ad32a3e60c bridgev2/legacymigrate: upgrade version table when migrating 2024-08-27 14:29:03 +03:00
Tulir Asokan
dfc92ee926 bridgev2/legacymigrate: add support for running another upgrader
Some checks are pending
Go / Lint (latest) (push) Waiting to run
Go / Build (old, libolm) (push) Waiting to run
Go / Build (latest, libolm) (push) Waiting to run
Go / Build (old, goolm) (push) Waiting to run
Go / Build (latest, goolm) (push) Waiting to run
2024-08-26 21:57:07 +03:00
Tulir Asokan
720648ffdf id: don't panic if URI methods are called with empty/nil values 2024-08-26 17:58:27 +03:00
Sumner Evans
7e7cb57ee7
bridgev2/commands: fix NPE on search
Some checks are pending
Go / Lint (latest) (push) Waiting to run
Go / Build (old, libolm) (push) Waiting to run
Go / Build (latest, libolm) (push) Waiting to run
Go / Build (old, goolm) (push) Waiting to run
Go / Build (latest, goolm) (push) Waiting to run
Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2024-08-26 08:54:15 -06:00
Tulir Asokan
b68fdc9057 bridgev2: fix username templates
Some checks are pending
Go / Lint (latest) (push) Waiting to run
Go / Build (old, libolm) (push) Waiting to run
Go / Build (latest, libolm) (push) Waiting to run
Go / Build (old, goolm) (push) Waiting to run
Go / Build (latest, goolm) (push) Waiting to run
2024-08-25 22:21:42 +03:00
Brad Murray
a7aa97679d
filter: Add com.beeper.to_device sync filter field (#274)
* Add com.beeper.to_device sync filter field

* Update filter.go

Co-authored-by: Tulir Asokan <tulir@maunium.net>

---------

Co-authored-by: Tulir Asokan <tulir@maunium.net>
2024-08-25 13:45:54 -04:00
Tulir Asokan
649a637350 bridgev2/simplevent: add transaction ID field to message event
Some checks are pending
Go / Lint (latest) (push) Waiting to run
Go / Build (old, libolm) (push) Waiting to run
Go / Build (latest, libolm) (push) Waiting to run
Go / Build (old, goolm) (push) Waiting to run
Go / Build (latest, goolm) (push) Waiting to run
2024-08-25 02:17:14 +03:00
Tulir Asokan
dae53d42c7 bridgev2: add standard error for unsupported media types
Some checks are pending
Go / Lint (latest) (push) Waiting to run
Go / Build (old, libolm) (push) Waiting to run
Go / Build (latest, libolm) (push) Waiting to run
Go / Build (old, goolm) (push) Waiting to run
Go / Build (latest, goolm) (push) Waiting to run
2024-08-25 00:54:19 +03:00
Sumner Evans
66c417825b
crypto/olm: add tests comparing libolm and goolm, replace crypto/ed25519 -> maunium.net/go/mautrix/crypto/ed25519
Some checks are pending
Go / Lint (latest) (push) Waiting to run
Go / Build (old, libolm) (push) Waiting to run
Go / Build (latest, libolm) (push) Waiting to run
Go / Build (old, goolm) (push) Waiting to run
Go / Build (latest, goolm) (push) Waiting to run
The following tests were added that compare libolm and goolm against
each other.

* account: add (un)pickle tests
* groupsession: add test for (en|de)cryption for group sessions
* account: test IdentityKeysJSON and OneTimeKeys
* session: add test for encrypt/decrypt
* session: add test for private key format
* outboundsession: add differential fuzz test for encryption

Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2024-08-23 09:16:58 -06:00
Sumner Evans
213b6ec80d
crypto/olm: make everything into an interface
This commit turns all of the crypto objects that are provided by olm
into interfaces so that multiple implementations (libolm and goolm right
now) can implement it.

As part of this refactor, the libolm code has been moved to a separate
package (goolm was already in its own package). Both packages now
implement structs which implement the various interfaces.

Additional changes:

* goolm/goolmbase64: split into separate package (needed to avoid import
  loops)
* olm/errors: unified all errors under the olm package
* ci: remove libolm before building with goolm flag (this allows us to
  use ./... to build all of the packages under goolm)

Signed-off-by: Sumner Evans <sumner.evans@automattic.com>

Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2024-08-23 09:16:33 -06:00
Sumner Evans
7fa9e14a88
crypto/ed25519: reimplementation with libolm-compatible byte layout
This PR adds a maunium.net/go/mautrix/crypto/ed25519 package to
mautrix-go which is a mirror of the crypto/ed25519 package for
generating Ed25519 signatures, but which uses a different private key
format.

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 as a 64-byte value that
results from the SHA512 of the seed.

This is the format used by libolm, and is required for pickle/unpickle
to work properly.

Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2024-08-23 08:53:01 -06:00
Sumner Evans
8ead76c67b
provisioning: fix return value from doResolveIdentifer
Some checks are pending
Go / Lint (latest) (push) Waiting to run
Go / Build (old, libolm) (push) Waiting to run
Go / Build (latest, libolm) (push) Waiting to run
Go / Build (old, goolm) (push) Waiting to run
Go / Build (latest, goolm) (push) Waiting to run
Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2024-08-22 13:07:56 -06:00
Tulir Asokan
afc796861a bridgev2/commands: fix error reply on invalid login flow 2024-08-22 17:58:34 +03:00
Tulir Asokan
675d176b46 bridgev2/user: rename GetCachedUserLogins to GetUserLogins
Some checks failed
Go / Lint (latest) (push) Has been cancelled
Go / Build (old, libolm) (push) Has been cancelled
Go / Build (latest, libolm) (push) Has been cancelled
Go / Build (old, goolm) (push) Has been cancelled
Go / Build (latest, goolm) (push) Has been cancelled
Fixes #271
2024-08-21 22:40:48 +03:00
Tulir Asokan
b09d3a99cb bridgev2/portal: replace member list with map 2024-08-21 22:40:48 +03:00
Toni Spets
ad20a9218f
Beeper extension for client side media dedup (#267)
The client can send a unique id (like a hash) of a file it intends to
upload in the create request. If the server already has the file it will
return a "completed" response.

The unique id is meant to be opaque for the server. For privacy reasons
it is recommended not to use the raw hash of a file.  The returned MXC
should be stable for the same unique id for the same user but is not
guaranteed.

The room id is used to tie the lifecycle of created media to an existing
room on the homeserver. If a room is purged from the homeserver the
media will be purged along with it.

If the file has been created but not uploaded the response will not have
a completed timestamp which allows the client to retry sending the file.
If the upload has already been completed the upload URL will be empty.

It is possible for multiple clients to send a create request
simultaneously with the same unique id and upload the file at the same
time. It is also possible for the server to forget the unique id and
allow reuploading the same file again returning a new MXC.

This commit also fixes UnusedExpiresAt type in the response which is a
breaking change.
2024-08-21 19:19:03 +03:00
Tulir Asokan
8ab31c8c46 bridgev2/userlogin: fix potential panic when kicking user from portals 2024-08-21 16:46:12 +03:00
Tulir Asokan
edef968c64 event: remove omitemptys from MSC1767Audio
Some checks are pending
Go / Lint (latest) (push) Waiting to run
Go / Build (old, libolm) (push) Waiting to run
Go / Build (latest, libolm) (push) Waiting to run
Go / Build (old, goolm) (push) Waiting to run
Go / Build (latest, goolm) (push) Waiting to run
2024-08-21 10:46:35 +03:00
Tulir Asokan
851a9485e7 bridgev2/matrix: fix replacement files in UploadMediaStream 2024-08-21 10:45:05 +03:00
Tulir Asokan
f99fb60f13 bridgev2/matrixinterface: move upload stream file name/mime into callback return values
Some checks are pending
Go / Lint (latest) (push) Waiting to run
Go / Build (old, libolm) (push) Waiting to run
Go / Build (latest, libolm) (push) Waiting to run
Go / Build (old, goolm) (push) Waiting to run
Go / Build (latest, goolm) (push) Waiting to run
2024-08-21 01:10:55 +03:00
Tulir Asokan
591ac60f0c bridgev2/portal: only forward backfill after room creation if enabled in config
Some checks are pending
Go / Lint (latest) (push) Waiting to run
Go / Build (old, libolm) (push) Waiting to run
Go / Build (latest, libolm) (push) Waiting to run
Go / Build (old, goolm) (push) Waiting to run
Go / Build (latest, goolm) (push) Waiting to run
2024-08-20 17:37:21 +03:00
Tulir Asokan
4b7fa711ce changelog: update 2024-08-20 16:29:41 +03:00
Tulir Asokan
9e1a8cd56e bridgev2/matrix: use cached member list if available 2024-08-20 16:21:41 +03:00
Tulir Asokan
063d374226 bridgev2/backfillqueue: don't try to backfill with non-logged-in client
Some checks are pending
Go / Lint (latest) (push) Waiting to run
Go / Build (old, libolm) (push) Waiting to run
Go / Build (latest, libolm) (push) Waiting to run
Go / Build (old, goolm) (push) Waiting to run
Go / Build (latest, goolm) (push) Waiting to run
2024-08-20 15:47:50 +03:00
Tulir Asokan
f4d4637625 bridgev2/portal: don't create backfill task before portal room 2024-08-20 13:56:52 +03:00
Tulir Asokan
4523807e56 bridgev2/portal: slightly refactor power level checks 2024-08-20 13:56:19 +03:00
Tulir Asokan
a614668174 bridgev2/matrix: remove custom buffer in stream upload
Some checks are pending
Go / Lint (latest) (push) Waiting to run
Go / Build (old, libolm) (push) Waiting to run
Go / Build (latest, libolm) (push) Waiting to run
Go / Build (old, goolm) (push) Waiting to run
Go / Build (latest, goolm) (push) Waiting to run
2024-08-20 01:20:39 +03:00
Tulir Asokan
feff2d5886 bridgev2/matrix: don't delete temp file before async upload completes 2024-08-20 01:00:10 +03:00
Tulir Asokan
38278ef37d bridgev2/unorganized-docs: update features 2024-08-20 00:53:07 +03:00
Tulir Asokan
ce2ffd8232
bridgev2/matrix: add new stream upload that uses a writer instead of a reader (#269) 2024-08-20 00:52:54 +03:00
Tulir Asokan
79527df26e bridgev2/matrixinterface: temporarily remove stream upload from interface
Some checks are pending
Go / Lint (latest) (push) Waiting to run
Go / Build (old, libolm) (push) Waiting to run
Go / Build (latest, libolm) (push) Waiting to run
Go / Build (old, goolm) (push) Waiting to run
Go / Build (latest, goolm) (push) Waiting to run
2024-08-19 20:13:38 +03:00
Tulir Asokan
20ce646435 bridgev2/matrixinterface: add stream upload method 2024-08-19 19:33:26 +03:00
Tulir Asokan
420db4fefb bridgev2/matrix: reduce upload semaphore size 2024-08-19 19:04:02 +03:00
Tulir Asokan
3481f29c1a bridgev2/matrix: disable megolm session destination tracking 2024-08-19 19:02:14 +03:00
Tulir Asokan
e217a5f8cd bridgev2/matrix: add missing semaphore release 2024-08-19 18:10:03 +03:00
Tulir Asokan
7f392b17b6 bridgev2/matrix: prevent too many async uploads at once 2024-08-19 17:52:54 +03:00
Brad Murray
b4927420cc
client: don't retry requests if context is cancelled (#268) 2024-08-19 16:23:25 +03:00
Tulir Asokan
f813c4f8b0 statestore: log warning if UpdateStateStore has unexpected content type
Some checks are pending
Go / Lint (latest) (push) Waiting to run
Go / Build (old, libolm) (push) Waiting to run
Go / Build (latest, libolm) (push) Waiting to run
Go / Build (old, goolm) (push) Waiting to run
Go / Build (latest, goolm) (push) Waiting to run
2024-08-19 11:29:06 +03:00
Tulir Asokan
6444f9bccc bridgev2/matrix: add double puppet value for redactions
Some checks failed
Go / Lint (latest) (push) Has been cancelled
Go / Build (old, libolm) (push) Has been cancelled
Go / Build (latest, libolm) (push) Has been cancelled
Go / Build (old, goolm) (push) Has been cancelled
Go / Build (latest, goolm) (push) Has been cancelled
2024-08-17 23:30:46 +03:00
Tulir Asokan
2355d70426 bridgev2/matrix: return error if trying to encrypt message without encryption enabled
Some checks are pending
Go / Lint (latest) (push) Waiting to run
Go / Build (old, libolm) (push) Waiting to run
Go / Build (latest, libolm) (push) Waiting to run
Go / Build (old, goolm) (push) Waiting to run
Go / Build (latest, goolm) (push) Waiting to run
2024-08-17 14:12:53 +03:00
Sumner Evans
d40aa8c7c6
verificationhelper: add function to dismiss verification request without cancelling it
Some checks are pending
Go / Lint (latest) (push) Waiting to run
Go / Build (old, libolm) (push) Waiting to run
Go / Build (latest, libolm) (push) Waiting to run
Go / Build (old, goolm) (push) Waiting to run
Go / Build (latest, goolm) (push) Waiting to run
Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2024-08-16 09:10:03 -06:00
Sumner Evans
6946d3cdb5
verificationhelper: send cancellations to other devices if cancelled from one device
Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2024-08-16 09:10:03 -06:00
Tulir Asokan
329157afde Bump version to v0.20.0
Some checks are pending
Go / Lint (latest) (push) Waiting to run
Go / Build (old, libolm) (push) Waiting to run
Go / Build (latest, libolm) (push) Waiting to run
Go / Build (old, goolm) (push) Waiting to run
Go / Build (latest, goolm) (push) Waiting to run
2024-08-16 11:54:48 +03:00
Tulir Asokan
e50a705cec client: update beeper inbox request content
Some checks are pending
Go / Lint (latest) (push) Waiting to run
Go / Build (old, libolm) (push) Waiting to run
Go / Build (latest, libolm) (push) Waiting to run
Go / Build (old, goolm) (push) Waiting to run
Go / Build (latest, goolm) (push) Waiting to run
2024-08-15 16:10:27 +03:00
Tulir Asokan
cb8583825d bridgev2: allow network connectors to provide message stream order 2024-08-15 15:11:03 +03:00
Tulir Asokan
a3f3445657 changelog: update 2024-08-15 14:32:39 +03:00
Tulir Asokan
9e031496a0 dependencies: update 2024-08-15 13:42:11 +03:00
Tulir Asokan
2d3862a65f bridgev2/portal: clear userportal cache when deleting portal 2024-08-15 13:40:14 +03:00
Tulir Asokan
59efa808cb main: drop support for Go 1.21
Some checks are pending
Go / Lint (latest) (push) Waiting to run
Go / Build (old, libolm) (push) Waiting to run
Go / Build (latest, libolm) (push) Waiting to run
Go / Build (old, goolm) (push) Waiting to run
Go / Build (latest, goolm) (push) Waiting to run
2024-08-15 13:12:01 +03:00
Tulir Asokan
e521ab675c crypto/keysharing: improve rejection message when recipient tracking is enabled
Some checks are pending
Go / Lint (latest) (push) Waiting to run
Go / Build (old, libolm) (push) Waiting to run
Go / Build (latest, libolm) (push) Waiting to run
Go / Build (old, goolm) (push) Waiting to run
Go / Build (latest, goolm) (push) Waiting to run
2024-08-14 22:46:38 +03:00
Tulir Asokan
0ea4b348fe bridgev2/backfill: add missing check to thread backfill 2024-08-14 21:28:11 +03:00
Tulir Asokan
3cc3f95017 bridgev2: ensure m.mentions is always set 2024-08-14 18:58:28 +03:00
Tulir Asokan
169e2db7ed bridgev2/backfill: send thread messages in same batch as root
Some checks are pending
Go / Lint (latest) (push) Waiting to run
Go / Build (old, libolm) (push) Waiting to run
Go / Build (latest, libolm) (push) Waiting to run
Go / Build (old, goolm) (push) Waiting to run
Go / Build (latest, goolm) (push) Waiting to run
2024-08-14 14:54:56 +03:00
Tulir Asokan
d791a70ade federation: add query profile and directory wrappers 2024-08-14 14:16:09 +03:00
Tulir Asokan
9b517179dc federation: fix signing requests with no body 2024-08-14 14:15:56 +03:00
Tulir Asokan
4f5dea4ca2 bridgev2/networkinterface: allow network connector to customize m.bridge data
Some checks are pending
Go / Lint (latest) (push) Waiting to run
Go / Build (old, libolm) (push) Waiting to run
Go / Build (latest, libolm) (push) Waiting to run
Go / Build (old, goolm) (push) Waiting to run
Go / Build (latest, goolm) (push) Waiting to run
2024-08-13 19:43:51 +03:00
Tulir Asokan
0df3f47c27 bridgev2: add hack for messages sent by nobody 2024-08-13 18:53:31 +03:00
Tulir Asokan
755ba0f7d6 bridgev2: add config option to only bridge tag/mute on room create 2024-08-13 16:23:39 +03:00
Tulir Asokan
81be525ab6 bridgev2/backfill: include portal key in backfill queue context logger 2024-08-13 15:55:04 +03:00
Tulir Asokan
2883ac8172 bridgev2/backfill: ignore messages with no parts 2024-08-13 15:48:25 +03:00
Tulir Asokan
47bdb8b2f6 bridgev2/matrix: send message status in background 2024-08-13 15:06:01 +03:00
Tulir Asokan
b899ef773f bridgev2/matrix: enable handling power level events 2024-08-13 15:05:44 +03:00
Tulir Asokan
8f5b3ac66f bridgev2/portal: fix last read TS when bridging read receipts from Matrix 2024-08-13 15:05:28 +03:00
Tulir Asokan
1e98cb6a2e bridgev2: add support for handling Matrix power level changes
Some checks are pending
Go / Lint (latest) (push) Waiting to run
Go / Build (old, libolm) (push) Waiting to run
Go / Build (latest, libolm) (push) Waiting to run
Go / Build (old, goolm) (push) Waiting to run
Go / Build (latest, goolm) (push) Waiting to run
2024-08-12 22:31:18 +03:00
Tulir Asokan
091a18d448 bridgev2/backfill: allow resync events to have bundled backfill data as an optimization
Some checks are pending
Go / Lint (latest) (push) Waiting to run
Go / Build (old, libolm) (push) Waiting to run
Go / Build (latest, libolm) (push) Waiting to run
Go / Build (old, goolm) (push) Waiting to run
Go / Build (latest, goolm) (push) Waiting to run
The network connector can provide arbitrary data in RemoteChatResync
events, which is passed to FetchMessages if the event triggers a
backfill. The network connector can then read the data and avoid
refetching those bundled messages.
2024-08-12 19:09:06 +03:00
Tulir Asokan
41f0abd38a bridgev2/matrix: ignore already in room errors when sending invites 2024-08-12 14:32:42 +03:00
Tulir Asokan
7a2b6a93bc bridgev2/errors: add helper to append to RespError message 2024-08-12 13:53:55 +03:00
Tulir Asokan
e13771ff61 dependencies: update
Some checks are pending
Go / Lint (latest) (push) Waiting to run
Go / Build (old, libolm) (push) Waiting to run
Go / Build (latest, libolm) (push) Waiting to run
Go / Build (old, goolm) (push) Waiting to run
Go / Build (latest, goolm) (push) Waiting to run
2024-08-11 21:49:47 +03:00
Tulir Asokan
4112286f55 bridgev2/matrix: update nocrypto build tag
Fixes mautrix/slack#55
2024-08-11 21:32:41 +03:00
Tulir Asokan
48b08ad8e9 errors: add status codes to predefined error variables
Some checks are pending
Go / Lint (latest) (push) Waiting to run
Go / Build (old, libolm) (push) Waiting to run
Go / Build (latest, libolm) (push) Waiting to run
Go / Build (old, goolm) (push) Waiting to run
Go / Build (latest, goolm) (push) Waiting to run
2024-08-10 23:30:07 +03:00
Tulir Asokan
fb87a6851e bridgev2/provisioning: allow network connectors to return custom HTTP errors 2024-08-10 23:26:05 +03:00
Tulir Asokan
55e96279b9 bridgev2/messagestatus: use internal error in notice message if custom message is not set 2024-08-10 22:45:58 +03:00
Tulir Asokan
5bfed60a37 bridgev2/matrix: add OpenAPI spec for provisioning API
Some checks are pending
Go / Lint (latest) (push) Waiting to run
Go / Build (old, libolm) (push) Waiting to run
Go / Build (latest, libolm) (push) Waiting to run
Go / Build (old, goolm) (push) Waiting to run
Go / Build (latest, goolm) (push) Waiting to run
2024-08-10 17:43:53 +03:00
Tulir Asokan
23c5446324 bridgev2/provisioning: nest bridge state in whoami response 2024-08-10 16:08:41 +03:00
Tulir Asokan
da4cbb554b bridgev2: update portalinternal 2024-08-10 14:00:06 +03:00
Tulir Asokan
49b1f240ed format: fix tests
Some checks are pending
Go / Lint (latest) (push) Waiting to run
Go / Build (old, libolm) (push) Waiting to run
Go / Build (latest, libolm) (push) Waiting to run
Go / Build (old, goolm) (push) Waiting to run
Go / Build (latest, goolm) (push) Waiting to run
2024-08-09 23:46:56 +03:00
Tulir Asokan
bbe62ee977 bridgev2/portal: move membership handler to be with the other handlers 2024-08-09 23:35:03 +03:00
Tulir Asokan
b83ac7d071 bridgev2/matrix: add better log when sending message status fails 2024-08-09 23:32:54 +03:00
Tulir Asokan
6d5ae8858b bridgev2: add support for starting DM by inviting ghost 2024-08-09 23:32:54 +03:00
Tulir Asokan
5735ea3420 format: generate m.mentions when parsing markdown 2024-08-09 23:00:38 +03:00
Tulir Asokan
11f93d735e bridgev2: don't change power levels without permission 2024-08-09 22:42:23 +03:00
Tulir Asokan
eb84187368 bridgev2/logout: fix has receiver check
Some checks are pending
Go / Lint (latest) (push) Waiting to run
Go / Build (old, libolm) (push) Waiting to run
Go / Build (latest, libolm) (push) Waiting to run
Go / Build (old, goolm) (push) Waiting to run
Go / Build (latest, goolm) (push) Waiting to run
2024-08-09 16:53:20 +03:00
Tulir Asokan
8c0f705ee9 bridgev2: add support for receiving events with uncertain portal receivers 2024-08-09 15:44:13 +03:00
Malte E
0e4780cf1f
bridgev2/networkinterface: add support for handling Matrix member changes (#265) 2024-08-09 14:30:12 +03:00
Tulir Asokan
5edfcff2b7 bridgev2/networkinterface: define function for validating remote user IDs
Some checks are pending
Go / Lint (latest) (push) Waiting to run
Go / Build (old, libolm) (push) Waiting to run
Go / Build (latest, libolm) (push) Waiting to run
Go / Build (old, goolm) (push) Waiting to run
Go / Build (latest, goolm) (push) Waiting to run
2024-08-08 20:44:55 +03:00
Tulir Asokan
e188e7abc3 bridgev2/portal: allow cancelling remote edit handling 2024-08-08 20:44:25 +03:00
Tulir Asokan
b5f968d8c3 bridgev2/backfill: log more details when inserting message fails
Some checks are pending
Go / Lint (latest) (push) Waiting to run
Go / Build (old, libolm) (push) Waiting to run
Go / Build (latest, libolm) (push) Waiting to run
Go / Build (old, goolm) (push) Waiting to run
Go / Build (latest, goolm) (push) Waiting to run
2024-08-08 13:51:12 +03:00
Tulir Asokan
962ac6bf17 appservice: ensure registered before uploading media 2024-08-08 12:45:24 +03:00
Tulir Asokan
5d4407950a bridgev2: add IsGhostMXID helper function 2024-08-08 01:58:30 +03:00
Tulir Asokan
6c836c6ebd crypto: adjust log when rejecting duplicate message index
Some checks are pending
Go / Lint (latest) (push) Waiting to run
Go / Build (old, libolm) (push) Waiting to run
Go / Build (latest, libolm) (push) Waiting to run
Go / Build (old, goolm) (push) Waiting to run
Go / Build (latest, goolm) (push) Waiting to run
2024-08-07 23:32:17 +03:00
Tulir Asokan
eabab27589 bridgev2/portal: log when event is dropped 2024-08-07 18:58:38 +03:00
Tulir Asokan
1bec37c942 bridgev2/simplevent: add type to typing events 2024-08-07 18:51:36 +03:00
Tulir Asokan
45527281cc bridgev2/backfill: add support for batch limit overrides
Some checks are pending
Go / Lint (latest) (push) Waiting to run
Go / Build (old, libolm) (push) Waiting to run
Go / Build (latest, libolm) (push) Waiting to run
Go / Build (old, goolm) (push) Waiting to run
Go / Build (latest, goolm) (push) Waiting to run
2024-08-07 14:38:00 +03:00
Tulir Asokan
e0f58dccf4 bridgev2/provisioning: remove leftover debug print
Some checks are pending
Go / Lint (latest) (push) Waiting to run
Go / Build (old, libolm) (push) Waiting to run
Go / Build (latest, libolm) (push) Waiting to run
Go / Build (old, goolm) (push) Waiting to run
Go / Build (latest, goolm) (push) Waiting to run
2024-08-07 00:42:51 +03:00
Tulir Asokan
213f9df4a4 bridgev2/portal: also fix state key when sending with bot fallback 2024-08-06 21:53:40 +03:00
Tulir Asokan
73a63a12fb bridgev2/portal: fix event type when sending state with bot fallback 2024-08-06 21:51:05 +03:00
Tulir Asokan
f6b0feab95 bridgev2/chatinfo: add utility for merging ExtraUpdaters 2024-08-06 18:58:36 +03:00
Tulir Asokan
9fffc05a7b bridgev2/portal: fix deleting database rows when syncing reactions 2024-08-06 18:58:18 +03:00
Tulir Asokan
6ed1d410aa bridgev2/portal: fix max count when syncing reactions
Some checks are pending
Go / Lint (latest) (push) Waiting to run
Go / Build (old, libolm) (push) Waiting to run
Go / Build (latest, libolm) (push) Waiting to run
Go / Build (old, goolm) (push) Waiting to run
Go / Build (latest, goolm) (push) Waiting to run
2024-08-06 00:47:41 +03:00
Sumner Evans
9fffe6e54d
bridgev2/database: allow querying ghosts by metadata
Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2024-08-05 13:46:56 -06:00
Tulir Asokan
0f6fa7d691 bridgev2/config: fix cleanup_on_logout section 2024-08-05 21:37:41 +03:00
Tulir Asokan
87d8d92867 bridgev2/userlogin: log when deleting 2024-08-05 20:42:12 +03:00
Tulir Asokan
fd078283b5 bridgev2/userlogin: add option to not clean up rooms when deleting 2024-08-05 20:13:18 +03:00
Tulir Asokan
8daaabcc01 bridgev2/mxmain: don't suggest using ignore foreign tables flag 2024-08-05 20:06:54 +03:00
Tulir Asokan
9d6622c293 bridgev2/simplevent: fix needs backfill check in ChatResync
Some checks are pending
Go / Lint (latest) (push) Waiting to run
Go / Build (old, libolm) (push) Waiting to run
Go / Build (latest, libolm) (push) Waiting to run
Go / Build (old, goolm) (push) Waiting to run
Go / Build (latest, goolm) (push) Waiting to run
2024-08-04 22:22:10 +03:00
Tulir Asokan
7f22cb4410 bridgev2/provisioning: include remote state info in whoami 2024-08-04 21:42:40 +03:00
Tulir Asokan
e37f91b3b1 bridgev2: include remote profile in provisioning API 2024-08-04 21:17:50 +03:00
Tulir Asokan
3a25416c01 bridgev2/login: merge remote profile data when relogining 2024-08-04 21:01:42 +03:00
Tulir Asokan
0e048db4f7 bridgev2: add remote profile field in bridge states 2024-08-04 20:53:00 +03:00
Tulir Asokan
956c13761e id: fix typo in ContentURI.IsValid 2024-08-03 22:06:39 +03:00
Tulir Asokan
dc35792d75 bridgev2/login: add token type for user input 2024-08-03 21:04:21 +03:00
Tulir Asokan
9a1e84d74e bridgev2/config: adjust public media section 2024-08-03 20:32:34 +03:00
Tulir Asokan
ed7ec6a066 bridgev2: don't try to delete non-existent rooms 2024-08-03 20:23:59 +03:00
Tulir Asokan
b71b32d0d6 bridgev2: redefine relay admin-only setting
Now users can still set relays from the `default_relays` list even if
`admin_only` is true.
2024-08-03 18:09:44 +03:00
Tulir Asokan
e6586537d3 bridgev2: add option for name formatting when using fancy relays
(like Discord webhooks and Slack bots that can set custom names)
2024-08-03 18:02:08 +03:00
Tulir Asokan
c6bc42f16c bridgev2/matrix: add support for generating public media URLs 2024-08-03 18:00:53 +03:00
Tulir Asokan
ea3cd96e25 format/mdext: add single-character bold, italic and strikethrough parsers 2024-08-02 23:53:42 +03:00
Tulir Asokan
83d3a0de5b bridgev2: add disambiguation for relayed user displaynames 2024-08-02 23:49:09 +03:00
Sumner Evans
0a17ac1cbe
crypto/ssss: remove id from key metadata
Instead, we will pass it into the key constructor functions directly.
This avoids the footgun where you don't set the key ID on the metadata
and then the ID is not properly propagated to the Key that is returned.

Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2024-08-01 18:47:06 -06:00
Tulir Asokan
7402f5a705 bridgev2/matrix: disable using inbox state endpoint 2024-08-01 22:13:14 +03:00
Tulir Asokan
07d30e77df bridgev2/portal: don't queue event in CreateMatrixRoom if portal already exists 2024-08-01 21:31:48 +03:00
Tulir Asokan
853273fabf bridgev2/util: add function to clean non-international phone numbers 2024-08-01 20:35:52 +03:00
Tulir Asokan
69fbdaf5ac bridgev2/ghost: add GetExistingGhostByID 2024-08-01 20:35:36 +03:00
Tulir Asokan
d644a962c8 bridgev2/provisioning: add option to delete previous login when making new one 2024-08-01 19:13:32 +03:00
Tulir Asokan
a1a245be10 bridgev2/matrix: temporarily disable mark unread bridging on Beeper 2024-08-01 18:19:52 +03:00
Tulir Asokan
865606c440 crypto/attachment: return io.ReadSeekCloser from stream functions 2024-08-01 16:57:50 +03:00
Tulir Asokan
ddbf9098f4 bridgev2/backfillqueue: fix tasks being marked as done too soon 2024-08-01 16:21:15 +03:00
Tulir Asokan
7d9e60dfdf client: fix beeper inbox method 2024-08-01 16:13:58 +03:00
Tulir Asokan
2e0dd48c5d bridgev2/backfillqueue: sleep on first start 2024-08-01 15:58:00 +03:00
Tulir Asokan
1050d07624 bridgev2/portal: actually fix order of operations for filling other user ID 2024-08-01 15:23:49 +03:00
Tulir Asokan
e939f164d2 bridgev2/matrix: use beeper inbox state endpoint if available 2024-08-01 15:19:05 +03:00
Tulir Asokan
52b5649abd bridgev2/portal: fix order of operations in room creation sync
`UpdateInfoFromGhost` won't work correctly before participants are
synced at least once.
2024-08-01 15:19:05 +03:00
Tulir Asokan
6d92dfa9ac bridgev2: add helper to convert reaction sync to backfill reactions 2024-08-01 15:19:05 +03:00
Tulir Asokan
1deb964288 bridgev2/portal: implement basic delivery receipts in DMs 2024-08-01 15:19:05 +03:00
Tulir Asokan
cf35e92ab8 bridgev2/portal: only set non-zero timestamps when handling pending message 2024-08-01 15:19:05 +03:00
Tulir Asokan
9827af3a8f bridgev2/networkinterface: add new type for delivery receipts 2024-08-01 15:19:05 +03:00
Tulir Asokan
057b2ee61d bridgev2: add option to force all messages to be sent as DM user 2024-08-01 15:19:05 +03:00
Sumner Evans
62671f147f
crypto/backup: update comment on computing MAC for encrypted session data
Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2024-07-31 08:05:15 -06:00
Tulir Asokan
11e8d62969 bridgev2/networkinterface: add option to save database parts after handling upsert 2024-07-31 15:57:34 +03:00
Tulir Asokan
c76ebc6947 bridgev2/networkinterface: add option to not send MSS event when handling remote echo 2024-07-31 15:57:30 +03:00
Tulir Asokan
87d540348c bridgev2/networkinterface: include reaction being overridden in HandleMatrixReaction 2024-07-31 15:56:37 +03:00
Tulir Asokan
ede6630d79 bridgev2/portal: add MaxCount option to mass reaction sync events 2024-07-30 17:43:59 +03:00
Tulir Asokan
779f61ac9c bridgev2/simplevent: add more events 2024-07-30 16:36:08 +03:00
Tulir Asokan
c1a993719e bridgev2/messagestatus: add common media errors 2024-07-30 16:29:42 +03:00
Tulir Asokan
27449f2562 bridgev2/portal: allow access to private methods 2024-07-30 16:29:10 +03:00
Tulir Asokan
74c2cd06a7 bridgev2/events: allow dangerously adding parts in edits 2024-07-30 16:28:40 +03:00
Tulir Asokan
a1f38e2867 bridgev2/events: add support for async sending and incoming event upserting 2024-07-30 16:28:02 +03:00
Tulir Asokan
3e01676eb5 bridgev2/events: add reaction mass resync event 2024-07-30 16:25:19 +03:00
Tulir Asokan
6042dbec53 bridgev2/caption: only merge if caption is m.text 2024-07-30 16:20:04 +03:00
Tulir Asokan
c47f6ea7b0 bridgev2: add option to skip bridging message but save to database 2024-07-30 16:19:17 +03:00
Tulir Asokan
c0218184e4 bridgev2/database: add sender MXID for reactions 2024-07-30 16:16:53 +03:00
Tulir Asokan
4dda4114b3 bridgev2/matrix: mention MSC4171 in private_chat_portal_meta doc 2024-07-30 12:19:34 +03:00
Tulir Asokan
ff3ec002ee bridgev2/backfill: fix message cutoff 2024-07-30 11:20:40 +03:00
Tulir Asokan
7701ba1a49 bridge,v2: fix special error messages for M_EXCLUSIVE and M_UNKNOWN_TOKEN
Regressed by 98a842c075
2024-07-30 01:01:22 +03:00
Tulir Asokan
a863c76ed8 bridgev2/portal: only consider Receiver for FindPreferredLogin when it's set 2024-07-29 19:20:19 +03:00
Tulir Asokan
2d0135d5c1 bridgev2/portal: make nickname and power level optional in ChatMember 2024-07-29 19:18:52 +03:00
Tulir Asokan
7a3b919723 bridgev2/simplevent: remove redundant prefix in types 2024-07-29 17:41:54 +03:00
Toni Spets
86c2f02d32
Add rudimentary MSC3414 encrypted state support (#260) 2024-07-29 17:37:29 +03:00
Tulir Asokan
9ce81b543e event: add generic helper to cast parsed event content 2024-07-29 14:41:28 +03:00
Tulir Asokan
593ad86b80 federation: add wrappers for some federation endpoints 2024-07-28 23:43:07 +03:00
Tulir Asokan
638fc77115 bridgev2/provisioning: use special token prefix instead of homeserver to detect federated auth 2024-07-28 18:49:50 +03:00
Tulir Asokan
2733a97c28 bridgev2/provisioning: use openid tokens directly for federated auth 2024-07-28 18:37:47 +03:00
Tulir Asokan
d237bab490 bridgev2/provisioning: add support for authenticating federated users 2024-07-28 15:54:39 +03:00
Tulir Asokan
b5c26a2fdb federation: add utilities for server name resolution 2024-07-28 15:20:06 +03:00
Tulir Asokan
3b9f7c8f23 appservice/wshttp: fill RequestURI and RemoteAddr fields 2024-07-28 01:22:28 +03:00
Tulir Asokan
3ba566d182 bridgev2/login: fix filling metadata in NewLogin 2024-07-28 01:08:44 +03:00
Tulir Asokan
426921e00a bridgev2/portal: move exact room type to new field 2024-07-26 20:12:46 +03:00
Tulir Asokan
ef99542dc1 hicli: fix context import 2024-07-26 16:37:05 +03:00
Tulir Asokan
f18a2c55c9 crypto/attachments: add type assertion for encryptingReader 2024-07-26 16:36:47 +03:00
Tulir Asokan
a503da55e3 bridgev2/backfill: never notify for thread batch sends 2024-07-26 00:49:46 +03:00
Tulir Asokan
82e5974d06 bridgev2: allow NetworkAPI to implement custom bridge state fillers 2024-07-25 16:35:25 +03:00
Tulir Asokan
3731f89525 bridgev2/portal: add remote event type to log context by default 2024-07-25 13:30:20 +03:00
Tulir Asokan
0e7bc4711f bridgev2/portal: handle remote chat delete event 2024-07-25 13:30:02 +03:00
Tulir Asokan
6b925e0f95 bridgev2/simplevent: add better default implementations of RemoteEvent 2024-07-25 13:21:45 +03:00
Tulir Asokan
3b8d8d8182 bridgev2: add chat delete event interface 2024-07-25 13:16:36 +03:00
Tulir Asokan
669e30f390 bridgev2/commands: add user search command 2024-07-24 18:37:30 +03:00
Tulir Asokan
14d23589be bridgev2/config: add missing copy statements 2024-07-24 18:22:09 +03:00
Tulir Asokan
e05f095a11 bridgev2/ghost: improve condition for updating DM portal metadata 2024-07-24 18:07:58 +03:00
Tulir Asokan
c27b51a41c bridgev2/portal: don't return other logins in portals with receiver set 2024-07-24 18:07:58 +03:00
Tulir Asokan
1b706d0e5c bridgev2: add options for deleting portals on logout 2024-07-24 18:07:58 +03:00
Tulir Asokan
1ace7749bb bridgev2/mxmain: make it easier to print example config to stdout 2024-07-23 22:56:13 +03:00
Tulir Asokan
5704fa0b3c bridgev2: implement sync_direct_chat_list option 2024-07-23 22:38:37 +03:00
Tulir Asokan
358f8702e4 bridgev2/login: fill Metadata if it's nil in NewLogin 2024-07-23 19:40:18 +03:00
Tulir Asokan
bc0eb86d18 bridgev2/portal: always get ghost when handling remote event
Otherwise the ghost row may not exist
2024-07-23 19:40:18 +03:00
Tulir Asokan
1632e6c9ed event: ignore is_falling_back in non-thread relations 2024-07-22 21:19:29 +03:00
Tulir Asokan
edb026c8a3 bridgev2: fix updating DM portal avatar when ghost has no avatar 2024-07-22 19:22:39 +03:00
Tulir Asokan
ac29c5e461 bridgev2/portal: handle remote reactions to unknown messages 2024-07-22 18:10:49 +03:00
Tulir Asokan
dc8ebb2c65 bridgev2/matrixinterface: include message/reaction meta for redactions 2024-07-22 17:34:54 +03:00
Tulir Asokan
f5beb85721 bridgev2/backfill: fix adding double puppet values 2024-07-22 16:38:43 +03:00
Tulir Asokan
d6e6b66df1 bridgev2: fix avatar deduplication when hash matches but mxc is empty 2024-07-22 16:21:02 +03:00
Tulir Asokan
5915bbfd5f legacymigrate: add column to database_was_migrated table for SQLite compat 2024-07-22 15:35:51 +03:00
Tulir Asokan
50f8bfac25 bridgev2/provisioning: add login flows to whoami endpoint 2024-07-21 01:44:43 +03:00
Tulir Asokan
e878ab1315 bridgev2/provisioning: add CORS headers 2024-07-21 01:44:31 +03:00
Tulir Asokan
24ead553b2 bridgev2/provisioning: add separate error for missing auth 2024-07-20 20:35:15 +03:00
Tulir Asokan
8cb5d5cc69 bridgev2/provisioning: fix invalid auth error code 2024-07-20 20:30:50 +03:00
Tulir Asokan
dff2164cd3 bridgev2/provisioning: add missing parameter to start_dm endpoint 2024-07-20 20:13:23 +03:00
Tulir Asokan
5a3a88cd39 bridgev2/provisioning: add whoami endpoint 2024-07-20 20:13:20 +03:00
Tulir Asokan
cfd7cb775f bridgev2: update definition of extract_js 2024-07-20 15:16:14 +03:00
Tulir Asokan
ea591b0a2e bridgev2/login: redo cookie login params 2024-07-19 21:18:00 +03:00
Tulir Asokan
cc5f225bc6 bridgev2/database: fix getting DM portals with user 2024-07-19 16:05:42 +03:00
Tulir Asokan
81028a6a08 bridgev2: add interfaces for mutes, tags and marked unread bridging 2024-07-19 16:04:55 +03:00
Tulir Asokan
b881a7d455 bridgev2: fix bugs in avatar handling 2024-07-19 14:07:19 +03:00
Tulir Asokan
b8a067206a bridgev2: implement private_chat_portal_meta option 2024-07-19 14:02:15 +03:00
Tulir Asokan
804fd19bb9 bridgev2/legacymigrate: move post-migration DM portal fixing from slack 2024-07-19 13:18:30 +03:00
Tulir Asokan
2677648188 bridgev2/backfill: wake up backfill queue after creating task 2024-07-19 12:48:07 +03:00
Tulir Asokan
a4b0b55db2 bridgev2/backfill: add support for reactions 2024-07-18 19:59:23 +03:00
Tulir Asokan
b395abf62e bridgev2: add extra metadata to SendMessage calls 2024-07-18 18:22:12 +03:00
Tulir Asokan
910e3ee771 bridgev2/backfill: create new timer every time 2024-07-18 17:53:20 +03:00
Tulir Asokan
5a7e002bcc bridgev2/backfill: do forward backfill after room creation 2024-07-18 17:37:22 +03:00
Tulir Asokan
e341bdf0e8 bridgev2: implement more fields in SimpleRemoteEvent 2024-07-18 17:19:09 +03:00
Tulir Asokan
ceb6640054 bridgev2/backfill: respect unread_hours_threshold config option 2024-07-18 17:13:38 +03:00
Tulir Asokan
28d15fa7b0 bridgev2/commands: add command to delete all portals 2024-07-18 17:10:48 +03:00
Tulir Asokan
3fe5071c3f bridgev2/database: rename backfill_queue to backfill_task 2024-07-18 16:51:25 +03:00
Tulir Asokan
6509b11d9c bridgev2/backfill: add more logs 2024-07-18 16:51:25 +03:00
Tulir Asokan
c0aa5898d8 bridgev2/ghost: adjust UpdateInfoIfNecessary logs 2024-07-18 16:51:25 +03:00
Tulir Asokan
edc71a5ee3 bridgev2/backfill: actually run backfill queue 2024-07-18 16:51:25 +03:00
Tulir Asokan
18bca337a5 bridgev2/backfill: insert backfill queue task when creating portal 2024-07-18 16:51:25 +03:00
Tulir Asokan
f80e3d6838 bridgev2/backfill: actually call backfill function 2024-07-18 16:51:25 +03:00
Tulir Asokan
328be908b5 bridgev2/backfill: add stub backfill queue 2024-07-18 16:51:25 +03:00
Tulir Asokan
c48630b4f3 bridgev2/backfill: add config 2024-07-18 16:51:25 +03:00
Toni Spets
62e36db08d
bridgev2: Use pointer type for parsed content in replies (#257) 2024-07-17 14:49:04 +03:00
Toni Spets
9e8d3050b0
Add MSC4144 per message profile types (#256) 2024-07-17 13:29:13 +03:00
Tulir Asokan
0d81a91c9f bridgev2: fix scanning message timestamp 2024-07-17 11:57:51 +03:00
Tulir Asokan
f120ac6b7e bridgev2: use add user-visible message to more errors 2024-07-17 11:44:56 +03:00
Tulir Asokan
085859bfdd bridgev2: add UserInfo to ChatMember to allow updating ghost info easily 2024-07-16 20:59:30 +03:00
Tulir Asokan
1bdadae180 Ensure forwarding_curve25519_key_chain is not null when sharing keys 2024-07-16 18:19:46 +03:00
Tulir Asokan
128781cffe bridgev2: fix some things in backfill 2024-07-16 17:33:45 +03:00
Tulir Asokan
0d122e5bb2 bridgev2: implement backwards backfilling method 2024-07-16 16:53:21 +03:00
Tulir Asokan
c24fd786af bridgev2: add basic support for backfilling threads 2024-07-16 16:36:29 +03:00
Tulir Asokan
a340612071 bridgev2: save other user ID in DM portals 2024-07-16 16:29:34 +03:00
Tulir Asokan
ccb40ff7b4 Bump version to v0.19.0 2024-07-16 11:13:11 +03:00
Tulir Asokan
cb850e3f02 dependencies: update 2024-07-15 15:35:57 +03:00
Tulir Asokan
fb9fb5ae44 bridgev2: add method for getting all portals with Matrix room 2024-07-14 15:10:51 +03:00
Tulir Asokan
edf1a8d8d0 bridge2/database: fix bugs in metadata move 2024-07-14 14:45:57 +03:00
Tulir Asokan
921f8fdfc4 main: rename master branch to main 2024-07-14 11:28:03 +03:00
Tulir Asokan
ffceb93f0f changelog: update 2024-07-14 11:24:57 +03:00
Tulir Asokan
d1905f6232 bridgev2: rename some uses of ID to Key in reference to portal keys 2024-07-14 11:06:19 +03:00
Tulir Asokan
c6da493283 event: ignore calls to Mentions.Add with empty user ID 2024-07-13 19:59:29 +03:00
Tulir Asokan
51aad3c0d7 bridgev2/database: add indexes for some foreign keys 2024-07-13 19:59:18 +03:00
Tulir Asokan
3a6249bf08 dependencies: update go-util 2024-07-13 16:45:02 +03:00
Tulir Asokan
9fdf94132a bridgev2/database: move standard metadata fields to columns, add typing for custom metadata 2024-07-13 12:09:52 +03:00
Tulir Asokan
85e0664cb4 bridgev2: register if /versions fails with M_FORBIDDEN 2024-07-12 19:43:09 +03:00
Tulir Asokan
98a842c075 bridge: register if /versions fails with M_FORBIDDEN 2024-07-12 19:42:29 +03:00
Tulir Asokan
88f4da3433 bridgev2: add method to get or create management room 2024-07-12 17:31:15 +03:00
Tulir Asokan
7c9b8cb287 bridgev2/matrix: add support for appservice websockets 2024-07-12 16:53:40 +03:00
Tulir Asokan
681b5449d5 bridgev2/backfill: fix handling forward backfills in empty rooms 2024-07-12 16:10:06 +03:00
Tulir Asokan
85cead8034 bridgev2: add support for forward backfilling 2024-07-12 15:52:32 +03:00
Tulir Asokan
0f9f923378 bridgev2/matrix: also add timestamp to BRIDGE checkpoints 2024-07-12 15:00:58 +03:00
Tulir Asokan
98918d7ab7 bridgev2: add timestamp to message checkpoints 2024-07-12 14:59:46 +03:00
Adam Van Ymeren
9e4bce17e7
decryptmegolm: Use ResolveTrustContext to ensure any DB transactions are carried forward (#254)
- also make verificationhelper interfaces public so client code can assert conformance
2024-07-11 13:17:44 -07:00
Tulir Asokan
ebcdde0c97 bridgev2: add support for legacy replies to thread messages 2024-07-11 17:34:25 +03:00
Tulir Asokan
32e6f25c34 event: add helper to append user ID to Mentions 2024-07-11 14:01:45 +03:00
Tulir Asokan
7f18d6b735 bridgev2: add caption merging utilities 2024-07-11 11:38:23 +03:00
Tulir Asokan
8893695f84 bridgev2/commands: fix panic and improve logs in start-chat 2024-07-11 10:49:28 +03:00
Tulir Asokan
dd16a8d1d9 bridgev2: replace relates_to with thread_root and reply_to columns 2024-07-10 23:46:02 +03:00
Sumner Evans
672ded60f9
pre-commit: update, enforce go mod tidy
Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2024-07-10 12:01:17 -06:00
Tulir Asokan
3ebe0e18ce bridgev2: allow returning nil in HandleMatrixReaction 2024-07-10 19:07:38 +03:00
Tulir Asokan
5e50b6a87b crypto: remove incorrect warning log when m.relates_to is in both contents 2024-07-10 19:07:21 +03:00
Tulir Asokan
8ebeb5e3ab bridgev2: add some more logs to handling remote events 2024-07-10 18:28:29 +03:00
Tulir Asokan
0cbe236550 crypto/sqlstore: fill account_id when updating crypto_secrets schema 2024-07-10 17:35:16 +03:00
Tulir Asokan
6ce34d7819 bridgev2: set network section in m.bridge events properly 2024-07-10 17:14:30 +03:00
Tulir Asokan
9ff2c21fa7 bridgev2: fix auto-joining if membership is blank 2024-07-10 15:37:28 +03:00
Tulir Asokan
b5f24a8b50 bridgev2/commands: fix logging error message in command panic handler 2024-07-10 15:37:13 +03:00
Tulir Asokan
b6bc70e101 bridgev2: fix filling global bridge state 2024-07-10 14:16:41 +03:00
Tulir Asokan
2675993fc2 bridgev2: fill global bridge state before sending 2024-07-10 14:15:41 +03:00
Tulir Asokan
890b23a332 bridgev2: don't add portals with parent into user login space 2024-07-10 14:13:21 +03:00
Tulir Asokan
e9097ad3a2 bridgev2: implement portal parent spaces 2024-07-10 13:17:52 +03:00
Tulir Asokan
cd334a3815 bridgev2: expose unlocked get functions and lock entire NewLogin 2024-07-10 13:17:32 +03:00
Tulir Asokan
989edc61a8 bridgev2: refetch info on room create if it's missing member list 2024-07-10 11:41:46 +03:00
Tulir Asokan
06f9e82d7c bridgev2: add total member count field to member list 2024-07-10 11:41:29 +03:00
Tulir Asokan
fc7ed77e26 bridgev2: add helper for finding existing portal receiver 2024-07-09 19:09:00 +03:00
Tulir Asokan
0aa773b973 bridgev2: add more features to reaction and receipt events 2024-07-09 19:08:41 +03:00
Tulir Asokan
b4057a26c3 bridgev2/portal: don't panic if IsSpace is unset in CreateMatrixRoom 2024-07-06 15:46:59 +03:00
Tulir Asokan
f323208831 bridgev2: link to mau.fi/ports for DefaultPort doc 2024-07-06 15:46:46 +03:00
Tulir Asokan
7eb0e962ce bridgev2/login: further improve user input command handling 2024-07-06 15:46:25 +03:00
Tulir Asokan
977eb24233 bridgev2/login: don't reply with empty instructions 2024-07-06 15:08:11 +03:00
Tulir Asokan
5f510014f0 bridgev2/login: improve user input handling in command login 2024-07-06 15:07:30 +03:00
Tulir Asokan
be24616d9f bridgev2/database: fix order of tables in initial schema 2024-07-06 15:00:32 +03:00
Tulir Asokan
e716b1ca08 bridgev2/mxmain: allow network connector to have no config 2024-07-06 14:59:05 +03:00
Tulir Asokan
e9034dc9f1 bridgev2/mxmain: refuse to write example config to existing file 2024-07-06 14:51:45 +03:00
Tulir Asokan
11421ec643 bridgev2/participants: treat empty membership as join 2024-07-06 14:32:52 +03:00
Tulir Asokan
e9545f9667 bridgev2/portal: catch panics in event handlers 2024-07-06 14:32:37 +03:00
Sumner Evans
017ca62223
bridgev2/portal: don't shadow redaction remove error (#251)
Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2024-07-06 10:12:02 +03:00
Sumner Evans
65ae2fce42
appservice/intent: fix error handling on double-puppet invites (#252)
Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2024-07-06 10:11:55 +03:00
Tulir Asokan
b5324dffde
crypto/attachment: implement io.Seeker in EncryptStream (#243) 2024-07-06 10:11:44 +03:00
Tulir Asokan
74c0110ee0 misc: remove some local functions in favor of generic ones 2024-07-02 11:20:21 +03:00
Tulir Asokan
82a579f10e bridgev2: add godocs for NewLogin 2024-07-02 10:29:03 +03:00
Tulir Asokan
9b647fe945 bridgev2: make User.NewLogin smarter
The function now knows how to reuse existing logins instead of only
inserting new ones
2024-07-02 09:59:04 +03:00
Simon Ruderich
bec59868d5
event/powerlevels: use 0 as default required level for invite (#250) 2024-07-02 09:14:21 +03:00
Tulir Asokan
69d7d79022 bridgev2: add godocs for most NetworkAPI functions 2024-07-01 09:45:53 +03:00
Tulir Asokan
d86f710aea bridgev2/mxmain: handle GetConfig returning a nil upgrader 2024-07-01 09:39:08 +03:00
Tulir Asokan
c6e87a260c bridgev2/matrix: expose appservice HTTP server to network connectors 2024-06-30 19:25:37 +03:00
rudis
407695e37a
client: don't log warning in State() when StateStore is set (#249)
State logs a warning that ClearCachedMembers() fails even when nil is
returned as error. Looks like this was forgotten in 581aa80.
2024-06-30 12:30:44 +03:00
Tulir Asokan
2b96826aa0 main: update dependencies 2024-06-30 01:15:31 +03:00
Tulir Asokan
0443daef0e crypto: use exzerolog.ArrayOfStrs instead of custom function 2024-06-30 01:14:27 +03:00
Tulir Asokan
b09e31d0b4 bridgev2: fix v5/v6 db migration name 2024-06-30 00:35:36 +03:00
Tulir Asokan
63c49bf840 bridgev2: send member events to olm machine 2024-06-30 00:25:45 +03:00
Tulir Asokan
47debaae16 bridgev2/database: fix inserting reactions 2024-06-30 00:18:02 +03:00
Tulir Asokan
9af9101c27 bridgev2: don't log warnings about not being logged in in ephemeral event handlers 2024-06-30 00:13:58 +03:00
Tulir Asokan
db0ec1ebf9 bridgev2: ensure user is joined when marking in portal 2024-06-30 00:06:31 +03:00
Tulir Asokan
4257c5edd3 bridgev2: add room receiver to message primary key 2024-06-29 23:56:06 +03:00
Tulir Asokan
f2585f7bcc bridgev2: don't use double puppet of other user in DMs 2024-06-29 23:32:59 +03:00
Tulir Asokan
9d082e1e2b bridgev2/commands: don't send MSS event for delete-portal command 2024-06-29 23:32:59 +03:00
Tulir Asokan
de5a5607ad bridgev2/matrix: ignore ephemeral events from bridge bot and ghosts 2024-06-29 23:32:59 +03:00
Tulir Asokan
012d542a07 bridgev2: improve handling bot invites 2024-06-29 23:18:17 +03:00
Tulir Asokan
302bbb739b bridgev2: fix adding portals to spaces when creating 2024-06-29 22:45:40 +03:00
Tulir Asokan
9dcaacae07 event: initialize map in Set(User|Event)Level if it's nil 2024-06-29 22:35:46 +03:00
Tulir Asokan
cf5284b9b6 bridgev2: fix relation type of MSS events 2024-06-29 22:30:12 +03:00
Tulir Asokan
2b668652ab bridgev2: implement un/set-relay commands 2024-06-29 16:01:30 +03:00
Tulir Asokan
07f7849c28 bridgev2: add permissions 2024-06-29 13:30:21 +03:00
Tulir Asokan
5782506e9e bridgev2: move commands to subpackage 2024-06-29 11:53:06 +03:00
Tulir Asokan
46b4ab4c9d bridgev2: start adding relay support 2024-06-29 11:53:06 +03:00
Tulir Asokan
86ac5c340b event/beeper: omit empty network value in MSS events 2024-06-28 23:01:58 +03:00
Tulir Asokan
b0bc6165e7 bridgev2: add support for room name/topic/avatar changes 2024-06-28 22:59:17 +03:00
Tulir Asokan
d7251a4c69 bridgev2: add support for all member actions, power levels and join rules 2024-06-28 18:43:12 +03:00
Tulir Asokan
a2353aaef7 bridgev2: rename PortalInfo to ChatInfo 2024-06-28 00:44:49 +03:00
Tulir Asokan
fce5d19205 bridgev2: remove chat mute and tag events
The chat info change event can be used to do both actions
2024-06-28 00:42:36 +03:00
Tulir Asokan
3d621312d0 bridgev2/matrix/intent: remove other tags in TagRoom 2024-06-28 00:37:56 +03:00
Tulir Asokan
6ef736b520 bridgev2: add generic common ExtraUpdates functions 2024-06-27 20:10:24 +03:00
Tulir Asokan
943c33d4ab bridgev2/ghost: add ExtraUpdates for UserInfo too 2024-06-27 19:57:45 +03:00
Tulir Asokan
35f8d837b5 bridgev2: add remote chat info change events 2024-06-27 18:51:10 +03:00
Tulir Asokan
b8837f1d8d bridgev2: allow updating disappearing timer via PortalInfo 2024-06-27 17:39:38 +03:00
Tulir Asokan
e25578d435 bridgev2: improve handling of user logins in bad credentials 2024-06-27 11:32:50 +03:00
Tulir Asokan
c8b03b087e bridgev2: add portals to per-userlogin space 2024-06-27 00:19:18 +03:00
Tulir Asokan
dbefc6e49c appservice/intent: set will_auto_accept for double puppets in EnsureJoined 2024-06-26 23:41:35 +03:00
Tulir Asokan
c9314c6a63 bridgev2/database: fix UserPortal.GetAllForLogin 2024-06-26 21:44:59 +03:00
Tulir Asokan
c4cb5dad04 bridgev2/mxmain/legacymigrate: apply SQL upgrade filters to copy data query 2024-06-26 21:44:39 +03:00
Tulir Asokan
1a18d9ee55 bridgev2/login: add display nothing type for wait step 2024-06-26 20:01:42 +03:00
Tulir Asokan
5dbfd7093e bridgev2/login: allow returning whole UserLogin in LoginCompleteParams 2024-06-26 14:42:48 +03:00
Tulir Asokan
d9a8b7ddbc bridgev2/mxmain: specify expected megabridge db version in legacy db migrator 2024-06-26 13:20:06 +03:00
Simon Ruderich
f246e70414 verificationhelper: fix deadlock when ignoring an unknown cancellation
vh.activeTransactionsLock must be unlocked before leaving the function.
The return when ignoring an unknown cancellation was the only one
missing the unlock.
2024-06-25 15:15:24 -06:00
Sumner Evans
6996bd1087
bridgev2/portal: allow sender to be empty
If the sender is empty, then default to the portal's bridge bot.

Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2024-06-25 15:06:22 -06:00
Tulir Asokan
3f8bb2fd54 bridgev2: don't panic if reply/thread target can't be found 2024-06-25 21:45:43 +03:00
Tulir Asokan
ae05417779 bridgev2: remove outdated comment 2024-06-25 21:31:46 +03:00
Tulir Asokan
5daf7a74a3 client: parse event content in Members 2024-06-25 21:29:48 +03:00
Tulir Asokan
bcb0eb5874 bridgev2: add delete-portal command 2024-06-25 16:07:50 +03:00
Tulir Asokan
34cbfc2601 event: fix content of io.element.functional_members 2024-06-25 16:05:59 +03:00
Tulir Asokan
c51be40fbe bridgev2: don't kick bridge bot when syncing members 2024-06-25 15:55:27 +03:00
Tulir Asokan
13b2d62753 bridgev2/matrix: implement resolve identifier not found responses 2024-06-25 15:04:22 +03:00
Tulir Asokan
54ff874fac bridgev2/config: add legacy bridge config migration 2024-06-25 13:44:56 +03:00
Tulir Asokan
a1ec390dc0 bridgev2/mxmain: add utility for legacy db migrations 2024-06-25 13:44:56 +03:00
Tulir Asokan
09a8a5104a bridgev2/mxmain: adjust database owner 2024-06-24 20:10:09 +03:00
Tulir Asokan
287547297e bridgev2: fill room type in m.bridge 2024-06-24 19:58:03 +03:00
Tulir Asokan
855715bbed bridgev2: add capability flag for refetching ghost info more often 2024-06-24 15:56:13 +03:00
Tulir Asokan
e13b62807f bridgev2: add capability flag for disappearing messages 2024-06-24 15:33:28 +03:00
Tulir Asokan
921240d99b bridgev2: add method to create space for user login 2024-06-21 20:12:15 +03:00
Tulir Asokan
78f2fda2d4 bridgev2: rename and fix GetUserLoginByID 2024-06-21 20:12:15 +03:00
Tulir Asokan
b23c553580 bridgev2: merge redundant FormatGhostMXID into GhostIntent 2024-06-21 20:12:15 +03:00
Tulir Asokan
c12ac71a39 bridgev2: split NetworkAPI interface and add capabilities 2024-06-21 20:12:15 +03:00
Tulir Asokan
c746b86741 bridgev2: add delete method for UserLogin 2024-06-21 20:12:15 +03:00
Tulir Asokan
7aea403e00 bridgev2/matrix: expose functions to allow custom provisioning API endpoints 2024-06-21 20:12:15 +03:00
Sumner Evans
8e1fdfda2c
event: add unstable audio and voice fields
Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2024-06-20 10:00:42 -06:00
Tulir Asokan
7b6f3ba054 bridgev2/matrix: add provisioning API for listing contacts 2024-06-20 17:28:53 +03:00
Tulir Asokan
0418273bdb bridgev2/database: maybe fix saving custom metadata fields 2024-06-20 16:51:16 +03:00
Tulir Asokan
7f901a2688 bridgev2/matrix: fix bugs in provisioning API login 2024-06-20 16:51:02 +03:00
Tulir Asokan
05320aee0c bridgev2/matrix: add provisioning API to get all login IDs 2024-06-20 16:21:40 +03:00
Tulir Asokan
44610ce65e bridgev2/matrix: add logout provisioning API 2024-06-20 16:21:30 +03:00
Tulir Asokan
7c2fdc703d bridgev2: add remote->matrix mark unread interfaces 2024-06-20 15:21:17 +03:00
Tulir Asokan
59b99dee70 bridgev2: add remote->matrix room tagging and muting interfaces 2024-06-20 14:53:18 +03:00
Tulir Asokan
2eb51d35e2 bridgev2: remove duplicate response in resolve-identifier command 2024-06-20 13:03:55 +03:00
Tulir Asokan
68d8ab6896 changelog: update 2024-06-19 23:41:09 +03:00
Tulir Asokan
557e53b5cd bridgev2: add bridge info in room create requests 2024-06-19 23:39:38 +03:00
Tulir Asokan
fb4aff7608 bridgev2: allow pre-uploaded avatars 2024-06-19 23:29:56 +03:00
Tulir Asokan
2a7a5070fb bridgev2/matrix: add provisioning API for starting DMs 2024-06-19 22:58:36 +03:00
Tulir Asokan
8134d17a02 bridgev2/disappear: fill disappear at timer for after_send type if it's unset 2024-06-19 22:35:50 +03:00
Tulir Asokan
eefa219183 bridgev2: add support for starting DMs 2024-06-19 22:06:43 +03:00
Tulir Asokan
e182928df7 bridgev2: fix panic when starting disappearing messages 2024-06-19 22:06:43 +03:00
Tulir Asokan
28d81a2b60 bridgev2: implement re-ID'ing portals properly 2024-06-19 22:06:43 +03:00
Tulir Asokan
c53c0c1860 bridgev2: fix typo 2024-06-19 22:06:43 +03:00
Sumner Evans
69e2b42d85
bridgev2/directmedia: add configurable media ID prefix
Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2024-06-19 09:43:25 -06:00
Tulir Asokan
2e23f6372f bridgev2: add support for typing notifications 2024-06-19 15:42:18 +03:00
Tulir Asokan
4516583742 mediaproxy: add option to force proxying legacy federated downloads 2024-06-19 12:28:12 +03:00
Tulir Asokan
bd2c40e815 mediaproxy: adjust default /version response 2024-06-19 12:19:41 +03:00
Tulir Asokan
3e302fb46f mediaproxy: add basic config structs 2024-06-19 11:46:03 +03:00
Tulir Asokan
7b845c947f
bridgev2: add direct media interface (#244)
Co-authored-by: Sumner Evans <sumner.evans@automattic.com>
2024-06-19 11:28:49 +03:00
Tulir Asokan
2c6ca02eeb bridgev2: add support for disappearing messages 2024-06-18 18:13:29 +03:00
Tulir Asokan
7d68995c85 bridgev2: add structs for metadata of all db tables 2024-06-18 16:55:54 +03:00
Tulir Asokan
afeadfb15f crypto: fix m.relates_to copying 2024-06-17 18:16:54 +03:00
Tulir Asokan
833995832b bridgev2: add clean shutdown 2024-06-17 16:00:07 +03:00
Tulir Asokan
3828c08f27 bridgev2: add debug command for registering pusher 2024-06-17 14:57:38 +03:00
Tulir Asokan
2759e45688 bridgev2: add network interface for registering push notifications 2024-06-17 14:11:39 +03:00
Tulir Asokan
ace2f37f01 Bump version to v0.19.0-beta.1 2024-06-16 23:31:11 +03:00
Tulir Asokan
8b7a3ea230 client: use MSC3916 endpoints for config and preview_url too 2024-06-16 23:29:14 +03:00
Tulir Asokan
f9ade4ded1
Merge pull request #231 from mautrix/tulir/hicli
Add high-level client framework
2024-06-16 13:12:41 +03:00
Tulir Asokan
f7de98ba77 client: add backwards-compatibility for MakeFullRequest 2024-06-16 13:11:21 +03:00
Tulir Asokan
ef97d96754 client: use authenticated media download endpoint if cached spec versions say it's supported 2024-06-15 20:19:48 +03:00
Tulir Asokan
b959fbf737 client: return *http.Response in MakeFullRequest 2024-06-15 20:18:40 +03:00
Tulir Asokan
3b55fedc17 client: cache spec versions supported by server 2024-06-15 20:16:19 +03:00
Tulir Asokan
a44362dc71 client: stop using MakeFullRequest unnecessarily 2024-06-15 20:15:06 +03:00
Tulir Asokan
9ed8ca3d37 hicli: check spec versions on startup 2024-06-15 19:44:46 +03:00
Tulir Asokan
b571d922e0 hicli: include all new events in sync completed event 2024-06-15 19:44:35 +03:00
Tulir Asokan
1d800734ac hicli: better get event functions 2024-06-15 19:44:04 +03:00
Tulir Asokan
bad4de70f7 hicli: fix some bugs 2024-06-15 14:33:30 +03:00
Tulir Asokan
a5fbfe2692 Merge branch 'master' into tulir/hicli 2024-06-15 14:25:35 +03:00
Tulir Asokan
9304e2b9d7 bridgev2: update FEATURES.md 2024-06-15 14:24:12 +03:00
Tulir Asokan
869a04dadb bridgev2/matrix: don't send status events for ephemeral events 2024-06-14 13:01:25 +03:00
Tulir Asokan
8a727c001d bridgev2: include remote name in user login list 2024-06-14 12:58:42 +03:00
Tulir Asokan
b456fb6e0a bridgev2: initialize bridge state queue when creating UserLogin via User.NewLogin 2024-06-14 12:47:08 +03:00
Tulir Asokan
5272547ae7 bridgev2/mxmain: implement version command 2024-06-14 12:42:50 +03:00
Tulir Asokan
2863a1323b bridgev2/matrix: always import postgres and litestream 2024-06-13 21:41:27 +03:00
Tulir Asokan
ecd47aa231 bridgev2/mxmain: fix some defaults in example config 2024-06-13 21:39:59 +03:00
Tulir Asokan
bbba811760 bridgev2/mxmain: actually exit if flags tell to exit 2024-06-13 16:36:57 +03:00
Tulir Asokan
5af319fe3f client: add support for retrying requests with seekable readers 2024-06-12 21:13:51 +03:00
Tulir Asokan
baa700e123 bridgev2: add feature list 2024-06-12 19:53:28 +03:00
Tulir Asokan
cf6b0e71f0 bridgev2: read receipt support 2024-06-12 19:44:08 +03:00
Tulir Asokan
f0690182c7 bridgev2: add interface for network connectors to find out the max file size 2024-06-12 19:24:13 +03:00
Tulir Asokan
3a98f57d19 bridgev2: add more unorganized documentation 2024-06-12 01:05:37 +03:00
Tulir Asokan
7262fa71df bridgev2/login: improve default phone number validator 2024-06-12 00:53:18 +03:00
Tulir Asokan
d33172c5d2 Move ldflag docs to InitVersion 2024-06-12 00:05:50 +03:00
Tulir Asokan
2d30fad138 Add some godocs and an example for mxmain 2024-06-12 00:00:39 +03:00
Tulir Asokan
fd5603f922 Update readme 2024-06-11 21:08:24 +03:00
Tulir Asokan
d58e8f8817 Add bridge state queue for user logins 2024-06-11 20:26:04 +03:00
Tulir Asokan
f9e2159842 Ignore incoming double puppeted events 2024-06-11 17:19:05 +03:00
Tulir Asokan
f14c5aafb9 Don't send MSS event for first decryption error 2024-06-11 17:06:47 +03:00
Tulir Asokan
1ff72aeffb Mostly implement double puppeting 2024-06-11 17:04:06 +03:00
Tulir Asokan
464b7bc44b Allow Matrix auth for provisioning API 2024-06-11 16:28:04 +03:00
Tulir Asokan
f97d365ea9 Add proper main for v2 matrix bridges 2024-06-11 15:04:02 +03:00
Tulir Asokan
f9dccaaea0
Merge pull request #213 from mautrix/bridgev2
Bridge architecture v2
2024-06-10 22:25:49 +03:00
Tulir Asokan
39ce0103d4 Add DM receivers to portals 2024-06-10 22:14:02 +03:00
Tulir Asokan
9ba40c5d17 Add message checkpoints, status events and error notices 2024-06-10 15:11:59 +03:00
Nick Mills-Barrett
9c77bffa43
Add Client.DefaultHTTPBackoff 2024-06-10 11:49:30 +01:00
Nick Mills-Barrett
92de4a8a51
Add ReqSync.Client field 2024-06-10 09:46:48 +01:00
Nick Mills-Barrett
2580ef78d7
Add FullRequest.BackoffDuration field 2024-06-10 09:46:38 +01:00
Tulir Asokan
6466bf9452 Add support for max reaction count when handling Matrix reactions 2024-06-07 12:55:01 +03:00
Tulir Asokan
476f6fbc25 Add support for remote message/reaction removals 2024-06-07 12:54:47 +03:00
Tulir Asokan
4836aec6cf Auto-fill more fields when handling Matrix reactions 2024-06-06 21:41:52 +03:00
Tulir Asokan
8670a1cbb3 Fix reaction deduplication 2024-06-06 21:36:23 +03:00
Tulir Asokan
a150a47604 Adjust remote event interfaces and add support for reactions 2024-06-06 20:58:22 +03:00
Tulir Asokan
97d803723b Add support for remote edits 2024-06-06 19:44:17 +03:00
Tulir Asokan
3204ffed2d Add new type for converted edits 2024-06-06 18:04:47 +03:00
Tulir Asokan
a0e309fa55 Mostly implement Matrix reactions and redactions 2024-06-06 16:11:10 +03:00
Tulir Asokan
be4a3e17ea Add more fields to cookie login 2024-06-06 13:42:18 +03:00
Tulir Asokan
55fa856e76 Allow overriding message rowid 2024-06-06 13:42:08 +03:00
Tulir Asokan
f7c4ff6455 Add more accurate logs in cmdprocessor 2024-06-06 13:41:53 +03:00
Sumner Evans
d7ffa71838
login: add default validate function depending on field type
Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2024-06-05 11:14:21 -06:00
Sumner Evans
feebb5813f
bridgev2/login: add description
Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2024-06-05 11:13:24 -06:00
Tulir Asokan
218ed06e73 Move ID and sender from ConvertedMessage to RemoteMessage 2024-06-05 13:50:31 +03:00
Tulir Asokan
0707c89fba Add logout support and adjust other things 2024-06-05 13:32:55 +03:00
Tulir Asokan
a599b15466 Add bridge login interface 2024-06-03 22:33:36 +03:00
Tulir Asokan
557fb94669 Allow using separate crypto db and adjust other things 2024-06-02 21:30:42 +03:00
Tulir Asokan
409a7a8166 Add initial message sending support 2024-06-01 23:52:19 +03:00
Tulir Asokan
320c99ce66 Add initial sync event dispatching 2024-06-01 22:50:21 +03:00
Tulir Asokan
2ed8d0d0b3 Add proper room name calculation and fix some bugs 2024-06-01 03:08:41 +03:00
Sumner Evans
b10a140a5c
goolm/crypto: use crypto/ed25519 Equal functions
Previously, the code was using raw byte comparisons, which is not
correct, as it makes timing attacks possible.

Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2024-05-31 12:14:04 -06:00
Tulir Asokan
2b78b75885 Add beeper streaming flag to sync requests 2024-05-31 14:54:01 +03:00
Tulir Asokan
2ec680ba4e Add receipt storage, relation caches, pagination and other stuff 2024-05-29 23:45:08 +03:00
Sumner Evans
57f6cd89e3
(*Client).Download: return entire response instead of just body
Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2024-05-29 09:55:17 -06:00
Tulir Asokan
248de0e6ad Add config field for network connector 2024-05-29 16:55:54 +03:00
Tulir Asokan
3c7b3e13ef Add initial user and room metadata support 2024-05-28 20:49:23 +03:00
Sumner Evans
0b10e7346d
verificationhelper: implement timeout logic
Added 10-minute timeout for verification requests as per
https://spec.matrix.org/v1.10/client-server-api/#error-and-exception-handling

Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2024-05-27 18:23:35 -06:00
Sumner Evans
cd4146f728
verificationhelper: make auto-cancellations more spec-compliant
* Prevents sending cancellation events in response to cancellation
  events that we don't know about.
* Streamlines sending cancellations for all other unknown-transaction
  cases.
* Ensures that the activeTransactionsLock is locked when calling
  cancelVerificationTxn.

Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2024-05-27 16:18:05 -06:00
Sumner Evans
3885a6378e
verificationhelper: cancel if multiple requests received from same device
Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2024-05-27 15:24:42 -06:00
Sumner Evans
1c750ffd0d
verificationhelper: fix CancelVerification
* Calling `CancelVerification` no longer echoes an error back
  representing the reason for the cancellation.
* Calling `CancelVerification` right after starting verification (but
  before another device has accepted the verification) now sends out the
  cancellation events to all devices that the request was initially sent
  out to.
* Adds a test to ensure that the above statements are actually true.

Signed-off-by: Sumner Evans <sumner.evans@automattic.com>

verificationhelper: add test for cancellating right after starting verification

Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2024-05-27 11:42:05 -06:00
Sumner Evans
289ef6f5db
verificationhelper: ensure cross-signing public keys are cached when handling QR data
Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2024-05-27 09:03:15 -06:00
Sumner Evans
a6a3876403
keybackup: don't NPE if we couldn't get cross signing pubkeys
Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2024-05-27 09:02:04 -06:00
Sumner Evans
c1e7cc5300
verificationhelper: add test for QR code cross-signing
Signed-off-by: Sumner Evans <me@sumnerevans.com>
2024-05-27 07:37:06 -06:00
Sumner Evans
5bdc3fdca0
verificationhelper: implement cross-signing
Signed-off-by: Sumner Evans <me@sumnerevans.com>
2024-05-27 07:37:06 -06:00
Sumner Evans
a2abce8215
verificationhelper: add tests for SAS flow
Signed-off-by: Sumner Evans <me@sumnerevans.com>
2024-05-27 07:37:06 -06:00
Sumner Evans
cd7f343cfd
verificationhelper: split QR code tests into separate file
Signed-off-by: Sumner Evans <me@sumnerevans.com>
2024-05-27 07:37:06 -06:00
Sumner Evans
84f77cbafe
crypto/cross signing: actually save signatures in store on publish
Signed-off-by: Sumner Evans <me@sumnerevans.com>
2024-05-27 07:37:05 -06:00
Tulir Asokan
9254a5d6c1 Add base for v2 bridge architecture 2024-05-27 13:53:18 +03:00
Tulir Asokan
c1eb217b9e Add draft of high-level client framework 2024-05-26 18:29:22 +03:00
Tulir Asokan
0b07ae9942 Ignore conflicts when inserting withheld group sessions 2024-05-26 18:27:48 +03:00
Tulir Asokan
ec471738fc Add interface to override UpdateStateStore behavior 2024-05-26 18:26:04 +03:00
Tulir Asokan
96676c1359 Remove separate go.mod for example 2024-05-26 18:26:04 +03:00
Tulir Asokan
5afa391317 Refactor MarkTrackedUsersOutdated to use single query 2024-05-26 18:26:04 +03:00
Tulir Asokan
d7011a7f8b Return imported session in ImportRoomKeyFromBackup 2024-05-26 00:59:30 +03:00
Tulir Asokan
2497fe4397 Export function to parse megolm message index 2024-05-26 00:59:30 +03:00
Tulir Asokan
797aed1e83 Update m.megolm_backup.v1 event type to reference secret ID constant 2024-05-26 00:59:30 +03:00
Tulir Asokan
98c491e069 Add constants for room versions 2024-05-26 00:59:30 +03:00
Tulir Asokan
a2169274da Include room ID and first known index in SessionReceived callback 2024-05-26 00:59:30 +03:00
Tulir Asokan
d64447c3f7 Clamp megolm session rotation periods to sensible limits 2024-05-26 00:59:30 +03:00
Tulir Asokan
881879ea0a Do first sync with timeout 0 2024-05-26 00:58:26 +03:00
Tulir Asokan
826c8cf28e Update m.relates_to in raw decrypted payload 2024-05-26 00:58:26 +03:00
Tulir Asokan
b196541e98 Fix crypto_secrets table schema 2024-05-26 00:58:17 +03:00
Sumner Evans
2195043eba
verificationhelper: add E2E tests
Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2024-05-24 16:42:10 -06:00
Sumner Evans
3dbf8ef2f0
verificationhelper: better errors/logs and more aggressive cancellations
Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2024-05-24 16:41:47 -06:00
Sumner Evans
2e50f99e52
verificationhelper: don't move state to done until both devices have sent the done event
Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2024-05-24 16:36:29 -06:00
Sumner Evans
3bb4648c01
verification/qr: use SigningKey instead of IdentityKey
It turns out that it's supposed to be the signing key. See discussion
about it in the #e2e:matrix.org room:
https://matrix.to/#/!vlnjqGLpLJlFmBSkfQ:matrix.org/$J6UbQwsakEsHMbv5yH7RUpM-OlklZ4U3Ti3VqWp9p8E?via=matrix.org&via=privacytools.io&via=envs.net

This commit reverts commit ef65138cf9:

	verification: check IdentityKey instead of SigningKey in QR mode 2

It also fixes generation to use the signing key instead of the identity
key.

Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2024-05-24 16:34:43 -06:00
Sumner Evans
30132a2c85
statestore: implement FindSharedRooms on MemoryStateStore
Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2024-05-24 16:34:43 -06:00
Sumner Evans
842852a6c1
crypto/cross_sign_ssss: trust master key during generation and upload
Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2024-05-24 16:34:43 -06:00
Sumner Evans
3e8221b17d
verificationhelper: don't send cancellation to self
Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2024-05-23 12:55:09 -06:00
Sumner Evans
843ba24d0a
cross signing: don't require master private key to sign master public key
Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2024-05-23 10:02:15 -06:00
Sumner Evans
55f47fbb16
verificationhelper: fix sending cancellation to other devices
Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2024-05-22 17:23:11 -06:00
Nick Mills-Barrett
9917b3ad3c
Add UpdateRequestOnRetry client hook
Enables modifying the request object between retries, eg. to switch
contexts after cancel.
2024-05-22 10:18:42 +01:00
Sumner Evans
4c8b63da5b
verification: log transaction ID and from_device on verification request
Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2024-05-21 09:54:06 -06:00
Sumner Evans
ef65138cf9
verification: check IdentityKey instead of SigningKey in QR mode 2
Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2024-05-21 07:12:53 -06:00
Sumner Evans
816d94077d
verificationhelper: verify we trust master key when scanning a device that doesn't
Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2024-05-20 11:13:15 -06:00
Sumner Evans
800d061426
verificationhelper: fix check for whether we trust the master key
Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2024-05-20 10:59:52 -06:00
Sumner Evans
1c054a4f5c
verificationhelper: actually sign master key
Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2024-05-20 10:59:34 -06:00
Sumner Evans
dd1dfb9bab
pkcs7: update parameter names and documentation
Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2024-05-20 10:57:51 -06:00
Tulir Asokan
3bd42f5a82 Add option to disable tracking megolm session ratchet state
The tracking is meant for bridges/bots that want to delete old ratchet states
after they're not needed, but for normal clients it's just unnecessary overhead
2024-05-16 17:14:13 +03:00
Tulir Asokan
654b82ec73 Update dependencies 2024-05-16 16:04:51 +03:00
Sumner Evans
3651e46c1e
ShareGroupSession: return error in more cases
* If getting the devices from the database fails
* If FetchKeys fails
* If createOutboundSessions fails

Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2024-05-14 22:39:58 -06:00
Sumner Evans
de0347db00
crypto: fix usages of Store interface
Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2024-05-14 12:47:45 -06:00
Sumner Evans
a87716a358
crypto/store: don't rely on sender key for storing and lookups
* Fixes compatibility with the Store interface
* Increases the usage of "defer"s for "gs.lock.Unlock" and
  "gs.lock.RUnlock"
* Increases the usage of "golang.org/x/exp/maps"

Signed-off-by: Sumner Evans <sumner.evans@automattic.com>

Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2024-05-14 12:47:35 -06:00
Sumner Evans
d0de43f395
crypto/sql_store: don't take sender key on group session methods
Fixes compatibility with the Store interface.

Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2024-05-14 12:32:57 -06:00
Sumner Evans
b31dbb0bd0
store: update interface to not take sender key
According to
https://spec.matrix.org/latest/client-server-api/#mmegolmv1aes-sha2,
clients MUST NOT store or lookup sessions using the sender key.

This commit removes the sender key from most of the functions related to
putting and getting group sessions from the Store interface. Notably,
RedactGroupSessions still accepts a sender key because it's meant for
batch deletion of group sessions.

Signed-off-by: Sumner Evans <sumner.evans@automattic.com>

Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2024-05-14 12:32:57 -06:00
Sumner Evans
34ef1b3705
crypto/sql_store: don't check sender_key in GetGroupSession
Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2024-05-14 12:32:57 -06:00
Sumner Evans
5490cc6aee
crypto/sql_store: add logging on PutGroupSession
Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2024-05-14 12:32:57 -06:00
Nick Mills-Barrett
78f5e4373b Pass error to Client.ResponseHook 2024-05-14 19:01:51 +01:00
Sumner Evans
0439180737
crypto/sql_store: fix a couple places where the error value is unused
Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2024-05-14 10:32:11 -06:00
Sumner Evans
d10103dcf5
crypto/encryptmegolm: return error if sharing outbound session fails
This allows us to catch and throw "database is locked" errors. This will
ensure that if saving the key fails, then we won't share the key out to
anyone.

Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2024-05-13 21:47:45 -06:00
Sumner Evans
01fde7d9a8
verificationhelper/StartVerification: actually set devices after FetchKeys
Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2024-05-13 15:42:51 -06:00
Sumner Evans
c0e030fc85
crypto/olm: remove Signatures definition
Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2024-05-12 18:10:48 -06:00
Sumner Evans
2810465ef2
verificationhelper: ensure that the keys are fetched before starting
Signed-off-by: Sumner Evans <sumner@beeper.com>
2024-04-25 09:40:57 -06:00
Malte E
6cc490d9ab
check ghost != nil in correct line (#208) 2024-04-21 15:22:26 +02:00
Sumner Evans
ff9e2e0f1d
machine/ShareKeys: save keys before sending server request in case it fails
Signed-off-by: Sumner Evans <sumner@beeper.com>
2024-04-19 08:47:36 -06:00
Tulir Asokan
a19dab1897 Bump version to v0.18.1 2024-04-16 13:57:50 +03:00
Tulir Asokan
423d32ddf6 Add real context to HTML parser context struct 2024-04-13 13:57:30 +03:00
Malte E
640086dbf9
Fix default prevContent in bridge membership event handler (#204) 2024-04-05 02:27:36 +03:00
Toni Spets
898b235a84 Allow overriding http.Client with FullRequest 2024-04-02 13:59:48 +03:00
Toni Spets
64cc843952 Invalidate memory cache when storing own cross-signing keys
When another device does cross-signing reset we would incorrectly
cache the old keys indefinitely.
2024-04-02 13:59:07 +03:00
Toni Spets
0095e1fb78 Assume the device list is up-to-date on key backup restore
Fetching devices in a loop can cause request storming if there's a lot
of unknown signatures for a key backup.

A client implementation should always ensure that the devices are
updated from device list changed updates from sync.
2024-03-28 10:42:29 +02:00
Tulir Asokan
ade00e8603
Merge pull request #193 from maltee1/join_rule
Join Rule & (Un)ban handling & Knock handling
2024-03-22 20:04:08 +02:00
Toni Spets
9fe66581e5 Check that shared IGS has higher index than stored
Copies the logic from key import.
2024-03-18 13:17:54 +02:00
Adam Van Ymeren
4dd7adc7be
Merge pull request #200 from beeper/adam/hsorder
Fix Unsigned.IsEmpty() when all we have is HSOrder
2024-03-16 11:41:48 -07:00
Adam Van Ymeren
8ba307b28d Fix Unsigned.IsEmpty() when all we have is HSOrder 2024-03-16 11:36:58 -07:00
Tulir Asokan
5dedc9806a Bump version to v0.18.0 2024-03-16 12:55:53 +02:00
Malte E
b556d65da9 add handler for accepting/rejecting/retracting invites 2024-03-15 22:29:33 +01:00
Toni Spets
fad4448ab7 Use a callback to receive secret response
To properly receive and store a requested secret, we usually need to
validate it against something like a public key to ensure we got the
correct one.

This changes the API so that we instead use a callback to receive any
incoming secret matching our request but we'll fail when we hit the
specified timeout if we never receive anything that is accepted.
2024-03-15 15:12:56 +02:00
Tulir Asokan
a7bf485893 Update changelog 2024-03-13 21:23:04 +02:00
Tulir Asokan
20fde3d163 Remove error in ParseCommonIdentifier 2024-03-13 17:01:07 +02:00
Tulir Asokan
5224780563 Split UserID.Parse into generic ParseCommonIdentifier 2024-03-13 16:57:16 +02:00
Toni Spets
f0b728f502 Require OGS update to succeed during EncryptMegolmEvent
Otherwise we could end up reusing the same ratchet multiple times.
2024-03-13 11:19:49 +02:00
Tulir Asokan
8128b00e00
Add key server that passes the federation tester (#197) 2024-03-12 21:15:39 +02:00
Brad Murray
08397c8b9a
Fix responding to m.secret.request messages (#195) 2024-03-11 18:50:06 -04:00
Tulir Asokan
94246ffc85 Drop maulogger support 2024-03-11 20:36:06 +02:00
Sumner Evans
2728a8f8aa
olm/pk: add fuzz test for the Sign function
Signed-off-by: Sumner Evans <sumner@beeper.com>
2024-03-11 09:00:11 -06:00
Sumner Evans
3b65d98c0c
olm/pk: make an interface
Signed-off-by: Sumner Evans <sumner@beeper.com>
2024-03-11 09:00:11 -06:00
Tulir Asokan
d18dcfc7eb Update dependencies 2024-03-11 15:37:57 +02:00
Toni Spets
a36f60a4f3 Parse Beeper inbox preview event in sync 2024-03-11 12:35:55 +02:00
Tulir Asokan
311a20cea9 Update CHANGELOG.md 2024-03-10 20:34:59 +02:00
Tulir Asokan
1423650a29 Don't use UIA wrapper for appservice user registrations 2024-03-10 20:34:52 +02:00
Malte E
db41583fdd add knock handling 2024-03-10 13:47:09 +01:00
Malte E
41dfb40064 add ban/unban handling 2024-03-09 21:17:27 +01:00
Malte E
6b1a039beb add join rule handler 2024-03-09 20:34:47 +01:00
Malte E
b8e4202c0f
Add handler for power levels in bridges (#189) 2024-03-09 16:33:09 +02:00
Sumner Evans
a6b4b3bf34
ci: run tests with goolm as well
Co-authored-by: Tulir Asokan <tulir@maunium.net>
Signed-off-by: Sumner Evans <sumner@beeper.com>
2024-03-08 15:40:43 -07:00
Sumner Evans
284ab0d62c
olm: remove SHA256 base64 utility
Signed-off-by: Sumner Evans <sumner@beeper.com>
2024-03-08 14:04:19 -07:00
Sumner Evans
a10c114203
verification: remove (go)olm SAS code
Signed-off-by: Sumner Evans <sumner@beeper.com>
2024-03-08 13:24:16 -07:00
Toni Spets
bb6c88faf3 Add callback on megolm session receive 2024-03-06 15:06:21 +02:00
Tulir Asokan
a8e1ae1936 Link to FAQ in some error cases 2024-03-03 12:47:29 +02:00
G-ht
cbd1334724
Add more Synapse admin API wrappers (#181)
Co-authored-by: Tulir Asokan <tulir@maunium.net>
2024-02-24 15:06:27 +02:00
Tulir Asokan
581aa80155 Fix some error logs 2024-02-23 21:12:08 +02:00
Toni Spets
6abf3c4adc Use the encoded form of megolm session key in backup session data
We're using the encoded presentation elsewhere as a string and this
inconsistency is a footgun.
2024-02-22 12:26:35 +02:00
Tulir Asokan
7f0d53ac91 Treat missing upload size limit as 50mb 2024-02-21 19:12:59 +02:00
Sumner Evans
a6644eb030
verificationhelper: add callback for scan QR code
This callback indicates that the other device is showing a QR code and
is ready for our device to scan it.

Signed-off-by: Sumner Evans <sumner@beeper.com>
2024-02-19 16:41:07 -07:00
Sumner Evans
128fc8cd89
event/verification: remove Supports* functions
Use slices.Contains instead

Signed-off-by: Sumner Evans <sumner@beeper.com>
2024-02-19 16:41:07 -07:00
Tulir Asokan
0f7c716964 Fix field name 2024-02-20 00:18:29 +02:00
Tulir Asokan
ccbf0ee988 Add note about event source being moved to 0.17 changelog
Fixes #184
2024-02-20 00:17:13 +02:00
Sumner Evans
5e73f1674a
verification: add CancelVerification to interface
Signed-off-by: Sumner Evans <sumner@beeper.com>
2024-02-18 19:26:12 -07:00
Tulir Asokan
b1b1c97a11 Make matched_url optional 2024-02-17 15:34:17 +02:00
Tulir Asokan
740c588b96 Add MSC4095 types 2024-02-17 14:46:10 +02:00
Sumner Evans
b7f434cd76
verificationhelper: streamline error/cancellation logic
Signed-off-by: Sumner Evans <sumner@beeper.com>
2024-02-16 16:26:02 -07:00
Sumner Evans
169ed443c8
verificationhelper: handle both devices sending start event
Signed-off-by: Sumner Evans <sumner@beeper.com>
2024-02-16 16:26:02 -07:00
Sumner Evans
492c42f8b2
verificationhelper/sas: actually exchange and verify MAC keys
Signed-off-by: Sumner Evans <sumner@beeper.com>
2024-02-16 16:26:02 -07:00
Sumner Evans
0b8e46e84d
event/verification: Mac -> MAC
Signed-off-by: Sumner Evans <sumner@beeper.com>
2024-02-16 12:01:42 -07:00
Sumner Evans
fd986fc43a
crypto: add some license headers
Signed-off-by: Sumner Evans <sumner@beeper.com>
2024-02-16 09:36:35 -07:00
Tulir Asokan
4cae27c445 Bump version to v0.18.0-beta.1 2024-02-16 16:54:18 +02:00
Tulir Asokan
a1b18a005a Add flag to run bridge even if homeserver is outdated 2024-02-16 16:52:21 +02:00
Toni Spets
66ba71153e Remove withheld keys when scanning all IGS rows 2024-02-16 12:27:33 +02:00
Toni Spets
7ffbe34f0c Log if importing partial megolm session 2024-02-16 12:27:33 +02:00
Sumner Evans
990b623244
pre-commit: prevent literal HTTP methods
Signed-off-by: Sumner Evans <sumner@beeper.com>
2024-02-15 21:26:29 -07:00
Sumner Evans
bfca571565
verificationhelper: more thoroughly check verification states for QR process
Signed-off-by: Sumner Evans <sumner@beeper.com>
2024-02-14 09:54:11 -07:00
Sumner Evans
2882267761
verificationhelper: trust and cross-sign on QR code confirmation
Signed-off-by: Sumner Evans <sumner@beeper.com>
2024-02-13 10:50:37 -07:00
Sumner Evans
08b1a6a00b
verificationhelper: sign other device with self-signing key if they don't trust the key
Signed-off-by: Sumner Evans <sumner@beeper.com>
2024-02-13 09:23:28 -07:00
Sumner Evans
f1c0d68dcc
verificationhelper/recipricate: properly check if the master key is trusted
Signed-off-by: Sumner Evans <sumner@beeper.com>
2024-02-12 17:42:02 -07:00
Sumner Evans
92362d93d2
verificationhelper: update a few docstrings
Signed-off-by: Sumner Evans <sumner@beeper.com>
2024-02-12 09:58:20 -07:00
Sumner Evans
6274cb650f
verificationhelper: update warnings
Signed-off-by: Sumner Evans <sumner@beeper.com>
2024-02-12 07:34:08 -07:00
Sumner Evans
64d2b52005
verificationhelper: only show QR code if other device can scan
Signed-off-by: Sumner Evans <sumner@beeper.com>
2024-02-12 07:31:49 -07:00
Toni Spets
340ab4239a Use device signing key to verify interactive verification
Remove unnecessary base64 as well.
2024-02-12 14:03:02 +02:00
Sumner Evans
2f279590fa
verificationhelper/sas: begin implementing flow
Signed-off-by: Sumner Evans <sumner@beeper.com>
2024-02-09 11:50:14 -07:00
Sumner Evans
582ce5de49
verificationhelper/qrcode: begin implementing flow
Signed-off-by: Sumner Evans <sumner@beeper.com>
2024-02-09 11:50:13 -07:00
Sumner Evans
f46d2d349a
verificationhelper/qrcode: add (en|de)coder
Signed-off-by: Sumner Evans <sumner@beeper.com>
2024-02-09 11:50:13 -07:00
Sumner Evans
7469dcf919
id/crypto: add VerificationTransactionID
Signed-off-by: Sumner Evans <sumner@beeper.com>
2024-02-09 10:30:37 -07:00
Sumner Evans
3aec0a3a6f
event/verification: improve documentation and other cleanup
Signed-off-by: Sumner Evans <sumner@beeper.com>
2024-02-09 10:30:36 -07:00
Sumner Evans
ef5eb04ff8
event/verification: add QR code verification method constants
Signed-off-by: Sumner Evans <sumner@beeper.com>
2024-02-09 10:30:36 -07:00
Sumner Evans
6aa214ad1a
events: reorder and add done event
This commit reorders the events to match what's in the spec, and adds
the missing m.key.verification.done event.

Signed-off-by: Sumner Evans <sumner@beeper.com>
2024-02-09 10:30:36 -07:00
Sumner Evans
6bfa468ee7
crypto: remove old verification code
Signed-off-by: Sumner Evans <sumner@beeper.com>
2024-02-09 10:20:48 -07:00
Sumner Evans
b369efbc06
goolm: rename a couple files
Signed-off-by: Sumner Evans <sumner@beeper.com>
2024-02-09 10:20:09 -07:00
Tulir Asokan
8bfa59b5d3 Add Go-version-independent names for actions 2024-02-09 13:44:49 +02:00
Toni Spets
f0f3e84acd Sign cross-signing master with device key 2024-02-09 13:38:30 +02:00
Tulir Asokan
719df9ba82 Use exzerolog defaults in example 2024-02-08 10:58:15 +02:00
Tulir Asokan
2d1786ced4 Configure zerolog globals with exzerolog 2024-02-08 10:56:32 +02:00
Tulir Asokan
a94162cde5 Update dependencies and bump minimum Go version to 1.21 2024-02-08 10:22:14 +02:00
Toni Spets
b131dab9de Allow any UI auth for uploading cross signing keys
Fix endless loop with UI auth causing 401 when uploading keys. Use any
type for key backup setup request auth data so that unmarshaled objects
can be used that have signatures embedded.

Generating keys will now also return them if we also want to setup key
backup without storing the keys in-between.
2024-02-06 13:39:53 +02:00
Toni Spets
11c2907f2e Database level support for key backup versioning
This doesn't plumb anything in yet but adds the columns and types for an
external implementation.

Key backup version is now typed.
2024-02-01 13:22:32 +02:00
Toni Spets
e08ed23845 Fix SQL CryptoStore GetSecret error handling 2024-02-01 13:15:55 +02:00
Toni Spets
75bb8452fa Always verify and return key backup version
Otherwise we can't save the known backup version if the remote one
is empty.
2024-02-01 13:15:55 +02:00
Toni Spets
a36dc59187 Plumb event.AccountDataMegolmBackupKey properly 2024-02-01 10:23:14 +02:00
Toni Spets
dd925e228b Allow restoring single sessions from backups 2024-01-29 13:35:41 +02:00
Toni Spets
cfe3ce5f7b Add test for secret store 2024-01-29 13:35:41 +02:00
Tulir Asokan
0ecb22e6f0 Add m.fully_read to event type class guesser 2024-01-26 19:55:58 +02:00
Sumner Evans
1027a3acde
keybackup: skip bad keys in backup instead of erroring
Signed-off-by: Sumner Evans <sumner@beeper.com>
2024-01-26 10:35:12 -07:00
Toni Spets
1f2e75ee94 Import group session signing key from backup 2024-01-26 14:25:23 +02:00
Toni Spets
8dc80b3178 Share room keys to known devices on request
If we have shared a session with a device once, allow asking for it
again.
2024-01-26 13:52:25 +02:00
Toni Spets
94664f1c8a Secret sharing implementation 2024-01-24 12:56:46 +02:00
saces
97d19484a3
add missing declarations for m.key.verification.ready
Signed-off-by: saces <saces@c-base.org>
2024-01-19 13:15:31 -07:00
Tulir Asokan
38b67b622d
Fix base64 in SSSS keys (#159) 2024-01-19 18:09:49 +02:00
Tulir Asokan
3756213bae Set type class for decrypted olm events correctly 2024-01-19 13:49:51 +02:00
Tulir Asokan
1045d29e07 Add some more godocs for appservices 2024-01-19 12:46:44 +02:00
Tulir Asokan
591e3a002a Add function for creating a ready-to-use AppService instance 2024-01-19 12:29:25 +02:00
Adam Van Ymeren
4ef20ba9cc
base64 encode the session before passing to libolm/goolm
also log the number of imported sessions for fun

Signed-off-by: Sumner Evans <sumner@beeper.com>
2024-01-18 09:17:19 -07:00
Sumner Evans
d8ae0dda14
crypto/keybackup: add function to download and store latest key backup
Signed-off-by: Sumner Evans <sumner@beeper.com>
2024-01-18 09:17:19 -07:00
Sumner Evans
65be59bfed
crypto: refactor to remove need for Utility struct
This also removes all dependence on libolm for the functions that were
provided by the Utility struct.

The crypto/signatures package should be used for all signature
verification operations, and for the occasional situation where a
base64-encoded SHA-256 hash is required, the olm.SHA256B64 function
should be used.

Signed-off-by: Sumner Evans <sumner@beeper.com>
2024-01-18 09:17:19 -07:00
Sumner Evans
c77d6fe1f9
event/type: add megolm backup key account data type
Signed-off-by: Sumner Evans <sumner@beeper.com>
2024-01-18 09:17:19 -07:00
Sumner Evans
950ad6bc7e
crypto/ssss: use errors.Is
Signed-off-by: Sumner Evans <sumner@beeper.com>
2024-01-18 09:17:19 -07:00
Sumner Evans
385449c9cc
responses: make key backup structs generic over {session,auth}_data
Also update the Client to specify the types

Signed-off-by: Sumner Evans <sumner@beeper.com>
2024-01-18 09:17:18 -07:00
Sumner Evans
6681e40deb
crypto/backup: add EncryptedSessionData struct and encrypt/decrypt methods
Signed-off-by: Sumner Evans <sumner@beeper.com>
2024-01-18 09:17:18 -07:00
Sumner Evans
e0d1c1de33
crypto/backup: add EphemeralKey struct with JSON (de)serialization
Signed-off-by: Sumner Evans <sumner@beeper.com>
2024-01-17 08:15:59 -07:00
Sumner Evans
e74304d022
crypto/aescbc: move to its own module
Signed-off-by: Sumner Evans <sumner@beeper.com>
2024-01-17 08:15:59 -07:00
Sumner Evans
066db534f6
crypto/pkcs7: add package for (un)padding data using PKCS#7
Signed-off-by: Sumner Evans <sumner@beeper.com>
2024-01-17 08:15:58 -07:00
Sumner Evans
d9c3b42564
crypto/backup: add structs for m.megolm_backup.v1.curve25519-aes-sha2 backup algorithm
Signed-off-by: Sumner Evans <sumner@beeper.com>
2024-01-17 08:15:58 -07:00
Sumner Evans
18bb31e1c7
crypto/signatures: move Signatures type alias to separate package and add helper method
Signed-off-by: Sumner Evans <sumner@beeper.com>
2024-01-17 08:15:58 -07:00
Sumner Evans
96d1d16226
id/crypto: add KeyBackupAlgorithm
Signed-off-by: Sumner Evans <sumner@beeper.com>
2024-01-17 08:15:58 -07:00
Sumner Evans
4020e9c2ea
client: add key backup functions
Signed-off-by: Sumner Evans <sumner@beeper.com>
2024-01-17 08:15:58 -07:00
Toni Spets
9f12b80726 Open up OlmMachine event handlers 2024-01-17 12:15:21 +02:00
Toni Spets
6ac759c8ff Only skip fetching keys during Megolm decryption if disabled
Blanket disabling caused a lot of side effects which were hard to
deal with without major refactoring.

This should probably be an argument to DecryptMegolm instead of a
flag.
2024-01-17 12:15:21 +02:00
Tulir Asokan
66cfa6389e Fix RawArgs when using command state function 2024-01-16 21:28:44 +02:00
Tulir Asokan
c8e9998e7f Drop support for legacy query param auth for appservices 2024-01-16 21:28:13 +02:00
Tulir Asokan
d151ec4711 Drop support for unprefixed appservice paths 2024-01-16 21:28:13 +02:00
463 changed files with 54493 additions and 14194 deletions

View file

@ -10,3 +10,6 @@ insert_final_newline = true
[*.{yaml,yml}]
indent_style = space
[provisioning.yaml]
indent_size = 2

View file

@ -2,16 +2,20 @@ name: Go
on: [push, pull_request]
env:
GOTOOLCHAIN: local
jobs:
lint:
runs-on: ubuntu-latest
name: Lint (latest)
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
- name: Set up Go
uses: actions/setup-go@v5
uses: actions/setup-go@v6
with:
go-version: "1.21"
go-version: "1.26"
cache: true
- name: Install libolm
@ -20,26 +24,25 @@ jobs:
- name: Install goimports
run: |
go install golang.org/x/tools/cmd/goimports@latest
go install honnef.co/go/tools/cmd/staticcheck@latest
export PATH="$HOME/go/bin:$PATH"
- name: Install pre-commit
run: pip install pre-commit
- name: Lint
run: pre-commit run -a
- name: Run pre-commit
uses: pre-commit/action@v3.0.1
build:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
go-version: ["1.20", "1.21"]
go-version: ["1.25", "1.26"]
name: Build (${{ matrix.go-version == '1.26' && 'latest' || 'old' }}, libolm)
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
- name: Set up Go ${{ matrix.go-version }}
uses: actions/setup-go@v5
uses: actions/setup-go@v6
with:
go-version: ${{ matrix.go-version }}
cache: true
@ -57,3 +60,30 @@ jobs:
- name: Test
run: go test -json -v ./... 2>&1 | gotestfmt
- name: Test (jsonv2)
env:
GOEXPERIMENT: jsonv2
run: go test -json -v ./... 2>&1 | gotestfmt
build-goolm:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
go-version: ["1.25", "1.26"]
name: Build (${{ matrix.go-version == '1.26' && 'latest' || 'old' }}, goolm)
steps:
- uses: actions/checkout@v6
- name: Set up Go ${{ matrix.go-version }}
uses: actions/setup-go@v6
with:
go-version: ${{ matrix.go-version }}
cache: true
- name: Build
run: |
rm -rf crypto/libolm
go build -tags=goolm -v ./...

29
.github/workflows/stale.yml vendored Normal file
View file

@ -0,0 +1,29 @@
name: 'Lock old issues'
on:
schedule:
- cron: '0 6 * * *'
workflow_dispatch:
permissions:
issues: write
# pull-requests: write
# discussions: write
concurrency:
group: lock-threads
jobs:
lock-stale:
runs-on: ubuntu-latest
steps:
- uses: dessant/lock-threads@v6
id: lock
with:
issue-inactive-days: 90
process-only: issues
- name: Log processed threads
run: |
if [ '${{ steps.lock.outputs.issues }}' ]; then
echo "Issues:" && echo '${{ steps.lock.outputs.issues }}' | jq -r '.[] | "https://github.com/\(.owner)/\(.repo)/issues/\(.issue_number)"'
fi

2
.gitignore vendored
View file

@ -1,4 +1,4 @@
.idea/
.vscode/
*.db
*.db*
*.log

View file

@ -1,6 +1,6 @@
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.4.0
rev: v6.0.0
hooks:
- id: trailing-whitespace
exclude_types: [markdown]
@ -9,7 +9,7 @@ repos:
- id: check-added-large-files
- repo: https://github.com/tekwizely/pre-commit-golang
rev: v1.0.0-rc.1
rev: v1.0.0-rc.4
hooks:
- id: go-imports-repo
args:
@ -17,3 +17,13 @@ repos:
- "maunium.net/go/mautrix"
- "-w"
- id: go-vet-repo-mod
- id: go-mod-tidy
- id: go-staticcheck-repo-mod
- repo: https://github.com/beeper/pre-commit-go
rev: v0.4.2
hooks:
- id: prevent-literal-http-methods
- id: zerolog-ban-global-log
- id: zerolog-ban-msgf
- id: zerolog-use-stringer

View file

@ -1,9 +1,658 @@
## v0.26.3 (2026-02-16)
* Bumped minimum Go version to 1.25.
* *(client)* Added fields for sending [MSC4354] sticky events.
* *(bridgev2)* Added automatic message request accepting when sending message.
* *(mediaproxy)* Added support for federation thumbnail endpoint.
* *(crypto/ssss)* Improved support for recovery keys with slightly broken
metadata.
* *(crypto)* Changed key import to call session received callback even for
sessions that already exist in the database.
* *(appservice)* Fixed building websocket URL accidentally using file path
separators instead of always `/`.
* *(crypto)* Fixed key exports not including the `sender_claimed_keys` field.
* *(client)* Fixed incorrect context usage in async uploads.
* *(crypto)* Fixed panic when passing invalid input to megolm message index
parser used for debugging.
* *(bridgev2/provisioning)* Fixed completed or failed logins not being cleaned
up properly.
[MSC4354]: https://github.com/matrix-org/matrix-spec-proposals/pull/4354
## v0.26.2 (2026-01-16)
* *(bridgev2)* Added chunked portal deletion to avoid database locks when
deleting large portals.
* *(crypto,bridgev2)* Added option to encrypt reaction and reply metadata
as per [MSC4392].
* *(bridgev2/login)* Added `default_value` for user input fields.
* *(bridgev2)* Added interfaces to let the Matrix connector provide suggested
HTTP client settings and to reset active connections of the network connector.
* *(bridgev2)* Added interface to let network connectors get the provisioning
API HTTP router and add new endpoints.
* *(event)* Added blurhash field to Beeper link preview objects.
* *(event)* Added [MSC4391] support for bot commands.
* *(event)* Dropped [MSC4332] support for bot commands.
* *(client)* Changed media download methods to return an error if the provided
MXC URI is empty.
* *(client)* Stabilized support for [MSC4323].
* *(bridgev2/matrix)* Fixed `GetEvent` panicking when trying to decrypt events.
* *(bridgev2)* Fixed some deadlocks when room creation happens in parallel with
a portal re-ID call.
[MSC4391]: https://github.com/matrix-org/matrix-spec-proposals/pull/4391
[MSC4392]: https://github.com/matrix-org/matrix-spec-proposals/pull/4392
## v0.26.1 (2025-12-16)
* **Breaking change *(mediaproxy)*** Changed `GetMediaResponseFile` to return
the mime type from the callback rather than in the return get media return
value. The callback can now also redirect the caller to a different file.
* *(federation)* Added join/knock/leave functions
(thanks to [@nexy7574] in [#422]).
* *(federation/eventauth)* Fixed various incorrect checks.
* *(client)* Added backoff for retrying media uploads to external URLs
(with MSC3870).
* *(bridgev2/config)* Added support for overriding config fields using
environment variables.
* *(bridgev2/commands)* Added command to mute chat on remote network.
* *(bridgev2)* Added interface for network connectors to redirect to a different
user ID when handling an invite from Matrix.
* *(bridgev2)* Added interface for signaling message request status of portals.
* *(bridgev2)* Changed portal creation to not backfill unless `CanBackfill` flag
is set in chat info.
* *(bridgev2)* Changed Matrix reaction handling to only delete old reaction if
bridging the new one is successful.
* *(bridgev2/mxmain)* Improved error message when trying to run bridge with
pre-megabridge database when no database migration exists.
* *(bridgev2)* Improved reliability of database migration when enabling split
portals.
* *(bridgev2)* Improved detection of orphaned DM rooms when starting new chats.
* *(bridgev2)* Stopped sending redundant invites when joining ghosts to public
portal rooms.
* *(bridgev2)* Stopped hardcoding room versions in favor of checking
server capabilities to determine appropriate `/createRoom` parameters.
[#422]: https://github.com/mautrix/go/pull/422
## v0.26.0 (2025-11-16)
* *(client,appservice)* Deprecated `SendMassagedStateEvent` as `SendStateEvent`
has been able to do the same for a while now.
* *(client,federation)* Added size limits for responses to make it safer to send
requests to untrusted servers.
* *(client)* Added wrapper for `/admin/whois` client API
(thanks to [@nexy7574] in [#411]).
* *(synapseadmin)* Added `force_purge` option to DeleteRoom
(thanks to [@nexy7574] in [#420]).
* *(statestore)* Added saving join rules for rooms.
* *(bridgev2)* Added optional automatic rollback of room state if bridging the
change to the remote network fails.
* *(bridgev2)* Added management room notices if transient disconnect state
doesn't resolve within 3 minutes.
* *(bridgev2)* Added interface to signal that certain participants couldn't be
invited when creating a group.
* *(bridgev2)* Added `select` type for user input fields in login.
* *(bridgev2)* Added interface to let network connector customize personal
filtering space.
* *(bridgev2/matrix)* Added checks to avoid sending error messages in reply to
other bots.
* *(bridgev2/matrix)* Switched to using [MSC4169] to send redactions whenever
possible.
* *(bridgev2/publicmedia)* Added support for custom path prefixes, file names,
and encrypted files.
* *(bridgev2/commands)* Added command to resync a single portal.
* *(bridgev2/commands)* Added create group command.
* *(bridgev2/config)* Added option to limit maximum number of logins.
* *(bridgev2)* Changed ghost joining to skip unnecessary invite if portal room
is public.
* *(bridgev2/disappear)* Changed read receipt handling to only start
disappearing timers for messages up to the read message (note: may not work in
all cases if the read receipt points at an unknown event).
* *(event/reply)* Changed plaintext reply fallback removal to only happen when
an HTML reply fallback is removed successfully.
* *(bridgev2/matrix)* Fixed unnecessary sleep after registering bot on first run.
* *(crypto/goolm)* Fixed panic when processing certain malformed Olm messages.
* *(federation)* Fixed HTTP method for sending transactions
(thanks to [@nexy7574] in [#426]).
* *(federation)* Fixed response body being closed even when using `DontReadBody`
parameter.
* *(federation)* Fixed validating auth for requests with query params.
* *(federation/eventauth)* Fixed typo causing restricted joins to not work.
[MSC4169]: https://github.com/matrix-org/matrix-spec-proposals/pull/4169
[#411]: github.com/mautrix/go/pull/411
[#420]: github.com/mautrix/go/pull/420
[#426]: github.com/mautrix/go/pull/426
## v0.25.2 (2025-10-16)
* **Breaking change *(id)*** Split `UserID.ParseAndValidate` into
`ParseAndValidateRelaxed` and `ParseAndValidateStrict`. Strict is the old
behavior, but most users likely want the relaxed version, as there are real
users whose user IDs aren't valid under the strict rules.
* *(crypto)* Added helper methods for generating and verifying with recovery
keys.
* *(bridgev2/matrix)* Added config option to automatically generate a recovery
key for the bridge bot and self-sign the bridge's device.
* *(bridgev2/matrix)* Added initial support for using appservice/MSC3202 mode
for encryption with standard servers like Synapse.
* *(bridgev2)* Added optional support for implicit read receipts.
* *(bridgev2)* Added interface for deleting chats on remote network.
* *(bridgev2)* Added local enforcement of media duration and size limits.
* *(bridgev2)* Extended event duration logging to log any event taking too long.
* *(bridgev2)* Improved validation in group creation provisioning API.
* *(event)* Added event type constant for poll end events.
* *(client)* Added wrapper for searching user directory.
* *(client)* Improved support for managing [MSC4140] delayed events.
* *(crypto/helper)* Changed default sync handling to not block on waiting for
decryption keys. On initial sync, keys won't be requested at all by default.
* *(crypto)* Fixed olm unwedging not working (regressed in v0.25.1).
* *(bridgev2)* Fixed various bugs with migrating to split portals.
* *(event)* Fixed poll start events having incorrect null `m.relates_to`.
* *(client)* Fixed `RespUserProfile` losing standard fields when re-marshaling.
* *(federation)* Fixed various bugs in event auth.
## v0.25.1 (2025-09-16)
* *(client)* Fixed HTTP method of delete devices API call
(thanks to [@fmseals] in [#393]).
* *(client)* Added wrappers for [MSC4323]: User suspension & locking endpoints
(thanks to [@nexy7574] in [#407]).
* *(client)* Stabilized support for extensible profiles.
* *(client)* Stabilized support for `state_after` in sync.
* *(client)* Removed deprecated MSC2716 requests.
* *(crypto)* Added fallback to ensure `m.relates_to` is always copied even if
the content struct doesn't implement `Relatable`.
* *(crypto)* Changed olm unwedging to ignore newly created sessions if they
haven't been used successfully in either direction.
* *(federation)* Added utilities for generating, parsing, validating and
authorizing PDUs.
* Note: the new PDU code depends on `GOEXPERIMENT=jsonv2`
* *(event)* Added `is_animated` flag from [MSC4230] to file info.
* *(event)* Added types for [MSC4332]: In-room bot commands.
* *(event)* Added missing poll end event type for [MSC3381].
* *(appservice)* Fixed URLs not being escaped properly when using unix socket
for homeserver connections.
* *(format)* Added more helpers for forming markdown links.
* *(event,bridgev2)* Added support for Beeper's disappearing message state event.
* *(bridgev2)* Redesigned group creation interface and added support in commands
and provisioning API.
* *(bridgev2)* Added GetEvent to Matrix interface to allow network connectors to
get an old event. The method is best effort only, as some configurations don't
allow fetching old events.
* *(bridgev2)* Added shared logic for provisioning that can be reused by the
API, commands and other sources.
* *(bridgev2)* Fixed mentions and URL previews not being copied over when
caption and media are merged.
* *(bridgev2)* Removed config option to change provisioning API prefix, which
had already broken in the previous release.
[@fmseals]: https://github.com/fmseals
[#393]: https://github.com/mautrix/go/pull/393
[#407]: https://github.com/mautrix/go/pull/407
[MSC3381]: https://github.com/matrix-org/matrix-spec-proposals/pull/3381
[MSC4230]: https://github.com/matrix-org/matrix-spec-proposals/pull/4230
[MSC4323]: https://github.com/matrix-org/matrix-spec-proposals/pull/4323
[MSC4332]: https://github.com/matrix-org/matrix-spec-proposals/pull/4332
## v0.25.0 (2025-08-16)
* Bumped minimum Go version to 1.24.
* **Breaking change *(appservice,bridgev2,federation)*** Replaced gorilla/mux
with standard library ServeMux.
* *(client,bridgev2)* Added support for creator power in room v12.
* *(client)* Added option to not set `User-Agent` header for improved Wasm
compatibility.
* *(bridgev2)* Added support for following tombstones.
* *(bridgev2)* Added interface for getting arbitrary state event from Matrix.
* *(bridgev2)* Added batching to disappearing message queue to ensure it doesn't
use too many resources even if there are a large number of messages.
* *(bridgev2/commands)* Added support for canceling QR login with `cancel`
command.
* *(client)* Added option to override HTTP client used for .well-known
resolution.
* *(crypto/backup)* Added method for encrypting key backup session without
private keys.
* *(event->id)* Moved room version type and constants to id package.
* *(bridgev2)* Bots in DM portals will now be added to the functional members
state event to hide them from the room name calculation.
* *(bridgev2)* Changed message delete handling to ignore "delete for me" events
if there are multiple Matrix users in the room.
* *(format/htmlparser)* Changed text processing to collapse multiple spaces into
one when outside `pre`/`code` tags.
* *(format/htmlparser)* Removed link suffix in plaintext output when link text
is only missing protocol part of href.
* e.g. `<a href="https://example.com">example.com</a>` will turn into
`example.com` rather than `example.com (https://example.com)`
* *(appservice)* Switched appservice websockets from gorilla/websocket to
coder/websocket.
* *(bridgev2/matrix)* Fixed encryption key sharing not ignoring ghosts properly.
* *(crypto/attachments)* Fixed hash check when decrypting file streams.
* *(crypto)* Removed unnecessary `AlreadyShared` error in `ShareGroupSession`.
The function will now act as if it was successful instead.
## v0.24.2 (2025-07-16)
* *(bridgev2)* Added support for return values from portal event handlers. Note
that the return value will always be "queued" unless the event buffer is
disabled.
* *(bridgev2)* Added support for [MSC4144] per-message profile passthrough in
relay mode.
* *(bridgev2)* Added option to auto-reconnect logins after a certain period if
they hit an `UNKNOWN_ERROR` state.
* *(bridgev2)* Added analytics for event handler panics.
* *(bridgev2)* Changed new room creation to hardcode room v11 to avoid v12 rooms
being created before proper support for them can be added.
* *(bridgev2)* Changed queuing events to block instead of dropping events if the
buffer is full.
* *(bridgev2)* Fixed assumption that replies to unknown messages are cross-room.
* *(id)* Fixed server name validation not including ports correctly
(thanks to [@krombel] in [#392]).
* *(federation)* Fixed base64 algorithm in signature generation.
* *(event)* Fixed [MSC4144] fallbacks not being removed from edits.
[@krombel]: https://github.com/krombel
[#392]: https://github.com/mautrix/go/pull/392
## v0.24.1 (2025-06-16)
* *(commands)* Added framework for using reactions as buttons that execute
command handlers.
* *(client)* Added wrapper for `/relations` endpoints.
* *(client)* Added support for stable version of room summary endpoint.
* *(client)* Fixed parsing URL preview responses where width/height are strings.
* *(federation)* Fixed bugs in server auth.
* *(id)* Added utilities for validating server names.
* *(event)* Fixed incorrect empty `entity` field when sending hashed moderation
policy events.
* *(event)* Added [MSC4293] redact events field to member events.
* *(event)* Added support for fallbacks in [MSC4144] per-message profiles.
* *(format)* Added `MarkdownLink` and `MarkdownMention` utility functions for
generating properly escaped markdown.
* *(synapseadmin)* Added support for synchronous (v1) room delete endpoint.
* *(synapseadmin)* Changed `Client` struct to not embed the `mautrix.Client`.
This is a breaking change if you were relying on accessing non-admin functions
from the admin client.
* *(bridgev2/provisioning)* Fixed `/display_and_wait` not passing through errors
from the network connector properly.
* *(bridgev2/crypto)* Fixed encryption not working if the user's ID had the same
prefix as the bridge ghosts (e.g. `@whatsappbridgeuser:example.com` with a
`@whatsapp_` prefix).
* *(bridgev2)* Fixed portals not being saved after creating a DM portal from a
Matrix DM invite.
* *(bridgev2)* Added config option to determine whether cross-room replies
should be bridged.
* *(appservice)* Fixed `EnsureRegistered` not being called when sending a custom
member event for the controlled user.
[MSC4293]: https://github.com/matrix-org/matrix-spec-proposals/pull/4293
## v0.24.0 (2025-05-16)
* *(commands)* Added generic framework for implementing bot commands.
* *(client)* Added support for specifying maximum number of HTTP retries using
a context value instead of having to call `MakeFullRequest` manually.
* *(client,federation)* Added methods for fetching room directories.
* *(federation)* Added support for server side of request authentication.
* *(synapseadmin)* Added wrapper for the account suspension endpoint.
* *(format)* Added method for safely wrapping a string in markdown inline code.
* *(crypto)* Added method to import key backup without persisting to database,
to allow the client more control over the process.
* *(bridgev2)* Added viewing chat interface to signal when the user is viewing
a given chat.
* *(bridgev2)* Added option to pass through transaction ID from client when
sending messages to remote network.
* *(crypto)* Fixed unnecessary error log when decrypting dummy events used for
unwedging Olm sessions.
* *(crypto)* Fixed `forwarding_curve25519_key_chain` not being set consistently
when backing up keys.
* *(event)* Fixed marshaling legacy VoIP events with no version field.
* *(bridgev2)* Fixed disappearing message references not being deleted when the
portal is deleted.
* *(bridgev2)* Fixed read receipt bridging not ignoring fake message entries
and causing unnecessary error logs.
## v0.23.3 (2025-04-16)
* *(commands)* Added generic command processing framework for bots.
* *(client)* Added `allowed_room_ids` field to room summary responses
(thanks to [@nexy7574] in [#367]).
* *(bridgev2)* Added support for custom timeouts on outgoing messages which have
to wait for a remote echo.
* *(bridgev2)* Added automatic typing stop event if the ghost user had sent a
typing event before a message.
* *(bridgev2)* The saved management room is now cleared if the user leaves the
room, allowing the next DM to be automatically marked as a management room.
* *(bridge)* Removed deprecated fallback package for bridge statuses.
The status package is now only available under bridgev2.
[#367]: https://github.com/mautrix/go/pull/367
## v0.23.2 (2025-03-16)
* **Breaking change *(bridge)*** Removed legacy bridge module.
* **Breaking change *(event)*** Changed `m.federate` field in room create event
content to a pointer to allow detecting omitted values.
* *(bridgev2/commands)* Added `set-management-room` command to set a new
management room.
* *(bridgev2/portal)* Changed edit bridging to ignore remote edits if the
original sender on Matrix can't be puppeted.
* *(bridgv2)* Added config option to disable bridging `m.notice` messages.
* *(appservice/http)* Switched access token validation to use constant time
comparisons.
* *(event)* Added support for [MSC3765] rich text topics.
* *(event)* Added fields to policy list event contents for [MSC4204] and
[MSC4205].
* *(client)* Added method for getting the content of a redacted event using
[MSC2815].
* *(client)* Added methods for sending and updating [MSC4140] delayed events.
* *(client)* Added support for [MSC4222] in sync payloads.
* *(crypto/cryptohelper)* Switched to using `sqlite3-fk-wal` instead of plain
`sqlite3` by default.
* *(crypto/encryptolm)* Added generic method for encrypting to-device events.
* *(crypto/ssss)* Fixed panic if server-side key metadata is corrupted.
* *(crypto/sqlstore)* Fixed error when marking over 32 thousand device lists
as outdated on SQLite.
[MSC2815]: https://github.com/matrix-org/matrix-spec-proposals/pull/2815
[MSC3765]: https://github.com/matrix-org/matrix-spec-proposals/pull/3765
[MSC4140]: https://github.com/matrix-org/matrix-spec-proposals/pull/4140
[MSC4204]: https://github.com/matrix-org/matrix-spec-proposals/pull/4204
[MSC4205]: https://github.com/matrix-org/matrix-spec-proposals/pull/4205
[MSC4222]: https://github.com/matrix-org/matrix-spec-proposals/pull/4222
## v0.23.1 (2025-02-16)
* *(client)* Added `FullStateEvent` method to get a state event including
metadata (using the `?format=event` query parameter).
* *(client)* Added wrapper method for [MSC4194]'s redact endpoint.
* *(pushrules)* Fixed content rules not considering word boundaries and being
case-sensitive.
* *(crypto)* Fixed bugs that would cause key exports to fail for no reason.
* *(crypto)* Deprecated `ResolveTrust` in favor of `ResolveTrustContext`.
* *(crypto)* Stopped accepting secret shares from unverified devices.
* **Breaking change *(crypto)*** Changed `GetAndVerifyLatestKeyBackupVersion`
to take an optional private key parameter. The method will now trust the
public key if it matches the provided private key even if there are no valid
signatures.
* **Breaking change *(crypto)*** Added context parameter to `IsDeviceTrusted`.
[MSC4194]: https://github.com/matrix-org/matrix-spec-proposals/pull/4194
## v0.23.0 (2025-01-16)
* **Breaking change *(client)*** Changed `JoinRoom` parameters to allow multiple
`via`s.
* **Breaking change *(bridgev2)*** Updated capability system.
* The return type of `NetworkAPI.GetCapabilities` is now different.
* Media type capabilities are enforced automatically by bridgev2.
* Capabilities are now sent to Matrix rooms using the
`com.beeper.room_features` state event.
* *(client)* Added `GetRoomSummary` to implement [MSC3266].
* *(client)* Added support for arbitrary profile fields to implement [MSC4133]
(thanks to [@nexy7574] in [#337]).
* *(crypto)* Started storing olm message hashes to prevent decryption errors
if messages are repeated (e.g. if the app crashes right after decrypting).
* *(crypto)* Improved olm session unwedging to check when the last session was
created instead of only relying on an in-memory map.
* *(crypto/verificationhelper)* Fixed emoji verification not doing cross-signing
properly after a successful verification.
* *(bridgev2/config)* Moved MSC4190 flag from `appservice` to `encryption`.
* *(bridgev2/space)* Fixed failing to add rooms to spaces if the room create
call was made with a temporary context.
* *(bridgev2/commands)* Changed `help` command to hide commands which require
interfaces that aren't implemented by the network connector.
* *(bridgev2/matrixinterface)* Moved deterministic room ID generation to Matrix
connector.
* *(bridgev2)* Fixed service member state event not being set correctly when
creating a DM by inviting a ghost user.
* *(bridgev2)* Fixed `RemoteReactionSync` events replacing all reactions every
time instead of only changed ones.
[MSC3266]: https://github.com/matrix-org/matrix-spec-proposals/pull/3266
[MSC4133]: https://github.com/matrix-org/matrix-spec-proposals/pull/4133
[@nexy7574]: https://github.com/nexy7574
[#337]: https://github.com/mautrix/go/pull/337
## v0.22.1 (2024-12-16)
* *(crypto)* Added automatic cleanup when there are too many olm sessions with
a single device.
* *(crypto)* Added helper for getting cached device list with cross-signing
status.
* *(crypto/verificationhelper)* Added interface for persisting the state of
in-progress verifications.
* *(client)* Added `GetMutualRooms` wrapper for [MSC2666].
* *(client)* Switched `JoinRoom` to use the `via` query param instead of
`server_name` as per [MSC4156].
* *(bridgev2/commands)* Fixed `pm` command not actually starting the chat.
* *(bridgev2/interface)* Added separate network API interface for starting
chats with a Matrix ghost user. This allows treating internal user IDs
differently than arbitrary user-input strings.
* *(bridgev2/crypto)* Added support for [MSC4190]
(thanks to [@onestacked] in [#288]).
[MSC2666]: https://github.com/matrix-org/matrix-spec-proposals/pull/2666
[MSC4156]: https://github.com/matrix-org/matrix-spec-proposals/pull/4156
[MSC4190]: https://github.com/matrix-org/matrix-spec-proposals/pull/4190
[#288]: https://github.com/mautrix/go/pull/288
[@onestacked]: https://github.com/onestacked
## v0.22.0 (2024-11-16)
* *(hicli)* Moved package into gomuks repo.
* *(bridgev2/commands)* Fixed cookie unescaping in login commands.
* *(bridgev2/portal)* Added special `DefaultChatName` constant to explicitly
reset portal names to the default (based on members).
* *(bridgev2/config)* Added options to disable room tag bridging.
* *(bridgev2/database)* Fixed reaction queries not including portal receiver.
* *(appservice)* Updated [MSC2409] stable registration field name from
`push_ephemeral` to `receive_ephemeral`. Homeserver admins must update
existing registrations manually.
* *(format)* Added support for `img` tags.
* *(format/mdext)* Added goldmark extensions for Matrix math and custom emojis.
* *(event/reply)* Removed support for generating reply fallbacks ([MSC2781]).
* *(pushrules)* Added support for `sender_notification_permission` condition
kind (used for `@room` mentions).
* *(crypto)* Added support for `json.RawMessage` in `EncryptMegolmEvent`.
* *(mediaproxy)* Added `GetMediaResponseCallback` and `GetMediaResponseFile`
to write proxied data directly to http response or temp file instead of
having to use an `io.Reader`.
* *(mediaproxy)* Dropped support for legacy media download endpoints.
* *(mediaproxy,bridgev2)* Made interface pass through query parameters.
[MSC2781]: https://github.com/matrix-org/matrix-spec-proposals/pull/2781
## 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.

View file

@ -1,11 +1,12 @@
# 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)
A Golang Matrix framework. Used by [gomuks](https://gomuks.app),
[go-neb](https://github.com/matrix-org/go-neb),
[mautrix-whatsapp](https://github.com/mautrix/whatsapp)
and others.
Matrix room: [`#maunium:maunium.net`](https://matrix.to/#/#maunium:maunium.net)
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).
@ -13,7 +14,11 @@ The original project is licensed under [Apache 2.0](https://github.com/matrix-or
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)
* End-to-end encryption support (incl. key backup, cross-signing, interactive verification, etc)
* High-level module for building puppeting bridges
* Partial federation module (making requests, PDU processing and event authorization)
* A media proxy server which can be used to expose anything as a Matrix media repo
* Wrapper functions for the Synapse admin API
* Structs for parsing event content
* Helpers for parsing and generating Matrix HTML
* Helpers for handling push rules

View file

@ -1,4 +1,4 @@
// Copyright (c) 2023 Tulir Asokan
// Copyright (c) 2025 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
@ -19,12 +19,10 @@ import (
"syscall"
"time"
"github.com/gorilla/mux"
"github.com/gorilla/websocket"
"github.com/coder/websocket"
"github.com/rs/zerolog"
"golang.org/x/net/publicsuffix"
"gopkg.in/yaml.v3"
"maunium.net/go/maulogger/v2/maulogadapt"
"maunium.net/go/mautrix"
"maunium.net/go/mautrix/event"
@ -33,9 +31,9 @@ import (
// EventChannelSize is the size for the Events channel in Appservice instances.
var EventChannelSize = 64
var OTKChannelSize = 4
var OTKChannelSize = 64
// Create a blank appservice instance.
// Create creates a blank appservice instance.
func Create() *AppService {
jar, _ := cookiejar.New(&cookiejar.Options{PublicSuffixList: publicsuffix.List})
as := &AppService{
@ -44,7 +42,7 @@ func Create() *AppService {
intents: make(map[id.UserID]*IntentAPI),
HTTPClient: &http.Client{Timeout: 180 * time.Second, Jar: jar},
StateStore: mautrix.NewMemoryStateStore().(StateStore),
Router: mux.NewRouter(),
Router: http.NewServeMux(),
UserAgent: mautrix.DefaultUserAgent,
txnIDC: NewTransactionIDCache(128),
Live: true,
@ -56,31 +54,72 @@ func Create() *AppService {
OTKCounts: make(chan *mautrix.OTKCount, OTKChannelSize),
DeviceLists: make(chan *mautrix.DeviceLists, EventChannelSize),
QueryHandler: &QueryHandlerStub{},
SpecVersions: &mautrix.RespVersions{},
DefaultHTTPRetries: 4,
}
as.Router.HandleFunc("/transactions/{txnID}", as.PutTransaction).Methods(http.MethodPut)
as.Router.HandleFunc("/rooms/{roomAlias}", as.GetRoom).Methods(http.MethodGet)
as.Router.HandleFunc("/users/{userID}", as.GetUser).Methods(http.MethodGet)
as.Router.HandleFunc("/_matrix/app/v1/transactions/{txnID}", as.PutTransaction).Methods(http.MethodPut)
as.Router.HandleFunc("/_matrix/app/v1/rooms/{roomAlias}", as.GetRoom).Methods(http.MethodGet)
as.Router.HandleFunc("/_matrix/app/v1/users/{userID}", as.GetUser).Methods(http.MethodGet)
as.Router.HandleFunc("/_matrix/app/v1/ping", as.PostPing).Methods(http.MethodPost)
as.Router.HandleFunc("/_matrix/app/unstable/fi.mau.msc2659/ping", as.PostPing).Methods(http.MethodPost)
as.Router.HandleFunc("/_matrix/mau/live", as.GetLive).Methods(http.MethodGet)
as.Router.HandleFunc("/_matrix/mau/ready", as.GetReady).Methods(http.MethodGet)
as.Router.HandleFunc("PUT /_matrix/app/v1/transactions/{txnID}", as.PutTransaction)
as.Router.HandleFunc("GET /_matrix/app/v1/rooms/{roomAlias}", as.GetRoom)
as.Router.HandleFunc("GET /_matrix/app/v1/users/{userID}", as.GetUser)
as.Router.HandleFunc("POST /_matrix/app/v1/ping", as.PostPing)
as.Router.HandleFunc("GET /_matrix/mau/live", as.GetLive)
as.Router.HandleFunc("GET /_matrix/mau/ready", as.GetReady)
return as
}
// CreateOpts contains the options for initializing a new [AppService] instance.
type CreateOpts struct {
// Required, the registration file data for this appservice.
Registration *Registration
// Required, the homeserver's server_name.
HomeserverDomain string
// Required, the homeserver URL to connect to. Should be either https://address or unix:path
HomeserverURL string
// Required if you want to use the standard HTTP server, optional for websockets (non-standard)
HostConfig HostConfig
// Optional, defaults to a memory state store
StateStore StateStore
}
// CreateFull creates a fully configured appservice instance that can be [Start]ed and used directly.
func CreateFull(opts CreateOpts) (*AppService, error) {
if opts.HomeserverDomain == "" {
return nil, fmt.Errorf("missing homeserver domain")
} else if opts.HomeserverURL == "" {
return nil, fmt.Errorf("missing homeserver URL")
} else if opts.Registration == nil {
return nil, fmt.Errorf("missing registration")
}
as := Create()
as.HomeserverDomain = opts.HomeserverDomain
as.Host = opts.HostConfig
as.Registration = opts.Registration
err := as.SetHomeserverURL(opts.HomeserverURL)
if err != nil {
return nil, err
}
if opts.StateStore != nil {
as.StateStore = opts.StateStore
} else {
as.StateStore = mautrix.NewMemoryStateStore().(StateStore)
}
return as, nil
}
var _ StateStore = (*mautrix.MemoryStateStore)(nil)
// QueryHandler handles room alias and user ID queries from the homeserver.
type QueryHandler interface {
QueryAlias(alias string) bool
QueryAlias(alias id.RoomAlias) bool
QueryUser(userID id.UserID) bool
}
type QueryHandlerStub struct{}
func (qh *QueryHandlerStub) QueryAlias(alias string) bool {
func (qh *QueryHandlerStub) QueryAlias(alias id.RoomAlias) bool {
return false
}
@ -88,7 +127,7 @@ func (qh *QueryHandlerStub) QueryUser(userID id.UserID) bool {
return false
}
type WebsocketHandler func(WebsocketCommand) (ok bool, data interface{})
type WebsocketHandler func(WebsocketCommand) (ok bool, data any)
type StateStore interface {
mautrix.StateStore
@ -120,12 +159,13 @@ type AppService struct {
QueryHandler QueryHandler
StateStore StateStore
Router *mux.Router
UserAgent string
server *http.Server
HTTPClient *http.Client
botClient *mautrix.Client
botIntent *IntentAPI
Router *http.ServeMux
UserAgent string
server *http.Server
HTTPClient *http.Client
botClient *mautrix.Client
botIntent *IntentAPI
SpecVersions *mautrix.RespVersions
DefaultHTTPRetries int
@ -138,7 +178,6 @@ type AppService struct {
intentsLock sync.RWMutex
ws *websocket.Conn
wsWriteLock sync.Mutex
StopWebsocket func(error)
websocketHandlers map[string]WebsocketHandler
websocketHandlersLock sync.RWMutex
@ -155,6 +194,7 @@ type AppService struct {
}
const DoublePuppetKey = "fi.mau.double_puppet_source"
const DoublePuppetTSKey = "fi.mau.double_puppet_ts"
func getDefaultProcessID() string {
pid := syscall.Getpid()
@ -178,10 +218,10 @@ func (as *AppService) PrepareWebsocket() {
// HostConfig contains info about how to host the appservice.
type HostConfig struct {
// Hostname can be an IP address or an absolute path for a unix socket.
Hostname string `yaml:"hostname"`
Port uint16 `yaml:"port"`
TLSKey string `yaml:"tls_key,omitempty"`
TLSCert string `yaml:"tls_cert,omitempty"`
// Port is required when Hostname is an IP address, optional for unix sockets
Port uint16 `yaml:"port"`
}
// Address gets the whole address of the Appservice.
@ -215,6 +255,7 @@ func (as *AppService) YAML() (string, error) {
return string(data), nil
}
// BotMXID returns the user ID corresponding to the appservice's sender_localpart
func (as *AppService) BotMXID() id.UserID {
return id.NewUserID(as.Registration.SenderLocalpart, as.HomeserverDomain)
}
@ -251,6 +292,12 @@ func (as *AppService) makeIntent(userID id.UserID) *IntentAPI {
return intent
}
// Intent returns an [IntentAPI] object for the given user ID.
//
// This will return nil if the given user ID has an empty localpart,
// or if the server name is not the same as the appservice's HomeserverDomain.
// It does not currently validate that the given user ID is actually in the
// appservice's namespace. Validation may be added later.
func (as *AppService) Intent(userID id.UserID) *IntentAPI {
as.intentsLock.RLock()
intent, ok := as.intents[userID]
@ -261,6 +308,7 @@ func (as *AppService) Intent(userID id.UserID) *IntentAPI {
return intent
}
// BotIntent returns an [IntentAPI] object for the appservice's sender_localpart user.
func (as *AppService) BotIntent() *IntentAPI {
if as.botIntent == nil {
as.botIntent = as.makeIntent(as.BotMXID())
@ -268,6 +316,10 @@ func (as *AppService) BotIntent() *IntentAPI {
return as.botIntent
}
// SetHomeserverURL updates the appservice's homeserver URL.
//
// Note that this does not affect already-created [IntentAPI] or [mautrix.Client] objects,
// so it should not be called after Intent or Client are called.
func (as *AppService) SetHomeserverURL(homeserverURL string) error {
parsedURL, err := url.Parse(homeserverURL)
if err != nil {
@ -282,7 +334,7 @@ func (as *AppService) SetHomeserverURL(homeserverURL string) error {
} else if as.hsURLForClient.Scheme == "" {
as.hsURLForClient.Scheme = "https"
}
as.hsURLForClient.RawPath = parsedURL.EscapedPath()
as.hsURLForClient.RawPath = as.hsURLForClient.EscapedPath()
jar, _ := cookiejar.New(&cookiejar.Options{PublicSuffixList: publicsuffix.List})
as.HTTPClient = &http.Client{Timeout: 180 * time.Second, Jar: jar}
@ -296,22 +348,30 @@ func (as *AppService) SetHomeserverURL(homeserverURL string) error {
return nil
}
// NewMautrixClient creates a new [mautrix.Client] instance for the given user ID.
//
// This does not do any validation, and it does not cache the client.
// Usually you should prefer [AppService.Client] or [AppService.Intent] over this method.
func (as *AppService) NewMautrixClient(userID id.UserID) *mautrix.Client {
client := &mautrix.Client{
return &mautrix.Client{
HomeserverURL: as.hsURLForClient,
UserID: userID,
SetAppServiceUserID: true,
AccessToken: as.Registration.AppToken,
UserAgent: as.UserAgent,
StateStore: as.StateStore,
Log: as.Log.With().Str("as_user_id", userID.String()).Logger(),
Log: as.Log.With().Stringer("as_user_id", userID).Logger(),
Client: as.HTTPClient,
DefaultHTTPRetries: as.DefaultHTTPRetries,
SpecVersions: as.SpecVersions,
}
client.Logger = maulogadapt.ZeroAsMau(&client.Log)
return client
}
// NewExternalMautrixClient creates a new [mautrix.Client] instance for an external user,
// with a token and homeserver URL separate from the main appservice.
//
// This is primarily meant to facilitate double puppeting in bridges, and is used by [bridge.doublePuppetUtil].
// Non-bridge appservices will likely not need this.
func (as *AppService) NewExternalMautrixClient(userID id.UserID, token string, homeserverURL string) (*mautrix.Client, error) {
client := as.NewMautrixClient(userID)
client.AccessToken = token
@ -339,6 +399,11 @@ func (as *AppService) makeClient(userID id.UserID) *mautrix.Client {
return client
}
// Client returns the [mautrix.Client] instance for the given user ID.
//
// Unlike [AppService.Intent], this does not do any validation, and will always return a value.
// Usually you should prefer creating intents and using intent methods over direct client methods.
// You can always access the client inside an intent with [IntentAPI.Client].
func (as *AppService) Client(userID id.UserID) *mautrix.Client {
as.clientsLock.RLock()
client, ok := as.clients[userID]
@ -349,6 +414,9 @@ func (as *AppService) Client(userID id.UserID) *mautrix.Client {
return client
}
// BotClient returns the [mautrix.Client] instance for the appservice's sender_localpart user.
//
// Like with the generic Client method, [AppService.BotIntent] should be preferred over this.
func (as *AppService) BotClient() *mautrix.Client {
if as.botClient == nil {
as.botClient = as.makeClient(as.BotMXID())

View file

@ -1,4 +1,4 @@
// Copyright (c) 2023 Tulir Asokan
// Copyright (c) 2025 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
@ -17,8 +17,9 @@ import (
"syscall"
"time"
"github.com/gorilla/mux"
"github.com/rs/zerolog"
"go.mau.fi/util/exhttp"
"go.mau.fi/util/exstrings"
"maunium.net/go/mautrix"
"maunium.net/go/mautrix/event"
@ -59,13 +60,8 @@ func (as *AppService) listenUnix() error {
}
func (as *AppService) listenTCP() error {
if len(as.Host.TLSCert) == 0 || len(as.Host.TLSKey) == 0 {
as.Log.Info().Str("address", as.server.Addr).Msg("Starting HTTP listener")
return as.server.ListenAndServe()
} else {
as.Log.Info().Str("address", as.server.Addr).Msg("Starting HTTP listener with TLS")
return as.server.ListenAndServeTLS(as.Host.TLSCert, as.Host.TLSKey)
}
as.Log.Info().Str("address", as.server.Addr).Msg("Starting HTTP listener")
return as.server.ListenAndServe()
}
func (as *AppService) Stop() {
@ -82,27 +78,12 @@ func (as *AppService) Stop() {
// CheckServerToken checks if the given request originated from the Matrix homeserver.
func (as *AppService) CheckServerToken(w http.ResponseWriter, r *http.Request) (isValid bool) {
authHeader := r.Header.Get("Authorization")
if len(authHeader) > 0 && strings.HasPrefix(authHeader, "Bearer ") {
isValid = authHeader[len("Bearer "):] == as.Registration.ServerToken
if !strings.HasPrefix(authHeader, "Bearer ") {
mautrix.MMissingToken.WithMessage("Missing access token").Write(w)
} else if !exstrings.ConstantTimeEqual(authHeader[len("Bearer "):], as.Registration.ServerToken) {
mautrix.MUnknownToken.WithMessage("Invalid access token").Write(w)
} else {
queryToken := r.URL.Query().Get("access_token")
if len(queryToken) > 0 {
isValid = queryToken == as.Registration.ServerToken
} else {
Error{
ErrorCode: ErrUnknownToken,
HTTPStatus: http.StatusForbidden,
Message: "Missing access token",
}.Write(w)
return
}
}
if !isValid {
Error{
ErrorCode: ErrUnknownToken,
HTTPStatus: http.StatusForbidden,
Message: "Incorrect access token",
}.Write(w)
isValid = true
}
return
}
@ -113,24 +94,15 @@ func (as *AppService) PutTransaction(w http.ResponseWriter, r *http.Request) {
return
}
vars := mux.Vars(r)
txnID := vars["txnID"]
txnID := r.PathValue("txnID")
if len(txnID) == 0 {
Error{
ErrorCode: ErrNoTransactionID,
HTTPStatus: http.StatusBadRequest,
Message: "Missing transaction ID",
}.Write(w)
mautrix.MInvalidParam.WithMessage("Missing transaction ID").Write(w)
return
}
defer r.Body.Close()
body, err := io.ReadAll(r.Body)
if err != nil || len(body) == 0 {
Error{
ErrorCode: ErrNotJSON,
HTTPStatus: http.StatusBadRequest,
Message: "Missing request body",
}.Write(w)
mautrix.MNotJSON.WithMessage("Failed to read response body").Write(w)
return
}
log := as.Log.With().Str("transaction_id", txnID).Logger()
@ -139,7 +111,7 @@ func (as *AppService) PutTransaction(w http.ResponseWriter, r *http.Request) {
ctx = log.WithContext(ctx)
if as.txnIDC.IsProcessed(txnID) {
// Duplicate transaction ID: no-op
WriteBlankOK(w)
exhttp.WriteEmptyJSONResponse(w, http.StatusOK)
log.Debug().Msg("Ignoring duplicate transaction")
return
}
@ -148,14 +120,10 @@ func (as *AppService) PutTransaction(w http.ResponseWriter, r *http.Request) {
err = json.Unmarshal(body, &txn)
if err != nil {
log.Error().Err(err).Msg("Failed to parse transaction content")
Error{
ErrorCode: ErrBadJSON,
HTTPStatus: http.StatusBadRequest,
Message: "Failed to parse body JSON",
}.Write(w)
mautrix.MBadJSON.WithMessage("Failed to parse transaction content").Write(w)
} else {
as.handleTransaction(ctx, txnID, &txn)
WriteBlankOK(w)
exhttp.WriteEmptyJSONResponse(w, http.StatusOK)
}
}
@ -218,15 +186,22 @@ func (as *AppService) handleEvents(ctx context.Context, evts []*event.Event, def
for _, evt := range evts {
evt.Mautrix.ReceivedAt = time.Now()
if defaultTypeClass != event.UnknownEventType {
if defaultTypeClass == event.EphemeralEventType {
evt.Mautrix.EventSource = event.SourceEphemeral
} else if defaultTypeClass == event.ToDeviceEventType {
evt.Mautrix.EventSource = event.SourceToDevice
}
evt.Type.Class = defaultTypeClass
} else if evt.StateKey != nil {
evt.Mautrix.EventSource = event.SourceTimeline & event.SourceJoin
evt.Type.Class = event.StateEventType
} else {
evt.Mautrix.EventSource = event.SourceTimeline
evt.Type.Class = event.MessageEventType
}
err := evt.Content.ParseRaw(evt.Type)
if errors.Is(err, event.ErrUnsupportedContentType) {
log.Debug().Str("event_id", evt.ID.String()).Msg("Not parsing content of unsupported event")
log.Debug().Stringer("event_id", evt.ID).Msg("Not parsing content of unsupported event")
} else if err != nil {
log.Warn().Err(err).
Str("event_id", evt.ID.String()).
@ -263,16 +238,12 @@ func (as *AppService) GetRoom(w http.ResponseWriter, r *http.Request) {
return
}
vars := mux.Vars(r)
roomAlias := vars["roomAlias"]
roomAlias := id.RoomAlias(r.PathValue("roomAlias"))
ok := as.QueryHandler.QueryAlias(roomAlias)
if ok {
WriteBlankOK(w)
exhttp.WriteEmptyJSONResponse(w, http.StatusOK)
} else {
Error{
ErrorCode: ErrUnknown,
HTTPStatus: http.StatusNotFound,
}.Write(w)
mautrix.MNotFound.WithMessage("Alias not found").Write(w)
}
}
@ -282,16 +253,12 @@ func (as *AppService) GetUser(w http.ResponseWriter, r *http.Request) {
return
}
vars := mux.Vars(r)
userID := id.UserID(vars["userID"])
userID := id.UserID(r.PathValue("userID"))
ok := as.QueryHandler.QueryUser(userID)
if ok {
WriteBlankOK(w)
exhttp.WriteEmptyJSONResponse(w, http.StatusOK)
} else {
Error{
ErrorCode: ErrUnknown,
HTTPStatus: http.StatusNotFound,
}.Write(w)
mautrix.MNotFound.WithMessage("User not found").Write(w)
}
}
@ -301,11 +268,7 @@ func (as *AppService) PostPing(w http.ResponseWriter, r *http.Request) {
}
body, err := io.ReadAll(r.Body)
if err != nil || len(body) == 0 || !json.Valid(body) {
Error{
ErrorCode: ErrNotJSON,
HTTPStatus: http.StatusBadRequest,
Message: "Missing request body",
}.Write(w)
mautrix.MNotJSON.WithMessage("Invalid or missing request body").Write(w)
return
}
@ -313,27 +276,21 @@ func (as *AppService) PostPing(w http.ResponseWriter, r *http.Request) {
_ = json.Unmarshal(body, &txn)
as.Log.Debug().Str("txn_id", txn.TxnID).Msg("Received ping from homeserver")
w.Header().Add("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
w.Write([]byte("{}"))
exhttp.WriteEmptyJSONResponse(w, http.StatusOK)
}
func (as *AppService) GetLive(w http.ResponseWriter, r *http.Request) {
w.Header().Add("Content-Type", "application/json")
if as.Live {
w.WriteHeader(http.StatusOK)
exhttp.WriteEmptyJSONResponse(w, http.StatusOK)
} else {
w.WriteHeader(http.StatusInternalServerError)
exhttp.WriteEmptyJSONResponse(w, http.StatusInternalServerError)
}
w.Write([]byte("{}"))
}
func (as *AppService) GetReady(w http.ResponseWriter, r *http.Request) {
w.Header().Add("Content-Type", "application/json")
if as.Ready {
w.WriteHeader(http.StatusOK)
exhttp.WriteEmptyJSONResponse(w, http.StatusOK)
} else {
w.WriteHeader(http.StatusInternalServerError)
exhttp.WriteEmptyJSONResponse(w, http.StatusInternalServerError)
}
w.Write([]byte("{}"))
}

View file

@ -10,6 +10,7 @@ import (
"context"
"errors"
"fmt"
"net/http"
"strings"
"sync"
@ -50,11 +51,11 @@ func (as *AppService) NewIntentAPI(localpart string) *IntentAPI {
}
func (intent *IntentAPI) Register(ctx context.Context) error {
_, _, err := intent.Client.Register(ctx, &mautrix.ReqRegister{
_, err := intent.Client.MakeRequest(ctx, http.MethodPost, intent.BuildClientURL("v3", "register"), &mautrix.ReqRegister[any]{
Username: intent.Localpart,
Type: mautrix.AuthTypeAppservice,
InhibitLogin: true,
})
}, nil)
return err
}
@ -85,6 +86,7 @@ func (intent *IntentAPI) EnsureRegistered(ctx context.Context) error {
type EnsureJoinedParams struct {
IgnoreCache bool
BotOverride *mautrix.Client
Via []string
}
func (intent *IntentAPI) EnsureJoined(ctx context.Context, roomID id.RoomID, extra ...EnsureJoinedParams) error {
@ -98,11 +100,17 @@ func (intent *IntentAPI) EnsureJoined(ctx context.Context, roomID id.RoomID, ext
return nil
}
if err := intent.EnsureRegistered(ctx); err != nil {
err := intent.EnsureRegistered(ctx)
if err != nil {
return fmt.Errorf("failed to ensure joined: %w", err)
}
resp, err := intent.JoinRoomByID(ctx, roomID)
var resp *mautrix.RespJoinRoom
if len(params.Via) > 0 {
resp, err = intent.JoinRoom(ctx, roomID.String(), &mautrix.ReqJoinRoom{Via: params.Via})
} else {
resp, err = intent.JoinRoomByID(ctx, roomID)
}
if err != nil {
bot := intent.bot
if params.BotOverride != nil {
@ -111,9 +119,21 @@ func (intent *IntentAPI) EnsureJoined(ctx context.Context, roomID id.RoomID, ext
if !errors.Is(err, mautrix.MForbidden) || bot == nil {
return fmt.Errorf("failed to ensure joined: %w", err)
}
_, inviteErr := bot.InviteUser(ctx, roomID, &mautrix.ReqInviteUser{
UserID: intent.UserID,
})
var inviteErr error
if intent.IsCustomPuppet {
_, inviteErr = bot.SendStateEvent(ctx, roomID, event.StateMember, intent.UserID.String(), &event.Content{
Raw: map[string]any{
"fi.mau.will_auto_accept": true,
},
Parsed: &event.MemberEventContent{
Membership: event.MembershipInvite,
},
})
} else {
_, inviteErr = bot.InviteUser(ctx, roomID, &mautrix.ReqInviteUser{
UserID: intent.UserID,
})
}
if inviteErr != nil {
return fmt.Errorf("failed to invite in ensure joined: %w", inviteErr)
}
@ -129,75 +149,110 @@ func (intent *IntentAPI) EnsureJoined(ctx context.Context, roomID id.RoomID, ext
return nil
}
func (intent *IntentAPI) AddDoublePuppetValue(into interface{}) interface{} {
if !intent.IsCustomPuppet || intent.as.DoublePuppetValue == "" {
func (intent *IntentAPI) IsDoublePuppet() bool {
return intent.IsCustomPuppet && intent.as.DoublePuppetValue != ""
}
func (intent *IntentAPI) AddDoublePuppetValue(into any) any {
return intent.AddDoublePuppetValueWithTS(into, 0)
}
func (intent *IntentAPI) AddDoublePuppetValueWithTS(into any, ts int64) any {
if !intent.IsDoublePuppet() {
return into
}
// Only use ts deduplication feature with appservice double puppeting
if !intent.SetAppServiceUserID {
ts = 0
}
switch val := into.(type) {
case *map[string]interface{}:
case *map[string]any:
if *val == nil {
valNonPtr := make(map[string]interface{})
valNonPtr := make(map[string]any)
*val = valNonPtr
}
(*val)[DoublePuppetKey] = intent.as.DoublePuppetValue
if ts != 0 {
(*val)[DoublePuppetTSKey] = ts
}
return val
case map[string]interface{}:
case map[string]any:
val[DoublePuppetKey] = intent.as.DoublePuppetValue
if ts != 0 {
val[DoublePuppetTSKey] = ts
}
return val
case *event.Content:
if val.Raw == nil {
val.Raw = make(map[string]interface{})
val.Raw = make(map[string]any)
}
val.Raw[DoublePuppetKey] = intent.as.DoublePuppetValue
if ts != 0 {
val.Raw[DoublePuppetTSKey] = ts
}
return val
case event.Content:
if val.Raw == nil {
val.Raw = make(map[string]interface{})
val.Raw = make(map[string]any)
}
val.Raw[DoublePuppetKey] = intent.as.DoublePuppetValue
if ts != 0 {
val.Raw[DoublePuppetTSKey] = ts
}
return val
default:
return &event.Content{
Raw: map[string]interface{}{
content := &event.Content{
Raw: map[string]any{
DoublePuppetKey: intent.as.DoublePuppetValue,
},
Parsed: val,
}
if ts != 0 {
content.Raw[DoublePuppetTSKey] = ts
}
return content
}
}
func (intent *IntentAPI) SendMessageEvent(ctx context.Context, roomID id.RoomID, eventType event.Type, contentJSON interface{}) (*mautrix.RespSendEvent, error) {
func (intent *IntentAPI) SendMessageEvent(ctx context.Context, roomID id.RoomID, eventType event.Type, contentJSON any, extra ...mautrix.ReqSendEvent) (*mautrix.RespSendEvent, error) {
if err := intent.EnsureJoined(ctx, roomID); err != nil {
return nil, err
}
contentJSON = intent.AddDoublePuppetValue(contentJSON)
return intent.Client.SendMessageEvent(ctx, roomID, eventType, contentJSON)
return intent.Client.SendMessageEvent(ctx, roomID, eventType, contentJSON, extra...)
}
func (intent *IntentAPI) BeeperSendEphemeralEvent(ctx context.Context, roomID id.RoomID, eventType event.Type, contentJSON any, extra ...mautrix.ReqSendEvent) (*mautrix.RespSendEvent, error) {
if err := intent.EnsureJoined(ctx, roomID); err != nil {
return nil, err
}
if !intent.SpecVersions.Supports(mautrix.BeeperFeatureEphemeralEvents) {
return nil, mautrix.MUnrecognized.WithMessage("Homeserver does not advertise com.beeper.ephemeral support")
}
contentJSON = intent.AddDoublePuppetValue(contentJSON)
return intent.Client.BeeperSendEphemeralEvent(ctx, roomID, eventType, contentJSON, extra...)
}
// Deprecated: use SendMessageEvent with mautrix.ReqSendEvent.Timestamp instead
func (intent *IntentAPI) SendMassagedMessageEvent(ctx context.Context, roomID id.RoomID, eventType event.Type, contentJSON interface{}, ts int64) (*mautrix.RespSendEvent, error) {
if err := intent.EnsureJoined(ctx, roomID); err != nil {
return nil, err
}
contentJSON = intent.AddDoublePuppetValue(contentJSON)
return intent.Client.SendMessageEvent(ctx, roomID, eventType, contentJSON, mautrix.ReqSendEvent{Timestamp: ts})
return intent.SendMessageEvent(ctx, roomID, eventType, contentJSON, mautrix.ReqSendEvent{Timestamp: ts})
}
func (intent *IntentAPI) SendStateEvent(ctx context.Context, roomID id.RoomID, eventType event.Type, stateKey string, contentJSON interface{}) (*mautrix.RespSendEvent, error) {
func (intent *IntentAPI) SendStateEvent(ctx context.Context, roomID id.RoomID, eventType event.Type, stateKey string, contentJSON any, extra ...mautrix.ReqSendEvent) (*mautrix.RespSendEvent, error) {
if eventType != event.StateMember || stateKey != string(intent.UserID) {
if err := intent.EnsureJoined(ctx, roomID); err != nil {
return nil, err
}
}
contentJSON = intent.AddDoublePuppetValue(contentJSON)
return intent.Client.SendStateEvent(ctx, roomID, eventType, stateKey, contentJSON)
}
func (intent *IntentAPI) SendMassagedStateEvent(ctx context.Context, roomID id.RoomID, eventType event.Type, stateKey string, contentJSON interface{}, ts int64) (*mautrix.RespSendEvent, error) {
if err := intent.EnsureJoined(ctx, roomID); err != nil {
} else if err := intent.EnsureRegistered(ctx); err != nil {
return nil, err
}
contentJSON = intent.AddDoublePuppetValue(contentJSON)
return intent.Client.SendMassagedStateEvent(ctx, roomID, eventType, stateKey, contentJSON, ts)
return intent.Client.SendStateEvent(ctx, roomID, eventType, stateKey, contentJSON, extra...)
}
// Deprecated: use SendStateEvent with mautrix.ReqSendEvent.Timestamp instead
func (intent *IntentAPI) SendMassagedStateEvent(ctx context.Context, roomID id.RoomID, eventType event.Type, stateKey string, contentJSON interface{}, ts int64) (*mautrix.RespSendEvent, error) {
return intent.SendStateEvent(ctx, roomID, eventType, stateKey, contentJSON, mautrix.ReqSendEvent{Timestamp: ts})
}
func (intent *IntentAPI) StateEvent(ctx context.Context, roomID id.RoomID, eventType event.Type, stateKey string, outContent interface{}) error {
@ -256,7 +311,7 @@ func (intent *IntentAPI) SendCustomMembershipEvent(ctx context.Context, roomID i
func (intent *IntentAPI) JoinRoomByID(ctx context.Context, roomID id.RoomID, extraContent ...map[string]interface{}) (resp *mautrix.RespJoinRoom, err error) {
if intent.IsCustomPuppet || len(extraContent) > 0 {
_, err = intent.SendCustomMembershipEvent(ctx, roomID, intent.UserID, event.MembershipJoin, "", extraContent...)
return &mautrix.RespJoinRoom{}, err
return &mautrix.RespJoinRoom{RoomID: roomID}, err
}
return intent.Client.JoinRoomByID(ctx, roomID)
}
@ -325,6 +380,24 @@ func (intent *IntentAPI) Member(ctx context.Context, roomID id.RoomID, userID id
return member
}
func (intent *IntentAPI) FillPowerLevelCreateEvent(ctx context.Context, roomID id.RoomID, pl *event.PowerLevelsEventContent) error {
if pl.CreateEvent != nil {
return nil
}
var err error
pl.CreateEvent, err = intent.StateStore.GetCreate(ctx, roomID)
if err != nil {
return fmt.Errorf("failed to get create event from cache: %w", err)
} else if pl.CreateEvent != nil {
return nil
}
pl.CreateEvent, err = intent.FullStateEvent(ctx, roomID, event.StateCreate, "")
if err != nil {
return fmt.Errorf("failed to get create event from server: %w", err)
}
return nil
}
func (intent *IntentAPI) PowerLevels(ctx context.Context, roomID id.RoomID) (pl *event.PowerLevelsEventContent, err error) {
pl, err = intent.as.StateStore.GetPowerLevels(ctx, roomID)
if err != nil {
@ -334,6 +407,12 @@ func (intent *IntentAPI) PowerLevels(ctx context.Context, roomID id.RoomID) (pl
if pl == nil {
pl = &event.PowerLevelsEventContent{}
err = intent.StateEvent(ctx, roomID, event.StatePowerLevels, "", pl)
if err != nil {
return
}
}
if pl.CreateEvent == nil {
pl.CreateEvent, err = intent.FullStateEvent(ctx, roomID, event.StateCreate, "")
}
return
}
@ -348,8 +427,7 @@ func (intent *IntentAPI) SetPowerLevel(ctx context.Context, roomID id.RoomID, us
return nil, err
}
if pl.GetUserLevel(userID) != level {
pl.SetUserLevel(userID, level)
if pl.EnsureUserLevelAs(intent.UserID, userID, level) {
return intent.SendStateEvent(ctx, roomID, event.StatePowerLevels, "", &pl)
}
return nil, nil
@ -399,6 +477,20 @@ func (intent *IntentAPI) SetRoomTopic(ctx context.Context, roomID id.RoomID, top
})
}
func (intent *IntentAPI) UploadMedia(ctx context.Context, data mautrix.ReqUploadMedia) (*mautrix.RespMediaUpload, error) {
if err := intent.EnsureRegistered(ctx); err != nil {
return nil, err
}
return intent.Client.UploadMedia(ctx, data)
}
func (intent *IntentAPI) UploadAsync(ctx context.Context, data mautrix.ReqUploadMedia) (*mautrix.RespCreateMXC, error) {
if err := intent.EnsureRegistered(ctx); err != nil {
return nil, err
}
return intent.Client.UploadAsync(ctx, data)
}
func (intent *IntentAPI) SetDisplayName(ctx context.Context, displayName string) error {
if err := intent.EnsureRegistered(ctx); err != nil {
return err
@ -424,11 +516,11 @@ func (intent *IntentAPI) SetAvatarURL(ctx context.Context, avatarURL id.ContentU
// No need to update
return nil
}
if !avatarURL.IsEmpty() {
if !avatarURL.IsEmpty() && !intent.SpecVersions.Supports(mautrix.BeeperFeatureHungry) {
// Some homeservers require the avatar to be downloaded before setting it
body, _ := intent.Client.Download(ctx, avatarURL)
if body != nil {
_ = body.Close()
resp, _ := intent.Download(ctx, avatarURL)
if resp != nil {
_ = resp.Body.Close()
}
}
return intent.Client.SetAvatarURL(ctx, avatarURL)

68
appservice/ping.go Normal file
View file

@ -0,0 +1,68 @@
// Copyright (c) 2025 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 appservice
import (
"context"
"encoding/json"
"errors"
"os"
"strings"
"time"
"github.com/rs/zerolog"
"maunium.net/go/mautrix"
)
func (intent *IntentAPI) EnsureAppserviceConnection(ctx context.Context) {
var pingResp *mautrix.RespAppservicePing
var txnID string
var retryCount int
var err error
const maxRetries = 6
for {
txnID = intent.TxnID()
pingResp, err = intent.AppservicePing(ctx, intent.as.Registration.ID, txnID)
if err == nil {
break
}
var httpErr mautrix.HTTPError
var pingErrBody string
if errors.As(err, &httpErr) && httpErr.RespError != nil {
if val, ok := httpErr.RespError.ExtraData["body"].(string); ok {
pingErrBody = strings.TrimSpace(val)
}
}
outOfRetries := retryCount >= maxRetries
level := zerolog.ErrorLevel
if outOfRetries {
level = zerolog.FatalLevel
}
evt := zerolog.Ctx(ctx).WithLevel(level).Err(err).Str("txn_id", txnID)
if pingErrBody != "" {
bodyBytes := []byte(pingErrBody)
if json.Valid(bodyBytes) {
evt.RawJSON("body", bodyBytes)
} else {
evt.Str("body", pingErrBody)
}
}
if outOfRetries {
evt.Msg("Homeserver -> appservice connection is not working")
zerolog.Ctx(ctx).Info().Msg("See https://docs.mau.fi/faq/as-ping for more info")
os.Exit(13)
}
evt.Msg("Homeserver -> appservice connection is not working, retrying in 5 seconds...")
time.Sleep(5 * time.Second)
retryCount++
}
zerolog.Ctx(ctx).Debug().
Str("txn_id", txnID).
Int64("duration_ms", pingResp.DurationMS).
Msg("Homeserver -> appservice connection works")
}

View file

@ -1,4 +1,4 @@
// Copyright (c) 2023 Tulir Asokan
// Copyright (c) 2025 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
@ -7,9 +7,7 @@
package appservice
import (
"encoding/json"
"fmt"
"net/http"
"strings"
"github.com/rs/zerolog"
@ -103,50 +101,3 @@ func (txn *Transaction) ContentString() string {
// EventListener is a function that receives events.
type EventListener func(evt *event.Event)
// WriteBlankOK writes a blank OK message as a reply to a HTTP request.
func WriteBlankOK(w http.ResponseWriter) {
w.Header().Add("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte("{}"))
}
// Respond responds to a HTTP request with a JSON object.
func Respond(w http.ResponseWriter, data interface{}) error {
w.Header().Add("Content-Type", "application/json")
dataStr, err := json.Marshal(data)
if err != nil {
return err
}
_, err = w.Write(dataStr)
return err
}
// Error represents a Matrix protocol error.
type Error struct {
HTTPStatus int `json:"-"`
ErrorCode ErrorCode `json:"errcode"`
Message string `json:"error"`
}
func (err Error) Write(w http.ResponseWriter) {
w.Header().Add("Content-Type", "application/json")
w.WriteHeader(err.HTTPStatus)
_ = Respond(w, &err)
}
// ErrorCode is the machine-readable code in an Error.
type ErrorCode string
// Native ErrorCodes
const (
ErrUnknownToken ErrorCode = "M_UNKNOWN_TOKEN"
ErrBadJSON ErrorCode = "M_BAD_JSON"
ErrNotJSON ErrorCode = "M_NOT_JSON"
ErrUnknown ErrorCode = "M_UNKNOWN"
)
// Custom ErrorCodes
const (
ErrNoTransactionID ErrorCode = "NET.MAUNIUM.NO_TRANSACTION_ID"
)

View file

@ -27,7 +27,9 @@ type Registration struct {
Protocols []string `yaml:"protocols,omitempty" json:"protocols,omitempty"`
SoruEphemeralEvents bool `yaml:"de.sorunome.msc2409.push_ephemeral,omitempty" json:"de.sorunome.msc2409.push_ephemeral,omitempty"`
EphemeralEvents bool `yaml:"push_ephemeral,omitempty" json:"push_ephemeral,omitempty"`
EphemeralEvents bool `yaml:"receive_ephemeral,omitempty" json:"receive_ephemeral,omitempty"`
MSC3202 bool `yaml:"org.matrix.msc3202,omitempty" json:"org.matrix.msc3202,omitempty"`
MSC4190 bool `yaml:"io.element.msc4190,omitempty" json:"io.element.msc4190,omitempty"`
}
// CreateRegistration creates a Registration with random appservice and homeserver tokens.

View file

@ -1,4 +1,4 @@
// Copyright (c) 2023 Tulir Asokan
// Copyright (c) 2025 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
@ -11,26 +11,26 @@ import (
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"net/url"
"path/filepath"
"path"
"strings"
"sync"
"sync/atomic"
"time"
"github.com/gorilla/websocket"
"github.com/coder/websocket"
"github.com/rs/zerolog"
"github.com/tidwall/gjson"
"github.com/tidwall/sjson"
"maunium.net/go/mautrix"
)
type WebsocketRequest struct {
ReqID int `json:"id,omitempty"`
Command string `json:"command"`
Data interface{} `json:"data"`
Deadline time.Duration `json:"-"`
ReqID int `json:"id,omitempty"`
Command string `json:"command"`
Data any `json:"data"`
}
type WebsocketCommand struct {
@ -41,7 +41,7 @@ type WebsocketCommand struct {
Ctx context.Context `json:"-"`
}
func (wsc *WebsocketCommand) MakeResponse(ok bool, data interface{}) *WebsocketRequest {
func (wsc *WebsocketCommand) MakeResponse(ok bool, data any) *WebsocketRequest {
if wsc.ReqID == 0 || wsc.Command == "response" || wsc.Command == "error" {
return nil
}
@ -56,7 +56,7 @@ func (wsc *WebsocketCommand) MakeResponse(ok bool, data interface{}) *WebsocketR
var prefixMessage string
for unwrappedErr != nil {
errorData, jsonErr = json.Marshal(unwrappedErr)
if errorData != nil && len(errorData) > 2 && jsonErr == nil {
if len(errorData) > 2 && jsonErr == nil {
prefixMessage = strings.Replace(err.Error(), unwrappedErr.Error(), "", 1)
prefixMessage = strings.TrimRight(prefixMessage, ": ")
break
@ -98,8 +98,8 @@ type WebsocketMessage struct {
}
const (
WebsocketCloseConnReplaced = 4001
WebsocketCloseTxnNotAcknowledged = 4002
WebsocketCloseConnReplaced websocket.StatusCode = 4001
WebsocketCloseTxnNotAcknowledged websocket.StatusCode = 4002
)
type MeowWebsocketCloseCode string
@ -133,7 +133,7 @@ func (mwcc MeowWebsocketCloseCode) String() string {
}
type CloseCommand struct {
Code int `json:"-"`
Code websocket.StatusCode `json:"-"`
Command string `json:"command"`
Status MeowWebsocketCloseCode `json:"status"`
}
@ -143,15 +143,15 @@ func (cc CloseCommand) Error() string {
}
func parseCloseError(err error) error {
closeError := &websocket.CloseError{}
var closeError websocket.CloseError
if !errors.As(err, &closeError) {
return err
}
var closeCommand CloseCommand
closeCommand.Code = closeError.Code
closeCommand.Command = "disconnect"
if len(closeError.Text) > 0 {
jsonErr := json.Unmarshal([]byte(closeError.Text), &closeCommand)
if len(closeError.Reason) > 0 {
jsonErr := json.Unmarshal([]byte(closeError.Reason), &closeCommand)
if jsonErr != nil {
return err
}
@ -159,7 +159,7 @@ func parseCloseError(err error) error {
if len(closeCommand.Status) == 0 {
if closeCommand.Code == WebsocketCloseConnReplaced {
closeCommand.Status = MeowConnectionReplaced
} else if closeCommand.Code == websocket.CloseServiceRestart {
} else if closeCommand.Code == websocket.StatusServiceRestart {
closeCommand.Status = MeowServerShuttingDown
}
}
@ -170,20 +170,23 @@ func (as *AppService) HasWebsocket() bool {
return as.ws != nil
}
func (as *AppService) SendWebsocket(cmd *WebsocketRequest) error {
func (as *AppService) SendWebsocket(ctx context.Context, cmd *WebsocketRequest) error {
ws := as.ws
if cmd == nil {
return nil
} else if ws == nil {
return ErrWebsocketNotConnected
}
as.wsWriteLock.Lock()
defer as.wsWriteLock.Unlock()
if cmd.Deadline == 0 {
cmd.Deadline = 3 * time.Minute
wr, err := ws.Writer(ctx, websocket.MessageText)
if err != nil {
return err
}
_ = ws.SetWriteDeadline(time.Now().Add(cmd.Deadline))
return ws.WriteJSON(cmd)
err = json.NewEncoder(wr).Encode(cmd)
if err != nil {
_ = wr.Close()
return err
}
return wr.Close()
}
func (as *AppService) clearWebsocketResponseWaiters() {
@ -220,12 +223,12 @@ func (er *ErrorResponse) Error() string {
return fmt.Sprintf("%s: %s", er.Code, er.Message)
}
func (as *AppService) RequestWebsocket(ctx context.Context, cmd *WebsocketRequest, response interface{}) error {
func (as *AppService) RequestWebsocket(ctx context.Context, cmd *WebsocketRequest, response any) error {
cmd.ReqID = int(atomic.AddInt32(&as.websocketRequestID, 1))
respChan := make(chan *WebsocketCommand, 1)
as.addWebsocketResponseWaiter(cmd.ReqID, respChan)
defer as.removeWebsocketResponseWaiter(cmd.ReqID, respChan)
err := as.SendWebsocket(cmd)
err := as.SendWebsocket(ctx, cmd)
if err != nil {
return err
}
@ -254,7 +257,7 @@ func (as *AppService) RequestWebsocket(ctx context.Context, cmd *WebsocketReques
}
}
func (as *AppService) unknownCommandHandler(cmd WebsocketCommand) (bool, interface{}) {
func (as *AppService) unknownCommandHandler(cmd WebsocketCommand) (bool, any) {
zerolog.Ctx(cmd.Ctx).Warn().Msg("No handler for websocket command")
return false, fmt.Errorf("unknown request type")
}
@ -278,14 +281,28 @@ func (as *AppService) defaultHandleWebsocketTransaction(ctx context.Context, msg
return true, &WebsocketTransactionResponse{TxnID: msg.TxnID}
}
func (as *AppService) consumeWebsocket(stopFunc func(error), ws *websocket.Conn) {
func (as *AppService) consumeWebsocket(ctx context.Context, stopFunc func(error), ws *websocket.Conn) {
defer stopFunc(ErrWebsocketUnknownError)
ctx := context.Background()
for {
var msg WebsocketMessage
err := ws.ReadJSON(&msg)
msgType, reader, err := ws.Reader(ctx)
if err != nil {
as.Log.Debug().Err(err).Msg("Error reading from websocket")
as.Log.Debug().Err(err).Msg("Error getting reader from websocket")
stopFunc(parseCloseError(err))
return
} else if msgType != websocket.MessageText {
as.Log.Debug().Msg("Ignoring non-text message from websocket")
continue
}
data, err := io.ReadAll(reader)
if err != nil {
as.Log.Debug().Err(err).Msg("Error reading data from websocket")
stopFunc(parseCloseError(err))
return
}
var msg WebsocketMessage
err = json.Unmarshal(data, &msg)
if err != nil {
as.Log.Debug().Err(err).Msg("Error parsing JSON received from websocket")
stopFunc(parseCloseError(err))
return
}
@ -296,11 +313,11 @@ func (as *AppService) consumeWebsocket(stopFunc func(error), ws *websocket.Conn)
with = with.Str("transaction_id", msg.TxnID)
}
log := with.Logger()
ctx = log.WithContext(ctx)
ctx := log.WithContext(ctx)
if msg.Command == "" || msg.Command == "transaction" {
ok, resp := as.WebsocketTransactionHandler(ctx, msg)
go func() {
err := as.SendWebsocket(msg.MakeResponse(ok, resp))
err := as.SendWebsocket(ctx, msg.MakeResponse(ok, resp))
if err != nil {
log.Warn().Err(err).Msg("Failed to send response to websocket transaction")
} else {
@ -332,7 +349,7 @@ func (as *AppService) consumeWebsocket(stopFunc func(error), ws *websocket.Conn)
}
go func() {
okResp, data := handler(msg.WebsocketCommand)
err := as.SendWebsocket(msg.MakeResponse(okResp, data))
err := as.SendWebsocket(ctx, msg.MakeResponse(okResp, data))
if err != nil {
log.Error().Err(err).Msg("Failed to send response to websocket command")
} else if okResp {
@ -345,7 +362,7 @@ func (as *AppService) consumeWebsocket(stopFunc func(error), ws *websocket.Conn)
}
}
func (as *AppService) StartWebsocket(baseURL string, onConnect func()) error {
func (as *AppService) StartWebsocket(ctx context.Context, baseURL string, onConnect func()) error {
var parsed *url.URL
if baseURL != "" {
var err error
@ -357,26 +374,29 @@ func (as *AppService) StartWebsocket(baseURL string, onConnect func()) error {
copiedURL := *as.hsURLForClient
parsed = &copiedURL
}
parsed.Path = filepath.Join(parsed.Path, "_matrix/client/unstable/fi.mau.as_sync")
parsed.Path = path.Join(parsed.Path, "_matrix/client/unstable/fi.mau.as_sync")
if parsed.Scheme == "http" {
parsed.Scheme = "ws"
} else if parsed.Scheme == "https" {
parsed.Scheme = "wss"
}
ws, resp, err := websocket.DefaultDialer.Dial(parsed.String(), http.Header{
"Authorization": []string{fmt.Sprintf("Bearer %s", as.Registration.AppToken)},
"User-Agent": []string{as.BotClient().UserAgent},
ws, resp, err := websocket.Dial(ctx, parsed.String(), &websocket.DialOptions{
HTTPClient: as.HTTPClient,
HTTPHeader: http.Header{
"Authorization": []string{fmt.Sprintf("Bearer %s", as.Registration.AppToken)},
"User-Agent": []string{as.BotClient().UserAgent},
"X-Mautrix-Process-ID": []string{as.ProcessID},
"X-Mautrix-Websocket-Version": []string{"3"},
"X-Mautrix-Process-ID": []string{as.ProcessID},
"X-Mautrix-Websocket-Version": []string{"3"},
},
})
if resp != nil && resp.StatusCode >= 400 {
var errResp Error
var errResp mautrix.RespError
err = json.NewDecoder(resp.Body).Decode(&errResp)
if err != nil {
return fmt.Errorf("websocket request returned HTTP %d with non-JSON body", resp.StatusCode)
} else {
return fmt.Errorf("websocket request returned %s (HTTP %d): %s", errResp.ErrorCode, resp.StatusCode, errResp.Message)
return fmt.Errorf("websocket request returned %s (HTTP %d): %s", errResp.ErrCode, resp.StatusCode, errResp.Err)
}
} else if err != nil {
return fmt.Errorf("failed to open websocket: %w", err)
@ -399,12 +419,13 @@ func (as *AppService) StartWebsocket(baseURL string, onConnect func()) error {
}
})
}
ws.SetReadLimit(50 * 1024 * 1024)
as.ws = ws
as.StopWebsocket = stopFunc
as.PrepareWebsocket()
as.Log.Debug().Msg("Appservice transaction websocket opened")
go as.consumeWebsocket(stopFunc, ws)
go as.consumeWebsocket(ctx, stopFunc, ws)
var onConnectDone atomic.Bool
if onConnect != nil {
@ -426,12 +447,7 @@ func (as *AppService) StartWebsocket(baseURL string, onConnect func()) error {
as.ws = nil
}
_ = ws.SetWriteDeadline(time.Now().Add(3 * time.Second))
err = ws.WriteMessage(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseGoingAway, ""))
if err != nil && !errors.Is(err, websocket.ErrCloseSent) {
as.Log.Warn().Err(err).Msg("Error writing close message to websocket")
}
err = ws.Close()
err = ws.Close(websocket.StatusGoingAway, "")
if err != nil {
as.Log.Warn().Err(err).Msg("Error closing websocket")
}

View file

@ -61,6 +61,11 @@ func (as *AppService) WebsocketHTTPProxy(cmd WebsocketCommand) (bool, interface{
if err != nil {
return false, fmt.Errorf("failed to create fake HTTP request: %w", err)
}
httpReq.RequestURI = req.Path
if req.Query != "" {
httpReq.RequestURI += "?" + req.Query
}
httpReq.RemoteAddr = "websocket"
httpReq.Header = req.Headers
var resp HTTPProxyResponse

View file

@ -1,872 +0,0 @@
// 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 bridge
import (
"context"
"encoding/json"
"errors"
"fmt"
"net/url"
"os"
"os/signal"
"runtime"
"strings"
"sync"
"syscall"
"time"
"github.com/lib/pq"
"github.com/mattn/go-sqlite3"
"github.com/rs/zerolog"
deflog "github.com/rs/zerolog/log"
"go.mau.fi/util/configupgrade"
"go.mau.fi/util/dbutil"
_ "go.mau.fi/util/dbutil/litestream"
"go.mau.fi/util/exzerolog"
"gopkg.in/yaml.v3"
flag "maunium.net/go/mauflag"
"maunium.net/go/maulogger/v2"
"maunium.net/go/maulogger/v2/maulogadapt"
"maunium.net/go/mautrix"
"maunium.net/go/mautrix/appservice"
"maunium.net/go/mautrix/bridge/bridgeconfig"
"maunium.net/go/mautrix/bridge/status"
"maunium.net/go/mautrix/event"
"maunium.net/go/mautrix/id"
"maunium.net/go/mautrix/sqlstatestore"
)
var configPath = flag.MakeFull("c", "config", "The path to your config file.", "config.yaml").String()
var dontSaveConfig = flag.MakeFull("n", "no-update", "Don't save updated config to disk.", "false").Bool()
var registrationPath = flag.MakeFull("r", "registration", "The path where to save the appservice registration.", "registration.yaml").String()
var generateRegistration = flag.MakeFull("g", "generate-registration", "Generate registration and quit.", "false").Bool()
var version = flag.MakeFull("v", "version", "View bridge version and quit.", "false").Bool()
var versionJSON = flag.Make().LongKey("version-json").Usage("Print a JSON object representing the bridge version and quit.").Default("false").Bool()
var ignoreUnsupportedDatabase = flag.Make().LongKey("ignore-unsupported-database").Usage("Run even if the database schema is too new").Default("false").Bool()
var ignoreForeignTables = flag.Make().LongKey("ignore-foreign-tables").Usage("Run even if the database contains tables from other programs (like Synapse)").Default("false").Bool()
var wantHelp, _ = flag.MakeHelpFlag()
var _ appservice.StateStore = (*sqlstatestore.SQLStateStore)(nil)
type Portal interface {
IsEncrypted() bool
IsPrivateChat() bool
MarkEncrypted()
MainIntent() *appservice.IntentAPI
ReceiveMatrixEvent(user User, evt *event.Event)
UpdateBridgeInfo(ctx context.Context)
}
type MembershipHandlingPortal interface {
Portal
HandleMatrixLeave(sender User, evt *event.Event)
HandleMatrixKick(sender User, ghost Ghost, evt *event.Event)
HandleMatrixInvite(sender User, ghost Ghost, evt *event.Event)
}
type ReadReceiptHandlingPortal interface {
Portal
HandleMatrixReadReceipt(sender User, eventID id.EventID, receipt event.ReadReceipt)
}
type TypingPortal interface {
Portal
HandleMatrixTyping(userIDs []id.UserID)
}
type MetaHandlingPortal interface {
Portal
HandleMatrixMeta(sender User, evt *event.Event)
}
type DisappearingPortal interface {
Portal
ScheduleDisappearing()
}
type User interface {
GetPermissionLevel() bridgeconfig.PermissionLevel
IsLoggedIn() bool
GetManagementRoomID() id.RoomID
SetManagementRoom(id.RoomID)
GetMXID() id.UserID
GetIDoublePuppet() DoublePuppet
GetIGhost() Ghost
}
type DoublePuppet interface {
CustomIntent() *appservice.IntentAPI
SwitchCustomMXID(accessToken string, userID id.UserID) error
ClearCustomMXID()
}
type Ghost interface {
DoublePuppet
DefaultIntent() *appservice.IntentAPI
GetMXID() id.UserID
}
type GhostWithProfile interface {
Ghost
GetDisplayname() string
GetAvatarURL() id.ContentURI
}
type ChildOverride interface {
GetExampleConfig() string
GetConfigPtr() interface{}
Init()
Start()
Stop()
GetIPortal(id.RoomID) Portal
GetAllIPortals() []Portal
GetIUser(id id.UserID, create bool) User
IsGhost(id.UserID) bool
GetIGhost(id.UserID) Ghost
CreatePrivatePortal(id.RoomID, User, Ghost)
}
type ConfigValidatingBridge interface {
ChildOverride
ValidateConfig() error
}
type FlagHandlingBridge interface {
ChildOverride
HandleFlags() bool
}
type PreInitableBridge interface {
ChildOverride
PreInit()
}
type WebsocketStartingBridge interface {
ChildOverride
OnWebsocketConnect()
}
type CSFeatureRequirer interface {
CheckFeatures(versions *mautrix.RespVersions) (string, bool)
}
type Bridge struct {
Name string
URL string
Description string
Version string
ProtocolName string
BeeperServiceName string
BeeperNetworkName string
AdditionalShortFlags string
AdditionalLongFlags string
VersionDesc string
LinkifiedVersion string
BuildTime string
commit string
baseVersion string
PublicHSAddress *url.URL
DoublePuppet *doublePuppetUtil
AS *appservice.AppService
EventProcessor *appservice.EventProcessor
CommandProcessor CommandProcessor
MatrixHandler *MatrixHandler
Bot *appservice.IntentAPI
Config bridgeconfig.BaseConfig
ConfigPath string
RegistrationPath string
SaveConfig bool
ConfigUpgrader configupgrade.BaseUpgrader
DB *dbutil.Database
StateStore *sqlstatestore.SQLStateStore
Crypto Crypto
CryptoPickleKey string
// Deprecated: Switch to ZLog
Log maulogger.Logger
ZLog *zerolog.Logger
MediaConfig mautrix.RespMediaConfig
SpecVersions mautrix.RespVersions
Child ChildOverride
manualStop chan int
Stopping bool
latestState *status.BridgeState
Websocket bool
wsStopPinger chan struct{}
wsStarted chan struct{}
wsStopped chan struct{}
wsShortCircuitReconnectBackoff chan struct{}
wsStartupWait *sync.WaitGroup
}
type Crypto interface {
HandleMemberEvent(context.Context, *event.Event)
Decrypt(context.Context, *event.Event) (*event.Event, error)
Encrypt(context.Context, id.RoomID, event.Type, *event.Content) 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)
ResetSession(context.Context, id.RoomID)
Init(ctx context.Context) error
Start()
Stop()
Reset(ctx context.Context, startAfterReset bool)
Client() *mautrix.Client
ShareKeys(context.Context) error
}
func (br *Bridge) GenerateRegistration() {
if !br.SaveConfig {
// We need to save the generated as_token and hs_token in the config
_, _ = fmt.Fprintln(os.Stderr, "--no-update is not compatible with --generate-registration")
os.Exit(5)
} else if br.Config.Homeserver.Domain == "example.com" {
_, _ = fmt.Fprintln(os.Stderr, "Homeserver domain is not set")
os.Exit(20)
}
reg := br.Config.GenerateRegistration()
err := reg.Save(br.RegistrationPath)
if err != nil {
_, _ = fmt.Fprintln(os.Stderr, "Failed to save registration:", err)
os.Exit(21)
}
updateTokens := func(helper *configupgrade.Helper) {
helper.Set(configupgrade.Str, reg.AppToken, "appservice", "as_token")
helper.Set(configupgrade.Str, reg.ServerToken, "appservice", "hs_token")
}
_, _, err = configupgrade.Do(br.ConfigPath, true, br.ConfigUpgrader, configupgrade.SimpleUpgrader(updateTokens))
if err != nil {
_, _ = fmt.Fprintln(os.Stderr, "Failed to save config:", err)
os.Exit(22)
}
fmt.Println("Registration generated. See https://docs.mau.fi/bridges/general/registering-appservices.html for instructions on installing the registration.")
os.Exit(0)
}
func (br *Bridge) InitVersion(tag, commit, buildTime string) {
br.baseVersion = br.Version
if len(tag) > 0 && tag[0] == 'v' {
tag = tag[1:]
}
if tag != br.Version {
suffix := ""
if !strings.HasSuffix(br.Version, "+dev") {
suffix = "+dev"
}
if len(commit) > 8 {
br.Version = fmt.Sprintf("%s%s.%s", br.Version, suffix, commit[:8])
} else {
br.Version = fmt.Sprintf("%s%s.unknown", br.Version, suffix)
}
}
br.LinkifiedVersion = fmt.Sprintf("v%s", br.Version)
if tag == br.Version {
br.LinkifiedVersion = fmt.Sprintf("[v%s](%s/releases/v%s)", br.Version, br.URL, tag)
} else if len(commit) > 8 {
br.LinkifiedVersion = strings.Replace(br.LinkifiedVersion, commit[:8], fmt.Sprintf("[%s](%s/commit/%s)", commit[:8], br.URL, commit), 1)
}
mautrix.DefaultUserAgent = fmt.Sprintf("%s/%s %s", br.Name, br.Version, mautrix.DefaultUserAgent)
br.VersionDesc = fmt.Sprintf("%s %s (%s with %s)", br.Name, br.Version, buildTime, runtime.Version())
br.commit = commit
br.BuildTime = buildTime
}
var MinSpecVersion = mautrix.SpecV11
func (br *Bridge) ensureConnection(ctx context.Context) {
for {
versions, err := br.Bot.Versions(ctx)
if err != nil {
br.ZLog.Err(err).Msg("Failed to connect to homeserver, retrying in 10 seconds...")
time.Sleep(10 * time.Second)
} else {
br.SpecVersions = *versions
break
}
}
if br.Config.Homeserver.Software == bridgeconfig.SoftwareHungry && !br.SpecVersions.Supports(mautrix.BeeperFeatureHungry) {
br.ZLog.WithLevel(zerolog.FatalLevel).Msg("The config claims the homeserver is hungryserv, but the /versions response didn't confirm it")
os.Exit(18)
} else if !br.SpecVersions.ContainsGreaterOrEqual(MinSpecVersion) {
br.ZLog.WithLevel(zerolog.FatalLevel).
Stringer("server_supports", br.SpecVersions.GetLatest()).
Stringer("bridge_requires", MinSpecVersion).
Msg("The homeserver is outdated (supported spec versions are below minimum required by bridge)")
os.Exit(18)
} else if fr, ok := br.Child.(CSFeatureRequirer); ok {
if msg, hasFeatures := fr.CheckFeatures(&br.SpecVersions); !hasFeatures {
br.ZLog.WithLevel(zerolog.FatalLevel).Msg(msg)
os.Exit(18)
}
}
resp, err := br.Bot.Whoami(ctx)
if err != nil {
if errors.Is(err, mautrix.MUnknownToken) {
br.ZLog.WithLevel(zerolog.FatalLevel).Msg("The as_token was not accepted. Is the registration file installed in your homeserver correctly?")
} else if errors.Is(err, mautrix.MExclusive) {
br.ZLog.WithLevel(zerolog.FatalLevel).Msg("The as_token was accepted, but the /register request was not. Are the homeserver domain, bot username and username template in the config correct, and do they match the values in the registration?")
} else {
br.ZLog.WithLevel(zerolog.FatalLevel).Err(err).Msg("/whoami request failed with unknown error")
}
os.Exit(16)
} else if resp.UserID != br.Bot.UserID {
br.ZLog.WithLevel(zerolog.FatalLevel).
Stringer("got_user_id", resp.UserID).
Stringer("expected_user_id", br.Bot.UserID).
Msg("Unexpected user ID in whoami call")
os.Exit(17)
}
if br.Websocket {
br.ZLog.Debug().Msg("Websocket mode: no need to check status of homeserver -> bridge connection")
return
} else if !br.SpecVersions.Supports(mautrix.FeatureAppservicePing) {
br.ZLog.Debug().Msg("Homeserver does not support checking status of homeserver -> bridge connection")
return
}
var pingResp *mautrix.RespAppservicePing
var txnID string
var retryCount int
const maxRetries = 6
for {
txnID = br.Bot.TxnID()
pingResp, err = br.Bot.AppservicePing(ctx, br.Config.AppService.ID, txnID)
if err == nil {
break
}
var httpErr mautrix.HTTPError
var pingErrBody string
if errors.As(err, &httpErr) && httpErr.RespError != nil {
if val, ok := httpErr.RespError.ExtraData["body"].(string); ok {
pingErrBody = strings.TrimSpace(val)
}
}
outOfRetries := retryCount >= maxRetries
level := zerolog.ErrorLevel
if outOfRetries {
level = zerolog.FatalLevel
}
evt := br.ZLog.WithLevel(level).Err(err).Str("txn_id", txnID)
if pingErrBody != "" {
bodyBytes := []byte(pingErrBody)
if json.Valid(bodyBytes) {
evt.RawJSON("body", bodyBytes)
} else {
evt.Str("body", pingErrBody)
}
}
if outOfRetries {
evt.Msg("Homeserver -> bridge connection is not working")
os.Exit(13)
}
evt.Msg("Homeserver -> bridge connection is not working, retrying in 5 seconds...")
time.Sleep(5 * time.Second)
retryCount++
}
br.ZLog.Debug().
Str("txn_id", txnID).
Int64("duration_ms", pingResp.DurationMS).
Msg("Homeserver -> bridge connection works")
}
func (br *Bridge) fetchMediaConfig(ctx context.Context) {
cfg, err := br.Bot.GetMediaConfig(ctx)
if err != nil {
br.ZLog.Warn().Err(err).Msg("Failed to fetch media config")
} else {
br.MediaConfig = *cfg
}
}
func (br *Bridge) UpdateBotProfile(ctx context.Context) {
br.ZLog.Debug().Msg("Updating bot profile")
botConfig := &br.Config.AppService.Bot
var err error
var mxc id.ContentURI
if botConfig.Avatar == "remove" {
err = br.Bot.SetAvatarURL(ctx, mxc)
} else if !botConfig.ParsedAvatar.IsEmpty() {
err = br.Bot.SetAvatarURL(ctx, botConfig.ParsedAvatar)
}
if err != nil {
br.ZLog.Warn().Err(err).Msg("Failed to update bot avatar")
}
if botConfig.Displayname == "remove" {
err = br.Bot.SetDisplayName(ctx, "")
} else if len(botConfig.Displayname) > 0 {
err = br.Bot.SetDisplayName(ctx, botConfig.Displayname)
}
if err != nil {
br.ZLog.Warn().Err(err).Msg("Failed to update bot displayname")
}
if br.SpecVersions.Supports(mautrix.BeeperFeatureArbitraryProfileMeta) && br.BeeperNetworkName != "" {
br.ZLog.Debug().Msg("Setting contact info on the appservice bot")
br.Bot.BeeperUpdateProfile(ctx, map[string]any{
"com.beeper.bridge.service": br.BeeperServiceName,
"com.beeper.bridge.network": br.BeeperNetworkName,
"com.beeper.bridge.is_bridge_bot": true,
})
}
}
func (br *Bridge) loadConfig() {
configData, upgraded, err := configupgrade.Do(br.ConfigPath, br.SaveConfig, br.ConfigUpgrader)
if err != nil {
_, _ = fmt.Fprintln(os.Stderr, "Error updating config:", err)
if configData == nil {
os.Exit(10)
}
}
target := br.Child.GetConfigPtr()
if !upgraded {
// Fallback: if config upgrading failed, load example config for base values
err = yaml.Unmarshal([]byte(br.Child.GetExampleConfig()), &target)
if err != nil {
_, _ = fmt.Fprintln(os.Stderr, "Failed to unmarshal example config:", err)
os.Exit(10)
}
}
err = yaml.Unmarshal(configData, target)
if err != nil {
_, _ = fmt.Fprintln(os.Stderr, "Failed to parse config:", err)
os.Exit(10)
}
}
func (br *Bridge) validateConfig() error {
switch {
case br.Config.Homeserver.Address == "https://matrix.example.com":
return errors.New("homeserver.address not configured")
case br.Config.Homeserver.Domain == "example.com":
return errors.New("homeserver.domain not configured")
case !bridgeconfig.AllowedHomeserverSoftware[br.Config.Homeserver.Software]:
return errors.New("invalid value for homeserver.software (use `standard` if you don't know what the field is for)")
case br.Config.AppService.ASToken == "This value is generated when generating the registration":
return errors.New("appservice.as_token not configured. Did you forget to generate the registration? ")
case br.Config.AppService.HSToken == "This value is generated when generating the registration":
return errors.New("appservice.hs_token not configured. Did you forget to generate the registration? ")
case br.Config.AppService.Database.URI == "postgres://user:password@host/database?sslmode=disable":
return errors.New("appservice.database not configured")
default:
err := br.Config.Bridge.Validate()
if err != nil {
return err
}
validator, ok := br.Child.(ConfigValidatingBridge)
if ok {
return validator.ValidateConfig()
}
return nil
}
}
func (br *Bridge) getProfile(userID id.UserID, roomID id.RoomID) *event.MemberEventContent {
ghost := br.Child.GetIGhost(userID)
if ghost == nil {
return nil
}
profilefulGhost, ok := ghost.(GhostWithProfile)
if ok {
return &event.MemberEventContent{
Displayname: profilefulGhost.GetDisplayname(),
AvatarURL: profilefulGhost.GetAvatarURL().CUString(),
}
}
return nil
}
func (br *Bridge) init() {
pib, ok := br.Child.(PreInitableBridge)
if ok {
pib.PreInit()
}
var err error
br.MediaConfig.UploadSize = 50 * 1024 * 1024
br.ZLog, err = br.Config.Logging.Compile()
if err != nil {
_, _ = fmt.Fprintln(os.Stderr, "Failed to initialize logger:", err)
os.Exit(12)
}
defaultCtxLog := br.ZLog.With().Bool("default_context_log", true).Caller().Logger()
zerolog.TimeFieldFormat = time.RFC3339Nano
zerolog.CallerMarshalFunc = exzerolog.CallerWithFunctionName
zerolog.DefaultContextLogger = &defaultCtxLog
deflog.Logger = br.ZLog.With().Bool("global_log", true).Caller().Logger()
br.Log = maulogadapt.ZeroAsMau(br.ZLog)
br.DoublePuppet = &doublePuppetUtil{br: br, log: br.ZLog.With().Str("component", "double puppet").Logger()}
err = br.validateConfig()
if err != nil {
br.ZLog.WithLevel(zerolog.FatalLevel).Err(err).Msg("Configuration error")
os.Exit(11)
}
br.ZLog.Info().
Str("name", br.Name).
Str("version", br.Version).
Str("built_at", br.BuildTime).
Str("go_version", runtime.Version()).
Msg("Initializing bridge")
br.ZLog.Debug().Msg("Initializing database connection")
dbConfig := br.Config.AppService.Database
if (dbConfig.Type == "sqlite3-fk-wal" || dbConfig.Type == "litestream") && dbConfig.MaxOpenConns != 1 && !strings.Contains(dbConfig.URI, "_txlock=immediate") {
var fixedExampleURI string
if !strings.HasPrefix(dbConfig.URI, "file:") {
fixedExampleURI = fmt.Sprintf("file:%s?_txlock=immediate", dbConfig.URI)
} else if !strings.ContainsRune(dbConfig.URI, '?') {
fixedExampleURI = fmt.Sprintf("%s?_txlock=immediate", dbConfig.URI)
} else {
fixedExampleURI = fmt.Sprintf("%s&_txlock=immediate", dbConfig.URI)
}
br.ZLog.Warn().
Str("fixed_uri_example", fixedExampleURI).
Msg("Using SQLite without _txlock=immediate is not recommended")
}
br.DB, err = dbutil.NewFromConfig(br.Name, dbConfig, dbutil.ZeroLogger(br.ZLog.With().Str("db_section", "main").Logger()))
if err != nil {
br.ZLog.WithLevel(zerolog.FatalLevel).Err(err).Msg("Failed to initialize database connection")
if sqlError := (&sqlite3.Error{}); errors.As(err, sqlError) && sqlError.Code == sqlite3.ErrCorrupt {
os.Exit(18)
}
os.Exit(14)
}
br.DB.IgnoreUnsupportedDatabase = *ignoreUnsupportedDatabase
br.DB.IgnoreForeignTables = *ignoreForeignTables
br.ZLog.Debug().Msg("Initializing state store")
br.StateStore = sqlstatestore.NewSQLStateStore(br.DB, dbutil.ZeroLogger(br.ZLog.With().Str("db_section", "matrix_state").Logger()), true)
br.AS = br.Config.MakeAppService()
br.AS.DoublePuppetValue = br.Name
br.AS.GetProfile = br.getProfile
br.AS.Log = *br.ZLog
br.AS.StateStore = br.StateStore
br.Bot = br.AS.BotIntent()
br.ZLog.Debug().Msg("Initializing Matrix event processor")
br.EventProcessor = appservice.NewEventProcessor(br.AS)
if !br.Config.AppService.AsyncTransactions {
br.EventProcessor.ExecMode = appservice.Sync
}
br.ZLog.Debug().Msg("Initializing Matrix event handler")
br.MatrixHandler = NewMatrixHandler(br)
br.Crypto = NewCryptoHelper(br)
hsURL := br.Config.Homeserver.Address
if br.Config.Homeserver.PublicAddress != "" {
hsURL = br.Config.Homeserver.PublicAddress
}
br.PublicHSAddress, err = url.Parse(hsURL)
if err != nil {
br.ZLog.WithLevel(zerolog.FatalLevel).Err(err).
Str("input", hsURL).
Msg("Failed to parse public homeserver URL")
os.Exit(15)
}
br.Child.Init()
}
type zerologPQError pq.Error
func (zpe *zerologPQError) MarshalZerologObject(evt *zerolog.Event) {
maybeStr := func(field, value string) {
if value != "" {
evt.Str(field, value)
}
}
maybeStr("severity", zpe.Severity)
if name := zpe.Code.Name(); name != "" {
evt.Str("code", name)
} else if zpe.Code != "" {
evt.Str("code", string(zpe.Code))
}
//maybeStr("message", zpe.Message)
maybeStr("detail", zpe.Detail)
maybeStr("hint", zpe.Hint)
maybeStr("position", zpe.Position)
maybeStr("internal_position", zpe.InternalPosition)
maybeStr("internal_query", zpe.InternalQuery)
maybeStr("where", zpe.Where)
maybeStr("schema", zpe.Schema)
maybeStr("table", zpe.Table)
maybeStr("column", zpe.Column)
maybeStr("data_type_name", zpe.DataTypeName)
maybeStr("constraint", zpe.Constraint)
maybeStr("file", zpe.File)
maybeStr("line", zpe.Line)
maybeStr("routine", zpe.Routine)
}
func (br *Bridge) LogDBUpgradeErrorAndExit(name string, err error) {
logEvt := br.ZLog.WithLevel(zerolog.FatalLevel).
Err(err).
Str("db_section", name)
var errWithLine *dbutil.PQErrorWithLine
if errors.As(err, &errWithLine) {
logEvt.Str("sql_line", errWithLine.Line)
}
var pqe *pq.Error
if errors.As(err, &pqe) {
logEvt.Object("pq_error", (*zerologPQError)(pqe))
}
logEvt.Msg("Failed to initialize database")
if sqlError := (&sqlite3.Error{}); errors.As(err, sqlError) && sqlError.Code == sqlite3.ErrCorrupt {
os.Exit(18)
} else if errors.Is(err, dbutil.ErrForeignTables) {
br.ZLog.Info().Msg("You can use --ignore-foreign-tables to ignore this error")
} else if errors.Is(err, dbutil.ErrNotOwned) {
br.ZLog.Info().Msg("Sharing the same database with different programs is not supported")
} else if errors.Is(err, dbutil.ErrUnsupportedDatabaseVersion) {
br.ZLog.Info().Msg("Downgrading the bridge is not supported")
}
os.Exit(15)
}
func (br *Bridge) WaitWebsocketConnected() {
if br.wsStartupWait != nil {
br.wsStartupWait.Wait()
}
}
func (br *Bridge) start() {
br.ZLog.Debug().Msg("Running database upgrades")
err := br.DB.Upgrade(br.ZLog.With().Str("db_section", "main").Logger().WithContext(context.TODO()))
if err != nil {
br.LogDBUpgradeErrorAndExit("main", err)
} else if err = br.StateStore.Upgrade(br.ZLog.With().Str("db_section", "matrix_state").Logger().WithContext(context.TODO())); err != nil {
br.LogDBUpgradeErrorAndExit("matrix_state", err)
}
if br.Config.Homeserver.Websocket || len(br.Config.Homeserver.WSProxy) > 0 {
br.Websocket = true
br.ZLog.Debug().Msg("Starting application service websocket")
var wg sync.WaitGroup
wg.Add(1)
br.wsStartupWait = &wg
br.wsShortCircuitReconnectBackoff = make(chan struct{})
go br.startWebsocket(&wg)
} else if br.AS.Host.IsConfigured() {
br.ZLog.Debug().Msg("Starting application service HTTP server")
go br.AS.Start()
} else {
br.ZLog.WithLevel(zerolog.FatalLevel).Msg("Neither appservice HTTP listener nor websocket is enabled")
os.Exit(23)
}
br.ZLog.Debug().Msg("Checking connection to homeserver")
ctx := br.ZLog.WithContext(context.Background())
br.ensureConnection(ctx)
go br.fetchMediaConfig(ctx)
if br.Crypto != nil {
err = br.Crypto.Init(ctx)
if err != nil {
br.ZLog.WithLevel(zerolog.FatalLevel).Err(err).Msg("Error initializing end-to-bridge encryption")
os.Exit(19)
}
}
br.ZLog.Debug().Msg("Starting event processor")
br.EventProcessor.Start(ctx)
go br.UpdateBotProfile(ctx)
if br.Crypto != nil {
go br.Crypto.Start()
}
br.Child.Start()
br.WaitWebsocketConnected()
br.AS.Ready = true
if br.Config.Bridge.GetResendBridgeInfo() {
go br.ResendBridgeInfo()
}
if br.Websocket && br.Config.Homeserver.WSPingInterval > 0 {
br.wsStopPinger = make(chan struct{}, 1)
go br.websocketServerPinger()
}
}
func (br *Bridge) ResendBridgeInfo() {
if !br.SaveConfig {
br.ZLog.Warn().Msg("Not setting resend_bridge_info to false in config due to --no-update flag")
} else {
_, _, err := configupgrade.Do(br.ConfigPath, true, br.ConfigUpgrader, configupgrade.SimpleUpgrader(func(helper *configupgrade.Helper) {
helper.Set(configupgrade.Bool, "false", "bridge", "resend_bridge_info")
}))
if err != nil {
br.ZLog.Err(err).Msg("Failed to save config after setting resend_bridge_info to false")
}
}
br.ZLog.Info().Msg("Re-sending bridge info state event to all portals")
for _, portal := range br.Child.GetAllIPortals() {
portal.UpdateBridgeInfo(context.TODO())
}
br.ZLog.Info().Msg("Finished re-sending bridge info state events")
}
func sendStopSignal(ch chan struct{}) {
if ch != nil {
select {
case ch <- struct{}{}:
default:
}
}
}
func (br *Bridge) stop() {
br.Stopping = true
if br.Crypto != nil {
br.Crypto.Stop()
}
waitForWS := false
if br.AS.StopWebsocket != nil {
br.ZLog.Debug().Msg("Stopping application service websocket")
br.AS.StopWebsocket(appservice.ErrWebsocketManualStop)
waitForWS = true
}
br.AS.Stop()
sendStopSignal(br.wsStopPinger)
sendStopSignal(br.wsShortCircuitReconnectBackoff)
br.EventProcessor.Stop()
br.Child.Stop()
err := br.DB.Close()
if err != nil {
br.ZLog.Warn().Err(err).Msg("Error closing database")
}
if waitForWS {
select {
case <-br.wsStopped:
case <-time.After(4 * time.Second):
br.ZLog.Warn().Msg("Timed out waiting for websocket to close")
}
}
}
func (br *Bridge) ManualStop(exitCode int) {
if br.manualStop != nil {
br.manualStop <- exitCode
} else {
os.Exit(exitCode)
}
}
type VersionJSONOutput struct {
Name string
URL string
Version string
IsRelease bool
Commit string
FormattedVersion string
BuildTime string
OS string
Arch string
Mautrix struct {
Version string
Commit string
}
}
func (br *Bridge) Main() {
flag.SetHelpTitles(
fmt.Sprintf("%s - %s", br.Name, br.Description),
fmt.Sprintf("%s [-hgvn%s] [-c <path>] [-r <path>]%s", br.Name, br.AdditionalShortFlags, br.AdditionalLongFlags))
err := flag.Parse()
br.ConfigPath = *configPath
br.RegistrationPath = *registrationPath
br.SaveConfig = !*dontSaveConfig
if err != nil {
_, _ = fmt.Fprintln(os.Stderr, err)
flag.PrintHelp()
os.Exit(1)
} else if *wantHelp {
flag.PrintHelp()
os.Exit(0)
} else if *version {
fmt.Println(br.VersionDesc)
return
} else if *versionJSON {
output := VersionJSONOutput{
URL: br.URL,
Name: br.Name,
Version: br.baseVersion,
IsRelease: br.Version == br.baseVersion,
Commit: br.commit,
FormattedVersion: br.Version,
BuildTime: br.BuildTime,
OS: runtime.GOOS,
Arch: runtime.GOARCH,
}
output.Mautrix.Commit = mautrix.Commit
output.Mautrix.Version = mautrix.Version
_ = json.NewEncoder(os.Stdout).Encode(output)
return
} else if flagHandler, ok := br.Child.(FlagHandlingBridge); ok && flagHandler.HandleFlags() {
return
}
br.loadConfig()
if *generateRegistration {
br.GenerateRegistration()
return
}
br.manualStop = make(chan int, 1)
br.init()
br.ZLog.Info().Msg("Bridge initialization complete, starting...")
br.start()
br.ZLog.Info().Msg("Bridge started!")
c := make(chan os.Signal, 1)
signal.Notify(c, os.Interrupt, syscall.SIGTERM)
var exitCode int
select {
case <-c:
br.ZLog.Info().Msg("Interrupt received, stopping...")
case exitCode = <-br.manualStop:
br.ZLog.Info().Int("exit_code", exitCode).Msg("Manual stop requested")
}
br.stop()
br.ZLog.Info().Msg("Bridge stopped.")
os.Exit(exitCode)
}

View file

@ -1,338 +0,0 @@
// 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 bridgeconfig
import (
"fmt"
"os"
"path/filepath"
"regexp"
"strings"
"github.com/rs/zerolog"
up "go.mau.fi/util/configupgrade"
"go.mau.fi/util/dbutil"
"go.mau.fi/util/random"
"go.mau.fi/zeroconfig"
"gopkg.in/yaml.v3"
"maunium.net/go/mautrix/appservice"
"maunium.net/go/mautrix/id"
)
type HomeserverSoftware string
const (
SoftwareStandard HomeserverSoftware = "standard"
SoftwareAsmux HomeserverSoftware = "asmux"
SoftwareHungry HomeserverSoftware = "hungry"
)
var AllowedHomeserverSoftware = map[HomeserverSoftware]bool{
SoftwareStandard: true,
SoftwareAsmux: true,
SoftwareHungry: true,
}
type HomeserverConfig struct {
Address string `yaml:"address"`
Domain string `yaml:"domain"`
AsyncMedia bool `yaml:"async_media"`
PublicAddress string `yaml:"public_address,omitempty"`
Software HomeserverSoftware `yaml:"software"`
StatusEndpoint string `yaml:"status_endpoint"`
MessageSendCheckpointEndpoint string `yaml:"message_send_checkpoint_endpoint"`
Websocket bool `yaml:"websocket"`
WSProxy string `yaml:"websocket_proxy"`
WSPingInterval int `yaml:"ping_interval_seconds"`
}
type AppserviceConfig struct {
Address string `yaml:"address"`
Hostname string `yaml:"hostname"`
Port uint16 `yaml:"port"`
Database dbutil.Config `yaml:"database"`
ID string `yaml:"id"`
Bot BotUserConfig `yaml:"bot"`
ASToken string `yaml:"as_token"`
HSToken string `yaml:"hs_token"`
EphemeralEvents bool `yaml:"ephemeral_events"`
AsyncTransactions bool `yaml:"async_transactions"`
}
func (config *BaseConfig) MakeUserIDRegex(matcher string) *regexp.Regexp {
usernamePlaceholder := strings.ToLower(random.String(16))
usernameTemplate := fmt.Sprintf("@%s:%s",
config.Bridge.FormatUsername(usernamePlaceholder),
config.Homeserver.Domain)
usernameTemplate = regexp.QuoteMeta(usernameTemplate)
usernameTemplate = strings.Replace(usernameTemplate, usernamePlaceholder, matcher, 1)
usernameTemplate = fmt.Sprintf("^%s$", usernameTemplate)
return regexp.MustCompile(usernameTemplate)
}
// GenerateRegistration generates a registration file for the homeserver.
func (config *BaseConfig) GenerateRegistration() *appservice.Registration {
registration := appservice.CreateRegistration()
config.AppService.HSToken = registration.ServerToken
config.AppService.ASToken = registration.AppToken
config.AppService.copyToRegistration(registration)
registration.SenderLocalpart = random.String(32)
botRegex := regexp.MustCompile(fmt.Sprintf("^@%s:%s$",
regexp.QuoteMeta(config.AppService.Bot.Username),
regexp.QuoteMeta(config.Homeserver.Domain)))
registration.Namespaces.UserIDs.Register(botRegex, true)
registration.Namespaces.UserIDs.Register(config.MakeUserIDRegex(".*"), true)
return registration
}
func (config *BaseConfig) MakeAppService() *appservice.AppService {
as := appservice.Create()
as.HomeserverDomain = config.Homeserver.Domain
_ = as.SetHomeserverURL(config.Homeserver.Address)
as.Host.Hostname = config.AppService.Hostname
as.Host.Port = config.AppService.Port
as.DefaultHTTPRetries = 4
as.Registration = config.AppService.GetRegistration()
return as
}
// GetRegistration copies the data from the bridge config into an *appservice.Registration struct.
// This can't be used with the homeserver, see GenerateRegistration for generating files for the homeserver.
func (asc *AppserviceConfig) GetRegistration() *appservice.Registration {
reg := &appservice.Registration{}
asc.copyToRegistration(reg)
reg.SenderLocalpart = asc.Bot.Username
reg.ServerToken = asc.HSToken
reg.AppToken = asc.ASToken
return reg
}
func (asc *AppserviceConfig) copyToRegistration(registration *appservice.Registration) {
registration.ID = asc.ID
registration.URL = asc.Address
falseVal := false
registration.RateLimited = &falseVal
registration.EphemeralEvents = asc.EphemeralEvents
registration.SoruEphemeralEvents = asc.EphemeralEvents
}
type BotUserConfig struct {
Username string `yaml:"username"`
Displayname string `yaml:"displayname"`
Avatar string `yaml:"avatar"`
ParsedAvatar id.ContentURI `yaml:"-"`
}
type serializableBUC BotUserConfig
func (buc *BotUserConfig) UnmarshalYAML(unmarshal func(interface{}) error) error {
var sbuc serializableBUC
err := unmarshal(&sbuc)
if err != nil {
return err
}
*buc = (BotUserConfig)(sbuc)
if buc.Avatar != "" && buc.Avatar != "remove" {
buc.ParsedAvatar, err = id.ParseContentURI(buc.Avatar)
if err != nil {
return fmt.Errorf("%w in bot avatar", err)
}
}
return nil
}
type BridgeConfig interface {
FormatUsername(username string) string
GetEncryptionConfig() EncryptionConfig
GetCommandPrefix() string
GetManagementRoomTexts() ManagementRoomTexts
GetDoublePuppetConfig() DoublePuppetConfig
GetResendBridgeInfo() bool
EnableMessageStatusEvents() bool
EnableMessageErrorNotices() bool
Validate() error
}
type DoublePuppetConfig struct {
ServerMap map[string]string `yaml:"double_puppet_server_map"`
AllowDiscovery bool `yaml:"double_puppet_allow_discovery"`
SharedSecretMap map[string]string `yaml:"login_shared_secret_map"`
}
type EncryptionConfig struct {
Allow bool `yaml:"allow"`
Default bool `yaml:"default"`
Require bool `yaml:"require"`
Appservice bool `yaml:"appservice"`
PlaintextMentions bool `yaml:"plaintext_mentions"`
DeleteKeys struct {
DeleteOutboundOnAck bool `yaml:"delete_outbound_on_ack"`
DontStoreOutbound bool `yaml:"dont_store_outbound"`
RatchetOnDecrypt bool `yaml:"ratchet_on_decrypt"`
DeleteFullyUsedOnDecrypt bool `yaml:"delete_fully_used_on_decrypt"`
DeletePrevOnNewSession bool `yaml:"delete_prev_on_new_session"`
DeleteOnDeviceDelete bool `yaml:"delete_on_device_delete"`
PeriodicallyDeleteExpired bool `yaml:"periodically_delete_expired"`
DeleteOutdatedInbound bool `yaml:"delete_outdated_inbound"`
} `yaml:"delete_keys"`
VerificationLevels struct {
Receive id.TrustState `yaml:"receive"`
Send id.TrustState `yaml:"send"`
Share id.TrustState `yaml:"share"`
} `yaml:"verification_levels"`
AllowKeySharing bool `yaml:"allow_key_sharing"`
Rotation struct {
EnableCustom bool `yaml:"enable_custom"`
Milliseconds int64 `yaml:"milliseconds"`
Messages int `yaml:"messages"`
DisableDeviceChangeKeyRotation bool `yaml:"disable_device_change_key_rotation"`
} `yaml:"rotation"`
}
type ManagementRoomTexts struct {
Welcome string `yaml:"welcome"`
WelcomeConnected string `yaml:"welcome_connected"`
WelcomeUnconnected string `yaml:"welcome_unconnected"`
AdditionalHelp string `yaml:"additional_help"`
}
type BaseConfig struct {
Homeserver HomeserverConfig `yaml:"homeserver"`
AppService AppserviceConfig `yaml:"appservice"`
Bridge BridgeConfig `yaml:"-"`
Logging zeroconfig.Config `yaml:"logging"`
}
func doUpgrade(helper *up.Helper) {
helper.Copy(up.Str, "homeserver", "address")
helper.Copy(up.Str, "homeserver", "domain")
if legacyAsmuxFlag, ok := helper.Get(up.Bool, "homeserver", "asmux"); ok && legacyAsmuxFlag == "true" {
helper.Set(up.Str, string(SoftwareAsmux), "homeserver", "software")
} else {
helper.Copy(up.Str, "homeserver", "software")
}
helper.Copy(up.Str|up.Null, "homeserver", "status_endpoint")
helper.Copy(up.Str|up.Null, "homeserver", "message_send_checkpoint_endpoint")
helper.Copy(up.Bool, "homeserver", "async_media")
helper.Copy(up.Str|up.Null, "homeserver", "websocket_proxy")
helper.Copy(up.Bool, "homeserver", "websocket")
helper.Copy(up.Int, "homeserver", "ping_interval_seconds")
helper.Copy(up.Str|up.Null, "appservice", "address")
helper.Copy(up.Str|up.Null, "appservice", "hostname")
helper.Copy(up.Int|up.Null, "appservice", "port")
if dbType, ok := helper.Get(up.Str, "appservice", "database", "type"); ok && dbType == "sqlite3" {
helper.Set(up.Str, "sqlite3-fk-wal", "appservice", "database", "type")
} else {
helper.Copy(up.Str, "appservice", "database", "type")
}
helper.Copy(up.Str, "appservice", "database", "uri")
helper.Copy(up.Int, "appservice", "database", "max_open_conns")
helper.Copy(up.Int, "appservice", "database", "max_idle_conns")
helper.Copy(up.Str|up.Null, "appservice", "database", "max_conn_idle_time")
helper.Copy(up.Str|up.Null, "appservice", "database", "max_conn_lifetime")
helper.Copy(up.Str, "appservice", "id")
helper.Copy(up.Str, "appservice", "bot", "username")
helper.Copy(up.Str, "appservice", "bot", "displayname")
helper.Copy(up.Str, "appservice", "bot", "avatar")
helper.Copy(up.Bool, "appservice", "ephemeral_events")
helper.Copy(up.Bool, "appservice", "async_transactions")
helper.Copy(up.Str, "appservice", "as_token")
helper.Copy(up.Str, "appservice", "hs_token")
if helper.GetNode("logging", "writers") == nil && (helper.GetNode("logging", "print_level") != nil || helper.GetNode("logging", "file_name_format") != nil) {
_, _ = fmt.Fprintln(os.Stderr, "Migrating legacy log config")
migrateLegacyLogConfig(helper)
} else if helper.GetNode("logging", "writers") == nil && (helper.GetNode("logging", "handlers") != nil) {
_, _ = fmt.Fprintln(os.Stderr, "Migrating Python log config is not currently supported")
// TODO implement?
//migratePythonLogConfig(helper)
} else {
helper.Copy(up.Map, "logging")
}
}
type legacyLogConfig struct {
Directory string `yaml:"directory"`
FileNameFormat string `yaml:"file_name_format"`
FileDateFormat string `yaml:"file_date_format"`
FileMode uint32 `yaml:"file_mode"`
TimestampFormat string `yaml:"timestamp_format"`
RawPrintLevel string `yaml:"print_level"`
JSONStdout bool `yaml:"print_json"`
JSONFile bool `yaml:"file_json"`
}
func migrateLegacyLogConfig(helper *up.Helper) {
var llc legacyLogConfig
var newConfig zeroconfig.Config
err := helper.GetBaseNode("logging").Decode(&newConfig)
if err != nil {
_, _ = fmt.Fprintln(os.Stderr, "Base config is corrupted: failed to decode example log config:", err)
return
} else if len(newConfig.Writers) != 2 || newConfig.Writers[0].Type != "stdout" || newConfig.Writers[1].Type != "file" {
_, _ = fmt.Fprintln(os.Stderr, "Base log config is not in expected format")
return
}
err = helper.GetNode("logging").Decode(&llc)
if err != nil {
_, _ = fmt.Fprintln(os.Stderr, "Failed to decode legacy log config:", err)
return
}
if llc.RawPrintLevel != "" {
level, err := zerolog.ParseLevel(llc.RawPrintLevel)
if err != nil {
_, _ = fmt.Fprintln(os.Stderr, "Failed to parse minimum stdout log level:", err)
} else {
newConfig.Writers[0].MinLevel = &level
}
}
if llc.Directory != "" && llc.FileNameFormat != "" {
if llc.FileNameFormat == "{{.Date}}-{{.Index}}.log" {
llc.FileNameFormat = "bridge.log"
} else {
llc.FileNameFormat = strings.ReplaceAll(llc.FileNameFormat, "{{.Date}}", "")
llc.FileNameFormat = strings.ReplaceAll(llc.FileNameFormat, "{{.Index}}", "")
}
newConfig.Writers[1].Filename = filepath.Join(llc.Directory, llc.FileNameFormat)
} else if llc.FileNameFormat == "" {
newConfig.Writers = newConfig.Writers[0:1]
}
if llc.JSONStdout {
newConfig.Writers[0].TimeFormat = ""
newConfig.Writers[0].Format = "json"
} else if llc.TimestampFormat != "" {
newConfig.Writers[0].TimeFormat = llc.TimestampFormat
}
var updatedConfig yaml.Node
err = updatedConfig.Encode(&newConfig)
if err != nil {
_, _ = fmt.Fprintln(os.Stderr, "Failed to encode migrated log config:", err)
return
}
*helper.GetBaseNode("logging").Node = updatedConfig
}
// Upgrader is a config upgrader that copies the default fields in the homeserver, appservice and logging blocks.
var Upgrader = up.SimpleUpgrader(doUpgrade)

View file

@ -1,71 +0,0 @@
// 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 bridgeconfig
import (
"strconv"
"strings"
"maunium.net/go/mautrix/id"
)
type PermissionConfig map[string]PermissionLevel
type PermissionLevel int
const (
PermissionLevelBlock PermissionLevel = 0
PermissionLevelRelay PermissionLevel = 5
PermissionLevelUser PermissionLevel = 10
PermissionLevelAdmin PermissionLevel = 100
)
var namesToLevels = map[string]PermissionLevel{
"block": PermissionLevelBlock,
"relay": PermissionLevelRelay,
"user": PermissionLevelUser,
"admin": PermissionLevelAdmin,
}
func RegisterPermissionLevel(name string, level PermissionLevel) {
namesToLevels[name] = level
}
func (pc *PermissionConfig) UnmarshalYAML(unmarshal func(interface{}) error) error {
rawPC := make(map[string]string)
err := unmarshal(&rawPC)
if err != nil {
return err
}
if *pc == nil {
*pc = make(map[string]PermissionLevel)
}
for key, value := range rawPC {
level, ok := namesToLevels[strings.ToLower(value)]
if ok {
(*pc)[key] = level
} else if val, err := strconv.Atoi(value); err == nil {
(*pc)[key] = PermissionLevel(val)
} else {
(*pc)[key] = PermissionLevelBlock
}
}
return nil
}
func (pc PermissionConfig) Get(userID id.UserID) PermissionLevel {
if level, ok := pc[string(userID)]; ok {
return level
} else if level, ok = pc[userID.Homeserver()]; len(userID.Homeserver()) > 0 && ok {
return level
} else if level, ok = pc["*"]; ok {
return level
} else {
return PermissionLevelBlock
}
}

View file

@ -1,156 +0,0 @@
// 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 bridge
import (
"context"
"runtime/debug"
"time"
"github.com/rs/zerolog"
"maunium.net/go/mautrix/appservice"
"maunium.net/go/mautrix/bridge/status"
)
func (br *Bridge) SendBridgeState(ctx context.Context, state *status.BridgeState) error {
if br.Websocket {
// FIXME this doesn't account for multiple users
br.latestState = state
return br.AS.SendWebsocket(&appservice.WebsocketRequest{
Command: "bridge_status",
Data: state,
})
} else if br.Config.Homeserver.StatusEndpoint != "" {
return state.SendHTTP(ctx, br.Config.Homeserver.StatusEndpoint, br.Config.AppService.ASToken)
} else {
return nil
}
}
func (br *Bridge) SendGlobalBridgeState(state status.BridgeState) {
if len(br.Config.Homeserver.StatusEndpoint) == 0 && !br.Websocket {
return
}
for {
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
if err := br.SendBridgeState(ctx, &state); err != nil {
br.ZLog.Warn().Err(err).Msg("Failed to update global bridge state")
cancel()
time.Sleep(5 * time.Second)
continue
} else {
br.ZLog.Debug().Interface("bridge_state", state).Msg("Sent new global bridge state")
cancel()
break
}
}
}
type BridgeStateQueue struct {
prev *status.BridgeState
ch chan status.BridgeState
bridge *Bridge
user status.BridgeStateFiller
}
func (br *Bridge) NewBridgeStateQueue(user status.BridgeStateFiller) *BridgeStateQueue {
if len(br.Config.Homeserver.StatusEndpoint) == 0 && !br.Websocket {
return nil
}
bsq := &BridgeStateQueue{
ch: make(chan status.BridgeState, 10),
bridge: br,
user: user,
}
go bsq.loop()
return bsq
}
func (bsq *BridgeStateQueue) loop() {
defer func() {
err := recover()
if err != nil {
bsq.bridge.ZLog.Error().
Str(zerolog.ErrorStackFieldName, string(debug.Stack())).
Interface(zerolog.ErrorFieldName, err).
Msg("Panic in bridge state loop")
}
}()
for state := range bsq.ch {
bsq.immediateSendBridgeState(state)
}
}
func (bsq *BridgeStateQueue) immediateSendBridgeState(state status.BridgeState) {
retryIn := 2
for {
if bsq.prev != nil && bsq.prev.ShouldDeduplicate(&state) {
bsq.bridge.ZLog.Debug().
Str("state_event", string(state.StateEvent)).
Msg("Not sending bridge state as it's a duplicate")
return
}
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
err := bsq.bridge.SendBridgeState(ctx, &state)
cancel()
if err != nil {
bsq.bridge.ZLog.Warn().Err(err).
Int("retry_in_seconds", retryIn).
Msg("Failed to update bridge state")
time.Sleep(time.Duration(retryIn) * time.Second)
retryIn *= 2
if retryIn > 64 {
retryIn = 64
}
} else {
bsq.prev = &state
bsq.bridge.ZLog.Debug().
Interface("bridge_state", state).
Msg("Sent new bridge state")
return
}
}
}
func (bsq *BridgeStateQueue) Send(state status.BridgeState) {
if bsq == nil {
return
}
state = state.Fill(bsq.user)
if len(bsq.ch) >= 8 {
bsq.bridge.ZLog.Warn().Msg("Bridge state queue is nearly full, discarding an item")
select {
case <-bsq.ch:
default:
}
}
select {
case bsq.ch <- state:
default:
bsq.bridge.ZLog.Error().Msg("Bridge state queue is full, dropped new state")
}
}
func (bsq *BridgeStateQueue) GetPrev() status.BridgeState {
if bsq != nil && bsq.prev != nil {
return *bsq.prev
}
return status.BridgeState{}
}
func (bsq *BridgeStateQueue) SetPrev(prev status.BridgeState) {
if bsq != nil {
bsq.prev = &prev
}
}

View file

@ -1,83 +0,0 @@
// 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 commands
var CommandLoginMatrix = &FullHandler{
Func: fnLoginMatrix,
Name: "login-matrix",
Help: HelpMeta{
Section: HelpSectionAuth,
Description: "Enable double puppeting.",
Args: "<_access token_>",
},
RequiresLogin: true,
}
func fnLoginMatrix(ce *Event) {
if len(ce.Args) == 0 {
ce.Reply("**Usage:** `login-matrix <access token>`")
return
}
puppet := ce.User.GetIDoublePuppet()
if puppet == nil {
puppet = ce.User.GetIGhost()
if puppet == nil {
ce.Reply("Didn't get a ghost :(")
return
}
}
err := puppet.SwitchCustomMXID(ce.Args[0], ce.User.GetMXID())
if err != nil {
ce.Reply("Failed to enable double puppeting: %v", err)
} else {
ce.Reply("Successfully switched puppet")
}
}
var CommandPingMatrix = &FullHandler{
Func: fnPingMatrix,
Name: "ping-matrix",
Help: HelpMeta{
Section: HelpSectionAuth,
Description: "Ping the Matrix server with the double puppet.",
},
RequiresLogin: true,
}
func fnPingMatrix(ce *Event) {
puppet := ce.User.GetIDoublePuppet()
if puppet == nil || puppet.CustomIntent() == nil {
ce.Reply("You are not logged in with your Matrix account.")
return
}
resp, err := puppet.CustomIntent().Whoami(ce.Ctx)
if err != nil {
ce.Reply("Failed to validate Matrix login: %v", err)
} else {
ce.Reply("Confirmed valid access token for %s / %s", resp.UserID, resp.DeviceID)
}
}
var CommandLogoutMatrix = &FullHandler{
Func: fnLogoutMatrix,
Name: "logout-matrix",
Help: HelpMeta{
Section: HelpSectionAuth,
Description: "Disable double puppeting.",
},
RequiresLogin: true,
}
func fnLogoutMatrix(ce *Event) {
puppet := ce.User.GetIDoublePuppet()
if puppet == nil || puppet.CustomIntent() == nil {
ce.Reply("You don't have double puppeting enabled.")
return
}
puppet.ClearCustomMXID()
ce.Reply("Successfully disabled double puppeting.")
}

View file

@ -1,98 +0,0 @@
// 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 commands
import (
"context"
"fmt"
"strings"
"github.com/rs/zerolog"
"maunium.net/go/maulogger/v2"
"maunium.net/go/mautrix"
"maunium.net/go/mautrix/appservice"
"maunium.net/go/mautrix/bridge"
"maunium.net/go/mautrix/event"
"maunium.net/go/mautrix/format"
"maunium.net/go/mautrix/id"
)
// Event stores all data which might be used to handle commands
type Event struct {
Bot *appservice.IntentAPI
Bridge *bridge.Bridge
Portal bridge.Portal
Processor *Processor
Handler MinimalHandler
RoomID id.RoomID
EventID id.EventID
User bridge.User
Command string
Args []string
RawArgs string
ReplyTo id.EventID
Ctx context.Context
ZLog *zerolog.Logger
// Deprecated: switch to ZLog
Log maulogger.Logger
}
// MainIntent returns the intent to use when replying to the command.
//
// It prefers the bridge bot, but falls back to the other user in DMs if the bridge bot is not present.
func (ce *Event) MainIntent() *appservice.IntentAPI {
intent := ce.Bot
if ce.Portal != nil && ce.Portal.IsPrivateChat() && !ce.Portal.IsEncrypted() {
intent = ce.Portal.MainIntent()
}
return intent
}
// Reply sends a reply to command as notice, with optional string formatting and automatic $cmdprefix replacement.
func (ce *Event) Reply(msg string, args ...interface{}) {
msg = strings.ReplaceAll(msg, "$cmdprefix ", ce.Bridge.Config.Bridge.GetCommandPrefix()+" ")
if len(args) > 0 {
msg = fmt.Sprintf(msg, args...)
}
ce.ReplyAdvanced(msg, true, false)
}
// ReplyAdvanced sends a reply to command as notice. It allows using HTML and disabling markdown,
// but doesn't have built-in string formatting.
func (ce *Event) ReplyAdvanced(msg string, allowMarkdown, allowHTML bool) {
content := format.RenderMarkdown(msg, allowMarkdown, allowHTML)
content.MsgType = event.MsgNotice
_, err := ce.MainIntent().SendMessageEvent(ce.Ctx, ce.RoomID, event.EventMessage, content)
if err != nil {
ce.ZLog.Error().Err(err).Msgf("Failed to reply to command")
}
}
// React sends a reaction to the command.
func (ce *Event) React(key string) {
_, err := ce.MainIntent().SendReaction(ce.Ctx, ce.RoomID, ce.EventID, key)
if err != nil {
ce.ZLog.Error().Err(err).Msgf("Failed to react to command")
}
}
// Redact redacts the command.
func (ce *Event) Redact(req ...mautrix.ReqRedact) {
_, err := ce.MainIntent().RedactEvent(ce.Ctx, ce.RoomID, ce.EventID, req...)
if err != nil {
ce.ZLog.Error().Err(err).Msgf("Failed to redact command")
}
}
// MarkRead marks the command event as read.
func (ce *Event) MarkRead() {
err := ce.MainIntent().SendReceipt(ce.Ctx, ce.RoomID, ce.EventID, event.ReceiptTypeRead, nil)
if err != nil {
ce.ZLog.Error().Err(err).Msgf("Failed to mark command as read")
}
}

View file

@ -1,100 +0,0 @@
// 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 commands
import (
"maunium.net/go/mautrix/bridge"
"maunium.net/go/mautrix/bridge/bridgeconfig"
"maunium.net/go/mautrix/event"
)
type MinimalHandler interface {
Run(*Event)
}
type MinimalHandlerFunc func(*Event)
func (mhf MinimalHandlerFunc) Run(ce *Event) {
mhf(ce)
}
type CommandState struct {
Next MinimalHandler
Action string
Meta interface{}
}
type CommandingUser interface {
bridge.User
GetCommandState() *CommandState
SetCommandState(*CommandState)
}
type Handler interface {
MinimalHandler
GetName() string
}
type AliasedHandler interface {
Handler
GetAliases() []string
}
type FullHandler struct {
Func func(*Event)
Name string
Aliases []string
Help HelpMeta
RequiresAdmin bool
RequiresPortal bool
RequiresLogin bool
RequiresEventLevel event.Type
}
func (fh *FullHandler) GetHelp() HelpMeta {
fh.Help.Command = fh.Name
return fh.Help
}
func (fh *FullHandler) GetName() string {
return fh.Name
}
func (fh *FullHandler) GetAliases() []string {
return fh.Aliases
}
func (fh *FullHandler) ShowInHelp(ce *Event) bool {
return !fh.RequiresAdmin || ce.User.GetPermissionLevel() >= bridgeconfig.PermissionLevelAdmin
}
func (fh *FullHandler) userHasRoomPermission(ce *Event) bool {
levels, err := ce.MainIntent().PowerLevels(ce.Ctx, ce.RoomID)
if err != nil {
ce.ZLog.Warn().Err(err).Msg("Failed to check room power levels")
ce.Reply("Failed to get room power levels to see if you're allowed to use that command")
return false
}
return levels.GetUserLevel(ce.User.GetMXID()) >= levels.GetEventLevel(fh.RequiresEventLevel)
}
func (fh *FullHandler) Run(ce *Event) {
if fh.RequiresAdmin && ce.User.GetPermissionLevel() < bridgeconfig.PermissionLevelAdmin {
ce.Reply("That command is limited to bridge administrators.")
} else if fh.RequiresEventLevel.Type != "" && ce.User.GetPermissionLevel() < bridgeconfig.PermissionLevelAdmin && !fh.userHasRoomPermission(ce) {
ce.Reply("That command requires room admin rights.")
} else if fh.RequiresPortal && ce.Portal == nil {
ce.Reply("That command can only be ran in portal rooms.")
} else if fh.RequiresLogin && !ce.User.IsLoggedIn() {
ce.Reply("That command requires you to be logged in.")
} else {
fh.Func(ce)
}
}

View file

@ -1,56 +0,0 @@
// 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 commands
var CommandHelp = &FullHandler{
Func: func(ce *Event) {
ce.Reply(FormatHelp(ce))
},
Name: "help",
Help: HelpMeta{
Section: HelpSectionGeneral,
Description: "Show this help message.",
},
}
var CommandVersion = &FullHandler{
Func: func(ce *Event) {
ce.Reply("[%s](%s) %s (%s)", ce.Bridge.Name, ce.Bridge.URL, ce.Bridge.LinkifiedVersion, ce.Bridge.BuildTime)
},
Name: "version",
Help: HelpMeta{
Section: HelpSectionGeneral,
Description: "Get the bridge version.",
},
}
var CommandCancel = &FullHandler{
Func: func(ce *Event) {
commandingUser, ok := ce.User.(CommandingUser)
if !ok {
ce.Reply("This bridge does not implement cancelable commands")
return
}
state := commandingUser.GetCommandState()
if state != nil {
action := state.Action
if action == "" {
action = "Unknown action"
}
commandingUser.SetCommandState(nil)
ce.Reply("%s cancelled.", action)
} else {
ce.Reply("No ongoing command.")
}
},
Name: "cancel",
Help: HelpMeta{
Section: HelpSectionGeneral,
Description: "Cancel an ongoing action.",
},
}

View file

@ -1,123 +0,0 @@
// 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 commands
import (
"context"
"runtime/debug"
"strings"
"github.com/rs/zerolog"
"maunium.net/go/maulogger/v2/maulogadapt"
"maunium.net/go/mautrix/bridge"
"maunium.net/go/mautrix/id"
)
type Processor struct {
bridge *bridge.Bridge
log *zerolog.Logger
handlers map[string]Handler
aliases map[string]string
}
// NewProcessor creates a Processor
func NewProcessor(bridge *bridge.Bridge) *Processor {
proc := &Processor{
bridge: bridge,
log: bridge.ZLog,
handlers: make(map[string]Handler),
aliases: make(map[string]string),
}
proc.AddHandlers(
CommandHelp, CommandVersion, CommandCancel,
CommandLoginMatrix, CommandLogoutMatrix, CommandPingMatrix,
CommandDiscardMegolmSession, CommandSetPowerLevel)
return proc
}
func (proc *Processor) AddHandlers(handlers ...Handler) {
for _, handler := range handlers {
proc.AddHandler(handler)
}
}
func (proc *Processor) AddHandler(handler Handler) {
proc.handlers[handler.GetName()] = handler
aliased, ok := handler.(AliasedHandler)
if ok {
for _, alias := range aliased.GetAliases() {
proc.aliases[alias] = handler.GetName()
}
}
}
// Handle handles messages to the bridge
func (proc *Processor) Handle(ctx context.Context, roomID id.RoomID, eventID id.EventID, user bridge.User, message string, replyTo id.EventID) {
defer func() {
err := recover()
if err != nil {
zerolog.Ctx(ctx).Error().
Str(zerolog.ErrorStackFieldName, string(debug.Stack())).
Interface(zerolog.ErrorFieldName, err).
Msg("Panic in Matrix command handler")
}
}()
args := strings.Fields(message)
if len(args) == 0 {
args = []string{"unknown-command"}
}
command := strings.ToLower(args[0])
rawArgs := strings.TrimLeft(strings.TrimPrefix(message, command), " ")
log := zerolog.Ctx(ctx).With().Str("mx_command", command).Logger()
ctx = log.WithContext(ctx)
ce := &Event{
Bot: proc.bridge.Bot,
Bridge: proc.bridge,
Portal: proc.bridge.Child.GetIPortal(roomID),
Processor: proc,
RoomID: roomID,
EventID: eventID,
User: user,
Command: command,
Args: args[1:],
RawArgs: rawArgs,
ReplyTo: replyTo,
Ctx: ctx,
ZLog: &log,
Log: maulogadapt.ZeroAsMau(&log),
}
log.Debug().Msg("Received command")
realCommand, ok := proc.aliases[ce.Command]
if !ok {
realCommand = ce.Command
}
commandingUser, ok := ce.User.(CommandingUser)
var handler MinimalHandler
handler, ok = proc.handlers[realCommand]
if !ok {
var state *CommandState
if commandingUser != nil {
state = commandingUser.GetCommandState()
}
if state != nil && state.Next != nil {
ce.Command = ""
ce.Args = args
ce.Handler = state.Next
state.Next.Run(ce)
} else {
ce.Reply("Unknown command, use the `help` command for help.")
}
} else {
ce.Handler = handler
handler.Run(ce)
}
}

View file

@ -1,173 +0,0 @@
// 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 bridge
import (
"context"
"crypto/hmac"
"crypto/sha512"
"encoding/hex"
"errors"
"fmt"
"strings"
"github.com/rs/zerolog"
"maunium.net/go/mautrix"
"maunium.net/go/mautrix/appservice"
"maunium.net/go/mautrix/id"
)
type doublePuppetUtil struct {
br *Bridge
log zerolog.Logger
}
func (dp *doublePuppetUtil) newClient(ctx context.Context, mxid id.UserID, accessToken string) (*mautrix.Client, error) {
_, homeserver, err := mxid.Parse()
if err != nil {
return nil, err
}
homeserverURL, found := dp.br.Config.Bridge.GetDoublePuppetConfig().ServerMap[homeserver]
if !found {
if homeserver == dp.br.AS.HomeserverDomain {
homeserverURL = ""
} else if dp.br.Config.Bridge.GetDoublePuppetConfig().AllowDiscovery {
resp, err := mautrix.DiscoverClientAPI(ctx, homeserver)
if err != nil {
return nil, fmt.Errorf("failed to find homeserver URL for %s: %v", homeserver, err)
}
homeserverURL = resp.Homeserver.BaseURL
dp.log.Debug().
Str("homeserver", homeserver).
Str("url", homeserverURL).
Str("user_id", mxid.String()).
Msg("Discovered URL to enable double puppeting for user")
} else {
return nil, fmt.Errorf("double puppeting from %s is not allowed", homeserver)
}
}
return dp.br.AS.NewExternalMautrixClient(mxid, accessToken, homeserverURL)
}
func (dp *doublePuppetUtil) newIntent(ctx context.Context, mxid id.UserID, accessToken string) (*appservice.IntentAPI, error) {
client, err := dp.newClient(ctx, mxid, accessToken)
if err != nil {
return nil, err
}
ia := dp.br.AS.NewIntentAPI("custom")
ia.Client = client
ia.Localpart, _, _ = mxid.Parse()
ia.UserID = mxid
ia.IsCustomPuppet = true
return ia, nil
}
func (dp *doublePuppetUtil) autoLogin(ctx context.Context, mxid id.UserID, loginSecret string) (string, error) {
dp.log.Debug().Str("user_id", mxid.String()).Msg("Logging into user account with shared secret")
client, err := dp.newClient(ctx, mxid, "")
if err != nil {
return "", fmt.Errorf("failed to create mautrix client to log in: %v", err)
}
bridgeName := fmt.Sprintf("%s Bridge", dp.br.ProtocolName)
req := mautrix.ReqLogin{
Identifier: mautrix.UserIdentifier{Type: mautrix.IdentifierTypeUser, User: string(mxid)},
DeviceID: id.DeviceID(bridgeName),
InitialDeviceDisplayName: bridgeName,
}
if loginSecret == "appservice" {
client.AccessToken = dp.br.AS.Registration.AppToken
req.Type = mautrix.AuthTypeAppservice
} else {
loginFlows, err := client.GetLoginFlows(ctx)
if err != nil {
return "", fmt.Errorf("failed to get supported login flows: %w", err)
}
mac := hmac.New(sha512.New, []byte(loginSecret))
mac.Write([]byte(mxid))
token := hex.EncodeToString(mac.Sum(nil))
switch {
case loginFlows.HasFlow(mautrix.AuthTypeDevtureSharedSecret):
req.Type = mautrix.AuthTypeDevtureSharedSecret
req.Token = token
case loginFlows.HasFlow(mautrix.AuthTypePassword):
req.Type = mautrix.AuthTypePassword
req.Password = token
default:
return "", fmt.Errorf("no supported auth types for shared secret auth found")
}
}
resp, err := client.Login(ctx, &req)
if err != nil {
return "", err
}
return resp.AccessToken, nil
}
var (
ErrMismatchingMXID = errors.New("whoami result does not match custom mxid")
ErrNoAccessToken = errors.New("no access token provided")
ErrNoMXID = errors.New("no mxid provided")
)
const useConfigASToken = "appservice-config"
const asTokenModePrefix = "as_token:"
func (dp *doublePuppetUtil) Setup(ctx context.Context, mxid id.UserID, savedAccessToken string, reloginOnFail bool) (intent *appservice.IntentAPI, newAccessToken string, err error) {
if len(mxid) == 0 {
err = ErrNoMXID
return
}
_, homeserver, _ := mxid.Parse()
loginSecret, hasSecret := dp.br.Config.Bridge.GetDoublePuppetConfig().SharedSecretMap[homeserver]
// Special case appservice: prefix to not login and use it as an as_token directly.
if hasSecret && strings.HasPrefix(loginSecret, asTokenModePrefix) {
intent, err = dp.newIntent(ctx, mxid, strings.TrimPrefix(loginSecret, asTokenModePrefix))
if err != nil {
return
}
intent.SetAppServiceUserID = true
if savedAccessToken != useConfigASToken {
var resp *mautrix.RespWhoami
resp, err = intent.Whoami(ctx)
if err == nil && resp.UserID != mxid {
err = ErrMismatchingMXID
}
}
return intent, useConfigASToken, err
}
if savedAccessToken == "" || savedAccessToken == useConfigASToken {
if reloginOnFail && hasSecret {
savedAccessToken, err = dp.autoLogin(ctx, mxid, loginSecret)
} else {
err = ErrNoAccessToken
}
if err != nil {
return
}
}
intent, err = dp.newIntent(ctx, mxid, savedAccessToken)
if err != nil {
return
}
var resp *mautrix.RespWhoami
resp, err = intent.Whoami(ctx)
if err != nil {
if reloginOnFail && hasSecret && errors.Is(err, mautrix.MUnknownToken) {
intent.AccessToken, err = dp.autoLogin(ctx, mxid, loginSecret)
if err == nil {
newAccessToken = intent.AccessToken
}
}
} else if resp.UserID != mxid {
err = ErrMismatchingMXID
} else {
newAccessToken = savedAccessToken
}
return
}

View file

@ -1,688 +0,0 @@
// 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 bridge
import (
"context"
"errors"
"fmt"
"strings"
"time"
"github.com/rs/zerolog"
"maunium.net/go/mautrix"
"maunium.net/go/mautrix/appservice"
"maunium.net/go/mautrix/bridge/bridgeconfig"
"maunium.net/go/mautrix/bridge/status"
"maunium.net/go/mautrix/event"
"maunium.net/go/mautrix/format"
"maunium.net/go/mautrix/id"
)
type CommandProcessor interface {
Handle(ctx context.Context, roomID id.RoomID, eventID id.EventID, user User, message string, replyTo id.EventID)
}
type MatrixHandler struct {
bridge *Bridge
as *appservice.AppService
log *zerolog.Logger
TrackEventDuration func(event.Type) func()
}
func noop() {}
func noopTrack(_ event.Type) func() {
return noop
}
func NewMatrixHandler(br *Bridge) *MatrixHandler {
handler := &MatrixHandler{
bridge: br,
as: br.AS,
log: br.ZLog,
TrackEventDuration: noopTrack,
}
for evtType := range status.CheckpointTypes {
br.EventProcessor.On(evtType, handler.sendBridgeCheckpoint)
}
br.EventProcessor.On(event.EventMessage, handler.HandleMessage)
br.EventProcessor.On(event.EventEncrypted, handler.HandleEncrypted)
br.EventProcessor.On(event.EventSticker, handler.HandleMessage)
br.EventProcessor.On(event.EventReaction, handler.HandleReaction)
br.EventProcessor.On(event.EventRedaction, handler.HandleRedaction)
br.EventProcessor.On(event.StateMember, handler.HandleMembership)
br.EventProcessor.On(event.StateRoomName, handler.HandleRoomMetadata)
br.EventProcessor.On(event.StateRoomAvatar, handler.HandleRoomMetadata)
br.EventProcessor.On(event.StateTopic, handler.HandleRoomMetadata)
br.EventProcessor.On(event.StateEncryption, handler.HandleEncryption)
br.EventProcessor.On(event.EphemeralEventReceipt, handler.HandleReceipt)
br.EventProcessor.On(event.EphemeralEventTyping, handler.HandleTyping)
return handler
}
func (mx *MatrixHandler) sendBridgeCheckpoint(_ context.Context, evt *event.Event) {
if !evt.Mautrix.CheckpointSent {
go mx.bridge.SendMessageSuccessCheckpoint(evt, status.MsgStepBridge, 0)
}
}
func (mx *MatrixHandler) HandleEncryption(ctx context.Context, evt *event.Event) {
defer mx.TrackEventDuration(evt.Type)()
if evt.Content.AsEncryption().Algorithm != id.AlgorithmMegolmV1 {
return
}
portal := mx.bridge.Child.GetIPortal(evt.RoomID)
if portal != nil && !portal.IsEncrypted() {
mx.log.Debug().
Str("user_id", evt.Sender.String()).
Str("room_id", evt.RoomID.String()).
Msg("Encryption was enabled in room")
portal.MarkEncrypted()
if portal.IsPrivateChat() {
err := mx.as.BotIntent().EnsureJoined(ctx, evt.RoomID, appservice.EnsureJoinedParams{BotOverride: portal.MainIntent().Client})
if err != nil {
mx.log.Err(err).
Str("room_id", evt.RoomID.String()).
Msg("Failed to join bot to room after encryption was enabled")
}
}
}
}
func (mx *MatrixHandler) joinAndCheckMembers(ctx context.Context, evt *event.Event, intent *appservice.IntentAPI) *mautrix.RespJoinedMembers {
log := zerolog.Ctx(ctx)
resp, err := intent.JoinRoomByID(ctx, evt.RoomID)
if err != nil {
log.Warn().Err(err).Msg("Failed to join room with invite")
return nil
}
members, err := intent.JoinedMembers(ctx, resp.RoomID)
if err != nil {
log.Warn().Err(err).Msg("Failed to get members in room after accepting invite, leaving room")
_, _ = intent.LeaveRoom(ctx, resp.RoomID)
return nil
}
if len(members.Joined) < 2 {
log.Debug().Msg("Leaving empty room after accepting invite")
_, _ = intent.LeaveRoom(ctx, resp.RoomID)
return nil
}
return members
}
func (mx *MatrixHandler) sendNoticeWithMarkdown(ctx context.Context, roomID id.RoomID, message string) (*mautrix.RespSendEvent, error) {
intent := mx.as.BotIntent()
content := format.RenderMarkdown(message, true, false)
content.MsgType = event.MsgNotice
return intent.SendMessageEvent(ctx, roomID, event.EventMessage, content)
}
func (mx *MatrixHandler) HandleBotInvite(ctx context.Context, evt *event.Event) {
intent := mx.as.BotIntent()
user := mx.bridge.Child.GetIUser(evt.Sender, true)
if user == nil {
return
}
members := mx.joinAndCheckMembers(ctx, evt, intent)
if members == nil {
return
}
if user.GetPermissionLevel() < bridgeconfig.PermissionLevelUser {
_, _ = intent.SendNotice(ctx, evt.RoomID, "You are not whitelisted to use this bridge.\n"+
"If you're the owner of this bridge, see the bridge.permissions section in your config file.")
_, _ = intent.LeaveRoom(ctx, evt.RoomID)
return
}
texts := mx.bridge.Config.Bridge.GetManagementRoomTexts()
_, _ = mx.sendNoticeWithMarkdown(ctx, evt.RoomID, texts.Welcome)
if len(members.Joined) == 2 && (len(user.GetManagementRoomID()) == 0 || evt.Content.AsMember().IsDirect) {
user.SetManagementRoom(evt.RoomID)
_, _ = intent.SendNotice(ctx, user.GetManagementRoomID(), "This room has been registered as your bridge management/status room.")
zerolog.Ctx(ctx).Debug().Msg("Registered room as management room with inviter")
}
if evt.RoomID == user.GetManagementRoomID() {
if user.IsLoggedIn() {
_, _ = mx.sendNoticeWithMarkdown(ctx, evt.RoomID, texts.WelcomeConnected)
} else {
_, _ = mx.sendNoticeWithMarkdown(ctx, evt.RoomID, texts.WelcomeUnconnected)
}
additionalHelp := texts.AdditionalHelp
if len(additionalHelp) > 0 {
_, _ = mx.sendNoticeWithMarkdown(ctx, evt.RoomID, additionalHelp)
}
}
}
func (mx *MatrixHandler) HandleGhostInvite(ctx context.Context, evt *event.Event, inviter User, ghost Ghost) {
log := zerolog.Ctx(ctx)
intent := ghost.DefaultIntent()
if inviter.GetPermissionLevel() < bridgeconfig.PermissionLevelUser {
log.Debug().Msg("Rejecting invite: inviter is not whitelisted")
_, err := intent.LeaveRoom(ctx, evt.RoomID, &mautrix.ReqLeave{
Reason: "You're not whitelisted to use this bridge",
})
if err != nil {
log.Error().Err(err).Msg("Failed to reject invite")
}
return
} else if !inviter.IsLoggedIn() {
log.Debug().Msg("Rejecting invite: inviter is not logged in")
_, err := intent.LeaveRoom(ctx, evt.RoomID, &mautrix.ReqLeave{
Reason: "You're not logged into this bridge",
})
if err != nil {
log.Error().Err(err).Msg("Failed to reject invite")
}
return
}
members := mx.joinAndCheckMembers(ctx, evt, intent)
if members == nil {
return
}
var createEvent event.CreateEventContent
if err := intent.StateEvent(ctx, evt.RoomID, event.StateCreate, "", &createEvent); err != nil {
log.Warn().Err(err).Msg("Failed to check m.room.create event in room")
} else if createEvent.Type != "" {
log.Warn().Str("room_type", string(createEvent.Type)).Msg("Non-standard room type, leaving room")
_, err = intent.LeaveRoom(ctx, evt.RoomID, &mautrix.ReqLeave{
Reason: "Unsupported room type",
})
if err != nil {
log.Error().Err(err).Msg("Failed to leave room")
}
return
}
var hasBridgeBot, hasOtherUsers bool
for mxid, _ := range members.Joined {
if mxid == intent.UserID || mxid == inviter.GetMXID() {
continue
} else if mxid == mx.bridge.Bot.UserID {
hasBridgeBot = true
} else {
hasOtherUsers = true
}
}
if !hasBridgeBot && !hasOtherUsers && evt.Content.AsMember().IsDirect {
mx.bridge.Child.CreatePrivatePortal(evt.RoomID, inviter, ghost)
} else if !hasBridgeBot {
log.Debug().Msg("Leaving multi-user room after accepting invite")
_, _ = intent.SendNotice(ctx, evt.RoomID, "Please invite the bridge bot first if you want to bridge to a remote chat.")
_, _ = intent.LeaveRoom(ctx, evt.RoomID)
} else {
_, _ = intent.SendNotice(ctx, evt.RoomID, "This puppet will remain inactive until this room is bridged to a remote chat.")
}
}
func (mx *MatrixHandler) HandleMembership(ctx context.Context, evt *event.Event) {
if evt.Sender == mx.bridge.Bot.UserID || mx.bridge.Child.IsGhost(evt.Sender) {
return
}
defer mx.TrackEventDuration(evt.Type)()
if mx.bridge.Crypto != nil {
mx.bridge.Crypto.HandleMemberEvent(ctx, evt)
}
log := mx.log.With().
Str("sender", evt.Sender.String()).
Str("target", evt.GetStateKey()).
Str("room_id", evt.RoomID.String()).
Logger()
ctx = log.WithContext(ctx)
content := evt.Content.AsMember()
if content.Membership == event.MembershipInvite && id.UserID(evt.GetStateKey()) == mx.as.BotMXID() {
mx.HandleBotInvite(ctx, evt)
return
}
if mx.shouldIgnoreEvent(evt) {
return
}
user := mx.bridge.Child.GetIUser(evt.Sender, true)
if user == nil {
return
}
isSelf := id.UserID(evt.GetStateKey()) == evt.Sender
ghost := mx.bridge.Child.GetIGhost(id.UserID(evt.GetStateKey()))
portal := mx.bridge.Child.GetIPortal(evt.RoomID)
if portal == nil {
if ghost != nil && content.Membership == event.MembershipInvite {
mx.HandleGhostInvite(ctx, evt, user, ghost)
}
return
} else if user.GetPermissionLevel() < bridgeconfig.PermissionLevelUser || !user.IsLoggedIn() {
return
}
mhp, ok := portal.(MembershipHandlingPortal)
if !ok {
return
}
if content.Membership == event.MembershipLeave {
if evt.Unsigned.PrevContent != nil {
_ = evt.Unsigned.PrevContent.ParseRaw(evt.Type)
prevContent, ok := evt.Unsigned.PrevContent.Parsed.(*event.MemberEventContent)
if ok && prevContent.Membership != "join" {
return
}
}
if isSelf {
mhp.HandleMatrixLeave(user, evt)
} else if ghost != nil {
mhp.HandleMatrixKick(user, ghost, evt)
}
} else if content.Membership == event.MembershipInvite && !isSelf && ghost != nil {
mhp.HandleMatrixInvite(user, ghost, evt)
}
// TODO kicking/inviting non-ghost users users
}
func (mx *MatrixHandler) HandleRoomMetadata(ctx context.Context, evt *event.Event) {
defer mx.TrackEventDuration(evt.Type)()
if mx.shouldIgnoreEvent(evt) {
return
}
user := mx.bridge.Child.GetIUser(evt.Sender, true)
if user == nil {
return
}
portal := mx.bridge.Child.GetIPortal(evt.RoomID)
if portal == nil || portal.IsPrivateChat() {
return
}
metaPortal, ok := portal.(MetaHandlingPortal)
if !ok {
return
}
metaPortal.HandleMatrixMeta(user, evt)
}
func (mx *MatrixHandler) shouldIgnoreEvent(evt *event.Event) bool {
if evt.Sender == mx.bridge.Bot.UserID || mx.bridge.Child.IsGhost(evt.Sender) {
return true
}
user := mx.bridge.Child.GetIUser(evt.Sender, true)
if user == nil || user.GetPermissionLevel() <= 0 {
return true
} else if val, ok := evt.Content.Raw[appservice.DoublePuppetKey]; ok && val == mx.bridge.Name && user.GetIDoublePuppet() != nil {
return true
}
return false
}
const initialSessionWaitTimeout = 3 * time.Second
const extendedSessionWaitTimeout = 22 * time.Second
func (mx *MatrixHandler) sendCryptoStatusError(ctx context.Context, evt *event.Event, editEvent id.EventID, err error, retryCount int, isFinal bool) id.EventID {
mx.bridge.SendMessageErrorCheckpoint(evt, status.MsgStepDecrypted, err, isFinal, retryCount)
if mx.bridge.Config.Bridge.EnableMessageStatusEvents() {
statusEvent := &event.BeeperMessageStatusEventContent{
// TODO: network
RelatesTo: event.RelatesTo{
Type: event.RelReference,
EventID: evt.ID,
},
Status: event.MessageStatusRetriable,
Reason: event.MessageStatusUndecryptable,
Error: err.Error(),
Message: errorToHumanMessage(err),
}
if !isFinal {
statusEvent.Status = event.MessageStatusPending
}
_, sendErr := mx.bridge.Bot.SendMessageEvent(ctx, evt.RoomID, event.BeeperMessageStatus, statusEvent)
if sendErr != nil {
zerolog.Ctx(ctx).Error().Err(err).Msg("Failed to send message status event")
}
}
if mx.bridge.Config.Bridge.EnableMessageErrorNotices() {
update := event.MessageEventContent{
MsgType: event.MsgNotice,
Body: fmt.Sprintf("\u26a0 Your message was not bridged: %v.", err),
}
if errors.Is(err, errNoCrypto) {
update.Body = "🔒 This bridge has not been configured to support encryption"
}
relatable, ok := evt.Content.Parsed.(event.Relatable)
if editEvent != "" {
update.SetEdit(editEvent)
} else if ok && relatable.OptionalGetRelatesTo().GetThreadParent() != "" {
update.GetRelatesTo().SetThread(relatable.OptionalGetRelatesTo().GetThreadParent(), evt.ID)
}
resp, sendErr := mx.bridge.Bot.SendMessageEvent(ctx, evt.RoomID, event.EventMessage, &update)
if sendErr != nil {
zerolog.Ctx(ctx).Error().Err(sendErr).Msg("Failed to send decryption error notice")
} else if resp != nil {
return resp.EventID
}
}
return ""
}
var (
errDeviceNotTrusted = errors.New("your device is not trusted")
errMessageNotEncrypted = errors.New("unencrypted message")
errNoDecryptionKeys = errors.New("the bridge hasn't received the decryption keys")
errNoCrypto = errors.New("this bridge has not been configured to support encryption")
)
func errorToHumanMessage(err error) string {
var withheld *event.RoomKeyWithheldEventContent
switch {
case errors.Is(err, errDeviceNotTrusted), errors.Is(err, errNoDecryptionKeys):
return err.Error()
case errors.Is(err, UnknownMessageIndex):
return "the keys received by the bridge can't decrypt the message"
case errors.Is(err, DuplicateMessageIndex):
return "your client encrypted multiple messages with the same key"
case errors.As(err, &withheld):
if withheld.Code == event.RoomKeyWithheldBeeperRedacted {
return "your client used an outdated encryption session"
}
return "your client refused to share decryption keys with the bridge"
case errors.Is(err, errMessageNotEncrypted):
return "the message is not encrypted"
default:
return "the bridge failed to decrypt the message"
}
}
func deviceUnverifiedErrorWithExplanation(trust id.TrustState) error {
var explanation string
switch trust {
case id.TrustStateBlacklisted:
explanation = "device is blacklisted"
case id.TrustStateUnset:
explanation = "unverified"
case id.TrustStateUnknownDevice:
explanation = "device info not found"
case id.TrustStateForwarded:
explanation = "keys were forwarded from an unknown device"
case id.TrustStateCrossSignedUntrusted:
explanation = "cross-signing keys changed after setting up the bridge"
default:
return errDeviceNotTrusted
}
return fmt.Errorf("%w (%s)", errDeviceNotTrusted, explanation)
}
func copySomeKeys(original, decrypted *event.Event) {
isScheduled, _ := original.Content.Raw["com.beeper.scheduled"].(bool)
_, alreadyExists := decrypted.Content.Raw["com.beeper.scheduled"]
if isScheduled && !alreadyExists {
decrypted.Content.Raw["com.beeper.scheduled"] = true
}
}
func (mx *MatrixHandler) postDecrypt(ctx context.Context, original, decrypted *event.Event, retryCount int, errorEventID id.EventID, duration time.Duration) {
log := zerolog.Ctx(ctx)
minLevel := mx.bridge.Config.Bridge.GetEncryptionConfig().VerificationLevels.Send
if decrypted.Mautrix.TrustState < minLevel {
logEvt := log.Warn().
Str("user_id", decrypted.Sender.String()).
Bool("forwarded_keys", decrypted.Mautrix.ForwardedKeys).
Stringer("device_trust", decrypted.Mautrix.TrustState).
Stringer("min_trust", minLevel)
if decrypted.Mautrix.TrustSource != nil {
dev := decrypted.Mautrix.TrustSource
logEvt.
Str("device_id", dev.DeviceID.String()).
Str("device_signing_key", dev.SigningKey.String())
} else {
logEvt.Str("device_id", "unknown")
}
logEvt.Msg("Dropping event due to insufficient verification level")
err := deviceUnverifiedErrorWithExplanation(decrypted.Mautrix.TrustState)
go mx.sendCryptoStatusError(ctx, decrypted, errorEventID, err, retryCount, true)
return
}
copySomeKeys(original, decrypted)
mx.bridge.SendMessageSuccessCheckpoint(decrypted, status.MsgStepDecrypted, retryCount)
decrypted.Mautrix.CheckpointSent = true
decrypted.Mautrix.DecryptionDuration = duration
decrypted.Mautrix.EventSource |= event.SourceDecrypted
mx.bridge.EventProcessor.Dispatch(ctx, decrypted)
if errorEventID != "" {
_, _ = mx.bridge.Bot.RedactEvent(ctx, decrypted.RoomID, errorEventID)
}
}
func (mx *MatrixHandler) HandleEncrypted(ctx context.Context, evt *event.Event) {
defer mx.TrackEventDuration(evt.Type)()
if mx.shouldIgnoreEvent(evt) {
return
}
content := evt.Content.AsEncrypted()
log := zerolog.Ctx(ctx).With().
Str("event_id", evt.ID.String()).
Str("session_id", content.SessionID.String()).
Logger()
ctx = log.WithContext(ctx)
if mx.bridge.Crypto == nil {
go mx.sendCryptoStatusError(ctx, evt, "", errNoCrypto, 0, true)
return
}
log.Debug().Msg("Decrypting received event")
decryptionStart := time.Now()
decrypted, err := mx.bridge.Crypto.Decrypt(ctx, evt)
decryptionRetryCount := 0
if errors.Is(err, NoSessionFound) {
decryptionRetryCount = 1
log.Debug().
Int("wait_seconds", int(initialSessionWaitTimeout.Seconds())).
Msg("Couldn't find session, waiting for keys to arrive...")
mx.bridge.SendMessageErrorCheckpoint(evt, status.MsgStepDecrypted, err, false, 0)
if mx.bridge.Crypto.WaitForSession(ctx, evt.RoomID, content.SenderKey, content.SessionID, initialSessionWaitTimeout) {
log.Debug().Msg("Got keys after waiting, trying to decrypt event again")
decrypted, err = mx.bridge.Crypto.Decrypt(ctx, evt)
} else {
go mx.waitLongerForSession(ctx, evt, decryptionStart)
return
}
}
if err != nil {
mx.bridge.SendMessageErrorCheckpoint(evt, status.MsgStepDecrypted, err, true, decryptionRetryCount)
log.Warn().Err(err).Msg("Failed to decrypt event")
go mx.sendCryptoStatusError(ctx, evt, "", err, decryptionRetryCount, true)
return
}
mx.postDecrypt(ctx, evt, decrypted, decryptionRetryCount, "", time.Since(decryptionStart))
}
func (mx *MatrixHandler) waitLongerForSession(ctx context.Context, evt *event.Event, decryptionStart time.Time) {
log := zerolog.Ctx(ctx)
content := evt.Content.AsEncrypted()
log.Debug().
Int("wait_seconds", int(extendedSessionWaitTimeout.Seconds())).
Msg("Couldn't find session, requesting keys and waiting longer...")
go mx.bridge.Crypto.RequestSession(ctx, evt.RoomID, content.SenderKey, content.SessionID, evt.Sender, content.DeviceID)
errorEventID := mx.sendCryptoStatusError(ctx, evt, "", fmt.Errorf("%w. The bridge will retry for %d seconds", errNoDecryptionKeys, int(extendedSessionWaitTimeout.Seconds())), 1, false)
if !mx.bridge.Crypto.WaitForSession(ctx, evt.RoomID, content.SenderKey, content.SessionID, extendedSessionWaitTimeout) {
log.Debug().Msg("Didn't get session, giving up trying to decrypt event")
mx.sendCryptoStatusError(ctx, evt, errorEventID, errNoDecryptionKeys, 2, true)
return
}
log.Debug().Msg("Got keys after waiting longer, trying to decrypt event again")
decrypted, err := mx.bridge.Crypto.Decrypt(ctx, evt)
if err != nil {
log.Error().Err(err).Msg("Failed to decrypt event")
mx.sendCryptoStatusError(ctx, evt, errorEventID, err, 2, true)
return
}
mx.postDecrypt(ctx, evt, decrypted, 2, errorEventID, time.Since(decryptionStart))
}
func (mx *MatrixHandler) HandleMessage(ctx context.Context, evt *event.Event) {
defer mx.TrackEventDuration(evt.Type)()
log := zerolog.Ctx(ctx).With().
Str("event_id", evt.ID.String()).
Str("room_id", evt.RoomID.String()).
Str("sender", evt.Sender.String()).
Logger()
ctx = log.WithContext(ctx)
if mx.shouldIgnoreEvent(evt) {
return
} else if !evt.Mautrix.WasEncrypted && mx.bridge.Config.Bridge.GetEncryptionConfig().Require {
log.Warn().Msg("Dropping unencrypted event")
mx.sendCryptoStatusError(ctx, evt, "", errMessageNotEncrypted, 0, true)
return
}
user := mx.bridge.Child.GetIUser(evt.Sender, true)
if user == nil {
return
}
content := evt.Content.AsMessage()
content.RemoveReplyFallback()
if user.GetPermissionLevel() >= bridgeconfig.PermissionLevelUser && content.MsgType == event.MsgText {
commandPrefix := mx.bridge.Config.Bridge.GetCommandPrefix()
hasCommandPrefix := strings.HasPrefix(content.Body, commandPrefix)
if hasCommandPrefix {
content.Body = strings.TrimLeft(strings.TrimPrefix(content.Body, commandPrefix), " ")
}
if hasCommandPrefix || evt.RoomID == user.GetManagementRoomID() {
go mx.bridge.CommandProcessor.Handle(ctx, evt.RoomID, evt.ID, user, content.Body, content.RelatesTo.GetReplyTo())
go mx.bridge.SendMessageSuccessCheckpoint(evt, status.MsgStepCommand, 0)
if mx.bridge.Config.Bridge.EnableMessageStatusEvents() {
statusEvent := &event.BeeperMessageStatusEventContent{
// TODO: network
RelatesTo: event.RelatesTo{
Type: event.RelReference,
EventID: evt.ID,
},
Status: event.MessageStatusSuccess,
}
_, sendErr := mx.bridge.Bot.SendMessageEvent(ctx, evt.RoomID, event.BeeperMessageStatus, statusEvent)
if sendErr != nil {
log.Warn().Err(sendErr).Msg("Failed to send message status event for command")
}
}
return
}
}
portal := mx.bridge.Child.GetIPortal(evt.RoomID)
if portal != nil {
portal.ReceiveMatrixEvent(user, evt)
} else {
mx.bridge.SendMessageErrorCheckpoint(evt, status.MsgStepRemote, fmt.Errorf("unknown room"), true, 0)
}
}
func (mx *MatrixHandler) HandleReaction(_ context.Context, evt *event.Event) {
defer mx.TrackEventDuration(evt.Type)()
if mx.shouldIgnoreEvent(evt) {
return
}
user := mx.bridge.Child.GetIUser(evt.Sender, true)
if user == nil || user.GetPermissionLevel() < bridgeconfig.PermissionLevelUser || !user.IsLoggedIn() {
return
}
portal := mx.bridge.Child.GetIPortal(evt.RoomID)
if portal != nil {
portal.ReceiveMatrixEvent(user, evt)
} else {
mx.bridge.SendMessageErrorCheckpoint(evt, status.MsgStepRemote, fmt.Errorf("unknown room"), true, 0)
}
}
func (mx *MatrixHandler) HandleRedaction(_ context.Context, evt *event.Event) {
defer mx.TrackEventDuration(evt.Type)()
if mx.shouldIgnoreEvent(evt) {
return
}
user := mx.bridge.Child.GetIUser(evt.Sender, true)
if user == nil {
return
}
portal := mx.bridge.Child.GetIPortal(evt.RoomID)
if portal != nil {
portal.ReceiveMatrixEvent(user, evt)
} else {
mx.bridge.SendMessageErrorCheckpoint(evt, status.MsgStepRemote, fmt.Errorf("unknown room"), true, 0)
}
}
func (mx *MatrixHandler) HandleReceipt(_ context.Context, evt *event.Event) {
portal := mx.bridge.Child.GetIPortal(evt.RoomID)
if portal == nil {
return
}
rrPortal, ok := portal.(ReadReceiptHandlingPortal)
if !ok {
return
}
for eventID, receipts := range *evt.Content.AsReceipt() {
for userID, receipt := range receipts[event.ReceiptTypeRead] {
user := mx.bridge.Child.GetIUser(userID, false)
if user == nil {
// Not a bridge user
continue
}
customPuppet := user.GetIDoublePuppet()
if val, ok := receipt.Extra[appservice.DoublePuppetKey].(string); ok && customPuppet != nil && val == mx.bridge.Name {
// Ignore double puppeted read receipts.
mx.log.Debug().Interface("content", evt.Content.Raw).Msg("Ignoring double-puppeted read receipt")
// But do start disappearing messages, because the user read the chat
dp, ok := portal.(DisappearingPortal)
if ok {
dp.ScheduleDisappearing()
}
} else {
rrPortal.HandleMatrixReadReceipt(user, eventID, receipt)
}
}
}
}
func (mx *MatrixHandler) HandleTyping(_ context.Context, evt *event.Event) {
portal := mx.bridge.Child.GetIPortal(evt.RoomID)
if portal == nil {
return
}
typingPortal, ok := portal.(TypingPortal)
if !ok {
return
}
typingPortal.HandleMatrixTyping(evt.Content.AsTyping().UserIDs)
}

View file

@ -1,61 +0,0 @@
// Copyright (c) 2021 Sumner Evans
// 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 bridge
import (
"maunium.net/go/mautrix/appservice"
"maunium.net/go/mautrix/bridge/status"
"maunium.net/go/mautrix/event"
)
func (br *Bridge) SendMessageSuccessCheckpoint(evt *event.Event, step status.MessageCheckpointStep, retryNum int) {
br.SendMessageCheckpoint(evt, step, nil, status.MsgStatusSuccess, retryNum)
}
func (br *Bridge) SendMessageErrorCheckpoint(evt *event.Event, step status.MessageCheckpointStep, err error, permanent bool, retryNum int) {
s := status.MsgStatusWillRetry
if permanent {
s = status.MsgStatusPermFailure
}
br.SendMessageCheckpoint(evt, step, err, s, retryNum)
}
func (br *Bridge) SendMessageCheckpoint(evt *event.Event, step status.MessageCheckpointStep, err error, s status.MessageCheckpointStatus, retryNum int) {
checkpoint := status.NewMessageCheckpoint(evt, step, s, retryNum)
if err != nil {
checkpoint.Info = err.Error()
}
go br.SendRawMessageCheckpoint(checkpoint)
}
func (br *Bridge) SendRawMessageCheckpoint(cp *status.MessageCheckpoint) {
err := br.SendMessageCheckpoints([]*status.MessageCheckpoint{cp})
if err != nil {
br.ZLog.Warn().Err(err).Interface("message_checkpoint", cp).Msg("Error sending message checkpoint")
} else {
br.ZLog.Debug().Interface("message_checkpoint", cp).Msg("Sent message checkpoint")
}
}
func (br *Bridge) SendMessageCheckpoints(checkpoints []*status.MessageCheckpoint) error {
checkpointsJSON := status.CheckpointsJSON{Checkpoints: checkpoints}
if br.Websocket {
return br.AS.SendWebsocket(&appservice.WebsocketRequest{
Command: "message_checkpoint",
Data: checkpointsJSON,
})
}
endpoint := br.Config.Homeserver.MessageSendCheckpointEndpoint
if endpoint == "" {
return nil
}
return checkpointsJSON.SendHTTP(endpoint, br.AS.Registration.AppToken)
}

248
bridgev2/backfillqueue.go Normal file
View file

@ -0,0 +1,248 @@
// 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 bridgev2
import (
"context"
"fmt"
"runtime/debug"
"time"
"github.com/rs/zerolog"
"maunium.net/go/mautrix/bridgev2/database"
)
const BackfillMinBackoffAfterRoomCreate = 1 * time.Minute
const BackfillQueueErrorBackoff = 1 * time.Minute
const BackfillQueueMaxEmptyBackoff = 10 * time.Minute
func (br *Bridge) WakeupBackfillQueue() {
select {
case br.wakeupBackfillQueue <- struct{}{}:
default:
}
}
func (br *Bridge) RunBackfillQueue() {
if !br.Config.Backfill.Queue.Enabled || !br.Config.Backfill.Enabled {
return
}
log := br.Log.With().Str("component", "backfill queue").Logger()
if !br.Matrix.GetCapabilities().BatchSending {
log.Warn().Msg("Backfill queue is enabled in config, but Matrix server doesn't support batch sending")
return
}
ctx, cancel := context.WithCancel(log.WithContext(context.Background()))
br.stopBackfillQueue.Clear()
stopChan := br.stopBackfillQueue.GetChan()
go func() {
<-stopChan
cancel()
}()
batchDelay := time.Duration(br.Config.Backfill.Queue.BatchDelay) * time.Second
log.Info().Stringer("batch_delay", batchDelay).Msg("Backfill queue starting")
noTasksFoundCount := 0
for {
nextDelay := batchDelay
if noTasksFoundCount > 0 {
extraDelay := batchDelay * time.Duration(noTasksFoundCount)
nextDelay += min(BackfillQueueMaxEmptyBackoff, extraDelay)
}
timer := time.NewTimer(nextDelay)
select {
case <-br.wakeupBackfillQueue:
if !timer.Stop() {
select {
case <-timer.C:
default:
}
}
noTasksFoundCount = 0
case <-stopChan:
if !timer.Stop() {
select {
case <-timer.C:
default:
}
}
log.Info().Msg("Stopping backfill queue")
return
case <-timer.C:
}
backfillTask, err := br.DB.BackfillTask.GetNext(ctx)
if err != nil {
log.Err(err).Msg("Failed to get next backfill queue entry")
time.Sleep(BackfillQueueErrorBackoff)
continue
} else if backfillTask != nil {
br.DoBackfillTask(ctx, backfillTask)
noTasksFoundCount = 0
}
}
}
func (br *Bridge) DoBackfillTask(ctx context.Context, task *database.BackfillTask) {
log := zerolog.Ctx(ctx).With().
Object("portal_key", task.PortalKey).
Str("login_id", string(task.UserLoginID)).
Logger()
defer func() {
err := recover()
if err != nil {
logEvt := log.Error().
Bytes(zerolog.ErrorStackFieldName, debug.Stack())
if realErr, ok := err.(error); ok {
logEvt = logEvt.Err(realErr)
} else {
logEvt = logEvt.Any(zerolog.ErrorFieldName, err)
}
logEvt.Msg("Panic in backfill queue")
}
}()
ctx = log.WithContext(ctx)
err := br.DB.BackfillTask.MarkDispatched(ctx, task)
if err != nil {
log.Err(err).Msg("Failed to mark backfill task as dispatched")
time.Sleep(BackfillQueueErrorBackoff)
return
}
completed, err := br.actuallyDoBackfillTask(ctx, task)
if err != nil {
log.Err(err).Msg("Failed to do backfill task")
time.Sleep(BackfillQueueErrorBackoff)
return
} else if completed {
log.Info().
Int("batch_count", task.BatchCount).
Bool("is_done", task.IsDone).
Msg("Backfill task completed successfully")
} else {
log.Info().
Int("batch_count", task.BatchCount).
Bool("is_done", task.IsDone).
Msg("Backfill task canceled")
}
err = br.DB.BackfillTask.Update(ctx, task)
if err != nil {
log.Err(err).Msg("Failed to update backfill task")
time.Sleep(BackfillQueueErrorBackoff)
}
}
func (portal *Portal) deleteBackfillQueueTaskIfRoomDoesNotExist(ctx context.Context) bool {
// Acquire the room create lock to ensure that task deletion doesn't race with room creation
portal.roomCreateLock.Lock()
defer portal.roomCreateLock.Unlock()
if portal.MXID == "" {
zerolog.Ctx(ctx).Debug().Msg("Portal for backfill task doesn't exist, deleting entry")
err := portal.Bridge.DB.BackfillTask.Delete(ctx, portal.PortalKey)
if err != nil {
zerolog.Ctx(ctx).Err(err).Msg("Failed to delete backfill task after portal wasn't found")
}
return true
}
return false
}
func (br *Bridge) actuallyDoBackfillTask(ctx context.Context, task *database.BackfillTask) (bool, error) {
log := zerolog.Ctx(ctx)
portal, err := br.GetExistingPortalByKey(ctx, task.PortalKey)
if err != nil {
return false, fmt.Errorf("failed to get portal for backfill task: %w", err)
} else if portal == nil {
log.Warn().Msg("Portal not found for backfill task")
err = br.DB.BackfillTask.Delete(ctx, task.PortalKey)
if err != nil {
log.Err(err).Msg("Failed to delete backfill task after portal wasn't found")
time.Sleep(BackfillQueueErrorBackoff)
}
return false, nil
} else if portal.MXID == "" {
portal.deleteBackfillQueueTaskIfRoomDoesNotExist(ctx)
return false, nil
}
login, err := br.GetExistingUserLoginByID(ctx, task.UserLoginID)
if err != nil {
return false, fmt.Errorf("failed to get user login for backfill task: %w", err)
} else if login == nil || !login.Client.IsLoggedIn() {
if login == nil {
log.Warn().Msg("User login not found for backfill task")
} else {
log.Warn().Msg("User login not logged in for backfill task")
}
logins, err := br.GetUserLoginsInPortal(ctx, portal.PortalKey)
if err != nil {
return false, fmt.Errorf("failed to get user portals for backfill task: %w", err)
} else if len(logins) == 0 {
log.Debug().Msg("No user logins found for backfill task")
task.NextDispatchMinTS = database.BackfillNextDispatchNever
if login == nil {
task.UserLoginID = ""
}
return false, nil
}
if login == nil {
task.UserLoginID = ""
}
foundLogin := false
for _, login = range logins {
if login.Client.IsLoggedIn() {
foundLogin = true
task.UserLoginID = login.ID
log.UpdateContext(func(c zerolog.Context) zerolog.Context {
return c.Str("overridden_login_id", string(login.ID))
})
log.Debug().Msg("Found user login for backfill task")
break
}
}
if !foundLogin {
log.Debug().Msg("No logged in user logins found for backfill task")
task.NextDispatchMinTS = database.BackfillNextDispatchNever
return false, nil
}
}
if task.BatchCount < 0 {
var msgCount int
msgCount, err = br.DB.Message.CountMessagesInPortal(ctx, task.PortalKey)
if err != nil {
return false, fmt.Errorf("failed to count messages in portal: %w", err)
}
task.BatchCount = msgCount / br.Config.Backfill.Queue.BatchSize
log.Debug().
Int("message_count", msgCount).
Int("batch_count", task.BatchCount).
Msg("Calculated existing batch count")
}
maxBatches := br.Config.Backfill.Queue.MaxBatches
api, ok := login.Client.(BackfillingNetworkAPI)
if !ok {
return false, fmt.Errorf("network API does not support backfilling")
}
limiterAPI, ok := api.(BackfillingNetworkAPIWithLimits)
if ok {
maxBatches = limiterAPI.GetBackfillMaxBatchCount(ctx, portal, task)
}
if maxBatches < 0 || maxBatches > task.BatchCount {
err = portal.DoBackwardsBackfill(ctx, login, task)
if err != nil {
return false, fmt.Errorf("failed to backfill: %w", err)
}
task.BatchCount++
} else {
log.Debug().
Int("max_batches", maxBatches).
Int("batch_count", task.BatchCount).
Msg("Not actually backfilling: max batches reached")
}
task.IsDone = task.IsDone || (maxBatches > 0 && task.BatchCount >= maxBatches)
batchDelay := time.Duration(br.Config.Backfill.Queue.BatchDelay) * time.Second
task.CompletedAt = time.Now()
task.NextDispatchMinTS = task.CompletedAt.Add(batchDelay)
return true, nil
}

458
bridgev2/bridge.go Normal file
View file

@ -0,0 +1,458 @@
// 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 bridgev2
import (
"context"
"fmt"
"os"
"sync"
"sync/atomic"
"time"
"github.com/rs/zerolog"
"go.mau.fi/util/dbutil"
"go.mau.fi/util/exhttp"
"go.mau.fi/util/exsync"
"maunium.net/go/mautrix/bridgev2/bridgeconfig"
"maunium.net/go/mautrix/bridgev2/database"
"maunium.net/go/mautrix/bridgev2/networkid"
"maunium.net/go/mautrix/bridgev2/status"
"maunium.net/go/mautrix/id"
)
type CommandProcessor interface {
Handle(ctx context.Context, roomID id.RoomID, eventID id.EventID, user *User, message string, replyTo id.EventID)
}
type Bridge struct {
ID networkid.BridgeID
DB *database.Database
Log zerolog.Logger
Matrix MatrixConnector
Bot MatrixAPI
Network NetworkConnector
Commands CommandProcessor
Config *bridgeconfig.BridgeConfig
DisappearLoop *DisappearLoop
usersByMXID map[id.UserID]*User
userLoginsByID map[networkid.UserLoginID]*UserLogin
portalsByKey map[networkid.PortalKey]*Portal
portalsByMXID map[id.RoomID]*Portal
ghostsByID map[networkid.UserID]*Ghost
cacheLock sync.Mutex
didSplitPortals bool
Background bool
ExternallyManagedDB bool
stopping atomic.Bool
wakeupBackfillQueue chan struct{}
stopBackfillQueue *exsync.Event
BackgroundCtx context.Context
cancelBackgroundCtx context.CancelFunc
}
func NewBridge(
bridgeID networkid.BridgeID,
db *dbutil.Database,
log zerolog.Logger,
cfg *bridgeconfig.BridgeConfig,
matrix MatrixConnector,
network NetworkConnector,
newCommandProcessor func(*Bridge) CommandProcessor,
) *Bridge {
br := &Bridge{
ID: bridgeID,
DB: database.New(bridgeID, network.GetDBMetaTypes(), db),
Log: log,
Matrix: matrix,
Network: network,
Config: cfg,
usersByMXID: make(map[id.UserID]*User),
userLoginsByID: make(map[networkid.UserLoginID]*UserLogin),
portalsByKey: make(map[networkid.PortalKey]*Portal),
portalsByMXID: make(map[id.RoomID]*Portal),
ghostsByID: make(map[networkid.UserID]*Ghost),
wakeupBackfillQueue: make(chan struct{}),
stopBackfillQueue: exsync.NewEvent(),
}
if br.Config == nil {
br.Config = &bridgeconfig.BridgeConfig{CommandPrefix: "!bridge"}
}
br.Commands = newCommandProcessor(br)
br.Matrix.Init(br)
br.Bot = br.Matrix.BotIntent()
br.Network.Init(br)
br.DisappearLoop = &DisappearLoop{br: br}
return br
}
type DBUpgradeError struct {
Err error
Section string
}
func (e DBUpgradeError) Error() string {
return e.Err.Error()
}
func (e DBUpgradeError) Unwrap() error {
return e.Err
}
func (br *Bridge) Start(ctx context.Context) error {
ctx = br.Log.WithContext(ctx)
err := br.StartConnectors(ctx)
if err != nil {
return err
}
err = br.StartLogins(ctx)
if err != nil {
return err
}
go br.PostStart(ctx)
return nil
}
func (br *Bridge) RunOnce(ctx context.Context, loginID networkid.UserLoginID, params *ConnectBackgroundParams) error {
br.Background = true
br.stopping.Store(false)
err := br.StartConnectors(ctx)
if err != nil {
return err
}
if loginID == "" {
br.Log.Info().Msg("No login ID provided to RunOnce, running all logins for 20 seconds")
err = br.StartLogins(ctx)
if err != nil {
return err
}
defer br.StopWithTimeout(5 * time.Second)
select {
case <-time.After(20 * time.Second):
case <-ctx.Done():
}
return nil
}
defer br.stop(true, 5*time.Second)
login, err := br.GetExistingUserLoginByID(ctx, loginID)
if err != nil {
return fmt.Errorf("failed to get user login: %w", err)
} else if login == nil {
return ErrNotLoggedIn
}
syncClient, ok := login.Client.(BackgroundSyncingNetworkAPI)
if !ok {
br.Log.Warn().Msg("Network connector doesn't implement background mode, using fallback mechanism for RunOnce")
login.Client.Connect(ctx)
defer login.DisconnectWithTimeout(5 * time.Second)
select {
case <-time.After(20 * time.Second):
case <-ctx.Done():
}
br.stopping.Store(true)
return nil
} else {
br.Log.Info().Str("user_login_id", string(login.ID)).Msg("Starting individual user login in background mode")
return syncClient.ConnectBackground(login.Log.WithContext(ctx), params)
}
}
func (br *Bridge) StartConnectors(ctx context.Context) error {
br.Log.Info().Msg("Starting bridge")
br.stopping.Store(false)
if br.BackgroundCtx == nil || br.BackgroundCtx.Err() != nil {
br.BackgroundCtx, br.cancelBackgroundCtx = context.WithCancel(context.Background())
br.BackgroundCtx = br.Log.WithContext(br.BackgroundCtx)
}
if !br.ExternallyManagedDB {
err := br.DB.Upgrade(ctx)
if err != nil {
return DBUpgradeError{Err: err, Section: "main"}
}
}
if !br.Background {
var postMigrate func()
br.didSplitPortals, postMigrate = br.MigrateToSplitPortals(ctx)
if postMigrate != nil {
defer postMigrate()
}
}
br.Log.Info().Msg("Starting Matrix connector")
err := br.Matrix.Start(ctx)
if err != nil {
return fmt.Errorf("failed to start Matrix connector: %w", err)
}
br.Log.Info().Msg("Starting network connector")
err = br.Network.Start(ctx)
if err != nil {
return fmt.Errorf("failed to start network connector: %w", err)
}
if br.Network.GetCapabilities().DisappearingMessages && !br.Background {
go br.DisappearLoop.Start()
}
return nil
}
func (br *Bridge) PostStart(ctx context.Context) {
if br.Background {
return
}
rawBridgeInfoVer := br.DB.KV.Get(ctx, database.KeyBridgeInfoVersion)
bridgeInfoVer, capVer, err := parseBridgeInfoVersion(rawBridgeInfoVer)
if err != nil {
br.Log.Err(err).Str("db_bridge_info_version", rawBridgeInfoVer).Msg("Failed to parse bridge info version")
return
}
expectedBridgeInfoVer, expectedCapVer := br.Network.GetBridgeInfoVersion()
doResendBridgeInfo := bridgeInfoVer != expectedBridgeInfoVer || br.didSplitPortals || br.Config.ResendBridgeInfo
doResendCapabilities := capVer != expectedCapVer || br.didSplitPortals
if doResendBridgeInfo || doResendCapabilities {
br.ResendBridgeInfo(ctx, doResendBridgeInfo, doResendCapabilities)
}
br.DB.KV.Set(ctx, database.KeyBridgeInfoVersion, fmt.Sprintf("%d,%d", expectedBridgeInfoVer, expectedCapVer))
}
func parseBridgeInfoVersion(version string) (info, capabilities int, err error) {
_, err = fmt.Sscanf(version, "%d,%d", &info, &capabilities)
if version == "" {
err = nil
}
return
}
func (br *Bridge) ResendBridgeInfo(ctx context.Context, resendInfo, resendCaps bool) {
log := zerolog.Ctx(ctx).With().Str("action", "resend bridge info").Logger()
portals, err := br.GetAllPortalsWithMXID(ctx)
if err != nil {
log.Err(err).Msg("Failed to get portals")
return
}
for _, portal := range portals {
if resendInfo {
portal.UpdateBridgeInfo(ctx)
}
if resendCaps {
logins, err := br.GetUserLoginsInPortal(ctx, portal.PortalKey)
if err != nil {
log.Err(err).
Stringer("room_id", portal.MXID).
Object("portal_key", portal.PortalKey).
Msg("Failed to get user logins in portal")
} else {
found := false
for _, login := range logins {
if portal.CapState.ID == "" || login.ID == portal.CapState.Source {
portal.UpdateCapabilities(ctx, login, true)
found = true
}
}
if !found && len(logins) > 0 {
portal.CapState.Source = ""
portal.UpdateCapabilities(ctx, logins[0], true)
} else if !found {
log.Warn().
Stringer("room_id", portal.MXID).
Object("portal_key", portal.PortalKey).
Msg("No user login found to update capabilities")
}
}
}
}
log.Info().
Bool("capabilities", resendCaps).
Bool("info", resendInfo).
Msg("Resent bridge info to all portals")
}
func (br *Bridge) MigrateToSplitPortals(ctx context.Context) (bool, func()) {
log := zerolog.Ctx(ctx).With().Str("action", "migrate to split portals").Logger()
ctx = log.WithContext(ctx)
if !br.Config.SplitPortals || br.DB.KV.Get(ctx, database.KeySplitPortalsEnabled) == "true" {
return false, nil
}
affected, err := br.DB.Portal.MigrateToSplitPortals(ctx)
if err != nil {
log.WithLevel(zerolog.FatalLevel).Err(err).Msg("Failed to migrate portals")
os.Exit(31)
return false, nil
}
log.Info().Int64("rows_affected", affected).Msg("Migrated to split portals")
affected2, err := br.DB.Portal.FixParentsAfterSplitPortalMigration(ctx)
if err != nil {
log.Err(err).Msg("Failed to fix parent portals after split portal migration")
os.Exit(31)
return false, nil
}
log.Info().Int64("rows_affected", affected2).Msg("Updated parent receivers after split portal migration")
withoutReceiver, err := br.DB.Portal.GetAllWithoutReceiver(ctx)
if err != nil {
log.WithLevel(zerolog.FatalLevel).Err(err).Msg("Failed to get portals that failed to migrate")
os.Exit(31)
return false, nil
}
var roomsToDelete []id.RoomID
log.Info().Int("remaining_portals", len(withoutReceiver)).Msg("Deleting remaining portals without receiver")
for _, portal := range withoutReceiver {
if err = br.DB.Portal.Delete(ctx, portal.PortalKey); err != nil {
log.Err(err).
Str("portal_id", string(portal.ID)).
Stringer("mxid", portal.MXID).
Msg("Failed to delete portal database row that failed to migrate")
} else if portal.MXID != "" {
log.Debug().
Str("portal_id", string(portal.ID)).
Stringer("mxid", portal.MXID).
Msg("Marked portal room for deletion from homeserver")
roomsToDelete = append(roomsToDelete, portal.MXID)
} else {
log.Debug().
Str("portal_id", string(portal.ID)).
Msg("Deleted portal row with no Matrix room")
}
}
br.DB.KV.Set(ctx, database.KeySplitPortalsEnabled, "true")
log.Info().Msg("Finished split portal migration successfully")
return affected > 0, func() {
for _, roomID := range roomsToDelete {
if err = br.Bot.DeleteRoom(ctx, roomID, true); err != nil {
log.Err(err).
Stringer("mxid", roomID).
Msg("Failed to delete portal room that failed to migrate")
}
}
log.Info().Int("room_count", len(roomsToDelete)).Msg("Finished deleting rooms that failed to migrate")
}
}
func (br *Bridge) StartLogins(ctx context.Context) error {
userIDs, err := br.DB.UserLogin.GetAllUserIDsWithLogins(ctx)
if err != nil {
return fmt.Errorf("failed to get users with logins: %w", err)
}
startedAny := false
for _, userID := range userIDs {
br.Log.Info().Stringer("user_id", userID).Msg("Loading user")
var user *User
user, err = br.GetUserByMXID(ctx, userID)
if err != nil {
br.Log.Err(err).Stringer("user_id", userID).Msg("Failed to load user")
} else {
for _, login := range user.GetUserLogins() {
startedAny = true
br.Log.Info().Str("id", string(login.ID)).Msg("Starting user login")
login.Client.Connect(login.Log.WithContext(ctx))
}
}
}
if !startedAny {
br.Log.Info().Msg("No user logins found")
br.SendGlobalBridgeState(status.BridgeState{StateEvent: status.StateUnconfigured})
}
if !br.Background {
go br.RunBackfillQueue()
}
br.Log.Info().Msg("Bridge started")
return nil
}
func (br *Bridge) ResetNetworkConnections() {
nrn, ok := br.Network.(NetworkResettingNetwork)
if ok {
br.Log.Info().Msg("Resetting network connections with NetworkConnector.ResetNetworkConnections")
nrn.ResetNetworkConnections()
return
}
br.Log.Info().Msg("Network connector doesn't support ResetNetworkConnections, recreating clients manually")
for _, login := range br.GetAllCachedUserLogins() {
login.Log.Debug().Msg("Disconnecting and recreating client for network reset")
ctx := login.Log.WithContext(br.BackgroundCtx)
login.Client.Disconnect()
err := login.recreateClient(ctx)
if err != nil {
login.Log.Err(err).Msg("Failed to recreate client during network reset")
login.BridgeState.Send(status.BridgeState{
StateEvent: status.StateUnknownError,
Error: "bridgev2-network-reset-fail",
Info: map[string]any{"go_error": err.Error()},
})
} else {
login.Client.Connect(ctx)
}
}
br.Log.Info().Msg("Finished resetting all user logins")
}
func (br *Bridge) GetHTTPClientSettings() exhttp.ClientSettings {
mchs, ok := br.Matrix.(MatrixConnectorWithHTTPSettings)
if ok {
return mchs.GetHTTPClientSettings()
}
return exhttp.SensibleClientSettings
}
func (br *Bridge) IsStopping() bool {
return br.stopping.Load()
}
func (br *Bridge) Stop() {
br.stop(false, 0)
}
func (br *Bridge) StopWithTimeout(timeout time.Duration) {
br.stop(false, timeout)
}
func (br *Bridge) stop(isRunOnce bool, timeout time.Duration) {
br.Log.Info().Msg("Shutting down bridge")
br.stopping.Store(true)
br.DisappearLoop.Stop()
br.stopBackfillQueue.Set()
br.Matrix.PreStop()
if !isRunOnce {
br.cacheLock.Lock()
var wg sync.WaitGroup
wg.Add(len(br.userLoginsByID))
for _, login := range br.userLoginsByID {
go func() {
login.DisconnectWithTimeout(timeout)
wg.Done()
}()
}
br.cacheLock.Unlock()
wg.Wait()
}
br.Matrix.Stop()
if br.cancelBackgroundCtx != nil {
br.cancelBackgroundCtx()
}
if stopNet, ok := br.Network.(StoppableNetwork); ok {
stopNet.Stop()
}
if !br.ExternallyManagedDB {
err := br.DB.Close()
if err != nil {
br.Log.Warn().Err(err).Msg("Failed to close database")
}
}
br.Log.Info().Msg("Shutdown complete")
}

View file

@ -0,0 +1,140 @@
// 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 bridgeconfig
import (
"fmt"
"regexp"
"strings"
"text/template"
"go.mau.fi/util/exerrors"
"go.mau.fi/util/random"
"gopkg.in/yaml.v3"
"maunium.net/go/mautrix/appservice"
"maunium.net/go/mautrix/id"
)
type AppserviceConfig struct {
Address string `yaml:"address"`
PublicAddress string `yaml:"public_address"`
Hostname string `yaml:"hostname"`
Port uint16 `yaml:"port"`
ID string `yaml:"id"`
Bot BotUserConfig `yaml:"bot"`
ASToken string `yaml:"as_token"`
HSToken string `yaml:"hs_token"`
EphemeralEvents bool `yaml:"ephemeral_events"`
AsyncTransactions bool `yaml:"async_transactions"`
UsernameTemplate string `yaml:"username_template"`
usernameTemplate *template.Template `yaml:"-"`
}
func (asc *AppserviceConfig) FormatUsername(username string) string {
if asc.usernameTemplate == nil {
asc.usernameTemplate = exerrors.Must(template.New("username").Parse(asc.UsernameTemplate))
}
var buf strings.Builder
_ = asc.usernameTemplate.Execute(&buf, username)
return buf.String()
}
func (config *Config) MakeUserIDRegex(matcher string) *regexp.Regexp {
usernamePlaceholder := strings.ToLower(random.String(16))
usernameTemplate := fmt.Sprintf("@%s:%s",
config.AppService.FormatUsername(usernamePlaceholder),
config.Homeserver.Domain)
usernameTemplate = regexp.QuoteMeta(usernameTemplate)
usernameTemplate = strings.Replace(usernameTemplate, usernamePlaceholder, matcher, 1)
usernameTemplate = fmt.Sprintf("^%s$", usernameTemplate)
return regexp.MustCompile(usernameTemplate)
}
// GetRegistration copies the data from the bridge config into an *appservice.Registration struct.
// This can't be used with the homeserver, see GenerateRegistration for generating files for the homeserver.
func (asc *AppserviceConfig) GetRegistration() *appservice.Registration {
reg := &appservice.Registration{}
asc.copyToRegistration(reg)
reg.SenderLocalpart = asc.Bot.Username
reg.ServerToken = asc.HSToken
reg.AppToken = asc.ASToken
return reg
}
func (asc *AppserviceConfig) copyToRegistration(registration *appservice.Registration) {
registration.ID = asc.ID
registration.URL = asc.Address
falseVal := false
registration.RateLimited = &falseVal
registration.EphemeralEvents = asc.EphemeralEvents
registration.SoruEphemeralEvents = asc.EphemeralEvents
}
func (ec *EncryptionConfig) applyUnstableFlags(registration *appservice.Registration) {
registration.MSC4190 = ec.MSC4190
registration.MSC3202 = ec.Appservice
}
// GenerateRegistration generates a registration file for the homeserver.
func (config *Config) GenerateRegistration() *appservice.Registration {
registration := appservice.CreateRegistration()
config.AppService.HSToken = registration.ServerToken
config.AppService.ASToken = registration.AppToken
config.AppService.copyToRegistration(registration)
config.Encryption.applyUnstableFlags(registration)
registration.SenderLocalpart = random.String(32)
botRegex := regexp.MustCompile(fmt.Sprintf("^@%s:%s$",
regexp.QuoteMeta(config.AppService.Bot.Username),
regexp.QuoteMeta(config.Homeserver.Domain)))
registration.Namespaces.UserIDs.Register(botRegex, true)
registration.Namespaces.UserIDs.Register(config.MakeUserIDRegex(".*"), true)
return registration
}
func (config *Config) MakeAppService() *appservice.AppService {
as := appservice.Create()
as.HomeserverDomain = config.Homeserver.Domain
_ = as.SetHomeserverURL(config.Homeserver.Address)
as.Host.Hostname = config.AppService.Hostname
as.Host.Port = config.AppService.Port
as.Registration = config.AppService.GetRegistration()
config.Encryption.applyUnstableFlags(as.Registration)
return as
}
type BotUserConfig struct {
Username string `yaml:"username"`
Displayname string `yaml:"displayname"`
Avatar string `yaml:"avatar"`
ParsedAvatar id.ContentURI `yaml:"-"`
}
type serializableBUC BotUserConfig
func (buc *BotUserConfig) UnmarshalYAML(node *yaml.Node) error {
var sbuc serializableBUC
err := node.Decode(&sbuc)
if err != nil {
return err
}
*buc = (BotUserConfig)(sbuc)
if buc.Avatar != "" && buc.Avatar != "remove" {
buc.ParsedAvatar, err = id.ParseContentURI(buc.Avatar)
if err != nil {
return fmt.Errorf("%w in bot avatar", err)
}
}
return nil
}

View file

@ -0,0 +1,45 @@
// 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 bridgeconfig
type BackfillConfig struct {
Enabled bool `yaml:"enabled"`
MaxInitialMessages int `yaml:"max_initial_messages"`
MaxCatchupMessages int `yaml:"max_catchup_messages"`
UnreadHoursThreshold int `yaml:"unread_hours_threshold"`
Threads BackfillThreadsConfig `yaml:"threads"`
Queue BackfillQueueConfig `yaml:"queue"`
// Flag to indicate that the creator will not run the backfill queue but will still paginate
// backfill by calling DoBackfillTask directly. Note that this is not used anywhere within
// mautrix-go and exists so bridges can use it to decide when to drop backfill data.
WillPaginateManually bool `yaml:"will_paginate_manually"`
}
type BackfillThreadsConfig struct {
MaxInitialMessages int `yaml:"max_initial_messages"`
}
type BackfillQueueConfig struct {
Enabled bool `yaml:"enabled"`
BatchSize int `yaml:"batch_size"`
BatchDelay int `yaml:"batch_delay"`
MaxBatches int `yaml:"max_batches"`
MaxBatchesOverride map[string]int `yaml:"max_batches_override"`
}
func (bqc *BackfillQueueConfig) GetOverride(names ...string) int {
for _, name := range names {
override, ok := bqc.MaxBatchesOverride[name]
if ok {
return override
}
}
return bqc.MaxBatches
}

View file

@ -0,0 +1,139 @@
// 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 bridgeconfig
import (
"time"
"go.mau.fi/util/dbutil"
"go.mau.fi/zeroconfig"
"gopkg.in/yaml.v3"
"maunium.net/go/mautrix/event"
"maunium.net/go/mautrix/mediaproxy"
)
type Config struct {
Network yaml.Node `yaml:"network"`
Bridge BridgeConfig `yaml:"bridge"`
Database dbutil.Config `yaml:"database"`
Homeserver HomeserverConfig `yaml:"homeserver"`
AppService AppserviceConfig `yaml:"appservice"`
Matrix MatrixConfig `yaml:"matrix"`
Analytics AnalyticsConfig `yaml:"analytics"`
Provisioning ProvisioningConfig `yaml:"provisioning"`
PublicMedia PublicMediaConfig `yaml:"public_media"`
DirectMedia DirectMediaConfig `yaml:"direct_media"`
Backfill BackfillConfig `yaml:"backfill"`
DoublePuppet DoublePuppetConfig `yaml:"double_puppet"`
Encryption EncryptionConfig `yaml:"encryption"`
Logging zeroconfig.Config `yaml:"logging"`
EnvConfigPrefix string `yaml:"env_config_prefix"`
ManagementRoomTexts ManagementRoomTexts `yaml:"management_room_texts"`
}
type CleanupAction string
const (
CleanupActionNull CleanupAction = ""
CleanupActionNothing CleanupAction = "nothing"
CleanupActionKick CleanupAction = "kick"
CleanupActionUnbridge CleanupAction = "unbridge"
CleanupActionDelete CleanupAction = "delete"
)
type CleanupOnLogout struct {
Private CleanupAction `yaml:"private"`
Relayed CleanupAction `yaml:"relayed"`
SharedNoUsers CleanupAction `yaml:"shared_no_users"`
SharedHasUsers CleanupAction `yaml:"shared_has_users"`
}
type CleanupOnLogouts struct {
Enabled bool `yaml:"enabled"`
Manual CleanupOnLogout `yaml:"manual"`
BadCredentials CleanupOnLogout `yaml:"bad_credentials"`
}
type BridgeConfig struct {
CommandPrefix string `yaml:"command_prefix"`
PersonalFilteringSpaces bool `yaml:"personal_filtering_spaces"`
PrivateChatPortalMeta bool `yaml:"private_chat_portal_meta"`
AsyncEvents bool `yaml:"async_events"`
SplitPortals bool `yaml:"split_portals"`
ResendBridgeInfo bool `yaml:"resend_bridge_info"`
NoBridgeInfoStateKey bool `yaml:"no_bridge_info_state_key"`
BridgeStatusNotices string `yaml:"bridge_status_notices"`
UnknownErrorAutoReconnect time.Duration `yaml:"unknown_error_auto_reconnect"`
UnknownErrorMaxAutoReconnects int `yaml:"unknown_error_max_auto_reconnects"`
BridgeMatrixLeave bool `yaml:"bridge_matrix_leave"`
BridgeNotices bool `yaml:"bridge_notices"`
TagOnlyOnCreate bool `yaml:"tag_only_on_create"`
OnlyBridgeTags []event.RoomTag `yaml:"only_bridge_tags"`
MuteOnlyOnCreate bool `yaml:"mute_only_on_create"`
DeduplicateMatrixMessages bool `yaml:"deduplicate_matrix_messages"`
CrossRoomReplies bool `yaml:"cross_room_replies"`
OutgoingMessageReID bool `yaml:"outgoing_message_re_id"`
RevertFailedStateChanges bool `yaml:"revert_failed_state_changes"`
KickMatrixUsers bool `yaml:"kick_matrix_users"`
CleanupOnLogout CleanupOnLogouts `yaml:"cleanup_on_logout"`
Relay RelayConfig `yaml:"relay"`
Permissions PermissionConfig `yaml:"permissions"`
Backfill BackfillConfig `yaml:"backfill"`
}
type MatrixConfig struct {
MessageStatusEvents bool `yaml:"message_status_events"`
DeliveryReceipts bool `yaml:"delivery_receipts"`
MessageErrorNotices bool `yaml:"message_error_notices"`
SyncDirectChatList bool `yaml:"sync_direct_chat_list"`
FederateRooms bool `yaml:"federate_rooms"`
UploadFileThreshold int64 `yaml:"upload_file_threshold"`
GhostExtraProfileInfo bool `yaml:"ghost_extra_profile_info"`
}
type AnalyticsConfig struct {
Token string `yaml:"token"`
URL string `yaml:"url"`
UserID string `yaml:"user_id"`
}
type ProvisioningConfig struct {
SharedSecret string `yaml:"shared_secret"`
DebugEndpoints bool `yaml:"debug_endpoints"`
EnableSessionTransfers bool `yaml:"enable_session_transfers"`
}
type DirectMediaConfig struct {
Enabled bool `yaml:"enabled"`
MediaIDPrefix string `yaml:"media_id_prefix"`
mediaproxy.BasicConfig `yaml:",inline"`
}
type PublicMediaConfig struct {
Enabled bool `yaml:"enabled"`
SigningKey string `yaml:"signing_key"`
Expiry int `yaml:"expiry"`
HashLength int `yaml:"hash_length"`
PathPrefix string `yaml:"path_prefix"`
UseDatabase bool `yaml:"use_database"`
}
type DoublePuppetConfig struct {
Servers map[string]string `yaml:"servers"`
AllowDiscovery bool `yaml:"allow_discovery"`
Secrets map[string]string `yaml:"secrets"`
}
type ManagementRoomTexts struct {
Welcome string `yaml:"welcome"`
WelcomeConnected string `yaml:"welcome_connected"`
WelcomeUnconnected string `yaml:"welcome_unconnected"`
AdditionalHelp string `yaml:"additional_help"`
}

View file

@ -0,0 +1,51 @@
// 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 bridgeconfig
import (
"maunium.net/go/mautrix/id"
)
type EncryptionConfig struct {
Allow bool `yaml:"allow"`
Default bool `yaml:"default"`
Require bool `yaml:"require"`
Appservice bool `yaml:"appservice"`
MSC4190 bool `yaml:"msc4190"`
MSC4392 bool `yaml:"msc4392"`
SelfSign bool `yaml:"self_sign"`
PlaintextMentions bool `yaml:"plaintext_mentions"`
PickleKey string `yaml:"pickle_key"`
DeleteKeys struct {
DeleteOutboundOnAck bool `yaml:"delete_outbound_on_ack"`
DontStoreOutbound bool `yaml:"dont_store_outbound"`
RatchetOnDecrypt bool `yaml:"ratchet_on_decrypt"`
DeleteFullyUsedOnDecrypt bool `yaml:"delete_fully_used_on_decrypt"`
DeletePrevOnNewSession bool `yaml:"delete_prev_on_new_session"`
DeleteOnDeviceDelete bool `yaml:"delete_on_device_delete"`
PeriodicallyDeleteExpired bool `yaml:"periodically_delete_expired"`
DeleteOutdatedInbound bool `yaml:"delete_outdated_inbound"`
} `yaml:"delete_keys"`
VerificationLevels struct {
Receive id.TrustState `yaml:"receive"`
Send id.TrustState `yaml:"send"`
Share id.TrustState `yaml:"share"`
} `yaml:"verification_levels"`
AllowKeySharing bool `yaml:"allow_key_sharing"`
Rotation struct {
EnableCustom bool `yaml:"enable_custom"`
Milliseconds int64 `yaml:"milliseconds"`
Messages int `yaml:"messages"`
DisableDeviceChangeKeyRotation bool `yaml:"disable_device_change_key_rotation"`
} `yaml:"rotation"`
}

View file

@ -0,0 +1,38 @@
// 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 bridgeconfig
type HomeserverSoftware string
const (
SoftwareStandard HomeserverSoftware = "standard"
SoftwareAsmux HomeserverSoftware = "asmux"
SoftwareHungry HomeserverSoftware = "hungry"
)
var AllowedHomeserverSoftware = map[HomeserverSoftware]bool{
SoftwareStandard: true,
SoftwareAsmux: true,
SoftwareHungry: true,
}
type HomeserverConfig struct {
Address string `yaml:"address"`
Domain string `yaml:"domain"`
AsyncMedia bool `yaml:"async_media"`
PublicAddress string `yaml:"public_address,omitempty"`
Software HomeserverSoftware `yaml:"software"`
StatusEndpoint string `yaml:"status_endpoint"`
MessageSendCheckpointEndpoint string `yaml:"message_send_checkpoint_endpoint"`
Websocket bool `yaml:"websocket"`
WSProxy string `yaml:"websocket_proxy"`
WSPingInterval int `yaml:"ping_interval_seconds"`
}

View file

@ -0,0 +1,174 @@
// 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 bridgeconfig
import (
"fmt"
"net/url"
"os"
"strings"
up "go.mau.fi/util/configupgrade"
)
var HackyMigrateLegacyNetworkConfig func(up.Helper)
func CopyToOtherLocation(helper up.Helper, fieldType up.YAMLType, source, dest []string) {
val, ok := helper.Get(fieldType, source...)
if ok {
helper.Set(fieldType, val, dest...)
}
}
func CopyMapToOtherLocation(helper up.Helper, source, dest []string) {
val := helper.GetNode(source...)
if val != nil && val.Map != nil {
helper.SetMap(val.Map, dest...)
}
}
func doMigrateLegacy(helper up.Helper, python bool) {
if HackyMigrateLegacyNetworkConfig == nil {
_, _ = fmt.Fprintln(os.Stderr, "Legacy bridge config detected, but hacky network config migrator is not set")
os.Exit(1)
}
_, _ = fmt.Fprintln(os.Stderr, "Migrating legacy bridge config")
helper.Copy(up.Str, "homeserver", "address")
helper.Copy(up.Str, "homeserver", "domain")
helper.Copy(up.Str, "homeserver", "software")
helper.Copy(up.Str|up.Null, "homeserver", "status_endpoint")
helper.Copy(up.Str|up.Null, "homeserver", "message_send_checkpoint_endpoint")
helper.Copy(up.Bool, "homeserver", "async_media")
helper.Copy(up.Str|up.Null, "homeserver", "websocket_proxy")
helper.Copy(up.Bool, "homeserver", "websocket")
helper.Copy(up.Int, "homeserver", "ping_interval_seconds")
helper.Copy(up.Str|up.Null, "appservice", "address")
helper.Copy(up.Str|up.Null, "appservice", "hostname")
helper.Copy(up.Int|up.Null, "appservice", "port")
helper.Copy(up.Str, "appservice", "id")
if python {
CopyToOtherLocation(helper, up.Str, []string{"appservice", "bot_username"}, []string{"appservice", "bot", "username"})
CopyToOtherLocation(helper, up.Str, []string{"appservice", "bot_displayname"}, []string{"appservice", "bot", "displayname"})
CopyToOtherLocation(helper, up.Str, []string{"appservice", "bot_avatar"}, []string{"appservice", "bot", "avatar"})
} else {
helper.Copy(up.Str, "appservice", "bot", "username")
helper.Copy(up.Str, "appservice", "bot", "displayname")
helper.Copy(up.Str, "appservice", "bot", "avatar")
}
helper.Copy(up.Bool, "appservice", "ephemeral_events")
helper.Copy(up.Bool, "appservice", "async_transactions")
helper.Copy(up.Str, "appservice", "as_token")
helper.Copy(up.Str, "appservice", "hs_token")
helper.Copy(up.Str, "bridge", "command_prefix")
helper.Copy(up.Bool, "bridge", "personal_filtering_spaces")
if oldPM, ok := helper.Get(up.Str, "bridge", "private_chat_portal_meta"); ok && (oldPM == "default" || oldPM == "always") {
helper.Set(up.Bool, "true", "bridge", "private_chat_portal_meta")
} else {
helper.Set(up.Bool, "false", "bridge", "private_chat_portal_meta")
}
helper.Copy(up.Bool, "bridge", "relay", "enabled")
helper.Copy(up.Bool, "bridge", "relay", "admin_only")
helper.Copy(up.Map, "bridge", "permissions")
if python {
legacyDB, ok := helper.Get(up.Str, "appservice", "database")
if ok {
if strings.HasPrefix(legacyDB, "postgres") {
parsedDB, err := url.Parse(legacyDB)
if err != nil {
panic(err)
}
q := parsedDB.Query()
if parsedDB.Host == "" && !q.Has("host") {
q.Set("host", "/var/run/postgresql")
} else if !q.Has("sslmode") {
q.Set("sslmode", "disable")
}
parsedDB.RawQuery = q.Encode()
helper.Set(up.Str, parsedDB.String(), "database", "uri")
helper.Set(up.Str, "postgres", "database", "type")
} else {
dbPath := strings.TrimPrefix(strings.TrimPrefix(legacyDB, "sqlite:"), "///")
helper.Set(up.Str, fmt.Sprintf("file:%s?_txlock=immediate", dbPath), "database", "uri")
helper.Set(up.Str, "sqlite3-fk-wal", "database", "type")
}
}
if legacyDBMinSize, ok := helper.Get(up.Int, "appservice", "database_opts", "min_size"); ok {
helper.Set(up.Int, legacyDBMinSize, "database", "max_idle_conns")
}
if legacyDBMaxSize, ok := helper.Get(up.Int, "appservice", "database_opts", "max_size"); ok {
helper.Set(up.Int, legacyDBMaxSize, "database", "max_open_conns")
}
} else {
if dbType, ok := helper.Get(up.Str, "appservice", "database", "type"); ok && dbType == "sqlite3" {
helper.Set(up.Str, "sqlite3-fk-wal", "database", "type")
} else {
CopyToOtherLocation(helper, up.Str, []string{"appservice", "database", "type"}, []string{"database", "type"})
}
CopyToOtherLocation(helper, up.Str, []string{"appservice", "database", "uri"}, []string{"database", "uri"})
CopyToOtherLocation(helper, up.Int, []string{"appservice", "database", "max_open_conns"}, []string{"database", "max_open_conns"})
CopyToOtherLocation(helper, up.Int, []string{"appservice", "database", "max_idle_conns"}, []string{"database", "max_idle_conns"})
CopyToOtherLocation(helper, up.Int, []string{"appservice", "database", "max_conn_idle_time"}, []string{"database", "max_conn_idle_time"})
CopyToOtherLocation(helper, up.Int, []string{"appservice", "database", "max_conn_lifetime"}, []string{"database", "max_conn_lifetime"})
}
if python {
if usernameTemplate, ok := helper.Get(up.Str, "bridge", "username_template"); ok && strings.Contains(usernameTemplate, "{userid}") {
helper.Set(up.Str, strings.ReplaceAll(usernameTemplate, "{userid}", "{{.}}"), "appservice", "username_template")
}
} else {
CopyToOtherLocation(helper, up.Str, []string{"bridge", "username_template"}, []string{"appservice", "username_template"})
}
CopyToOtherLocation(helper, up.Bool, []string{"bridge", "message_status_events"}, []string{"matrix", "message_status_events"})
CopyToOtherLocation(helper, up.Bool, []string{"bridge", "delivery_receipts"}, []string{"matrix", "delivery_receipts"})
CopyToOtherLocation(helper, up.Bool, []string{"bridge", "message_error_notices"}, []string{"matrix", "message_error_notices"})
CopyToOtherLocation(helper, up.Bool, []string{"bridge", "sync_direct_chat_list"}, []string{"matrix", "sync_direct_chat_list"})
CopyToOtherLocation(helper, up.Bool, []string{"bridge", "federate_rooms"}, []string{"matrix", "federate_rooms"})
CopyToOtherLocation(helper, up.Str, []string{"bridge", "provisioning", "shared_secret"}, []string{"provisioning", "shared_secret"})
CopyToOtherLocation(helper, up.Str, []string{"appservice", "provisioning", "shared_secret"}, []string{"provisioning", "shared_secret"})
CopyToOtherLocation(helper, up.Bool, []string{"bridge", "provisioning", "debug_endpoints"}, []string{"provisioning", "debug_endpoints"})
CopyToOtherLocation(helper, up.Bool, []string{"bridge", "double_puppet_allow_discovery"}, []string{"double_puppet", "allow_discovery"})
CopyMapToOtherLocation(helper, []string{"bridge", "double_puppet_server_map"}, []string{"double_puppet", "servers"})
CopyMapToOtherLocation(helper, []string{"bridge", "login_shared_secret_map"}, []string{"double_puppet", "secrets"})
CopyToOtherLocation(helper, up.Bool, []string{"bridge", "encryption", "allow"}, []string{"encryption", "allow"})
CopyToOtherLocation(helper, up.Bool, []string{"bridge", "encryption", "default"}, []string{"encryption", "default"})
CopyToOtherLocation(helper, up.Bool, []string{"bridge", "encryption", "require"}, []string{"encryption", "require"})
CopyToOtherLocation(helper, up.Bool, []string{"bridge", "encryption", "appservice"}, []string{"encryption", "appservice"})
CopyToOtherLocation(helper, up.Bool, []string{"bridge", "encryption", "allow_key_sharing"}, []string{"encryption", "allow_key_sharing"})
CopyToOtherLocation(helper, up.Bool, []string{"bridge", "encryption", "delete_keys", "delete_outbound_on_ack"}, []string{"encryption", "delete_keys", "delete_outbound_on_ack"})
CopyToOtherLocation(helper, up.Bool, []string{"bridge", "encryption", "delete_keys", "dont_store_outbound"}, []string{"encryption", "delete_keys", "dont_store_outbound"})
CopyToOtherLocation(helper, up.Bool, []string{"bridge", "encryption", "delete_keys", "ratchet_on_decrypt"}, []string{"encryption", "delete_keys", "ratchet_on_decrypt"})
CopyToOtherLocation(helper, up.Bool, []string{"bridge", "encryption", "delete_keys", "delete_fully_used_on_decrypt"}, []string{"encryption", "delete_keys", "delete_fully_used_on_decrypt"})
CopyToOtherLocation(helper, up.Bool, []string{"bridge", "encryption", "delete_keys", "delete_prev_on_new_session"}, []string{"encryption", "delete_keys", "delete_prev_on_new_session"})
CopyToOtherLocation(helper, up.Bool, []string{"bridge", "encryption", "delete_keys", "delete_on_device_delete"}, []string{"encryption", "delete_keys", "delete_on_device_delete"})
CopyToOtherLocation(helper, up.Bool, []string{"bridge", "encryption", "delete_keys", "periodically_delete_expired"}, []string{"encryption", "delete_keys", "periodically_delete_expired"})
CopyToOtherLocation(helper, up.Bool, []string{"bridge", "encryption", "delete_keys", "delete_outdated_inbound"}, []string{"encryption", "delete_keys", "delete_outdated_inbound"})
CopyToOtherLocation(helper, up.Str, []string{"bridge", "encryption", "verification_levels", "receive"}, []string{"encryption", "verification_levels", "receive"})
CopyToOtherLocation(helper, up.Str, []string{"bridge", "encryption", "verification_levels", "send"}, []string{"encryption", "verification_levels", "send"})
CopyToOtherLocation(helper, up.Str, []string{"bridge", "encryption", "verification_levels", "share"}, []string{"encryption", "verification_levels", "share"})
CopyToOtherLocation(helper, up.Bool, []string{"bridge", "encryption", "rotation", "enable_custom"}, []string{"encryption", "rotation", "enable_custom"})
CopyToOtherLocation(helper, up.Int, []string{"bridge", "encryption", "rotation", "milliseconds"}, []string{"encryption", "rotation", "milliseconds"})
CopyToOtherLocation(helper, up.Int, []string{"bridge", "encryption", "rotation", "messages"}, []string{"encryption", "rotation", "messages"})
CopyToOtherLocation(helper, up.Bool, []string{"bridge", "encryption", "rotation", "disable_device_change_key_rotation"}, []string{"encryption", "rotation", "disable_device_change_key_rotation"})
if helper.GetNode("logging", "writers") == nil && (helper.GetNode("logging", "print_level") != nil || helper.GetNode("logging", "file_name_format") != nil) {
_, _ = fmt.Fprintln(os.Stderr, "Migrating maulogger configs is not supported")
} else if (helper.GetNode("logging", "writers") == nil && (helper.GetNode("logging", "handlers") != nil)) || python {
_, _ = fmt.Fprintln(os.Stderr, "Migrating Python log configs is not supported")
} else {
helper.Copy(up.Map, "logging")
}
HackyMigrateLegacyNetworkConfig(helper)
}

View file

@ -0,0 +1,124 @@
// 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 bridgeconfig
import (
"fmt"
"os"
"strconv"
"strings"
"gopkg.in/yaml.v3"
"maunium.net/go/mautrix/id"
)
type Permissions struct {
SendEvents bool `yaml:"send_events"`
Commands bool `yaml:"commands"`
Login bool `yaml:"login"`
DoublePuppet bool `yaml:"double_puppet"`
Admin bool `yaml:"admin"`
ManageRelay bool `yaml:"manage_relay"`
MaxLogins int `yaml:"max_logins"`
}
type PermissionConfig map[string]*Permissions
func boolToInt(val bool) int {
if val {
return 1
}
return 0
}
func (pc PermissionConfig) IsConfigured() bool {
_, hasWildcard := pc["*"]
_, hasExampleDomain := pc["example.com"]
_, hasExampleUser := pc["@admin:example.com"]
exampleLen := boolToInt(hasWildcard) + boolToInt(hasExampleUser) + boolToInt(hasExampleDomain)
return len(pc) > exampleLen
}
func (pc PermissionConfig) Get(userID id.UserID) Permissions {
if level, ok := pc[string(userID)]; ok {
return *level
} else if level, ok = pc[userID.Homeserver()]; len(userID.Homeserver()) > 0 && ok {
return *level
} else if level, ok = pc["*"]; ok {
return *level
} else {
return PermissionLevelBlock
}
}
var (
PermissionLevelBlock = Permissions{}
PermissionLevelRelay = Permissions{SendEvents: true}
PermissionLevelCommands = Permissions{SendEvents: true, Commands: true, ManageRelay: true}
PermissionLevelUser = Permissions{SendEvents: true, Commands: true, ManageRelay: true, Login: true, DoublePuppet: true}
PermissionLevelAdmin = Permissions{SendEvents: true, Commands: true, ManageRelay: true, Login: true, DoublePuppet: true, Admin: true}
)
var namesToLevels = map[string]Permissions{
"block": PermissionLevelBlock,
"relay": PermissionLevelRelay,
"commands": PermissionLevelCommands,
"user": PermissionLevelUser,
"admin": PermissionLevelAdmin,
}
var levelsToNames = map[Permissions]string{
PermissionLevelBlock: "block",
PermissionLevelRelay: "relay",
PermissionLevelCommands: "commands",
PermissionLevelUser: "user",
PermissionLevelAdmin: "admin",
}
type umPerm Permissions
func (p *Permissions) UnmarshalYAML(perm *yaml.Node) error {
switch perm.Tag {
case "!!str":
var ok bool
*p, ok = namesToLevels[strings.ToLower(perm.Value)]
if !ok {
return fmt.Errorf("invalid permissions level %s", perm.Value)
}
return nil
case "!!map":
err := perm.Decode((*umPerm)(p))
return err
case "!!int":
val, err := strconv.Atoi(perm.Value)
if err != nil {
return fmt.Errorf("invalid permissions level %s", perm.Value)
}
_, _ = fmt.Fprintln(os.Stderr, "Warning: config contains deprecated integer permission values")
// Integer values are deprecated, so they're hardcoded
if val < 5 {
*p = PermissionLevelBlock
} else if val < 10 {
*p = PermissionLevelRelay
} else if val < 100 {
*p = PermissionLevelUser
} else {
*p = PermissionLevelAdmin
}
return nil
default:
return fmt.Errorf("invalid permissions type %s", perm.Tag)
}
}
func (p *Permissions) MarshalYAML() (any, error) {
if level, ok := levelsToNames[*p]; ok {
return level, nil
}
return umPerm(*p), nil
}

View file

@ -0,0 +1,109 @@
// 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 bridgeconfig
import (
"fmt"
"strings"
"text/template"
"gopkg.in/yaml.v3"
"maunium.net/go/mautrix/bridgev2/networkid"
"maunium.net/go/mautrix/event"
"maunium.net/go/mautrix/format"
)
type RelayConfig struct {
Enabled bool `yaml:"enabled"`
AdminOnly bool `yaml:"admin_only"`
DefaultRelays []networkid.UserLoginID `yaml:"default_relays"`
MessageFormats map[event.MessageType]string `yaml:"message_formats"`
DisplaynameFormat string `yaml:"displayname_format"`
messageTemplates *template.Template `yaml:"-"`
nameTemplate *template.Template `yaml:"-"`
}
type umRelayConfig RelayConfig
func (rc *RelayConfig) UnmarshalYAML(node *yaml.Node) error {
err := node.Decode((*umRelayConfig)(rc))
if err != nil {
return err
}
rc.messageTemplates = template.New("messageTemplates")
for key, template := range rc.MessageFormats {
_, err = rc.messageTemplates.New(string(key)).Parse(template)
if err != nil {
return err
}
}
rc.nameTemplate, err = template.New("nameTemplate").Parse(rc.DisplaynameFormat)
if err != nil {
return err
}
return nil
}
type formatData struct {
Sender any
Content *event.MessageEventContent
Caption string
Message string
FileName string
}
func isMedia(msgType event.MessageType) bool {
switch msgType {
case event.MsgImage, event.MsgVideo, event.MsgAudio, event.MsgFile:
return true
default:
return false
}
}
func (rc *RelayConfig) FormatMessage(content *event.MessageEventContent, sender any) (*event.MessageEventContent, error) {
_, isSupported := rc.MessageFormats[content.MsgType]
if !isSupported {
return nil, fmt.Errorf("unsupported msgtype for relaying")
}
contentCopy := *content
content = &contentCopy
content.EnsureHasHTML()
fd := &formatData{
Sender: sender,
Content: content,
Message: content.FormattedBody,
}
fd.Message = content.FormattedBody
if content.FileName != "" {
fd.FileName = content.FileName
if content.FileName != content.Body {
fd.Caption = fd.Message
}
} else if isMedia(content.MsgType) {
content.FileName = content.Body
fd.FileName = content.Body
}
var output strings.Builder
err := rc.messageTemplates.ExecuteTemplate(&output, string(content.MsgType), fd)
if err != nil {
return nil, err
}
content.FormattedBody = output.String()
content.Body = format.HTMLToText(content.FormattedBody)
return content, nil
}
func (rc *RelayConfig) FormatName(sender any) string {
var buf strings.Builder
_ = rc.nameTemplate.Execute(&buf, sender)
return strings.TrimSpace(buf.String())
}

View file

@ -0,0 +1,227 @@
// 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 bridgeconfig
import (
"fmt"
up "go.mau.fi/util/configupgrade"
"go.mau.fi/util/random"
"maunium.net/go/mautrix/federation"
)
func doUpgrade(helper up.Helper) {
if _, isLegacyConfig := helper.Get(up.Str, "appservice", "database", "uri"); isLegacyConfig {
doMigrateLegacy(helper, false)
return
} else if _, isLegacyPython := helper.Get(up.Str, "appservice", "database"); isLegacyPython {
doMigrateLegacy(helper, true)
return
}
helper.Copy(up.Str, "bridge", "command_prefix")
helper.Copy(up.Bool, "bridge", "personal_filtering_spaces")
helper.Copy(up.Bool, "bridge", "private_chat_portal_meta")
helper.Copy(up.Bool, "bridge", "async_events")
helper.Copy(up.Bool, "bridge", "split_portals")
helper.Copy(up.Bool, "bridge", "resend_bridge_info")
helper.Copy(up.Bool, "bridge", "no_bridge_info_state_key")
helper.Copy(up.Str|up.Null, "bridge", "bridge_status_notices")
helper.Copy(up.Str|up.Int|up.Null, "bridge", "unknown_error_auto_reconnect")
helper.Copy(up.Int, "bridge", "unknown_error_max_auto_reconnects")
helper.Copy(up.Bool, "bridge", "bridge_matrix_leave")
helper.Copy(up.Bool, "bridge", "bridge_notices")
helper.Copy(up.Bool, "bridge", "tag_only_on_create")
helper.Copy(up.List, "bridge", "only_bridge_tags")
helper.Copy(up.Bool, "bridge", "mute_only_on_create")
helper.Copy(up.Bool, "bridge", "deduplicate_matrix_messages")
helper.Copy(up.Bool, "bridge", "cross_room_replies")
helper.Copy(up.Bool, "bridge", "revert_failed_state_changes")
helper.Copy(up.Bool, "bridge", "kick_matrix_users")
helper.Copy(up.Bool, "bridge", "cleanup_on_logout", "enabled")
helper.Copy(up.Str, "bridge", "cleanup_on_logout", "manual", "private")
helper.Copy(up.Str, "bridge", "cleanup_on_logout", "manual", "relayed")
helper.Copy(up.Str, "bridge", "cleanup_on_logout", "manual", "shared_no_users")
helper.Copy(up.Str, "bridge", "cleanup_on_logout", "manual", "shared_has_users")
helper.Copy(up.Str, "bridge", "cleanup_on_logout", "bad_credentials", "private")
helper.Copy(up.Str, "bridge", "cleanup_on_logout", "bad_credentials", "relayed")
helper.Copy(up.Str, "bridge", "cleanup_on_logout", "bad_credentials", "shared_no_users")
helper.Copy(up.Str, "bridge", "cleanup_on_logout", "bad_credentials", "shared_has_users")
helper.Copy(up.Bool, "bridge", "relay", "enabled")
helper.Copy(up.Bool, "bridge", "relay", "admin_only")
helper.Copy(up.List, "bridge", "relay", "default_relays")
helper.Copy(up.Map, "bridge", "relay", "message_formats")
helper.Copy(up.Str, "bridge", "relay", "displayname_format")
helper.Copy(up.Map, "bridge", "permissions")
if dbType, ok := helper.Get(up.Str, "database", "type"); ok && dbType == "sqlite3" {
fmt.Println("Warning: invalid database type sqlite3 in config. Autocorrecting to sqlite3-fk-wal")
helper.Set(up.Str, "sqlite3-fk-wal", "database", "type")
} else {
helper.Copy(up.Str, "database", "type")
}
helper.Copy(up.Str, "database", "uri")
helper.Copy(up.Int, "database", "max_open_conns")
helper.Copy(up.Int, "database", "max_idle_conns")
helper.Copy(up.Str|up.Null, "database", "max_conn_idle_time")
helper.Copy(up.Str|up.Null, "database", "max_conn_lifetime")
helper.Copy(up.Str, "homeserver", "address")
helper.Copy(up.Str, "homeserver", "domain")
helper.Copy(up.Str, "homeserver", "software")
helper.Copy(up.Str|up.Null, "homeserver", "status_endpoint")
helper.Copy(up.Str|up.Null, "homeserver", "message_send_checkpoint_endpoint")
helper.Copy(up.Bool, "homeserver", "async_media")
helper.Copy(up.Str|up.Null, "homeserver", "websocket_proxy")
helper.Copy(up.Bool, "homeserver", "websocket")
helper.Copy(up.Int, "homeserver", "ping_interval_seconds")
helper.Copy(up.Str|up.Null, "appservice", "address")
helper.Copy(up.Str|up.Null, "appservice", "public_address")
helper.Copy(up.Str|up.Null, "appservice", "hostname")
helper.Copy(up.Int|up.Null, "appservice", "port")
helper.Copy(up.Str, "appservice", "id")
helper.Copy(up.Str, "appservice", "bot", "username")
helper.Copy(up.Str, "appservice", "bot", "displayname")
helper.Copy(up.Str, "appservice", "bot", "avatar")
helper.Copy(up.Bool, "appservice", "ephemeral_events")
helper.Copy(up.Bool, "appservice", "async_transactions")
helper.Copy(up.Str, "appservice", "as_token")
helper.Copy(up.Str, "appservice", "hs_token")
helper.Copy(up.Str, "appservice", "username_template")
helper.Copy(up.Bool, "matrix", "message_status_events")
helper.Copy(up.Bool, "matrix", "delivery_receipts")
helper.Copy(up.Bool, "matrix", "message_error_notices")
helper.Copy(up.Bool, "matrix", "sync_direct_chat_list")
helper.Copy(up.Bool, "matrix", "federate_rooms")
helper.Copy(up.Int, "matrix", "upload_file_threshold")
helper.Copy(up.Bool, "matrix", "ghost_extra_profile_info")
helper.Copy(up.Str|up.Null, "analytics", "token")
helper.Copy(up.Str|up.Null, "analytics", "url")
helper.Copy(up.Str|up.Null, "analytics", "user_id")
if secret, ok := helper.Get(up.Str, "provisioning", "shared_secret"); !ok || secret == "generate" {
sharedSecret := random.String(64)
helper.Set(up.Str, sharedSecret, "provisioning", "shared_secret")
} else {
helper.Copy(up.Str, "provisioning", "shared_secret")
}
helper.Copy(up.Bool, "provisioning", "debug_endpoints")
helper.Copy(up.Bool, "provisioning", "enable_session_transfers")
helper.Copy(up.Bool, "direct_media", "enabled")
helper.Copy(up.Str|up.Null, "direct_media", "media_id_prefix")
helper.Copy(up.Str, "direct_media", "server_name")
helper.Copy(up.Str|up.Null, "direct_media", "well_known_response")
helper.Copy(up.Bool, "direct_media", "allow_proxy")
if serverKey, ok := helper.Get(up.Str, "direct_media", "server_key"); !ok || serverKey == "generate" {
serverKey = federation.GenerateSigningKey().SynapseString()
helper.Set(up.Str, serverKey, "direct_media", "server_key")
} else {
helper.Copy(up.Str, "direct_media", "server_key")
}
helper.Copy(up.Bool, "public_media", "enabled")
if signingKey, ok := helper.Get(up.Str, "public_media", "signing_key"); !ok || signingKey == "generate" {
helper.Set(up.Str, random.String(64), "public_media", "signing_key")
} else {
helper.Copy(up.Str, "public_media", "signing_key")
}
helper.Copy(up.Int, "public_media", "expiry")
helper.Copy(up.Int, "public_media", "hash_length")
helper.Copy(up.Str|up.Null, "public_media", "path_prefix")
helper.Copy(up.Bool, "public_media", "use_database")
helper.Copy(up.Bool, "backfill", "enabled")
helper.Copy(up.Int, "backfill", "max_initial_messages")
helper.Copy(up.Int, "backfill", "max_catchup_messages")
helper.Copy(up.Int, "backfill", "unread_hours_threshold")
helper.Copy(up.Int, "backfill", "threads", "max_initial_messages")
helper.Copy(up.Bool, "backfill", "queue", "enabled")
helper.Copy(up.Int, "backfill", "queue", "batch_size")
helper.Copy(up.Int, "backfill", "queue", "batch_delay")
helper.Copy(up.Int, "backfill", "queue", "max_batches")
helper.Copy(up.Map, "backfill", "queue", "max_batches_override")
helper.Copy(up.Map, "double_puppet", "servers")
helper.Copy(up.Bool, "double_puppet", "allow_discovery")
helper.Copy(up.Map, "double_puppet", "secrets")
helper.Copy(up.Bool, "encryption", "allow")
helper.Copy(up.Bool, "encryption", "default")
helper.Copy(up.Bool, "encryption", "require")
helper.Copy(up.Bool, "encryption", "appservice")
if val, ok := helper.Get(up.Bool, "appservice", "msc4190"); ok {
helper.Set(up.Bool, val, "encryption", "msc4190")
} else {
helper.Copy(up.Bool, "encryption", "msc4190")
}
helper.Copy(up.Bool, "encryption", "msc4392")
helper.Copy(up.Bool, "encryption", "self_sign")
helper.Copy(up.Bool, "encryption", "allow_key_sharing")
if secret, ok := helper.Get(up.Str, "encryption", "pickle_key"); !ok || secret == "generate" {
helper.Set(up.Str, random.String(64), "encryption", "pickle_key")
} else {
helper.Copy(up.Str, "encryption", "pickle_key")
}
helper.Copy(up.Bool, "encryption", "delete_keys", "delete_outbound_on_ack")
helper.Copy(up.Bool, "encryption", "delete_keys", "dont_store_outbound")
helper.Copy(up.Bool, "encryption", "delete_keys", "ratchet_on_decrypt")
helper.Copy(up.Bool, "encryption", "delete_keys", "delete_fully_used_on_decrypt")
helper.Copy(up.Bool, "encryption", "delete_keys", "delete_prev_on_new_session")
helper.Copy(up.Bool, "encryption", "delete_keys", "delete_on_device_delete")
helper.Copy(up.Bool, "encryption", "delete_keys", "periodically_delete_expired")
helper.Copy(up.Bool, "encryption", "delete_keys", "delete_outdated_inbound")
helper.Copy(up.Str, "encryption", "verification_levels", "receive")
helper.Copy(up.Str, "encryption", "verification_levels", "send")
helper.Copy(up.Str, "encryption", "verification_levels", "share")
helper.Copy(up.Bool, "encryption", "rotation", "enable_custom")
helper.Copy(up.Int, "encryption", "rotation", "milliseconds")
helper.Copy(up.Int, "encryption", "rotation", "messages")
helper.Copy(up.Bool, "encryption", "rotation", "disable_device_change_key_rotation")
helper.Copy(up.Str|up.Null, "env_config_prefix")
helper.Copy(up.Map, "logging")
}
var SpacedBlocks = [][]string{
{"bridge"},
{"bridge", "bridge_matrix_leave"},
{"bridge", "cleanup_on_logout"},
{"bridge", "relay"},
{"bridge", "permissions"},
{"database"},
{"homeserver"},
{"homeserver", "software"},
{"homeserver", "websocket"},
{"appservice"},
{"appservice", "hostname"},
{"appservice", "id"},
{"appservice", "ephemeral_events"},
{"appservice", "as_token"},
{"appservice", "username_template"},
{"matrix"},
{"analytics"},
{"provisioning"},
{"public_media"},
{"direct_media"},
{"backfill"},
{"double_puppet"},
{"encryption"},
{"env_config_prefix"},
{"logging"},
}
// Upgrader is a config upgrader that copies the default fields in the homeserver, appservice and logging blocks.
var Upgrader up.SpacedUpgrader = &up.StructUpgrader{
SimpleUpgrader: up.SimpleUpgrader(doUpgrade),
Blocks: SpacedBlocks,
}

333
bridgev2/bridgestate.go Normal file
View file

@ -0,0 +1,333 @@
// 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 bridgev2
import (
"context"
"fmt"
"math/rand/v2"
"runtime/debug"
"sync/atomic"
"time"
"github.com/rs/zerolog"
"go.mau.fi/util/exfmt"
"maunium.net/go/mautrix/bridgev2/status"
"maunium.net/go/mautrix/event"
"maunium.net/go/mautrix/format"
)
var CatchBridgeStateQueuePanics = true
type BridgeStateQueue struct {
prevUnsent *status.BridgeState
prevSent *status.BridgeState
errorSent bool
ch chan status.BridgeState
bridge *Bridge
login *UserLogin
firstTransientDisconnect time.Time
cancelScheduledNotice atomic.Pointer[context.CancelFunc]
stopChan chan struct{}
stopReconnect atomic.Pointer[context.CancelFunc]
unknownErrorReconnects int
}
func (br *Bridge) SendGlobalBridgeState(state status.BridgeState) {
state = state.Fill(nil)
for {
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
if err := br.Matrix.SendBridgeStatus(ctx, &state); err != nil {
br.Log.Warn().Err(err).Msg("Failed to update global bridge state")
cancel()
time.Sleep(5 * time.Second)
continue
} else {
br.Log.Debug().Any("bridge_state", state).Msg("Sent new global bridge state")
cancel()
break
}
}
}
func (br *Bridge) NewBridgeStateQueue(login *UserLogin) *BridgeStateQueue {
bsq := &BridgeStateQueue{
ch: make(chan status.BridgeState, 10),
stopChan: make(chan struct{}),
bridge: br,
login: login,
}
go bsq.loop()
return bsq
}
func (bsq *BridgeStateQueue) Destroy() {
close(bsq.stopChan)
close(bsq.ch)
bsq.StopUnknownErrorReconnect()
}
func (bsq *BridgeStateQueue) StopUnknownErrorReconnect() {
if bsq == nil {
return
}
if cancelFn := bsq.stopReconnect.Swap(nil); cancelFn != nil {
(*cancelFn)()
}
if cancelFn := bsq.cancelScheduledNotice.Swap(nil); cancelFn != nil {
(*cancelFn)()
}
}
func (bsq *BridgeStateQueue) loop() {
if CatchBridgeStateQueuePanics {
defer func() {
err := recover()
if err != nil {
bsq.login.Log.Error().
Bytes(zerolog.ErrorStackFieldName, debug.Stack()).
Any(zerolog.ErrorFieldName, err).
Msg("Panic in bridge state loop")
}
}()
}
for state := range bsq.ch {
bsq.immediateSendBridgeState(state)
}
}
func (bsq *BridgeStateQueue) scheduleNotice(triggeredBy status.BridgeState) {
log := bsq.login.Log.With().Str("action", "transient disconnect notice").Logger()
ctx := log.WithContext(bsq.bridge.BackgroundCtx)
if !bsq.waitForTransientDisconnectReconnect(ctx) {
return
}
prevUnsent := bsq.GetPrevUnsent()
prev := bsq.GetPrev()
if triggeredBy.Timestamp != prev.Timestamp || len(bsq.ch) > 0 || bsq.errorSent ||
prevUnsent.StateEvent != status.StateTransientDisconnect || prev.StateEvent != status.StateTransientDisconnect {
log.Trace().Any("triggered_by", triggeredBy).Msg("Not sending delayed transient disconnect notice")
return
}
log.Debug().Any("triggered_by", triggeredBy).Msg("Sending delayed transient disconnect notice")
bsq.sendNotice(ctx, triggeredBy, true)
}
func (bsq *BridgeStateQueue) sendNotice(ctx context.Context, state status.BridgeState, isDelayed bool) {
noticeConfig := bsq.bridge.Config.BridgeStatusNotices
isError := state.StateEvent == status.StateBadCredentials ||
state.StateEvent == status.StateUnknownError ||
state.UserAction == status.UserActionOpenNative ||
(isDelayed && state.StateEvent == status.StateTransientDisconnect)
sendNotice := noticeConfig == "all" || (noticeConfig == "errors" &&
(isError || (bsq.errorSent && state.StateEvent == status.StateConnected)))
if state.StateEvent != status.StateTransientDisconnect && state.StateEvent != status.StateUnknownError {
bsq.firstTransientDisconnect = time.Time{}
}
if !sendNotice {
if !bsq.errorSent && !isDelayed && noticeConfig == "errors" && state.StateEvent == status.StateTransientDisconnect {
if bsq.firstTransientDisconnect.IsZero() {
bsq.firstTransientDisconnect = time.Now()
}
go bsq.scheduleNotice(state)
}
return
}
managementRoom, err := bsq.login.User.GetManagementRoom(ctx)
if err != nil {
bsq.login.Log.Err(err).Msg("Failed to get management room")
return
}
name := bsq.login.RemoteName
if name == "" {
name = fmt.Sprintf("`%s`", bsq.login.ID)
}
message := fmt.Sprintf("State update for %s: `%s`", name, state.StateEvent)
if state.Error != "" {
message += fmt.Sprintf(" (`%s`)", state.Error)
}
if isDelayed {
message += fmt.Sprintf(" not resolved after waiting %s", exfmt.Duration(TransientDisconnectNoticeDelay))
}
if state.Message != "" {
message += fmt.Sprintf(": %s", state.Message)
}
content := format.RenderMarkdown(message, true, false)
if !isError {
content.MsgType = event.MsgNotice
}
_, err = bsq.bridge.Bot.SendMessage(ctx, managementRoom, event.EventMessage, &event.Content{
Parsed: content,
Raw: map[string]any{
"fi.mau.bridge_state": state,
},
}, nil)
if err != nil {
bsq.login.Log.Err(err).Msg("Failed to send bridge state notice")
} else {
bsq.errorSent = isError
}
}
func (bsq *BridgeStateQueue) unknownErrorReconnect(triggeredBy status.BridgeState) {
log := bsq.login.Log.With().Str("action", "unknown error reconnect").Logger()
ctx := log.WithContext(bsq.bridge.BackgroundCtx)
if !bsq.waitForUnknownErrorReconnect(ctx) {
return
}
prevUnsent := bsq.GetPrevUnsent()
prev := bsq.GetPrev()
if triggeredBy.Timestamp != prev.Timestamp {
log.Debug().Msg("Not reconnecting as a new bridge state was sent after the unknown error")
return
} else if len(bsq.ch) > 0 {
log.Warn().Msg("Not reconnecting as there are unsent bridge states")
return
} else if prevUnsent.StateEvent != status.StateUnknownError || prev.StateEvent != status.StateUnknownError {
log.Debug().Msg("Not reconnecting as the previous state was not an unknown error")
return
} else if bsq.unknownErrorReconnects > bsq.bridge.Config.UnknownErrorMaxAutoReconnects {
log.Warn().Msg("Not reconnecting as the maximum number of unknown error reconnects has been reached")
return
}
bsq.unknownErrorReconnects++
log.Info().
Int("reconnect_num", bsq.unknownErrorReconnects).
Msg("Disconnecting and reconnecting login due to unknown error")
bsq.login.Disconnect()
log.Debug().Msg("Disconnection finished, recreating client and reconnecting")
err := bsq.login.recreateClient(ctx)
if err != nil {
log.Err(err).Msg("Failed to recreate client after unknown error")
return
}
bsq.login.Client.Connect(ctx)
log.Debug().Msg("Reconnection finished")
}
func (bsq *BridgeStateQueue) waitForUnknownErrorReconnect(ctx context.Context) bool {
reconnectIn := bsq.bridge.Config.UnknownErrorAutoReconnect
// Don't allow too low values
if reconnectIn < 1*time.Minute {
return false
}
reconnectIn += time.Duration(rand.Int64N(int64(float64(reconnectIn)*0.4)) - int64(float64(reconnectIn)*0.2))
return bsq.waitForReconnect(ctx, reconnectIn, &bsq.stopReconnect)
}
const TransientDisconnectNoticeDelay = 3 * time.Minute
func (bsq *BridgeStateQueue) waitForTransientDisconnectReconnect(ctx context.Context) bool {
timeUntilSchedule := time.Until(bsq.firstTransientDisconnect.Add(TransientDisconnectNoticeDelay))
zerolog.Ctx(ctx).Trace().
Stringer("duration", timeUntilSchedule).
Msg("Waiting before sending notice about transient disconnect")
return bsq.waitForReconnect(ctx, timeUntilSchedule, &bsq.cancelScheduledNotice)
}
func (bsq *BridgeStateQueue) waitForReconnect(
ctx context.Context, reconnectIn time.Duration, ptr *atomic.Pointer[context.CancelFunc],
) bool {
cancelCtx, cancel := context.WithCancel(ctx)
defer cancel()
if oldCancel := ptr.Swap(&cancel); oldCancel != nil {
(*oldCancel)()
}
select {
case <-time.After(reconnectIn):
return ptr.CompareAndSwap(&cancel, nil)
case <-cancelCtx.Done():
return false
case <-bsq.stopChan:
return false
}
}
func (bsq *BridgeStateQueue) immediateSendBridgeState(state status.BridgeState) {
if bsq.prevSent != nil && bsq.prevSent.ShouldDeduplicate(&state) {
bsq.login.Log.Debug().
Str("state_event", string(state.StateEvent)).
Msg("Not sending bridge state as it's a duplicate")
return
}
if state.StateEvent == status.StateUnknownError {
go bsq.unknownErrorReconnect(state)
}
ctx := bsq.login.Log.WithContext(context.Background())
bsq.sendNotice(ctx, state, false)
retryIn := 2
for {
ctx, cancel := context.WithTimeout(ctx, 30*time.Second)
err := bsq.bridge.Matrix.SendBridgeStatus(ctx, &state)
cancel()
if err != nil {
bsq.login.Log.Warn().Err(err).
Int("retry_in_seconds", retryIn).
Msg("Failed to update bridge state")
time.Sleep(time.Duration(retryIn) * time.Second)
retryIn *= 2
if retryIn > 64 {
retryIn = 64
}
} else {
bsq.prevSent = &state
bsq.login.Log.Debug().
Any("bridge_state", state).
Msg("Sent new bridge state")
return
}
}
}
func (bsq *BridgeStateQueue) Send(state status.BridgeState) {
if bsq == nil {
return
}
state = state.Fill(bsq.login)
bsq.prevUnsent = &state
if len(bsq.ch) >= 8 {
bsq.login.Log.Warn().Msg("Bridge state queue is nearly full, discarding an item")
select {
case <-bsq.ch:
default:
}
}
select {
case bsq.ch <- state:
default:
bsq.login.Log.Error().Msg("Bridge state queue is full, dropped new state")
}
}
func (bsq *BridgeStateQueue) GetPrev() status.BridgeState {
if bsq != nil && bsq.prevSent != nil {
return *bsq.prevSent
}
return status.BridgeState{}
}
func (bsq *BridgeStateQueue) GetPrevUnsent() status.BridgeState {
if bsq != nil && bsq.prevSent != nil {
return *bsq.prevUnsent
}
return status.BridgeState{}
}
func (bsq *BridgeStateQueue) SetPrev(prev status.BridgeState) {
if bsq != nil {
bsq.prevSent = &prev
}
}

View file

@ -0,0 +1,97 @@
// 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 commands
import (
"maunium.net/go/mautrix/bridgev2"
)
var CommandDeletePortal = &FullHandler{
Func: func(ce *Event) {
// TODO clean up child portals?
err := ce.Portal.Delete(ce.Ctx)
if err != nil {
ce.Reply("Failed to delete portal: %v", err)
return
}
err = ce.Bot.DeleteRoom(ce.Ctx, ce.Portal.MXID, false)
if err != nil {
ce.Reply("Failed to clean up room: %v", err)
}
ce.MessageStatus.DisableMSS = true
},
Name: "delete-portal",
Help: HelpMeta{
Section: HelpSectionAdmin,
Description: "Delete the current portal room",
},
RequiresAdmin: true,
RequiresPortal: true,
}
var CommandDeleteAllPortals = &FullHandler{
Func: func(ce *Event) {
portals, err := ce.Bridge.GetAllPortals(ce.Ctx)
if err != nil {
ce.Reply("Failed to get portals: %v", err)
return
}
bridgev2.DeleteManyPortals(ce.Ctx, portals, func(portal *bridgev2.Portal, delete bool, err error) {
if !delete {
ce.Reply("Failed to delete portal %s: %v", portal.MXID, err)
} else {
ce.Reply("Failed to clean up room %s: %v", portal.MXID, err)
}
})
},
Name: "delete-all-portals",
Help: HelpMeta{
Section: HelpSectionAdmin,
Description: "Delete all portals the bridge knows about",
},
RequiresAdmin: true,
}
var CommandSetManagementRoom = &FullHandler{
Func: func(ce *Event) {
if ce.User.ManagementRoom == ce.RoomID {
ce.Reply("This room is already your management room")
return
} else if ce.Portal != nil {
ce.Reply("This is a portal room: you can't set this as your management room")
return
}
members, err := ce.Bridge.Matrix.GetMembers(ce.Ctx, ce.RoomID)
if err != nil {
ce.Log.Err(err).Msg("Failed to get room members to check if room can be a management room")
ce.Reply("Failed to get room members")
return
}
_, hasBot := members[ce.Bot.GetMXID()]
if !hasBot {
// This reply will probably fail, but whatever
ce.Reply("The bridge bot must be in the room to set it as your management room")
return
} else if len(members) != 2 {
ce.Reply("Your management room must not have any members other than you and the bridge bot")
return
}
ce.User.ManagementRoom = ce.RoomID
err = ce.User.Save(ce.Ctx)
if err != nil {
ce.Log.Err(err).Msg("Failed to save management room")
ce.Reply("Failed to save management room")
} else {
ce.Reply("Management room updated")
}
},
Name: "set-management-room",
Help: HelpMeta{
Section: HelpSectionGeneral,
Description: "Mark this room as your management room",
},
}

125
bridgev2/commands/debug.go Normal file
View file

@ -0,0 +1,125 @@
// 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 commands
import (
"encoding/json"
"strings"
"time"
"maunium.net/go/mautrix/bridgev2"
"maunium.net/go/mautrix/bridgev2/networkid"
"maunium.net/go/mautrix/event"
)
var CommandRegisterPush = &FullHandler{
Func: func(ce *Event) {
if len(ce.Args) < 3 {
ce.Reply("Usage: `$cmdprefix debug-register-push <login ID> <push type> <push token>`\n\nYour logins:\n\n%s", ce.User.GetFormattedUserLogins())
return
}
pushType := bridgev2.PushTypeFromString(ce.Args[1])
if pushType == bridgev2.PushTypeUnknown {
ce.Reply("Unknown push type `%s`. Allowed types: `web`, `apns`, `fcm`", ce.Args[1])
return
}
login := ce.Bridge.GetCachedUserLoginByID(networkid.UserLoginID(ce.Args[0]))
if login == nil || login.UserMXID != ce.User.MXID {
ce.Reply("Login `%s` not found", ce.Args[0])
return
}
pushable, ok := login.Client.(bridgev2.PushableNetworkAPI)
if !ok {
ce.Reply("This network connector does not support push registration")
return
}
pushToken := strings.Join(ce.Args[2:], " ")
if pushToken == "null" {
pushToken = ""
}
err := pushable.RegisterPushNotifications(ce.Ctx, pushType, pushToken)
if err != nil {
ce.Reply("Failed to register pusher: %v", err)
return
}
if pushToken == "" {
ce.Reply("Pusher de-registered successfully")
} else {
ce.Reply("Pusher registered successfully")
}
},
Name: "debug-register-push",
Help: HelpMeta{
Section: HelpSectionAdmin,
Description: "Register a pusher",
Args: "<_login ID_> <_push type_> <_push token_>",
},
RequiresAdmin: true,
RequiresLogin: true,
NetworkAPI: NetworkAPIImplements[bridgev2.PushableNetworkAPI],
}
var CommandSendAccountData = &FullHandler{
Func: func(ce *Event) {
if len(ce.Args) < 2 {
ce.Reply("Usage: `$cmdprefix debug-account-data <type> <content>")
return
}
var content event.Content
evtType := event.Type{Type: ce.Args[0], Class: event.AccountDataEventType}
ce.RawArgs = strings.TrimSpace(strings.Trim(ce.RawArgs, ce.Args[0]))
err := json.Unmarshal([]byte(ce.RawArgs), &content)
if err != nil {
ce.Reply("Failed to parse JSON: %v", err)
return
}
err = content.ParseRaw(evtType)
if err != nil {
ce.Reply("Failed to deserialize content: %v", err)
return
}
res := ce.Bridge.QueueMatrixEvent(ce.Ctx, &event.Event{
Sender: ce.User.MXID,
Type: evtType,
Timestamp: time.Now().UnixMilli(),
RoomID: ce.RoomID,
Content: content,
})
ce.Reply("Result: %+v", res)
},
Name: "debug-account-data",
Help: HelpMeta{
Section: HelpSectionAdmin,
Description: "Send a room account data event to the bridge",
Args: "<_type_> <_content_>",
},
RequiresAdmin: true,
RequiresPortal: true,
RequiresLogin: true,
}
var CommandResetNetwork = &FullHandler{
Func: func(ce *Event) {
if strings.Contains(strings.ToLower(ce.RawArgs), "--reset-transport") {
nrn, ok := ce.Bridge.Network.(bridgev2.NetworkResettingNetwork)
if ok {
nrn.ResetHTTPTransport()
} else {
ce.Reply("Network connector does not support resetting HTTP transport")
}
}
ce.Bridge.ResetNetworkConnections()
ce.React("✅️")
},
Name: "debug-reset-network",
Help: HelpMeta{
Section: HelpSectionAdmin,
Description: "Reset network connections to the remote network",
Args: "[--reset-transport]",
},
RequiresAdmin: true,
}

100
bridgev2/commands/event.go Normal file
View file

@ -0,0 +1,100 @@
// 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 commands
import (
"context"
"fmt"
"strings"
"time"
"github.com/rs/zerolog"
"maunium.net/go/mautrix/bridgev2"
"maunium.net/go/mautrix"
"maunium.net/go/mautrix/event"
"maunium.net/go/mautrix/format"
"maunium.net/go/mautrix/id"
)
// Event stores all data which might be used to handle commands
type Event struct {
Bot bridgev2.MatrixAPI
Bridge *bridgev2.Bridge
Portal *bridgev2.Portal
Processor *Processor
Handler MinimalCommandHandler
RoomID id.RoomID
OrigRoomID id.RoomID
EventID id.EventID
User *bridgev2.User
Command string
Args []string
RawArgs string
ReplyTo id.EventID
Ctx context.Context
Log *zerolog.Logger
MessageStatus *bridgev2.MessageStatus
}
// Reply sends a reply to command as notice, with optional string formatting and automatic $cmdprefix replacement.
func (ce *Event) Reply(msg string, args ...any) {
msg = strings.ReplaceAll(msg, "$cmdprefix ", ce.Bridge.Config.CommandPrefix+" ")
if len(args) > 0 {
msg = fmt.Sprintf(msg, args...)
}
ce.ReplyAdvanced(msg, true, false)
}
// ReplyAdvanced sends a reply to command as notice. It allows using HTML and disabling markdown,
// but doesn't have built-in string formatting.
func (ce *Event) ReplyAdvanced(msg string, allowMarkdown, allowHTML bool) {
content := format.RenderMarkdown(msg, allowMarkdown, allowHTML)
content.MsgType = event.MsgNotice
_, err := ce.Bot.SendMessage(ce.Ctx, ce.OrigRoomID, event.EventMessage, &event.Content{Parsed: &content}, nil)
if err != nil {
ce.Log.Err(err).Msg("Failed to reply to command")
}
}
// React sends a reaction to the command.
func (ce *Event) React(key string) {
_, err := ce.Bot.SendMessage(ce.Ctx, ce.OrigRoomID, event.EventReaction, &event.Content{
Parsed: &event.ReactionEventContent{
RelatesTo: event.RelatesTo{
Type: event.RelAnnotation,
EventID: ce.EventID,
Key: key,
},
},
}, nil)
if err != nil {
ce.Log.Err(err).Msg("Failed to react to command")
}
}
// Redact redacts the command.
func (ce *Event) Redact(req ...mautrix.ReqRedact) {
_, err := ce.Bot.SendMessage(ce.Ctx, ce.OrigRoomID, event.EventRedaction, &event.Content{
Parsed: &event.RedactionEventContent{
Redacts: ce.EventID,
},
}, nil)
if err != nil {
ce.Log.Err(err).Msg("Failed to redact command")
}
}
// MarkRead marks the command event as read.
func (ce *Event) MarkRead() {
err := ce.Bot.MarkRead(ce.Ctx, ce.RoomID, ce.EventID, time.Now())
if err != nil {
ce.Log.Err(err).Msg("Failed to mark command as read")
}
}

View file

@ -0,0 +1,118 @@
// 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 commands
import (
"maunium.net/go/mautrix/bridgev2"
"maunium.net/go/mautrix/event"
)
type MinimalCommandHandler interface {
Run(*Event)
}
type MinimalCommandHandlerFunc func(*Event)
func (mhf MinimalCommandHandlerFunc) Run(ce *Event) {
mhf(ce)
}
type CommandState struct {
Next MinimalCommandHandler
Action string
Meta any
Cancel func()
}
type CommandHandler interface {
MinimalCommandHandler
GetName() string
}
type AliasedCommandHandler interface {
CommandHandler
GetAliases() []string
}
func NetworkAPIImplements[T bridgev2.NetworkAPI](val bridgev2.NetworkAPI) bool {
_, ok := val.(T)
return ok
}
func NetworkConnectorImplements[T bridgev2.NetworkConnector](val bridgev2.NetworkConnector) bool {
_, ok := val.(T)
return ok
}
type ImplementationChecker[T any] func(val T) bool
type FullHandler struct {
Func func(*Event)
Name string
Aliases []string
Help HelpMeta
RequiresAdmin bool
RequiresPortal bool
RequiresLogin bool
RequiresEventLevel event.Type
RequiresLoginPermission bool
NetworkAPI ImplementationChecker[bridgev2.NetworkAPI]
NetworkConnector ImplementationChecker[bridgev2.NetworkConnector]
}
func (fh *FullHandler) GetHelp() HelpMeta {
fh.Help.Command = fh.Name
return fh.Help
}
func (fh *FullHandler) GetName() string {
return fh.Name
}
func (fh *FullHandler) GetAliases() []string {
return fh.Aliases
}
func (fh *FullHandler) ImplementationsFulfilled(ce *Event) bool {
// TODO add dedicated method to get an empty NetworkAPI instead of getting default login
client := ce.User.GetDefaultLogin()
return (fh.NetworkAPI == nil || client == nil || fh.NetworkAPI(client.Client)) &&
(fh.NetworkConnector == nil || fh.NetworkConnector(ce.Bridge.Network))
}
func (fh *FullHandler) ShowInHelp(ce *Event) bool {
return fh.ImplementationsFulfilled(ce) && (!fh.RequiresAdmin || ce.User.Permissions.Admin)
}
func (fh *FullHandler) userHasRoomPermission(ce *Event) bool {
levels, err := ce.Bridge.Matrix.GetPowerLevels(ce.Ctx, ce.RoomID)
if err != nil {
ce.Log.Warn().Err(err).Msg("Failed to check room power levels")
ce.Reply("Failed to get room power levels to see if you're allowed to use that command")
return false
}
return levels.GetUserLevel(ce.User.MXID) >= levels.GetEventLevel(fh.RequiresEventLevel)
}
func (fh *FullHandler) Run(ce *Event) {
if fh.RequiresAdmin && !ce.User.Permissions.Admin {
ce.Reply("That command is limited to bridge administrators.")
} else if fh.RequiresLoginPermission && !ce.User.Permissions.Login {
ce.Reply("You do not have permissions to log into this bridge.")
} else if fh.RequiresEventLevel.Type != "" && !ce.User.Permissions.Admin && !fh.userHasRoomPermission(ce) {
ce.Reply("That command requires room admin rights.")
} else if fh.RequiresPortal && ce.Portal == nil {
ce.Reply("That command can only be ran in portal rooms.")
} else if fh.RequiresLogin && ce.User.GetDefaultLogin() == nil {
ce.Reply("That command requires you to be logged in.")
} else {
fh.Func(ce)
}
}

View file

@ -13,7 +13,7 @@ import (
)
type HelpfulHandler interface {
Handler
CommandHandler
GetHelp() HelpMeta
ShowInHelp(*Event) bool
}
@ -29,6 +29,7 @@ var (
HelpSectionGeneral = HelpSection{"General", 0}
HelpSectionAuth = HelpSection{"Authentication", 10}
HelpSectionChats = HelpSection{"Starting and managing chats", 20}
HelpSectionAdmin = HelpSection{"Administration", 50}
)
@ -101,14 +102,14 @@ func FormatHelp(ce *Event) string {
output.Grow(10240)
var prefixMsg string
if ce.RoomID == ce.User.GetManagementRoomID() {
if ce.RoomID == ce.User.ManagementRoom {
prefixMsg = "This is your management room: prefixing commands with `%s` is not required."
} else if ce.Portal != nil {
prefixMsg = "**This is a portal room**: you must always prefix commands with `%s`. Management commands will not be bridged."
} else {
prefixMsg = "This is not your management room: prefixing commands with `%s` is required."
}
_, _ = fmt.Fprintf(&output, prefixMsg, ce.Bridge.Config.Bridge.GetCommandPrefix())
_, _ = fmt.Fprintf(&output, prefixMsg, ce.Bridge.Config.CommandPrefix)
output.WriteByte('\n')
output.WriteString("Parameters in [square brackets] are optional, while parameters in <angle brackets> are required.")
output.WriteByte('\n')
@ -127,3 +128,14 @@ func FormatHelp(ce *Event) string {
}
return output.String()
}
var CommandHelp = &FullHandler{
Func: func(ce *Event) {
ce.Reply(FormatHelp(ce))
},
Name: "help",
Help: HelpMeta{
Section: HelpSectionGeneral,
Description: "Show this help message.",
},
}

616
bridgev2/commands/login.go Normal file
View file

@ -0,0 +1,616 @@
// 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 commands
import (
"context"
"encoding/json"
"fmt"
"html"
"net/url"
"regexp"
"slices"
"strings"
"github.com/skip2/go-qrcode"
"go.mau.fi/util/curl"
"maunium.net/go/mautrix/bridgev2"
"maunium.net/go/mautrix/bridgev2/networkid"
"maunium.net/go/mautrix/bridgev2/status"
"maunium.net/go/mautrix/event"
"maunium.net/go/mautrix/id"
)
var CommandLogin = &FullHandler{
Func: fnLogin,
Name: "login",
Help: HelpMeta{
Section: HelpSectionAuth,
Description: "Log into the bridge",
Args: "[_flow ID_]",
},
RequiresLoginPermission: true,
}
var CommandRelogin = &FullHandler{
Func: fnLogin,
Name: "relogin",
Help: HelpMeta{
Section: HelpSectionAuth,
Description: "Re-authenticate an existing login",
Args: "<_login ID_> [_flow ID_]",
},
RequiresLoginPermission: true,
}
func formatFlowsReply(flows []bridgev2.LoginFlow) string {
var buf strings.Builder
for _, flow := range flows {
_, _ = fmt.Fprintf(&buf, "* `%s` - %s\n", flow.ID, flow.Description)
}
return buf.String()
}
func fnLogin(ce *Event) {
var reauth *bridgev2.UserLogin
if ce.Command == "relogin" {
if len(ce.Args) == 0 {
ce.Reply("Usage: `$cmdprefix relogin <login ID> [_flow ID_]`\n\nYour logins:\n\n%s", ce.User.GetFormattedUserLogins())
return
}
reauth = ce.Bridge.GetCachedUserLoginByID(networkid.UserLoginID(ce.Args[0]))
if reauth == nil {
ce.Reply("Login `%s` not found", ce.Args[0])
return
}
ce.Args = ce.Args[1:]
}
if reauth == nil && ce.User.HasTooManyLogins() {
ce.Reply(
"You have reached the maximum number of logins (%d). "+
"Please logout from an existing login before creating a new one. "+
"If you want to re-authenticate an existing login, use the `$cmdprefix relogin` command.",
ce.User.Permissions.MaxLogins,
)
return
}
flows := ce.Bridge.Network.GetLoginFlows()
var chosenFlowID string
if len(ce.Args) > 0 {
inputFlowID := strings.ToLower(ce.Args[0])
ce.Args = ce.Args[1:]
for _, flow := range flows {
if flow.ID == inputFlowID {
chosenFlowID = flow.ID
break
}
}
if chosenFlowID == "" {
ce.Reply("Invalid login flow `%s`. Available options:\n\n%s", inputFlowID, formatFlowsReply(flows))
return
}
} else if len(flows) == 1 {
chosenFlowID = flows[0].ID
} else {
if reauth != nil {
ce.Reply("Please specify a login flow, e.g. `relogin %s %s`.\n\n%s", reauth.ID, flows[0].ID, formatFlowsReply(flows))
} else {
ce.Reply("Please specify a login flow, e.g. `login %s`.\n\n%s", flows[0].ID, formatFlowsReply(flows))
}
return
}
login, err := ce.Bridge.Network.CreateLogin(ce.Ctx, ce.User, chosenFlowID)
if err != nil {
ce.Reply("Failed to prepare login process: %v", err)
return
}
overridable, ok := login.(bridgev2.LoginProcessWithOverride)
var nextStep *bridgev2.LoginStep
if ok && reauth != nil {
nextStep, err = overridable.StartWithOverride(ce.Ctx, reauth)
} else {
nextStep, err = login.Start(ce.Ctx)
}
if err != nil {
ce.Reply("Failed to start login: %v", err)
return
}
ce.Log.Debug().Any("first_step", nextStep).Msg("Created login process")
nextStep = checkLoginCommandDirectParams(ce, login, nextStep)
if nextStep != nil {
doLoginStep(ce, login, nextStep, reauth)
}
}
func checkLoginCommandDirectParams(ce *Event, login bridgev2.LoginProcess, nextStep *bridgev2.LoginStep) *bridgev2.LoginStep {
if len(ce.Args) == 0 {
return nextStep
}
var ok bool
defer func() {
if !ok {
login.Cancel()
}
}()
var err error
switch nextStep.Type {
case bridgev2.LoginStepTypeDisplayAndWait:
ce.Reply("Invalid extra parameters for display and wait login step")
return nil
case bridgev2.LoginStepTypeUserInput:
if len(ce.Args) != len(nextStep.UserInputParams.Fields) {
ce.Reply("Invalid number of extra parameters (expected 0 or %d, got %d)", len(nextStep.UserInputParams.Fields), len(ce.Args))
return nil
}
input := make(map[string]string)
var shouldRedact bool
for i, param := range nextStep.UserInputParams.Fields {
param.FillDefaultValidate()
input[param.ID], err = param.Validate(ce.Args[i])
if err != nil {
ce.Reply("Invalid value for %s: %v", param.Name, err)
return nil
}
if param.Type == bridgev2.LoginInputFieldTypePassword || param.Type == bridgev2.LoginInputFieldTypeToken {
shouldRedact = true
}
}
if shouldRedact {
ce.Redact()
}
nextStep, err = login.(bridgev2.LoginProcessUserInput).SubmitUserInput(ce.Ctx, input)
case bridgev2.LoginStepTypeCookies:
if len(ce.Args) != len(nextStep.CookiesParams.Fields) {
ce.Reply("Invalid number of extra parameters (expected 0 or %d, got %d)", len(nextStep.CookiesParams.Fields), len(ce.Args))
return nil
}
input := make(map[string]string)
for i, param := range nextStep.CookiesParams.Fields {
val := maybeURLDecodeCookie(ce.Args[i], &param)
if match, _ := regexp.MatchString(param.Pattern, val); !match {
ce.Reply("Invalid value for %s: `%s` doesn't match regex `%s`", param.ID, val, param.Pattern)
return nil
}
input[param.ID] = val
}
ce.Redact()
nextStep, err = login.(bridgev2.LoginProcessCookies).SubmitCookies(ce.Ctx, input)
}
if err != nil {
ce.Reply("Failed to submit input: %v", err)
return nil
}
ok = true
return nextStep
}
type userInputLoginCommandState struct {
Login bridgev2.LoginProcessUserInput
Data map[string]string
RemainingFields []bridgev2.LoginInputDataField
Override *bridgev2.UserLogin
}
func (uilcs *userInputLoginCommandState) promptNext(ce *Event) {
field := uilcs.RemainingFields[0]
parts := []string{fmt.Sprintf("Please enter your %s", field.Name)}
if field.Description != "" {
parts = append(parts, field.Description)
}
if len(field.Options) > 0 {
parts = append(parts, fmt.Sprintf("Options: `%s`", strings.Join(field.Options, "`, `")))
}
ce.Reply(strings.Join(parts, "\n"))
StoreCommandState(ce.User, &CommandState{
Next: MinimalCommandHandlerFunc(uilcs.submitNext),
Action: "Login",
Meta: uilcs,
Cancel: uilcs.Login.Cancel,
})
}
func (uilcs *userInputLoginCommandState) submitNext(ce *Event) {
field := uilcs.RemainingFields[0]
field.FillDefaultValidate()
if field.Type == bridgev2.LoginInputFieldTypePassword || field.Type == bridgev2.LoginInputFieldTypeToken {
ce.Redact()
}
var err error
uilcs.Data[field.ID], err = field.Validate(ce.RawArgs)
if err != nil {
ce.Reply("Invalid value: %v", err)
return
} else if len(uilcs.RemainingFields) > 1 {
uilcs.RemainingFields = uilcs.RemainingFields[1:]
uilcs.promptNext(ce)
return
}
StoreCommandState(ce.User, nil)
if nextStep, err := uilcs.Login.SubmitUserInput(ce.Ctx, uilcs.Data); err != nil {
ce.Reply("Failed to submit input: %v", err)
} else {
doLoginStep(ce, uilcs.Login, nextStep, uilcs.Override)
}
}
const qrSizePx = 512
func sendQR(ce *Event, qr string, prevEventID *id.EventID) error {
qrData, err := qrcode.Encode(qr, qrcode.Low, qrSizePx)
if err != nil {
return fmt.Errorf("failed to encode QR code: %w", err)
}
qrMXC, qrFile, err := ce.Bot.UploadMedia(ce.Ctx, ce.RoomID, qrData, "qr.png", "image/png")
if err != nil {
return fmt.Errorf("failed to upload image: %w", err)
}
content := &event.MessageEventContent{
MsgType: event.MsgImage,
FileName: "qr.png",
URL: qrMXC,
File: qrFile,
Body: qr,
Format: event.FormatHTML,
FormattedBody: fmt.Sprintf("<pre><code>%s</code></pre>", html.EscapeString(qr)),
Info: &event.FileInfo{
MimeType: "image/png",
Width: qrSizePx,
Height: qrSizePx,
Size: len(qrData),
},
}
if *prevEventID != "" {
content.SetEdit(*prevEventID)
}
newEventID, err := ce.Bot.SendMessage(ce.Ctx, ce.RoomID, event.EventMessage, &event.Content{Parsed: content}, nil)
if err != nil {
return err
}
if *prevEventID == "" {
*prevEventID = newEventID.EventID
}
return nil
}
func sendUserInputAttachments(ce *Event, atts []*bridgev2.LoginUserInputAttachment) error {
for _, att := range atts {
if att.FileName == "" {
return fmt.Errorf("missing attachment filename")
}
mxc, file, err := ce.Bot.UploadMedia(ce.Ctx, ce.RoomID, att.Content, att.FileName, att.Info.MimeType)
if err != nil {
return fmt.Errorf("failed to upload attachment %q: %w", att.FileName, err)
}
content := &event.MessageEventContent{
MsgType: att.Type,
FileName: att.FileName,
URL: mxc,
File: file,
Info: &event.FileInfo{
MimeType: att.Info.MimeType,
Width: att.Info.Width,
Height: att.Info.Height,
Size: att.Info.Size,
},
Body: att.FileName,
}
_, err = ce.Bot.SendMessage(ce.Ctx, ce.RoomID, event.EventMessage, &event.Content{Parsed: content}, nil)
if err != nil {
return nil
}
}
return nil
}
type contextKey int
const (
contextKeyPrevEventID contextKey = iota
)
func doLoginDisplayAndWait(ce *Event, login bridgev2.LoginProcessDisplayAndWait, step *bridgev2.LoginStep, override *bridgev2.UserLogin) {
prevEvent, ok := ce.Ctx.Value(contextKeyPrevEventID).(*id.EventID)
if !ok {
prevEvent = new(id.EventID)
ce.Ctx = context.WithValue(ce.Ctx, contextKeyPrevEventID, prevEvent)
}
cancelCtx, cancelFunc := context.WithCancel(ce.Ctx)
defer cancelFunc()
StoreCommandState(ce.User, &CommandState{
Action: "Login",
Cancel: cancelFunc,
})
defer StoreCommandState(ce.User, nil)
switch step.DisplayAndWaitParams.Type {
case bridgev2.LoginDisplayTypeQR:
err := sendQR(ce, step.DisplayAndWaitParams.Data, prevEvent)
if err != nil {
ce.Reply("Failed to send QR code: %v", err)
login.Cancel()
return
}
case bridgev2.LoginDisplayTypeEmoji:
ce.ReplyAdvanced(step.DisplayAndWaitParams.Data, false, false)
case bridgev2.LoginDisplayTypeCode:
ce.ReplyAdvanced(fmt.Sprintf("<code>%s</code>", html.EscapeString(step.DisplayAndWaitParams.Data)), false, true)
case bridgev2.LoginDisplayTypeNothing:
// Do nothing
default:
ce.Reply("Unsupported display type %q", step.DisplayAndWaitParams.Type)
login.Cancel()
return
}
nextStep, err := login.Wait(cancelCtx)
// Redact the QR code, unless the next step is refreshing the code (in which case the event is just edited)
if *prevEvent != "" && (nextStep == nil || nextStep.StepID != step.StepID) {
_, _ = ce.Bot.SendMessage(ce.Ctx, ce.RoomID, event.EventRedaction, &event.Content{
Parsed: &event.RedactionEventContent{
Redacts: *prevEvent,
},
}, nil)
*prevEvent = ""
}
if err != nil {
ce.Reply("Login failed: %v", err)
return
}
doLoginStep(ce, login, nextStep, override)
}
type cookieLoginCommandState struct {
Login bridgev2.LoginProcessCookies
Data *bridgev2.LoginCookiesParams
Override *bridgev2.UserLogin
}
func (clcs *cookieLoginCommandState) prompt(ce *Event) {
ce.Reply("Login URL: <%s>", clcs.Data.URL)
StoreCommandState(ce.User, &CommandState{
Next: MinimalCommandHandlerFunc(clcs.submit),
Action: "Login",
Meta: clcs,
Cancel: clcs.Login.Cancel,
})
}
func (clcs *cookieLoginCommandState) submit(ce *Event) {
ce.Redact()
cookiesInput := make(map[string]string)
if strings.HasPrefix(strings.TrimSpace(ce.RawArgs), "curl") {
parsed, err := curl.Parse(ce.RawArgs)
if err != nil {
ce.Reply("Failed to parse curl: %v", err)
return
}
reqCookies := make(map[string]string)
for _, cookie := range parsed.Cookies() {
reqCookies[cookie.Name], err = url.PathUnescape(cookie.Value)
if err != nil {
ce.Reply("Failed to parse cookie %s: %v", cookie.Name, err)
return
}
}
var missingKeys, unsupportedKeys []string
for _, field := range clcs.Data.Fields {
var value string
var supported bool
for _, src := range field.Sources {
switch src.Type {
case bridgev2.LoginCookieTypeCookie:
supported = true
value = reqCookies[src.Name]
case bridgev2.LoginCookieTypeRequestHeader:
supported = true
value = parsed.Header.Get(src.Name)
case bridgev2.LoginCookieTypeRequestBody:
supported = true
switch {
case parsed.MultipartForm != nil:
values, ok := parsed.MultipartForm.Value[src.Name]
if ok && len(values) > 0 {
value = values[0]
}
case parsed.ParsedJSON != nil:
untypedValue, ok := parsed.ParsedJSON[src.Name]
if ok {
value = fmt.Sprintf("%v", untypedValue)
}
}
}
if value != "" {
cookiesInput[field.ID] = value
break
}
}
if value == "" && field.Required {
if supported {
missingKeys = append(missingKeys, field.ID)
} else {
unsupportedKeys = append(unsupportedKeys, field.ID)
}
}
}
if len(unsupportedKeys) > 0 {
ce.Reply("Some keys can't be extracted from a cURL request: %+v\n\nPlease provide a JSON object instead.", unsupportedKeys)
return
} else if len(missingKeys) > 0 {
ce.Reply("Missing some keys: %+v", missingKeys)
return
}
} else {
err := json.Unmarshal([]byte(ce.RawArgs), &cookiesInput)
if err != nil {
ce.Reply("Failed to parse input as JSON: %v", err)
return
}
for _, field := range clcs.Data.Fields {
val, ok := cookiesInput[field.ID]
if ok {
cookiesInput[field.ID] = maybeURLDecodeCookie(val, &field)
}
}
}
var missingKeys []string
for _, field := range clcs.Data.Fields {
val, ok := cookiesInput[field.ID]
if !ok && field.Required {
missingKeys = append(missingKeys, field.ID)
}
if match, _ := regexp.MatchString(field.Pattern, val); !match {
ce.Reply("Invalid value for %s: `%s` doesn't match regex `%s`", field.ID, val, field.Pattern)
return
}
}
if len(missingKeys) > 0 {
ce.Reply("Missing some keys: %+v", missingKeys)
return
}
StoreCommandState(ce.User, nil)
nextStep, err := clcs.Login.SubmitCookies(ce.Ctx, cookiesInput)
if err != nil {
ce.Reply("Login failed: %v", err)
return
}
doLoginStep(ce, clcs.Login, nextStep, clcs.Override)
}
func maybeURLDecodeCookie(val string, field *bridgev2.LoginCookieField) string {
if val == "" {
return val
}
isCookie := slices.ContainsFunc(field.Sources, func(src bridgev2.LoginCookieFieldSource) bool {
return src.Type == bridgev2.LoginCookieTypeCookie
})
if !isCookie {
return val
}
decoded, err := url.PathUnescape(val)
if err != nil {
return val
}
return decoded
}
func doLoginStep(ce *Event, login bridgev2.LoginProcess, step *bridgev2.LoginStep, override *bridgev2.UserLogin) {
ce.Log.Debug().Any("next_step", step).Msg("Got next login step")
if step.Instructions != "" {
ce.Reply(step.Instructions)
}
switch step.Type {
case bridgev2.LoginStepTypeDisplayAndWait:
doLoginDisplayAndWait(ce, login.(bridgev2.LoginProcessDisplayAndWait), step, override)
case bridgev2.LoginStepTypeCookies:
(&cookieLoginCommandState{
Login: login.(bridgev2.LoginProcessCookies),
Data: step.CookiesParams,
Override: override,
}).prompt(ce)
case bridgev2.LoginStepTypeUserInput:
err := sendUserInputAttachments(ce, step.UserInputParams.Attachments)
if err != nil {
ce.Reply("Failed to send attachments: %v", err)
}
(&userInputLoginCommandState{
Login: login.(bridgev2.LoginProcessUserInput),
RemainingFields: step.UserInputParams.Fields,
Data: make(map[string]string),
Override: override,
}).promptNext(ce)
case bridgev2.LoginStepTypeComplete:
if override != nil && override.ID != step.CompleteParams.UserLoginID {
ce.Log.Info().
Str("old_login_id", string(override.ID)).
Str("new_login_id", string(step.CompleteParams.UserLoginID)).
Msg("Login resulted in different remote ID than what was being overridden. Deleting previous login")
override.Delete(ce.Ctx, status.BridgeState{
StateEvent: status.StateLoggedOut,
Reason: "LOGIN_OVERRIDDEN",
}, bridgev2.DeleteOpts{LogoutRemote: true})
}
default:
panic(fmt.Errorf("unknown login step type %q", step.Type))
}
}
var CommandListLogins = &FullHandler{
Func: fnListLogins,
Name: "list-logins",
Help: HelpMeta{
Section: HelpSectionAuth,
Description: "List your logins",
},
RequiresLoginPermission: true,
}
func fnListLogins(ce *Event) {
logins := ce.User.GetFormattedUserLogins()
if len(logins) == 0 {
ce.Reply("You're not logged in")
} else {
ce.Reply("%s", logins)
}
}
var CommandLogout = &FullHandler{
Func: fnLogout,
Name: "logout",
Help: HelpMeta{
Section: HelpSectionAuth,
Description: "Log out of the bridge",
Args: "<_login ID_>",
},
}
func fnLogout(ce *Event) {
if len(ce.Args) == 0 {
ce.Reply("Usage: `$cmdprefix logout <login ID>`\n\nYour logins:\n\n%s", ce.User.GetFormattedUserLogins())
return
}
login := ce.Bridge.GetCachedUserLoginByID(networkid.UserLoginID(ce.Args[0]))
if login == nil || login.UserMXID != ce.User.MXID {
ce.Reply("Login `%s` not found", ce.Args[0])
return
}
login.Logout(ce.Ctx)
ce.Reply("Logged out")
}
var CommandSetPreferredLogin = &FullHandler{
Func: fnSetPreferredLogin,
Name: "set-preferred-login",
Aliases: []string{"prefer"},
Help: HelpMeta{
Section: HelpSectionAuth,
Description: "Set the preferred login ID for sending messages to this portal (only relevant when logged into multiple accounts via the bridge)",
Args: "<_login ID_>",
},
RequiresPortal: true,
RequiresLoginPermission: true,
}
func fnSetPreferredLogin(ce *Event) {
if len(ce.Args) == 0 {
ce.Reply("Usage: `$cmdprefix set-preferred-login <login ID>`\n\nYour logins:\n\n%s", ce.User.GetFormattedUserLogins())
return
}
login := ce.Bridge.GetCachedUserLoginByID(networkid.UserLoginID(ce.Args[0]))
if login == nil || login.UserMXID != ce.User.MXID {
ce.Reply("Login `%s` not found", ce.Args[0])
return
}
err := login.MarkAsPreferredIn(ce.Ctx, ce.Portal)
if err != nil {
ce.Reply("Failed to set preferred login: %v", err)
} else {
ce.Reply("Preferred login set")
}
}

View file

@ -0,0 +1,206 @@
// 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 commands
import (
"context"
"fmt"
"runtime/debug"
"strings"
"sync/atomic"
"unsafe"
"github.com/rs/zerolog"
"maunium.net/go/mautrix/bridgev2"
"maunium.net/go/mautrix/bridgev2/status"
"maunium.net/go/mautrix/event"
"maunium.net/go/mautrix/id"
)
type Processor struct {
bridge *bridgev2.Bridge
log *zerolog.Logger
handlers map[string]CommandHandler
aliases map[string]string
}
// NewProcessor creates a Processor
func NewProcessor(bridge *bridgev2.Bridge) bridgev2.CommandProcessor {
proc := &Processor{
bridge: bridge,
log: &bridge.Log,
handlers: make(map[string]CommandHandler),
aliases: make(map[string]string),
}
proc.AddHandlers(
CommandHelp, CommandCancel,
CommandRegisterPush, CommandSendAccountData, CommandResetNetwork,
CommandDeletePortal, CommandDeleteAllPortals, CommandSetManagementRoom,
CommandLogin, CommandRelogin, CommandListLogins, CommandLogout, CommandSetPreferredLogin,
CommandSetRelay, CommandUnsetRelay,
CommandResolveIdentifier, CommandStartChat, CommandCreateGroup, CommandSearch, CommandSyncChat, CommandMute,
CommandSudo, CommandDoIn,
)
return proc
}
func (proc *Processor) AddHandlers(handlers ...CommandHandler) {
for _, handler := range handlers {
proc.AddHandler(handler)
}
}
func (proc *Processor) AddHandler(handler CommandHandler) {
proc.handlers[handler.GetName()] = handler
aliased, ok := handler.(AliasedCommandHandler)
if ok {
for _, alias := range aliased.GetAliases() {
proc.aliases[alias] = handler.GetName()
}
}
}
// Handle handles messages to the bridge
func (proc *Processor) Handle(ctx context.Context, roomID id.RoomID, eventID id.EventID, user *bridgev2.User, message string, replyTo id.EventID) {
ms := &bridgev2.MessageStatus{
Step: status.MsgStepCommand,
Status: event.MessageStatusSuccess,
}
logCopy := zerolog.Ctx(ctx).With().Logger()
log := &logCopy
defer func() {
statusInfo := &bridgev2.MessageStatusEventInfo{
RoomID: roomID,
SourceEventID: eventID,
EventType: event.EventMessage,
Sender: user.MXID,
}
err := recover()
if err != nil {
logEvt := log.Error().
Bytes(zerolog.ErrorStackFieldName, debug.Stack())
if realErr, ok := err.(error); ok {
logEvt = logEvt.Err(realErr)
} else {
logEvt = logEvt.Any(zerolog.ErrorFieldName, err)
}
logEvt.Msg("Panic in Matrix command handler")
ms.Status = event.MessageStatusFail
ms.IsCertain = true
if realErr, ok := err.(error); ok {
ms.InternalError = realErr
} else {
ms.InternalError = fmt.Errorf("%v", err)
}
ms.ErrorAsMessage = true
}
proc.bridge.Matrix.SendMessageStatus(ctx, ms, statusInfo)
}()
args := strings.Fields(message)
if len(args) == 0 {
args = []string{"unknown-command"}
}
command := strings.ToLower(args[0])
rawArgs := strings.TrimLeft(strings.TrimPrefix(message, command), " ")
portal, err := proc.bridge.GetPortalByMXID(ctx, roomID)
if err != nil {
log.Err(err).Msg("Failed to get portal")
// :(
}
ce := &Event{
Bot: proc.bridge.Bot,
Bridge: proc.bridge,
Portal: portal,
Processor: proc,
RoomID: roomID,
OrigRoomID: roomID,
EventID: eventID,
User: user,
Command: command,
Args: args[1:],
RawArgs: rawArgs,
ReplyTo: replyTo,
Ctx: ctx,
Log: log,
MessageStatus: ms,
}
proc.handleCommand(ctx, ce, message, args)
}
func (proc *Processor) handleCommand(ctx context.Context, ce *Event, origMessage string, origArgs []string) {
realCommand, ok := proc.aliases[ce.Command]
if !ok {
realCommand = ce.Command
}
log := zerolog.Ctx(ctx)
var handler MinimalCommandHandler
handler, ok = proc.handlers[realCommand]
if !ok {
state := LoadCommandState(ce.User)
if state != nil && state.Next != nil {
ce.Command = ""
ce.RawArgs = origMessage
ce.Args = origArgs
ce.Handler = state.Next
log.UpdateContext(func(c zerolog.Context) zerolog.Context {
return c.Str("action", state.Action)
})
log.Debug().Msg("Received reply to command state")
state.Next.Run(ce)
} else {
zerolog.Ctx(ctx).Debug().Str("mx_command", ce.Command).Msg("Received unknown command")
ce.Reply("Unknown command, use the `help` command for help.")
}
} else {
log.UpdateContext(func(c zerolog.Context) zerolog.Context {
return c.Str("mx_command", ce.Command)
})
log.Debug().Msg("Received command")
ce.Handler = handler
handler.Run(ce)
}
}
func LoadCommandState(user *bridgev2.User) *CommandState {
return (*CommandState)(atomic.LoadPointer(&user.CommandState))
}
func StoreCommandState(user *bridgev2.User, cs *CommandState) {
atomic.StorePointer(&user.CommandState, unsafe.Pointer(cs))
}
func SwapCommandState(user *bridgev2.User, cs *CommandState) *CommandState {
return (*CommandState)(atomic.SwapPointer(&user.CommandState, unsafe.Pointer(cs)))
}
var CommandCancel = &FullHandler{
Func: func(ce *Event) {
state := SwapCommandState(ce.User, nil)
if state != nil {
action := state.Action
if action == "" {
action = "Unknown action"
}
if state.Cancel != nil {
state.Cancel()
}
ce.Reply("%s cancelled.", action)
} else {
ce.Reply("No ongoing command.")
}
},
Name: "cancel",
Help: HelpMeta{
Section: HelpSectionGeneral,
Description: "Cancel an ongoing action.",
},
}

156
bridgev2/commands/relay.go Normal file
View file

@ -0,0 +1,156 @@
// 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 commands
import (
"golang.org/x/exp/slices"
"maunium.net/go/mautrix/bridgev2"
"maunium.net/go/mautrix/bridgev2/networkid"
"maunium.net/go/mautrix/event"
)
var fakeEvtSetRelay = event.Type{Type: "fi.mau.bridge.set_relay", Class: event.StateEventType}
var CommandSetRelay = &FullHandler{
Func: fnSetRelay,
Name: "set-relay",
Help: HelpMeta{
Section: HelpSectionAuth,
Description: "Use your account to relay messages sent by users who haven't logged in",
Args: "[_login ID_]",
},
RequiresPortal: true,
}
func fnSetRelay(ce *Event) {
if !ce.Bridge.Config.Relay.Enabled {
ce.Reply("This bridge does not allow relay mode")
return
} else if !canManageRelay(ce) {
ce.Reply("You don't have permission to manage the relay in this room")
return
}
onlySetDefaultRelays := !ce.User.Permissions.Admin && ce.Bridge.Config.Relay.AdminOnly
var relay *bridgev2.UserLogin
if len(ce.Args) == 0 && ce.Portal.Receiver == "" {
relay = ce.User.GetDefaultLogin()
isLoggedIn := relay != nil
if onlySetDefaultRelays {
relay = nil
}
if relay == nil {
if len(ce.Bridge.Config.Relay.DefaultRelays) == 0 {
ce.Reply("You're not logged in and there are no default relay users configured")
return
}
logins, err := ce.Bridge.GetUserLoginsInPortal(ce.Ctx, ce.Portal.PortalKey)
if err != nil {
ce.Log.Err(err).Msg("Failed to get user logins in portal")
ce.Reply("Failed to get logins in portal to find default relay")
return
}
Outer:
for _, loginID := range ce.Bridge.Config.Relay.DefaultRelays {
for _, login := range logins {
if login.ID == loginID {
relay = login
break Outer
}
}
}
if relay == nil {
if isLoggedIn {
ce.Reply("You're not allowed to use yourself as relay and none of the default relay users are in the chat")
} else {
ce.Reply("You're not logged in and none of the default relay users are in the chat")
}
return
}
}
} else {
var targetID networkid.UserLoginID
if ce.Portal.Receiver != "" {
targetID = ce.Portal.Receiver
if len(ce.Args) > 0 && ce.Args[0] != string(targetID) {
ce.Reply("In split portals, only the receiver (%s) can be set as relay", targetID)
return
}
} else {
targetID = networkid.UserLoginID(ce.Args[0])
}
relay = ce.Bridge.GetCachedUserLoginByID(targetID)
if relay == nil {
ce.Reply("User login with ID `%s` not found", targetID)
return
} else if slices.Contains(ce.Bridge.Config.Relay.DefaultRelays, relay.ID) {
// All good
} else if relay.UserMXID != ce.User.MXID && !ce.User.Permissions.Admin {
ce.Reply("Only bridge admins can set another user's login as the relay")
return
} else if onlySetDefaultRelays {
ce.Reply("You're not allowed to use yourself as relay")
return
}
}
err := ce.Portal.SetRelay(ce.Ctx, relay)
if err != nil {
ce.Log.Err(err).Msg("Failed to unset relay")
ce.Reply("Failed to save relay settings")
} else {
ce.Reply(
"Messages sent by users who haven't logged in will now be relayed through %s ([%s](%s)'s login)",
relay.RemoteName,
relay.UserMXID,
// TODO this will need to stop linkifying if we ever allow UserLogins that aren't bound to a real user.
relay.UserMXID.URI().MatrixToURL(),
)
}
}
var CommandUnsetRelay = &FullHandler{
Func: fnUnsetRelay,
Name: "unset-relay",
Help: HelpMeta{
Section: HelpSectionAuth,
Description: "Stop relaying messages sent by users who haven't logged in",
},
RequiresPortal: true,
}
func fnUnsetRelay(ce *Event) {
if ce.Portal.Relay == nil {
ce.Reply("This portal doesn't have a relay set.")
return
} else if !canManageRelay(ce) {
ce.Reply("You don't have permission to manage the relay in this room")
return
}
err := ce.Portal.SetRelay(ce.Ctx, nil)
if err != nil {
ce.Log.Err(err).Msg("Failed to unset relay")
ce.Reply("Failed to save relay settings")
} else {
ce.Reply("Stopped relaying messages for users who haven't logged in")
}
}
func canManageRelay(ce *Event) bool {
return ce.User.Permissions.ManageRelay &&
(ce.User.Permissions.Admin ||
(ce.Portal.Relay != nil && ce.Portal.Relay.UserMXID == ce.User.MXID) ||
hasRelayRoomPermissions(ce))
}
func hasRelayRoomPermissions(ce *Event) bool {
levels, err := ce.Bridge.Matrix.GetPowerLevels(ce.Ctx, ce.RoomID)
if err != nil {
ce.Log.Err(err).Msg("Failed to check room power levels")
return false
}
return levels.GetUserLevel(ce.User.MXID) >= levels.GetEventLevel(fakeEvtSetRelay)
}

View file

@ -0,0 +1,333 @@
// Copyright (c) 2025 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 commands
import (
"context"
"errors"
"fmt"
"html"
"maps"
"slices"
"strings"
"time"
"github.com/rs/zerolog"
"maunium.net/go/mautrix/bridgev2"
"maunium.net/go/mautrix/bridgev2/networkid"
"maunium.net/go/mautrix/bridgev2/provisionutil"
"maunium.net/go/mautrix/event"
"maunium.net/go/mautrix/format"
"maunium.net/go/mautrix/id"
)
var CommandResolveIdentifier = &FullHandler{
Func: fnResolveIdentifier,
Name: "resolve-identifier",
Help: HelpMeta{
Section: HelpSectionChats,
Description: "Check if a given identifier is on the remote network",
Args: "[_login ID_] <_identifier_>",
},
RequiresLogin: true,
NetworkAPI: NetworkAPIImplements[bridgev2.IdentifierResolvingNetworkAPI],
}
var CommandSyncChat = &FullHandler{
Func: func(ce *Event) {
login, _, err := ce.Portal.FindPreferredLogin(ce.Ctx, ce.User, false)
if err != nil {
ce.Log.Err(err).Msg("Failed to find login for sync")
ce.Reply("Failed to find login: %v", err)
return
} else if login == nil {
ce.Reply("No login found for sync")
return
}
info, err := login.Client.GetChatInfo(ce.Ctx, ce.Portal)
if err != nil {
ce.Log.Err(err).Msg("Failed to get chat info for sync")
ce.Reply("Failed to get chat info: %v", err)
return
}
ce.Portal.UpdateInfo(ce.Ctx, info, login, nil, time.Time{})
ce.React("✅️")
},
Name: "sync-portal",
Help: HelpMeta{
Section: HelpSectionChats,
Description: "Sync the current portal room",
},
RequiresPortal: true,
RequiresLogin: true,
}
var CommandStartChat = &FullHandler{
Func: fnResolveIdentifier,
Name: "start-chat",
Aliases: []string{"pm"},
Help: HelpMeta{
Section: HelpSectionChats,
Description: "Start a direct chat with the given user",
Args: "[_login ID_] <_identifier_>",
},
RequiresLogin: true,
NetworkAPI: NetworkAPIImplements[bridgev2.IdentifierResolvingNetworkAPI],
}
func getClientForStartingChat[T bridgev2.NetworkAPI](ce *Event, thing string) (*bridgev2.UserLogin, T, []string) {
var remainingArgs []string
if len(ce.Args) > 1 {
remainingArgs = ce.Args[1:]
}
var login *bridgev2.UserLogin
if len(ce.Args) > 0 {
login = ce.Bridge.GetCachedUserLoginByID(networkid.UserLoginID(ce.Args[0]))
}
if login == nil || login.UserMXID != ce.User.MXID {
remainingArgs = ce.Args
login = ce.User.GetDefaultLogin()
}
api, ok := login.Client.(T)
if !ok {
ce.Reply("This bridge does not support %s", thing)
}
return login, api, remainingArgs
}
func formatResolveIdentifierResult(resp *provisionutil.RespResolveIdentifier) string {
if resp.MXID != "" {
return fmt.Sprintf("`%s` / [%s](%s)", resp.ID, resp.Name, resp.MXID.URI().MatrixToURL())
} else if resp.Name != "" {
return fmt.Sprintf("`%s` / %s", resp.ID, resp.Name)
} else {
return fmt.Sprintf("`%s`", resp.ID)
}
}
func fnResolveIdentifier(ce *Event) {
if len(ce.Args) == 0 {
ce.Reply("Usage: `$cmdprefix %s <identifier>`", ce.Command)
return
}
login, api, identifierParts := getClientForStartingChat[bridgev2.IdentifierResolvingNetworkAPI](ce, "resolving identifiers")
if api == nil {
return
}
allLogins := ce.User.GetUserLogins()
createChat := ce.Command == "start-chat" || ce.Command == "pm"
identifier := strings.Join(identifierParts, " ")
resp, err := provisionutil.ResolveIdentifier(ce.Ctx, login, identifier, createChat)
for i := 0; i < len(allLogins) && errors.Is(err, bridgev2.ErrResolveIdentifierTryNext); i++ {
resp, err = provisionutil.ResolveIdentifier(ce.Ctx, allLogins[i], identifier, createChat)
}
if err != nil {
ce.Reply("Failed to resolve identifier: %v", err)
return
} else if resp == nil {
ce.ReplyAdvanced(fmt.Sprintf("Identifier <code>%s</code> not found", html.EscapeString(identifier)), false, true)
return
}
formattedName := formatResolveIdentifierResult(resp)
if createChat {
name := resp.Portal.Name
if name == "" {
name = resp.Portal.MXID.String()
}
if !resp.JustCreated {
ce.Reply("You already have a direct chat with %s at [%s](%s)", formattedName, name, resp.Portal.MXID.URI().MatrixToURL())
} else {
ce.Reply("Created chat with %s: [%s](%s)", formattedName, name, resp.Portal.MXID.URI().MatrixToURL())
}
} else {
ce.Reply("Found %s", formattedName)
}
}
var CommandCreateGroup = &FullHandler{
Func: fnCreateGroup,
Name: "create-group",
Aliases: []string{"create"},
Help: HelpMeta{
Section: HelpSectionChats,
Description: "Create a new group chat for the current Matrix room",
Args: "[_group type_]",
},
RequiresLogin: true,
NetworkAPI: NetworkAPIImplements[bridgev2.GroupCreatingNetworkAPI],
}
func getState[T any](ctx context.Context, roomID id.RoomID, evtType event.Type, provider bridgev2.MatrixConnectorWithArbitraryRoomState) (content T) {
evt, err := provider.GetStateEvent(ctx, roomID, evtType, "")
if err != nil {
zerolog.Ctx(ctx).Err(err).Stringer("event_type", evtType).Msg("Failed to get state event for group creation")
} else if evt != nil {
content, _ = evt.Content.Parsed.(T)
}
return
}
func fnCreateGroup(ce *Event) {
ce.Bridge.Matrix.GetCapabilities()
login, api, remainingArgs := getClientForStartingChat[bridgev2.GroupCreatingNetworkAPI](ce, "creating group")
if api == nil {
return
}
stateProvider, ok := ce.Bridge.Matrix.(bridgev2.MatrixConnectorWithArbitraryRoomState)
if !ok {
ce.Reply("Matrix connector doesn't support fetching room state")
return
}
members, err := ce.Bridge.Matrix.GetMembers(ce.Ctx, ce.RoomID)
if err != nil {
ce.Log.Err(err).Msg("Failed to get room members for group creation")
ce.Reply("Failed to get room members: %v", err)
return
}
caps := ce.Bridge.Network.GetCapabilities()
params := &bridgev2.GroupCreateParams{
Username: "",
Participants: make([]networkid.UserID, 0, len(members)-2),
Parent: nil, // TODO check space parent event
Name: getState[*event.RoomNameEventContent](ce.Ctx, ce.RoomID, event.StateRoomName, stateProvider),
Avatar: getState[*event.RoomAvatarEventContent](ce.Ctx, ce.RoomID, event.StateRoomAvatar, stateProvider),
Topic: getState[*event.TopicEventContent](ce.Ctx, ce.RoomID, event.StateTopic, stateProvider),
Disappear: getState[*event.BeeperDisappearingTimer](ce.Ctx, ce.RoomID, event.StateBeeperDisappearingTimer, stateProvider),
RoomID: ce.RoomID,
}
for userID, member := range members {
if userID == ce.User.MXID || userID == ce.Bot.GetMXID() || !member.Membership.IsInviteOrJoin() {
continue
}
if parsedUserID, ok := ce.Bridge.Matrix.ParseGhostMXID(userID); ok {
params.Participants = append(params.Participants, parsedUserID)
} else if !ce.Bridge.Config.SplitPortals {
if user, err := ce.Bridge.GetExistingUserByMXID(ce.Ctx, userID); err != nil {
ce.Log.Err(err).Stringer("user_id", userID).Msg("Failed to get user for room member")
} else if user != nil {
// TODO add user logins to participants
//for _, login := range user.GetUserLogins() {
// params.Participants = append(params.Participants, login.GetUserID())
//}
}
}
}
if len(caps.Provisioning.GroupCreation) == 0 {
ce.Reply("No group creation types defined in network capabilities")
return
} else if len(remainingArgs) > 0 {
params.Type = remainingArgs[0]
} else if len(caps.Provisioning.GroupCreation) == 1 {
for params.Type = range caps.Provisioning.GroupCreation {
// The loop assigns the variable we want
}
} else {
types := strings.Join(slices.Collect(maps.Keys(caps.Provisioning.GroupCreation)), "`, `")
ce.Reply("Please specify type of group to create: `%s`", types)
return
}
resp, err := provisionutil.CreateGroup(ce.Ctx, login, params)
if err != nil {
ce.Reply("Failed to create group: %v", err)
return
}
var postfix string
if len(resp.FailedParticipants) > 0 {
failedParticipantsStrings := make([]string, len(resp.FailedParticipants))
i := 0
for participantID, meta := range resp.FailedParticipants {
failedParticipantsStrings[i] = fmt.Sprintf("* %s: %s", format.SafeMarkdownCode(participantID), meta.Reason)
i++
}
postfix += "\n\nFailed to add some participants:\n" + strings.Join(failedParticipantsStrings, "\n")
}
ce.Reply("Successfully created group `%s`%s", resp.ID, postfix)
}
var CommandSearch = &FullHandler{
Func: fnSearch,
Name: "search",
Help: HelpMeta{
Section: HelpSectionChats,
Description: "Search for users on the remote network",
Args: "<_query_>",
},
RequiresLogin: true,
NetworkAPI: NetworkAPIImplements[bridgev2.UserSearchingNetworkAPI],
}
func fnSearch(ce *Event) {
if len(ce.Args) == 0 {
ce.Reply("Usage: `$cmdprefix search <query>`")
return
}
login, api, queryParts := getClientForStartingChat[bridgev2.UserSearchingNetworkAPI](ce, "searching users")
if api == nil {
return
}
resp, err := provisionutil.SearchUsers(ce.Ctx, login, strings.Join(queryParts, " "))
if err != nil {
ce.Reply("Failed to search for users: %v", err)
return
}
resultsString := make([]string, len(resp.Results))
for i, res := range resp.Results {
formattedName := formatResolveIdentifierResult(res)
resultsString[i] = fmt.Sprintf("* %s", formattedName)
if res.Portal != nil && res.Portal.MXID != "" {
portalName := res.Portal.Name
if portalName == "" {
portalName = res.Portal.MXID.String()
}
resultsString[i] = fmt.Sprintf("%s - DM portal: [%s](%s)", resultsString[i], portalName, res.Portal.MXID.URI().MatrixToURL())
}
}
ce.Reply("Search results:\n\n%s", strings.Join(resultsString, "\n"))
}
var CommandMute = &FullHandler{
Func: fnMute,
Name: "mute",
Aliases: []string{"unmute"},
Help: HelpMeta{
Section: HelpSectionChats,
Description: "Mute or unmute a chat on the remote network",
Args: "[duration]",
},
RequiresPortal: true,
RequiresLogin: true,
NetworkAPI: NetworkAPIImplements[bridgev2.MuteHandlingNetworkAPI],
}
func fnMute(ce *Event) {
_, api, _ := getClientForStartingChat[bridgev2.MuteHandlingNetworkAPI](ce, "muting chats")
var mutedUntil int64
if ce.Command == "mute" {
mutedUntil = -1
if len(ce.Args) > 0 {
duration, err := time.ParseDuration(ce.Args[0])
if err != nil {
ce.Reply("Invalid duration: %v", err)
return
}
mutedUntil = time.Now().Add(duration).UnixMilli()
}
}
err := api.HandleMute(ce.Ctx, &bridgev2.MatrixMute{
MatrixEventBase: bridgev2.MatrixEventBase[*event.BeeperMuteEventContent]{
Content: &event.BeeperMuteEventContent{MutedUntil: mutedUntil},
Portal: ce.Portal,
},
})
if err != nil {
ce.Reply("Failed to %s chat: %v", ce.Command, err)
} else {
ce.React("✅️")
}
}

107
bridgev2/commands/sudo.go Normal file
View file

@ -0,0 +1,107 @@
// 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 commands
import (
"strings"
"maunium.net/go/mautrix/bridgev2"
"maunium.net/go/mautrix/event"
"maunium.net/go/mautrix/id"
)
var CommandSudo = &FullHandler{
Func: fnSudo,
Name: "sudo",
Aliases: []string{"doas", "do-as", "runas", "run-as"},
Help: HelpMeta{
Section: HelpSectionAdmin,
Description: "Run a command as a different user.",
Args: "[--create] <_user ID_> <_command_> [_args..._]",
},
RequiresAdmin: true,
}
func fnSudo(ce *Event) {
forceNonexistentUser := len(ce.Args) > 0 && strings.ToLower(ce.Args[0]) == "--create"
if forceNonexistentUser {
ce.Args = ce.Args[1:]
}
if len(ce.Args) < 2 {
ce.Reply("Usage: `$cmdprefix sudo [--create] <user ID> <command> [args...]`")
return
}
targetUserID := id.UserID(ce.Args[0])
if _, _, err := targetUserID.Parse(); err != nil || len(targetUserID) > id.UserIDMaxLength {
ce.Reply("Invalid user ID `%s`", targetUserID)
return
}
var targetUser *bridgev2.User
var err error
if forceNonexistentUser {
targetUser, err = ce.Bridge.GetUserByMXID(ce.Ctx, targetUserID)
} else {
targetUser, err = ce.Bridge.GetExistingUserByMXID(ce.Ctx, targetUserID)
}
if err != nil {
ce.Log.Err(err).Msg("Failed to get user from database")
ce.Reply("Failed to get user")
return
} else if targetUser == nil {
ce.Reply("User not found. Use `--create` if you want to run commands as a user who has never used the bridge.")
return
}
ce.User = targetUser
origArgs := ce.Args[1:]
ce.Command = strings.ToLower(ce.Args[1])
ce.Args = ce.Args[2:]
ce.RawArgs = strings.Join(ce.Args, " ")
ce.Processor.handleCommand(ce.Ctx, ce, strings.Join(origArgs, " "), origArgs)
}
var CommandDoIn = &FullHandler{
Func: fnDoIn,
Name: "doin",
Aliases: []string{"do-in", "runin", "run-in"},
Help: HelpMeta{
Section: HelpSectionAdmin,
Description: "Run a command in a different room.",
Args: "<_room ID_> <_command_> [_args..._]",
},
}
func fnDoIn(ce *Event) {
if len(ce.Args) < 2 {
ce.Reply("Usage: `$cmdprefix doin <room ID> <command> [args...]`")
return
}
targetRoomID := id.RoomID(ce.Args[0])
if !ce.User.Permissions.Admin {
memberInfo, err := ce.Bridge.Matrix.GetMemberInfo(ce.Ctx, targetRoomID, ce.User.MXID)
if err != nil {
ce.Log.Err(err).Msg("Failed to check if user is in doin target room")
ce.Reply("Failed to check if you're in the target room")
return
} else if memberInfo == nil || memberInfo.Membership != event.MembershipJoin {
ce.Reply("You must be in the target room to run commands there")
return
}
}
ce.RoomID = targetRoomID
var err error
ce.Portal, err = ce.Bridge.GetPortalByMXID(ce.Ctx, targetRoomID)
if err != nil {
ce.Log.Err(err).Msg("Failed to get target portal")
ce.Reply("Failed to get portal")
return
}
origArgs := ce.Args[1:]
ce.Command = strings.ToLower(ce.Args[1])
ce.Args = ce.Args[2:]
ce.RawArgs = strings.Join(ce.Args, " ")
ce.Processor.handleCommand(ce.Ctx, ce, strings.Join(origArgs, " "), origArgs)
}

View file

@ -0,0 +1,182 @@
// 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 database
import (
"context"
"database/sql"
"time"
"go.mau.fi/util/dbutil"
"maunium.net/go/mautrix/bridgev2/networkid"
)
type BackfillTaskQuery struct {
BridgeID networkid.BridgeID
*dbutil.QueryHelper[*BackfillTask]
}
type BackfillTask struct {
BridgeID networkid.BridgeID
PortalKey networkid.PortalKey
UserLoginID networkid.UserLoginID
BatchCount int
IsDone bool
Cursor networkid.PaginationCursor
OldestMessageID networkid.MessageID
DispatchedAt time.Time
CompletedAt time.Time
NextDispatchMinTS time.Time
}
var BackfillNextDispatchNever = time.Unix(0, (1<<63)-1)
const (
ensureBackfillExistsQuery = `
INSERT INTO backfill_task (bridge_id, portal_id, portal_receiver, user_login_id, batch_count, is_done, next_dispatch_min_ts)
VALUES ($1, $2, $3, $4, -1, false, $5)
ON CONFLICT (bridge_id, portal_id, portal_receiver) DO UPDATE
SET user_login_id=CASE
WHEN backfill_task.user_login_id=''
THEN excluded.user_login_id
ELSE backfill_task.user_login_id
END,
next_dispatch_min_ts=CASE
WHEN backfill_task.next_dispatch_min_ts=9223372036854775807
THEN excluded.next_dispatch_min_ts
ELSE backfill_task.next_dispatch_min_ts
END
`
upsertBackfillQueueQuery = `
INSERT INTO backfill_task (
bridge_id, portal_id, portal_receiver, user_login_id, batch_count, is_done, cursor,
oldest_message_id, dispatched_at, completed_at, next_dispatch_min_ts
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)
ON CONFLICT (bridge_id, portal_id, portal_receiver) DO UPDATE
SET user_login_id=excluded.user_login_id,
batch_count=excluded.batch_count,
is_done=excluded.is_done,
cursor=excluded.cursor,
oldest_message_id=excluded.oldest_message_id,
dispatched_at=excluded.dispatched_at,
completed_at=excluded.completed_at,
next_dispatch_min_ts=excluded.next_dispatch_min_ts
`
markBackfillDispatchedQuery = `
UPDATE backfill_task SET dispatched_at=$4, completed_at=NULL, next_dispatch_min_ts=$5
WHERE bridge_id = $1 AND portal_id = $2 AND portal_receiver = $3
`
updateBackfillQueueQuery = `
UPDATE backfill_task
SET user_login_id=$4, batch_count=$5, is_done=$6, cursor=$7, oldest_message_id=$8,
dispatched_at=$9, completed_at=$10, next_dispatch_min_ts=$11
WHERE bridge_id = $1 AND portal_id = $2 AND portal_receiver = $3
`
markBackfillTaskNotDoneQuery = `
UPDATE backfill_task
SET is_done = false
WHERE bridge_id = $1 AND portal_id = $2 AND portal_receiver = $3 AND user_login_id = $4
`
getNextBackfillQuery = `
SELECT
bridge_id, portal_id, portal_receiver, user_login_id, batch_count, is_done,
cursor, oldest_message_id, dispatched_at, completed_at, next_dispatch_min_ts
FROM backfill_task
WHERE bridge_id = $1 AND next_dispatch_min_ts < $2 AND is_done = false AND user_login_id <> ''
ORDER BY next_dispatch_min_ts LIMIT 1
`
getNextBackfillQueryForPortal = `
SELECT
bridge_id, portal_id, portal_receiver, user_login_id, batch_count, is_done,
cursor, oldest_message_id, dispatched_at, completed_at, next_dispatch_min_ts
FROM backfill_task
WHERE bridge_id = $1 AND portal_id = $2 AND portal_receiver = $3 AND is_done = false AND user_login_id <> ''
`
deleteBackfillQueueQuery = `
DELETE FROM backfill_task
WHERE bridge_id = $1 AND portal_id = $2 AND portal_receiver = $3
`
)
func (btq *BackfillTaskQuery) EnsureExists(ctx context.Context, portal networkid.PortalKey, loginID networkid.UserLoginID) error {
return btq.Exec(ctx, ensureBackfillExistsQuery, btq.BridgeID, portal.ID, portal.Receiver, loginID, time.Now().UnixNano())
}
func (btq *BackfillTaskQuery) Upsert(ctx context.Context, bq *BackfillTask) error {
ensureBridgeIDMatches(&bq.BridgeID, btq.BridgeID)
return btq.Exec(ctx, upsertBackfillQueueQuery, bq.sqlVariables()...)
}
const UnfinishedBackfillBackoff = 1 * time.Hour
func (btq *BackfillTaskQuery) MarkDispatched(ctx context.Context, bq *BackfillTask) error {
ensureBridgeIDMatches(&bq.BridgeID, btq.BridgeID)
bq.DispatchedAt = time.Now()
bq.CompletedAt = time.Time{}
bq.NextDispatchMinTS = bq.DispatchedAt.Add(UnfinishedBackfillBackoff)
return btq.Exec(
ctx, markBackfillDispatchedQuery,
bq.BridgeID, bq.PortalKey.ID, bq.PortalKey.Receiver,
bq.DispatchedAt.UnixNano(), bq.NextDispatchMinTS.UnixNano(),
)
}
func (btq *BackfillTaskQuery) Update(ctx context.Context, bq *BackfillTask) error {
ensureBridgeIDMatches(&bq.BridgeID, btq.BridgeID)
return btq.Exec(ctx, updateBackfillQueueQuery, bq.sqlVariables()...)
}
func (btq *BackfillTaskQuery) MarkNotDone(ctx context.Context, portalKey networkid.PortalKey, userLoginID networkid.UserLoginID) error {
return btq.Exec(ctx, markBackfillTaskNotDoneQuery, btq.BridgeID, portalKey.ID, portalKey.Receiver, userLoginID)
}
func (btq *BackfillTaskQuery) GetNext(ctx context.Context) (*BackfillTask, error) {
return btq.QueryOne(ctx, getNextBackfillQuery, btq.BridgeID, time.Now().UnixNano())
}
func (btq *BackfillTaskQuery) GetNextForPortal(ctx context.Context, portalKey networkid.PortalKey) (*BackfillTask, error) {
return btq.QueryOne(ctx, getNextBackfillQueryForPortal, btq.BridgeID, portalKey.ID, portalKey.Receiver)
}
func (btq *BackfillTaskQuery) Delete(ctx context.Context, portalKey networkid.PortalKey) error {
return btq.Exec(ctx, deleteBackfillQueueQuery, btq.BridgeID, portalKey.ID, portalKey.Receiver)
}
func (bt *BackfillTask) Scan(row dbutil.Scannable) (*BackfillTask, error) {
var cursor, oldestMessageID sql.NullString
var dispatchedAt, completedAt, nextDispatchMinTS sql.NullInt64
err := row.Scan(
&bt.BridgeID, &bt.PortalKey.ID, &bt.PortalKey.Receiver, &bt.UserLoginID, &bt.BatchCount, &bt.IsDone,
&cursor, &oldestMessageID, &dispatchedAt, &completedAt, &nextDispatchMinTS)
if err != nil {
return nil, err
}
bt.Cursor = networkid.PaginationCursor(cursor.String)
bt.OldestMessageID = networkid.MessageID(oldestMessageID.String)
if dispatchedAt.Valid {
bt.DispatchedAt = time.Unix(0, dispatchedAt.Int64)
}
if completedAt.Valid {
bt.CompletedAt = time.Unix(0, completedAt.Int64)
}
if nextDispatchMinTS.Valid {
bt.NextDispatchMinTS = time.Unix(0, nextDispatchMinTS.Int64)
}
return bt, nil
}
func (bt *BackfillTask) sqlVariables() []any {
return []any{
bt.BridgeID, bt.PortalKey.ID, bt.PortalKey.Receiver, bt.UserLoginID, bt.BatchCount, bt.IsDone,
dbutil.StrPtr(bt.Cursor), dbutil.StrPtr(bt.OldestMessageID),
dbutil.ConvertedPtr(bt.DispatchedAt, time.Time.UnixNano),
dbutil.ConvertedPtr(bt.CompletedAt, time.Time.UnixNano),
bt.NextDispatchMinTS.UnixNano(),
}
}

View file

@ -0,0 +1,154 @@
// 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 database
import (
"go.mau.fi/util/dbutil"
"maunium.net/go/mautrix/bridgev2/networkid"
"maunium.net/go/mautrix/bridgev2/database/upgrades"
)
type Database struct {
*dbutil.Database
BridgeID networkid.BridgeID
Portal *PortalQuery
Ghost *GhostQuery
Message *MessageQuery
DisappearingMessage *DisappearingMessageQuery
Reaction *ReactionQuery
User *UserQuery
UserLogin *UserLoginQuery
UserPortal *UserPortalQuery
BackfillTask *BackfillTaskQuery
KV *KVQuery
PublicMedia *PublicMediaQuery
}
type MetaMerger interface {
CopyFrom(other any)
}
type MetaTypeCreator func() any
type MetaTypes struct {
Portal MetaTypeCreator
Ghost MetaTypeCreator
Message MetaTypeCreator
Reaction MetaTypeCreator
UserLogin MetaTypeCreator
}
type blankMeta struct{}
var blankMetaItem = &blankMeta{}
func blankMetaCreator() any {
return blankMetaItem
}
func New(bridgeID networkid.BridgeID, mt MetaTypes, db *dbutil.Database) *Database {
if mt.Portal == nil {
mt.Portal = blankMetaCreator
}
if mt.Ghost == nil {
mt.Ghost = blankMetaCreator
}
if mt.Message == nil {
mt.Message = blankMetaCreator
}
if mt.Reaction == nil {
mt.Reaction = blankMetaCreator
}
if mt.UserLogin == nil {
mt.UserLogin = blankMetaCreator
}
db.UpgradeTable = upgrades.Table
return &Database{
Database: db,
BridgeID: bridgeID,
Portal: &PortalQuery{
BridgeID: bridgeID,
MetaType: mt.Portal,
QueryHelper: dbutil.MakeQueryHelper(db, func(_ *dbutil.QueryHelper[*Portal]) *Portal {
return (&Portal{}).ensureHasMetadata(mt.Portal)
}),
},
Ghost: &GhostQuery{
BridgeID: bridgeID,
MetaType: mt.Ghost,
QueryHelper: dbutil.MakeQueryHelper(db, func(_ *dbutil.QueryHelper[*Ghost]) *Ghost {
return (&Ghost{}).ensureHasMetadata(mt.Ghost)
}),
},
Message: &MessageQuery{
BridgeID: bridgeID,
MetaType: mt.Message,
QueryHelper: dbutil.MakeQueryHelper(db, func(_ *dbutil.QueryHelper[*Message]) *Message {
return (&Message{}).ensureHasMetadata(mt.Message)
}),
},
DisappearingMessage: &DisappearingMessageQuery{
BridgeID: bridgeID,
QueryHelper: dbutil.MakeQueryHelper(db, func(_ *dbutil.QueryHelper[*DisappearingMessage]) *DisappearingMessage {
return &DisappearingMessage{}
}),
},
Reaction: &ReactionQuery{
BridgeID: bridgeID,
MetaType: mt.Reaction,
QueryHelper: dbutil.MakeQueryHelper(db, func(_ *dbutil.QueryHelper[*Reaction]) *Reaction {
return (&Reaction{}).ensureHasMetadata(mt.Reaction)
}),
},
User: &UserQuery{
BridgeID: bridgeID,
QueryHelper: dbutil.MakeQueryHelper(db, func(_ *dbutil.QueryHelper[*User]) *User {
return &User{}
}),
},
UserLogin: &UserLoginQuery{
BridgeID: bridgeID,
MetaType: mt.UserLogin,
QueryHelper: dbutil.MakeQueryHelper(db, func(_ *dbutil.QueryHelper[*UserLogin]) *UserLogin {
return (&UserLogin{}).ensureHasMetadata(mt.UserLogin)
}),
},
UserPortal: &UserPortalQuery{
BridgeID: bridgeID,
QueryHelper: dbutil.MakeQueryHelper(db, func(_ *dbutil.QueryHelper[*UserPortal]) *UserPortal {
return &UserPortal{}
}),
},
BackfillTask: &BackfillTaskQuery{
BridgeID: bridgeID,
QueryHelper: dbutil.MakeQueryHelper(db, func(_ *dbutil.QueryHelper[*BackfillTask]) *BackfillTask {
return &BackfillTask{}
}),
},
KV: &KVQuery{
BridgeID: bridgeID,
Database: db,
},
PublicMedia: &PublicMediaQuery{
BridgeID: bridgeID,
QueryHelper: dbutil.MakeQueryHelper(db, func(_ *dbutil.QueryHelper[*PublicMedia]) *PublicMedia {
return &PublicMedia{}
}),
},
}
}
func ensureBridgeIDMatches(ptr *networkid.BridgeID, expected networkid.BridgeID) {
if *ptr == "" {
*ptr = expected
} else if *ptr != expected {
panic("bridge ID mismatch")
}
}

View file

@ -0,0 +1,142 @@
// 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 database
import (
"context"
"database/sql"
"time"
"go.mau.fi/util/dbutil"
"go.mau.fi/util/jsontime"
"maunium.net/go/mautrix/bridgev2/networkid"
"maunium.net/go/mautrix/event"
"maunium.net/go/mautrix/id"
)
// Deprecated: use [event.DisappearingType]
type DisappearingType = event.DisappearingType
// Deprecated: use constants in event package
const (
DisappearingTypeNone = event.DisappearingTypeNone
DisappearingTypeAfterRead = event.DisappearingTypeAfterRead
DisappearingTypeAfterSend = event.DisappearingTypeAfterSend
)
// DisappearingSetting represents a disappearing message timer setting
// by combining a type with a timer and an optional start timestamp.
type DisappearingSetting struct {
Type event.DisappearingType
Timer time.Duration
DisappearAt time.Time
}
func DisappearingSettingFromEvent(evt *event.BeeperDisappearingTimer) DisappearingSetting {
if evt == nil || evt.Type == event.DisappearingTypeNone {
return DisappearingSetting{}
}
return DisappearingSetting{
Type: evt.Type,
Timer: evt.Timer.Duration,
}
}
func (ds DisappearingSetting) Normalize() DisappearingSetting {
if ds.Type == event.DisappearingTypeNone {
ds.Timer = 0
} else if ds.Timer == 0 {
ds.Type = event.DisappearingTypeNone
}
return ds
}
func (ds DisappearingSetting) StartingAt(start time.Time) DisappearingSetting {
ds.DisappearAt = start.Add(ds.Timer)
return ds
}
func (ds DisappearingSetting) ToEventContent() *event.BeeperDisappearingTimer {
if ds.Type == event.DisappearingTypeNone || ds.Timer == 0 {
return &event.BeeperDisappearingTimer{}
}
return &event.BeeperDisappearingTimer{
Type: ds.Type,
Timer: jsontime.MS(ds.Timer),
}
}
type DisappearingMessageQuery struct {
BridgeID networkid.BridgeID
*dbutil.QueryHelper[*DisappearingMessage]
}
type DisappearingMessage struct {
BridgeID networkid.BridgeID
RoomID id.RoomID
EventID id.EventID
Timestamp time.Time
DisappearingSetting
}
const (
upsertDisappearingMessageQuery = `
INSERT INTO disappearing_message (bridge_id, mx_room, mxid, timestamp, type, timer, disappear_at)
VALUES ($1, $2, $3, $4, $5, $6, $7)
ON CONFLICT (bridge_id, mxid) DO UPDATE SET timer=excluded.timer, disappear_at=excluded.disappear_at
`
startDisappearingMessagesQuery = `
UPDATE disappearing_message
SET disappear_at=$1 + timer
WHERE bridge_id=$2 AND mx_room=$3 AND disappear_at IS NULL AND type='after_read' AND timestamp<=$4
RETURNING bridge_id, mx_room, mxid, timestamp, type, timer, disappear_at
`
getUpcomingDisappearingMessagesQuery = `
SELECT bridge_id, mx_room, mxid, timestamp, type, timer, disappear_at
FROM disappearing_message WHERE bridge_id = $1 AND disappear_at IS NOT NULL AND disappear_at < $2
ORDER BY disappear_at LIMIT $3
`
deleteDisappearingMessageQuery = `
DELETE FROM disappearing_message WHERE bridge_id=$1 AND mxid=$2
`
)
func (dmq *DisappearingMessageQuery) Put(ctx context.Context, dm *DisappearingMessage) error {
ensureBridgeIDMatches(&dm.BridgeID, dmq.BridgeID)
return dmq.Exec(ctx, upsertDisappearingMessageQuery, dm.sqlVariables()...)
}
func (dmq *DisappearingMessageQuery) StartAllBefore(ctx context.Context, roomID id.RoomID, beforeTS time.Time) ([]*DisappearingMessage, error) {
return dmq.QueryMany(ctx, startDisappearingMessagesQuery, time.Now().UnixNano(), dmq.BridgeID, roomID, beforeTS.UnixNano())
}
func (dmq *DisappearingMessageQuery) GetUpcoming(ctx context.Context, duration time.Duration, limit int) ([]*DisappearingMessage, error) {
return dmq.QueryMany(ctx, getUpcomingDisappearingMessagesQuery, dmq.BridgeID, time.Now().Add(duration).UnixNano(), limit)
}
func (dmq *DisappearingMessageQuery) Delete(ctx context.Context, eventID id.EventID) error {
return dmq.Exec(ctx, deleteDisappearingMessageQuery, dmq.BridgeID, eventID)
}
func (d *DisappearingMessage) Scan(row dbutil.Scannable) (*DisappearingMessage, error) {
var timestamp int64
var disappearAt sql.NullInt64
err := row.Scan(&d.BridgeID, &d.RoomID, &d.EventID, &timestamp, &d.Type, &d.Timer, &disappearAt)
if err != nil {
return nil, err
}
if disappearAt.Valid {
d.DisappearAt = time.Unix(0, disappearAt.Int64)
}
d.Timestamp = time.Unix(0, timestamp)
return d, nil
}
func (d *DisappearingMessage) sqlVariables() []any {
return []any{d.BridgeID, d.RoomID, d.EventID, d.Timestamp.UnixNano(), d.Type, d.Timer, dbutil.ConvertedPtr(d.DisappearAt, time.Time.UnixNano)}
}

177
bridgev2/database/ghost.go Normal file
View file

@ -0,0 +1,177 @@
// 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 database
import (
"bytes"
"context"
"encoding/hex"
"encoding/json"
"fmt"
"go.mau.fi/util/dbutil"
"go.mau.fi/util/exerrors"
"maunium.net/go/mautrix/bridgev2/networkid"
"maunium.net/go/mautrix/crypto/canonicaljson"
"maunium.net/go/mautrix/id"
)
type GhostQuery struct {
BridgeID networkid.BridgeID
MetaType MetaTypeCreator
*dbutil.QueryHelper[*Ghost]
}
type ExtraProfile map[string]json.RawMessage
func (ep *ExtraProfile) Set(key string, value any) error {
if key == "displayname" || key == "avatar_url" {
return fmt.Errorf("cannot set reserved profile key %q", key)
}
marshaled, err := json.Marshal(value)
if err != nil {
return err
}
if *ep == nil {
*ep = make(ExtraProfile)
}
(*ep)[key] = canonicaljson.CanonicalJSONAssumeValid(marshaled)
return nil
}
func (ep *ExtraProfile) With(key string, value any) *ExtraProfile {
exerrors.PanicIfNotNil(ep.Set(key, value))
return ep
}
func canonicalizeIfObject(data json.RawMessage) json.RawMessage {
if len(data) > 0 && (data[0] == '{' || data[0] == '[') {
return canonicaljson.CanonicalJSONAssumeValid(data)
}
return data
}
func (ep *ExtraProfile) CopyTo(dest *ExtraProfile) (changed bool) {
if len(*ep) == 0 {
return
}
if *dest == nil {
*dest = make(ExtraProfile)
}
for key, val := range *ep {
if key == "displayname" || key == "avatar_url" {
continue
}
existing, exists := (*dest)[key]
if !exists || !bytes.Equal(canonicalizeIfObject(existing), val) {
(*dest)[key] = val
changed = true
}
}
return
}
type Ghost struct {
BridgeID networkid.BridgeID
ID networkid.UserID
Name string
AvatarID networkid.AvatarID
AvatarHash [32]byte
AvatarMXC id.ContentURIString
NameSet bool
AvatarSet bool
ContactInfoSet bool
IsBot bool
Identifiers []string
ExtraProfile ExtraProfile
Metadata any
}
const (
getGhostBaseQuery = `
SELECT bridge_id, id, name, avatar_id, avatar_hash, avatar_mxc,
name_set, avatar_set, contact_info_set, is_bot, identifiers, extra_profile, metadata
FROM ghost
`
getGhostByIDQuery = getGhostBaseQuery + `WHERE bridge_id=$1 AND id=$2`
getGhostByMetadataQuery = getGhostBaseQuery + `WHERE bridge_id=$1 AND metadata->>$2=$3`
insertGhostQuery = `
INSERT INTO ghost (
bridge_id, id, name, avatar_id, avatar_hash, avatar_mxc,
name_set, avatar_set, contact_info_set, is_bot, identifiers, extra_profile, metadata
)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13)
`
updateGhostQuery = `
UPDATE ghost SET name=$3, avatar_id=$4, avatar_hash=$5, avatar_mxc=$6,
name_set=$7, avatar_set=$8, contact_info_set=$9, is_bot=$10,
identifiers=$11, extra_profile=$12, metadata=$13
WHERE bridge_id=$1 AND id=$2
`
)
func (gq *GhostQuery) GetByID(ctx context.Context, id networkid.UserID) (*Ghost, error) {
return gq.QueryOne(ctx, getGhostByIDQuery, gq.BridgeID, id)
}
// GetByMetadata returns the ghosts whose metadata field at the given JSON key
// matches the given value.
func (gq *GhostQuery) GetByMetadata(ctx context.Context, key string, value any) ([]*Ghost, error) {
return gq.QueryMany(ctx, getGhostByMetadataQuery, gq.BridgeID, key, value)
}
func (gq *GhostQuery) Insert(ctx context.Context, ghost *Ghost) error {
ensureBridgeIDMatches(&ghost.BridgeID, gq.BridgeID)
return gq.Exec(ctx, insertGhostQuery, ghost.ensureHasMetadata(gq.MetaType).sqlVariables()...)
}
func (gq *GhostQuery) Update(ctx context.Context, ghost *Ghost) error {
ensureBridgeIDMatches(&ghost.BridgeID, gq.BridgeID)
return gq.Exec(ctx, updateGhostQuery, ghost.ensureHasMetadata(gq.MetaType).sqlVariables()...)
}
func (g *Ghost) Scan(row dbutil.Scannable) (*Ghost, error) {
var avatarHash string
err := row.Scan(
&g.BridgeID, &g.ID,
&g.Name, &g.AvatarID, &avatarHash, &g.AvatarMXC,
&g.NameSet, &g.AvatarSet, &g.ContactInfoSet, &g.IsBot,
dbutil.JSON{Data: &g.Identifiers}, dbutil.JSON{Data: &g.ExtraProfile}, dbutil.JSON{Data: g.Metadata},
)
if err != nil {
return nil, err
}
if avatarHash != "" {
data, _ := hex.DecodeString(avatarHash)
if len(data) == 32 {
g.AvatarHash = *(*[32]byte)(data)
}
}
return g, nil
}
func (g *Ghost) ensureHasMetadata(metaType MetaTypeCreator) *Ghost {
if g.Metadata == nil {
g.Metadata = metaType()
}
return g
}
func (g *Ghost) sqlVariables() []any {
var avatarHash string
if g.AvatarHash != [32]byte{} {
avatarHash = hex.EncodeToString(g.AvatarHash[:])
}
return []any{
g.BridgeID, g.ID,
g.Name, g.AvatarID, avatarHash, g.AvatarMXC,
g.NameSet, g.AvatarSet, g.ContactInfoSet, g.IsBot,
dbutil.JSON{Data: &g.Identifiers}, dbutil.JSON{Data: g.ExtraProfile}, dbutil.JSON{Data: g.Metadata},
}
}

View file

@ -0,0 +1,59 @@
// 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 database
import (
"context"
"database/sql"
"errors"
"github.com/rs/zerolog"
"go.mau.fi/util/dbutil"
"maunium.net/go/mautrix/bridgev2/networkid"
)
type Key string
const (
KeySplitPortalsEnabled Key = "split_portals_enabled"
KeyBridgeInfoVersion Key = "bridge_info_version"
KeyEncryptionStateResynced Key = "encryption_state_resynced"
KeyRecoveryKey Key = "recovery_key"
)
type KVQuery struct {
BridgeID networkid.BridgeID
*dbutil.Database
}
const (
getKVQuery = `SELECT value FROM kv_store WHERE bridge_id = $1 AND key = $2`
setKVQuery = `
INSERT INTO kv_store (bridge_id, key, value) VALUES ($1, $2, $3)
ON CONFLICT (bridge_id, key) DO UPDATE SET value = $3
`
)
func (kvq *KVQuery) Get(ctx context.Context, key Key) string {
var value string
err := kvq.QueryRow(ctx, getKVQuery, kvq.BridgeID, key).Scan(&value)
if err != nil && !errors.Is(err, sql.ErrNoRows) {
zerolog.Ctx(ctx).Err(err).Str("key", string(key)).Msg("Failed to get key from kvstore")
}
return value
}
func (kvq *KVQuery) Set(ctx context.Context, key Key, value string) {
_, err := kvq.Exec(ctx, setKVQuery, kvq.BridgeID, key, value)
if err != nil {
zerolog.Ctx(ctx).Err(err).
Str("key", string(key)).
Str("value", value).
Msg("Failed to set key in kvstore")
}
}

View file

@ -0,0 +1,334 @@
// 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 database
import (
"context"
"crypto/sha256"
"database/sql"
"encoding/base64"
"fmt"
"strings"
"sync"
"time"
"github.com/rs/zerolog"
"go.mau.fi/util/dbutil"
"maunium.net/go/mautrix/bridgev2/networkid"
"maunium.net/go/mautrix/id"
)
type MessageQuery struct {
BridgeID networkid.BridgeID
MetaType MetaTypeCreator
*dbutil.QueryHelper[*Message]
chunkDeleteLock sync.Mutex
}
type Message struct {
RowID int64
BridgeID networkid.BridgeID
ID networkid.MessageID
PartID networkid.PartID
MXID id.EventID
Room networkid.PortalKey
SenderID networkid.UserID
SenderMXID id.UserID
Timestamp time.Time
EditCount int
IsDoublePuppeted bool
ThreadRoot networkid.MessageID
ReplyTo networkid.MessageOptionalPartID
SendTxnID networkid.RawTransactionID
Metadata any
}
const (
getMessageBaseQuery = `
SELECT rowid, bridge_id, id, part_id, mxid, room_id, room_receiver, sender_id, sender_mxid,
timestamp, edit_count, double_puppeted, thread_root_id, reply_to_id, reply_to_part_id,
send_txn_id, metadata
FROM message
`
getAllMessagePartsByIDQuery = getMessageBaseQuery + `WHERE bridge_id=$1 AND (room_receiver=$2 OR room_receiver='') AND id=$3`
getMessagePartByIDQuery = getMessageBaseQuery + `WHERE bridge_id=$1 AND (room_receiver=$2 OR room_receiver='') AND id=$3 AND part_id=$4`
getMessagePartByRowIDQuery = getMessageBaseQuery + `WHERE bridge_id=$1 AND rowid=$2`
getMessageByMXIDQuery = getMessageBaseQuery + `WHERE bridge_id=$1 AND mxid=$2`
getMessageByTxnIDQuery = getMessageBaseQuery + `WHERE bridge_id=$1 AND (room_receiver=$2 OR room_receiver='') AND (mxid=$3 OR send_txn_id=$4)`
getLastMessagePartByIDQuery = getMessageBaseQuery + `WHERE bridge_id=$1 AND (room_receiver=$2 OR room_receiver='') AND id=$3 ORDER BY part_id DESC LIMIT 1`
getFirstMessagePartByIDQuery = getMessageBaseQuery + `WHERE bridge_id=$1 AND (room_receiver=$2 OR room_receiver='') AND id=$3 ORDER BY part_id ASC LIMIT 1`
getMessagesBetweenTimeQuery = getMessageBaseQuery + `WHERE bridge_id=$1 AND room_id=$2 AND room_receiver=$3 AND timestamp>$4 AND timestamp<=$5`
getOldestMessageInPortal = getMessageBaseQuery + `WHERE bridge_id=$1 AND room_id=$2 AND room_receiver=$3 ORDER BY timestamp ASC, part_id ASC LIMIT 1`
getFirstMessageInThread = getMessageBaseQuery + `WHERE bridge_id=$1 AND room_id=$2 AND room_receiver=$3 AND (id=$4 OR thread_root_id=$4) ORDER BY thread_root_id NULLS FIRST, timestamp ASC, part_id ASC LIMIT 1`
getLastMessageInThread = getMessageBaseQuery + `WHERE bridge_id=$1 AND room_id=$2 AND room_receiver=$3 AND (id=$4 OR thread_root_id=$4) ORDER BY thread_root_id NULLS LAST, timestamp DESC, part_id DESC LIMIT 1`
getLastNInPortal = getMessageBaseQuery + `WHERE bridge_id=$1 AND room_id=$2 AND room_receiver=$3 ORDER BY timestamp DESC, part_id DESC LIMIT $4`
getLastMessagePartAtOrBeforeTimeQuery = getMessageBaseQuery + `WHERE bridge_id = $1 AND room_id=$2 AND room_receiver=$3 AND timestamp<=$4 ORDER BY timestamp DESC, part_id DESC LIMIT 1`
getLastNonFakeMessagePartAtOrBeforeTimeQuery = getMessageBaseQuery + `WHERE bridge_id = $1 AND room_id=$2 AND room_receiver=$3 AND timestamp<=$4 AND mxid NOT LIKE '~fake:%' ORDER BY timestamp DESC, part_id DESC LIMIT 1`
countMessagesInPortalQuery = `
SELECT COUNT(*) FROM message WHERE bridge_id=$1 AND room_id=$2 AND room_receiver=$3
`
insertMessageQuery = `
INSERT INTO message (
bridge_id, id, part_id, mxid, room_id, room_receiver, sender_id, sender_mxid,
timestamp, edit_count, double_puppeted, thread_root_id, reply_to_id, reply_to_part_id,
send_txn_id, metadata
)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16)
RETURNING rowid
`
updateMessageQuery = `
UPDATE message SET id=$2, part_id=$3, mxid=$4, room_id=$5, room_receiver=$6, sender_id=$7, sender_mxid=$8,
timestamp=$9, edit_count=$10, double_puppeted=$11, thread_root_id=$12, reply_to_id=$13,
reply_to_part_id=$14, send_txn_id=$15, metadata=$16
WHERE bridge_id=$1 AND rowid=$17
`
deleteAllMessagePartsByIDQuery = `
DELETE FROM message WHERE bridge_id=$1 AND (room_receiver=$2 OR room_receiver='') AND id=$3
`
deleteMessagePartByRowIDQuery = `
DELETE FROM message WHERE bridge_id=$1 AND rowid=$2
`
deleteMessageChunkQuery = `
DELETE FROM message WHERE bridge_id=$1 AND room_id=$2 AND room_receiver=$3 AND rowid > $4 AND rowid <= $5
`
getMaxMessageRowIDQuery = `SELECT MAX(rowid) FROM message WHERE bridge_id=$1`
)
func (mq *MessageQuery) GetAllPartsByID(ctx context.Context, receiver networkid.UserLoginID, id networkid.MessageID) ([]*Message, error) {
return mq.QueryMany(ctx, getAllMessagePartsByIDQuery, mq.BridgeID, receiver, id)
}
func (mq *MessageQuery) GetPartByID(ctx context.Context, receiver networkid.UserLoginID, id networkid.MessageID, partID networkid.PartID) (*Message, error) {
return mq.QueryOne(ctx, getMessagePartByIDQuery, mq.BridgeID, receiver, id, partID)
}
func (mq *MessageQuery) GetPartByMXID(ctx context.Context, mxid id.EventID) (*Message, error) {
return mq.QueryOne(ctx, getMessageByMXIDQuery, mq.BridgeID, mxid)
}
func (mq *MessageQuery) GetPartByTxnID(ctx context.Context, receiver networkid.UserLoginID, mxid id.EventID, txnID networkid.RawTransactionID) (*Message, error) {
return mq.QueryOne(ctx, getMessageByTxnIDQuery, mq.BridgeID, receiver, mxid, txnID)
}
func (mq *MessageQuery) GetLastPartByID(ctx context.Context, receiver networkid.UserLoginID, id networkid.MessageID) (*Message, error) {
return mq.QueryOne(ctx, getLastMessagePartByIDQuery, mq.BridgeID, receiver, id)
}
func (mq *MessageQuery) GetFirstPartByID(ctx context.Context, receiver networkid.UserLoginID, id networkid.MessageID) (*Message, error) {
return mq.QueryOne(ctx, getFirstMessagePartByIDQuery, mq.BridgeID, receiver, id)
}
func (mq *MessageQuery) GetByRowID(ctx context.Context, rowID int64) (*Message, error) {
return mq.QueryOne(ctx, getMessagePartByRowIDQuery, mq.BridgeID, rowID)
}
func (mq *MessageQuery) GetFirstOrSpecificPartByID(ctx context.Context, receiver networkid.UserLoginID, id networkid.MessageOptionalPartID) (*Message, error) {
if id.PartID == nil {
return mq.GetFirstPartByID(ctx, receiver, id.MessageID)
} else {
return mq.GetPartByID(ctx, receiver, id.MessageID, *id.PartID)
}
}
func (mq *MessageQuery) GetLastPartAtOrBeforeTime(ctx context.Context, portal networkid.PortalKey, maxTS time.Time) (*Message, error) {
return mq.QueryOne(ctx, getLastMessagePartAtOrBeforeTimeQuery, mq.BridgeID, portal.ID, portal.Receiver, maxTS.UnixNano())
}
func (mq *MessageQuery) GetLastNonFakePartAtOrBeforeTime(ctx context.Context, portal networkid.PortalKey, maxTS time.Time) (*Message, error) {
return mq.QueryOne(ctx, getLastNonFakeMessagePartAtOrBeforeTimeQuery, mq.BridgeID, portal.ID, portal.Receiver, maxTS.UnixNano())
}
func (mq *MessageQuery) GetMessagesBetweenTimeQuery(ctx context.Context, portal networkid.PortalKey, start, end time.Time) ([]*Message, error) {
return mq.QueryMany(ctx, getMessagesBetweenTimeQuery, mq.BridgeID, portal.ID, portal.Receiver, start.UnixNano(), end.UnixNano())
}
func (mq *MessageQuery) GetFirstPortalMessage(ctx context.Context, portal networkid.PortalKey) (*Message, error) {
return mq.QueryOne(ctx, getOldestMessageInPortal, mq.BridgeID, portal.ID, portal.Receiver)
}
func (mq *MessageQuery) GetFirstThreadMessage(ctx context.Context, portal networkid.PortalKey, threadRoot networkid.MessageID) (*Message, error) {
return mq.QueryOne(ctx, getFirstMessageInThread, mq.BridgeID, portal.ID, portal.Receiver, threadRoot)
}
func (mq *MessageQuery) GetLastThreadMessage(ctx context.Context, portal networkid.PortalKey, threadRoot networkid.MessageID) (*Message, error) {
return mq.QueryOne(ctx, getLastMessageInThread, mq.BridgeID, portal.ID, portal.Receiver, threadRoot)
}
func (mq *MessageQuery) GetLastNInPortal(ctx context.Context, portal networkid.PortalKey, n int) ([]*Message, error) {
return mq.QueryMany(ctx, getLastNInPortal, mq.BridgeID, portal.ID, portal.Receiver, n)
}
func (mq *MessageQuery) Insert(ctx context.Context, msg *Message) error {
ensureBridgeIDMatches(&msg.BridgeID, mq.BridgeID)
return mq.GetDB().QueryRow(ctx, insertMessageQuery, msg.ensureHasMetadata(mq.MetaType).sqlVariables()...).Scan(&msg.RowID)
}
func (mq *MessageQuery) Update(ctx context.Context, msg *Message) error {
ensureBridgeIDMatches(&msg.BridgeID, mq.BridgeID)
return mq.Exec(ctx, updateMessageQuery, msg.ensureHasMetadata(mq.MetaType).updateSQLVariables()...)
}
func (mq *MessageQuery) DeleteAllParts(ctx context.Context, receiver networkid.UserLoginID, id networkid.MessageID) error {
return mq.Exec(ctx, deleteAllMessagePartsByIDQuery, mq.BridgeID, receiver, id)
}
func (mq *MessageQuery) Delete(ctx context.Context, rowID int64) error {
return mq.Exec(ctx, deleteMessagePartByRowIDQuery, mq.BridgeID, rowID)
}
func (mq *MessageQuery) deleteChunk(ctx context.Context, portal networkid.PortalKey, minRowID, maxRowID int64) (int64, error) {
res, err := mq.GetDB().Exec(ctx, deleteMessageChunkQuery, mq.BridgeID, portal.ID, portal.Receiver, minRowID, maxRowID)
if err != nil {
return 0, err
}
return res.RowsAffected()
}
func (mq *MessageQuery) getMaxRowID(ctx context.Context) (maxRowID int64, err error) {
err = mq.GetDB().QueryRow(ctx, getMaxMessageRowIDQuery, mq.BridgeID).Scan(&maxRowID)
return
}
const deleteChunkSize = 100_000
func (mq *MessageQuery) DeleteInChunks(ctx context.Context, portal networkid.PortalKey) error {
if mq.GetDB().Dialect != dbutil.SQLite {
return nil
}
log := zerolog.Ctx(ctx).With().
Str("action", "delete messages in chunks").
Stringer("portal_key", portal).
Logger()
if !mq.chunkDeleteLock.TryLock() {
log.Warn().Msg("Portal deletion lock is being held, waiting...")
mq.chunkDeleteLock.Lock()
log.Debug().Msg("Acquired portal deletion lock after waiting")
}
defer mq.chunkDeleteLock.Unlock()
total, err := mq.CountMessagesInPortal(ctx, portal)
if err != nil {
return fmt.Errorf("failed to count messages in portal: %w", err)
} else if total < deleteChunkSize/3 {
return nil
}
globalMaxRowID, err := mq.getMaxRowID(ctx)
if err != nil {
return fmt.Errorf("failed to get max row ID: %w", err)
}
log.Debug().
Int("total_count", total).
Int64("global_max_row_id", globalMaxRowID).
Msg("Portal has lots of messages, deleting in chunks to avoid database locks")
maxRowID := int64(deleteChunkSize)
globalMaxRowID += deleteChunkSize * 1.2
var dbTimeUsed time.Duration
globalStart := time.Now()
for total > 500 && maxRowID < globalMaxRowID {
start := time.Now()
count, err := mq.deleteChunk(ctx, portal, maxRowID-deleteChunkSize, maxRowID)
duration := time.Since(start)
dbTimeUsed += duration
if err != nil {
return fmt.Errorf("failed to delete chunk of messages before %d: %w", maxRowID, err)
}
total -= int(count)
maxRowID += deleteChunkSize
sleepTime := max(10*time.Millisecond, min(250*time.Millisecond, time.Duration(count/100)*time.Millisecond))
log.Debug().
Int64("max_row_id", maxRowID).
Int64("deleted_count", count).
Int("remaining_count", total).
Dur("duration", duration).
Dur("sleep_time", sleepTime).
Msg("Deleted chunk of messages")
select {
case <-time.After(sleepTime):
case <-ctx.Done():
return ctx.Err()
}
}
log.Debug().
Int("remaining_count", total).
Dur("db_time_used", dbTimeUsed).
Dur("total_duration", time.Since(globalStart)).
Msg("Finished chunked delete of messages in portal")
return nil
}
func (mq *MessageQuery) CountMessagesInPortal(ctx context.Context, key networkid.PortalKey) (count int, err error) {
err = mq.GetDB().QueryRow(ctx, countMessagesInPortalQuery, mq.BridgeID, key.ID, key.Receiver).Scan(&count)
return
}
func (m *Message) Scan(row dbutil.Scannable) (*Message, error) {
var timestamp int64
var threadRootID, replyToID, replyToPartID, sendTxnID sql.NullString
var doublePuppeted sql.NullBool
err := row.Scan(
&m.RowID, &m.BridgeID, &m.ID, &m.PartID, &m.MXID, &m.Room.ID, &m.Room.Receiver, &m.SenderID, &m.SenderMXID,
&timestamp, &m.EditCount, &doublePuppeted, &threadRootID, &replyToID, &replyToPartID, &sendTxnID,
dbutil.JSON{Data: m.Metadata},
)
if err != nil {
return nil, err
}
m.Timestamp = time.Unix(0, timestamp)
m.ThreadRoot = networkid.MessageID(threadRootID.String)
m.IsDoublePuppeted = doublePuppeted.Valid
if replyToID.Valid {
m.ReplyTo.MessageID = networkid.MessageID(replyToID.String)
if replyToPartID.Valid {
m.ReplyTo.PartID = (*networkid.PartID)(&replyToPartID.String)
}
}
if sendTxnID.Valid {
m.SendTxnID = networkid.RawTransactionID(sendTxnID.String)
}
return m, nil
}
func (m *Message) ensureHasMetadata(metaType MetaTypeCreator) *Message {
if m.Metadata == nil {
m.Metadata = metaType()
}
return m
}
func (m *Message) sqlVariables() []any {
return []any{
m.BridgeID, m.ID, m.PartID, m.MXID, m.Room.ID, m.Room.Receiver, m.SenderID, m.SenderMXID,
m.Timestamp.UnixNano(), m.EditCount, m.IsDoublePuppeted, dbutil.StrPtr(m.ThreadRoot),
dbutil.StrPtr(m.ReplyTo.MessageID), m.ReplyTo.PartID, dbutil.StrPtr(m.SendTxnID),
dbutil.JSON{Data: m.Metadata},
}
}
func (m *Message) updateSQLVariables() []any {
return append(m.sqlVariables(), m.RowID)
}
const FakeMXIDPrefix = "~fake:"
const TxnMXIDPrefix = "~txn:"
const NetworkTxnMXIDPrefix = TxnMXIDPrefix + "network:"
const RandomTxnMXIDPrefix = TxnMXIDPrefix + "random:"
func (m *Message) SetFakeMXID() {
hash := sha256.Sum256([]byte(m.ID))
m.MXID = id.EventID(FakeMXIDPrefix + base64.RawURLEncoding.EncodeToString(hash[:]))
}
func (m *Message) HasFakeMXID() bool {
return strings.HasPrefix(m.MXID.String(), FakeMXIDPrefix)
}

296
bridgev2/database/portal.go Normal file
View file

@ -0,0 +1,296 @@
// 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 database
import (
"context"
"database/sql"
"encoding/hex"
"errors"
"time"
"go.mau.fi/util/dbutil"
"maunium.net/go/mautrix/bridgev2/networkid"
"maunium.net/go/mautrix/event"
"maunium.net/go/mautrix/id"
)
type RoomType string
const (
RoomTypeDefault RoomType = ""
RoomTypeDM RoomType = "dm"
RoomTypeGroupDM RoomType = "group_dm"
RoomTypeSpace RoomType = "space"
)
type PortalQuery struct {
BridgeID networkid.BridgeID
MetaType MetaTypeCreator
*dbutil.QueryHelper[*Portal]
}
type CapStateFlags uint32
func (csf CapStateFlags) Has(flag CapStateFlags) bool {
return csf&flag != 0
}
const (
CapStateFlagDisappearingTimerSet CapStateFlags = 1 << iota
)
type CapabilityState struct {
Source networkid.UserLoginID `json:"source"`
ID string `json:"id"`
Flags CapStateFlags `json:"flags"`
}
type Portal struct {
BridgeID networkid.BridgeID
networkid.PortalKey
MXID id.RoomID
ParentKey networkid.PortalKey
RelayLoginID networkid.UserLoginID
OtherUserID networkid.UserID
Name string
Topic string
AvatarID networkid.AvatarID
AvatarHash [32]byte
AvatarMXC id.ContentURIString
NameSet bool
TopicSet bool
AvatarSet bool
NameIsCustom bool
InSpace bool
MessageRequest bool
RoomType RoomType
Disappear DisappearingSetting
CapState CapabilityState
Metadata any
}
const (
getPortalBaseQuery = `
SELECT bridge_id, id, receiver, mxid, parent_id, parent_receiver, relay_login_id, other_user_id,
name, topic, avatar_id, avatar_hash, avatar_mxc,
name_set, topic_set, avatar_set, name_is_custom, in_space, message_request,
room_type, disappear_type, disappear_timer, cap_state,
metadata
FROM portal
`
getPortalByKeyQuery = getPortalBaseQuery + `WHERE bridge_id=$1 AND id=$2 AND receiver=$3`
getPortalByIDWithUncertainReceiverQuery = getPortalBaseQuery + `WHERE bridge_id=$1 AND id=$2 AND (receiver=$3 OR receiver='')`
getPortalByMXIDQuery = getPortalBaseQuery + `WHERE bridge_id=$1 AND mxid=$2`
getAllPortalsWithMXIDQuery = getPortalBaseQuery + `WHERE bridge_id=$1 AND mxid IS NOT NULL`
getAllPortalsWithoutReceiver = getPortalBaseQuery + `WHERE bridge_id=$1 AND (receiver='' OR (parent_id<>'' AND parent_receiver='')) ORDER BY parent_id DESC`
getAllDMPortalsQuery = getPortalBaseQuery + `WHERE bridge_id=$1 AND room_type='dm' AND other_user_id=$2`
getDMPortalQuery = getPortalBaseQuery + `WHERE bridge_id=$1 AND room_type='dm' AND receiver=$2 AND other_user_id=$3`
getAllPortalsQuery = getPortalBaseQuery + `WHERE bridge_id=$1`
getChildPortalsQuery = getPortalBaseQuery + `WHERE bridge_id=$1 AND parent_id=$2 AND parent_receiver=$3`
findPortalReceiverQuery = `SELECT id, receiver FROM portal WHERE bridge_id=$1 AND id=$2 AND (receiver=$3 OR receiver='') LIMIT 1`
insertPortalQuery = `
INSERT INTO portal (
bridge_id, id, receiver, mxid,
parent_id, parent_receiver, relay_login_id, other_user_id,
name, topic, avatar_id, avatar_hash, avatar_mxc,
name_set, avatar_set, topic_set, name_is_custom, in_space, message_request,
room_type, disappear_type, disappear_timer, cap_state,
metadata, relay_bridge_id
) VALUES (
$1, $2, $3, $4, $5, $6, cast($7 AS TEXT), $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22, $23, $24,
CASE WHEN cast($7 AS TEXT) IS NULL THEN NULL ELSE $1 END
)
`
updatePortalQuery = `
UPDATE portal
SET mxid=$4, parent_id=$5, parent_receiver=$6,
relay_login_id=cast($7 AS TEXT), relay_bridge_id=CASE WHEN cast($7 AS TEXT) IS NULL THEN NULL ELSE bridge_id END,
other_user_id=$8, name=$9, topic=$10, avatar_id=$11, avatar_hash=$12, avatar_mxc=$13,
name_set=$14, avatar_set=$15, topic_set=$16, name_is_custom=$17, in_space=$18, message_request=$19,
room_type=$20, disappear_type=$21, disappear_timer=$22, cap_state=$23, metadata=$24
WHERE bridge_id=$1 AND id=$2 AND receiver=$3
`
deletePortalQuery = `
DELETE FROM portal
WHERE bridge_id=$1 AND id=$2 AND receiver=$3
`
reIDPortalQuery = `UPDATE portal SET id=$4, receiver=$5 WHERE bridge_id=$1 AND id=$2 AND receiver=$3`
migrateToSplitPortalsQuery = `
UPDATE portal
SET receiver=new_receiver
FROM (
SELECT bridge_id, id, COALESCE((
SELECT login_id
FROM user_portal
WHERE bridge_id=portal.bridge_id AND portal_id=portal.id AND portal_receiver=''
LIMIT 1
), (
SELECT login_id
FROM user_portal
WHERE portal.parent_id<>'' AND bridge_id=portal.bridge_id AND portal_id=portal.parent_id
LIMIT 1
), (
SELECT id FROM user_login WHERE bridge_id=portal.bridge_id LIMIT 1
), '') AS new_receiver
FROM portal
WHERE receiver='' AND bridge_id=$1
) updates
WHERE portal.bridge_id=updates.bridge_id AND portal.id=updates.id AND portal.receiver='' AND NOT EXISTS (
SELECT 1 FROM portal p2 WHERE p2.bridge_id=updates.bridge_id AND p2.id=updates.id AND p2.receiver=updates.new_receiver
)
`
fixParentsAfterSplitPortalMigrationQuery = `
UPDATE portal
SET parent_receiver=receiver
WHERE bridge_id=$1 AND parent_receiver='' AND receiver<>'' AND parent_id<>''
AND EXISTS(SELECT 1 FROM portal pp WHERE pp.bridge_id=$1 AND pp.id=portal.parent_id AND pp.receiver=portal.receiver);
`
)
func (pq *PortalQuery) GetByKey(ctx context.Context, key networkid.PortalKey) (*Portal, error) {
return pq.QueryOne(ctx, getPortalByKeyQuery, pq.BridgeID, key.ID, key.Receiver)
}
func (pq *PortalQuery) FindReceiver(ctx context.Context, id networkid.PortalID, maybeReceiver networkid.UserLoginID) (key networkid.PortalKey, err error) {
err = pq.GetDB().QueryRow(ctx, findPortalReceiverQuery, pq.BridgeID, id, maybeReceiver).Scan(&key.ID, &key.Receiver)
if errors.Is(err, sql.ErrNoRows) {
err = nil
}
return
}
func (pq *PortalQuery) GetByIDWithUncertainReceiver(ctx context.Context, key networkid.PortalKey) (*Portal, error) {
return pq.QueryOne(ctx, getPortalByIDWithUncertainReceiverQuery, pq.BridgeID, key.ID, key.Receiver)
}
func (pq *PortalQuery) GetByMXID(ctx context.Context, mxid id.RoomID) (*Portal, error) {
return pq.QueryOne(ctx, getPortalByMXIDQuery, pq.BridgeID, mxid)
}
func (pq *PortalQuery) GetAllWithMXID(ctx context.Context) ([]*Portal, error) {
return pq.QueryMany(ctx, getAllPortalsWithMXIDQuery, pq.BridgeID)
}
func (pq *PortalQuery) GetAllWithoutReceiver(ctx context.Context) ([]*Portal, error) {
return pq.QueryMany(ctx, getAllPortalsWithoutReceiver, pq.BridgeID)
}
func (pq *PortalQuery) GetAll(ctx context.Context) ([]*Portal, error) {
return pq.QueryMany(ctx, getAllPortalsQuery, pq.BridgeID)
}
func (pq *PortalQuery) GetAllDMsWith(ctx context.Context, otherUserID networkid.UserID) ([]*Portal, error) {
return pq.QueryMany(ctx, getAllDMPortalsQuery, pq.BridgeID, otherUserID)
}
func (pq *PortalQuery) GetDM(ctx context.Context, receiver networkid.UserLoginID, otherUserID networkid.UserID) (*Portal, error) {
return pq.QueryOne(ctx, getDMPortalQuery, pq.BridgeID, receiver, otherUserID)
}
func (pq *PortalQuery) GetChildren(ctx context.Context, parentKey networkid.PortalKey) ([]*Portal, error) {
return pq.QueryMany(ctx, getChildPortalsQuery, pq.BridgeID, parentKey.ID, parentKey.Receiver)
}
func (pq *PortalQuery) ReID(ctx context.Context, oldID, newID networkid.PortalKey) error {
return pq.Exec(ctx, reIDPortalQuery, pq.BridgeID, oldID.ID, oldID.Receiver, newID.ID, newID.Receiver)
}
func (pq *PortalQuery) Insert(ctx context.Context, p *Portal) error {
ensureBridgeIDMatches(&p.BridgeID, pq.BridgeID)
return pq.Exec(ctx, insertPortalQuery, p.ensureHasMetadata(pq.MetaType).sqlVariables()...)
}
func (pq *PortalQuery) Update(ctx context.Context, p *Portal) error {
ensureBridgeIDMatches(&p.BridgeID, pq.BridgeID)
return pq.Exec(ctx, updatePortalQuery, p.ensureHasMetadata(pq.MetaType).sqlVariables()...)
}
func (pq *PortalQuery) Delete(ctx context.Context, key networkid.PortalKey) error {
return pq.Exec(ctx, deletePortalQuery, pq.BridgeID, key.ID, key.Receiver)
}
func (pq *PortalQuery) MigrateToSplitPortals(ctx context.Context) (int64, error) {
res, err := pq.GetDB().Exec(ctx, migrateToSplitPortalsQuery, pq.BridgeID)
if err != nil {
return 0, err
}
return res.RowsAffected()
}
func (pq *PortalQuery) FixParentsAfterSplitPortalMigration(ctx context.Context) (int64, error) {
res, err := pq.GetDB().Exec(ctx, fixParentsAfterSplitPortalMigrationQuery, pq.BridgeID)
if err != nil {
return 0, err
}
return res.RowsAffected()
}
func (p *Portal) Scan(row dbutil.Scannable) (*Portal, error) {
var mxid, parentID, parentReceiver, relayLoginID, otherUserID, disappearType sql.NullString
var disappearTimer sql.NullInt64
var avatarHash string
err := row.Scan(
&p.BridgeID, &p.ID, &p.Receiver, &mxid,
&parentID, &parentReceiver, &relayLoginID, &otherUserID,
&p.Name, &p.Topic, &p.AvatarID, &avatarHash, &p.AvatarMXC,
&p.NameSet, &p.TopicSet, &p.AvatarSet, &p.NameIsCustom, &p.InSpace, &p.MessageRequest,
&p.RoomType, &disappearType, &disappearTimer,
dbutil.JSON{Data: &p.CapState}, dbutil.JSON{Data: p.Metadata},
)
if err != nil {
return nil, err
}
if avatarHash != "" {
data, _ := hex.DecodeString(avatarHash)
if len(data) == 32 {
p.AvatarHash = *(*[32]byte)(data)
}
}
if disappearType.Valid {
p.Disappear = DisappearingSetting{
Type: event.DisappearingType(disappearType.String),
Timer: time.Duration(disappearTimer.Int64),
}
}
p.MXID = id.RoomID(mxid.String)
p.OtherUserID = networkid.UserID(otherUserID.String)
if parentID.Valid {
p.ParentKey = networkid.PortalKey{
ID: networkid.PortalID(parentID.String),
Receiver: networkid.UserLoginID(parentReceiver.String),
}
}
p.RelayLoginID = networkid.UserLoginID(relayLoginID.String)
return p, nil
}
func (p *Portal) ensureHasMetadata(metaType MetaTypeCreator) *Portal {
if p.Metadata == nil {
p.Metadata = metaType()
}
return p
}
func (p *Portal) sqlVariables() []any {
var avatarHash string
if p.AvatarHash != [32]byte{} {
avatarHash = hex.EncodeToString(p.AvatarHash[:])
}
return []any{
p.BridgeID, p.ID, p.Receiver, dbutil.StrPtr(p.MXID),
dbutil.StrPtr(p.ParentKey.ID), p.ParentKey.Receiver, dbutil.StrPtr(p.RelayLoginID), dbutil.StrPtr(p.OtherUserID),
p.Name, p.Topic, p.AvatarID, avatarHash, p.AvatarMXC,
p.NameSet, p.TopicSet, p.AvatarSet, p.NameIsCustom, p.InSpace, p.MessageRequest,
p.RoomType, dbutil.StrPtr(p.Disappear.Type), dbutil.NumPtr(p.Disappear.Timer),
dbutil.JSON{Data: p.CapState}, dbutil.JSON{Data: p.Metadata},
}
}

View file

@ -0,0 +1,72 @@
// Copyright (c) 2025 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 database
import (
"context"
"database/sql"
"time"
"go.mau.fi/util/dbutil"
"maunium.net/go/mautrix/bridgev2/networkid"
"maunium.net/go/mautrix/crypto/attachment"
"maunium.net/go/mautrix/id"
)
type PublicMediaQuery struct {
BridgeID networkid.BridgeID
*dbutil.QueryHelper[*PublicMedia]
}
type PublicMedia struct {
BridgeID networkid.BridgeID
PublicID string
MXC id.ContentURI
Keys *attachment.EncryptedFile
MimeType string
Expiry time.Time
}
const (
upsertPublicMediaQuery = `
INSERT INTO public_media (bridge_id, public_id, mxc, keys, mimetype, expiry)
VALUES ($1, $2, $3, $4, $5, $6)
ON CONFLICT (bridge_id, public_id) DO UPDATE SET expiry=EXCLUDED.expiry
`
getPublicMediaQuery = `
SELECT bridge_id, public_id, mxc, keys, mimetype, expiry
FROM public_media WHERE bridge_id=$1 AND public_id=$2
`
)
func (pmq *PublicMediaQuery) Put(ctx context.Context, pm *PublicMedia) error {
ensureBridgeIDMatches(&pm.BridgeID, pmq.BridgeID)
return pmq.Exec(ctx, upsertPublicMediaQuery, pm.sqlVariables()...)
}
func (pmq *PublicMediaQuery) Get(ctx context.Context, publicID string) (*PublicMedia, error) {
return pmq.QueryOne(ctx, getPublicMediaQuery, pmq.BridgeID, publicID)
}
func (pm *PublicMedia) Scan(row dbutil.Scannable) (*PublicMedia, error) {
var expiry sql.NullInt64
var mimetype sql.NullString
err := row.Scan(&pm.BridgeID, &pm.PublicID, &pm.MXC, dbutil.JSON{Data: &pm.Keys}, &mimetype, &expiry)
if err != nil {
return nil, err
}
if expiry.Valid {
pm.Expiry = time.Unix(0, expiry.Int64)
}
pm.MimeType = mimetype.String
return pm, nil
}
func (pm *PublicMedia) sqlVariables() []any {
return []any{pm.BridgeID, pm.PublicID, &pm.MXC, dbutil.JSONPtr(pm.Keys), dbutil.StrPtr(pm.MimeType), dbutil.ConvertedPtr(pm.Expiry, time.Time.UnixNano)}
}

View file

@ -0,0 +1,120 @@
// 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 database
import (
"context"
"time"
"go.mau.fi/util/dbutil"
"maunium.net/go/mautrix/bridgev2/networkid"
"maunium.net/go/mautrix/id"
)
type ReactionQuery struct {
BridgeID networkid.BridgeID
MetaType MetaTypeCreator
*dbutil.QueryHelper[*Reaction]
}
type Reaction struct {
BridgeID networkid.BridgeID
Room networkid.PortalKey
MessageID networkid.MessageID
MessagePartID networkid.PartID
SenderID networkid.UserID
SenderMXID id.UserID
EmojiID networkid.EmojiID
MXID id.EventID
Timestamp time.Time
Emoji string
Metadata any
}
const (
getReactionBaseQuery = `
SELECT bridge_id, message_id, message_part_id, sender_id, sender_mxid, emoji_id, emoji, room_id, room_receiver, mxid, timestamp, metadata FROM reaction
`
getReactionByIDQuery = getReactionBaseQuery + `WHERE bridge_id=$1 AND room_receiver=$2 AND message_id=$3 AND message_part_id=$4 AND sender_id=$5 AND emoji_id=$6`
getReactionByIDWithoutMessagePartQuery = getReactionBaseQuery + `WHERE bridge_id=$1 AND room_receiver=$2 AND message_id=$3 AND sender_id=$4 AND emoji_id=$5 ORDER BY message_part_id ASC LIMIT 1`
getAllReactionsToMessageBySenderQuery = getReactionBaseQuery + `WHERE bridge_id=$1 AND room_receiver=$2 AND message_id=$3 AND sender_id=$4 ORDER BY timestamp DESC`
getAllReactionsToMessageQuery = getReactionBaseQuery + `WHERE bridge_id=$1 AND room_receiver=$2 AND message_id=$3`
getAllReactionsToMessagePartQuery = getReactionBaseQuery + `WHERE bridge_id=$1 AND room_receiver=$2 AND message_id=$3 AND message_part_id=$4`
getReactionByMXIDQuery = getReactionBaseQuery + `WHERE bridge_id=$1 AND mxid=$2`
upsertReactionQuery = `
INSERT INTO reaction (bridge_id, message_id, message_part_id, sender_id, sender_mxid, emoji_id, emoji, room_id, room_receiver, mxid, timestamp, metadata)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12)
ON CONFLICT (bridge_id, room_receiver, message_id, message_part_id, sender_id, emoji_id)
DO UPDATE SET sender_mxid=excluded.sender_mxid, mxid=excluded.mxid, timestamp=excluded.timestamp, emoji=excluded.emoji, metadata=excluded.metadata
`
deleteReactionQuery = `
DELETE FROM reaction WHERE bridge_id=$1 AND room_receiver=$2 AND message_id=$3 AND message_part_id=$4 AND sender_id=$5 AND emoji_id=$6
`
)
func (rq *ReactionQuery) GetByID(ctx context.Context, receiver networkid.UserLoginID, messageID networkid.MessageID, messagePartID networkid.PartID, senderID networkid.UserID, emojiID networkid.EmojiID) (*Reaction, error) {
return rq.QueryOne(ctx, getReactionByIDQuery, rq.BridgeID, receiver, messageID, messagePartID, senderID, emojiID)
}
func (rq *ReactionQuery) GetByIDWithoutMessagePart(ctx context.Context, receiver networkid.UserLoginID, messageID networkid.MessageID, senderID networkid.UserID, emojiID networkid.EmojiID) (*Reaction, error) {
return rq.QueryOne(ctx, getReactionByIDWithoutMessagePartQuery, rq.BridgeID, receiver, messageID, senderID, emojiID)
}
func (rq *ReactionQuery) GetAllToMessageBySender(ctx context.Context, receiver networkid.UserLoginID, messageID networkid.MessageID, senderID networkid.UserID) ([]*Reaction, error) {
return rq.QueryMany(ctx, getAllReactionsToMessageBySenderQuery, rq.BridgeID, receiver, messageID, senderID)
}
func (rq *ReactionQuery) GetAllToMessage(ctx context.Context, receiver networkid.UserLoginID, messageID networkid.MessageID) ([]*Reaction, error) {
return rq.QueryMany(ctx, getAllReactionsToMessageQuery, rq.BridgeID, receiver, messageID)
}
func (rq *ReactionQuery) GetAllToMessagePart(ctx context.Context, receiver networkid.UserLoginID, messageID networkid.MessageID, partID networkid.PartID) ([]*Reaction, error) {
return rq.QueryMany(ctx, getAllReactionsToMessagePartQuery, rq.BridgeID, receiver, messageID, partID)
}
func (rq *ReactionQuery) GetByMXID(ctx context.Context, mxid id.EventID) (*Reaction, error) {
return rq.QueryOne(ctx, getReactionByMXIDQuery, rq.BridgeID, mxid)
}
func (rq *ReactionQuery) Upsert(ctx context.Context, reaction *Reaction) error {
ensureBridgeIDMatches(&reaction.BridgeID, rq.BridgeID)
return rq.Exec(ctx, upsertReactionQuery, reaction.ensureHasMetadata(rq.MetaType).sqlVariables()...)
}
func (rq *ReactionQuery) Delete(ctx context.Context, reaction *Reaction) error {
ensureBridgeIDMatches(&reaction.BridgeID, rq.BridgeID)
return rq.Exec(ctx, deleteReactionQuery, reaction.BridgeID, reaction.Room.Receiver, reaction.MessageID, reaction.MessagePartID, reaction.SenderID, reaction.EmojiID)
}
func (r *Reaction) Scan(row dbutil.Scannable) (*Reaction, error) {
var timestamp int64
err := row.Scan(
&r.BridgeID, &r.MessageID, &r.MessagePartID, &r.SenderID, &r.SenderMXID, &r.EmojiID, &r.Emoji,
&r.Room.ID, &r.Room.Receiver, &r.MXID, &timestamp, dbutil.JSON{Data: r.Metadata},
)
if err != nil {
return nil, err
}
r.Timestamp = time.Unix(0, timestamp)
return r, nil
}
func (r *Reaction) ensureHasMetadata(metaType MetaTypeCreator) *Reaction {
if r.Metadata == nil {
r.Metadata = metaType()
}
return r
}
func (r *Reaction) sqlVariables() []any {
return []any{
r.BridgeID, r.MessageID, r.MessagePartID, r.SenderID, r.SenderMXID, r.EmojiID, r.Emoji,
r.Room.ID, r.Room.Receiver, r.MXID, r.Timestamp.UnixNano(), dbutil.JSON{Data: r.Metadata},
}
}

View file

@ -0,0 +1,233 @@
-- v0 -> v27 (compatible with v9+): Latest revision
CREATE TABLE "user" (
bridge_id TEXT NOT NULL,
mxid TEXT NOT NULL,
management_room TEXT,
access_token TEXT,
PRIMARY KEY (bridge_id, mxid)
);
CREATE TABLE user_login (
bridge_id TEXT NOT NULL,
user_mxid TEXT NOT NULL,
id TEXT NOT NULL,
remote_name TEXT NOT NULL,
remote_profile jsonb,
space_room TEXT,
metadata jsonb NOT NULL,
PRIMARY KEY (bridge_id, id),
CONSTRAINT user_login_user_fkey FOREIGN KEY (bridge_id, user_mxid)
REFERENCES "user" (bridge_id, mxid)
ON DELETE CASCADE ON UPDATE CASCADE
);
CREATE TABLE portal (
bridge_id TEXT NOT NULL,
id TEXT NOT NULL,
receiver TEXT NOT NULL,
mxid TEXT,
parent_id TEXT,
parent_receiver TEXT NOT NULL DEFAULT '',
relay_bridge_id TEXT,
relay_login_id TEXT,
other_user_id TEXT,
name TEXT NOT NULL,
topic TEXT NOT NULL,
avatar_id TEXT NOT NULL,
avatar_hash TEXT NOT NULL,
avatar_mxc TEXT NOT NULL,
name_set BOOLEAN NOT NULL,
avatar_set BOOLEAN NOT NULL,
topic_set BOOLEAN NOT NULL,
name_is_custom BOOLEAN NOT NULL DEFAULT false,
in_space BOOLEAN NOT NULL,
message_request BOOLEAN NOT NULL DEFAULT false,
room_type TEXT NOT NULL,
disappear_type TEXT,
disappear_timer BIGINT,
cap_state jsonb,
metadata jsonb NOT NULL,
PRIMARY KEY (bridge_id, id, receiver),
CONSTRAINT portal_parent_fkey FOREIGN KEY (bridge_id, parent_id, parent_receiver)
-- Deletes aren't allowed to cascade here:
-- children should be re-parented or cleaned up manually
REFERENCES portal (bridge_id, id, receiver) ON UPDATE CASCADE,
CONSTRAINT portal_relay_fkey FOREIGN KEY (relay_bridge_id, relay_login_id)
REFERENCES user_login (bridge_id, id)
ON DELETE SET NULL ON UPDATE CASCADE
);
CREATE UNIQUE INDEX portal_bridge_mxid_idx ON portal (bridge_id, mxid);
CREATE INDEX portal_parent_idx ON portal (bridge_id, parent_id, parent_receiver);
CREATE TABLE ghost (
bridge_id TEXT NOT NULL,
id TEXT NOT NULL,
name TEXT NOT NULL,
avatar_id TEXT NOT NULL,
avatar_hash TEXT NOT NULL,
avatar_mxc TEXT NOT NULL,
name_set BOOLEAN NOT NULL,
avatar_set BOOLEAN NOT NULL,
contact_info_set BOOLEAN NOT NULL,
is_bot BOOLEAN NOT NULL,
identifiers jsonb NOT NULL,
extra_profile jsonb,
metadata jsonb NOT NULL,
PRIMARY KEY (bridge_id, id)
);
CREATE TABLE message (
-- Messages have an extra rowid to allow a single relates_to column with ON DELETE SET NULL
-- If the foreign key used (bridge_id, relates_to), then deleting the target column
-- would try to set bridge_id to null as well.
-- only: sqlite (line commented)
-- rowid INTEGER PRIMARY KEY,
-- only: postgres
rowid BIGINT PRIMARY KEY GENERATED BY DEFAULT AS IDENTITY,
bridge_id TEXT NOT NULL,
id TEXT NOT NULL,
part_id TEXT NOT NULL,
mxid TEXT NOT NULL,
room_id TEXT NOT NULL,
room_receiver TEXT NOT NULL,
sender_id TEXT NOT NULL,
sender_mxid TEXT NOT NULL,
timestamp BIGINT NOT NULL,
edit_count INTEGER NOT NULL,
double_puppeted BOOLEAN,
thread_root_id TEXT,
reply_to_id TEXT,
reply_to_part_id TEXT,
send_txn_id TEXT,
metadata jsonb NOT NULL,
CONSTRAINT message_room_fkey FOREIGN KEY (bridge_id, room_id, room_receiver)
REFERENCES portal (bridge_id, id, receiver)
ON DELETE CASCADE ON UPDATE CASCADE,
CONSTRAINT message_sender_fkey FOREIGN KEY (bridge_id, sender_id)
REFERENCES ghost (bridge_id, id)
ON DELETE CASCADE ON UPDATE CASCADE,
CONSTRAINT message_real_pkey UNIQUE (bridge_id, room_receiver, id, part_id),
CONSTRAINT message_mxid_unique UNIQUE (bridge_id, mxid),
CONSTRAINT message_txn_id_unique UNIQUE (bridge_id, room_receiver, send_txn_id)
);
CREATE INDEX message_room_idx ON message (bridge_id, room_id, room_receiver);
CREATE TABLE disappearing_message (
bridge_id TEXT NOT NULL,
mx_room TEXT NOT NULL,
mxid TEXT NOT NULL,
timestamp BIGINT NOT NULL DEFAULT 0,
type TEXT NOT NULL,
timer BIGINT NOT NULL,
disappear_at BIGINT,
PRIMARY KEY (bridge_id, mxid),
CONSTRAINT disappearing_message_portal_fkey
FOREIGN KEY (bridge_id, mx_room)
REFERENCES portal (bridge_id, mxid)
ON DELETE CASCADE
);
CREATE INDEX disappearing_message_portal_idx ON disappearing_message (bridge_id, mx_room);
CREATE TABLE reaction (
bridge_id TEXT NOT NULL,
message_id TEXT NOT NULL,
message_part_id TEXT NOT NULL,
sender_id TEXT NOT NULL,
sender_mxid TEXT NOT NULL DEFAULT '',
emoji_id TEXT NOT NULL,
room_id TEXT NOT NULL,
room_receiver TEXT NOT NULL,
mxid TEXT NOT NULL,
timestamp BIGINT NOT NULL,
emoji TEXT NOT NULL,
metadata jsonb NOT NULL,
PRIMARY KEY (bridge_id, room_receiver, message_id, message_part_id, sender_id, emoji_id),
CONSTRAINT reaction_room_fkey FOREIGN KEY (bridge_id, room_id, room_receiver)
REFERENCES portal (bridge_id, id, receiver)
ON DELETE CASCADE ON UPDATE CASCADE,
CONSTRAINT reaction_message_fkey FOREIGN KEY (bridge_id, room_receiver, message_id, message_part_id)
REFERENCES message (bridge_id, room_receiver, id, part_id)
ON DELETE CASCADE ON UPDATE CASCADE,
CONSTRAINT reaction_sender_fkey FOREIGN KEY (bridge_id, sender_id)
REFERENCES ghost (bridge_id, id)
ON DELETE CASCADE ON UPDATE CASCADE,
CONSTRAINT reaction_mxid_unique UNIQUE (bridge_id, mxid)
);
CREATE INDEX reaction_room_idx ON reaction (bridge_id, room_id, room_receiver);
CREATE TABLE user_portal (
bridge_id TEXT NOT NULL,
user_mxid TEXT NOT NULL,
login_id TEXT NOT NULL,
portal_id TEXT NOT NULL,
portal_receiver TEXT NOT NULL,
in_space BOOLEAN NOT NULL,
preferred BOOLEAN NOT NULL,
last_read BIGINT,
PRIMARY KEY (bridge_id, user_mxid, login_id, portal_id, portal_receiver),
CONSTRAINT user_portal_user_login_fkey FOREIGN KEY (bridge_id, login_id)
REFERENCES user_login (bridge_id, id)
ON DELETE CASCADE ON UPDATE CASCADE,
CONSTRAINT user_portal_portal_fkey FOREIGN KEY (bridge_id, portal_id, portal_receiver)
REFERENCES portal (bridge_id, id, receiver)
ON DELETE CASCADE ON UPDATE CASCADE
);
CREATE INDEX user_portal_login_idx ON user_portal (bridge_id, login_id);
CREATE INDEX user_portal_portal_idx ON user_portal (bridge_id, portal_id, portal_receiver);
CREATE TABLE backfill_task (
bridge_id TEXT NOT NULL,
portal_id TEXT NOT NULL,
portal_receiver TEXT NOT NULL,
user_login_id TEXT NOT NULL,
batch_count INTEGER NOT NULL,
is_done BOOLEAN NOT NULL,
cursor TEXT,
oldest_message_id TEXT,
dispatched_at BIGINT,
completed_at BIGINT,
next_dispatch_min_ts BIGINT NOT NULL,
PRIMARY KEY (bridge_id, portal_id, portal_receiver),
CONSTRAINT backfill_queue_portal_fkey FOREIGN KEY (bridge_id, portal_id, portal_receiver)
REFERENCES portal (bridge_id, id, receiver)
ON DELETE CASCADE ON UPDATE CASCADE
);
CREATE TABLE kv_store (
bridge_id TEXT NOT NULL,
key TEXT NOT NULL,
value TEXT NOT NULL,
PRIMARY KEY (bridge_id, key)
);
CREATE TABLE public_media (
bridge_id TEXT NOT NULL,
public_id TEXT NOT NULL,
mxc TEXT NOT NULL,
keys jsonb,
mimetype TEXT,
expiry BIGINT,
PRIMARY KEY (bridge_id, public_id)
);

View file

@ -0,0 +1,11 @@
-- v2 (compatible with v1+): Add disappearing messages table
CREATE TABLE disappearing_message (
bridge_id TEXT NOT NULL,
mx_room TEXT NOT NULL,
mxid TEXT NOT NULL,
type TEXT NOT NULL,
timer BIGINT NOT NULL,
disappear_at BIGINT,
PRIMARY KEY (bridge_id, mxid)
);

View file

@ -0,0 +1,13 @@
-- v3 (compatible with v1+): Add relay column for portals (Postgres)
-- only: postgres
ALTER TABLE portal ADD COLUMN relay_bridge_id TEXT;
ALTER TABLE portal ADD COLUMN relay_login_id TEXT;
ALTER TABLE user_portal DROP CONSTRAINT user_portal_user_login_fkey;
ALTER TABLE user_login DROP CONSTRAINT user_login_pkey;
ALTER TABLE user_login ADD CONSTRAINT user_login_pkey PRIMARY KEY (bridge_id, id);
ALTER TABLE user_portal ADD CONSTRAINT user_portal_user_login_fkey FOREIGN KEY (bridge_id, login_id)
REFERENCES user_login (bridge_id, id)
ON DELETE CASCADE ON UPDATE CASCADE;
ALTER TABLE portal ADD CONSTRAINT portal_relay_fkey FOREIGN KEY (relay_bridge_id, relay_login_id)
REFERENCES user_login (bridge_id, id)
ON DELETE CASCADE ON UPDATE CASCADE;

View file

@ -0,0 +1,100 @@
-- v4 (compatible with v1+): Add relay column for portals (SQLite)
-- transaction: off
-- only: sqlite
PRAGMA foreign_keys = OFF;
BEGIN;
CREATE TABLE user_login_new (
bridge_id TEXT NOT NULL,
user_mxid TEXT NOT NULL,
id TEXT NOT NULL,
space_room TEXT,
metadata jsonb NOT NULL,
PRIMARY KEY (bridge_id, id),
CONSTRAINT user_login_user_fkey FOREIGN KEY (bridge_id, user_mxid)
REFERENCES "user" (bridge_id, mxid)
ON DELETE CASCADE ON UPDATE CASCADE
);
INSERT INTO user_login_new
SELECT bridge_id, user_mxid, id, space_room, metadata
FROM user_login;
DROP TABLE user_login;
ALTER TABLE user_login_new RENAME TO user_login;
CREATE TABLE user_portal_new (
bridge_id TEXT NOT NULL,
user_mxid TEXT NOT NULL,
login_id TEXT NOT NULL,
portal_id TEXT NOT NULL,
portal_receiver TEXT NOT NULL,
in_space BOOLEAN NOT NULL,
preferred BOOLEAN NOT NULL,
last_read BIGINT,
PRIMARY KEY (bridge_id, user_mxid, login_id, portal_id, portal_receiver),
CONSTRAINT user_portal_user_login_fkey FOREIGN KEY (bridge_id, login_id)
REFERENCES user_login (bridge_id, id)
ON DELETE CASCADE ON UPDATE CASCADE,
CONSTRAINT user_portal_portal_fkey FOREIGN KEY (bridge_id, portal_id, portal_receiver)
REFERENCES portal (bridge_id, id, receiver)
ON DELETE CASCADE ON UPDATE CASCADE
);
INSERT INTO user_portal_new
SELECT bridge_id, user_mxid, login_id, portal_id, portal_receiver, in_space, preferred, last_read
FROM user_portal;
DROP TABLE user_portal;
ALTER TABLE user_portal_new RENAME TO user_portal;
CREATE TABLE portal_new (
bridge_id TEXT NOT NULL,
id TEXT NOT NULL,
receiver TEXT NOT NULL,
mxid TEXT,
parent_id TEXT,
-- This is not accessed by the bridge, it's only used for the portal parent foreign key.
-- Parent groups are probably never DMs, so they don't need a receiver.
parent_receiver TEXT NOT NULL DEFAULT '',
relay_bridge_id TEXT,
relay_login_id TEXT,
name TEXT NOT NULL,
topic TEXT NOT NULL,
avatar_id TEXT NOT NULL,
avatar_hash TEXT NOT NULL,
avatar_mxc TEXT NOT NULL,
name_set BOOLEAN NOT NULL,
avatar_set BOOLEAN NOT NULL,
topic_set BOOLEAN NOT NULL,
in_space BOOLEAN NOT NULL,
metadata jsonb NOT NULL,
PRIMARY KEY (bridge_id, id, receiver),
CONSTRAINT portal_parent_fkey FOREIGN KEY (bridge_id, parent_id, parent_receiver)
-- Deletes aren't allowed to cascade here:
-- children should be re-parented or cleaned up manually
REFERENCES portal (bridge_id, id, receiver) ON UPDATE CASCADE,
CONSTRAINT portal_relay_fkey FOREIGN KEY (relay_bridge_id, relay_login_id)
REFERENCES user_login (bridge_id, id)
ON DELETE SET NULL ON UPDATE CASCADE
);
INSERT INTO portal_new
SELECT bridge_id, id, receiver, mxid, parent_id, parent_receiver, NULL, NULL,
name, topic, avatar_id, avatar_hash, avatar_mxc, name_set, avatar_set, topic_set, in_space, metadata
FROM portal;
DROP TABLE portal;
ALTER TABLE portal_new RENAME TO portal;
PRAGMA foreign_key_check;
COMMIT;
PRAGMA foreign_keys = ON;

View file

@ -0,0 +1,10 @@
-- v5 (compatible with v1+): Add room_receiver to message unique key (Postgres)
-- only: postgres
ALTER TABLE reaction DROP CONSTRAINT reaction_message_fkey;
ALTER TABLE reaction DROP CONSTRAINT reaction_pkey1;
ALTER TABLE reaction ADD PRIMARY KEY (bridge_id, room_receiver, message_id, message_part_id, sender_id, emoji_id);
ALTER TABLE message DROP CONSTRAINT message_real_pkey;
ALTER TABLE message ADD CONSTRAINT message_real_pkey UNIQUE (bridge_id, room_receiver, id, part_id);
ALTER TABLE reaction ADD CONSTRAINT reaction_message_fkey FOREIGN KEY (bridge_id, room_receiver, message_id, message_part_id)
REFERENCES message (bridge_id, room_receiver, id, part_id)
ON DELETE CASCADE ON UPDATE CASCADE;

View file

@ -0,0 +1,75 @@
-- v6 (compatible with v1+): Add room_receiver to message unique key (SQLite)
-- transaction: off
-- only: sqlite
PRAGMA foreign_keys = OFF;
BEGIN;
CREATE TABLE message_new (
rowid INTEGER PRIMARY KEY,
bridge_id TEXT NOT NULL,
id TEXT NOT NULL,
part_id TEXT NOT NULL,
mxid TEXT NOT NULL,
room_id TEXT NOT NULL,
room_receiver TEXT NOT NULL,
sender_id TEXT NOT NULL,
timestamp BIGINT NOT NULL,
relates_to BIGINT,
metadata jsonb NOT NULL,
CONSTRAINT message_relation_fkey FOREIGN KEY (relates_to)
REFERENCES message (rowid) ON DELETE SET NULL,
CONSTRAINT message_room_fkey FOREIGN KEY (bridge_id, room_id, room_receiver)
REFERENCES portal (bridge_id, id, receiver)
ON DELETE CASCADE ON UPDATE CASCADE,
CONSTRAINT message_sender_fkey FOREIGN KEY (bridge_id, sender_id)
REFERENCES ghost (bridge_id, id)
ON DELETE CASCADE ON UPDATE CASCADE,
CONSTRAINT message_real_pkey UNIQUE (bridge_id, room_receiver, id, part_id)
);
INSERT INTO message_new (rowid, bridge_id, id, part_id, mxid, room_id, room_receiver, sender_id, timestamp, relates_to, metadata)
SELECT rowid, bridge_id, id, part_id, mxid, room_id, room_receiver, sender_id, timestamp, relates_to, metadata
FROM message;
DROP TABLE message;
ALTER TABLE message_new RENAME TO message;
CREATE TABLE reaction_new (
bridge_id TEXT NOT NULL,
message_id TEXT NOT NULL,
message_part_id TEXT NOT NULL,
sender_id TEXT NOT NULL,
emoji_id TEXT NOT NULL,
room_id TEXT NOT NULL,
room_receiver TEXT NOT NULL,
mxid TEXT NOT NULL,
timestamp BIGINT NOT NULL,
metadata jsonb NOT NULL,
PRIMARY KEY (bridge_id, room_receiver, message_id, message_part_id, sender_id, emoji_id),
CONSTRAINT reaction_room_fkey FOREIGN KEY (bridge_id, room_id, room_receiver)
REFERENCES portal (bridge_id, id, receiver)
ON DELETE CASCADE ON UPDATE CASCADE,
CONSTRAINT reaction_message_fkey FOREIGN KEY (bridge_id, room_receiver, message_id, message_part_id)
REFERENCES message (bridge_id, room_receiver, id, part_id)
ON DELETE CASCADE ON UPDATE CASCADE,
CONSTRAINT reaction_sender_fkey FOREIGN KEY (bridge_id, sender_id)
REFERENCES ghost (bridge_id, id)
ON DELETE CASCADE ON UPDATE CASCADE
);
INSERT INTO reaction_new
SELECT bridge_id, message_id, message_part_id, sender_id, emoji_id, room_id, room_receiver, mxid, timestamp, metadata
FROM reaction;
DROP TABLE reaction;
ALTER TABLE reaction_new RENAME TO reaction;
PRAGMA foreign_key_check;
COMMIT;
PRAGMA foreign_keys = ON;

View file

@ -0,0 +1,4 @@
-- v7: Add new relation columns to messages
ALTER TABLE message ADD COLUMN thread_root_id TEXT;
ALTER TABLE message ADD COLUMN reply_to_id TEXT;
ALTER TABLE message ADD COLUMN reply_to_part_id TEXT;

View file

@ -0,0 +1,3 @@
-- v8: Drop relates_to column in messages
-- transaction: off
ALTER TABLE message DROP COLUMN relates_to;

View file

@ -0,0 +1,41 @@
-- v8: Drop relates_to column in messages
-- transaction: off
PRAGMA foreign_keys = OFF;
BEGIN;
CREATE TABLE message_new (
rowid INTEGER PRIMARY KEY,
bridge_id TEXT NOT NULL,
id TEXT NOT NULL,
part_id TEXT NOT NULL,
mxid TEXT NOT NULL,
room_id TEXT NOT NULL,
room_receiver TEXT NOT NULL,
sender_id TEXT NOT NULL,
timestamp BIGINT NOT NULL,
thread_root_id TEXT,
reply_to_id TEXT,
reply_to_part_id TEXT,
metadata jsonb NOT NULL,
CONSTRAINT message_room_fkey FOREIGN KEY (bridge_id, room_id, room_receiver)
REFERENCES portal (bridge_id, id, receiver)
ON DELETE CASCADE ON UPDATE CASCADE,
CONSTRAINT message_sender_fkey FOREIGN KEY (bridge_id, sender_id)
REFERENCES ghost (bridge_id, id)
ON DELETE CASCADE ON UPDATE CASCADE,
CONSTRAINT message_real_pkey UNIQUE (bridge_id, room_receiver, id, part_id)
);
INSERT INTO message_new (rowid, bridge_id, id, part_id, mxid, room_id, room_receiver, sender_id, timestamp, metadata)
SELECT rowid, bridge_id, id, part_id, mxid, room_id, room_receiver, sender_id, timestamp, metadata
FROM message;
DROP TABLE message;
ALTER TABLE message_new RENAME TO message;
PRAGMA foreign_key_check;
COMMIT;
PRAGMA foreign_keys = ON;

View file

@ -0,0 +1,45 @@
-- v9: Move standard metadata to separate columns
ALTER TABLE message ADD COLUMN sender_mxid TEXT NOT NULL DEFAULT '';
UPDATE message SET sender_mxid=COALESCE((metadata->>'sender_mxid'), '');
ALTER TABLE message ADD COLUMN edit_count INTEGER NOT NULL DEFAULT 0;
UPDATE message SET edit_count=COALESCE(CAST((metadata->>'edit_count') AS INTEGER), 0);
ALTER TABLE portal ADD COLUMN disappear_type TEXT;
UPDATE portal SET disappear_type=(metadata->>'disappear_type');
ALTER TABLE portal ADD COLUMN disappear_timer BIGINT;
-- only: postgres
UPDATE portal SET disappear_timer=(metadata->>'disappear_timer')::BIGINT;
-- only: sqlite
UPDATE portal SET disappear_timer=CAST(metadata->>'disappear_timer' AS INTEGER);
ALTER TABLE portal ADD COLUMN room_type TEXT NOT NULL DEFAULT '';
UPDATE portal SET room_type='dm' WHERE CAST(metadata->>'is_direct' AS BOOLEAN) IS true;
UPDATE portal SET room_type='space' WHERE CAST(metadata->>'is_space' AS BOOLEAN) IS true;
ALTER TABLE reaction ADD COLUMN emoji TEXT NOT NULL DEFAULT '';
UPDATE reaction SET emoji=COALESCE((metadata->>'emoji'), '');
ALTER TABLE user_login ADD COLUMN remote_name TEXT NOT NULL DEFAULT '';
UPDATE user_login SET remote_name=COALESCE((metadata->>'remote_name'), '');
ALTER TABLE ghost ADD COLUMN contact_info_set BOOLEAN NOT NULL DEFAULT false;
UPDATE ghost SET contact_info_set=COALESCE(CAST((metadata->>'contact_info_set') AS BOOLEAN), false);
ALTER TABLE ghost ADD COLUMN is_bot BOOLEAN NOT NULL DEFAULT false;
UPDATE ghost SET is_bot=COALESCE(CAST((metadata->>'is_bot') AS BOOLEAN), false);
ALTER TABLE ghost ADD COLUMN identifiers jsonb NOT NULL DEFAULT '[]';
UPDATE ghost SET identifiers=COALESCE((metadata->'identifiers'), '[]');
-- only: postgres until "end only"
ALTER TABLE message ALTER COLUMN sender_mxid DROP DEFAULT;
ALTER TABLE message ALTER COLUMN edit_count DROP DEFAULT;
ALTER TABLE portal ALTER COLUMN room_type DROP DEFAULT;
ALTER TABLE reaction ALTER COLUMN emoji DROP DEFAULT;
ALTER TABLE user_login ALTER COLUMN remote_name DROP DEFAULT;
ALTER TABLE ghost ALTER COLUMN contact_info_set DROP DEFAULT;
ALTER TABLE ghost ALTER COLUMN is_bot DROP DEFAULT;
ALTER TABLE ghost ALTER COLUMN identifiers DROP DEFAULT;
-- end only postgres

View file

@ -0,0 +1,4 @@
-- v10 (compatible with v9+): Fix Signal portal revisions
UPDATE portal
SET metadata=jsonb_set(metadata, '{revision}', CAST((metadata->>'revision') AS jsonb))
WHERE jsonb_typeof(metadata->'revision')='string';

View file

@ -0,0 +1,4 @@
-- v10 (compatible with v9+): Fix Signal portal revisions
UPDATE portal
SET metadata=json_set(metadata, '$.revision', CAST(json_extract(metadata, '$.revision') AS INTEGER))
WHERE json_type(metadata, '$.revision')='text';

View file

@ -0,0 +1,5 @@
-- v11: Add indexes for some foreign keys
CREATE INDEX message_room_idx ON message (bridge_id, room_id, room_receiver);
CREATE INDEX reaction_room_idx ON reaction (bridge_id, room_id, room_receiver);
CREATE INDEX user_portal_portal_idx ON user_portal (bridge_id, portal_id, portal_receiver);
CREATE INDEX user_portal_login_idx ON user_portal (bridge_id, login_id);

View file

@ -0,0 +1,2 @@
-- v12 (compatible with v9+): Save other user ID in DM portals
ALTER TABLE portal ADD COLUMN other_user_id TEXT;

View file

@ -0,0 +1,20 @@
-- v13 (compatible with v9+): Add backfill queue
CREATE TABLE backfill_task (
bridge_id TEXT NOT NULL,
portal_id TEXT NOT NULL,
portal_receiver TEXT NOT NULL,
user_login_id TEXT NOT NULL,
batch_count INTEGER NOT NULL,
is_done BOOLEAN NOT NULL,
cursor TEXT,
oldest_message_id TEXT,
dispatched_at BIGINT,
completed_at BIGINT,
next_dispatch_min_ts BIGINT NOT NULL,
PRIMARY KEY (bridge_id, portal_id, portal_receiver),
CONSTRAINT backfill_queue_portal_fkey FOREIGN KEY (bridge_id, portal_id, portal_receiver)
REFERENCES portal (bridge_id, id, receiver)
ON DELETE CASCADE ON UPDATE CASCADE
);

View file

@ -0,0 +1,2 @@
-- v14 (compatible with v9+): Save whether name is custom in portals
ALTER TABLE portal ADD COLUMN name_is_custom BOOLEAN NOT NULL DEFAULT false;

View file

@ -0,0 +1,2 @@
-- v15 (compatible with v9+): Save sender MXID for reactions
ALTER TABLE reaction ADD COLUMN sender_mxid TEXT NOT NULL DEFAULT '';

View file

@ -0,0 +1,2 @@
-- v16 (compatible with v9+): Save remote profile in user logins
ALTER TABLE user_login ADD COLUMN remote_profile jsonb;

View file

@ -0,0 +1,8 @@
-- v17 (compatible with v9+): Add unique constraint for message and reaction mxids
DELETE FROM message WHERE mxid IN (SELECT mxid FROM message GROUP BY mxid HAVING COUNT(*) > 1);
-- only: postgres for next 2 lines
ALTER TABLE message ADD CONSTRAINT message_mxid_unique UNIQUE (bridge_id, mxid);
ALTER TABLE reaction ADD CONSTRAINT reaction_mxid_unique UNIQUE (bridge_id, mxid);
-- only: sqlite for next 2 lines
CREATE UNIQUE INDEX message_mxid_unique ON message (bridge_id, mxid);
CREATE UNIQUE INDEX reaction_mxid_unique ON reaction (bridge_id, mxid);

View file

@ -0,0 +1,8 @@
-- v18 (compatible with v9+): Add generic key-value store
CREATE TABLE kv_store (
bridge_id TEXT NOT NULL,
key TEXT NOT NULL,
value TEXT NOT NULL,
PRIMARY KEY (bridge_id, key)
);

View file

@ -0,0 +1,2 @@
-- v19 (compatible with v9+): Add double puppeted state to messages
ALTER TABLE message ADD COLUMN double_puppeted BOOLEAN;

View file

@ -0,0 +1,2 @@
-- v20 (compatible with v9+): Add portal capability state
ALTER TABLE portal ADD COLUMN cap_state jsonb;

View file

@ -0,0 +1,8 @@
-- v21 (compatible with v9+): Add foreign key constraint from disappearing_message.mx_room to portals.mxid
CREATE UNIQUE INDEX portal_bridge_mxid_idx ON portal (bridge_id, mxid);
DELETE FROM disappearing_message WHERE mx_room NOT IN (SELECT mxid FROM portal WHERE mxid IS NOT NULL);
ALTER TABLE disappearing_message
ADD CONSTRAINT disappearing_message_portal_fkey
FOREIGN KEY (bridge_id, mx_room)
REFERENCES portal (bridge_id, mxid)
ON DELETE CASCADE;

View file

@ -0,0 +1,24 @@
-- v21 (compatible with v9+): Add foreign key constraint from disappearing_message.mx_room to portals.mxid
CREATE UNIQUE INDEX portal_bridge_mxid_idx ON portal (bridge_id, mxid);
CREATE TABLE disappearing_message_new (
bridge_id TEXT NOT NULL,
mx_room TEXT NOT NULL,
mxid TEXT NOT NULL,
type TEXT NOT NULL,
timer BIGINT NOT NULL,
disappear_at BIGINT,
PRIMARY KEY (bridge_id, mxid),
CONSTRAINT disappearing_message_portal_fkey
FOREIGN KEY (bridge_id, mx_room)
REFERENCES portal (bridge_id, mxid)
ON DELETE CASCADE
);
WITH portal_mxids AS (SELECT mxid FROM portal WHERE mxid IS NOT NULL)
INSERT INTO disappearing_message_new (bridge_id, mx_room, mxid, type, timer, disappear_at)
SELECT bridge_id, mx_room, mxid, type, timer, disappear_at
FROM disappearing_message WHERE mx_room IN portal_mxids;
DROP TABLE disappearing_message;
ALTER TABLE disappearing_message_new RENAME TO disappearing_message;

View file

@ -0,0 +1,6 @@
-- v22 (compatible with v9+): Add message send transaction ID column
ALTER TABLE message ADD COLUMN send_txn_id TEXT;
-- only: postgres
ALTER TABLE message ADD CONSTRAINT message_txn_id_unique UNIQUE (bridge_id, room_receiver, send_txn_id);
-- only: sqlite
CREATE UNIQUE INDEX message_txn_id_unique ON message (bridge_id, room_receiver, send_txn_id);

View file

@ -0,0 +1,2 @@
-- v23 (compatible with v9+): Add event timestamp for disappearing messages
ALTER TABLE disappearing_message ADD COLUMN timestamp BIGINT NOT NULL DEFAULT 0;

View file

@ -0,0 +1,11 @@
-- v24 (compatible with v9+): Custom URLs for public media
CREATE TABLE public_media (
bridge_id TEXT NOT NULL,
public_id TEXT NOT NULL,
mxc TEXT NOT NULL,
keys jsonb,
mimetype TEXT,
expiry BIGINT,
PRIMARY KEY (bridge_id, public_id)
);

View file

@ -0,0 +1,2 @@
-- v25 (compatible with v9+): Flag for message request portals
ALTER TABLE portal ADD COLUMN message_request BOOLEAN NOT NULL DEFAULT false;

View file

@ -0,0 +1,3 @@
-- v26 (compatible with v9+): Add room index for disappearing message table and portal parents
CREATE INDEX disappearing_message_portal_idx ON disappearing_message (bridge_id, mx_room);
CREATE INDEX portal_parent_idx ON portal (bridge_id, parent_id, parent_receiver);

View file

@ -0,0 +1,2 @@
-- v27 (compatible with v9+): Add column for extra ghost profile metadata
ALTER TABLE ghost ADD COLUMN extra_profile jsonb;

View file

@ -0,0 +1,22 @@
// 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 upgrades
import (
"embed"
"go.mau.fi/util/dbutil"
)
var Table dbutil.UpgradeTable
//go:embed *.sql
var rawUpgrades embed.FS
func init() {
Table.RegisterFS(rawUpgrades)
}

74
bridgev2/database/user.go Normal file
View file

@ -0,0 +1,74 @@
// 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 database
import (
"context"
"database/sql"
"go.mau.fi/util/dbutil"
"maunium.net/go/mautrix/bridgev2/networkid"
"maunium.net/go/mautrix/id"
)
type UserQuery struct {
BridgeID networkid.BridgeID
*dbutil.QueryHelper[*User]
}
type User struct {
BridgeID networkid.BridgeID
MXID id.UserID
ManagementRoom id.RoomID
AccessToken string
}
const (
getUserBaseQuery = `
SELECT bridge_id, mxid, management_room, access_token FROM "user"
`
getUserByMXIDQuery = getUserBaseQuery + `WHERE bridge_id=$1 AND mxid=$2`
insertUserQuery = `
INSERT INTO "user" (bridge_id, mxid, management_room, access_token)
VALUES ($1, $2, $3, $4)
`
updateUserQuery = `
UPDATE "user" SET management_room=$3, access_token=$4
WHERE bridge_id=$1 AND mxid=$2
`
)
func (uq *UserQuery) GetByMXID(ctx context.Context, userID id.UserID) (*User, error) {
return uq.QueryOne(ctx, getUserByMXIDQuery, uq.BridgeID, userID)
}
func (uq *UserQuery) Insert(ctx context.Context, user *User) error {
ensureBridgeIDMatches(&user.BridgeID, uq.BridgeID)
return uq.Exec(ctx, insertUserQuery, user.sqlVariables()...)
}
func (uq *UserQuery) Update(ctx context.Context, user *User) error {
ensureBridgeIDMatches(&user.BridgeID, uq.BridgeID)
return uq.Exec(ctx, updateUserQuery, user.sqlVariables()...)
}
func (u *User) Scan(row dbutil.Scannable) (*User, error) {
var managementRoom, accessToken sql.NullString
err := row.Scan(&u.BridgeID, &u.MXID, &managementRoom, &accessToken)
if err != nil {
return nil, err
}
u.ManagementRoom = id.RoomID(managementRoom.String)
u.AccessToken = accessToken.String
return u, nil
}
func (u *User) sqlVariables() []any {
return []any{u.BridgeID, u.MXID, dbutil.StrPtr(u.ManagementRoom), dbutil.StrPtr(u.AccessToken)}
}

View file

@ -0,0 +1,123 @@
// 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 database
import (
"context"
"database/sql"
"go.mau.fi/util/dbutil"
"maunium.net/go/mautrix/bridgev2/networkid"
"maunium.net/go/mautrix/bridgev2/status"
"maunium.net/go/mautrix/id"
)
type UserLoginQuery struct {
BridgeID networkid.BridgeID
MetaType MetaTypeCreator
*dbutil.QueryHelper[*UserLogin]
}
type UserLogin struct {
BridgeID networkid.BridgeID
UserMXID id.UserID
ID networkid.UserLoginID
RemoteName string
RemoteProfile status.RemoteProfile
SpaceRoom id.RoomID
Metadata any
}
const (
getUserLoginBaseQuery = `
SELECT bridge_id, user_mxid, id, remote_name, remote_profile, space_room, metadata FROM user_login
`
getLoginByIDQuery = getUserLoginBaseQuery + `WHERE bridge_id=$1 AND id=$2`
getAllUsersWithLoginsQuery = `SELECT DISTINCT user_mxid FROM user_login WHERE bridge_id=$1`
getAllLoginsForUserQuery = getUserLoginBaseQuery + `WHERE bridge_id=$1 AND user_mxid=$2`
getAllLoginsInPortalQuery = `
SELECT ul.bridge_id, ul.user_mxid, ul.id, ul.remote_name, ul.remote_profile, ul.space_room, ul.metadata FROM user_portal
LEFT JOIN user_login ul ON user_portal.bridge_id=ul.bridge_id AND user_portal.user_mxid=ul.user_mxid AND user_portal.login_id=ul.id
WHERE user_portal.bridge_id=$1 AND user_portal.portal_id=$2 AND user_portal.portal_receiver=$3
`
insertUserLoginQuery = `
INSERT INTO user_login (bridge_id, user_mxid, id, remote_name, remote_profile, space_room, metadata)
VALUES ($1, $2, $3, $4, $5, $6, $7)
`
updateUserLoginQuery = `
UPDATE user_login SET remote_name=$4, remote_profile=$5, space_room=$6, metadata=$7
WHERE bridge_id=$1 AND user_mxid=$2 AND id=$3
`
deleteUserLoginQuery = `
DELETE FROM user_login WHERE bridge_id=$1 AND id=$2
`
)
func (uq *UserLoginQuery) GetByID(ctx context.Context, id networkid.UserLoginID) (*UserLogin, error) {
return uq.QueryOne(ctx, getLoginByIDQuery, uq.BridgeID, id)
}
func (uq *UserLoginQuery) GetAllUserIDsWithLogins(ctx context.Context) ([]id.UserID, error) {
rows, err := uq.GetDB().Query(ctx, getAllUsersWithLoginsQuery, uq.BridgeID)
return dbutil.NewRowIterWithError(rows, dbutil.ScanSingleColumn[id.UserID], err).AsList()
}
func (uq *UserLoginQuery) GetAllInPortal(ctx context.Context, portal networkid.PortalKey) ([]*UserLogin, error) {
return uq.QueryMany(ctx, getAllLoginsInPortalQuery, uq.BridgeID, portal.ID, portal.Receiver)
}
func (uq *UserLoginQuery) GetAllForUser(ctx context.Context, userID id.UserID) ([]*UserLogin, error) {
return uq.QueryMany(ctx, getAllLoginsForUserQuery, uq.BridgeID, userID)
}
func (uq *UserLoginQuery) Insert(ctx context.Context, login *UserLogin) error {
ensureBridgeIDMatches(&login.BridgeID, uq.BridgeID)
return uq.Exec(ctx, insertUserLoginQuery, login.ensureHasMetadata(uq.MetaType).sqlVariables()...)
}
func (uq *UserLoginQuery) Update(ctx context.Context, login *UserLogin) error {
ensureBridgeIDMatches(&login.BridgeID, uq.BridgeID)
return uq.Exec(ctx, updateUserLoginQuery, login.ensureHasMetadata(uq.MetaType).sqlVariables()...)
}
func (uq *UserLoginQuery) Delete(ctx context.Context, loginID networkid.UserLoginID) error {
return uq.Exec(ctx, deleteUserLoginQuery, uq.BridgeID, loginID)
}
func (u *UserLogin) Scan(row dbutil.Scannable) (*UserLogin, error) {
var spaceRoom sql.NullString
err := row.Scan(
&u.BridgeID,
&u.UserMXID,
&u.ID,
&u.RemoteName,
dbutil.JSON{Data: &u.RemoteProfile},
&spaceRoom,
dbutil.JSON{Data: u.Metadata},
)
if err != nil {
return nil, err
}
u.SpaceRoom = id.RoomID(spaceRoom.String)
return u, nil
}
func (u *UserLogin) ensureHasMetadata(metaType MetaTypeCreator) *UserLogin {
if u.Metadata == nil {
u.Metadata = metaType()
}
return u
}
func (u *UserLogin) sqlVariables() []any {
var remoteProfile dbutil.JSON
if !u.RemoteProfile.IsZero() {
remoteProfile.Data = &u.RemoteProfile
}
return []any{u.BridgeID, u.UserMXID, u.ID, u.RemoteName, remoteProfile, dbutil.StrPtr(u.SpaceRoom), dbutil.JSON{Data: u.Metadata}}
}

View file

@ -0,0 +1,155 @@
// 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 database
import (
"context"
"database/sql"
"time"
"go.mau.fi/util/dbutil"
"maunium.net/go/mautrix/bridgev2/networkid"
"maunium.net/go/mautrix/id"
)
type UserPortalQuery struct {
BridgeID networkid.BridgeID
*dbutil.QueryHelper[*UserPortal]
}
type UserPortal struct {
BridgeID networkid.BridgeID
UserMXID id.UserID
LoginID networkid.UserLoginID
Portal networkid.PortalKey
InSpace *bool
Preferred *bool
LastRead time.Time
}
const (
getUserPortalBaseQuery = `
SELECT bridge_id, user_mxid, login_id, portal_id, portal_receiver, in_space, preferred, last_read
FROM user_portal
`
getUserPortalQuery = getUserPortalBaseQuery + `
WHERE bridge_id=$1 AND user_mxid=$2 AND login_id=$3 AND portal_id=$4 AND portal_receiver=$5
`
findUserLoginsOfUserByPortalIDQuery = getUserPortalBaseQuery + `
WHERE bridge_id=$1 AND user_mxid=$2 AND portal_id=$3 AND portal_receiver=$4
ORDER BY CASE WHEN preferred THEN 0 ELSE 1 END, login_id
`
getAllUserLoginsInPortalQuery = getUserPortalBaseQuery + `
WHERE bridge_id=$1 AND portal_id=$2 AND portal_receiver=$3
`
getAllPortalsForLoginQuery = getUserPortalBaseQuery + `
WHERE bridge_id=$1 AND user_mxid=$2 AND login_id=$3
`
getOrCreateUserPortalQuery = `
INSERT INTO user_portal (bridge_id, user_mxid, login_id, portal_id, portal_receiver, in_space, preferred)
VALUES ($1, $2, $3, $4, $5, false, false)
ON CONFLICT (bridge_id, user_mxid, login_id, portal_id, portal_receiver) DO UPDATE SET portal_id=user_portal.portal_id
RETURNING bridge_id, user_mxid, login_id, portal_id, portal_receiver, in_space, preferred, last_read
`
upsertUserPortalQuery = `
INSERT INTO user_portal (bridge_id, user_mxid, login_id, portal_id, portal_receiver, in_space, preferred, last_read)
VALUES ($1, $2, $3, $4, $5, COALESCE($6, false), COALESCE($7, false), $8)
ON CONFLICT (bridge_id, user_mxid, login_id, portal_id, portal_receiver) DO UPDATE
SET in_space=COALESCE($6, user_portal.in_space),
preferred=COALESCE($7, user_portal.preferred),
last_read=COALESCE($8, user_portal.last_read)
`
markLoginAsPreferredQuery = `
UPDATE user_portal SET preferred=(login_id=$3) WHERE bridge_id=$1 AND user_mxid=$2 AND portal_id=$4 AND portal_receiver=$5
`
markAllNotInSpaceQuery = `
UPDATE user_portal SET in_space=false WHERE bridge_id=$1 AND portal_id=$2 AND portal_receiver=$3
`
deleteUserPortalQuery = `
DELETE FROM user_portal WHERE bridge_id=$1 AND user_mxid=$2 AND login_id=$3 AND portal_id=$4 AND portal_receiver=$5
`
)
func UserPortalFor(ul *UserLogin, portal networkid.PortalKey) *UserPortal {
return &UserPortal{
BridgeID: ul.BridgeID,
UserMXID: ul.UserMXID,
LoginID: ul.ID,
Portal: portal,
}
}
func (upq *UserPortalQuery) GetAllForUserInPortal(ctx context.Context, userID id.UserID, portal networkid.PortalKey) ([]*UserPortal, error) {
return upq.QueryMany(ctx, findUserLoginsOfUserByPortalIDQuery, upq.BridgeID, userID, portal.ID, portal.Receiver)
}
func (upq *UserPortalQuery) GetAllForLogin(ctx context.Context, login *UserLogin) ([]*UserPortal, error) {
return upq.QueryMany(ctx, getAllPortalsForLoginQuery, upq.BridgeID, login.UserMXID, login.ID)
}
func (upq *UserPortalQuery) GetAllInPortal(ctx context.Context, portal networkid.PortalKey) ([]*UserPortal, error) {
return upq.QueryMany(ctx, getAllUserLoginsInPortalQuery, upq.BridgeID, portal.ID, portal.Receiver)
}
func (upq *UserPortalQuery) Get(ctx context.Context, login *UserLogin, portal networkid.PortalKey) (*UserPortal, error) {
return upq.QueryOne(ctx, getUserPortalQuery, upq.BridgeID, login.UserMXID, login.ID, portal.ID, portal.Receiver)
}
func (upq *UserPortalQuery) GetOrCreate(ctx context.Context, login *UserLogin, portal networkid.PortalKey) (*UserPortal, error) {
return upq.QueryOne(ctx, getOrCreateUserPortalQuery, upq.BridgeID, login.UserMXID, login.ID, portal.ID, portal.Receiver)
}
func (upq *UserPortalQuery) Put(ctx context.Context, up *UserPortal) error {
ensureBridgeIDMatches(&up.BridgeID, upq.BridgeID)
return upq.Exec(ctx, upsertUserPortalQuery, up.sqlVariables()...)
}
func (upq *UserPortalQuery) MarkAsPreferred(ctx context.Context, login *UserLogin, portal networkid.PortalKey) error {
return upq.Exec(ctx, markLoginAsPreferredQuery, upq.BridgeID, login.UserMXID, login.ID, portal.ID, portal.Receiver)
}
func (upq *UserPortalQuery) MarkAllNotInSpace(ctx context.Context, portal networkid.PortalKey) error {
return upq.Exec(ctx, markAllNotInSpaceQuery, upq.BridgeID, portal.ID, portal.Receiver)
}
func (upq *UserPortalQuery) Delete(ctx context.Context, up *UserPortal) error {
return upq.Exec(ctx, deleteUserPortalQuery, up.BridgeID, up.UserMXID, up.LoginID, up.Portal.ID, up.Portal.Receiver)
}
func (up *UserPortal) Scan(row dbutil.Scannable) (*UserPortal, error) {
var lastRead sql.NullInt64
err := row.Scan(
&up.BridgeID, &up.UserMXID, &up.LoginID, &up.Portal.ID, &up.Portal.Receiver,
&up.InSpace, &up.Preferred, &lastRead,
)
if err != nil {
return nil, err
}
if lastRead.Valid {
up.LastRead = time.Unix(0, lastRead.Int64)
}
return up, nil
}
func (up *UserPortal) sqlVariables() []any {
return []any{
up.BridgeID, up.UserMXID, up.LoginID, up.Portal.ID, up.Portal.Receiver,
up.InSpace,
up.Preferred,
dbutil.ConvertedPtr(up.LastRead, time.Time.UnixNano),
}
}
func (up *UserPortal) CopyWithoutValues() *UserPortal {
return &UserPortal{
BridgeID: up.BridgeID,
UserMXID: up.UserMXID,
LoginID: up.LoginID,
Portal: up.Portal,
}
}

153
bridgev2/disappear.go Normal file
View file

@ -0,0 +1,153 @@
// 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 bridgev2
import (
"context"
"sync/atomic"
"time"
"github.com/rs/zerolog"
"golang.org/x/exp/slices"
"maunium.net/go/mautrix/bridgev2/database"
"maunium.net/go/mautrix/event"
"maunium.net/go/mautrix/id"
)
type DisappearLoop struct {
br *Bridge
nextCheck atomic.Pointer[time.Time]
stop atomic.Pointer[context.CancelFunc]
}
const DisappearCheckInterval = 1 * time.Hour
func (dl *DisappearLoop) Start() {
log := dl.br.Log.With().Str("component", "disappear loop").Logger()
ctx, stop := context.WithCancel(log.WithContext(context.Background()))
if oldStop := dl.stop.Swap(&stop); oldStop != nil {
(*oldStop)()
}
log.Debug().Msg("Disappearing message loop starting")
for {
nextCheck := time.Now().Add(DisappearCheckInterval)
dl.nextCheck.Store(&nextCheck)
const MessageLimit = 200
messages, err := dl.br.DB.DisappearingMessage.GetUpcoming(ctx, DisappearCheckInterval, MessageLimit)
if err != nil {
log.Err(err).Msg("Failed to get upcoming disappearing messages")
} else if len(messages) > 0 {
if len(messages) >= MessageLimit {
lastDisappearTime := messages[len(messages)-1].DisappearAt
log.Debug().
Int("message_count", len(messages)).
Time("last_due", lastDisappearTime).
Msg("Deleting disappearing messages synchronously and checking again immediately")
// Store the expected next check time to avoid Add spawning unnecessary goroutines.
// This can be in the past, in which case Add will put everything in the db, which is also fine.
dl.nextCheck.Store(&lastDisappearTime)
// If there are many messages, process them synchronously and then check again.
dl.sleepAndDisappear(ctx, messages...)
continue
}
go dl.sleepAndDisappear(ctx, messages...)
}
select {
case <-time.After(time.Until(dl.GetNextCheck())):
case <-ctx.Done():
log.Debug().Msg("Disappearing message loop stopping")
return
}
}
}
func (dl *DisappearLoop) GetNextCheck() time.Time {
if dl == nil {
return time.Time{}
}
nextCheck := dl.nextCheck.Load()
if nextCheck == nil {
return time.Time{}
}
return *nextCheck
}
func (dl *DisappearLoop) Stop() {
if dl == nil {
return
}
if stop := dl.stop.Load(); stop != nil {
(*stop)()
}
}
func (dl *DisappearLoop) StartAllBefore(ctx context.Context, roomID id.RoomID, beforeTS time.Time) {
startedMessages, err := dl.br.DB.DisappearingMessage.StartAllBefore(ctx, roomID, beforeTS)
if err != nil {
zerolog.Ctx(ctx).Err(err).Msg("Failed to start disappearing messages")
return
}
startedMessages = slices.DeleteFunc(startedMessages, func(dm *database.DisappearingMessage) bool {
return dm.DisappearAt.After(dl.GetNextCheck())
})
slices.SortFunc(startedMessages, func(a, b *database.DisappearingMessage) int {
return a.DisappearAt.Compare(b.DisappearAt)
})
if len(startedMessages) > 0 {
go dl.sleepAndDisappear(ctx, startedMessages...)
}
}
func (dl *DisappearLoop) Add(ctx context.Context, dm *database.DisappearingMessage) {
err := dl.br.DB.DisappearingMessage.Put(ctx, dm)
if err != nil {
zerolog.Ctx(ctx).Err(err).
Stringer("event_id", dm.EventID).
Msg("Failed to save disappearing message")
}
if !dm.DisappearAt.IsZero() && dm.DisappearAt.Before(dl.GetNextCheck()) {
go dl.sleepAndDisappear(zerolog.Ctx(ctx).WithContext(dl.br.BackgroundCtx), dm)
}
}
func (dl *DisappearLoop) sleepAndDisappear(ctx context.Context, dms ...*database.DisappearingMessage) {
for _, msg := range dms {
timeUntilDisappear := time.Until(msg.DisappearAt)
if timeUntilDisappear <= 0 {
if ctx.Err() != nil {
return
}
} else {
select {
case <-time.After(timeUntilDisappear):
case <-ctx.Done():
return
}
}
resp, err := dl.br.Bot.SendMessage(ctx, msg.RoomID, event.EventRedaction, &event.Content{
Parsed: &event.RedactionEventContent{
Redacts: msg.EventID,
Reason: "Message disappeared",
},
}, nil)
if err != nil {
zerolog.Ctx(ctx).Err(err).Stringer("target_event_id", msg.EventID).Msg("Failed to disappear message")
} else {
zerolog.Ctx(ctx).Debug().
Stringer("target_event_id", msg.EventID).
Stringer("redaction_event_id", resp.EventID).
Msg("Disappeared message")
}
err = dl.br.DB.DisappearingMessage.Delete(ctx, msg.EventID)
if err != nil {
zerolog.Ctx(ctx).Err(err).
Stringer("event_id", msg.EventID).
Msg("Failed to delete disappearing message entry from database")
}
}
}

133
bridgev2/errors.go Normal file
View file

@ -0,0 +1,133 @@
// 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 bridgev2
import (
"errors"
"fmt"
"net/http"
"maunium.net/go/mautrix"
"maunium.net/go/mautrix/event"
)
// ErrIgnoringRemoteEvent can be returned by [RemoteMessage.ConvertMessage] or [RemoteEdit.ConvertEdit]
// to indicate that the event should be ignored after all. Handling the event will be cancelled immediately.
var ErrIgnoringRemoteEvent = errors.New("ignoring remote event")
// ErrNoStatus can be returned by [MatrixMessageResponse.HandleEcho] to indicate that the message is still in-flight
// and a status should not be sent yet. The message will still be saved into the database.
var ErrNoStatus = errors.New("omit message status")
// ErrResolveIdentifierTryNext can be returned by ResolveIdentifier or CreateChatWithGhost to signal that
// the identifier is valid, but can't be reached by the current login, and the caller should try the next
// login if there are more.
//
// This should generally only be returned when resolving internal IDs (which happens when initiating chats via Matrix).
// For example, Google Messages would return this when trying to resolve another login's user ID,
// and Telegram would return this when the access hash isn't available.
var ErrResolveIdentifierTryNext = errors.New("that identifier is not available via this login")
var ErrNotLoggedIn = errors.New("not logged in")
// ErrDirectMediaNotEnabled may be returned by Matrix connectors if [MatrixConnector.GenerateContentURI] is called,
// but direct media is not enabled.
var ErrDirectMediaNotEnabled = errors.New("direct media is not enabled")
var ErrPortalIsDeleted = errors.New("portal is deleted")
var ErrPortalNotFoundInEventHandler = errors.New("portal not found to handle remote event")
// Common message status errors
var (
ErrPanicInEventHandler error = WrapErrorInStatus(errors.New("panic in event handler")).WithSendNotice(true).WithErrorAsMessage()
ErrNoPortal error = WrapErrorInStatus(errors.New("room is not a portal")).WithIsCertain(true).WithSendNotice(false)
ErrIgnoringReactionFromRelayedUser error = WrapErrorInStatus(errors.New("ignoring reaction event from relayed user")).WithIsCertain(true).WithSendNotice(false)
ErrIgnoringPollFromRelayedUser error = WrapErrorInStatus(errors.New("ignoring poll event from relayed user")).WithIsCertain(true).WithSendNotice(false)
ErrIgnoringDeleteChatRelayedUser error = WrapErrorInStatus(errors.New("ignoring delete chat event from relayed user")).WithIsCertain(true).WithSendNotice(false)
ErrIgnoringAcceptRequestRelayedUser error = WrapErrorInStatus(errors.New("ignoring accept message request event from relayed user")).WithIsCertain(true).WithSendNotice(false)
ErrEditsNotSupported error = WrapErrorInStatus(errors.New("this bridge does not support edits")).WithIsCertain(true).WithErrorAsMessage().WithErrorReason(event.MessageStatusUnsupported)
ErrEditsNotSupportedInPortal error = WrapErrorInStatus(errors.New("edits are not allowed in this chat")).WithIsCertain(true).WithErrorAsMessage().WithErrorReason(event.MessageStatusUnsupported)
ErrCaptionsNotAllowed error = WrapErrorInStatus(errors.New("captions are not supported here")).WithIsCertain(true).WithErrorAsMessage().WithErrorReason(event.MessageStatusUnsupported)
ErrLocationMessagesNotAllowed error = WrapErrorInStatus(errors.New("location messages are not supported here")).WithIsCertain(true).WithErrorAsMessage().WithErrorReason(event.MessageStatusUnsupported)
ErrEditTargetTooOld error = WrapErrorInStatus(errors.New("the message is too old to be edited")).WithIsCertain(true).WithErrorAsMessage().WithErrorReason(event.MessageStatusUnsupported)
ErrEditTargetTooManyEdits error = WrapErrorInStatus(errors.New("the message has been edited too many times")).WithIsCertain(true).WithErrorAsMessage().WithErrorReason(event.MessageStatusUnsupported)
ErrReactionsNotSupported error = WrapErrorInStatus(errors.New("this bridge does not support reactions")).WithIsCertain(true).WithErrorAsMessage().WithErrorReason(event.MessageStatusUnsupported)
ErrPollsNotSupported error = WrapErrorInStatus(errors.New("this bridge does not support polls")).WithIsCertain(true).WithErrorAsMessage().WithErrorReason(event.MessageStatusUnsupported)
ErrRoomMetadataNotSupported error = WrapErrorInStatus(errors.New("this bridge does not support changing room metadata")).WithIsCertain(true).WithErrorAsMessage().WithSendNotice(false).WithErrorReason(event.MessageStatusUnsupported)
ErrRoomMetadataNotAllowed error = WrapErrorInStatus(errors.New("changes are not allowed here")).WithIsCertain(true).WithErrorAsMessage().WithSendNotice(false).WithErrorReason(event.MessageStatusUnsupported)
ErrRedactionsNotSupported error = WrapErrorInStatus(errors.New("this bridge does not support deleting messages")).WithIsCertain(true).WithErrorAsMessage().WithErrorReason(event.MessageStatusUnsupported)
ErrUnexpectedParsedContentType error = WrapErrorInStatus(errors.New("unexpected parsed content type")).WithErrorAsMessage().WithIsCertain(true).WithSendNotice(true)
ErrInvalidStateKey error = WrapErrorInStatus(errors.New("room metadata state key is unset or non-empty")).WithErrorAsMessage().WithIsCertain(true).WithSendNotice(false)
ErrDatabaseError error = WrapErrorInStatus(errors.New("database error")).WithMessage("internal database error").WithIsCertain(true).WithSendNotice(true)
ErrTargetMessageNotFound error = WrapErrorInStatus(errors.New("target message not found")).WithErrorAsMessage().WithIsCertain(true).WithSendNotice(false)
ErrUnsupportedMessageType error = WrapErrorInStatus(errors.New("unsupported message type")).WithErrorAsMessage().WithIsCertain(true).WithSendNotice(true).WithErrorReason(event.MessageStatusUnsupported)
ErrUnsupportedMediaType error = WrapErrorInStatus(errors.New("unsupported media type")).WithErrorAsMessage().WithIsCertain(true).WithSendNotice(true).WithErrorReason(event.MessageStatusUnsupported)
ErrMediaDurationTooLong error = WrapErrorInStatus(errors.New("media duration too long")).WithErrorAsMessage().WithSendNotice(true).WithErrorReason(event.MessageStatusUnsupported)
ErrVoiceMessageDurationTooLong error = WrapErrorInStatus(errors.New("voice message too long")).WithErrorAsMessage().WithSendNotice(true).WithErrorReason(event.MessageStatusUnsupported)
ErrMediaTooLarge error = WrapErrorInStatus(errors.New("media too large")).WithErrorAsMessage().WithIsCertain(true).WithSendNotice(true).WithErrorReason(event.MessageStatusUnsupported)
ErrIgnoringMNotice error = WrapErrorInStatus(errors.New("ignoring m.notice message")).WithIsCertain(true).WithErrorAsMessage().WithSendNotice(false)
ErrMediaDownloadFailed error = WrapErrorInStatus(errors.New("failed to download media")).WithMessage("failed to download media").WithIsCertain(true).WithSendNotice(true)
ErrMediaReuploadFailed error = WrapErrorInStatus(errors.New("failed to reupload media")).WithMessage("failed to reupload media").WithIsCertain(true).WithSendNotice(true)
ErrMediaConvertFailed error = WrapErrorInStatus(errors.New("failed to convert media")).WithMessage("failed to convert media").WithIsCertain(true).WithSendNotice(true)
ErrMembershipNotSupported error = WrapErrorInStatus(errors.New("this bridge does not support changing group membership")).WithIsCertain(true).WithErrorAsMessage().WithSendNotice(false).WithErrorReason(event.MessageStatusUnsupported)
ErrDeleteChatNotSupported error = WrapErrorInStatus(errors.New("this bridge does not support deleting chats")).WithIsCertain(true).WithErrorAsMessage().WithSendNotice(false).WithErrorReason(event.MessageStatusUnsupported)
ErrBeeperAIStreamNotSupported error = WrapErrorInStatus(errors.New("this bridge does not support Beeper AI stream events")).WithIsCertain(true).WithErrorAsMessage().WithSendNotice(false).WithErrorReason(event.MessageStatusUnsupported)
ErrPowerLevelsNotSupported error = WrapErrorInStatus(errors.New("this bridge does not support changing group power levels")).WithIsCertain(true).WithErrorAsMessage().WithSendNotice(false).WithErrorReason(event.MessageStatusUnsupported)
ErrRemoteEchoTimeout = WrapErrorInStatus(errors.New("remote echo timed out")).WithIsCertain(false).WithSendNotice(true).WithErrorReason(event.MessageStatusTooOld)
ErrRemoteAckTimeout = WrapErrorInStatus(errors.New("remote ack timed out")).WithIsCertain(false).WithSendNotice(true).WithErrorReason(event.MessageStatusTooOld)
ErrPublicMediaDisabled = WrapErrorInStatus(errors.New("public media is not enabled in the bridge config")).WithIsCertain(true).WithErrorAsMessage().WithErrorReason(event.MessageStatusUnsupported).WithSendNotice(true)
ErrPublicMediaDatabaseDisabled = WrapErrorInStatus(errors.New("public media database storage is disabled")).WithIsCertain(true).WithErrorAsMessage().WithErrorReason(event.MessageStatusUnsupported).WithSendNotice(true)
ErrPublicMediaGenerateFailed = WrapErrorInStatus(errors.New("failed to generate public media URL")).WithIsCertain(true).WithMessage("failed to generate public media URL").WithErrorReason(event.MessageStatusUnsupported).WithSendNotice(true)
ErrDisappearingTimerUnsupported error = WrapErrorInStatus(errors.New("invalid disappearing timer")).WithIsCertain(true)
)
// Common login interface errors
var (
ErrInvalidLoginFlowID error = RespError(mautrix.MNotFound.WithMessage("Invalid login flow ID"))
)
// RespError is a class of error that certain network interface methods can return to ensure that the error
// is properly translated into an HTTP error when the method is called via the provisioning API.
//
// However, unlike mautrix.RespError, this does not include the error code
// in the message shown to users when used outside HTTP contexts.
type RespError mautrix.RespError
func (re RespError) Error() string {
return re.Err
}
func (re RespError) Is(err error) bool {
var e2 RespError
if errors.As(err, &e2) {
return e2.Err == re.Err
}
return errors.Is(err, mautrix.RespError(re))
}
func (re RespError) Write(w http.ResponseWriter) {
mautrix.RespError(re).Write(w)
}
func (re RespError) WithMessage(msg string, args ...any) RespError {
return RespError(mautrix.RespError(re).WithMessage(msg, args...))
}
func (re RespError) AppendMessage(append string, args ...any) RespError {
re.Err += fmt.Sprintf(append, args...)
return re
}
func WrapRespErrManual(err error, code string, status int) RespError {
return RespError{ErrCode: code, Err: err.Error(), StatusCode: status}
}
func WrapRespErr(err error, target mautrix.RespError) RespError {
return RespError{ErrCode: target.ErrCode, Err: err.Error(), StatusCode: target.StatusCode}
}

321
bridgev2/ghost.go Normal file
View file

@ -0,0 +1,321 @@
// 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 bridgev2
import (
"context"
"crypto/sha256"
"encoding/json"
"fmt"
"maps"
"net/http"
"slices"
"github.com/rs/zerolog"
"go.mau.fi/util/exerrors"
"go.mau.fi/util/exmime"
"maunium.net/go/mautrix/bridgev2/database"
"maunium.net/go/mautrix/bridgev2/networkid"
"maunium.net/go/mautrix/event"
"maunium.net/go/mautrix/id"
)
type Ghost struct {
*database.Ghost
Bridge *Bridge
Log zerolog.Logger
Intent MatrixAPI
}
func (br *Bridge) loadGhost(ctx context.Context, dbGhost *database.Ghost, queryErr error, id *networkid.UserID) (*Ghost, error) {
if queryErr != nil {
return nil, fmt.Errorf("failed to query db: %w", queryErr)
}
if dbGhost == nil {
if id == nil {
return nil, nil
}
dbGhost = &database.Ghost{
BridgeID: br.ID,
ID: *id,
}
err := br.DB.Ghost.Insert(ctx, dbGhost)
if err != nil {
return nil, fmt.Errorf("failed to insert new ghost: %w", err)
}
}
ghost := &Ghost{
Ghost: dbGhost,
Bridge: br,
Log: br.Log.With().Str("ghost_id", string(dbGhost.ID)).Logger(),
Intent: br.Matrix.GhostIntent(dbGhost.ID),
}
br.ghostsByID[ghost.ID] = ghost
return ghost, nil
}
func (br *Bridge) unlockedGetGhostByID(ctx context.Context, id networkid.UserID, onlyIfExists bool) (*Ghost, error) {
cached, ok := br.ghostsByID[id]
if ok {
return cached, nil
}
idPtr := &id
if onlyIfExists {
idPtr = nil
}
db, err := br.DB.Ghost.GetByID(ctx, id)
return br.loadGhost(ctx, db, err, idPtr)
}
func (br *Bridge) IsGhostMXID(userID id.UserID) bool {
_, isGhost := br.Matrix.ParseGhostMXID(userID)
return isGhost
}
func (br *Bridge) GetGhostByMXID(ctx context.Context, mxid id.UserID) (*Ghost, error) {
ghostID, ok := br.Matrix.ParseGhostMXID(mxid)
if !ok {
return nil, nil
}
return br.GetGhostByID(ctx, ghostID)
}
func (br *Bridge) GetGhostByID(ctx context.Context, id networkid.UserID) (*Ghost, error) {
br.cacheLock.Lock()
defer br.cacheLock.Unlock()
ghost, err := br.unlockedGetGhostByID(ctx, id, false)
if err != nil {
return nil, err
} else if ghost == nil {
panic(fmt.Errorf("unlockedGetGhostByID(ctx, %q, false) returned nil", id))
}
return ghost, nil
}
func (br *Bridge) GetExistingGhostByID(ctx context.Context, id networkid.UserID) (*Ghost, error) {
br.cacheLock.Lock()
defer br.cacheLock.Unlock()
return br.unlockedGetGhostByID(ctx, id, true)
}
type Avatar struct {
ID networkid.AvatarID
Get func(ctx context.Context) ([]byte, error)
Remove bool
// For pre-uploaded avatars, the MXC URI and hash can be provided directly
MXC id.ContentURIString
Hash [32]byte
}
func (a *Avatar) Reupload(ctx context.Context, intent MatrixAPI, currentHash [32]byte, currentMXC id.ContentURIString) (id.ContentURIString, [32]byte, error) {
if a.MXC != "" || a.Hash != [32]byte{} {
return a.MXC, a.Hash, nil
} else if a.Get == nil {
return "", [32]byte{}, fmt.Errorf("no Get function provided for avatar")
}
data, err := a.Get(ctx)
if err != nil {
return "", [32]byte{}, err
}
hash := sha256.Sum256(data)
if hash == currentHash && currentMXC != "" {
return currentMXC, hash, nil
}
mime := http.DetectContentType(data)
fileName := "avatar" + exmime.ExtensionFromMimetype(mime)
uri, _, err := intent.UploadMedia(ctx, "", data, fileName, mime)
if err != nil {
return "", hash, err
}
return uri, hash, nil
}
type UserInfo struct {
Identifiers []string
Name *string
Avatar *Avatar
IsBot *bool
ExtraProfile database.ExtraProfile
ExtraUpdates ExtraUpdater[*Ghost]
}
func (ghost *Ghost) UpdateName(ctx context.Context, name string) bool {
if ghost.Name == name && ghost.NameSet {
return false
}
ghost.Name = name
ghost.NameSet = false
err := ghost.Intent.SetDisplayName(ctx, name)
if err != nil {
zerolog.Ctx(ctx).Err(err).Msg("Failed to set display name")
} else {
ghost.NameSet = true
}
return true
}
func (ghost *Ghost) UpdateAvatar(ctx context.Context, avatar *Avatar) bool {
if ghost.AvatarID == avatar.ID && (avatar.Remove || ghost.AvatarMXC != "") && ghost.AvatarSet {
return false
}
ghost.AvatarID = avatar.ID
if !avatar.Remove {
newMXC, newHash, err := avatar.Reupload(ctx, ghost.Intent, ghost.AvatarHash, ghost.AvatarMXC)
if err != nil {
ghost.AvatarSet = false
zerolog.Ctx(ctx).Err(err).Msg("Failed to reupload avatar")
return true
} else if newHash == ghost.AvatarHash && ghost.AvatarMXC != "" && ghost.AvatarSet {
return true
}
ghost.AvatarHash = newHash
ghost.AvatarMXC = newMXC
} else {
ghost.AvatarMXC = ""
}
ghost.AvatarSet = false
if err := ghost.Intent.SetAvatarURL(ctx, ghost.AvatarMXC); err != nil {
zerolog.Ctx(ctx).Err(err).Msg("Failed to set avatar URL")
} else {
ghost.AvatarSet = true
}
return true
}
func (ghost *Ghost) getExtraProfileMeta() any {
bridgeName := ghost.Bridge.Network.GetName()
baseExtra := &event.BeeperProfileExtra{
RemoteID: string(ghost.ID),
Identifiers: ghost.Identifiers,
Service: bridgeName.BeeperBridgeType,
Network: bridgeName.NetworkID,
IsBridgeBot: false,
IsNetworkBot: ghost.IsBot,
}
if len(ghost.ExtraProfile) == 0 {
return baseExtra
}
mergedExtra := maps.Clone(ghost.ExtraProfile)
baseExtraMarshaled := exerrors.Must(json.Marshal(baseExtra))
exerrors.PanicIfNotNil(json.Unmarshal(baseExtraMarshaled, &mergedExtra))
return mergedExtra
}
func (ghost *Ghost) UpdateContactInfo(ctx context.Context, identifiers []string, isBot *bool, extraProfile database.ExtraProfile) bool {
if !ghost.Bridge.Matrix.GetCapabilities().ExtraProfileMeta {
ghost.ContactInfoSet = false
return false
}
if identifiers != nil {
slices.Sort(identifiers)
}
changed := extraProfile.CopyTo(&ghost.ExtraProfile)
if identifiers != nil {
changed = changed || !slices.Equal(identifiers, ghost.Identifiers)
ghost.Identifiers = identifiers
}
if isBot != nil {
changed = changed || *isBot != ghost.IsBot
ghost.IsBot = *isBot
}
if ghost.ContactInfoSet && !changed {
return false
}
err := ghost.Intent.SetExtraProfileMeta(ctx, ghost.getExtraProfileMeta())
if err != nil {
zerolog.Ctx(ctx).Err(err).Msg("Failed to set extra profile metadata")
} else {
ghost.ContactInfoSet = true
}
return true
}
func (br *Bridge) allowAggressiveUpdateForType(evtType RemoteEventType) bool {
if !br.Network.GetCapabilities().AggressiveUpdateInfo {
return false
}
switch evtType {
case RemoteEventUnknown, RemoteEventMessage, RemoteEventEdit, RemoteEventReaction:
return true
default:
return false
}
}
func (ghost *Ghost) UpdateInfoIfNecessary(ctx context.Context, source *UserLogin, evtType RemoteEventType) {
if ghost.Name != "" && ghost.NameSet && ghost.AvatarSet && !ghost.Bridge.allowAggressiveUpdateForType(evtType) {
return
}
info, err := source.Client.GetUserInfo(ctx, ghost)
if err != nil {
zerolog.Ctx(ctx).Err(err).Str("ghost_id", string(ghost.ID)).Msg("Failed to get info to update ghost")
} else if info != nil {
zerolog.Ctx(ctx).Debug().
Bool("has_name", ghost.Name != "").
Bool("name_set", ghost.NameSet).
Bool("has_avatar", ghost.AvatarMXC != "").
Bool("avatar_set", ghost.AvatarSet).
Msg("Updating ghost info in IfNecessary call")
ghost.UpdateInfo(ctx, info)
} else {
zerolog.Ctx(ctx).Trace().
Bool("has_name", ghost.Name != "").
Bool("name_set", ghost.NameSet).
Bool("has_avatar", ghost.AvatarMXC != "").
Bool("avatar_set", ghost.AvatarSet).
Msg("No ghost info received in IfNecessary call")
}
}
func (ghost *Ghost) updateDMPortals(ctx context.Context) {
if !ghost.Bridge.Config.PrivateChatPortalMeta {
return
}
dmPortals, err := ghost.Bridge.GetDMPortalsWith(ctx, ghost.ID)
if err != nil {
zerolog.Ctx(ctx).Err(err).Msg("Failed to get DM portals to update info")
return
}
for _, portal := range dmPortals {
go portal.lockedUpdateInfoFromGhost(ctx, ghost)
}
}
func (ghost *Ghost) UpdateInfo(ctx context.Context, info *UserInfo) {
update := false
oldName := ghost.Name
oldAvatar := ghost.AvatarMXC
if info.Name != nil {
update = ghost.UpdateName(ctx, *info.Name) || update
}
if info.Avatar != nil {
update = ghost.UpdateAvatar(ctx, info.Avatar) || update
} else if oldAvatar == "" && !ghost.AvatarSet {
// Special case: nil avatar means we're not expecting one ever, if we don't currently have
// one we flag it as set to avoid constantly refetching in UpdateInfoIfNecessary.
ghost.AvatarSet = true
update = true
}
if info.Identifiers != nil || info.IsBot != nil || info.ExtraProfile != nil {
update = ghost.UpdateContactInfo(ctx, info.Identifiers, info.IsBot, info.ExtraProfile) || update
}
if info.ExtraUpdates != nil {
update = info.ExtraUpdates(ctx, ghost) || update
}
if oldName != ghost.Name || oldAvatar != ghost.AvatarMXC {
ghost.updateDMPortals(ctx)
}
if update {
err := ghost.Bridge.DB.Ghost.Update(ctx, ghost.Ghost)
if err != nil {
zerolog.Ctx(ctx).Err(err).Msg("Failed to update ghost in database after updating info")
}
}
}

301
bridgev2/login.go Normal file
View file

@ -0,0 +1,301 @@
// 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 bridgev2
import (
"context"
"fmt"
"regexp"
"strings"
"maunium.net/go/mautrix/bridgev2/networkid"
"maunium.net/go/mautrix/event"
)
// LoginProcess represents a single occurrence of a user logging into the remote network.
type LoginProcess interface {
// Start starts the process and returns the first step.
//
// For example, a network using QR login may connect to the network, fetch a QR code,
// and return a DisplayAndWait-type step.
//
// This will only ever be called once.
Start(ctx context.Context) (*LoginStep, error)
// Cancel stops the login process and cleans up any resources.
// No other methods will be called after cancel.
//
// Cancel will not be called if any other method returned an error:
// errors are always treated as fatal and the process is assumed to be automatically cancelled.
Cancel()
}
type LoginProcessWithOverride interface {
LoginProcess
// StartWithOverride starts the process with the intent of re-authenticating an existing login.
//
// The call to this is mutually exclusive with the call to the default Start method.
//
// The user login being overridden will still be logged out automatically
// in case the complete step returns a different login.
StartWithOverride(ctx context.Context, override *UserLogin) (*LoginStep, error)
}
type LoginProcessDisplayAndWait interface {
LoginProcess
Wait(ctx context.Context) (*LoginStep, error)
}
type LoginProcessUserInput interface {
LoginProcess
SubmitUserInput(ctx context.Context, input map[string]string) (*LoginStep, error)
}
type LoginProcessCookies interface {
LoginProcess
SubmitCookies(ctx context.Context, cookies map[string]string) (*LoginStep, error)
}
type LoginFlow struct {
Name string `json:"name"`
Description string `json:"description"`
ID string `json:"id"`
}
type LoginStepType string
const (
LoginStepTypeUserInput LoginStepType = "user_input"
LoginStepTypeCookies LoginStepType = "cookies"
LoginStepTypeDisplayAndWait LoginStepType = "display_and_wait"
LoginStepTypeComplete LoginStepType = "complete"
)
type LoginDisplayType string
const (
LoginDisplayTypeQR LoginDisplayType = "qr"
LoginDisplayTypeEmoji LoginDisplayType = "emoji"
LoginDisplayTypeCode LoginDisplayType = "code"
LoginDisplayTypeNothing LoginDisplayType = "nothing"
)
type LoginStep struct {
// The type of login step
Type LoginStepType `json:"type"`
// A unique ID for this step. The ID should be same for every login using the same flow,
// but it should be different for different bridges and step types.
//
// For example, Telegram's QR scan followed by a 2-factor password
// might use the IDs `fi.mau.telegram.qr` and `fi.mau.telegram.2fa_password`.
StepID string `json:"step_id"`
// Instructions contains human-readable instructions for completing the login step.
Instructions string `json:"instructions"`
// Exactly one of the following structs must be filled depending on the step type.
DisplayAndWaitParams *LoginDisplayAndWaitParams `json:"display_and_wait,omitempty"`
CookiesParams *LoginCookiesParams `json:"cookies,omitempty"`
UserInputParams *LoginUserInputParams `json:"user_input,omitempty"`
CompleteParams *LoginCompleteParams `json:"complete,omitempty"`
}
type LoginDisplayAndWaitParams struct {
// The type of thing to display (QR, emoji or text code)
Type LoginDisplayType `json:"type"`
// The thing to display (raw data for QR, unicode emoji for emoji, plain string for code, omitted for nothing)
Data string `json:"data,omitempty"`
// An image containing the thing to display. If present, this is recommended over using data directly.
// For emojis, the URL to the canonical image representation of the emoji
ImageURL string `json:"image_url,omitempty"`
}
type LoginCookieFieldSourceType string
const (
LoginCookieTypeCookie LoginCookieFieldSourceType = "cookie"
LoginCookieTypeLocalStorage LoginCookieFieldSourceType = "local_storage"
LoginCookieTypeRequestHeader LoginCookieFieldSourceType = "request_header"
LoginCookieTypeRequestBody LoginCookieFieldSourceType = "request_body"
LoginCookieTypeSpecial LoginCookieFieldSourceType = "special"
)
type LoginCookieFieldSource struct {
// The type of source.
Type LoginCookieFieldSourceType `json:"type"`
// The name of the field. The exact meaning depends on the type of source.
// Cookie: cookie name
// Local storage: key in local storage
// Request header: header name
// Request body: field name inside body after it's parsed (as JSON or multipart form data)
// Special: a namespaced identifier that clients can implement special handling for
Name string `json:"name"`
// For request header & body types, a regex matching request URLs where the value can be extracted from.
RequestURLRegex string `json:"request_url_regex,omitempty"`
// For cookie types, the domain the cookie is present on.
CookieDomain string `json:"cookie_domain,omitempty"`
}
type LoginCookieField struct {
// The key in the map that is submitted to the connector.
ID string `json:"id"`
Required bool `json:"required"`
// The sources that can be used to acquire the field value. Only one of these needs to be used.
Sources []LoginCookieFieldSource `json:"sources"`
// A regex pattern that the client can use to validate value client-side.
Pattern string `json:"pattern,omitempty"`
}
type LoginCookiesParams struct {
URL string `json:"url"`
UserAgent string `json:"user_agent,omitempty"`
// The fields that are needed for this cookie login.
Fields []LoginCookieField `json:"fields"`
// A JavaScript snippet that can extract some or all of the fields.
// The snippet will evaluate to a promise that resolves when the relevant fields are found.
// Fields that are not present in the promise result must be extracted another way.
ExtractJS string `json:"extract_js,omitempty"`
// A regex pattern that the URL should match before the client closes the webview.
//
// The client may submit the login if the user closes the webview after all cookies are collected
// even if this URL is not reached, but it should only automatically close the webview after
// both cookies and the URL match.
WaitForURLPattern string `json:"wait_for_url_pattern,omitempty"`
}
type LoginInputFieldType string
const (
LoginInputFieldTypeUsername LoginInputFieldType = "username"
LoginInputFieldTypePassword LoginInputFieldType = "password"
LoginInputFieldTypePhoneNumber LoginInputFieldType = "phone_number"
LoginInputFieldTypeEmail LoginInputFieldType = "email"
LoginInputFieldType2FACode LoginInputFieldType = "2fa_code"
LoginInputFieldTypeToken LoginInputFieldType = "token"
LoginInputFieldTypeURL LoginInputFieldType = "url"
LoginInputFieldTypeDomain LoginInputFieldType = "domain"
LoginInputFieldTypeSelect LoginInputFieldType = "select"
LoginInputFieldTypeCaptchaCode LoginInputFieldType = "captcha_code"
)
type LoginInputDataField struct {
// The type of input field as a hint for the client.
Type LoginInputFieldType `json:"type"`
// The ID of the field to be used as the key in the map that is submitted to the connector.
ID string `json:"id"`
// The name of the field shown to the user.
Name string `json:"name"`
// The description of the field shown to the user.
Description string `json:"description"`
// A default value that the client can pre-fill the field with.
DefaultValue string `json:"default_value,omitempty"`
// A regex pattern that the client can use to validate input client-side.
Pattern string `json:"pattern,omitempty"`
// For fields of type select, the valid options.
// Pattern may also be filled with a regex that matches the same options.
Options []string `json:"options,omitempty"`
// A function that validates the input and optionally cleans it up before it's submitted to the connector.
Validate func(string) (string, error) `json:"-"`
}
var numberCleaner = strings.NewReplacer("-", "", " ", "", "(", "", ")", "")
func isOnlyNumbers(input string) bool {
for _, r := range input {
if r < '0' || r > '9' {
return false
}
}
return true
}
func CleanNonInternationalPhoneNumber(phone string) (string, error) {
phone = numberCleaner.Replace(phone)
if !isOnlyNumbers(strings.TrimPrefix(phone, "+")) {
return "", fmt.Errorf("phone number must only contain numbers")
}
return phone, nil
}
func CleanPhoneNumber(phone string) (string, error) {
phone = numberCleaner.Replace(phone)
if len(phone) < 2 {
return "", fmt.Errorf("phone number must start with + and contain numbers")
} else if phone[0] != '+' {
return "", fmt.Errorf("phone number must start with +")
} else if !isOnlyNumbers(phone[1:]) {
return "", fmt.Errorf("phone number must only contain numbers")
}
return phone, nil
}
func noopValidate(input string) (string, error) {
return input, nil
}
func (f *LoginInputDataField) FillDefaultValidate() {
if f.Validate != nil {
return
}
switch f.Type {
case LoginInputFieldTypePhoneNumber:
f.Validate = CleanPhoneNumber
case LoginInputFieldTypeEmail:
f.Validate = func(email string) (string, error) {
if !strings.ContainsRune(email, '@') {
return "", fmt.Errorf("invalid email")
}
return email, nil
}
default:
if f.Pattern != "" {
f.Validate = func(s string) (string, error) {
match, err := regexp.MatchString(f.Pattern, s)
if err != nil {
return "", err
} else if !match {
return "", fmt.Errorf("doesn't match regex `%s`", f.Pattern)
} else {
return s, nil
}
}
} else {
f.Validate = noopValidate
}
}
}
type LoginUserInputParams struct {
// The fields that the user needs to fill in.
Fields []LoginInputDataField `json:"fields"`
// Attachments to display alongside the input fields.
Attachments []*LoginUserInputAttachment `json:"attachments"`
}
type LoginUserInputAttachment struct {
Type event.MessageType `json:"type,omitempty"`
FileName string `json:"filename,omitempty"`
Content []byte `json:"content,omitempty"`
Info LoginUserInputAttachmentInfo `json:"info,omitempty"`
}
type LoginUserInputAttachmentInfo struct {
MimeType string `json:"mimetype,omitempty"`
Width int `json:"w,omitempty"`
Height int `json:"h,omitempty"`
Size int `json:"size,omitempty"`
}
type LoginCompleteParams struct {
UserLoginID networkid.UserLoginID `json:"user_login_id"`
UserLogin *UserLogin `json:"-"`
}
type LoginSubmit struct {
}

View file

@ -0,0 +1,62 @@
package matrix
import (
"bytes"
"encoding/json"
"fmt"
"net/http"
"maunium.net/go/mautrix/id"
)
func (br *Connector) trackSync(userID id.UserID, event string, properties map[string]any) error {
var buf bytes.Buffer
var analyticsUserID string
if br.Config.Analytics.UserID != "" {
analyticsUserID = br.Config.Analytics.UserID
} else {
analyticsUserID = userID.String()
}
err := json.NewEncoder(&buf).Encode(map[string]any{
"userId": analyticsUserID,
"event": event,
"properties": properties,
})
if err != nil {
return err
}
req, err := http.NewRequest(http.MethodPost, br.Config.Analytics.URL, &buf)
if err != nil {
return err
}
req.SetBasicAuth(br.Config.Analytics.Token, "")
resp, err := br.AS.HTTPClient.Do(req)
if err != nil {
return err
}
_ = resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return fmt.Errorf("unexpected status code %d", resp.StatusCode)
}
return nil
}
func (br *Connector) TrackAnalytics(userID id.UserID, event string, props map[string]any) {
if br.Config.Analytics.Token == "" || br.Config.Analytics.URL == "" {
return
}
if props == nil {
props = map[string]any{}
}
props["bridge"] = br.Bridge.Network.GetName().BeeperBridgeType
go func() {
err := br.trackSync(userID, event, props)
if err != nil {
br.Log.Err(err).Str("component", "analytics").Str("event", event).Msg("Error tracking event")
} else {
br.Log.Debug().Str("component", "analytics").Str("event", event).Msg("Tracked event")
}
}()
}

View file

@ -1,36 +1,38 @@
// Copyright (c) 2022 Tulir Asokan
// 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 commands
package matrix
import (
"strconv"
"maunium.net/go/mautrix/bridgev2/commands"
"maunium.net/go/mautrix/id"
)
var CommandDiscardMegolmSession = &FullHandler{
Func: func(ce *Event) {
if ce.Bridge.Crypto == nil {
var CommandDiscardMegolmSession = &commands.FullHandler{
Func: func(ce *commands.Event) {
matrix := ce.Bridge.Matrix.(*Connector)
if matrix.Crypto == nil {
ce.Reply("This bridge instance doesn't have end-to-bridge encryption enabled")
} else {
ce.Bridge.Crypto.ResetSession(ce.Ctx, ce.RoomID)
matrix.Crypto.ResetSession(ce.Ctx, ce.RoomID)
ce.Reply("Successfully reset Megolm session in this room. New decryption keys will be shared the next time a message is sent from the remote network.")
}
},
Name: "discard-megolm-session",
Aliases: []string{"discard-session"},
Help: HelpMeta{
Section: HelpSectionAdmin,
Help: commands.HelpMeta{
Section: commands.HelpSectionAdmin,
Description: "Discard the Megolm session in the room",
},
RequiresAdmin: true,
}
func fnSetPowerLevel(ce *Event) {
func fnSetPowerLevel(ce *commands.Event) {
var level int
var userID id.UserID
var err error
@ -40,7 +42,7 @@ func fnSetPowerLevel(ce *Event) {
ce.Reply("Invalid power level \"%s\"", ce.Args[0])
return
}
userID = ce.User.GetMXID()
userID = ce.User.MXID
} else if len(ce.Args) == 2 {
userID = id.UserID(ce.Args[0])
_, _, err := userID.Parse()
@ -57,18 +59,18 @@ func fnSetPowerLevel(ce *Event) {
ce.Reply("**Usage:** `set-pl [user] <level>`")
return
}
_, err = ce.Portal.MainIntent().SetPowerLevel(ce.Ctx, ce.RoomID, userID, level)
_, err = ce.Bot.(*ASIntent).Matrix.SetPowerLevel(ce.Ctx, ce.RoomID, userID, level)
if err != nil {
ce.Reply("Failed to set power levels: %v", err)
}
}
var CommandSetPowerLevel = &FullHandler{
var CommandSetPowerLevel = &commands.FullHandler{
Func: fnSetPowerLevel,
Name: "set-pl",
Aliases: []string{"set-power-level"},
Help: HelpMeta{
Section: HelpSectionAdmin,
Help: commands.HelpMeta{
Section: commands.HelpSectionAdmin,
Description: "Change the power level in a portal room.",
Args: "[_user ID_] <_power level_>",
},

View file

@ -0,0 +1,90 @@
// 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 matrix
import (
"maunium.net/go/mautrix/bridgev2/commands"
)
var CommandLoginMatrix = &commands.FullHandler{
Func: fnLoginMatrix,
Name: "login-matrix",
Help: commands.HelpMeta{
Section: commands.HelpSectionAuth,
Description: "Enable double puppeting.",
Args: "<_access token_>",
},
RequiresLogin: true,
}
func fnLoginMatrix(ce *commands.Event) {
if !ce.User.Permissions.DoublePuppet {
ce.Reply("You don't have permission to manage double puppeting.")
return
}
if len(ce.Args) == 0 {
ce.Reply("**Usage:** `login-matrix <access token>`")
return
}
err := ce.User.LoginDoublePuppet(ce.Ctx, ce.Args[0])
if err != nil {
ce.Reply("Failed to enable double puppeting: %v", err)
} else {
ce.Reply("Successfully switched puppets")
}
}
var CommandPingMatrix = &commands.FullHandler{
Func: fnPingMatrix,
Name: "ping-matrix",
Help: commands.HelpMeta{
Section: commands.HelpSectionAuth,
Description: "Ping the Matrix server with the double puppet.",
},
}
func fnPingMatrix(ce *commands.Event) {
intent := ce.User.DoublePuppet(ce.Ctx)
if intent == nil {
ce.Reply("You don't have double puppeting enabled.")
return
}
asIntent := intent.(*ASIntent)
resp, err := asIntent.Matrix.Whoami(ce.Ctx)
if err != nil {
ce.Reply("Failed to validate Matrix login: %v", err)
} else {
if asIntent.Matrix.SetAppServiceUserID && resp.DeviceID == "" {
ce.Reply("Confirmed valid access token for %s (appservice double puppeting)", resp.UserID)
} else {
ce.Reply("Confirmed valid access token for %s / %s", resp.UserID, resp.DeviceID)
}
}
}
var CommandLogoutMatrix = &commands.FullHandler{
Func: fnLogoutMatrix,
Name: "logout-matrix",
Help: commands.HelpMeta{
Section: commands.HelpSectionAuth,
Description: "Disable double puppeting.",
},
RequiresLogin: true,
}
func fnLogoutMatrix(ce *commands.Event) {
if !ce.User.Permissions.DoublePuppet {
ce.Reply("You don't have permission to manage double puppeting.")
return
}
if ce.User.AccessToken == "" {
ce.Reply("You don't have double puppeting enabled.")
return
}
ce.User.LogoutDoublePuppet(ce.Ctx)
ce.Reply("Successfully disabled double puppeting.")
}

View file

@ -0,0 +1,758 @@
// 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 matrix
import (
"context"
"crypto/sha256"
"encoding/base64"
"errors"
"fmt"
"net/http"
"net/url"
"os"
"regexp"
"strings"
"sync"
"time"
_ "github.com/lib/pq"
"github.com/rs/zerolog"
"go.mau.fi/util/dbutil"
_ "go.mau.fi/util/dbutil/litestream"
"go.mau.fi/util/exbytes"
"go.mau.fi/util/exsync"
"go.mau.fi/util/ptr"
"go.mau.fi/util/random"
"golang.org/x/sync/semaphore"
"maunium.net/go/mautrix"
"maunium.net/go/mautrix/appservice"
"maunium.net/go/mautrix/bridgev2"
"maunium.net/go/mautrix/bridgev2/bridgeconfig"
"maunium.net/go/mautrix/bridgev2/commands"
"maunium.net/go/mautrix/bridgev2/database"
"maunium.net/go/mautrix/bridgev2/networkid"
"maunium.net/go/mautrix/bridgev2/status"
"maunium.net/go/mautrix/event"
"maunium.net/go/mautrix/id"
"maunium.net/go/mautrix/mediaproxy"
"maunium.net/go/mautrix/sqlstatestore"
)
type Crypto interface {
HandleMemberEvent(context.Context, *event.Event)
Decrypt(context.Context, *event.Event) (*event.Event, error)
Encrypt(context.Context, id.RoomID, event.Type, *event.Content) 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)
ResetSession(context.Context, id.RoomID)
Init(ctx context.Context) error
Start()
Stop()
Reset(ctx context.Context, startAfterReset bool)
Client() *mautrix.Client
ShareKeys(context.Context) error
}
type Connector struct {
AS *appservice.AppService
Bot *appservice.IntentAPI
StateStore *sqlstatestore.SQLStateStore
Crypto Crypto
Log *zerolog.Logger
Config *bridgeconfig.Config
Bridge *bridgev2.Bridge
Provisioning *ProvisioningAPI
DoublePuppet *doublePuppetUtil
MediaProxy *mediaproxy.MediaProxy
uploadSema *semaphore.Weighted
dmaSigKey [32]byte
pubMediaSigKey []byte
doublePuppetIntents *exsync.Map[id.UserID, *appservice.IntentAPI]
deterministicEventIDServer string
MediaConfig mautrix.RespMediaConfig
SpecVersions *mautrix.RespVersions
SpecCaps *mautrix.RespCapabilities
specCapsLock sync.Mutex
Capabilities *bridgev2.MatrixCapabilities
IgnoreUnsupportedServer bool
EventProcessor *appservice.EventProcessor
userIDRegex *regexp.Regexp
Websocket bool
wsStopPinger chan struct{}
wsStarted chan struct{}
wsStopped chan struct{}
wsShortCircuitReconnectBackoff chan struct{}
wsStartupWait *sync.WaitGroup
stopping bool
hasSentAnyStates bool
OnWebsocketReplaced func()
}
var (
_ bridgev2.MatrixConnector = (*Connector)(nil)
_ bridgev2.MatrixConnectorWithServer = (*Connector)(nil)
_ bridgev2.MatrixConnectorWithArbitraryRoomState = (*Connector)(nil)
_ bridgev2.MatrixConnectorWithPostRoomBridgeHandling = (*Connector)(nil)
_ bridgev2.MatrixConnectorWithPublicMedia = (*Connector)(nil)
_ bridgev2.MatrixConnectorWithNameDisambiguation = (*Connector)(nil)
_ bridgev2.MatrixConnectorWithURLPreviews = (*Connector)(nil)
_ bridgev2.MatrixConnectorWithAnalytics = (*Connector)(nil)
)
func NewConnector(cfg *bridgeconfig.Config) *Connector {
c := &Connector{}
c.Config = cfg
c.userIDRegex = cfg.MakeUserIDRegex("(.+)")
c.MediaConfig.UploadSize = 50 * 1024 * 1024
c.uploadSema = semaphore.NewWeighted(c.MediaConfig.UploadSize + 1)
c.Capabilities = &bridgev2.MatrixCapabilities{}
c.doublePuppetIntents = exsync.NewMap[id.UserID, *appservice.IntentAPI]()
return c
}
func (br *Connector) Init(bridge *bridgev2.Bridge) {
br.Bridge = bridge
br.Log = &bridge.Log
br.StateStore = sqlstatestore.NewSQLStateStore(bridge.DB.Database, dbutil.ZeroLogger(br.Log.With().Str("db_section", "matrix_state").Logger()), false)
br.AS = br.Config.MakeAppService()
br.AS.Log = bridge.Log
br.AS.StateStore = br.StateStore
br.EventProcessor = appservice.NewEventProcessor(br.AS)
if !br.Config.AppService.AsyncTransactions {
br.EventProcessor.ExecMode = appservice.Sync
}
for evtType := range status.CheckpointTypes {
br.EventProcessor.On(evtType, br.sendBridgeCheckpoint)
}
br.EventProcessor.On(event.EventMessage, br.handleRoomEvent)
br.EventProcessor.On(event.EventSticker, br.handleRoomEvent)
br.EventProcessor.On(event.EventUnstablePollStart, br.handleRoomEvent)
br.EventProcessor.On(event.EventUnstablePollResponse, br.handleRoomEvent)
br.EventProcessor.On(event.EventReaction, br.handleRoomEvent)
br.EventProcessor.On(event.EventRedaction, br.handleRoomEvent)
br.EventProcessor.On(event.EventEncrypted, br.handleEncryptedEvent)
br.EventProcessor.On(event.EphemeralEventEncrypted, br.handleEncryptedEvent)
br.EventProcessor.On(event.StateMember, br.handleRoomEvent)
br.EventProcessor.On(event.StatePowerLevels, br.handleRoomEvent)
br.EventProcessor.On(event.StateRoomName, br.handleRoomEvent)
br.EventProcessor.On(event.BeeperSendState, br.handleRoomEvent)
br.EventProcessor.On(event.StateRoomAvatar, br.handleRoomEvent)
br.EventProcessor.On(event.StateTopic, br.handleRoomEvent)
br.EventProcessor.On(event.StateTombstone, br.handleRoomEvent)
br.EventProcessor.On(event.StateBeeperDisappearingTimer, br.handleRoomEvent)
br.EventProcessor.On(event.BeeperDeleteChat, br.handleRoomEvent)
br.EventProcessor.On(event.BeeperAcceptMessageRequest, br.handleRoomEvent)
br.EventProcessor.On(event.EphemeralEventReceipt, br.handleEphemeralEvent)
br.EventProcessor.On(event.EphemeralEventTyping, br.handleEphemeralEvent)
br.EventProcessor.On(event.BeeperEphemeralEventAIStream, br.handleEphemeralEvent)
br.Bot = br.AS.BotIntent()
br.Crypto = NewCryptoHelper(br)
br.Bridge.Commands.(*commands.Processor).AddHandlers(
CommandDiscardMegolmSession, CommandSetPowerLevel,
CommandLoginMatrix, CommandPingMatrix, CommandLogoutMatrix,
)
br.Provisioning = &ProvisioningAPI{br: br}
br.DoublePuppet = newDoublePuppetUtil(br)
br.deterministicEventIDServer = "backfill." + br.Config.Homeserver.Domain
}
func (br *Connector) Start(ctx context.Context) error {
br.Provisioning.Init()
err := br.initDirectMedia()
if err != nil {
return err
}
err = br.initPublicMedia()
if err != nil {
return err
}
needsStateResync := br.Config.Encryption.Default &&
br.Bridge.DB.KV.Get(ctx, database.KeyEncryptionStateResynced) != "true"
if needsStateResync {
dbExists, err := br.StateStore.TableExists(ctx, "mx_version")
if err != nil {
return fmt.Errorf("failed to check if mx_version table exists: %w", err)
} else if !dbExists {
needsStateResync = false
br.Bridge.DB.KV.Set(ctx, database.KeyEncryptionStateResynced, "true")
}
}
err = br.StateStore.Upgrade(ctx)
if err != nil {
return bridgev2.DBUpgradeError{Section: "matrix_state", Err: err}
}
if br.Config.Homeserver.Websocket || len(br.Config.Homeserver.WSProxy) > 0 {
br.Websocket = true
br.Log.Debug().Msg("Starting appservice websocket")
var wg sync.WaitGroup
wg.Add(1)
br.wsStartupWait = &wg
br.wsShortCircuitReconnectBackoff = make(chan struct{})
go br.startWebsocket(&wg)
} else if br.AS.Host.IsConfigured() {
br.Log.Debug().Msg("Starting appservice HTTP server")
go br.AS.Start()
} else {
br.Log.WithLevel(zerolog.FatalLevel).Msg("Neither appservice HTTP listener nor websocket is enabled")
os.Exit(23)
}
br.Log.Debug().Msg("Checking connection to homeserver")
br.ensureConnection(ctx)
go br.fetchMediaConfig(ctx)
if br.Crypto != nil {
err = br.Crypto.Init(ctx)
if err != nil {
return err
}
}
br.EventProcessor.Start(ctx)
go br.UpdateBotProfile(ctx)
if br.Crypto != nil {
go br.Crypto.Start()
}
parsed, _ := url.Parse(br.Bridge.Network.GetName().NetworkURL)
if parsed != nil {
br.deterministicEventIDServer = strings.TrimPrefix(parsed.Hostname(), "www.")
}
br.AS.Ready = true
if br.Websocket && br.Config.Homeserver.WSPingInterval > 0 {
br.wsStopPinger = make(chan struct{}, 1)
go br.websocketServerPinger()
}
if needsStateResync {
br.ResyncEncryptionState(ctx)
}
return nil
}
func (br *Connector) ResyncEncryptionState(ctx context.Context) {
log := zerolog.Ctx(ctx)
roomIDScanner := dbutil.ConvertRowFn[id.RoomID](dbutil.ScanSingleColumn[id.RoomID])
rooms, err := roomIDScanner.NewRowIter(br.Bridge.DB.Query(ctx, `
SELECT rooms.room_id
FROM (SELECT DISTINCT(room_id) FROM mx_user_profile WHERE room_id<>'') rooms
LEFT JOIN mx_room_state ON rooms.room_id = mx_room_state.room_id
WHERE mx_room_state.encryption IS NULL
`)).AsList()
if err != nil {
log.Err(err).Msg("Failed to get room list to resync state")
return
}
var failedCount, successCount, forbiddenCount int
for _, roomID := range rooms {
if roomID == "" {
continue
}
var outContent *event.EncryptionEventContent
err = br.Bot.Client.StateEvent(ctx, roomID, event.StateEncryption, "", &outContent)
if errors.Is(err, mautrix.MForbidden) {
// Most likely non-existent room
log.Debug().Err(err).Stringer("room_id", roomID).Msg("Failed to get state for room")
forbiddenCount++
} else if err != nil {
log.Err(err).Stringer("room_id", roomID).Msg("Failed to get state for room")
failedCount++
} else {
successCount++
}
}
br.Bridge.DB.KV.Set(ctx, database.KeyEncryptionStateResynced, "true")
log.Info().
Int("success_count", successCount).
Int("forbidden_count", forbiddenCount).
Int("failed_count", failedCount).
Msg("Resynced rooms")
}
func (br *Connector) GetPublicAddress() string {
if br.Config.AppService.PublicAddress == "https://bridge.example.com" {
return ""
}
return strings.TrimRight(br.Config.AppService.PublicAddress, "/")
}
func (br *Connector) GetRouter() *http.ServeMux {
if br.GetPublicAddress() != "" {
return br.AS.Router
}
return nil
}
func (br *Connector) GetCapabilities() *bridgev2.MatrixCapabilities {
return br.Capabilities
}
func sendStopSignal(ch chan struct{}) {
if ch != nil {
select {
case ch <- struct{}{}:
default:
}
}
}
func (br *Connector) PreStop() {
br.stopping = true
br.AS.Stop()
if stopWebsocket := br.AS.StopWebsocket; stopWebsocket != nil {
stopWebsocket(appservice.ErrWebsocketManualStop)
}
sendStopSignal(br.wsStopPinger)
sendStopSignal(br.wsShortCircuitReconnectBackoff)
}
func (br *Connector) Stop() {
br.EventProcessor.Stop()
if br.Crypto != nil {
br.Crypto.Stop()
}
if wsStopChan := br.wsStopped; wsStopChan != nil {
select {
case <-wsStopChan:
case <-time.After(4 * time.Second):
br.Log.Warn().Msg("Timed out waiting for websocket to close")
}
}
}
var MinSpecVersion = mautrix.SpecV14
func (br *Connector) logInitialRequestError(err error, defaultMessage string) {
if errors.Is(err, mautrix.MUnknownToken) {
br.Log.WithLevel(zerolog.FatalLevel).Msg("The as_token was not accepted. Is the registration file installed in your homeserver correctly?")
br.Log.Info().Msg("See https://docs.mau.fi/faq/as-token for more info")
} else if errors.Is(err, mautrix.MExclusive) {
br.Log.WithLevel(zerolog.FatalLevel).Msg("The as_token was accepted, but the /register request was not. Are the homeserver domain, bot username and username template in the config correct, and do they match the values in the registration?")
br.Log.Info().Msg("See https://docs.mau.fi/faq/as-register for more info")
} else {
br.Log.WithLevel(zerolog.FatalLevel).Err(err).Msg(defaultMessage)
}
}
func (br *Connector) ensureConnection(ctx context.Context) {
triedToRegister := false
for {
versions, err := br.Bot.Versions(ctx)
if err != nil {
if errors.Is(err, mautrix.MForbidden) && !triedToRegister {
br.Log.Debug().Msg("M_FORBIDDEN in /versions, trying to register before retrying")
err = br.Bot.EnsureRegistered(ctx)
if err != nil {
br.logInitialRequestError(err, "Failed to register after /versions failed with M_FORBIDDEN")
os.Exit(16)
}
triedToRegister = true
} else if errors.Is(err, mautrix.MUnknownToken) || errors.Is(err, mautrix.MExclusive) {
br.logInitialRequestError(err, "/versions request failed with auth error")
os.Exit(16)
} else {
br.Log.Err(err).Msg("Failed to connect to homeserver, retrying in 10 seconds...")
time.Sleep(10 * time.Second)
}
} else {
br.SpecVersions = versions
*br.AS.SpecVersions = *versions
br.Capabilities.AutoJoinInvites = br.SpecVersions.Supports(mautrix.BeeperFeatureAutojoinInvites)
br.Capabilities.BatchSending = br.SpecVersions.Supports(mautrix.BeeperFeatureBatchSending)
br.Capabilities.ArbitraryMemberChange = br.SpecVersions.Supports(mautrix.BeeperFeatureArbitraryMemberChange)
br.Capabilities.ExtraProfileMeta = br.SpecVersions.Supports(mautrix.BeeperFeatureArbitraryProfileMeta) ||
(br.SpecVersions.Supports(mautrix.FeatureArbitraryProfileFields) && br.Config.Matrix.GhostExtraProfileInfo)
break
}
}
unsupportedServerLogLevel := zerolog.FatalLevel
if br.IgnoreUnsupportedServer {
unsupportedServerLogLevel = zerolog.ErrorLevel
}
if br.Config.Homeserver.Software == bridgeconfig.SoftwareHungry && !br.SpecVersions.Supports(mautrix.BeeperFeatureHungry) {
br.Log.WithLevel(zerolog.FatalLevel).Msg("The config claims the homeserver is hungryserv, but the /versions response didn't confirm it")
os.Exit(18)
} else if !br.SpecVersions.ContainsGreaterOrEqual(MinSpecVersion) {
br.Log.WithLevel(unsupportedServerLogLevel).
Stringer("server_supports", br.SpecVersions.GetLatest()).
Stringer("bridge_requires", MinSpecVersion).
Msg("The homeserver is outdated (supported spec versions are below minimum required by bridge)")
if !br.IgnoreUnsupportedServer {
os.Exit(18)
}
}
resp, err := br.Bot.Whoami(ctx)
if err != nil {
br.logInitialRequestError(err, "/whoami request failed with unknown error")
os.Exit(16)
} else if resp.UserID != br.Bot.UserID {
br.Log.WithLevel(zerolog.FatalLevel).
Stringer("got_user_id", resp.UserID).
Stringer("expected_user_id", br.Bot.UserID).
Msg("Unexpected user ID in whoami call")
os.Exit(17)
}
if br.Websocket {
br.Log.Debug().Msg("Websocket mode: no need to check status of homeserver -> bridge connection")
return
} else if !br.SpecVersions.Supports(mautrix.FeatureAppservicePing) {
br.Log.Debug().Msg("Homeserver does not support checking status of homeserver -> bridge connection")
return
}
br.Bot.EnsureAppserviceConnection(ctx)
}
func (br *Connector) fetchCapabilities(ctx context.Context) *mautrix.RespCapabilities {
br.specCapsLock.Lock()
defer br.specCapsLock.Unlock()
if br.SpecCaps != nil {
return br.SpecCaps
}
caps, err := br.Bot.Capabilities(ctx)
if err != nil {
br.Log.Err(err).Msg("Failed to fetch capabilities from homeserver")
return nil
}
br.SpecCaps = caps
return caps
}
func (br *Connector) fetchMediaConfig(ctx context.Context) {
cfg, err := br.Bot.GetMediaConfig(ctx)
if err != nil {
br.Log.Warn().Err(err).Msg("Failed to fetch media config")
} else {
if cfg.UploadSize == 0 {
cfg.UploadSize = 50 * 1024 * 1024
}
br.MediaConfig = *cfg
mfsn, ok := br.Bridge.Network.(bridgev2.MaxFileSizeingNetwork)
if ok {
mfsn.SetMaxFileSize(br.MediaConfig.UploadSize)
}
br.uploadSema = semaphore.NewWeighted(br.MediaConfig.UploadSize + 1)
}
}
func (br *Connector) UpdateBotProfile(ctx context.Context) {
br.Log.Debug().Msg("Updating bot profile")
botConfig := &br.Config.AppService.Bot
var err error
var mxc id.ContentURI
if botConfig.Avatar == "remove" {
err = br.Bot.SetAvatarURL(ctx, mxc)
} else if !botConfig.ParsedAvatar.IsEmpty() {
err = br.Bot.SetAvatarURL(ctx, botConfig.ParsedAvatar)
}
if err != nil {
br.Log.Warn().Err(err).Msg("Failed to update bot avatar")
}
if botConfig.Displayname == "remove" {
err = br.Bot.SetDisplayName(ctx, "")
} else if len(botConfig.Displayname) > 0 {
err = br.Bot.SetDisplayName(ctx, botConfig.Displayname)
}
if err != nil {
br.Log.Warn().Err(err).Msg("Failed to update bot displayname")
}
if br.SpecVersions.Supports(mautrix.BeeperFeatureArbitraryProfileMeta) {
br.Log.Debug().Msg("Setting contact info on the appservice bot")
netName := br.Bridge.Network.GetName()
err = br.Bot.BeeperUpdateProfile(ctx, event.BeeperProfileExtra{
Service: netName.BeeperBridgeType,
Network: netName.NetworkID,
IsBridgeBot: true,
})
if err != nil {
br.Log.Warn().Err(err).Msg("Failed to update bot contact info")
}
}
}
func (br *Connector) GhostIntent(userID networkid.UserID) bridgev2.MatrixAPI {
return &ASIntent{
Matrix: br.AS.Intent(br.FormatGhostMXID(userID)),
Connector: br,
}
}
func (br *Connector) SendBridgeStatus(ctx context.Context, state *status.BridgeState) error {
if br.Websocket {
br.hasSentAnyStates = true
return br.AS.SendWebsocket(ctx, &appservice.WebsocketRequest{
Command: "bridge_status",
Data: state,
})
} else if br.Config.Homeserver.StatusEndpoint != "" {
// Connecting states aren't really relevant unless the bridge runs somewhere with an unreliable network
if state.StateEvent == status.StateConnecting {
return nil
}
return state.SendHTTP(ctx, br.Config.Homeserver.StatusEndpoint, br.Config.AppService.ASToken)
} else {
return nil
}
}
func (br *Connector) SendMessageStatus(ctx context.Context, ms *bridgev2.MessageStatus, evt *bridgev2.MessageStatusEventInfo) {
go br.internalSendMessageStatus(ctx, ms, evt, "")
}
func (br *Connector) internalSendMessageStatus(ctx context.Context, ms *bridgev2.MessageStatus, evt *bridgev2.MessageStatusEventInfo, editEvent id.EventID) id.EventID {
if evt.EventType.IsEphemeral() || evt.SourceEventID == "" {
return ""
}
log := zerolog.Ctx(ctx)
if !evt.IsSourceEventDoublePuppeted {
err := br.SendMessageCheckpoints(ctx, []*status.MessageCheckpoint{ms.ToCheckpoint(evt)})
if err != nil {
log.Err(err).Msg("Failed to send message checkpoint")
}
}
if !ms.DisableMSS && br.Config.Matrix.MessageStatusEvents {
mssEvt := ms.ToMSSEvent(evt)
_, err := br.Bot.SendMessageEvent(ctx, evt.RoomID, event.BeeperMessageStatus, mssEvt)
if err != nil {
log.Err(err).
Stringer("room_id", evt.RoomID).
Stringer("event_id", evt.SourceEventID).
Any("mss_content", mssEvt).
Msg("Failed to send MSS event")
}
}
if ms.SendNotice && br.Config.Matrix.MessageErrorNotices && evt.MessageType != event.MsgNotice &&
(ms.Status == event.MessageStatusFail || ms.Status == event.MessageStatusRetriable || ms.Step == status.MsgStepDecrypted) {
content := ms.ToNoticeEvent(evt)
if editEvent != "" {
content.SetEdit(editEvent)
}
resp, err := br.Bot.SendMessageEvent(ctx, evt.RoomID, event.EventMessage, content)
if err != nil {
log.Err(err).
Stringer("room_id", evt.RoomID).
Stringer("event_id", evt.SourceEventID).
Str("notice_message", content.Body).
Msg("Failed to send notice event")
} else {
return resp.EventID
}
}
if ms.Status == event.MessageStatusSuccess && br.Config.Matrix.DeliveryReceipts {
err := br.Bot.SendReceipt(ctx, evt.RoomID, evt.SourceEventID, event.ReceiptTypeRead, nil)
if err != nil {
log.Err(err).
Stringer("room_id", evt.RoomID).
Stringer("event_id", evt.SourceEventID).
Msg("Failed to send Matrix delivery receipt")
}
}
return ""
}
func (br *Connector) SendMessageCheckpoints(ctx context.Context, checkpoints []*status.MessageCheckpoint) error {
checkpointsJSON := status.CheckpointsJSON{Checkpoints: checkpoints}
if br.Websocket {
return br.AS.SendWebsocket(ctx, &appservice.WebsocketRequest{
Command: "message_checkpoint",
Data: checkpointsJSON,
})
}
endpoint := br.Config.Homeserver.MessageSendCheckpointEndpoint
if endpoint == "" {
return nil
}
return checkpointsJSON.SendHTTP(ctx, br.AS.HTTPClient, endpoint, br.AS.Registration.AppToken)
}
func (br *Connector) ParseGhostMXID(userID id.UserID) (networkid.UserID, bool) {
match := br.userIDRegex.FindStringSubmatch(string(userID))
if match == nil || userID == br.Bot.UserID {
return "", false
}
decoded, err := id.DecodeUserLocalpart(match[1])
if err != nil {
return "", false
}
return networkid.UserID(decoded), true
}
func (br *Connector) FormatGhostMXID(userID networkid.UserID) id.UserID {
localpart := br.Config.AppService.FormatUsername(id.EncodeUserLocalpart(string(userID)))
return id.NewUserID(localpart, br.Config.Homeserver.Domain)
}
func (br *Connector) NewUserIntent(ctx context.Context, userID id.UserID, accessToken string) (bridgev2.MatrixAPI, string, error) {
intent, newToken, err := br.DoublePuppet.Setup(ctx, userID, accessToken)
if err != nil {
if errors.Is(err, ErrNoAccessToken) {
err = nil
}
return nil, accessToken, err
}
br.doublePuppetIntents.Set(userID, intent)
return &ASIntent{Connector: br, Matrix: intent}, newToken, nil
}
func (br *Connector) BotIntent() bridgev2.MatrixAPI {
return &ASIntent{Connector: br, Matrix: br.Bot}
}
func (br *Connector) GetPowerLevels(ctx context.Context, roomID id.RoomID) (*event.PowerLevelsEventContent, error) {
return br.Bot.PowerLevels(ctx, roomID)
}
func (br *Connector) GetStateEvent(ctx context.Context, roomID id.RoomID, eventType event.Type, stateKey string) (*event.Event, error) {
if stateKey == "" {
switch eventType {
case event.StateCreate:
createEvt, err := br.Bot.StateStore.GetCreate(ctx, roomID)
if err != nil || createEvt != nil {
return createEvt, err
}
case event.StateJoinRules:
joinRulesContent, err := br.Bot.StateStore.GetJoinRules(ctx, roomID)
if err != nil {
return nil, err
} else if joinRulesContent != nil {
return &event.Event{
Type: event.StateJoinRules,
RoomID: roomID,
StateKey: ptr.Ptr(""),
Content: event.Content{Parsed: joinRulesContent},
}, nil
}
}
}
return br.Bot.FullStateEvent(ctx, roomID, eventType, stateKey)
}
func (br *Connector) GetMembers(ctx context.Context, roomID id.RoomID) (map[id.UserID]*event.MemberEventContent, error) {
fetched, err := br.Bot.StateStore.HasFetchedMembers(ctx, roomID)
if err != nil {
return nil, err
} else if fetched {
return br.Bot.StateStore.GetAllMembers(ctx, roomID)
}
members, err := br.Bot.Members(ctx, roomID)
if err != nil {
return nil, err
}
output := make(map[id.UserID]*event.MemberEventContent, len(members.Chunk))
for _, evt := range members.Chunk {
output[id.UserID(evt.GetStateKey())] = evt.Content.AsMember()
}
return output, nil
}
func (br *Connector) GetMemberInfo(ctx context.Context, roomID id.RoomID, userID id.UserID) (*event.MemberEventContent, error) {
// TODO fetch from network sometimes?
return br.AS.StateStore.GetMember(ctx, roomID, userID)
}
func (br *Connector) IsConfusableName(ctx context.Context, roomID id.RoomID, userID id.UserID, name string) ([]id.UserID, error) {
return br.AS.StateStore.IsConfusableName(ctx, roomID, userID, name)
}
func (br *Connector) GetUniqueBridgeID() string {
return fmt.Sprintf("%s/%s", br.Config.Homeserver.Domain, br.Config.AppService.ID)
}
func (br *Connector) BatchSend(ctx context.Context, roomID id.RoomID, req *mautrix.ReqBeeperBatchSend, extras []*bridgev2.MatrixSendExtra) (*mautrix.RespBeeperBatchSend, error) {
if encrypted, err := br.StateStore.IsEncrypted(ctx, roomID); err != nil {
return nil, fmt.Errorf("failed to check if room is encrypted: %w", err)
} else if encrypted {
for _, evt := range req.Events {
intent, _ := br.doublePuppetIntents.Get(evt.Sender)
if intent != nil {
intent.AddDoublePuppetValueWithTS(&evt.Content, evt.Timestamp)
}
if evt.Type != event.EventEncrypted && evt.Type != event.EventReaction {
err = br.Crypto.Encrypt(ctx, roomID, evt.Type, &evt.Content)
if err != nil {
return nil, err
}
evt.Type = event.EventEncrypted
if intent != nil {
intent.AddDoublePuppetValueWithTS(&evt.Content, evt.Timestamp)
}
}
}
}
return br.Bot.BeeperBatchSend(ctx, roomID, req)
}
func (br *Connector) GenerateDeterministicEventID(roomID id.RoomID, _ networkid.PortalKey, messageID networkid.MessageID, partID networkid.PartID) id.EventID {
data := make([]byte, 0, len(roomID)+1+len(messageID)+1+len(partID))
data = append(data, roomID...)
data = append(data, 0)
data = append(data, messageID...)
data = append(data, 0)
data = append(data, partID...)
hash := sha256.Sum256(data)
hashB64Len := base64.RawURLEncoding.EncodedLen(len(hash))
eventID := make([]byte, 1+hashB64Len+1+len(br.deterministicEventIDServer))
eventID[0] = '$'
base64.RawURLEncoding.Encode(eventID[1:1+hashB64Len], hash[:])
eventID[1+hashB64Len] = ':'
copy(eventID[1+hashB64Len+1:], br.deterministicEventIDServer)
return id.EventID(exbytes.UnsafeString(eventID))
}
func (br *Connector) GenerateDeterministicRoomID(key networkid.PortalKey) id.RoomID {
return id.RoomID(fmt.Sprintf("!%s.%s:%s", key.ID, key.Receiver, br.ServerName()))
}
func (br *Connector) GenerateReactionEventID(roomID id.RoomID, targetMessage *database.Message, sender networkid.UserID, emojiID networkid.EmojiID) id.EventID {
// We don't care about determinism for reactions
return id.EventID(fmt.Sprintf("$%s:%s", base64.RawURLEncoding.EncodeToString(random.Bytes(32)), br.deterministicEventIDServer))
}
func (br *Connector) ServerName() string {
return br.Config.Homeserver.Domain
}
func (br *Connector) HandleNewlyBridgedRoom(ctx context.Context, roomID id.RoomID) error {
_, err := br.Bot.Members(ctx, roomID)
if err != nil {
zerolog.Ctx(ctx).Err(err).Msg("Failed to fetch members in newly bridged room")
}
if !br.Config.Encryption.Default {
return nil
}
_, err = br.Bot.SendStateEvent(ctx, roomID, event.StateEncryption, "", &event.Content{
Parsed: br.getDefaultEncryptionEvent(),
})
if err != nil {
zerolog.Ctx(ctx).Err(err).Msg("Failed to enable encryption in newly bridged room")
return fmt.Errorf("failed to enable encryption")
}
return nil
}
func (br *Connector) GetURLPreview(ctx context.Context, url string) (*event.LinkPreview, error) {
return br.Bot.GetURLPreview(ctx, url)
}

Some files were not shown because too many files have changed in this diff Show more