Compare commits
1346 Commits
v0.9.0-rc2
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
| e3bb26aee1 | |||
| 7c2c72bbde | |||
| 2ffbde7448 | |||
| 2a0da7801a | |||
| eaf387abfe | |||
| 64d80c3d1d | |||
| c78b1abd2d | |||
| 12f900a7bd | |||
| cdb77f938a | |||
| 5a1a478992 | |||
| d2a06ebbbe | |||
| e6243d8935 | |||
| 9e1c42a992 | |||
| 6eacf38d74 | |||
| 65fcf712d3 | |||
| 8512cfe6a6 | |||
| 7a6d1bf17a | |||
| 18f831553d | |||
| dce0c4dbe1 | |||
| ac2a2c2980 | |||
| b9f0962881 | |||
| d7864fcd3a | |||
| 0f0b21b22c | |||
| 1dd11f1086 | |||
| 3f155672a7 | |||
| b761f04621 | |||
| 95db7a6d0d | |||
| 8b3707b0ee | |||
| 4d46c5ee7c | |||
| 009ce8c0d3 | |||
| a06b7d607d | |||
| 659b0f82ae | |||
| 53dec19878 | |||
| a5b1927acb | |||
| 0988de1267 | |||
| 5c1975808a | |||
| 2b79d842a4 | |||
| 6e08539d57 | |||
| 634cec5ba9 | |||
| 506e13f6b8 | |||
| e55e596d68 | |||
| 11495e6e7e | |||
| 53574754be | |||
| 3f725b37b9 | |||
| 2ad36b6366 | |||
| fe119542f8 | |||
| 98f24f9b5e | |||
| 60e3cf9c01 | |||
| cc32d48fea | |||
| 117c5cd0ce | |||
| 41f2166feb | |||
| abbcacec4b | |||
| 92fdf7b8e9 | |||
| f13af2ef54 | |||
| 0172a5733b | |||
| 8350230693 | |||
| c3216f1e4d | |||
| 0bee8da0f8 | |||
| 795b27275f | |||
| ba7d51e785 | |||
| cbff082e4d | |||
| 9b92aa3d50 | |||
| e96f12cfea | |||
| e7099d26f3 | |||
| 8b68fdce79 | |||
| 1dc01bcffd | |||
| dbab7f0ee4 | |||
| 835afb0100 | |||
| 693ced7dea | |||
| b6c7b0e78b | |||
| 3b939f423a | |||
| f4555782cf | |||
| 1ff046db0b | |||
| a44cc41933 | |||
| 770b3b8d8c | |||
| 7630340ffc | |||
| dc2b16e1a6 | |||
| f7b220f711 | |||
| e37619e826 | |||
| 0dc5045b00 | |||
| c8c5c7d272 | |||
| c358222ab4 | |||
| fdebeb9ca8 | |||
| bd6d40cad0 | |||
| 358318c734 | |||
| a0323a5233 | |||
| 3500590f11 | |||
| 011894f7b4 | |||
| af9ce963a9 | |||
| 50a1c21fd1 | |||
| 606bf92ab1 | |||
| 9b7ee5e2c3 | |||
| e4195fadb4 | |||
| 7cf65b6f6a | |||
| a5a3b9f380 | |||
| 58d99a806a | |||
| acf716c031 | |||
| 8de8170619 | |||
| b2f99ec5c0 | |||
| ec960a7372 | |||
| 6d085f477e | |||
| e68ef24657 | |||
| f7cbf751a0 | |||
| d124008443 | |||
| 190e65edfb | |||
| 4f4680b19a | |||
| c46a3189e0 | |||
| b084627248 | |||
| ce70aacdb8 | |||
| b6aff6784f | |||
| 5d05d7ab05 | |||
| 5c37b186d8 | |||
| 0881e76205 | |||
| c5cdde83e4 | |||
| 8bd4ff8f82 | |||
| 4a538f77ef | |||
| f64b605443 | |||
| dbe9be2102 | |||
| 0727857ed0 | |||
| c97c5f6bec | |||
| 01357fe5df | |||
| d8188743ba | |||
| 6d373885d2 | |||
| 1cd589dbd1 | |||
| a96bf7ed95 | |||
| bb405b4773 | |||
| b43adb6bab | |||
| abae7b2854 | |||
| 472b9df44c | |||
| bec7ee8f5e | |||
| ae5f2f3093 | |||
| 3910e44639 | |||
| 50aefd6897 | |||
| b17bb0d5c7 | |||
| 64724aa654 | |||
| 800c15f7b7 | |||
| 7f71e5f09c | |||
| bfe5999951 | |||
| b1b5745033 | |||
| 98936fdf7a | |||
| 7baed1c77b | |||
| b695e0b4ea | |||
| 326906644e | |||
| 0122ab91d6 | |||
| 0d818303f4 | |||
| 7fa51da335 | |||
| 49d99aff82 | |||
| cfd9b74d34 | |||
| d067348ac5 | |||
| 84392278c2 | |||
| 0aed201869 | |||
| 62efa2e7b9 | |||
| 58f40aeba5 | |||
| 29000146ba | |||
| a9cb55d109 | |||
| f7ae7ba804 | |||
| 0e45edd1f4 | |||
| 7fb4539885 | |||
| 42465f1aca | |||
| 5bf7461566 | |||
| a84dd2f30c | |||
| 67adededff | |||
| e5914196c5 | |||
| 189dbdfc52 | |||
| 7738fc21f5 | |||
| 4511c82cb0 | |||
| 6af986ded5 | |||
| 93fe3cb0ea | |||
| 52b2373528 | |||
| a59c755dd8 | |||
| 4793b01a29 | |||
| e8114ff5ad | |||
| e597eace68 | |||
| 4071502854 | |||
| c7a7f6ec20 | |||
| 8f3cc2e28e | |||
| 6700403118 | |||
| f4830c71d8 | |||
| 78ba8e4d45 | |||
| 2b1cfae52f | |||
| 274141a1b0 | |||
| cac1f5acde | |||
| 56fe704934 | |||
| 3696c9cff4 | |||
| d83c0ede15 | |||
| b5d6e2ac6b | |||
| 37d34a4ab6 | |||
| baba8bd712 | |||
| 7573e3d5a7 | |||
| a887f26023 | |||
| ced0a2d067 | |||
| 09227510bc | |||
| dc2a422bbe | |||
| 49bb93bdc2 | |||
| a3ec7f7c33 | |||
| cd660472d8 | |||
| 20446d0d7d | |||
| 769a397a03 | |||
| 1dde2a4a77 | |||
| 85e1a6dc05 | |||
| e9d95a6e9a | |||
| e1497999d6 | |||
| dd63436149 | |||
| dd34256f3d | |||
| 7f17c2728e | |||
| 4342635b8a | |||
| 80ccbeb449 | |||
| 283dfc5c77 | |||
| d00de62ee7 | |||
| 1a2fd67ee9 | |||
| 4873ed77ff | |||
| d6a8e6a648 | |||
| 095bd65d51 | |||
| cd9970055f | |||
| 3663f91c8a | |||
| 0c3749a2ca | |||
| ba4dd48d5a | |||
| c1d92ce051 | |||
| ae05331420 | |||
| ef65f9f1ea | |||
| 29d8c1b7dd | |||
| 4775e67476 | |||
| 43b230148b | |||
| d03260c4a7 | |||
| 042304f147 | |||
| de2e87ed52 | |||
| 581ba79c84 | |||
| 390f9f422e | |||
| f80d6de818 | |||
| 69fcbd30ce | |||
| 0e3b1b63a9 | |||
| 7f13284b59 | |||
| 4268ee9909 | |||
| 35bf11c158 | |||
| 08703e9efb | |||
| e13502750e | |||
| 0da121aebb | |||
| d07d2af048 | |||
| 76e06d4a33 | |||
| 10f1583da9 | |||
| 48fed1c026 | |||
| abb4671a16 | |||
| 6729a9ad09 | |||
| 96b2afeed1 | |||
| d5f87d2ec1 | |||
| 14b3b1fed7 | |||
| 16a57d78ac | |||
| 548672d243 | |||
| ef23946cbc | |||
| 9b2b691afd | |||
| c04866c854 | |||
| fa28593635 | |||
| 9dd8f30480 | |||
| 19c3121e77 | |||
| 6232a27881 | |||
| 526903cb7c | |||
| 2cac8f8b4a | |||
| c83a361c0b | |||
| c6dd85040c | |||
| 08a2fe9753 | |||
| 2580e28bee | |||
| b7e5078053 | |||
| 8bef95e237 | |||
| 04a10f361a | |||
| fed5752f38 | |||
| 35c161185c | |||
| 1ecb9e8b64 | |||
| 2004085312 | |||
| abd5d058ff | |||
| 09185e8e53 | |||
| 097211cba1 | |||
| 8e7a7db85f | |||
| 55f8d1423b | |||
| 66b84a7b44 | |||
| b38c3cc935 | |||
| b7459ec9eb | |||
| 5eb883e934 | |||
| b9d19a3aad | |||
| 74a5dfccd5 | |||
| b3f9bfb5b3 | |||
| ca46d36998 | |||
| 795a732720 | |||
| 3aa7bdfa91 | |||
| 14f40abeca | |||
| b1f3c4c1db | |||
| 4410415776 | |||
| a38c3e5d00 | |||
| a280c3a4b9 | |||
| 280c74e9cd | |||
| eb5bfb4666 | |||
| fcace69cbd | |||
| f48737c894 | |||
| d359bafb53 | |||
| 44be515705 | |||
| 34683e6d1b | |||
| 8f998cd9cb | |||
| 9706deb27d | |||
| 233516ca4d | |||
| 0051042555 | |||
| 93f55497f4 | |||
| 63e44fb5ad | |||
| 17b69a6eac | |||
| 170b263a6c | |||
| 7a726e36a0 | |||
| 4f12f5103a | |||
| 8341492c9f | |||
| 9e4b6c3c46 | |||
| 597d0e996b | |||
| 4f1482e7b0 | |||
| 4641215e97 | |||
| 2f34ebfed9 | |||
| 3ae88caa80 | |||
| 3462b75c76 | |||
| 6f949b5a1c | |||
| bdf7194691 | |||
| f5bfe421d1 | |||
| 0bc1bd04c5 | |||
| a31787f894 | |||
| e1c0e6dd9a | |||
| ecb3921260 | |||
| e7fe66a23e | |||
| 03fe8bf782 | |||
| 840788c1e5 | |||
| d4f6be8155 | |||
| d10c528895 | |||
| 635345f61d | |||
| e9abeda916 | |||
| b65a1cc60a | |||
| 53bf278f1e | |||
| b16061db34 | |||
| 4b56b6d016 | |||
| 3fb554b934 | |||
| 890851e85e | |||
| a7aa96ef2b | |||
| aa02639759 | |||
| 35f137ccc1 | |||
| a501042f1d | |||
| 7970a678fc | |||
| cb98833590 | |||
| e6c3454e9f | |||
| 4c9555eded | |||
| 38f87becb6 | |||
| 9e2e2421d2 | |||
| 10bc44d17d | |||
| 125be97201 | |||
| d4239d520a | |||
| 399cd5585a | |||
| ac3ce3c097 | |||
| 6280a7bae7 | |||
| af630ecbd1 | |||
| 7a04f298d2 | |||
| 0952df0244 | |||
| 8ac519d1e5 | |||
| a49818b863 | |||
| 31846e7a98 | |||
| 9ab2ee2970 | |||
| c7dd08ecd1 | |||
| fa237a20f7 | |||
| 41279ae996 | |||
| e7b87835b6 | |||
| c96a241794 | |||
| 3c3c3f1dec | |||
| a9a267bc0d | |||
| 05b1eb1214 | |||
| 0f36833e89 | |||
| ea4626107c | |||
| 39c1b685d6 | |||
| 14c784f2a2 | |||
| 11f105c0e7 | |||
| 9e719429e7 | |||
| d2bb02b259 | |||
| 8fbd723bfa | |||
| 7e75c8ef83 | |||
| 9d3e9df57e | |||
| 849f3c6f1e | |||
| 483816cc2b | |||
| 7c13481ede | |||
| eb5ae65402 | |||
| 53e89441b7 | |||
| 48d91fdf76 | |||
| 530bd9e52e | |||
| bdae6dd620 | |||
| 75964a00ed | |||
| 224b01e7a4 | |||
| 5421de8e76 | |||
| a64a178dc3 | |||
| 538f2a2ec0 | |||
| 10b8c4b635 | |||
| b955252a6a | |||
| 6347383788 | |||
| 28d8276554 | |||
| 6480e7925e | |||
| c70ab2a12b | |||
| 09b1e69c0f | |||
| 070bfd4f55 | |||
| 88c3a93526 | |||
| 854f66cb04 | |||
| 1bc3a2538e | |||
| dcc8689835 | |||
| ebc1aa05b1 | |||
| a56f2977b4 | |||
| 36bb741c68 | |||
| f0f92c9dd9 | |||
| e8ee5f174e | |||
| f86ebad162 | |||
| 0712ca5d0c | |||
| dad34f9a3c | |||
| c2e07d5e3f | |||
| 255c1e2e57 | |||
| a680036177 | |||
| 94789daed3 | |||
| e0841e252d | |||
| ab9ff87815 | |||
| 4b08ab6ac0 | |||
| 823eda7589 | |||
| caefda582b | |||
| e1b181ed55 | |||
| cc6a915ef4 | |||
| de4df57278 | |||
| b158ba6b8b | |||
| 571152cb41 | |||
| c82b273155 | |||
| 4bef6ea09e | |||
| 386cfa4cfb | |||
| f4052dcfd3 | |||
| 664d6050df | |||
| 9e868e4614 | |||
| 2743d5375a | |||
| c3fc77c2a8 | |||
| 6c7727d6b5 | |||
| 3ef2cbe102 | |||
| 487f11ffd7 | |||
| 655cd98f27 | |||
| f14c90dc87 | |||
| ee0c2e4f68 | |||
| c8590ca402 | |||
| 964ea69de7 | |||
| 1de97c9ae0 | |||
| 987395914e | |||
| 2a7146d987 | |||
| 71ebb72ede | |||
| dc2216e60b | |||
| 73934a0594 | |||
| 4d33af7f81 | |||
| 80f17d5fbd | |||
| 6c68351e1f | |||
| 83acac5175 | |||
| 46a4b68073 | |||
| 68f4b0e21f | |||
| 32282a242f | |||
| 2129dd803d | |||
| 74d9edf42e | |||
| a1f58cad11 | |||
| bf3e0ec8ab | |||
| 124f0967ed | |||
| 16040adc53 | |||
| 8e994edbde | |||
| 54157de58f | |||
| 7ce3dacf00 | |||
| 6d82ac18b4 | |||
| d6765157ab | |||
| 7bda4f7855 | |||
| e603aa6058 | |||
| 4b5ae24a67 | |||
| 6b6a6ba275 | |||
| 22f44734cf | |||
| 844f31827c | |||
| 21ef73d69c | |||
| 9d80c9e396 | |||
| b316cb131a | |||
| dd64d2c559 | |||
| 1f22aa2072 | |||
| d887887d8b | |||
| 5b7a170ad9 | |||
| 463277def0 | |||
| 40f259da5e | |||
| d1d3c18670 | |||
| 6100335809 | |||
| 869fef0828 | |||
| ada41742a1 | |||
| 1b4416f291 | |||
| 11a832c575 | |||
| 303274acb6 | |||
| b6d3131caf | |||
| 22c3938b52 | |||
| 827116658b | |||
| ca8aff0534 | |||
| 5adb2a6572 | |||
| 69e3a183c7 | |||
| 52c39eefe0 | |||
| 6fa19bbbda | |||
| 8025404958 | |||
| 07a8553b22 | |||
| 15fdd89e3d | |||
| bda33687af | |||
| ea9bd01d06 | |||
| e846fb168c | |||
| 0046975aa5 | |||
| aa7a2d186b | |||
| f195e2cac0 | |||
| b33209fafa | |||
| 265c2835e8 | |||
| 86a77996d4 | |||
| da894bec25 | |||
| cc8dce3959 | |||
| 22488fbc5f | |||
| 3498ed8dc1 | |||
| bcea875e66 | |||
| 7cb70d9753 | |||
| e266d1ac80 | |||
| 0f933f691b | |||
| c6afaf5504 | |||
| 9f6a54be81 | |||
| 229efdd487 | |||
| 4bdd415dbe | |||
| 31dc0259f3 | |||
| 13f21a7c70 | |||
| a573740b9a | |||
| 5448648c32 | |||
| 706d4a5e5c | |||
| df3cd765fe | |||
| bd7c724341 | |||
| f076376caa | |||
| 19a3c8a4d9 | |||
| a9f8a3aa0f | |||
| f91b429c47 | |||
| b0e6dcb1d6 | |||
| 132585de34 | |||
| 4d1cec979b | |||
| 679b4bd157 | |||
| 73d0b189bb | |||
| 48059a3a51 | |||
| 4205047aab | |||
| 03c7028460 | |||
| c75ac58763 | |||
| a85659df9d | |||
| 9a8f356348 | |||
| 9576f48c5b | |||
| 96331761b8 | |||
| 4821865cad | |||
| 7efad4a990 | |||
| 6c44ba487a | |||
| 57b32f6ac6 | |||
| 17e4e20a93 | |||
| 8480c8aa68 | |||
| d14f365fe1 | |||
| 7d9836c86b | |||
| 1c7e626c97 | |||
| 4bd57f7cab | |||
| 9da87fc789 | |||
| 2139bf25eb | |||
| 083837aa9e | |||
| abba9bcf81 | |||
| 171b621999 | |||
| 52fab81e55 | |||
| 6f4e32fad0 | |||
| 9609f437d5 | |||
| e1a56778f5 | |||
| 23bb0febe9 | |||
| 31397681f5 | |||
| 332bbb8de1 | |||
| 7ccd8ab4ab | |||
| 7af4ecc719 | |||
| ce1c28832e | |||
| 81c913bdd3 | |||
| 5c23e9695f | |||
| c6e96682b6 | |||
| 65da56b2a6 | |||
| a73f9d1ec2 | |||
| 7a02d6a35b | |||
| ff48398430 | |||
| 7ed3c46f23 | |||
| 3acd95741f | |||
| 3f69f29d49 | |||
| fab98cfdea | |||
| 4692d46305 | |||
| 87f9f008e6 | |||
| 50ab23423f | |||
| cd0d940889 | |||
| 777225c252 | |||
| 89b1caadbf | |||
| 4d4060f37b | |||
| 75eea8e2cb | |||
| 3b6af95976 | |||
| 8925318ec4 | |||
| ec330c72be | |||
| 088900aee1 | |||
| 86a2b3fa15 | |||
| afd9850c4b | |||
| 60fe2e07c2 | |||
| c2d94947ee | |||
| 4d9ad4f0af | |||
| bbf53fb28b | |||
| ced27a9974 | |||
| 3378467378 | |||
| 1c2b902de4 | |||
| f7be907633 | |||
| 1e39877af3 | |||
| 6b092026c3 | |||
| 68e835c658 | |||
| e3e709eec6 | |||
| d7508579e5 | |||
| d8d4a60855 | |||
| 196eaac917 | |||
| 91ce540a66 | |||
| dcf43ca9d9 | |||
| 15b0dc51b3 | |||
| 9d8f162f41 | |||
| eec5cbe447 | |||
| b25c09fc53 | |||
| e611c87342 | |||
| a6946f8119 | |||
| 5960a2307e | |||
| 6aaf786ea9 | |||
| 8b8b689187 | |||
| 24d0d4687a | |||
| 0670c2b2bc | |||
| 284178df65 | |||
| 56f83315ed | |||
| 7e2d9bbc4e | |||
| d11af1a463 | |||
| dc4c3ee382 | |||
| 0ef8581764 | |||
| 6c4c0f4821 | |||
| b11479e4e2 | |||
| 3a11ac217e | |||
| d94dbe81dc | |||
| 6462b709f5 | |||
| a86c2c2544 | |||
| 838f291220 | |||
| aeb8fba288 | |||
| 497bfb152e | |||
| 83695b4336 | |||
| e0194f7621 | |||
| 7fd280ea10 | |||
| ca4d566490 | |||
| 7e53698696 | |||
| 54f971f578 | |||
| 18337c6941 | |||
| f56f520308 | |||
| b539e5d63d | |||
| e8b5d286dc | |||
| 882582456e | |||
| e7522be252 | |||
| 5ea342e788 | |||
| 314b2da99f | |||
| 5a3b52dff2 | |||
| 98a0ed0a5b | |||
| 29c3c4009a | |||
| fe550da243 | |||
| a0d88da480 | |||
| ec56fb6b28 | |||
| 9d77bebe3e | |||
| 48858ac28f | |||
| 0e6ea310d1 | |||
| 69c9e3c38c | |||
| 0068341185 | |||
| a8142cd8a0 | |||
| 6e0f604209 | |||
| 34832c7ff7 | |||
| 62f77686c4 | |||
| 5eaec4d0e0 | |||
| 35c5518d1d | |||
| 548356189b | |||
| aa45619244 | |||
| efcf1535ff | |||
| 92b8541654 | |||
| 62d6145c14 | |||
| 58cc638058 | |||
| 7e680f1fee | |||
| a63f264804 | |||
| 99f633e98d | |||
| 0137bfcbf6 | |||
| 33dc5bad03 | |||
| 5d39fc8c5f | |||
| 0921168b91 | |||
| cbba340da6 | |||
| a2b810e34e | |||
| feab4607b5 | |||
| 15cb6ef44f | |||
| f524f365f1 | |||
| 3d8b9d6291 | |||
| 55a9375938 | |||
| 6a6e129c0a | |||
| 6bd2ef5b34 | |||
| 7437240f2f | |||
| 8ad516c5a4 | |||
| eef68706d9 | |||
| 752107ffb0 | |||
| 5193cd899f | |||
| 1563ee014d | |||
| a24079494d | |||
| 44cb928707 | |||
| 4d82cb7883 | |||
| 867cbd582e | |||
| 17badab358 | |||
| ee583af4f9 | |||
| 499678d092 | |||
| 9d9c82c9e9 | |||
| c0c7ad7d0f | |||
| 63645e50b2 | |||
| 891750592d | |||
| b568ef8d8c | |||
| 16706d8338 | |||
| 2df6f73098 | |||
| 7963e52405 | |||
| d0626e670c | |||
| f3f6ea8b2f | |||
| 871a9705e3 | |||
| 5de193d087 | |||
| 60f668deb4 | |||
| 61c06396fc | |||
| 323fe1603e | |||
| a4aedec044 | |||
| 6c88b21b75 | |||
| 6511adc480 | |||
| f6cb26f7f5 | |||
| 6418202118 | |||
| 4b25e855e0 | |||
| a35f6abfd1 | |||
| 716222a671 | |||
| 31801a436c | |||
| f2219a1e06 | |||
| 72fc81b239 | |||
| 43212ad8db | |||
| 0d502a8c55 | |||
| 043cb7f854 | |||
| 8bd5a4e367 | |||
| 43d17a335b | |||
| 84a3fde1ca | |||
| 05d05e671b | |||
| ab6a6654f7 | |||
| dbfbf12862 | |||
| 6166173376 | |||
| 2232d9898e | |||
| 3cf279718f | |||
| 65ec4491e2 | |||
| ce43607c56 | |||
| 150bf5e338 | |||
| 77cbbebfb2 | |||
| 511043a720 | |||
| 19a4b4374d | |||
| 731d5e028a | |||
| 5ea9e48954 | |||
| 73b26e3fbd | |||
| 48be895938 | |||
| 87909d07ec | |||
| 3609eb2b70 | |||
| 562f646fea | |||
| ab3cf5bc5f | |||
| 1b2f07dfa2 | |||
| 2a67c96db3 | |||
| 3fdb789745 | |||
| e4c239e6bc | |||
| 897a35be5d | |||
| d72897dfe8 | |||
| 27723f5055 | |||
| a84e5ebc6a | |||
| 90a8583ad0 | |||
| bf2cef424b | |||
| 6809ebcde9 | |||
| 6fafc533ab | |||
| 060dd647c3 | |||
| 812b4ec8db | |||
| 8c1ddec136 | |||
| 08db5a687c | |||
| ec298b2b90 | |||
| 22f91d51a3 | |||
| d033042ee1 | |||
| 2270f4fe40 | |||
| 6d208b37a5 | |||
| 55ebaef6e3 | |||
| 215f077cf0 | |||
| 4e4f409f87 | |||
| 4d145f4716 | |||
| b833a41a88 | |||
| 768d51c4ae | |||
| f7db298fda | |||
| 4f2118c7ee | |||
| 4f0770b92d | |||
| 1fb8a7a0a5 | |||
| f79ab283f3 | |||
| 23ec691128 | |||
| 59213ebeae | |||
| 36b2f6af2e | |||
| b2249f7756 | |||
| 212023d296 | |||
| 4b03134620 | |||
| 806eea53eb | |||
| 4ca3ee58ac | |||
| 8b003f1187 | |||
| c06a2b2473 | |||
| f2194c6f33 | |||
| b5c294a558 | |||
| c6b6ec048e | |||
| fb461109c1 | |||
| 0411affc88 | |||
| dfe22800dd | |||
| 7868b05ed3 | |||
| 0474f81044 | |||
| ed471a6623 | |||
| 4504973aff | |||
| a5a71edede | |||
| e1c800f3e6 | |||
| 810f86343a | |||
| 5f7d3ac8c1 | |||
| cb5c51cd27 | |||
| 759ccf301c | |||
| 40e4c7e251 | |||
| e12f1784e2 | |||
| 6b8e265f8b | |||
| de33b553be | |||
| ed24a0b89f | |||
| e2697e5a17 | |||
| c4037ccf11 | |||
| 6c6fe134ba | |||
| e3c45f6f27 | |||
| 732258c093 | |||
| 8726fa5d74 | |||
| da61ba96f1 | |||
| 815ce40989 | |||
| 4ff6a62dab | |||
| 918582c967 | |||
| 40c584b121 | |||
| f189dc8c88 | |||
| b291c246f4 | |||
| 59ab7be283 | |||
| 60981386ec | |||
| 436781215f | |||
| 9c4b24475c | |||
| ff8d1fc9ec | |||
| 5f04729ce8 | |||
| 60526f981a | |||
| e39d4972fb | |||
| 233468b37b | |||
| 6eda8bd165 | |||
| 7372e7cbea | |||
| 1fed2201db | |||
| 60b1573386 | |||
| f4695d8395 | |||
| f63c679d3e | |||
| 4e5305c91b | |||
| f30c03a727 | |||
| 354b49d9e5 | |||
| 7b60ee1337 | |||
| ab1d9b246e | |||
| f7b694c9e4 | |||
| be6f6bbfac | |||
| a32f797b0b | |||
| f12abbe038 | |||
| ad2b49928a | |||
| 67f75796fa | |||
| c235ced030 | |||
| d53764fd84 | |||
| 529d8ae3ba | |||
| f864f66e62 | |||
| b1b633bcf9 | |||
| e655e0a882 | |||
| db88fbb694 | |||
| ace3e42281 | |||
| a40000e6b7 | |||
| 21d2d7dfea | |||
| a61731a289 | |||
| c250076032 | |||
| c6d35b103a | |||
| 596c9a5055 | |||
| 9fae4f14d2 | |||
| f1f0b86696 | |||
| e3d2a1fcef | |||
| 2303622475 | |||
| 732277be5e | |||
| 28f205057f | |||
| 9e32ec3e39 | |||
| 1fa86cbb52 | |||
| 9d8a4d4269 | |||
| cb22615bb5 | |||
| 989dc32481 | |||
| 02dd44ad63 | |||
| d6517959d8 | |||
| d9d539c4b8 | |||
| 5b18ffb7ec | |||
| cf70efb6a2 | |||
| a42699e1fb | |||
| 597e82a33b | |||
| e302143b8a | |||
| e99b6af2c5 | |||
| 35a16ac7e0 | |||
| 0d20d9069a | |||
| 8b1d272827 | |||
| 24b3384570 | |||
| 4ca5bfb1ab | |||
| 7c8cf3cb50 | |||
| 6b55d5bb41 | |||
| 5558fc7157 | |||
| 30a7121000 | |||
| fb1568d019 | |||
| a0dca671d8 | |||
| d79870801b | |||
| 2a238a95a9 | |||
| 4bfcf46e36 | |||
| 894316f035 | |||
| 1c47924624 | |||
| 2973b0f200 | |||
| 4fc5751ae1 | |||
| d37ca7eae3 | |||
| 7960f22be9 | |||
| 1b11ec290a | |||
| 751f1d93f3 | |||
| f63a7857a6 | |||
| 017ca24b13 | |||
| 3c22ab7bd1 | |||
| 0bbf64d240 | |||
| af2f20f7b2 | |||
| fef03ddec0 | |||
| f2d0489488 | |||
| f815d5e2fd | |||
| c4a5a3eaf7 | |||
| 921cc6ffa9 | |||
| b582e59eee | |||
| c9f8b83f62 | |||
| 8ff99ce916 | |||
| 27b23a96b6 | |||
| 8ae34223c5 | |||
| 699fc9df1f | |||
| 951d02bfc3 | |||
| 9b9a3b452d | |||
| 02f21a30a8 | |||
| e053664c99 | |||
| 949c6a318f | |||
| f5cb8baf99 | |||
| 025b864bd8 | |||
| b4fcccbe10 | |||
| b9331b5f5a | |||
| 81aa0084e7 | |||
| 58bc6788aa | |||
| 5a767a2d92 | |||
| 282ad43180 | |||
| bcb30ce807 | |||
| 2d865f006e | |||
| b2daebead6 | |||
| 4210091e9a | |||
| 4db09f2240 | |||
| e0260eb551 | |||
| ed1e5474bf | |||
| 65bd7fcc49 | |||
| 80834ccec1 | |||
| 026c39a3de | |||
| 95939dfa02 | |||
| 279da9097c | |||
| 97126332da | |||
| 6641b9a16c | |||
| 927c9afa84 | |||
| d41d7ca0a6 | |||
| ad0c6cfc8d | |||
| 0289f4b524 | |||
| 85b8f5def7 | |||
| f012cb790f | |||
| 05476d7435 | |||
| 583427da05 | |||
| e3a067c27a | |||
| b3ed4cf657 | |||
| 952c81eadc | |||
| cc29ce19ca | |||
| 941aa5f9d8 | |||
| 15e5cc8da1 | |||
| 2cf9205cda | |||
| 2ec89bc57e | |||
| 89294c57d8 | |||
| 624c72fa99 | |||
| 34af580846 | |||
| 910a681f4b | |||
| c4c225343c | |||
| f13a9d0e96 | |||
| c54ae9548f | |||
| 1216607763 | |||
| ecd4d5c338 | |||
| a5fe05cff2 | |||
| 76eafbf48c | |||
| 473ab17fe7 | |||
| bea9bc4ec0 | |||
| 5df1e84fae | |||
| 8665871502 | |||
| ef57f1021c | |||
| b6312f306a | |||
| 70b73868c7 | |||
| 0717b4a290 | |||
| a9b6539910 | |||
| 49520bb8a3 | |||
| 7abe19aec9 | |||
| 3dd0c51be7 | |||
| 565bb87470 | |||
| 9188251501 | |||
| cb11e147ce | |||
| eb1190359d | |||
| cdfc6fd007 | |||
| df9b7d343e | |||
| f26973f46c | |||
| 2335431060 | |||
| 8fd97af0a9 | |||
| 3ea491d379 | |||
| 3bd7d846f4 | |||
| 99344c38a4 | |||
| d917499d1f | |||
| 98da5fecc3 | |||
| 6b0ece5da1 | |||
| 448b149e8e | |||
| 120514125f | |||
| cd4b4365bd | |||
| 8f68801aa9 | |||
| 1d0e8c7e0c | |||
| 3ff43165c2 | |||
| 1fdbdb654a | |||
| 0e024b3b7c | |||
| e1a5e30a75 | |||
| 05d4923db9 | |||
| f18713cd5e | |||
| ef05875bfd | |||
| 59d85a1e16 | |||
| 7eec0d1ed3 | |||
| f917ee189d | |||
| ab2e38b33b | |||
| 38af35e776 | |||
| c6adb87aea | |||
| e8eef1c31e | |||
| bac3abcb4c | |||
| c682bdc01e | |||
| 50cd878f13 | |||
| ea49ba8be2 | |||
| b60056c560 | |||
| 820210dc44 | |||
| 7d998dca3f | |||
| 037d93471d | |||
| 5cb2b871cd | |||
| 44f2c648a8 | |||
| 0ae8a5877e | |||
| 18f6622340 | |||
| 591e79f5a0 | |||
| d898486b49 | |||
| 74e0aee421 | |||
| 07f32e1256 | |||
| ea680cf871 | |||
| e89c75c6cd | |||
| 59d052afd2 | |||
| 9383249ade | |||
| 0a4f30bf02 | |||
| 190f452910 | |||
| 3c59a1af97 | |||
| 11ff628ef8 | |||
| 908e600dc9 | |||
| eb43fde3e4 | |||
| e6ef40e51d | |||
| 7feea5aa6d | |||
| d084cca983 | |||
| d9018868a1 | |||
| 72360457ef | |||
| 0e4c1b71e6 | |||
| 575b761f77 | |||
| 68e950a6bc | |||
| ba5bbebb3e | |||
| cb38896593 | |||
| 21c6a7d87f | |||
| 7c2a569235 | |||
| 1f5b91cbec | |||
| 937f37eff0 | |||
| 4f9f74204a | |||
| ed6735f10f | |||
| 5acd3cf007 | |||
| 279b997bd3 | |||
| 4eb6095822 | |||
| da5b8556f2 | |||
| 261f99ac82 | |||
| 61f3c39cc2 | |||
| 39ab1d0c22 | |||
| 8abb9c3884 | |||
| 58f8ee2ee2 | |||
| 474bcc9544 | |||
| a3f4e25101 | |||
| 8befb664b6 | |||
| 819dd1bcff | |||
| 2b8b853fec | |||
| c536c4a265 | |||
| f13acfe825 | |||
| 8e763ba067 | |||
| 8d7cfd8e46 | |||
| 601058d61c | |||
| f8596ef368 | |||
| 7f0494d52d | |||
| 828478514b | |||
| 146f5437d1 | |||
| c28760f2a8 | |||
| 04f30f6f29 | |||
| caa1d3565b | |||
| 1a7a020bb2 | |||
| 077ab2bb38 | |||
| 6f491bf7d1 | |||
| 9b80c21d0a | |||
| e9dc76a860 | |||
| 9e73324a20 | |||
| 7df93485d8 | |||
| 9018cea5ae | |||
| 32e023231d | |||
| 4766d14359 | |||
| 526b99ec04 | |||
| da132438bd | |||
| 54176ba2db | |||
| 1eca3c2ffd | |||
| 98142f28cd | |||
| 2cf7fc7059 | |||
| a34a18c6cc | |||
| fa738fbadf | |||
| 9ea0516166 | |||
| b760aadb01 | |||
| 24162e14ac | |||
| 9ea495324d | |||
| 437e86a15b | |||
| d9e0b75e9b | |||
| 9606518ba7 | |||
| e2774b830f | |||
| 951d82ad27 | |||
| 4a55cf589c | |||
| b07d80d876 | |||
| ff995b2149 | |||
| 2fb08d59c7 | |||
| 7950c5aa61 | |||
| bf65824429 | |||
| 4013f822de | |||
| b27519fd88 | |||
| 22f97756f7 | |||
| da3f4af171 | |||
| a55d9ae36a | |||
| ecf3a12bd4 | |||
| e7248e2418 | |||
| fba118f0d9 | |||
| 100394d161 | |||
| a9908781be | |||
| 0f050edcd9 | |||
| 2182dfc86b | |||
| 99fa7a57d2 | |||
| 6bf3d10e29 | |||
| ebd2a38e56 | |||
| 03b094e4d4 | |||
| 21b509e5a0 | |||
| 2732a85f9e | |||
| 033141e435 | |||
| 251458a1d7 | |||
| 7c4f406ac6 | |||
| 984c52afc9 | |||
| f664d4ad90 | |||
| 8f61be76f9 | |||
| 8003b9aa1c | |||
| a0fd98b9e2 | |||
| feac31e841 | |||
| dd83d6278c | |||
| 2a6b075ff2 | |||
| e321bc30d0 | |||
| 63fafec1b7 | |||
| 9f48eca5a6 | |||
| 28845b9daf | |||
| 113f41d1d2 | |||
| da3180e290 | |||
| 1a62463678 | |||
| e584cf534d | |||
| 4c1267cd32 | |||
| dc8a3d0c2d | |||
| c481ec850d | |||
| a54dd58de7 | |||
| b13da92520 | |||
| 2b6db85e1a | |||
| e7a1216ef7 | |||
| b1da5c7c2c | |||
| 3b72de34b3 | |||
| af893554cc | |||
| d108ac5d94 | |||
| e446121192 | |||
| afb73b1d17 | |||
| aae8f78cb4 | |||
| 2a1e5c9d1e | |||
| 89a7c4a0f3 | |||
| bdc9de8070 | |||
| de4db8c8a6 | |||
| cb97c127d6 | |||
| 173b5ec2e7 | |||
| ce0c18003b | |||
| 80082639b5 | |||
| 7e885f1be2 | |||
| 9fd506e098 | |||
| 7e6978bc10 | |||
| 4e571e6b10 | |||
| 0127bb04ae | |||
| 0711cfb5f7 | |||
| 8bc361a154 | |||
| 50c6f2b009 | |||
| 190064bfff | |||
| 9189d917d0 | |||
| f55d6606df | |||
| 2615e11e34 | |||
| 7595b9c015 | |||
| dfe9bd94b1 | |||
| 737c4a1104 | |||
| 82024a3250 | |||
| 3447762d91 | |||
| 055034ed67 | |||
| f768254b83 | |||
| 6d25e9687e | |||
| f2af17d359 | |||
| 89ab29ea5f | |||
| f12f3fe007 | |||
| 9c14c86358 | |||
| 8603c67347 | |||
| 74ec8f4fa6 | |||
| 3ddd4449b1 | |||
| 782cd426a4 | |||
| 2744e7a5a0 | |||
| ae28d125f2 | |||
| 05cf150982 | |||
| 6245c4066f | |||
| f7ecc3fdfc | |||
| 292a218a16 | |||
| c095498247 | |||
| 8276692ebf | |||
| 7e369dabdc | |||
| 631ed49ec7 | |||
| 25761215c3 | |||
| b4d4f84161 | |||
| 8e8360a992 | |||
| a1f389cb73 | |||
| 2cc439853f | |||
| 76b2937c18 | |||
| f2a9f4ab33 | |||
| ec375e79d7 | |||
| 338a4d9761 | |||
| 83d457f2b3 | |||
| 3507095572 | |||
| 4e7cf481fd | |||
| 0915bb9402 | |||
| 7c5d1c2959 | |||
| 8aecf1f84b | |||
| 2c45d8dd5b | |||
| fac337eaf1 | |||
| e7d8948334 | |||
| 6b8831872c | |||
| 4e8c373d1b | |||
| 8865dab6b0 | |||
| e4a2bd2f69 | |||
| a132916525 | |||
| a9dcb34b2d | |||
| 74c43355e4 | |||
| 7255e86595 | |||
| e4098a226e | |||
| 5dea5977ad | |||
| 1c9a30773e | |||
| e276944b40 | |||
| 2e14991815 | |||
| 3083727aff | |||
| d778c639dc | |||
| 10de186598 | |||
| 64107fab17 | |||
| 52bfbddcca | |||
| 5d9cc490d7 | |||
| 13cac8db9a | |||
| 3ab5e4d8cc | |||
| 7e728dd5af | |||
| 597d2e3282 | |||
| 57611a3f30 | |||
| ec64c83cb0 | |||
| ecdaaea3b9 | |||
| bda41417aa | |||
| 5a76b5bcdc | |||
| 4edd8eaa7b | |||
| 742a925040 | |||
| bcede7710f | |||
| c02f67e0d1 | |||
| 31650aac96 | |||
| 730f6bab6f | |||
| f923552f86 | |||
| eca1032d16 | |||
| 570372fa83 | |||
| 5ed09ad783 | |||
| c385aa0b8d | |||
| ec152cbd9d | |||
| b36fc35e04 | |||
| 198e77cae9 | |||
| 9c4beb29a5 | |||
| 6accb530c6 | |||
| 1a77ba5fcd | |||
| 7e9dd8b895 | |||
| 78fcacf7aa | |||
| 077f5d588b | |||
| 8b73c67836 | |||
| 92fa05cb06 | |||
| 18f5a33279 | |||
| f9a6e9c4fb | |||
| abfefab545 | |||
| 79f8c520bd | |||
| fa35ed1cb6 | |||
| 2e8d612078 | |||
| 4801b0f323 | |||
| 783c94dadd | |||
| c8cf662ad0 | |||
| cd70e6b836 | |||
| 72cfbf71f8 | |||
| cb36800c75 | |||
| 559c504e8b | |||
| de3a37f40c | |||
| 6020cdf8bf | |||
| 429cb07b79 | |||
| 2cf93c5765 | |||
| db41c8d806 | |||
| 5313369d85 | |||
| c8c17dac01 | |||
| bbb864773f | |||
| 4767fec86e | |||
| 6d57f070f9 | |||
| 97d47d80ee | |||
| 35f59b5f95 | |||
| 697fb06909 | |||
| efd536357c | |||
| 2c917a559c | |||
| b97c1a1b59 | |||
| 9237046b96 | |||
| 646bbceb99 | |||
| e9e164c679 | |||
| 033c6c698a | |||
| 3d403c2471 | |||
| b22e3d2573 | |||
| 7d20c5b732 | |||
| 2ce2337674 | |||
| 3fe26ae4dd | |||
| 6f4faf7a58 | |||
| e1dcfb76f4 | |||
| f658f2c5b7 | |||
| dd7eed834c | |||
| e4f8b22bc6 | |||
| 0b8fa5ea06 | |||
| 140fcae403 | |||
| 95920728f4 | |||
| e85be95d2d | |||
| 3006b3ab3b | |||
| d4d6cfa87d | |||
| b8cfcbe5ee | |||
| 9875833c90 | |||
| 38d94484bb | |||
| 0b3014ff88 | |||
| 04c64949e7 | |||
| be59d50678 | |||
| 2e27e85ac5 |
@@ -1,8 +0,0 @@
|
|||||||
engines:
|
|
||||||
sonar-python:
|
|
||||||
enabled: true
|
|
||||||
checks:
|
|
||||||
python:S107:
|
|
||||||
enabled: false
|
|
||||||
exclude_patterns:
|
|
||||||
- "alembic/"
|
|
||||||
@@ -1,9 +1,7 @@
|
|||||||
.editorconfig
|
.editorconfig
|
||||||
.codeclimate.yml
|
|
||||||
*.png
|
*.png
|
||||||
*.md
|
*.md
|
||||||
logs
|
logs
|
||||||
.venv
|
|
||||||
start
|
start
|
||||||
config.yaml
|
config.yaml
|
||||||
registration.yaml
|
registration.yaml
|
||||||
|
|||||||
+7
-5
@@ -8,11 +8,13 @@ charset = utf-8
|
|||||||
trim_trailing_whitespace = true
|
trim_trailing_whitespace = true
|
||||||
insert_final_newline = true
|
insert_final_newline = true
|
||||||
|
|
||||||
[*.py]
|
[*.md]
|
||||||
max_line_length = 99
|
trim_trailing_whitespace = false
|
||||||
|
indent_size = 2
|
||||||
[*.{yaml,yml,py}]
|
|
||||||
indent_style = space
|
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
|
indent_size = 2
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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.
|
||||||
@@ -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.
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
---
|
||||||
|
name: Enhancement request
|
||||||
|
about: Submit a feature request or other suggestion
|
||||||
|
type: Feature
|
||||||
|
|
||||||
|
---
|
||||||
@@ -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
|
||||||
@@ -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
@@ -1,18 +1,16 @@
|
|||||||
/.idea/
|
.idea
|
||||||
|
|
||||||
/.venv
|
*.yaml
|
||||||
/env/
|
!.pre-commit-config.yaml
|
||||||
pip-selfcheck.json
|
!example-config.yaml
|
||||||
*.pyc
|
!provisioning-spec.yaml
|
||||||
__pycache__
|
|
||||||
/build
|
|
||||||
/dist
|
|
||||||
/*.egg-info
|
|
||||||
/.eggs
|
|
||||||
|
|
||||||
/config.yaml
|
*.json
|
||||||
/registration.yaml
|
!pkg/connector/emojis/unicodemojipack.json
|
||||||
*.log*
|
*.db*
|
||||||
*.db
|
*.log
|
||||||
*.pickle
|
|
||||||
*.bak
|
*.bak
|
||||||
|
|
||||||
|
/mautrix-telegram
|
||||||
|
/mautrix-telegramgo
|
||||||
|
/start
|
||||||
|
|||||||
+3
-41
@@ -1,41 +1,3 @@
|
|||||||
image: docker:stable
|
include:
|
||||||
|
- project: 'mautrix/ci'
|
||||||
stages:
|
file: '/gov2-as-default.yml'
|
||||||
- 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
|
|
||||||
|
|
||||||
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
|
|
||||||
|
|||||||
Generated
+1
@@ -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 |
-16
@@ -1,16 +0,0 @@
|
|||||||
[settings]
|
|
||||||
line_length=99
|
|
||||||
indent=4
|
|
||||||
|
|
||||||
multi_line_output=5
|
|
||||||
|
|
||||||
sections=FUTURE,STDLIB,THIRDPARTY,TELETHON,MAUTRIX,FIRSTPARTY,LOCALFOLDER
|
|
||||||
no_lines_before=LOCALFOLDER
|
|
||||||
default_section=FIRSTPARTY
|
|
||||||
|
|
||||||
known_thirdparty=aiohttp,sqlalchemy,alembic,commonmark,ruamel.yaml,PIL,moviepy,prometheus_client,yarl,mako,pkg_resources
|
|
||||||
known_telethon=telethon,alchemysession,cryptg
|
|
||||||
known_mautrix=mautrix
|
|
||||||
|
|
||||||
balanced_wrapping=True
|
|
||||||
length_sort=True
|
|
||||||
@@ -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
|
||||||
+1078
File diff suppressed because it is too large
Load Diff
+12
-65
@@ -1,73 +1,20 @@
|
|||||||
FROM dock.mau.dev/tulir/lottieconverter:alpine-3.12
|
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 echo $'\
|
COPY . /build
|
||||||
@edge http://dl-cdn.alpinelinux.org/alpine/edge/main\n\
|
WORKDIR /build
|
||||||
@edge http://dl-cdn.alpinelinux.org/alpine/edge/testing\n\
|
RUN ./build.sh
|
||||||
@edge http://dl-cdn.alpinelinux.org/alpine/edge/community' >> /etc/apk/repositories
|
|
||||||
|
|
||||||
RUN apk add --no-cache \
|
FROM alpine:3.23
|
||||||
python3 py3-pip py3-setuptools py3-wheel \
|
|
||||||
py3-virtualenv \
|
|
||||||
py3-pillow \
|
|
||||||
py3-aiohttp \
|
|
||||||
py3-magic \
|
|
||||||
py3-sqlalchemy \
|
|
||||||
py3-telethon-session-sqlalchemy@edge \
|
|
||||||
py3-alembic@edge \
|
|
||||||
py3-psycopg2 \
|
|
||||||
py3-ruamel.yaml \
|
|
||||||
py3-commonmark@edge \
|
|
||||||
# Indirect dependencies
|
|
||||||
py3-idna \
|
|
||||||
#moviepy
|
|
||||||
py3-decorator \
|
|
||||||
py3-tqdm \
|
|
||||||
py3-requests \
|
|
||||||
#imageio
|
|
||||||
py3-numpy \
|
|
||||||
#py3-telethon@edge \ (outdated)
|
|
||||||
# Optional for socks proxies
|
|
||||||
py3-pysocks \
|
|
||||||
# cryptg
|
|
||||||
py3-cffi \
|
|
||||||
py3-qrcode@edge \
|
|
||||||
py3-brotli \
|
|
||||||
# Other dependencies
|
|
||||||
ffmpeg \
|
|
||||||
ca-certificates \
|
|
||||||
su-exec \
|
|
||||||
netcat-openbsd \
|
|
||||||
# encryption
|
|
||||||
olm-dev \
|
|
||||||
py3-pycryptodome \
|
|
||||||
py3-unpaddedbase64 \
|
|
||||||
py3-future \
|
|
||||||
bash \
|
|
||||||
curl \
|
|
||||||
jq && \
|
|
||||||
curl -sLo yq https://github.com/mikefarah/yq/releases/download/3.3.2/yq_linux_${TARGETARCH} && \
|
|
||||||
chmod +x yq && mv yq /usr/bin/yq
|
|
||||||
|
|
||||||
COPY requirements.txt /opt/mautrix-telegram/requirements.txt
|
ENV UID=1337 \
|
||||||
COPY optional-requirements.txt /opt/mautrix-telegram/optional-requirements.txt
|
GID=1337
|
||||||
WORKDIR /opt/mautrix-telegram
|
|
||||||
RUN apk add --virtual .build-deps \
|
|
||||||
python3-dev \
|
|
||||||
libffi-dev \
|
|
||||||
build-base \
|
|
||||||
&& sed -Ei 's/psycopg2-binary.+//' optional-requirements.txt \
|
|
||||||
&& pip3 install -r requirements.txt -r optional-requirements.txt \
|
|
||||||
&& apk del .build-deps
|
|
||||||
|
|
||||||
COPY . /opt/mautrix-telegram
|
RUN apk add --no-cache ffmpeg su-exec ca-certificates olm bash jq curl yq-go lottieconverter
|
||||||
RUN apk add git && pip3 install .[speedups,hq_thumbnails,metrics,e2be] && 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
|
|
||||||
|
|
||||||
|
COPY --from=builder /build/mautrix-telegram /usr/bin/mautrix-telegram
|
||||||
|
COPY --from=builder /build/docker-run.sh /docker-run.sh
|
||||||
VOLUME /data
|
VOLUME /data
|
||||||
ENV UID=1337 GID=1337 \
|
|
||||||
FFMPEG_BINARY=/usr/bin/ffmpeg
|
|
||||||
|
|
||||||
CMD ["/opt/mautrix-telegram/docker-run.sh"]
|
CMD ["/docker-run.sh"]
|
||||||
|
|||||||
@@ -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"]
|
||||||
@@ -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.
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
include README.md
|
|
||||||
include LICENSE
|
|
||||||
include requirements.txt
|
|
||||||
include optional-requirements.txt
|
|
||||||
@@ -1,33 +1,27 @@
|
|||||||
# mautrix-telegram
|
# mautrix-telegram
|
||||||

|

|
||||||
[](LICENSE)
|
[](LICENSE)
|
||||||
[](https://github.com/tulir/mautrix-telegram/releases)
|
[](https://github.com/mautrix/telegram/releases)
|
||||||
[](https://mau.dev/tulir/mautrix-telegram/container_registry)
|
[](https://mau.dev/mautrix/telegram/container_registry)
|
||||||
[](https://codeclimate.com/github/tulir/mautrix-telegram)
|
|
||||||
|
|
||||||
A Matrix-Telegram hybrid puppeting/relaybot bridge.
|
A Matrix-Telegram puppeting/relaybot bridge.
|
||||||
|
|
||||||
## Sponsors
|
## Sponsors
|
||||||
* [Joel Lehtonen / Zouppen](https://github.com/zouppen)
|
* [Joel Lehtonen / Zouppen](https://github.com/zouppen)
|
||||||
|
|
||||||
### Wiki
|
## Documentation
|
||||||
All setup and usage instructions are located in the GitHub
|
All setup and usage instructions are located on
|
||||||
[wiki](https://github.com/tulir/mautrix-telegram/wiki). Some quick links:
|
[docs.mau.fi](https://docs.mau.fi/bridges/go/telegram/index.html).
|
||||||
|
Some quick links:
|
||||||
|
|
||||||
* [Bridge setup](https://github.com/tulir/mautrix-telegram/wiki/Bridge-setup)
|
* [Bridge setup](https://docs.mau.fi/bridges/go/setup.html?bridge=telegram)
|
||||||
(or [with Docker](https://github.com/tulir/mautrix-telegram/wiki/Bridge-setup-with-Docker))
|
(or [with Docker](https://docs.mau.fi/bridges/general/docker-setup.html?bridge=telegram))
|
||||||
* Basic usage: [Authentication](https://github.com/tulir/mautrix-telegram/wiki/Authentication),
|
* Basic usage: [Authentication](https://docs.mau.fi/bridges/go/telegram/authentication.html)
|
||||||
[Creating chats](https://github.com/tulir/mautrix-telegram/wiki/Creating-and-managing-chats),
|
|
||||||
[Relaybot setup](https://github.com/tulir/mautrix-telegram/wiki/Relay-bot)
|
|
||||||
|
|
||||||
### Features & Roadmap
|
### Features & Roadmap
|
||||||
[ROADMAP.md](https://github.com/tulir/mautrix-telegram/blob/master/ROADMAP.md)
|
[ROADMAP.md](ROADMAP.md) contains a general overview of what is supported by the bridge.
|
||||||
contains a general overview of what is supported by the bridge.
|
|
||||||
|
|
||||||
## Discussion
|
## Discussion
|
||||||
Matrix room: [`#telegram:maunium.net`](https://matrix.to/#/#telegram:maunium.net)
|
Matrix room: [`#telegram:maunium.net`](https://matrix.to/#/#telegram:maunium.net)
|
||||||
|
|
||||||
Telegram chat: [`mautrix_telegram`](https://t.me/mautrix_telegram) (bridged to Matrix room)
|
Telegram chat: [`mautrix_telegram`](https://t.me/mautrix_telegram) (bridged to Matrix room)
|
||||||
|
|
||||||
## Preview
|
|
||||||

|
|
||||||
|
|||||||
+20
-25
@@ -3,42 +3,37 @@
|
|||||||
* Matrix → Telegram
|
* Matrix → Telegram
|
||||||
* [x] Message content (text, formatting, files, etc..)
|
* [x] Message content (text, formatting, files, etc..)
|
||||||
* [x] Message redactions
|
* [x] Message redactions
|
||||||
|
* [x] Message reactions
|
||||||
* [x] Message edits
|
* [x] Message edits
|
||||||
* [ ] ‡ Message history
|
* [ ] ‡ Message history
|
||||||
* [x] Presence
|
* [ ] Presence
|
||||||
* [x] Typing notifications
|
* [x] Typing notifications
|
||||||
* [x] Read receipts
|
* [x] Read receipts
|
||||||
* [x] Pinning messages
|
* [ ] Pinning messages
|
||||||
* [x] Power level
|
* [ ] Power level
|
||||||
* [x] Normal chats
|
* [ ] Membership actions (invite/kick/join/leave)
|
||||||
* [ ] Non-hardcoded PL requirements
|
* [ ] Room metadata changes (name, topic, avatar)
|
||||||
* [x] Supergroups/channels
|
* [ ] Initial room metadata
|
||||||
* [ ] 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
|
|
||||||
* Telegram → Matrix
|
* Telegram → Matrix
|
||||||
* [x] Message content (text, formatting, files, etc..)
|
* [x] Message content (text, formatting, files, etc..)
|
||||||
* [ ] Advanced message content/media
|
* [ ] Advanced message content/media
|
||||||
* [x] Polls
|
* [x] Custom emojis
|
||||||
* [x] Games
|
* [ ] Polls
|
||||||
* [ ] Buttons
|
* [ ] Games
|
||||||
|
* [ ] Buttons
|
||||||
* [x] Message deletions
|
* [x] Message deletions
|
||||||
|
* [x] Message reactions
|
||||||
* [x] Message edits
|
* [x] Message edits
|
||||||
* [x] Message history
|
* [x] Message history
|
||||||
* [x] Manually (`!tg backfill`)
|
|
||||||
* [x] Automatically when creating portal
|
* [x] Automatically when creating portal
|
||||||
* [x] Automatically for missed messages
|
* [x] Automatically for missed messages
|
||||||
* [x] Avatars
|
* [x] Avatars
|
||||||
* [x] Presence
|
* [ ] Presence
|
||||||
* [x] Typing notifications
|
* [x] Typing notifications
|
||||||
* [x] Read receipts (private chat only)
|
* [x] Read receipts (DMs only)
|
||||||
* [x] Pinning messages
|
* [ ] Pinning messages
|
||||||
* [x] Admin/chat creator status
|
* [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)
|
* [x] Membership actions (invite/kick/join/leave)
|
||||||
* [ ] Chat metadata changes
|
* [ ] Chat metadata changes
|
||||||
* [x] Title
|
* [x] Title
|
||||||
@@ -48,16 +43,16 @@
|
|||||||
* [x] Initial chat metadata (about text missing)
|
* [x] Initial chat metadata (about text missing)
|
||||||
* [x] User metadata (displayname/avatar)
|
* [x] User metadata (displayname/avatar)
|
||||||
* [x] Supergroup upgrade
|
* [x] Supergroup upgrade
|
||||||
|
* [x] Topics (spaces)
|
||||||
* Misc
|
* Misc
|
||||||
* [x] Automatic portal creation
|
* [x] Automatic portal creation
|
||||||
* [x] At startup
|
* [x] At startup
|
||||||
* [x] When receiving invite or message
|
* [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 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)
|
* [x] Option to use own Matrix account for messages sent from other Telegram clients (double puppeting)
|
||||||
* [ ] ‡ Calls (hard, not yet supported by Telethon)
|
* [ ] ‡ Calls
|
||||||
* [ ] ‡ Secret chats (i.e. End-to-bridge encryption on Telegram)
|
* [ ] ‡ 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))
|
|
||||||
|
|
||||||
† Information not automatically sent from source, i.e. implementation may not be possible
|
† 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
|
‡ Maybe, i.e. this feature may or may not be implemented at some point
|
||||||
|
|||||||
-71
@@ -1,71 +0,0 @@
|
|||||||
# A generic, single database configuration.
|
|
||||||
|
|
||||||
[alembic]
|
|
||||||
# path to migration scripts
|
|
||||||
script_location = alembic
|
|
||||||
|
|
||||||
# template used to generate migration files
|
|
||||||
# file_template = %%(rev)s_%%(slug)s
|
|
||||||
|
|
||||||
# timezone to use when rendering the date
|
|
||||||
# within the migration file as well as the filename.
|
|
||||||
# string value is passed to dateutil.tz.gettz()
|
|
||||||
# leave blank for localtime
|
|
||||||
# timezone =
|
|
||||||
|
|
||||||
# max length of characters to apply to the
|
|
||||||
# "slug" field
|
|
||||||
#truncate_slug_length = 40
|
|
||||||
|
|
||||||
# set to 'true' to run the environment during
|
|
||||||
# the 'revision' command, regardless of autogenerate
|
|
||||||
# revision_environment = false
|
|
||||||
|
|
||||||
# set to 'true' to allow .pyc and .pyo files without
|
|
||||||
# a source .py file to be detected as revisions in the
|
|
||||||
# versions/ directory
|
|
||||||
# sourceless = false
|
|
||||||
|
|
||||||
# version location specification; this defaults
|
|
||||||
# to alembic/versions. When using multiple version
|
|
||||||
# directories, initial revisions must be specified with --version-path
|
|
||||||
# version_locations = %(here)s/bar %(here)s/bat alembic/versions
|
|
||||||
|
|
||||||
# the output encoding used when revision files
|
|
||||||
# are written from script.py.mako
|
|
||||||
# output_encoding = utf-8
|
|
||||||
|
|
||||||
# Logging configuration
|
|
||||||
[loggers]
|
|
||||||
keys = root,sqlalchemy,alembic
|
|
||||||
|
|
||||||
[handlers]
|
|
||||||
keys = console
|
|
||||||
|
|
||||||
[formatters]
|
|
||||||
keys = generic
|
|
||||||
|
|
||||||
[logger_root]
|
|
||||||
level = WARN
|
|
||||||
handlers = console
|
|
||||||
qualname =
|
|
||||||
|
|
||||||
[logger_sqlalchemy]
|
|
||||||
level = WARN
|
|
||||||
handlers =
|
|
||||||
qualname = sqlalchemy.engine
|
|
||||||
|
|
||||||
[logger_alembic]
|
|
||||||
level = INFO
|
|
||||||
handlers =
|
|
||||||
qualname = alembic
|
|
||||||
|
|
||||||
[handler_console]
|
|
||||||
class = StreamHandler
|
|
||||||
args = (sys.stderr,)
|
|
||||||
level = NOTSET
|
|
||||||
formatter = generic
|
|
||||||
|
|
||||||
[formatter_generic]
|
|
||||||
format = %(levelname)-5.5s [%(name)s] %(message)s
|
|
||||||
datefmt = %H:%M:%S
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
Generic single-database configuration.
|
|
||||||
@@ -1,90 +0,0 @@
|
|||||||
from alembic import context
|
|
||||||
from sqlalchemy import engine_from_config, pool
|
|
||||||
from logging.config import fileConfig
|
|
||||||
|
|
||||||
import sys
|
|
||||||
from os.path import abspath, dirname
|
|
||||||
|
|
||||||
sys.path.insert(0, dirname(dirname(abspath(__file__))))
|
|
||||||
|
|
||||||
from mautrix.util.db import Base
|
|
||||||
import mautrix_telegram.db
|
|
||||||
from mautrix_telegram.config import Config
|
|
||||||
from alchemysession import AlchemySessionContainer
|
|
||||||
|
|
||||||
# this is the Alembic Config object, which provides
|
|
||||||
# access to the values within the .ini file in use.
|
|
||||||
config = context.config
|
|
||||||
|
|
||||||
mxtg_config_path = context.get_x_argument(as_dictionary=True).get("config", "config.yaml")
|
|
||||||
mxtg_config = Config(mxtg_config_path, None, None)
|
|
||||||
mxtg_config.load()
|
|
||||||
config.set_main_option("sqlalchemy.url", mxtg_config["appservice.database"].replace("%", "%%"))
|
|
||||||
|
|
||||||
AlchemySessionContainer.create_table_classes(None, "telethon_", Base)
|
|
||||||
|
|
||||||
# Interpret the config file for Python logging.
|
|
||||||
# This line sets up loggers basically.
|
|
||||||
fileConfig(config.config_file_name)
|
|
||||||
|
|
||||||
# add your model's MetaData object here
|
|
||||||
# for 'autogenerate' support
|
|
||||||
# from myapp import mymodel
|
|
||||||
# target_metadata = mymodel.Base.metadata
|
|
||||||
target_metadata = Base.metadata
|
|
||||||
|
|
||||||
|
|
||||||
# other values from the config, defined by the needs of env.py,
|
|
||||||
# can be acquired:
|
|
||||||
# my_important_option = config.get_main_option("my_important_option")
|
|
||||||
# ... etc.
|
|
||||||
|
|
||||||
|
|
||||||
def run_migrations_offline():
|
|
||||||
"""Run migrations in 'offline' mode.
|
|
||||||
|
|
||||||
This configures the context with just a URL
|
|
||||||
and not an Engine, though an Engine is acceptable
|
|
||||||
here as well. By skipping the Engine creation
|
|
||||||
we don't even need a DBAPI to be available.
|
|
||||||
|
|
||||||
Calls to context.execute() here emit the given string to the
|
|
||||||
script output.
|
|
||||||
|
|
||||||
"""
|
|
||||||
url = config.get_main_option("sqlalchemy.url")
|
|
||||||
context.configure(
|
|
||||||
url=url, target_metadata=target_metadata, literal_binds=True,
|
|
||||||
render_as_batch=True)
|
|
||||||
|
|
||||||
with context.begin_transaction():
|
|
||||||
context.run_migrations()
|
|
||||||
|
|
||||||
|
|
||||||
def run_migrations_online():
|
|
||||||
"""Run migrations in 'online' mode.
|
|
||||||
|
|
||||||
In this scenario we need to create an Engine
|
|
||||||
and associate a connection with the context.
|
|
||||||
|
|
||||||
"""
|
|
||||||
connectable = engine_from_config(
|
|
||||||
config.get_section(config.config_ini_section),
|
|
||||||
prefix='sqlalchemy.',
|
|
||||||
poolclass=pool.NullPool)
|
|
||||||
|
|
||||||
with connectable.connect() as connection:
|
|
||||||
context.configure(
|
|
||||||
connection=connection,
|
|
||||||
target_metadata=target_metadata,
|
|
||||||
render_as_batch=True
|
|
||||||
)
|
|
||||||
|
|
||||||
with context.begin_transaction():
|
|
||||||
context.run_migrations()
|
|
||||||
|
|
||||||
|
|
||||||
if context.is_offline_mode():
|
|
||||||
run_migrations_offline()
|
|
||||||
else:
|
|
||||||
run_migrations_online()
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
"""${message}
|
|
||||||
|
|
||||||
Revision ID: ${up_revision}
|
|
||||||
Revises: ${down_revision | comma,n}
|
|
||||||
Create Date: ${create_date}
|
|
||||||
|
|
||||||
"""
|
|
||||||
from alembic import op
|
|
||||||
import sqlalchemy as sa
|
|
||||||
${imports if imports else ""}
|
|
||||||
|
|
||||||
# revision identifiers, used by Alembic.
|
|
||||||
revision = ${repr(up_revision)}
|
|
||||||
down_revision = ${repr(down_revision)}
|
|
||||||
branch_labels = ${repr(branch_labels)}
|
|
||||||
depends_on = ${repr(depends_on)}
|
|
||||||
|
|
||||||
|
|
||||||
def upgrade():
|
|
||||||
${upgrades if upgrades else "pass"}
|
|
||||||
|
|
||||||
|
|
||||||
def downgrade():
|
|
||||||
${downgrades if downgrades else "pass"}
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
"""Add disable_updates field for puppets
|
|
||||||
|
|
||||||
Revision ID: 17574c57f3f8
|
|
||||||
Revises: a9119be92164
|
|
||||||
Create Date: 2019-05-15 00:24:46.967529
|
|
||||||
|
|
||||||
"""
|
|
||||||
from alembic import op
|
|
||||||
import sqlalchemy as sa
|
|
||||||
|
|
||||||
|
|
||||||
# revision identifiers, used by Alembic.
|
|
||||||
revision = '17574c57f3f8'
|
|
||||||
down_revision = 'a9119be92164'
|
|
||||||
branch_labels = None
|
|
||||||
depends_on = None
|
|
||||||
|
|
||||||
|
|
||||||
def upgrade():
|
|
||||||
with op.batch_alter_table("puppet") as batch_op:
|
|
||||||
batch_op.add_column(sa.Column("disable_updates", sa.Boolean(), nullable=False,
|
|
||||||
server_default=sa.sql.expression.false()))
|
|
||||||
|
|
||||||
|
|
||||||
def downgrade():
|
|
||||||
with op.batch_alter_table("puppet") as batch_op:
|
|
||||||
batch_op.drop_column("disable_updates")
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
"""Add TelegramFile table
|
|
||||||
|
|
||||||
Revision ID: 1b241f7e8530
|
|
||||||
Revises: 97d2a942bcf8
|
|
||||||
Create Date: 2018-02-19 23:52:06.605741
|
|
||||||
|
|
||||||
"""
|
|
||||||
from alembic import op
|
|
||||||
import sqlalchemy as sa
|
|
||||||
|
|
||||||
# revision identifiers, used by Alembic.
|
|
||||||
revision = '1b241f7e8530'
|
|
||||||
down_revision = '97d2a942bcf8'
|
|
||||||
branch_labels = None
|
|
||||||
depends_on = None
|
|
||||||
|
|
||||||
|
|
||||||
def upgrade():
|
|
||||||
op.create_table('telegram_file',
|
|
||||||
sa.Column('id', sa.String(), nullable=False),
|
|
||||||
sa.Column('mxc', sa.String(), nullable=True),
|
|
||||||
sa.Column('mime_type', sa.String(), nullable=True),
|
|
||||||
sa.Column('was_converted', sa.Boolean(), nullable=True),
|
|
||||||
sa.PrimaryKeyConstraint('id'))
|
|
||||||
|
|
||||||
|
|
||||||
def downgrade():
|
|
||||||
op.drop_table('telegram_file')
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
"""Add is_bot field to puppets
|
|
||||||
|
|
||||||
Revision ID: 1fa46383a9d3
|
|
||||||
Revises: 30eca60587f1
|
|
||||||
Create Date: 2018-04-29 23:44:40.102333
|
|
||||||
|
|
||||||
"""
|
|
||||||
from alembic import op
|
|
||||||
import sqlalchemy as sa
|
|
||||||
|
|
||||||
|
|
||||||
# revision identifiers, used by Alembic.
|
|
||||||
revision = '1fa46383a9d3'
|
|
||||||
down_revision = '30eca60587f1'
|
|
||||||
branch_labels = None
|
|
||||||
depends_on = None
|
|
||||||
|
|
||||||
|
|
||||||
def upgrade():
|
|
||||||
with op.batch_alter_table("puppet") as batch_op:
|
|
||||||
batch_op.add_column(sa.Column('is_bot', sa.Boolean(), nullable=True))
|
|
||||||
|
|
||||||
|
|
||||||
def downgrade():
|
|
||||||
with op.batch_alter_table("puppet") as batch_op:
|
|
||||||
batch_op.drop_column('is_bot')
|
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
"""Add cascade rules to UserPortal
|
|
||||||
|
|
||||||
Revision ID: 2228d49c383f
|
|
||||||
Revises: bcfefa1f1299
|
|
||||||
Create Date: 2018-05-31 11:11:59.482112
|
|
||||||
|
|
||||||
"""
|
|
||||||
from alembic import op
|
|
||||||
import sqlalchemy as sa
|
|
||||||
|
|
||||||
# revision identifiers, used by Alembic.
|
|
||||||
revision = '2228d49c383f'
|
|
||||||
down_revision = 'bcfefa1f1299'
|
|
||||||
branch_labels = None
|
|
||||||
depends_on = None
|
|
||||||
|
|
||||||
|
|
||||||
def upgrade():
|
|
||||||
try:
|
|
||||||
with op.batch_alter_table("user_portal") as batch_op:
|
|
||||||
batch_op.drop_constraint("user_portal_user_fkey", type_="foreignkey")
|
|
||||||
batch_op.drop_constraint("user_portal_portal_fkey", type_="foreignkey")
|
|
||||||
batch_op.create_foreign_key("user_portal_user_fkey", "user", ["user"], ["tgid"],
|
|
||||||
onupdate="CASCADE", ondelete="CASCADE")
|
|
||||||
batch_op.create_foreign_key("user_portal_portal_fkey", "portal",
|
|
||||||
["portal", "portal_receiver"], ["tgid", "tg_receiver"],
|
|
||||||
onupdate="CASCADE", ondelete="CASCADE")
|
|
||||||
except ValueError:
|
|
||||||
return
|
|
||||||
|
|
||||||
|
|
||||||
def downgrade():
|
|
||||||
try:
|
|
||||||
with op.batch_alter_table("user_portal") as batch_op:
|
|
||||||
batch_op.drop_constraint("user_portal_user_fkey", type_="foreignkey")
|
|
||||||
batch_op.drop_constraint("user_portal_portal_fkey", type_="foreignkey")
|
|
||||||
batch_op.create_foreign_key("user_portal_user_fkey", "portal",
|
|
||||||
["portal", "portal_receiver"], ["tgid", "tg_receiver"])
|
|
||||||
batch_op.create_foreign_key("user_portal_portal_fkey", "user", ["user"], ["tgid"])
|
|
||||||
except ValueError:
|
|
||||||
return
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
"""Add encrypted field for portals
|
|
||||||
|
|
||||||
Revision ID: 24f31fc8a72b
|
|
||||||
Revises: a7c04a56041b
|
|
||||||
Create Date: 2020-03-28 20:14:29.046699
|
|
||||||
|
|
||||||
"""
|
|
||||||
from alembic import op
|
|
||||||
import sqlalchemy as sa
|
|
||||||
|
|
||||||
|
|
||||||
# revision identifiers, used by Alembic.
|
|
||||||
revision = "24f31fc8a72b"
|
|
||||||
down_revision = "a7c04a56041b"
|
|
||||||
branch_labels = None
|
|
||||||
depends_on = None
|
|
||||||
|
|
||||||
|
|
||||||
def upgrade():
|
|
||||||
with op.batch_alter_table("portal") as batch_op:
|
|
||||||
batch_op.add_column(sa.Column("encrypted", sa.Boolean(), nullable=False,
|
|
||||||
server_default=sa.sql.expression.false()))
|
|
||||||
|
|
||||||
|
|
||||||
def downgrade():
|
|
||||||
with op.batch_alter_table("portal") as batch_op:
|
|
||||||
batch_op.drop_column("encrypted")
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
"""Add megagroup field to portals
|
|
||||||
|
|
||||||
Revision ID: 30eca60587f1
|
|
||||||
Revises: cfc972368e50
|
|
||||||
Create Date: 2018-04-29 15:51:04.656605
|
|
||||||
|
|
||||||
"""
|
|
||||||
from alembic import op
|
|
||||||
import sqlalchemy as sa
|
|
||||||
|
|
||||||
# revision identifiers, used by Alembic.
|
|
||||||
revision = '30eca60587f1'
|
|
||||||
down_revision = 'cfc972368e50'
|
|
||||||
branch_labels = None
|
|
||||||
depends_on = None
|
|
||||||
|
|
||||||
|
|
||||||
def upgrade():
|
|
||||||
with op.batch_alter_table("portal") as batch_op:
|
|
||||||
batch_op.add_column(sa.Column('megagroup', sa.Boolean()))
|
|
||||||
|
|
||||||
|
|
||||||
def downgrade():
|
|
||||||
with op.batch_alter_table("portal") as batch_op:
|
|
||||||
batch_op.drop_column('megagroup')
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
"""Store Matrix avatar URL in database
|
|
||||||
|
|
||||||
Revision ID: 3e3745baa458
|
|
||||||
Revises: dff56c93da8d
|
|
||||||
Create Date: 2020-06-15 14:32:10.454033
|
|
||||||
|
|
||||||
"""
|
|
||||||
from alembic import op
|
|
||||||
import sqlalchemy as sa
|
|
||||||
|
|
||||||
|
|
||||||
# revision identifiers, used by Alembic.
|
|
||||||
revision = '3e3745baa458'
|
|
||||||
down_revision = 'dff56c93da8d'
|
|
||||||
branch_labels = None
|
|
||||||
depends_on = None
|
|
||||||
|
|
||||||
|
|
||||||
def upgrade():
|
|
||||||
# ### commands auto generated by Alembic - please adjust! ###
|
|
||||||
with op.batch_alter_table('portal', schema=None) as batch_op:
|
|
||||||
batch_op.add_column(sa.Column('avatar_url', sa.String(), nullable=True))
|
|
||||||
|
|
||||||
# ### end Alembic commands ###
|
|
||||||
|
|
||||||
|
|
||||||
def downgrade():
|
|
||||||
# ### commands auto generated by Alembic - please adjust! ###
|
|
||||||
with op.batch_alter_table('portal', schema=None) as batch_op:
|
|
||||||
batch_op.drop_column('avatar_url')
|
|
||||||
|
|
||||||
# ### end Alembic commands ###
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
"""Switch mx_user_profile to native enum
|
|
||||||
|
|
||||||
Revision ID: 4f7d7ed5792a
|
|
||||||
Revises: 9e9c89b0b877
|
|
||||||
Create Date: 2019-08-04 17:47:36.568120
|
|
||||||
|
|
||||||
"""
|
|
||||||
from alembic import op
|
|
||||||
|
|
||||||
|
|
||||||
# revision identifiers, used by Alembic.
|
|
||||||
revision = '4f7d7ed5792a'
|
|
||||||
down_revision = '9e9c89b0b877'
|
|
||||||
branch_labels = None
|
|
||||||
depends_on = None
|
|
||||||
|
|
||||||
|
|
||||||
def upgrade():
|
|
||||||
conn = op.get_bind()
|
|
||||||
conn.execute("UPDATE mx_user_profile SET membership=UPPER(membership)")
|
|
||||||
conn.execute("UPDATE mx_user_profile SET membership='LEAVE' WHERE membership='LEFT'")
|
|
||||||
|
|
||||||
|
|
||||||
def downgrade():
|
|
||||||
conn = op.get_bind()
|
|
||||||
conn.execute("UPDATE mx_user_profile SET membership=LOWER(membership)")
|
|
||||||
@@ -1,111 +0,0 @@
|
|||||||
"""Move sessions to main database
|
|
||||||
|
|
||||||
Revision ID: 501dad2868bc
|
|
||||||
Revises: 7d47d84380b6
|
|
||||||
Create Date: 2018-03-02 19:15:53.826985
|
|
||||||
|
|
||||||
"""
|
|
||||||
from alembic import op
|
|
||||||
import sqlalchemy as sa
|
|
||||||
import sqlite3
|
|
||||||
import os
|
|
||||||
|
|
||||||
# revision identifiers, used by Alembic.
|
|
||||||
revision = '501dad2868bc'
|
|
||||||
down_revision = '7d47d84380b6'
|
|
||||||
branch_labels = None
|
|
||||||
depends_on = None
|
|
||||||
|
|
||||||
|
|
||||||
def upgrade():
|
|
||||||
Session = op.create_table('telethon_sessions',
|
|
||||||
sa.Column('session_id', sa.String, nullable=False),
|
|
||||||
sa.Column('dc_id', sa.Integer, nullable=False),
|
|
||||||
sa.Column('server_address', sa.String, nullable=True),
|
|
||||||
sa.Column('port', sa.Integer, nullable=True),
|
|
||||||
sa.Column('auth_key', sa.LargeBinary, nullable=True),
|
|
||||||
sa.PrimaryKeyConstraint('session_id', 'dc_id'))
|
|
||||||
SentFile = op.create_table('telethon_sent_files',
|
|
||||||
sa.Column('session_id', sa.String, nullable=False),
|
|
||||||
sa.Column('md5_digest', sa.LargeBinary, nullable=False),
|
|
||||||
sa.Column('file_size', sa.Integer, nullable=False),
|
|
||||||
sa.Column('type', sa.Integer, nullable=False),
|
|
||||||
sa.Column('id', sa.BigInteger, nullable=True),
|
|
||||||
sa.Column('hash', sa.BigInteger, nullable=True),
|
|
||||||
sa.PrimaryKeyConstraint('session_id', 'md5_digest', 'file_size',
|
|
||||||
'type'))
|
|
||||||
Entity = op.create_table('telethon_entities',
|
|
||||||
sa.Column('session_id', sa.String, nullable=False),
|
|
||||||
sa.Column('id', sa.Integer, nullable=False),
|
|
||||||
sa.Column('hash', sa.Integer, nullable=False),
|
|
||||||
sa.Column('username', sa.String, nullable=True),
|
|
||||||
sa.Column('phone', sa.Integer, nullable=True),
|
|
||||||
sa.Column('name', sa.String, nullable=True),
|
|
||||||
sa.PrimaryKeyConstraint('session_id', 'id'))
|
|
||||||
Version = op.create_table('telethon_version',
|
|
||||||
sa.Column('version', sa.Integer, nullable=False),
|
|
||||||
sa.PrimaryKeyConstraint('version'))
|
|
||||||
conn = op.get_bind()
|
|
||||||
sessions = [os.path.basename(f) for f in os.listdir(".") if f.endswith(".session")]
|
|
||||||
for session in sessions:
|
|
||||||
session_to_sqlalchemy(conn, session, Session, SentFile, Entity)
|
|
||||||
|
|
||||||
|
|
||||||
def session_to_sqlalchemy(conn, path, Session, SentFile, Entity):
|
|
||||||
session_conn = sqlite3.connect(path)
|
|
||||||
session_id = os.path.splitext(path)[0]
|
|
||||||
c = session_conn.cursor()
|
|
||||||
|
|
||||||
auth_data_tuples = c.execute("SELECT * FROM sessions").fetchall()
|
|
||||||
auth_data_dicts = []
|
|
||||||
for row in auth_data_tuples:
|
|
||||||
dc_id, server_address, port, auth_key = row
|
|
||||||
auth_data_dicts.append({
|
|
||||||
"session_id": session_id,
|
|
||||||
"dc_id": dc_id,
|
|
||||||
"server_address": server_address,
|
|
||||||
"port": port,
|
|
||||||
"auth_key": auth_key,
|
|
||||||
})
|
|
||||||
if auth_data_dicts:
|
|
||||||
conn.execute(Session.insert().values(auth_data_dicts))
|
|
||||||
|
|
||||||
sent_file_tuples = c.execute("SELECT * FROM sent_files").fetchall()
|
|
||||||
sent_file_dicts = []
|
|
||||||
for row in sent_file_tuples:
|
|
||||||
md5_digest, file_size, type, id, hash = row
|
|
||||||
sent_file_dicts.append({
|
|
||||||
"session_id": session_id,
|
|
||||||
"md5_digest": md5_digest,
|
|
||||||
"file_size": file_size,
|
|
||||||
"type": type,
|
|
||||||
"id": id,
|
|
||||||
"hash": hash,
|
|
||||||
})
|
|
||||||
if sent_file_dicts:
|
|
||||||
conn.execute(SentFile.insert().values(sent_file_dicts))
|
|
||||||
|
|
||||||
entity_tuples = c.execute("SELECT * FROM entities").fetchall()
|
|
||||||
entity_dicts = []
|
|
||||||
for row in entity_tuples:
|
|
||||||
id, hash, username, phone, name = row
|
|
||||||
entity_dicts.append({
|
|
||||||
"session_id": session_id,
|
|
||||||
"id": id,
|
|
||||||
"hash": hash,
|
|
||||||
"username": username,
|
|
||||||
"phone": phone,
|
|
||||||
"name": name,
|
|
||||||
})
|
|
||||||
if entity_dicts:
|
|
||||||
conn.execute(Entity.insert().values(entity_dicts))
|
|
||||||
|
|
||||||
c.close()
|
|
||||||
session_conn.close()
|
|
||||||
|
|
||||||
|
|
||||||
def downgrade():
|
|
||||||
op.drop_table('telethon_version')
|
|
||||||
op.drop_table('telethon_entities')
|
|
||||||
op.drop_table('telethon_sent_files')
|
|
||||||
op.drop_table('telethon_sessions')
|
|
||||||
@@ -1,136 +0,0 @@
|
|||||||
"""Move state store to main database
|
|
||||||
|
|
||||||
Revision ID: 6ca3d74d51e4
|
|
||||||
Revises: 2228d49c383f
|
|
||||||
Create Date: 2018-06-26 21:31:26.911307
|
|
||||||
|
|
||||||
"""
|
|
||||||
import json
|
|
||||||
import re
|
|
||||||
|
|
||||||
from alembic import context, op
|
|
||||||
import sqlalchemy.orm as orm
|
|
||||||
import sqlalchemy as sa
|
|
||||||
|
|
||||||
from mautrix.util.db import Base
|
|
||||||
|
|
||||||
from mautrix_telegram.config import Config
|
|
||||||
|
|
||||||
# revision identifiers, used by Alembic.
|
|
||||||
revision = "6ca3d74d51e4"
|
|
||||||
down_revision = "2228d49c383f"
|
|
||||||
branch_labels = None
|
|
||||||
depends_on = None
|
|
||||||
|
|
||||||
|
|
||||||
class RoomState(Base):
|
|
||||||
__tablename__ = "mx_room_state"
|
|
||||||
__table_args__ = {"extend_existing": True}
|
|
||||||
|
|
||||||
room_id = sa.Column(sa.String, primary_key=True)
|
|
||||||
power_levels = sa.Column("power_levels", sa.Text, nullable=True)
|
|
||||||
|
|
||||||
|
|
||||||
class UserProfile(Base):
|
|
||||||
__tablename__ = "mx_user_profile"
|
|
||||||
__table_args__ = {"extend_existing": True}
|
|
||||||
|
|
||||||
room_id = sa.Column(sa.String, primary_key=True)
|
|
||||||
user_id = sa.Column(sa.String, primary_key=True)
|
|
||||||
membership = sa.Column(sa.String, nullable=False, default="leave")
|
|
||||||
displayname = sa.Column(sa.String, nullable=True)
|
|
||||||
avatar_url = sa.Column(sa.String, nullable=True)
|
|
||||||
|
|
||||||
|
|
||||||
class Puppet(Base):
|
|
||||||
__tablename__ = "puppet"
|
|
||||||
__table_args__ = {"extend_existing": True}
|
|
||||||
|
|
||||||
id = sa.Column(sa.Integer, primary_key=True)
|
|
||||||
displayname = sa.Column(sa.String, nullable=True)
|
|
||||||
displayname_source = sa.Column(sa.Integer, nullable=True)
|
|
||||||
username = sa.Column(sa.String, nullable=True)
|
|
||||||
photo_id = sa.Column(sa.String, nullable=True)
|
|
||||||
is_bot = sa.Column(sa.Boolean, nullable=True)
|
|
||||||
matrix_registered = sa.Column(sa.Boolean, nullable=False, default=False)
|
|
||||||
|
|
||||||
|
|
||||||
def upgrade():
|
|
||||||
with op.batch_alter_table("puppet") as batch_op:
|
|
||||||
batch_op.add_column(sa.Column("matrix_registered", sa.Boolean(), nullable=False,
|
|
||||||
server_default=sa.sql.expression.false()))
|
|
||||||
op.create_table("mx_room_state",
|
|
||||||
sa.Column("room_id", sa.String(), nullable=False),
|
|
||||||
sa.Column("power_levels", sa.Text(), nullable=True),
|
|
||||||
sa.PrimaryKeyConstraint("room_id"))
|
|
||||||
op.create_table("mx_user_profile",
|
|
||||||
sa.Column("room_id", sa.String(), nullable=False),
|
|
||||||
sa.Column("user_id", sa.String(), nullable=False),
|
|
||||||
sa.Column("membership", sa.String(), nullable=False,
|
|
||||||
default="leave"),
|
|
||||||
sa.Column("displayname", sa.String(), nullable=True),
|
|
||||||
sa.Column("avatar_url", sa.String(), nullable=True),
|
|
||||||
sa.PrimaryKeyConstraint("room_id", "user_id"))
|
|
||||||
|
|
||||||
try:
|
|
||||||
migrate_state_store()
|
|
||||||
except Exception as e:
|
|
||||||
print("Failed to migrate state store:", e)
|
|
||||||
print("Migrating the state store isn't required, but you can retry by alembic downgrading "
|
|
||||||
"to revision 2228d49c383f and upgrading again.")
|
|
||||||
|
|
||||||
|
|
||||||
def migrate_state_store():
|
|
||||||
conn = op.get_bind()
|
|
||||||
session: orm.Session = orm.sessionmaker(bind=conn)()
|
|
||||||
|
|
||||||
try:
|
|
||||||
with open("mx-state.json") as file:
|
|
||||||
data = json.load(file)
|
|
||||||
except FileNotFoundError:
|
|
||||||
return
|
|
||||||
if not data:
|
|
||||||
return
|
|
||||||
registrations = data.get("registrations", [])
|
|
||||||
|
|
||||||
mxtg_config_path = context.get_x_argument(as_dictionary=True).get("config", "config.yaml")
|
|
||||||
mxtg_config = Config(mxtg_config_path, None, None)
|
|
||||||
mxtg_config.load()
|
|
||||||
|
|
||||||
username_template = mxtg_config.get("bridge.username_template", "telegram_{userid}")
|
|
||||||
hs_domain = mxtg_config["homeserver.domain"]
|
|
||||||
localpart = username_template.format(userid="(.+)")
|
|
||||||
mxid_regex = re.compile("@{}:{}".format(localpart, hs_domain))
|
|
||||||
for user in registrations:
|
|
||||||
match = mxid_regex.match(user)
|
|
||||||
if not match:
|
|
||||||
continue
|
|
||||||
|
|
||||||
puppet = session.query(Puppet).get(match.group(1))
|
|
||||||
if not puppet:
|
|
||||||
continue
|
|
||||||
|
|
||||||
puppet.matrix_registered = True
|
|
||||||
session.merge(puppet)
|
|
||||||
session.commit()
|
|
||||||
|
|
||||||
user_profiles = [UserProfile(room_id=room, user_id=user,
|
|
||||||
membership=member.get("membership", "leave"),
|
|
||||||
displayname=member.get("displayname", None),
|
|
||||||
avatar_url=member.get("avatar_url", None))
|
|
||||||
for room, members in data.get("members", {}).items()
|
|
||||||
for user, member in members.items()]
|
|
||||||
session.add_all(user_profiles)
|
|
||||||
session.commit()
|
|
||||||
|
|
||||||
room_state = [RoomState(room_id=room, power_levels=json.dumps(levels))
|
|
||||||
for room, levels in data.get("power_levels", {}).items()]
|
|
||||||
session.add_all(room_state)
|
|
||||||
session.commit()
|
|
||||||
|
|
||||||
|
|
||||||
def downgrade():
|
|
||||||
op.drop_table("mx_user_profile")
|
|
||||||
op.drop_table("mx_room_state")
|
|
||||||
with op.batch_alter_table("puppet") as batch_op:
|
|
||||||
batch_op.drop_column("matrix_registered")
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
"""Add timestamp to TelegramFile
|
|
||||||
|
|
||||||
Revision ID: 7d47d84380b6
|
|
||||||
Revises: 1b241f7e8530
|
|
||||||
Create Date: 2018-02-19 23:53:18.050871
|
|
||||||
|
|
||||||
"""
|
|
||||||
from alembic import op
|
|
||||||
import sqlalchemy as sa
|
|
||||||
|
|
||||||
# revision identifiers, used by Alembic.
|
|
||||||
revision = '7d47d84380b6'
|
|
||||||
down_revision = '1b241f7e8530'
|
|
||||||
branch_labels = None
|
|
||||||
depends_on = None
|
|
||||||
|
|
||||||
|
|
||||||
def upgrade():
|
|
||||||
op.add_column('telegram_file',
|
|
||||||
sa.Column('timestamp', sa.BigInteger(), nullable=True, default=0,
|
|
||||||
server_default="0"))
|
|
||||||
|
|
||||||
|
|
||||||
def downgrade():
|
|
||||||
with op.batch_alter_table("telegram_file") as batch_op:
|
|
||||||
batch_op.drop_column('timestamp')
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
"""Add double puppet base URL to puppet table
|
|
||||||
|
|
||||||
Revision ID: 888275d58e57
|
|
||||||
Revises: a328bf4f0932
|
|
||||||
Create Date: 2020-10-14 18:52:00.730666
|
|
||||||
|
|
||||||
"""
|
|
||||||
from alembic import op
|
|
||||||
import sqlalchemy as sa
|
|
||||||
from sqlalchemy.dialects import postgresql
|
|
||||||
|
|
||||||
# revision identifiers, used by Alembic.
|
|
||||||
revision = '888275d58e57'
|
|
||||||
down_revision = 'a328bf4f0932'
|
|
||||||
branch_labels = None
|
|
||||||
depends_on = None
|
|
||||||
|
|
||||||
|
|
||||||
def upgrade():
|
|
||||||
# ### commands auto generated by Alembic - please adjust! ###
|
|
||||||
with op.batch_alter_table('puppet', schema=None) as batch_op:
|
|
||||||
batch_op.add_column(sa.Column('base_url', sa.Text(), nullable=True))
|
|
||||||
# ### end Alembic commands ###
|
|
||||||
|
|
||||||
|
|
||||||
def downgrade():
|
|
||||||
# ### commands auto generated by Alembic - please adjust! ###
|
|
||||||
with op.batch_alter_table('puppet', schema=None) as batch_op:
|
|
||||||
batch_op.drop_column('base_url')
|
|
||||||
# ### end Alembic commands ###
|
|
||||||
@@ -1,80 +0,0 @@
|
|||||||
"""initial revision
|
|
||||||
|
|
||||||
Revision ID: 97d2a942bcf8
|
|
||||||
Revises:
|
|
||||||
Create Date: 2018-02-11 18:40:55.483842
|
|
||||||
|
|
||||||
"""
|
|
||||||
from alembic import op
|
|
||||||
import sqlalchemy as sa
|
|
||||||
|
|
||||||
# revision identifiers, used by Alembic.
|
|
||||||
revision = '97d2a942bcf8'
|
|
||||||
down_revision = None
|
|
||||||
branch_labels = None
|
|
||||||
depends_on = None
|
|
||||||
|
|
||||||
|
|
||||||
def upgrade():
|
|
||||||
op.create_table('portal',
|
|
||||||
sa.Column('tgid', sa.Integer),
|
|
||||||
sa.Column('tg_receiver', sa.Integer),
|
|
||||||
sa.Column('peer_type', sa.String, nullable=False, default=""),
|
|
||||||
sa.Column('mxid', sa.String, nullable=True),
|
|
||||||
sa.Column('username', sa.String, nullable=True),
|
|
||||||
sa.Column('title', sa.String, nullable=True),
|
|
||||||
sa.Column('about', sa.String, nullable=True),
|
|
||||||
sa.Column('photo_id', sa.String, nullable=True),
|
|
||||||
sa.PrimaryKeyConstraint('tgid', 'tg_receiver'),
|
|
||||||
sa.UniqueConstraint('mxid'))
|
|
||||||
op.create_table('user',
|
|
||||||
sa.Column('mxid', sa.String),
|
|
||||||
sa.Column('tgid', sa.Integer, nullable=True, unique=True),
|
|
||||||
sa.Column('tg_username', sa.String, nullable=True),
|
|
||||||
sa.Column('saved_contacts', sa.Integer, nullable=False, default=0),
|
|
||||||
sa.PrimaryKeyConstraint('mxid'))
|
|
||||||
op.create_table('puppet',
|
|
||||||
sa.Column('id', sa.Integer),
|
|
||||||
sa.Column('displayname', sa.String, nullable=True),
|
|
||||||
sa.Column('username', sa.String, nullable=True),
|
|
||||||
sa.Column('photo_id', sa.String, nullable=True),
|
|
||||||
sa.PrimaryKeyConstraint('id'))
|
|
||||||
op.create_table('contact',
|
|
||||||
sa.Column('user', sa.Integer),
|
|
||||||
sa.Column('contact', sa.Integer),
|
|
||||||
sa.ForeignKeyConstraint(("user",), ("user.tgid",)),
|
|
||||||
sa.ForeignKeyConstraint(("contact",), ("puppet.id",)),
|
|
||||||
sa.PrimaryKeyConstraint('user', 'contact'))
|
|
||||||
op.create_table('user_portal',
|
|
||||||
sa.Column('user', sa.Integer),
|
|
||||||
sa.Column('portal', sa.Integer),
|
|
||||||
sa.Column('portal_receiver', sa.Integer),
|
|
||||||
sa.PrimaryKeyConstraint('user', 'portal', 'portal_receiver'),
|
|
||||||
sa.ForeignKeyConstraint(("user",), ("user.tgid",),
|
|
||||||
name="user_portal_user_fkey",
|
|
||||||
onupdate="CASCADE", ondelete="CASCADE"),
|
|
||||||
sa.ForeignKeyConstraint(("portal", "portal_receiver"),
|
|
||||||
("portal.tgid", "portal.tg_receiver"),
|
|
||||||
name="user_portal_portal_fkey",
|
|
||||||
onupdate="CASCADE", ondelete="CASCADE"))
|
|
||||||
op.create_table('message',
|
|
||||||
sa.Column('mxid', sa.String),
|
|
||||||
sa.Column('mx_room', sa.String),
|
|
||||||
sa.Column('tgid', sa.Integer),
|
|
||||||
sa.Column('tg_space', sa.Integer),
|
|
||||||
sa.PrimaryKeyConstraint('tgid', 'tg_space'),
|
|
||||||
sa.UniqueConstraint("mxid", "mx_room", "tg_space", name="_mx_id_room"))
|
|
||||||
op.create_table('bot_chat',
|
|
||||||
sa.Column('id', sa.Integer),
|
|
||||||
sa.Column('type', sa.String, nullable=False, default=""),
|
|
||||||
sa.PrimaryKeyConstraint('id'))
|
|
||||||
|
|
||||||
|
|
||||||
def downgrade():
|
|
||||||
op.drop_table('bot_chat')
|
|
||||||
op.drop_table('message')
|
|
||||||
op.drop_table('user_portal')
|
|
||||||
op.drop_table('contact')
|
|
||||||
op.drop_table('puppet')
|
|
||||||
op.drop_table('user')
|
|
||||||
op.drop_table('portal')
|
|
||||||
@@ -1,48 +0,0 @@
|
|||||||
"""Add edit index to messages
|
|
||||||
|
|
||||||
Revision ID: 9e9c89b0b877
|
|
||||||
Revises: 17574c57f3f8
|
|
||||||
Create Date: 2019-05-29 15:28:23.128377
|
|
||||||
|
|
||||||
"""
|
|
||||||
from alembic import op
|
|
||||||
import sqlalchemy as sa
|
|
||||||
|
|
||||||
# revision identifiers, used by Alembic.
|
|
||||||
revision = '9e9c89b0b877'
|
|
||||||
down_revision = '17574c57f3f8'
|
|
||||||
branch_labels = None
|
|
||||||
depends_on = None
|
|
||||||
|
|
||||||
|
|
||||||
def upgrade():
|
|
||||||
op.create_table('_message_temp',
|
|
||||||
sa.Column('mxid', sa.String),
|
|
||||||
sa.Column('mx_room', sa.String),
|
|
||||||
sa.Column('tgid', sa.Integer),
|
|
||||||
sa.Column('tg_space', sa.Integer),
|
|
||||||
sa.Column('edit_index', sa.Integer),
|
|
||||||
sa.PrimaryKeyConstraint('tgid', 'tg_space', 'edit_index'),
|
|
||||||
sa.UniqueConstraint("mxid", "mx_room", "tg_space", name="_mx_id_room_2"))
|
|
||||||
c = op.get_bind()
|
|
||||||
c.execute("INSERT INTO _message_temp (mxid, mx_room, tgid, tg_space, edit_index) "
|
|
||||||
"SELECT message.mxid, message.mx_room, message.tgid, message.tg_space, 0 "
|
|
||||||
"FROM message")
|
|
||||||
c.execute("DROP TABLE message")
|
|
||||||
c.execute("ALTER TABLE _message_temp RENAME TO message")
|
|
||||||
|
|
||||||
|
|
||||||
def downgrade():
|
|
||||||
op.create_table('_message_temp',
|
|
||||||
sa.Column('mxid', sa.String),
|
|
||||||
sa.Column('mx_room', sa.String),
|
|
||||||
sa.Column('tgid', sa.Integer),
|
|
||||||
sa.Column('tg_space', sa.Integer),
|
|
||||||
sa.PrimaryKeyConstraint('tgid', 'tg_space'),
|
|
||||||
sa.UniqueConstraint("mxid", "mx_room", "tg_space", name="_mx_id_room"))
|
|
||||||
c = op.get_bind()
|
|
||||||
c.execute("INSERT INTO _message_temp (mxid, mx_room, tgid, tg_space) "
|
|
||||||
"SELECT message.mxid, message.mx_room, message.tgid, message.tg_space "
|
|
||||||
"FROM message")
|
|
||||||
c.execute("DROP TABLE message")
|
|
||||||
c.execute("ALTER TABLE _message_temp RENAME TO message")
|
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
"""Store encryption state event in db
|
|
||||||
|
|
||||||
Revision ID: a328bf4f0932
|
|
||||||
Revises: ccbaff858240
|
|
||||||
Create Date: 2020-07-11 21:31:27.059813
|
|
||||||
|
|
||||||
"""
|
|
||||||
from alembic import op
|
|
||||||
import sqlalchemy as sa
|
|
||||||
|
|
||||||
from mautrix.client.state_store.sqlalchemy import SerializableType
|
|
||||||
from mautrix.types import RoomEncryptionStateEventContent
|
|
||||||
|
|
||||||
# revision identifiers, used by Alembic.
|
|
||||||
revision = 'a328bf4f0932'
|
|
||||||
down_revision = 'ccbaff858240'
|
|
||||||
branch_labels = None
|
|
||||||
depends_on = None
|
|
||||||
|
|
||||||
|
|
||||||
def upgrade():
|
|
||||||
# ### commands auto generated by Alembic - please adjust! ###
|
|
||||||
with op.batch_alter_table('mx_room_state', schema=None) as batch_op:
|
|
||||||
batch_op.add_column(sa.Column('encryption',
|
|
||||||
SerializableType(RoomEncryptionStateEventContent),
|
|
||||||
nullable=True))
|
|
||||||
batch_op.add_column(sa.Column('has_full_member_list', sa.Boolean(), nullable=True))
|
|
||||||
batch_op.add_column(sa.Column('is_encrypted', sa.Boolean(), nullable=True))
|
|
||||||
# ### end Alembic commands ###
|
|
||||||
|
|
||||||
|
|
||||||
def downgrade():
|
|
||||||
# ### commands auto generated by Alembic - please adjust! ###
|
|
||||||
with op.batch_alter_table('mx_room_state', schema=None) as batch_op:
|
|
||||||
batch_op.drop_column('is_encrypted')
|
|
||||||
batch_op.drop_column('has_full_member_list')
|
|
||||||
batch_op.drop_column('encryption')
|
|
||||||
# ### end Alembic commands ###
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
"""Store custom puppet next_batch in database
|
|
||||||
|
|
||||||
Revision ID: a7c04a56041b
|
|
||||||
Revises: 4f7d7ed5792a
|
|
||||||
Create Date: 2019-08-06 23:08:51.087651
|
|
||||||
|
|
||||||
"""
|
|
||||||
from alembic import op
|
|
||||||
import sqlalchemy as sa
|
|
||||||
|
|
||||||
|
|
||||||
# revision identifiers, used by Alembic.
|
|
||||||
revision = "a7c04a56041b"
|
|
||||||
down_revision = "4f7d7ed5792a"
|
|
||||||
branch_labels = None
|
|
||||||
depends_on = None
|
|
||||||
|
|
||||||
|
|
||||||
def upgrade():
|
|
||||||
with op.batch_alter_table("puppet") as batch_op:
|
|
||||||
batch_op.add_column(sa.Column("next_batch", sa.String(), nullable=True))
|
|
||||||
|
|
||||||
|
|
||||||
def downgrade():
|
|
||||||
with op.batch_alter_table("puppet") as batch_op:
|
|
||||||
batch_op.drop_column("next_batch")
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
"""Add phone number field to users
|
|
||||||
|
|
||||||
Revision ID: a9119be92164
|
|
||||||
Revises: b54929c22c86
|
|
||||||
Create Date: 2018-09-28 02:38:40.626282
|
|
||||||
|
|
||||||
"""
|
|
||||||
from alembic import op
|
|
||||||
import sqlalchemy as sa
|
|
||||||
|
|
||||||
|
|
||||||
# revision identifiers, used by Alembic.
|
|
||||||
revision = "a9119be92164"
|
|
||||||
down_revision = "b54929c22c86"
|
|
||||||
branch_labels = None
|
|
||||||
depends_on = None
|
|
||||||
|
|
||||||
|
|
||||||
def upgrade():
|
|
||||||
op.add_column("user", sa.Column("tg_phone", sa.String(), nullable=True))
|
|
||||||
|
|
||||||
|
|
||||||
def downgrade():
|
|
||||||
with op.batch_alter_table("user") as batch_op:
|
|
||||||
batch_op.drop_column("tg_phone")
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
"""Add portal-specific config
|
|
||||||
|
|
||||||
Revision ID: b54929c22c86
|
|
||||||
Revises: d5f7b8b4b456
|
|
||||||
Create Date: 2018-09-24 23:40:33.528710
|
|
||||||
|
|
||||||
"""
|
|
||||||
from alembic import op
|
|
||||||
import sqlalchemy as sa
|
|
||||||
|
|
||||||
|
|
||||||
# revision identifiers, used by Alembic.
|
|
||||||
revision = "b54929c22c86"
|
|
||||||
down_revision = "d5f7b8b4b456"
|
|
||||||
branch_labels = None
|
|
||||||
depends_on = None
|
|
||||||
|
|
||||||
|
|
||||||
def upgrade():
|
|
||||||
op.add_column("portal", sa.Column("config", sa.Text(), nullable=True))
|
|
||||||
|
|
||||||
|
|
||||||
def downgrade():
|
|
||||||
with op.batch_alter_table("portal") as batch_op:
|
|
||||||
batch_op.drop_column("config")
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
"""Add displayname source fields for puppets
|
|
||||||
|
|
||||||
Revision ID: bcfefa1f1299
|
|
||||||
Revises: bdadd173ee02
|
|
||||||
Create Date: 2018-05-19 17:00:21.078098
|
|
||||||
|
|
||||||
"""
|
|
||||||
from alembic import op
|
|
||||||
import sqlalchemy as sa
|
|
||||||
|
|
||||||
# revision identifiers, used by Alembic.
|
|
||||||
revision = 'bcfefa1f1299'
|
|
||||||
down_revision = 'bdadd173ee02'
|
|
||||||
branch_labels = None
|
|
||||||
depends_on = None
|
|
||||||
|
|
||||||
|
|
||||||
def upgrade():
|
|
||||||
op.add_column('puppet', sa.Column('displayname_source', sa.Integer(), nullable=True))
|
|
||||||
|
|
||||||
|
|
||||||
def downgrade():
|
|
||||||
with op.batch_alter_table("puppet") as batch_op:
|
|
||||||
batch_op.drop_column('displayname_source')
|
|
||||||
@@ -1,43 +0,0 @@
|
|||||||
"""Update telethon update state table
|
|
||||||
|
|
||||||
Revision ID: bdadd173ee02
|
|
||||||
Revises: eeaf0dae87ce
|
|
||||||
Create Date: 2018-05-13 10:42:59.395597
|
|
||||||
|
|
||||||
"""
|
|
||||||
from alembic import op
|
|
||||||
import sqlalchemy as sa
|
|
||||||
|
|
||||||
# revision identifiers, used by Alembic.
|
|
||||||
revision = 'bdadd173ee02'
|
|
||||||
down_revision = 'eeaf0dae87ce'
|
|
||||||
branch_labels = None
|
|
||||||
depends_on = None
|
|
||||||
|
|
||||||
|
|
||||||
def upgrade():
|
|
||||||
with op.batch_alter_table("telethon_entities") as batch_op:
|
|
||||||
batch_op.alter_column("id", existing_type=sa.Integer, type_=sa.BigInteger)
|
|
||||||
batch_op.alter_column("hash", existing_type=sa.Integer, type_=sa.BigInteger)
|
|
||||||
|
|
||||||
with op.batch_alter_table("telethon_update_state") as batch_op:
|
|
||||||
batch_op.alter_column("entity_id", existing_type=sa.Integer, type_=sa.BigInteger)
|
|
||||||
batch_op.alter_column("pts", existing_type=sa.Integer, type_=sa.BigInteger)
|
|
||||||
batch_op.alter_column("qts", existing_type=sa.Integer, type_=sa.BigInteger)
|
|
||||||
batch_op.alter_column("date", existing_type=sa.Integer, type_=sa.BigInteger)
|
|
||||||
batch_op.alter_column("seq", existing_type=sa.Integer, type_=sa.BigInteger)
|
|
||||||
batch_op.add_column(sa.Column("unread_count", sa.Integer))
|
|
||||||
|
|
||||||
|
|
||||||
def downgrade():
|
|
||||||
with op.batch_alter_table("telethon_entities") as batch_op:
|
|
||||||
batch_op.alter_column("id", existing_type=sa.BigInteger, type_=sa.Integer)
|
|
||||||
batch_op.alter_column("hash", existing_type=sa.BigInteger, type_=sa.Integer)
|
|
||||||
|
|
||||||
with op.batch_alter_table("telethon_update_state") as batch_op:
|
|
||||||
batch_op.alter_column("entity_id", existing_type=sa.BigInteger, type_=sa.Integer)
|
|
||||||
batch_op.alter_column("pts", existing_type=sa.BigInteger, type_=sa.Integer)
|
|
||||||
batch_op.alter_column("qts", existing_type=sa.BigInteger, type_=sa.Integer)
|
|
||||||
batch_op.alter_column("date", existing_type=sa.BigInteger, type_=sa.Integer)
|
|
||||||
batch_op.alter_column("seq", existing_type=sa.BigInteger, type_=sa.Integer)
|
|
||||||
batch_op.drop_column("unread_count")
|
|
||||||
@@ -1,71 +0,0 @@
|
|||||||
"""Switch to mautrix-python crypto
|
|
||||||
|
|
||||||
Revision ID: ccbaff858240
|
|
||||||
Revises: 3e3745baa458
|
|
||||||
Create Date: 2020-07-08 19:06:12.588047
|
|
||||||
|
|
||||||
"""
|
|
||||||
from alembic import op
|
|
||||||
import sqlalchemy as sa
|
|
||||||
from sqlalchemy.dialects import postgresql
|
|
||||||
|
|
||||||
# revision identifiers, used by Alembic.
|
|
||||||
revision = 'ccbaff858240'
|
|
||||||
down_revision = '3e3745baa458'
|
|
||||||
branch_labels = None
|
|
||||||
depends_on = None
|
|
||||||
|
|
||||||
|
|
||||||
def upgrade():
|
|
||||||
# ### commands auto generated by Alembic - please adjust! ###
|
|
||||||
op.drop_table('nio_account')
|
|
||||||
op.drop_table('nio_device_key')
|
|
||||||
op.drop_table('nio_outgoing_key_request')
|
|
||||||
op.drop_table('nio_olm_session')
|
|
||||||
op.drop_table('nio_megolm_inbound_session')
|
|
||||||
# ### end Alembic commands ###
|
|
||||||
|
|
||||||
|
|
||||||
def downgrade():
|
|
||||||
# ### commands auto generated by Alembic - please adjust! ###
|
|
||||||
op.create_table('nio_megolm_inbound_session',
|
|
||||||
sa.Column('session_id', sa.VARCHAR(length=255), autoincrement=False, nullable=False),
|
|
||||||
sa.Column('sender_key', sa.VARCHAR(length=255), autoincrement=False, nullable=False),
|
|
||||||
sa.Column('fp_key', sa.VARCHAR(length=255), autoincrement=False, nullable=False),
|
|
||||||
sa.Column('room_id', sa.VARCHAR(length=255), autoincrement=False, nullable=False),
|
|
||||||
sa.Column('session', postgresql.BYTEA(), autoincrement=False, nullable=False),
|
|
||||||
sa.Column('forwarded_chains', postgresql.BYTEA(), autoincrement=False, nullable=False),
|
|
||||||
sa.PrimaryKeyConstraint('session_id', name='nio_megolm_inbound_session_pkey')
|
|
||||||
)
|
|
||||||
op.create_table('nio_olm_session',
|
|
||||||
sa.Column('session_id', sa.VARCHAR(length=255), autoincrement=False, nullable=False),
|
|
||||||
sa.Column('sender_key', sa.VARCHAR(length=255), autoincrement=False, nullable=False),
|
|
||||||
sa.Column('session', postgresql.BYTEA(), autoincrement=False, nullable=False),
|
|
||||||
sa.Column('created_at', postgresql.TIMESTAMP(), autoincrement=False, nullable=False),
|
|
||||||
sa.Column('last_used', postgresql.TIMESTAMP(), autoincrement=False, nullable=False),
|
|
||||||
sa.PrimaryKeyConstraint('session_id', name='nio_olm_session_pkey')
|
|
||||||
)
|
|
||||||
op.create_table('nio_outgoing_key_request',
|
|
||||||
sa.Column('request_id', sa.VARCHAR(length=255), autoincrement=False, nullable=False),
|
|
||||||
sa.Column('session_id', sa.VARCHAR(length=255), autoincrement=False, nullable=False),
|
|
||||||
sa.Column('room_id', sa.VARCHAR(length=255), autoincrement=False, nullable=False),
|
|
||||||
sa.Column('algorithm', sa.VARCHAR(length=255), autoincrement=False, nullable=False),
|
|
||||||
sa.PrimaryKeyConstraint('request_id', name='nio_outgoing_key_request_pkey')
|
|
||||||
)
|
|
||||||
op.create_table('nio_device_key',
|
|
||||||
sa.Column('user_id', sa.VARCHAR(length=255), autoincrement=False, nullable=False),
|
|
||||||
sa.Column('device_id', sa.VARCHAR(length=255), autoincrement=False, nullable=False),
|
|
||||||
sa.Column('display_name', sa.VARCHAR(length=255), autoincrement=False, nullable=False),
|
|
||||||
sa.Column('deleted', sa.BOOLEAN(), autoincrement=False, nullable=False),
|
|
||||||
sa.Column('keys', postgresql.BYTEA(), autoincrement=False, nullable=False),
|
|
||||||
sa.PrimaryKeyConstraint('user_id', 'device_id', name='nio_device_key_pkey')
|
|
||||||
)
|
|
||||||
op.create_table('nio_account',
|
|
||||||
sa.Column('user_id', sa.VARCHAR(length=255), autoincrement=False, nullable=False),
|
|
||||||
sa.Column('device_id', sa.VARCHAR(length=255), autoincrement=False, nullable=False),
|
|
||||||
sa.Column('shared', sa.BOOLEAN(), autoincrement=False, nullable=False),
|
|
||||||
sa.Column('sync_token', sa.TEXT(), autoincrement=False, nullable=False),
|
|
||||||
sa.Column('account', postgresql.BYTEA(), autoincrement=False, nullable=False),
|
|
||||||
sa.PrimaryKeyConstraint('user_id', 'device_id', name='nio_account_pkey')
|
|
||||||
)
|
|
||||||
# ### end Alembic commands ###
|
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
"""Add metadata to TelegramFile
|
|
||||||
|
|
||||||
Revision ID: cfc972368e50
|
|
||||||
Revises: 501dad2868bc
|
|
||||||
Create Date: 2018-03-09 16:07:01.236712
|
|
||||||
|
|
||||||
"""
|
|
||||||
from alembic import op
|
|
||||||
import sqlalchemy as sa
|
|
||||||
|
|
||||||
# revision identifiers, used by Alembic.
|
|
||||||
revision = 'cfc972368e50'
|
|
||||||
down_revision = '501dad2868bc'
|
|
||||||
branch_labels = None
|
|
||||||
depends_on = None
|
|
||||||
|
|
||||||
|
|
||||||
def upgrade():
|
|
||||||
with op.batch_alter_table("telegram_file") as batch_op:
|
|
||||||
batch_op.add_column(sa.Column('size', sa.Integer(), nullable=True))
|
|
||||||
batch_op.add_column(sa.Column('width', sa.Integer(), nullable=True))
|
|
||||||
batch_op.add_column(sa.Column('height', sa.Integer(), nullable=True))
|
|
||||||
batch_op.add_column(sa.Column('thumbnail', sa.String(), nullable=True))
|
|
||||||
batch_op.create_foreign_key(constraint_name="fk_file_thumbnail",
|
|
||||||
referent_table="telegram_file",
|
|
||||||
local_cols=['thumbnail'],
|
|
||||||
remote_cols=['id'])
|
|
||||||
|
|
||||||
|
|
||||||
def downgrade():
|
|
||||||
with op.batch_alter_table("telegram_file") as batch_op:
|
|
||||||
batch_op.drop_column('size')
|
|
||||||
batch_op.drop_column('width')
|
|
||||||
batch_op.drop_column('height')
|
|
||||||
batch_op.drop_column('thumbnail')
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
"""Add decryption info field for reuploaded telegram files
|
|
||||||
|
|
||||||
Revision ID: d3c922a6acd2
|
|
||||||
Revises: 24f31fc8a72b
|
|
||||||
Create Date: 2020-03-30 20:07:17.340346
|
|
||||||
|
|
||||||
"""
|
|
||||||
from alembic import op
|
|
||||||
import sqlalchemy as sa
|
|
||||||
|
|
||||||
|
|
||||||
# revision identifiers, used by Alembic.
|
|
||||||
revision = 'd3c922a6acd2'
|
|
||||||
down_revision = '24f31fc8a72b'
|
|
||||||
branch_labels = None
|
|
||||||
depends_on = None
|
|
||||||
|
|
||||||
|
|
||||||
def upgrade():
|
|
||||||
with op.batch_alter_table("telegram_file") as batch_op:
|
|
||||||
batch_op.add_column(sa.Column("decryption_info", sa.Text(), nullable=True))
|
|
||||||
|
|
||||||
|
|
||||||
def downgrade():
|
|
||||||
with op.batch_alter_table("telegram_file") as batch_op:
|
|
||||||
batch_op.drop_column("decryption_info")
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
"""Add access_token and custom_mxid fields for puppets
|
|
||||||
|
|
||||||
Revision ID: d5f7b8b4b456
|
|
||||||
Revises: 6ca3d74d51e4
|
|
||||||
Create Date: 2018-07-20 12:09:30.277960
|
|
||||||
|
|
||||||
"""
|
|
||||||
from alembic import op
|
|
||||||
import sqlalchemy as sa
|
|
||||||
|
|
||||||
# revision identifiers, used by Alembic.
|
|
||||||
revision = "d5f7b8b4b456"
|
|
||||||
down_revision = "6ca3d74d51e4"
|
|
||||||
branch_labels = None
|
|
||||||
depends_on = None
|
|
||||||
|
|
||||||
|
|
||||||
def upgrade():
|
|
||||||
op.add_column("puppet", sa.Column("access_token", sa.String(), nullable=True))
|
|
||||||
op.add_column("puppet", sa.Column("custom_mxid", sa.String(), nullable=True))
|
|
||||||
|
|
||||||
|
|
||||||
def downgrade():
|
|
||||||
with op.batch_alter_table("puppet") as batch_op:
|
|
||||||
batch_op.drop_column("custom_mxid")
|
|
||||||
batch_op.drop_column("access_token")
|
|
||||||
@@ -1,71 +0,0 @@
|
|||||||
"""Add matrix-nio state store to main db
|
|
||||||
|
|
||||||
Revision ID: dff56c93da8d
|
|
||||||
Revises: d3c922a6acd2
|
|
||||||
Create Date: 2020-03-31 22:04:04.014048
|
|
||||||
|
|
||||||
"""
|
|
||||||
from alembic import op
|
|
||||||
import sqlalchemy as sa
|
|
||||||
|
|
||||||
|
|
||||||
# revision identifiers, used by Alembic.
|
|
||||||
revision = 'dff56c93da8d'
|
|
||||||
down_revision = 'd3c922a6acd2'
|
|
||||||
branch_labels = None
|
|
||||||
depends_on = None
|
|
||||||
|
|
||||||
|
|
||||||
def upgrade():
|
|
||||||
# ### commands auto generated by Alembic - please adjust! ###
|
|
||||||
op.create_table('nio_account',
|
|
||||||
sa.Column('user_id', sa.String(length=255), nullable=False),
|
|
||||||
sa.Column('device_id', sa.String(length=255), nullable=False),
|
|
||||||
sa.Column('shared', sa.Boolean(), nullable=False),
|
|
||||||
sa.Column('sync_token', sa.Text(), nullable=False),
|
|
||||||
sa.Column('account', sa.LargeBinary(), nullable=False),
|
|
||||||
sa.PrimaryKeyConstraint('user_id', 'device_id')
|
|
||||||
)
|
|
||||||
op.create_table('nio_device_key',
|
|
||||||
sa.Column('user_id', sa.String(length=255), nullable=False),
|
|
||||||
sa.Column('device_id', sa.String(length=255), nullable=False),
|
|
||||||
sa.Column('display_name', sa.String(length=255), nullable=False),
|
|
||||||
sa.Column('deleted', sa.Boolean(), nullable=False),
|
|
||||||
sa.Column('keys', sa.PickleType(), nullable=False),
|
|
||||||
sa.PrimaryKeyConstraint('user_id', 'device_id')
|
|
||||||
)
|
|
||||||
op.create_table('nio_megolm_inbound_session',
|
|
||||||
sa.Column('session_id', sa.String(length=255), nullable=False),
|
|
||||||
sa.Column('sender_key', sa.String(length=255), nullable=False),
|
|
||||||
sa.Column('fp_key', sa.String(length=255), nullable=False),
|
|
||||||
sa.Column('room_id', sa.String(length=255), nullable=False),
|
|
||||||
sa.Column('session', sa.LargeBinary(), nullable=False),
|
|
||||||
sa.Column('forwarded_chains', sa.PickleType(), nullable=False),
|
|
||||||
sa.PrimaryKeyConstraint('session_id')
|
|
||||||
)
|
|
||||||
op.create_table('nio_olm_session',
|
|
||||||
sa.Column('session_id', sa.String(length=255), nullable=False),
|
|
||||||
sa.Column('sender_key', sa.String(length=255), nullable=False),
|
|
||||||
sa.Column('session', sa.LargeBinary(), nullable=False),
|
|
||||||
sa.Column('created_at', sa.DateTime(), nullable=False),
|
|
||||||
sa.Column('last_used', sa.DateTime(), nullable=False),
|
|
||||||
sa.PrimaryKeyConstraint('session_id')
|
|
||||||
)
|
|
||||||
op.create_table('nio_outgoing_key_request',
|
|
||||||
sa.Column('request_id', sa.String(length=255), nullable=False),
|
|
||||||
sa.Column('session_id', sa.String(length=255), nullable=False),
|
|
||||||
sa.Column('room_id', sa.String(length=255), nullable=False),
|
|
||||||
sa.Column('algorithm', sa.String(length=255), nullable=False),
|
|
||||||
sa.PrimaryKeyConstraint('request_id')
|
|
||||||
)
|
|
||||||
# ### end Alembic commands ###
|
|
||||||
|
|
||||||
|
|
||||||
def downgrade():
|
|
||||||
# ### commands auto generated by Alembic - please adjust! ###
|
|
||||||
op.drop_table('nio_outgoing_key_request')
|
|
||||||
op.drop_table('nio_olm_session')
|
|
||||||
op.drop_table('nio_megolm_inbound_session')
|
|
||||||
op.drop_table('nio_device_key')
|
|
||||||
op.drop_table('nio_account')
|
|
||||||
# ### end Alembic commands ###
|
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
"""Add telethon update state table
|
|
||||||
|
|
||||||
Revision ID: eeaf0dae87ce
|
|
||||||
Revises: 1fa46383a9d3
|
|
||||||
Create Date: 2018-04-30 17:30:59.610885
|
|
||||||
|
|
||||||
"""
|
|
||||||
from alembic import op
|
|
||||||
import sqlalchemy as sa
|
|
||||||
|
|
||||||
# revision identifiers, used by Alembic.
|
|
||||||
revision = 'eeaf0dae87ce'
|
|
||||||
down_revision = '1fa46383a9d3'
|
|
||||||
branch_labels = None
|
|
||||||
depends_on = None
|
|
||||||
|
|
||||||
|
|
||||||
def upgrade():
|
|
||||||
with op.batch_alter_table("telethon_entities") as batch_op:
|
|
||||||
batch_op.alter_column('phone', existing_type=sa.Integer, type_=sa.BigInteger)
|
|
||||||
op.create_table('telethon_update_state',
|
|
||||||
sa.Column('session_id', sa.String, nullable=False),
|
|
||||||
sa.Column('entity_id', sa.Integer, nullable=False),
|
|
||||||
sa.Column('pts', sa.Integer, nullable=True),
|
|
||||||
sa.Column('qts', sa.Integer, nullable=True),
|
|
||||||
sa.Column('date', sa.Integer, nullable=True),
|
|
||||||
sa.Column('seq', sa.Integer, nullable=True),
|
|
||||||
sa.PrimaryKeyConstraint('session_id', 'entity_id'))
|
|
||||||
|
|
||||||
|
|
||||||
def downgrade():
|
|
||||||
with op.batch_alter_table("telethon_entities") as batch_op:
|
|
||||||
batch_op.alter_column('phone', existing_type=sa.BigInteger, type_=sa.Integer)
|
|
||||||
op.drop_table('telethon_update_state')
|
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
BINARY_NAME=mautrix-telegram go tool maubuild "$@"
|
||||||
@@ -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"})
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
@@ -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()
|
||||||
|
}
|
||||||
+20
-23
@@ -1,40 +1,37 @@
|
|||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
|
|
||||||
# Define functions.
|
if [[ -z "$GID" ]]; then
|
||||||
function fixperms {
|
GID="$UID"
|
||||||
chown -R $UID:$GID /data /opt/mautrix-telegram
|
|
||||||
}
|
|
||||||
|
|
||||||
cd /opt/mautrix-telegram
|
|
||||||
|
|
||||||
# Replace database path in config.
|
|
||||||
sed -i "s#sqlite:///mautrix-telegram.db#sqlite:////data/mautrix-telegram.db#" /data/config.yaml
|
|
||||||
|
|
||||||
if [ -f /data/mx-state.json ]; then
|
|
||||||
ln -s /data/mx-state.json
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [ ! -f /data/config.yaml ]; then
|
BINARY_NAME=/usr/bin/mautrix-telegram
|
||||||
cp example-config.yaml /data/config.yaml
|
|
||||||
|
function fixperms {
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
if [[ ! -f /data/config.yaml ]]; then
|
||||||
|
$BINARY_NAME -c /data/config.yaml -e
|
||||||
echo "Didn't find a config file."
|
echo "Didn't find a config file."
|
||||||
echo "Copied default config file to /data/config.yaml"
|
echo "Copied default config file to /data/config.yaml"
|
||||||
echo "Modify that config file to your liking."
|
echo "Modify that config file to your liking."
|
||||||
echo "Start the container again after that to generate the registration file."
|
echo "Start the container again after that to generate the registration file."
|
||||||
fixperms
|
|
||||||
exit
|
exit
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [ ! -f /data/registration.yaml ]; then
|
if [[ ! -f /data/registration.yaml ]]; then
|
||||||
python3 -m mautrix_telegram -g -c /data/config.yaml -r /data/registration.yaml
|
$BINARY_NAME -g -c /data/config.yaml -r /data/registration.yaml
|
||||||
echo "Didn't find a registration file."
|
echo "Didn't find a registration file."
|
||||||
echo "Generated one for you."
|
echo "Generated one for you."
|
||||||
echo "Copy that over to synapses app service directory."
|
echo "See https://docs.mau.fi/bridges/general/registering-appservices.html on how to use it."
|
||||||
fixperms
|
|
||||||
exit
|
exit
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Check that database is in the right state
|
cd /data
|
||||||
alembic -x config=/data/config.yaml upgrade head
|
|
||||||
|
|
||||||
fixperms
|
fixperms
|
||||||
exec su-exec $UID:$GID python3 -m mautrix_telegram -c /data/config.yaml
|
exec su-exec $UID:$GID $BINARY_NAME
|
||||||
|
|||||||
@@ -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
|
||||||
|
)
|
||||||
@@ -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=
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
[*.{yaml,yml}]
|
|
||||||
indent_size = 2
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
charts/*
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
# Patterns to ignore when building packages.
|
|
||||||
# This supports shell glob matching, relative path matching, and
|
|
||||||
# negation (prefixed with !). Only one pattern per line.
|
|
||||||
.DS_Store
|
|
||||||
# Common VCS dirs
|
|
||||||
.git/
|
|
||||||
.gitignore
|
|
||||||
.bzr/
|
|
||||||
.bzrignore
|
|
||||||
.hg/
|
|
||||||
.hgignore
|
|
||||||
.svn/
|
|
||||||
# Common backup files
|
|
||||||
*.swp
|
|
||||||
*.bak
|
|
||||||
*.tmp
|
|
||||||
*~
|
|
||||||
# Various IDEs
|
|
||||||
.project
|
|
||||||
.idea/
|
|
||||||
*.tmproj
|
|
||||||
.vscode/
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
apiVersion: v1
|
|
||||||
name: mautrix-telegram
|
|
||||||
version: 0.1.0
|
|
||||||
appVersion: "0.7.0"
|
|
||||||
description: A Matrix-Telegram hybrid puppeting/relaybot bridge.
|
|
||||||
keywords:
|
|
||||||
- matrix
|
|
||||||
- bridge
|
|
||||||
- telegram
|
|
||||||
maintainers:
|
|
||||||
- name: Tulir Asokan
|
|
||||||
email: tulir@maunium.net
|
|
||||||
sources:
|
|
||||||
- https://github.com/tulir/mautrix-telegram
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
dependencies:
|
|
||||||
- name: postgresql
|
|
||||||
repository: https://kubernetes-charts.storage.googleapis.com/
|
|
||||||
version: 6.5.0
|
|
||||||
digest: sha256:85139e9d4207e49c11c5f84d7920d0135cffd3d427f3f3638d4e51258990de2a
|
|
||||||
generated: "2019-10-23T22:11:37.005827507+03:00"
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
dependencies:
|
|
||||||
- name: postgresql
|
|
||||||
version: 6.5.0
|
|
||||||
repository: https://kubernetes-charts.storage.googleapis.com/
|
|
||||||
condition: postgresql.enabled
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
Your registration file is below. Save it into a YAML file and give the path to that file to synapse:
|
|
||||||
|
|
||||||
id: {{ .Values.appservice.id }}
|
|
||||||
as_token: {{ .Values.appservice.asToken }}
|
|
||||||
hs_token: {{ .Values.appservice.hsToken }}
|
|
||||||
namespaces:
|
|
||||||
users:
|
|
||||||
- exclusive: true
|
|
||||||
regex: "@{{ .Values.bridge.username_template | replace "{userid}" ".+"}}:{{ .Values.homeserver.domain }}"
|
|
||||||
{{- if .Values.appservice.communityID }}
|
|
||||||
group_id: {{ .Values.appservice.communityID }}
|
|
||||||
{{- end }}
|
|
||||||
aliases:
|
|
||||||
- exclusive: true
|
|
||||||
regex: "@{{ .Values.bridge.alias_template | replace "{groupname}" ".+"}}:{{ .Values.homeserver.domain }}"
|
|
||||||
{{- if .Values.appservice.communityID }}
|
|
||||||
group_id: {{ .Values.appservice.communityID }}
|
|
||||||
{{- end }}
|
|
||||||
url: {{ .Values.appservice.address }}
|
|
||||||
sender_localpart: {{ .Values.appservice.botUsername }}
|
|
||||||
rate_limited: false
|
|
||||||
@@ -1,55 +0,0 @@
|
|||||||
{{/*
|
|
||||||
Expand the name of the chart.
|
|
||||||
*/}}
|
|
||||||
{{- define "mautrix-telegram.name" -}}
|
|
||||||
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" -}}
|
|
||||||
{{- end -}}
|
|
||||||
|
|
||||||
{{/*
|
|
||||||
Create a default fully qualified app name.
|
|
||||||
We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec).
|
|
||||||
If release name contains chart name it will be used as a full name.
|
|
||||||
*/}}
|
|
||||||
{{- define "mautrix-telegram.fullname" -}}
|
|
||||||
{{- if .Values.fullnameOverride -}}
|
|
||||||
{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" -}}
|
|
||||||
{{- else -}}
|
|
||||||
{{- $name := default .Chart.Name .Values.nameOverride -}}
|
|
||||||
{{- if contains $name .Release.Name -}}
|
|
||||||
{{- .Release.Name | trunc 63 | trimSuffix "-" -}}
|
|
||||||
{{- else -}}
|
|
||||||
{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" -}}
|
|
||||||
{{- end -}}
|
|
||||||
{{- end -}}
|
|
||||||
{{- end -}}
|
|
||||||
|
|
||||||
{{/*
|
|
||||||
Create chart name and version as used by the chart label.
|
|
||||||
*/}}
|
|
||||||
{{- define "mautrix-telegram.chart" -}}
|
|
||||||
{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" -}}
|
|
||||||
{{- end -}}
|
|
||||||
|
|
||||||
{{/*
|
|
||||||
Common labels
|
|
||||||
*/}}
|
|
||||||
{{- define "mautrix-telegram.labels" -}}
|
|
||||||
app.kubernetes.io/name: {{ include "mautrix-telegram.name" . }}
|
|
||||||
helm.sh/chart: {{ include "mautrix-telegram.chart" . }}
|
|
||||||
app.kubernetes.io/instance: {{ .Release.Name }}
|
|
||||||
{{- if .Chart.AppVersion }}
|
|
||||||
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
|
|
||||||
{{- end }}
|
|
||||||
app.kubernetes.io/managed-by: {{ .Release.Service }}
|
|
||||||
{{- end -}}
|
|
||||||
|
|
||||||
{{/*
|
|
||||||
Create the name of the service account to use
|
|
||||||
*/}}
|
|
||||||
{{- define "mautrix-telegram.serviceAccountName" -}}
|
|
||||||
{{- if .Values.serviceAccount.create -}}
|
|
||||||
{{ default (include "mautrix-telegram.fullname" .) .Values.serviceAccount.name }}
|
|
||||||
{{- else -}}
|
|
||||||
{{ default "default" .Values.serviceAccount.name }}
|
|
||||||
{{- end -}}
|
|
||||||
{{- end -}}
|
|
||||||
@@ -1,57 +0,0 @@
|
|||||||
apiVersion: v1
|
|
||||||
kind: ConfigMap
|
|
||||||
metadata:
|
|
||||||
name: {{ template "mautrix-telegram.fullname" . }}
|
|
||||||
labels:
|
|
||||||
app.kubernetes.io/managed-by: {{ .Release.Service }}
|
|
||||||
app.kubernetes.io/instance: {{ .Release.Name }}
|
|
||||||
helm.sh/chart: {{ .Chart.Name }}-{{ .Chart.Version }}
|
|
||||||
app.kubernetes.io/name: {{ template "mautrix-telegram.name" . }}
|
|
||||||
data:
|
|
||||||
config.yaml: |
|
|
||||||
homeserver:
|
|
||||||
address: {{ .Values.homeserver.address }}
|
|
||||||
domain: {{ .Values.homeserver.domain }}
|
|
||||||
verify_ssl: {{ .Values.homeserver.verifySSL }}
|
|
||||||
|
|
||||||
appservice:
|
|
||||||
address: http://{{ include "mautrix-telegram.fullname" . }}:{{ .Values.service.port }}
|
|
||||||
|
|
||||||
hostname: 0.0.0.0
|
|
||||||
port: {{ .Values.service.port }}
|
|
||||||
max_body_size: {{ .Values.appservice.maxBodySize }}
|
|
||||||
|
|
||||||
{{- if .Values.postgresql.enabled }}
|
|
||||||
database: "postgres://postgres:{{ .Values.postgresql.postgresqlPassword }}@{{ .Release.Name }}-postgresql/{{ .Values.postgresql.postgresqlDatabase }}"
|
|
||||||
{{- else }}
|
|
||||||
database: {{ .Values.appservice.database | quote }}
|
|
||||||
{{- end }}
|
|
||||||
|
|
||||||
public:
|
|
||||||
{{- toYaml .Values.appservice.public | nindent 8 }}
|
|
||||||
|
|
||||||
provisioning:
|
|
||||||
{{- toYaml .Values.appservice.provisioning | nindent 8 }}
|
|
||||||
|
|
||||||
id: {{ .Values.appservice.id }}
|
|
||||||
bot_username: {{ .Values.appservice.botUsername }}
|
|
||||||
bot_displayname: {{ .Values.appservice.botDisplayname }}
|
|
||||||
bot_avatar: {{ .Values.appservice.botAvatar }}
|
|
||||||
|
|
||||||
community_id: {{ .Values.appservice.communityID }}
|
|
||||||
|
|
||||||
as_token: {{ .Values.appservice.asToken }}
|
|
||||||
hs_token: {{ .Values.appservice.hsToken }}
|
|
||||||
|
|
||||||
metrics:
|
|
||||||
{{- toYaml .Values.metrics | nindent 6 }}
|
|
||||||
|
|
||||||
bridge:
|
|
||||||
{{- toYaml .Values.bridge | nindent 6 }}
|
|
||||||
|
|
||||||
telegram:
|
|
||||||
{{- toYaml .Values.telegram | nindent 6 }}
|
|
||||||
|
|
||||||
logging:
|
|
||||||
{{- toYaml .Values.logging | nindent 6 }}
|
|
||||||
registration.yaml: ""
|
|
||||||
@@ -1,69 +0,0 @@
|
|||||||
apiVersion: apps/v1
|
|
||||||
kind: Deployment
|
|
||||||
metadata:
|
|
||||||
name: {{ include "mautrix-telegram.fullname" . }}
|
|
||||||
labels:
|
|
||||||
{{- include "mautrix-telegram.labels" . | nindent 4 }}
|
|
||||||
spec:
|
|
||||||
replicas: 1
|
|
||||||
selector:
|
|
||||||
matchLabels:
|
|
||||||
app.kubernetes.io/name: {{ include "mautrix-telegram.name" . }}
|
|
||||||
app.kubernetes.io/instance: {{ .Release.Name }}
|
|
||||||
template:
|
|
||||||
{{- if .Values.podAnnotations }}
|
|
||||||
annotations:
|
|
||||||
{{- toYaml .Values.podAnnotations | nindent 6 }}
|
|
||||||
{{- end }}
|
|
||||||
metadata:
|
|
||||||
labels:
|
|
||||||
app.kubernetes.io/name: {{ include "mautrix-telegram.name" . }}
|
|
||||||
app.kubernetes.io/instance: {{ .Release.Name }}
|
|
||||||
spec:
|
|
||||||
serviceAccountName: {{ template "mautrix-telegram.serviceAccountName" . }}
|
|
||||||
containers:
|
|
||||||
- name: {{ .Chart.Name }}
|
|
||||||
securityContext:
|
|
||||||
{{- toYaml .Values.securityContext | nindent 12 }}
|
|
||||||
image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
|
|
||||||
imagePullPolicy: {{ .Values.image.pullPolicy }}
|
|
||||||
volumeMounts:
|
|
||||||
- mountPath: /data
|
|
||||||
name: config-volume
|
|
||||||
ports:
|
|
||||||
- name: http
|
|
||||||
containerPort: {{ .Values.service.port }}
|
|
||||||
protocol: TCP
|
|
||||||
livenessProbe:
|
|
||||||
httpGet:
|
|
||||||
path: /_matrix/mau/live
|
|
||||||
port: http
|
|
||||||
initialDelaySeconds: 60
|
|
||||||
periodSeconds: 5
|
|
||||||
readinessProbe:
|
|
||||||
httpGet:
|
|
||||||
path: /_matrix/mau/ready
|
|
||||||
port: http
|
|
||||||
initialDelaySeconds: 60
|
|
||||||
periodSeconds: 5
|
|
||||||
resources:
|
|
||||||
{{- toYaml .Values.resources | nindent 12 }}
|
|
||||||
volumes:
|
|
||||||
- name: config-volume
|
|
||||||
configMap:
|
|
||||||
name: {{ template "mautrix-telegram.fullname" . }}
|
|
||||||
|
|
||||||
securityContext:
|
|
||||||
{{- toYaml .Values.podSecurityContext | nindent 8 }}
|
|
||||||
{{- with .Values.nodeSelector }}
|
|
||||||
nodeSelector:
|
|
||||||
{{- toYaml . | nindent 8 }}
|
|
||||||
{{- end }}
|
|
||||||
{{- with .Values.affinity }}
|
|
||||||
affinity:
|
|
||||||
{{- toYaml . | nindent 8 }}
|
|
||||||
{{- end }}
|
|
||||||
{{- with .Values.tolerations }}
|
|
||||||
tolerations:
|
|
||||||
{{- toYaml . | nindent 8 }}
|
|
||||||
{{- end }}
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
apiVersion: v1
|
|
||||||
kind: Service
|
|
||||||
metadata:
|
|
||||||
name: {{ include "mautrix-telegram.fullname" . }}
|
|
||||||
labels:
|
|
||||||
{{ include "mautrix-telegram.labels" . | indent 4 }}
|
|
||||||
spec:
|
|
||||||
type: {{ .Values.service.type }}
|
|
||||||
ports:
|
|
||||||
- port: {{ .Values.service.port }}
|
|
||||||
targetPort: http
|
|
||||||
protocol: TCP
|
|
||||||
name: http
|
|
||||||
selector:
|
|
||||||
app.kubernetes.io/name: {{ include "mautrix-telegram.name" . }}
|
|
||||||
app.kubernetes.io/instance: {{ .Release.Name }}
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
{{- if .Values.serviceAccount.create -}}
|
|
||||||
apiVersion: v1
|
|
||||||
kind: ServiceAccount
|
|
||||||
metadata:
|
|
||||||
name: {{ template "mautrix-telegram.serviceAccountName" . }}
|
|
||||||
labels:
|
|
||||||
{{ include "mautrix-telegram.labels" . | indent 4 }}
|
|
||||||
{{- end -}}
|
|
||||||
@@ -1,141 +0,0 @@
|
|||||||
image:
|
|
||||||
repository: dock.mau.dev/tulir/mautrix-telegram
|
|
||||||
tag: latest
|
|
||||||
pullPolicy: IfNotPresent
|
|
||||||
|
|
||||||
nameOverride: ""
|
|
||||||
fullnameOverride: ""
|
|
||||||
|
|
||||||
serviceAccount:
|
|
||||||
# Specifies whether a service account should be created
|
|
||||||
create: true
|
|
||||||
# The name of the service account to use.
|
|
||||||
# If not set and create is true, a name is generated using the fullname template
|
|
||||||
name:
|
|
||||||
|
|
||||||
service:
|
|
||||||
type: ClusterIP
|
|
||||||
port: 29317
|
|
||||||
|
|
||||||
resources: {}
|
|
||||||
# limits:
|
|
||||||
# cpu: 100m
|
|
||||||
# memory: 128Mi
|
|
||||||
# requests:
|
|
||||||
# cpu: 100m
|
|
||||||
# memory: 128Mi
|
|
||||||
|
|
||||||
nodeSelector: {}
|
|
||||||
|
|
||||||
tolerations: []
|
|
||||||
|
|
||||||
affinity: {}
|
|
||||||
|
|
||||||
# Postgres pod configs
|
|
||||||
postgresql:
|
|
||||||
enabled: true
|
|
||||||
postgresqlDatabase: mxtg
|
|
||||||
postgresqlPassword: SET TO RANDOM STRING
|
|
||||||
persistence:
|
|
||||||
size: 2Gi
|
|
||||||
resources:
|
|
||||||
requests:
|
|
||||||
memory: 256Mi
|
|
||||||
cpu: 100m
|
|
||||||
|
|
||||||
# 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://
|
|
||||||
verifySSL: true
|
|
||||||
|
|
||||||
# Application service host/registration related details
|
|
||||||
# Changing these values requires regeneration of the registration.
|
|
||||||
appservice:
|
|
||||||
# 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
|
|
||||||
maxBodySize: 1
|
|
||||||
|
|
||||||
# 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: true
|
|
||||||
# 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 Dimension (https://dimension.t2bot.io/).
|
|
||||||
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.
|
|
||||||
shared_secret: SET TO RANDOM STRING
|
|
||||||
|
|
||||||
id: telegram
|
|
||||||
botUsername: telegrambot
|
|
||||||
# Display name and avatar for bot. Set to "remove" to remove display name/avatar, leave empty
|
|
||||||
# to leave display name/avatar as-is.
|
|
||||||
botDisplayname: Telegram bridge bot
|
|
||||||
botAvatar: mxc://maunium.net/tJCRmUyJDsgRNgqhOgoiHWbX
|
|
||||||
|
|
||||||
# Community ID for bridged users (changes registration file) and rooms.
|
|
||||||
# Must be created manually.
|
|
||||||
communityID: false
|
|
||||||
|
|
||||||
# Authentication tokens for AS <-> HS communication. Autogenerated; do not modify.
|
|
||||||
asToken: SET TO RANDOM STRING
|
|
||||||
hsToken: SET TO RANDOM STRING
|
|
||||||
|
|
||||||
# The keys below can be used to override the configs in the base config:
|
|
||||||
# https://github.com/tulir/mautrix-telegram/blob/master/example-config.yaml
|
|
||||||
# Note that the "appservice" and "homeserver" sections are above and slightly different than the base.
|
|
||||||
|
|
||||||
# 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}"
|
|
||||||
|
|
||||||
# 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"
|
|
||||||
|
|
||||||
# Prometheus telemetry config.
|
|
||||||
metrics:
|
|
||||||
enabled: false
|
|
||||||
listen_port: 8000
|
|
||||||
|
|
||||||
# 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: 123456789:
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
__version__ = "0.9.0rc2"
|
|
||||||
__author__ = "Tulir Asokan <tulir@maunium.net>"
|
|
||||||
@@ -1,133 +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 typing import Optional
|
|
||||||
|
|
||||||
from alchemysession import AlchemySessionContainer
|
|
||||||
|
|
||||||
from mautrix.types import UserID, RoomID
|
|
||||||
from mautrix.bridge import Bridge
|
|
||||||
from mautrix.util.db import Base
|
|
||||||
|
|
||||||
from .web.provisioning import ProvisioningAPI
|
|
||||||
from .web.public import PublicBridgeWebsite
|
|
||||||
from .commands.manhole import ManholeState
|
|
||||||
from .abstract_user import init as init_abstract_user
|
|
||||||
from .bot import Bot, init as init_bot
|
|
||||||
from .config import Config
|
|
||||||
from .context import Context
|
|
||||||
from .db import init as init_db
|
|
||||||
from .formatter import init as init_formatter
|
|
||||||
from .matrix import MatrixHandler
|
|
||||||
from .portal import Portal, init as init_portal
|
|
||||||
from .puppet import Puppet, init as init_puppet
|
|
||||||
from .user import User, init as init_user
|
|
||||||
from .version import version, linkified_version
|
|
||||||
|
|
||||||
try:
|
|
||||||
import prometheus_client as prometheus
|
|
||||||
except ImportError:
|
|
||||||
prometheus = None
|
|
||||||
|
|
||||||
|
|
||||||
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/tulir/mautrix-telegram"
|
|
||||||
real_user_content_key = "net.maunium.telegram.puppet"
|
|
||||||
version = version
|
|
||||||
markdown_version = linkified_version
|
|
||||||
config_class = Config
|
|
||||||
matrix_class = MatrixHandler
|
|
||||||
|
|
||||||
config: Config
|
|
||||||
session_container: AlchemySessionContainer
|
|
||||||
bot: Bot
|
|
||||||
manhole: Optional[ManholeState]
|
|
||||||
|
|
||||||
def prepare_db(self) -> None:
|
|
||||||
super().prepare_db()
|
|
||||||
init_db(self.db)
|
|
||||||
self.session_container = AlchemySessionContainer(
|
|
||||||
engine=self.db, table_base=Base, session=False,
|
|
||||||
table_prefix="telethon_", manage_tables=False)
|
|
||||||
|
|
||||||
def _prepare_website(self, context: Context) -> None:
|
|
||||||
if self.config["appservice.public.enabled"]:
|
|
||||||
public_website = PublicBridgeWebsite(self.loop)
|
|
||||||
self.az.app.add_subapp(self.config["appservice.public.prefix"], public_website.app)
|
|
||||||
context.public_website = public_website
|
|
||||||
|
|
||||||
if self.config["appservice.provisioning.enabled"]:
|
|
||||||
provisioning_api = ProvisioningAPI(context)
|
|
||||||
self.az.app.add_subapp(self.config["appservice.provisioning.prefix"],
|
|
||||||
provisioning_api.app)
|
|
||||||
context.provisioning_api = provisioning_api
|
|
||||||
|
|
||||||
def prepare_bridge(self) -> None:
|
|
||||||
self.bot = init_bot(self.config)
|
|
||||||
context = Context(self.az, self.config, self.loop, self.session_container, self, self.bot)
|
|
||||||
self._prepare_website(context)
|
|
||||||
self.matrix = context.mx = MatrixHandler(context)
|
|
||||||
self.manhole = None
|
|
||||||
|
|
||||||
init_abstract_user(context)
|
|
||||||
init_formatter(context)
|
|
||||||
init_portal(context)
|
|
||||||
self.add_startup_actions(init_puppet(context))
|
|
||||||
self.add_startup_actions(init_user(context))
|
|
||||||
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")
|
|
||||||
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())
|
|
||||||
if self.manhole:
|
|
||||||
self.manhole.close()
|
|
||||||
self.manhole = None
|
|
||||||
|
|
||||||
async def get_user(self, user_id: UserID, create: bool = True) -> User:
|
|
||||||
user = 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:
|
|
||||||
return Portal.get_by_mxid(room_id)
|
|
||||||
|
|
||||||
async def get_puppet(self, user_id: UserID, create: bool = False) -> Puppet:
|
|
||||||
return await Puppet.get_by_mxid(user_id, create=create)
|
|
||||||
|
|
||||||
async def get_double_puppet(self, user_id: UserID) -> Puppet:
|
|
||||||
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))
|
|
||||||
|
|
||||||
|
|
||||||
TelegramBridge().run()
|
|
||||||
@@ -1,486 +0,0 @@
|
|||||||
# mautrix-telegram - A Matrix-Telegram puppeting bridge
|
|
||||||
# Copyright (C) 2020 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 Tuple, Optional, Union, Dict, Type, Any, TYPE_CHECKING
|
|
||||||
from abc import ABC, abstractmethod
|
|
||||||
import asyncio
|
|
||||||
import logging
|
|
||||||
import platform
|
|
||||||
import time
|
|
||||||
|
|
||||||
from telethon.sessions import Session
|
|
||||||
from telethon.network import (ConnectionTcpMTProxyRandomizedIntermediate, ConnectionTcpFull,
|
|
||||||
Connection)
|
|
||||||
from telethon.tl.patched import MessageService, Message
|
|
||||||
from telethon.tl.types import (
|
|
||||||
Channel, Chat, MessageActionChannelMigrateFrom, PeerUser, TypeUpdate, UpdateChatPinnedMessage,
|
|
||||||
UpdateChannelPinnedMessage, UpdateChatParticipantAdmin, UpdateChatParticipants, PeerChat,
|
|
||||||
UpdateChatUserTyping, UpdateDeleteChannelMessages, UpdateNewMessage, UpdateDeleteMessages,
|
|
||||||
UpdateEditChannelMessage, UpdateEditMessage, UpdateNewChannelMessage, UpdateReadHistoryOutbox,
|
|
||||||
UpdateShortChatMessage, UpdateShortMessage, UpdateUserName, UpdateUserPhoto, UpdateUserStatus,
|
|
||||||
UpdateUserTyping, User, UserStatusOffline, UserStatusOnline, UpdateReadHistoryInbox,
|
|
||||||
UpdateReadChannelInbox, MessageEmpty)
|
|
||||||
|
|
||||||
from mautrix.types import UserID, PresenceState
|
|
||||||
from mautrix.errors import MatrixError
|
|
||||||
from mautrix.appservice import AppService
|
|
||||||
from mautrix.util.logging import TraceLogger
|
|
||||||
from mautrix.util.opt_prometheus import Histogram, Counter
|
|
||||||
from alchemysession import AlchemySessionContainer
|
|
||||||
|
|
||||||
from . import portal as po, puppet as pu, __version__
|
|
||||||
from .db import Message as DBMessage
|
|
||||||
from .types import TelegramID
|
|
||||||
from .tgclient import MautrixTelegramClient
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
|
||||||
from .context import Context
|
|
||||||
from .config import Config
|
|
||||||
from .bot import Bot
|
|
||||||
|
|
||||||
config: Optional['Config'] = None
|
|
||||||
# Value updated from config in init()
|
|
||||||
MAX_DELETIONS: int = 10
|
|
||||||
|
|
||||||
UpdateMessage = Union[UpdateShortChatMessage, UpdateShortMessage, UpdateNewChannelMessage,
|
|
||||||
UpdateNewMessage, UpdateEditMessage, UpdateEditChannelMessage]
|
|
||||||
UpdateMessageContent = Union[UpdateShortMessage, UpdateShortChatMessage, Message, MessageService]
|
|
||||||
|
|
||||||
UPDATE_TIME = Histogram("bridge_telegram_update", "Time spent processing Telegram updates",
|
|
||||||
("update_type",))
|
|
||||||
UPDATE_ERRORS = Counter("bridge_telegram_update_error",
|
|
||||||
"Number of fatal errors while handling Telegram updates", ("update_type",))
|
|
||||||
|
|
||||||
|
|
||||||
class AbstractUser(ABC):
|
|
||||||
session_container: AlchemySessionContainer = None
|
|
||||||
loop: asyncio.AbstractEventLoop = None
|
|
||||||
log: TraceLogger
|
|
||||||
az: AppService
|
|
||||||
relaybot: Optional['Bot']
|
|
||||||
ignore_incoming_bot_events: bool = True
|
|
||||||
|
|
||||||
client: Optional[MautrixTelegramClient]
|
|
||||||
mxid: Optional[UserID]
|
|
||||||
|
|
||||||
tgid: Optional[TelegramID]
|
|
||||||
username: Optional['str']
|
|
||||||
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], Optional[Tuple[Any, ...]]]:
|
|
||||||
proxy_type = config["telegram.proxy.type"].lower()
|
|
||||||
connection = ConnectionTcpFull
|
|
||||||
connection_data = (config["telegram.proxy.address"],
|
|
||||||
config["telegram.proxy.port"],
|
|
||||||
config["telegram.proxy.rdns"],
|
|
||||||
config["telegram.proxy.username"],
|
|
||||||
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
|
|
||||||
|
|
||||||
def _init_client(self) -> None:
|
|
||||||
self.log.debug(f"Initializing client for {self.name}")
|
|
||||||
|
|
||||||
self.session = self.session_container.new_session(self.name)
|
|
||||||
if config["telegram.server.enabled"]:
|
|
||||||
self.session.set_dc(config["telegram.server.dc"],
|
|
||||||
config["telegram.server.ip"],
|
|
||||||
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 = config["telegram.device_info.device_model"]
|
|
||||||
sysversion = config["telegram.device_info.system_version"]
|
|
||||||
appversion = config["telegram.device_info.app_version"]
|
|
||||||
connection, proxy = self._proxy_settings
|
|
||||||
|
|
||||||
assert isinstance(self.session, Session)
|
|
||||||
|
|
||||||
self.client = MautrixTelegramClient(
|
|
||||||
session=self.session,
|
|
||||||
|
|
||||||
api_id=config["telegram.api_id"],
|
|
||||||
api_hash=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=config["telegram.connection.timeout"],
|
|
||||||
connection_retries=config["telegram.connection.retries"],
|
|
||||||
retry_delay=config["telegram.connection.retry_delay"],
|
|
||||||
flood_sleep_threshold=config["telegram.connection.flood_sleep_threshold"],
|
|
||||||
request_retries=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(f"Failed to handle Telegram update {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:
|
|
||||||
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 self.session_container.has_session(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.ensure_future(self._handle_entity_updates(getattr(update, "_entities", {})),
|
|
||||||
loop=self.loop)
|
|
||||||
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, 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, (UpdateChannelPinnedMessage, UpdateChatPinnedMessage)):
|
|
||||||
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)
|
|
||||||
else:
|
|
||||||
self.log.trace("Unhandled update: %s", update)
|
|
||||||
|
|
||||||
async def update_pinned_messages(self, update: Union[UpdateChannelPinnedMessage,
|
|
||||||
UpdateChatPinnedMessage]) -> None:
|
|
||||||
if isinstance(update, UpdateChatPinnedMessage):
|
|
||||||
portal = po.Portal.get_by_tgid(TelegramID(update.chat_id))
|
|
||||||
else:
|
|
||||||
portal = po.Portal.get_by_tgid(TelegramID(update.channel_id))
|
|
||||||
if portal and portal.mxid:
|
|
||||||
await portal.receive_telegram_pin_id(update.id, self.tgid)
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
async def update_participants(update: UpdateChatParticipants) -> None:
|
|
||||||
portal = 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 = po.Portal.get_by_tgid(TelegramID(update.peer.user_id), 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 = DBMessage.get_one_by_tgid(TelegramID(update.max_id), self.tgid, edit_index=-1)
|
|
||||||
if not message:
|
|
||||||
return
|
|
||||||
|
|
||||||
puppet = pu.Puppet.get(TelegramID(update.peer.user_id))
|
|
||||||
await puppet.intent.mark_read(portal.mxid, message.mxid)
|
|
||||||
|
|
||||||
async def update_own_read_receipt(self, update: Union[UpdateReadHistoryInbox,
|
|
||||||
UpdateReadChannelInbox]) -> None:
|
|
||||||
puppet = pu.Puppet.get(self.tgid)
|
|
||||||
if not puppet.is_real_user:
|
|
||||||
return
|
|
||||||
|
|
||||||
if isinstance(update, UpdateReadChannelInbox):
|
|
||||||
portal = po.Portal.get_by_tgid(TelegramID(update.channel_id))
|
|
||||||
elif isinstance(update.peer, PeerChat):
|
|
||||||
portal = po.Portal.get_by_tgid(TelegramID(update.peer.chat_id))
|
|
||||||
elif isinstance(update.peer, PeerUser):
|
|
||||||
portal = po.Portal.get_by_tgid(TelegramID(update.peer.user_id), 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 = 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 = 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: Union[UpdateUserTyping, UpdateChatUserTyping]) -> None:
|
|
||||||
if isinstance(update, UpdateUserTyping):
|
|
||||||
portal = po.Portal.get_by_tgid(TelegramID(update.user_id), self.tgid, "user")
|
|
||||||
else:
|
|
||||||
portal = po.Portal.get_by_tgid(TelegramID(update.chat_id))
|
|
||||||
|
|
||||||
if not portal or not portal.mxid:
|
|
||||||
return
|
|
||||||
|
|
||||||
sender = pu.Puppet.get(TelegramID(update.user_id))
|
|
||||||
await portal.handle_telegram_typing(sender, update)
|
|
||||||
|
|
||||||
async def _handle_entity_updates(self, entities: Dict[int, Union[User, Chat, Channel]]
|
|
||||||
) -> None:
|
|
||||||
try:
|
|
||||||
users = (entity for entity in entities.values() if isinstance(entity, User))
|
|
||||||
puppets = ((pu.Puppet.get(TelegramID(user.id)), user) for user in users)
|
|
||||||
await asyncio.gather(*[puppet.try_update_info(self, info)
|
|
||||||
for puppet, info in puppets if puppet])
|
|
||||||
except Exception:
|
|
||||||
self.log.exception("Failed to handle entity updates")
|
|
||||||
|
|
||||||
async def update_others_info(self, update: Union[UpdateUserName, UpdateUserPhoto]) -> None:
|
|
||||||
# TODO duplication not checked
|
|
||||||
puppet = pu.Puppet.get(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 = pu.Puppet.get(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
|
|
||||||
|
|
||||||
def get_message_details(self, update: UpdateMessage) -> Tuple[UpdateMessageContent,
|
|
||||||
Optional[pu.Puppet],
|
|
||||||
Optional[po.Portal]]:
|
|
||||||
if isinstance(update, UpdateShortChatMessage):
|
|
||||||
portal = 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 = pu.Puppet.get(TelegramID(update.from_id))
|
|
||||||
elif isinstance(update, UpdateShortMessage):
|
|
||||||
portal = po.Portal.get_by_tgid(TelegramID(update.user_id), self.tgid, "user")
|
|
||||||
sender = pu.Puppet.get(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 = po.Portal.get_by_entity(update.peer_id, receiver_id=self.tgid)
|
|
||||||
if update.out:
|
|
||||||
sender = pu.Puppet.get(self.tgid)
|
|
||||||
elif isinstance(update.from_id, PeerUser):
|
|
||||||
sender = pu.Puppet.get(TelegramID(update.from_id.user_id))
|
|
||||||
else:
|
|
||||||
sender = None
|
|
||||||
else:
|
|
||||||
self.log.warning("Unexpected message type in User#get_message_details: "
|
|
||||||
f"{type(update)}")
|
|
||||||
return update, None, None
|
|
||||||
return update, sender, portal
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
async def _try_redact(message: DBMessage) -> None:
|
|
||||||
portal = 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) > MAX_DELETIONS:
|
|
||||||
return
|
|
||||||
|
|
||||||
for message_id in update.messages:
|
|
||||||
for message in DBMessage.get_all_by_tgid(TelegramID(message_id), self.tgid):
|
|
||||||
message.delete()
|
|
||||||
number_left = 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) > MAX_DELETIONS:
|
|
||||||
return
|
|
||||||
|
|
||||||
channel_id = TelegramID(update.channel_id)
|
|
||||||
|
|
||||||
for message_id in update.messages:
|
|
||||||
for message in DBMessage.get_all_by_tgid(TelegramID(message_id), channel_id):
|
|
||||||
message.delete()
|
|
||||||
await self._try_redact(message)
|
|
||||||
|
|
||||||
async def update_message(self, original_update: UpdateMessage) -> None:
|
|
||||||
update, sender, portal = 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 config["bridge.relaybot.private_chat.invite"]:
|
|
||||||
self.log.debug(f"Ignoring private message to bot from {sender.id}")
|
|
||||||
return
|
|
||||||
elif not portal.mxid and config["bridge.relaybot.ignore_unbridged_group_chat"]:
|
|
||||||
self.log.debug("Ignoring message received by bot"
|
|
||||||
f" 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(f"Ignoring relaybot-sent message %s to %s", update.id, portal.tgid_log)
|
|
||||||
return
|
|
||||||
|
|
||||||
await portal.backfill_lock.wait(update.id)
|
|
||||||
|
|
||||||
if isinstance(update, MessageService):
|
|
||||||
if isinstance(update.action, MessageActionChannelMigrateFrom):
|
|
||||||
self.log.trace(f"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)
|
|
||||||
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
|
|
||||||
|
|
||||||
|
|
||||||
def init(context: 'Context') -> None:
|
|
||||||
global config, MAX_DELETIONS
|
|
||||||
AbstractUser.az, config, AbstractUser.loop, AbstractUser.relaybot = context.core
|
|
||||||
AbstractUser.ignore_incoming_bot_events = config["bridge.relaybot.ignore_own_incoming_events"]
|
|
||||||
AbstractUser.session_container = context.session_container
|
|
||||||
MAX_DELETIONS = config.get("bridge.max_telegram_delete", 10)
|
|
||||||
@@ -1,311 +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 typing import Awaitable, Callable, Dict, List, Optional, Tuple, TYPE_CHECKING
|
|
||||||
import logging
|
|
||||||
|
|
||||||
from telethon.tl.patched import Message, MessageService
|
|
||||||
from telethon.tl.types import (
|
|
||||||
ChannelParticipantAdmin, ChannelParticipantCreator, ChatForbidden, ChatParticipantAdmin,
|
|
||||||
ChatParticipantCreator, InputChannel, InputUser, MessageActionChatAddUser, PeerUser,
|
|
||||||
MessageActionChatDeleteUser, MessageEntityBotCommand, PeerChannel, PeerChat, TypePeer,
|
|
||||||
UpdateNewChannelMessage, UpdateNewMessage, MessageActionChatMigrateTo, User)
|
|
||||||
from telethon.tl.functions.messages import GetChatsRequest, GetFullChatRequest
|
|
||||||
from telethon.tl.functions.channels import GetChannelsRequest, GetParticipantRequest
|
|
||||||
from telethon.errors import ChannelInvalidError, ChannelPrivateError
|
|
||||||
|
|
||||||
from mautrix.types import UserID
|
|
||||||
|
|
||||||
from .abstract_user import AbstractUser
|
|
||||||
from .db import BotChat
|
|
||||||
from .types import TelegramID
|
|
||||||
from . import puppet as pu, portal as po, user as u
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
|
||||||
from .config import Config
|
|
||||||
|
|
||||||
config: Optional['Config'] = None
|
|
||||||
|
|
||||||
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.username = None
|
|
||||||
self.is_relaybot = True
|
|
||||||
self.is_bot = True
|
|
||||||
self.chats = {}
|
|
||||||
self.tg_whitelist = []
|
|
||||||
self.whitelist_group_admins = (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 = 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 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.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:
|
|
||||||
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):
|
|
||||||
self.remove_chat(TelegramID(channel_id.channel_id))
|
|
||||||
|
|
||||||
async def register_portal(self, portal: po.Portal) -> None:
|
|
||||||
self.add_chat(portal.tgid, portal.peer_type)
|
|
||||||
|
|
||||||
async def unregister_portal(self, tgid: int, tg_receiver: int) -> None:
|
|
||||||
self.remove_chat(tgid)
|
|
||||||
|
|
||||||
def add_chat(self, chat_id: TelegramID, chat_type: str) -> None:
|
|
||||||
if chat_id not in self.chats:
|
|
||||||
self.chats[chat_id] = chat_type
|
|
||||||
BotChat(id=TelegramID(chat_id), type=chat_type).insert()
|
|
||||||
|
|
||||||
def remove_chat(self, chat_id: TelegramID) -> None:
|
|
||||||
try:
|
|
||||||
del self.chats[chat_id]
|
|
||||||
except KeyError:
|
|
||||||
pass
|
|
||||||
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 = 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:
|
|
||||||
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 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_by_mxid(mxid_input).ensure_started()
|
|
||||||
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.username}" if user.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.main_intent.invite_user(portal.mxid, 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.from_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.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 = 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 = 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))
|
|
||||||
|
|
||||||
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:
|
|
||||||
self.add_chat(to_id, chat_type)
|
|
||||||
elif isinstance(action, MessageActionChatDeleteUser) and action.user_id == self.tgid:
|
|
||||||
self.remove_chat(to_id)
|
|
||||||
elif isinstance(action, MessageActionChatMigrateTo):
|
|
||||||
self.remove_chat(to_id)
|
|
||||||
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):
|
|
||||||
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"
|
|
||||||
|
|
||||||
|
|
||||||
def init(cfg: 'Config') -> Optional[Bot]:
|
|
||||||
global config
|
|
||||||
config = cfg
|
|
||||||
token = config["telegram.bot_token"]
|
|
||||||
if token and not token.lower().startswith("disable"):
|
|
||||||
return Bot(token)
|
|
||||||
return None
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
from .handler import (command_handler, CommandHandler, CommandProcessor, CommandEvent,
|
|
||||||
SECTION_AUTH, SECTION_CREATING_PORTALS, SECTION_PORTAL_MANAGEMENT,
|
|
||||||
SECTION_MISC, SECTION_ADMIN)
|
|
||||||
from . import portal, telegram, matrix_auth, manhole
|
|
||||||
|
|
||||||
__all__ = ["command_handler", "CommandHandler", "CommandProcessor", "CommandEvent",
|
|
||||||
"SECTION_AUTH", "SECTION_MISC", "SECTION_ADMIN", "SECTION_CREATING_PORTALS",
|
|
||||||
"SECTION_PORTAL_MANAGEMENT"]
|
|
||||||
@@ -1,123 +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/>.
|
|
||||||
"""This module contains classes handling commands issued by Matrix users."""
|
|
||||||
from typing import Awaitable, Callable, List, Optional, NamedTuple, Any
|
|
||||||
|
|
||||||
from telethon.errors import FloodWaitError
|
|
||||||
|
|
||||||
from mautrix.types import RoomID, EventID, MessageEventContent
|
|
||||||
from mautrix.bridge.commands import (HelpSection, CommandEvent as BaseCommandEvent,
|
|
||||||
CommandHandler as BaseCommandHandler,
|
|
||||||
CommandProcessor as BaseCommandProcessor,
|
|
||||||
CommandHandlerFunc, command_handler as base_command_handler)
|
|
||||||
|
|
||||||
from ..util import format_duration
|
|
||||||
from .. import user as u, context as c, portal as po
|
|
||||||
|
|
||||||
|
|
||||||
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: Optional['po.Portal'], 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) -> 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)
|
|
||||||
|
|
||||||
async def get_permission_error(self, evt: CommandEvent) -> Optional[str]:
|
|
||||||
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: Optional[CommandHandlerFunc] = None, *, needs_auth: bool = True,
|
|
||||||
needs_puppeting: bool = True, needs_matrix_puppeting: bool = False,
|
|
||||||
needs_admin: bool = False, management_only: bool = False,
|
|
||||||
name: Optional[str] = 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, context: c.Context) -> None:
|
|
||||||
super().__init__(event_class=CommandEvent, bridge=context.bridge)
|
|
||||||
self.tgbot = context.bot
|
|
||||||
self.public_website = context.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)}")
|
|
||||||
@@ -1,128 +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 typing import Set, Callable
|
|
||||||
import asyncio
|
|
||||||
import sys
|
|
||||||
import os
|
|
||||||
|
|
||||||
from attr import dataclass
|
|
||||||
|
|
||||||
from telethon import __version__ as __telethon_version__
|
|
||||||
|
|
||||||
from mautrix import __version__ as __mautrix_version__
|
|
||||||
from mautrix.types import UserID
|
|
||||||
from mautrix.errors import MatrixConnectionError
|
|
||||||
from mautrix.util.manhole import start_manhole
|
|
||||||
|
|
||||||
from .. import __version__
|
|
||||||
from . import command_handler, CommandEvent, SECTION_ADMIN
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class ManholeState:
|
|
||||||
server: asyncio.AbstractServer
|
|
||||||
opened_by: UserID
|
|
||||||
close: Callable[[], None]
|
|
||||||
whitelist: Set[int]
|
|
||||||
|
|
||||||
|
|
||||||
@command_handler(needs_auth=False, needs_admin=True, help_section=SECTION_ADMIN,
|
|
||||||
help_text="Open a manhole into the bridge.", help_args="<_uid..._>")
|
|
||||||
async def open_manhole(evt: CommandEvent) -> None:
|
|
||||||
if not evt.config["manhole.enabled"]:
|
|
||||||
await evt.reply("The manhole has been disabled in the config.")
|
|
||||||
return
|
|
||||||
elif len(evt.args) == 0:
|
|
||||||
await evt.reply("**Usage:** `$cmdprefix+sp open-manhole <uid...>`")
|
|
||||||
return
|
|
||||||
|
|
||||||
whitelist = set()
|
|
||||||
whitelist_whitelist = evt.config["manhole.whitelist"]
|
|
||||||
for arg in evt.args:
|
|
||||||
try:
|
|
||||||
uid = int(arg)
|
|
||||||
except ValueError:
|
|
||||||
await evt.reply(f"{arg} is not an integer.")
|
|
||||||
return
|
|
||||||
if whitelist_whitelist and uid not in whitelist_whitelist:
|
|
||||||
await evt.reply(f"{uid} is not in the list of allowed UIDs.")
|
|
||||||
return
|
|
||||||
whitelist.add(uid)
|
|
||||||
|
|
||||||
if evt.bridge.manhole:
|
|
||||||
added = [uid for uid in whitelist
|
|
||||||
if uid not in evt.bridge.manhole.whitelist]
|
|
||||||
evt.bridge.manhole.whitelist |= set(added)
|
|
||||||
if len(added) == 0:
|
|
||||||
await evt.reply(f"There's an existing manhole opened by {evt.bridge.manhole.opened_by}"
|
|
||||||
" and all the given UIDs are already whitelisted.")
|
|
||||||
else:
|
|
||||||
added_str = (f"{', '.join(str(uid) for uid in added[:-1])} and {added[-1]}"
|
|
||||||
if len(added) > 1 else added[0])
|
|
||||||
await evt.reply(f"There's an existing manhole opened by {evt.bridge.manhole.opened_by}"
|
|
||||||
f". Added {added_str} to the whitelist.")
|
|
||||||
evt.log.info(f"{evt.sender.mxid} added {added_str} to the manhole whitelist.")
|
|
||||||
return
|
|
||||||
|
|
||||||
from ..portal import Portal
|
|
||||||
from ..puppet import Puppet
|
|
||||||
from ..user import User
|
|
||||||
namespace = {
|
|
||||||
"bridge": evt.bridge,
|
|
||||||
"User": User,
|
|
||||||
"Portal": Portal,
|
|
||||||
"Puppet": Puppet,
|
|
||||||
}
|
|
||||||
banner = (f"Python {sys.version} on {sys.platform}\n"
|
|
||||||
f"mautrix-telegram {__version__} with mautrix-python {__mautrix_version__} "
|
|
||||||
f"and Telethon {__telethon_version__}\n\nManhole opened by {evt.sender.mxid}\n")
|
|
||||||
path = evt.config["manhole.path"]
|
|
||||||
|
|
||||||
wl_list = list(whitelist)
|
|
||||||
whitelist_str = (f"{', '.join(str(uid) for uid in wl_list[:-1])} and {wl_list[-1]}"
|
|
||||||
if len(wl_list) > 1 else wl_list[0])
|
|
||||||
evt.log.info(f"{evt.sender.mxid} opened a manhole with {whitelist_str} whitelisted.")
|
|
||||||
server, close = await start_manhole(path=path, banner=banner, namespace=namespace,
|
|
||||||
loop=evt.loop, whitelist=whitelist)
|
|
||||||
evt.bridge.manhole = ManholeState(server=server, opened_by=evt.sender.mxid, close=close,
|
|
||||||
whitelist=whitelist)
|
|
||||||
plrl = "s" if len(whitelist) != 1 else ""
|
|
||||||
await evt.reply(f"Opened manhole at unix://{path} with UID{plrl} {whitelist_str} whitelisted")
|
|
||||||
await server.wait_closed()
|
|
||||||
evt.bridge.manhole = None
|
|
||||||
try:
|
|
||||||
os.unlink(path)
|
|
||||||
except FileNotFoundError:
|
|
||||||
pass
|
|
||||||
evt.log.info(f"{evt.sender.mxid}'s manhole was closed.")
|
|
||||||
try:
|
|
||||||
await evt.reply("Your manhole was closed.")
|
|
||||||
except (AttributeError, MatrixConnectionError) as e:
|
|
||||||
evt.log.warning(f"Failed to send manhole close notification: {e}")
|
|
||||||
|
|
||||||
|
|
||||||
@command_handler(needs_auth=False, needs_admin=True, help_section=SECTION_ADMIN,
|
|
||||||
help_text="Close an open manhole.")
|
|
||||||
async def close_manhole(evt: CommandEvent) -> None:
|
|
||||||
if not evt.bridge.manhole:
|
|
||||||
await evt.reply("There is no open manhole.")
|
|
||||||
return
|
|
||||||
|
|
||||||
opened_by = evt.bridge.manhole.opened_by
|
|
||||||
evt.bridge.manhole.close()
|
|
||||||
evt.bridge.manhole = None
|
|
||||||
if opened_by != evt.sender.mxid:
|
|
||||||
await evt.reply(f"Closed manhole opened by {opened_by}")
|
|
||||||
@@ -1,113 +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 mautrix.types import EventID
|
|
||||||
from mautrix.bridge import InvalidAccessToken, OnlyLoginSelf
|
|
||||||
|
|
||||||
from . import command_handler, CommandEvent, SECTION_AUTH
|
|
||||||
from .. import puppet as pu
|
|
||||||
|
|
||||||
|
|
||||||
@command_handler(needs_auth=True, needs_matrix_puppeting=True,
|
|
||||||
help_section=SECTION_AUTH, help_text="Revert your Telegram account's Matrix "
|
|
||||||
"puppet to use the default Matrix account.")
|
|
||||||
async def logout_matrix(evt: CommandEvent) -> EventID:
|
|
||||||
puppet = pu.Puppet.get(evt.sender.tgid)
|
|
||||||
if not puppet.is_real_user:
|
|
||||||
return await evt.reply("You are not logged in with your Matrix account.")
|
|
||||||
await puppet.switch_mxid(None, None)
|
|
||||||
return await evt.reply("Reverted your Telegram account's Matrix puppet back to the default.")
|
|
||||||
|
|
||||||
|
|
||||||
@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 = pu.Puppet.get(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.")
|
|
||||||
|
|
||||||
|
|
||||||
@command_handler(needs_auth=True, needs_matrix_puppeting=True,
|
|
||||||
help_section=SECTION_AUTH,
|
|
||||||
help_text="Pings the server with the stored matrix authentication.")
|
|
||||||
async def ping_matrix(evt: CommandEvent) -> EventID:
|
|
||||||
puppet = pu.Puppet.get(evt.sender.tgid)
|
|
||||||
if not puppet.is_real_user:
|
|
||||||
return await evt.reply("You are not logged in with your Matrix account.")
|
|
||||||
try:
|
|
||||||
await puppet.start()
|
|
||||||
except InvalidAccessToken:
|
|
||||||
return await evt.reply("Your access token is invalid.")
|
|
||||||
return await evt.reply("Your Matrix login is working.")
|
|
||||||
|
|
||||||
|
|
||||||
@command_handler(needs_auth=True, needs_matrix_puppeting=True, help_section=SECTION_AUTH,
|
|
||||||
help_text="Clear the Matrix sync token stored for your custom puppet.")
|
|
||||||
async def clear_cache_matrix(evt: CommandEvent) -> EventID:
|
|
||||||
puppet = pu.Puppet.get(evt.sender.tgid)
|
|
||||||
if not puppet.is_real_user:
|
|
||||||
return await evt.reply("You are not logged in with your Matrix account.")
|
|
||||||
try:
|
|
||||||
puppet.stop()
|
|
||||||
puppet.next_batch = None
|
|
||||||
await puppet.start()
|
|
||||||
except InvalidAccessToken:
|
|
||||||
return await evt.reply("Your access token is invalid.")
|
|
||||||
return await evt.reply("Cleared cache successfully.")
|
|
||||||
|
|
||||||
|
|
||||||
async def enter_matrix_token(evt: CommandEvent) -> EventID:
|
|
||||||
evt.sender.command_status = None
|
|
||||||
|
|
||||||
puppet = pu.Puppet.get(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 "
|
|
||||||
f"with {puppet.custom_mxid}.")
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
from . import admin, bridge, config, create_chat, filter, misc, unbridge
|
|
||||||
@@ -1,76 +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 asyncio
|
|
||||||
|
|
||||||
from mautrix.types import EventID
|
|
||||||
|
|
||||||
from ... import portal as po, puppet as pu, user as u
|
|
||||||
from .. import command_handler, CommandEvent, SECTION_ADMIN
|
|
||||||
|
|
||||||
|
|
||||||
@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.cache = {}
|
|
||||||
for puppet in pu.Puppet.by_custom_mxid.values():
|
|
||||||
puppet.sync_task.cancel()
|
|
||||||
pu.Puppet.by_custom_mxid = {}
|
|
||||||
await asyncio.gather(*[puppet.try_start() for puppet in pu.Puppet.all_with_custom_mxid()],
|
|
||||||
loop=evt.loop)
|
|
||||||
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 = 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.sync_task.cancel()
|
|
||||||
await user.stop()
|
|
||||||
user.delete(delete_db=False)
|
|
||||||
user = 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})")
|
|
||||||
@@ -1,193 +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 typing import Optional, Tuple, Awaitable
|
|
||||||
import asyncio
|
|
||||||
|
|
||||||
from telethon.tl.types import ChatForbidden, ChannelForbidden
|
|
||||||
|
|
||||||
from mautrix.types import EventID, RoomID
|
|
||||||
|
|
||||||
from ...types import TelegramID
|
|
||||||
from ... import portal as po
|
|
||||||
from .. import command_handler, CommandEvent, SECTION_CREATING_PORTALS
|
|
||||||
from .util import user_has_power_level, get_initial_state
|
|
||||||
|
|
||||||
|
|
||||||
@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 = 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 = 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, Optional[Awaitable[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) -> Optional[EventID]:
|
|
||||||
status = evt.sender.command_status
|
|
||||||
try:
|
|
||||||
portal = 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.ensure_future(coro, loop=evt.loop)
|
|
||||||
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) -> Optional[EventID]:
|
|
||||||
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()
|
|
||||||
|
|
||||||
asyncio.ensure_future(portal.update_matrix_room(user, entity, direct=False, levels=levels),
|
|
||||||
loop=evt.loop)
|
|
||||||
|
|
||||||
return await evt.reply("Bridging complete. Portal synchronization should begin momentarily.")
|
|
||||||
@@ -1,151 +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 typing import Awaitable, Any
|
|
||||||
from io import StringIO
|
|
||||||
|
|
||||||
from ruamel.yaml import YAMLError
|
|
||||||
|
|
||||||
from mautrix.util.config import yaml
|
|
||||||
from mautrix.types import EventID
|
|
||||||
|
|
||||||
from ... import portal as po, util
|
|
||||||
from .. import command_handler, CommandEvent, SECTION_PORTAL_MANAGEMENT
|
|
||||||
|
|
||||||
|
|
||||||
@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 = 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. "
|
|
||||||
f"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,60 +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 mautrix.types import EventID
|
|
||||||
|
|
||||||
from ... import portal as po
|
|
||||||
from ...types import TelegramID
|
|
||||||
from .. import command_handler, CommandEvent, SECTION_CREATING_PORTALS
|
|
||||||
from .util import user_has_power_level, get_initial_state
|
|
||||||
|
|
||||||
|
|
||||||
@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 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), peer_type=type, mxid=evt.room_id,
|
|
||||||
title=title, about=about, encrypted=encrypted)
|
|
||||||
try:
|
|
||||||
await portal.create_telegram_chat(evt.sender, 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}")
|
|
||||||
@@ -1,95 +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 mautrix.types import EventID
|
|
||||||
|
|
||||||
from ... import portal as po
|
|
||||||
from .. import command_handler, CommandEvent, SECTION_ADMIN
|
|
||||||
|
|
||||||
|
|
||||||
@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>`")
|
|
||||||
@@ -1,154 +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 telethon.tl.functions.channels import GetFullChannelRequest
|
|
||||||
from telethon.tl.functions.messages import GetFullChatRequest
|
|
||||||
from telethon.errors import (ChatAdminRequiredError, UsernameInvalidError,
|
|
||||||
UsernameNotModifiedError, UsernameOccupiedError, RPCError)
|
|
||||||
|
|
||||||
from mautrix.types import EventID
|
|
||||||
|
|
||||||
from ... import portal as po
|
|
||||||
from .. import command_handler, CommandEvent, SECTION_PORTAL_MANAGEMENT, SECTION_MISC
|
|
||||||
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 = 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 = 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 = 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}`.")
|
|
||||||
|
|
||||||
|
|
||||||
@command_handler(help_section=SECTION_PORTAL_MANAGEMENT,
|
|
||||||
help_text="Get a Telegram invite link to the current chat.")
|
|
||||||
async def invite_link(evt: CommandEvent) -> EventID:
|
|
||||||
portal = 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)
|
|
||||||
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 = 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 = 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,100 +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 typing import Dict, Callable, Optional
|
|
||||||
|
|
||||||
from mautrix.types import RoomID, EventID
|
|
||||||
|
|
||||||
from ... import portal as po
|
|
||||||
from .. import command_handler, CommandEvent, SECTION_PORTAL_MANAGEMENT
|
|
||||||
from .util import user_has_power_level
|
|
||||||
|
|
||||||
|
|
||||||
async def _get_portal_and_check_permission(evt: CommandEvent) -> Optional[po.Portal]:
|
|
||||||
room_id = RoomID(evt.args[0]) if len(evt.args) > 0 else evt.room_id
|
|
||||||
|
|
||||||
portal = 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) -> Optional[EventID]:
|
|
||||||
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) -> Optional[EventID]:
|
|
||||||
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) -> Optional[EventID]:
|
|
||||||
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`")
|
|
||||||
@@ -1,62 +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 typing import Tuple, Optional
|
|
||||||
|
|
||||||
from mautrix.errors import MatrixRequestError
|
|
||||||
from mautrix.appservice import IntentAPI
|
|
||||||
from mautrix.types import RoomID, EventType, PowerLevelStateEventContent
|
|
||||||
|
|
||||||
from ... import user as u
|
|
||||||
|
|
||||||
OptStr = Optional[str]
|
|
||||||
|
|
||||||
|
|
||||||
async def get_initial_state(intent: IntentAPI, room_id: RoomID
|
|
||||||
) -> Tuple[OptStr, OptStr, Optional[PowerLevelStateEventContent], bool]:
|
|
||||||
state = await intent.get_state(room_id)
|
|
||||||
title: OptStr = None
|
|
||||||
about: OptStr = None
|
|
||||||
levels: Optional[PowerLevelStateEventContent] = 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 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,125 +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 typing import Optional
|
|
||||||
|
|
||||||
from telethon.errors import (UsernameInvalidError, UsernameNotModifiedError, UsernameOccupiedError,
|
|
||||||
HashInvalidError, AuthKeyError, FirstNameInvalidError)
|
|
||||||
from telethon.tl.types import Authorization
|
|
||||||
from telethon.tl.functions.account import (UpdateUsernameRequest, GetAuthorizationsRequest,
|
|
||||||
ResetAuthorizationRequest, UpdateProfileRequest)
|
|
||||||
|
|
||||||
from mautrix.types import EventID
|
|
||||||
|
|
||||||
from .. import command_handler, CommandEvent, SECTION_AUTH
|
|
||||||
|
|
||||||
|
|
||||||
@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.username:
|
|
||||||
await evt.reply("Username removed")
|
|
||||||
else:
|
|
||||||
await evt.reply(f"Username changed to {evt.sender.username}")
|
|
||||||
|
|
||||||
|
|
||||||
@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"
|
|
||||||
f" **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]`")
|
|
||||||
@@ -1,355 +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 typing import Any, Dict, Optional
|
|
||||||
import asyncio
|
|
||||||
import io
|
|
||||||
|
|
||||||
from telethon.errors import ( # isort: skip
|
|
||||||
AccessTokenExpiredError, AccessTokenInvalidError, FirstNameInvalidError, FloodWaitError,
|
|
||||||
PasswordHashInvalidError, PhoneCodeExpiredError, PhoneCodeInvalidError,
|
|
||||||
PhoneNumberAppSignupForbiddenError, PhoneNumberBannedError, PhoneNumberFloodError,
|
|
||||||
PhoneNumberOccupiedError, PhoneNumberUnoccupiedError, SessionPasswordNeededError,
|
|
||||||
PhoneNumberInvalidError)
|
|
||||||
from telethon.tl.types import User
|
|
||||||
|
|
||||||
from mautrix.types import (EventID, UserID, MediaMessageEventContent, ImageInfo, MessageType,
|
|
||||||
TextMessageEventContent)
|
|
||||||
|
|
||||||
from ... import user as u
|
|
||||||
from ...types import TelegramID
|
|
||||||
from ...commands import command_handler, CommandEvent, SECTION_AUTH
|
|
||||||
from ...util import format_duration as fmt_duration
|
|
||||||
|
|
||||||
try:
|
|
||||||
import qrcode
|
|
||||||
import PIL as _
|
|
||||||
from telethon.tl.custom import QRLogin
|
|
||||||
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:
|
|
||||||
me = await evt.sender.client.get_me() if await evt.sender.is_logged_in() else None
|
|
||||||
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'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.ensure_future(evt.sender.post_login(user, first_login=True), loop=evt.loop)
|
|
||||||
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 = 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: Optional[EventID] = 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_by_mxid(UserID(evt.args[0])).ensure_started()
|
|
||||||
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) -> Optional[EventID]:
|
|
||||||
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) -> Optional[EventID]:
|
|
||||||
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) -> Optional[EventID]:
|
|
||||||
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 = 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}]"
|
|
||||||
f"(https://matrix.to/#/{existing_user.mxid})"
|
|
||||||
" was logged out from the account.")
|
|
||||||
asyncio.ensure_future(login_as.post_login(user, first_login=True), loop=evt.loop)
|
|
||||||
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=True,
|
|
||||||
help_section=SECTION_AUTH,
|
|
||||||
help_text="Log out from Telegram.")
|
|
||||||
async def logout(evt: CommandEvent) -> EventID:
|
|
||||||
if await evt.sender.log_out():
|
|
||||||
return await evt.reply("Logged out successfully.")
|
|
||||||
return await evt.reply("Failed to log out.")
|
|
||||||
@@ -1,361 +0,0 @@
|
|||||||
# mautrix-telegram - A Matrix-Telegram puppeting bridge
|
|
||||||
# Copyright (C) 2020 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, Tuple, cast
|
|
||||||
import logging
|
|
||||||
import codecs
|
|
||||||
import base64
|
|
||||||
import re
|
|
||||||
|
|
||||||
from telethon.errors import (InviteHashInvalidError, InviteHashExpiredError, OptionsTooMuchError,
|
|
||||||
UserAlreadyParticipantError, ChatIdInvalidError,
|
|
||||||
TakeoutInitDelayError, EmoticonInvalidError)
|
|
||||||
from telethon.tl.patched import Message
|
|
||||||
from telethon.tl.types import (User as TLUser, TypeUpdates, MessageMediaGame, MessageMediaPoll,
|
|
||||||
TypeInputPeer, InputMediaDice)
|
|
||||||
from telethon.tl.types.messages import BotCallbackAnswer
|
|
||||||
from telethon.tl.functions.messages import (ImportChatInviteRequest, CheckChatInviteRequest,
|
|
||||||
GetBotCallbackAnswerRequest, SendVoteRequest)
|
|
||||||
from telethon.tl.functions.channels import JoinChannelRequest
|
|
||||||
|
|
||||||
from mautrix.types import EventID, Format
|
|
||||||
|
|
||||||
from ... import puppet as pu, portal as po
|
|
||||||
from ...abstract_user import AbstractUser
|
|
||||||
from ...db import Message as DBMessage
|
|
||||||
from ...types import TelegramID
|
|
||||||
from ...commands import (command_handler, CommandEvent, SECTION_MISC, SECTION_CREATING_PORTALS,
|
|
||||||
SECTION_PORTAL_MANAGEMENT)
|
|
||||||
|
|
||||||
|
|
||||||
@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 = po.Portal.get_by_entity(user, evt.sender.tgid)
|
|
||||||
await portal.create_matrix_room(evt.sender, user, [evt.sender.mxid])
|
|
||||||
return await evt.reply("Created private chat room with "
|
|
||||||
f"{pu.Puppet.get_displayname(user, False)}")
|
|
||||||
|
|
||||||
|
|
||||||
async def _join(evt: CommandEvent, arg: str) -> Tuple[Optional[TypeUpdates], Optional[EventID]]:
|
|
||||||
if arg.startswith("joinchat/"):
|
|
||||||
invite_hash = arg[len("joinchat/"):]
|
|
||||||
try:
|
|
||||||
await evt.sender.client(CheckChatInviteRequest(invite_hash))
|
|
||||||
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(invite_hash))), None
|
|
||||||
except UserAlreadyParticipantError:
|
|
||||||
return None, await evt.reply("You are already in that chat.")
|
|
||||||
else:
|
|
||||||
channel = await evt.sender.client.get_entity(arg)
|
|
||||||
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) -> Optional[EventID]:
|
|
||||||
if len(evt.args) == 0:
|
|
||||||
return await evt.reply("**Usage:** `$cmdprefix+sp join <invite link>`")
|
|
||||||
|
|
||||||
regex = re.compile(r"(?:https?://)?t(?:elegram)?\.(?:dog|me)(?:joinchat/)?/(.+)")
|
|
||||||
arg = regex.match(evt.args[0])
|
|
||||||
if not arg:
|
|
||||||
return await evt.reply("That doesn't look like a Telegram invite link.")
|
|
||||||
|
|
||||||
updates, _ = await _join(evt, arg.group(1))
|
|
||||||
if not updates:
|
|
||||||
return None
|
|
||||||
|
|
||||||
for chat in updates.chats:
|
|
||||||
portal = 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:
|
|
||||||
logging.getLogger("mau.commands").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 = 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 = 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:
|
|
||||||
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}. "
|
|
||||||
f"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:
|
|
||||||
resp = 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 = 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 = 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)
|
|
||||||
@@ -1,242 +0,0 @@
|
|||||||
# mautrix-telegram - A Matrix-Telegram puppeting bridge
|
|
||||||
# Copyright (C) 2020 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
|
|
||||||
from ruamel.yaml.comments import CommentedMap
|
|
||||||
import os
|
|
||||||
|
|
||||||
from mautrix.types import UserID
|
|
||||||
from mautrix.client import Client
|
|
||||||
from mautrix.bridge.config import BaseBridgeConfig
|
|
||||||
from mautrix.util.config import ForbiddenKey, ForbiddenDefault, ConfigUpdateHelper
|
|
||||||
|
|
||||||
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.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()
|
|
||||||
|
|
||||||
copy("appservice.community_id")
|
|
||||||
|
|
||||||
copy("metrics.enabled")
|
|
||||||
copy("metrics.listen_port")
|
|
||||||
|
|
||||||
copy("manhole.enabled")
|
|
||||||
copy("manhole.path")
|
|
||||||
copy("manhole.whitelist")
|
|
||||||
|
|
||||||
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.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.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("*")
|
|
||||||
@@ -1,57 +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 typing import Optional, Tuple, TYPE_CHECKING
|
|
||||||
import asyncio
|
|
||||||
|
|
||||||
from alchemysession import AlchemySessionContainer
|
|
||||||
|
|
||||||
from mautrix.appservice import AppService
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
|
||||||
from .web import PublicBridgeWebsite, ProvisioningAPI
|
|
||||||
from .config import Config
|
|
||||||
from .bot import Bot
|
|
||||||
from .matrix import MatrixHandler
|
|
||||||
from .__main__ import TelegramBridge
|
|
||||||
|
|
||||||
|
|
||||||
class Context:
|
|
||||||
az: AppService
|
|
||||||
config: 'Config'
|
|
||||||
loop: asyncio.AbstractEventLoop
|
|
||||||
bridge: 'TelegramBridge'
|
|
||||||
bot: Optional['Bot']
|
|
||||||
mx: Optional['MatrixHandler']
|
|
||||||
session_container: AlchemySessionContainer
|
|
||||||
public_website: Optional['PublicBridgeWebsite']
|
|
||||||
provisioning_api: Optional['ProvisioningAPI']
|
|
||||||
|
|
||||||
def __init__(self, az: AppService, config: 'Config', loop: asyncio.AbstractEventLoop,
|
|
||||||
session_container: AlchemySessionContainer, bridge: 'TelegramBridge',
|
|
||||||
bot: Optional['Bot']) -> None:
|
|
||||||
self.az = az
|
|
||||||
self.config = config
|
|
||||||
self.loop = loop
|
|
||||||
self.bridge = bridge
|
|
||||||
self.bot = bot
|
|
||||||
self.mx = None
|
|
||||||
self.session_container = session_container
|
|
||||||
self.public_website = None
|
|
||||||
self.provisioning_api = None
|
|
||||||
|
|
||||||
@property
|
|
||||||
def core(self) -> Tuple[AppService, 'Config', asyncio.AbstractEventLoop, Optional['Bot']]:
|
|
||||||
return self.az, self.config, self.loop, self.bot
|
|
||||||
@@ -1,31 +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 sqlalchemy.engine.base import Engine
|
|
||||||
|
|
||||||
from mautrix.client.state_store.sqlalchemy import UserProfile, RoomState
|
|
||||||
|
|
||||||
from .bot_chat import BotChat
|
|
||||||
from .message import Message
|
|
||||||
from .portal import Portal
|
|
||||||
from .puppet import Puppet
|
|
||||||
from .telegram_file import TelegramFile
|
|
||||||
from .user import User, UserPortal, Contact
|
|
||||||
|
|
||||||
|
|
||||||
def init(db_engine: Engine) -> None:
|
|
||||||
for table in (Portal, Message, User, Contact, UserPortal, Puppet, TelegramFile, UserProfile,
|
|
||||||
RoomState, BotChat):
|
|
||||||
table.bind(db_engine)
|
|
||||||
@@ -1,38 +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 typing import Iterable
|
|
||||||
|
|
||||||
from sqlalchemy import Column, Integer, String
|
|
||||||
|
|
||||||
from mautrix.util.db import Base
|
|
||||||
|
|
||||||
from ..types import TelegramID
|
|
||||||
|
|
||||||
|
|
||||||
# Fucking Telegram not telling bots what chats they are in 3:<
|
|
||||||
class BotChat(Base):
|
|
||||||
__tablename__ = "bot_chat"
|
|
||||||
id: TelegramID = Column(Integer, primary_key=True)
|
|
||||||
type: str = Column(String, nullable=False)
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def delete_by_id(cls, chat_id: TelegramID) -> None:
|
|
||||||
with cls.db.begin() as conn:
|
|
||||||
conn.execute(cls.t.delete().where(cls.c.id == chat_id))
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def all(cls) -> Iterable['BotChat']:
|
|
||||||
return cls._select_all()
|
|
||||||
@@ -1,94 +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 typing import Optional, Iterator
|
|
||||||
|
|
||||||
from sqlalchemy import Column, UniqueConstraint, Integer, String, and_, func, desc, select
|
|
||||||
|
|
||||||
from mautrix.types import RoomID, EventID
|
|
||||||
from mautrix.util.db import Base
|
|
||||||
|
|
||||||
from ..types import TelegramID
|
|
||||||
|
|
||||||
|
|
||||||
class Message(Base):
|
|
||||||
__tablename__ = "message"
|
|
||||||
|
|
||||||
mxid: EventID = Column(String)
|
|
||||||
mx_room: RoomID = Column(String)
|
|
||||||
tgid: TelegramID = Column(Integer, primary_key=True)
|
|
||||||
tg_space: TelegramID = Column(Integer, primary_key=True)
|
|
||||||
edit_index: int = Column(Integer, primary_key=True)
|
|
||||||
|
|
||||||
__table_args__ = (UniqueConstraint("mxid", "mx_room", "tg_space", name="_mx_id_room_2"),)
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def get_all_by_tgid(cls, tgid: TelegramID, tg_space: TelegramID) -> Iterator['Message']:
|
|
||||||
return cls._select_all(cls.c.tgid == tgid, cls.c.tg_space == tg_space)
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def get_one_by_tgid(cls, tgid: TelegramID, tg_space: TelegramID, edit_index: int = 0
|
|
||||||
) -> Optional['Message']:
|
|
||||||
if edit_index < 0:
|
|
||||||
return cls._one_or_none(cls.db.execute(
|
|
||||||
cls.t.select()
|
|
||||||
.where(and_(cls.c.tgid == tgid, cls.c.tg_space == tg_space))
|
|
||||||
.order_by(desc(cls.c.edit_index))
|
|
||||||
.limit(1).offset(-edit_index - 1)))
|
|
||||||
else:
|
|
||||||
return cls._select_one_or_none(cls.c.tgid == tgid, cls.c.tg_space == tg_space,
|
|
||||||
cls.c.edit_index == edit_index)
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def count_spaces_by_mxid(cls, mxid: EventID, mx_room: RoomID) -> int:
|
|
||||||
rows = cls.db.execute(select([func.count(cls.c.tg_space)])
|
|
||||||
.where(and_(cls.c.mxid == mxid, cls.c.mx_room == mx_room)))
|
|
||||||
try:
|
|
||||||
count, = next(rows)
|
|
||||||
return count
|
|
||||||
except StopIteration:
|
|
||||||
return 0
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def find_last(cls, mx_room: RoomID, tg_space: TelegramID) -> Optional['Message']:
|
|
||||||
return cls._one_or_none(cls.db.execute(
|
|
||||||
cls._make_simple_select(cls.c.mx_room == mx_room, cls.c.tg_space == tg_space)
|
|
||||||
.order_by(desc(cls.c.tgid)).limit(1)))
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def delete_all(cls, mx_room: RoomID) -> None:
|
|
||||||
cls.db.execute(cls.t.delete().where(cls.c.mx_room == mx_room))
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def get_by_mxid(cls, mxid: EventID, mx_room: RoomID, tg_space: TelegramID
|
|
||||||
) -> Optional['Message']:
|
|
||||||
return cls._select_one_or_none(cls.c.mxid == mxid, cls.c.mx_room == mx_room,
|
|
||||||
cls.c.tg_space == tg_space)
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def update_by_tgid(cls, s_tgid: TelegramID, s_tg_space: TelegramID, s_edit_index: int,
|
|
||||||
**values) -> None:
|
|
||||||
with cls.db.begin() as conn:
|
|
||||||
conn.execute(cls.t.update()
|
|
||||||
.where(and_(cls.c.tgid == s_tgid, cls.c.tg_space == s_tg_space,
|
|
||||||
cls.c.edit_index == s_edit_index))
|
|
||||||
.values(**values))
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def update_by_mxid(cls, s_mxid: EventID, s_mx_room: RoomID, **values) -> None:
|
|
||||||
with cls.db.begin() as conn:
|
|
||||||
conn.execute(cls.t.update()
|
|
||||||
.where(and_(cls.c.mxid == s_mxid, cls.c.mx_room == s_mx_room))
|
|
||||||
.values(**values))
|
|
||||||
@@ -1,66 +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 typing import Optional, Iterable
|
|
||||||
|
|
||||||
from sqlalchemy import Column, Integer, String, Boolean, Text, func, sql
|
|
||||||
|
|
||||||
from mautrix.types import RoomID, ContentURI
|
|
||||||
from mautrix.util.db import Base
|
|
||||||
|
|
||||||
from ..types import TelegramID
|
|
||||||
|
|
||||||
|
|
||||||
class Portal(Base):
|
|
||||||
__tablename__ = "portal"
|
|
||||||
|
|
||||||
# Telegram chat information
|
|
||||||
tgid: TelegramID = Column(Integer, primary_key=True)
|
|
||||||
tg_receiver: TelegramID = Column(Integer, primary_key=True)
|
|
||||||
peer_type: str = Column(String, nullable=False)
|
|
||||||
megagroup: bool = Column(Boolean)
|
|
||||||
|
|
||||||
# Matrix portal information
|
|
||||||
mxid: Optional[RoomID] = Column(String, unique=True, nullable=True)
|
|
||||||
avatar_url: Optional[ContentURI] = Column(String, nullable=True)
|
|
||||||
encrypted: bool = Column(Boolean, nullable=False, server_default=sql.expression.false())
|
|
||||||
|
|
||||||
config: str = Column(Text, nullable=True)
|
|
||||||
|
|
||||||
# Telegram chat metadata
|
|
||||||
username: str = Column(String, nullable=True)
|
|
||||||
title: str = Column(String, nullable=True)
|
|
||||||
about: str = Column(String, nullable=True)
|
|
||||||
photo_id: str = Column(String, nullable=True)
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def get_by_tgid(cls, tgid: TelegramID, tg_receiver: TelegramID) -> Optional['Portal']:
|
|
||||||
return cls._select_one_or_none(cls.c.tgid == tgid, cls.c.tg_receiver == tg_receiver)
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def find_private_chats(cls, tg_receiver: TelegramID) -> Iterable['Portal']:
|
|
||||||
yield from cls._select_all(cls.c.tg_receiver == tg_receiver, cls.c.peer_type == "user")
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def get_by_mxid(cls, mxid: RoomID) -> Optional['Portal']:
|
|
||||||
return cls._select_one_or_none(cls.c.mxid == mxid)
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def get_by_username(cls, username: str) -> Optional['Portal']:
|
|
||||||
return cls._select_one_or_none(func.lower(cls.c.username) == username)
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def all(cls) -> Iterable['Portal']:
|
|
||||||
yield from cls._select_all()
|
|
||||||
@@ -1,61 +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 typing import Optional, Iterable
|
|
||||||
|
|
||||||
from sqlalchemy import Column, Integer, String, Text, Boolean
|
|
||||||
from sqlalchemy.sql import expression, func
|
|
||||||
|
|
||||||
from mautrix.types import UserID, SyncToken
|
|
||||||
from mautrix.util.db import Base
|
|
||||||
|
|
||||||
from ..types import TelegramID
|
|
||||||
|
|
||||||
|
|
||||||
class Puppet(Base):
|
|
||||||
__tablename__ = "puppet"
|
|
||||||
|
|
||||||
id: TelegramID = Column(Integer, primary_key=True)
|
|
||||||
custom_mxid: UserID = Column(String, nullable=True)
|
|
||||||
access_token: str = Column(String, nullable=True)
|
|
||||||
next_batch: SyncToken = Column(String, nullable=True)
|
|
||||||
base_url: str = Column(Text, nullable=True)
|
|
||||||
displayname: str = Column(String, nullable=True)
|
|
||||||
displayname_source: TelegramID = Column(Integer, nullable=True)
|
|
||||||
username: str = Column(String, nullable=True)
|
|
||||||
photo_id: str = Column(String, nullable=True)
|
|
||||||
is_bot: bool = Column(Boolean, nullable=True)
|
|
||||||
matrix_registered: bool = Column(Boolean, nullable=False, server_default=expression.false())
|
|
||||||
disable_updates: bool = Column(Boolean, nullable=False, server_default=expression.false())
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def all_with_custom_mxid(cls) -> Iterable['Puppet']:
|
|
||||||
yield from cls._select_all(cls.c.custom_mxid != None)
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def get_by_tgid(cls, tgid: TelegramID) -> Optional['Puppet']:
|
|
||||||
return cls._select_one_or_none(cls.c.id == tgid)
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def get_by_custom_mxid(cls, mxid: UserID) -> Optional['Puppet']:
|
|
||||||
return cls._select_one_or_none(cls.c.custom_mxid == mxid)
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def get_by_username(cls, username: str) -> Optional['Puppet']:
|
|
||||||
return cls._select_one_or_none(func.lower(cls.c.username) == username)
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def get_by_displayname(cls, displayname: str) -> Optional['Puppet']:
|
|
||||||
return cls._select_one_or_none(cls.c.displayname == displayname)
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user