Compare commits

...

1176 Commits

Author SHA1 Message Date
Tulir Asokan e3bb26aee1 handlematrix: allow bridging cached custom emoji reactions with any scheme 2026-04-30 16:55:03 +03:00
Tulir Asokan 7c2c72bbde imagepack: implement listing interface 2026-04-30 15:49:10 +03:00
Tulir Asokan 2ffbde7448 .github: add another item to bug report template 2026-04-30 13:23:26 +03:00
Tulir Asokan 2a0da7801a imagepack: move emoji shortcodes to go-util 2026-04-30 12:24:08 +03:00
Tulir Asokan eaf387abfe imagepack: switch to bridgev2 API for importing 2026-04-29 18:01:36 +03:00
Tulir Asokan 64d80c3d1d imagepack: populate cache when importing pack 2026-04-29 16:18:52 +03:00
Tulir Asokan c78b1abd2d imagepack: use emoji shortcode as fallback when importing packs 2026-04-29 14:51:38 +03:00
Tulir Asokan 12f900a7bd dependencies: update mautrix-go 2026-04-29 09:10:02 +03:00
Tulir Asokan cdb77f938a tomatrix: include external_url field in messages 2026-04-28 22:01:54 +03:00
Tulir Asokan 5a1a478992 matrixfmt: convert matrix.to links in other direction too 2026-04-28 21:46:07 +03:00
Tulir Asokan d2a06ebbbe capabilities: mark lottie and webm as allowed sticker formats 2026-04-28 16:09:13 +03:00
Tulir Asokan e6243d8935 imagepack: switch to new shared metadata field 2026-04-27 20:24:10 +03:00
Tulir Asokan 9e1c42a992 matrixfmt: fix trimming all-space entity string 2026-04-27 20:24:10 +03:00
Tulir Asokan 6eacf38d74 tomatrix: use extra field in info for custom fields 2026-04-27 20:24:10 +03:00
Gerardo Rodriguez 65fcf712d3 client: treat pool.ErrConnDead as transient in onPing (#1066) 2026-04-24 13:58:43 +03:00
Tulir Asokan 8512cfe6a6 commands/imagepack: include pack metadata in sticker info 2026-04-23 14:26:52 +03:00
Tulir Asokan 7a6d1bf17a dependencies: update mautrix-go 2026-04-20 23:22:06 +03:00
Tulir Asokan 18f831553d changelog: update 2026-04-20 16:51:51 +03:00
Tulir Asokan dce0c4dbe1 handletelegram: add support for updateBotMessageReaction
Fixes #1064
2026-04-19 17:30:20 +03:00
Tulir Asokan ac2a2c2980 legacymigrate: fix mx_room_state migration on sqlite 2026-04-16 23:11:15 +03:00
Tulir Asokan b9f0962881 Bump version to v26.04 2026-04-16 16:45:33 +03:00
lavacat d7864fcd3a client: add initial proxy support (#1062) 2026-04-16 13:14:29 +03:00
Tulir Asokan 0f0b21b22c dependencies: update mautrix-go 2026-04-15 15:32:24 +03:00
Tulir Asokan 1dd11f1086 dependencies: update mautrix-go 2026-04-14 20:09:17 +03:00
Tulir Asokan 3f155672a7 login: always set update handler 2026-04-14 17:26:33 +03:00
Tulir Asokan b761f04621 dependencies: update mautrix-go 2026-04-14 14:39:13 +03:00
Tulir Asokan 95db7a6d0d tomatrix: fix default file names 2026-04-13 16:48:19 +03:00
Tulir Asokan 8b3707b0ee handlematrix: redact previous sponsored message when sending new one 2026-04-13 12:24:02 +03:00
Tulir Asokan 4d46c5ee7c tomatrix: use m.image for bridging document images 2026-04-12 01:03:06 +03:00
Tulir Asokan 009ce8c0d3 handlematrix: remove unnecessary nil checks 2026-04-11 19:58:39 +03:00
Tulir Asokan a06b7d607d handlematrix: add video document attribute 2026-04-11 19:56:45 +03:00
Tulir Asokan 659b0f82ae dependencies: update mautrix-go 2026-04-10 23:14:46 +03:00
Tulir Asokan 53dec19878 login: increase buffer for QR renewal 2026-04-10 22:58:16 +03:00
Tulir Asokan a5b1927acb handletelegram: don't sync empty reactions on new messages 2026-04-10 20:33:31 +03:00
Tulir Asokan 0988de1267 tomatrix: consistently add extensions for all files 2026-04-10 20:19:16 +03:00
Tulir Asokan 5c1975808a tomatrix: add extension to unnamed documents from telegram 2026-04-10 20:01:30 +03:00
Tulir Asokan 2b79d842a4 dependencies: update mautrix-go 2026-04-10 20:01:17 +03:00
Tulir Asokan 6e08539d57 dependencies: update mautrix-go 2026-04-10 17:15:49 +03:00
Tulir Asokan 634cec5ba9 tomatrix: avoid multipart messages 2026-04-10 14:43:37 +03:00
Tulir Asokan 506e13f6b8 commands: remove extra dots 2026-04-10 13:21:29 +03:00
Tulir Asokan e55e596d68 commands/join: allow different invite hash lengths 2026-04-10 13:20:48 +03:00
Tulir Asokan 11495e6e7e client: fix handling mentions of non-logged-in users 2026-04-09 23:32:34 +03:00
Tulir Asokan 53574754be dependencies: update mautrix-go again 2026-04-09 22:21:06 +03:00
Tulir Asokan 3f725b37b9 dependencies: update mautrix-go 2026-04-09 21:55:08 +03:00
Tulir Asokan 2ad36b6366 legacymigrate: recreate mx_room_state on sqlite only 2026-04-09 13:15:47 +03:00
Tulir Asokan fe119542f8 Revert "legacymigrate: recreate mx_user_profile table to work around broken schemas on sqlite"
This reverts commit 98f24f9b5e.
2026-04-09 13:12:37 +03:00
Tulir Asokan 98f24f9b5e legacymigrate: recreate mx_user_profile table to work around broken schemas on sqlite 2026-04-09 13:00:25 +03:00
Tulir Asokan 60e3cf9c01 gotd/tgerr: reduce default flood wait max duration 2026-04-08 00:41:22 +03:00
Tulir Asokan cc32d48fea backfill: add support for forward backfilling more than 100 messages 2026-04-08 00:41:22 +03:00
Tulir Asokan 117c5cd0ce tomatrix: always add extension for photos 2026-04-06 18:00:08 +03:00
Tulir Asokan 41f2166feb tomatrix: fix adding per-message profile for channel messages 2026-04-06 00:38:31 +03:00
Tulir Asokan abbcacec4b readme: remove broken link 2026-04-05 21:25:00 +03:00
Tulir Asokan 92fdf7b8e9 all: fix inconsistent method receiver names 2026-04-05 21:22:22 +03:00
Tulir Asokan f13af2ef54 userinfo: add missing changed condition 2026-04-05 21:20:51 +03:00
Tulir Asokan 0172a5733b tomatrix: add support for partial quotes 2026-04-04 22:37:22 +03:00
Tulir Asokan 8350230693 legacymigrate: add create_event column to mx_room_state if necessary 2026-04-04 18:30:17 +03:00
Tulir Asokan c3216f1e4d commands/join: include chat name in response 2026-04-03 23:41:34 +03:00
Tulir Asokan 0bee8da0f8 commands: add join group command 2026-04-03 21:59:32 +03:00
Tulir Asokan 795b27275f startchat: allow username links when starting DMs 2026-04-03 21:51:56 +03:00
Tulir Asokan ba7d51e785 .github: add checklist to bug report template
[skip ci]
2026-04-03 18:16:29 +03:00
Tulir Asokan cbff082e4d config: re-add displayname template
Fixes #1057
2026-04-03 15:09:36 +03:00
Tulir Asokan 9b92aa3d50 dependencies: update mautrix-go 2026-04-03 12:13:07 +03:00
Tulir Asokan e96f12cfea changelog: mention how to fix management rooms
Fixes #1058
2026-04-03 12:12:23 +03:00
Tulir Asokan e7099d26f3 handletelegram: set dont_render_edited flag 2026-04-03 01:42:20 +03:00
Tulir Asokan 8b68fdce79 handlematrix: fix delete chat error messages 2026-04-03 00:46:09 +03:00
Tulir Asokan 1dc01bcffd handlematrix: ignore more updates in send response 2026-04-03 00:46:09 +03:00
Tulir Asokan dbab7f0ee4 commands: add upgrade command 2026-04-03 00:46:09 +03:00
Tulir Asokan 835afb0100 matrixfmt,telegramfmt: correctly bridge mentions of other logged-in users 2026-04-02 23:57:27 +03:00
Tulir Asokan 693ced7dea handletelegram: add workaround for instantly deleted messages 2026-04-02 23:53:06 +03:00
Tulir Asokan b6c7b0e78b chatinfo: always bridge own power level in channels 2026-04-02 00:51:47 +03:00
Tulir Asokan 3b939f423a handletelegram: don't refetch channel info if the event already contains it 2026-04-02 00:51:47 +03:00
Tulir Asokan f4555782cf handlematrix: fetch sponsored messages in channels after read receipt 2026-04-02 00:51:47 +03:00
Tulir Asokan 1ff046db0b changelog,roadmap: update 2026-04-01 21:36:14 +03:00
Tulir Asokan a44cc41933 tomatrix: bridge live photos as videos 2026-04-01 21:22:58 +03:00
Tulir Asokan 770b3b8d8c gotd: update to layer 224 2026-04-01 21:08:49 +03:00
Tulir Asokan 7630340ffc dependencies: update mautrix-go 2026-03-31 19:35:58 +03:00
Tulir Asokan dc2b16e1a6 changelog: add more words
Fixes #1050
Fixes #1051

[skip ci]
2026-03-31 14:21:58 +03:00
Tulir Asokan f7b220f711 reactions: handle reactions sent by channels 2026-03-30 23:15:47 +03:00
Tulir Asokan e37619e826 reactions: deduplicate reaction sync code 2026-03-30 23:06:01 +03:00
Tulir Asokan 0dc5045b00 handletelegram: don't sync reactions on channel message edit 2026-03-30 22:57:43 +03:00
Tulir Asokan c8c5c7d272 matrixfmt: fix bridging code blocks 2026-03-30 21:28:55 +03:00
Tulir Asokan c358222ab4 store: fix telegram_file migration delete query on sqlite 2026-03-30 20:57:48 +03:00
Tulir Asokan fdebeb9ca8 handletelegram: avoid redundant getCustomEmojiDocuments calls 2026-03-30 18:06:28 +03:00
Tulir Asokan bd6d40cad0 handlematrix: also read edit responses properly 2026-03-30 17:58:35 +03:00
Tulir Asokan 358318c734 commands/imagepack: use double puppet for fetching matrix packs 2026-03-30 17:41:22 +03:00
Tulir Asokan a0323a5233 handletelegram: handle UpdateMessageReactions 2026-03-30 17:20:19 +03:00
Tulir Asokan 3500590f11 handlematrix: fix reading send response 2026-03-30 17:05:42 +03:00
Tulir Asokan 011894f7b4 store: add support for slightly older postgres versions 2026-03-30 16:44:08 +03:00
Tulir Asokan af9ce963a9 userinfo: sync ghost info for non-broadcast channels too 2026-03-30 14:42:58 +03:00
Tulir Asokan 50a1c21fd1 dependencies: update mautrix-go 2026-03-30 12:58:28 +03:00
Tulir Asokan 606bf92ab1 commands/imagepack: catch both errors
(why are there two with the same description?)
2026-03-30 12:58:06 +03:00
Tulir Asokan 9b7ee5e2c3 commands/imagepack: fail when reaching pack size limit 2026-03-29 22:33:02 +03:00
Tulir Asokan e4195fadb4 commands/imagepack: add missing image/gif import 2026-03-29 22:20:21 +03:00
Tulir Asokan 7cf65b6f6a commands/imagepack: always use decoded dimensions 2026-03-29 22:15:28 +03:00
Tulir Asokan a5a3b9f380 commands/imagepack: deduplicate mxcs in same pack 2026-03-29 22:12:33 +03:00
Tulir Asokan 58d99a806a commands/imagepack: also ignore re-encoding errors 2026-03-29 22:06:56 +03:00
Tulir Asokan acf716c031 commands/imagepack: add duration length for animated emojis 2026-03-29 22:03:06 +03:00
Tulir Asokan 8de8170619 commands/imagepack: ignore images that fail to be added to pack 2026-03-29 22:03:03 +03:00
Tulir Asokan b2f99ec5c0 commands: fix image pack upload room id check 2026-03-29 21:57:02 +03:00
Tulir Asokan ec960a7372 commands: allow spaces in image pack state keys 2026-03-29 21:50:34 +03:00
Tulir Asokan 6d085f477e store: delete conflicting telegram_file rows 2026-03-29 21:38:18 +03:00
Tulir Asokan e68ef24657 commands: add support for bridging image packs 2026-03-29 21:32:58 +03:00
Tulir Asokan f7cbf751a0 store: fix GetByMXC parameter type 2026-03-29 17:44:59 +03:00
Tulir Asokan d124008443 store: fix latest version number 2026-03-29 17:13:02 +03:00
Tulir Asokan 190e65edfb media: read dimensions from file if needed 2026-03-29 17:02:13 +03:00
Tulir Asokan 4f4680b19a store: add more info to telegram_file table 2026-03-29 16:37:06 +03:00
Tulir Asokan c46a3189e0 legacymigrate: delete conflicting index 2026-03-29 16:29:25 +03:00
Tulir Asokan b084627248 commands: remove unnecessary sync types 2026-03-29 14:36:47 +03:00
Tulir Asokan ce70aacdb8 handletelegram: don't log presence updates 2026-03-29 14:25:33 +03:00
Tulir Asokan b6aff6784f matrixfmt: add support for sending pre-bridged custom emojis in text 2026-03-29 14:25:23 +03:00
Tulir Asokan 5d05d7ab05 store: normalize ids in telegram_file and add index 2026-03-29 14:05:17 +03:00
Tulir Asokan 5c37b186d8 config: clarify contact_names option 2026-03-29 14:04:32 +03:00
Tulir Asokan 0881e76205 client: fix link parser log levels 2026-03-29 13:43:30 +03:00
Tulir Asokan c5cdde83e4 tomatrix: include image mime type in url previews 2026-03-29 13:36:56 +03:00
Tulir Asokan 8bd4ff8f82 tomatrix: use channel ghost if portal not found 2026-03-29 13:36:25 +03:00
Tulir Asokan 4a538f77ef gotd: log download response length 2026-03-29 13:11:33 +03:00
Tulir Asokan f64b605443 media: assume thumbnails are jpeg 2026-03-29 13:09:56 +03:00
Tulir Asokan dbe9be2102 userinfo: ignore non-applyMinPhoto avatars 2026-03-29 02:01:26 +02:00
Tulir Asokan 0727857ed0 readme,changelog: update 2026-03-29 01:30:58 +02:00
Tulir Asokan c97c5f6bec misc: remove unused files 2026-03-29 00:27:43 +02:00
Tulir Asokan 01357fe5df ci: switch to v2-as-default script 2026-03-29 00:26:40 +02:00
Tulir Asokan d8188743ba Merge branch 'master' 2026-03-29 00:22:08 +02:00
Tulir Asokan 6d373885d2 dependencies: update mautrix-go 2026-03-28 23:58:52 +02:00
Tulir Asokan 1cd589dbd1 legacymigrate: update ghost metadata fields 2026-03-28 22:44:40 +02:00
Tulir Asokan a96bf7ed95 userinfo: remove redundant custom is bot field 2026-03-28 22:44:24 +02:00
Tulir Asokan bb405b4773 dependencies: update 2026-03-28 22:43:32 +02:00
Tulir Asokan b43adb6bab gotd: update readme
[skip ci]
2026-03-28 16:58:28 +02:00
Tulir Asokan abae7b2854 gotd: assume any response is an ack 2026-03-28 16:50:51 +02:00
Tulir Asokan 472b9df44c gotd: fix infinite loop if server keeps replying with timeout to download request 2026-03-28 16:50:51 +02:00
Nick Mills-Barrett bec7ee8f5e push: only send direct notifications if we have data (#133) 2026-03-24 10:12:04 +00:00
Tulir Asokan ae5f2f3093 dependencies: update mautrix-go again 2026-03-22 12:25:23 +02:00
Tulir Asokan 3910e44639 dependencies: update mautrix-go 2026-03-22 12:18:01 +02:00
Tulir Asokan 50aefd6897 dependencies: update mautrix-go 2026-03-22 00:06:34 +02:00
Tulir Asokan b17bb0d5c7 client: resume chat list sync after restart 2026-03-19 16:48:06 +02:00
Tulir Asokan 64724aa654 commands: restart dialog sync on command 2026-03-19 16:15:44 +02:00
Tulir Asokan 800c15f7b7 backfill: retry takeout if it gets invalidated 2026-03-19 16:14:04 +02:00
Tulir Asokan 7f71e5f09c chatinfo: look inside channelParticipantBanned 2026-03-19 14:16:53 +02:00
Tulir Asokan bfe5999951 chatsync: merge post-login and takeout syncs and refactor everything 2026-03-19 13:13:01 +02:00
Tulir Asokan b1b5745033 gotd: add max duration and log for flood wait 2026-03-19 01:36:24 +02:00
Tulir Asokan 98936fdf7a media: fix sticker dimensions 2026-03-18 21:31:53 +02:00
Tulir Asokan 7baed1c77b handletelegram: don't update remote profile with min info 2026-03-18 21:25:59 +02:00
Tulir Asokan b695e0b4ea tomatrix: add forward headers 2026-03-17 12:01:20 +02:00
Tulir Asokan 326906644e telegramfmt: ignore unrecognized entities 2026-03-16 21:54:21 +02:00
Tulir Asokan 0122ab91d6 dependencies: update mautrix-go 2026-03-16 21:54:14 +02:00
Tulir Asokan 0d818303f4 userinfo: don't apply min names 2026-03-16 17:20:17 +02:00
Tulir Asokan 7fa51da335 dependencies: update mautrix-go 2026-03-16 16:49:23 +02:00
Tulir Asokan 49d99aff82 userinfo: save source_is_contact flag properly 2026-03-15 21:27:13 +02:00
Tulir Asokan cfd9b74d34 userinfo: prefer non-contacts as info source 2026-03-15 20:39:15 +02:00
Tulir Asokan d067348ac5 commands: fix sync command section 2026-03-15 20:38:57 +02:00
Tulir Asokan 84392278c2 legacymigrate: update mx_room_state version 2026-03-15 18:53:10 +02:00
Tulir Asokan 0aed201869 userinfo: add support for avoiding contact names/avatars 2026-03-15 16:14:55 +02:00
Tulir Asokan 62efa2e7b9 userinfo: add support for getting user info via InputUserFromMessage 2026-03-15 12:49:21 +02:00
Tulir Asokan 58f40aeba5 userinfo: use min access hashes for avatars 2026-03-15 12:28:14 +02:00
Tulir Asokan 29000146ba directmedia: fix panic if url preview has no photo 2026-03-11 01:21:59 +02:00
Tulir Asokan a9cb55d109 dependencies: update mautrix-go 2026-03-06 21:10:23 +02:00
Tulir Asokan f7ae7ba804 dependencies: update mautrix-go 2026-03-04 13:58:51 +02:00
Tulir Asokan 0e45edd1f4 gotd: always set field in logger 2026-03-04 01:59:56 +02:00
Tulir Asokan 7fb4539885 gotd: don't log uploaded bytes 2026-03-03 18:13:32 +02:00
Tulir Asokan 42465f1aca handlematrix: add todo for avatar handling 2026-03-03 18:13:32 +02:00
Tulir Asokan 5bf7461566 handlematrix: convert webp images to jpeg 2026-03-03 18:13:32 +02:00
Tulir Asokan a84dd2f30c handletelegram: add log for stuck update handlers 2026-03-03 16:23:50 +02:00
Tulir Asokan 67adededff gotd/message: fix generators and update entity utilities 2026-03-03 15:16:44 +02:00
Tulir Asokan e5914196c5 gotd: update to layer 223 2026-03-03 15:13:10 +02:00
Tulir Asokan 189dbdfc52 gotd: move update dispatcher out of generator 2026-03-03 15:09:05 +02:00
Tulir Asokan 7738fc21f5 handletelegram,gotd: add missing log context 2026-03-03 14:34:02 +02:00
Tulir Asokan 4511c82cb0 gotd: only update server time offset once 2026-03-03 13:16:25 +02:00
Tulir Asokan 6af986ded5 gotd: add time synchronization 2026-02-26 18:24:51 +02:00
Tulir Asokan 93fe3cb0ea media: adjust log level of transfer log 2026-02-26 17:48:15 +02:00
Tulir Asokan 52b2373528 dependencies: update 2026-02-16 15:41:08 +02:00
Tulir Asokan a59c755dd8 login: fix context used for starting takeout 2026-02-11 13:25:32 +02:00
Tulir Asokan 4793b01a29 docker: update to Alpine 3.23 2026-02-11 00:10:56 +02:00
Tulir Asokan e8114ff5ad Pin setuptools version 2026-02-11 00:08:59 +02:00
Tulir Asokan e597eace68 login: allow retrying phone codes and 2fa passwords (#131) 2026-02-10 16:49:49 +02:00
SpiritCroc 4071502854 handletelegram: assign beeper action message content for incoming calls (#132) 2026-01-23 16:45:27 +01:00
Tulir Asokan c7a7f6ec20 dependencies: update mautrix-go 2026-01-20 12:29:37 +02:00
Tulir Asokan 8f3cc2e28e media: fix lottie stickers not being decompressed 2026-01-19 19:59:10 +02:00
Tulir Asokan 6700403118 dependencies: update 2026-01-17 01:23:17 +02:00
Tulir Asokan f4830c71d8 reactions: ignore channel reactions when polling 2026-01-12 15:02:25 +02:00
Tulir Asokan 78ba8e4d45 gotd: add missing getDifference retries 2026-01-07 16:11:41 +02:00
Tulir Asokan 2b1cfae52f tomatrix: fix sticker metadata 2025-12-29 23:25:08 +02:00
Tulir Asokan 274141a1b0 client: add lock for onTransfer
There should never be multiple clients to actually need this, but just in case
2025-12-29 23:21:08 +02:00
Tulir Asokan cac1f5acde gotd: retry auth transfers on AUTH_BYTES_INVALID error 2025-12-29 23:15:40 +02:00
Tulir Asokan 56fe704934 gotd: don't log file download responses 2025-12-29 23:15:40 +02:00
Tulir Asokan 3696c9cff4 handletelegram: don't fail migrateChat if getting chat info fails 2025-12-24 00:42:57 +02:00
Tulir Asokan d83c0ede15 dependencies: update mautrix-go 2025-12-19 13:33:17 +02:00
Tulir Asokan b5d6e2ac6b Revert "handletelegram: do portal re-id in background"
This reverts commit 37d34a4ab6.
2025-12-19 13:33:01 +02:00
Tulir Asokan 37d34a4ab6 handletelegram: do portal re-id in background 2025-12-19 13:25:20 +02:00
Tulir Asokan baba8bd712 handletelegram: improve chat migrate logs 2025-12-19 13:08:54 +02:00
Tulir Asokan 7573e3d5a7 gotd: return fatal errors from all getDifference calls 2025-12-18 17:32:51 +02:00
Tulir Asokan a887f26023 gotd: retry non-fatal errors in getDifference calls 2025-12-18 16:48:03 +02:00
Tulir Asokan ced0a2d067 gotd: don't emit duplicate updates for channels 2025-12-18 16:36:23 +02:00
Tulir Asokan 09227510bc startchat: clean up resolving identifiers 2025-12-17 12:48:11 +02:00
Tulir Asokan dc2a422bbe handletelegram: move raw update logging 2025-12-17 12:47:25 +02:00
Tulir Asokan 49bb93bdc2 dependencies: update mautrix-go again 2025-12-16 19:30:18 +02:00
Tulir Asokan a3ec7f7c33 dependencies: update mautrix-go 2025-12-16 18:58:10 +02:00
Tulir Asokan cd660472d8 reactions,telegramfmt: remove unnecessary warning logs 2025-12-16 18:52:43 +02:00
Tulir Asokan 20446d0d7d gotd: fix logging response payload 2025-12-16 17:33:43 +02:00
Tulir Asokan 769a397a03 tomatrix: don't use portal disappearing timer for incoming messages 2025-12-16 17:32:59 +02:00
Tulir Asokan 1dde2a4a77 changelog: update 2025-12-16 17:32:59 +02:00
Tulir Asokan 85e1a6dc05 telegramfmt: merge duplicate cases 2025-12-16 17:11:59 +02:00
Tulir Asokan e9d95a6e9a reactions: remove incorrect error 2025-12-16 17:11:47 +02:00
Tulir Asokan e1497999d6 dependencies: update mautrix-go for portal deletion fixes 2025-12-16 17:11:34 +02:00
Tulir Asokan dd63436149 changelog: fix merge with legacy bridge 2025-12-16 14:45:24 +02:00
Tulir Asokan dd34256f3d telegramfmt: fix message link urls losing text 2025-12-15 21:23:01 +02:00
Tulir Asokan 7f17c2728e client: use context rather than separate flag for expected disconnects 2025-12-15 16:23:44 +02:00
Tulir Asokan 4342635b8a ci: update actions and pre-commit hooks 2025-12-13 11:11:05 +02:00
Tulir Asokan 80ccbeb449 dependencies: update mautrix-go 2025-12-13 11:10:37 +02:00
Tulir Asokan 283dfc5c77 Merge branch 'master'
[skip cd]
2025-12-12 19:08:41 +02:00
Tulir Asokan d00de62ee7 dependencies: update mautrix-go 2025-12-12 17:32:21 +02:00
Tulir Asokan 1a2fd67ee9 gotd: remove extra wrapping in check participant error 2025-12-12 17:26:41 +02:00
Tulir Asokan 4873ed77ff client: disconnect on auth error 2025-12-12 17:26:16 +02:00
Tulir Asokan d6a8e6a648 gotd: don't return run context error from channel state 2025-12-12 16:16:06 +02:00
Tulir Asokan 095bd65d51 gotd: add extra cache for left channels 2025-12-12 16:15:49 +02:00
Tulir Asokan cd9970055f gotd: fix channel membership check not doing anything 2025-12-12 16:15:26 +02:00
Tulir Asokan 3663f91c8a gotd: add more error wrapping around update loop 2025-12-12 16:04:40 +02:00
Tulir Asokan 0c3749a2ca gotd: don't start getDifference for left channels 2025-12-12 15:57:14 +02:00
Tulir Asokan ba4dd48d5a gotd: ensure user is member of channels before starting getDifference loop 2025-12-12 15:45:39 +02:00
Tulir Asokan c1d92ce051 startchat: fix getting cached contact list 2025-12-12 14:39:13 +02:00
Tulir Asokan ae05331420 startchat: don't allow spamming get contact list request 2025-12-12 14:30:06 +02:00
Tulir Asokan ef65f9f1ea client: log main context status on disconnect 2025-12-12 13:19:44 +02:00
Tulir Asokan 29d8c1b7dd client: send error state if client.Run returns unexpectedly 2025-12-12 13:12:33 +02:00
Tulir Asokan 4775e67476 client: adjust start/stop logs 2025-12-11 15:29:42 +02:00
Tulir Asokan 43b230148b client: don't drop errors from client.Run() 2025-12-11 15:01:12 +02:00
Tulir Asokan d03260c4a7 gotd/updates: initialize channel state runctx immediately 2025-12-11 14:55:43 +02:00
Tulir Asokan 042304f147 dependencies: update 2025-12-11 14:20:54 +02:00
Tulir Asokan de2e87ed52 client,gotd: remove unnecessary dispatcher wrapper 2025-12-11 14:07:48 +02:00
Tulir Asokan 581ba79c84 handletelegram,gotd: stop get difference polling after leaving channel 2025-12-11 13:56:48 +02:00
Tulir Asokan 390f9f422e backfill: clear saved takeout ID on takeout invalid error 2025-12-10 19:47:51 +02:00
Tulir Asokan f80d6de818 gotd: use constants for error strings 2025-12-10 19:39:56 +02:00
Tulir Asokan 69fcbd30ce gotd: don't stop connection on channel error 2025-12-10 19:17:50 +02:00
Tulir Asokan 0e3b1b63a9 gotd/updates: stop listening to channel on ChannelForbidden/Invalid 2025-12-10 19:14:08 +02:00
Tulir Asokan 7f13284b59 gotd: remove redundant closures and improve logs on disconnect 2025-12-10 18:34:07 +02:00
Tulir Asokan 4268ee9909 gotd/transport: add default read/write deadlines 2025-12-10 18:33:37 +02:00
Tulir Asokan 35bf11c158 loginqr: remove unused field 2025-12-08 14:22:34 +02:00
Tulir Asokan 08703e9efb loginqr: fix context used for background request 2025-12-08 14:20:19 +02:00
Tulir Asokan e13502750e handletelegram: log edit content 2025-12-08 00:22:02 +02:00
Tulir Asokan 0da121aebb ids: fix reaction ids 2025-12-08 00:20:06 +02:00
Tulir Asokan d07d2af048 handletelegram: don't try to get app config on bot accounts 2025-12-07 21:24:51 +02:00
Tulir Asokan 76e06d4a33 emojis: initialize maps lazily
Closes #99
2025-12-07 21:24:26 +02:00
Tulir Asokan 10f1583da9 login: add support for bot tokens 2025-12-07 20:06:30 +02:00
Tulir Asokan 48fed1c026 login: refactor to share more code 2025-12-07 20:02:19 +02:00
Tulir Asokan abb4671a16 client: add shortcut field for user login metadata 2025-12-07 20:02:19 +02:00
Tulir Asokan 6729a9ad09 main: switch versioning scheme 2025-12-06 15:30:52 +02:00
Tulir Asokan 96b2afeed1 dbmeta: remove disallowed fields in ghosts 2025-12-06 15:27:51 +02:00
Tulir Asokan d5f87d2ec1 all: add support for topics and refactor other things 2025-12-06 15:27:51 +02:00
Tulir Asokan 14b3b1fed7 handletelegram: adjust some message handling code 2025-12-06 01:02:55 +02:00
Tulir Asokan 16a57d78ac handletelegram: flatten service message handling 2025-12-05 23:51:17 +02:00
Tulir Asokan 548672d243 client: simplify IsLoggedIn check 2025-12-05 23:50:44 +02:00
Tulir Asokan ef23946cbc store: remove redundant index 2025-12-05 23:45:41 +02:00
Tulir Asokan 9b2b691afd handletelegram: resync channel on update event 2025-12-05 23:29:12 +02:00
Tulir Asokan c04866c854 reactions: flatten poll code 2025-12-05 15:39:00 +02:00
Tulir Asokan fa28593635 handlematrix: don't block read receipt handler on reaction poll 2025-12-05 15:39:00 +02:00
Tulir Asokan 9dd8f30480 handlematrix: fix reaction polling logic
Only supergroups need it. The map also needed a lock
2025-12-05 14:55:52 +02:00
Tulir Asokan 19c3121e77 login*: apply zap log level shifting 2025-12-04 16:32:26 +02:00
Tulir Asokan 6232a27881 startchat: don't allow starting chat without access hash 2025-12-04 16:13:04 +02:00
Tulir Asokan 526903cb7c userinfo: refactor GetUserInfo and remote profile handling 2025-12-04 15:39:46 +02:00
Tulir Asokan 2cac8f8b4a client,gotd: refactor connection event handling
This might cause regressions if the onSession handler was load bearing
2025-12-04 14:53:35 +02:00
Tulir Asokan c83a361c0b gotd: reduce unnecessary debug logs 2025-12-04 14:44:13 +02:00
Tulir Asokan c6dd85040c client: fix parsing username message links 2025-12-03 23:23:07 +02:00
Tulir Asokan 08a2fe9753 chatinfo: refactor processing group chat info 2025-12-03 22:34:13 +02:00
Tulir Asokan 2580e28bee media: refactor sticker conversion 2025-12-03 22:15:59 +02:00
Tulir Asokan b7e5078053 chatinfo: ensure own member is always added 2025-12-03 17:44:18 +02:00
Tulir Asokan 8bef95e237 chatinfo,backfill,tomatrix: downgrade unnecessary warnings 2025-12-03 17:11:20 +02:00
Tulir Asokan 04a10f361a gotd: skip broken manager test 2025-12-03 17:11:20 +02:00
Tulir Asokan fed5752f38 handletelegram: don't return errors from message converter 2025-12-03 17:11:20 +02:00
Tulir Asokan 35c161185c directdownload,tomatrix: add missing nil checks 2025-12-03 17:11:20 +02:00
Tulir Asokan 1ecb9e8b64 login*: set device config on login clients 2025-12-03 17:11:20 +02:00
Tulir Asokan 2004085312 connector: fix import ordering 2025-12-03 17:11:20 +02:00
ip75 abd5d058ff gotd: add filename to AudioDocumentBuilder
Cherry-picked from https://github.com/gotd/td/commit/52e0fcb1f655e7c1c09ae45e41fa333422cc4cab
2025-12-03 17:11:20 +02:00
Oleksii Kyslytsia 09185e8e53 gotd: handle all login token response types in QR login
Cherry-picked from https://github.com/gotd/td/commit/4c22747e9a0299b457f45084c3dbcbcbb5a7a5e7
2025-12-03 17:11:20 +02:00
Vadim Tertilov 097211cba1 gotd: add fallback handlers
Cherry-picked from https://github.com/gotd/td/commit/3238f7e7d3623ecd3648e2124f962c4ea0d03134
2025-12-03 17:11:20 +02:00
Tulir Asokan 8e7a7db85f humanise: update error list and move generator 2025-12-03 17:11:20 +02:00
Tulir Asokan 55f8d1423b gotd: update hash computing sort 2025-12-03 17:11:20 +02:00
Tulir Asokan 66b84a7b44 gotd: update to layer 218 2025-12-03 17:11:20 +02:00
Tulir Asokan b38c3cc935 gotd: invoke with layer every time
Cherry-picked from upstream PR 1640. The reason is that tracking whether
the Telegram servers received our layer number is hard, so it's more
reliable to just always send it.
2025-12-03 17:11:19 +02:00
Tulir Asokan b7459ec9eb .gitattributes: exclude gotd generated files 2025-12-03 17:11:19 +02:00
Tulir Asokan 5eb883e934 dependencies: update 2025-12-03 17:11:19 +02:00
1Conan b9d19a3aad handlematrix: implement RoomAvatarHandlingNetworkAPI 2025-11-26 01:27:41 +08:00
1Conan 74a5dfccd5 handlematrix: implement RoomNameHandlingNetworkAPI 2025-11-26 01:25:50 +08:00
Conan b3f9bfb5b3 handlematrix: implement group chat deletes (#126) 2025-11-20 01:22:02 +08:00
Nick Mills-Barrett ca46d36998 media: default unknown media mime to application/octet-stream
Keeps the photo jpeg default but only for photos so we don't incorrectly
flag unknown extensions as jpegs.
2025-11-18 11:22:39 +00:00
Tulir Asokan 795a732720 dependencies: update mautrix-go 2025-10-27 20:54:08 +02:00
Conan 3aa7bdfa91 handlematrix: fix delete chat (#125) 2025-10-28 02:34:24 +08:00
Tulir Asokan 14f40abeca config: update default saved message avatar 2025-10-19 23:37:16 +03:00
Conan b1f3c4c1db handlematrix: Implement DeleteChatHandlingNetworkAPI (#122) 2025-10-07 21:26:10 +08:00
Tulir Asokan 4410415776 chatinfo: flag group resyncs as excluded from timeline 2025-10-01 16:17:06 +03:00
Adam Van Ymeren a38c3e5d00 resync: resync portals upon viewing if they haven't been synced in the last 24h (#124) 2025-09-25 12:34:41 -07:00
Tulir Asokan a280c3a4b9 dependencies: update mautrix-go 2025-09-25 00:15:43 +03:00
Tulir Asokan 280c74e9cd Add config option to self-sign bot device 2025-09-24 00:08:57 +03:00
Tulir Asokan eb5bfb4666 chatinfo: set power level for disappearing timer event 2025-09-17 14:43:53 +03:00
Tulir Asokan fcace69cbd capabilities: fix definition of a month 2025-09-17 00:17:14 +03:00
Tulir Asokan f48737c894 capabilities: fix disappearing timer capability 2025-09-16 23:04:31 +03:00
Tulir Asokan d359bafb53 connector: rename files to be more consistent with other bridges 2025-09-16 22:47:48 +03:00
Tulir Asokan 44be515705 matrix: use Normalize instead of manually changing disappearing type 2025-09-16 22:43:22 +03:00
Tulir Asokan 34683e6d1b handletelegram: fix disabling disappearing timer 2025-09-13 01:39:45 +03:00
Tulir Asokan 8f998cd9cb dependencies: update mautrix-go 2025-09-12 21:09:41 +03:00
Adam Van Ymeren 9706deb27d media: always give the unsupported notice if we don't yet handle the type 2025-09-12 09:22:43 -07:00
Adam Van Ymeren 233516ca4d media: Don't error on MessageMediaPaidMedia, just bridge unsupported notice 2025-09-12 09:17:17 -07:00
Tulir Asokan 0051042555 database: migrate disappearing timer to standard location (#121) 2025-09-12 18:35:09 +03:00
Tulir Asokan 93f55497f4 capabilities: change group creation type name 2025-09-08 21:08:59 +03:00
Tulir Asokan 63e44fb5ad loginphone: clarify where code is sent 2025-09-08 17:16:23 +03:00
Conan 17b69a6eac login: initiate data export immediately after login (#119) 2025-09-08 17:25:06 +08:00
Conan 170b263a6c sync: use GetChatInfo for handleDialogs (#120) 2025-09-06 00:24:02 +08:00
Tulir Asokan 7a726e36a0 startchat: update group creation interface 2025-09-02 20:14:52 +03:00
Tulir Asokan 4f12f5103a matrix: add support for changing disappearing message timer 2025-08-26 18:17:50 +03:00
Tulir Asokan 8341492c9f dependencies: update mautrix-go 2025-08-26 18:07:45 +03:00
Tulir Asokan 9e4b6c3c46 ci: update Go version and pre-commit hooks 2025-08-26 18:05:20 +03:00
Tulir Asokan 597d0e996b docker: install lottieconverter 2025-08-26 18:04:21 +03:00
Tulir Asokan 4f1482e7b0 Update mautrix-python 2025-08-17 14:11:11 +03:00
Tulir Asokan 4641215e97 Update issue templates 2025-08-12 16:21:05 +03:00
Tulir Asokan 2f34ebfed9 Disable kicking unauthenticated joiners too 2025-08-12 16:20:45 +03:00
Conan 3ae88caa80 connector: handle supergroup upgrades (#118)
Co-authored-by: Tulir Asokan <tulir@maunium.net>
2025-08-07 03:09:24 +08:00
Tulir Asokan 3462b75c76 dependencies: update mautrix-go 2025-08-01 11:51:10 +03:00
Conan 6f949b5a1c build: support darwin hosts (#117) 2025-07-31 20:41:27 +08:00
Conan bdf7194691 sync: generate read receipt on SyncChat (#116) 2025-07-31 20:35:12 +08:00
Tulir Asokan f5bfe421d1 handletelegram: stop returning unnecessary errors 2025-07-31 14:12:14 +03:00
Tulir Asokan 0bc1bd04c5 dependencies: update mautrix-go 2025-07-31 14:02:31 +03:00
Tulir Asokan a31787f894 client: include event handling error in returns 2025-07-31 14:02:15 +03:00
Tulir Asokan e1c0e6dd9a gotd: replace old nhooyr websocket 2025-07-31 14:02:14 +03:00
Conan ecb3921260 push: filter muted rooms (#115) 2025-07-30 04:22:23 +08:00
Brad Murray e7fe66a23e login: wrap some common errors to return 400 instead of 500 (#114) 2025-07-25 16:48:09 -04:00
Tulir Asokan 03fe8bf782 from-matrix: don't expect mute timestamp to be max int32 2025-07-23 13:43:06 +03:00
Adam Van Ymeren 840788c1e5 auth: fix cloud auth (#113) 2025-07-19 08:39:48 -07:00
Tulir Asokan d4f6be8155 sync: fix handling chats with no messages 2025-07-17 18:26:16 +03:00
Tulir Asokan d10c528895 dependencies: update mautrix-go again 2025-07-17 17:41:40 +03:00
Tulir Asokan 635345f61d dependencies: update mautrix-go 2025-07-17 16:58:39 +03:00
Adam Van Ymeren e9abeda916 Rework telegram client lifecycle to hopefully fix not stopping issues (#112) 2025-07-17 09:56:11 -04:00
Tulir Asokan b65a1cc60a Update Docker image to Alpine 3.22 2025-07-16 23:50:21 +03:00
Tulir Asokan 53bf278f1e Bump version to 0.15.3 2025-07-16 11:50:47 +03:00
Tulir Asokan b16061db34 docker: update to Alpine 3.22 2025-07-16 11:47:39 +03:00
Tulir Asokan 4b56b6d016 dependencies: update mautrix-go 2025-07-16 11:47:17 +03:00
Tulir Asokan 3fb554b934 client: add todo for usernames in links
[skip ci]
2025-07-15 16:39:50 +03:00
Tulir Asokan 890851e85e dependencies: update mautrix-go again 2025-07-15 14:58:17 +03:00
Tulir Asokan a7aa96ef2b tomatrix: add more nil safety to media id hashing 2025-07-15 14:53:12 +03:00
Tulir Asokan aa02639759 dependencies: update mautrix-go 2025-07-15 14:52:07 +03:00
Tulir Asokan 35f137ccc1 Hardcode 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:20:19 +03:00
Adam Van Ymeren a501042f1d updates: don't error on edit if not tg.Message, just warn 2025-07-14 11:34:49 -07:00
Adam Van Ymeren 7970a678fc updates: elevate start/stop logs to debug bridge not stopping 2025-07-14 11:33:23 -07:00
Toni Spets cb98833590 directmedia: handle custom emojis 2025-07-10 09:15:58 +03:00
Adam Van Ymeren e6c3454e9f updates: don't try to fetch one more difference when context has been cancelled 2025-07-08 11:43:32 -07:00
Adam Van Ymeren 4c9555eded HandleMute: property set silent and MuteUntil 2025-07-07 14:44:59 -07:00
Tulir Asokan 38f87becb6 ci: disable go 1.23 linting 2025-07-07 18:53:45 +03:00
Tulir Asokan 9e2e2421d2 gotd: remove _tools package that was breaking pre-commit 2025-07-07 17:19:33 +03:00
Adam Van Ymeren 10bc44d17d scoped_store: Fix user access hash fetching 2025-07-04 15:03:59 -07:00
Adam Van Ymeren 125be97201 conn/rpc: Don't bother sending RPCDrop requests to server 2025-07-04 14:16:20 -07:00
Adam Van Ymeren d4239d520a backfill: fix forward backfill
only skip too new messages if we're actually doing backwards backfill
2025-07-03 21:42:33 -07:00
Adam Van Ymeren 399cd5585a ScopedStore: fix GetAccessHash always returning found: false 2025-07-02 14:10:50 -07:00
Adam Van Ymeren ac3ce3c097 don't error on unsupported peer type in settings handler 2025-06-27 21:53:42 -07:00
Adam Van Ymeren 6280a7bae7 updates: don't error just warn on unknown messages/actions 2025-06-27 20:34:30 -07:00
Adam Van Ymeren af630ecbd1 updates: don't die when deleting a message we don't have in db, just log and ignore 2025-06-27 20:20:53 -07:00
Adam Van Ymeren 7a04f298d2 move gotd fork into repo. (#111)
- update to latest telegram layer
- remove some references to fields in tg.Entities that don't exist in
the schema
- originally added here:
https://github.com/beeper/td/commit/820929062a2ba0104397bc01235ab58a9cff780e
  - referenced here
-
https://github.com/mautrix/telegramgo/commit/124f0967ed195b5a380c9bd02e170ada9710dde3
-
https://github.com/mautrix/telegramgo/commit/4205047aab2e0639217148b5d125bfaab668bd8e
2025-06-27 20:03:37 -07:00
Adam Van Ymeren 0952df0244 all: respect/propagate errors from QueueRemoteEvent (#110) 2025-06-26 13:43:35 -07:00
Toni Spets 8ac519d1e5 connector: Disconnect in a goroutine on auth error
If we get bad creds on initial connect the lock is being held when the
callback is called.
2025-06-25 08:55:36 +03:00
Tulir Asokan a49818b863 dependencies: update mautrix-go 2025-06-17 20:35:16 +03:00
Tulir Asokan 31846e7a98 Update changelog 2025-06-16 13:33:52 +03:00
Tulir Asokan 9ab2ee2970 Disable reply fallbacks by default 2025-06-16 13:24:17 +03:00
Tulir Asokan c7dd08ecd1 Update dependencies 2025-06-16 13:20:07 +03:00
Toni Spets fa237a20f7 logging: Move gotd debug logs to trace
Zap doesn't have trace level logging so to make it less noisy we need to
shift them to trace.
2025-06-09 15:01:25 +03:00
Toni Spets 41279ae996 Bump gotd with streaming download fix 2025-06-06 13:46:24 +03:00
Toni Spets e7b87835b6 Fix a race between connect and disconnect
If we get an auth error during connect we did deadlock.
2025-06-06 10:57:07 +03:00
Tulir Asokan c96a241794 legacymigrate: drop invalid disappearing message rows 2025-06-03 16:54:13 +03:00
Toni Spets 3c3c3f1dec client: Prefer contact name if exists
Don't allow TG user to override your own contact name for them after
they have been made a contact.
2025-05-30 13:22:57 +03:00
Toni Spets a9a267bc0d directmedia: handle webpage previews as well 2025-05-27 14:39:47 +03:00
Tulir Asokan 05b1eb1214 handletelegram: provide stream order in read receipts (#102) 2025-05-27 11:45:39 +03:00
Toni Spets 0f36833e89 Revert "Revert "client: unblock connect without network""
This reverts commit ea4626107c.

Adds waiting support for initial connection established to avoid locking
up gotd. This isn't extremely pretty but should do the job for now.
2025-05-27 07:40:38 +03:00
Toni Spets ea4626107c Revert "client: unblock connect without network"
This reverts commit 14c784f2a2.
2025-05-23 10:32:51 +03:00
Toni Spets 39c1b685d6 client: logout with timeout to API call to unblock it 2025-05-23 09:28:32 +03:00
Toni Spets 14c784f2a2 client: unblock connect without network
It'll still probably not race too much if disconnect gets called while
connecting is still stuck doing something.
2025-05-23 09:20:38 +03:00
Toni Spets 11f105c0e7 media: convert png and jpeg stickers to webp without ffmpeg 2025-05-22 13:52:51 +03:00
Toni Spets 9e719429e7 Fix handle channel photo edits as well as group chats 2025-05-21 08:15:23 +03:00
Tulir Asokan d2bb02b259 handlematrix: use client-generated transaction IDs 2025-05-09 16:42:44 +03:00
Tulir Asokan 8fbd723bfa Enable captions by default 2025-05-07 13:40:39 +03:00
Toni Spets 7e75c8ef83 media: make all media direct downloadable
The only exception is emojis.

Also changed direct download encoding field names to be more generic
when used in mixed manner depending on peer type.

Direct downloads are still somewhat inefficient as they require an API
round trip to succeed but we can cache things in the database if needed.
2025-05-07 06:43:51 +03:00
Brad Murray 9d3e9df57e Merge pull request #98 from mautrix/dont-500-on-resolve-identifier-not-resolving
Use 404 status when not matching a valid identifier
2025-05-06 13:16:13 -04:00
Toni Spets 849f3c6f1e Bump gotd with sticky DC hack 2025-04-29 06:38:42 +03:00
Toni Spets 483816cc2b media: Request 1MB chunks for direct media streaming
The default is 512kB and the RPC request overhead has more impact
on download speed than having half smaller chunks.

Next level speed improvement would be to use parallel downloads and
have an on-disk buffer to stream out the rebuilt file on-the-fly when
we consistent stream of data available.
2025-04-28 12:38:02 +03:00
Toni Spets 7c13481ede client: Handle connect/disconnect/auth races properly
When logging out, we should first handle network level logout and after
that ensure the client is disconnected before removing state to avoid
having event handling during disconnect from touching anything anymore.

I don't know why we nilled the client but since so many places use it
we'd rather get errors rather than panics if it's being used after
logging out but previous lifecycle fixes should avoid that.
2025-04-26 08:19:33 +03:00
Adam Van Ymeren eb5ae65402 reactions: fix db race when handling reactions on newly received old messages (#100) 2025-04-25 13:19:50 -07:00
Toni Spets 53e89441b7 media: fix lottie mime type for direct download 2025-04-25 07:38:06 +03:00
Tulir Asokan 48d91fdf76 media/sticker: fix lottie mime type
It's always gunzipped, so should never send the application/x-tgsticker mime.
Also, video/lottie+json was recently registered with IANA, so use that instead
of the old image/lottie+json: <https://www.iana.org/assignments/media-types/video/lottie+json>
2025-04-24 15:56:44 +03:00
Tulir Asokan 530bd9e52e Update telethon 2025-04-19 15:32:39 +03:00
Brad Murray bdae6dd620 Use 404 status when not matching a valid identifier 2025-04-17 13:10:34 -04:00
Toni Spets 75964a00ed dependencies: update mautrix-go 2025-04-15 12:19:28 +03:00
Toni Spets 224b01e7a4 client: Wait for updates manager to finish on disconnect 2025-04-15 12:19:28 +03:00
Tulir Asokan 5421de8e76 handlematrix: include stream order in response 2025-04-11 23:01:06 +03:00
Toni Spets a64a178dc3 client: Don't try to reconnect with canceled context 2025-04-10 10:13:02 +03:00
Toni Spets 538f2a2ec0 client: Wait before returning from disconnect 2025-04-09 10:49:45 +03:00
Nick Mills-Barrett 10b8c4b635 chatinfo: log when chats have no or unknown photo types 2025-04-07 13:58:53 +01:00
Sumner Evans b955252a6a backfill: add stream order
Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2025-03-28 08:33:33 -06:00
Tulir Asokan 6347383788 tomatrix: fix refetching channel media when bridging messages 2025-03-27 18:31:48 +02:00
Tulir Asokan 28d8276554 dependencies: update mautrix-go 2025-03-25 17:00:55 +02:00
Tulir Asokan 6480e7925e Fix login QR filename 2025-03-19 20:47:08 +02:00
Tulir Asokan c70ab2a12b Update Telethon 2025-03-19 20:47:08 +02:00
Sumner Evans 09b1e69c0f backfill: fix NPE if no messages found
Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2025-03-13 11:58:18 -06:00
Tulir Asokan 070bfd4f55 Update dependencies 2025-03-09 13:10:39 +02:00
Tulir Asokan 88c3a93526 Fix text in poll bridging 2025-03-09 13:07:29 +02:00
Sumner Evans 854f66cb04 store: make finding by username case-insensitive
Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2025-03-05 18:17:30 -07:00
Sumner Evans 1bc3a2538e treewide: add copyright/license notices
Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2025-03-05 10:16:50 -07:00
Sumner Evans dcc8689835 emojis: properly handle inline emojis on local
Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2025-03-04 11:27:09 -07:00
Sumner Evans ebc1aa05b1 connector/login: fix a context issue on phone number login
Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2025-02-26 11:22:48 -07:00
Sumner Evans a56f2977b4 login: fix contexts
Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2025-02-25 08:35:31 -07:00
Sumner Evans 36bb741c68 client: refetch message during conversion if file reference expired
Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2025-02-24 11:45:21 -07:00
Sumner Evans f0f92c9dd9 gitattributes: mark humanise/errors.go as generated
Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2025-02-21 09:56:27 -07:00
Sumner Evans e8ee5f174e connector/tomatrix: include previewed URL in hash
This will hopefully make it so that if the preview gets edited in by
Telegram at a later time, it will be bridged.

Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2025-02-17 14:14:15 -07:00
Sumner Evans f86ebad162 pre-commit: update all
Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2025-02-17 14:14:15 -07:00
Sumner Evans 0712ca5d0c dependencies: update go
Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2025-02-17 14:13:12 -07:00
Sumner Evans dad34f9a3c connector: fix getting media filename fallback
Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2025-02-17 13:53:16 -07:00
Tulir Asokan c2e07d5e3f push: add APNs push parsing 2025-02-06 15:13:31 +02:00
Tulir Asokan 255c1e2e57 push: fix types 2025-02-04 19:14:57 +02:00
Tulir Asokan a680036177 push: fix parsing decrypted json 2025-02-04 18:08:23 +02:00
Tulir Asokan 94789daed3 capabilities: fix image mime types 2025-02-04 18:06:25 +02:00
Tulir Asokan e0841e252d dependencies: update 2025-02-04 15:57:33 +02:00
Tulir Asokan ab9ff87815 push: log data if it's not json 2025-02-04 15:46:58 +02:00
Tulir Asokan 4b08ab6ac0 push: implement ConnectBackground (#88)
* push: implement ConnectBackground

* push: disable background resync by default
2025-01-29 15:35:54 +02:00
Tulir Asokan 823eda7589 push: implement parsing native notifications (#87) 2025-01-24 15:34:34 +02:00
Tulir Asokan caefda582b Disable kicking unauthenticated users 2025-01-19 20:38:39 +02:00
Tulir Asokan e1b181ed55 Update mautrix-python to support MSC4190 2025-01-15 18:54:54 +02:00
Tulir Asokan cc6a915ef4 Update dependencies 2025-01-15 17:54:33 +02:00
Tulir Asokan de4df57278 Ignore partial quotes on sticker messages 2025-01-15 17:49:18 +02:00
Tulir Asokan b158ba6b8b capabilities: add default emoji list hash to ID
The list can change, so it should change the ID too
2025-01-14 14:57:58 +02:00
Tulir Asokan 571152cb41 capabilities: update reaction settings 2025-01-14 14:56:22 +02:00
Sumner Evans c82b273155 connector/reactions: return error if not logged in
Previously, the getAvailableReactions function was only called with a
logged in client. However, now that it is called in the GetCapabilities
call, the client is no longer guaranteed to be logged in.

This was causing an NPE due to the (*TelegramClient).client being nil.

This commit makes the getAvailableReactions function not panic when the
client is not logged in.

Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2025-01-13 21:28:26 -07:00
Sumner Evans 4bef6ea09e connector/tomatrix: add timeout for getting webpage preview
Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2025-01-13 11:00:15 -07:00
Tulir Asokan 386cfa4cfb capabilities: update to new format 2025-01-10 21:17:10 +02:00
Sumner Evans f4052dcfd3 connector: set IsSuperGroup on dialog sync
Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2025-01-09 18:59:13 -07:00
Sumner Evans 664d6050df backfill: manually skip too-new messages in backwards backfill
For some reason, even though we provide an offset, Telegram sometimes
sends us more events than we request, including newer events than the
offset ID. Messages beyond the offset are then chopped off by the
bridgev2 code, but we continue trying to backfill the portal thinking
that there is more to backfill. This causes infinite backfill loops.

Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2025-01-09 17:46:36 -07:00
Sumner Evans 9e868e4614 connector: fix linking to premium messages
Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2025-01-09 17:46:36 -07:00
Sumner Evans 2743d5375a connector/tomatrix: fix broadcast messages with no From user
Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2025-01-09 17:46:18 -07:00
Sumner Evans c3fc77c2a8 connector: always use channel sender in broadcast rooms and add per-message profile
Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2025-01-09 12:52:43 -07:00
Sumner Evans 6c7727d6b5 connector/media: fix comment
Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2025-01-07 22:56:32 -07:00
Tulir Asokan 3ef2cbe102 push: extract app sandbox flag to global variable 2025-01-07 18:32:33 +02:00
Sumner Evans 487f11ffd7 connector/tomatrix: strip filename unconditionally on stickers
Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2025-01-06 10:44:17 -07:00
Sumner Evans 655cd98f27 connector/tomatrix: fix video stickers
Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2025-01-06 10:26:02 -07:00
Sumner Evans f14c90dc87 deps/td: upgrade
Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2024-12-19 08:36:01 -07:00
Sumner Evans ee0c2e4f68 connector/client: don't call disconnect on pipe error
Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2024-12-09 16:17:06 -07:00
Sumner Evans c8590ca402 connector/client: add more logging on Connect
Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2024-12-09 15:12:15 -07:00
Sumner Evans 964ea69de7 connector/client: check for client context nil on logged in check
Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2024-12-09 15:11:49 -07:00
Sumner Evans 1de97c9ae0 deps/td: upgrade so secondary connections don't have OnDead handler
Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2024-12-09 11:42:23 -07:00
Sumner Evans 987395914e connector: add stream order to new messages
Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2024-12-09 09:43:30 -07:00
Sumner Evans 2a7146d987 client: improve disconnection detection
Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2024-12-06 14:20:45 -07:00
Sumner Evans 71ebb72ede deps/td: update to remove extraneous logs
Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2024-12-06 14:14:05 -07:00
Sumner Evans dc2216e60b client: let connect send the bridge state on updates manager fail
Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2024-12-05 13:09:48 -07:00
Sumner Evans 73934a0594 client: try reconnecting on update manager run error
Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2024-12-05 13:07:01 -07:00
Sumner Evans 4d33af7f81 client: fix detection of bad credentials on connect
Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2024-12-05 08:58:09 -07:00
Sumner Evans 80f17d5fbd connector: send BAD_CREDENTIALS if error is an auth error
Previously, we were going into UNKNOWN_ERROR too aggressively

Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2024-12-04 23:49:49 -07:00
Sumner Evans 6c68351e1f connector/tomatrix: error early if client is nil
Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2024-12-04 15:21:55 -07:00
Sumner Evans 83acac5175 connector/client: handle updates manager errors
Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2024-12-04 15:21:55 -07:00
Sumner Evans 46a4b68073 connector/tomatrix: fix nil handling again
Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2024-12-04 15:21:55 -07:00
Sumner Evans 68f4b0e21f direct media: don't panic if userLogin or userLogin.Client is nil
Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2024-12-04 15:21:55 -07:00
Sumner Evans 32282a242f login: timeout client after an hour
Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2024-12-04 15:21:55 -07:00
Sumner Evans 2129dd803d connector/edits: handle edge cases where there are multiple parts to existing messages
Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2024-12-03 11:44:41 -07:00
Sumner Evans 74d9edf42e connector/edits: add better logging when parts change
Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2024-12-03 11:14:12 -07:00
Sumner Evans a1f58cad11 connector/client: ignore messages in more situations
Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2024-12-03 09:01:41 -07:00
Sumner Evans bf3e0ec8ab connector: simplify some of the dispatcher handlers
Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2024-12-02 15:53:43 -07:00
Sumner Evans 124f0967ed connector: leave chats more aggressively on entity updates
Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2024-12-02 15:39:14 -07:00
Nick Mills-Barrett 16040adc53 dependencies: update mautrix-go 2024-12-02 13:56:25 -07:00
Sumner Evans 8e994edbde connector: only send UNKNOWN_ERROR if not pipe error
Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2024-12-02 13:12:38 -07:00
Sumner Evans 54157de58f connector: reconnect on broken pipe error
Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2024-12-02 12:57:53 -07:00
Sumner Evans 7ce3dacf00 metadata: clear more things from user login metadata on auth error
Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2024-12-02 12:24:49 -07:00
Sumner Evans 6d82ac18b4 deps/td: upgrade to handle AUTH_KEY_DUPLICATED better
Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2024-12-02 12:00:33 -07:00
Sumner Evans d6765157ab connector: don't use Part IDs
Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2024-12-02 09:54:12 -07:00
Sumner Evans 7bda4f7855 connector: humanise connection errors
Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2024-11-25 17:22:11 -07:00
Sumner Evans e603aa6058 connector/mss: humanise send errors
Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2024-11-25 17:15:28 -07:00
Sumner Evans 4b5ae24a67 humanise: add package to print human-friendly errors
Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2024-11-25 17:15:28 -07:00
Sumner Evans 6b6a6ba275 connector/ids: fix MakeMessageID
Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2024-11-21 16:32:40 -07:00
Sumner Evans 22f44734cf connector/edits: prettify error messages
Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2024-11-21 12:22:11 -07:00
Sumner Evans 844f31827c connector/client: don't explode if client not available on connection state change
Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2024-11-20 15:33:55 -07:00
Sumner Evans 21ef73d69c connector/client: add more logging to IsLoggedIn
Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2024-11-20 14:39:12 -07:00
Sumner Evans 9d80c9e396 connector/matrix: more logging for matrix message handling
Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2024-11-20 14:39:12 -07:00
Tulir Asokan b316cb131a push: enable push encryption key
[skip cd]
2024-11-19 16:06:53 +02:00
Sumner Evans dd64d2c559 connector/matrix: force .jpg suffix on image filenames without extensions
Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2024-11-18 08:54:14 -07:00
Sumner Evans 1f22aa2072 connector/client: make NormalizeURL not panic if message not found
Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2024-11-18 08:39:08 -07:00
Sumner Evans d887887d8b connector/matrix: make error messages on message sends more human-readable
Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2024-11-17 22:36:06 -07:00
Tulir Asokan 5b7a170ad9 dependencies: update 2024-11-14 16:21:32 +02:00
Sumner Evans 463277def0 connector/tomatrix: fix video captions
Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2024-11-12 09:58:06 -07:00
Sumner Evans 40f259da5e directdownload: don't panic if user not logged in
Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2024-11-08 02:34:52 -07:00
Sumner Evans d1d3c18670 connector/client: update IsLoggedIn check
Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2024-11-08 02:13:48 -07:00
Scott Weber 6100335809 deps/mautrix: upgrade 2024-11-06 17:36:10 +01:00
Sumner Evans 869fef0828 connector/matrix: fix uploading non-images
Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2024-11-06 08:51:38 -07:00
Sumner Evans ada41742a1 connector/matrix: check the telegram image size limits
Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2024-11-06 08:47:02 -07:00
Sumner Evans 1b4416f291 connector/media: fix transferring non-lottie stickers
Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2024-11-06 07:02:08 -07:00
Sumner Evans 11a832c575 connector/matrix: fix sending media
Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2024-11-06 04:20:07 -07:00
Sumner Evans 303274acb6 connector/matrix: send UNSUPPORTED MSS for invalid reactions
Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2024-11-06 03:07:36 -07:00
Sumner Evans b6d3131caf deps/td: upgrade
Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2024-11-05 02:00:12 -07:00
Sumner Evans 22c3938b52 connector/client: fix IsLoggedIn check
Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2024-11-04 10:05:48 -07:00
Sumner Evans 827116658b connector/matrix: implement image size/dimension limits
Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2024-11-04 09:53:04 -07:00
Sumner Evans ca8aff0534 connector/login: fix crash on login
Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2024-11-04 08:47:31 -07:00
Sumner Evans 5adb2a6572 connector/client: early return on logout remote
Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2024-11-04 07:40:24 -07:00
Sumner Evans 69e3a183c7 connector/client: init scoped store earlier
Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2024-11-04 07:35:15 -07:00
Sumner Evans 52c39eefe0 legacyprovisioning: add check for auth key on logout
Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2024-11-04 07:28:59 -07:00
Sumner Evans 6fa19bbbda legacyprovisioning: fix reconnect
Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2024-11-04 03:18:51 -07:00
Sumner Evans 8025404958 connector/client: don't unset auth key on unknown error
Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2024-11-04 03:00:29 -07:00
Sumner Evans 07a8553b22 connector: fix chat info for Saved Messages chat
Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2024-10-31 14:23:18 -06:00
Sumner Evans 15fdd89e3d connector/client: convert some bad credentials to unknown errors
Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2024-10-30 14:27:28 -06:00
Sumner Evans bda33687af connector/client: send bad credentials in the correct places
Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2024-10-30 14:08:10 -06:00
Sumner Evans ea9bd01d06 connector/chatinfo: allow bridging non-supergroup channels with lots of subscribers
Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2024-10-30 10:45:23 -06:00
Sumner Evans e846fb168c pre-commit: github.com/beeper/pre-commit-go v0.4.0 -> v0.4.1
Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2024-10-30 09:32:37 -06:00
Sumner Evans 0046975aa5 treewide: ban global zerolog
Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2024-10-30 09:27:58 -06:00
Sumner Evans aa7a2d186b connector/client: check for auth key on login check
Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2024-10-29 14:28:03 -06:00
Sumner Evans f195e2cac0 sync: fix setting membership of channel user
Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2024-10-29 09:53:39 -06:00
Sumner Evans b33209fafa connector: remove debug line
Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2024-10-29 09:47:54 -06:00
Tulir Asokan 265c2835e8 legacymigrate: ignore sessions without auth key 2024-10-29 17:04:04 +02:00
Tulir Asokan 86a77996d4 legacymigrate: remove unknown message sender values 2024-10-29 16:17:08 +02:00
Sumner Evans da894bec25 connector/tomatrix: fix nil handling
Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2024-10-29 08:12:11 -06:00
Sumner Evans cc8dce3959 deps/mautrix: upgrade for more ergonomic event meta handling
Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2024-10-28 14:01:22 -06:00
Sumner Evans 22488fbc5f connector: add notice on chat creation
Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2024-10-28 13:16:55 -06:00
Sumner Evans 3498ed8dc1 calls: fix notifications
Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2024-10-28 13:06:37 -06:00
Sumner Evans bcea875e66 connector/tomatrix: handle nil better in mediaHashID
Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2024-10-28 08:04:18 -06:00
Sumner Evans 7cb70d9753 connector: only save access hash if not a min entity
Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2024-10-26 22:48:54 -06:00
Sumner Evans e266d1ac80 reactions: poll for reactions on read receipt
Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2024-10-24 12:57:43 -06:00
Sumner Evans 0f933f691b typing: support typing as a channel user
Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2024-10-24 11:15:48 -06:00
Sumner Evans c6afaf5504 sync: always needs backfill if no latest message present
Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2024-10-24 11:01:31 -06:00
Sumner Evans 9f6a54be81 connector/tomatrix: log when hashing unsupported media type
Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2024-10-24 10:53:11 -06:00
Sumner Evans 229efdd487 chatinfo: handle forbidden channels/chats without panicking
Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2024-10-24 10:45:32 -06:00
Sumner Evans 4bdd415dbe connector: send notice about TTL changes
Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2024-10-23 10:12:23 -06:00
Sumner Evans 31dc0259f3 connector/matrixfmt: use different bullet types for each nesting of lists
Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2024-10-22 13:39:32 -06:00
Sumner Evans 13f21a7c70 media: implement streaming for direct downloads
Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2024-10-22 11:37:59 -06:00
Sumner Evans a573740b9a media/transfer: add function to directly download bytes
Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2024-10-22 09:21:56 -06:00
Sumner Evans 5448648c32 deps/mautrix: upgrade
Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2024-10-22 08:10:34 -06:00
Tulir Asokan 706d4a5e5c .github: update issue template 2024-10-22 12:51:58 +03:00
Tulir Asokan df3cd765fe legacymigrate: set members fetched to not null on sqlite too 2024-10-22 12:51:23 +03:00
Sumner Evans bd7c724341 stickers: support sending
Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2024-10-21 16:40:27 -06:00
Sumner Evans f076376caa connector/tomatrix: handle circular videos
Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2024-10-21 15:55:45 -06:00
Sumner Evans 19a3c8a4d9 github/dependabot: enable for GitHub Actions
Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2024-10-21 15:35:37 -06:00
Sumner Evans a9f8a3aa0f deps/td: update to fix read receipts
Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2024-10-16 09:35:03 -06:00
Sumner Evans f91b429c47 connector: notify when call starts/ends
Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2024-10-15 08:26:05 -06:00
Sumner Evans b0e6dcb1d6 client: support TG -> Matrix disappearing messages
Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2024-10-14 14:33:17 -06:00
Sumner Evans 132585de34 user info: handle deleted users
Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2024-10-14 14:09:08 -06:00
Sumner Evans 4d1cec979b backfill: use offset ID instead of max ID
According to Telethon, max_id doesn't work:
https://github.com/tulir/telethon/blob/c1e961ce2506d92f962a7d4ca5897d57cdaeb6d3/telethon/client/messages.py#L33-L34

Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2024-10-14 13:02:02 -06:00
Sumner Evans 679b4bd157 connector/reactions: fallback to sensible defaults if config doesn't have the correct values
Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2024-10-11 10:22:21 -06:00
Sumner Evans 73d0b189bb scoped store: implement new AccessHasher interface
Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2024-10-11 08:52:26 -06:00
Sumner Evans 48059a3a51 logout: delete user-specific state
Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2024-10-10 10:07:26 -06:00
Sumner Evans 4205047aab chat delete: bridge properly
Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2024-10-09 11:24:55 -06:00
Sumner Evans 03c7028460 power levels: prevent sending to blocked users
Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2024-10-09 09:57:02 -06:00
Sumner Evans c75ac58763 client: add option to disable bridging view-once and disappearing media
Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2024-10-08 13:41:00 -06:00
Sumner Evans a85659df9d backfill: fix request on forward backfill
Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2024-10-08 12:59:22 -06:00
Sumner Evans 9a8f356348 backfill: fix dialog fetch, HasMore, and skip forbidden channels
Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2024-10-08 12:22:04 -06:00
Sumner Evans 9576f48c5b stickers: strip filename
Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2024-10-08 08:21:23 -06:00
Sumner Evans 96331761b8 snc: fix resolving identifier
Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2024-10-07 16:32:48 -06:00
Sumner Evans 4821865cad deps: un-upgrade gorilla packages
Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2024-10-07 09:45:25 -06:00
Sumner Evans 7efad4a990 deps: upgrade all
Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2024-10-07 09:26:13 -06:00
Sumner Evans 6c44ba487a backfill: set CanBackfill in the correct places
Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2024-10-07 09:16:24 -06:00
Sumner Evans 57b32f6ac6 backfill: implement marking read
Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2024-10-04 08:17:33 -06:00
Sumner Evans 17e4e20a93 sync: fix setting memberships
Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2024-10-04 08:12:46 -06:00
Sumner Evans 8480c8aa68 client: make GetUserInfo work for channels
Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2024-10-04 07:59:57 -06:00
Sumner Evans d14f365fe1 sync: fix room name bridging on backfill
Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2024-10-04 07:59:36 -06:00
Sumner Evans 7d9836c86b power levels: bridge rights for group chats
Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2024-10-03 11:16:18 -06:00
Tulir Asokan 1c7e626c97 sticker: fix lottie conversion 2024-10-03 14:07:11 +03:00
Tulir Asokan 4bd57f7cab ci: add old issue locking 2024-10-03 14:04:14 +03:00
Tulir Asokan 9da87fc789 dependencies: update 2024-10-03 14:03:57 +03:00
Tulir Asokan 2139bf25eb push: implement PushableNetworkAPI 2024-10-03 14:02:32 +03:00
Sumner Evans 083837aa9e pins: bridge from Telegram -> Matrix
Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2024-10-02 12:28:05 -06:00
Sumner Evans abba9bcf81 pins: handle (un)favourite tags from the network connector
Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2024-10-02 12:17:04 -06:00
Sumner Evans 171b621999 client: implement MuteHandlingNetworkAPI
Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2024-10-02 10:12:41 -06:00
Sumner Evans 52fab81e55 mute: sync from Telegram -> Matrix
Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2024-10-02 10:12:33 -06:00
Sumner Evans 6f4e32fad0 client: handle group chat and channel creation events
Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2024-10-01 11:10:12 -06:00
Sumner Evans 9609f437d5 deps/mautrix: upgrade
Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2024-10-01 10:50:25 -06:00
Sumner Evans e1a56778f5 media: default to JPEG MIME-type for direct-downloaded images
Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2024-09-27 16:14:28 -06:00
Sumner Evans 23bb0febe9 client: ignore messages in left channels
Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2024-09-27 13:12:32 -06:00
Sumner Evans 31397681f5 client: save channel usernames in database
Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2024-09-26 14:01:09 -06:00
Sumner Evans 332bbb8de1 client: handle channel updates
Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2024-09-26 11:50:11 -06:00
Sumner Evans 7ccd8ab4ab portal: handle self-leaves of groups
Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2024-09-26 10:02:55 -06:00
Sumner Evans 7af4ecc719 backfill: fix stopTakeoutTimer
Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2024-09-26 10:01:05 -06:00
Sumner Evans ce1c28832e reactions: use allowed reactions when possible
Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2024-09-26 07:48:51 -06:00
Sumner Evans 81c913bdd3 client: better logging on connection state changes
Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2024-09-25 13:58:03 -06:00
Sumner Evans 5c23e9695f deps/td: upgrade
Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2024-09-25 13:52:05 -06:00
Sumner Evans c6e96682b6 treewide: separate user and channel namespaces
Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2024-09-25 07:16:05 -06:00
Sumner Evans 65da56b2a6 lottie: include in Docker image
Closes PLAT-27635

Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2024-09-17 10:41:08 -06:00
Sumner Evans a73f9d1ec2 connector/tomatrix: fix replies
Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2024-09-16 15:50:29 -06:00
Sumner Evans 7a02d6a35b client: use ping callback to determine if connection is still alive
Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2024-09-16 14:20:36 -06:00
Tulir Asokan ff48398430 ids: add support for split portals 2024-09-14 12:50:31 +03:00
Tulir Asokan 7ed3c46f23 dependencies: update 2024-09-13 23:54:47 +03:00
Sumner Evans 3acd95741f connector: check for nil on cancel
Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2024-09-10 13:02:49 -06:00
Tulir Asokan 3f69f29d49 config: remove pointer 2024-09-10 21:24:28 +03:00
Sumner Evans fab98cfdea takeout: use takeout to list dialogs once permission granted
Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2024-09-10 11:27:00 -06:00
Sumner Evans 4692d46305 client: use both reconnection detection methods
Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2024-09-10 08:30:33 -06:00
Sumner Evans 87f9f008e6 formatting: fix username parsing and insertion
Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2024-09-10 00:08:00 -06:00
Sumner Evans 50ab23423f client: update for better connection detection
Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2024-09-09 21:50:41 -06:00
Sumner Evans cd0d940889 connector/login: normalize phone number on finalize
Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2024-09-09 21:47:19 -06:00
Sumner Evans 777225c252 roadmap: update
Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2024-09-05 10:47:45 -06:00
Sumner Evans 89b1caadbf takeout: use takeout for backwards backfill
Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2024-09-05 08:42:00 -06:00
Tulir Asokan 4d4060f37b legacymigrate: fix handling empty content hashes 2024-09-05 02:29:39 +03:00
Sumner Evans 75eea8e2cb reactions: fix double-puppeting
Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2024-09-03 15:10:56 -06:00
Sumner Evans 3b6af95976 connector: support messages sent by a channel
Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
Co-authored-by: Tulir Asokan <tulir@maunium.net>
2024-09-03 13:34:13 -06:00
Sumner Evans 8925318ec4 legacyprovisioning: implement SNC endpoints
Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2024-09-03 09:51:58 -06:00
Sumner Evans ec330c72be legacyprovisioning: fix getting user ID from request
Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2024-09-02 16:58:47 -06:00
Sumner Evans 088900aee1 connector: save channel access hashes in more places
Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2024-09-02 11:52:31 -06:00
Sumner Evans 86a2b3fa15 provisioning: send code faster and fix password after QR support
Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2024-09-02 11:52:31 -06:00
Tulir Asokan afd9850c4b legacymigrate: drop telegram_file table separately on sqlite (#28)
* legacymigrate: drop telegram_file table separately on sqlite

* legacymigrate: check foreign keys after dropping table just to be safe
2024-09-02 11:21:59 -06:00
Sumner Evans 60fe2e07c2 bridge state: set remote name and profile
Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2024-08-28 12:22:58 -06:00
Sumner Evans c2d94947ee provisioning: implement legacy QR endpoint
Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2024-08-28 09:26:30 -06:00
Sumner Evans 4d9ad4f0af login: implement QR login
Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2024-08-28 09:26:30 -06:00
Sumner Evans bbf53fb28b provisioning: implement legacy endpoints
Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2024-08-28 09:26:30 -06:00
Tulir Asokan ced27a9974 legacymigrate: fix ghost metadata booleans 2024-08-28 12:18:19 +03:00
Tulir Asokan 3378467378 config: preserve spacing when rewriting config 2024-08-27 17:16:54 +03:00
Tulir Asokan 1c2b902de4 legacymigrate: handle portal and message metadata 2024-08-27 17:01:29 +03:00
Tulir Asokan f7be907633 matrix: fix making message ID 2024-08-27 16:51:57 +03:00
Tulir Asokan 1e39877af3 ids: remove emoji ID prefix 2024-08-27 16:45:55 +03:00
Tulir Asokan 6b092026c3 legacymigrate: add peer type to portal IDs and fix other things 2024-08-27 16:36:09 +03:00
Tulir Asokan 68e835c658 legacymigrate: add support for migrating legacy database and config (#23) 2024-08-27 15:13:11 +03:00
Tulir Asokan e3e709eec6 ids: add channel ID to message ID to ensure uniqueness (#25) 2024-08-26 20:42:06 +03:00
Sumner Evans d7508579e5 deps/mautrix: upgrade with fix to NPE on search
Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2024-08-26 08:57:44 -06:00
Sumner Evans d8d4a60855 snc: implement creating normal groups
Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2024-08-22 14:22:56 -06:00
Sumner Evans 196eaac917 snc: use local caches for resolving identifiers
Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2024-08-22 13:47:55 -06:00
Sumner Evans 91ce540a66 contact list: implement fetching
Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2024-08-22 13:47:55 -06:00
Sumner Evans dcf43ca9d9 search: implement searching by username
Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2024-08-22 13:47:55 -06:00
Sumner Evans 15b0dc51b3 snc: implement resolving Telegram IDs, usernames, and phone numbers
Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2024-08-22 12:18:21 -06:00
Tulir Asokan 9d8f162f41 chatinfo: override name for saved messages 2024-08-22 18:26:08 +03:00
Tulir Asokan eec5cbe447 login: fix bugs in refactor 2024-08-22 18:02:57 +03:00
Tulir Asokan b25c09fc53 store: refactor access hash and session tables
* Move sessions to user_login metadata, as that data rarely changes after login.
* Merge user and channel access hashes. Those IDs don't conflict.
* Split usernames into a new table to allow better `ON CONFLICT` updates
  (when a username moves to another entity, we want the old row to be replaced).
  Usernames also don't need to be scoped to a login.
2024-08-22 17:54:10 +03:00
Tulir Asokan e611c87342 all: add some todos and fix small issues 2024-08-22 17:53:50 +03:00
Sumner Evans a6946f8119 sync: skip deleted users and use messages from GetDialogs call
Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2024-08-22 08:51:35 -06:00
Sumner Evans 5960a2307e sync: fix check for needing backfill
Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2024-08-22 08:34:16 -06:00
Sumner Evans 6aaf786ea9 backfill: run on login
Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2024-08-22 08:32:43 -06:00
Sumner Evans 8b8b689187 sync: add on-command sync
Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2024-08-22 07:57:28 -06:00
Sumner Evans 24d0d4687a connector/tomatrix: fix NPE with unsupported media
Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2024-08-22 07:57:28 -06:00
Sumner Evans 0670c2b2bc updates: add wrapper for API calls to update users
Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2024-08-22 07:57:28 -06:00
Sumner Evans 284178df65 client: enqueue backfill if channel too long
Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2024-08-22 07:57:28 -06:00
Sumner Evans 56f83315ed backfill: implement
Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2024-08-22 07:57:28 -06:00
Sumner Evans 7e2d9bbc4e avatar: fix downloading avatars
Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2024-08-20 14:13:50 -06:00
Sumner Evans d11af1a463 db: fix latest revision
Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2024-08-20 14:12:42 -06:00
Sumner Evans dc4c3ee382 connector: fix NPE with read receipts
Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2024-08-20 08:55:32 -06:00
Sumner Evans 0ef8581764 connector/client: cleanup
Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2024-08-20 08:55:11 -06:00
Sumner Evans 6c4c0f4821 connector/chatinfo: use access hash for user
Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2024-08-20 08:54:34 -06:00
Sumner Evans b11479e4e2 client: clean up connection code and add bad credentials handling
Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2024-08-20 07:42:30 -06:00
Sumner Evans 3a11ac217e client: make ping interval and timeout configurable
Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2024-08-15 17:36:47 -06:00
Sumner Evans d94dbe81dc bridge states: send CONNECTED/TRANSIENT_DISCONNECT
Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2024-08-15 17:35:41 -06:00
Sumner Evans 6462b709f5 deps/td: use Beeper fork that doesn't eat receipts
Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2024-08-14 13:11:18 -06:00
Sumner Evans a86c2c2544 read receipts: bridges TG <-> Matrix
Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2024-08-08 15:51:27 -06:00
Sumner Evans 838f291220 store: move the access_hash and username to separate per-user table
Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2024-08-08 12:12:17 -06:00
Sumner Evans aeb8fba288 msgconv: annotate GIFs bridged as videos with correct flags
Closes PLAT-25993

Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2024-08-08 11:36:37 -06:00
Sumner Evans 497bfb152e media: bridge GIFs as documents rather than images
This allows them to be animated.

Closes PLAT-25990

Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2024-08-07 16:25:22 -06:00
Sumner Evans 83695b4336 directdownload: include receiver in media ID
Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2024-08-07 11:18:59 -06:00
Sumner Evans e0194f7621 typing: support TG <-> Matrix
Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2024-08-07 11:01:24 -06:00
Sumner Evans 7fd280ea10 chat metadata: bridge join/leave events TG -> Matrix
Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2024-08-06 14:39:20 -06:00
Sumner Evans ca4d566490 chat metadata: bridge title/avatar edits TG -> Matrix
Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2024-08-06 12:57:32 -06:00
Sumner Evans 7e53698696 roadmap: update
Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2024-08-06 12:34:14 -06:00
Sumner Evans 54f971f578 connector: convert to simplevent
Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2024-08-06 10:24:23 -06:00
Sumner Evans 18337c6941 reactions: use ReactionSync event
Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2024-08-06 10:24:23 -06:00
Sumner Evans f56f520308 (telegram|matrix)fmt: mention formatting
Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2024-08-05 15:19:53 -06:00
Sumner Evans b539e5d63d ghost: improve metadata handling
Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2024-08-05 15:19:53 -06:00
Sumner Evans e8b5d286dc matrixfmt: text formatting Matrix -> TG
Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2024-08-05 15:19:53 -06:00
Sumner Evans 882582456e telegramfmt: text formatting TG -> Matrix
Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2024-08-05 15:19:39 -06:00
Sumner Evans e7522be252 reactions: handle async
Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2024-07-19 14:49:49 -06:00
Sumner Evans 5ea342e788 edits: bridge Matrix -> TG
Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2024-07-19 14:33:52 -06:00
Sumner Evans 314b2da99f edits: bridge TG -> Matrix
Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2024-07-19 14:33:52 -06:00
Sumner Evans 5a3b52dff2 reactions: remove as the correct user
Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2024-07-19 12:34:28 -06:00
Sumner Evans 98a0ed0a5b deps: td v0.102.0 -> v0.105.0, mautrix@cc5f225
Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2024-07-19 09:42:10 -06:00
Sumner Evans 29c3c4009a client: improve logging on getEventSender
Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2024-07-18 17:40:21 -06:00
Sumner Evans fe550da243 metadata: allow disabling channel memebr sync
Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2024-07-18 08:50:35 -06:00
Sumner Evans a0d88da480 metadata: add pagination config for members initial sync
Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2024-07-17 16:44:53 -06:00
Sumner Evans ec56fb6b28 metadata: refactor getting chat info
Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2024-07-17 15:26:34 -06:00
Sumner Evans 9d77bebe3e config: remove set_private_chat_portal_meta option
It's handled by bridgev2

Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2024-07-17 12:34:28 -06:00
Sumner Evans 48858ac28f config: add member list and max member count options
Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2024-07-17 12:03:58 -06:00
Sumner Evans 0e6ea310d1 metadata: gate setting DM portal metadata behind config option
Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2024-07-16 18:50:15 -06:00
Sumner Evans 69c9e3c38c client: fix GetChatInfo for channels
Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2024-07-16 12:30:02 -06:00
Tulir Asokan 0068341185 Bump version to 0.15.2 2024-07-16 11:53:19 +03:00
Sumner Evans a8142cd8a0 remove all printf's and update logging
Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2024-07-15 23:53:15 -06:00
Sumner Evans 6e0f604209 updates: don't panic on channel too long
Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2024-07-15 22:14:09 -06:00
Sumner Evans 34832c7ff7 channels: handle messages Matrix <-> TG
Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2024-07-15 15:23:50 -06:00
Sumner Evans 62f77686c4 ci: remove unnecessary files and align with what mautrix-slack has
Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2024-07-15 11:02:40 -06:00
Scott Weber 5eaec4d0e0 Quick hack to get the bridge to send CONNECTED 2024-07-15 11:01:18 -06:00
Sumner Evans 35c5518d1d Revert "metadata: prefix fields"
This reverts commit 548356189b.
2024-07-15 09:06:23 -06:00
Sumner Evans 548356189b metadata: prefix fields
Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2024-07-15 08:30:13 -06:00
Scott Weber aa45619244 Update mautrix-go (and update to new metadata system) 2024-07-15 08:30:13 -06:00
Tulir Asokan efcf1535ff Update mautrix-python 2024-07-12 20:25:57 +03:00
Sumner Evans 92b8541654 pre-commit: enforce go mod tidy, no literal HTTP methods
Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2024-07-10 12:00:29 -06:00
Sumner Evans 62d6145c14 stickers: support receiving and converting
Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2024-07-10 10:45:56 -06:00
Sumner Evans 58cc638058 media: major refactor of downloading/direct URL
Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2024-07-09 23:03:15 -06:00
Sumner Evans 7e680f1fee reactions: support deletions
Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2024-07-09 14:04:49 -06:00
Sumner Evans a63f264804 reactions: support custom emojis
Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2024-07-09 14:04:25 -06:00
Tulir Asokan 99f633e98d Update telethon and changelog 2024-07-09 12:15:41 +03:00
Tulir Asokan 0137bfcbf6 Update mautrix-python 2024-07-09 12:15:41 +03:00
Sumner Evans 33dc5bad03 reactions: support Matrix -> TG
Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2024-07-08 08:47:30 -06:00
Sumner Evans 5d39fc8c5f pkg/download -> pkg/media
Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2024-07-08 08:47:30 -06:00
Sumner Evans 0921168b91 pkg/store -> pkg/connector/store
Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2024-07-08 08:47:30 -06:00
Sumner Evans cbba340da6 db: add telegram_file table
Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2024-07-08 08:47:30 -06:00
Sumner Evans a2b810e34e reactions: support unicode custom emojis
Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2024-07-08 08:47:30 -06:00
Sumner Evans feab4607b5 reactions: support TG -> Matrix
Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2024-07-08 08:47:29 -06:00
Sumner Evans 15cb6ef44f deps/mautrix: upgrade to latest
Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2024-07-08 08:21:09 -06:00
Sumner Evans f524f365f1 deps/go-util: upgrade
Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2024-07-02 14:21:24 -06:00
Sumner Evans 3d8b9d6291 client: handle message deletions TG <-> Matrix
Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2024-06-25 15:33:00 -06:00
Sumner Evans 55a9375938 media: support thumbnails
Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2024-06-25 13:52:55 -06:00
Sumner Evans 6a6e129c0a client: remove unnecessary log
Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2024-06-25 12:20:24 -06:00
Sumner Evans 6bd2ef5b34 media: decode waveform TG -> Matrix
Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2024-06-25 11:02:55 -06:00
Sumner Evans 7437240f2f user metadata: bridge profile pictures
This commit includes bridging of both the initial profile pictures and
real-time updates to the profile pictures.

Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2024-06-25 10:47:03 -06:00
Sumner Evans 8ad516c5a4 cmd/directdl: delete experiment
Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2024-06-24 14:20:50 -06:00
Sumner Evans eef68706d9 dockerfile: add v2 CI
Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2024-06-24 14:14:05 -06:00
Sumner Evans 752107ffb0 initial metadata: set room avatar
Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2024-06-24 14:05:22 -06:00
Sumner Evans 5193cd899f initial metadata: set room name
Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2024-06-24 14:05:16 -06:00
Sumner Evans 1563ee014d deps/mautrix: upgrade
Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2024-06-24 11:52:21 -06:00
Sumner Evans a24079494d directdownload: fix logging and remove outdated comment
Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2024-06-24 10:57:50 -06:00
Sumner Evans 44cb928707 msgconv: fix location messages
Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2024-06-24 09:33:07 -06:00
Sumner Evans 4d82cb7883 media: add fallbacks for a couple more types
Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2024-06-20 18:42:09 -06:00
Sumner Evans 867cbd582e media: fallback for games
Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2024-06-20 18:42:09 -06:00
Sumner Evans 17badab358 media: handle dice rolls
Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2024-06-20 18:42:09 -06:00
Sumner Evans ee583af4f9 media: handle polls
Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2024-06-20 18:42:09 -06:00
Sumner Evans 499678d092 media: handle location shares
Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2024-06-20 18:42:09 -06:00
Sumner Evans 9d9c82c9e9 media: handle unsupported types
Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2024-06-20 18:42:09 -06:00
Sumner Evans c0c7ad7d0f media: handle contact shares
Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2024-06-20 18:42:09 -06:00
Sumner Evans 63645e50b2 handle matrix message: suppress previews if event's link previews is as empty array
Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2024-06-20 18:42:09 -06:00
Sumner Evans 891750592d converter: handle link previews TG -> Matrix
Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2024-06-20 18:42:09 -06:00
Sumner Evans b568ef8d8c media: support voice messages
Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2024-06-20 18:42:09 -06:00
Sumner Evans 16706d8338 media: support documents
Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2024-06-20 18:42:09 -06:00
Sumner Evans 2df6f73098 disappearing images: implement
Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2024-06-19 13:11:35 -06:00
Sumner Evans 7963e52405 direct media: implement direct download for photos
Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2024-06-19 12:47:41 -06:00
Sumner Evans d0626e670c deps/mautrix: upgrade to latest bridgev2
Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2024-06-19 12:36:10 -06:00
Sumner Evans f3f6ea8b2f connector: ensure it adheres to the network connector interface
Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2024-06-18 11:21:53 -06:00
Sumner Evans 871a9705e3 images: implement sending from Matrix -> Telegram
Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2024-06-17 17:34:49 -06:00
Sumner Evans 5de193d087 ci: use gov2 builder
Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2024-06-17 17:34:49 -06:00
Sumner Evans 60f668deb4 msgconv: clean up TG->Matrix photo logic
Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2024-06-17 16:11:59 -06:00
Sumner Evans 61c06396fc msgconv: basic photo support
Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2024-06-17 14:46:45 -06:00
Sumner Evans 323fe1603e store: save updates state in database
Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2024-06-17 14:46:45 -06:00
Sumner Evans a4aedec044 dms: implement basic text message handling
Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2024-06-17 14:46:45 -06:00
Sumner Evans 6c88b21b75 example config: update for bridgev2
Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2024-06-17 14:46:45 -06:00
Sumner Evans 6511adc480 login: reimplement login in connector interface
Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2024-06-17 14:46:44 -06:00
Javier Cuevas f6cb26f7f5 Merge pull request #964 from mautrix/feature/periodic-refresh
Add periodic connection refresh
2024-05-24 10:19:43 +02:00
Javier Cuevas 6418202118 Update mautrix_telegram/abstract_user.py
Co-authored-by: Tulir Asokan <tulir@maunium.net>
2024-05-24 09:39:20 +02:00
Javier Cuevas 4b25e855e0 Add force_refresh_interval_seconds to config.py 2024-05-24 09:36:09 +02:00
Javier Cuevas a35f6abfd1 Change default for force_refresh_interval_seconds (disabled by default) 2024-05-24 09:36:03 +02:00
Javier Cuevas 716222a671 Format to pass linting 2024-05-23 17:18:06 +02:00
Javier Cuevas 31801a436c Add periodic connection refresh 2024-05-23 17:06:04 +02:00
Sumner Evans f2219a1e06 cmd/directdl: add experimental direct download handler
Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2024-05-15 16:54:34 -06:00
Sumner Evans 72fc81b239 msgconv: start experimenting with direct download URL
Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2024-05-15 16:53:57 -06:00
Sumner Evans 43212ad8db ci: install staticcheck
Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2024-05-14 13:49:29 -06:00
Sumner Evans 0d502a8c55 Basic message converter and login
Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2024-05-14 10:17:19 -06:00
Tulir Asokan 043cb7f854 Remove everything and add stub Go module 2024-05-14 19:04:23 +03:00
Tulir Asokan 8bd5a4e367 Update changelog 2024-05-03 11:47:48 +02:00
Tulir Asokan 43d17a335b Fix call end message 2024-04-08 17:47:44 +03:00
Nick Mills-Barrett 84a3fde1ca Implement bot/channel file size limit 2024-03-25 14:36:29 +00:00
Nick Mills-Barrett 05d05e671b Add config to limit size of documents from bots/channels copied to Matrix 2024-03-25 14:36:29 +00:00
Nick Mills-Barrett ab6a6654f7 Pass through is channel to msg conversion 2024-03-25 14:36:29 +00:00
Tulir Asokan dbfbf12862 Fix error handling replies in some cases 2024-03-19 12:02:58 +02:00
Tulir Asokan 6166173376 Fix message in MSS events 2024-03-14 13:08:53 +02:00
Tulir Asokan 2232d9898e Avoid logging RPCErrors twice 2024-03-14 13:07:22 +02:00
Tulir Asokan 3cf279718f Don't send notices for some errors 2024-03-14 13:05:55 +02:00
Tulir Asokan 65ec4491e2 Merge branch 'tulir/bot-reactions' 2024-03-13 15:21:33 +02:00
Tulir Asokan ce43607c56 Update dependencies 2024-03-13 15:20:41 +02:00
Nick Mills-Barrett 150bf5e338 Return if no document contained in media document event 2024-02-14 09:58:24 +00:00
Tulir Asokan 77cbbebfb2 Update Black to 2024 style and Python 3.10 target 2024-01-29 18:52:10 +02:00
Tulir Asokan 511043a720 Add support for bot-specific reaction update 2024-01-13 22:42:42 +02:00
Tulir Asokan 19a4b4374d Update dependencies and drop Python 3.9 support 2024-01-08 17:35:37 +02:00
Tulir Asokan 731d5e028a Bump version to 0.15.1 2023-12-26 17:07:43 +01:00
Tulir Asokan 5ea9e48954 Don't trust member list if source user isn't there 2023-12-26 16:57:43 +01:00
Tulir Asokan 73b26e3fbd Update Telethon 2023-12-26 16:54:18 +01:00
Tulir Asokan 48be895938 Update dependencies 2023-12-15 22:36:46 +02:00
Tulir Asokan 87909d07ec Fix potential issues with ignore_unbridged_group_chat option 2023-12-15 22:28:10 +02:00
Tulir Asokan 3609eb2b70 Update Docker image to Alpine 3.19 2023-12-08 15:39:02 +02:00
Tulir Asokan 562f646fea Bump version to 0.15.0 2023-11-26 20:15:48 +02:00
Nick Mills-Barrett ab3cf5bc5f Add missing break in connect loop 2023-11-13 18:28:23 +00:00
Nick Mills-Barrett 1b2f07dfa2 Add quick retries when connecting to Telegram (#941)
* Add quick 5 retries when connecting to Telegram

* Fix attempt initialisation

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

* Log only when retrying

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

---------

Co-authored-by: Tulir Asokan <tulir@maunium.net>
2023-11-13 18:21:39 +00:00
Tulir Asokan 2a67c96db3 Update mautrix-python 2023-11-10 22:07:56 +02:00
Tulir Asokan 3fdb789745 Update dependencies 2023-11-10 14:51:02 +02:00
Tulir Asokan e4c239e6bc Update changelog 2023-11-10 14:46:41 +02:00
Tulir Asokan 897a35be5d Add commands to add and delete contacts. Fixes #885 2023-11-10 14:42:06 +02:00
Tulir Asokan d72897dfe8 Remove support for MSC2716 2023-11-01 01:03:45 +02:00
Tulir Asokan 27723f5055 Update Telethon again 2023-10-30 12:14:54 +02:00
Tulir Asokan a84e5ebc6a Remove redundant <br>'s after block tags when converting from Telegram 2023-10-29 12:06:39 +02:00
Tulir Asokan 90a8583ad0 Include partial quote target text in Matrix event 2023-10-29 12:04:46 +02:00
Tulir Asokan bf2cef424b Add support for cross-room replies from Telegram 2023-10-29 02:12:17 +03:00
Tulir Asokan 6809ebcde9 Update Telethon 2023-10-29 02:00:10 +03:00
Tulir Asokan 6fafc533ab Catch AuthKeyNotFound in start 2023-10-22 11:43:56 +03:00
Tulir Asokan 060dd647c3 Add comment 2023-10-22 11:43:56 +03:00
Tulir Asokan 812b4ec8db Adjust kick message when user joins portal with no relaybot
Closes #875
2023-10-16 19:36:16 +03:00
Tulir Asokan 8c1ddec136 Update Telethon again 2023-10-16 18:07:47 +03:00
Tulir Asokan 08db5a687c Improve .gitignore 2023-10-16 12:58:17 +03:00
Tulir Asokan ec298b2b90 Update Telethon and fix handling disappearing media 2023-10-16 12:58:16 +03:00
Tulir Asokan 22f91d51a3 Handle weird missing sizes in stickers 2023-09-19 15:55:43 -04:00
Tulir Asokan d033042ee1 Bump version to 0.14.2 2023-09-19 12:55:37 -04:00
Tulir Asokan 2270f4fe40 Update pillow 2023-09-19 10:59:15 -04:00
Tulir Asokan 6d208b37a5 Stringify sticker pack IDs and include sticker ID 2023-09-14 17:15:17 -04:00
Tulir Asokan 55ebaef6e3 Include sticker pack reference in events 2023-09-14 14:06:19 -04:00
Tulir Asokan 215f077cf0 Make forward backfill timeout configurable 2023-08-29 21:10:37 +03:00
Tulir Asokan 4e4f409f87 Update changelog 2023-08-27 00:52:38 +03:00
Tulir Asokan 4d145f4716 Update mautrix-python 2023-08-27 00:52:21 +03:00
Tulir Asokan b833a41a88 Update Telethon 2023-08-27 00:11:05 +03:00
Tulir Asokan 768d51c4ae Add fallback message for invoices 2023-08-19 12:09:22 +03:00
Tulir Asokan f7db298fda Ignore stories and story replies properly 2023-08-19 12:08:12 +03:00
Tulir Asokan 4f2118c7ee Fix sending media 2023-08-14 20:47:40 +03:00
Tulir Asokan 4f0770b92d Update Telethon 2023-08-13 17:40:39 +03:00
Sumner Evans 1fb8a7a0a5 stickers: passthrough webm and tgs files
I got the mime type of tgs files from here:
https://github.com/tulir/Telethon/blob/main/telethon/utils.py#L54

Signed-off-by: Sumner Evans <sumner@beeper.com>
2023-08-08 13:51:28 -06:00
Tulir Asokan f79ab283f3 Don't clear saved username based on min user object 2023-08-03 20:37:01 +03:00
Tulir Asokan 23ec691128 Handle "inactive" reactions when fetching allowed list 2023-07-21 12:33:28 +03:00
Tulir Asokan 59213ebeae Add warning log if GetAvailableReactions returns unexpected data 2023-07-06 13:26:01 +03:00
Tulir Asokan 36b2f6af2e Fix bridging reactions if server was rebooted less than 12 hours ago
Fixes #915
2023-07-06 13:24:13 +03:00
Tulir Asokan b2249f7756 Add log when message handling finishes 2023-07-06 13:24:10 +03:00
Tulir Asokan 212023d296 Don't send logout bridge state event if the user was already logged out 2023-07-03 19:21:24 +03:00
Tulir Asokan 4b03134620 Log when username changes 2023-07-03 19:20:12 +03:00
Tulir Asokan 806eea53eb Bump version to 0.14.1 2023-06-26 13:11:49 +03:00
Tulir Asokan 4ca3ee58ac Update changelog 2023-06-26 13:08:47 +03:00
Tulir Asokan 8b003f1187 Drop Python 3.8 support 2023-06-26 13:08:38 +03:00
Tulir Asokan c06a2b2473 Update Docker image to Alpine 3.18 2023-06-26 13:08:02 +03:00
Tulir Asokan f2194c6f33 Update mautrix-python 2023-06-25 13:47:01 +03:00
Tulir Asokan b5c294a558 Update dependencies 2023-06-14 16:15:56 +03:00
Tulir Asokan c6b6ec048e Add debug logs and workaround for forward backfill getting stuck 2023-06-14 16:15:56 +03:00
Nick Mills-Barrett fb461109c1 Fix socks proxy (#921)
* Replace pysocks with python-socks

* Log proxy settings on init

* Rename extra requirement group

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

---------

Co-authored-by: Tulir Asokan <tulir@maunium.net>
2023-06-09 15:38:32 +01:00
Tulir Asokan 0411affc88 Merge pull request #920 from exciler/support_ipv6
Add support for IPv6-only hosts
2023-06-06 11:48:13 +03:00
Andreas Palm dfe22800dd Add support for IPv6-only hosts 2023-06-05 22:53:37 +02:00
Tulir Asokan 7868b05ed3 Fix typo when reading config option. Fixes #916 2023-05-31 21:42:53 +03:00
Tulir Asokan 0474f81044 Update Telethon 2023-05-26 13:43:19 +03:00
Tulir Asokan ed471a6623 Add db wal files to gitignore 2023-05-26 13:43:14 +03:00
Tulir Asokan 4504973aff Bump version to 0.14.0 2023-05-26 12:24:43 +03:00
Tulir Asokan a5a71edede Add missing word 2023-05-17 19:04:54 +03:00
Tulir Asokan e1c800f3e6 Update mautrix-python 2023-05-16 19:47:01 +03:00
Tulir Asokan 810f86343a Fix group backfill limit copying 2023-05-08 17:56:27 +03:00
Tulir Asokan 5f7d3ac8c1 Split forward backfill limits by chat type 2023-05-08 17:46:09 +03:00
Malte E cb5c51cd27 Add portal to cache when creating chat from Matrix side (#902) 2023-05-07 18:09:20 +03:00
Stefano Pigozzi 759ccf301c Allow filtering direct chats with filter config (#892) 2023-05-07 18:03:48 +03:00
Tulir Asokan 40e4c7e251 Update changelog 2023-05-07 17:57:21 +03:00
Tulir Asokan e12f1784e2 Only handle /start in private chats 2023-05-07 17:39:25 +03:00
Tulir Asokan 6b8e265f8b Fix case of word in error response 2023-04-30 22:20:55 +03:00
Tulir Asokan de33b553be Add messages to MSS events 2023-04-26 15:46:09 +03:00
Tulir Asokan ed24a0b89f Handle flood waits in provisioning API code and password steps 2023-04-25 19:29:25 +03:00
Tulir Asokan e2697e5a17 Update dependencies 2023-04-24 18:42:19 +03:00
Tulir Asokan c4037ccf11 Add option to disable reply fallbacks 2023-04-23 22:47:28 +03:00
Sumner Evans 6c6fe134ba contact info: omit is_bridge_bot, is_bot -> is_network_bot
Signed-off-by: Sumner Evans <sumner@beeper.com>
2023-04-18 10:20:36 -06:00
Sumner Evans e3c45f6f27 puppet/contact info: set is_bot correctly
Signed-off-by: Sumner Evans <sumner@beeper.com>
2023-04-18 09:46:10 -06:00
Tulir Asokan 732258c093 Don't sync dialogs with no real messages 2023-04-18 17:22:57 +03:00
Sumner Evans 8726fa5d74 puppet: add contact info to all member events
Signed-off-by: Sumner Evans <sumner@beeper.com>
2023-04-14 09:36:09 -06:00
Sumner Evans da61ba96f1 db/puppet: add contact_info_set flag
Signed-off-by: Sumner Evans <sumner@beeper.com>
2023-04-14 09:36:06 -06:00
Tulir Asokan 815ce40989 Add option to not set room meta in encrypted rooms 2023-04-14 14:32:55 +03:00
Tulir Asokan 4ff6a62dab Update mautrix-python 2023-04-14 12:16:59 +03:00
Sumner Evans 918582c967 auth: change wording of error when user terminates all sessions
Signed-off-by: Sumner Evans <sumner@beeper.com>
2023-04-14 00:21:59 -06:00
Tulir Asokan 40c584b121 Add options to automatically delete/ratchet megolm sessions 2023-04-13 21:23:44 +03:00
Tulir Asokan f189dc8c88 Update mautrix-python 2023-04-13 11:25:09 +03:00
Sumner Evans b291c246f4 auth: better error when user terminates session
Signed-off-by: Sumner Evans <sumner@beeper.com>
2023-04-12 22:49:08 -06:00
Tulir Asokan 59ab7be283 Add fi.mau.gif flag to gifs and animated stickers 2023-03-28 12:26:17 +03:00
Tulir Asokan 60981386ec Update mautrix-python 2023-03-23 14:06:23 +02:00
Tulir Asokan 436781215f Don't explode if fetching dialog info fails 2023-03-18 12:05:42 +02:00
Tulir Asokan 9c4b24475c Add missing int casts when sending audio/video 2023-03-14 10:45:00 +02:00
Tulir Asokan ff8d1fc9ec Fix variable name. Fixes #898 2023-03-13 17:17:53 +02:00
Tulir Asokan 5f04729ce8 Preserve reaction timestamps if possible 2023-03-13 13:45:32 +02:00
Tulir Asokan 60526f981a Add another warning to double_puppet_backfill option 2023-03-13 13:39:42 +02:00
Tulir Asokan e39d4972fb Update Telethon 2023-03-13 13:39:25 +02:00
Tulir Asokan 233468b37b Sync mute status even if portal is created outside dialog sync
Closes #897
2023-03-10 13:35:26 +02:00
Tulir Asokan 6eda8bd165 Update Telethon
Fixes #896
2023-03-10 13:23:15 +02:00
Tulir Asokan 7372e7cbea Add fallback messages for calls and premium gifts 2023-03-01 14:02:17 +02:00
Tulir Asokan 1fed2201db Update Telethon to fix handling logouts and other update loop errors 2023-02-28 13:49:41 +02:00
Tulir Asokan 60b1573386 Bump version to 0.13.0 2023-02-26 17:24:11 +02:00
Tulir Asokan f4695d8395 Update changelog 2023-02-26 15:05:49 +02:00
Tulir Asokan f63c679d3e Catch errors updating initial profile. Fixes #860 2023-02-22 01:31:32 +02:00
Tulir Asokan 4e5305c91b Update Telethon to save update state more actively (ref #894) 2023-02-22 01:02:47 +02:00
Tulir Asokan f30c03a727 Block creating rooms for deactivated chats (ref #894) 2023-02-21 22:34:21 +02:00
Tulir Asokan 354b49d9e5 Remove unnecessary dependencies in dockerfile and update changelog 2023-02-15 23:01:09 +02:00
Tulir Asokan 7b60ee1337 Actually save timestamps for telegram_file 2023-02-15 21:51:49 +02:00
Tulir Asokan ab1d9b246e Replace moviepy with directly using ffmpeg for video thumbnails
Fixes #809
2023-02-15 21:51:44 +02:00
Tulir Asokan f7b694c9e4 Use new wrapper for creating background tasks 2023-02-11 22:41:15 +02:00
Tulir Asokan be6f6bbfac Update linters 2023-02-11 22:40:50 +02:00
Tulir Asokan a32f797b0b Remove support for registering accounts 2023-02-10 21:20:51 +02:00
vurpo f12abbe038 Merge pull request #887 from mautrix/vurpo/qr-websocket
Add websocket for QR login to provisioning API
2023-01-27 18:40:35 +02:00
Max Sandholm ad2b49928a Sort imports 2023-01-27 17:40:12 +02:00
Max Sandholm 67f75796fa Correct retry and timeout for QR websocket 2023-01-27 17:37:48 +02:00
Tulir Asokan c235ced030 Update dependencies 2023-01-27 15:11:15 +02:00
Tulir Asokan d53764fd84 Remove custom TTLs in bridge states 2023-01-27 15:11:15 +02:00
Tulir Asokan 529d8ae3ba Recreate whole connection instead of only update loop on error 2023-01-27 15:11:15 +02:00
Max Sandholm f864f66e62 Add websocket for QR login to provisioning API 2023-01-26 23:43:44 +02:00
Tulir Asokan b1b633bcf9 Add option to notify portal if incoming message bridging fails 2023-01-26 16:01:59 +02:00
Tulir Asokan e655e0a882 Only send marker for backwards backfills on hungryserv 2023-01-18 14:28:12 +02:00
Tulir Asokan db88fbb694 Remove internal ID from pm command help (ref #882) 2023-01-15 19:05:24 +02:00
Tulir Asokan ace3e42281 Update mautrix-python 2023-01-14 14:28:45 +02:00
Tulir Asokan a40000e6b7 Only fill bridge state if tgid is set 2023-01-14 14:28:22 +02:00
Tulir Asokan 21d2d7dfea Update telethon 2023-01-11 12:13:59 +02:00
Tulir Asokan a61731a289 Update changelog 2023-01-10 16:03:50 +02:00
Tulir Asokan c250076032 Update mautrix-python 2023-01-10 16:03:39 +02:00
vurpo c6d35b103a Merge pull request #880 from mautrix/max/bri-5580
Fix remaining reconnect bug in provision API
2023-01-04 18:49:03 +02:00
Max Sandholm 596c9a5055 None check puppet on logout call 2023-01-04 18:21:25 +02:00
Tulir Asokan 9fae4f14d2 Handle getting logged out the same way in all cases 2023-01-03 21:45:25 +02:00
Tulir Asokan f1f0b86696 Fix deleting existing backfill queue items 2023-01-03 20:45:55 +02:00
Tulir Asokan e3d2a1fcef Catch ValueErrors in 2fa login step 2023-01-02 17:46:54 +02:00
Tulir Asokan 2303622475 Update changelog 2023-01-02 17:16:24 +02:00
vurpo 732277be5e Merge pull request #879 from mautrix/stickersets
Add provisioning API function to get list of user's sticker sets
2023-01-02 16:27:40 +02:00
Max Sandholm 28f205057f Lint imports after enabling linting 2023-01-02 15:11:27 +02:00
Max Sandholm 9e32ec3e39 Add provisioning API function to get list of user's sticker sets 2023-01-02 15:04:49 +02:00
Tulir Asokan 1fa86cbb52 Fix handling username updates 2022-12-31 12:24:33 +02:00
Tulir Asokan 9d8a4d4269 Use allow_contact_info flag for names too 2022-12-30 20:29:35 +02:00
Tulir Asokan cb22615bb5 Update Telethon 2022-12-30 20:17:25 +02:00
Tulir Asokan 989dc32481 Don't fail on unnamed files with unknown mime types 2022-12-28 13:15:13 +02:00
Tulir Asokan 02dd44ad63 Update Telethon 2022-12-22 22:50:21 +02:00
Tulir Asokan d6517959d8 Update dependencies 2022-12-21 18:31:18 +02:00
Tulir Asokan d9d539c4b8 Don't fail file transfer entirely if thumbnailing fails 2022-12-21 18:23:21 +02:00
Tulir Asokan 5b18ffb7ec Fix handling UpdateUserName 2022-12-11 13:37:08 +02:00
Tulir Asokan cf70efb6a2 Clear backfill queue when chat is upgraded 2022-12-02 16:53:58 +02:00
Tulir Asokan a42699e1fb Fix cryptg version range 2022-11-28 12:00:03 +02:00
Tulir Asokan 597e82a33b Update Docker image to Alpine 3.17 2022-11-26 22:02:34 +02:00
Tulir Asokan e302143b8a Bump version to 0.12.2 2022-11-26 19:49:45 +02:00
Tulir Asokan e99b6af2c5 Update Telethon again 2022-11-26 19:48:07 +02:00
Tulir Asokan 35a16ac7e0 Update Telethon 2022-11-24 11:04:20 +02:00
Tulir Asokan 0d20d9069a Remove cchardet. Fixes #869 2022-11-24 11:04:15 +02:00
Tulir Asokan 8b1d272827 Remove unused TARGETARCH build arg in dockerfile 2022-11-22 16:33:37 +02:00
Tulir Asokan 24b3384570 Update asyncpg
Fixes #867
2022-11-18 19:30:19 +02:00
Tulir Asokan 4ca5bfb1ab Use deterministic event IDs for backfill on hungryserv 2022-11-18 18:59:38 +02:00
Tulir Asokan 7c8cf3cb50 Always treat UpdateShortChatMessage as minigroup messages 2022-11-18 17:11:45 +02:00
Tulir Asokan 6b55d5bb41 Adjust heading size in readme 2022-11-18 14:46:19 +02:00
Tulir Asokan 5558fc7157 Add more logs for own read receipts 2022-11-08 10:42:42 +02:00
Tulir Asokan 30a7121000 Update Telethon 2022-11-05 22:55:45 +02:00
Tulir Asokan fb1568d019 Update changelog 2022-11-05 19:27:04 +02:00
Tulir Asokan a0dca671d8 Remove regex filters in provisioning API paths
They're broken due to https://github.com/aio-libs/aiohttp/issues/5621
2022-11-05 19:25:47 +02:00
Andrew Ferrazzutti d79870801b Add index to speed up Message.find_recent query (#862) 2022-11-01 21:25:55 +02:00
Tulir Asokan 2a238a95a9 Merge pull request #861 from vector-im/bot-future-type-check
Add type checking & None check on bot login future
2022-10-31 14:36:19 +02:00
Tulir Asokan 4bfcf46e36 Bridge changes to permissions from Telegram 2022-10-31 14:31:55 +02:00
Andrew Ferrazzutti 894316f035 Add type checking & None check on bot login future 2022-10-28 11:50:38 +02:00
Tulir Asokan 1c47924624 Update mautrix-python 2022-10-24 22:02:49 +03:00
Tulir Asokan 2973b0f200 Update dependencies 2022-10-20 15:29:22 +03:00
Tulir Asokan 4fc5751ae1 Add note about timestamp massaging to double_puppet_backfill 2022-10-20 15:29:22 +03:00
Sumner Evans d37ca7eae3 provisioning API: client -> app
Signed-off-by: Sumner Evans <sumner@beeper.com>
2022-10-18 16:14:53 -06:00
Tulir Asokan 7960f22be9 Add some more logs in dialog sync 2022-10-14 16:04:22 +03:00
Tulir Asokan 1b11ec290a Fix inserting backfill queue items 2022-10-14 16:04:11 +03:00
Tulir Asokan 751f1d93f3 Try to improve getting forwarded message source entity 2022-10-14 14:53:54 +03:00
Tulir Asokan f63a7857a6 Reduce takeout loop timeout 2022-10-14 14:38:06 +03:00
Tulir Asokan 017ca24b13 Try to improve handling avatar updates for new users 2022-10-14 14:37:12 +03:00
Tulir Asokan 3c22ab7bd1 Try to automatically detect when data export is accepted 2022-10-14 13:55:43 +03:00
Tulir Asokan 0bbf64d240 Add option to sync portals in backfill queue 2022-10-14 13:55:12 +03:00
Tulir Asokan af2f20f7b2 Add support for sending members in /createRoom 2022-10-13 15:31:22 +03:00
Tulir Asokan fef03ddec0 Maybe actually fix time comparison 2022-10-12 22:09:23 +03:00
Tulir Asokan f2d0489488 Fix another bug 2022-10-12 16:46:42 +03:00
Tulir Asokan f815d5e2fd Fix mistake in legacy backfill 2022-10-12 16:42:41 +03:00
Tulir Asokan c4a5a3eaf7 Cut too long plaintext messages 2022-10-12 16:41:54 +03:00
Tulir Asokan 921cc6ffa9 Update changelog 2022-10-12 11:25:01 +03:00
Tulir Asokan b582e59eee Add option to mark old chats as read even if they're unread on Telegram 2022-10-12 11:24:52 +03:00
Tulir Asokan c9f8b83f62 Set double puppet key in backfill events 2022-10-12 10:56:45 +03:00
Tulir Asokan 8ff99ce916 Improve handling of reaching the start of a chat in backfill 2022-10-11 20:34:19 +03:00
Tulir Asokan 27b23a96b6 Properly use takeout client for backfilling 2022-10-11 17:53:41 +03:00
Tulir Asokan 8ae34223c5 Add timeout for backfill queue waiter to handle retries 2022-10-11 17:32:59 +03:00
Tulir Asokan 699fc9df1f Skip unsupported messages in backfill 2022-10-11 17:28:14 +03:00
Tulir Asokan 951d02bfc3 Don't try to backfill if limit is zero 2022-10-11 16:11:34 +03:00
Tulir Asokan 9b9a3b452d Infinite backfill with MSC2716 (#817)
Disabled by default, with non-infinite fallback mode as the default behavior
2022-10-11 16:03:52 +03:00
Tulir Asokan 02f21a30a8 Update latest revision upgrade 2022-10-11 16:00:04 +03:00
Tulir Asokan e053664c99 Merge remote-tracking branch 'Half-Shot/hs/index-custom-mxid' 2022-10-11 15:59:33 +03:00
Tulir Asokan 949c6a318f Don't remove all reactions when one is redacted 2022-10-01 17:32:35 +03:00
Tulir Asokan f5cb8baf99 Get reaction limit from server app config 2022-10-01 17:27:56 +03:00
Tulir Asokan 025b864bd8 Allow reacting with any unicode emoji using custom pack 2022-10-01 17:17:27 +03:00
Half-Shot b4fcccbe10 fix filename 2022-09-30 10:04:57 +01:00
Half-Shot b9331b5f5a Add index to puppet custom_mxid column 2022-09-30 10:00:16 +01:00
Tulir Asokan 81aa0084e7 Update Telethon 2022-09-27 18:52:02 +03:00
Tulir Asokan 58bc6788aa Bump version to 0.12.1 2022-09-26 21:42:51 +03:00
Tulir Asokan 5a767a2d92 Update Telethon 2022-09-25 17:06:17 +03:00
Tulir Asokan 282ad43180 Update changelog and mautrix-python 2022-09-24 13:58:13 +03:00
Tulir Asokan bcb30ce807 Update Telethon 2022-09-21 15:27:41 +03:00
Tulir Asokan 2d865f006e Don't use row.get to be compatible with sqlite3.Row 2022-09-20 18:43:41 +03:00
Tulir Asokan b2daebead6 Catch errors when updating read status or tags. Fixes #812 2022-09-20 11:11:59 +03:00
Tulir Asokan 4210091e9a Fix some bugs 2022-09-20 01:59:47 +03:00
Tulir Asokan 4db09f2240 Update Telethon 2022-09-20 00:32:47 +03:00
Tulir Asokan e0260eb551 Don't recreate update loop on UnauthorizedErrors 2022-09-20 00:26:42 +03:00
Tulir Asokan ed1e5474bf Update latest revision migration 2022-09-19 19:10:16 +03:00
Tulir Asokan 65bd7fcc49 Use mautrix-python magic wrapper. Fixes #594 2022-09-17 15:00:49 +03:00
Tulir Asokan 80834ccec1 Update changelog 2022-09-17 14:29:50 +03:00
Tulir Asokan 026c39a3de Add support for new reaction stuff
* Custom emojis in reactions
* Premium users can react 3 times to a single message
* Reactions to recent messages are now polled on read receipt
2022-09-17 14:25:06 +03:00
Tulir Asokan 95939dfa02 Update mautrix-python to fix encrypting when a single device is out of OTKs 2022-09-15 21:55:01 +03:00
Tulir Asokan 279da9097c Update mautrix-python 2022-09-15 17:18:35 +03:00
Tulir Asokan 97126332da Add option to bypass startup script. Closes #838 2022-09-15 17:18:35 +03:00
Tulir Asokan 6641b9a16c Save own ID as message sender ID for messages without sender 2022-09-15 17:18:35 +03:00
Tulir Asokan 927c9afa84 Move config env overrides to mautrix-python 2022-09-15 17:18:35 +03:00
Tulir Asokan d41d7ca0a6 Handle ChatParticipantsForbidden 2022-09-15 17:18:35 +03:00
Tulir Asokan ad0c6cfc8d Run connection tracking task if status_endpoint is set 2022-09-13 16:36:38 +03:00
Tulir Asokan 0289f4b524 Bump version to 0.12.0 2022-08-26 16:22:06 +03:00
Malte E 85b8f5def7 Don't check whether User is channel, add peer property to User 2022-08-24 21:13:11 +03:00
Tulir Asokan f012cb790f Update mautrix-python again 2022-08-22 17:48:10 +03:00
Tulir Asokan 05476d7435 Update mautrix-python 2022-08-22 13:00:08 +03:00
Tulir Asokan 583427da05 Enable appservice ephemeral events by default 2022-08-22 12:57:41 +03:00
Tulir Asokan e3a067c27a Update mautrix-python 2022-08-17 15:20:38 +03:00
Tulir Asokan b3ed4cf657 Fix handling messages with no sender 2022-08-17 15:14:07 +03:00
Tulir Asokan 952c81eadc Update mautrix-python 2022-08-15 11:40:28 +03:00
Tulir Asokan cc29ce19ca Add missing parameter when handling Matrix files 2022-08-15 11:09:10 +03:00
Tulir Asokan 941aa5f9d8 Fix mistake in mark_disappearing 2022-08-14 14:28:23 +03:00
Tulir Asokan 15e5cc8da1 Add command to kick relaybot users from Telegram 2022-08-14 14:20:43 +03:00
Tulir Asokan 2cf9205cda Add command to ban relaybot users from Telegram
Fixes #357
Closes #819
2022-08-14 14:07:48 +03:00
Tulir Asokan 2ec89bc57e Add keywords to mark_matrix_handled calls 2022-08-14 13:47:00 +03:00
Tulir Asokan 89294c57d8 Store message sender in database 2022-08-14 13:44:59 +03:00
Tulir Asokan 624c72fa99 Merge remote-tracking branch 'zsinskri/delivery-receipts' 2022-08-14 12:52:33 +03:00
Tulir Asokan 34af580846 Move misc things from infinite backfill PR 2022-08-14 12:50:28 +03:00
Tulir Asokan 910a681f4b Mark key parameters as positional-only in async getter lock methods 2022-08-14 12:49:45 +03:00
Tulir Asokan c4c225343c Add backfill queue table 2022-08-14 12:49:13 +03:00
Tulir Asokan f13a9d0e96 Add support for disappearing messages 2022-08-14 01:49:39 +03:00
Tulir Asokan c54ae9548f Add support for converting video stickers to images 2022-08-14 00:53:21 +03:00
Tulir Asokan 1216607763 Add custom attribute for custom emojis 2022-08-12 22:45:52 +03:00
Tulir Asokan ecd4d5c338 Limit number of custom emoji being transferred simultaneously 2022-08-12 22:14:53 +03:00
Tulir Asokan a5fe05cff2 Add support for converting animated stickers to webp 2022-08-12 22:07:52 +03:00
Tulir Asokan 76eafbf48c Add basic support for bridging custom emojis from Telegram 2022-08-12 21:35:50 +03:00
Tulir Asokan 473ab17fe7 Update Telethon and strip empty entities when sending to Telegram 2022-08-02 13:46:06 +03:00
Tulir Asokan bea9bc4ec0 Mention forwarding limitations in changelog. Closes #818 2022-07-29 12:24:41 +03:00
Tulir Asokan 5df1e84fae Update mautrix-python 2022-07-29 12:23:46 +03:00
Tulir Asokan 8665871502 Fix some issues with auto-creating groups 2022-07-18 13:01:50 +03:00
Zsin Skri ef57f1021c Revert "Don't send delivery receipts to unencrypted private chat portals. Fixes #483"
This reverts commit a4595b427d.

Commit a4595b4 avoids sending delivery receipts to rooms that do not contain the
bridge bot.  That was necessary as trying to send a read marker would
automatically attempt to join the bridge bot to the room.
That join without invite would fail, hence #483.

But since
https://github.com/mautrix/python/commit/f272f16a1d151a1c6612c9349776eda985c8ea3e
we no longer attempt to join the sender of read receipts, fixing #483 without
necessarily sacrificing the delivery receipt functionality.

Thus:
- a4595b4 is no longer necessary, its original purpose is fulfilled by f272f16.
- a4595b4 prevents delivery receipts from working in unencrypted rooms.
- This reverts a4595b4, thus enabling delivery receipts in unencrypted rooms.
2022-07-17 20:57:17 +02:00
Tulir Asokan b6312f306a Move config check when handling ghost invites 2022-07-17 16:09:02 +03:00
Tulir Asokan 70b73868c7 Merge remote-tracking branch 'maltee1/auto_create_group' 2022-07-17 16:04:56 +03:00
Tulir Asokan 0717b4a290 Disable public portals by default 2022-07-17 16:04:20 +03:00
Tulir Asokan a9b6539910 Update changelog 2022-07-13 14:24:13 +03:00
Tulir Asokan 49520bb8a3 Try to avoid race conditions with supergroup upgrades 2022-07-13 14:20:39 +03:00
Tulir Asokan 7abe19aec9 Add another backfill column 2022-07-13 12:17:35 +03:00
Malte E 3dd0c51be7 add config option, update roadmap 2022-07-12 21:37:41 +02:00
Malte E 565bb87470 implement handle_puppet_group_invite to auto-create groups 2022-07-12 21:02:14 +02:00
Tulir Asokan 9188251501 Add status field to message status events 2022-07-12 15:05:06 +03:00
Tulir Asokan cb11e147ce Add support for Matrix -> Telegram captions with MSC2530 2022-07-12 11:35:51 +03:00
Tulir Asokan eb1190359d Update asyncpg 2022-07-10 18:20:53 +03:00
Tulir Asokan cdfc6fd007 Remove noisy error on ignored messages 2022-07-10 18:20:46 +03:00
Tulir Asokan df9b7d343e Add support for forwarding messages 2022-07-07 13:02:01 +03:00
Tulir Asokan f26973f46c Update changelog and mautrix-python 2022-07-06 19:37:29 +03:00
Tulir Asokan 2335431060 Update mautrix-python again 2022-07-05 20:06:29 +03:00
Tulir Asokan 8fd97af0a9 Update mautrix-python 2022-07-05 13:26:03 +03:00
Tulir Asokan 3ea491d379 Fix handling location messages 2022-07-04 10:44:37 +03:00
Tulir Asokan 3bd7d846f4 Update mautrix-python 2022-06-28 19:35:23 +03:00
Tulir Asokan 99344c38a4 Create room on UpdateChannel 2022-06-28 19:20:18 +03:00
Tulir Asokan d917499d1f Fix check for using double puppeted leaves 2022-06-28 19:14:37 +03:00
Tulir Asokan 98da5fecc3 Use wildcard for cryptg wheel name 2022-06-27 21:41:59 +03:00
Tulir Asokan 6b0ece5da1 Update cryptg 2022-06-27 21:39:18 +03:00
Tulir Asokan 448b149e8e Make docker image smaller 2022-06-27 21:14:14 +03:00
Tulir Asokan 120514125f Update changelog 2022-06-27 20:52:47 +03:00
Tulir Asokan cd4b4365bd Update Docker image to Alpine 3.16 2022-06-27 15:59:11 +03:00
Tulir Asokan 8f68801aa9 Maybe improve channel leave handling 2022-06-27 15:59:11 +03:00
Sumner Evans 1d0e8c7e0c Merge pull request #810 from mautrix/mautrix-0.16.10
deps/mautrix: update to v0.16.10
2022-06-24 11:42:23 -06:00
Sumner Evans 3ff43165c2 deps/mautrix: update to v0.16.10
Signed-off-by: Sumner Evans <sumner@beeper.com>
2022-06-24 11:33:49 -06:00
Tulir Asokan 1fdbdb654a Update Telethon for API layer update 2022-06-24 11:17:24 +03:00
Tulir Asokan 0e024b3b7c Fetch participant count if it's not included in the entity 2022-06-23 23:59:44 +03:00
Tulir Asokan e1a5e30a75 Remove legacy crypto database from example config
It doesn't seem to be used anywhere
2022-06-23 09:58:10 +03:00
Sumner Evans 05d4923db9 encryption: add ability to control rotation settings
Signed-off-by: Sumner Evans <sumner@beeper.com>
2022-06-23 09:58:10 +03:00
Tulir Asokan f18713cd5e Update changelog 2022-06-22 13:28:16 +03:00
Tulir Asokan ef05875bfd Remove plaintext_highlights config option
The code using it was removed in v0.11.0, so it hasn't actually worked
for a while now.
2022-06-22 12:40:30 +03:00
Tulir Asokan 59d85a1e16 Fix incorrect method call 2022-06-22 12:35:58 +03:00
Tulir Asokan 7eec0d1ed3 Add index on puppet username. Fixes #799 2022-06-22 12:30:07 +03:00
Tulir Asokan f917ee189d Don't require puppeting for caption and config commands
Fixes #790
2022-06-22 12:30:01 +03:00
Tulir Asokan ab2e38b33b Merge remote-tracking branch 'ofalvai/patch-1' 2022-06-22 12:10:52 +03:00
Tulir Asokan 38af35e776 Merge remote-tracking branch 'cynhr/cmd-prefix' 2022-06-22 12:10:44 +03:00
Tulir Asokan c6adb87aea Set sync_channel_members to false by default 2022-06-22 12:09:38 +03:00
Tulir Asokan e8eef1c31e Add option to not bridge chats with too many members 2022-06-22 12:05:48 +03:00
Tulir Asokan bac3abcb4c Update mautrix-python 2022-06-21 15:42:27 +03:00
Tulir Asokan c682bdc01e Update dependencies 2022-06-19 15:05:59 +03:00
Cy Nhr 50cd878f13 Include command prefix in game and poll messages
Game and poll messages send by the bridge to matrix each include a command the
receiver might want to run (to play the game or to vote in the poll).

But these command suggestions did always include the "!tg" command prefix, even
if the command prefix was changed to a different value in the config.  That
could lead to the bridge ignoring the exact command it suggested earlier.

With this commit, these messages contain the correct command prefix as defined
in the config so that the command suggestions can be executed by the user
without manually correcting the prefix.
2022-06-18 20:23:59 +02:00
Tulir Asokan ea49ba8be2 Move CI script to mautrix/ci repo 2022-06-18 14:23:01 +03:00
Tulir Asokan b60056c560 Add missing prefix to bridge info endpoint 2022-06-18 09:56:54 +03:00
Tulir Asokan 820210dc44 Fix bridging polls from Telegram 2022-06-02 19:40:23 +03:00
Tulir Asokan 7d998dca3f Add support for custom message bridging status events 2022-06-01 15:36:22 +03:00
Tulir Asokan 037d93471d Catch PhoneNumberUnoccupied in /login/send_code provisioning API 2022-05-30 22:18:28 +03:00
Tulir Asokan 5cb2b871cd Fix sticker event type 2022-05-29 00:35:25 +03:00
Tulir Asokan 44f2c648a8 Add config option to exit if telethon update loop fails 2022-05-26 17:37:21 +03:00
Tulir Asokan 0ae8a5877e Rename db upgrade 2022-05-26 17:28:44 +03:00
Tulir Asokan 18f6622340 Separate Telegram message conversion code from Matrix sending 2022-05-26 15:46:20 +03:00
Tulir Asokan 591e79f5a0 Enable catch_up and sequential_updates by default 2022-05-25 16:49:59 +03:00
Tulir Asokan d898486b49 Add first_event_id and next_batch_id columns for portals 2022-05-25 14:56:41 +03:00
Tulir Asokan 74e0aee421 Update Telethon a third time 2022-05-23 17:58:34 +03:00
Tulir Asokan 07f32e1256 Update Telethon again 2022-05-23 14:59:36 +03:00
Tulir Asokan ea680cf871 Update Telethon 2022-05-23 14:22:11 +03:00
Tulir Asokan e89c75c6cd Don't try to stop relaybot if it's not enabled 2022-05-23 10:46:00 +03:00
Tulir Asokan 59d052afd2 Update Telethon 2022-05-20 21:55:22 +03:00
Tulir Asokan 9383249ade Stop relaybot connection cleanly 2022-05-20 18:44:36 +03:00
Tulir Asokan 0a4f30bf02 Update setup doc links 2022-05-20 15:10:30 +03:00
Tulir Asokan 190f452910 Fix some bugs and update Telethon 2022-05-20 14:24:28 +03:00
Tulir Asokan 3c59a1af97 Adjust logs slightly 2022-05-20 12:28:39 +03:00
Tulir Asokan 11ff628ef8 Always check database before handling message 2022-05-20 12:02:32 +03:00
Tulir Asokan 908e600dc9 Switch /resolve_identifier to GET 2022-05-19 18:22:04 +03:00
Tulir Asokan eb43fde3e4 Add provisioning API for resolving identifiers 2022-05-19 13:15:44 +03:00
Tulir Asokan e6ef40e51d Update Telethon 2022-05-19 13:15:39 +03:00
Tulir Asokan 7feea5aa6d Redact QR code after login 2022-05-16 19:13:06 +03:00
Lonami d084cca983 Add get_update_states to telethon_session (#795)
This is needed for an upcoming patch in order to
properly catch up on all channels the client is in.
2022-05-16 19:09:39 +03:00
Tulir Asokan d9018868a1 Use new helper method to redact command 2022-05-10 17:27:03 +03:00
Tulir Asokan 72360457ef Bridge audio and video metadata properly 2022-05-10 17:13:14 +03:00
Tulir Asokan 0e4c1b71e6 Redact 2fa password when using in-Matrix login 2022-05-10 17:04:39 +03:00
Olivér Falvai 575b761f77 Increase image_as_file_pixels default value 2022-05-07 12:23:01 +02:00
Tulir Asokan 68e950a6bc Add issue templates 2022-04-20 14:02:36 +03:00
Sumner Evans ba5bbebb3e Merge pull request #788 from mautrix/dev-update-stable-and-nightly
ci: automatically update both STABLE and NIGHTLY on dev environment
2022-04-19 08:56:58 -06:00
Sumner Evans cb38896593 ci: automatically update both STABLE and NIGHTLY on dev environment 2022-04-18 19:23:04 -06:00
Tulir Asokan 21c6a7d87f Bump version to 0.11.3 2022-04-17 13:30:38 +03:00
Tulir Asokan 7c2a569235 Remove some unused fields 2022-04-13 14:43:53 +03:00
Tulir Asokan 1f5b91cbec Update mautrix-python 2022-04-09 20:52:45 +03:00
Tulir Asokan 937f37eff0 Don't print generated registration message if config is invalid 2022-04-09 20:46:25 +03:00
Tulir Asokan 4f9f74204a Update dependencies 2022-04-08 18:06:24 +03:00
Tulir Asokan ed6735f10f Fix creating new database 2022-04-06 19:04:12 +03:00
Tulir Asokan 5acd3cf007 Move API version number to endpoint definition 2022-04-06 14:33:03 +03:00
Tulir Asokan 279b997bd3 Add contacts and PM endpoints to OpenAPI spec 2022-04-06 14:29:50 +03:00
Tulir Asokan 4eb6095822 Update provisioning API spec to OpenAPI 3.1.0 2022-04-06 14:06:10 +03:00
Tulir Asokan da5b8556f2 Add phone number field for puppets 2022-04-06 12:49:01 +03:00
Tulir Asokan 261f99ac82 Add provisioning API for listing contacts and starting DMs 2022-04-06 12:40:55 +03:00
Tulir Asokan 61f3c39cc2 Mark reactions as read when reading from Matrix 2022-04-01 15:56:16 +03:00
Tulir Asokan 39ab1d0c22 Fix another bug 2022-03-31 01:58:40 +03:00
Tulir Asokan 8abb9c3884 Fix bugs in Telegram entity parser 2022-03-31 01:53:51 +03:00
Tulir Asokan 58f8ee2ee2 Add config option to mark joined Telegram notices as read automatically 2022-03-30 11:58:40 +03:00
Tulir Asokan 474bcc9544 Update and unpin black 2022-03-28 22:29:22 +03:00
Tulir Asokan a3f4e25101 Fix some bugs and add command to list invite links 2022-03-28 15:49:08 +03:00
Tulir Asokan 8befb664b6 Handle accepted into group action messages 2022-03-28 15:06:35 +03:00
Tulir Asokan 819dd1bcff Allow generating invite links that need join approval 2022-03-28 15:03:22 +03:00
Tulir Asokan 2b8b853fec Add proper message when requesting to join via invite link 2022-03-28 15:03:05 +03:00
Tulir Asokan c536c4a265 Update changelog 2022-03-27 23:39:46 +03:00
Tulir Asokan f13acfe825 Clarify that supergroups are channels in !tg bridge 2022-03-27 23:39:46 +03:00
Sumner Evans 8e763ba067 Merge pull request #775 from mautrix/sumner/bri-2582
async media: add ability to upload media asynchronously
2022-03-27 12:31:39 -06:00
Sumner Evans 8d7cfd8e46 parallel transfer: disable async_upload 2022-03-27 12:26:44 -06:00
Sumner Evans 601058d61c async media: add ability to upload media asynchronously
Co-authored-by: Tulir Asokan <tulir@maunium.net>
2022-03-27 12:26:44 -06:00
Tulir Asokan f8596ef368 Use new ensure_has_html method instead of duplicating code 2022-03-23 19:51:01 +02:00
Tulir Asokan 7f0494d52d Merge remote-tracking branch 'origin/sumner/bri-2496' 2022-03-22 16:29:48 +02:00
Sumner Evans 828478514b Merge pull request #772 from mautrix/fix-kick-from-portals
user: fix bug in kick_from_portals
2022-03-22 08:00:02 -06:00
Tulir Asokan 146f5437d1 Drop Python 3.7 support 2022-03-22 13:44:52 +02:00
Tulir Asokan c28760f2a8 Adjust permission error messages 2022-03-22 13:44:52 +02:00
Tulir Asokan 04f30f6f29 Update mautrix-python 2022-03-22 13:44:52 +02:00
Tulir Asokan caa1d3565b Update changelog 2022-03-22 13:44:52 +02:00
Sumner Evans 1a7a020bb2 backfill: set timestamp on backfilled reactions to message timestamp 2022-03-22 00:48:12 -06:00
Sumner Evans 077ab2bb38 user: fix bug in kick_from_portals 2022-03-22 00:46:32 -06:00
Sumner Evans 6f491bf7d1 Merge pull request #771 from ProkopRandacek/master
Add missing f in front of the f-string
2022-03-21 10:51:51 -06:00
Prokop Randacek 9b80c21d0a add missing F 2022-03-21 10:11:45 +01:00
Tulir Asokan e9dc76a860 Fix public channel mentions always using user instead of portal mxid 2022-03-15 16:32:21 +02:00
Tulir Asokan 9e73324a20 Fix bridge_matrix_leave config option 2022-03-14 12:00:14 +02:00
Tulir Asokan 7df93485d8 Remove extra parameter in call 2022-03-11 12:02:02 +02:00
Tulir Asokan 9018cea5ae Update changelog 2022-03-07 18:52:15 +02:00
Tulir Asokan 32e023231d Catch invalid integers passed to !tg create 2022-03-05 20:16:04 +02:00
Tulir Asokan 4766d14359 Move DM creation code to mautrix-python 2022-03-04 16:12:02 +02:00
Tulir Asokan 526b99ec04 Disable file logging in Docker by default
To enable it, use a custom path that points at a writable volume
2022-03-04 10:57:08 +02:00
Nick Mills-Barrett da132438bd Only change the data directory ownership on Docker start 2022-03-03 18:17:39 +02:00
Tulir Asokan 54176ba2db Fix self parameter name in _mute_room. Fixes #764 2022-03-02 14:33:09 +02:00
Tulir Asokan 1eca3c2ffd Check peer_type in database when manually bridging portal 2022-03-02 14:33:06 +02:00
Tulir Asokan 98142f28cd Improve logging of backfill count 2022-02-28 12:36:43 +02:00
Tulir Asokan 2cf7fc7059 Improve backfilling to fetch less redundant messages 2022-02-28 12:26:24 +02:00
Tulir Asokan a34a18c6cc Deduplicate user joined telegram messages 2022-02-28 11:59:44 +02:00
Tulir Asokan fa738fbadf Fix condition 2022-02-26 17:20:22 +02:00
Tulir Asokan 9ea0516166 Log when tagging and muting rooms 2022-02-25 19:35:05 +02:00
Tulir Asokan b760aadb01 Add custom flag for force sending images as document 2022-02-25 12:38:01 +02:00
Tulir Asokan 24162e14ac Remove msgtype in stickers 2022-02-23 14:36:53 +02:00
Tulir Asokan 9ea495324d Don't try to set room state in non-existent portals 2022-02-23 12:46:16 +02:00
Tulir Asokan 437e86a15b Keep newlines as-is in code blocks 2022-02-23 12:44:56 +02:00
Tulir Asokan d9e0b75e9b Update mautrix-python again 2022-02-22 13:53:43 +02:00
Tulir Asokan 9606518ba7 Update mautrix-python again 2022-02-22 12:40:16 +02:00
Tulir Asokan e2774b830f Update mautrix-python version 2022-02-22 11:58:27 +02:00
Tulir Asokan 951d82ad27 Remove max_document_size option and use media repo config directly 2022-02-20 13:47:40 +02:00
Tulir Asokan 4a55cf589c Add initial db upgrade that jumps to latest version 2022-02-19 00:19:49 +02:00
Tulir Asokan b07d80d876 Add support for converting t.me/c/<id>/<msgid> links 2022-02-18 17:22:26 +02:00
Tulir Asokan ff995b2149 Bump version to 0.11.2 2022-02-14 18:19:03 +02:00
Tulir Asokan 2fb08d59c7 Return error if user tries to send empty login code to API 2022-02-09 12:05:16 +02:00
Sumner Evans 7950c5aa61 Merge pull request #754 from mautrix/sumner/bri-1893
link previews: support from Telegram -> Beeper
2022-02-08 11:37:23 -07:00
Sumner Evans bf65824429 link previews: support from Telegram -> Beeper
Co-authored-by: Tulir Asokan <tulir@maunium.net>
2022-02-08 11:35:38 -07:00
Tulir Asokan 4013f822de Remove community_id config option 2022-02-06 17:38:15 +02:00
Tulir Asokan b27519fd88 Add proper error message for syntax errors in !tg login. Fixes #755 2022-02-05 12:27:02 +02:00
Tulir Asokan 22f97756f7 Update CHANGELOG.md 2022-02-03 19:26:11 +02:00
Tulir Asokan da3f4af171 Fix newlines in unformatted channel posts 2022-02-03 17:43:35 +02:00
Tulir Asokan a55d9ae36a Improve profile info syncing 2022-02-01 20:51:55 +02:00
Tulir Asokan ecf3a12bd4 Mark user joined Telegram notice as read if it's backfilled 2022-02-01 17:33:53 +02:00
Tulir Asokan e7248e2418 Fix timestamp of photo has expired messages in backfill 2022-02-01 16:48:51 +02:00
Tulir Asokan fba118f0d9 Send joined telegram message instead of leaving portal empty 2022-02-01 16:44:31 +02:00
Tulir Asokan 100394d161 Add support for relay user distinguishers. Fixes #750 2022-02-01 16:05:56 +02:00
Tulir Asokan a9908781be Add basic support for MSC3488 location descriptions 2022-02-01 15:25:24 +02:00
Tulir Asokan 0f050edcd9 Add proper support for receiving messages sent as a channel. Fixes #740 2022-02-01 15:20:05 +02:00
Tulir Asokan 2182dfc86b Update to Telegram API layer 138 2022-02-01 13:35:27 +02:00
Tulir Asokan 99fa7a57d2 Add config option to set maximum image pixels before sending as document
Fixes #552
2022-01-31 15:57:00 +02:00
Tulir Asokan 6bf3d10e29 Improve handling of disappearing photos and files
Fixes #508
2022-01-31 15:49:39 +02:00
Tulir Asokan ebd2a38e56 Update black and fix version in CI 2022-01-30 12:29:05 +02:00
Tulir Asokan 03b094e4d4 Update mautrix-python 2022-01-30 12:04:21 +02:00
Tulir Asokan 21b509e5a0 Copy animated sticker args explicitly to remove unsupported args 2022-01-29 18:15:54 +02:00
Tulir Asokan 2732a85f9e Update dependencies 2022-01-26 13:41:20 +02:00
Tulir Asokan 033141e435 Add warning for users who don't know what they're doing 2022-01-22 16:31:43 +02:00
Sumner Evans 251458a1d7 Merge pull request #745 from mautrix/pre-commit-config
pre-commit: add configuration
2022-01-21 14:13:44 -07:00
Sumner Evans 7c4f406ac6 ci: add pre-commit-hooks to lint process 2022-01-21 11:15:52 -07:00
Sumner Evans 984c52afc9 dev-requirements: add pre-commit, isort, black 2022-01-21 11:15:21 -07:00
Sumner Evans f664d4ad90 pre-commit: add configuration 2022-01-21 10:07:12 -07:00
Sumner Evans 8f61be76f9 Merge pull request #738 from mautrix/sumner/bri-1583-telegram-has-disconnected-i-woke-up-to
bridge state: use TRANSIENT_DISCONNECT if connection drops and is expected to come back soon
2022-01-13 07:44:34 -07:00
Tulir Asokan 8003b9aa1c Fix bug in !tg create. Fixes #736 2022-01-12 21:52:25 +02:00
Sumner Evans a0fd98b9e2 bridge state: use TRANSIENT_DISCONNECT if connection drops and is expected to come back soon 2022-01-12 08:59:09 -07:00
Scott Weber feac31e841 Very basic support for live location 2022-01-11 13:36:15 +02:00
Tulir Asokan dd83d6278c Add support for t.me/+code invite links 2022-01-10 23:23:16 +02:00
Tulir Asokan 2a6b075ff2 Bump version to 0.11.1 2022-01-10 15:45:30 +02:00
Tulir Asokan e321bc30d0 Update some small things 2022-01-09 00:06:35 +02:00
Tulir Asokan 63fafec1b7 Make telegram blue text more readable on dark themes. Fixes #729 2022-01-08 23:27:57 +02:00
Tulir Asokan 9f48eca5a6 Use min() instead of sorting list 2022-01-05 21:23:58 +02:00
Tulir Asokan 28845b9daf Update dependencies and fix some things in config updater 2022-01-05 21:01:12 +02:00
Tulir Asokan 113f41d1d2 Deduplicate lottieconverter calls in tgs_converter
Also fix finding first frame file

Fixes #690
Closes #728
2022-01-05 21:00:53 +02:00
Tulir Asokan da3180e290 Delete nulls in message table. Fixes #731 2022-01-05 18:53:10 +02:00
Tulir Asokan 1a62463678 Update changelog 2022-01-05 12:30:38 +02:00
Tulir Asokan e584cf534d Merge branch 'sumner/bri-1517-bridge-voice-messages-telegram-matrix' 2022-01-05 12:09:25 +02:00
Tulir Asokan 4c1267cd32 Merge branch 'maybe-fix-corrupted-db-schema'
Closes #719
2022-01-05 12:09:16 +02:00
Tulir Asokan dc8a3d0c2d Don't use parameters for pg_constraint query 2022-01-05 01:53:57 +02:00
Sumner Evans c481ec850d voice messages: bridge from Telegram to native Matrix
Co-authored-by: Tulir Asokan <tulir@maunium.net>
2022-01-04 14:16:57 -07:00
Tulir Asokan a54dd58de7 Send message checkpoints for Matrix edits too 2022-01-04 21:37:41 +02:00
Tulir Asokan b13da92520 Find constraint names dynamically to work around schemas broken by pgloader 2022-01-03 20:12:55 +02:00
Dominik Fuchß 2b6db85e1a Add missing await to get_input_entity in HTML parser (#724) 2021-12-31 11:19:41 +02:00
Tulir Asokan e7a1216ef7 Don't redact reactions in chats with relaybot
There are usually other Matrix users, so redacting reactions only from
logged-in users would be weird.
2021-12-30 23:34:14 +02:00
Tulir Asokan b1da5c7c2c Don't alter columns to not null on sqlite 2021-12-30 19:59:41 +02:00
Tulir Asokan 3b72de34b3 Fix some things in dedup changes 2021-12-30 19:41:45 +02:00
Tulir Asokan af893554cc Add support for Matrix->Telegram reactions 2021-12-30 18:32:10 +02:00
Tulir Asokan d108ac5d94 Add support for Telegram->Matrix reactions 2021-12-30 17:43:45 +02:00
Tulir Asokan e446121192 Fix order of operations when syncing contacts 2021-12-30 12:20:36 +02:00
Tulir Asokan afb73b1d17 Add support for bridging spoilers 2021-12-29 22:11:11 +02:00
Tulir Asokan aae8f78cb4 Try to drop identity in addition to default and id_seq in puppet/bot_chat tables
Closes #720
Closes #721

Co-authored-by: Carl Ambroselli <git@carl-ambroselli.de>
2021-12-29 12:47:32 +02:00
18121 changed files with 954808 additions and 15676 deletions
-2
View File
@@ -1,9 +1,7 @@
.editorconfig
.codeclimate.yml
*.png
*.md
logs
.venv
start
config.yaml
registration.yaml
+7 -5
View File
@@ -8,11 +8,13 @@ charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
[*.py]
max_line_length = 99
[*.{yaml,yml,py}]
[*.md]
trim_trailing_whitespace = false
indent_size = 2
indent_style = space
[.gitlab-ci.yml]
[*.{yaml,yml,sql}]
indent_style = space
[{.gitlab-ci.yml,.pre-commit-config.yaml,provisioning-spec.yaml,.github/workflows/*.yml}]
indent_size = 2
+4
View File
@@ -0,0 +1,4 @@
pkg/connector/humanise/errors.go linguist-generated=true
pkg/gotd/tg/* linguist-generated=true
tl_*_gen.go linguist-generated=true
*.gen.go linguist-generated=true
+18
View File
@@ -0,0 +1,18 @@
---
name: Bug report
about: If something is definitely wrong in the bridge (rather than just a setup issue),
file a bug report. Remember to include relevant logs. Asking in the Matrix room first
is strongly recommended.
type: Bug
---
<!-- Include relevant logs, the bridge version and other important details here -->
### Checklist
<!-- All items below are mandatory. Issues not following the rules may be closed without comment. -->
* [ ] This is an actual bug, not just a setup issue (see the [troubleshooting docs](https://docs.mau.fi/bridges/general/troubleshooting.html) or ask in the Matrix room for setup help).
* [ ] I am certain that sufficient information is included. Ask in the Matrix room first if not.
* [ ] The bug is still present on the main branch.
+7
View File
@@ -0,0 +1,7 @@
contact_links:
- name: Troubleshooting docs & FAQ
url: https://docs.mau.fi/bridges/general/troubleshooting.html
about: Check this first if you're having problems setting up the bridge.
- name: Support room
url: https://matrix.to/#/#telegram:maunium.net
about: For setup issues not answered by the troubleshooting docs, ask in the Matrix room.
+6
View File
@@ -0,0 +1,6 @@
---
name: Enhancement request
about: Submit a feature request or other suggestion
type: Feature
---
+39
View File
@@ -0,0 +1,39 @@
name: Go
on: [push, pull_request]
env:
GOTOOLCHAIN: local
jobs:
lint:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
go-version: ["1.25", "1.26"]
name: Lint ${{ matrix.go-version == '1.26' && '(latest)' || '(old)' }}
steps:
- uses: actions/checkout@v6
- name: Set up Go
uses: actions/setup-go@v6
with:
go-version: ${{ matrix.go-version }}
cache: true
- name: Install libolm
run: sudo apt-get install libolm-dev libolm3
- name: Install dependencies
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
-18
View File
@@ -1,18 +0,0 @@
name: Python lint
on: [push, pull_request]
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-python@v2
with:
python-version: "3.10"
- uses: isort/isort-action@master
with:
sortPaths: "./mautrix_telegram"
- uses: psf/black@21.12b0
with:
src: "./mautrix_telegram"
+29
View File
@@ -0,0 +1,29 @@
name: 'Lock old issues'
on:
schedule:
- cron: '0 20 * * *'
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
+13 -15
View File
@@ -1,18 +1,16 @@
/.idea/
.idea
/.venv
/env/
pip-selfcheck.json
*.pyc
__pycache__
/build
/dist
/*.egg-info
/.eggs
*.yaml
!.pre-commit-config.yaml
!example-config.yaml
!provisioning-spec.yaml
/config.yaml
/registration.yaml
*.log*
*.db
*.pickle
*.json
!pkg/connector/emojis/unicodemojipack.json
*.db*
*.log
*.bak
/mautrix-telegram
/mautrix-telegramgo
/start
+3 -66
View File
@@ -1,66 +1,3 @@
image: docker:stable
stages:
- build
- manifest
default:
before_script:
- docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
build amd64:
stage: build
tags:
- amd64
script:
- docker pull $CI_REGISTRY_IMAGE:latest || true
- docker build --pull --cache-from $CI_REGISTRY_IMAGE:latest --build-arg TARGETARCH=amd64 --tag $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA-amd64 .
- docker push $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA-amd64
- docker rmi $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA-amd64
after_script:
- |
if [[ "$CI_COMMIT_BRANCH" == "master" && "$CI_JOB_STATUS" == "success" ]]; then
apk add --update curl jq
rm -rf /var/cache/apk/*
jq -n '
{
password: env.BEEPER_DEV_ADMIN_NIGHTLY_PASS,
bridge: env.BEEPER_BRIDGE_TYPE,
image: "\(env.CI_REGISTRY_IMAGE):\(env.CI_COMMIT_SHA)-amd64",
channel: "STABLE"
}
' | curl "$BEEPER_DEV_ADMIN_API_URL" -H "Content-Type: application/json" -d @-
jq -n '
{
password: env.BEEPER_PROD_ADMIN_NIGHTLY_PASS,
bridge: env.BEEPER_BRIDGE_TYPE,
image: "\(env.CI_REGISTRY_IMAGE):\(env.CI_COMMIT_SHA)-amd64",
channel: "INTERNAL",
deployNext: true
}
' | curl "$BEEPER_PROD_ADMIN_API_URL" -H "Content-Type: application/json" -d @-
fi
build arm64:
stage: build
tags:
- arm64
script:
- docker pull $CI_REGISTRY_IMAGE:latest || true
- docker build --pull --cache-from $CI_REGISTRY_IMAGE:latest --build-arg TARGETARCH=arm64 --tag $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA-arm64 .
- docker push $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA-arm64
- docker rmi $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA-arm64
manifest:
stage: manifest
before_script:
- "mkdir -p $HOME/.docker && echo '{\"experimental\": \"enabled\"}' > $HOME/.docker/config.json"
- docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
script:
- docker pull $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA-amd64
- docker pull $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA-arm64
- if [ "$CI_COMMIT_BRANCH" = "master" ]; then docker manifest create $CI_REGISTRY_IMAGE:latest $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA-amd64 $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA-arm64 && docker manifest push $CI_REGISTRY_IMAGE:latest; fi
- if [ "$CI_COMMIT_BRANCH" != "master" ]; then docker manifest create $CI_REGISTRY_IMAGE:$CI_COMMIT_REF_NAME $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA-amd64 $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA-arm64 && docker manifest push $CI_REGISTRY_IMAGE:$CI_COMMIT_REF_NAME; fi
- docker rmi $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA-amd64 $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA-arm64
include:
- project: 'mautrix/ci'
file: '/gov2-as-default.yml'
+1
View File
@@ -0,0 +1 @@
<svg id="Livello_1" data-name="Livello 1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 240 240"><defs><linearGradient id="linear-gradient" x1="120" y1="240" x2="120" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#1d93d2"/><stop offset="1" stop-color="#38b0e3"/></linearGradient></defs><title>Telegram_logo</title><circle cx="120" cy="120" r="120" fill="url(#linear-gradient)"/><path d="M81.229,128.772l14.237,39.406s1.78,3.687,3.686,3.687,30.255-29.492,30.255-29.492l31.525-60.89L81.737,118.6Z" fill="#c8daea"/><path d="M100.106,138.878l-2.733,29.046s-1.144,8.9,7.754,0,17.415-15.763,17.415-15.763" fill="#a9c6d8"/><path d="M81.486,130.178,52.2,120.636s-3.5-1.42-2.373-4.64c.232-.664.7-1.229,2.1-2.2,6.489-4.523,120.106-45.36,120.106-45.36s3.208-1.081,5.1-.362a2.766,2.766,0,0,1,1.885,2.055,9.357,9.357,0,0,1,.254,2.585c-.009.752-.1,1.449-.169,2.542-.692,11.165-21.4,94.493-21.4,94.493s-1.239,4.876-5.678,5.043A8.13,8.13,0,0,1,146.1,172.5c-8.711-7.493-38.819-27.727-45.472-32.177a1.27,1.27,0,0,1-.546-.9c-.093-.469.417-1.05.417-1.05s52.426-46.6,53.821-51.492c.108-.379-.3-.566-.848-.4-3.482,1.281-63.844,39.4-70.506,43.607A3.21,3.21,0,0,1,81.486,130.178Z" fill="#fff"/></svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

+31
View File
@@ -0,0 +1,31 @@
exclude: pkg/gotd/_fuzz/.*|pkg/gotd/_schema/.*|pkg/gotd/.*\.tmpl
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v6.0.0
hooks:
- id: trailing-whitespace
exclude_types: [markdown]
- id: end-of-file-fixer
- id: check-yaml
- id: check-added-large-files
- repo: https://github.com/tekwizely/pre-commit-golang
rev: v1.0.0-rc.4
hooks:
- id: go-imports
args:
- "-local"
- "go.mau.fi/mautrix-telegram"
- "-w"
- id: go-vet-mod
# Disabled for now until we can find a way to filter out the gotd package
# - id: go-staticcheck-repo-mod
- id: go-mod-tidy
- 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
+379 -5
View File
@@ -1,8 +1,382 @@
# (unreleased)
# unreleased
* Added support for message reactions
* Added support for bridging message reactions from Telegram when logged in as
a bot.
* Fixed `mx_room_state` table not being migrated correctly from the Python
bridge in SQLite databases.
# v26.04
* Rewrote bridge in Go using bridgev2 architecture.
* To migrate the bridge, simply upgrade in-place from v0.15.3 or later. The
database and config will be migrated automatically, although some parts of
the config aren't migrated (e.g. log config, permission types specific to
the legacy bridge). Taking backups beforehand is always recommended.
* It is recommended to check the config file after upgrading. If you have
prevented the bridge from writing to the config, you should update it
manually.
* The old-style relaybot is not yet supported and will not be migrated.
Setups using the relaybot will have to manually log in as a bot and use
`set-relay` to enable the generic [relay mode](https://docs.mau.fi/bridges/general/relay-mode.html).
The `default_relays` config option can be used to allow users to bridge
chats through the relay user to emulate the old-style relaybot.
* For multi-user bridges, normal (mini/non-super) group portal rooms are no
longer shared, which means every Matrix user will have their own room. The
old room will be assigned to one Matrix user randomly and others will get
a new room created automatically when receiving a message in the chat or
when using the `!tg sync-chats` command.
* If you want shared portals, upgrade the affected groups to supergroups.
You can upgrade groups using the `!tg upgrade` command, or in the
official apps by enabling any setting that requires a supergroup
(e.g. add a member tag for any admin).
* Management room status will not be migrated. Use `!tg set-management-room`
after the upgrade to re-allow commands without the `!tg` prefix in your
private chat with the bridge bot.
* Any migration issues should be reported in the Matrix room linked in the
readme.
* Notable new features include:
* Topic groups are now bridged as spaces
* Entire sticker and emoji packs can be synced in both directions
Note: the last (unreleased) version of the legacy Python bridge is available in
the [`python-final` tag](https://github.com/mautrix/telegram/tree/python-final).
# v0.15.3 (2025-07-16)
* Updated Telegram API to layer 204.
* Added support for MSC4190.
* Enabled captions by default, as they are now supported by most clients.
* Existing configs will still need to enable `caption_in_message` manually.
* Changed new room creation to hardcode room v11 to avoid v12 rooms being
created before proper support for them can be added.
* Fixed bridging sticker messages with partial quote replies from Telegram.
* Fixed text in poll bridging.
* Disabled kicking unauthenticated users from portals.
# v0.15.2 (2024-07-16)
* Dropped support for Python 3.9.
* Updated Telegram API to layer 183.
* Added support for authenticated media downloads.
* Added support for receiving reactions when using a bot account.
* Added option to limit file size by chat type.
* Fixed reply bridging breaking in some cases.
# v0.15.1 (2023-12-26)
* Updated Telegram API to layer 169.
* Updated Docker image to Alpine 3.19.
* Fixed some potential cases where a portal room would be created for the
relaybot even if `ignore_unbridged_group_chat` was enabled.
* Fixed member sync in groups with hidden members causing puppeted Matrix users
to be kicked even if they're still in the group.
# v0.15.0 (2023-11-26)
* Removed support for MSC2716 backfilling.
* Added `add-contact` and `delete-contact` commands.
* Updated Telegram API layer to 166.
* Includes receiving view-once media, blockquotes, quote replies and other
such things
* Fixed AuthKeyNotFound errors not being handled and causing users to get stuck
in a non-logged-in state.
# v0.14.2 (2023-09-19)
* **Security:** Updated Pillow to 10.0.1.
* Added support for double puppeting with arbitrary `as_token`s.
See [docs](https://docs.mau.fi/bridges/general/double-puppeting.html#appservice-method-new) for more info.
* Added support for sending webm and tgs files as stickers.
* Updated to Telegram API layer 161.
* Fixed cached usernames for Telegram users being cleared incorrectly, leading
to mentions not being bridged as usernames.
* Fixed reaction bridging failing if the server running the bridge was rebooted
less than 12 hours ago.
# v0.14.1 (2023-06-26)
### Added
* Added option to delete megolm sessions that were received before the
automatic ratcheting options were introduced.
* Added config option to use IPv6 for Telegram connection
(thanks to [@exciler] in [#920]).
### Improved
* Dropped support for Python 3.8.
* Updated Docker image to Alpine 3.18.
* Added timeout for forward backfills to prevent it from getting stuck
permanently.
### Fixed
* Fixed `bridge.filter.users` config option not being read correctly.
* Fixed proxy support to use python-socks instead of pysocks.
[@exciler]: https://github.com/exciler
[#920]: https://github.com/mautrix/telegram/pull/920
# v0.14.0 (2023-05-26)
### Added
* Added fallback messages for calls and premium gifts.
* Added options to automatically ratchet/delete megolm sessions to minimize
access to old messages.
* Added option to not set room name/avatar even in encrypted rooms.
* Implemented appservice pinging using MSC2659.
* Added option to disable or filter bridging direct chats
(thanks to [@Steffo99] in [#892]).
* Added options to specify different limits for forward and catchup backfilling
depending on chat type.
### Improved
* Improved handling logouts and certain connection errors.
* Changed reaction bridging to preserve timestamps.
* Disabled creating portals for DMs that don't have any messages when
`sync_direct_chats` is enabled.
### Fixed
* Fixed syncing mute status when portal is created through incoming message
rather than in startup sync.
* Fixed bridge incorrectly trusting member list and kicking users when
supergroup has member list hidden.
* Fixed sending messages after creating groups from Matrix using relaybot
instead of puppet (thanks to [@maltee1] in [#902]).
[@Steffo99]: https://github.com/Steffo99
[@maltee1]: https://github.com/maltee1
[#892]: https://github.com/mautrix/telegram/pull/892
[#902]: https://github.com/mautrix/telegram/pull/902
# v0.13.0 (2023-02-26)
### Added
* Added `allow_contact_info` config option to specify whether personal names
and avatars for other users should be bridged.
* The option is only safe to enable on single-user instances, using it
anywhere else will cause ghost user profiles to flip back and forth between
personal and default ones.
* Added config option to notify Matrix room if bridging an incoming message
fails.
### Improved
* Updated Docker image to Alpine 3.17.
* Updated to Telegram API layer 152.
* Improved handling users getting logged out.
* Removed support for creating accounts, as Telegram only allows requesting SMS
login codes on the official mobile clients now.
* Replaced moviepy with calling ffmpeg directly for generating video thumbnails.
### Fixed
* Fixed handling Telegram chat upgrades when backfilling is enabled.
* Fixed file transfers failing if transfering the thumbnail fails.
* Fixed bridging unnamed files with unrecognized mime types.
* Fixed enqueueing more backfill.
* Fixed timestamps not being saved in `telegram_file` table.
* Fixed issues with old events being replayed if the bridge was shut down
uncleanly.
# v0.12.2 (2022-11-26)
### Added
* Added built-in custom emoji packs to allow reacting with any standard unicode
emoji from Matrix (note that only premium users can use custom emojis).
* Added infinite backfill using [MSC2716].
* The new system includes a backwards compatibility mechanism which uses the
old method of just sending events to the room. By default, MSC2716 is not
enabled and the legacy method will be used.
### Improved
* Redacting reactions on Matrix no longer removes the user's other reactions to
the same message (premium users can have up to 3 reactions per message).
* Changes to default user permissions on Telegram are now bridged.
* Added database index to make reaction polling more efficient
(thanks to [@AndrewFerr] in [#862]).
### Fixed
* Fixed provisioning API not working with URL-encoded parameters.
[MSC2716]: https://github.com/matrix-org/matrix-spec-proposals/pull/2716
[@AndrewFerr]: https://github.com/AndrewFerr
[#862]: https://github.com/mautrix/telegram/pull/862
# v0.12.1 (2022-09-26)
### Added
* Support for custom emojis in reactions.
* Like other bridges with custom emoji reactions, they're bridged as `mxc://`
URIs, so client support is required to render them properly.
### Improved
* The bridge will now poll for reactions to 20 most recent messages when
receiving a read receipt. This works around Telegram's bad protocol that
doesn't notify clients on reactions to other users' messages.
* The docker image now has an option to bypass the startup script by setting
the `MAUTRIX_DIRECT_STARTUP` environment variable. Additionally, it will
refuse to run as a non-root user if that variable is not set (and print an
error message suggesting to either set the variable or use a custom command).
* Moved environment variable overrides for config fields to mautrix-python.
The new system also allows loading JSON values to enable overriding maps like
`login_shared_secret_map`.
### Fixed
* `ChatParticipantsForbidden` is handled properly when syncing non-supergroup
info.
* Fixed some bugs with file transfers when using SQLite.
* Fixed error when attempting to log in again after logging out.
* Fixed QR login not working.
* Fixed error syncing chats if bridging a message had previously been
interrupted.
# v0.12.0 (2022-08-26)
**N.B.** This release requires a homeserver with Matrix v1.1 support, which
bumps up the minimum homeserver versions to Synapse 1.54 and Dendrite 0.8.7.
Minimum Conduit version remains at 0.4.0.
### Added
* Added provisioning API for resolving Telegram identifiers (like usernames).
* Added support for bridging Telegram custom emojis to Matrix.
* Added option to not bridge chats with lots of members.
* Added option to include captions in the same message as the media to
implement [MSC2530]. Sending captions the same way is also supported and
enabled by default.
* Added commands to kick or ban relaybot users from Telegram.
* Added support for Telegram's disappearing messages.
* Added support for bridging forwarded messages as forwards on Telegram.
* Forwarding is not allowed in relay mode as the bot wouldn't be able to
specify who sent the message.
* Matrix doesn't have real forwarding (there's no forwarding metadata), so
only messages bridged from Telegram can be forwarded.
* Double puppeted messages from Telegram currently can't be forwarded without
removing the `fi.mau.double_puppet_source` key from the content.
* If forwarding fails (e.g. due to it being blocked in the source chat), the
bridge will automatically fall back to sending it as a normal new message.
* Added options to make encryption more secure.
* The `encryption` -> `verification_levels` config options can be used to
make the bridge require encrypted messages to come from cross-signed
devices, with trust-on-first-use validation of the cross-signing master
key.
* The `encryption` -> `require` option can be used to make the bridge ignore
any unencrypted messages.
* Key rotation settings can be configured with the `encryption` -> `rotation`
config.
### Improved
* Improved handling the bridge user leaving chats on Telegram, and new users
being added on Telegram.
* Improved animated sticker conversion options: added support for animated webp
and added option to convert video stickers (webm) to the specified image
format.
* Audio and video metadata is now bridged properly to Telegram.
* Added database index on Telegram usernames (used when bridging username
@-mentions in messages).
* Changed `/login/send_code` provisioning API to return a proper error when the
phone number is not registered on Telegram.
* The same login code can be used for registering an account, but registering
is not currently supported in the provisioning API.
* Removed `plaintext_highlights` config option (the code using it was already
removed in v0.11.0).
* Enabled appservice ephemeral events by default for new installations.
* Existing bridges can turn it on by enabling `ephemeral_events` and disabling
`sync_with_custom_puppets` in the config, then regenerating the registration
file.
* Updated to API layer 144 so that Telegram would send new message types like
premium stickers to the bridge.
* Updated Docker image to Alpine 3.16 and made it smaller.
### Fixed
* Fixed command prefix in game and poll messages (thanks to [@cynhr] in [#804]).
[MSC2530]: https://github.com/matrix-org/matrix-spec-proposals/pull/2530
[@cynhr]: https://github.com/cynhr
[#804]: https://github.com/mautrix/telegram/pull/804
# v0.11.3 (2022-04-17)
**N.B.** This release drops support for old homeservers which don't support the
new `/v3` API endpoints. Synapse 1.48+, Dendrite 0.6.5+ and Conduit 0.4.0+ are
supported. Legacy `r0` API support can be temporarily re-enabled with `pip install mautrix==0.16.0`.
However, this option will not be available in future releases.
### Added
* Added `list-invite-links` command to list invite links in a chat.
* Added option to use [MSC2246] async media uploads.
* Provisioning API for listing contacts and starting private chats.
### Improved
* Dropped Python 3.7 support.
* Telegram->Matrix message formatter will now replace `t.me/c/chatid/messageid`
style links with a link to the bridged Matrix event (in addition to the
previously supported `t.me/username/messageid` links).
* Updated formatting converter to keep newlines in code blocks as `\n` instead
of converting them to `<br/>`.
* Removed `max_document_size` option. The bridge will now fetch the max size
automatically using the media repo config endpoint.
* Removed redundant `msgtype` field in sticker events sent to Matrix.
* Disabled file logging in Docker image by default.
* If you want to enable it, set the `filename` in the file log handler to a
path that is writable, then add `"file"` back to `logging.root.handlers`.
* Reactions are now marked as read when bridging read receipts from Matrix.
### Fixed
* Fixed `!tg bridge` throwing error if the parameter is not an integer
* Fixed `!tg bridge` failing if the command had been previously run with an
incorrectly prefixed chat ID (e.g. `!tg bridge -1234567` followed by
`!tg bridge -1001234567`).
* Fixed `bridge_matrix_leave` config option not actually being used correctly.
* Fixed public channel mentions always bridging into a user mention on Matrix
rather than a room mention.
* The bridge will now make room mentions if the portal exists and fall back
to user mentions otherwise.
* Fixed newlines being lost in unformatted forwarded messages.
[MSC2246]: https://github.com/matrix-org/matrix-spec-proposals/pull/2246
# v0.11.2 (2022-02-14)
**N.B.** This will be the last release to support Python 3.7. Future versions
will require Python 3.8 or higher. In general, the mautrix bridges will only
support the lowest Python version in the latest Debian or Ubuntu LTS.
### Added
* Added simple fallback message for live location and venue messages from Telegram.
* Added support for `t.me/+code` style invite links in `!tg join`.
* Added support for showing channel profile when users send messages as a channel.
* Added "user joined Telegram" message when Telegram auto-creates a DM chat for
a new user.
### Improved
* Added option for adding a random prefix to relayed user displaynames to help
distinguish them on the Telegram side.
* Improved syncing profile info to room info when using encryption and/or the
`private_chat_profile_meta` config option.
* Removed legacy `community_id` config option.
### Fixed
* Fixed newlines disappearing when bridging channel messages with signatures.
* Fixed login throwing an error if a previous login code expired.
* Fixed bug in v0.11.0 that broke `!tg create`.
# v0.11.1 (2022-01-10)
### Added
* Added support for message reactions.
* Added support for spoiler text.
### Improved
* Support for voice messages.
* Changed color of blue text from Telegram to be more readable on dark themes.
### Fixed
* Fixed syncing contacts throwing an error for new accounts.
* Fixed migrating pre-v0.11 legacy databases if the database schema had been
corrupted (e.g. by using 3rd party tools for SQLite -> Postgres migration).
* Fixed converting animated stickers to webm with >33 FPS.
* Fixed a bug in v0.11.0 that broke mentioning users in groups
(thanks to [@dfuchss] in [#724]).
[@dfuchss]: https://github.com/dfuchss
[#724]: https://github.com/mautrix/telegram/pull/724
# v0.11.0 (2021-12-28)
* Switched from SQLAlchemy to asyncpg/aiosqlite.
@@ -188,8 +562,8 @@ path.
* Bridging events of a user whose power level is malformed (i.e. a string
instead of an integer) now works.
[MSC2409]: https://github.com/matrix-org/matrix-doc/pull/2409
[MSC2778]: https://github.com/matrix-org/matrix-doc/pull/2778
[MSC2409]: https://github.com/matrix-org/matrix-spec-proposals/pull/2409
[MSC2778]: https://github.com/matrix-org/matrix-spec-proposals/pull/2778
# v0.8.2 (2020-07-27)
@@ -237,7 +611,7 @@ update (v0.5.8) and a fix to the Docker image.
* Fixed `sync_direct_chats` option creating non-working portals.
* Fixed video thumbnailing sometimes leaving behind downloaded videos in `/tmp`.
[MSC2346]: https://github.com/matrix-org/matrix-doc/pull/2346
[MSC2346]: https://github.com/matrix-org/matrix-spec-proposals/pull/2346
## rc1 (2020-04-25)
+13 -54
View File
@@ -1,61 +1,20 @@
FROM dock.mau.dev/tulir/lottieconverter:alpine-3.15
FROM golang:1-alpine3.23 AS builder
ARG TARGETARCH=amd64
RUN apk add --no-cache git ca-certificates build-base su-exec olm-dev
RUN apk add --no-cache \
python3 py3-pip py3-setuptools py3-wheel \
py3-virtualenv \
py3-pillow \
py3-aiohttp \
py3-magic \
py3-ruamel.yaml \
py3-commonmark \
py3-prometheus-client \
# Indirect dependencies
py3-idna \
#moviepy
py3-decorator \
py3-tqdm \
py3-requests \
#imageio
py3-numpy \
#py3-telethon \ (outdated)
# Optional for socks proxies
py3-pysocks \
py3-pyaes \
# cryptg
py3-cffi \
py3-qrcode \
py3-brotli \
# Other dependencies
ffmpeg \
ca-certificates \
su-exec \
netcat-openbsd \
# encryption
py3-olm \
py3-pycryptodome \
py3-unpaddedbase64 \
py3-future \
bash \
curl \
jq \
yq
COPY . /build
WORKDIR /build
RUN ./build.sh
COPY requirements.txt /opt/mautrix-telegram/requirements.txt
COPY optional-requirements.txt /opt/mautrix-telegram/optional-requirements.txt
WORKDIR /opt/mautrix-telegram
RUN apk add --virtual .build-deps python3-dev libffi-dev build-base \
&& pip3 install -r requirements.txt -r optional-requirements.txt \
&& apk del .build-deps
FROM alpine:3.23
COPY . /opt/mautrix-telegram
RUN apk add git && pip3 install .[all] && apk del git \
# This doesn't make the image smaller, but it's needed so that the `version` command works properly
&& cp mautrix_telegram/example-config.yaml . && rm -rf mautrix_telegram
ENV UID=1337 \
GID=1337
RUN apk add --no-cache ffmpeg su-exec ca-certificates olm bash jq curl yq-go lottieconverter
COPY --from=builder /build/mautrix-telegram /usr/bin/mautrix-telegram
COPY --from=builder /build/docker-run.sh /docker-run.sh
VOLUME /data
ENV UID=1337 GID=1337 \
FFMPEG_BINARY=/usr/bin/ffmpeg
CMD ["/opt/mautrix-telegram/docker-run.sh"]
CMD ["/docker-run.sh"]
+17
View File
@@ -0,0 +1,17 @@
ARG DOCKER_HUB="docker.io"
FROM ${DOCKER_HUB}/alpine:3.23
ENV UID=1337 \
GID=1337
RUN apk add --no-cache ffmpeg su-exec ca-certificates bash jq curl yq-go lottieconverter
ARG EXECUTABLE=./mautrix-telegram
COPY $EXECUTABLE /usr/bin/mautrix-telegram
COPY ./docker-run.sh /docker-run.sh
ENV BRIDGEV2=1
VOLUME /data
WORKDIR /data
CMD ["/docker-run.sh"]
+12
View File
@@ -0,0 +1,12 @@
The mautrix-telegram developers grant the following special exceptions:
* to Beeper the right to embed the program in the Beeper clients and servers,
and use and distribute the collective work without applying the license to
the whole.
* to Element the right to distribute compiled binaries of the program as a part
of the Element Server Suite and other server bundles without applying the
license.
All exceptions are only valid under the condition that any modifications to
the source code of mautrix-telegram remain publicly available under the terms
of the GNU AGPL version 3 or later.
-5
View File
@@ -1,5 +0,0 @@
include README.md
include CHANGELOG.md
include LICENSE
include requirements.txt
include optional-requirements.txt
+8 -16
View File
@@ -2,34 +2,26 @@
![Languages](https://img.shields.io/github/languages/top/mautrix/telegram.svg)
[![License](https://img.shields.io/github/license/mautrix/telegram.svg)](LICENSE)
[![Release](https://img.shields.io/github/release/mautrix/telegram/all.svg)](https://github.com/mautrix/telegram/releases)
[![GitLab CI](https://mau.dev/mautrix/telegram/badges/master/pipeline.svg)](https://mau.dev/mautrix/telegram/container_registry)
[![Code style](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black)
[![Imports](https://img.shields.io/badge/%20imports-isort-%231674b1?style=flat&labelColor=ef8336)](https://pycqa.github.io/isort/)
[![GitLab CI](https://mau.dev/mautrix/telegram/badges/main/pipeline.svg)](https://mau.dev/mautrix/telegram/container_registry)
A Matrix-Telegram hybrid puppeting/relaybot bridge.
A Matrix-Telegram puppeting/relaybot bridge.
## Sponsors
* [Joel Lehtonen / Zouppen](https://github.com/zouppen)
### Documentation
## Documentation
All setup and usage instructions are located on
[docs.mau.fi](https://docs.mau.fi/bridges/python/telegram/index.html).
[docs.mau.fi](https://docs.mau.fi/bridges/go/telegram/index.html).
Some quick links:
* [Bridge setup](https://docs.mau.fi/bridges/python/setup/index.html?bridge=telegram)
(or [with Docker](https://docs.mau.fi/bridges/python/setup/docker.html?bridge=telegram))
* Basic usage: [Authentication](https://docs.mau.fi/bridges/python/telegram/authentication.html),
[Creating chats](https://docs.mau.fi/bridges/python/telegram/creating-and-managing-chats.html),
[Relaybot setup](https://docs.mau.fi/bridges/python/telegram/relay-bot.html)
* [Bridge setup](https://docs.mau.fi/bridges/go/setup.html?bridge=telegram)
(or [with Docker](https://docs.mau.fi/bridges/general/docker-setup.html?bridge=telegram))
* Basic usage: [Authentication](https://docs.mau.fi/bridges/go/telegram/authentication.html)
### Features & Roadmap
[ROADMAP.md](https://github.com/mautrix/telegram/blob/master/ROADMAP.md)
contains a general overview of what is supported by the bridge.
[ROADMAP.md](ROADMAP.md) contains a general overview of what is supported by the bridge.
## Discussion
Matrix room: [`#telegram:maunium.net`](https://matrix.to/#/#telegram:maunium.net)
Telegram chat: [`mautrix_telegram`](https://t.me/mautrix_telegram) (bridged to Matrix room)
## Preview
![Preview](preview.png)
+20 -25
View File
@@ -3,42 +3,37 @@
* Matrix → Telegram
* [x] Message content (text, formatting, files, etc..)
* [x] Message redactions
* [x] Message reactions
* [x] Message edits
* [ ] ‡ Message history
* [x] Presence
* [ ] Presence
* [x] Typing notifications
* [x] Read receipts
* [x] Pinning messages
* [x] Power level
* [x] Normal chats
* [ ] Non-hardcoded PL requirements
* [x] Supergroups/channels
* [ ] Precise bridging (non-hardcoded PL requirements, bridge specific permissions, etc..)
* [x] Membership actions (invite/kick/join/leave)
* [x] Room metadata changes (name, topic, avatar)
* [x] Initial room metadata
* [ ] User metadata
* [ ] Initial displayname/username/avatar at register
* [ ] ‡ Changes to displayname/avatar
* [ ] Pinning messages
* [ ] Power level
* [ ] Membership actions (invite/kick/join/leave)
* [ ] Room metadata changes (name, topic, avatar)
* [ ] Initial room metadata
* Telegram → Matrix
* [x] Message content (text, formatting, files, etc..)
* [ ] Advanced message content/media
* [x] Polls
* [x] Games
* [ ] Buttons
* [x] Custom emojis
* [ ] Polls
* [ ] Games
* [ ] Buttons
* [x] Message deletions
* [x] Message reactions
* [x] Message edits
* [x] Message history
* [x] Manually (`!tg backfill`)
* [x] Automatically when creating portal
* [x] Automatically for missed messages
* [x] Avatars
* [x] Presence
* [ ] Presence
* [x] Typing notifications
* [x] Read receipts (private chat only)
* [x] Pinning messages
* [x] Read receipts (DMs only)
* [ ] Pinning messages
* [x] Admin/chat creator status
* [ ] Supergroup/channel permissions (precise per-user permissions not supported in Matrix)
* [x] Supergroup/channel permissions (precise per-user permissions not supported in Matrix)
* [x] Membership actions (invite/kick/join/leave)
* [ ] Chat metadata changes
* [x] Title
@@ -48,16 +43,16 @@
* [x] Initial chat metadata (about text missing)
* [x] User metadata (displayname/avatar)
* [x] Supergroup upgrade
* [x] Topics (spaces)
* Misc
* [x] Automatic portal creation
* [x] At startup
* [x] When receiving invite or message
* [x] Private chat creation by inviting Matrix puppet of Telegram user to new room
* [x] Private chat creation by inviting Matrix ghost of Telegram user to new room
* [x] Option to use bot to relay messages for unauthenticated Matrix users (relaybot)
* [x] Option to use own Matrix account for messages sent from other Telegram clients (double puppeting)
* [ ] ‡ Calls (hard, not yet supported by Telethon)
* [ ] ‡ Secret chats (i.e. End-to-bridge encryption on Telegram)
* [x] End-to-bridge encryption in Matrix rooms (see [wiki](https://github.com/tulir/mautrix-telegram/wiki/End%E2%80%90to%E2%80%90bridge-encryption))
* [ ] ‡ Calls
* [ ] ‡ Secret chats (i.e. end-to-bridge encryption on Telegram)
† Information not automatically sent from source, i.e. implementation may not be possible
‡ Maybe, i.e. this feature may or may not be implemented at some point
Executable
+2
View File
@@ -0,0 +1,2 @@
#!/bin/sh
BINARY_NAME=mautrix-telegram go tool maubuild "$@"
+102
View File
@@ -0,0 +1,102 @@
// mautrix-telegram - A Matrix-Telegram puppeting bridge.
// Copyright (C) 2025 Sumner Evans
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
package main
import (
"context"
_ "embed"
"fmt"
"github.com/rs/zerolog"
up "go.mau.fi/util/configupgrade"
"go.mau.fi/util/dbutil"
"maunium.net/go/mautrix/bridgev2/bridgeconfig"
)
const legacyMigrateRenameTablesQuery = `
ALTER TABLE backfill_queue RENAME TO backfill_queue_old;
ALTER TABLE bot_chat RENAME TO bot_chat_old;
ALTER TABLE contact RENAME TO contact_old;
ALTER TABLE disappearing_message RENAME TO disappearing_message_old;
ALTER TABLE message RENAME TO message_old;
ALTER TABLE portal RENAME TO portal_old;
ALTER TABLE puppet RENAME TO puppet_old;
ALTER TABLE reaction RENAME TO reaction_old;
ALTER TABLE telegram_file RENAME TO telegram_file_old;
ALTER TABLE telethon_entities RENAME TO telethon_entities_old;
ALTER TABLE telethon_sent_files RENAME TO telethon_sent_files_old;
ALTER TABLE telethon_sessions RENAME TO telethon_sessions_old;
ALTER TABLE telethon_update_state RENAME TO telethon_update_state_old;
ALTER TABLE "user" RENAME TO user_old;
ALTER TABLE user_portal RENAME TO user_portal_old;
DROP INDEX IF EXISTS telegram_file_mxc_idx;
`
func legacyMigrateRenameTables(ctx context.Context, db *dbutil.Database) error {
_, err := db.Exec(ctx, legacyMigrateRenameTablesQuery)
if err != nil {
return err
}
var mxVersion int
err = db.QueryRow(ctx, "SELECT version FROM mx_version").Scan(&mxVersion)
if err != nil {
return fmt.Errorf("failed to get mx_version: %w", err)
} else if mxVersion == 3 {
zerolog.Ctx(ctx).Debug().Msg("mx_version is 3, adding create_event column before running actual migration")
_, err = db.Exec(ctx, `
ALTER TABLE mx_room_state ADD COLUMN create_event TEXT;
UPDATE mx_version SET version=4;
`)
if err != nil {
return fmt.Errorf("failed to add create_event column to mx_room_state: %w", err)
}
}
return nil
}
//go:embed legacymigrate.sql
var legacyMigrateCopyData string
func migrateLegacyConfig(helper up.Helper) {
helper.Set(up.Str, "mautrix.bridge.e2ee", "encryption", "pickle_key")
bridgeconfig.CopyToOtherLocation(helper, up.Int, []string{"telegram", "api_id"}, []string{"network", "api_id"})
bridgeconfig.CopyToOtherLocation(helper, up.Str, []string{"telegram", "api_hash"}, []string{"network", "api_hash"})
bridgeconfig.CopyToOtherLocation(helper, up.Str, []string{"telegram", "device_info", "device_model"}, []string{"network", "device_info", "device_model"})
bridgeconfig.CopyToOtherLocation(helper, up.Str, []string{"telegram", "device_info", "system_version"}, []string{"network", "device_info", "system_version"})
bridgeconfig.CopyToOtherLocation(helper, up.Str, []string{"telegram", "device_info", "app_version"}, []string{"network", "device_info", "app_version"})
bridgeconfig.CopyToOtherLocation(helper, up.Str, []string{"telegram", "device_info", "lang_code"}, []string{"network", "device_info", "lang_code"})
bridgeconfig.CopyToOtherLocation(helper, up.Str, []string{"telegram", "device_info", "system_lang_code"}, []string{"network", "device_info", "system_lang_code"})
bridgeconfig.CopyToOtherLocation(helper, up.Str, []string{"bridge", "animated_sticker", "target"}, []string{"network", "animated_sticker", "target"})
bridgeconfig.CopyToOtherLocation(helper, up.Bool, []string{"bridge", "animated_sticker", "convert_from_webm"}, []string{"network", "animated_sticker", "convert_from_webm"})
bridgeconfig.CopyToOtherLocation(helper, up.Int, []string{"bridge", "animated_sticker", "width"}, []string{"network", "animated_sticker", "width"})
bridgeconfig.CopyToOtherLocation(helper, up.Int, []string{"bridge", "animated_sticker", "height"}, []string{"network", "animated_sticker", "height"})
bridgeconfig.CopyToOtherLocation(helper, up.Int, []string{"bridge", "animated_sticker", "fps"}, []string{"network", "animated_sticker", "fps"})
bridgeconfig.CopyToOtherLocation(helper, up.Int, []string{"bridge", "max_initial_member_sync"}, []string{"network", "member_list", "max_initial_sync"})
bridgeconfig.CopyToOtherLocation(helper, up.Bool, []string{"bridge", "sync_channel_members"}, []string{"network", "member_list", "sync_broadcast_channels"})
bridgeconfig.CopyToOtherLocation(helper, up.Bool, []string{"bridge", "skip_deleted_members"}, []string{"network", "member_list", "skip_deleted"})
bridgeconfig.CopyToOtherLocation(helper, up.Str, []string{"telegram", "proxy", "type"}, []string{"network", "proxy", "type"})
proxyAddress, _ := helper.Get(up.Str, "telegram", "proxy", "address")
proxyPort, _ := helper.Get(up.Int, "telegram", "proxy", "port")
helper.Set(up.Str, fmt.Sprintf("%s:%s", proxyAddress, proxyPort), "network", "proxy", "address")
bridgeconfig.CopyToOtherLocation(helper, up.Str, []string{"telegram", "proxy", "username"}, []string{"network", "proxy", "username"})
bridgeconfig.CopyToOtherLocation(helper, up.Str, []string{"telegram", "proxy", "password"}, []string{"network", "proxy", "password"})
bridgeconfig.CopyToOtherLocation(helper, up.Int, []string{"bridge", "max_member_count"}, []string{"network", "max_member_count"})
bridgeconfig.CopyToOtherLocation(helper, up.Int, []string{"bridge", "sync_update_limit"}, []string{"network", "sync", "update_limit"})
bridgeconfig.CopyToOtherLocation(helper, up.Int, []string{"bridge", "sync_create_limit"}, []string{"network", "sync", "create_limit"})
bridgeconfig.CopyToOtherLocation(helper, up.Bool, []string{"bridge", "sync_direct_chats"}, []string{"network", "sync", "direct_chats"})
bridgeconfig.CopyToOtherLocation(helper, up.Bool, []string{"bridge", "always_custom_emoji_reaction"}, []string{"network", "always_custom_emoji_reaction"})
}
+300
View File
@@ -0,0 +1,300 @@
INSERT INTO "user" (bridge_id, mxid)
SELECT '', mxid FROM user_old;
DELETE FROM telethon_sessions_old WHERE auth_key IS NULL;
ALTER TABLE telethon_sessions_old ADD COLUMN json_data jsonb;
UPDATE telethon_sessions_old SET json_data=
-- only: postgres
jsonb_build_object
-- only: sqlite (line commented)
-- json_object
(
'auth_key', encode(auth_key, 'base64'),
'dc_id', dc_id,
'server_address', server_address,
'port', port
);
INSERT INTO user_login (bridge_id, user_mxid, id, remote_name, remote_profile, space_room, metadata)
SELECT
'', -- bridge_id
mxid, -- user_mxid
CAST(tgid AS TEXT), -- id
COALESCE(tg_username, tg_phone, ''), -- remote_name
'{}', -- remote_profile
'', -- space_room
-- only: postgres
jsonb_build_object
-- only: sqlite (line commented)
-- json_object
(
'phone', COALESCE('+' || tg_phone, ''),
'session', json((SELECT json_data FROM telethon_sessions_old WHERE session_id=mxid))
) -- metadata
FROM user_old
WHERE tgid IS NOT NULL;
INSERT INTO ghost (
bridge_id, id, name, avatar_id, avatar_hash, avatar_mxc,
name_set, avatar_set, contact_info_set, is_bot, identifiers, metadata
)
SELECT
'', -- bridge_id
CAST(id AS TEXT), -- id
COALESCE(displayname, ''), -- name
COALESCE(photo_id, ''), -- avatar_id
'', -- avatar_hash
COALESCE(avatar_url, ''), -- avatar_mxc
name_set,
avatar_set,
contact_info_set,
COALESCE(is_bot, false),
'[]', -- identifiers
-- only: postgres
jsonb_build_object
-- only: sqlite (line commented)
-- json_object
(
'is_premium', CASE WHEN is_premium THEN json('true') ELSE json('false') END,
'is_channel', CASE WHEN is_channel THEN json('true') ELSE json('false') END,
'contact_source', displayname_source,
'source_is_contact', CASE WHEN displayname_contact THEN json('true') ELSE json('false') END
) -- metadata
FROM puppet_old;
DELETE FROM user_portal_old WHERE portal IN (SELECT tgid FROM portal_old WHERE peer_type<>'channel');
DELETE FROM backfill_queue_old WHERE portal_tgid IN (SELECT tgid FROM portal_old WHERE peer_type<>'channel');
UPDATE portal_old
SET tg_receiver=COALESCE((SELECT "user" FROM user_portal_old WHERE portal=portal_old.tgid LIMIT 1), tg_receiver)
WHERE peer_type='chat' AND tgid=tg_receiver;
UPDATE portal_old
SET tg_receiver=COALESCE((SELECT tgid FROM user_old WHERE tgid IS NOT NULL LIMIT 1), tg_receiver)
WHERE peer_type='chat' AND tgid=tg_receiver;
DELETE FROM portal_old WHERE peer_type='chat' AND tgid=tg_receiver;
INSERT INTO portal (
bridge_id, id, receiver, mxid, other_user_id, name, topic, avatar_id, avatar_hash, avatar_mxc,
name_set, avatar_set, topic_set, name_is_custom, in_space, room_type, metadata
)
SELECT
'', -- bridge_id
peer_type || ':' || CAST(tgid AS TEXT), -- id
CASE WHEN peer_type='channel' THEN '' ELSE CAST(tg_receiver AS TEXT) END, -- receiver
mxid, -- mxid
CASE WHEN peer_type='user' THEN CAST(tgid AS TEXT) END, -- other_user_id
COALESCE(title, ''), -- name
COALESCE(about, ''), -- topic
COALESCE(photo_id, ''), -- avatar_id
'', -- avatar_hash
COALESCE(avatar_url, ''), -- avatar_mxc
name_set, -- name_set
avatar_set, -- avatar_set
false, -- topic_set
peer_type<>'user', -- name_is_custom
false, -- in_space
CASE WHEN peer_type='user' THEN 'dm' ELSE '' END, -- room_type
-- only: postgres
jsonb_build_object
-- only: sqlite (line commented)
-- json_object
(
'is_supergroup', CASE WHEN megagroup THEN json('true') ELSE json('false') END
) -- metadata
FROM portal_old;
INSERT INTO user_portal (bridge_id, user_mxid, login_id, portal_id, portal_receiver, in_space, preferred)
SELECT
'', -- bridge_id
user_old.mxid, -- user_mxid
CAST(user_portal_old.user AS TEXT), -- login_id
portal_old.peer_type || ':' || CAST(user_portal_old.portal AS TEXT), -- portal_id
CASE WHEN peer_type='channel' THEN '' ELSE CAST(user_portal_old.portal_receiver AS TEXT) END, -- portal_receiver
false, -- in_space
false -- preferred
FROM user_portal_old
INNER JOIN user_old ON user_portal_old."user" = user_old.tgid
INNER JOIN portal_old ON user_portal_old.portal = portal_old.tgid and user_portal_old.portal_receiver = portal_old.tg_receiver;
INSERT INTO user_portal (bridge_id, user_mxid, login_id, portal_id, portal_receiver, in_space, preferred)
SELECT
'', -- bridge_id
user_old.mxid, -- user_mxid
CAST(portal_old.tg_receiver AS TEXT), -- login_id
portal_old.peer_type || ':' || CAST(portal_old.tgid AS TEXT), -- portal_id
CAST(portal_old.tg_receiver AS TEXT), -- portal_receiver
false, -- in_space
false -- preferred
FROM portal_old
INNER JOIN user_old ON portal_old.tg_receiver = user_old.tgid
WHERE portal_old.tg_receiver<>portal_old.tgid
ON CONFLICT (bridge_id, user_mxid, login_id, portal_id, portal_receiver) DO NOTHING;
INSERT INTO ghost (bridge_id, id, name, avatar_id, avatar_hash, avatar_mxc, name_set, avatar_set, contact_info_set, is_bot, identifiers, metadata)
VALUES ('', '', '', '', '', '', false, false, false, false, '[]', '{}');
UPDATE message_old SET sender=NULL WHERE sender IS NOT NULL AND NOT EXISTS(SELECT 1 FROM puppet_old WHERE id=sender);
INSERT INTO message (
bridge_id, id, part_id, mxid, room_id, room_receiver, sender_id, sender_mxid, timestamp, edit_count, metadata
)
SELECT
'', -- bridge_id
CASE WHEN tg_space=portal_old.tgid THEN (CAST(tg_space AS TEXT) || '.') ELSE '' END || CAST(message_old.tgid AS TEXT), -- id
'', -- part_id
message_old.mxid, -- mxid
portal_old.peer_type || ':' || CAST(portal_old.tgid AS TEXT), -- room_id
CASE WHEN portal_old.peer_type='channel' THEN '' ELSE CAST(portal_old.tg_receiver AS TEXT) END, -- room_receiver
COALESCE(CAST(sender AS TEXT), ''), -- sender_id
COALESCE(sender_mxid, ''),
0, -- timestamp
0, -- edit_count
-- only: postgres
jsonb_build_object
-- only: sqlite (line commented)
-- json_object
(
'content_hash', CASE WHEN content_hash IS NULL THEN '' ELSE encode(content_hash, 'base64') END
) -- metadata
FROM message_old
INNER JOIN portal_old ON mx_room=portal_old.mxid
WHERE (tg_space=portal_old.tgid OR tg_space=portal_old.tg_receiver) AND edit_index=0;
-- TODO migrate edit_index?
INSERT INTO reaction (
bridge_id, message_id, message_part_id, sender_id, emoji_id, room_id, room_receiver, mxid, timestamp, emoji, metadata
)
SELECT
'', -- bridge_id
message.id, -- message_id
message.part_id, -- message_part_id
CAST(tg_sender AS TEXT), -- sender_id
reaction, -- emoji_id
message.room_id, -- room_id
message.room_receiver, -- room_receiver
reaction_old.mxid, -- mxid
0, -- timestamp
reaction, -- emoji
'{}' -- metadata
FROM reaction_old
INNER JOIN message ON reaction_old.msg_mxid=message.mxid;
INSERT INTO telegram_access_hash (user_id, entity_type, entity_id, access_hash)
SELECT
user_old.tgid,
CASE WHEN id < 0 THEN 'channel' ELSE 'user' END,
CASE WHEN id < 0 THEN -id - 1000000000000 ELSE id END,
hash
FROM telethon_entities_old
LEFT JOIN user_old ON user_old.mxid=session_id
WHERE user_old.tgid IS NOT NULL AND hash<>0;
INSERT INTO telegram_user_state (user_id, pts, qts, date, seq)
SELECT user_old.tgid, pts, qts, date, seq
FROM telethon_update_state_old
LEFT JOIN user_old ON user_old.mxid=session_id
WHERE entity_id=0 AND user_old.tgid IS NOT NULL;
INSERT INTO telegram_channel_state (user_id, channel_id, pts)
SELECT user_old.tgid, entity_id, pts
FROM telethon_update_state_old
LEFT JOIN user_old ON user_old.mxid=session_id
WHERE entity_id<>0 AND user_old.tgid IS NOT NULL;
INSERT INTO telegram_username (username, entity_type, entity_id)
SELECT
username,
CASE WHEN id < 0 THEN 'channel' ELSE 'user' END,
CASE WHEN id < 0 THEN -id - 1000000000000 ELSE id END
FROM telethon_entities_old
WHERE username<>''
ON CONFLICT DO NOTHING;
INSERT INTO telegram_phone_number (phone_number, entity_id)
SELECT phone, id
FROM telethon_entities_old
WHERE phone<>''
ON CONFLICT DO NOTHING;
INSERT INTO telegram_file (id, mxc, mime_type, size, width, height, timestamp)
SELECT id, mxc, mime_type, size, width, height, timestamp
FROM telegram_file_old;
INSERT INTO disappearing_message (bridge_id, mx_room, mxid, type, timer, disappear_at)
SELECT
'', -- bridge_id
room_id,
event_id,
'after_send',
expiration_seconds * 1000000000,
expiration_ts * 1000000
FROM disappearing_message_old
WHERE expiration_ts<9999999999999 AND expiration_seconds<999999
AND room_id IN (SELECT mxid FROM portal WHERE mxid IS NOT NULL);
-- TODO do something with the bot_chat table?
-- Python -> Go mx_ table migration
-- only: postgres until "end only"
ALTER TABLE mx_room_state DROP COLUMN is_encrypted;
ALTER TABLE mx_room_state RENAME COLUMN has_full_member_list TO members_fetched;
UPDATE mx_room_state SET members_fetched=false WHERE members_fetched IS NULL;
ALTER TABLE mx_room_state ADD COLUMN join_rules jsonb;
ALTER TABLE mx_room_state ALTER COLUMN power_levels TYPE jsonb USING power_levels::jsonb;
ALTER TABLE mx_room_state ALTER COLUMN encryption TYPE jsonb USING encryption::jsonb;
ALTER TABLE mx_room_state ALTER COLUMN create_event TYPE jsonb USING create_event::jsonb;
ALTER TABLE mx_room_state ALTER COLUMN members_fetched SET DEFAULT false;
ALTER TABLE mx_room_state ALTER COLUMN members_fetched SET NOT NULL;
-- end only postgres
-- only: sqlite until "end only"
CREATE TABLE new_mx_room_state (
room_id TEXT PRIMARY KEY,
power_levels jsonb,
encryption jsonb,
create_event jsonb,
join_rules jsonb,
members_fetched BOOLEAN NOT NULL DEFAULT false
);
INSERT INTO new_mx_room_state (room_id, encryption, power_levels, create_event, members_fetched)
SELECT room_id, encryption, power_levels, create_event, COALESCE(has_full_member_list, false)
FROM mx_room_state;
DROP TABLE mx_room_state;
ALTER TABLE new_mx_room_state RENAME TO mx_room_state;
-- end only sqlite
ALTER TABLE mx_user_profile ADD COLUMN name_skeleton bytea;
CREATE INDEX mx_user_profile_membership_idx ON mx_user_profile (room_id, membership);
CREATE INDEX mx_user_profile_name_skeleton_idx ON mx_user_profile (room_id, name_skeleton);
UPDATE mx_user_profile SET displayname='' WHERE displayname IS NULL;
UPDATE mx_user_profile SET avatar_url='' WHERE avatar_url IS NULL;
CREATE TABLE mx_registrations (
user_id TEXT PRIMARY KEY
);
UPDATE mx_version SET version=10;
DELETE FROM mx_user_profile WHERE room_id='' OR user_id='';
DELETE FROM mx_room_state WHERE room_id='';
DROP TABLE user_portal_old;
DROP TABLE backfill_queue_old;
DROP TABLE bot_chat_old;
DROP TABLE contact_old;
DROP TABLE disappearing_message_old;
DROP TABLE message_old;
DROP TABLE reaction_old;
DROP TABLE portal_old;
DROP TABLE puppet_old;
DROP TABLE user_old;
-- only: postgres (this is deleted separately for sqlite)
DROP TABLE telegram_file_old;
DROP TABLE telethon_entities_old;
DROP TABLE telethon_sent_files_old;
DROP TABLE telethon_sessions_old;
DROP TABLE telethon_update_state_old;
+427
View File
@@ -0,0 +1,427 @@
// mautrix-telegram - A Matrix-Telegram puppeting bridge.
// Copyright (C) 2024 Sumner Evans
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
package main
import (
"context"
"encoding/json"
"fmt"
"net/http"
"strings"
"sync"
"github.com/gorilla/websocket"
"github.com/rs/zerolog"
"go.mau.fi/util/exhttp"
"maunium.net/go/mautrix"
"maunium.net/go/mautrix/bridgev2"
"maunium.net/go/mautrix/bridgev2/status"
"maunium.net/go/mautrix/id"
"go.mau.fi/mautrix-telegram/pkg/connector"
"go.mau.fi/mautrix-telegram/pkg/connector/ids"
)
type response struct {
Username id.UserID `json:"username,omitempty"`
State string `json:"state,omitempty"`
Message string `json:"message,omitempty"`
Error string `json:"error,omitempty"`
ErrCode string `json:"errcode,omitempty"`
}
func (r response) WithState(state string) response {
r.State = state
return r
}
func (r response) WithMessage(message string) response {
r.Message = message
return r
}
func (r response) WithError(errCode, error string) response {
r.ErrCode = errCode
r.Error = error
return r
}
type legacyLogin struct {
Process bridgev2.LoginProcess
NextStep *bridgev2.LoginStep
}
var inflightLegacyLoginsLock sync.RWMutex
var inflightLegacyLogins = map[id.UserID]*legacyLogin{}
var upgrader = websocket.Upgrader{
CheckOrigin: func(r *http.Request) bool {
return true
},
Subprotocols: []string{"net.maunium.telegram.login"},
}
func legacyProvLoginQR(w http.ResponseWriter, r *http.Request) {
log := zerolog.Ctx(r.Context()).With().Str("prov_method", "qr_login").Logger()
ctx := log.WithContext(r.Context())
user := m.Matrix.Provisioning.GetUser(r)
resp := response{Username: user.MXID}
var err error
var loginProcess bridgev2.LoginProcess
var nextStep *bridgev2.LoginStep
if loginProcess, err = c.CreateLogin(ctx, user, connector.LoginFlowIDQR); err != nil {
exhttp.WriteJSONResponse(w, http.StatusInternalServerError, resp.WithError("create_login_failed", fmt.Sprintf("Failed to create a QR login process: %s", err.Error())))
} else if nextStep, err = loginProcess.Start(ctx); err != nil {
exhttp.WriteJSONResponse(w, http.StatusInternalServerError, resp.WithError("start_login_failed", fmt.Sprintf("Failed to start login process: %s", err.Error())))
} else if nextStep.StepID != connector.LoginStepIDShowQR {
exhttp.WriteJSONResponse(w, http.StatusInternalServerError, resp.WithError("unexpected_step", fmt.Sprintf("Unexpected first step %s", nextStep.StepID)))
}
ws, err := upgrader.Upgrade(w, r, nil)
if err != nil {
log.Err(err).Msg("Failed to upgrade connection to websocket")
return
}
defer func() {
err := ws.Close()
if err != nil {
log.Debug().Err(err).Msg("Error closing websocket")
}
}()
go func() {
// Read everything so SetCloseHandler() works
for {
_, _, err = ws.ReadMessage()
if err != nil {
break
}
}
}()
ctx, cancel := context.WithCancel(context.Background())
ws.SetCloseHandler(func(code int, text string) error {
log.Debug().Int("close_code", code).Msg("Login websocket closed, cancelling login")
cancel()
return nil
})
for {
switch nextStep.StepID {
case connector.LoginStepIDShowQR:
ws.WriteJSON(map[string]any{"code": nextStep.DisplayAndWaitParams.Data})
nextStep, err = loginProcess.(bridgev2.LoginProcessDisplayAndWait).Wait(ctx)
if err != nil {
ws.WriteJSON(map[string]any{
"success": false,
"error": "qr_login_failed",
"message": fmt.Sprintf("Failed to login using QR code: %s", err),
})
return
}
case connector.LoginStepIDComplete:
ws.WriteJSON(map[string]any{"success": true})
go handleLoginComplete(ctx, user, nextStep.CompleteParams.UserLogin)
return
case connector.LoginStepIDPassword:
inflightLegacyLoginsLock.Lock()
inflightLegacyLogins[user.MXID] = &legacyLogin{Process: loginProcess, NextStep: nextStep}
inflightLegacyLoginsLock.Unlock()
ws.WriteJSON(map[string]any{"success": false, "error": "password-needed"})
return
default:
ws.WriteJSON(map[string]any{
"success": false,
"error": "unexpected_step",
"message": fmt.Sprintf("Unexpected step in QR code login process %s", nextStep.StepID),
})
return
}
}
}
func legacyProvLoginRequestCode(w http.ResponseWriter, r *http.Request) {
log := zerolog.Ctx(r.Context()).With().Str("prov_step", "request_code").Logger()
ctx := log.WithContext(r.Context())
user := m.Matrix.Provisioning.GetUser(r)
resp := response{Username: user.MXID, State: "request"}
legacyProvRequestCodeReq := map[string]string{}
if err := json.NewDecoder(r.Body).Decode(&legacyProvRequestCodeReq); err != nil {
exhttp.WriteJSONResponse(w, http.StatusBadRequest, resp.WithError("request_body_invalid", "Request body is invalid"))
} else if phone, ok := legacyProvRequestCodeReq["phone"]; !ok || phone == "" {
exhttp.WriteJSONResponse(w, http.StatusBadRequest, resp.WithError("phone_missing", "Phone number missing"))
} else if loginProcess, err := c.CreateLogin(ctx, user, connector.LoginFlowIDPhone); err != nil {
exhttp.WriteJSONResponse(w, http.StatusInternalServerError, resp.WithError("create_login_failed", fmt.Sprintf("Failed to create a phone number login process: %s", err.Error())))
} else if firstStep, err := loginProcess.Start(ctx); err != nil {
exhttp.WriteJSONResponse(w, http.StatusInternalServerError, resp.WithError("start_login_failed", fmt.Sprintf("Failed to start login process: %s", err.Error())))
} else if firstStep.StepID != connector.LoginStepIDPhoneNumber {
exhttp.WriteJSONResponse(w, http.StatusInternalServerError, resp.WithError("unexpected_step", fmt.Sprintf("Unexpected first step %s", firstStep.StepID)))
} else if nextStep, err := loginProcess.(bridgev2.LoginProcessUserInput).SubmitUserInput(ctx, map[string]string{connector.LoginStepIDPhoneNumber: phone}); err != nil {
exhttp.WriteJSONResponse(w, http.StatusBadRequest, resp.WithError("request_code_failed", fmt.Sprintf("Failed to request code: %s", err.Error())))
} else if nextStep.StepID != connector.LoginStepIDCode {
exhttp.WriteJSONResponse(w, http.StatusInternalServerError, resp.WithError("unexpected_step", fmt.Sprintf("Unexpected step %s", nextStep.StepID)))
} else {
inflightLegacyLoginsLock.Lock()
inflightLegacyLogins[user.MXID] = &legacyLogin{Process: loginProcess, NextStep: nextStep}
inflightLegacyLoginsLock.Unlock()
exhttp.WriteJSONResponse(w, http.StatusOK, resp.
WithState("code").
WithMessage("Code requested successfully. Check your SMS or Telegram app and enter the code below."),
)
}
}
func legacyProvLoginSendCode(w http.ResponseWriter, r *http.Request) {
log := zerolog.Ctx(r.Context()).With().Str("prov_step", "send_code").Logger()
ctx := log.WithContext(r.Context())
user := m.Matrix.Provisioning.GetUser(r)
resp := response{Username: user.MXID, State: "code"}
legacyProvSendCodeReq := map[string]string{}
if inflightLogin, ok := inflightLegacyLogins[user.MXID]; !ok {
exhttp.WriteJSONResponse(w, http.StatusBadRequest, resp.WithError("no_login", "No login process in progress"))
} else if inflightLogin.NextStep.StepID != connector.LoginStepIDCode {
exhttp.WriteJSONResponse(w, http.StatusBadRequest, resp.WithError("unexpected_step", fmt.Sprintf("Unexpected step %s", inflightLogin.NextStep.StepID)))
} else if err := json.NewDecoder(r.Body).Decode(&legacyProvSendCodeReq); err != nil {
exhttp.WriteJSONResponse(w, http.StatusBadRequest, resp.WithError("request_body_invalid", "Request body is invalid"))
} else if code, ok := legacyProvSendCodeReq["code"]; !ok || code == "" {
exhttp.WriteJSONResponse(w, http.StatusBadRequest, resp.WithError("phone_code_missing", "You must provide the code from your phone."))
} else if nextStep, err := inflightLogin.Process.(bridgev2.LoginProcessUserInput).SubmitUserInput(ctx, map[string]string{connector.LoginStepIDCode: code}); err != nil {
exhttp.WriteJSONResponse(w, http.StatusBadRequest, resp.WithError("send_code_failed", fmt.Sprintf("Failed to send code: %s", err.Error())))
} else if nextStep.StepID == connector.LoginStepIDPassword {
inflightLegacyLoginsLock.Lock()
defer inflightLegacyLoginsLock.Unlock()
inflightLegacyLogins[user.MXID].NextStep = nextStep
exhttp.WriteJSONResponse(w, http.StatusAccepted, resp.
WithState("password").
WithMessage("Code accepted, but you have 2-factor authentication enabled. Please enter your password."),
)
return // Don't delete the inflight login yet, we need to submit the password.
} else if nextStep.StepID == connector.LoginStepIDComplete {
exhttp.WriteJSONResponse(w, http.StatusOK, resp.WithState("logged-in"))
go handleLoginComplete(ctx, user, nextStep.CompleteParams.UserLogin)
} else {
exhttp.WriteJSONResponse(w, http.StatusInternalServerError, resp.WithError("unexpected_step", fmt.Sprintf("Unexpected step %s", nextStep.StepID)))
}
// If we got here, then there was an error, or the login is complete.
// Delete the in-flight login.
inflightLegacyLoginsLock.Lock()
delete(inflightLegacyLogins, user.MXID)
inflightLegacyLoginsLock.Unlock()
}
func legacyProvLoginSendPassword(w http.ResponseWriter, r *http.Request) {
log := zerolog.Ctx(r.Context()).With().Str("prov_step", "send_password").Logger()
ctx := log.WithContext(r.Context())
user := m.Matrix.Provisioning.GetUser(r)
resp := response{Username: user.MXID, State: "password"}
legacyProvSendPasswordReq := map[string]string{}
if inflightLogin, ok := inflightLegacyLogins[user.MXID]; !ok {
exhttp.WriteJSONResponse(w, http.StatusBadRequest, resp.WithError("no_login", "No login process in progress"))
} else if inflightLogin.NextStep.StepID != connector.LoginStepIDPassword {
exhttp.WriteJSONResponse(w, http.StatusBadRequest, resp.WithError("unexpected_step", fmt.Sprintf("Unexpected step %s", inflightLogin.NextStep.StepID)))
} else if err := json.NewDecoder(r.Body).Decode(&legacyProvSendPasswordReq); err != nil {
exhttp.WriteJSONResponse(w, http.StatusBadRequest, resp.WithError("request_body_invalid", "Request body is invalid"))
} else if password, ok := legacyProvSendPasswordReq["password"]; !ok || password == "" {
exhttp.WriteJSONResponse(w, http.StatusBadRequest, resp.WithError("password_missing", "You must provide your password."))
} else if nextStep, err := inflightLogin.Process.(bridgev2.LoginProcessUserInput).SubmitUserInput(ctx, map[string]string{connector.LoginStepIDPassword: password}); err != nil {
exhttp.WriteJSONResponse(w, http.StatusBadRequest, resp.WithError("send_password_failed", fmt.Sprintf("Failed to send password: %s", err.Error())))
} else if nextStep.StepID == connector.LoginStepIDComplete {
exhttp.WriteJSONResponse(w, http.StatusOK, resp.WithState("logged-in").WithMessage(nextStep.Instructions))
go handleLoginComplete(ctx, user, nextStep.CompleteParams.UserLogin)
} else {
exhttp.WriteJSONResponse(w, http.StatusInternalServerError, resp.WithError("unexpected_step", fmt.Sprintf("Unexpected step %s", nextStep.StepID)))
}
// If we got here, then there was an error, or the login is complete.
// Delete the in-flight login.
inflightLegacyLoginsLock.Lock()
delete(inflightLegacyLogins, user.MXID)
inflightLegacyLoginsLock.Unlock()
}
func legacyProvLogout(w http.ResponseWriter, r *http.Request) {
user := m.Matrix.Provisioning.GetUser(r)
resp := response{Username: user.MXID}
logins := user.GetUserLogins()
if len(logins) == 0 {
exhttp.WriteJSONResponse(w, http.StatusOK, resp.WithError("not logged in", "You're not logged in"))
return
}
for _, login := range logins {
login.Client.(*connector.TelegramClient).LogoutRemote(r.Context())
}
exhttp.WriteEmptyJSONResponse(w, http.StatusOK)
}
func handleLoginComplete(ctx context.Context, user *bridgev2.User, newLogin *bridgev2.UserLogin) {
allLogins := user.GetUserLogins()
for _, login := range allLogins {
if login.ID != newLogin.ID {
login.Delete(ctx, status.BridgeState{StateEvent: status.StateLoggedOut, Reason: "LOGIN_OVERRIDDEN"}, bridgev2.DeleteOpts{})
}
}
}
func legacyProvContacts(w http.ResponseWriter, r *http.Request) {
log := zerolog.Ctx(r.Context()).With().
Str("prov_method", "contacts").
Logger()
ctx := log.WithContext(r.Context())
var resp response
login := m.Matrix.Provisioning.GetLoginForRequest(w, r)
if login == nil {
exhttp.WriteJSONResponse(w, http.StatusNotFound, resp.WithError(mautrix.MNotFound.ErrCode, "No login found"))
return
}
api := login.Client.(bridgev2.ContactListingNetworkAPI)
contacts, err := api.GetContactList(ctx)
if err != nil {
log.Err(err).Msg("Failed to get contacts")
exhttp.WriteJSONResponse(w, http.StatusInternalServerError, resp.WithError("M_UNKNOWN", fmt.Sprintf("Failed to get contacts: %v", err)))
return
}
contactsMap := map[int64]*legacyContactInfo{}
for _, contact := range contacts {
peerType, id, err := ids.ParseUserID(contact.UserID)
if err != nil {
log.Err(err).Msg("Failed to parse user id")
exhttp.WriteJSONResponse(w, http.StatusInternalServerError, resp.WithError("M_UNKNOWN", fmt.Sprintf("Failed to parse user id: %v", err)))
return
} else if peerType != ids.PeerTypeUser {
log.Err(err).Msg("Unexpected peer type")
exhttp.WriteJSONResponse(w, http.StatusInternalServerError, resp.WithError("M_UNKNOWN", fmt.Sprintf("Unexpected peer type: %s", peerType)))
return
}
if contact.UserInfo != nil {
contact.Ghost.UpdateInfo(ctx, contact.UserInfo)
}
contactsMap[id] = legacyContactInfoFromGhost(contact.Ghost)
}
exhttp.WriteJSONResponse(w, http.StatusOK, contactsMap)
}
func legacyProvResolveIdentifier(w http.ResponseWriter, r *http.Request) {
legacyResolveIdentifierOrStartChat(w, r, false)
}
func legacyProvPM(w http.ResponseWriter, r *http.Request) {
legacyResolveIdentifierOrStartChat(w, r, true)
}
type legacyResolveIdentifierResponse struct {
RoomID id.RoomID `json:"room_id,omitempty"`
JustCreated bool `json:"just_created,omitempty"`
ID int `json:"id,omitempty"`
ContactInfo *legacyContactInfo `json:"contact_info,omitempty"`
}
type legacyContactInfo struct {
Name string `json:"name,omitempty"`
Username string `json:"username,omitempty"`
Phone string `json:"phone,omitempty"`
IsBot bool `json:"is_bot,omitempty"`
AvatarURL id.ContentURIString `json:"avatar_url,omitempty"`
}
func legacyContactInfoFromGhost(ghost *bridgev2.Ghost) *legacyContactInfo {
var username, phone string
for _, id := range ghost.Identifiers {
if strings.HasPrefix(id, "telegram:") {
username = strings.TrimPrefix(id, "telegram:")
} else if strings.HasPrefix(id, "tel:") {
phone = strings.TrimPrefix(id, "tel:")
}
}
return &legacyContactInfo{
Name: ghost.Name,
Username: username,
Phone: phone,
IsBot: ghost.IsBot,
AvatarURL: ghost.AvatarMXC,
}
}
func legacyResolveIdentifierOrStartChat(w http.ResponseWriter, r *http.Request, create bool) {
log := zerolog.Ctx(r.Context()).With().
Str("prov_method", "resolve_identifier").
Bool("create", create).
Logger()
ctx := log.WithContext(r.Context())
var resp response
login := m.Matrix.Provisioning.GetLoginForRequest(w, r)
if login == nil {
exhttp.WriteJSONResponse(w, http.StatusNotFound, resp.WithError(mautrix.MNotFound.ErrCode, "No login found"))
return
}
api := login.Client.(bridgev2.IdentifierResolvingNetworkAPI)
identResp, err := api.ResolveIdentifier(ctx, r.PathValue("identifier"), create)
if err != nil {
log.Err(err).Msg("Failed to resolve identifier")
exhttp.WriteJSONResponse(w, http.StatusInternalServerError,
resp.WithError("M_UNKNOWN", fmt.Sprintf("Failed to resolve identifier: %v", err)))
return
} else if identResp == nil {
exhttp.WriteJSONResponse(w, http.StatusNotFound,
resp.WithError(mautrix.MNotFound.ErrCode, "User not found on Telegram"))
return
}
status := http.StatusOK
var apiResp legacyResolveIdentifierResponse
if identResp.Ghost != nil {
if identResp.UserInfo != nil {
identResp.Ghost.UpdateInfo(ctx, identResp.UserInfo)
}
apiResp.ContactInfo = legacyContactInfoFromGhost(identResp.Ghost)
}
if identResp.Chat != nil {
if identResp.Chat.Portal == nil {
identResp.Chat.Portal, err = m.Bridge.GetPortalByKey(ctx, identResp.Chat.PortalKey)
if err != nil {
log.Err(err).Msg("Failed to get portal")
exhttp.WriteJSONResponse(w, http.StatusInternalServerError,
resp.WithError("M_UNKNOWN", "Failed to get portal"))
return
}
}
if create && identResp.Chat.Portal.MXID == "" {
apiResp.JustCreated = true
status = http.StatusCreated
err = identResp.Chat.Portal.CreateMatrixRoom(ctx, login, identResp.Chat.PortalInfo)
if err != nil {
log.Err(err).Msg("Failed to create portal room")
exhttp.WriteJSONResponse(w, http.StatusInternalServerError,
resp.WithError("M_UNKNOWN", "Failed to create portal room"))
return
}
}
apiResp.RoomID = identResp.Chat.Portal.MXID
}
exhttp.WriteJSONResponse(w, status, &apiResp)
}
+127
View File
@@ -0,0 +1,127 @@
// mautrix-telegram - A Matrix-Telegram puppeting bridge.
// Copyright (C) 2024 Sumner Evans
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
package main
import (
"context"
"encoding/base64"
"fmt"
"net/http"
"strings"
"go.mau.fi/util/dbutil/litestream"
"go.mau.fi/util/exerrors"
"maunium.net/go/mautrix/bridgev2/bridgeconfig"
"maunium.net/go/mautrix/bridgev2/matrix/mxmain"
"maunium.net/go/mautrix/id"
"go.mau.fi/mautrix-telegram/pkg/connector"
"go.mau.fi/mautrix-telegram/pkg/connector/store/upgrades"
)
// Information to find out exactly which commit the bridge was built from.
// These are filled at build time with the -X linker flag.
var (
Tag = "unknown"
Commit = "unknown"
BuildTime = "unknown"
)
var c = &connector.TelegramConnector{}
var m = mxmain.BridgeMain{
Name: "mautrix-telegram",
URL: "https://github.com/mautrix/telegram",
Description: "A Matrix-Telegram puppeting bridge.",
Version: "26.04",
SemCalVer: true,
Connector: c,
}
func init() {
litestream.Functions["encode"] = func(data []byte, encoding string) string {
if encoding == "base64" {
return base64.StdEncoding.EncodeToString(data)
}
panic(fmt.Errorf("unknown encoding %q", encoding))
}
}
func main() {
bridgeconfig.HackyMigrateLegacyNetworkConfig = migrateLegacyConfig
versionWithoutCommit := m.Version
m.PostStart = func() {
if m.Matrix.Provisioning != nil {
m.Matrix.Provisioning.GetAuthFromRequest = func(r *http.Request) string {
if !strings.HasSuffix(r.URL.Path, "/login/qr") {
return ""
}
authParts := strings.Split(r.Header.Get("Sec-WebSocket-Protocol"), ",")
for _, part := range authParts {
part = strings.TrimSpace(part)
if strings.HasPrefix(part, "net.maunium.telegram.auth-") {
return strings.TrimPrefix(part, "net.maunium.telegram.auth-")
}
}
return ""
}
m.Matrix.Provisioning.GetUserIDFromRequest = func(r *http.Request) id.UserID {
return id.UserID(r.PathValue("userID"))
}
m.Matrix.Provisioning.Router.HandleFunc("/v1/user/{userID}/login/qr", legacyProvLoginQR)
m.Matrix.Provisioning.Router.HandleFunc("POST /v1/user/{userID}/login/request_code", legacyProvLoginRequestCode)
m.Matrix.Provisioning.Router.HandleFunc("POST /v1/user/{userID}/login/send_code", legacyProvLoginSendCode)
m.Matrix.Provisioning.Router.HandleFunc("POST /v1/user/{userID}/login/send_password", legacyProvLoginSendPassword)
m.Matrix.Provisioning.Router.HandleFunc("POST /v1/user/{userID}/logout", legacyProvLogout)
m.Matrix.Provisioning.Router.HandleFunc("GET /v1/user/{userID}/contacts", legacyProvContacts)
m.Matrix.Provisioning.Router.HandleFunc("/v1/user/{userID}/resolve_identifier/{identifier}", legacyProvResolveIdentifier)
m.Matrix.Provisioning.Router.HandleFunc("POST /v1/user/{userID}/pm/{identifier}", legacyProvPM)
}
}
m.PostInit = func() {
if c.Config.DeviceInfo.AppVersion == "auto" {
c.Config.DeviceInfo.AppVersion = versionWithoutCommit
}
if c.Config.DeviceInfo.SystemVersion == "auto" {
c.Config.DeviceInfo.SystemVersion = ""
}
if c.Config.DeviceInfo.DeviceModel == "auto" || c.Config.DeviceInfo.DeviceModel == "" {
c.Config.DeviceInfo.DeviceModel = "mautrix-telegram"
}
m.CheckLegacyDB(
18,
"v0.14.0",
"v26.04",
m.LegacyMigrateWithAnotherUpgrader(
legacyMigrateRenameTables, legacyMigrateCopyData, 27,
upgrades.Table, "telegram_version", 6,
),
true,
)
ctx := context.TODO()
if exists, _ := m.DB.TableExists(ctx, "telegram_file_old"); exists {
exerrors.Must(m.DB.Exec(ctx, `
PRAGMA foreign_keys = 'OFF';
DROP TABLE telegram_file_old;
PRAGMA foreign_key_check;
PRAGMA foreign_keys = 'ON';
`))
}
}
m.InitVersion(Tag, Commit, BuildTime)
m.Run()
}
+21 -11
View File
@@ -1,27 +1,37 @@
#!/bin/sh
# Define functions.
if [[ -z "$GID" ]]; then
GID="$UID"
fi
BINARY_NAME=/usr/bin/mautrix-telegram
function fixperms {
chown -R $UID:$GID /data /opt/mautrix-telegram
chown -R $UID:$GID /data
# /opt/mautrix-telegram is read-only, so disable file logging if it's pointing there.
if [[ "$(yq e '.logging.writers[1].filename' /data/config.yaml)" == "./logs/mautrix-telegram.log" ]]; then
yq -I4 e -i 'del(.logging.writers[1])' /data/config.yaml
fi
}
cd /opt/mautrix-telegram
if [ ! -f /data/config.yaml ]; then
cp example-config.yaml /data/config.yaml
if [[ ! -f /data/config.yaml ]]; then
$BINARY_NAME -c /data/config.yaml -e
echo "Didn't find a config file."
echo "Copied default config file to /data/config.yaml"
echo "Modify that config file to your liking."
echo "Start the container again after that to generate the registration file."
fixperms
exit
fi
if [ ! -f /data/registration.yaml ]; then
python3 -m mautrix_telegram -g -c /data/config.yaml -r /data/registration.yaml
fixperms
if [[ ! -f /data/registration.yaml ]]; then
$BINARY_NAME -g -c /data/config.yaml -r /data/registration.yaml
echo "Didn't find a registration file."
echo "Generated one for you."
echo "See https://docs.mau.fi/bridges/general/registering-appservices.html on how to use it."
exit
fi
cd /data
fixperms
exec su-exec $UID:$GID python3 -m mautrix_telegram -c /data/config.yaml
exec su-exec $UID:$GID $BINARY_NAME
+81
View File
@@ -0,0 +1,81 @@
module go.mau.fi/mautrix-telegram
go 1.25.0
toolchain go1.26.2
tool (
go.mau.fi/util/cmd/maubuild
golang.org/x/tools/cmd/stringer
)
require (
github.com/cenkalti/backoff/v4 v4.3.0
github.com/coder/websocket v1.8.14
github.com/go-faster/errors v0.7.1
github.com/go-faster/jx v1.2.0
github.com/go-faster/xor v1.0.0
github.com/go-openapi/inflect v0.21.5
github.com/gorilla/websocket v1.5.0
github.com/gotd/getdoc v0.51.0
github.com/gotd/ige v0.2.2
github.com/gotd/neo v0.1.5
github.com/gotd/tl v0.4.0
github.com/k0kubun/pp/v3 v3.5.1
github.com/klauspost/compress v1.18.5
github.com/rogpeppe/go-internal v1.14.1
github.com/rs/zerolog v1.35.0
github.com/stretchr/testify v1.11.1
github.com/tidwall/gjson v1.18.0
go.mau.fi/util v0.9.9-0.20260430092340-8772e7714ea5
go.mau.fi/webp v0.2.0
go.mau.fi/zerozap v0.1.2
go.opentelemetry.io/otel v1.42.0
go.opentelemetry.io/otel/trace v1.42.0
go.uber.org/atomic v1.11.0
go.uber.org/multierr v1.11.0
go.uber.org/zap v1.27.1
golang.org/x/crypto v0.50.0
golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f
golang.org/x/image v0.39.0
golang.org/x/net v0.53.0
golang.org/x/sync v0.20.0
golang.org/x/tools v0.44.0
gopkg.in/yaml.v3 v3.0.1
maunium.net/go/mautrix v0.27.1-0.20260430124810-125ac2c48014
rsc.io/qr v0.2.0
)
require (
filippo.io/edwards25519 v1.2.0 // indirect
github.com/PuerkitoBio/goquery v1.10.3 // indirect
github.com/andybalholm/cascadia v1.3.3 // indirect
github.com/benbjohnson/clock v1.3.0 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/coreos/go-systemd/v22 v22.7.0 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/go-faster/sdk v0.28.0 // indirect
github.com/kr/pretty v0.3.1 // indirect
github.com/lib/pq v1.12.3 // indirect
github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-sqlite3 v1.14.42 // indirect
github.com/petermattis/goid v0.0.0-20260330135022-df67b199bc81 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/rs/xid v1.6.0 // indirect
github.com/segmentio/asm v1.2.1 // indirect
github.com/sergi/go-diff v1.1.0 // indirect
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e // indirect
github.com/tidwall/match v1.2.0 // indirect
github.com/tidwall/pretty v1.2.1 // indirect
github.com/tidwall/sjson v1.2.5 // indirect
github.com/yuin/goldmark v1.8.2 // indirect
go.mau.fi/zeroconfig v0.2.0 // indirect
go.uber.org/ratelimit v0.3.1 // indirect
golang.org/x/mod v0.35.0 // indirect
golang.org/x/sys v0.43.0 // indirect
golang.org/x/text v0.36.0 // indirect
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect
maunium.net/go/mauflag v1.0.0 // indirect
)
+242
View File
@@ -0,0 +1,242 @@
filippo.io/edwards25519 v1.2.0 h1:crnVqOiS4jqYleHd9vaKZ+HKtHfllngJIiOpNpoJsjo=
filippo.io/edwards25519 v1.2.0/go.mod h1:xzAOLCNug/yB62zG1bQ8uziwrIqIuxhctzJT18Q77mc=
github.com/DATA-DOG/go-sqlmock v1.5.2 h1:OcvFkGmslmlZibjAjaHm3L//6LiuBgolP7OputlJIzU=
github.com/DATA-DOG/go-sqlmock v1.5.2/go.mod h1:88MAG/4G7SMwSE3CeA0ZKzrT5CiOU3OJ+JlNzwDqpNU=
github.com/PuerkitoBio/goquery v1.10.3 h1:pFYcNSqHxBD06Fpj/KsbStFRsgRATgnf3LeXiUkhzPo=
github.com/PuerkitoBio/goquery v1.10.3/go.mod h1:tMUX0zDMHXYlAQk6p35XxQMqMweEKB7iK7iLNd4RH4Y=
github.com/andybalholm/cascadia v1.3.3 h1:AG2YHrzJIm4BZ19iwJ/DAua6Btl3IwJX+VI4kktS1LM=
github.com/andybalholm/cascadia v1.3.3/go.mod h1:xNd9bqTn98Ln4DwST8/nG+H0yuB8Hmgu1YHNnWw0GeA=
github.com/benbjohnson/clock v1.3.0 h1:ip6w0uFQkncKQ979AypyG0ER7mqUSBdKLOgAle/AT8A=
github.com/benbjohnson/clock v1.3.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/coder/websocket v1.8.14 h1:9L0p0iKiNOibykf283eHkKUHHrpG7f65OE3BhhO7v9g=
github.com/coder/websocket v1.8.14/go.mod h1:NX3SzP+inril6yawo5CQXx8+fk145lPDC6pumgx0mVg=
github.com/coreos/go-systemd/v22 v22.7.0 h1:LAEzFkke61DFROc7zNLX/WA2i5J8gYqe0rSj9KI28KA=
github.com/coreos/go-systemd/v22 v22.7.0/go.mod h1:xNUYtjHu2EDXbsxz1i41wouACIwT7Ybq9o0BQhMwD0w=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/go-faster/errors v0.7.1 h1:MkJTnDoEdi9pDabt1dpWf7AA8/BaSYZqibYyhZ20AYg=
github.com/go-faster/errors v0.7.1/go.mod h1:5ySTjWFiphBs07IKuiL69nxdfd5+fzh1u7FPGZP2quo=
github.com/go-faster/jx v1.2.0 h1:T2YHJPrFaYu21fJtUxC9GzmluKu8rVIFDwwGBKTDseI=
github.com/go-faster/jx v1.2.0/go.mod h1:UWLOVDmMG597a5tBFPLIWJdUxz5/2emOpfsj9Neg0PE=
github.com/go-faster/sdk v0.28.0 h1:zIu1bt0aeujpUJ/3GxaKy/Yn8Y5K9em4yYNsMHqOl+4=
github.com/go-faster/sdk v0.28.0/go.mod h1:Ts+Rd1B0ltePMxuuCwphkfPVtTIbJhV6jzsV46MVM5w=
github.com/go-faster/xor v0.3.0/go.mod h1:x5CaDY9UKErKzqfRfFZdfu+OSTfoZny3w5Ak7UxcipQ=
github.com/go-faster/xor v1.0.0 h1:2o8vTOgErSGHP3/7XwA5ib1FTtUsNtwCoLLBjl31X38=
github.com/go-faster/xor v1.0.0/go.mod h1:x5CaDY9UKErKzqfRfFZdfu+OSTfoZny3w5Ak7UxcipQ=
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/go-openapi/inflect v0.21.5 h1:M2RCq6PPS3YbIaL7CXosGL3BbzAcmfBAT0nC3YfesZA=
github.com/go-openapi/inflect v0.21.5/go.mod h1:GypUyi6bU880NYurWaEH2CmH84zFDNd+EhhmzroHmB4=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/gotd/getdoc v0.51.0 h1:69sHC34TnRCizsL/2fGWkbs88GYmEHbRHmHQxF8+umM=
github.com/gotd/getdoc v0.51.0/go.mod h1:yGFagVr+5jxhUkVTQGOqiBG4Pty0slsLGBqEYhNrGIU=
github.com/gotd/ige v0.2.2 h1:XQ9dJZwBfDnOGSTxKXBGP4gMud3Qku2ekScRjDWWfEk=
github.com/gotd/ige v0.2.2/go.mod h1:tuCRb+Y5Y3eNTo3ypIfNpQ4MFjrnONiL2jN2AKZXmb0=
github.com/gotd/neo v0.1.5 h1:oj0iQfMbGClP8xI59x7fE/uHoTJD7NZH9oV1WNuPukQ=
github.com/gotd/neo v0.1.5/go.mod h1:9A2a4bn9zL6FADufBdt7tZt+WMhvZoc5gWXihOPoiBQ=
github.com/gotd/tl v0.4.0 h1:8k2z0drujiPyhpLDa9PRm/yU1Gwlfn3iUzeInPiXwMA=
github.com/gotd/tl v0.4.0/go.mod h1:CMIcjPWFS4qxxJ+1Ce7U/ilbtPrkoVo/t8uhN5Y/D7c=
github.com/k0kubun/pp/v3 v3.5.1 h1:fS8Xt0MWVVSiKwfXeIdE0WJlktdA87/gt0Hs0+j2R2s=
github.com/k0kubun/pp/v3 v3.5.1/go.mod h1:s7qPOSp65uuilpprLJs2yDi9DNd7JGyWJPtPvDFpG9w=
github.com/klauspost/compress v1.18.5 h1:/h1gH5Ce+VWNLSWqPzOVn6XBO+vJbCNGvjoaGBFW2IE=
github.com/klauspost/compress v1.18.5/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/lib/pq v1.12.3 h1:tTWxr2YLKwIvK90ZXEw8GP7UFHtcbTtty8zsI+YjrfQ=
github.com/lib/pq v1.12.3/go.mod h1:/p+8NSbOcwzAEI7wiMXFlgydTwcgTr3OSKMsD2BitpA=
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-sqlite3 v1.14.42 h1:MigqEP4ZmHw3aIdIT7T+9TLa90Z6smwcthx+Azv4Cgo=
github.com/mattn/go-sqlite3 v1.14.42/go.mod h1:pjEuOr8IwzLJP2MfGeTb0A35jauH+C2kbHKBr7yXKVQ=
github.com/petermattis/goid v0.0.0-20260330135022-df67b199bc81 h1:WDsQxOJDy0N1VRAjXLpi8sCEZRSGarLWQevDxpTBRrM=
github.com/petermattis/goid v0.0.0-20260330135022-df67b199bc81/go.mod h1:pxMtw7cyUw6B2bRH0ZBANSPg+AoSud1I1iyJHI69jH4=
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
github.com/rs/xid v1.6.0 h1:fV591PaemRlL6JfRxGDEPl69wICngIQ3shQtzfy2gxU=
github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0=
github.com/rs/zerolog v1.35.0 h1:VD0ykx7HMiMJytqINBsKcbLS+BJ4WYjz+05us+LRTdI=
github.com/rs/zerolog v1.35.0/go.mod h1:EjML9kdfa/RMA7h/6z6pYmq1ykOuA8/mjWaEvGI+jcw=
github.com/sebdah/goldie/v2 v2.5.3 h1:9ES/mNN+HNUbNWpVAlrzuZ7jE+Nrczbj8uFRjM7624Y=
github.com/sebdah/goldie/v2 v2.5.3/go.mod h1:oZ9fp0+se1eapSRjfYbsV/0Hqhbuu3bJVvKI/NNtssI=
github.com/segmentio/asm v1.2.1 h1:DTNbBqs57ioxAD4PrArqftgypG4/qNpXoJx8TVXxPR0=
github.com/segmentio/asm v1.2.1/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs=
github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0=
github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e h1:MRM5ITcdelLK2j1vwZ3Je0FKVCfqOLp5zO6trqMLYs0=
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e/go.mod h1:XV66xRDqSt+GTGFMVlhk3ULuV0y9ZmzeVGR4mloJI3M=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY=
github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
github.com/tidwall/match v1.2.0 h1:0pt8FlkOwjN2fPt4bIl4BoNxb98gGHN2ObFEDkrfZnM=
github.com/tidwall/match v1.2.0/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4=
github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY=
github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
github.com/yuin/goldmark v1.8.2 h1:kEGpgqJXdgbkhcOgBxkC0X0PmoPG1ZyoZ117rDVp4zE=
github.com/yuin/goldmark v1.8.2/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg=
go.mau.fi/util v0.9.9-0.20260430092340-8772e7714ea5 h1:cNm4gkt7j907g1Q4XvyNKW8tTM8BaU91Kbfa5GGyiCs=
go.mau.fi/util v0.9.9-0.20260430092340-8772e7714ea5/go.mod h1:up/5mbzH2M1pSBNXqRxODn8dg/hEKbLJu92W4/SNAX0=
go.mau.fi/webp v0.2.0 h1:QVMenHw7JDb4vall5sV75JNBQj9Hw4u8AKbi1QetHvg=
go.mau.fi/webp v0.2.0/go.mod h1:VSg9MyODn12Mb5pyG0NIyNFhujrmoFSsZBs8syOZD1Q=
go.mau.fi/zeroconfig v0.2.0 h1:e/OGEERqVRRKlgaro7E6bh8xXiKFSXB3eNNIud7FUjU=
go.mau.fi/zeroconfig v0.2.0/go.mod h1:J0Vn0prHNOm493oZoQ84kq83ZaNCYZnq+noI1b1eN8w=
go.mau.fi/zerozap v0.1.2 h1:ffH+8kPveX1qE0IbzeBu4pJ15vwp7Sz3H13qlZ1myGs=
go.mau.fi/zerozap v0.1.2/go.mod h1:I+w0ErpJijmc7q/63ns98W1jkBqF8iXwAe1krrd1IHU=
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
go.opentelemetry.io/otel v1.42.0 h1:lSQGzTgVR3+sgJDAU/7/ZMjN9Z+vUip7leaqBKy4sho=
go.opentelemetry.io/otel v1.42.0/go.mod h1:lJNsdRMxCUIWuMlVJWzecSMuNjE7dOYyWlqOXWkdqCc=
go.opentelemetry.io/otel/metric v1.42.0 h1:2jXG+3oZLNXEPfNmnpxKDeZsFI5o4J+nz6xUlaFdF/4=
go.opentelemetry.io/otel/metric v1.42.0/go.mod h1:RlUN/7vTU7Ao/diDkEpQpnz3/92J9ko05BIwxYa2SSI=
go.opentelemetry.io/otel/sdk v1.37.0 h1:ItB0QUqnjesGRvNcmAcU0LyvkVyGJ2xftD29bWdDvKI=
go.opentelemetry.io/otel/sdk v1.37.0/go.mod h1:VredYzxUvuo2q3WRcDnKDjbdvmO0sCzOvVAiY+yUkAg=
go.opentelemetry.io/otel/trace v1.42.0 h1:OUCgIPt+mzOnaUTpOQcBiM/PLQ/Op7oq6g4LenLmOYY=
go.opentelemetry.io/otel/trace v1.42.0/go.mod h1:f3K9S+IFqnumBkKhRJMeaZeNk9epyhnCmQh/EysQCdc=
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
go.uber.org/ratelimit v0.3.1 h1:K4qVE+byfv/B3tC+4nYWP7v/6SimcO7HzHekoMNBma0=
go.uber.org/ratelimit v0.3.1/go.mod h1:6euWsTB6U/Nb3X++xEUXA8ciPJvr19Q/0h1+oDcJhRk=
go.uber.org/zap v1.27.1 h1:08RqriUEv8+ArZRYSTXy1LeBScaMpVSTBhCeaZYfMYc=
go.uber.org/zap v1.27.1/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
golang.org/x/crypto v0.50.0 h1:zO47/JPrL6vsNkINmLoo/PH1gcxpls50DNogFvB5ZGI=
golang.org/x/crypto v0.50.0/go.mod h1:3muZ7vA7PBCE6xgPX7nkzzjiUq87kRItoJQM1Yo8S+Q=
golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f h1:W3F4c+6OLc6H2lb//N1q4WpJkhzJCK5J6kUi1NTVXfM=
golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f/go.mod h1:J1xhfL/vlindoeF/aINzNzt2Bket5bjo9sdOYzOsU80=
golang.org/x/image v0.39.0 h1:skVYidAEVKgn8lZ602XO75asgXBgLj9G/FE3RbuPFww=
golang.org/x/image v0.39.0/go.mod h1:sIbmppfU+xFLPIG0FoVUTvyBMmgng1/XAMhQ2ft0hpA=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/mod v0.35.0 h1:Ww1D637e6Pg+Zb2KrWfHQUnH2dQRLBQyAtpr/haaJeM=
golang.org/x/mod v0.35.0/go.mod h1:+GwiRhIInF8wPm+4AoT6L0FA1QWAad3OMdTRx4tFYlU=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
golang.org/x/net v0.53.0 h1:d+qAbo5L0orcWAr0a9JweQpjXF19LMXJE8Ey7hwOdUA=
golang.org/x/net v0.53.0/go.mod h1:JvMuJH7rrdiCfbeHoo3fCQU24Lf5JJwT9W3sJFulfgs=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI=
golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
golang.org/x/text v0.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg=
golang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
golang.org/x/tools v0.44.0 h1:UP4ajHPIcuMjT1GqzDWRlalUEoY+uzoZKnhOjbIPD2c=
golang.org/x/tools v0.44.0/go.mod h1:KA0AfVErSdxRZIsOVipbv3rQhVXTnlU6UhKxHd1seDI=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc=
gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
maunium.net/go/mauflag v1.0.0 h1:YiaRc0tEI3toYtJMRIfjP+jklH45uDHtT80nUamyD4M=
maunium.net/go/mauflag v1.0.0/go.mod h1:nLivPOpTpHnpzEh8jEdSL9UqO9+/KBJFmNRlwKfkPeA=
maunium.net/go/mautrix v0.27.1-0.20260430124810-125ac2c48014 h1:KwXGBWwUHYJKVTYWgbZEFcaM6uYLMvfjzHJg/TLwvKc=
maunium.net/go/mautrix v0.27.1-0.20260430124810-125ac2c48014/go.mod h1:4fZ0M0xB5ZtueQI65RilX28J/3794BeK+LaCg4U61Jk=
rsc.io/qr v0.2.0 h1:6vBLea5/NRMVTz8V66gipeLycZMl/+UlFmk8DvqQ6WY=
rsc.io/qr v0.2.0/go.mod h1:IF+uZjkb9fqyeF/4tlBoynqmQxUoPfWEKh921coOuXs=
-2
View File
@@ -1,2 +0,0 @@
__version__ = "0.11.0"
__author__ = "Tulir Asokan <tulir@maunium.net>"
-141
View File
@@ -1,141 +0,0 @@
# mautrix-telegram - A Matrix-Telegram puppeting bridge
# Copyright (C) 2021 Tulir Asokan
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
from __future__ import annotations
from typing import Any
from telethon import __version__ as __telethon_version__
from mautrix.bridge import Bridge
from mautrix.types import RoomID, UserID
from .bot import Bot
from .config import Config
from .db import init as init_db, upgrade_table
from .matrix import MatrixHandler
from .portal import Portal
from .puppet import Puppet
from .user import User
from .version import linkified_version, version
from .web.provisioning import ProvisioningAPI
from .web.public import PublicBridgeWebsite
from .abstract_user import AbstractUser # isort: skip
class TelegramBridge(Bridge):
module = "mautrix_telegram"
name = "mautrix-telegram"
command = "python -m mautrix-telegram"
description = "A Matrix-Telegram puppeting bridge."
repo_url = "https://github.com/mautrix/telegram"
version = version
markdown_version = linkified_version
config_class = Config
matrix_class = MatrixHandler
upgrade_table = upgrade_table
config: Config
bot: Bot | None
public_website: PublicBridgeWebsite | None
provisioning_api: ProvisioningAPI | None
def prepare_db(self) -> None:
super().prepare_db()
init_db(self.db)
def _prepare_website(self) -> None:
if self.config["appservice.provisioning.enabled"]:
self.provisioning_api = ProvisioningAPI(self)
self.az.app.add_subapp(
self.config["appservice.provisioning.prefix"], self.provisioning_api.app
)
else:
self.provisioning_api = None
if self.config["appservice.public.enabled"]:
self.public_website = PublicBridgeWebsite(self.loop)
self.az.app.add_subapp(
self.config["appservice.public.prefix"], self.public_website.app
)
else:
self.public_website = None
def prepare_bridge(self) -> None:
self._prepare_website()
AbstractUser.init_cls(self)
bot_token: str = self.config["telegram.bot_token"]
if bot_token and not bot_token.lower().startswith("disable"):
self.bot = AbstractUser.relaybot = Bot(bot_token)
else:
self.bot = AbstractUser.relaybot = None
self.matrix = MatrixHandler(self)
Portal.init_cls(self)
self.add_startup_actions(Puppet.init_cls(self))
self.add_startup_actions(User.init_cls(self))
if self.bot:
self.add_startup_actions(self.bot.start())
if self.config["bridge.resend_bridge_info"]:
self.add_startup_actions(self.resend_bridge_info())
async def resend_bridge_info(self) -> None:
self.config["bridge.resend_bridge_info"] = False
self.config.save()
self.log.info("Re-sending bridge info state event to all portals")
async for portal in Portal.all():
await portal.update_bridge_info()
self.log.info("Finished re-sending bridge info state events")
def prepare_stop(self) -> None:
for puppet in Puppet.by_custom_mxid.values():
puppet.stop()
self.shutdown_actions = (user.stop() for user in User.by_tgid.values())
async def get_user(self, user_id: UserID, create: bool = True) -> User | None:
user = await User.get_by_mxid(user_id, create=create)
if user:
await user.ensure_started()
return user
async def get_portal(self, room_id: RoomID) -> Portal | None:
return await Portal.get_by_mxid(room_id)
async def get_puppet(self, user_id: UserID, create: bool = False) -> Puppet | None:
return await Puppet.get_by_mxid(user_id, create=create)
async def get_double_puppet(self, user_id: UserID) -> Puppet | None:
return await Puppet.get_by_custom_mxid(user_id)
def is_bridge_ghost(self, user_id: UserID) -> bool:
return bool(Puppet.get_id_from_mxid(user_id))
async def count_logged_in_users(self) -> int:
return len([user for user in User.by_tgid.values() if user.tgid])
async def manhole_global_namespace(self, user_id: UserID) -> dict[str, Any]:
return {
**await super().manhole_global_namespace(user_id),
"User": User,
"Portal": Portal,
"Puppet": Puppet,
}
@property
def manhole_banner_program_version(self) -> str:
return f"{super().manhole_banner_program_version} and Telethon {__telethon_version__}"
TelegramBridge().run()
-616
View File
@@ -1,616 +0,0 @@
# mautrix-telegram - A Matrix-Telegram puppeting bridge
# Copyright (C) 2021 Tulir Asokan
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
from __future__ import annotations
from typing import TYPE_CHECKING, Any, Type, Union
from abc import ABC, abstractmethod
import asyncio
import logging
import platform
import time
from telethon.network import (
Connection,
ConnectionTcpFull,
ConnectionTcpMTProxyRandomizedIntermediate,
)
from telethon.sessions import Session
from telethon.tl.patched import Message, MessageService
from telethon.tl.types import (
Channel,
Chat,
MessageActionChannelMigrateFrom,
MessageEmpty,
PeerChat,
PeerUser,
TypeUpdate,
UpdateChannelUserTyping,
UpdateChatParticipantAdmin,
UpdateChatParticipants,
UpdateChatUserTyping,
UpdateDeleteChannelMessages,
UpdateDeleteMessages,
UpdateEditChannelMessage,
UpdateEditMessage,
UpdateFolderPeers,
UpdateNewChannelMessage,
UpdateNewMessage,
UpdateNotifySettings,
UpdatePinnedChannelMessages,
UpdatePinnedDialogs,
UpdatePinnedMessages,
UpdateReadChannelInbox,
UpdateReadHistoryInbox,
UpdateReadHistoryOutbox,
UpdateShortChatMessage,
UpdateShortMessage,
UpdateUserName,
UpdateUserPhoto,
UpdateUserStatus,
UpdateUserTyping,
User,
UserStatusOffline,
UserStatusOnline,
)
from mautrix.appservice import AppService
from mautrix.errors import MatrixError
from mautrix.types import PresenceState, UserID
from mautrix.util.logging import TraceLogger
from mautrix.util.opt_prometheus import Counter, Histogram
from . import __version__, portal as po, puppet as pu
from .config import Config
from .db import Message as DBMessage, PgSession
from .tgclient import MautrixTelegramClient
from .types import TelegramID
if TYPE_CHECKING:
from .__main__ import TelegramBridge
from .bot import Bot
UpdateMessage = Union[
UpdateShortChatMessage,
UpdateShortMessage,
UpdateNewChannelMessage,
UpdateNewMessage,
UpdateEditMessage,
UpdateEditChannelMessage,
]
UpdateMessageContent = Union[
UpdateShortMessage, UpdateShortChatMessage, Message, MessageService, MessageEmpty
]
UPDATE_TIME = Histogram(
name="bridge_telegram_update",
documentation="Time spent processing Telegram updates",
labelnames=("update_type",),
)
UPDATE_ERRORS = Counter(
name="bridge_telegram_update_error",
documentation="Number of fatal errors while handling Telegram updates",
labelnames=("update_type",),
)
class AbstractUser(ABC):
loop: asyncio.AbstractEventLoop = None
log: TraceLogger
az: AppService
bridge: "TelegramBridge"
config: Config
relaybot: "Bot"
ignore_incoming_bot_events: bool = True
max_deletions: int = 10
client: MautrixTelegramClient | None
mxid: UserID | None
tgid: TelegramID | None
username: str | None
is_bot: bool
is_relaybot: bool
puppet_whitelisted: bool
whitelisted: bool
relaybot_whitelisted: bool
matrix_puppet_whitelisted: bool
is_admin: bool
def __init__(self) -> None:
self.is_admin = False
self.matrix_puppet_whitelisted = False
self.puppet_whitelisted = False
self.whitelisted = False
self.relaybot_whitelisted = False
self.client = None
self.is_relaybot = False
self.is_bot = False
@property
def connected(self) -> bool:
return self.client and self.client.is_connected()
@property
def _proxy_settings(self) -> tuple[Type[Connection], tuple[Any, ...] | None]:
proxy_type = self.config["telegram.proxy.type"].lower()
connection = ConnectionTcpFull
connection_data = (
self.config["telegram.proxy.address"],
self.config["telegram.proxy.port"],
self.config["telegram.proxy.rdns"],
self.config["telegram.proxy.username"],
self.config["telegram.proxy.password"],
)
if proxy_type == "disabled":
connection_data = None
elif proxy_type == "socks4":
connection_data = (1,) + connection_data
elif proxy_type == "socks5":
connection_data = (2,) + connection_data
elif proxy_type == "http":
connection_data = (3,) + connection_data
elif proxy_type == "mtproxy":
connection = ConnectionTcpMTProxyRandomizedIntermediate
connection_data = (connection_data[0], connection_data[1], connection_data[4])
return connection, connection_data
@classmethod
def init_cls(cls, bridge: "TelegramBridge") -> None:
cls.bridge = bridge
cls.config = bridge.config
cls.loop = bridge.loop
cls.az = bridge.az
cls.ignore_incoming_bot_events = cls.config["bridge.relaybot.ignore_own_incoming_events"]
cls.max_deletions = cls.config["bridge.max_telegram_delete"]
async def _init_client(self) -> None:
self.log.debug(f"Initializing client for {self.name}")
session = await PgSession.get(self.name)
if self.config["telegram.server.enabled"]:
session.set_dc(
self.config["telegram.server.dc"],
self.config["telegram.server.ip"],
self.config["telegram.server.port"],
)
if self.is_relaybot:
base_logger = logging.getLogger("telethon.relaybot")
else:
base_logger = logging.getLogger(f"telethon.{self.tgid or -hash(self.mxid)}")
device = self.config["telegram.device_info.device_model"]
sysversion = self.config["telegram.device_info.system_version"]
appversion = self.config["telegram.device_info.app_version"]
connection, proxy = self._proxy_settings
assert isinstance(session, Session)
self.client = MautrixTelegramClient(
session=session,
api_id=self.config["telegram.api_id"],
api_hash=self.config["telegram.api_hash"],
app_version=__version__ if appversion == "auto" else appversion,
system_version=(
MautrixTelegramClient.__version__ if sysversion == "auto" else sysversion
),
device_model=(
f"{platform.system()} {platform.release()}" if device == "auto" else device
),
timeout=self.config["telegram.connection.timeout"],
connection_retries=self.config["telegram.connection.retries"],
retry_delay=self.config["telegram.connection.retry_delay"],
flood_sleep_threshold=self.config["telegram.connection.flood_sleep_threshold"],
request_retries=self.config["telegram.connection.request_retries"],
connection=connection,
proxy=proxy,
raise_last_call_error=True,
loop=self.loop,
base_logger=base_logger,
)
self.client.add_event_handler(self._update_catch)
@abstractmethod
async def update(self, update: TypeUpdate) -> bool:
return False
@abstractmethod
async def post_login(self) -> None:
raise NotImplementedError()
@abstractmethod
async def register_portal(self, portal: po.Portal) -> None:
raise NotImplementedError()
@abstractmethod
async def unregister_portal(self, tgid: int, tg_receiver: int) -> None:
raise NotImplementedError()
async def _update_catch(self, update: TypeUpdate) -> None:
start_time = time.time()
update_type = type(update).__name__
try:
if not await self.update(update):
await self._update(update)
except Exception:
self.log.exception("Failed to handle Telegram update")
UPDATE_ERRORS.labels(update_type=update_type).inc()
UPDATE_TIME.labels(update_type=update_type).observe(time.time() - start_time)
@property
@abstractmethod
def name(self) -> str:
raise NotImplementedError()
async def is_logged_in(self) -> bool:
return (
self.client and self.client.is_connected() and await self.client.is_user_authorized()
)
async def has_full_access(self, allow_bot: bool = False) -> bool:
return (
self.puppet_whitelisted
and (not self.is_bot or allow_bot)
and await self.is_logged_in()
)
async def start(self, delete_unless_authenticated: bool = False) -> AbstractUser:
if not self.client:
await self._init_client()
await self.client.connect()
self.log.debug(f"{'Bot' if self.is_relaybot else self.mxid} connected: {self.connected}")
return self
async def ensure_started(self, even_if_no_session=False) -> AbstractUser:
if self.connected:
return self
if even_if_no_session or await PgSession.has(self.mxid):
self.log.debug(
"Starting client due to ensure_started"
f"(even_if_no_session={even_if_no_session})"
)
await self.start(delete_unless_authenticated=not even_if_no_session)
return self
async def stop(self) -> None:
await self.client.disconnect()
self.client = None
# region Telegram update handling
async def _update(self, update: TypeUpdate) -> None:
asyncio.create_task(self._handle_entity_updates(getattr(update, "_entities", {})))
if isinstance(
update,
(
UpdateShortChatMessage,
UpdateShortMessage,
UpdateNewChannelMessage,
UpdateNewMessage,
UpdateEditMessage,
UpdateEditChannelMessage,
),
):
await self.update_message(update)
elif isinstance(update, UpdateDeleteMessages):
await self.delete_message(update)
elif isinstance(update, UpdateDeleteChannelMessages):
await self.delete_channel_message(update)
elif isinstance(update, (UpdateChatUserTyping, UpdateChannelUserTyping, UpdateUserTyping)):
await self.update_typing(update)
elif isinstance(update, UpdateUserStatus):
await self.update_status(update)
elif isinstance(update, UpdateChatParticipantAdmin):
await self.update_admin(update)
elif isinstance(update, UpdateChatParticipants):
await self.update_participants(update)
elif isinstance(update, (UpdatePinnedMessages, UpdatePinnedChannelMessages)):
await self.update_pinned_messages(update)
elif isinstance(update, (UpdateUserName, UpdateUserPhoto)):
await self.update_others_info(update)
elif isinstance(update, UpdateReadHistoryOutbox):
await self.update_read_receipt(update)
elif isinstance(update, (UpdateReadHistoryInbox, UpdateReadChannelInbox)):
await self.update_own_read_receipt(update)
elif isinstance(update, UpdateFolderPeers):
await self.update_folder_peers(update)
elif isinstance(update, UpdatePinnedDialogs):
await self.update_pinned_dialogs(update)
elif isinstance(update, UpdateNotifySettings):
await self.update_notify_settings(update)
else:
self.log.trace("Unhandled update: %s", update)
async def update_folder_peers(self, update: UpdateFolderPeers) -> None:
pass
async def update_pinned_dialogs(self, update: UpdatePinnedDialogs) -> None:
pass
async def update_notify_settings(self, update: UpdateNotifySettings) -> None:
pass
async def update_pinned_messages(
self, update: UpdatePinnedMessages | UpdatePinnedChannelMessages
) -> None:
if isinstance(update, UpdatePinnedMessages):
portal = await po.Portal.get_by_entity(update.peer, tg_receiver=self.tgid)
else:
portal = await po.Portal.get_by_tgid(TelegramID(update.channel_id))
if portal and portal.mxid:
await portal.receive_telegram_pin_ids(
update.messages, self.tgid, remove=not update.pinned
)
@staticmethod
async def update_participants(update: UpdateChatParticipants) -> None:
portal = await po.Portal.get_by_tgid(TelegramID(update.participants.chat_id))
if portal and portal.mxid:
await portal.update_power_levels(update.participants.participants)
async def update_read_receipt(self, update: UpdateReadHistoryOutbox) -> None:
if not isinstance(update.peer, PeerUser):
self.log.debug("Unexpected read receipt peer: %s", update.peer)
return
portal = await po.Portal.get_by_tgid(
TelegramID(update.peer.user_id), tg_receiver=self.tgid
)
if not portal or not portal.mxid:
return
# We check that these are user read receipts, so tg_space is always the user ID.
message = await DBMessage.get_one_by_tgid(
TelegramID(update.max_id), self.tgid, edit_index=-1
)
if not message:
return
puppet = await pu.Puppet.get_by_tgid(TelegramID(update.peer.user_id))
await puppet.intent.mark_read(portal.mxid, message.mxid)
async def update_own_read_receipt(
self, update: UpdateReadHistoryInbox | UpdateReadChannelInbox
) -> None:
puppet = await pu.Puppet.get_by_tgid(self.tgid)
if not puppet.is_real_user:
return
if isinstance(update, UpdateReadChannelInbox):
portal = await po.Portal.get_by_tgid(TelegramID(update.channel_id))
elif isinstance(update.peer, PeerChat):
portal = await po.Portal.get_by_tgid(TelegramID(update.peer.chat_id))
elif isinstance(update.peer, PeerUser):
portal = await po.Portal.get_by_tgid(
TelegramID(update.peer.user_id), tg_receiver=self.tgid
)
else:
self.log.debug("Unexpected own read receipt peer: %s", update.peer)
return
if not portal or not portal.mxid:
return
tg_space = portal.tgid if portal.peer_type == "channel" else self.tgid
message = await DBMessage.get_one_by_tgid(
TelegramID(update.max_id), tg_space, edit_index=-1
)
if not message:
return
await puppet.intent.mark_read(portal.mxid, message.mxid)
async def update_admin(self, update: UpdateChatParticipantAdmin) -> None:
# TODO duplication not checked
portal = await po.Portal.get_by_tgid(TelegramID(update.chat_id))
if not portal or not portal.mxid:
return
await portal.set_telegram_admin(TelegramID(update.user_id))
async def update_typing(
self, update: UpdateUserTyping | UpdateChatUserTyping | UpdateChannelUserTyping
) -> None:
sender = None
if isinstance(update, UpdateUserTyping):
portal = await po.Portal.get_by_tgid(
TelegramID(update.user_id), tg_receiver=self.tgid, peer_type="user"
)
sender = await pu.Puppet.get_by_tgid(TelegramID(update.user_id))
elif isinstance(update, UpdateChannelUserTyping):
portal = await po.Portal.get_by_tgid(TelegramID(update.channel_id))
elif isinstance(update, UpdateChatUserTyping):
portal = await po.Portal.get_by_tgid(TelegramID(update.chat_id))
else:
return
if isinstance(update, (UpdateChannelUserTyping, UpdateChatUserTyping)):
# Can typing notifications come from non-user peers?
if not update.from_id.user_id:
return
sender = await pu.Puppet.get_by_tgid(TelegramID(update.from_id.user_id))
if not sender or not portal or not portal.mxid:
return
await portal.handle_telegram_typing(sender, update)
async def _handle_entity_updates(self, entities: dict[int, User | Chat | Channel]) -> None:
try:
users = (entity for entity in entities.values() if isinstance(entity, User))
puppets = ((await pu.Puppet.get_by_tgid(TelegramID(user.id)), user) for user in users)
await asyncio.gather(
*[puppet.try_update_info(self, info) async for puppet, info in puppets if puppet]
)
except Exception:
self.log.exception("Failed to handle entity updates")
async def update_others_info(self, update: UpdateUserName | UpdateUserPhoto) -> None:
# TODO duplication not checked
puppet = await pu.Puppet.get_by_tgid(TelegramID(update.user_id))
if isinstance(update, UpdateUserName):
puppet.username = update.username
if await puppet.update_displayname(self, update):
await puppet.save()
elif isinstance(update, UpdateUserPhoto):
if await puppet.update_avatar(self, update.photo):
await puppet.save()
else:
self.log.warning(f"Unexpected other user info update: {type(update)}")
async def update_status(self, update: UpdateUserStatus) -> None:
puppet = await pu.Puppet.get_by_tgid(TelegramID(update.user_id))
if isinstance(update.status, UserStatusOnline):
await puppet.default_mxid_intent.set_presence(PresenceState.ONLINE)
elif isinstance(update.status, UserStatusOffline):
await puppet.default_mxid_intent.set_presence(PresenceState.OFFLINE)
else:
self.log.warning(f"Unexpected user status update: type({update})")
return
async def get_message_details(
self, update: UpdateMessage
) -> tuple[UpdateMessageContent, pu.Puppet | None, po.Portal | None]:
if isinstance(update, UpdateShortChatMessage):
portal = await po.Portal.get_by_tgid(TelegramID(update.chat_id))
if not portal:
self.log.warning(f"Received message in chat with unknown type {update.chat_id}")
sender = await pu.Puppet.get_by_tgid(TelegramID(update.from_id))
elif isinstance(update, UpdateShortMessage):
portal = await po.Portal.get_by_tgid(
TelegramID(update.user_id), tg_receiver=self.tgid, peer_type="user"
)
sender = await pu.Puppet.get_by_tgid(self.tgid if update.out else update.user_id)
elif isinstance(
update,
(
UpdateNewMessage,
UpdateNewChannelMessage,
UpdateEditMessage,
UpdateEditChannelMessage,
),
):
update = update.message
if isinstance(update, MessageEmpty):
return update, None, None
portal = await po.Portal.get_by_entity(update.peer_id, tg_receiver=self.tgid)
if update.out:
sender = await pu.Puppet.get_by_tgid(self.tgid)
elif isinstance(update.from_id, PeerUser):
sender = await pu.Puppet.get_by_tgid(TelegramID(update.from_id.user_id))
else:
sender = None
else:
self.log.warning(
f"Unexpected message type in User#get_message_details: {type(update)}"
)
return update, None, None
return update, sender, portal
@staticmethod
async def _try_redact(message: DBMessage) -> None:
portal = await po.Portal.get_by_mxid(message.mx_room)
if not portal:
return
try:
await portal.main_intent.redact(message.mx_room, message.mxid)
except MatrixError:
pass
async def delete_message(self, update: UpdateDeleteMessages) -> None:
if len(update.messages) > self.max_deletions:
return
for message_id in update.messages:
for message in await DBMessage.get_all_by_tgid(TelegramID(message_id), self.tgid):
if message.redacted:
continue
await message.delete()
number_left = await DBMessage.count_spaces_by_mxid(message.mxid, message.mx_room)
if number_left == 0:
await self._try_redact(message)
async def delete_channel_message(self, update: UpdateDeleteChannelMessages) -> None:
if len(update.messages) > self.max_deletions:
return
channel_id = TelegramID(update.channel_id)
for message_id in update.messages:
for message in await DBMessage.get_all_by_tgid(TelegramID(message_id), channel_id):
if message.redacted:
continue
await message.delete()
await self._try_redact(message)
async def update_message(self, original_update: UpdateMessage) -> None:
update, sender, portal = await self.get_message_details(original_update)
if not portal:
return
elif portal and not portal.allow_bridging:
self.log.debug(f"Ignoring message in portal {portal.tgid_log} (bridging disallowed)")
return
if self.is_relaybot:
if update.is_private:
if not self.config["bridge.relaybot.private_chat.invite"]:
if sender:
self.log.debug(f"Ignoring private message to bot from {sender.id}")
return
elif not portal.mxid and self.config["bridge.relaybot.ignore_unbridged_group_chat"]:
self.log.debug(
f"Ignoring message received by bot in unbridged chat {portal.tgid_log}"
)
return
if (
self.ignore_incoming_bot_events
and self.relaybot
and sender
and sender.id == self.relaybot.tgid
):
self.log.debug("Ignoring relaybot-sent message %s to %s", update.id, portal.tgid_log)
return
await portal.backfill_lock.wait(f"update {update.id}")
if isinstance(update, MessageService):
if isinstance(update.action, MessageActionChannelMigrateFrom):
self.log.trace(
"Received %s in %s by %d, unregistering portal...",
update.action,
portal.tgid_log,
sender.id,
)
await self.unregister_portal(update.action.chat_id, update.action.chat_id)
await self.register_portal(portal)
return
self.log.trace(
"Handling action %s to %s by %d",
update.action,
portal.tgid_log,
(sender.id if sender else 0),
)
return await portal.handle_telegram_action(self, sender, update)
if isinstance(original_update, (UpdateEditMessage, UpdateEditChannelMessage)):
return await portal.handle_telegram_edit(self, sender, update)
return await portal.handle_telegram_message(self, sender, update)
# endregion
-323
View File
@@ -1,323 +0,0 @@
# mautrix-telegram - A Matrix-Telegram puppeting bridge
# Copyright (C) 2021 Tulir Asokan
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
from typing import Awaitable, Callable, Dict, List, Optional, Tuple
import logging
from telethon.errors import ChannelInvalidError, ChannelPrivateError
from telethon.tl.functions.channels import GetChannelsRequest, GetParticipantRequest
from telethon.tl.functions.messages import GetChatsRequest, GetFullChatRequest
from telethon.tl.patched import Message, MessageService
from telethon.tl.types import (
ChannelParticipantAdmin,
ChannelParticipantCreator,
ChatForbidden,
ChatParticipantAdmin,
ChatParticipantCreator,
InputChannel,
InputUser,
MessageActionChatAddUser,
MessageActionChatDeleteUser,
MessageActionChatMigrateTo,
MessageEntityBotCommand,
PeerChannel,
PeerChat,
PeerUser,
TypePeer,
UpdateNewChannelMessage,
UpdateNewMessage,
User,
)
from mautrix.types import UserID
from . import portal as po, puppet as pu, user as u
from .abstract_user import AbstractUser
from .db import BotChat
from .types import TelegramID
ReplyFunc = Callable[[str], Awaitable[Message]]
class Bot(AbstractUser):
log: logging.Logger = logging.getLogger("mau.user.bot")
token: str
chats: Dict[int, str]
tg_whitelist: List[int]
whitelist_group_admins: bool
_me_info: Optional[User]
_me_mxid: Optional[UserID]
def __init__(self, token: str) -> None:
super().__init__()
self.token = token
self.tgid = None
self.mxid = None
self.puppet_whitelisted = True
self.whitelisted = True
self.relaybot_whitelisted = True
self.tg_username = None
self.is_relaybot = True
self.is_bot = True
self.chats = {}
self.tg_whitelist = []
self.whitelist_group_admins = (
self.config["bridge.relaybot.whitelist_group_admins"] or False
)
self._me_info = None
self._me_mxid = None
async def get_me(self, use_cache: bool = True) -> Tuple[User, UserID]:
if not use_cache or not self._me_mxid:
self._me_info = await self.client.get_me()
self._me_mxid = pu.Puppet.get_mxid_from_id(TelegramID(self._me_info.id))
return self._me_info, self._me_mxid
async def init_permissions(self) -> None:
whitelist = self.config["bridge.relaybot.whitelist"] or []
for user_id in whitelist:
if isinstance(user_id, str):
entity = await self.client.get_input_entity(user_id)
if isinstance(entity, InputUser):
user_id = entity.user_id
else:
user_id = None
if isinstance(user_id, int):
self.tg_whitelist.append(user_id)
async def start(self, delete_unless_authenticated: bool = False) -> "Bot":
self.chats = {chat.id: chat.type for chat in await BotChat.all()}
await super().start(delete_unless_authenticated)
if not await self.is_logged_in():
await self.client.sign_in(bot_token=self.token)
await self.post_login()
return self
async def post_login(self) -> None:
await self.init_permissions()
info = await self.client.get_me()
self.tgid = TelegramID(info.id)
self.tg_username = info.username
self.mxid = pu.Puppet.get_mxid_from_id(self.tgid)
chat_ids = [chat_id for chat_id, chat_type in self.chats.items() if chat_type == "chat"]
response = await self.client(GetChatsRequest(chat_ids))
for chat in response.chats:
if isinstance(chat, ChatForbidden) or chat.left or chat.deactivated:
await self.remove_chat(TelegramID(chat.id))
channel_ids = [
InputChannel(chat_id, 0)
for chat_id, chat_type in self.chats.items()
if chat_type == "channel"
]
for channel_id in channel_ids:
try:
await self.client(GetChannelsRequest([channel_id]))
except (ChannelPrivateError, ChannelInvalidError):
await self.remove_chat(TelegramID(channel_id.channel_id))
async def register_portal(self, portal: po.Portal) -> None:
await self.add_chat(portal.tgid, portal.peer_type)
async def unregister_portal(self, tgid: TelegramID, tg_receiver: TelegramID) -> None:
await self.remove_chat(tgid)
async def add_chat(self, chat_id: TelegramID, chat_type: str) -> None:
if chat_id not in self.chats:
self.chats[chat_id] = chat_type
await BotChat(id=chat_id, type=chat_type).insert()
async def remove_chat(self, chat_id: TelegramID) -> None:
try:
del self.chats[chat_id]
except KeyError:
pass
await BotChat.delete_by_id(chat_id)
async def _can_use_commands(self, chat: TypePeer, tgid: TelegramID) -> bool:
if tgid in self.tg_whitelist:
return True
user = await u.User.get_by_tgid(tgid)
if user and user.is_admin:
self.tg_whitelist.append(user.tgid)
return True
if self.whitelist_group_admins:
if isinstance(chat, PeerChannel):
p = await self.client(GetParticipantRequest(chat, tgid))
return isinstance(
p.participant, (ChannelParticipantCreator, ChannelParticipantAdmin)
)
elif isinstance(chat, PeerChat):
chat = await self.client(GetFullChatRequest(chat.chat_id))
participants = chat.full_chat.participants.participants
for p in participants:
if p.user_id == tgid:
return isinstance(p, (ChatParticipantCreator, ChatParticipantAdmin))
return False
async def check_can_use_commands(self, event: Message, reply: ReplyFunc) -> bool:
# FIXME event.from_id is not int
if not await self._can_use_commands(event.to_id, TelegramID(event.from_id)):
await reply("You do not have the permission to use that command.")
return False
return True
async def handle_command_portal(self, portal: po.Portal, reply: ReplyFunc) -> Message:
if not self.config["bridge.relaybot.authless_portals"]:
return await reply("This bridge doesn't allow portal creation from Telegram.")
if not portal.allow_bridging:
return await reply("This bridge doesn't allow bridging this chat.")
await portal.create_matrix_room(self)
if portal.mxid:
if portal.username:
return await reply(
f"Portal is public: [{portal.alias}](https://matrix.to/#/{portal.alias})"
)
else:
return await reply("Portal is not public. Use `/invite <mxid>` to get an invite.")
async def handle_command_invite(
self, portal: po.Portal, reply: ReplyFunc, mxid_input: UserID
) -> Message:
if len(mxid_input) == 0:
return await reply("Usage: `/invite <mxid>`")
elif not portal.mxid:
return await reply("Portal does not have Matrix room. Create one with /portal first.")
if mxid_input[0] != "@" or mxid_input.find(":") < 2:
return await reply("That doesn't look like a Matrix ID.")
user = await u.User.get_and_start_by_mxid(mxid_input)
if not user.relaybot_whitelisted:
return await reply("That user is not whitelisted to use the bridge.")
elif await user.is_logged_in():
displayname = f"@{user.tg_username}" if user.tg_username else user.displayname
return await reply(
"That user seems to be logged in. "
f"Just invite [{displayname}](tg://user?id={user.tgid})"
)
else:
await portal.invite_to_matrix(user.mxid)
return await reply(f"Invited `{user.mxid}` to the portal.")
@staticmethod
def handle_command_id(message: Message, reply: ReplyFunc) -> Awaitable[Message]:
# Provide the prefixed ID to the user so that the user wouldn't need to specify whether the
# chat is a normal group or a supergroup/channel when using the ID.
if isinstance(message.to_id, PeerChannel):
return reply(f"-100{message.to_id.channel_id}")
elif isinstance(message.to_id, PeerChat):
return reply(str(-message.to_id.chat_id))
elif isinstance(message.to_id, PeerUser):
return reply(f"Your user ID is {message.to_id.user_id}.")
else:
return reply("Failed to find chat ID.")
def match_command(self, text: str, command: str) -> bool:
text = text.lower()
command = f"/{command.lower()}"
command_targeted = f"{command}@{self.tg_username.lower()}"
is_plain_command = text == command or text == command_targeted
if is_plain_command:
return True
is_arg_command = text.startswith(command + " ") or text.startswith(command_targeted + " ")
if is_arg_command:
return True
return False
async def handle_command(self, message: Message) -> None:
def reply(reply_text: str) -> Awaitable[Message]:
return self.client.send_message(message.chat_id, reply_text, reply_to=message.id)
text = message.message
if self.match_command(text, "start"):
pcm = self.config["bridge.relaybot.private_chat.message"]
if pcm:
await reply(pcm)
return
elif self.match_command(text, "id"):
await self.handle_command_id(message, reply)
return
elif message.is_private:
return
portal = await po.Portal.get_by_entity(message.to_id)
is_portal_cmd = self.match_command(text, "portal")
is_invite_cmd = self.match_command(text, "invite")
if is_portal_cmd or is_invite_cmd:
if not await self.check_can_use_commands(message, reply):
return
if is_portal_cmd:
await self.handle_command_portal(portal, reply)
elif is_invite_cmd:
try:
mxid = text[text.index(" ") + 1 :]
except ValueError:
mxid = ""
await self.handle_command_invite(portal, reply, mxid_input=UserID(mxid))
async def handle_service_message(self, message: MessageService) -> None:
to_peer = message.to_id
if isinstance(to_peer, PeerChannel):
to_id = TelegramID(to_peer.channel_id)
chat_type = "channel"
elif isinstance(to_peer, PeerChat):
to_id = TelegramID(to_peer.chat_id)
chat_type = "chat"
else:
return
action = message.action
if isinstance(action, MessageActionChatAddUser) and self.tgid in action.users:
await self.add_chat(to_id, chat_type)
elif isinstance(action, MessageActionChatDeleteUser) and action.user_id == self.tgid:
await self.remove_chat(to_id)
elif isinstance(action, MessageActionChatMigrateTo):
await self.remove_chat(to_id)
await self.add_chat(TelegramID(action.channel_id), "channel")
async def update(self, update) -> bool:
if not isinstance(update, (UpdateNewMessage, UpdateNewChannelMessage)):
return False
if isinstance(update.message, MessageService):
await self.handle_service_message(update.message)
return False
is_command = (
isinstance(update.message, Message)
and update.message.entities
and len(update.message.entities) > 0
and isinstance(update.message.entities[0], MessageEntityBotCommand)
and update.message.entities[0].offset == 0
)
if is_command:
await self.handle_command(update.message)
return False
def is_in_chat(self, peer_id) -> bool:
return peer_id in self.chats
@property
def name(self) -> str:
return "bot"
-26
View File
@@ -1,26 +0,0 @@
from .handler import (
SECTION_ADMIN,
SECTION_AUTH,
SECTION_CREATING_PORTALS,
SECTION_MISC,
SECTION_PORTAL_MANAGEMENT,
CommandEvent,
CommandHandler,
CommandProcessor,
command_handler,
)
# This has to happen after the handler imports
from . import matrix_auth, portal, telegram # isort: skip
__all__ = [
"command_handler",
"CommandHandler",
"CommandProcessor",
"CommandEvent",
"SECTION_AUTH",
"SECTION_MISC",
"SECTION_ADMIN",
"SECTION_CREATING_PORTALS",
"SECTION_PORTAL_MANAGEMENT",
]
-194
View File
@@ -1,194 +0,0 @@
# mautrix-telegram - A Matrix-Telegram puppeting bridge
# Copyright (C) 2021 Tulir Asokan
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
from __future__ import annotations
from typing import TYPE_CHECKING, Any, Awaitable, Callable, NamedTuple
from telethon.errors import FloodWaitError
from mautrix.bridge.commands import (
CommandEvent as BaseCommandEvent,
CommandHandler as BaseCommandHandler,
CommandHandlerFunc,
CommandProcessor as BaseCommandProcessor,
HelpSection,
command_handler as base_command_handler,
)
from mautrix.types import EventID, MessageEventContent, RoomID
from mautrix.util.format_duration import format_duration
from .. import portal as po, user as u
if TYPE_CHECKING:
from ..__main__ import TelegramBridge
class HelpCacheKey(NamedTuple):
is_management: bool
is_portal: bool
puppet_whitelisted: bool
matrix_puppet_whitelisted: bool
is_admin: bool
is_logged_in: bool
SECTION_AUTH = HelpSection("Authentication", 10, "")
SECTION_CREATING_PORTALS = HelpSection("Creating portals", 20, "")
SECTION_PORTAL_MANAGEMENT = HelpSection("Portal management", 30, "")
SECTION_MISC = HelpSection("Miscellaneous", 40, "")
SECTION_ADMIN = HelpSection("Administration", 50, "")
class CommandEvent(BaseCommandEvent):
sender: u.User
portal: po.Portal
def __init__(
self,
processor: CommandProcessor,
room_id: RoomID,
event_id: EventID,
sender: u.User,
command: str,
args: list[str],
content: MessageEventContent,
portal: po.Portal | None,
is_management: bool,
has_bridge_bot: bool,
) -> None:
super().__init__(
processor,
room_id,
event_id,
sender,
command,
args,
content,
portal,
is_management,
has_bridge_bot,
)
self.bridge = processor.bridge
self.tgbot = processor.tgbot
self.config = processor.config
self.public_website = processor.public_website
@property
def print_error_traceback(self) -> bool:
return self.sender.is_admin
async def get_help_key(self) -> HelpCacheKey:
return HelpCacheKey(
self.is_management,
self.portal is not None,
self.sender.puppet_whitelisted,
self.sender.matrix_puppet_whitelisted,
self.sender.is_admin,
await self.sender.is_logged_in(),
)
class CommandHandler(BaseCommandHandler):
name: str
needs_puppeting: bool
needs_matrix_puppeting: bool
def __init__(
self,
handler: Callable[[CommandEvent], Awaitable[EventID]],
management_only: bool,
name: str,
help_text: str,
help_args: str,
help_section: HelpSection,
needs_auth: bool,
needs_puppeting: bool,
needs_matrix_puppeting: bool,
needs_admin: bool,
**kwargs,
) -> None:
super().__init__(
handler,
management_only,
name,
help_text,
help_args,
help_section,
needs_auth=needs_auth,
needs_puppeting=needs_puppeting,
needs_matrix_puppeting=needs_matrix_puppeting,
needs_admin=needs_admin,
**kwargs,
)
async def get_permission_error(self, evt: CommandEvent) -> str | None:
if self.needs_puppeting and not evt.sender.puppet_whitelisted:
return "This command requires puppeting privileges."
elif self.needs_matrix_puppeting and not evt.sender.matrix_puppet_whitelisted:
return "This command requires Matrix puppeting privileges."
return await super().get_permission_error(evt)
def has_permission(self, key: HelpCacheKey) -> bool:
return (
super().has_permission(key)
and (not self.needs_puppeting or key.puppet_whitelisted)
and (not self.needs_matrix_puppeting or key.matrix_puppet_whitelisted)
)
def command_handler(
_func: CommandHandlerFunc | None = None,
*,
needs_auth: bool = True,
needs_puppeting: bool = True,
needs_matrix_puppeting: bool = False,
needs_admin: bool = False,
management_only: bool = False,
name: str | None = None,
help_text: str = "",
help_args: str = "",
help_section: HelpSection = None,
) -> Callable[[CommandHandlerFunc], CommandHandler]:
return base_command_handler(
_func,
_handler_class=CommandHandler,
name=name,
help_text=help_text,
help_args=help_args,
help_section=help_section,
management_only=management_only,
needs_auth=needs_auth,
needs_admin=needs_admin,
needs_puppeting=needs_puppeting,
needs_matrix_puppeting=needs_matrix_puppeting,
)
class CommandProcessor(BaseCommandProcessor):
def __init__(self, bridge: "TelegramBridge") -> None:
super().__init__(event_class=CommandEvent, bridge=bridge)
self.tgbot = bridge.bot
self.public_website = bridge.public_website
@staticmethod
async def _run_handler(
handler: Callable[[CommandEvent], Awaitable[Any]], evt: CommandEvent
) -> Any:
try:
return await handler(evt)
except FloodWaitError as e:
return await evt.reply(f"Flood error: Please wait {format_duration(e.seconds)}")
-85
View File
@@ -1,85 +0,0 @@
# mautrix-telegram - A Matrix-Telegram puppeting bridge
# Copyright (C) 2021 Tulir Asokan
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
from mautrix.bridge import InvalidAccessToken, OnlyLoginSelf
from mautrix.types import EventID
from .. import puppet as pu
from . import SECTION_AUTH, CommandEvent, command_handler
@command_handler(
needs_auth=True,
management_only=True,
needs_matrix_puppeting=True,
help_section=SECTION_AUTH,
help_text="Replace your Telegram account's Matrix puppet with your own Matrix account.",
)
async def login_matrix(evt: CommandEvent) -> EventID:
puppet = await pu.Puppet.get_by_tgid(evt.sender.tgid)
if puppet.is_real_user:
return await evt.reply(
"You have already logged in with your Matrix account. "
"Log out with `$cmdprefix+sp logout-matrix` first."
)
allow_matrix_login = evt.config.get("bridge.allow_matrix_login", True)
if allow_matrix_login:
evt.sender.command_status = {
"next": enter_matrix_token,
"action": "Matrix login",
}
if evt.config["appservice.public.enabled"]:
prefix = evt.config["appservice.public.external"]
token = evt.public_website.make_token(evt.sender.mxid, "/matrix-login")
url = f"{prefix}/matrix-login?token={token}"
if allow_matrix_login:
return await evt.reply(
"This bridge instance allows you to log in inside or outside Matrix.\n\n"
"If you would like to log in within Matrix, please send your Matrix access token "
"here.\n"
f"If you would like to log in outside of Matrix, [click here]({url}).\n\n"
"Logging in outside of Matrix is recommended, because in-Matrix login would save "
"your access token in the message history."
)
return await evt.reply(
"This bridge instance does not allow logging in inside Matrix.\n\n"
f"Please visit [the login page]({url}) to log in."
)
elif allow_matrix_login:
return await evt.reply(
"This bridge instance does not allow you to log in outside of Matrix.\n\n"
"Please send your Matrix access token here to log in."
)
return await evt.reply("This bridge instance has been configured to not allow logging in.")
async def enter_matrix_token(evt: CommandEvent) -> EventID:
evt.sender.command_status = None
puppet = await pu.Puppet.get_by_tgid(evt.sender.tgid)
if puppet.is_real_user:
return await evt.reply(
"You have already logged in with your Matrix account. "
"Log out with `$cmdprefix+sp logout-matrix` first."
)
try:
await puppet.switch_mxid(" ".join(evt.args), evt.sender.mxid)
except OnlyLoginSelf:
return await evt.reply("You can only log in as your own Matrix user.")
except InvalidAccessToken:
return await evt.reply("Failed to verify access token.")
return await evt.reply(
"Replaced your Telegram account's Matrix puppet with {puppet.custom_mxid}."
)
@@ -1 +0,0 @@
from . import admin, bridge, config, create_chat, filter, misc, unbridge
-81
View File
@@ -1,81 +0,0 @@
# mautrix-telegram - A Matrix-Telegram puppeting bridge
# Copyright (C) 2021 Tulir Asokan
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
import asyncio
from mautrix.types import EventID
from ... import portal as po, puppet as pu, user as u
from .. import SECTION_ADMIN, CommandEvent, command_handler
@command_handler(
needs_admin=True,
needs_auth=False,
help_section=SECTION_ADMIN,
help_args="<`portal`|`puppet`|`user`>",
help_text="Clear internal bridge caches",
)
async def clear_db_cache(evt: CommandEvent) -> EventID:
try:
section = evt.args[0].lower()
except IndexError:
return await evt.reply("**Usage:** `$cmdprefix+sp clear-db-cache <section>`")
if section == "portal":
po.Portal.by_tgid = {}
po.Portal.by_mxid = {}
await evt.reply("Cleared portal cache")
elif section == "puppet":
pu.Puppet.by_tgid = {}
for puppet in pu.Puppet.by_custom_mxid.values():
puppet.stop()
pu.Puppet.by_custom_mxid = {}
await asyncio.gather(
*[puppet.try_start() async for puppet in pu.Puppet.all_with_custom_mxid()]
)
await evt.reply("Cleared puppet cache and restarted custom puppet syncers")
elif section == "user":
u.User.by_mxid = {user.mxid: user for user in u.User.by_tgid.values()}
await evt.reply("Cleared non-logged-in user cache")
else:
return await evt.reply("**Usage:** `$cmdprefix+sp clear-db-cache <section>`")
@command_handler(
needs_admin=True,
needs_auth=False,
help_section=SECTION_ADMIN,
help_args="[_mxid_]",
help_text="Reload and reconnect a user",
)
async def reload_user(evt: CommandEvent) -> EventID:
if len(evt.args) > 0:
mxid = evt.args[0]
else:
mxid = evt.sender.mxid
user = await u.User.get_by_mxid(mxid, create=False)
if not user:
return await evt.reply("User not found")
puppet = await pu.Puppet.get_by_custom_mxid(mxid)
if puppet:
puppet.stop()
await user.stop()
del u.User.by_tgid[user.tgid]
del u.User.by_mxid[user.mxid]
user = await u.User.get_by_mxid(mxid)
await user.ensure_started()
if puppet:
await puppet.start()
return await evt.reply(f"Reloaded and reconnected {user.mxid} (telegram: {user.human_tg_id})")
-228
View File
@@ -1,228 +0,0 @@
# mautrix-telegram - A Matrix-Telegram puppeting bridge
# Copyright (C) 2021 Tulir Asokan
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
from __future__ import annotations
from typing import Awaitable
import asyncio
from telethon.tl.types import ChannelForbidden, ChatForbidden
from mautrix.types import EventID, RoomID
from ... import portal as po
from ...types import TelegramID
from .. import SECTION_CREATING_PORTALS, CommandEvent, command_handler
from .util import get_initial_state, user_has_power_level, warn_missing_power
@command_handler(
needs_auth=False,
needs_puppeting=False,
help_section=SECTION_CREATING_PORTALS,
help_args="[_id_]",
help_text=(
"Bridge the current Matrix room to the Telegram chat with the given ID. The ID must be "
"the prefixed version that you get with the `/id` command of the Telegram-side bot."
),
)
async def bridge(evt: CommandEvent) -> EventID:
if len(evt.args) == 0:
return await evt.reply(
"**Usage:** `$cmdprefix+sp bridge <Telegram chat ID> [Matrix room ID]`"
)
force_use_bot = False
if evt.args[0] == "--usebot" and evt.sender.is_admin:
force_use_bot = True
evt.args = evt.args[1:]
room_id = RoomID(evt.args[1]) if len(evt.args) > 1 else evt.room_id
that_this = "This" if room_id == evt.room_id else "That"
portal = await po.Portal.get_by_mxid(room_id)
if portal:
return await evt.reply(f"{that_this} room is already a portal room.")
if not await user_has_power_level(room_id, evt.az.intent, evt.sender, "bridge"):
return await evt.reply(f"You do not have the permissions to bridge {that_this} room.")
# The /id bot command provides the prefixed ID, so we assume
tgid_str = evt.args[0]
if tgid_str.startswith("-100"):
tgid = TelegramID(int(tgid_str[4:]))
peer_type = "channel"
elif tgid_str.startswith("-"):
tgid = TelegramID(-int(tgid_str))
peer_type = "chat"
else:
return await evt.reply(
"That doesn't seem like a prefixed Telegram chat ID.\n\n"
"If you did not get the ID using the `/id` bot command, please "
"prefix channel IDs with `-100` and normal group IDs with `-`.\n\n"
"Bridging private chats to existing rooms is not allowed."
)
portal = await po.Portal.get_by_tgid(tgid, peer_type=peer_type)
if not portal.allow_bridging:
return await evt.reply(
"This bridge doesn't allow bridging that Telegram chat.\n"
"If you're the bridge admin, try "
"`$cmdprefix+sp filter whitelist <Telegram chat ID>` first."
)
if portal.mxid:
has_portal_message = (
"That Telegram chat already has a portal at "
f"[{portal.alias or portal.mxid}](https://matrix.to/#/{portal.mxid}). "
)
if not await user_has_power_level(portal.mxid, evt.az.intent, evt.sender, "unbridge"):
return await evt.reply(
f"{has_portal_message}"
"Additionally, you do not have the permissions to unbridge that room."
)
evt.sender.command_status = {
"next": confirm_bridge,
"action": "Room bridging",
"mxid": portal.mxid,
"bridge_to_mxid": room_id,
"tgid": portal.tgid,
"peer_type": portal.peer_type,
"force_use_bot": force_use_bot,
}
return await evt.reply(
f"{has_portal_message}"
"However, you have the permissions to unbridge that room.\n\n"
"To delete that portal completely and continue bridging, use "
"`$cmdprefix+sp delete-and-continue`. To unbridge the portal "
"without kicking Matrix users, use `$cmdprefix+sp unbridge-and-"
"continue`. To cancel, use `$cmdprefix+sp cancel`"
)
evt.sender.command_status = {
"next": confirm_bridge,
"action": "Room bridging",
"bridge_to_mxid": room_id,
"tgid": portal.tgid,
"peer_type": portal.peer_type,
"force_use_bot": force_use_bot,
}
return await evt.reply(
"That Telegram chat has no existing portal. To confirm bridging the "
"chat to this room, use `$cmdprefix+sp continue`"
)
async def cleanup_old_portal_while_bridging(
evt: CommandEvent, portal: po.Portal
) -> tuple[bool, Awaitable[None] | None]:
if not portal.mxid:
await evt.reply(
"The portal seems to have lost its Matrix room between you"
"calling `$cmdprefix+sp bridge` and this command.\n\n"
"Continuing without touching previous Matrix room..."
)
return True, None
elif evt.args[0] == "delete-and-continue":
return True, portal.cleanup_portal("Portal deleted (moving to another room)", delete=False)
elif evt.args[0] == "unbridge-and-continue":
return True, portal.cleanup_portal(
"Room unbridged (portal moving to another room)", puppets_only=True, delete=False
)
else:
await evt.reply(
"The chat you were trying to bridge already has a Matrix portal room.\n\n"
"Please use `$cmdprefix+sp delete-and-continue` or `$cmdprefix+sp unbridge-and-"
"continue` to either delete or unbridge the existing room (respectively) and "
"continue with the bridging.\n\n"
"If you changed your mind, use `$cmdprefix+sp cancel` to cancel."
)
return False, None
async def confirm_bridge(evt: CommandEvent) -> EventID | None:
status = evt.sender.command_status
try:
portal = await po.Portal.get_by_tgid(status["tgid"], peer_type=status["peer_type"])
bridge_to_mxid = status["bridge_to_mxid"]
except KeyError:
evt.sender.command_status = None
return await evt.reply(
"Fatal error: tgid or peer_type missing from command_status. "
"This shouldn't happen unless you're messing with the command handler code."
)
is_logged_in = await evt.sender.is_logged_in() and not status["force_use_bot"]
if "mxid" in status:
ok, coro = await cleanup_old_portal_while_bridging(evt, portal)
if not ok:
return None
elif coro:
asyncio.create_task(coro)
await evt.reply("Cleaning up previous portal room...")
elif portal.mxid:
evt.sender.command_status = None
return await evt.reply(
"The portal seems to have created a Matrix room between you "
"calling `$cmdprefix+sp bridge` and this command.\n\n"
"Please start over by calling the bridge command again."
)
elif evt.args[0] != "continue":
return await evt.reply(
"Please use `$cmdprefix+sp continue` to confirm the bridging or "
"`$cmdprefix+sp cancel` to cancel."
)
evt.sender.command_status = None
async with portal._room_create_lock:
await _locked_confirm_bridge(
evt, portal=portal, room_id=bridge_to_mxid, is_logged_in=is_logged_in
)
async def _locked_confirm_bridge(
evt: CommandEvent, portal: po.Portal, room_id: RoomID, is_logged_in: bool
) -> EventID | None:
user = evt.sender if is_logged_in else evt.tgbot
try:
entity = await user.client.get_entity(portal.peer)
except Exception:
evt.log.exception("Failed to get_entity(%s) for manual bridging.", portal.peer)
if is_logged_in:
return await evt.reply(
"Failed to get info of telegram chat. You are logged in, are you in that chat?"
)
else:
return await evt.reply(
"Failed to get info of telegram chat. "
"You're not logged in, is the relay bot in the chat?"
)
if isinstance(entity, (ChatForbidden, ChannelForbidden)):
if is_logged_in:
return await evt.reply("You don't seem to be in that chat.")
else:
return await evt.reply("The bot doesn't seem to be in that chat.")
portal.mxid = room_id
portal.by_mxid[portal.mxid] = portal
(portal.title, portal.about, levels, portal.encrypted) = await get_initial_state(
evt.az.intent, evt.room_id
)
portal.photo_id = ""
await portal.save()
await portal.update_bridge_info()
asyncio.create_task(portal.update_matrix_room(user, entity, direct=False, levels=levels))
await warn_missing_power(levels, evt)
return await evt.reply("Bridging complete. Portal synchronization should begin momentarily.")
-161
View File
@@ -1,161 +0,0 @@
# mautrix-telegram - A Matrix-Telegram puppeting bridge
# Copyright (C) 2021 Tulir Asokan
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
from __future__ import annotations
from typing import Any, Awaitable
from io import StringIO
from ruamel.yaml import YAMLError
from mautrix.types import EventID
from mautrix.util.config import yaml
from ... import portal as po, util
from .. import SECTION_PORTAL_MANAGEMENT, CommandEvent, command_handler
@command_handler(
needs_auth=False,
help_section=SECTION_PORTAL_MANAGEMENT,
help_text="View or change per-portal settings.",
help_args="<`help`|_subcommand_> [...]",
)
async def config(evt: CommandEvent) -> None:
cmd = evt.args[0].lower() if len(evt.args) > 0 else "help"
if cmd not in ("view", "defaults", "set", "unset", "add", "del"):
await config_help(evt)
return
elif cmd == "defaults":
await config_defaults(evt)
return
portal = await po.Portal.get_by_mxid(evt.room_id)
if not portal:
await evt.reply("This is not a portal room.")
return
elif cmd == "view":
await config_view(evt, portal)
return
if not await portal.can_user_perform(evt.sender, "config"):
await evt.reply("You do not have the permissions to configure this room.")
return
key = evt.args[1] if len(evt.args) > 1 else None
try:
value = yaml.load(" ".join(evt.args[2:])) if len(evt.args) > 2 else None
except YAMLError as e:
await evt.reply(f"Invalid value provided. Values must be valid YAML.\n{e}")
return
if cmd == "set":
await config_set(evt, portal, key, value)
elif cmd == "unset":
await config_unset(evt, portal, key)
elif cmd == "add" or cmd == "del":
await config_add_del(evt, portal, key, value, cmd)
else:
return
await portal.save()
def config_help(evt: CommandEvent) -> Awaitable[EventID]:
return evt.reply(
"""**Usage:** `$cmdprefix config <subcommand> [...]`. Subcommands:
* **help** - View this help text.
* **view** - View the current config data.
* **defaults** - View the default config values.
* **set** <_key_> <_value_> - Set a config value.
* **unset** <_key_> - Remove a config value.
* **add** <_key_> <_value_> - Add a value to an array.
* **del** <_key_> <_value_> - Remove a value from an array.
"""
)
def config_view(evt: CommandEvent, portal: po.Portal) -> Awaitable[EventID]:
return evt.reply(f"Room-specific config:\n{_str_value(portal.local_config).rstrip()}")
def config_defaults(evt: CommandEvent) -> Awaitable[EventID]:
value = _str_value(
{
"bridge_notices": {
"default": evt.config["bridge.bridge_notices.default"],
"exceptions": evt.config["bridge.bridge_notices.exceptions"],
},
"bot_messages_as_notices": evt.config["bridge.bot_messages_as_notices"],
"inline_images": evt.config["bridge.inline_images"],
"message_formats": evt.config["bridge.message_formats"],
"emote_format": evt.config["bridge.emote_format"],
"state_event_formats": evt.config["bridge.state_event_formats"],
"telegram_link_preview": evt.config["bridge.telegram_link_preview"],
}
)
return evt.reply(f"Bridge instance wide config:\n{value.rstrip()}")
def _str_value(value: Any) -> str:
stream = StringIO()
yaml.dump(value, stream)
value_str = stream.getvalue()
if "\n" in value_str:
return f"\n```yaml\n{value_str}\n```\n"
else:
return f"`{value_str}`"
def config_set(evt: CommandEvent, portal: po.Portal, key: str, value: Any) -> Awaitable[EventID]:
if not key or value is None:
return evt.reply(f"**Usage:** `$cmdprefix+sp config set <key> <value>`")
elif util.recursive_set(portal.local_config, key, value):
return evt.reply(f"Successfully set the value of `{key}` to {_str_value(value)}".rstrip())
else:
return evt.reply(f"Failed to set value of `{key}`. Does the path contain non-map types?")
def config_unset(evt: CommandEvent, portal: po.Portal, key: str) -> Awaitable[EventID]:
if not key:
return evt.reply(f"**Usage:** `$cmdprefix+sp config unset <key>`")
elif util.recursive_del(portal.local_config, key):
return evt.reply(f"Successfully deleted `{key}` from config.")
else:
return evt.reply(f"`{key}` not found in config.")
def config_add_del(
evt: CommandEvent, portal: po.Portal, key: str, value: str, cmd: str
) -> Awaitable[EventID]:
if not key or value is None:
return evt.reply(f"**Usage:** `$cmdprefix+sp config {cmd} <key> <value>`")
arr = util.recursive_get(portal.local_config, key)
if not arr:
return evt.reply(
f"`{key}` not found in config. Maybe do `$cmdprefix+sp config set {key} []` first?"
)
elif not isinstance(arr, list):
return evt.reply("`{key}` does not seem to be an array.")
elif cmd == "add":
if value in arr:
return evt.reply(f"The array at `{key}` already contains {_str_value(value)}".rstrip())
arr.append(value)
return evt.reply(f"Successfully added {_str_value(value)} to the array at `{key}`")
else:
if value not in arr:
return evt.reply(f"The array at `{key}` does not contain {_str_value(value)}")
arr.remove(value)
return evt.reply(f"Successfully removed {_str_value(value)} from the array at `{key}`")
@@ -1,84 +0,0 @@
# mautrix-telegram - A Matrix-Telegram puppeting bridge
# Copyright (C) 2021 Tulir Asokan
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
from __future__ import annotations
from mautrix.types import EventID
from ... import portal as po
from ...types import TelegramID
from .. import SECTION_CREATING_PORTALS, CommandEvent, command_handler
from .util import get_initial_state, user_has_power_level, warn_missing_power
@command_handler(
help_section=SECTION_CREATING_PORTALS,
help_args="[_type_]",
help_text=(
"Create a Telegram chat of the given type for the current Matrix room. "
"The type is either `group`, `supergroup` or `channel` (defaults to `supergroup`)."
),
)
async def create(evt: CommandEvent) -> EventID:
type = evt.args[0] if len(evt.args) > 0 else "supergroup"
if type not in ("chat", "group", "supergroup", "channel"):
return await evt.reply(
"**Usage:** `$cmdprefix+sp create ['group'/'supergroup'/'channel']`"
)
if await po.Portal.get_by_mxid(evt.room_id):
return await evt.reply("This is already a portal room.")
if not await user_has_power_level(evt.room_id, evt.az.intent, evt.sender, "bridge"):
return await evt.reply("You do not have the permissions to bridge this room.")
title, about, levels, encrypted = await get_initial_state(evt.az.intent, evt.room_id)
if not title:
return await evt.reply("Please set a title before creating a Telegram chat.")
supergroup = type == "supergroup"
type = {
"supergroup": "channel",
"channel": "channel",
"chat": "chat",
"group": "chat",
}[type]
portal = po.Portal(
tgid=TelegramID(0),
tg_receiver=TelegramID(0),
peer_type=type,
mxid=evt.room_id,
title=title,
about=about,
encrypted=encrypted,
)
invites, errors = await portal.get_telegram_users_in_matrix_room(evt.sender)
if len(errors) > 0:
error_list = "\n".join(f"* [{mxid}](https://matrix.to/#/{mxid})" for mxid in errors)
await evt.reply(
f"Failed to add the following users to the chat:\n\n{error_list}\n\n"
"You can try `$cmdprefix+sp search -r <username>` to help the bridge find "
"those users."
)
await warn_missing_power(levels, evt)
try:
await portal.create_telegram_chat(evt.sender, invites=invites, supergroup=supergroup)
except ValueError as e:
await portal.delete()
return await evt.reply(e.args[0])
return await evt.reply(f"Telegram chat created. ID: {portal.tgid}")
-105
View File
@@ -1,105 +0,0 @@
# mautrix-telegram - A Matrix-Telegram puppeting bridge
# Copyright (C) 2021 Tulir Asokan
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
from __future__ import annotations
from mautrix.types import EventID
from ... import portal as po
from .. import SECTION_ADMIN, CommandEvent, command_handler
@command_handler(
needs_admin=True,
help_section=SECTION_ADMIN,
help_args="<`whitelist`|`blacklist`>",
help_text="Change whether the bridge will allow or disallow bridging rooms by default.",
)
async def filter_mode(evt: CommandEvent) -> EventID:
try:
mode = evt.args[0]
if mode not in ("whitelist", "blacklist"):
raise ValueError()
except (IndexError, ValueError):
return await evt.reply("**Usage:** `$cmdprefix+sp filter-mode <whitelist/blacklist>`")
evt.config["bridge.filter.mode"] = mode
evt.config.save()
po.Portal.filter_mode = mode
if mode == "whitelist":
return await evt.reply(
"The bridge will now disallow bridging chats by default.\n"
"To allow bridging a specific chat, use"
"`!filter whitelist <chat ID>`."
)
else:
return await evt.reply(
"The bridge will now allow bridging chats by default.\n"
"To disallow bridging a specific chat, use"
"`!filter blacklist <chat ID>`."
)
@command_handler(
name="filter",
needs_admin=True,
help_section=SECTION_ADMIN,
help_args="<`whitelist`|`blacklist`> <_chat ID_>",
help_text="Allow or disallow bridging a specific chat.",
)
async def edit_filter(evt: CommandEvent) -> EventID:
try:
action = evt.args[0]
if action not in ("whitelist", "blacklist", "add", "remove"):
raise ValueError()
id_str = evt.args[1]
if id_str.startswith("-100"):
filter_id = int(id_str[4:])
elif id_str.startswith("-"):
filter_id = int(id_str[1:])
else:
filter_id = int(id_str)
except (IndexError, ValueError):
return await evt.reply("**Usage:** `$cmdprefix+sp filter <whitelist/blacklist> <chat ID>`")
mode = evt.config["bridge.filter.mode"]
if mode not in ("blacklist", "whitelist"):
return await evt.reply(f'Unknown filter mode "{mode}". Please fix the bridge config.')
filter_id_list = evt.config["bridge.filter.list"]
if action in ("blacklist", "whitelist"):
action = "add" if mode == action else "remove"
def save() -> None:
evt.config["bridge.filter.list"] = filter_id_list
evt.config.save()
po.Portal.filter_list = filter_id_list
if action == "add":
if filter_id in filter_id_list:
return await evt.reply(f"That chat is already {mode}ed.")
filter_id_list.append(filter_id)
save()
return await evt.reply(f"Chat ID added to {mode}.")
elif action == "remove":
if filter_id not in filter_id_list:
return await evt.reply(f"That chat is not {mode}ed.")
filter_id_list.remove(filter_id)
save()
return await evt.reply(f"Chat ID removed from {mode}.")
else:
return await evt.reply("**Usage:** `$cmdprefix+sp filter <whitelist/blacklist> <chat ID>`")
-257
View File
@@ -1,257 +0,0 @@
# mautrix-telegram - A Matrix-Telegram puppeting bridge
# Copyright (C) 2021 Tulir Asokan
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
from __future__ import annotations
from datetime import datetime, timedelta
import re
from telethon.errors import (
ChatAdminRequiredError,
RPCError,
UsernameInvalidError,
UsernameNotModifiedError,
UsernameOccupiedError,
)
from telethon.tl.functions.channels import GetFullChannelRequest
from telethon.tl.functions.messages import GetFullChatRequest
from mautrix.types import EventID
from ... import portal as po
from .. import SECTION_MISC, SECTION_PORTAL_MANAGEMENT, CommandEvent, command_handler
from .util import user_has_power_level
@command_handler(
needs_admin=False,
needs_puppeting=False,
needs_auth=False,
help_section=SECTION_MISC,
help_text="Fetch Matrix room state to ensure the bridge has up-to-date info.",
)
async def sync_state(evt: CommandEvent) -> EventID:
portal = await po.Portal.get_by_mxid(evt.room_id)
if not portal:
return await evt.reply("This is not a portal room.")
elif not await user_has_power_level(evt.room_id, evt.az.intent, evt.sender, "bridge"):
return await evt.reply(f"You do not have the permissions to synchronize this room.")
await portal.main_intent.get_joined_members(portal.mxid)
await evt.reply("Synchronization complete")
@command_handler(
needs_admin=False, needs_puppeting=False, needs_auth=False, help_section=SECTION_MISC
)
async def sync_full(evt: CommandEvent) -> EventID:
portal = await po.Portal.get_by_mxid(evt.room_id)
if not portal:
return await evt.reply("This is not a portal room.")
if len(evt.args) > 0 and evt.args[0] == "--usebot" and evt.sender.is_admin:
src = evt.tgbot
else:
src = evt.tgbot if await evt.sender.needs_relaybot(portal) else evt.sender
try:
if portal.peer_type == "channel":
res = await src.client(GetFullChannelRequest(portal.peer))
elif portal.peer_type == "chat":
res = await src.client(GetFullChatRequest(portal.tgid))
else:
return await evt.reply("This is not a channel or chat portal.")
except (ValueError, RPCError):
return await evt.reply("Failed to get portal info from Telegram.")
await portal.update_matrix_room(src, res.full_chat)
return await evt.reply("Portal synced successfully.")
@command_handler(
name="id",
needs_admin=False,
needs_puppeting=False,
needs_auth=False,
help_section=SECTION_MISC,
help_text="Get the ID of the Telegram chat where this room is bridged.",
)
async def get_id(evt: CommandEvent) -> EventID:
portal = await po.Portal.get_by_mxid(evt.room_id)
if not portal:
return await evt.reply("This is not a portal room.")
tgid = portal.tgid
if portal.peer_type == "chat":
tgid = -tgid
elif portal.peer_type == "channel":
tgid = f"-100{tgid}"
await evt.reply(f"This room is bridged to Telegram chat ID `{tgid}`.")
invite_link_usage = (
"**Usage:** `$cmdprefix+sp invite-link [--uses=<amount>] [--expire=<delta>]`"
"\n\n"
"* `--uses`: the number of times the invite link can be used."
" Defaults to unlimited.\n"
"* `--expire`: the duration after which the link will expire."
" A number suffixed with d(ay), h(our), m(inute) or s(econd)"
)
def _parse_flag(args: list[str]) -> tuple[str, str]:
arg = args.pop(0).lower()
if arg.startswith("--"):
value_start = arg.index("=")
if value_start:
flag = arg[2:value_start]
value = arg[value_start + 1 :]
else:
flag = arg[2:]
value = args.pop(0).lower()
elif arg.startswith("-"):
flag = arg[1]
if len(arg) > 3 and arg[2] == "=":
value = arg[3:]
else:
value = args.pop(0).lower()
else:
raise ValueError("invalid flag")
return flag, value
delta_regex = re.compile(
"([0-9]+)(w(?:eek)?|d(?:ay)?|h(?:our)?|m(?:in(?:ute)?)?|s(?:ec(?:ond)?)?)"
)
def _parse_delta(value: str) -> timedelta | None:
match = delta_regex.fullmatch(value)
if not match:
return None
number = int(match.group(1))
unit = match.group(2)[0]
if unit == "w":
return timedelta(weeks=number)
elif unit == "d":
return timedelta(days=number)
elif unit == "h":
return timedelta(hours=number)
elif unit == "m":
return timedelta(minutes=number)
elif unit == "s":
return timedelta(seconds=number)
else:
return None
@command_handler(
help_section=SECTION_PORTAL_MANAGEMENT,
help_text="Get a Telegram invite link to the current chat.",
help_args="[--uses=<amount>] [--expire=<time delta, e.g. 1d>]",
)
async def invite_link(evt: CommandEvent) -> EventID:
# TODO once we switch to Python 3.9 minimum, use argparse with exit_on_error=False
uses = None
expire = None
while evt.args:
try:
flag, value = _parse_flag(evt.args)
except (ValueError, IndexError):
return await evt.reply(invite_link_usage)
if flag in ("uses", "u"):
try:
uses = int(value)
except ValueError:
await evt.reply("The number of uses must be an integer")
elif flag in ("expire", "e"):
expire_delta = _parse_delta(value)
if not expire_delta:
await evt.reply("Invalid format for expiry time delta")
expire = datetime.now() + expire_delta
portal = await po.Portal.get_by_mxid(evt.room_id)
if not portal:
return await evt.reply("This is not a portal room.")
if portal.peer_type == "user":
return await evt.reply("You can't invite users to private chats.")
try:
link = await portal.get_invite_link(evt.sender, uses=uses, expire=expire)
return await evt.reply(f"Invite link to {portal.title}: {link}")
except ValueError as e:
return await evt.reply(e.args[0])
except ChatAdminRequiredError:
return await evt.reply("You don't have the permission to create an invite link.")
@command_handler(
help_section=SECTION_PORTAL_MANAGEMENT,
help_text="Upgrade a normal Telegram group to a supergroup.",
)
async def upgrade(evt: CommandEvent) -> EventID:
portal = await po.Portal.get_by_mxid(evt.room_id)
if not portal:
return await evt.reply("This is not a portal room.")
elif portal.peer_type == "channel":
return await evt.reply("This is already a supergroup or a channel.")
elif portal.peer_type == "user":
return await evt.reply("You can't upgrade private chats.")
try:
await portal.upgrade_telegram_chat(evt.sender)
return await evt.reply(f"Group upgraded to supergroup. New ID: -100{portal.tgid}")
except ChatAdminRequiredError:
return await evt.reply("You don't have the permission to upgrade this group.")
except ValueError as e:
return await evt.reply(e.args[0])
@command_handler(
help_section=SECTION_PORTAL_MANAGEMENT,
help_args="<_name_|`-`>",
help_text=(
"Change the username of a supergroup/channel. To disable, use a dash (`-`) as the name."
),
)
async def group_name(evt: CommandEvent) -> EventID:
if len(evt.args) == 0:
return await evt.reply("**Usage:** `$cmdprefix+sp group-name <name/->`")
portal = await po.Portal.get_by_mxid(evt.room_id)
if not portal:
return await evt.reply("This is not a portal room.")
elif portal.peer_type != "channel":
return await evt.reply("Only channels and supergroups have usernames.")
try:
await portal.set_telegram_username(evt.sender, evt.args[0] if evt.args[0] != "-" else "")
if portal.username:
return await evt.reply(f"Username of channel changed to {portal.username}.")
else:
return await evt.reply(f"Channel is now private.")
except ChatAdminRequiredError:
return await evt.reply(
"You don't have the permission to set the username of this channel."
)
except UsernameNotModifiedError:
if portal.username:
return await evt.reply("That is already the username of this channel.")
else:
return await evt.reply("This channel is already private")
except UsernameOccupiedError:
return await evt.reply("That username is already in use.")
except UsernameInvalidError:
return await evt.reply("Invalid username")
@@ -1,118 +0,0 @@
# mautrix-telegram - A Matrix-Telegram puppeting bridge
# Copyright (C) 2021 Tulir Asokan
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
from __future__ import annotations
from typing import Callable
from mautrix.types import EventID, RoomID
from ... import portal as po
from .. import SECTION_PORTAL_MANAGEMENT, CommandEvent, command_handler
from .util import user_has_power_level
async def _get_portal_and_check_permission(evt: CommandEvent) -> po.Portal | None:
room_id = RoomID(evt.args[0]) if len(evt.args) > 0 else evt.room_id
portal = await po.Portal.get_by_mxid(room_id)
if not portal:
that_this = "This" if room_id == evt.room_id else "That"
await evt.reply(f"{that_this} is not a portal room.")
return None
if portal.peer_type == "user":
if portal.tg_receiver != evt.sender.tgid:
await evt.reply("You do not have the permissions to unbridge that portal.")
return None
return portal
if not await user_has_power_level(portal.mxid, evt.az.intent, evt.sender, "unbridge"):
await evt.reply("You do not have the permissions to unbridge that portal.")
return None
return portal
def _get_portal_murder_function(
action: str, room_id: str, function: Callable, command: str, completed_message: str
) -> dict:
async def post_confirm(confirm) -> EventID | None:
confirm.sender.command_status = None
if len(confirm.args) > 0 and confirm.args[0] == f"confirm-{command}":
await function()
if confirm.room_id != room_id:
return await confirm.reply(completed_message)
else:
return await confirm.reply(f"{action} cancelled.")
return None
return {
"next": post_confirm,
"action": action,
}
@command_handler(
needs_auth=False,
needs_puppeting=False,
help_section=SECTION_PORTAL_MANAGEMENT,
help_text=(
"Remove all users from the current portal room and forget the portal. "
"Only works for group chats; to delete a private chat portal, simply leave the room."
),
)
async def delete_portal(evt: CommandEvent) -> EventID | None:
portal = await _get_portal_and_check_permission(evt)
if not portal:
return None
evt.sender.command_status = _get_portal_murder_function(
"Portal deletion",
portal.mxid,
portal.cleanup_and_delete,
"delete",
"Portal successfully deleted.",
)
return await evt.reply(
"Please confirm deletion of portal "
f"[{portal.alias or portal.mxid}](https://matrix.to/#/{portal.mxid}) "
f'to Telegram chat "{portal.title}" '
"by typing `$cmdprefix+sp confirm-delete`"
"\n\n"
"**WARNING:** If the bridge bot has the power level to do so, **this "
"will kick ALL users** in the room. If you just want to remove the "
"bridge, use `$cmdprefix+sp unbridge` instead."
)
@command_handler(
needs_auth=False,
needs_puppeting=False,
help_section=SECTION_PORTAL_MANAGEMENT,
help_text="Remove puppets from the current portal room and forget the portal.",
)
async def unbridge(evt: CommandEvent) -> EventID | None:
portal = await _get_portal_and_check_permission(evt)
if not portal:
return None
evt.sender.command_status = _get_portal_murder_function(
"Room unbridging", portal.mxid, portal.unbridge, "unbridge", "Room successfully unbridged."
)
return await evt.reply(
f'Please confirm unbridging chat "{portal.title}" from room '
f"[{portal.alias or portal.mxid}](https://matrix.to/#/{portal.mxid}) "
"by typing `$cmdprefix+sp confirm-unbridge`"
)
-72
View File
@@ -1,72 +0,0 @@
# mautrix-telegram - A Matrix-Telegram puppeting bridge
# Copyright (C) 2021 Tulir Asokan
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
from __future__ import annotations
from mautrix.appservice import IntentAPI
from mautrix.errors import MatrixRequestError
from mautrix.types import EventType, PowerLevelStateEventContent, RoomID
from ... import user as u
from .. import CommandEvent
async def get_initial_state(
intent: IntentAPI, room_id: RoomID
) -> tuple[str | None, str | None, PowerLevelStateEventContent | None, bool]:
state = await intent.get_state(room_id)
title: str | None = None
about: str | None = None
levels: PowerLevelStateEventContent | None = None
encrypted: bool = False
for event in state:
try:
if event.type == EventType.ROOM_NAME:
title = event.content.name
elif event.type == EventType.ROOM_TOPIC:
about = event.content.topic
elif event.type == EventType.ROOM_POWER_LEVELS:
levels = event.content
elif event.type == EventType.ROOM_CANONICAL_ALIAS:
title = title or event.content.canonical_alias
elif event.type == EventType.ROOM_ENCRYPTION:
encrypted = True
except KeyError:
# Some state event probably has empty content
pass
return title, about, levels, encrypted
async def warn_missing_power(levels: PowerLevelStateEventContent, evt: CommandEvent) -> None:
if levels.get_user_level(evt.az.bot_mxid) < levels.redact:
await evt.reply(
"Warning: The bot does not have privileges to redact messages on Matrix. "
"Message deletions from Telegram will not be bridged unless you give "
f"redaction permissions to [{evt.az.bot_mxid}](https://matrix.to/#/{evt.az.bot_mxid})"
)
async def user_has_power_level(
room_id: RoomID, intent: IntentAPI, sender: u.User, event: str
) -> bool:
if sender.is_admin:
return True
# Make sure the state store contains the power levels.
try:
await intent.get_power_levels(room_id)
except MatrixRequestError:
return False
event_type = EventType.find(f"net.maunium.telegram.{event}", t_class=EventType.Class.STATE)
return await intent.state_store.has_power_level(room_id, sender.mxid, event_type)
@@ -1 +0,0 @@
from . import account, auth, misc
@@ -1,173 +0,0 @@
# mautrix-telegram - A Matrix-Telegram puppeting bridge
# Copyright (C) 2021 Tulir Asokan
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
from __future__ import annotations
from telethon.errors import (
AboutTooLongError,
AuthKeyError,
FirstNameInvalidError,
HashInvalidError,
UsernameInvalidError,
UsernameNotModifiedError,
UsernameOccupiedError,
)
from telethon.tl.functions.account import (
GetAuthorizationsRequest,
ResetAuthorizationRequest,
UpdateProfileRequest,
UpdateUsernameRequest,
)
from telethon.tl.types import Authorization
from mautrix.types import EventID
from .. import SECTION_AUTH, CommandEvent, command_handler
@command_handler(
needs_auth=True,
help_section=SECTION_AUTH,
help_args="<_new username_>",
help_text="Change your Telegram username.",
)
async def username(evt: CommandEvent) -> EventID:
if len(evt.args) == 0:
return await evt.reply("**Usage:** `$cmdprefix+sp username <new username>`")
if evt.sender.is_bot:
return await evt.reply("Bots can't set their own username.")
new_name = evt.args[0]
if new_name == "-":
new_name = ""
try:
await evt.sender.client(UpdateUsernameRequest(username=new_name))
except UsernameInvalidError:
return await evt.reply(
"Invalid username. Usernames must be between 5 and 30 alphanumeric characters."
)
except UsernameNotModifiedError:
return await evt.reply("That is your current username.")
except UsernameOccupiedError:
return await evt.reply("That username is already in use.")
await evt.sender.update_info()
if not evt.sender.tg_username:
await evt.reply("Username removed")
else:
await evt.reply(f"Username changed to {evt.sender.tg_username}")
@command_handler(
needs_auth=True,
help_section=SECTION_AUTH,
help_args="<_new about_>",
help_text="Change your Telegram about section.",
)
async def about(evt: CommandEvent) -> EventID:
if len(evt.args) == 0:
return await evt.reply("**Usage:** `$cmdprefix+sp about <new about>`")
if evt.sender.is_bot:
return await evt.reply("Bots can't set their own about section.")
new_about = " ".join(evt.args)
if new_about == "-":
new_about = ""
try:
await evt.sender.client(UpdateProfileRequest(about=new_about))
except AboutTooLongError:
return await evt.reply("The provided about section is too long")
return await evt.reply("About section updated")
@command_handler(
needs_auth=True,
help_section=SECTION_AUTH,
help_args="<_new displayname_>",
help_text="Change your Telegram displayname.",
)
async def displayname(evt: CommandEvent) -> EventID:
if len(evt.args) == 0:
return await evt.reply("**Usage:** `$cmdprefix+sp displayname <new displayname>`")
if evt.sender.is_bot:
return await evt.reply("Bots can't set their own displayname.")
first_name, last_name = (
(evt.args[0], "") if len(evt.args) == 1 else (" ".join(evt.args[:-1]), evt.args[-1])
)
try:
await evt.sender.client(UpdateProfileRequest(first_name=first_name, last_name=last_name))
except FirstNameInvalidError:
return await evt.reply("Invalid first name")
await evt.sender.update_info()
return await evt.reply("Displayname updated")
def _format_session(sess: Authorization) -> str:
return (
f"**{sess.app_name} {sess.app_version}** \n"
f" **Platform:** {sess.device_model} {sess.platform} {sess.system_version} \n"
f" **Active:** {sess.date_active} (created {sess.date_created}) \n"
f" **From:** {sess.ip} - {sess.region}, {sess.country}"
)
@command_handler(
needs_auth=True,
help_section=SECTION_AUTH,
help_args="<`list`|`terminate`> [_hash_]",
help_text="View or delete other Telegram sessions.",
)
async def session(evt: CommandEvent) -> EventID:
if len(evt.args) == 0:
return await evt.reply("**Usage:** `$cmdprefix+sp session <list|terminate> [hash]`")
elif evt.sender.is_bot:
return await evt.reply("Bots can't manage their sessions")
cmd = evt.args[0].lower()
if cmd == "list":
res = await evt.sender.client(GetAuthorizationsRequest())
session_list = res.authorizations
current = [s for s in session_list if s.current][0]
current_text = _format_session(current)
other_text = "\n".join(
f"* {_format_session(sess)} \n **Hash:** {sess.hash}"
for sess in session_list
if not sess.current
)
return await evt.reply(
f"### Current session\n"
f"{current_text}\n"
f"\n"
f"### Other active sessions\n"
f"{other_text}"
)
elif cmd == "terminate" and len(evt.args) > 1:
try:
session_hash = int(evt.args[1])
except ValueError:
return await evt.reply("Hash must be an integer")
try:
ok = await evt.sender.client(ResetAuthorizationRequest(hash=session_hash))
except HashInvalidError:
return await evt.reply("Invalid session hash.")
except AuthKeyError as e:
if e.message == "FRESH_RESET_AUTHORISATION_FORBIDDEN":
return await evt.reply(
"New sessions can't terminate other sessions. Please wait a while."
)
raise
if ok:
return await evt.reply("Session terminated successfully.")
else:
return await evt.reply("Session not found.")
else:
return await evt.reply("**Usage:** `$cmdprefix+sp session <list|terminate> [hash]`")
-433
View File
@@ -1,433 +0,0 @@
# mautrix-telegram - A Matrix-Telegram puppeting bridge
# Copyright (C) 2021 Tulir Asokan
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
from __future__ import annotations
from typing import Any
import asyncio
import io
from telethon.errors import (
AccessTokenExpiredError,
AccessTokenInvalidError,
FirstNameInvalidError,
FloodWaitError,
PasswordHashInvalidError,
PhoneCodeExpiredError,
PhoneCodeInvalidError,
PhoneNumberAppSignupForbiddenError,
PhoneNumberBannedError,
PhoneNumberFloodError,
PhoneNumberInvalidError,
PhoneNumberOccupiedError,
PhoneNumberUnoccupiedError,
SessionPasswordNeededError,
)
from telethon.tl.types import User
from mautrix.types import (
EventID,
ImageInfo,
MediaMessageEventContent,
MessageType,
TextMessageEventContent,
UserID,
)
from mautrix.util.format_duration import format_duration as fmt_duration
from ... import user as u
from ...commands import SECTION_AUTH, CommandEvent, command_handler
from ...types import TelegramID
try:
from telethon.tl.custom import QRLogin
import PIL as _
import qrcode
except ImportError:
qrcode = None
QRLogin = None
@command_handler(
needs_auth=False, help_section=SECTION_AUTH, help_text="Check if you're logged into Telegram."
)
async def ping(evt: CommandEvent) -> EventID:
if await evt.sender.is_logged_in():
me = await evt.sender.get_me()
if me:
human_tg_id = f"@{me.username}" if me.username else f"+{me.phone}"
return await evt.reply(f"You're logged in as {human_tg_id}")
else:
return await evt.reply("You were logged in, but there appears to have been an error.")
else:
return await evt.reply("You're not logged in.")
@command_handler(
needs_auth=False,
needs_puppeting=False,
help_section=SECTION_AUTH,
help_text="Get the info of the message relay Telegram bot.",
)
async def ping_bot(evt: CommandEvent) -> EventID:
if not evt.tgbot:
return await evt.reply("Telegram message relay bot not configured.")
info, mxid = await evt.tgbot.get_me(use_cache=False)
return await evt.reply(
"Telegram message relay bot is active: "
f"[{info.first_name}](https://matrix.to/#/{mxid}) (ID {info.id})\n\n"
"To use the bot, simply invite it to a portal room."
)
@command_handler(
needs_auth=False,
management_only=True,
help_section=SECTION_AUTH,
help_args="<_phone_> <_full name_>",
help_text="Register to Telegram",
)
async def register(evt: CommandEvent) -> EventID:
if await evt.sender.is_logged_in():
return await evt.reply("You are already logged in.")
elif len(evt.args) < 1:
return await evt.reply("**Usage:** `$cmdprefix+sp register <phone> <full name>`")
phone_number = evt.args[0]
if len(evt.args) == 2:
full_name = evt.args[1], ""
else:
full_name = " ".join(evt.args[1:-1]), evt.args[-1]
await _request_code(
evt,
phone_number,
{
"next": enter_code_register,
"action": "Register",
"full_name": full_name,
},
)
return await evt.reply(
"By signing up for Telegram, you agree to "
"the terms of service: https://telegram.org/tos"
)
async def enter_code_register(evt: CommandEvent) -> EventID:
if len(evt.args) == 0:
return await evt.reply("**Usage:** `$cmdprefix+sp <code>`")
try:
await evt.sender.ensure_started(even_if_no_session=True)
first_name, last_name = evt.sender.command_status["full_name"]
user = await evt.sender.client.sign_up(evt.args[0], first_name, last_name)
asyncio.create_task(evt.sender.post_login(user, first_login=True))
evt.sender.command_status = None
return await evt.reply(f"Successfully registered to Telegram.")
except PhoneNumberOccupiedError:
return await evt.reply(
"That phone number has already been registered. "
"You can log in with `$cmdprefix+sp login`."
)
except FirstNameInvalidError:
return await evt.reply("Invalid name. Please set a Matrix displayname before registering.")
except PhoneCodeExpiredError:
return await evt.reply(
"Phone code expired. Try again with `$cmdprefix+sp register <phone>`."
)
except PhoneCodeInvalidError:
return await evt.reply("Invalid phone code.")
except Exception:
evt.log.exception("Error sending phone code")
return await evt.reply(
"Unhandled exception while sending code. Check console for more details."
)
@command_handler(
needs_auth=False,
management_only=True,
help_section=SECTION_AUTH,
help_text="Log in by scanning a QR code.",
)
async def login_qr(evt: CommandEvent) -> EventID:
login_as = evt.sender
if len(evt.args) > 0 and evt.sender.is_admin:
login_as = await u.User.get_by_mxid(UserID(evt.args[0]))
if not qrcode or not QRLogin:
return await evt.reply("This bridge instance does not support logging in with a QR code.")
if await login_as.is_logged_in():
return await evt.reply(f"You are already logged in as {login_as.human_tg_id}.")
await login_as.ensure_started(even_if_no_session=True)
qr_login = QRLogin(login_as.client, ignored_ids=[])
qr_event_id: EventID | None = None
async def upload_qr() -> None:
nonlocal qr_event_id
buffer = io.BytesIO()
image = qrcode.make(qr_login.url)
size = image.pixel_size
image.save(buffer, "PNG")
qr = buffer.getvalue()
mxc = await evt.az.intent.upload_media(qr, "image/png", "login-qr.png", len(qr))
content = MediaMessageEventContent(
body=qr_login.url,
url=mxc,
msgtype=MessageType.IMAGE,
info=ImageInfo(mimetype="image/png", size=len(qr), width=size, height=size),
)
if qr_event_id:
content.set_edit(qr_event_id)
await evt.az.intent.send_message(evt.room_id, content)
else:
content.set_reply(evt.event_id)
qr_event_id = await evt.az.intent.send_message(evt.room_id, content)
retries = 4
while retries > 0:
await qr_login.recreate()
await upload_qr()
try:
user = await qr_login.wait()
break
except asyncio.TimeoutError:
retries -= 1
except SessionPasswordNeededError:
evt.sender.command_status = {
"next": enter_password,
"login_as": login_as if login_as != evt.sender else None,
"action": "Login (password entry)",
}
return await evt.reply(
"Your account has two-factor authentication. Please send your password here."
)
else:
timeout = TextMessageEventContent(body="Login timed out", msgtype=MessageType.TEXT)
timeout.set_edit(qr_event_id)
return await evt.az.intent.send_message(evt.room_id, timeout)
return await _finish_sign_in(evt, user, login_as=login_as)
@command_handler(
needs_auth=False,
management_only=True,
help_section=SECTION_AUTH,
help_text="Get instructions on how to log in.",
)
async def login(evt: CommandEvent) -> EventID:
override_sender = False
if len(evt.args) > 0 and evt.sender.is_admin:
evt.sender = await u.User.get_and_start_by_mxid(UserID(evt.args[0]))
override_sender = True
if await evt.sender.is_logged_in():
return await evt.reply(f"You are already logged in as {evt.sender.human_tg_id}.")
allow_matrix_login = evt.config["bridge.allow_matrix_login"]
if allow_matrix_login and not override_sender:
evt.sender.command_status = {
"next": enter_phone_or_token,
"action": "Login",
}
nb = "**N.B. Logging in grants the bridge full access to your Telegram account.**"
if evt.config["appservice.public.enabled"]:
prefix = evt.config["appservice.public.external"]
url = f"{prefix}/login?token={evt.public_website.make_token(evt.sender.mxid, '/login')}"
if override_sender:
return await evt.reply(
f"[Click here to log in]({url}) as "
f"[{evt.sender.mxid}](https://matrix.to/#/{evt.sender.mxid})."
)
elif allow_matrix_login:
return await evt.reply(
f"[Click here to log in]({url}). Alternatively, send your phone"
f" number (or bot auth token) here to log in.\n\n{nb}"
)
return await evt.reply(f"[Click here to log in]({url}).\n\n{nb}")
elif allow_matrix_login:
if override_sender:
return await evt.reply(
"This bridge instance does not allow you to log in outside of Matrix. "
"Logging in as another user inside Matrix is not currently possible."
)
return await evt.reply(
"Please send your phone number (or bot auth token) here to start "
f"the login process.\n\n{nb}"
)
return await evt.reply("This bridge instance has been configured to not allow logging in.")
async def _request_code(
evt: CommandEvent, phone_number: str, next_status: dict[str, Any]
) -> EventID:
ok = False
try:
await evt.sender.ensure_started(even_if_no_session=True)
await evt.sender.client.sign_in(phone_number)
ok = True
return await evt.reply(f"Login code sent to {phone_number}. Please send the code here.")
except PhoneNumberAppSignupForbiddenError:
return await evt.reply("Your phone number does not allow 3rd party apps to sign in.")
except PhoneNumberFloodError:
return await evt.reply(
"Your phone number has been temporarily blocked for flooding. "
"The ban is usually applied for around a day."
)
except FloodWaitError as e:
return await evt.reply(
"Your phone number has been temporarily blocked for flooding. "
f"Please wait for {fmt_duration(e.seconds)} before trying again."
)
except PhoneNumberBannedError:
return await evt.reply("Your phone number has been banned from Telegram.")
except PhoneNumberUnoccupiedError:
return await evt.reply(
"That phone number has not been registered. "
"Please register with `$cmdprefix+sp register <phone>`."
)
except PhoneNumberInvalidError:
return await evt.reply("That phone number is not valid.")
except Exception:
evt.log.exception("Error requesting phone code")
return await evt.reply(
"Unhandled exception while requesting code. Check console for more details."
)
finally:
evt.sender.command_status = next_status if ok else None
@command_handler(needs_auth=False)
async def enter_phone_or_token(evt: CommandEvent) -> EventID | None:
if len(evt.args) == 0:
return await evt.reply("**Usage:** `$cmdprefix+sp enter-phone-or-token <phone-or-token>`")
elif not evt.config.get("bridge.allow_matrix_login", True):
return await evt.reply(
"This bridge instance does not allow in-Matrix login. "
"Please use `$cmdprefix+sp login` to get login instructions"
)
# phone numbers don't contain colons but telegram bot auth tokens do
if evt.args[0].find(":") > 0:
try:
await _sign_in(evt, bot_token=evt.args[0])
except Exception:
evt.log.exception("Error sending auth token")
return await evt.reply(
"Unhandled exception while sending auth token. Check console for more details."
)
else:
await _request_code(evt, evt.args[0], {"next": enter_code, "action": "Login"})
return None
@command_handler(needs_auth=False)
async def enter_code(evt: CommandEvent) -> EventID | None:
if len(evt.args) == 0:
return await evt.reply("**Usage:** `$cmdprefix+sp enter-code <code>`")
elif not evt.config.get("bridge.allow_matrix_login", True):
return await evt.reply(
"This bridge instance does not allow in-Matrix login. "
"Please use `$cmdprefix+sp login` to get login instructions"
)
try:
await _sign_in(evt, code=evt.args[0])
except Exception:
evt.log.exception("Error sending phone code")
return await evt.reply(
"Unhandled exception while sending code. Check console for more details."
)
return None
@command_handler(needs_auth=False)
async def enter_password(evt: CommandEvent) -> EventID | None:
if len(evt.args) == 0:
return await evt.reply("**Usage:** `$cmdprefix+sp enter-password <password>`")
elif not evt.config.get("bridge.allow_matrix_login", True):
return await evt.reply(
"This bridge instance does not allow in-Matrix login. "
"Please use `$cmdprefix+sp login` to get login instructions"
)
try:
await _sign_in(
evt,
login_as=evt.sender.command_status.get("login_as", None),
password=" ".join(evt.args),
)
except AccessTokenInvalidError:
return await evt.reply("That bot token is not valid.")
except AccessTokenExpiredError:
return await evt.reply("That bot token has expired.")
except Exception:
evt.log.exception("Error sending password")
return await evt.reply(
"Unhandled exception while sending password. Check console for more details."
)
return None
async def _sign_in(evt: CommandEvent, login_as: u.User = None, **sign_in_info) -> EventID:
login_as = login_as or evt.sender
try:
await login_as.ensure_started(even_if_no_session=True)
user = await login_as.client.sign_in(**sign_in_info)
await _finish_sign_in(evt, user)
except PhoneCodeExpiredError:
return await evt.reply("Phone code expired. Try again with `$cmdprefix+sp login`.")
except PhoneCodeInvalidError:
return await evt.reply("Invalid phone code.")
except PasswordHashInvalidError:
return await evt.reply("Incorrect password.")
except SessionPasswordNeededError:
evt.sender.command_status = {
"next": enter_password,
"action": "Login (password entry)",
}
return await evt.reply(
"Your account has two-factor authentication. Please send your password here."
)
async def _finish_sign_in(evt: CommandEvent, user: User, login_as: u.User = None) -> EventID:
login_as = login_as or evt.sender
existing_user = await u.User.get_by_tgid(TelegramID(user.id))
if existing_user and existing_user != login_as:
await existing_user.log_out()
await evt.reply(
f"[{existing_user.displayname}] (https://matrix.to/#/{existing_user.mxid})"
" was logged out from the account."
)
asyncio.create_task(login_as.post_login(user, first_login=True))
evt.sender.command_status = None
name = f"@{user.username}" if user.username else f"+{user.phone}"
if login_as != evt.sender:
msg = (
f"Successfully logged in [{login_as.mxid}](https://matrix.to/#/{login_as.mxid})"
f" as {name}"
)
else:
msg = f"Successfully logged in as {name}"
return await evt.reply(msg)
@command_handler(needs_auth=False, help_section=SECTION_AUTH, help_text="Log out from Telegram.")
async def logout(evt: CommandEvent) -> EventID:
if not evt.sender.tgid:
return await evt.reply("You're not logged in")
if await evt.sender.log_out():
return await evt.reply("Logged out successfully.")
return await evt.reply("Failed to log out.")
-440
View File
@@ -1,440 +0,0 @@
# mautrix-telegram - A Matrix-Telegram puppeting bridge
# Copyright (C) 2021 Tulir Asokan
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
from __future__ import annotations
from typing import cast
import base64
import codecs
import re
from aiohttp import ClientSession, InvalidURL
from telethon.errors import (
ChatIdInvalidError,
EmoticonInvalidError,
InviteHashExpiredError,
InviteHashInvalidError,
OptionsTooMuchError,
TakeoutInitDelayError,
UserAlreadyParticipantError,
)
from telethon.tl.functions.channels import JoinChannelRequest
from telethon.tl.functions.messages import (
CheckChatInviteRequest,
GetBotCallbackAnswerRequest,
ImportChatInviteRequest,
SendVoteRequest,
)
from telethon.tl.patched import Message
from telethon.tl.types import (
InputMediaDice,
MessageMediaGame,
MessageMediaPoll,
TypeInputPeer,
TypeUpdates,
User as TLUser,
)
from telethon.tl.types.messages import BotCallbackAnswer
from mautrix.types import EventID, Format
from ... import portal as po, puppet as pu
from ...abstract_user import AbstractUser
from ...commands import (
SECTION_CREATING_PORTALS,
SECTION_MISC,
SECTION_PORTAL_MANAGEMENT,
CommandEvent,
command_handler,
)
from ...db import Message as DBMessage
from ...types import TelegramID
@command_handler(
needs_auth=False,
help_section=SECTION_MISC,
help_args="<_caption_>",
help_text="Set a caption for the next image you send",
)
async def caption(evt: CommandEvent) -> EventID:
if len(evt.args) == 0:
return await evt.reply("**Usage:** `$cmdprefix+sp caption <caption>`")
prefix = f"{evt.command_prefix} caption "
if evt.content.format == Format.HTML:
evt.content.formatted_body = evt.content.formatted_body.replace(prefix, "", 1)
evt.content.body = evt.content.body.replace(prefix, "", 1)
evt.sender.command_status = {"caption": evt.content, "action": "Caption"}
return await evt.reply(
"Your next image or file will be sent with that caption. "
"Use `$cmdprefix+sp cancel` to cancel the caption."
)
@command_handler(
help_section=SECTION_MISC,
help_args="[_-r|--remote_] <_query_>",
help_text="Search your contacts or the Telegram servers for users.",
)
async def search(evt: CommandEvent) -> EventID:
if len(evt.args) == 0:
return await evt.reply("**Usage:** `$cmdprefix+sp search [-r|--remote] <query>`")
force_remote = False
if evt.args[0] in {"-r", "--remote"}:
force_remote = True
evt.args.pop(0)
query = " ".join(evt.args)
if force_remote and len(query) < 5:
return await evt.reply("Minimum length of query for remote search is 5 characters.")
results, remote = await evt.sender.search(query, force_remote)
if not results:
if len(query) < 5 and remote:
return await evt.reply(
"No local results. Minimum length of remote query is 5 characters."
)
return await evt.reply("No results 3:")
reply: list[str] = []
if remote:
reply += ["**Results from Telegram server:**", ""]
else:
reply += ["**Results in contacts:**", ""]
reply += [
(
f"* [{puppet.displayname}](https://matrix.to/#/{puppet.mxid}): "
f"{puppet.id} ({similarity}% match)"
)
for puppet, similarity in results
]
# TODO somehow show remote channel results when joining by alias is possible?
return await evt.reply("\n".join(reply))
@command_handler(
help_section=SECTION_CREATING_PORTALS,
help_args="<_identifier_>",
help_text="Open a private chat with the given Telegram user. The identifier is "
"either the internal user ID, the username or the phone number. "
"**N.B.** The phone numbers you start chats with must already be in "
"your contacts.",
)
async def pm(evt: CommandEvent) -> EventID:
if len(evt.args) == 0:
return await evt.reply("**Usage:** `$cmdprefix+sp pm <user identifier>`")
try:
id = "".join(evt.args).translate({ord(c): None for c in "+()- "})
user = await evt.sender.client.get_entity(id)
except ValueError:
return await evt.reply("Invalid user identifier or user not found.")
if not user:
return await evt.reply("User not found.")
elif not isinstance(user, TLUser):
return await evt.reply("That doesn't seem to be a user.")
portal = await po.Portal.get_by_entity(user, tg_receiver=evt.sender.tgid)
await portal.create_matrix_room(evt.sender, user, [evt.sender.mxid])
displayname, _ = pu.Puppet.get_displayname(user, False)
return await evt.reply(f"Created private chat room with {displayname}")
async def _join(
evt: CommandEvent, identifier: str, link_type: str
) -> tuple[TypeUpdates | None, EventID | None]:
if link_type == "joinchat":
try:
await evt.sender.client(CheckChatInviteRequest(identifier))
except InviteHashInvalidError:
return None, await evt.reply("Invalid invite link.")
except InviteHashExpiredError:
return None, await evt.reply("Invite link expired.")
try:
return (await evt.sender.client(ImportChatInviteRequest(identifier))), None
except UserAlreadyParticipantError:
return None, await evt.reply("You are already in that chat.")
else:
channel = await evt.sender.client.get_entity(identifier)
if not channel:
return None, await evt.reply("Channel/supergroup not found.")
return await evt.sender.client(JoinChannelRequest(channel)), None
@command_handler(
help_section=SECTION_CREATING_PORTALS,
help_args="<_link_>",
help_text="Join a chat with an invite link.",
)
async def join(evt: CommandEvent) -> EventID | None:
if len(evt.args) == 0:
return await evt.reply("**Usage:** `$cmdprefix+sp join <invite link>`")
url = evt.args[0]
if evt.config["bridge.invite_link_resolve"]:
try:
async with ClientSession() as sess, sess.get(url) as resp:
url = str(resp.url)
except InvalidURL:
return await evt.reply("That doesn't look like a Telegram invite link.")
regex = re.compile(
r"(?:https?://)?t(?:elegram)?\.(?:dog|me)(?:/(?P<type>joinchat|s))?/(?P<id>[^/]+)/?",
flags=re.IGNORECASE,
)
arg = regex.match(url)
if not arg:
return await evt.reply("That doesn't look like a Telegram invite link.")
data = arg.groupdict()
identifier = data["id"]
link_type = data["type"]
if link_type:
link_type = link_type.lower()
updates, _ = await _join(evt, identifier, link_type)
if not updates:
return None
for chat in updates.chats:
portal = await po.Portal.get_by_entity(chat)
if portal.mxid:
await portal.invite_to_matrix([evt.sender.mxid])
return await evt.reply(f"Invited you to portal of {portal.title}")
else:
await evt.reply(f"Creating room for {chat.title}... This might take a while.")
try:
await portal.create_matrix_room(evt.sender, chat, [evt.sender.mxid])
except ChatIdInvalidError as e:
evt.log.trace(
"ChatIdInvalidError while creating portal from !tg join command: %s",
updates.stringify(),
)
raise e
return await evt.reply(f"Created room for {portal.title}")
return None
@command_handler(
help_section=SECTION_MISC,
help_args="[`chats`|`contacts`|`me`]",
help_text="Synchronize your chat portals, contacts and/or own info.",
)
async def sync(evt: CommandEvent) -> EventID:
if len(evt.args) > 0:
sync_only = evt.args[0]
if sync_only not in ("chats", "contacts", "me"):
return await evt.reply("**Usage:** `$cmdprefix+sp sync [chats|contacts|me]`")
else:
sync_only = None
if not sync_only or sync_only == "chats":
await evt.reply("Synchronizing chats...")
await evt.sender.sync_dialogs()
if not sync_only or sync_only == "contacts":
await evt.reply("Synchronizing contacts...")
await evt.sender.sync_contacts()
if not sync_only or sync_only == "me":
await evt.sender.update_info()
return await evt.reply("Synchronization complete.")
PEER_TYPE_CHAT = b"g"
class MessageIDError(ValueError):
def __init__(self, message: str) -> None:
super().__init__(message)
self.message = message
async def _parse_encoded_msgid(
user: AbstractUser, enc_id: str, type_name: str
) -> tuple[TypeInputPeer, Message]:
try:
enc_id += (4 - len(enc_id) % 4) * "="
enc_id = base64.b64decode(enc_id)
peer_type, enc_id = bytes([enc_id[0]]), enc_id[1:]
tgid = TelegramID(int(codecs.encode(enc_id[0:5], "hex_codec"), 16))
msg_id = TelegramID(int(codecs.encode(enc_id[5:10], "hex_codec"), 16))
space = None
if peer_type == PEER_TYPE_CHAT:
space = TelegramID(int(codecs.encode(enc_id[10:15], "hex_codec"), 16))
except ValueError as e:
raise MessageIDError(f"Invalid {type_name} ID (format)") from e
if peer_type == PEER_TYPE_CHAT:
orig_msg = await DBMessage.get_one_by_tgid(msg_id, space)
if not orig_msg:
raise MessageIDError(f"Invalid {type_name} ID (original message not found in db)")
new_msg = await DBMessage.get_by_mxid(orig_msg.mxid, orig_msg.mx_room, user.tgid)
if not new_msg:
raise MessageIDError(f"Invalid {type_name} ID (your copy of message not found in db)")
msg_id = new_msg.tgid
try:
peer = await user.client.get_input_entity(tgid)
except ValueError as e:
raise MessageIDError(f"Invalid {type_name} ID (chat not found)") from e
msg = await user.client.get_messages(entity=peer, ids=msg_id)
if not msg:
raise MessageIDError(f"Invalid {type_name} ID (message not found)")
return peer, cast(Message, msg)
@command_handler(
help_section=SECTION_MISC, help_args="<_play ID_>", help_text="Play a Telegram game."
)
async def play(evt: CommandEvent) -> EventID:
if len(evt.args) < 1:
return await evt.reply("**Usage:** `$cmdprefix+sp play <play ID>`")
elif not await evt.sender.is_logged_in():
return await evt.reply("You must be logged in with a real account to play games.")
elif evt.sender.is_bot:
return await evt.reply("Bots can't play games :(")
try:
peer, msg = await _parse_encoded_msgid(evt.sender, evt.args[0], type_name="play")
except MessageIDError as e:
return await evt.reply(e.message)
if not isinstance(msg.media, MessageMediaGame):
return await evt.reply("Invalid play ID (message doesn't look like a game)")
game = await evt.sender.client(
GetBotCallbackAnswerRequest(peer=peer, msg_id=msg.id, game=True)
)
if not isinstance(game, BotCallbackAnswer):
return await evt.reply("Game request response invalid")
return await evt.reply(
f"Click [here]({game.url}) to play {msg.media.game.title}:\n\n"
f"{msg.media.game.description}"
)
@command_handler(
help_section=SECTION_MISC,
help_args="<_poll ID_> <_choice number_>",
help_text="Vote in a Telegram poll.",
)
async def vote(evt: CommandEvent) -> EventID | None:
if len(evt.args) < 1:
return await evt.reply("**Usage:** `$cmdprefix+sp vote <poll ID> <choice number>`")
elif not await evt.sender.is_logged_in():
return await evt.reply("You must be logged in with a real account to vote in polls.")
elif evt.sender.is_bot:
return await evt.reply("Bots can't vote in polls :(")
try:
peer, msg = await _parse_encoded_msgid(evt.sender, evt.args[0], type_name="poll")
except MessageIDError as e:
return await evt.reply(e.message)
if not isinstance(msg.media, MessageMediaPoll):
return await evt.reply("Invalid poll ID (message doesn't look like a poll)")
options = []
for option in evt.args[1:]:
try:
if len(option) > 10:
raise ValueError("option index too long")
option_index = int(option) - 1
except ValueError:
option_index = None
if option_index is None:
return await evt.reply(
f'Invalid option number "{option}"', render_markdown=False, allow_html=False
)
elif option_index < 0:
return await evt.reply(
f"Invalid option number {option}. Option numbers must be positive."
)
elif option_index >= len(msg.media.poll.answers):
return await evt.reply(
f"Invalid option number {option}. "
f"The poll only has {len(msg.media.poll.answers)} options."
)
options.append(msg.media.poll.answers[option_index].option)
options = [msg.media.poll.answers[int(option) - 1].option for option in evt.args[1:]]
try:
await evt.sender.client(SendVoteRequest(peer=peer, msg_id=msg.id, options=options))
except OptionsTooMuchError:
return await evt.reply("You passed too many options.")
# TODO use response
return await evt.mark_read()
@command_handler(
help_section=SECTION_MISC,
help_args="<_emoji_>",
help_text="Roll a dice (\U0001F3B2), kick a football (\u26BD\uFE0F) or throw a "
"dart (\U0001F3AF) or basketball (\U0001F3C0) on the Telegram servers.",
)
async def random(evt: CommandEvent) -> EventID:
if not evt.is_portal:
return await evt.reply("You can only randomize values in portal rooms")
portal = await po.Portal.get_by_mxid(evt.room_id)
arg = evt.args[0] if len(evt.args) > 0 else "dice"
emoticon = {
"dart": "\U0001F3AF",
"dice": "\U0001F3B2",
"ball": "\U0001F3C0",
"basketball": "\U0001F3C0",
"football": "\u26BD",
"soccer": "\u26BD",
}.get(arg, arg)
try:
await evt.sender.client.send_media(
await portal.get_input_entity(evt.sender), InputMediaDice(emoticon)
)
except EmoticonInvalidError:
return await evt.reply("Invalid emoji for randomization")
@command_handler(
help_section=SECTION_PORTAL_MANAGEMENT,
help_args="[_limit_]",
help_text="Backfill messages from Telegram history.",
)
async def backfill(evt: CommandEvent) -> None:
if not evt.is_portal:
await evt.reply("You can only use backfill in portal rooms")
return
try:
limit = int(evt.args[0])
except (ValueError, IndexError):
limit = -1
portal = await po.Portal.get_by_mxid(evt.room_id)
if not evt.config["bridge.backfill.normal_groups"] and portal.peer_type == "chat":
await evt.reply("Backfilling normal groups is disabled in the bridge config")
return
try:
await portal.backfill(evt.sender, limit=limit)
except TakeoutInitDelayError:
msg = (
"Please accept the data export request from a mobile device, "
"then re-run the backfill command."
)
if portal.peer_type == "user":
from mautrix.appservice import IntentAPI
await portal.main_intent.send_notice(evt.room_id, msg)
else:
await evt.reply(msg)
-272
View File
@@ -1,272 +0,0 @@
# mautrix-telegram - A Matrix-Telegram puppeting bridge
# Copyright (C) 2021 Tulir Asokan
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
from typing import Any, List, NamedTuple
import os
from ruamel.yaml.comments import CommentedMap
from mautrix.bridge.config import BaseBridgeConfig
from mautrix.client import Client
from mautrix.types import UserID
from mautrix.util.config import ConfigUpdateHelper, ForbiddenDefault, ForbiddenKey
Permissions = NamedTuple(
"Permissions",
relaybot=bool,
user=bool,
puppeting=bool,
matrix_puppeting=bool,
admin=bool,
level=str,
)
class Config(BaseBridgeConfig):
def __getitem__(self, key: str) -> Any:
try:
return os.environ[f"MAUTRIX_TELEGRAM_{key.replace('.', '_').upper()}"]
except KeyError:
return super().__getitem__(key)
@property
def forbidden_defaults(self) -> List[ForbiddenDefault]:
return [
*super().forbidden_defaults,
ForbiddenDefault(
"appservice.database",
"postgres://username:password@hostname/dbname",
),
ForbiddenDefault(
"appservice.public.external",
"https://example.com/public",
condition="appservice.public.enabled",
),
ForbiddenDefault("bridge.permissions", ForbiddenKey("example.com")),
ForbiddenDefault("telegram.api_id", 12345),
ForbiddenDefault("telegram.api_hash", "tjyd5yge35lbodk1xwzw2jstp90k55qz"),
]
def do_update(self, helper: ConfigUpdateHelper) -> None:
super().do_update(helper)
copy, copy_dict, base = helper
copy("homeserver.asmux")
if "appservice.protocol" in self and "appservice.address" not in self:
protocol, hostname, port = (
self["appservice.protocol"],
self["appservice.hostname"],
self["appservice.port"],
)
base["appservice.address"] = f"{protocol}://{hostname}:{port}"
if "appservice.debug" in self and "logging" not in self:
level = "DEBUG" if self["appservice.debug"] else "INFO"
base["logging.root.level"] = level
base["logging.loggers.mau.level"] = level
base["logging.loggers.telethon.level"] = level
copy("appservice.public.enabled")
copy("appservice.public.prefix")
copy("appservice.public.external")
copy("appservice.provisioning.enabled")
copy("appservice.provisioning.prefix")
copy("appservice.provisioning.shared_secret")
if base["appservice.provisioning.shared_secret"] == "generate":
base["appservice.provisioning.shared_secret"] = self._new_token()
if "pool_size" in base["appservice.database_opts"]:
pool_size = base["appservice.database_opts"].pop("pool_size")
base["appservice.database_opts.min_size"] = pool_size
base["appservice.database_opts.max_size"] = pool_size
if "pool_pre_ping" in base["appservice.database_opts"]:
del base["appservice.database_opts.pool_pre_ping"]
copy("appservice.community_id")
copy("metrics.enabled")
copy("metrics.listen_port")
copy("bridge.username_template")
copy("bridge.alias_template")
copy("bridge.displayname_template")
copy("bridge.displayname_preference")
copy("bridge.displayname_max_length")
copy("bridge.allow_avatar_remove")
copy("bridge.max_initial_member_sync")
copy("bridge.sync_channel_members")
copy("bridge.skip_deleted_members")
copy("bridge.startup_sync")
if "bridge.sync_dialog_limit" in self:
base["bridge.sync_create_limit"] = self["bridge.sync_dialog_limit"]
base["bridge.sync_update_limit"] = self["bridge.sync_dialog_limit"]
else:
copy("bridge.sync_update_limit")
copy("bridge.sync_create_limit")
copy("bridge.sync_direct_chats")
copy("bridge.max_telegram_delete")
copy("bridge.sync_matrix_state")
copy("bridge.allow_matrix_login")
copy("bridge.plaintext_highlights")
copy("bridge.public_portals")
copy("bridge.sync_with_custom_puppets")
copy("bridge.sync_direct_chat_list")
copy("bridge.double_puppet_server_map")
copy("bridge.double_puppet_allow_discovery")
if "bridge.login_shared_secret" in self:
base["bridge.login_shared_secret_map"] = {
base["homeserver.domain"]: self["bridge.login_shared_secret"]
}
else:
copy("bridge.login_shared_secret_map")
copy("bridge.telegram_link_preview")
copy("bridge.invite_link_resolve")
copy("bridge.inline_images")
copy("bridge.image_as_file_size")
copy("bridge.max_document_size")
copy("bridge.parallel_file_transfer")
copy("bridge.federate_rooms")
copy("bridge.animated_sticker.target")
copy("bridge.animated_sticker.args")
copy("bridge.encryption.allow")
copy("bridge.encryption.default")
copy("bridge.encryption.database")
copy("bridge.encryption.key_sharing.allow")
copy("bridge.encryption.key_sharing.require_cross_signing")
copy("bridge.encryption.key_sharing.require_verification")
copy("bridge.private_chat_portal_meta")
copy("bridge.delivery_receipts")
copy("bridge.delivery_error_reports")
copy("bridge.resend_bridge_info")
copy("bridge.mute_bridging")
copy("bridge.pinned_tag")
copy("bridge.archive_tag")
copy("bridge.tag_only_on_create")
copy("bridge.bridge_matrix_leave")
copy("bridge.kick_on_logout")
copy("bridge.backfill.invite_own_puppet")
copy("bridge.backfill.takeout_limit")
copy("bridge.backfill.initial_limit")
copy("bridge.backfill.missed_limit")
copy("bridge.backfill.disable_notifications")
copy("bridge.backfill.normal_groups")
copy("bridge.initial_power_level_overrides.group")
copy("bridge.initial_power_level_overrides.user")
copy("bridge.bot_messages_as_notices")
if isinstance(self["bridge.bridge_notices"], bool):
base["bridge.bridge_notices"] = {
"default": self["bridge.bridge_notices"],
"exceptions": ["@importantbot:example.com"],
}
else:
copy("bridge.bridge_notices")
copy("bridge.deduplication.pre_db_check")
copy("bridge.deduplication.cache_queue_length")
if "bridge.message_formats.m_text" in self:
del self["bridge.message_formats"]
copy_dict("bridge.message_formats", override_existing_map=False)
copy("bridge.emote_format")
copy("bridge.state_event_formats.join")
copy("bridge.state_event_formats.leave")
copy("bridge.state_event_formats.name_change")
copy("bridge.filter.mode")
copy("bridge.filter.list")
copy("bridge.command_prefix")
migrate_permissions = (
"bridge.permissions" not in self
or "bridge.whitelist" in self
or "bridge.admins" in self
)
if migrate_permissions:
permissions = self["bridge.permissions"] or CommentedMap()
for entry in self["bridge.whitelist"] or []:
permissions[entry] = "full"
for entry in self["bridge.admins"] or []:
permissions[entry] = "admin"
base["bridge.permissions"] = permissions
else:
copy_dict("bridge.permissions")
if "bridge.relaybot" not in self:
copy("bridge.authless_relaybot_portals", "bridge.relaybot.authless_portals")
else:
copy("bridge.relaybot.private_chat.invite")
copy("bridge.relaybot.private_chat.state_changes")
copy("bridge.relaybot.private_chat.message")
copy("bridge.relaybot.group_chat_invite")
copy("bridge.relaybot.ignore_unbridged_group_chat")
copy("bridge.relaybot.authless_portals")
copy("bridge.relaybot.whitelist_group_admins")
copy("bridge.relaybot.whitelist")
copy("bridge.relaybot.ignore_own_incoming_events")
copy("telegram.api_id")
copy("telegram.api_hash")
copy("telegram.bot_token")
copy("telegram.connection.timeout")
copy("telegram.connection.retries")
copy("telegram.connection.retry_delay")
copy("telegram.connection.flood_sleep_threshold")
copy("telegram.connection.request_retries")
copy("telegram.device_info.device_model")
copy("telegram.device_info.system_version")
copy("telegram.device_info.app_version")
copy("telegram.device_info.lang_code")
copy("telegram.device_info.system_lang_code")
copy("telegram.server.enabled")
copy("telegram.server.dc")
copy("telegram.server.ip")
copy("telegram.server.port")
copy("telegram.proxy.type")
copy("telegram.proxy.address")
copy("telegram.proxy.port")
copy("telegram.proxy.rdns")
copy("telegram.proxy.username")
copy("telegram.proxy.password")
def _get_permissions(self, key: str) -> Permissions:
level = self["bridge.permissions"].get(key, "")
admin = level == "admin"
matrix_puppeting = level == "full" or admin
puppeting = level == "puppeting" or matrix_puppeting
user = level == "user" or puppeting
relaybot = level == "relaybot" or user
return Permissions(relaybot, user, puppeting, matrix_puppeting, admin, level)
def get_permissions(self, mxid: UserID) -> Permissions:
permissions = self["bridge.permissions"]
if mxid in permissions:
return self._get_permissions(mxid)
_, homeserver = Client.parse_user_id(mxid)
if homeserver in permissions:
return self._get_permissions(homeserver)
return self._get_permissions("*")
-43
View File
@@ -1,43 +0,0 @@
# mautrix-telegram - A Matrix-Telegram puppeting bridge
# Copyright (C) 2021 Tulir Asokan
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
from mautrix.util.async_db import Database
from .bot_chat import BotChat
from .message import Message
from .portal import Portal
from .puppet import Puppet
from .telegram_file import TelegramFile
from .telethon_session import PgSession
from .upgrade import upgrade_table
from .user import User
def init(db: Database) -> None:
for table in (Portal, Message, User, Puppet, TelegramFile, BotChat, PgSession):
table.db = db
__all__ = [
"upgrade_table",
"init",
"Portal",
"Message",
"User",
"Puppet",
"TelegramFile",
"BotChat",
"PgSession",
]
-55
View File
@@ -1,55 +0,0 @@
# mautrix-telegram - A Matrix-Telegram puppeting bridge
# Copyright (C) 2021 Tulir Asokan
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
from __future__ import annotations
from typing import TYPE_CHECKING, ClassVar
from asyncpg import Record
from attr import dataclass
from mautrix.util.async_db import Database
from ..types import TelegramID
fake_db = Database.create("") if TYPE_CHECKING else None
# Fucking Telegram not telling bots what chats they are in 3:<
@dataclass
class BotChat:
db: ClassVar[Database] = fake_db
id: TelegramID
type: str
@classmethod
def _from_row(cls, row: Record | None) -> BotChat | None:
if row is None:
return None
return cls(**row)
@classmethod
async def delete_by_id(cls, chat_id: TelegramID) -> None:
await cls.db.execute("DELETE FROM bot_chat WHERE id=$1", chat_id)
@classmethod
async def all(cls) -> list[BotChat]:
rows = await cls.db.fetch("SELECT id, type FROM bot_chat")
return [cls._from_row(row) for row in rows]
async def insert(self) -> None:
q = "INSERT INTO bot_chat (id, type) VALUES ($1, $2)"
await self.db.execute(q, self.id, self.type)
-161
View File
@@ -1,161 +0,0 @@
# mautrix-telegram - A Matrix-Telegram puppeting bridge
# Copyright (C) 2021 Tulir Asokan
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
from __future__ import annotations
from typing import TYPE_CHECKING, ClassVar
from asyncpg import Record
from attr import dataclass
from mautrix.types import EventID, RoomID
from mautrix.util.async_db import Database
from ..types import TelegramID
fake_db = Database.create("") if TYPE_CHECKING else None
@dataclass
class Message:
db: ClassVar[Database] = fake_db
mxid: EventID
mx_room: RoomID
tgid: TelegramID
tg_space: TelegramID
edit_index: int
redacted: bool = False
@classmethod
def _from_row(cls, row: Record | None) -> Message | None:
if row is None:
return None
return cls(**row)
columns: ClassVar[str] = "mxid, mx_room, tgid, tg_space, edit_index, redacted"
@classmethod
async def get_all_by_tgid(cls, tgid: TelegramID, tg_space: TelegramID) -> list[Message]:
q = f"SELECT {cls.columns} FROM message WHERE tgid=$1 AND tg_space=$2"
rows = await cls.db.fetch(q, tgid, tg_space)
return [cls._from_row(row) for row in rows]
@classmethod
async def get_one_by_tgid(
cls, tgid: TelegramID, tg_space: TelegramID, edit_index: int = 0
) -> Message | None:
if edit_index < 0:
q = (
f"SELECT {cls.columns} FROM message WHERE tgid=$1 AND tg_space=$2 "
f"ORDER BY edit_index DESC LIMIT 1 OFFSET {-edit_index - 1}"
)
row = await cls.db.fetchrow(q, tgid, tg_space)
else:
q = (
f"SELECT {cls.columns} FROM message"
" WHERE tgid=$1 AND tg_space=$2 AND edit_index=$3"
)
row = await cls.db.fetchrow(q, tgid, tg_space, edit_index)
return cls._from_row(row)
@classmethod
async def get_first_by_tgids(
cls, tgids: list[TelegramID], tg_space: TelegramID
) -> list[Message]:
if cls.db.scheme == "postgres":
q = (
f"SELECT {cls.columns} FROM message"
" WHERE tgid=ANY($1) AND tg_space=$2 AND edit_index=0"
)
rows = await cls.db.fetch(q, tgids, tg_space)
else:
tgid_placeholders = ("?," * len(tgids)).rstrip(",")
q = (
f"SELECT {cls.columns} FROM message "
f"WHERE tg_space=? AND edit_index=0 AND tgid IN ({tgid_placeholders})"
)
rows = await cls.db.fetch(q, tg_space, *tgids)
return [cls._from_row(row) for row in rows]
@classmethod
async def count_spaces_by_mxid(cls, mxid: EventID, mx_room: RoomID) -> int:
return (
await cls.db.fetchval(
"SELECT COUNT(tg_space) FROM message WHERE mxid=$1 AND mx_room=$2", mxid, mx_room
)
or 0
)
@classmethod
async def find_last(cls, mx_room: RoomID, tg_space: TelegramID) -> Message | None:
q = (
f"SELECT {cls.columns} FROM message WHERE mx_room=$1 AND tg_space=$2 "
f"ORDER BY tgid DESC LIMIT 1"
)
return cls._from_row(await cls.db.fetchrow(q, mx_room, tg_space))
@classmethod
async def delete_all(cls, mx_room: RoomID) -> None:
await cls.db.execute("DELETE FROM message WHERE mx_room=$1", mx_room)
@classmethod
async def get_by_mxid(
cls, mxid: EventID, mx_room: RoomID, tg_space: TelegramID
) -> Message | None:
q = f"SELECT {cls.columns} FROM message WHERE mxid=$1 AND mx_room=$2 AND tg_space=$3"
return cls._from_row(await cls.db.fetchrow(q, mxid, mx_room, tg_space))
@classmethod
async def get_by_mxids(
cls, mxids: list[EventID], mx_room: RoomID, tg_space: TelegramID
) -> list[Message]:
if cls.db.scheme == "postgres":
q = (
f"SELECT {cls.columns} FROM message"
" WHERE mxid=ANY($1) AND mx_room=$2 AND tg_space=$3"
)
rows = await cls.db.fetch(q, mxids, mx_room, tg_space)
else:
mxid_placeholders = ("?," * len(mxids)).rstrip(",")
q = (
f"SELECT {cls.columns} FROM message "
f"WHERE mx_room=? AND tg_space=? AND mxid IN ({mxid_placeholders})"
)
rows = await cls.db.fetch(q, mx_room, tg_space, *mxids)
return [cls._from_row(row) for row in rows]
@classmethod
async def replace_temp_mxid(cls, temp_mxid: str, mx_room: RoomID, real_mxid: EventID) -> None:
q = "UPDATE message SET mxid=$1 WHERE mxid=$2 AND mx_room=$3"
await cls.db.execute(q, real_mxid, temp_mxid, mx_room)
async def insert(self) -> None:
q = (
"INSERT INTO message (mxid, mx_room, tgid, tg_space, edit_index, redacted) "
"VALUES ($1, $2, $3, $4, $5, $6)"
)
await self.db.execute(
q, self.mxid, self.mx_room, self.tgid, self.tg_space, self.edit_index, self.redacted
)
async def delete(self) -> None:
q = "DELETE FROM message WHERE mxid=$1 AND mx_room=$2 AND tg_space=$3"
await self.db.execute(q, self.mxid, self.mx_room, self.tg_space)
async def mark_redacted(self) -> None:
self.redacted = True
q = "UPDATE message SET redacted=true WHERE mxid=$1 AND mx_room=$2"
await self.db.execute(q, self.mxid, self.mx_room)
-148
View File
@@ -1,148 +0,0 @@
# mautrix-telegram - A Matrix-Telegram puppeting bridge
# Copyright (C) 2021 Tulir Asokan
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
from __future__ import annotations
from typing import TYPE_CHECKING, Any, ClassVar
import json
from asyncpg import Record
from attr import dataclass
import attr
from mautrix.types import ContentURI, EventID, RoomID
from mautrix.util.async_db import Database
from ..types import TelegramID
fake_db = Database.create("") if TYPE_CHECKING else None
@dataclass
class Portal:
db: ClassVar[Database] = fake_db
# Telegram chat information
tgid: TelegramID
tg_receiver: TelegramID
peer_type: str
megagroup: bool
# Matrix portal information
mxid: RoomID | None
avatar_url: ContentURI | None
encrypted: bool
sponsored_event_id: EventID | None
sponsored_event_ts: int | None
sponsored_msg_random_id: bytes | None
# Telegram chat metadata
username: str | None
title: str | None
about: str | None
photo_id: str | None
local_config: dict[str, Any] = attr.ib(factory=lambda: {})
@classmethod
def _from_row(cls, row: Record | None) -> Portal | None:
if row is None:
return None
data = {**row}
data["local_config"] = json.loads(data.pop("config", None) or "{}")
return cls(**data)
columns: ClassVar[str] = (
"tgid, tg_receiver, peer_type, megagroup, mxid, avatar_url, encrypted, sponsored_event_id,"
"sponsored_event_ts, sponsored_msg_random_id, username, title, about, photo_id, config"
)
@classmethod
async def get_by_tgid(cls, tgid: TelegramID, tg_receiver: TelegramID) -> Portal | None:
q = f"SELECT {cls.columns} FROM portal WHERE tgid=$1 AND tg_receiver=$2"
return cls._from_row(await cls.db.fetchrow(q, tgid, tg_receiver))
@classmethod
async def get_by_mxid(cls, mxid: RoomID) -> Portal | None:
q = f"SELECT {cls.columns} FROM portal WHERE mxid=$1"
return cls._from_row(await cls.db.fetchrow(q, mxid))
@classmethod
async def find_by_username(cls, username: str) -> Portal | None:
q = f"SELECT {cls.columns} FROM portal WHERE lower(username)=$1"
return cls._from_row(await cls.db.fetchrow(q, username.lower()))
@classmethod
async def find_private_chats(cls, tg_receiver: TelegramID) -> list[Portal]:
q = f"SELECT {cls.columns} FROM portal WHERE tg_receiver=$1 AND peer_type='user'"
return [cls._from_row(row) for row in await cls.db.fetch(q, tg_receiver)]
@classmethod
async def all(cls) -> list[Portal]:
rows = await cls.db.fetch(f"SELECT {cls.columns} FROM portal")
return [cls._from_row(row) for row in rows]
@property
def _values(self):
return (
self.tgid,
self.tg_receiver,
self.peer_type,
self.mxid,
self.avatar_url,
self.encrypted,
self.sponsored_event_id,
self.sponsored_event_ts,
self.sponsored_msg_random_id,
self.username,
self.title,
self.about,
self.photo_id,
self.megagroup,
json.dumps(self.local_config) if self.local_config else None,
)
async def save(self) -> None:
q = (
"UPDATE portal SET mxid=$4, avatar_url=$5, encrypted=$6, sponsored_event_id=$7,"
" sponsored_event_ts=$8, sponsored_msg_random_id=$9, username=$10,"
" title=$11, about=$12, photo_id=$13, megagroup=$14, config=$15 "
"WHERE tgid=$1 AND tg_receiver=$2 AND (peer_type=$3 OR true)"
)
await self.db.execute(q, *self._values)
async def update_id(self, id: TelegramID, peer_type: str) -> None:
q = (
"UPDATE portal SET tgid=$1, tg_receiver=$1, peer_type=$2 "
"WHERE tgid=$3 AND tg_receiver=$3"
)
await self.db.execute(q, id, peer_type, self.tgid)
self.tgid = id
self.tg_receiver = id
self.peer_type = peer_type
async def insert(self) -> None:
q = (
"INSERT INTO portal (tgid, tg_receiver, peer_type, mxid, avatar_url, encrypted,"
" sponsored_event_id, sponsored_event_ts, sponsored_msg_random_id,"
" username, title, about, photo_id, megagroup, config) "
"VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15)"
)
await self.db.execute(q, *self._values)
async def delete(self) -> None:
q = "DELETE FROM portal WHERE tgid=$1 AND tg_receiver=$2"
await self.db.execute(q, self.tgid, self.tg_receiver)
-130
View File
@@ -1,130 +0,0 @@
# mautrix-telegram - A Matrix-Telegram puppeting bridge
# Copyright (C) 2021 Tulir Asokan
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
from __future__ import annotations
from typing import TYPE_CHECKING, ClassVar
from asyncpg import Record
from attr import dataclass
from yarl import URL
from mautrix.types import SyncToken, UserID
from mautrix.util.async_db import Database
from ..types import TelegramID
fake_db = Database.create("") if TYPE_CHECKING else None
@dataclass
class Puppet:
db: ClassVar[Database] = fake_db
id: TelegramID
is_registered: bool
displayname: str | None
displayname_source: TelegramID | None
displayname_contact: bool
displayname_quality: int
disable_updates: bool
username: str | None
photo_id: str | None
is_bot: bool | None
custom_mxid: UserID | None
access_token: str | None
next_batch: SyncToken | None
base_url: URL | None
@classmethod
def _from_row(cls, row: Record | None) -> Puppet | None:
if row is None:
return None
data = {**row}
base_url = data.pop("base_url", None)
return cls(**data, base_url=URL(base_url) if base_url else None)
columns: ClassVar[str] = (
"id, is_registered, displayname, displayname_source, displayname_contact, "
"displayname_quality, disable_updates, username, photo_id, is_bot, "
"custom_mxid, access_token, next_batch, base_url"
)
@classmethod
async def all_with_custom_mxid(cls) -> list[Puppet]:
q = f"SELECT {cls.columns} FROM puppet WHERE custom_mxid<>''"
return [cls._from_row(row) for row in await cls.db.fetch(q)]
@classmethod
async def get_by_tgid(cls, tgid: TelegramID) -> Puppet | None:
q = f"SELECT {cls.columns} FROM puppet WHERE id=$1"
return cls._from_row(await cls.db.fetchrow(q, tgid))
@classmethod
async def get_by_custom_mxid(cls, mxid: UserID) -> Puppet | None:
q = f"SELECT {cls.columns} FROM puppet WHERE custom_mxid=$1"
return cls._from_row(await cls.db.fetchrow(q, mxid))
@classmethod
async def find_by_username(cls, username: str) -> Puppet | None:
q = f"SELECT {cls.columns} FROM puppet WHERE lower(username)=$1"
return cls._from_row(await cls.db.fetchrow(q, username.lower()))
@classmethod
async def find_by_displayname(cls, displayname: str) -> Puppet | None:
q = f"SELECT {cls.columns} FROM puppet WHERE displayname=$1"
return cls._from_row(await cls.db.fetchrow(q, displayname))
@property
def _values(self):
return (
self.id,
self.is_registered,
self.displayname,
self.displayname_source,
self.displayname_contact,
self.displayname_quality,
self.disable_updates,
self.username,
self.photo_id,
self.is_bot,
self.custom_mxid,
self.access_token,
self.next_batch,
str(self.base_url) if self.base_url else None,
)
async def save(self) -> None:
q = (
"UPDATE puppet "
"SET is_registered=$2, displayname=$3, displayname_source=$4, displayname_contact=$5,"
" displayname_quality=$6, disable_updates=$7, username=$8, photo_id=$9, is_bot=$10,"
" custom_mxid=$11, access_token=$12, next_batch=$13, base_url=$14 "
"WHERE id=$1"
)
await self.db.execute(q, *self._values)
async def insert(self) -> None:
q = (
"INSERT INTO puppet ("
" id, is_registered, displayname, displayname_source, displayname_contact,"
" displayname_quality, disable_updates, username, photo_id, is_bot,"
" custom_mxid, access_token, next_batch, base_url"
") VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14)"
)
await self.db.execute(q, *self._values)
-82
View File
@@ -1,82 +0,0 @@
# mautrix-telegram - A Matrix-Telegram puppeting bridge
# Copyright (C) 2021 Tulir Asokan
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
from __future__ import annotations
from typing import TYPE_CHECKING, ClassVar
from attr import dataclass
from mautrix.types import ContentURI, EncryptedFile
from mautrix.util.async_db import Database
fake_db = Database.create("") if TYPE_CHECKING else None
@dataclass
class TelegramFile:
db: ClassVar[Database] = fake_db
id: str
mxc: ContentURI
mime_type: str
was_converted: bool
timestamp: int
size: int | None
width: int | None
height: int | None
decryption_info: EncryptedFile | None
thumbnail: TelegramFile | None = None
@classmethod
async def get(cls, loc_id: str, *, _thumbnail: bool = False) -> TelegramFile | None:
q = (
"SELECT id, mxc, mime_type, was_converted, timestamp, size, width, height, thumbnail,"
" decryption_info "
"FROM telegram_file WHERE id=$1"
)
row = await cls.db.fetchrow(q, loc_id)
if row is None:
return None
data = {**row}
thumbnail_id = data.pop("thumbnail", None)
if _thumbnail:
# Don't allow more than one level of recursion
thumbnail_id = None
decryption_info = data.pop("decryption_info", None)
return cls(
**data,
thumbnail=(await cls.get(thumbnail_id, _thumbnail=True)) if thumbnail_id else None,
decryption_info=EncryptedFile.parse_json(decryption_info) if decryption_info else None,
)
async def insert(self) -> None:
q = (
"INSERT INTO telegram_file (id, mxc, mime_type, was_converted, size, width, height, "
" thumbnail, decryption_info) "
"VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)"
)
await self.db.execute(
q,
self.id,
self.mxc,
self.mime_type,
self.was_converted,
self.size,
self.width,
self.height,
self.thumbnail.id if self.thumbnail else None,
self.decryption_info.json() if self.decryption_info else None,
)
-207
View File
@@ -1,207 +0,0 @@
# mautrix-telegram - A Matrix-Telegram puppeting bridge
# Copyright (C) 2021 Tulir Asokan
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
from __future__ import annotations
from typing import TYPE_CHECKING, ClassVar
import asyncio
import datetime
from telethon import utils
from telethon.crypto import AuthKey
from telethon.sessions import MemorySession
from telethon.tl.types import PeerChannel, PeerChat, PeerUser, updates
from mautrix.util.async_db import Database
fake_db = Database.create("") if TYPE_CHECKING else None
class PgSession(MemorySession):
db: ClassVar[Database] = fake_db
session_id: str
_dc_id: int
_server_address: str | None
_port: int | None
_auth_key: AuthKey | None
_takeout_id: int | None
_process_entities_lock: asyncio.Lock
def __init__(
self,
session_id: str,
dc_id: int = 0,
server_address: str | None = None,
port: int | None = None,
auth_key: AuthKey | None = None,
takeout_id: int | None = None,
) -> None:
super().__init__()
self.session_id = session_id
self._dc_id = dc_id
self._server_address = server_address
self._port = port
self._auth_key = auth_key
self._takeout_id = takeout_id
self._process_entities_lock = asyncio.Lock()
def clone(self, to_instance=None) -> MemorySession:
# We don't want to store data of clones
# (which are used for temporarily connecting to different DCs)
return super().clone(MemorySession())
@property
def auth_key_bytes(self) -> bytes | None:
return self._auth_key.key if self._auth_key else None
@classmethod
async def get(cls, session_id: str) -> PgSession:
q = (
"SELECT session_id, dc_id, server_address, port, auth_key FROM telethon_sessions "
"WHERE session_id=$1"
)
row = await cls.db.fetchrow(q, session_id)
if row is None:
return cls(session_id)
data = {**row}
auth_key = AuthKey(data.pop("auth_key", None))
return cls(**data, auth_key=auth_key)
@classmethod
async def has(cls, session_id: str) -> bool:
q = "SELECT COUNT(*) FROM telethon_sessions WHERE session_id=$1"
count = await cls.db.fetchval(q, session_id)
return count > 0
async def save(self) -> None:
q = (
"INSERT INTO telethon_sessions (session_id, dc_id, server_address, port, auth_key) "
"VALUES ($1, $2, $3, $4, $5) ON CONFLICT (session_id) "
"DO UPDATE SET dc_id=$2, server_address=$3, port=$4, auth_key=$5"
)
await self.db.execute(
q, self.session_id, self.dc_id, self.server_address, self.port, self.auth_key_bytes
)
_tables: ClassVar[tuple[str, ...]] = (
"telethon_sessions",
"telethon_entities",
"telethon_sent_files",
"telethon_update_state",
)
async def delete(self) -> None:
async with self.db.acquire() as conn, conn.transaction():
for table in self._tables:
await conn.execute(f"DELETE FROM {table} WHERE session_id=$1", self.session_id)
async def close(self) -> None:
# Nothing to do here, DB connection is global
pass
async def get_update_state(self, entity_id: int) -> updates.State | None:
q = (
"SELECT pts, qts, date, seq, unread_count FROM telethon_update_state "
"WHERE session_id=$1 AND entity_id=$2"
)
row = await self.db.fetchrow(q, self.session_id, entity_id)
if row is None:
return None
date = datetime.datetime.utcfromtimestamp(row["date"])
return updates.State(row["pts"], row["qts"], date, row["seq"], row["unread_count"])
async def set_update_state(self, entity_id: int, row: updates.State) -> None:
q = (
"INSERT INTO telethon_update_state"
" (session_id, entity_id, pts, qts, date, seq, unread_count) "
"VALUES ($1, $2, $3, $4, $5, $6, $7)"
"ON CONFLICT (session_id, entity_id) DO UPDATE"
" SET pts=$3, qts=$4, date=$5, seq=$6, unread_count=$7"
)
ts = row.date.timestamp()
await self.db.execute(
q, self.session_id, entity_id, row.pts, row.qts, ts, row.seq, row.unread_count
)
def _entity_values_to_row(
self, id: int, hash: int, username: str | None, phone: str | int | None, name: str | None
) -> tuple[str, int, int, str | None, str | None, str | None]:
return self.session_id, id, hash, username, str(phone) if phone else None, name
async def process_entities(self, tlo) -> None:
# Postgres likes to deadlock on simultaneous upserts, so just lock the whole thing here
# TODO: make sure postgres doesn't deadlock on upserts when session_id is different
async with self._process_entities_lock:
await self._locked_process_entities(tlo)
async def _locked_process_entities(self, tlo) -> None:
rows: list[
tuple[str, int, int, str | None, str | None, str | None]
] = self._entities_to_rows(tlo)
if not rows:
return
if self.db.scheme == "postgres":
q = (
"INSERT INTO telethon_entities (session_id, id, hash, username, phone, name) "
"VALUES ($1, unnest($2::bigint[]), unnest($3::bigint[]), "
" unnest($4::text[]), unnest($5::text[]), unnest($6::text[])) "
"ON CONFLICT (session_id, id) DO UPDATE"
" SET hash=excluded.hash, username=excluded.username,"
" phone=excluded.phone, name=excluded.name"
)
_, ids, hashes, usernames, phones, names = zip(*rows)
await self.db.execute(q, self.session_id, ids, hashes, usernames, phones, names)
else:
q = (
"INSERT INTO telethon_entities (session_id, id, hash, username, phone, name) "
"VALUES ($1, $2, $3, $4, $5, $6) "
"ON CONFLICT (session_id, id) DO UPDATE "
" SET hash=$3, username=$4, phone=$5, name=$6"
)
await self.db.executemany(q, rows)
async def _select_entity(
self, constraint: str, *args: str | int | tuple[int, ...]
) -> tuple[int, int] | None:
row = await self.db.fetchrow(
f"SELECT id, hash FROM telethon_entities WHERE {constraint}", *args
)
if row is None:
return None
return row["id"], row["hash"]
async def get_entity_rows_by_phone(self, key: str | int) -> tuple[int, int] | None:
return await self._select_entity("phone=$1", str(key))
async def get_entity_rows_by_username(self, key: str) -> tuple[int, int] | None:
return await self._select_entity("username=$1", key)
async def get_entity_rows_by_name(self, key: str) -> tuple[int, int] | None:
return await self._select_entity("name=$1", key)
async def get_entity_rows_by_id(self, key: int, exact: bool = True) -> tuple[int, int] | None:
if exact:
return await self._select_entity("id=$1", key)
ids = (
utils.get_peer_id(PeerUser(key)),
utils.get_peer_id(PeerChat(key)),
utils.get_peer_id(PeerChannel(key)),
)
if self.db.scheme == "postgres":
return await self._select_entity("id=ANY($1)", ids)
else:
return await self._select_entity(f"id IN ($1, $2, $3)", *ids)
-5
View File
@@ -1,5 +0,0 @@
from mautrix.util.async_db import UpgradeTable
upgrade_table = UpgradeTable()
from . import v01_initial_revision, v02_sponsored_events
@@ -1,315 +0,0 @@
# mautrix-telegram - A Matrix-Telegram puppeting bridge
# Copyright (C) 2021 Tulir Asokan
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
from asyncpg import Connection
from . import upgrade_table
legacy_version_query = "SELECT version_num FROM alembic_version"
last_legacy_version = "bfc0a39bfe02"
def table_exists(scheme: str, name: str) -> str:
if scheme == "sqlite":
return f"SELECT EXISTS(SELECT 1 FROM sqlite_master WHERE type='table' AND name='{name}')"
elif scheme == "postgres":
return f"SELECT EXISTS(SELECT FROM information_schema.tables WHERE table_name='{name}')"
raise RuntimeError("unsupported database scheme")
@upgrade_table.register(description="Initial asyncpg revision")
async def upgrade_v1(conn: Connection, scheme: str) -> None:
is_legacy = await conn.fetchval(table_exists(scheme, "alembic_version"))
if is_legacy:
await migrate_legacy_to_v1(conn, scheme)
else:
await create_v1_tables(conn)
async def migrate_legacy_to_v1(conn: Connection, scheme: str) -> None:
legacy_version = await conn.fetchval(legacy_version_query)
if legacy_version != last_legacy_version:
raise RuntimeError(
"Legacy database is not on last version. "
"Please upgrade the old database with alembic or drop it completely first."
)
if scheme != "sqlite":
await conn.execute(
"""
ALTER TABLE contact
DROP CONSTRAINT contact_user_fkey,
DROP CONSTRAINT contact_contact_fkey,
ADD CONSTRAINT contact_user_fkey FOREIGN KEY (contact) REFERENCES puppet(id)
ON DELETE CASCADE ON UPDATE CASCADE,
ADD CONSTRAINT contact_contact_fkey FOREIGN KEY ("user") REFERENCES "user"(tgid)
ON DELETE CASCADE ON UPDATE CASCADE
"""
)
await conn.execute(
"""
ALTER TABLE telethon_sessions
DROP CONSTRAINT telethon_sessions_pkey,
ADD CONSTRAINT telethon_sessions_pkey PRIMARY KEY (session_id)
"""
)
await conn.execute(
"""
ALTER TABLE telegram_file
DROP CONSTRAINT fk_file_thumbnail,
ADD CONSTRAINT fk_file_thumbnail
FOREIGN KEY (thumbnail) REFERENCES telegram_file(id)
ON UPDATE CASCADE ON DELETE SET NULL
"""
)
await conn.execute("ALTER TABLE puppet ALTER COLUMN id DROP DEFAULT")
await conn.execute("DROP SEQUENCE puppet_id_seq")
await conn.execute("ALTER TABLE bot_chat ALTER COLUMN id DROP DEFAULT")
await conn.execute("DROP SEQUENCE bot_chat_id_seq")
await conn.execute("ALTER TABLE portal ALTER COLUMN config TYPE jsonb USING config::jsonb")
await conn.execute(
"ALTER TABLE telegram_file ALTER COLUMN decryption_info TYPE jsonb "
"USING decryption_info::jsonb"
)
await varchar_to_text(conn)
else:
await conn.execute(
"""CREATE TABLE telethon_sessions_new (
session_id TEXT PRIMARY KEY,
dc_id INTEGER,
server_address TEXT,
port INTEGER,
auth_key bytea
)"""
)
await conn.execute(
"""
INSERT INTO telethon_sessions_new (session_id, dc_id, server_address, port, auth_key)
SELECT session_id, dc_id, server_address, port, auth_key FROM telethon_sessions
"""
)
await conn.execute("DROP TABLE telethon_sessions")
await conn.execute("ALTER TABLE telethon_sessions_new RENAME TO telethon_sessions")
await update_state_store(conn, scheme)
await conn.execute('ALTER TABLE "user" ADD COLUMN is_bot BOOLEAN NOT NULL DEFAULT false')
await conn.execute("ALTER TABLE puppet RENAME COLUMN matrix_registered TO is_registered")
await conn.execute("DROP TABLE telethon_version")
await conn.execute("DROP TABLE alembic_version")
async def update_state_store(conn: Connection, scheme: str) -> None:
# The Matrix state store already has more or less the correct schema, so set the version
await conn.execute("CREATE TABLE mx_version (version INTEGER PRIMARY KEY)")
await conn.execute("INSERT INTO mx_version (version) VALUES (2)")
await conn.execute("UPDATE mx_user_profile SET membership='LEAVE' WHERE membership='LEFT'")
if scheme != "sqlite":
# Also add the membership type on postgres
await conn.execute(
"CREATE TYPE membership AS ENUM ('join', 'leave', 'invite', 'ban', 'knock')"
)
await conn.execute(
"ALTER TABLE mx_user_profile ALTER COLUMN membership TYPE membership "
"USING LOWER(membership)::membership"
)
else:
# On SQLite there's no custom type, but we still want to lowercase everything
await conn.execute("UPDATE mx_user_profile SET membership=LOWER(membership)")
async def varchar_to_text(conn: Connection) -> None:
columns_to_adjust = {
"user": ("mxid", "tg_username", "tg_phone"),
"portal": (
"peer_type",
"mxid",
"username",
"title",
"about",
"photo_id",
"avatar_url",
"config",
),
"message": ("mxid", "mx_room"),
"puppet": (
"displayname",
"username",
"photo_id",
"access_token",
"custom_mxid",
"next_batch",
"base_url",
),
"bot_chat": ("type",),
"telegram_file": ("id", "mxc", "mime_type", "thumbnail"),
# Phone is a bigint in the old schema, which is safe, but we don't do math on it,
# so let's change it to a string
"telethon_entities": ("session_id", "username", "name", "phone"),
"telethon_sent_files": ("session_id",),
"telethon_sessions": ("session_id", "server_address"),
"telethon_update_state": ("session_id",),
"mx_room_state": ("room_id",),
"mx_user_profile": ("room_id", "user_id", "displayname", "avatar_url"),
}
for table, columns in columns_to_adjust.items():
for column in columns:
await conn.execute(f'ALTER TABLE "{table}" ALTER COLUMN {column} TYPE TEXT')
async def create_v1_tables(conn: Connection) -> None:
await conn.execute(
"""CREATE TABLE "user" (
mxid TEXT PRIMARY KEY,
tgid BIGINT UNIQUE,
tg_username TEXT,
tg_phone TEXT,
is_bot BOOLEAN NOT NULL DEFAULT false,
saved_contacts INTEGER NOT NULL DEFAULT 0
)"""
)
await conn.execute(
"""CREATE TABLE portal (
tgid BIGINT,
tg_receiver BIGINT,
peer_type TEXT NOT NULL,
mxid TEXT UNIQUE,
avatar_url TEXT,
encrypted BOOLEAN NOT NULL DEFAULT false,
username TEXT,
title TEXT,
about TEXT,
photo_id TEXT,
megagroup BOOLEAN,
config jsonb,
PRIMARY KEY (tgid, tg_receiver)
)"""
)
await conn.execute(
"""CREATE TABLE message (
mxid TEXT,
mx_room TEXT,
tgid BIGINT NOT NULL,
tg_space BIGINT NOT NULL,
edit_index INTEGER NOT NULL,
redacted BOOLEAN NOT NULL DEFAULT false,
PRIMARY KEY (tgid, tg_space, edit_index),
UNIQUE (mxid, mx_room, tg_space)
)"""
)
await conn.execute(
"""CREATE TABLE puppet (
id BIGINT PRIMARY KEY,
is_registered BOOLEAN NOT NULL DEFAULT false,
displayname TEXT,
displayname_source BIGINT,
displayname_contact BOOLEAN NOT NULL DEFAULT true,
displayname_quality INTEGER NOT NULL DEFAULT 0,
disable_updates BOOLEAN NOT NULL DEFAULT false,
username TEXT,
photo_id TEXT,
is_bot BOOLEAN,
access_token TEXT,
custom_mxid TEXT,
next_batch TEXT,
base_url TEXT
)"""
)
await conn.execute(
"""CREATE TABLE telegram_file (
id TEXT PRIMARY KEY,
mxc TEXT NOT NULL,
mime_type TEXT,
was_converted BOOLEAN NOT NULL DEFAULT false,
timestamp BIGINT NOT NULL DEFAULT 0,
size BIGINT,
width INTEGER,
height INTEGER,
thumbnail TEXT,
decryption_info jsonb,
FOREIGN KEY (thumbnail) REFERENCES telegram_file(id)
ON UPDATE CASCADE ON DELETE SET NULL
)"""
)
await conn.execute(
"""CREATE TABLE bot_chat (
id BIGINT PRIMARY KEY,
type TEXT NOT NULL
)"""
)
await conn.execute(
"""CREATE TABLE user_portal (
"user" BIGINT,
portal BIGINT,
portal_receiver BIGINT,
PRIMARY KEY ("user", portal, portal_receiver),
FOREIGN KEY ("user") REFERENCES "user"(tgid) ON DELETE CASCADE ON UPDATE CASCADE,
FOREIGN KEY (portal, portal_receiver) REFERENCES portal(tgid, tg_receiver)
ON DELETE CASCADE ON UPDATE CASCADE
)"""
)
await conn.execute(
"""CREATE TABLE contact (
"user" BIGINT,
contact BIGINT,
PRIMARY KEY ("user", contact),
FOREIGN KEY ("user") REFERENCES "user"(tgid) ON DELETE CASCADE ON UPDATE CASCADE,
FOREIGN KEY (contact) REFERENCES puppet(id) ON DELETE CASCADE ON UPDATE CASCADE
)"""
)
await conn.execute(
"""CREATE TABLE telethon_sessions (
session_id TEXT PRIMARY KEY,
dc_id INTEGER,
server_address TEXT,
port INTEGER,
auth_key bytea
)"""
)
await conn.execute(
"""CREATE TABLE telethon_entities (
session_id TEXT,
id BIGINT,
hash BIGINT NOT NULL,
username TEXT,
phone TEXT,
name TEXT,
PRIMARY KEY (session_id, id)
)"""
)
await conn.execute(
"""CREATE TABLE telethon_sent_files (
session_id TEXT,
md5_digest bytea,
file_size INTEGER,
type INTEGER,
id BIGINT,
hash BIGINT,
PRIMARY KEY (session_id, md5_digest, file_size, type)
)"""
)
await conn.execute(
"""CREATE TABLE telethon_update_state (
session_id TEXT,
entity_id BIGINT,
pts BIGINT,
qts BIGINT,
date BIGINT,
seq BIGINT,
unread_count INTEGER,
PRIMARY KEY (session_id, entity_id)
)"""
)
@@ -1,25 +0,0 @@
# mautrix-telegram - A Matrix-Telegram puppeting bridge
# Copyright (C) 2021 Tulir Asokan
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
from asyncpg import Connection
from . import upgrade_table
@upgrade_table.register(description="Add column to store sponsored message event ID in channels")
async def upgrade_v2(conn: Connection) -> None:
await conn.execute("ALTER TABLE portal ADD COLUMN sponsored_event_id TEXT")
await conn.execute("ALTER TABLE portal ADD COLUMN sponsored_event_ts BIGINT")
await conn.execute("ALTER TABLE portal ADD COLUMN sponsored_msg_random_id bytea")
-138
View File
@@ -1,138 +0,0 @@
# mautrix-telegram - A Matrix-Telegram puppeting bridge
# Copyright (C) 2021 Tulir Asokan
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
from __future__ import annotations
from typing import TYPE_CHECKING, ClassVar, Iterable
from asyncpg import Record
from attr import dataclass
from mautrix.types import UserID
from mautrix.util.async_db import Database
from ..types import TelegramID
fake_db = Database.create("") if TYPE_CHECKING else None
@dataclass
class User:
db: ClassVar[Database] = fake_db
mxid: UserID
tgid: TelegramID | None
tg_username: str | None
tg_phone: str | None
is_bot: bool
saved_contacts: int
@classmethod
def _from_row(cls, row: Record | None) -> User | None:
if row is None:
return None
return cls(**row)
columns: ClassVar[str] = "mxid, tgid, tg_username, tg_phone, is_bot, saved_contacts"
@classmethod
async def get_by_tgid(cls, tgid: TelegramID) -> User | None:
q = f'SELECT {cls.columns} FROM "user" WHERE tgid=$1'
return cls._from_row(await cls.db.fetchrow(q, tgid))
@classmethod
async def get_by_mxid(cls, mxid: UserID) -> User | None:
q = f'SELECT {cls.columns} FROM "user" WHERE mxid=$1'
return cls._from_row(await cls.db.fetchrow(q, mxid))
@classmethod
async def find_by_username(cls, username: str) -> User | None:
q = f'SELECT {cls.columns} FROM "user" WHERE lower(tg_username)=$1'
return cls._from_row(await cls.db.fetchrow(q, username.lower()))
@classmethod
async def all_with_tgid(cls) -> list[User]:
q = f'SELECT {cls.columns} FROM "user" WHERE tgid IS NOT NULL'
return [cls._from_row(row) for row in await cls.db.fetch(q)]
async def delete(self) -> None:
await self.db.execute('DELETE FROM "user" WHERE mxid=$1', self.mxid)
@property
def _values(self):
return (
self.mxid,
self.tgid,
self.tg_username,
self.tg_phone,
self.is_bot,
self.saved_contacts,
)
async def save(self) -> None:
q = (
'UPDATE "user" SET tgid=$2, tg_username=$3, tg_phone=$4, is_bot=$5, saved_contacts=$6 '
"WHERE mxid=$1"
)
await self.db.execute(q, *self._values)
async def insert(self) -> None:
q = (
'INSERT INTO "user" (mxid, tgid, tg_username, tg_phone, is_bot, saved_contacts) '
"VALUES ($1, $2, $3, $4, $5, $6)"
)
await self.db.execute(q, *self._values)
async def get_contacts(self) -> list[TelegramID]:
rows = await self.db.fetch('SELECT contact FROM contact WHERE "user"=$1', self.tgid)
return [TelegramID(row["contact"]) for row in rows]
async def set_contacts(self, puppets: Iterable[TelegramID]) -> None:
columns = ["user", "contact"]
records = [(self.tgid, puppet_id) for puppet_id in puppets]
async with self.db.acquire() as conn, conn.transaction():
await conn.execute('DELETE FROM contact WHERE "user"=$1', self.tgid)
if self.db.scheme == "postgres":
await conn.copy_records_to_table("contact", records=records, columns=columns)
else:
q = 'INSERT INTO contact ("user", contact) VALUES ($1, $2)'
await conn.executemany(q, records)
async def get_portals(self) -> list[tuple[TelegramID, TelegramID]]:
q = 'SELECT portal, portal_receiver FROM user_portal WHERE "user"=$1'
rows = await self.db.fetch(q, self.tgid)
return [(TelegramID(row["portal"]), TelegramID(row["portal_receiver"])) for row in rows]
async def set_portals(self, portals: Iterable[tuple[TelegramID, TelegramID]]) -> None:
columns = ["user", "portal", "portal_receiver"]
records = [(self.tgid, tgid, tg_receiver) for tgid, tg_receiver in portals]
async with self.db.acquire() as conn, conn.transaction():
await conn.execute('DELETE FROM user_portal WHERE "user"=$1', self.tgid)
if self.db.scheme == "postgres":
await conn.copy_records_to_table("user_portal", records=records, columns=columns)
else:
q = 'INSERT INTO user_portal ("user", portal, portal_receiver) VALUES ($1, $2, $3)'
await conn.executemany(q, records)
async def register_portal(self, tgid: TelegramID, tg_receiver: TelegramID) -> None:
q = (
'INSERT INTO user_portal ("user", portal, portal_receiver) VALUES ($1, $2, $3) '
'ON CONFLICT ("user", portal, portal_receiver) DO NOTHING'
)
await self.db.execute(q, self.tgid, tgid, tg_receiver)
async def unregister_portal(self, tgid: TelegramID, tg_receiver: TelegramID) -> None:
q = 'DELETE FROM user_portal WHERE "user"=$1 AND portal=$2 AND portal_receiver=$3'
await self.db.execute(q, self.tgid, tgid, tg_receiver)
-555
View File
@@ -1,555 +0,0 @@
# Homeserver details
homeserver:
# The address that this appservice can use to connect to the homeserver.
address: https://example.com
# The domain of the homeserver (for MXIDs, etc).
domain: example.com
# Whether or not to verify the SSL certificate of the homeserver.
# Only applies if address starts with https://
verify_ssl: true
asmux: false
# Number of retries for all HTTP requests if the homeserver isn't reachable.
http_retry_count: 4
# The URL to push real-time bridge status to.
# If set, the bridge will make POST requests to this URL whenever a user's Telegram connection state changes.
# The bridge will use the appservice as_token to authorize requests.
status_endpoint: null
# Endpoint for reporting per-message status.
message_send_checkpoint_endpoint: null
# Application service host/registration related details
# Changing these values requires regeneration of the registration.
appservice:
# The address that the homeserver can use to connect to this appservice.
address: http://localhost:29317
# When using https:// the TLS certificate and key files for the address.
tls_cert: false
tls_key: false
# The hostname and port where this appservice should listen.
hostname: 0.0.0.0
port: 29317
# The maximum body size of appservice API requests (from the homeserver) in mebibytes
# Usually 1 is enough, but on high-traffic bridges you might need to increase this to avoid 413s
max_body_size: 1
# The full URI to the database. SQLite and Postgres are supported.
# Format examples:
# SQLite: sqlite:///filename.db
# Postgres: postgres://username:password@hostname/dbname
database: postgres://username:password@hostname/dbname
# Additional arguments for asyncpg.create_pool() or sqlite3.connect()
# https://magicstack.github.io/asyncpg/current/api/index.html#asyncpg.pool.create_pool
# https://docs.python.org/3/library/sqlite3.html#sqlite3.connect
# For sqlite, min_size is used as the connection thread pool size and max_size is ignored.
database_opts:
min_size: 1
max_size: 10
# Public part of web server for out-of-Matrix interaction with the bridge.
# Used for things like login if the user wants to make sure the 2FA password isn't stored in
# the HS database.
public:
# Whether or not the public-facing endpoints should be enabled.
enabled: false
# The prefix to use in the public-facing endpoints.
prefix: /public
# The base URL where the public-facing endpoints are available. The prefix is not added
# implicitly.
external: https://example.com/public
# Provisioning API part of the web server for automated portal creation and fetching information.
# Used by things like mautrix-manager (https://github.com/tulir/mautrix-manager).
provisioning:
# Whether or not the provisioning API should be enabled.
enabled: true
# The prefix to use in the provisioning API endpoints.
prefix: /_matrix/provision/v1
# The shared secret to authorize users of the API.
# Set to "generate" to generate and save a new token.
shared_secret: generate
# The unique ID of this appservice.
id: telegram
# Username of the appservice bot.
bot_username: telegrambot
# Display name and avatar for bot. Set to "remove" to remove display name/avatar, leave empty
# to leave display name/avatar as-is.
bot_displayname: Telegram bridge bot
bot_avatar: mxc://maunium.net/tJCRmUyJDsgRNgqhOgoiHWbX
# Community ID for bridged users (changes registration file) and rooms.
# Must be created manually.
#
# Example: "+telegram:example.com". Set to false to disable.
community_id: false
# Whether or not to receive ephemeral events via appservice transactions.
# Requires MSC2409 support (i.e. Synapse 1.22+).
# You should disable bridge -> sync_with_custom_puppets when this is enabled.
ephemeral_events: false
# Authentication tokens for AS <-> HS communication. Autogenerated; do not modify.
as_token: "This value is generated when generating the registration"
hs_token: "This value is generated when generating the registration"
# Prometheus telemetry config. Requires prometheus-client to be installed.
metrics:
enabled: false
listen_port: 8000
# Manhole config.
manhole:
# Whether or not opening the manhole is allowed.
enabled: false
# The path for the unix socket.
path: /var/tmp/mautrix-telegram.manhole
# The list of UIDs who can be added to the whitelist.
# If empty, any UIDs can be specified in the open-manhole command.
whitelist:
- 0
# Bridge config
bridge:
# Localpart template of MXIDs for Telegram users.
# {userid} is replaced with the user ID of the Telegram user.
username_template: "telegram_{userid}"
# Localpart template of room aliases for Telegram portal rooms.
# {groupname} is replaced with the name part of the public channel/group invite link ( https://t.me/{} )
alias_template: "telegram_{groupname}"
# Displayname template for Telegram users.
# {displayname} is replaced with the display name of the Telegram user.
displayname_template: "{displayname} (Telegram)"
# Set the preferred order of user identifiers which to use in the Matrix puppet display name.
# In the (hopefully unlikely) scenario that none of the given keys are found, the numeric user
# ID is used.
#
# If the bridge is working properly, a phone number or an username should always be known, but
# the other one can very well be empty.
#
# Valid keys:
# "full name" (First and/or last name)
# "full name reversed" (Last and/or first name)
# "first name"
# "last name"
# "username"
# "phone number"
displayname_preference:
- full name
- username
- phone number
# Maximum length of displayname
displayname_max_length: 100
# Remove avatars from Telegram ghost users when removed on Telegram. This is disabled by default
# as there's no way to determine whether an avatar is removed or just hidden from some users. If
# you're on a single-user instance, this should be safe to enable.
allow_avatar_remove: false
# Maximum number of members to sync per portal when starting up. Other members will be
# synced when they send messages. The maximum is 10000, after which the Telegram server
# will not send any more members.
# -1 means no limit (which means it's limited to 10000 by the server)
max_initial_member_sync: 100
# Whether or not to sync the member list in channels.
# If no channel admins have logged into the bridge, the bridge won't be able to sync the member
# list regardless of this setting.
sync_channel_members: true
# Whether or not to skip deleted members when syncing members.
skip_deleted_members: true
# Whether or not to automatically synchronize contacts and chats of Matrix users logged into
# their Telegram account at startup.
startup_sync: true
# Number of most recently active dialogs to check when syncing chats.
# Set to 0 to remove limit.
sync_update_limit: 0
# Number of most recently active dialogs to create portals for when syncing chats.
# Set to 0 to remove limit.
sync_create_limit: 30
# Whether or not to sync and create portals for direct chats at startup.
sync_direct_chats: false
# The maximum number of simultaneous Telegram deletions to handle.
# A large number of simultaneous redactions could put strain on your homeserver.
max_telegram_delete: 10
# Whether or not to automatically sync the Matrix room state (mostly unpuppeted displaynames)
# at startup and when creating a bridge.
sync_matrix_state: true
# Allow logging in within Matrix. If false, users can only log in using login-qr or the
# out-of-Matrix login website (see appservice.public config section)
allow_matrix_login: true
# Whether or not to bridge plaintext highlights.
# Only enable this if your displayname_template has some static part that the bridge can use to
# reliably identify what is a plaintext highlight.
plaintext_highlights: false
# Whether or not to make portals of publicly joinable channels/supergroups publicly joinable on Matrix.
public_portals: true
# Whether or not to use /sync to get presence, read receipts and typing notifications
# when double puppeting is enabled
sync_with_custom_puppets: true
# Whether or not to update the m.direct account data event when double puppeting is enabled.
# Note that updating the m.direct event is not atomic (except with mautrix-asmux)
# and is therefore prone to race conditions.
sync_direct_chat_list: false
# Servers to always allow double puppeting from
double_puppet_server_map:
example.com: https://example.com
# Allow using double puppeting from any server with a valid client .well-known file.
double_puppet_allow_discovery: false
# Shared secrets for https://github.com/devture/matrix-synapse-shared-secret-auth
#
# If set, custom puppets will be enabled automatically for local users
# instead of users having to find an access token and run `login-matrix`
# manually.
# If using this for other servers than the bridge's server,
# you must also set the URL in the double_puppet_server_map.
login_shared_secret_map:
example.com: foobar
# Set to false to disable link previews in messages sent to Telegram.
telegram_link_preview: true
# Whether or not the !tg join command should do a HTTP request
# to resolve redirects in invite links.
invite_link_resolve: false
# Use inline images instead of a separate message for the caption.
# N.B. Inline images are not supported on all clients (e.g. Element iOS/Android).
inline_images: false
# Maximum size of image in megabytes before sending to Telegram as a document.
image_as_file_size: 10
# Maximum size of Telegram documents in megabytes to bridge.
max_document_size: 100
# Enable experimental parallel file transfer, which makes uploads/downloads much faster by
# streaming from/to Matrix and using many connections for Telegram.
# Note that generating HQ thumbnails for videos is not possible with streamed transfers.
# This option uses internal Telethon implementation details and may break with minor updates.
parallel_file_transfer: false
# Whether or not created rooms should have federation enabled.
# If false, created portal rooms will never be federated.
federate_rooms: true
# Settings for converting animated stickers.
animated_sticker:
# Format to which animated stickers should be converted.
# disable - No conversion, send as-is (gzipped lottie)
# png - converts to non-animated png (fastest),
# gif - converts to animated gif
# webm - converts to webm video, requires ffmpeg executable with vp9 codec and webm container support
target: gif
# Arguments for converter. All converters take width and height.
args:
width: 256
height: 256
fps: 25 # only for webm and gif (2, 5, 10, 20 or 25 recommended)
# End-to-bridge encryption support options.
#
# See https://docs.mau.fi/bridges/general/end-to-bridge-encryption.html for more info.
encryption:
# Allow encryption, work in group chat rooms with e2ee enabled
allow: false
# Default to encryption, force-enable encryption in all portals the bridge creates
# This will cause the bridge bot to be in private chats for the encryption to work properly.
default: false
# Database for the encryption data. If set to `default`, will use the appservice database.
database: default
# Options for automatic key sharing.
key_sharing:
# Enable key sharing? If enabled, key requests for rooms where users are in will be fulfilled.
# You must use a client that supports requesting keys from other users to use this feature.
allow: false
# Require the requesting device to have a valid cross-signing signature?
# This doesn't require that the bridge has verified the device, only that the user has verified it.
# Not yet implemented.
require_cross_signing: false
# Require devices to be verified by the bridge?
# Verification by the bridge is not yet implemented.
require_verification: true
# Whether or not to explicitly set the avatar and room name for private
# chat portal rooms. This will be implicitly enabled if encryption.default is true.
private_chat_portal_meta: false
# Whether or not the bridge should send a read receipt from the bridge bot when a message has
# been sent to Telegram.
delivery_receipts: false
# Whether or not delivery errors should be reported as messages in the Matrix room.
delivery_error_reports: false
# Set this to true to tell the bridge to re-send m.bridge events to all rooms on the next run.
# This field will automatically be changed back to false after it,
# except if the config file is not writable.
resend_bridge_info: false
# When using double puppeting, should muted chats be muted in Matrix?
mute_bridging: false
# When using double puppeting, should pinned chats be moved to a specific tag in Matrix?
# The favorites tag is `m.favourite`.
pinned_tag: null
# Same as above for archived chats, the low priority tag is `m.lowpriority`.
archive_tag: null
# Whether or not mute status and tags should only be bridged when the portal room is created.
tag_only_on_create: true
# Should leaving the room on Matrix make the user leave on Telegram?
bridge_matrix_leave: true
# Should the user be kicked out of all portals when logging out of the bridge?
kick_on_logout: true
# Settings for backfilling messages from Telegram.
backfill:
# Whether or not the Telegram ghosts of logged in Matrix users should be
# invited to private chats when backfilling history from Telegram. This is
# usually needed to prevent rate limits and to allow timestamp massaging.
invite_own_puppet: true
# Maximum number of messages to backfill without using a takeout.
# The first time a takeout is used, the user has to manually approve it from a different
# device. If initial_limit or missed_limit are higher than this value, the bridge will ask
# the user to accept the takeout after logging in before syncing any chats.
takeout_limit: 100
# Maximum number of messages to backfill initially.
# Set to 0 to disable backfilling when creating portal, or -1 to disable the limit.
#
# N.B. Initial backfill will only start after member sync. Make sure your
# max_initial_member_sync is set to a low enough value so it doesn't take forever.
initial_limit: 0
# Maximum number of messages to backfill if messages were missed while the bridge was
# disconnected. Note that this only works for logged in users and only if the chat isn't
# older than sync_update_limit
# Set to 0 to disable backfilling missed messages.
missed_limit: 50
# If using double puppeting, should notifications be disabled
# while the initial backfill is in progress?
disable_notifications: false
# Whether or not to enable backfilling in normal groups.
# Normal groups have numerous technical problems in Telegram, and backfilling normal groups
# will likely cause problems if there are multiple Matrix users in the group.
normal_groups: false
# Overrides for base power levels.
initial_power_level_overrides:
user: {}
group: {}
# Whether to bridge Telegram bot messages as m.notices or m.texts.
bot_messages_as_notices: true
bridge_notices:
# Whether or not Matrix bot messages (type m.notice) should be bridged.
default: false
# List of user IDs for whom the previous flag is flipped.
# e.g. if bridge_notices.default is false, notices from other users will not be bridged, but
# notices from users listed here will be bridged.
exceptions:
- "@importantbot:example.com"
# Some config options related to Telegram message deduplication.
# The default values are usually fine, but some debug messages/warnings might recommend you
# change these.
deduplication:
# Whether or not to check the database if the message about to be sent is a duplicate.
pre_db_check: false
# The number of latest events to keep when checking for duplicates.
# You might need to increase this on high-traffic bridge instances.
cache_queue_length: 20
# The formats to use when sending messages to Telegram via the relay bot.
# Text msgtypes (m.text, m.notice and m.emote) support HTML, media msgtypes don't.
#
# Available variables:
# $sender_displayname - The display name of the sender (e.g. Example User)
# $sender_username - The username (Matrix ID localpart) of the sender (e.g. exampleuser)
# $sender_mxid - The Matrix ID of the sender (e.g. @exampleuser:example.com)
# $message - The message content
message_formats:
m.text: "<b>$sender_displayname</b>: $message"
m.notice: "<b>$sender_displayname</b>: $message"
m.emote: "* <b>$sender_displayname</b> $message"
m.file: "<b>$sender_displayname</b> sent a file: $message"
m.image: "<b>$sender_displayname</b> sent an image: $message"
m.audio: "<b>$sender_displayname</b> sent an audio file: $message"
m.video: "<b>$sender_displayname</b> sent a video: $message"
m.location: "<b>$sender_displayname</b> sent a location: $message"
# Telegram doesn't have built-in emotes, this field specifies how m.emote's from authenticated
# users are sent to telegram. All fields in message_formats are supported. Additionally, the
# Telegram user info is available in the following variables:
# $displayname - Telegram displayname
# $username - Telegram username (may not exist)
# $mention - Telegram @username or displayname mention (depending on which exists)
emote_format: "* $mention $formatted_body"
# The formats to use when sending state events to Telegram via the relay bot.
#
# Variables from `message_formats` that have the `sender_` prefix are available without the prefix.
# In name_change events, `$prev_displayname` is the previous displayname.
#
# Set format to an empty string to disable the messages for that event.
state_event_formats:
join: "<b>$displayname</b> joined the room."
leave: "<b>$displayname</b> left the room."
name_change: "<b>$prev_displayname</b> changed their name to <b>$displayname</b>"
# Filter rooms that can/can't be bridged. Can also be managed using the `filter` and
# `filter-mode` management commands.
#
# Filters do not affect direct chats.
# An empty blacklist will essentially disable the filter.
filter:
# Filter mode to use. Either "blacklist" or "whitelist".
# If the mode is "blacklist", the listed chats will never be bridged.
# If the mode is "whitelist", only the listed chats can be bridged.
mode: blacklist
# The list of group/channel IDs to filter.
list: []
# The prefix for commands. Only required in non-management rooms.
command_prefix: "!tg"
# Messages sent upon joining a management room.
# Markdown is supported. The defaults are listed below.
management_room_text:
# Sent when joining a room.
welcome: "Hello, I'm a Telegram bridge bot."
# Sent when joining a management room and the user is already logged in.
welcome_connected: "Use `help` for help."
# Sent when joining a management room and the user is not logged in.
welcome_unconnected: "Use `help` for help or `login` to log in."
# Optional extra text sent when joining a management room.
additional_help: ""
# Send each message separately (for readability in some clients)
management_room_multiple_messages: false
# Permissions for using the bridge.
# Permitted values:
# relaybot - Only use the bridge via the relaybot, no access to commands.
# user - Relaybot level + access to commands to create bridges.
# puppeting - User level + logging in with a Telegram account.
# full - Full access to use the bridge, i.e. previous levels + Matrix login.
# admin - Full access to use the bridge and some extra administration commands.
# Permitted keys:
# * - All Matrix users
# domain - All users on that homeserver
# mxid - Specific user
permissions:
"*": "relaybot"
"public.example.com": "user"
"example.com": "full"
"@admin:example.com": "admin"
# Options related to the message relay Telegram bot.
relaybot:
private_chat:
# List of users to invite to the portal when someone starts a private chat with the bot.
# If empty, private chats with the bot won't create a portal.
invite: []
# Whether or not to bridge state change messages in relaybot private chats.
state_changes: true
# When private_chat_invite is empty, this message is sent to users /starting the
# relaybot. Telegram's "markdown" is supported.
message: This is a Matrix bridge relaybot and does not support direct chats
# List of users to invite to all group chat portals created by the bridge.
group_chat_invite: []
# Whether or not the relaybot should not bridge events in unbridged group chats.
# If false, portals will be created when the relaybot receives messages, just like normal
# users. This behavior is usually not desirable, as it interferes with manually bridging
# the chat to another room.
ignore_unbridged_group_chat: true
# Whether or not to allow creating portals from Telegram.
authless_portals: true
# Whether or not to allow Telegram group admins to use the bot commands.
whitelist_group_admins: true
# Whether or not to ignore incoming events sent by the relay bot.
ignore_own_incoming_events: true
# List of usernames/user IDs who are also allowed to use the bot commands.
whitelist:
- myusername
- 12345678
# Telegram config
telegram:
# Get your own API keys at https://my.telegram.org/apps
api_id: 12345
api_hash: tjyd5yge35lbodk1xwzw2jstp90k55qz
# (Optional) Create your own bot at https://t.me/BotFather
bot_token: disabled
# Telethon connection options.
connection:
# The timeout in seconds to be used when connecting.
timeout: 120
# How many times the reconnection should retry, either on the initial connection or when
# Telegram disconnects us. May be set to a negative or null value for infinite retries, but
# this is not recommended, since the program can get stuck in an infinite loop.
retries: 5
# The delay in seconds to sleep between automatic reconnections.
retry_delay: 1
# The threshold below which the library should automatically sleep on flood wait errors
# (inclusive). For instance, if a FloodWaitError for 17s occurs and flood_sleep_threshold
# is 20s, the library will sleep automatically. If the error was for 21s, it would raise
# the error instead. Values larger than a day (86400) will be changed to a day.
flood_sleep_threshold: 60
# How many times a request should be retried. Request are retried when Telegram is having
# internal issues, when there is a FloodWaitError less than flood_sleep_threshold, or when
# there's a migrate error. May take a negative or null value for infinite retries, but this
# is not recommended, since some requests can always trigger a call fail (such as searching
# for messages).
request_retries: 5
# Device info sent to Telegram.
device_info:
# "auto" = OS name+version.
device_model: auto
# "auto" = Telethon version.
system_version: auto
# "auto" = mautrix-telegram version.
app_version: auto
lang_code: en
system_lang_code: en
# Custom server to connect to.
server:
# Set to true to use these server settings. If false, will automatically
# use production server assigned by Telegram. Set to false in production.
enabled: false
# The DC ID to connect to.
dc: 2
# The IP to connect to.
ip: 149.154.167.40
# The port to connect to. 443 may not work, 80 is better and both are equally secure.
port: 80
# Telethon proxy configuration.
# You must install PySocks from pip for proxies to work.
proxy:
# Allowed types: disabled, socks4, socks5, http, mtproxy
type: disabled
# Proxy IP address and port.
address: 127.0.0.1
port: 1080
# Whether or not to perform DNS resolving remotely. Only for socks/http proxies.
rdns: true
# Proxy authentication (optional). Put MTProxy secret in password field.
username: ""
password: ""
# Python logging configuration.
#
# See section 16.7.2 of the Python documentation for more info:
# https://docs.python.org/3.6/library/logging.config.html#configuration-dictionary-schema
logging:
version: 1
formatters:
colored:
(): mautrix_telegram.util.ColorFormatter
format: "[%(asctime)s] [%(levelname)s@%(name)s] %(message)s"
normal:
format: "[%(asctime)s] [%(levelname)s@%(name)s] %(message)s"
handlers:
file:
class: logging.handlers.RotatingFileHandler
formatter: normal
filename: ./mautrix-telegram.log
maxBytes: 10485760
backupCount: 10
console:
class: logging.StreamHandler
formatter: colored
loggers:
mau:
level: DEBUG
telethon:
level: INFO
aiohttp:
level: INFO
root:
level: DEBUG
handlers: [file, console]
-2
View File
@@ -1,2 +0,0 @@
from .from_matrix import matrix_reply_to_telegram, matrix_to_telegram
from .from_telegram import telegram_reply_to_matrix, telegram_to_matrix
@@ -1,105 +0,0 @@
# mautrix-telegram - A Matrix-Telegram puppeting bridge
# Copyright (C) 2021 Tulir Asokan
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
from __future__ import annotations
import re
from telethon import TelegramClient
from telethon.helpers import add_surrogate, del_surrogate
from telethon.tl.types import MessageEntityItalic, TypeMessageEntity
from mautrix.types import MessageEventContent, RoomID
from ...db import Message as DBMessage
from ...types import TelegramID
from .parser import MatrixParser
command_regex = re.compile(r"^!([A-Za-z0-9@]+)")
not_command_regex = re.compile(r"^\\(![A-Za-z0-9@]+)")
MAX_LENGTH = 4096
CUTOFF_TEXT = " [message cut]"
CUT_MAX_LENGTH = MAX_LENGTH - len(CUTOFF_TEXT)
class FormatError(Exception):
pass
async def matrix_reply_to_telegram(
content: MessageEventContent, tg_space: TelegramID, room_id: RoomID | None = None
) -> TelegramID | None:
event_id = content.get_reply_to()
if not event_id:
return
content.trim_reply_fallback()
message = await DBMessage.get_by_mxid(event_id, room_id, tg_space)
if message:
return message.tgid
return None
async def matrix_to_telegram(
client: TelegramClient, *, text: str | None = None, html: str | None = None
) -> tuple[str, list[TypeMessageEntity]]:
if html is not None:
return await _matrix_html_to_telegram(client, html)
elif text is not None:
return _matrix_text_to_telegram(text), []
else:
raise ValueError("text or html must be provided to convert formatting")
async def _matrix_html_to_telegram(
client: TelegramClient, html: str
) -> tuple[str, list[TypeMessageEntity]]:
try:
html = command_regex.sub(r"<command>\1</command>", html)
html = html.replace("\t", " " * 4)
html = not_command_regex.sub(r"\1", html)
parsed = await MatrixParser(client).parse(add_surrogate(html))
text = del_surrogate(parsed.text.strip())
text, entities = _cut_long_message(text, parsed.telegram_entities)
return text, entities
except Exception as e:
raise FormatError(f"Failed to convert Matrix format: {html}") from e
def _cut_long_message(
message: str, entities: list[TypeMessageEntity]
) -> tuple[str, list[TypeMessageEntity]]:
if len(message) > MAX_LENGTH:
message = message[0:CUT_MAX_LENGTH] + CUTOFF_TEXT
new_entities = []
for entity in entities:
if entity.offset > CUT_MAX_LENGTH:
continue
if entity.offset + entity.length > CUT_MAX_LENGTH:
entity.length = CUT_MAX_LENGTH - entity.offset
new_entities.append(entity)
new_entities.append(MessageEntityItalic(CUT_MAX_LENGTH, len(CUTOFF_TEXT)))
entities = new_entities
return message, entities
def _matrix_text_to_telegram(text: str) -> str:
text = command_regex.sub(r"/\1", text)
text = text.replace("\t", " " * 4)
text = not_command_regex.sub(r"\1", text)
return text
@@ -1,99 +0,0 @@
# mautrix-telegram - A Matrix-Telegram puppeting bridge
# Copyright (C) 2021 Tulir Asokan
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
from __future__ import annotations
import logging
from telethon import TelegramClient
from mautrix.types import RoomID, UserID
from mautrix.util.formatter import MatrixParser as BaseMatrixParser, RecursionContext
from mautrix.util.formatter.html_reader_htmlparser import HTMLNode, read_html
from mautrix.util.logging import TraceLogger
from ... import portal as po, puppet as pu, user as u
from .telegram_message import TelegramEntityType, TelegramMessage
log: TraceLogger = logging.getLogger("mau.fmt.mx")
class MatrixParser(BaseMatrixParser[TelegramMessage]):
e = TelegramEntityType
fs = TelegramMessage
read_html = staticmethod(read_html)
client: TelegramClient
def __init__(self, client: TelegramClient) -> None:
self.client = client
async def custom_node_to_fstring(
self, node: HTMLNode, ctx: RecursionContext
) -> TelegramMessage | None:
msg = await self.tag_aware_parse_node(node, ctx)
if node.tag == "command":
return msg.prepend("/").format(TelegramEntityType.COMMAND)
return None
async def user_pill_to_fstring(self, msg: TelegramMessage, user_id: UserID) -> TelegramMessage:
user = await pu.Puppet.get_by_mxid(user_id) or await u.User.get_by_mxid(
user_id, create=False
)
if not user:
return msg
if user.tg_username:
return TelegramMessage(f"@{user.tg_username}").format(TelegramEntityType.MENTION)
elif user.tgid:
displayname = user.plain_displayname or msg.text
msg = TelegramMessage(displayname)
try:
input_entity = self.client.get_input_entity(user.tgid)
except (ValueError, TypeError) as e:
log.trace(f"Dropping mention of {user.tgid}: {e}")
else:
msg = msg.format(TelegramEntityType.MENTION_NAME, user_id=input_entity)
return msg
async def url_to_fstring(self, msg: TelegramMessage, url: str) -> TelegramMessage:
if url == msg.text:
return msg.format(self.e.URL)
else:
return msg.format(self.e.INLINE_URL, url=url)
async def room_pill_to_fstring(self, msg: TelegramMessage, room_id: RoomID) -> TelegramMessage:
username = po.Portal.get_username_from_mx_alias(room_id)
portal = await po.Portal.find_by_username(username)
if portal and portal.username:
return TelegramMessage(f"@{portal.username}").format(TelegramEntityType.MENTION)
async def header_to_fstring(self, node: HTMLNode, ctx: RecursionContext) -> TelegramMessage:
children = await self.node_to_fstrings(node, ctx)
length = int(node.tag[1])
prefix = "#" * length + " "
return TelegramMessage.join(children, "").prepend(prefix).format(TelegramEntityType.BOLD)
async def blockquote_to_fstring(
self, node: HTMLNode, ctx: RecursionContext
) -> TelegramMessage:
msg = await self.tag_aware_parse_node(node, ctx)
children = msg.trim().split("\n")
children = [child.prepend("> ") for child in children]
return TelegramMessage.join(children, "\n")
async def color_to_fstring(self, msg: TelegramMessage, color: str) -> TelegramMessage:
return msg
async def spoiler_to_fstring(self, msg: TelegramMessage, spoiler: str) -> TelegramMessage:
return msg
@@ -1,120 +0,0 @@
# mautrix-telegram - A Matrix-Telegram puppeting bridge
# Copyright (C) 2021 Tulir Asokan
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
from __future__ import annotations
from typing import Any, Type
from enum import Enum
from telethon.tl.types import (
InputMessageEntityMentionName as InputMentionName,
MessageEntityBlockquote as Blockquote,
MessageEntityBold as Bold,
MessageEntityBotCommand as Command,
MessageEntityCode as Code,
MessageEntityEmail as Email,
MessageEntityItalic as Italic,
MessageEntityMention as Mention,
MessageEntityMentionName as MentionName,
MessageEntityPre as Pre,
MessageEntityStrike as Strike,
MessageEntityTextUrl as TextURL,
MessageEntityUnderline as Underline,
MessageEntityUrl as URL,
TypeMessageEntity,
)
from mautrix.util.formatter import EntityString, SemiAbstractEntity
class TelegramEntityType(Enum):
"""EntityType is a Matrix formatting entity type."""
BOLD = Bold
ITALIC = Italic
STRIKETHROUGH = Strike
UNDERLINE = Underline
URL = URL
INLINE_URL = TextURL
EMAIL = Email
PREFORMATTED = Pre
INLINE_CODE = Code
BLOCKQUOTE = Blockquote
MENTION = Mention
MENTION_NAME = InputMentionName
COMMAND = Command
USER_MENTION = 1
ROOM_MENTION = 2
HEADER = 3
class TelegramEntity(SemiAbstractEntity):
internal: TypeMessageEntity
def __init__(
self,
type: TelegramEntityType | Type[TypeMessageEntity],
offset: int,
length: int,
extra_info: dict[str, Any],
) -> None:
if isinstance(type, TelegramEntityType):
if isinstance(type.value, int):
raise ValueError(f"Can't create Entity with non-Telegram EntityType {type}")
type = type.value
self.internal = type(offset=offset, length=length, **extra_info)
def copy(self) -> TelegramEntity:
extra_info = {}
if isinstance(self.internal, Pre):
extra_info["language"] = self.internal.language
elif isinstance(self.internal, TextURL):
extra_info["url"] = self.internal.url
elif isinstance(self.internal, (MentionName, InputMentionName)):
extra_info["user_id"] = self.internal.user_id
return TelegramEntity(
type(self.internal),
offset=self.internal.offset,
length=self.internal.length,
extra_info=extra_info,
)
def __repr__(self) -> str:
return str(self.internal)
@property
def offset(self) -> int:
return self.internal.offset
@offset.setter
def offset(self, value: int) -> None:
self.internal.offset = value
@property
def length(self) -> int:
return self.internal.length
@length.setter
def length(self, value: int) -> None:
self.internal.length = value
class TelegramMessage(EntityString[TelegramEntity, TelegramEntityType]):
entity_class = TelegramEntity
@property
def telegram_entities(self) -> list[TypeMessageEntity]:
return [entity.internal for entity in self.entities]
-365
View File
@@ -1,365 +0,0 @@
# mautrix-telegram - A Matrix-Telegram puppeting bridge
# Copyright (C) 2021 Tulir Asokan
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
from __future__ import annotations
from html import escape
import logging
import re
from telethon.errors import RPCError
from telethon.helpers import add_surrogate, del_surrogate
from telethon.tl.custom import Message
from telethon.tl.types import (
MessageEntityBlockquote,
MessageEntityBold,
MessageEntityBotCommand,
MessageEntityCashtag,
MessageEntityCode,
MessageEntityEmail,
MessageEntityHashtag,
MessageEntityItalic,
MessageEntityMention,
MessageEntityMentionName,
MessageEntityPhone,
MessageEntityPre,
MessageEntityStrike,
MessageEntityTextUrl,
MessageEntityUnderline,
MessageEntityUrl,
MessageFwdHeader,
PeerChannel,
PeerChat,
PeerUser,
SponsoredMessage,
TypeMessageEntity,
)
from mautrix.appservice import IntentAPI
from mautrix.types import (
EventType,
Format,
MessageType,
RelatesTo,
RelationType,
TextMessageEventContent,
)
from .. import abstract_user as au, portal as po, puppet as pu, user as u
from ..db import Message as DBMessage
from ..types import TelegramID
log: logging.Logger = logging.getLogger("mau.fmt.tg")
async def telegram_reply_to_matrix(evt: Message, source: au.AbstractUser) -> RelatesTo | None:
if evt.reply_to:
space = (
evt.peer_id.channel_id
if isinstance(evt, Message) and isinstance(evt.peer_id, PeerChannel)
else source.tgid
)
msg = await DBMessage.get_one_by_tgid(TelegramID(evt.reply_to.reply_to_msg_id), space)
if msg:
return RelatesTo(rel_type=RelationType.REPLY, event_id=msg.mxid)
return None
async def _add_forward_header(
source: au.AbstractUser, content: TextMessageEventContent, fwd_from: MessageFwdHeader
) -> None:
if not content.formatted_body or content.format != Format.HTML:
content.format = Format.HTML
content.formatted_body = escape(content.body)
fwd_from_html, fwd_from_text = None, None
if isinstance(fwd_from.from_id, PeerUser):
user = await u.User.get_by_tgid(TelegramID(fwd_from.from_id.user_id))
if user:
fwd_from_text = user.displayname or user.mxid
fwd_from_html = (
f"<a href='https://matrix.to/#/{user.mxid}'>{escape(fwd_from_text)}</a>"
)
if not fwd_from_text:
puppet = await pu.Puppet.get_by_tgid(
TelegramID(fwd_from.from_id.user_id), create=False
)
if puppet and puppet.displayname:
fwd_from_text = puppet.displayname or puppet.mxid
fwd_from_html = (
f"<a href='https://matrix.to/#/{puppet.mxid}'>{escape(fwd_from_text)}</a>"
)
if not fwd_from_text:
try:
user = await source.client.get_entity(fwd_from.from_id)
if user:
fwd_from_text, _ = pu.Puppet.get_displayname(user, False)
fwd_from_html = f"<b>{escape(fwd_from_text)}</b>"
except (ValueError, RPCError):
fwd_from_text = fwd_from_html = "unknown user"
elif isinstance(fwd_from.from_id, (PeerChannel, PeerChat)):
from_id = (
fwd_from.from_id.chat_id
if isinstance(fwd_from.from_id, PeerChat)
else fwd_from.from_id.channel_id
)
portal = await po.Portal.get_by_tgid(TelegramID(from_id))
if portal and portal.title:
fwd_from_text = portal.title
if portal.alias:
fwd_from_html = (
f"<a href='https://matrix.to/#/{portal.alias}'>{escape(fwd_from_text)}</a>"
)
else:
fwd_from_html = f"channel <b>{escape(fwd_from_text)}</b>"
else:
try:
channel = await source.client.get_entity(fwd_from.from_id)
if channel:
fwd_from_text = f"channel {channel.title}"
fwd_from_html = f"channel <b>{escape(channel.title)}</b>"
except (ValueError, RPCError):
fwd_from_text = fwd_from_html = "unknown channel"
elif fwd_from.from_name:
fwd_from_text = fwd_from.from_name
fwd_from_html = f"<b>{escape(fwd_from.from_name)}</b>"
else:
fwd_from_text = "unknown source"
fwd_from_html = f"unknown source"
content.body = "\n".join([f"> {line}" for line in content.body.split("\n")])
content.body = f"Forwarded from {fwd_from_text}:\n{content.body}"
content.formatted_body = (
f"Forwarded message from {fwd_from_html}<br/>"
f"<tg-forward><blockquote>{content.formatted_body}</blockquote></tg-forward>"
)
async def _add_reply_header(
source: au.AbstractUser, content: TextMessageEventContent, evt: Message, main_intent: IntentAPI
) -> None:
space = (
evt.peer_id.channel_id
if isinstance(evt, Message) and isinstance(evt.peer_id, PeerChannel)
else source.tgid
)
msg = await DBMessage.get_one_by_tgid(TelegramID(evt.reply_to.reply_to_msg_id), space)
if not msg:
return
content.relates_to = RelatesTo(rel_type=RelationType.REPLY, event_id=msg.mxid)
try:
event = await main_intent.get_event(msg.mx_room, msg.mxid)
if event.type == EventType.ROOM_ENCRYPTED and source.bridge.matrix.e2ee:
event = await source.bridge.matrix.e2ee.decrypt(event)
if isinstance(event.content, TextMessageEventContent):
event.content.trim_reply_fallback()
puppet = await pu.Puppet.get_by_mxid(event.sender, create=False)
content.set_reply(event, displayname=puppet.displayname if puppet else event.sender)
except Exception:
log.exception("Failed to get event to add reply fallback")
async def telegram_to_matrix(
evt: Message | SponsoredMessage,
source: au.AbstractUser,
main_intent: IntentAPI | None = None,
prefix_text: str | None = None,
prefix_html: str | None = None,
override_text: str = None,
override_entities: list[TypeMessageEntity] = None,
no_reply_fallback: bool = False,
require_html: bool = False,
) -> TextMessageEventContent:
content = TextMessageEventContent(
msgtype=MessageType.TEXT,
body=add_surrogate(override_text or evt.message),
)
entities = override_entities or evt.entities
if entities:
content.format = Format.HTML
html = await _telegram_entities_to_matrix_catch(add_surrogate(content.body), entities)
content.formatted_body = del_surrogate(html).replace("\n", "<br/>")
def force_html():
if not content.formatted_body:
content.format = Format.HTML
content.formatted_body = escape(content.body)
if require_html:
force_html()
if prefix_html:
force_html()
content.formatted_body = prefix_html + content.formatted_body
if prefix_text:
content.body = prefix_text + content.body
if getattr(evt, "fwd_from", None):
await _add_forward_header(source, content, evt.fwd_from)
if getattr(evt, "reply_to", None) and not no_reply_fallback:
await _add_reply_header(source, content, evt, main_intent)
if isinstance(evt, Message) and evt.post and evt.post_author:
force_html()
content.body += f"\n- {evt.post_author}"
content.formatted_body += f"<br/><i>- <u>{evt.post_author}</u></i>"
return content
async def _telegram_entities_to_matrix_catch(text: str, entities: list[TypeMessageEntity]) -> str:
try:
return await _telegram_entities_to_matrix(text, entities)
except Exception:
log.exception(
"Failed to convert Telegram format:\nmessage=%s\nentities=%s", text, entities
)
return "[failed conversion in _telegram_entities_to_matrix]"
async def _telegram_entities_to_matrix(
text: str, entities: list[TypeMessageEntity], offset: int = 0, length: int = None
) -> str:
if not entities:
return escape(text)
if length is None:
length = len(text)
html = []
last_offset = 0
for i, entity in enumerate(entities):
if entity.offset > offset + length:
break
relative_offset = entity.offset - offset
if relative_offset > last_offset:
html.append(escape(text[last_offset:relative_offset]))
elif relative_offset < last_offset:
continue
skip_entity = False
entity_text = await _telegram_entities_to_matrix(
text=text[relative_offset : relative_offset + entity.length],
entities=entities[i + 1 :],
offset=entity.offset,
length=entity.length,
)
entity_type = type(entity)
if entity_type == MessageEntityBold:
html.append(f"<strong>{entity_text}</strong>")
elif entity_type == MessageEntityItalic:
html.append(f"<em>{entity_text}</em>")
elif entity_type == MessageEntityUnderline:
html.append(f"<u>{entity_text}</u>")
elif entity_type == MessageEntityStrike:
html.append(f"<del>{entity_text}</del>")
elif entity_type == MessageEntityBlockquote:
html.append(f"<blockquote>{entity_text}</blockquote>")
elif entity_type == MessageEntityCode:
html.append(
f"<pre><code>{entity_text}</code></pre>"
if "\n" in entity_text
else f"<code>{entity_text}</code>"
)
elif entity_type == MessageEntityPre:
skip_entity = _parse_pre(html, entity_text, entity.language)
elif entity_type == MessageEntityMention:
skip_entity = await _parse_mention(html, entity_text)
elif entity_type == MessageEntityMentionName:
skip_entity = await _parse_name_mention(html, entity_text, TelegramID(entity.user_id))
elif entity_type == MessageEntityEmail:
html.append(f"<a href='mailto:{entity_text}'>{entity_text}</a>")
elif entity_type in (MessageEntityTextUrl, MessageEntityUrl):
skip_entity = await _parse_url(
html, entity_text, entity.url if entity_type == MessageEntityTextUrl else None
)
elif entity_type == MessageEntityBotCommand:
html.append(f"<font color='blue'>{entity_text}</font>")
elif entity_type in (MessageEntityHashtag, MessageEntityCashtag, MessageEntityPhone):
html.append(f"<font color='blue'>{entity_text}</font>")
else:
skip_entity = True
last_offset = relative_offset + (0 if skip_entity else entity.length)
html.append(escape(text[last_offset:]))
return "".join(html)
def _parse_pre(html: list[str], entity_text: str, language: str) -> bool:
if language:
html.append(f"<pre><code class='language-{language}'>{entity_text}</code></pre>")
else:
html.append(f"<pre><code>{entity_text}</code></pre>")
return False
async def _parse_mention(html: list[str], entity_text: str) -> bool:
username = entity_text[1:]
user = await u.User.find_by_username(username) or await pu.Puppet.find_by_username(username)
if user:
mxid = user.mxid
else:
portal = await po.Portal.find_by_username(username)
mxid = portal.alias or portal.mxid if portal else None
if mxid:
html.append(f"<a href='https://matrix.to/#/{mxid}'>{entity_text}</a>")
else:
return True
return False
async def _parse_name_mention(html: list[str], entity_text: str, user_id: TelegramID) -> bool:
user = await u.User.get_by_tgid(user_id)
if user:
mxid = user.mxid
else:
puppet = await pu.Puppet.get_by_tgid(user_id, create=False)
mxid = puppet.mxid if puppet else None
if mxid:
html.append(f"<a href='https://matrix.to/#/{mxid}'>{entity_text}</a>")
else:
return True
return False
message_link_regex = re.compile(
r"https?://t(?:elegram)?\.(?:me|dog)/([A-Za-z][A-Za-z0-9_]{3,}[A-Za-z0-9])/([0-9]{1,50})"
)
async def _parse_url(html: list[str], entity_text: str, url: str) -> bool:
url = escape(url) if url else entity_text
if not url.startswith(("https://", "http://", "ftp://", "magnet://")):
url = "http://" + url
message_link_match = message_link_regex.match(url)
if message_link_match:
group, msgid_str = message_link_match.groups()
msgid = int(msgid_str)
portal = await po.Portal.find_by_username(group)
if portal:
message = await DBMessage.get_one_by_tgid(TelegramID(msgid), portal.tgid)
if message:
url = f"https://matrix.to/#/{portal.mxid}/{message.mxid}"
html.append(f"<a href='{url}'>{entity_text}</a>")
return False
-49
View File
@@ -1,49 +0,0 @@
import os
import shutil
import subprocess
from . import __version__
cmd_env = {
"PATH": os.environ["PATH"],
"HOME": os.environ["HOME"],
"LANG": "C",
"LC_ALL": "C",
}
def run(cmd):
return subprocess.check_output(cmd, stderr=subprocess.DEVNULL, env=cmd_env)
if os.path.exists(".git") and shutil.which("git"):
try:
git_revision = run(["git", "rev-parse", "HEAD"]).strip().decode("ascii")
git_revision_url = f"https://github.com/mautrix/telegram/commit/{git_revision}"
git_revision = git_revision[:8]
except (subprocess.SubprocessError, OSError):
git_revision = "unknown"
git_revision_url = None
try:
git_tag = run(["git", "describe", "--exact-match", "--tags"]).strip().decode("ascii")
except (subprocess.SubprocessError, OSError):
git_tag = None
else:
git_revision = "unknown"
git_revision_url = None
git_tag = None
git_tag_url = f"https://github.com/mautrix/telegram/releases/tag/{git_tag}" if git_tag else None
if git_tag and __version__ == git_tag[1:].replace("-", ""):
version = __version__
linkified_version = f"[{version}]({git_tag_url})"
else:
if not __version__.endswith("+dev"):
__version__ += "+dev"
version = f"{__version__}.{git_revision}"
if git_revision_url:
linkified_version = f"{__version__}.[{git_revision}]({git_revision_url})"
else:
linkified_version = version
-423
View File
@@ -1,423 +0,0 @@
# mautrix-telegram - A Matrix-Telegram puppeting bridge
# Copyright (C) 2021 Tulir Asokan
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
from __future__ import annotations
from typing import TYPE_CHECKING, Iterable
from mautrix.appservice import DOUBLE_PUPPET_SOURCE_KEY
from mautrix.bridge import BaseMatrixHandler
from mautrix.errors import MatrixError
from mautrix.types import (
Event,
EventID,
EventType,
MemberStateEventContent,
MessageType,
PresenceEvent,
PresenceState,
ReceiptEvent,
ReceiptEventContent,
ReceiptType,
RedactionEvent,
RoomAvatarStateEventContent as AvatarContent,
RoomID,
RoomNameStateEventContent as NameContent,
RoomTopicStateEventContent as TopicContent,
SingleReceiptEventContent,
StateEvent,
TextMessageEventContent,
TypingEvent,
UserID,
)
from . import commands as com, portal as po, puppet as pu, user as u
if TYPE_CHECKING:
from .__main__ import TelegramBridge
class MatrixHandler(BaseMatrixHandler):
commands: com.CommandProcessor
_previously_typing: dict[RoomID, set[UserID]]
def __init__(self, bridge: "TelegramBridge") -> None:
prefix, suffix = bridge.config["bridge.username_template"].format(userid=":").split(":")
homeserver = bridge.config["homeserver.domain"]
self.user_id_prefix = f"@{prefix}"
self.user_id_suffix = f"{suffix}:{homeserver}"
super().__init__(command_processor=com.CommandProcessor(bridge), bridge=bridge)
self._previously_typing = {}
async def handle_puppet_invite(
self, room_id: RoomID, puppet: pu.Puppet, inviter: u.User, event_id: EventID
) -> None:
intent = puppet.default_mxid_intent
self.log.debug(f"{inviter.mxid} invited puppet for {puppet.tgid} to {room_id}")
if not await inviter.is_logged_in():
await intent.error_and_leave(
room_id, text="Please log in before inviting Telegram puppets."
)
return
portal = await po.Portal.get_by_mxid(room_id)
if portal:
if portal.peer_type == "user":
await intent.error_and_leave(
room_id, text="You can not invite additional users to private chats."
)
return
await portal.invite_telegram(inviter, puppet)
await intent.join_room(room_id)
return
try:
members = await intent.get_room_members(room_id)
except MatrixError:
self.log.exception(f"Failed to get members after joining {room_id} as {intent.mxid}")
return
if self.az.bot_mxid not in members:
if len(members) > 2:
await intent.error_and_leave(
room_id,
text=None,
html=(
f"Please invite "
f"<a href='https://matrix.to/#/{self.az.bot_mxid}'>the bridge bot</a> "
f"first if you want to create a Telegram chat."
),
)
return
await intent.join_room(room_id)
portal = await po.Portal.get_by_tgid(
puppet.tgid, tg_receiver=inviter.tgid, peer_type="user"
)
if portal.mxid:
try:
await portal.invite_to_matrix(inviter.mxid)
await intent.send_notice(
room_id,
text=f"You already have a private chat with me: {portal.mxid}",
html=(
"You already have a private chat with me: "
f"<a href='https://matrix.to/#/{portal.mxid}'>Link to room</a>"
),
)
await intent.leave_room(room_id)
return
except MatrixError:
pass
portal.mxid = room_id
e2be_ok = await portal.check_dm_encryption()
await portal.save()
await inviter.register_portal(portal)
if e2be_ok is True:
evt_type, content = await self.e2ee.encrypt(
room_id,
EventType.ROOM_MESSAGE,
TextMessageEventContent(
msgtype=MessageType.NOTICE,
body=(
"Portal to private chat created and end-to-bridge encryption enabled."
),
),
)
await intent.send_message_event(room_id, evt_type, content)
else:
message = "Portal to private chat created."
if e2be_ok is False:
message += "\n\nWarning: Failed to enable end-to-bridge encryption"
await intent.send_notice(room_id, message)
await portal.update_bridge_info()
else:
await intent.join_room(room_id)
await intent.send_notice(
room_id,
"This puppet will remain inactive until a Telegram chat is created for this room.",
)
async def handle_invite(
self, room_id: RoomID, user_id: UserID, inviter: u.User, event_id: EventID
) -> None:
user = await u.User.get_by_mxid(user_id, create=False)
if not user:
return
await user.ensure_started()
portal = await po.Portal.get_by_mxid(room_id)
if user and await user.has_full_access(allow_bot=True):
if portal and portal.allow_bridging:
await portal.invite_telegram(inviter, user)
async def handle_join(self, room_id: RoomID, user_id: UserID, event_id: EventID) -> None:
user = await u.User.get_and_start_by_mxid(user_id)
portal = await po.Portal.get_by_mxid(room_id)
if not portal or not portal.allow_bridging:
return
if not user.relaybot_whitelisted:
await portal.main_intent.kick_user(
room_id, user.mxid, "You are not whitelisted on this Telegram bridge."
)
return
elif not await user.is_logged_in() and not portal.has_bot:
await portal.main_intent.kick_user(
room_id,
user.mxid,
"This chat does not have a bot relaying messages for unauthenticated users.",
)
return
self.log.debug(f"{user.mxid} joined {room_id}")
if await user.is_logged_in() or portal.has_bot:
await portal.join_matrix(user, event_id)
async def handle_leave(self, room_id: RoomID, user_id: UserID, event_id: EventID) -> None:
self.log.debug(f"{user_id} left {room_id}")
portal = await po.Portal.get_by_mxid(room_id)
if not portal or not portal.allow_bridging:
return
user = await u.User.get_by_mxid(user_id, create=False)
if not user:
return
await user.ensure_started()
await portal.leave_matrix(user, event_id)
async def handle_kick_ban(
self,
ban: bool,
room_id: RoomID,
user_id: UserID,
sender: UserID,
reason: str,
event_id: EventID,
) -> None:
action = "banned" if ban else "kicked"
self.log.debug(f"{user_id} was {action} from {room_id} by {sender} for {reason}")
portal = await po.Portal.get_by_mxid(room_id)
if not portal or not portal.allow_bridging:
return
if user_id == self.az.bot_mxid:
# Direct chat portal unbridging is handled in portal.kick_matrix
if portal.peer_type != "user":
await portal.unbridge()
return
sender = await u.User.get_by_mxid(sender, create=False)
if not sender:
return
await sender.ensure_started()
puppet = await pu.Puppet.get_by_mxid(user_id)
if puppet:
if ban:
await portal.ban_matrix(puppet, sender)
else:
await portal.kick_matrix(puppet, sender)
return
user = await u.User.get_by_mxid(user_id, create=False)
if not user:
return
await user.ensure_started()
if ban:
await portal.ban_matrix(user, sender)
else:
await portal.kick_matrix(user, sender)
async def handle_kick(
self, room_id: RoomID, user_id: UserID, kicked_by: UserID, reason: str, event_id: EventID
) -> None:
await self.handle_kick_ban(False, room_id, user_id, kicked_by, reason, event_id)
async def handle_unban(
self, room_id: RoomID, user_id: UserID, unbanned_by: UserID, reason: str, event_id: EventID
) -> None:
# TODO handle unbans properly instead of handling it as a kick
await self.handle_kick_ban(False, room_id, user_id, unbanned_by, reason, event_id)
async def handle_ban(
self, room_id: RoomID, user_id: UserID, banned_by: UserID, reason: str, event_id: EventID
) -> None:
await self.handle_kick_ban(True, room_id, user_id, banned_by, reason, event_id)
async def allow_message(self, user: u.User) -> bool:
return user.relaybot_whitelisted
async def allow_command(self, user: u.User) -> bool:
return user.whitelisted
@staticmethod
async def allow_bridging_message(user: u.User, portal: po.Portal) -> bool:
return await user.is_logged_in() or portal.has_bot
@staticmethod
async def handle_redaction(evt: RedactionEvent) -> None:
sender = await u.User.get_and_start_by_mxid(evt.sender)
if not sender.relaybot_whitelisted:
return
portal = await po.Portal.get_by_mxid(evt.room_id)
if not portal or not portal.allow_bridging:
return
await portal.handle_matrix_deletion(sender, evt.redacts, evt.event_id)
@staticmethod
async def handle_power_levels(evt: StateEvent) -> None:
portal = await po.Portal.get_by_mxid(evt.room_id)
sender = await u.User.get_and_start_by_mxid(evt.sender)
if await sender.has_full_access(allow_bot=True) and portal and portal.allow_bridging:
await portal.handle_matrix_power_levels(
sender, evt.content.users, evt.unsigned.prev_content.users, evt.event_id
)
@staticmethod
async def handle_room_meta(
evt_type: EventType,
room_id: RoomID,
sender_mxid: UserID,
content: NameContent | AvatarContent | TopicContent,
event_id: EventID,
) -> None:
portal = await po.Portal.get_by_mxid(room_id)
sender = await u.User.get_and_start_by_mxid(sender_mxid)
if await sender.has_full_access(allow_bot=True) and portal and portal.allow_bridging:
handler, content_type, content_key = {
EventType.ROOM_NAME: (portal.handle_matrix_title, NameContent, "name"),
EventType.ROOM_TOPIC: (portal.handle_matrix_about, TopicContent, "topic"),
EventType.ROOM_AVATAR: (portal.handle_matrix_avatar, AvatarContent, "url"),
}[evt_type]
if not isinstance(content, content_type):
return
await handler(sender, content[content_key], event_id)
@staticmethod
async def handle_room_pin(
room_id: RoomID,
sender_mxid: UserID,
new_events: set[str],
old_events: set[str],
event_id: EventID,
) -> None:
portal = await po.Portal.get_by_mxid(room_id)
sender = await u.User.get_and_start_by_mxid(sender_mxid)
if await sender.has_full_access(allow_bot=True) and portal and portal.allow_bridging:
if not new_events:
await portal.handle_matrix_unpin_all(sender, event_id)
else:
changes = {
event_id: event_id in new_events for event_id in new_events ^ old_events
}
await portal.handle_matrix_pin(sender, changes, event_id)
@staticmethod
async def handle_room_upgrade(
room_id: RoomID, sender: UserID, new_room_id: RoomID, event_id: EventID
) -> None:
portal = await po.Portal.get_by_mxid(room_id)
if portal and portal.allow_bridging:
await portal.handle_matrix_upgrade(sender, new_room_id, event_id)
async def handle_member_info_change(
self,
room_id: RoomID,
user_id: UserID,
profile: MemberStateEventContent,
prev_profile: MemberStateEventContent,
event_id: EventID,
) -> None:
if profile.displayname == prev_profile.displayname:
return
portal = await po.Portal.get_by_mxid(room_id)
if not portal or not portal.has_bot or not portal.allow_bridging:
return
user = await u.User.get_and_start_by_mxid(user_id)
if await user.needs_relaybot(portal):
await portal.name_change_matrix(
user, profile.displayname, prev_profile.displayname, event_id
)
async def handle_read_receipt(
self, user: u.User, portal: po.Portal, event_id: EventID, data: SingleReceiptEventContent
) -> None:
if not portal.allow_bridging:
return
await portal.mark_read(user, event_id, data.get("ts", 0))
@staticmethod
async def handle_presence(user_id: UserID, presence: PresenceState) -> None:
user = await u.User.get_by_mxid(user_id, check_db=False, create=False)
if user and await user.is_logged_in():
await user.set_presence(presence == PresenceState.ONLINE)
async def handle_typing(self, room_id: RoomID, now_typing: set[UserID]) -> None:
portal = await po.Portal.get_by_mxid(room_id)
if not portal or not portal.allow_bridging:
return
previously_typing = self._previously_typing.get(room_id, set())
for user_id in set(previously_typing | now_typing):
is_typing = user_id in now_typing
was_typing = user_id in previously_typing
if is_typing and was_typing:
continue
user = await u.User.get_by_mxid(user_id, check_db=False, create=False)
if user and await user.is_logged_in():
await portal.set_typing(user, is_typing)
self._previously_typing[room_id] = now_typing
async def handle_ephemeral_event(
self, evt: ReceiptEvent | PresenceEvent | TypingEvent
) -> None:
if evt.type == EventType.RECEIPT:
await self.handle_receipt(evt)
elif evt.type == EventType.PRESENCE:
await self.handle_presence(evt.sender, evt.content.presence)
elif evt.type == EventType.TYPING:
await self.handle_typing(evt.room_id, set(evt.content.user_ids))
async def handle_event(self, evt: Event) -> None:
if evt.type == EventType.ROOM_REDACTION:
await self.handle_redaction(evt)
async def handle_state_event(self, evt: StateEvent) -> None:
if evt.type == EventType.ROOM_POWER_LEVELS:
await self.handle_power_levels(evt)
elif evt.type in (EventType.ROOM_NAME, EventType.ROOM_AVATAR, EventType.ROOM_TOPIC):
await self.handle_room_meta(
evt.type, evt.room_id, evt.sender, evt.content, evt.event_id
)
elif evt.type == EventType.ROOM_PINNED_EVENTS:
new_events = set(evt.content.pinned)
try:
old_events = set(evt.unsigned.prev_content.pinned)
except (KeyError, ValueError, TypeError, AttributeError):
old_events = set()
await self.handle_room_pin(
evt.room_id, evt.sender, new_events, old_events, evt.event_id
)
elif evt.type == EventType.ROOM_TOMBSTONE:
await self.handle_room_upgrade(
evt.room_id, evt.sender, evt.content.replacement_room, evt.event_id
)
File diff suppressed because it is too large Load Diff
-6
View File
@@ -1,6 +0,0 @@
from .deduplication import PortalDedup
from .media_fallback import make_contact_event_content, make_dice_event_content
from .participants import get_users
from .power_levels import get_base_power_levels, participants_to_power_levels
from .send_lock import PortalSendLock
from .sponsored_message import get_sponsored_message, make_sponsored_message_content
@@ -1,134 +0,0 @@
# mautrix-telegram - A Matrix-Telegram puppeting bridge
# Copyright (C) 2021 Tulir Asokan
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
from __future__ import annotations
from typing import Tuple
from collections import deque
import hashlib
from telethon.tl.patched import Message, MessageService
from telethon.tl.types import (
MessageMediaContact,
MessageMediaDocument,
MessageMediaGeo,
MessageMediaPhoto,
TypeMessage,
TypeUpdates,
UpdateNewChannelMessage,
UpdateNewMessage,
)
from mautrix.types import EventID
from .. import portal as po
from ..types import TelegramID
DedupMXID = Tuple[EventID, TelegramID]
class PortalDedup:
pre_db_check: bool = False
cache_queue_length: int = 20
_dedup: deque[str]
_dedup_mxid: dict[str, DedupMXID]
_dedup_action: deque[str]
_portal: po.Portal
def __init__(self, portal: po.Portal) -> None:
self._dedup = deque()
self._dedup_mxid = {}
self._dedup_action = deque()
self._portal = portal
@property
def _always_force_hash(self) -> bool:
return self._portal.peer_type == "chat"
@staticmethod
def _hash_event(event: TypeMessage) -> str:
# Non-channel messages are unique per-user (wtf telegram), so we have no other choice than
# to deduplicate based on a hash of the message content.
# The timestamp is only accurate to the second, so we can't rely solely on that either.
if isinstance(event, MessageService):
hash_content = [event.date.timestamp(), event.from_id, event.action]
else:
hash_content = [event.date.timestamp(), event.message.strip()]
if event.fwd_from:
hash_content += [event.fwd_from.from_id]
elif isinstance(event, Message) and event.media:
try:
hash_content += {
MessageMediaContact: lambda media: [media.user_id],
MessageMediaDocument: lambda media: [media.document.id],
MessageMediaPhoto: lambda media: [media.photo.id if media.photo else 0],
MessageMediaGeo: lambda media: [media.geo.long, media.geo.lat],
}[type(event.media)](event.media)
except KeyError:
pass
return hashlib.md5("-".join(str(a) for a in hash_content).encode("utf-8")).hexdigest()
def check_action(self, event: TypeMessage) -> bool:
evt_hash = self._hash_event(event) if self._always_force_hash else event.id
if evt_hash in self._dedup_action:
return True
self._dedup_action.append(evt_hash)
if len(self._dedup_action) > self.cache_queue_length:
self._dedup_action.popleft()
return False
def update(
self,
event: TypeMessage,
mxid: DedupMXID = None,
expected_mxid: DedupMXID | None = None,
force_hash: bool = False,
) -> DedupMXID | None:
evt_hash = self._hash_event(event) if self._always_force_hash or force_hash else event.id
try:
found_mxid = self._dedup_mxid[evt_hash]
except KeyError:
return EventID("None"), TelegramID(0)
if found_mxid != expected_mxid:
return found_mxid
self._dedup_mxid[evt_hash] = mxid
return None
def check(
self, event: TypeMessage, mxid: DedupMXID = None, force_hash: bool = False
) -> DedupMXID | None:
evt_hash = self._hash_event(event) if self._always_force_hash or force_hash else event.id
if evt_hash in self._dedup:
return self._dedup_mxid[evt_hash]
self._dedup_mxid[evt_hash] = mxid
self._dedup.append(evt_hash)
if len(self._dedup) > self.cache_queue_length:
del self._dedup_mxid[self._dedup.popleft()]
return None
def register_outgoing_actions(self, response: TypeUpdates) -> None:
for update in response.updates:
check_dedup = isinstance(
update, (UpdateNewMessage, UpdateNewChannelMessage)
) and isinstance(update.message, MessageService)
if check_dedup:
self.check(update.message)
@@ -1,133 +0,0 @@
# mautrix-telegram - A Matrix-Telegram puppeting bridge
# Copyright (C) 2021 Tulir Asokan
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
from __future__ import annotations
import html
from telethon.tl.types import MessageMediaContact, MessageMediaDice, PeerUser
from mautrix.types import Format, MessageType, TextMessageEventContent
from .. import abstract_user as au, puppet as pu
from ..types import TelegramID
try:
import phonenumbers
except ImportError:
phonenumbers = None
def _format_dice(roll: MessageMediaDice) -> str:
if roll.emoticon == "\U0001F3B0":
emojis = {
0: "\U0001F36B", # "🍫",
1: "\U0001F352", # "🍒",
2: "\U0001F34B", # "🍋",
3: "7\ufe0f\u20e3", # "7️⃣",
}
res = roll.value - 1
slot1, slot2, slot3 = emojis[res % 4], emojis[res // 4 % 4], emojis[res // 16]
return f"{slot1} {slot2} {slot3} ({roll.value})"
elif roll.emoticon == "\u26BD":
results = {
1: "miss",
2: "hit the woodwork",
3: "goal", # seems to go in through the center
4: "goal",
5: "goal 🎉", # seems to go in through the top right corner, includes confetti
}
elif roll.emoticon == "\U0001F3B3":
results = {
1: "miss",
2: "1 pin down",
3: "3 pins down, split",
4: "4 pins down, split",
5: "5 pins down",
6: "strike 🎉",
}
# elif roll.emoticon == "\U0001F3C0":
# results = {
# 2: "rolled off",
# 3: "stuck",
# }
# elif roll.emoticon == "\U0001F3AF":
# results = {
# 1: "bounced off",
# 2: "outer rim",
#
# 6: "bullseye",
# }
else:
return str(roll.value)
return f"{results[roll.value]} ({roll.value})"
def make_dice_event_content(roll: MessageMediaDice) -> TextMessageEventContent:
emoji_text = {
"\U0001F3AF": " Dart throw",
"\U0001F3B2": " Dice roll",
"\U0001F3C0": " Basketball throw",
"\U0001F3B0": " Slot machine",
"\U0001F3B3": " Bowling",
"\u26BD": " Football kick",
}
text = f"{roll.emoticon}{emoji_text.get(roll.emoticon, '')} result: {_format_dice(roll)}"
content = TextMessageEventContent(
msgtype=MessageType.TEXT, format=Format.HTML, body=text, formatted_body=f"<h4>{text}</h4>"
)
content["net.maunium.telegram.dice"] = {"emoticon": roll.emoticon, "value": roll.value}
return content
async def make_contact_event_content(
source: au.AbstractUser, contact: MessageMediaContact
) -> TextMessageEventContent:
name = " ".join(x for x in [contact.first_name, contact.last_name] if x)
formatted_phone = f"+{contact.phone_number}"
if phonenumbers is not None:
try:
parsed = phonenumbers.parse(formatted_phone)
fmt = phonenumbers.PhoneNumberFormat.INTERNATIONAL
formatted_phone = phonenumbers.format_number(parsed, fmt)
except phonenumbers.NumberParseException:
pass
content = TextMessageEventContent(
msgtype=MessageType.TEXT,
body=f"Shared contact info for {name}: {formatted_phone}",
)
content["net.maunium.telegram.contact"] = {
"user_id": contact.user_id,
"first_name": contact.first_name,
"last_name": contact.last_name,
"phone_number": contact.phone_number,
"vcard": contact.vcard,
}
puppet = await pu.Puppet.get_by_tgid(TelegramID(contact.user_id))
if not puppet.displayname:
try:
entity = await source.client.get_entity(PeerUser(contact.user_id))
await puppet.update_info(source, entity)
except Exception as e:
source.log.warning(f"Failed to sync puppet info of received contact: {e}")
else:
content.format = Format.HTML
content.formatted_body = (
f"Shared contact info for "
f"<a href='https://matrix.to/#/{puppet.mxid}'>{html.escape(name)}</a>: "
f"{html.escape(formatted_phone)}"
)
return content
@@ -1,106 +0,0 @@
# mautrix-telegram - A Matrix-Telegram puppeting bridge
# Copyright (C) 2021 Tulir Asokan
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
from __future__ import annotations
from typing import Iterable
from telethon.errors import ChatAdminRequiredError
from telethon.tl.functions.channels import GetParticipantsRequest
from telethon.tl.functions.messages import GetFullChatRequest
from telethon.tl.types import (
ChannelParticipantBanned,
ChannelParticipantsRecent,
ChannelParticipantsSearch,
InputChannel,
InputUser,
TypeChannelParticipant,
TypeChat,
TypeChatParticipant,
TypeInputPeer,
TypeUser,
)
from ..tgclient import MautrixTelegramClient
def _filter_participants(
users: list[TypeUser], participants: list[TypeChatParticipant | TypeChannelParticipant]
) -> Iterable[TypeUser]:
participant_map = {
part.user_id: part
for part in participants
if not isinstance(part, ChannelParticipantBanned)
}
for user in users:
try:
user.participant = participant_map[user.id]
except KeyError:
pass
else:
yield user
async def _get_channel_users(
client: MautrixTelegramClient, entity: InputChannel, limit: int
) -> list[TypeUser]:
if 0 < limit <= 200:
response = await client(
GetParticipantsRequest(
entity, ChannelParticipantsRecent(), offset=0, limit=limit, hash=0
)
)
return list(_filter_participants(response.users, response.participants))
elif limit > 200 or limit == -1:
users: list[TypeUser] = []
offset = 0
remaining_quota = limit if limit > 0 else 1000000
query = ChannelParticipantsSearch("") if limit == -1 else ChannelParticipantsRecent()
while True:
if remaining_quota <= 0:
break
response = await client(
GetParticipantsRequest(
entity, query, offset=offset, limit=min(remaining_quota, 200), hash=0
)
)
if not response.users:
break
users += _filter_participants(response.users, response.participants)
offset += len(response.participants)
remaining_quota -= len(response.participants)
return users
async def get_users(
client: MautrixTelegramClient,
tgid: int,
entity: TypeInputPeer | InputUser | TypeChat | TypeUser | InputChannel,
limit: int,
peer_type: str,
) -> list[TypeUser]:
if peer_type == "chat":
chat = await client(GetFullChatRequest(chat_id=tgid))
users = list(_filter_participants(chat.users, chat.full_chat.participants.participants))
return users[:limit] if limit > 0 else users
elif peer_type == "channel":
try:
return await _get_channel_users(client, entity, limit)
except ChatAdminRequiredError:
return []
elif peer_type == "user":
return [entity]
else:
raise RuntimeError(f"Unexpected peer type {peer_type}")
@@ -1,154 +0,0 @@
# mautrix-telegram - A Matrix-Telegram puppeting bridge
# Copyright (C) 2021 Tulir Asokan
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
from __future__ import annotations
from telethon.tl.types import (
ChannelParticipantAdmin,
ChannelParticipantCreator,
ChatBannedRights,
ChatParticipantAdmin,
ChatParticipantCreator,
TypeChannelParticipant,
TypeChat,
TypeChatParticipant,
TypeUser,
)
from mautrix.types import EventType, PowerLevelStateEventContent as PowerLevelContent, UserID
from .. import portal as po, puppet as pu, user as u
from ..types import TelegramID
def get_base_power_levels(
portal: po.Portal, levels: PowerLevelContent = None, entity: TypeChat = None
) -> PowerLevelContent:
levels = levels or PowerLevelContent()
if portal.peer_type == "user":
overrides = portal.config["bridge.initial_power_level_overrides.user"]
levels.ban = overrides.get("ban", 100)
levels.kick = overrides.get("kick", 100)
levels.invite = overrides.get("invite", 100)
levels.redact = overrides.get("redact", 0)
levels.events[EventType.ROOM_NAME] = 0
levels.events[EventType.ROOM_AVATAR] = 0
levels.events[EventType.ROOM_TOPIC] = 0
levels.state_default = overrides.get("state_default", 0)
levels.users_default = overrides.get("users_default", 0)
levels.events_default = overrides.get("events_default", 0)
else:
overrides = portal.config["bridge.initial_power_level_overrides.group"]
dbr = entity.default_banned_rights
if not dbr:
portal.log.debug(f"default_banned_rights is None in {entity}")
dbr = ChatBannedRights(
invite_users=True,
change_info=True,
pin_messages=True,
send_stickers=False,
send_messages=False,
until_date=None,
)
levels.ban = overrides.get("ban", 50)
levels.kick = overrides.get("kick", 50)
levels.redact = overrides.get("redact", 50)
levels.invite = overrides.get("invite", 50 if dbr.invite_users else 0)
levels.events[EventType.ROOM_ENCRYPTION] = 50 if portal.matrix.e2ee else 99
levels.events[EventType.ROOM_TOMBSTONE] = 99
levels.events[EventType.ROOM_NAME] = 50 if dbr.change_info else 0
levels.events[EventType.ROOM_AVATAR] = 50 if dbr.change_info else 0
levels.events[EventType.ROOM_TOPIC] = 50 if dbr.change_info else 0
levels.events[EventType.ROOM_PINNED_EVENTS] = 50 if dbr.pin_messages else 0
levels.events[EventType.ROOM_POWER_LEVELS] = 75
levels.events[EventType.ROOM_HISTORY_VISIBILITY] = 75
levels.events[EventType.STICKER] = 50 if dbr.send_stickers else levels.events_default
levels.state_default = overrides.get("state_default", 50)
levels.users_default = overrides.get("users_default", 0)
levels.events_default = overrides.get(
"events_default",
50
if (
portal.peer_type == "channel"
and not entity.megagroup
or entity.default_banned_rights.send_messages
)
else 0,
)
for evt_type, value in overrides.get("events", {}).items():
levels.events[EventType.find(evt_type)] = value
levels.users = overrides.get("users", {})
if portal.main_intent.mxid not in levels.users:
levels.users[portal.main_intent.mxid] = 100
return levels
async def participants_to_power_levels(
portal: po.Portal,
users: list[TypeUser | TypeChatParticipant | TypeChannelParticipant],
levels: PowerLevelContent,
) -> bool:
bot_level = levels.get_user_level(portal.main_intent.mxid)
if bot_level < levels.get_event_level(EventType.ROOM_POWER_LEVELS):
return False
changed = False
admin_power_level = min(75 if portal.peer_type == "channel" else 50, bot_level)
if levels.get_event_level(EventType.ROOM_POWER_LEVELS) != admin_power_level:
changed = True
levels.events[EventType.ROOM_POWER_LEVELS] = admin_power_level
for user in users:
# The User objects we get from TelegramClient.get_participants have a custom
# participant property
participant = getattr(user, "participant", user)
puppet = await pu.Puppet.get_by_tgid(TelegramID(participant.user_id))
user = await u.User.get_by_tgid(TelegramID(participant.user_id))
new_level = _get_level_from_participant(portal.az.bot_mxid, participant, levels)
if user:
await user.register_portal(portal)
changed = _participant_to_power_levels(levels, user, new_level, bot_level) or changed
if puppet:
changed = _participant_to_power_levels(levels, puppet, new_level, bot_level) or changed
return changed
def _get_level_from_participant(
bot_mxid: UserID,
participant: TypeUser | TypeChatParticipant | TypeChannelParticipant,
levels: PowerLevelContent,
) -> int:
# TODO use the power level requirements to get better precision in channels
if isinstance(participant, (ChatParticipantAdmin, ChannelParticipantAdmin)):
return levels.state_default or 50
elif isinstance(participant, (ChatParticipantCreator, ChannelParticipantCreator)):
return levels.get_user_level(bot_mxid) - 5
return levels.users_default or 0
def _participant_to_power_levels(
levels: PowerLevelContent,
user: u.User | pu.Puppet,
new_level: int,
bot_level: int,
) -> bool:
new_level = min(new_level, bot_level)
user_level = levels.get_user_level(user.mxid)
if user_level != new_level and user_level < bot_level:
levels.users[user.mxid] = new_level
return True
return False
-44
View File
@@ -1,44 +0,0 @@
# mautrix-telegram - A Matrix-Telegram puppeting bridge
# Copyright (C) 2021 Tulir Asokan
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
from __future__ import annotations
from asyncio import Lock
from ..types import TelegramID
class FakeLock:
async def __aenter__(self) -> None:
pass
async def __aexit__(self, exc_type, exc, tb) -> None:
pass
class PortalSendLock:
_send_locks: dict[int, Lock]
_noop_lock: Lock = FakeLock()
def __init__(self) -> None:
self._send_locks = {}
def __call__(self, user_id: TelegramID, required: bool = True) -> Lock:
if user_id is None and required:
raise ValueError("Required send lock for none id")
try:
return self._send_locks[user_id]
except KeyError:
return self._send_locks.setdefault(user_id, Lock()) if required else self._noop_lock
@@ -1,95 +0,0 @@
# mautrix-telegram - A Matrix-Telegram puppeting bridge
# Copyright (C) 2021 Tulir Asokan
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
from __future__ import annotations
import base64
import html
from telethon.tl.functions.channels import GetSponsoredMessagesRequest
from telethon.tl.types import Channel, InputChannel, PeerChannel, PeerUser, SponsoredMessage, User
from mautrix.types import MessageType, TextMessageEventContent
from .. import user as u
from ..formatter import telegram_to_matrix
async def get_sponsored_message(
user: u.User,
entity: InputChannel,
) -> tuple[SponsoredMessage | None, int | None, Channel | User | None]:
resp = await user.client(GetSponsoredMessagesRequest(entity))
if len(resp.messages) == 0:
return None, None, None
msg = resp.messages[0]
if isinstance(msg.from_id, PeerUser):
entities = resp.users
target_id = msg.from_id.user_id
else:
entities = resp.chats
target_id = msg.from_id.channel_id
try:
entity = next(ent for ent in entities if ent.id == target_id)
except StopIteration:
entity = None
return msg, target_id, entity
async def make_sponsored_message_content(
source: u.User, msg: SponsoredMessage, entity: Channel | User
) -> TextMessageEventContent | None:
content = await telegram_to_matrix(msg, source, require_html=True)
content.external_url = f"https://t.me/{entity.username}"
content.msgtype = MessageType.NOTICE
sponsored_meta = {
"random_id": base64.b64encode(msg.random_id).decode("utf-8"),
}
if isinstance(msg.from_id, PeerChannel):
sponsored_meta["channel_id"] = msg.from_id.channel_id
if getattr(msg, "channel_post", None) is not None:
sponsored_meta["channel_post"] = msg.channel_post
content.external_url += f"/{msg.channel_post}"
action = "View Post"
else:
action = "View Channel"
elif isinstance(msg.from_id, PeerUser):
sponsored_meta["bot_id"] = msg.from_id.user_id
if msg.start_param:
content.external_url += f"?start={msg.start_param}"
action = "View Bot"
else:
return None
if isinstance(entity, User):
name_parts = [entity.first_name, entity.last_name]
sponsor_name = " ".join(x for x in name_parts if x)
sponsor_name_html = f"<strong>{html.escape(sponsor_name)}</strong>"
elif isinstance(entity, Channel):
sponsor_name = entity.title
sponsor_name_html = f"<strong>{html.escape(sponsor_name)}</strong>"
else:
sponsor_name = sponsor_name_html = "unknown entity"
content["net.maunium.telegram.sponsored"] = sponsored_meta
content.formatted_body += (
f"<br/><br/>Sponsored message from {sponsor_name_html} "
f"- <a href='{content.external_url}'>{action}</a>"
)
content.body += (
f"\n\nSponsored message from {sponsor_name} - {action} at {content.external_url}"
)
return content
-448
View File
@@ -1,448 +0,0 @@
# mautrix-telegram - A Matrix-Telegram puppeting bridge
# Copyright (C) 2021 Tulir Asokan
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
from __future__ import annotations
from typing import TYPE_CHECKING, AsyncGenerator, AsyncIterable, Awaitable, cast
from difflib import SequenceMatcher
import unicodedata
from telethon.tl.types import (
InputPeerPhotoFileLocation,
PeerUser,
TypeInputPeer,
TypeInputUser,
UpdateUserName,
User,
UserProfilePhoto,
UserProfilePhotoEmpty,
)
from yarl import URL
from mautrix.appservice import IntentAPI
from mautrix.bridge import BasePuppet, async_getter_lock
from mautrix.errors import MatrixError
from mautrix.types import ContentURI, RoomID, SyncToken, UserID
from mautrix.util.simple_template import SimpleTemplate
from . import abstract_user as au, portal as p, util
from .config import Config
from .db import Puppet as DBPuppet
from .types import TelegramID
if TYPE_CHECKING:
from .__main__ import TelegramBridge
class Puppet(DBPuppet, BasePuppet):
config: Config
hs_domain: str
mxid_template: SimpleTemplate[TelegramID]
displayname_template: SimpleTemplate[str]
by_tgid: dict[TelegramID, Puppet] = {}
by_custom_mxid: dict[UserID, Puppet] = {}
def __init__(
self,
id: TelegramID,
is_registered: bool = False,
displayname: str | None = None,
displayname_source: TelegramID | None = None,
displayname_contact: bool = True,
displayname_quality: int = 0,
disable_updates: bool = False,
username: str | None = None,
photo_id: str | None = None,
is_bot: bool = False,
custom_mxid: UserID | None = None,
access_token: str | None = None,
next_batch: SyncToken | None = None,
base_url: str | None = None,
) -> None:
super().__init__(
id=id,
is_registered=is_registered,
displayname=displayname,
displayname_source=displayname_source,
displayname_contact=displayname_contact,
displayname_quality=displayname_quality,
disable_updates=disable_updates,
username=username,
photo_id=photo_id,
is_bot=is_bot,
custom_mxid=custom_mxid,
access_token=access_token,
next_batch=next_batch,
base_url=base_url,
)
self.default_mxid = self.get_mxid_from_id(self.id)
self.default_mxid_intent = self.az.intent.user(self.default_mxid)
self.intent = self._fresh_intent()
self.by_tgid[id] = self
if self.custom_mxid:
self.by_custom_mxid[self.custom_mxid] = self
self.log = self.log.getChild(str(self.id))
@property
def tgid(self) -> TelegramID:
return self.id
@property
def tg_username(self) -> str | None:
return self.username
@property
def peer(self) -> PeerUser:
return PeerUser(user_id=self.tgid)
@property
def plain_displayname(self) -> str:
return self.displayname_template.parse(self.displayname) or self.displayname
def get_input_entity(self, user: au.AbstractUser) -> Awaitable[TypeInputPeer | TypeInputUser]:
return user.client.get_input_entity(self.peer)
def intent_for(self, portal: p.Portal) -> IntentAPI:
if portal.tgid == self.tgid:
return self.default_mxid_intent
return self.intent
@classmethod
def init_cls(cls, bridge: "TelegramBridge") -> AsyncIterable[Awaitable[None]]:
cls.config = bridge.config
cls.loop = bridge.loop
cls.mx = bridge.matrix
cls.az = bridge.az
cls.hs_domain = cls.config["homeserver.domain"]
mxid_tpl = SimpleTemplate(
cls.config["bridge.username_template"],
"userid",
prefix="@",
suffix=f":{Puppet.hs_domain}",
type=int,
)
cls.mxid_template = cast(SimpleTemplate[TelegramID], mxid_tpl)
cls.displayname_template = SimpleTemplate(
cls.config["bridge.displayname_template"], "displayname"
)
cls.sync_with_custom_puppets = cls.config["bridge.sync_with_custom_puppets"]
cls.homeserver_url_map = {
server: URL(url)
for server, url in cls.config["bridge.double_puppet_server_map"].items()
}
cls.allow_discover_url = cls.config["bridge.double_puppet_allow_discovery"]
cls.login_shared_secret_map = {
server: secret.encode("utf-8")
for server, secret in cls.config["bridge.login_shared_secret_map"].items()
}
cls.login_device_name = "Telegram Bridge"
return (puppet.try_start() async for puppet in cls.all_with_custom_mxid())
# region Info updating
def similarity(self, query: str) -> int:
username_similarity = (
SequenceMatcher(None, self.username, query).ratio() if self.username else 0
)
displayname_similarity = (
SequenceMatcher(None, self.plain_displayname, query).ratio() if self.displayname else 0
)
similarity = max(username_similarity, displayname_similarity)
return int(round(similarity * 100))
@staticmethod
def _filter_name(name: str) -> str:
if not name:
return ""
whitespace = (
"\t\n\r\v\f \u00a0\u034f\u180e\u2063\u202f\u205f\u2800\u3000\u3164\ufeff\u2000\u2001"
"\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200a\u200b\u200c\u200d\u200e\u200f"
"\ufe0f"
)
allowed_other_format = ("\u200d", "\u200c")
name = "".join(
c
for c in name.strip(whitespace)
if unicodedata.category(c) != "Cf" or c in allowed_other_format
)
return name
@classmethod
def get_displayname(cls, info: User, enable_format: bool = True) -> tuple[str, int]:
fn = cls._filter_name(info.first_name)
ln = cls._filter_name(info.last_name)
data = {
"phone number": info.phone if hasattr(info, "phone") else None,
"username": info.username,
"full name": " ".join([fn, ln]).strip(),
"full name reversed": " ".join([ln, fn]).strip(),
"first name": fn,
"last name": ln,
}
preferences = cls.config["bridge.displayname_preference"]
name = None
quality = 99
for preference in preferences:
name = data[preference]
if name:
break
quality -= 1
if isinstance(info, User) and info.deleted:
name = f"Deleted account {info.id}"
quality = 99
elif not name:
name = str(info.id)
quality = 0
return (cls.displayname_template.format_full(name) if enable_format else name), quality
async def try_update_info(self, source: au.AbstractUser, info: User) -> None:
try:
await self.update_info(source, info)
except Exception:
source.log.exception(f"Failed to update info of {self.tgid}")
async def update_info(self, source: au.AbstractUser, info: User) -> None:
changed = False
if self.username != info.username:
self.username = info.username
changed = True
if not self.disable_updates:
try:
changed = await self.update_displayname(source, info) or changed
changed = await self.update_avatar(source, info.photo) or changed
except Exception:
self.log.exception(f"Failed to update info from source {source.tgid}")
self.is_bot = info.bot
if changed:
await self.save()
async def update_displayname(
self, source: au.AbstractUser, info: User | UpdateUserName
) -> bool:
if self.disable_updates:
return False
if source.is_relaybot or source.is_bot:
allow_because = "user is bot"
elif self.displayname_source == source.tgid:
allow_because = "user is the primary source"
elif not isinstance(info, UpdateUserName) and not info.contact:
allow_because = "user is not a contact"
elif not self.displayname_source:
allow_because = "no primary source set"
elif not self.displayname:
allow_because = "user has no name"
else:
return False
if isinstance(info, UpdateUserName):
info = await source.client.get_entity(PeerUser(self.tgid))
if not info.contact:
self.displayname_contact = False
elif not self.displayname_contact:
if not self.displayname:
self.displayname_contact = True
else:
return False
displayname, quality = self.get_displayname(info)
if displayname != self.displayname and quality >= self.displayname_quality:
allow_because = f"{allow_because} and quality {quality} >= {self.displayname_quality}"
self.log.debug(
f"Updating displayname of {self.id} (src: {source.tgid}, allowed "
f"because {allow_because}) from {self.displayname} to {displayname}"
)
self.log.trace("Displayname source data: %s", info)
self.displayname = displayname
self.displayname_source = source.tgid
self.displayname_quality = quality
try:
await self.default_mxid_intent.set_displayname(
displayname[: self.config["bridge.displayname_max_length"]]
)
except MatrixError:
self.log.exception("Failed to set displayname")
self.displayname = ""
self.displayname_source = None
self.displayname_quality = 0
return True
elif source.is_relaybot or self.displayname_source is None:
self.displayname_source = source.tgid
return True
return False
async def update_avatar(
self, source: au.AbstractUser, photo: UserProfilePhoto | UserProfilePhotoEmpty
) -> bool:
if self.disable_updates:
return False
if photo is None or isinstance(photo, UserProfilePhotoEmpty):
photo_id = ""
elif isinstance(photo, UserProfilePhoto):
photo_id = str(photo.photo_id)
else:
self.log.warning(f"Unknown user profile photo type: {type(photo)}")
return False
if not photo_id and not self.config["bridge.allow_avatar_remove"]:
return False
if self.photo_id != photo_id:
if not photo_id:
self.photo_id = ""
try:
await self.default_mxid_intent.set_avatar_url(ContentURI(""))
except MatrixError:
self.log.exception("Failed to set avatar")
self.photo_id = ""
return True
loc = InputPeerPhotoFileLocation(
peer=await self.get_input_entity(source), photo_id=photo.photo_id, big=True
)
file = await util.transfer_file_to_matrix(source.client, self.default_mxid_intent, loc)
if file:
self.photo_id = photo_id
try:
await self.default_mxid_intent.set_avatar_url(file.mxc)
except MatrixError:
self.log.exception("Failed to set avatar")
self.photo_id = ""
return True
return False
async def default_puppet_should_leave_room(self, room_id: RoomID) -> bool:
portal: p.Portal = await p.Portal.get_by_mxid(room_id)
return portal and not portal.backfill_lock.locked and portal.peer_type != "user"
# endregion
# region Getters
def _add_to_cache(self) -> None:
self.by_tgid[self.id] = self
if self.custom_mxid:
self.by_custom_mxid[self.custom_mxid] = self
@classmethod
@async_getter_lock
async def get_by_tgid(cls, tgid: TelegramID, *, create: bool = True) -> Puppet | None:
if tgid is None:
return None
try:
return cls.by_tgid[tgid]
except KeyError:
pass
puppet = cast(cls, await super().get_by_tgid(tgid))
if puppet:
puppet._add_to_cache()
return puppet
if create:
puppet = cls(tgid)
await puppet.insert()
puppet._add_to_cache()
return puppet
return None
@classmethod
def get_by_mxid(cls, mxid: UserID, create: bool = True) -> Awaitable[Puppet | None]:
return cls.get_by_tgid(cls.get_id_from_mxid(mxid), create=create)
@classmethod
@async_getter_lock
async def get_by_custom_mxid(cls, mxid: UserID) -> Puppet | None:
try:
return cls.by_custom_mxid[mxid]
except KeyError:
pass
puppet = cast(cls, await super().get_by_custom_mxid(mxid))
if puppet:
puppet._add_to_cache()
return puppet
return None
@classmethod
async def all_with_custom_mxid(cls) -> AsyncGenerator[Puppet, None]:
puppets = await super().all_with_custom_mxid()
puppet: cls
for puppet in puppets:
try:
yield cls.by_tgid[puppet.tgid]
except KeyError:
puppet._add_to_cache()
yield puppet
@classmethod
def get_id_from_mxid(cls, mxid: UserID) -> TelegramID | None:
return cls.mxid_template.parse(mxid)
@classmethod
def get_mxid_from_id(cls, tgid: TelegramID) -> UserID:
return UserID(cls.mxid_template.format_full(tgid))
@classmethod
async def find_by_username(cls, username: str) -> Puppet | None:
if not username:
return None
username = username.lower()
for _, puppet in cls.by_tgid.items():
if puppet.username and puppet.username.lower() == username:
return puppet
puppet = cast(cls, await super().find_by_username(username))
if puppet:
try:
return cls.by_tgid[puppet.tgid]
except KeyError:
puppet._add_to_cache()
return puppet
return None
@classmethod
async def find_by_displayname(cls, displayname: str) -> Puppet | None:
if not displayname:
return None
for _, puppet in cls.by_tgid.items():
if puppet.displayname and puppet.displayname == displayname:
return puppet
puppet = cast(cls, await super().find_by_displayname(displayname))
if puppet:
try:
return cls.by_tgid[puppet.tgid]
except KeyError:
puppet._add_to_cache()
return puppet
return None
# endregion
-72
View File
@@ -1,72 +0,0 @@
# mautrix-telegram - A Matrix-Telegram puppeting bridge
# Copyright (C) 2021 Tulir Asokan
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
from typing import List, Optional, Union
from telethon import TelegramClient, utils
from telethon.sessions.abstract import Session
from telethon.tl.functions.messages import SendMediaRequest
from telethon.tl.patched import Message
from telethon.tl.types import (
InputMediaUploadedDocument,
InputMediaUploadedPhoto,
TypeDocumentAttribute,
TypeInputMedia,
TypeInputPeer,
TypeMessageEntity,
TypeMessageMedia,
TypePeer,
)
class MautrixTelegramClient(TelegramClient):
session: Session
async def upload_file_direct(
self,
file: bytes,
mime_type: str = None,
attributes: List[TypeDocumentAttribute] = None,
file_name: str = None,
max_image_size: float = 10 * 1000 ** 2,
) -> Union[InputMediaUploadedDocument, InputMediaUploadedPhoto]:
file_handle = await super().upload_file(file, file_name=file_name)
if (mime_type == "image/png" or mime_type == "image/jpeg") and len(file) < max_image_size:
return InputMediaUploadedPhoto(file_handle)
else:
attributes = attributes or []
attr_dict = {type(attr): attr for attr in attributes}
return InputMediaUploadedDocument(
file=file_handle,
mime_type=mime_type or "application/octet-stream",
attributes=list(attr_dict.values()),
)
async def send_media(
self,
entity: Union[TypeInputPeer, TypePeer],
media: Union[TypeInputMedia, TypeMessageMedia],
caption: str = None,
entities: List[TypeMessageEntity] = None,
reply_to: int = None,
) -> Optional[Message]:
entity = await self.get_input_entity(entity)
reply_to = utils.get_message_id(reply_to)
request = SendMediaRequest(
entity, media, message=caption or "", entities=entities or [], reply_to_msg_id=reply_to
)
return self._get_response_message(request, await self(request), entity)
-3
View File
@@ -1,3 +0,0 @@
from typing import NewType
TelegramID = NewType("TelegramID", int)
-753
View File
@@ -1,753 +0,0 @@
# mautrix-telegram - A Matrix-Telegram puppeting bridge
# Copyright (C) 2021 Tulir Asokan
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
from __future__ import annotations
from typing import TYPE_CHECKING, AsyncGenerator, AsyncIterable, Awaitable, NamedTuple, cast
from datetime import datetime, timezone
import asyncio
from telethon.errors import AuthKeyDuplicatedError, RPCError, UnauthorizedError
from telethon.tl.custom import Dialog
from telethon.tl.functions.account import UpdateStatusRequest
from telethon.tl.functions.contacts import GetContactsRequest, SearchRequest
from telethon.tl.functions.updates import GetStateRequest
from telethon.tl.functions.users import GetUsersRequest
from telethon.tl.types import (
Chat,
ChatForbidden,
InputUserSelf,
NotifyPeer,
TypeUpdate,
UpdateFolderPeers,
UpdateNewChannelMessage,
UpdateNewMessage,
UpdateNotifySettings,
UpdatePinnedDialogs,
UpdateShortChatMessage,
UpdateShortMessage,
User as TLUser,
)
from telethon.tl.types.contacts import ContactsNotModified
from mautrix.appservice import DOUBLE_PUPPET_SOURCE_KEY
from mautrix.bridge import BaseUser, async_getter_lock
from mautrix.client import Client
from mautrix.errors import MatrixRequestError, MNotFound
from mautrix.types import PushActionType, PushRuleKind, PushRuleScope, RoomID, RoomTagInfo, UserID
from mautrix.util.bridge_state import BridgeState, BridgeStateEvent
from mautrix.util.opt_prometheus import Gauge
from . import portal as po, puppet as pu
from .abstract_user import AbstractUser
from .db import Message as DBMessage, PgSession, User as DBUser
from .types import TelegramID
if TYPE_CHECKING:
from .__main__ import TelegramBridge
SearchResult = NamedTuple("SearchResult", puppet="pu.Puppet", similarity=int)
METRIC_LOGGED_IN = Gauge("bridge_logged_in", "Users logged into bridge")
METRIC_CONNECTED = Gauge("bridge_connected", "Users connected to Telegram")
BridgeState.human_readable_errors.update(
{
"tg-not-connected": "Your Telegram connection failed",
"tg-auth-key-duplicated": "The bridge accidentally logged you out",
"tg-not-authenticated": "The stored auth token did not work",
"tg-no-auth": "You're not logged in",
}
)
class User(DBUser, AbstractUser, BaseUser):
by_mxid: dict[str, User] = {}
by_tgid: dict[int, User] = {}
_portals_cache: dict[tuple[TelegramID, TelegramID], po.Portal] | None
_ensure_started_lock: asyncio.Lock
_track_connection_task: asyncio.Task | None
_is_backfilling: bool
def __init__(
self,
mxid: UserID,
tgid: TelegramID | None = None,
tg_username: str | None = None,
tg_phone: str | None = None,
is_bot: bool = False,
saved_contacts: int = 0,
) -> None:
super().__init__(
mxid=mxid,
tgid=tgid,
tg_username=tg_username,
tg_phone=tg_phone,
is_bot=is_bot,
saved_contacts=saved_contacts,
)
AbstractUser.__init__(self)
BaseUser.__init__(self)
self._ensure_started_lock = asyncio.Lock()
self._track_connection_task = None
self._is_backfilling = False
self._portals_cache = None
(
self.relaybot_whitelisted,
self.whitelisted,
self.puppet_whitelisted,
self.matrix_puppet_whitelisted,
self.is_admin,
self.permissions,
) = self.config.get_permissions(self.mxid)
@property
def name(self) -> str:
return self.mxid
@property
def mxid_localpart(self) -> str:
localpart, server = Client.parse_user_id(self.mxid)
return localpart
@property
def human_tg_id(self) -> str:
return f"@{self.tg_username}" if self.tg_username else f"+{self.tg_phone}" or None
# TODO replace with proper displayname getting everywhere
@property
def displayname(self) -> str:
return self.mxid_localpart
@property
def plain_displayname(self) -> str:
return self.displayname
@classmethod
def init_cls(cls, bridge: "TelegramBridge") -> AsyncIterable[Awaitable[User]]:
cls.config = bridge.config
cls.bridge = bridge
cls.az = bridge.az
cls.loop = bridge.loop
return (user.try_ensure_started() async for user in cls.all_with_tgid())
# region Telegram connection management
async def try_ensure_started(self) -> None:
try:
await self.ensure_started()
except Exception:
self.log.exception("Exception in ensure_started")
else:
if not self.client and not await PgSession.has(self.mxid):
self.log.warning("Didn't start user: no session stored")
if self.tgid:
await self.push_bridge_state(
BridgeStateEvent.BAD_CREDENTIALS, error="tg-no-auth"
)
async def ensure_started(self, even_if_no_session=False) -> User:
if not self.puppet_whitelisted or self.connected:
return self
async with self._ensure_started_lock:
return cast(User, await super().ensure_started(even_if_no_session))
async def start(self, delete_unless_authenticated: bool = False) -> User:
try:
await super().start()
except AuthKeyDuplicatedError:
self.log.warning("Got AuthKeyDuplicatedError in start()")
await self.push_bridge_state(
BridgeStateEvent.BAD_CREDENTIALS, error="tg-auth-key-duplicated"
)
await self.client.disconnect()
await self.client.session.delete()
self.client = None
if not delete_unless_authenticated:
# The caller wants the client to be connected, so restart the connection.
await super().start()
return self
except Exception:
await self.push_bridge_state(BridgeStateEvent.UNKNOWN_ERROR)
raise
try:
assert self.client, "client is undefined"
assert self.client.is_connected(), "client is not connected"
await self.client(GetStateRequest())
except AssertionError as e:
self.log.error(f"Client in bad state after start(): {e}")
if self.tgid:
await self.push_bridge_state(BridgeStateEvent.UNKNOWN_ERROR, message=str(e))
except UnauthorizedError as e:
self.log.error(f"Authorization error in start(): {type(e)}: {e}")
if self.tgid:
await self.push_bridge_state(
BridgeStateEvent.BAD_CREDENTIALS,
error="tg-auth-error",
message=str(e),
ttl=3600,
)
except RPCError as e:
self.log.error(f"Unknown RPC error in start(): {type(e)}: {e}")
if self.tgid:
await self.push_bridge_state(BridgeStateEvent.UNKNOWN_ERROR, message=str(e))
else:
# Authenticated, run post login
self.log.debug(f"Ensuring post_login() for {self.name}")
asyncio.create_task(self.post_login())
return self
# Not authenticated, delete data if necessary
if delete_unless_authenticated:
self.log.debug(f"Unauthenticated user {self.name} start()ed, deleting session...")
await self.client.disconnect()
await self.client.session.delete()
return self
@property
def _is_connected(self) -> bool:
return bool(
self.client and self.client._sender and self.client._sender._transport_connected()
)
async def _track_connection(self) -> None:
self.log.debug("Starting loop to track connection state")
while True:
await asyncio.sleep(3)
connected = self._is_connected
self._track_metric(METRIC_CONNECTED, connected)
if connected:
await self.push_bridge_state(
BridgeStateEvent.BACKFILLING
if self._is_backfilling
else BridgeStateEvent.CONNECTED,
ttl=3600,
)
else:
await self.push_bridge_state(
BridgeStateEvent.UNKNOWN_ERROR, ttl=240, error="tg-not-connected"
)
async def fill_bridge_state(self, state: BridgeState) -> None:
await super().fill_bridge_state(state)
state.remote_id = str(self.tgid)
state.remote_name = self.human_tg_id
async def get_bridge_states(self) -> list[BridgeState]:
if not self.tgid:
return []
if self._is_connected and await self.is_logged_in():
state_event = (
BridgeStateEvent.BACKFILLING
if self._is_backfilling
else BridgeStateEvent.CONNECTED
)
ttl = 3600
else:
state_event = BridgeStateEvent.UNKNOWN_ERROR
ttl = 240
return [BridgeState(state_event=state_event, ttl=ttl)]
async def get_puppet(self) -> pu.Puppet | None:
if not self.tgid:
return None
return await pu.Puppet.get_by_tgid(self.tgid)
async def stop(self) -> None:
if self._track_connection_task:
self._track_connection_task.cancel()
self._track_connection_task = None
await super().stop()
self._track_metric(METRIC_CONNECTED, False)
async def post_login(self, info: TLUser = None, first_login: bool = False) -> None:
if self.config["metrics.enabled"] and not self._track_connection_task:
self._track_connection_task = asyncio.create_task(self._track_connection())
try:
await self.update_info(info)
except Exception:
self.log.exception("Failed to update telegram account info")
return
self._track_metric(METRIC_LOGGED_IN, True)
try:
puppet = await pu.Puppet.get_by_tgid(self.tgid)
if puppet.custom_mxid != self.mxid and puppet.can_auto_login(self.mxid):
self.log.info(f"Automatically enabling custom puppet")
await puppet.switch_mxid(access_token="auto", mxid=self.mxid)
except Exception:
self.log.exception("Failed to automatically enable custom puppet")
if not self.is_bot and self.config["bridge.startup_sync"]:
try:
self._is_backfilling = True
await self.sync_dialogs()
await self.sync_contacts()
except Exception:
self.log.exception("Failed to run post-login sync")
finally:
self._is_backfilling = False
async def update(self, update: TypeUpdate) -> bool:
if not self.is_bot:
return False
if isinstance(update, (UpdateNewMessage, UpdateNewChannelMessage)):
portal = await po.Portal.get_by_entity(update.message.peer_id, tg_receiver=self.tgid)
elif isinstance(update, UpdateShortChatMessage):
portal = await po.Portal.get_by_tgid(TelegramID(update.chat_id))
elif isinstance(update, UpdateShortMessage):
portal = await po.Portal.get_by_tgid(
TelegramID(update.user_id), tg_receiver=self.tgid, peer_type="user"
)
else:
return False
if portal:
await self.register_portal(portal)
return False
# Don't bother handling the update
return True
# endregion
# region Telegram actions that need custom methods
async def set_presence(self, online: bool = True) -> None:
if not self.is_bot:
await self.client(UpdateStatusRequest(offline=not online))
async def get_me(self) -> TLUser | None:
try:
return (await self.client(GetUsersRequest([InputUserSelf()])))[0]
except UnauthorizedError as e:
self.log.error(f"Authorization error in get_me(): {type(e)}: {e}")
await self.push_bridge_state(
BridgeStateEvent.BAD_CREDENTIALS, error="tg-auth-error", message=str(e), ttl=3600
)
await self.stop()
return None
async def update_info(self, info: TLUser = None) -> None:
if not info:
info = await self.get_me()
if not info:
self.log.warning("get_me() returned None, aborting update_info()")
return
changed = False
if self.is_bot != info.bot:
self.is_bot = info.bot
changed = True
if self.tg_username != info.username:
self.tg_username = info.username
changed = True
if self.tg_phone != info.phone:
self.tg_phone = info.phone
changed = True
if self.tgid != info.id:
self.tgid = TelegramID(info.id)
self.by_tgid[self.tgid] = self
if changed:
await self.save()
async def kick_from_portals(self) -> None:
if not self.config["bridge.kick_on_logout"]:
return
portals = await self.get_cached_portals()
for _, portal in portals.values():
if not portal or portal.deleted or not portal.mxid or portal.has_bot:
continue
if portal.peer_type == "user":
await portal.cleanup_portal("Logged out of Telegram")
else:
try:
await portal.main_intent.kick_user(
portal.mxid, self.mxid, "Logged out of Telegram."
)
except MatrixRequestError:
pass
async def log_out(self) -> bool:
puppet = await pu.Puppet.get_by_tgid(self.tgid)
if puppet.is_real_user:
await puppet.switch_mxid(None, None)
try:
await self.kick_from_portals()
except Exception:
self.log.exception("Failed to kick user from portals on logout")
await self.push_bridge_state(BridgeStateEvent.LOGGED_OUT)
if self.tgid:
try:
del self.by_tgid[self.tgid]
except KeyError:
pass
self.tgid = None
ok = await self.client.log_out()
await self.client.session.delete()
await self.delete()
self.by_mxid.pop(self.mxid, None)
await self.stop()
self._track_metric(METRIC_LOGGED_IN, False)
return ok
async def _search_local(
self, query: str, max_results: int = 5, min_similarity: int = 45
) -> list[SearchResult]:
results: list[SearchResult] = []
for contact_id in await self.get_contacts():
contact = await pu.Puppet.get_by_tgid(contact_id, create=False)
if not contact:
continue
similarity = contact.similarity(query)
if similarity >= min_similarity:
results.append(SearchResult(contact, similarity))
results.sort(key=lambda tup: tup[1], reverse=True)
return results[0:max_results]
async def _search_remote(self, query: str, max_results: int = 5) -> list[SearchResult]:
if len(query) < 5:
return []
server_results = await self.client(SearchRequest(q=query, limit=max_results))
results: list[SearchResult] = []
for user in server_results.users:
puppet = await pu.Puppet.get_by_tgid(user.id)
await puppet.update_info(self, user)
results.append(SearchResult(puppet, puppet.similarity(query)))
results.sort(key=lambda tup: tup[1], reverse=True)
return results[0:max_results]
async def search(
self, query: str, force_remote: bool = False
) -> tuple[list[SearchResult], bool]:
if force_remote:
return await self._search_remote(query), True
results = await self._search_local(query)
if results:
return results, False
return await self._search_remote(query), True
async def get_direct_chats(self) -> dict[UserID, list[RoomID]]:
return {
pu.Puppet.get_mxid_from_id(portal.tgid): [portal.mxid]
async for portal in po.Portal.find_private_chats(self.tgid)
if portal.mxid
}
async def _tag_room(
self, puppet: pu.Puppet, portal: po.Portal, tag: str, active: bool
) -> None:
if not tag or not portal or not portal.mxid:
return
tag_info = await puppet.intent.get_room_tag(portal.mxid, tag)
if active and tag_info is None:
tag_info = RoomTagInfo(order=0.5)
tag_info[DOUBLE_PUPPET_SOURCE_KEY] = self.bridge.name
await puppet.intent.set_room_tag(portal.mxid, tag, tag_info)
elif (
not active and tag_info and tag_info.get(DOUBLE_PUPPET_SOURCE_KEY) == self.bridge.name
):
await puppet.intent.remove_room_tag(portal.mxid, tag)
async def _mute_room(cls, puppet: pu.Puppet, portal: po.Portal, mute_until: datetime) -> None:
if not cls.config["bridge.mute_bridging"] or not portal or not portal.mxid:
return
now = datetime.utcnow().replace(tzinfo=timezone.utc)
if mute_until is not None and mute_until > now:
await puppet.intent.set_push_rule(
PushRuleScope.GLOBAL,
PushRuleKind.ROOM,
portal.mxid,
actions=[PushActionType.DONT_NOTIFY],
)
else:
try:
await puppet.intent.remove_push_rule(
PushRuleScope.GLOBAL, PushRuleKind.ROOM, portal.mxid
)
except MNotFound:
pass
async def update_folder_peers(self, update: UpdateFolderPeers) -> None:
if self.config["bridge.tag_only_on_create"]:
return
puppet = await pu.Puppet.get_by_custom_mxid(self.mxid)
if not puppet or not puppet.is_real_user:
return
for peer in update.folder_peers:
portal = await po.Portal.get_by_entity(peer.peer, tg_receiver=self.tgid, create=False)
await self._tag_room(
puppet, portal, self.config["bridge.archive_tag"], peer.folder_id == 1
)
async def update_pinned_dialogs(self, update: UpdatePinnedDialogs) -> None:
if self.config["bridge.tag_only_on_create"]:
return
puppet = await pu.Puppet.get_by_custom_mxid(self.mxid)
if not puppet or not puppet.is_real_user:
return
# TODO bridge unpinning properly
for pinned in update.order:
portal = await po.Portal.get_by_entity(
pinned.peer, tg_receiver=self.tgid, create=False
)
await self._tag_room(puppet, portal, self.config["bridge.pinned_tag"], True)
async def update_notify_settings(self, update: UpdateNotifySettings) -> None:
if self.config["bridge.tag_only_on_create"]:
return
elif not isinstance(update.peer, NotifyPeer):
# TODO handle global notification setting changes?
return
puppet = await pu.Puppet.get_by_custom_mxid(self.mxid)
if not puppet or not puppet.is_real_user:
return
portal = await po.Portal.get_by_entity(
update.peer.peer, tg_receiver=self.tgid, create=False
)
await self._mute_room(puppet, portal, update.notify_settings.mute_until)
async def _sync_dialog(
self, portal: po.Portal, dialog: Dialog, should_create: bool, puppet: pu.Puppet | None
) -> None:
was_created = False
if portal.mxid:
try:
await portal.backfill(self, last_id=dialog.message.id)
except Exception:
self.log.exception(f"Error while backfilling {portal.tgid_log}")
try:
await portal.update_matrix_room(self, dialog.entity)
except Exception:
self.log.exception(f"Error while updating {portal.tgid_log}")
elif should_create:
try:
await portal.create_matrix_room(self, dialog.entity, invites=[self.mxid])
was_created = True
except Exception:
self.log.exception(f"Error while creating {portal.tgid_log}")
if portal.mxid and puppet and puppet.is_real_user:
tg_space = portal.tgid if portal.peer_type == "channel" else self.tgid
if dialog.unread_count == 0:
# This is usually more reliable than finding a specific message
# e.g. if the last read message is a service message that isn't in the message db
last_read = await DBMessage.find_last(portal.mxid, tg_space)
else:
last_read = await DBMessage.get_one_by_tgid(
portal.tgid, tg_space, dialog.dialog.read_inbox_max_id
)
if last_read:
await puppet.intent.mark_read(last_read.mx_room, last_read.mxid)
if was_created or not self.config["bridge.tag_only_on_create"]:
await self._mute_room(puppet, portal, dialog.dialog.notify_settings.mute_until)
await self._tag_room(
puppet, portal, self.config["bridge.pinned_tag"], dialog.pinned
)
await self._tag_room(
puppet, portal, self.config["bridge.archive_tag"], dialog.archived
)
async def get_cached_portals(self) -> dict[tuple[TelegramID, TelegramID], po.Portal]:
if self._portals_cache is None:
self._portals_cache = {
(tgid, tg_receiver): await po.Portal.get_by_tgid(tgid, tg_receiver=tg_receiver)
for tgid, tg_receiver in await self.get_portals()
}
return self._portals_cache
async def sync_dialogs(self) -> None:
if self.is_bot:
return
creators = []
update_limit = self.config["bridge.sync_update_limit"] or None
create_limit = self.config["bridge.sync_create_limit"]
index = 0
self.log.debug(
f"Syncing dialogs (update_limit={update_limit}, create_limit={create_limit})"
)
await self.push_bridge_state(BridgeStateEvent.BACKFILLING)
puppet = await pu.Puppet.get_by_custom_mxid(self.mxid)
dialog: Dialog
old_portal_cache = await self.get_cached_portals()
new_portal_cache = old_portal_cache.copy()
async for dialog in self.client.iter_dialogs(
limit=update_limit, ignore_migrated=True, archived=False
):
entity = dialog.entity
if isinstance(entity, ChatForbidden):
self.log.warning(f"Ignoring forbidden chat {entity} while syncing")
continue
elif isinstance(entity, Chat) and (entity.deactivated or entity.left):
self.log.warning(f"Ignoring deactivated or left chat {entity} while syncing")
continue
elif isinstance(entity, TLUser) and not self.config["bridge.sync_direct_chats"]:
self.log.trace(f"Ignoring user {entity.id} while syncing")
continue
portal = await po.Portal.get_by_entity(entity, tg_receiver=self.tgid)
new_portal_cache[portal.tgid_full] = portal
coro = self._sync_dialog(
portal=portal,
dialog=dialog,
puppet=puppet,
should_create=not create_limit or index < create_limit,
)
creators.append(asyncio.create_task(coro))
index += 1
if new_portal_cache.keys() != old_portal_cache.keys():
await self.set_portals(new_portal_cache.keys())
self._portals_cache = new_portal_cache
await asyncio.gather(*creators)
await self.update_direct_chats()
self.log.debug("Dialog syncing complete")
async def register_portal(self, portal: po.Portal) -> None:
self.log.trace(f"Registering portal {portal.tgid_full}")
if self._portals_cache is not None:
if self._portals_cache.get(portal.tgid_full) == portal:
return
self._portals_cache[portal.tgid_full] = portal
await super().register_portal(portal.tgid, portal.tg_receiver)
async def unregister_portal(self, tgid: TelegramID, tg_receiver: TelegramID) -> None:
self.log.trace(f"Unregistering portal {(tgid, tg_receiver)}")
if self._portals_cache is not None:
self._portals_cache.pop((tgid, tg_receiver), None)
await super().unregister_portal(tgid, tg_receiver)
async def needs_relaybot(self, portal: po.Portal) -> bool:
return not await self.is_logged_in() or (
(portal.has_bot or self.is_bot)
and portal.tgid_full not in await self.get_cached_portals()
)
@staticmethod
def _hash_contacts(count: int, ids: list[TelegramID]) -> int:
acc = 0
for contact in sorted([count] + ids):
acc = (acc * 20261 + contact) & 0xFFFFFFFF
return acc & 0x7FFFFFFF
async def sync_contacts(self) -> None:
existing_contacts = await self.get_contacts()
contact_hash = self._hash_contacts(self.saved_contacts, existing_contacts)
response = await self.client(GetContactsRequest(hash=contact_hash))
if isinstance(response, ContactsNotModified):
return
self.log.debug(f"Updating contacts of {self.name}...")
if self.saved_contacts != response.saved_count:
self.saved_contacts = response.saved_count
await self.save()
await self.set_contacts(user.id for user in response.users)
for user in response.users:
puppet = await pu.Puppet.get_by_tgid(user.id)
await puppet.update_info(self, user)
# endregion
# region Class instance lookup
def _add_to_cache(self) -> None:
self.by_mxid[self.mxid] = self
if self.tgid:
self.by_tgid[self.tgid] = self
@classmethod
async def get_and_start_by_mxid(cls, mxid: UserID, even_if_no_session: bool = False) -> User:
user = await cls.get_by_mxid(mxid, create=True)
await user.ensure_started(even_if_no_session=even_if_no_session)
return user
@classmethod
async def all_with_tgid(cls) -> AsyncGenerator[User, None]:
users = await super().all_with_tgid()
user: cls
for user in users:
try:
yield cls.by_mxid[user.mxid]
except KeyError:
user._add_to_cache()
yield user
@classmethod
@async_getter_lock
async def get_by_mxid(
cls, mxid: UserID, *, check_db: bool = True, create: bool = True
) -> User | None:
if not mxid or pu.Puppet.get_id_from_mxid(mxid) or mxid == cls.az.bot_mxid:
return None
try:
return cls.by_mxid[mxid]
except KeyError:
pass
if not check_db:
return None
user = cast(cls, await super().get_by_mxid(mxid))
if user is not None:
user._add_to_cache()
return user
if create:
cls.log.debug(f"Creating user instance for {mxid}")
user = cls(mxid)
await user.insert()
user._add_to_cache()
return user
return None
@classmethod
@async_getter_lock
async def get_by_tgid(cls, tgid: TelegramID) -> User | None:
try:
return cls.by_tgid[tgid]
except KeyError:
pass
user = cast(cls, await super().get_by_tgid(tgid))
if user is not None:
user._add_to_cache()
return user
return None
@classmethod
async def find_by_username(cls, username: str) -> User | None:
if not username:
return None
username = username.lower()
for _, user in cls.by_tgid.items():
if user.tg_username and user.tg_username.lower() == username:
return user
user = cast(cls, await super().find_by_username(username))
if user:
try:
return cls.by_mxid[user.mxid]
except KeyError:
user._add_to_cache()
return user
return None
# endregion
-4
View File
@@ -1,4 +0,0 @@
from .color_log import ColorFormatter
from .file_transfer import convert_image, transfer_file_to_matrix
from .parallel_file_transfer import parallel_transfer_to_telegram
from .recursive_dict import recursive_del, recursive_get, recursive_set
-36
View File
@@ -1,36 +0,0 @@
# mautrix-telegram - A Matrix-Telegram puppeting bridge
# Copyright (C) 2021 Tulir Asokan
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
from mautrix.util.logging.color import (
MXID_COLOR,
PREFIX,
RESET,
ColorFormatter as BaseColorFormatter,
)
TELETHON_COLOR = PREFIX + "35;1m" # magenta
TELETHON_MODULE_COLOR = PREFIX + "35m"
class ColorFormatter(BaseColorFormatter):
def _color_name(self, module: str) -> str:
if module.startswith("telethon"):
prefix, user_id, module = module.split(".", 2)
return (
f"{TELETHON_COLOR}{prefix}{RESET}."
f"{MXID_COLOR}{user_id}{RESET}."
f"{TELETHON_MODULE_COLOR}{module}{RESET}"
)
return super()._color_name(module)
-353
View File
@@ -1,353 +0,0 @@
# mautrix-telegram - A Matrix-Telegram puppeting bridge
# Copyright (C) 2021 Tulir Asokan
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
from __future__ import annotations
from typing import Optional, Union
from io import BytesIO
from sqlite3 import IntegrityError
import asyncio
import logging
import tempfile
import time
from asyncpg import UniqueViolationError
from telethon.errors import (
AuthBytesInvalidError,
AuthKeyInvalidError,
FileIdInvalidError,
LocationInvalidError,
SecurityError,
)
from telethon.tl.types import (
Document,
InputDocumentFileLocation,
InputFileLocation,
InputPeerPhotoFileLocation,
InputPhotoFileLocation,
PhotoCachedSize,
PhotoSize,
TypePhotoSize,
)
import magic
from mautrix.appservice import IntentAPI
from ..db import TelegramFile as DBTelegramFile
from ..tgclient import MautrixTelegramClient
from ..util import sane_mimetypes
from .parallel_file_transfer import parallel_transfer_to_matrix
from .tgs_converter import convert_tgs_to
try:
from PIL import Image
except ImportError:
Image = None
try:
from moviepy.editor import VideoFileClip
except ImportError:
VideoFileClip = None
try:
from mautrix.crypto.attachments import encrypt_attachment
except ImportError:
encrypt_attachment = None
log: logging.Logger = logging.getLogger("mau.util")
TypeLocation = Union[
Document,
InputDocumentFileLocation,
InputPeerPhotoFileLocation,
InputFileLocation,
InputPhotoFileLocation,
]
def convert_image(
file: bytes,
source_mime: str = "image/webp",
target_type: str = "png",
thumbnail_to: tuple[int, int] | None = None,
) -> tuple[str, bytes, int | None, int | None]:
if not Image:
return source_mime, file, None, None
try:
image: Image.Image = Image.open(BytesIO(file)).convert("RGBA")
if thumbnail_to:
image.thumbnail(thumbnail_to, Image.ANTIALIAS)
new_file = BytesIO()
image.save(new_file, target_type)
w, h = image.size
return f"image/{target_type}", new_file.getvalue(), w, h
except Exception:
log.exception(f"Failed to convert {source_mime} to {target_type}")
return source_mime, file, None, None
def _read_video_thumbnail(
data: bytes,
video_ext: str = "mp4",
frame_ext: str = "png",
max_size: tuple[int, int] = (1024, 720),
) -> tuple[bytes, int, int]:
with tempfile.NamedTemporaryFile(prefix="mxtg_video_", suffix=f".{video_ext}") as file:
# We don't have any way to read the video from memory, so save it to disk.
file.write(data)
# Read temp file and get frame
frame = VideoFileClip(file.name).get_frame(0)
# Convert to png and save to BytesIO
image = Image.fromarray(frame).convert("RGBA")
thumbnail_file = BytesIO()
if max_size:
image.thumbnail(max_size, Image.ANTIALIAS)
image.save(thumbnail_file, frame_ext)
w, h = image.size
return thumbnail_file.getvalue(), w, h
def _location_to_id(location: TypeLocation) -> str:
if isinstance(location, Document):
return f"{location.id}-{location.access_hash}"
elif isinstance(location, (InputDocumentFileLocation, InputPhotoFileLocation)):
return f"{location.id}-{location.access_hash}-{location.thumb_size}"
elif isinstance(location, InputFileLocation):
return f"{location.volume_id}-{location.local_id}"
elif isinstance(location, InputPeerPhotoFileLocation):
return str(location.photo_id)
async def transfer_thumbnail_to_matrix(
client: MautrixTelegramClient,
intent: IntentAPI,
thumbnail_loc: TypeLocation,
mime_type: str,
encrypt: bool,
video: bytes | None,
custom_data: bytes | None = None,
width: int | None = None,
height: int | None = None,
) -> DBTelegramFile | None:
if not Image or not VideoFileClip:
return None
loc_id = _location_to_id(thumbnail_loc)
if not loc_id:
return None
if custom_data:
loc_id += "-mau_custom_thumbnail"
db_file = await DBTelegramFile.get(loc_id)
if db_file:
return db_file
video_ext = sane_mimetypes.guess_extension(mime_type)
if custom_data:
file = custom_data
elif VideoFileClip and video_ext and video:
try:
file, width, height = _read_video_thumbnail(video, video_ext, frame_ext="png")
except OSError:
return None
mime_type = "image/png"
else:
file = await client.download_file(thumbnail_loc)
width, height = None, None
mime_type = magic.from_buffer(file, mime=True)
decryption_info = None
upload_mime_type = mime_type
if encrypt:
file, decryption_info = encrypt_attachment(file)
upload_mime_type = "application/octet-stream"
content_uri = await intent.upload_media(file, upload_mime_type)
if decryption_info:
decryption_info.url = content_uri
db_file = DBTelegramFile(
id=loc_id,
mxc=content_uri,
mime_type=mime_type,
was_converted=False,
timestamp=int(time.time()),
size=len(file),
width=width,
height=height,
decryption_info=decryption_info,
)
try:
await db_file.insert()
except (UniqueViolationError, IntegrityError) as e:
log.exception(
f"{e.__class__.__name__} while saving transferred file thumbnail data. "
"This was probably caused by two simultaneous transfers of the same file, "
"and might (but probably won't) cause problems with thumbnails or something."
)
return db_file
transfer_locks: dict[str, asyncio.Lock] = {}
TypeThumbnail = Optional[Union[TypeLocation, TypePhotoSize]]
async def transfer_file_to_matrix(
client: MautrixTelegramClient,
intent: IntentAPI,
location: TypeLocation,
thumbnail: TypeThumbnail = None,
*,
is_sticker: bool = False,
tgs_convert: dict | None = None,
filename: str | None = None,
encrypt: bool = False,
parallel_id: int | None = None,
) -> DBTelegramFile | None:
location_id = _location_to_id(location)
if not location_id:
return None
db_file = await DBTelegramFile.get(location_id)
if db_file:
return db_file
try:
lock = transfer_locks[location_id]
except KeyError:
lock = asyncio.Lock()
transfer_locks[location_id] = lock
async with lock:
return await _unlocked_transfer_file_to_matrix(
client,
intent,
location_id,
location,
thumbnail,
is_sticker,
tgs_convert,
filename,
encrypt,
parallel_id,
)
async def _unlocked_transfer_file_to_matrix(
client: MautrixTelegramClient,
intent: IntentAPI,
loc_id: str,
location: TypeLocation,
thumbnail: TypeThumbnail,
is_sticker: bool,
tgs_convert: dict | None,
filename: str | None,
encrypt: bool,
parallel_id: int | None,
) -> DBTelegramFile | None:
db_file = await DBTelegramFile.get(loc_id)
if db_file:
return db_file
converted_anim = None
if parallel_id and isinstance(location, Document) and (not is_sticker or not tgs_convert):
db_file = await parallel_transfer_to_matrix(
client, intent, loc_id, location, filename, encrypt, parallel_id
)
mime_type = location.mime_type
file = None
else:
try:
file = await client.download_file(location)
except (LocationInvalidError, FileIdInvalidError):
return None
except (AuthBytesInvalidError, AuthKeyInvalidError, SecurityError) as e:
log.exception(f"{e.__class__.__name__} while downloading a file.")
return None
width, height = None, None
mime_type = magic.from_buffer(file, mime=True)
image_converted = False
# A weird bug in alpine/magic makes it return application/octet-stream for gzips...
is_tgs = mime_type == "application/gzip" or (
mime_type == "application/octet-stream" and magic.from_buffer(file).startswith("gzip")
)
if is_sticker and tgs_convert and is_tgs:
converted_anim = await convert_tgs_to(
file, tgs_convert["target"], **tgs_convert["args"]
)
mime_type = converted_anim.mime
file = converted_anim.data
width, height = converted_anim.width, converted_anim.height
image_converted = mime_type != "application/gzip"
thumbnail = None
decryption_info = None
upload_mime_type = mime_type
if encrypt and encrypt_attachment:
file, decryption_info = encrypt_attachment(file)
upload_mime_type = "application/octet-stream"
content_uri = await intent.upload_media(file, upload_mime_type)
if decryption_info:
decryption_info.url = content_uri
db_file = DBTelegramFile(
id=loc_id,
mxc=content_uri,
decryption_info=decryption_info,
mime_type=mime_type,
was_converted=image_converted,
timestamp=int(time.time()),
size=len(file),
width=width,
height=height,
)
if thumbnail and (mime_type.startswith("video/") or mime_type == "image/gif"):
if isinstance(thumbnail, (PhotoSize, PhotoCachedSize)):
thumbnail = thumbnail.location
try:
db_file.thumbnail = await transfer_thumbnail_to_matrix(
client, intent, thumbnail, video=file, mime_type=mime_type, encrypt=encrypt
)
except FileIdInvalidError:
log.warning(f"Failed to transfer thumbnail for {thumbnail!s}", exc_info=True)
elif converted_anim and converted_anim.thumbnail_data:
db_file.thumbnail = await transfer_thumbnail_to_matrix(
client,
intent,
location,
video=None,
encrypt=encrypt,
custom_data=converted_anim.thumbnail_data,
mime_type=converted_anim.thumbnail_mime,
width=converted_anim.width,
height=converted_anim.height,
)
try:
await db_file.insert()
except (UniqueViolationError, IntegrityError) as e:
log.exception(
f"{e.__class__.__name__} while saving transferred file data. "
"This was probably caused by two simultaneous transfers of the same file, "
"and should not cause any problems."
)
return db_file
@@ -1,400 +0,0 @@
# mautrix-telegram - A Matrix-Telegram puppeting bridge
# Copyright (C) 2019 Tulir Asokan
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
from __future__ import annotations
from typing import AsyncGenerator, Awaitable, Union, cast
from collections import defaultdict
import asyncio
import hashlib
import logging
import math
import time
from aiohttp import ClientResponse
from telethon import helpers, utils
from telethon.crypto import AuthKey
from telethon.network import MTProtoSender
from telethon.tl.alltlobjects import LAYER
from telethon.tl.functions import InvokeWithLayerRequest
from telethon.tl.functions.auth import ExportAuthorizationRequest, ImportAuthorizationRequest
from telethon.tl.functions.upload import (
GetFileRequest,
SaveBigFilePartRequest,
SaveFilePartRequest,
)
from telethon.tl.types import (
Document,
InputDocumentFileLocation,
InputFile,
InputFileBig,
InputFileLocation,
InputPeerPhotoFileLocation,
InputPhotoFileLocation,
TypeInputFile,
)
from mautrix.appservice import IntentAPI
from mautrix.types import ContentURI, EncryptedFile
from mautrix.util.logging import TraceLogger
from ..db import TelegramFile as DBTelegramFile
from ..tgclient import MautrixTelegramClient
try:
from mautrix.crypto.attachments import async_encrypt_attachment
except ImportError:
async_encrypt_attachment = None
log: TraceLogger = cast(TraceLogger, logging.getLogger("mau.util"))
TypeLocation = Union[
Document,
InputDocumentFileLocation,
InputPeerPhotoFileLocation,
InputFileLocation,
InputPhotoFileLocation,
]
class DownloadSender:
sender: MTProtoSender
request: GetFileRequest
remaining: int
stride: int
def __init__(
self,
sender: MTProtoSender,
file: TypeLocation,
offset: int,
limit: int,
stride: int,
count: int,
) -> None:
self.sender = sender
self.request = GetFileRequest(file, offset=offset, limit=limit)
self.stride = stride
self.remaining = count
async def next(self) -> bytes | None:
if not self.remaining:
return None
result = await self.sender.send(self.request)
self.remaining -= 1
self.request.offset += self.stride
return result.bytes
def disconnect(self) -> Awaitable[None]:
return self.sender.disconnect()
class UploadSender:
sender: MTProtoSender
request: SaveFilePartRequest < SaveBigFilePartRequest
part_count: int
stride: int
previous: asyncio.Task | None
loop: asyncio.AbstractEventLoop
def __init__(
self,
sender: MTProtoSender,
file_id: int,
part_count: int,
big: bool,
index: int,
stride: int,
loop: asyncio.AbstractEventLoop,
) -> None:
self.sender = sender
self.part_count = part_count
if big:
self.request = SaveBigFilePartRequest(file_id, index, part_count, b"")
else:
self.request = SaveFilePartRequest(file_id, index, b"")
self.stride = stride
self.previous = None
self.loop = loop
async def next(self, data: bytes) -> None:
if self.previous:
await self.previous
self.previous = asyncio.create_task(self._next(data))
async def _next(self, data: bytes) -> None:
self.request.bytes = data
log.trace(
f"Sending file part {self.request.file_part}/{self.part_count}"
f" with {len(data)} bytes"
)
await self.sender.send(self.request)
self.request.file_part += self.stride
async def disconnect(self) -> None:
if self.previous:
await self.previous
return await self.sender.disconnect()
class ParallelTransferrer:
client: MautrixTelegramClient
loop: asyncio.AbstractEventLoop
dc_id: int
senders: list[DownloadSender | UploadSender] | None
auth_key: AuthKey
upload_ticker: int
def __init__(self, client: MautrixTelegramClient, dc_id: int | None = None) -> None:
self.client = client
self.loop = self.client.loop
self.dc_id = dc_id or self.client.session.dc_id
self.auth_key = (
None if dc_id and self.client.session.dc_id != dc_id else self.client.session.auth_key
)
self.senders = None
self.upload_ticker = 0
async def _cleanup(self) -> None:
await asyncio.gather(*(sender.disconnect() for sender in self.senders))
self.senders = None
@staticmethod
def _get_connection_count(
file_size: int, max_count: int = 20, full_size: int = 100 * 1024 * 1024
) -> int:
if file_size > full_size:
return max_count
return math.ceil((file_size / full_size) * max_count)
async def _init_download(
self, connections: int, file: TypeLocation, part_count: int, part_size: int
) -> None:
minimum, remainder = divmod(part_count, connections)
def get_part_count() -> int:
nonlocal remainder
if remainder > 0:
remainder -= 1
return minimum + 1
return minimum
# The first cross-DC sender will export+import the authorization, so we always create it
# before creating any other senders.
self.senders = [
await self._create_download_sender(
file, 0, part_size, connections * part_size, get_part_count()
),
*await asyncio.gather(
*(
self._create_download_sender(
file, i, part_size, connections * part_size, get_part_count()
)
for i in range(1, connections)
)
),
]
async def _create_download_sender(
self, file: TypeLocation, index: int, part_size: int, stride: int, part_count: int
) -> DownloadSender:
return DownloadSender(
await self._create_sender(), file, index * part_size, part_size, stride, part_count
)
async def _init_upload(
self, connections: int, file_id: int, part_count: int, big: bool
) -> None:
self.senders = [
await self._create_upload_sender(file_id, part_count, big, 0, connections),
*await asyncio.gather(
*(
self._create_upload_sender(file_id, part_count, big, i, connections)
for i in range(1, connections)
)
),
]
async def _create_upload_sender(
self, file_id: int, part_count: int, big: bool, index: int, stride: int
) -> UploadSender:
return UploadSender(
await self._create_sender(), file_id, part_count, big, index, stride, loop=self.loop
)
async def _create_sender(self) -> MTProtoSender:
dc = await self.client._get_dc(self.dc_id)
sender = MTProtoSender(self.auth_key, loggers=self.client._log)
await sender.connect(
self.client._connection(
dc.ip_address, dc.port, dc.id, loggers=self.client._log, proxy=self.client._proxy
)
)
if not self.auth_key:
log.debug(f"Exporting auth to DC {self.dc_id}")
auth = await self.client(ExportAuthorizationRequest(self.dc_id))
self.client._init_request.query = ImportAuthorizationRequest(
id=auth.id, bytes=auth.bytes
)
req = InvokeWithLayerRequest(LAYER, self.client._init_request)
await sender.send(req)
self.auth_key = sender.auth_key
return sender
async def init_upload(
self,
file_id: int,
file_size: int,
part_size_kb: float | None = None,
connection_count: int | None = None,
) -> tuple[int, int, bool]:
connection_count = connection_count or self._get_connection_count(file_size)
part_size = (part_size_kb or utils.get_appropriated_part_size(file_size)) * 1024
part_count = (file_size + part_size - 1) // part_size
is_large = file_size > 10 * 1024 * 1024
await self._init_upload(connection_count, file_id, part_count, is_large)
return part_size, part_count, is_large
async def upload(self, part: bytes) -> None:
await self.senders[self.upload_ticker].next(part)
self.upload_ticker = (self.upload_ticker + 1) % len(self.senders)
async def finish_upload(self) -> None:
await self._cleanup()
async def download(
self,
file: TypeLocation,
file_size: int,
part_size_kb: float | None = None,
connection_count: int | None = None,
) -> AsyncGenerator[bytes, None]:
connection_count = connection_count or self._get_connection_count(file_size)
part_size = (part_size_kb or utils.get_appropriated_part_size(file_size)) * 1024
part_count = math.ceil(file_size / part_size)
log.debug(
f"Starting parallel download: {connection_count} {part_size} {part_count} {file!s}"
)
await self._init_download(connection_count, file, part_count, part_size)
part = 0
while part < part_count:
tasks = []
for sender in self.senders:
tasks.append(asyncio.create_task(sender.next()))
for task in tasks:
data = await task
if not data:
break
yield data
part += 1
log.trace(f"Part {part} downloaded")
log.debug("Parallel download finished, cleaning up connections")
await self._cleanup()
parallel_transfer_locks: defaultdict[int, asyncio.Lock] = defaultdict(lambda: asyncio.Lock())
async def parallel_transfer_to_matrix(
client: MautrixTelegramClient,
intent: IntentAPI,
loc_id: str,
location: TypeLocation,
filename: str,
encrypt: bool,
parallel_id: int,
) -> DBTelegramFile:
size = location.size
mime_type = location.mime_type
dc_id, location = utils.get_input_location(location)
# We lock the transfers because telegram has connection count limits
async with parallel_transfer_locks[parallel_id]:
downloader = ParallelTransferrer(client, dc_id)
data = downloader.download(location, size)
decryption_info = None
up_mime_type = mime_type
if encrypt and async_encrypt_attachment:
async def encrypted(stream):
nonlocal decryption_info
async for chunk in async_encrypt_attachment(stream):
if isinstance(chunk, EncryptedFile):
decryption_info = chunk
else:
yield chunk
data = encrypted(data)
up_mime_type = "application/octet-stream"
content_uri = await intent.upload_media(
data, mime_type=up_mime_type, filename=filename, size=size if not encrypt else None
)
if decryption_info:
decryption_info.url = content_uri
return DBTelegramFile(
id=loc_id,
mxc=content_uri,
mime_type=mime_type,
was_converted=False,
timestamp=int(time.time()),
size=size,
width=None,
height=None,
decryption_info=decryption_info,
)
async def _internal_transfer_to_telegram(
client: MautrixTelegramClient, response: ClientResponse
) -> tuple[TypeInputFile, int]:
file_id = helpers.generate_random_long()
file_size = response.content_length
hash_md5 = hashlib.md5()
uploader = ParallelTransferrer(client)
part_size, part_count, is_large = await uploader.init_upload(file_id, file_size)
buffer = bytearray()
async for data in response.content:
if not is_large:
hash_md5.update(data)
if len(buffer) == 0 and len(data) == part_size:
await uploader.upload(data)
continue
new_len = len(buffer) + len(data)
if new_len >= part_size:
cutoff = part_size - len(buffer)
buffer.extend(data[:cutoff])
await uploader.upload(bytes(buffer))
buffer.clear()
buffer.extend(data[cutoff:])
else:
buffer.extend(data)
if len(buffer) > 0:
await uploader.upload(bytes(buffer))
await uploader.finish_upload()
if is_large:
return InputFileBig(file_id, part_count, "upload"), file_size
else:
return InputFile(file_id, part_count, "upload", hash_md5.hexdigest()), file_size
async def parallel_transfer_to_telegram(
client: MautrixTelegramClient, intent: IntentAPI, uri: ContentURI, parallel_id: int
) -> tuple[TypeInputFile, int]:
url = intent.api.get_download_url(uri)
async with parallel_transfer_locks[parallel_id]:
async with intent.api.session.get(url) as response:
return await _internal_transfer_to_telegram(client, response)
-56
View File
@@ -1,56 +0,0 @@
# mautrix-telegram - A Matrix-Telegram puppeting bridge
# Copyright (C) 2021 Tulir Asokan
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
from __future__ import annotations
from typing import Any
from mautrix.util.config import RecursiveDict
def recursive_set(data: dict[str, Any], key: str, value: Any) -> bool:
key, next_key = RecursiveDict.parse_key(key)
if next_key is not None:
if key not in data:
data[key] = {}
next_data = data.get(key, {})
if not isinstance(next_data, dict):
return False
return recursive_set(next_data, next_key, value)
data[key] = value
return True
def recursive_get(data: dict[str, Any], key: str) -> Any:
key, next_key = RecursiveDict.parse_key(key)
if next_key is not None:
next_data = data.get(key, None)
if not next_data:
return None
return recursive_get(next_data, next_key)
return data.get(key, None)
def recursive_del(data: dict[str, any], key: str) -> bool:
key, next_key = RecursiveDict.parse_key(key)
if next_key is not None:
if key not in data:
return False
next_data = data.get(key, {})
return recursive_del(next_data, next_key)
if key in data:
del data[key]
return True
return False
-37
View File
@@ -1,37 +0,0 @@
# mautrix-telegram - A Matrix-Telegram puppeting bridge
# Copyright (C) 2019 Tulir Asokan
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
import mimetypes
mimetypes.init()
sanity_overrides = {
"image/jpeg": ".jpeg",
"image/tiff": ".tiff",
"text/plain": ".txt",
"text/html": ".html",
"audio/mpeg": ".mp3",
"audio/ogg": ".ogg",
"application/xml": ".xml",
"application/octet-stream": "",
"application/x-msdos-program": ".exe",
}
def guess_extension(mime: str) -> str:
try:
return sanity_overrides[mime]
except KeyError:
return mimetypes.guess_extension(mime)
-190
View File
@@ -1,190 +0,0 @@
# mautrix-telegram - A Matrix-Telegram puppeting bridge
# Telegram lottie sticker converter
# Copyright (C) 2019 Randall Eramde Lawrence
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
from __future__ import annotations
from typing import Any, Awaitable, Callable
import asyncio.subprocess
import logging
import os.path
import shutil
import tempfile
from attr import dataclass
log: logging.Logger = logging.getLogger("mau.util.tgs")
@dataclass
class ConvertedSticker:
mime: str
data: bytes
thumbnail_mime: str | None = None
thumbnail_data: bytes | None = None
width: int = 0
height: int = 0
Converter = Callable[[bytes, int, int, Any], Awaitable[ConvertedSticker]]
converters: dict[str, Converter] = {}
def abswhich(program: str | None) -> str | None:
path = shutil.which(program)
return os.path.abspath(path) if path else None
lottieconverter = abswhich("lottieconverter")
ffmpeg = abswhich("ffmpeg")
if lottieconverter:
async def tgs_to_png(file: bytes, width: int, height: int, **_: Any) -> ConvertedSticker:
frame = 1
proc = await asyncio.create_subprocess_exec(
lottieconverter,
"-",
"-",
"png",
f"{width}x{height}",
str(frame),
stdout=asyncio.subprocess.PIPE,
stdin=asyncio.subprocess.PIPE,
)
stdout, stderr = await proc.communicate(file)
if proc.returncode == 0:
return ConvertedSticker("image/png", stdout)
else:
log.error(
"lottieconverter error: "
+ (
stderr.decode("utf-8")
if stderr is not None
else f"unknown ({proc.returncode})"
)
)
return ConvertedSticker("application/gzip", file)
async def tgs_to_gif(
file: bytes, width: int, height: int, fps: int = 25, **_: Any
) -> ConvertedSticker:
proc = await asyncio.create_subprocess_exec(
lottieconverter,
"-",
"-",
"gif",
f"{width}x{height}",
str(fps),
stdout=asyncio.subprocess.PIPE,
stdin=asyncio.subprocess.PIPE,
)
stdout, stderr = await proc.communicate(file)
if proc.returncode == 0:
return ConvertedSticker("image/gif", stdout)
else:
log.error(
"lottieconverter error: "
+ (
stderr.decode("utf-8")
if stderr is not None
else f"unknown ({proc.returncode})"
)
)
return ConvertedSticker("application/gzip", file)
converters["png"] = tgs_to_png
converters["gif"] = tgs_to_gif
if lottieconverter and ffmpeg:
async def tgs_to_webm(
file: bytes, width: int, height: int, fps: int = 30, **_: Any
) -> ConvertedSticker:
with tempfile.TemporaryDirectory(prefix="tgs_") as tmpdir:
file_template = tmpdir + "/out_"
proc = await asyncio.create_subprocess_exec(
lottieconverter,
"-",
file_template,
"pngs",
f"{width}x{height}",
str(fps),
stdout=asyncio.subprocess.PIPE,
stdin=asyncio.subprocess.PIPE,
)
_, stderr = await proc.communicate(file)
if proc.returncode == 0:
with open(f"{file_template}00.png", "rb") as first_frame_file:
first_frame_data = first_frame_file.read()
proc = await asyncio.create_subprocess_exec(
ffmpeg,
"-hide_banner",
"-loglevel",
"error",
"-framerate",
str(fps),
"-pattern_type",
"glob",
"-i",
file_template + "*.png",
"-c:v",
"libvpx-vp9",
"-pix_fmt",
"yuva420p",
"-f",
"webm",
"-",
stdout=asyncio.subprocess.PIPE,
stdin=asyncio.subprocess.PIPE,
)
stdout, stderr = await proc.communicate()
if proc.returncode == 0:
return ConvertedSticker("video/webm", stdout, "image/png", first_frame_data)
else:
log.error(
"ffmpeg error: "
+ (
stderr.decode("utf-8")
if stderr is not None
else f"unknown ({proc.returncode})"
)
)
else:
log.error(
"lottieconverter error: "
+ (
stderr.decode("utf-8")
if stderr is not None
else f"unknown ({proc.returncode})"
)
)
return ConvertedSticker("application/gzip", file)
converters["webm"] = tgs_to_webm
async def convert_tgs_to(
file: bytes, convert_to: str, width: int, height: int, **kwargs: Any
) -> ConvertedSticker:
if convert_to in converters:
converter = converters[convert_to]
converted = await converter(file, width, height, **kwargs)
converted.width = width
converted.height = height
return converted
elif convert_to != "disable":
log.warning(f"Unable to convert animated sticker, type {convert_to} not supported")
return ConvertedSticker("application/gzip", file)
-1
View File
@@ -1 +0,0 @@
from .get_version import git_revision, git_tag, linkified_version, version
-2
View File
@@ -1,2 +0,0 @@
from .provisioning import ProvisioningAPI
from .public import PublicBridgeWebsite
-1
View File
@@ -1 +0,0 @@
from .auth_api import AuthAPI
-330
View File
@@ -1,330 +0,0 @@
# mautrix-telegram - A Matrix-Telegram puppeting bridge
# Copyright (C) 2021 Tulir Asokan
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
from __future__ import annotations
from abc import abstractmethod
import abc
import asyncio
import logging
from aiohttp import web
from telethon.errors import (
AccessTokenExpiredError,
AccessTokenInvalidError,
FloodWaitError,
PasswordEmptyError,
PasswordHashInvalidError,
PhoneCodeExpiredError,
PhoneCodeInvalidError,
PhoneNumberAppSignupForbiddenError,
PhoneNumberBannedError,
PhoneNumberFloodError,
PhoneNumberInvalidError,
PhoneNumberUnoccupiedError,
SessionPasswordNeededError,
)
from mautrix.bridge import InvalidAccessToken, OnlyLoginSelf
from mautrix.util.format_duration import format_duration
from ...commands.telegram.auth import enter_password
from ...puppet import Puppet
from ...user import User
class AuthAPI(abc.ABC):
log: logging.Logger = logging.getLogger("mau.web.auth")
loop: asyncio.AbstractEventLoop
def __init__(self, loop: asyncio.AbstractEventLoop):
self.loop = loop
@abstractmethod
def get_login_response(
self,
status: int = 200,
state: str = "",
username: str = "",
phone: str = "",
human_tg_id: str = "",
mxid: str = "",
message: str = "",
error: str = "",
errcode: str = "",
) -> web.Response:
raise NotImplementedError()
@abstractmethod
def get_mx_login_response(
self,
status: int = 200,
state: str = "",
username: str = "",
phone: str = "",
human_tg_id: str = "",
mxid: str = "",
message: str = "",
error: str = "",
errcode: str = "",
) -> web.Response:
raise NotImplementedError()
async def post_matrix_token(self, user: User, token: str) -> web.Response:
puppet = await Puppet.get_by_tgid(user.tgid)
if puppet.is_real_user:
return self.get_mx_login_response(
state="already-logged-in",
status=409,
error="You have already logged in with your Matrix account.",
errcode="already-logged-in",
)
try:
await puppet.switch_mxid(token.strip(), user.mxid)
except OnlyLoginSelf:
return self.get_mx_login_response(
status=403,
errcode="only-login-self",
error="You can only log in as your own Matrix user.",
)
except InvalidAccessToken:
return self.get_mx_login_response(
status=401, errcode="invalid-access-token", error="Failed to verify access token."
)
return self.get_mx_login_response(mxid=user.mxid, status=200, state="logged-in")
async def post_matrix_password(self, user: User, password: str) -> web.Response:
return self.get_mx_login_response(
mxid=user.mxid, status=501, error="Not yet implemented", errcode="not-yet-implemented"
)
async def post_login_phone(self, user: User, phone: str) -> web.Response:
if not phone or not phone.strip():
return self.get_login_response(
mxid=user.mxid,
state="request",
status=400,
errcode="phone_number_invalid",
error="Phone number not given.",
)
try:
await user.client.sign_in(phone.strip())
return self.get_login_response(
mxid=user.mxid,
state="code",
status=200,
message="Code requested successfully. Check your SMS "
"or Telegram client and enter the code below.",
)
except PhoneNumberInvalidError:
return self.get_login_response(
mxid=user.mxid,
state="request",
status=400,
errcode="phone_number_invalid",
error="Invalid phone number.",
)
except PhoneNumberBannedError:
return self.get_login_response(
mxid=user.mxid,
state="request",
status=403,
errcode="phone_number_banned",
error="Your phone number is banned from Telegram.",
)
except PhoneNumberAppSignupForbiddenError:
return self.get_login_response(
mxid=user.mxid,
state="request",
status=403,
errcode="phone_number_app_signup_forbidden",
error="You have disabled 3rd party apps on your account.",
)
except PhoneNumberUnoccupiedError:
return self.get_login_response(
mxid=user.mxid,
state="request",
status=404,
errcode="phone_number_unoccupied",
error="That phone number has not been registered.",
)
except PhoneNumberFloodError:
return self.get_login_response(
mxid=user.mxid,
state="request",
status=429,
errcode="phone_number_flood",
error="Your phone number has been temporarily blocked for flooding. "
"The ban is usually applied for around a day.",
)
except FloodWaitError as e:
return self.get_login_response(
mxid=user.mxid,
state="request",
status=429,
errcode="flood_wait",
error="Your phone number has been temporarily blocked for flooding. "
f"Please wait for {format_duration(e.seconds)} before trying again.",
)
except Exception:
self.log.exception("Error requesting phone code")
return self.get_login_response(
mxid=user.mxid,
state="request",
status=500,
errcode="unknown_error",
error="Internal server error while requesting code.",
)
async def postprocess_login(self, user: User, user_info) -> None:
existing_user = await User.get_by_tgid(user_info.id)
if existing_user and existing_user != user:
await existing_user.log_out()
asyncio.create_task(user.post_login(user_info, first_login=True))
if user.command_status and user.command_status["action"] == "Login":
user.command_status = None
async def post_login_token(self, user: User, token: str) -> web.Response:
try:
user_info = await user.client.sign_in(bot_token=token.strip())
await self.postprocess_login(user, user_info)
return self.get_login_response(
mxid=user.mxid,
state="logged-in",
status=200,
username=user_info.username,
phone=None,
human_tg_id=f"@{user_info.username}",
)
except AccessTokenInvalidError:
return self.get_login_response(
mxid=user.mxid,
state="token",
status=401,
errcode="bot_token_invalid",
error="Bot token invalid.",
)
except AccessTokenExpiredError:
return self.get_login_response(
mxid=user.mxid,
state="token",
status=403,
errcode="bot_token_expired",
error="Bot token expired.",
)
except Exception:
self.log.exception("Error sending bot token")
return self.get_login_response(
mxid=user.mxid,
state="token",
status=500,
error="Internal server error while sending token.",
)
async def post_login_code(
self, user: User, code: int, password_in_data: bool
) -> web.Response | None:
try:
user_info = await user.client.sign_in(code=code)
await self.postprocess_login(user, user_info)
human_tg_id = f"@{user_info.username}" if user_info.username else f"+{user_info.phone}"
return self.get_login_response(
mxid=user.mxid,
state="logged-in",
status=200,
username=user_info.username,
phone=user_info.phone,
human_tg_id=human_tg_id,
)
except PhoneCodeInvalidError:
return self.get_login_response(
mxid=user.mxid,
state="code",
status=401,
errcode="phone_code_invalid",
error="Incorrect phone code.",
)
except PhoneCodeExpiredError:
return self.get_login_response(
mxid=user.mxid,
state="code",
status=403,
errcode="phone_code_expired",
error="Phone code expired.",
)
except SessionPasswordNeededError:
if not password_in_data:
if user.command_status and user.command_status["action"] == "Login":
user.command_status = {
"next": enter_password,
"action": "Login (password entry)",
}
message = (
"Code accepted, but you have 2-factor "
"authentication enabled. Please enter your password."
)
return self.get_login_response(
mxid=user.mxid, state="password", status=202, message=message
)
return None
except Exception:
self.log.exception("Error sending phone code")
return self.get_login_response(
mxid=user.mxid,
state="code",
status=500,
errcode="unknown_error",
error="Internal server error while sending code.",
)
async def post_login_password(self, user: User, password: str) -> web.Response:
try:
user_info = await user.client.sign_in(password=password.strip())
await self.postprocess_login(user, user_info)
human_tg_id = f"@{user_info.username}" if user_info.username else f"+{user_info.phone}"
return self.get_login_response(
mxid=user.mxid,
state="logged-in",
status=200,
username=user_info.username,
phone=user_info.phone,
human_tg_id=human_tg_id,
)
except PasswordEmptyError:
return self.get_login_response(
mxid=user.mxid,
state="password",
status=400,
errcode="password_empty",
error="Empty password.",
)
except PasswordHashInvalidError:
return self.get_login_response(
mxid=user.mxid,
state="password",
status=401,
errcode="password_invalid",
error="Incorrect password.",
)
except Exception:
self.log.exception("Error sending password")
return self.get_login_response(
mxid=user.mxid,
state="password",
status=500,
errcode="unknown_error",
error="Internal server error while sending password.",
)
@@ -1,589 +0,0 @@
# mautrix-telegram - A Matrix-Telegram puppeting bridge
# Copyright (C) 2021 Tulir Asokan
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
from __future__ import annotations
from typing import TYPE_CHECKING, Awaitable, Callable
import asyncio
import json
import logging
from aiohttp import web
from telethon.tl.types import ChannelForbidden, ChatForbidden, TypeChat
from telethon.utils import get_peer_id, resolve_id
from mautrix.appservice import AppService
from mautrix.errors import IntentError, MatrixRequestError
from mautrix.types import UserID
from ...commands.portal.util import get_initial_state, user_has_power_level
from ...portal import Portal
from ...types import TelegramID
from ...user import User
from ..common import AuthAPI
if TYPE_CHECKING:
from ...__main__ import TelegramBridge
class ProvisioningAPI(AuthAPI):
log: logging.Logger = logging.getLogger("mau.web.provisioning")
secret: str
az: AppService
bridge: "TelegramBridge"
app: web.Application
def __init__(self, bridge: "TelegramBridge") -> None:
super().__init__(bridge.loop)
self.secret = bridge.config["appservice.provisioning.shared_secret"]
self.az = bridge.az
self.bridge = bridge
self.app = web.Application(loop=bridge.loop, middlewares=[self.error_middleware])
portal_prefix = "/portal/{mxid:![^/]+}"
self.app.router.add_route("GET", f"{portal_prefix}", self.get_portal_by_mxid)
self.app.router.add_route("GET", "/portal/{tgid:-[0-9]+}", self.get_portal_by_tgid)
self.app.router.add_route(
"POST", portal_prefix + "/connect/{chat_id:-[0-9]+}", self.connect_chat
)
self.app.router.add_route("POST", f"{portal_prefix}/create", self.create_chat)
self.app.router.add_route("POST", f"{portal_prefix}/disconnect", self.disconnect_chat)
user_prefix = "/user/{mxid:@[^:]*:[^/]+}"
self.app.router.add_route("GET", f"{user_prefix}", self.get_user_info)
self.app.router.add_route("GET", f"{user_prefix}/chats", self.get_chats)
self.app.router.add_route("POST", f"{user_prefix}/logout", self.logout)
self.app.router.add_route("POST", f"{user_prefix}/login/bot_token", self.send_bot_token)
self.app.router.add_route("POST", f"{user_prefix}/login/request_code", self.request_code)
self.app.router.add_route("POST", f"{user_prefix}/login/send_code", self.send_code)
self.app.router.add_route("POST", f"{user_prefix}/login/send_password", self.send_password)
self.app.router.add_route("GET", "/bridge", self.bridge_info)
async def get_portal_by_mxid(self, request: web.Request) -> web.Response:
err = self.check_authorization(request)
if err is not None:
return err
mxid = request.match_info["mxid"]
portal = await Portal.get_by_mxid(mxid)
if not portal:
return self.get_error_response(
404, "portal_not_found", "Portal with given Matrix ID not found."
)
return await self._get_portal_response(UserID(request.query.get("user_id", "")), portal)
async def get_portal_by_tgid(self, request: web.Request) -> web.Response:
err = self.check_authorization(request)
if err is not None:
return err
try:
tgid, _ = resolve_id(int(request.match_info["tgid"]))
except ValueError:
return self.get_error_response(400, "tgid_invalid", "Given chat ID is not valid.")
portal = await Portal.get_by_tgid(tgid)
if not portal:
return self.get_error_response(
404, "portal_not_found", "Portal to given Telegram chat not found."
)
return await self._get_portal_response(UserID(request.query.get("user_id", "")), portal)
async def _get_portal_response(self, user_id: UserID, portal: Portal) -> web.Response:
user, _ = await self.get_user(user_id, expect_logged_in=None, require_puppeting=False)
return web.json_response(
{
"mxid": portal.mxid,
"chat_id": get_peer_id(portal.peer),
"peer_type": portal.peer_type,
"title": portal.title,
"about": portal.about,
"username": portal.username,
"megagroup": portal.megagroup,
"can_unbridge": (await portal.can_user_perform(user, "unbridge"))
if user
else False,
}
)
async def connect_chat(self, request: web.Request) -> web.Response:
err = self.check_authorization(request)
if err is not None:
return err
room_id = request.match_info["mxid"]
if await Portal.get_by_mxid(room_id):
return self.get_error_response(
409, "room_already_bridged", "Room is already bridged to another Telegram chat."
)
chat_id = request.match_info["chat_id"]
if chat_id.startswith("-100"):
tgid = TelegramID(int(chat_id[4:]))
peer_type = "channel"
elif chat_id.startswith("-"):
tgid = TelegramID(-int(chat_id))
peer_type = "chat"
else:
return self.get_error_response(400, "tgid_invalid", "Invalid Telegram chat ID.")
user, err = await self.get_user(
request.query.get("user_id", None), expect_logged_in=None, require_puppeting=False
)
if err is not None:
return err
elif user and not await user_has_power_level(room_id, self.az.intent, user, "bridge"):
return self.get_error_response(
403,
"not_enough_permissions",
"You do not have the permissions to bridge that room.",
)
is_logged_in = user is not None and await user.is_logged_in()
acting_user = user if is_logged_in else self.bridge.bot
if not acting_user:
return self.get_login_response(
status=403,
errcode="not_logged_in",
error="You are not logged in and there is no relay bot.",
)
portal = await Portal.get_by_tgid(tgid, peer_type=peer_type)
if portal.mxid == room_id:
return self.get_error_response(
200, "bridge_exists", "Telegram chat is already bridged to that Matrix room."
)
elif portal.mxid:
force = request.query.get("force", None)
if force in ("delete", "unbridge"):
delete = force == "delete"
await portal.cleanup_portal(
"Portal deleted (moving to another room)"
if delete
else "Room unbridged (portal moving to another room)",
puppets_only=not delete,
)
else:
return self.get_error_response(
409,
"chat_already_bridged",
"Telegram chat is already bridged to another Matrix room.",
)
async with portal._room_create_lock:
entity: TypeChat | None = None
try:
entity = await acting_user.client.get_entity(portal.peer)
except Exception:
self.log.exception("Failed to get_entity(%s) for manual bridging.", portal.peer)
if not entity or isinstance(entity, (ChatForbidden, ChannelForbidden)):
if is_logged_in:
return self.get_error_response(
403,
"user_not_in_chat",
"Failed to get info of Telegram chat. Are you in the chat?",
)
return self.get_error_response(
403,
"bot_not_in_chat",
"Failed to get info of Telegram chat. Is the relay bot in the chat?",
)
portal.mxid = room_id
portal.by_mxid[portal.mxid] = portal
(portal.title, portal.about, levels, portal.encrypted) = await get_initial_state(
self.az.intent, room_id
)
portal.photo_id = ""
await portal.save()
asyncio.create_task(portal.update_matrix_room(user, entity, direct=False, levels=levels))
return web.Response(status=202, body="{}")
async def create_chat(self, request: web.Request) -> web.Response:
err = self.check_authorization(request)
if err is not None:
return err
data = await self.get_data(request)
if not data:
return self.get_error_response(400, "json_invalid", "Invalid JSON.")
room_id = request.match_info["mxid"]
if await Portal.get_by_mxid(room_id):
return self.get_error_response(
409, "room_already_bridged", "Room is already bridged to another Telegram chat."
)
user, err = await self.get_user(
request.query.get("user_id", None), expect_logged_in=None, require_puppeting=False
)
if err is not None:
return err
elif not await user.is_logged_in() or user.is_bot:
return self.get_error_response(
403, "not_logged_in_real_account", "You are not logged in with a real account."
)
elif not await user_has_power_level(room_id, self.az.intent, user, "bridge"):
return self.get_error_response(
403,
"not_enough_permissions",
"You do not have the permissions to bridge that room.",
)
try:
title, about, _, encrypted = await get_initial_state(self.az.intent, room_id)
except (MatrixRequestError, IntentError):
return self.get_error_response(
403, "bot_not_in_room", "The bridge bot is not in the given room."
)
about = data.get("about", about)
title = data.get("title", title)
if len(title) == 0:
return self.get_error_response(400, "body_value_invalid", "Title can not be empty.")
type = data.get("type", "")
if type not in ("group", "chat", "supergroup", "channel"):
return self.get_error_response(
400, "body_value_invalid", "Given chat type is not valid."
)
supergroup = type == "supergroup"
type = {
"supergroup": "channel",
"channel": "channel",
"chat": "chat",
"group": "chat",
}[type]
portal = Portal(
tgid=TelegramID(0),
mxid=room_id,
title=title,
about=about,
peer_type=type,
encrypted=encrypted,
tg_receiver=TelegramID(0),
)
try:
await portal.create_telegram_chat(user, supergroup=supergroup)
except ValueError as e:
await portal.delete()
return self.get_error_response(500, "unknown_error", e.args[0])
return web.json_response(
{
"chat_id": portal.tgid,
},
status=201,
)
async def disconnect_chat(self, request: web.Request) -> web.Response:
err = self.check_authorization(request)
if err is not None:
return err
portal = await Portal.get_by_mxid(request.match_info["mxid"])
if not portal or not portal.tgid:
return self.get_error_response(404, "portal_not_found", "Room is not a portal.")
user, err = await self.get_user(
request.query.get("user_id", None),
expect_logged_in=None,
require_puppeting=False,
require_user=False,
)
if err is not None:
return err
elif user and not await user_has_power_level(
portal.mxid, self.az.intent, user, "unbridge"
):
return self.get_error_response(
403,
"not_enough_permissions",
"You do not have the permissions to unbridge that room.",
)
delete = request.query.get("delete", "").lower() in ("true", "t", "1", "yes", "y")
sync = request.query.get("delete", "").lower() in ("true", "t", "1", "yes", "y")
coro = portal.cleanup_and_delete() if delete else portal.unbridge()
if sync:
try:
await coro
except Exception:
self.log.exception("Failed to disconnect chat")
return self.get_error_response(500, "exception", "Failed to disconnect chat")
else:
asyncio.create_task(coro)
return web.json_response({}, status=200 if sync else 202)
async def get_user_info(self, request: web.Request) -> web.Response:
data, user, err = await self.get_user_request_info(
request, expect_logged_in=None, require_puppeting=False
)
if err is not None:
return err
user_data = None
if await user.is_logged_in():
me = await user.get_me()
if me:
await user.update_info(me)
user_data = {
"id": user.tgid,
"username": user.tg_username,
"first_name": me.first_name,
"last_name": me.last_name,
"phone": user.tg_phone,
"is_bot": user.is_bot,
}
return web.json_response(
{
"telegram": user_data,
"mxid": user.mxid,
"permissions": user.permissions,
}
)
async def get_chats(self, request: web.Request) -> web.Response:
data, user, err = await self.get_user_request_info(request, expect_logged_in=True)
if err is not None:
return err
if not user.is_bot:
return web.json_response(
[
{
"id": chat.id,
"title": chat.title,
}
async for chat in user.client.iter_dialogs(
ignore_migrated=True, archived=False
)
]
)
else:
return web.json_response(
[
{
"id": get_peer_id(chat.peer),
"title": chat.title,
}
for chat in (await user.get_cached_portals()).values()
if chat.tgid
]
)
async def send_bot_token(self, request: web.Request) -> web.Response:
data, user, err = await self.get_user_request_info(request)
if err is not None:
return err
return await self.post_login_token(user, data.get("token", ""))
async def request_code(self, request: web.Request) -> web.Response:
data, user, err = await self.get_user_request_info(request)
if err is not None:
return err
return await self.post_login_phone(user, data.get("phone", ""))
async def send_code(self, request: web.Request) -> web.Response:
data, user, err = await self.get_user_request_info(request)
if err is not None:
return err
return await self.post_login_code(user, data.get("code", 0), password_in_data=False)
async def send_password(self, request: web.Request) -> web.Response:
data, user, err = await self.get_user_request_info(request)
if err is not None:
return err
return await self.post_login_password(user, data.get("password", ""))
async def logout(self, request: web.Request) -> web.Response:
_, user, err = await self.get_user_request_info(
request, expect_logged_in=None, require_puppeting=False, want_data=False
)
if err is not None:
return err
await user.log_out()
return web.json_response({}, status=200)
async def bridge_info(self, request: web.Request) -> web.Response:
return web.json_response(
{
"relaybot_username": (
self.bridge.bot.tg_username if self.bridge.bot is not None else None
),
},
status=200,
)
@staticmethod
async def error_middleware(
_, handler: Callable[[web.Request], Awaitable[web.Response]]
) -> Callable[[web.Request], Awaitable[web.Response]]:
async def middleware_handler(request: web.Request) -> web.Response:
try:
return await handler(request)
except web.HTTPException as ex:
return web.json_response(
{
"error": f"Unhandled HTTP {ex.status}",
"errcode": f"unhandled_http_{ex.status}",
},
status=ex.status,
)
return middleware_handler
@staticmethod
def get_error_response(status=200, errcode="", error="") -> web.Response:
return web.json_response(
{
"error": error,
"errcode": errcode,
},
status=status,
)
def get_mx_login_response(
self,
status=200,
state="",
username="",
phone="",
human_tg_id="",
mxid="",
message="",
error="",
errcode="",
):
raise NotImplementedError()
def get_login_response(
self,
status=200,
state="",
username="",
phone: str = "",
human_tg_id: str = "",
mxid="",
message="",
error="",
errcode="",
) -> web.Response:
if username or phone:
resp = {
"state": "logged-in",
"username": username,
"phone": phone,
}
elif message:
resp = {
"state": state,
"message": message,
}
else:
resp = {
"error": error,
"errcode": errcode,
}
if state:
resp["state"] = state
return web.json_response(resp, status=status)
def check_authorization(self, request: web.Request) -> web.Response | None:
auth = request.headers.get("Authorization", "")
if auth != f"Bearer {self.secret}":
return self.get_error_response(
error="Shared secret is not valid.", errcode="shared_secret_invalid", status=401
)
return None
@staticmethod
async def get_data(request: web.Request) -> dict | None:
try:
return await request.json()
except json.JSONDecodeError:
return None
async def get_user(
self,
mxid: UserID | None,
expect_logged_in: bool | None = False,
require_puppeting: bool = True,
require_user: bool = True,
) -> tuple[User | None, web.Response | None]:
if not mxid:
if not require_user:
return None, None
return None, self.get_login_response(
error="User ID not given.", errcode="mxid_empty", status=400
)
user = await User.get_and_start_by_mxid(mxid, even_if_no_session=True)
if require_puppeting and not user.puppet_whitelisted:
return user, self.get_login_response(
error="You are not whitelisted.", errcode="mxid_not_whitelisted", status=403
)
if expect_logged_in is not None:
logged_in = await user.is_logged_in()
if not expect_logged_in and logged_in:
return user, self.get_login_response(
username=user.tg_username,
phone=user.tg_phone,
status=409,
error="You are already logged in.",
errcode="already_logged_in",
)
elif expect_logged_in and not logged_in:
return user, self.get_login_response(
status=403, error="You are not logged in.", errcode="not_logged_in"
)
return user, None
async def get_user_request_info(
self,
request: web.Request,
expect_logged_in: bool | None = False,
require_puppeting: bool = False,
want_data: bool = True,
) -> tuple[dict | None, User | None, web.Response | None]:
err = self.check_authorization(request)
if err is not None:
return None, None, err
data = None
if want_data and (request.method == "POST" or request.method == "PUT"):
data = await self.get_data(request)
if not data:
return (
None,
None,
self.get_login_response(
error="Invalid JSON.", errcode="json_invalid", status=400
),
)
mxid = request.match_info["mxid"]
user, err = await self.get_user(mxid, expect_logged_in, require_puppeting)
return data, user, err
-895
View File
@@ -1,895 +0,0 @@
swagger: "2.0"
info:
title: Mautrix-Telegram provisioning
version: 0.3.0
description: The provisioning API for Mautrix-Telegram, the Matrix-Telegram puppeting/relaybot bridge.
license:
name: AGPLv3
url: https://github.com/mautrix/telegram/blob/master/LICENSE
externalDocs:
description: Provisioning API docs on docs.mau.fi
url: https://docs.mau.fi/bridges/python/telegram/provisioning-api.html
basePath: /_matrix/provision/v1
schemes: [https]
consumes: [application/json]
produces: [application/json]
tags:
- name: User info
- name: Authentication
- name: Bridging
- name: Misc
paths:
/bridge:
get:
operationId: get_bridge
summary: Get the bridge's information
tags: [Misc]
responses:
200:
description: The bridge information
schema:
type: object
properties:
relaybot_username:
type: string
description: The relay bot's username on Telegram
/portal/{room_id}:
get:
operationId: get_portal
summary: Get the bridging status and info of the connected Telegram chat
tags: [Bridging]
responses:
200:
description: Room is bridged
schema:
$ref: "#/definitions/PortalInfo"
400:
$ref: "#/responses/BadRequest"
404:
description: Unknown portal
schema:
type: object
title: Error
properties:
errcode:
type: string
title: Error code
description: A machine-readable error code
enum:
- portal_not_found
error:
$ref: "#/definitions/HumanReadableError"
parameters:
- name: room_id
in: path
description: The Matrix ID of the room whose bridging status to get
required: true
type: string
pattern: "![^/]+"
- name: user_id
in: query
description: Optional Matrix user ID to check if the user has permissions to do bridging.
required: false
type: string
/portal/{chat_id}:
get:
operationId: get_portal_by_tgid
summary: Get the bridging status and info of the connected Telegram chat
tags: [Bridging]
responses:
200:
description: Chat is bridged
schema:
$ref: "#/definitions/PortalInfo"
400:
description: Invalid Telegram chat ID
schema:
type: object
title: Error
properties:
errcode:
type: string
title: Error code
description: A machine-readable error code
enum:
- tgid_invalid
error:
$ref: "#/definitions/HumanReadableError"
404:
description: Unknown portal
schema:
type: object
title: Error
properties:
errcode:
type: string
title: Error code
description: A machine-readable error code
enum:
- portal_not_found
error:
$ref: "#/definitions/HumanReadableError"
parameters:
- name: chat_id
in: path
description: The Matrix ID of the room whose bridging status to get
required: true
type: integer
pattern: "-[0-9]+"
- name: user_id
in: query
description: Optional Matrix user ID to check if the user has permissions to do bridging.
required: false
type: string
/portal/{room_id}/connect/{chat_id}:
post:
operationId: connect_portal
summary: Connect an existing Telegram chat to the given room
tags: [Bridging]
parameters:
- name: room_id
in: path
description: The Matrix ID of the room to which the Telegram chat should be connected
required: true
type: string
- name: chat_id
in: path
description: The ID of the Telegram chat to connect
required: true
type: integer
pattern: "-[0-9]+"
- name: force
in: query
description: Set to force bridging by unbridging or deleting existing portal rooms.
required: false
type: string
enum:
- delete
- unbridge
- name: user_id
in: query
description: Optional Matrix user ID to check if the user has permissions to do the bridging.
required: false
type: string
responses:
200:
description: Telegram chat was already bridged to given room.
202:
description: Room bridging initiated
400:
$ref: "#/responses/BadRequest"
403:
description: "Given user doesn't have permission to bridge the room, or the bridge bot is not in the room"
schema:
type: object
title: Error
properties:
errcode:
type: string
title: Error code
description: A machine-readable error code
enum:
- not_enough_permissions
- bot_not_in_room
- bot_not_in_chat
- not_logged_in
error:
$ref: "#/definitions/HumanReadableError"
409:
description: Matrix room or Telegram chat is already bridged to another chat/room
schema:
type: object
title: Error
properties:
errcode:
type: string
title: Error code
description: A machine-readable error code
example: <room|chat>_already_bridged
enum:
- room_already_bridged
- chat_already_bridged
error:
$ref: "#/definitions/HumanReadableError"
/portal/{room_id}/create:
post:
operationId: create_portal
summary: Create a new Telegram chat for the given room
tags: [Bridging]
responses:
201:
description: Telegram chat created
schema:
type: object
properties:
chat_id:
type: integer
400:
$ref: "#/responses/BadRequest"
403:
description: "Given user isn't logged in with a real account or doesn't have permission to bridge the room, or the bridge bot is not in the room"
schema:
type: object
title: Error
properties:
errcode:
type: string
title: Error code
description: A machine-readable error code
enum:
- not_logged_in_real_account
- not_enough_permissions
- bot_not_in_room
error:
$ref: "#/definitions/HumanReadableError"
409:
description: Room is already bridged
schema:
type: object
title: Error
properties:
errcode:
type: string
title: Error code
description: A machine-readable error code
enum:
- room_already_bridged
error:
$ref: "#/definitions/HumanReadableError"
parameters:
- name: room_id
in: path
description: The Matrix ID of the room whose bridging status to get
required: true
type: string
- name: body
in: body
required: true
schema:
type: object
required: [type]
properties:
type:
description: The type of chat to create
type: string
example: supergroup
enum:
- chat
- supergroup
- channel
title:
description: Title for the new chat
type: string
example: Mautrix-Telegram Bridge
about:
description: About text for the new chat
type: string
example: Discussion about mautrix-telegram
- name: user_id
in: query
description: Matrix user to create the chat as.
required: true
type: string
/portal/{room_id}/disconnect:
post:
operationId: disconnect_portal
summary: Disconnect the Telegram chat from the room
tags: [Bridging]
responses:
202:
description: Room unbridging initiated
400:
$ref: "#/responses/BadRequest"
403:
$ref: "#/responses/PermissionError"
404:
description: Unknown portal
schema:
type: object
title: Error
properties:
errcode:
type: string
title: Error code
description: A machine-readable error code
enum:
- portal_not_found
error:
$ref: "#/definitions/HumanReadableError"
parameters:
- name: room_id
in: path
description: The Matrix ID of the room whose bridging status to get
required: true
type: string
- name: user_id
in: query
description: Optional Matrix user ID to check if the user has permissions to do the bridging.
required: false
type: string
- name: delete
in: query
description: Whether or not to delete the room completely (kick all users instead of just Telegram puppets)
required: false
type: boolean
default: false
- name: sync
in: query
description: Whether or not to wait for the unbridging to be completed before responding. **Could cause timeouts in large rooms**
required: false
type: boolean
default: false
/user/{user_id}:
get:
operationId: get_me
summary: Get the info of the Telegram user the given Matrix user is logged in as
tags: [User info]
responses:
200:
description: User found
schema:
$ref: "#/definitions/UserInfo"
400:
$ref: "#/responses/BadRequest"
403:
$ref: "#/responses/NotWhitelistedError"
500:
$ref: "#/responses/UnknownError"
parameters:
- name: user_id
in: path
description: The Matrix ID of the user who to log in as
required: true
type: string
/user/{user_id}/chats:
get:
operationId: get_chats
summary: Get the list of Telegram chats the given Matrix user has access to
tags: [User info]
responses:
200:
description: User is logged in
schema:
$ref: "#/definitions/UserChats"
400:
$ref: "#/responses/BadRequest"
403:
description: User is not logged in or not whitelisted
schema:
type: object
title: Error
properties:
errcode:
type: string
title: Error code
description: A machine-readable error code
enum:
- not_logged_in
- mxid_not_whitelisted
error:
$ref: "#/definitions/HumanReadableError"
500:
$ref: "#/responses/UnknownError"
parameters:
- name: user_id
in: path
description: The Matrix ID of the user who to log in as
required: true
type: string
/user/{user_id}/login/bot_token:
post:
operationId: post_bot_token
summary: Log in with a bot token
tags: [Authentication]
responses:
200:
description: Login successful
schema:
$ref: "#/definitions/AuthSuccess"
400:
$ref: "#/responses/BadRequest"
401:
description: Invalid or expired bot token or invalid shared secret
schema:
type: object
title: Error
properties:
errcode:
type: string
title: Error code
description: A machine-readable error code
example: bot_token_<error>
enum:
- bot_token_invalid
- bot_token_expired
- shared_secret_invalid
error:
$ref: "#/definitions/HumanReadableError"
403:
$ref: "#/responses/NotWhitelistedError"
409:
$ref: "#/responses/AlreadyLoggedInError"
500:
$ref: "#/responses/UnknownError"
parameters:
- name: user_id
in: path
description: The Matrix ID of the user who to log in as
required: true
type: string
- name: body
in: body
required: true
schema:
type: object
properties:
token:
type: string
description: The access token of the bot to log in as
example: "297900271:IXjeGEcAN61zHnjPgkWnYWyvVp9K4ulHBEv"
/user/{user_id}/login/request_code:
post:
operationId: post_login_phone
summary: Request a phone code from Telegram
tags: [Authentication]
responses:
200:
description: Code requested successfully
schema:
$ref: "#/definitions/AuthSuccess"
400:
description: Invalid phone number or JSON
schema:
type: object
title: Error
properties:
errcode:
type: string
title: Error code
description: A machine-readable error code
example: machine_readable_error
enum:
- phone_number_invalid
- json_invalid
error:
$ref: "#/definitions/HumanReadableError"
401:
description: Invalid shared secret
schema:
type: object
title: Error
properties:
errcode:
type: string
title: Error code
description: A machine-readable error code
enum:
- shared_secret_invalid
error:
$ref: "#/definitions/HumanReadableError"
403:
description: Matrix ID is not whitelisted or phone number is banned or has forbidden 3rd party apps
schema:
type: object
title: Error
properties:
errcode:
type: string
title: Error code
description: A machine-readable error code
example: machine_readable_error
enum:
- mxid_not_whitelisted
- phone_number_banned
- phone_number_app_signup_forbidden
error:
$ref: "#/definitions/HumanReadableError"
404:
description: Unregistered phone number
schema:
type: object
title: Error
properties:
errcode:
type: string
title: Error code
description: A machine-readable error code
enum:
- phone_number_unoccupied
error:
$ref: "#/definitions/HumanReadableError"
409:
$ref: "#/responses/AlreadyLoggedInError"
429:
description: Phone number has been temporarily blocked for flooding
schema:
type: object
title: Error
properties:
errcode:
type: string
title: Error code
description: A machine-readable error code
enum:
- flood_wait
- phone_number_flood
error:
$ref: "#/definitions/HumanReadableError"
500:
$ref: "#/responses/UnknownError"
parameters:
- name: user_id
in: path
description: The Matrix ID of the user who to log in as
required: true
type: string
- name: body
in: body
required: true
schema:
type: object
properties:
phone:
type: string
description: The phone number to log in as.
example: "+123456789"
/user/{user_id}/login/send_code:
post:
operationId: post_login_code
summary: Send the login code
tags: [Authentication]
responses:
200:
description: Login successful
schema:
$ref: "#/definitions/AuthSuccess"
202:
description: Correct code, but two-factor authentication is enabled
schema:
$ref: "#/definitions/AuthSuccess"
400:
$ref: "#/responses/BadRequest"
401:
description: Invalid phone code or shared secret
schema:
type: object
title: Error
properties:
errcode:
type: string
title: Error code
description: A machine-readable error code
enum:
- phone_code_invalid
- shared_secret_invalid
error:
$ref: "#/definitions/HumanReadableError"
403:
description: Matrix ID not whitelisted or phone code expired
schema:
type: object
title: Error
properties:
errcode:
type: string
title: Error code
description: A machine-readable error code
example: machine_readable_error
enum:
- mxid_not_whitelisted
- phone_code_expired
error:
$ref: "#/definitions/HumanReadableError"
409:
$ref: "#/responses/AlreadyLoggedInError"
500:
$ref: "#/responses/UnknownError"
parameters:
- name: user_id
in: path
description: The Matrix ID of the user who to log in as
required: true
type: string
- name: body
in: body
required: true
schema:
type: object
properties:
code:
type: integer
description: The phone code from Telegram.
format: int32
example: 123456
/user/{user_id}/login/send_password:
post:
operationId: post_login_password
summary: Send the two-factor auth password
tags: [Authentication]
responses:
200:
description: Login successful
schema:
$ref: "#/definitions/AuthSuccess"
400:
description: Missing password or invalid JSON
schema:
type: object
title: Error
properties:
errcode:
type: string
title: Error code
description: A machine-readable error code
example: <field>_empty
enum:
- password_empty
- json_invalid
error:
$ref: "#/definitions/HumanReadableError"
401:
description: Incorrect password or invalid shared secret
schema:
type: object
title: Error
properties:
errcode:
type: string
title: Error code
description: A machine-readable error code
enum:
- password_invalid
- shared_secret_invalid
error:
$ref: "#/definitions/HumanReadableError"
403:
$ref: "#/responses/NotWhitelistedError"
409:
$ref: "#/responses/AlreadyLoggedInError"
500:
$ref: "#/responses/UnknownError"
parameters:
- name: user_id
in: path
description: The Matrix ID of the user who to log in as
required: true
type: string
- name: body
in: body
required: true
schema:
type: object
properties:
password:
type: string
description: The two-factor auth password
format: password
example: hunter2
/user/{user_id}/logout:
post:
operationId: logout
summary: Log out
tags: [Authentication]
responses:
200:
description: Logout successful
403:
description: User was not logged in
schema:
type: object
title: Error
properties:
errcode:
type: string
title: Error code
description: A machine-readable error code
enum:
- not_logged_in
error:
$ref: "#/definitions/HumanReadableError"
500:
$ref: "#/responses/UnknownError"
parameters:
- name: user_id
in: path
description: The Matrix ID of the user who to log out as
required: true
type: string
responses:
NotWhitelistedError:
description: Matrix ID not whitelisted for puppeting
schema:
type: object
title: Error
properties:
errcode:
type: string
title: Error code
description: A machine-readable error code
enum:
- mxid_not_whitelisted
error:
$ref: "#/definitions/HumanReadableError"
AlreadyLoggedInError:
description: The Matrix user is already logged in
schema:
type: object
properties:
state:
type: string
enum:
- logged-in
username:
type: string
description: The Telegram username the user is logged in as.
phone:
type: string
description: The phone number of the account the user is logged into.
BadRequest:
description: Invalid JSON.
schema:
type: object
title: Error
properties:
errcode:
type: string
title: Error code
description: A machine-readable error code
enum:
- json_invalid
- mxid_empty
- body_value_missing
- body_value_invalid
error:
$ref: "#/definitions/HumanReadableError"
UnknownError:
description: Unknown error
schema:
type: object
title: UnknownError
properties:
errcode:
type: string
title: Error code
description: A machine-readable error code
enum:
- unknown_error
- unhandled_error
error:
type: string
title: Error
description: A human-readable description of the error
example: Internal server error while <action>.
PermissionError:
description: The given Matrix user doesn't have the permissions to do that.
schema:
type: object
title: Error
properties:
errcode:
type: string
title: Error code
description: A machine-readable error code
example: not_enough_permissions
enum:
- not_enough_permissions
error:
$ref: "#/definitions/HumanReadableError"
definitions:
UserInfo:
type: object
properties:
mxid:
type: string
example: "@usern:example.com"
permissions:
type: string
example: user
enum:
- none
- relaybot
- user
- full
- admin
telegram:
type: object
properties:
id:
type: integer
example: 123456789
username:
type: string
example: username
first_name:
type: string
example: Usern
last_name:
type: string
example: A.
phone:
type: string
example: 123456789
is_bot:
type: boolean
example: false
UserChats:
type: array
items:
type: object
properties:
id:
type: integer
example: -123456789
description: A bot API style chat ID.
title:
type: string
PortalInfo:
type: object
properties:
mxid:
type: string
example: "!foo:example.com"
chat_id:
type: integer
example: -100123456789
peer_type:
type: string
enum:
- user
- chat
- channel
megagroup:
type: boolean
username:
type: string
title:
type: string
about:
type: string
can_unbridge:
type: boolean
description: If a user ID was provided with the request, this will indicate whether or not the user can unbridge the room.
AuthSuccess:
type: object
properties:
state:
type: string
description: The state/next step after the successful operation.
enum:
- code
- request
- password
- token
- logged-in
username:
type: string
description: The Telegram username the user is logged in as. Only applicable if state=logged-in
phone:
type: string
description: The phone number of the account the user logged into. Only applicable if state=logged-in
HumanReadableError:
type: string
description: A human-readable description of the error
example: A human-readable description of the error
security:
- Bearer: []
securityDefinitions:
Bearer:
description: Required authentication for all endpoints
name: Authorization
in: header
type: apiKey
-235
View File
@@ -1,235 +0,0 @@
# mautrix-telegram - A Matrix-Telegram puppeting bridge
# Copyright (C) 2021 Tulir Asokan
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
from __future__ import annotations
import asyncio
import logging
import random
import string
import time
from aiohttp import web
from mako.template import Template
import pkg_resources
from mautrix.types import UserID
from mautrix.util.signed_token import sign_token, verify_token
from ...puppet import Puppet
from ...user import User
from ..common import AuthAPI
class PublicBridgeWebsite(AuthAPI):
log: logging.Logger = logging.getLogger("mau.web.public")
secret_key: str
login: Template
mx_login: Template
app: web.Application
def __init__(self, loop: asyncio.AbstractEventLoop):
super().__init__(loop)
self.secret_key = "".join(random.choices(string.ascii_lowercase + string.digits, k=64))
self.login = Template(
pkg_resources.resource_string("mautrix_telegram", "web/public/login.html.mako")
)
self.mx_login = Template(
pkg_resources.resource_string("mautrix_telegram", "web/public/matrix-login.html.mako")
)
self.app = web.Application(loop=loop)
self.app.router.add_route("GET", "/login", self.get_login)
self.app.router.add_route("POST", "/login", self.post_login)
self.app.router.add_route("GET", "/matrix-login", self.get_matrix_login)
self.app.router.add_route("POST", "/matrix-login", self.post_matrix_login)
self.app.router.add_static(
"/", pkg_resources.resource_filename("mautrix_telegram", "web/public/")
)
def make_token(self, mxid: str, endpoint: str = "/login", expires_in: int = 900) -> str:
return sign_token(
self.secret_key,
{
"mxid": mxid,
"endpoint": endpoint,
"expiry": int(time.time()) + expires_in,
},
)
def verify_token(self, token: str, endpoint: str = "/login") -> UserID | None:
token = verify_token(self.secret_key, token)
if token and (
token.get("expiry", 0) > int(time.time()) and token.get("endpoint", None) == endpoint
):
return UserID(token.get("mxid", None))
return None
async def get_login(self, request: web.Request) -> web.Response:
state = "bot_token" if request.rel_url.query.get("mode", "") == "bot" else "request"
mxid = self.verify_token(request.rel_url.query.get("token", None), endpoint="/login")
if not mxid:
return self.get_login_response(status=401, state="invalid-token")
user = await User.get_by_mxid(mxid, create=False) if mxid else None
if not user:
return self.get_login_response(mxid=mxid, state=state)
elif not user.puppet_whitelisted:
return self.get_login_response(
mxid=user.mxid, error="You are not whitelisted.", status=403
)
await user.ensure_started()
if not await user.is_logged_in():
return self.get_login_response(mxid=user.mxid, state=state)
return self.get_login_response(mxid=user.mxid, human_tg_id=user.human_tg_id)
async def get_matrix_login(self, request: web.Request) -> web.Response:
mxid = self.verify_token(
request.rel_url.query.get("token", None), endpoint="/matrix-login"
)
if not mxid:
return self.get_mx_login_response(status=401, state="invalid-token")
user = await User.get_by_mxid(mxid, create=False) if mxid else None
if not user:
return self.get_mx_login_response(mxid=mxid)
elif not user.puppet_whitelisted:
return self.get_mx_login_response(
mxid=user.mxid, error="You are not whitelisted.", status=403
)
await user.ensure_started()
if not await user.is_logged_in():
return self.get_mx_login_response(
mxid=user.mxid, status=403, error="You are not logged in to Telegram."
)
puppet = await Puppet.get_by_tgid(user.tgid)
if puppet.is_real_user:
return self.get_mx_login_response(state="already-logged-in", status=409)
return self.get_mx_login_response(mxid=user.mxid)
def get_login_response(
self,
status: int = 200,
state: str = "",
username: str = "",
phone: str = "",
human_tg_id: str = "",
mxid: str = "",
message: str = "",
error: str = "",
errcode: str = "",
) -> web.Response:
return web.Response(
status=status,
content_type="text/html",
text=self.login.render(
human_tg_id=human_tg_id, state=state, error=error, message=message, mxid=mxid
),
)
def get_mx_login_response(
self,
status: int = 200,
state: str = "",
username: str = "",
phone: str = "",
human_tg_id: str = "",
mxid: str = "",
message: str = "",
error: str = "",
errcode: str = "",
) -> web.Response:
return web.Response(
status=status,
content_type="text/html",
text=self.mx_login.render(
human_tg_id=human_tg_id, state=state, error=error, message=message, mxid=mxid
),
)
async def post_matrix_login(self, request: web.Request) -> web.Response:
mxid = self.verify_token(
request.rel_url.query.get("token", None), endpoint="/matrix-login"
)
if not mxid:
return self.get_mx_login_response(status=401, state="invalid-token")
data = await request.post()
user = await User.get_and_start_by_mxid(mxid)
if not user.puppet_whitelisted:
return self.get_mx_login_response(
mxid=user.mxid, error="You are not whitelisted.", status=403
)
elif not await user.is_logged_in():
return self.get_mx_login_response(
mxid=user.mxid, status=403, error="You are not logged in to Telegram."
)
mode = data.get("mode", "access_token")
if mode == "password":
return await self.post_matrix_password(user, data["value"])
elif mode == "access_token":
return await self.post_matrix_token(user, data["value"])
return self.get_mx_login_response(
mxid=user.mxid, status=400, error="You must provide an access token or password."
)
async def post_login(self, request: web.Request) -> web.Response:
mxid = self.verify_token(request.rel_url.query.get("token", None), endpoint="/login")
if not mxid:
return self.get_login_response(status=401, state="invalid-token")
data = await request.post()
user = await User.get_and_start_by_mxid(mxid, even_if_no_session=True)
if not user.puppet_whitelisted:
return self.get_login_response(
mxid=user.mxid, error="You are not whitelisted.", status=403
)
elif await user.is_logged_in():
return self.get_login_response(mxid=user.mxid, human_tg_id=user.human_tg_id)
await user.ensure_started(even_if_no_session=True)
if "phone" in data:
return await self.post_login_phone(user, data["phone"])
elif "bot_token" in data:
return await self.post_login_token(user, data["bot_token"])
elif "code" in data:
try:
code = int(data["code"].strip())
except ValueError:
return self.get_login_response(
mxid=user.mxid,
state="code",
status=400,
errcode="phone_code_invalid",
error="Phone code must be a number.",
)
resp = await self.post_login_code(user, code, password_in_data="password" in data)
if resp or "password" not in data:
return resp
elif "password" not in data:
return self.get_login_response(error="No data given.", status=400)
if "password" in data:
return await self.post_login_password(user, data["password"])
return self.get_login_response(error="This should never happen.", status=500)
Binary file not shown.

Before

Width:  |  Height:  |  Size: 42 KiB

-99
View File
@@ -1,99 +0,0 @@
/*
* mautrix-telegram - A Matrix-Telegram puppeting bridge
* Copyright (C) 2019 Tulir Asokan
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
form > div {
display: none;
}
form[data-status="request"] > div.status-request,
form[data-status="code"] > div.status-code,
form[data-status="password"] > div.status-password {
display: initial;
}
.container {
margin-top: 3rem;
max-width: 60rem;
}
.error, .message {
border-radius: .25rem;
padding: .5rem 1rem;
border: 1px solid transparent;
margin: .5rem 0;
}
.error {
border-color: #f5c6cb;
background-color: #f8d7da;
color: #721c24;
}
.message {
border-color: #c3e6cb;
background-color: #d4edda;
color: #155724;
}
[type="checkbox"], [type="radio"] {
position: absolute;
opacity: 0;
}
[type="checkbox"] + label, [type="radio"] + label {
position: relative;
padding-left: 2.5rem;
cursor: pointer;
display: inline-block;
}
[type="checkbox"] + label:before, [type="radio"] + label:before {
content: '';
position: absolute;
left: 0;
top: 0.4rem;
width: 1.8rem;
height: 1.8rem;
border: 0.1rem solid #d1d1d1;
}
[type="radio"] + label:before, [type="radio"] + label:after {
border-radius: 50%;
}
[type="checkbox"]:checked + label:after,
[type="radio"]:checked + label:after {
content: '';
width: 0.8rem;
height: 0.8rem;
background: #9b4dca;
position: absolute;
top: 0.9rem;
left: 0.5rem;
}
[type="radio"]:disabled + label:before, [type="checkbox"]:disabled + label:before {
background-color: #d1d1d1;
}
[type="radio"]:disabled + label, [type="checkbox"]:disabled + label {
color: #d1d1d1;
}
[type="radio"]:disabled:checked + label:after, [type="checkbox"]:disabled:checked + label:after {
background: #606c76;
}
-129
View File
@@ -1,129 +0,0 @@
<!--
mautrix-telegram - A Matrix-Telegram puppeting bridge
Copyright (C) 2019 Tulir Asokan
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
-->
<!DOCTYPE html>
<html lang="en">
<head>
<title>Login - Mautrix-Telegram bridge</title>
<link rel="icon" type="image/png" href="favicon.png"/>
<meta property="og:title" content="Login - Mautrix-Telegram bridge">
<meta property="og:description" content="A hybrid puppeting/relaybot Matrix-Telegram bridge">
<meta property="og:image" content="favicon.png">
<meta charset="utf-8">
<link rel="stylesheet" href="//fonts.googleapis.com/css?family=Roboto:300,700">
<link rel="stylesheet"
href="https://cdnjs.cloudflare.com/ajax/libs/normalize/8.0.1/normalize.min.css">
<link rel="stylesheet"
href="https://cdnjs.cloudflare.com/ajax/libs/milligram/1.3.0/milligram.min.css">
<link rel="stylesheet" href="login.css"/>
<script>
function switchToBotLogin() {
const params = new URLSearchParams(location.search.slice(1))
params.set("mode", "bot")
location.search = "?" + params.toString()
console.log(location.search)
}
function goBack() {
let params = new URLSearchParams(location.search.slice(1))
const token = params.get("token")
params = new URLSearchParams()
if (token) {
params.set("token", token)
}
location.replace(location.href.split("?")[0] + "?" + params.toString())
}
</script>
</head>
<body>
<main class="container">
% if human_tg_id:
% if state == "logged-in":
<h1>Logged in successfully!</h1>
<p>
Logged in as ${human_tg_id}.
You can now close this page.
You should be invited to Telegram portals on Matrix momentarily.
</p>
% elif state == "bot-logged-in":
<h1>Logged in successfully!</h1>
<p>
Logged in as ${human_tg_id}.
You can now close this page.
You should be invited to Telegram portals on Matrix momentarily.
</p>
% else:
<h1>You're already logged in!</h1>
<p>
You're logged in as ${human_tg_id}.
</p>
<p>
If you want to log in with another account, log out using the <code>logout</code>
management command first.
</p>
% endif
% elif state == "invalid-token":
<h1>Invalid or expired token</h1>
<div class="error">Please ask the bridge bot for a new login link.</div>
% else:
<h1>Log in to Telegram</h1>
% if error:
<div class="error">${error}</div>
% endif
% if message:
<div class="message">${message}</div>
% endif
<form method="post">
<fieldset>
<label for="mxid">Matrix ID</label>
<input type="text" id="mxid" name="mxid" disabled value="${mxid}"/>
% if state == "request":
<label for="value">Phone number</label>
<input type="tel" id="value" name="phone" placeholder="Enter phone number"/>
<button type="submit">Start</button>
<button class="button-clear float-right" type="button" onclick="switchToBotLogin()">
Use bot token
</button>
% elif state == "bot_token":
<label for="value">Bot token</label>
<input type="text" id="value" name="bot_token"
placeholder="Enter bot API token"/>
<button type="submit">Sign in</button>
% elif state == "code":
<label for="value">Phone code</label>
<input type="number" id="value" name="code" placeholder="Enter phone code"/>
<button type="submit">Sign in</button>
% elif state == "password":
<label for="value">Password</label>
<input type="password" id="value" name="password"
placeholder="Enter password"/>
<button type="submit">Sign in</button>
% endif
% if state != "request":
<div class="float-right">
<button class="button-clear" type="button" onclick="goBack()">
Go back
</button>
</div>
% endif
</fieldset>
</form>
% endif
</main>
</body>
</html>
@@ -1,79 +0,0 @@
<!--
mautrix-telegram - A Matrix-Telegram puppeting bridge
Copyright (C) 2019 Tulir Asokan
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
-->
<!DOCTYPE html>
<html lang="en">
<head>
<title>Matrix login - Mautrix-Telegram bridge</title>
<link rel="icon" type="image/png" href="favicon.png"/>
<meta property="og:title" content="Matrix login - Mautrix-Telegram bridge">
<meta property="og:description" content="A hybrid puppeting/relaybot Matrix-Telegram bridge">
<meta property="og:image" content="favicon.png">
<meta charset="utf-8">
<link rel="stylesheet" href="//fonts.googleapis.com/css?family=Roboto:300,700">
<link rel="stylesheet"
href="https://cdnjs.cloudflare.com/ajax/libs/normalize/8.0.1/normalize.min.css">
<link rel="stylesheet"
href="https://cdnjs.cloudflare.com/ajax/libs/milligram/1.3.0/milligram.min.css">
<link rel="stylesheet" href="login.css"/>
</head>
<body>
<main class="container">
% if state == "logged-in":
<h1>Logged in successfully!</h1>
<p>
Logged in as ${mxid}.
You can now close this page.
</p>
% elif state == "already-logged-in":
<h1>You're already logged in!</h1>
<p>
If you want to log in with another account, log out using the
<code>logout-matrix</code> management command first.
</p>
% elif state == "invalid-token":
<h1>Invalid or expired token</h1>
<div class="error">Please ask the bridge bot for a new login link.</div>
% else:
<h1>Log in to Matrix</h1>
% if error:
<div class="error">${error}</div>
% endif
% if message:
<div class="message">${message}</div>
% endif
<form method="post">
<fieldset>
<label for="mxid">Matrix ID</label>
<input type="text" id="mxid" name="mxid" disabled value="${mxid}"/>
<input id="access_token" type="radio" name="mode" value="access_token" checked>
<label for="access_token">Access token</label><br>
<input id="password" type="radio" name="mode" value="password" disabled>
<label for="password">Password</label><br>
<label for="value">Value</label>
<input type="text" id="value" name="value"
placeholder="Enter Matrix access token or password"/>
<button type="submit">Sign in</button>
</fieldset>
</form>
% endif
</main>
</body>
</html>
-29
View File
@@ -1,29 +0,0 @@
# Format: #/name defines a new extras_require group called name
# Uncommented lines after the group definition insert things into that group.
#/speedups
cryptg>=0.1,<0.3
cchardet
aiodns
brotli
#/qr_login
pillow>=4,<9
qrcode>=6,<8
#/hq_thumbnails
moviepy>=1,<2
#/formattednumbers
phonenumbers>=8,<9
#/metrics
prometheus_client>=0.6,<0.13
#/e2be
python-olm>=3,<4
pycryptodome>=3,<4
unpaddedbase64>=1,<3
#/sqlite
aiosqlite>=0.16,<0.18
+373
View File
@@ -0,0 +1,373 @@
// mautrix-telegram - A Matrix-Telegram puppeting bridge.
// Copyright (C) 2025 Sumner Evans
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
package connector
import (
"context"
"fmt"
"slices"
"sync"
"time"
"github.com/rs/zerolog"
"maunium.net/go/mautrix/bridgev2"
"maunium.net/go/mautrix/bridgev2/database"
"maunium.net/go/mautrix/bridgev2/networkid"
"go.mau.fi/mautrix-telegram/pkg/connector/ids"
"go.mau.fi/mautrix-telegram/pkg/gotd/bin"
"go.mau.fi/mautrix-telegram/pkg/gotd/tg"
"go.mau.fi/mautrix-telegram/pkg/gotd/tgerr"
)
var (
_ bridgev2.BackfillingNetworkAPI = (*TelegramClient)(nil)
_ bridgev2.BackfillingNetworkAPIWithLimits = (*TelegramClient)(nil)
)
// getTakeoutID blocks until the takeout ID is available.
func (tc *TelegramClient) getTakeoutID(ctx context.Context) (takeoutID int64, err error) {
// Always stop the takeout timeout timer
if tc.stopTakeoutTimer != nil {
tc.stopTakeoutTimer.Stop()
}
log := zerolog.Ctx(ctx).With().Str("function", "getTakeoutID").Logger()
if tc.metadata.TakeoutID != 0 {
// Resume fetching dialogs using takeout and enqueueing them for
// backfill.
go tc.takeoutDialogsOnce.Do(func() {
if err = tc.syncChats(ctx, takeoutID, false, false); err != nil {
log.Err(err).Msg("Failed to takeout dialogs")
}
})
return tc.metadata.TakeoutID, nil
}
tc.stopTakeoutTimer = time.AfterFunc(max(time.Hour, time.Duration(tc.main.Bridge.Config.Backfill.Queue.BatchDelay*2)), sync.OnceFunc(func() { tc.stopTakeout(ctx) }))
for {
tc.takeoutAccepted.Clear()
accountTakeout, err := tc.client.API().AccountInitTakeoutSession(ctx, &tg.AccountInitTakeoutSessionRequest{
MessageUsers: true,
MessageChats: true,
MessageMegagroups: true,
MessageChannels: true,
Files: true,
FileMaxSize: min(tc.main.maxFileSize, 2000*1024*1024),
})
if rpcErr, ok := tgerr.As(err); ok && rpcErr.IsOneOf(tg.ErrTakeoutInitDelay) {
log.Warn().
Err(err).
Int("delay", rpcErr.Argument).
Msg("Takeout requested, will wait for retry request or delay")
tc.takeoutAccepted.WaitTimeout(time.Duration(rpcErr.Argument) * time.Second)
continue
} else if err != nil {
return 0, err
}
// Fetch all dialogs using takeout and enqueue them for backfill.
go tc.takeoutDialogsOnce.Do(func() {
if err = tc.syncChats(ctx, takeoutID, false, false); err != nil {
log.Err(err).Msg("Failed to takeout dialogs")
}
})
tc.metadata.TakeoutID = accountTakeout.ID
return accountTakeout.ID, tc.userLogin.Save(ctx)
}
}
func (tc *TelegramClient) stopTakeout(ctx context.Context) error {
tc.takeoutLock.Lock()
defer tc.takeoutLock.Unlock()
_, err := tc.client.API().AccountFinishTakeoutSession(ctx, &tg.AccountFinishTakeoutSessionRequest{Success: true})
if err != nil {
return err
}
tc.metadata.TakeoutID = 0
return tc.userLogin.Save(ctx)
}
func (tc *TelegramClient) FetchMessages(ctx context.Context, fetchParams bridgev2.FetchMessagesParams) (*bridgev2.FetchMessagesResponse, error) {
if tc.metadata.IsBot {
return nil, fmt.Errorf("bots cannot backfill messages")
}
log := zerolog.Ctx(ctx).With().Str("method", "FetchMessages").Logger()
ctx = log.WithContext(ctx)
var takeoutID int64
var err error
if (tc.main.Config.Takeout.ForwardBackfill && fetchParams.Forward) || (tc.main.Config.Takeout.BackwardBackfill && !fetchParams.Forward) {
tc.takeoutLock.Lock()
defer tc.takeoutLock.Unlock()
takeoutID, err = tc.getTakeoutID(ctx)
if err != nil {
return nil, err
}
if takeoutID != 0 {
defer func() {
if tc.stopTakeoutTimer == nil {
tc.stopTakeoutTimer = time.AfterFunc(max(time.Hour, time.Duration(tc.main.Bridge.Config.Backfill.Queue.BatchDelay*2)), sync.OnceFunc(func() { tc.stopTakeout(ctx) }))
} else {
tc.stopTakeoutTimer.Reset(max(time.Hour, time.Duration(tc.main.Bridge.Config.Backfill.Queue.BatchDelay*2)))
}
}()
}
}
peer, topicID, err := tc.inputPeerForPortalID(ctx, fetchParams.Portal.ID)
if err != nil {
return nil, err
}
var minID, offsetID int
if fetchParams.AnchorMessage != nil {
if fetchParams.Forward {
_, minID, err = ids.ParseMessageID(fetchParams.AnchorMessage.ID)
} else {
_, offsetID, err = ids.ParseMessageID(fetchParams.AnchorMessage.ID)
}
if err != nil {
return nil, err
}
}
if fetchParams.Portal.Metadata.(*PortalMetadata).IsForumGeneral {
topicID = 1
}
if topicID == ids.TopicIDSpaceRoom {
return nil, nil
}
limit := fetchParams.Count
const chunkLimit = 100
makeReq := func() bin.Object {
if topicID > 0 {
return &tg.MessagesGetRepliesRequest{
Peer: peer,
MsgID: topicID,
Limit: min(limit, chunkLimit),
MinID: minID,
OffsetID: offsetID,
}
}
return &tg.MessagesGetHistoryRequest{
Peer: peer,
Limit: min(limit, chunkLimit),
MinID: minID,
OffsetID: offsetID,
}
}
var messages []tg.MessageClass
requestCount := 0
for limit > 0 {
requestCount++
req := makeReq()
if takeoutID != 0 {
req = &tg.InvokeWithTakeoutRequest{TakeoutID: takeoutID, Query: req}
}
log.Info().Any("req", req).Msg("Fetching messages")
resp, err := APICallWithUpdates(ctx, tc, func() (tg.ModifiedMessagesMessages, error) {
var box tg.MessagesMessagesBox
retry := true
attempts := 0
var err error
for retry && attempts < 5 {
retry, err = tgerr.FloodWait(ctx, tc.client.Invoke(ctx, req, &box))
attempts++
}
if err != nil {
return nil, err
}
msgs, ok := box.Messages.(tg.ModifiedMessagesMessages)
if !ok {
return nil, fmt.Errorf("unsupported messages type %T", box.Messages)
}
return msgs, nil
})
if err != nil {
if tgerr.Is(err, tg.ErrTakeoutInvalid) {
tc.metadata.TakeoutID = 0
err := tc.userLogin.Save(ctx)
if err != nil {
log.Err(err).Msg("Failed to save user login after clearing takeout ID")
} else {
log.Debug().Msg("Cleared invalid takeout ID")
}
}
return nil, err
}
newMessages := resp.GetMessages()
if messages == nil {
messages = newMessages
} else {
messages = append(messages, resp.GetMessages()...)
}
if len(newMessages) < chunkLimit || !fetchParams.Forward {
break
}
limit -= len(newMessages)
offsetID = newMessages[len(newMessages)-1].GetID()
if takeoutID == 0 {
waitTime := time.Duration(min(requestCount*2, 15)) * time.Second
log.Debug().
Dur("wait_time", waitTime).
Msg("Not using takeout, waiting before requesting another batch of messages")
select {
case <-time.After(waitTime):
case <-ctx.Done():
return nil, ctx.Err()
}
}
}
portal := fetchParams.Portal
// If the first message is the last read message, mark the chat as read
// during backfill.
markRead := fetchParams.Forward &&
len(messages) > 0 &&
portal.Metadata.(*PortalMetadata).ReadUpTo == messages[0].GetID()
var cursor networkid.PaginationCursor
if len(messages) > 0 {
cursor = ids.MakePaginationCursorID(messages[len(messages)-1].GetID())
}
var stopAt int
if fetchParams.AnchorMessage != nil {
_, stopAt, err = ids.ParseMessageID(fetchParams.AnchorMessage.ID)
if err != nil {
return nil, err
}
log = log.With().Int("stop_at", stopAt).Logger()
}
var backfillMessages []*bridgev2.BackfillMessage
for _, msg := range messages {
log := log.With().Int("message_id", msg.GetID()).Logger()
if stopAt > 0 {
if fetchParams.Forward && msg.GetID() <= stopAt {
// If we are doing forward backfill and we get to the anchor
// message, don't convert any more messages.
log.Debug().Msg("stopping at anchor message")
break
} else if !fetchParams.Forward && msg.GetID() >= stopAt {
// If we are doing backwards backfill and we get a message more
// recent than the anchor message, skip it.
log.Debug().Msg("skipping message past anchor message")
continue
}
}
message, ok := msg.(*tg.Message)
if !ok {
log.Debug().Str("type", msg.TypeName()).Msg("skipping backfilling unsupported message type")
continue
}
sender := tc.getEventSender(message, !portal.Metadata.(*PortalMetadata).IsSuperGroup)
intent, ok := portal.GetIntentFor(ctx, sender, tc.userLogin, bridgev2.RemoteEventBackfill)
if !ok {
continue
}
converted, err := tc.convertToMatrix(ctx, portal, intent, message)
if err != nil {
return nil, err
}
backfillMessage := bridgev2.BackfillMessage{
ConvertedMessage: converted,
Sender: sender,
ID: ids.GetMessageIDFromMessage(message),
Timestamp: time.Unix(int64(message.Date), 0),
StreamOrder: int64(message.GetID()),
}
if reactions, ok := message.GetReactions(); ok {
reactionsList, _, customEmojis, err := tc.computeReactionsList(ctx, message.PeerID, message.ID, reactions)
if err != nil {
return nil, err
}
for _, reaction := range reactionsList {
peer, ok := reaction.PeerID.(*tg.PeerUser)
if !ok {
return nil, fmt.Errorf("unknown peer type %T", reaction.PeerID)
}
emojiID, emoji, err := computeEmojiAndID(reaction.Reaction, customEmojis)
if err != nil {
return nil, fmt.Errorf("failed to compute emoji and ID: %w", err)
}
backfillMessage.Reactions = append(backfillMessage.Reactions, &bridgev2.BackfillReaction{
Timestamp: time.Unix(int64(reaction.Date), 0),
Sender: tc.senderForUserID(peer.UserID),
EmojiID: emojiID,
Emoji: emoji,
})
}
}
backfillMessages = append(backfillMessages, &backfillMessage)
}
// They are returned with most recent message first, so reverse the order.
slices.Reverse(backfillMessages)
return &bridgev2.FetchMessagesResponse{
Messages: backfillMessages,
Cursor: cursor,
HasMore: len(backfillMessages) > 0,
Forward: fetchParams.Forward,
MarkRead: markRead,
}, nil
}
func (tc *TelegramClient) GetBackfillMaxBatchCount(ctx context.Context, portal *bridgev2.Portal, task *database.BackfillTask) int {
log := zerolog.Ctx(ctx).With().
Str("method", "GetBackfillMaxBatchCount").
Logger()
peerType, _, topicID, err := ids.ParsePortalID(portal.ID)
if err != nil {
log.Err(err).Msg("failed to parse portal ID")
return 0
}
switch peerType {
case ids.PeerTypeUser:
return tc.main.Bridge.Config.Backfill.Queue.GetOverride("user")
case ids.PeerTypeChat:
return tc.main.Bridge.Config.Backfill.Queue.GetOverride("normal_group")
case ids.PeerTypeChannel:
if topicID == ids.TopicIDSpaceRoom {
return 0
} else if topicID > 0 {
return tc.main.Bridge.Config.Backfill.Queue.GetOverride("topic", "supergroup")
} else if portal.Metadata.(*PortalMetadata).IsSuperGroup {
return tc.main.Bridge.Config.Backfill.Queue.GetOverride("supergroup")
} else {
return tc.main.Bridge.Config.Backfill.Queue.GetOverride("channel")
}
default:
log.Error().Str("peer_type", string(peerType)).Msg("unknown peer type")
return 0
}
}

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