Compare commits
142 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e8114ff5ad | |||
| 280c74e9cd | |||
| 4f1482e7b0 | |||
| 4641215e97 | |||
| 2f34ebfed9 | |||
| b65a1cc60a | |||
| 53bf278f1e | |||
| 35f137ccc1 | |||
| 31846e7a98 | |||
| 9ab2ee2970 | |||
| c7dd08ecd1 | |||
| 8fbd723bfa | |||
| 530bd9e52e | |||
| 6480e7925e | |||
| c70ab2a12b | |||
| 070bfd4f55 | |||
| 88c3a93526 | |||
| caefda582b | |||
| e1b181ed55 | |||
| cc6a915ef4 | |||
| de4df57278 | |||
| 0068341185 | |||
| efcf1535ff | |||
| 99f633e98d | |||
| 0137bfcbf6 | |||
| f6cb26f7f5 | |||
| 6418202118 | |||
| 4b25e855e0 | |||
| a35f6abfd1 | |||
| 716222a671 | |||
| 31801a436c | |||
| 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 |
@@ -1,7 +1,16 @@
|
|||||||
---
|
---
|
||||||
name: Bug report
|
name: Bug report
|
||||||
about: If something is definitely wrong in the bridge (rather than just a setup issue),
|
about: If something is definitely wrong in the bridge (rather than just a setup issue),
|
||||||
file a bug report. Remember to include relevant logs.
|
file a bug report. Remember to include relevant logs. Asking in the Matrix room first
|
||||||
labels: bug
|
is strongly recommended.
|
||||||
|
type: Bug
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
<!--
|
||||||
|
Remember to include relevant logs, the bridge version and any other details.
|
||||||
|
|
||||||
|
It's always best to ask in the Matrix room first, especially if you aren't sure
|
||||||
|
what details are needed. Issues with insufficient detail will likely just be
|
||||||
|
ignored or closed immediately.
|
||||||
|
-->
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
---
|
---
|
||||||
name: Enhancement request
|
name: Enhancement request
|
||||||
about: Submit a feature request or other suggestion
|
about: Submit a feature request or other suggestion
|
||||||
labels: enhancement
|
type: Feature
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|||||||
@@ -6,17 +6,17 @@ jobs:
|
|||||||
lint:
|
lint:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v4
|
||||||
- uses: actions/setup-python@v2
|
- uses: actions/setup-python@v5
|
||||||
with:
|
with:
|
||||||
python-version: "3.11"
|
python-version: "3.12"
|
||||||
- uses: isort/isort-action@master
|
- uses: isort/isort-action@master
|
||||||
with:
|
with:
|
||||||
sortPaths: "./mautrix_telegram"
|
sortPaths: "./mautrix_telegram"
|
||||||
- uses: psf/black@stable
|
- uses: psf/black@stable
|
||||||
with:
|
with:
|
||||||
src: "./mautrix_telegram"
|
src: "./mautrix_telegram"
|
||||||
version: "23.1.0"
|
version: "24.1.1"
|
||||||
- name: pre-commit
|
- name: pre-commit
|
||||||
run: |
|
run: |
|
||||||
pip install pre-commit
|
pip install pre-commit
|
||||||
|
|||||||
+11
-4
@@ -10,12 +10,19 @@ __pycache__
|
|||||||
/*.egg-info
|
/*.egg-info
|
||||||
/.eggs
|
/.eggs
|
||||||
|
|
||||||
/config.yaml
|
*.yaml
|
||||||
/registration.yaml
|
!.pre-commit-config.yaml
|
||||||
|
!example-config.yaml
|
||||||
|
!/mautrix_telegram/web/provisioning/spec.yaml
|
||||||
|
!/.github/workflows/*.yaml
|
||||||
|
|
||||||
|
/start
|
||||||
|
/mautrix
|
||||||
|
/telethon
|
||||||
|
|
||||||
*.log*
|
*.log*
|
||||||
*.db
|
*.db
|
||||||
|
*.db-*
|
||||||
/*.pickle
|
/*.pickle
|
||||||
*.bak
|
*.bak
|
||||||
/*.session
|
|
||||||
/*.session-journal
|
|
||||||
/*.json
|
/*.json
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
repos:
|
repos:
|
||||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||||
rev: v4.4.0
|
rev: v4.5.0
|
||||||
hooks:
|
hooks:
|
||||||
- id: trailing-whitespace
|
- id: trailing-whitespace
|
||||||
exclude_types: [markdown]
|
exclude_types: [markdown]
|
||||||
@@ -8,13 +8,13 @@ repos:
|
|||||||
- id: check-yaml
|
- id: check-yaml
|
||||||
- id: check-added-large-files
|
- id: check-added-large-files
|
||||||
- repo: https://github.com/psf/black
|
- repo: https://github.com/psf/black
|
||||||
rev: 23.1.0
|
rev: 24.1.1
|
||||||
hooks:
|
hooks:
|
||||||
- id: black
|
- id: black
|
||||||
language_version: python3
|
language_version: python3
|
||||||
files: ^mautrix_telegram/.*\.pyi?$
|
files: ^mautrix_telegram/.*\.pyi?$
|
||||||
- repo: https://github.com/PyCQA/isort
|
- repo: https://github.com/PyCQA/isort
|
||||||
rev: 5.12.0
|
rev: 5.13.2
|
||||||
hooks:
|
hooks:
|
||||||
- id: isort
|
- id: isort
|
||||||
files: ^mautrix_telegram/.*\.pyi?$
|
files: ^mautrix_telegram/.*\.pyi?$
|
||||||
|
|||||||
+105
@@ -1,3 +1,108 @@
|
|||||||
|
# v0.15.3 (2025-07-16)
|
||||||
|
|
||||||
|
* Updated Telegram API to layer 204.
|
||||||
|
* Added support for MSC4190.
|
||||||
|
* Enabled captions by default, as they are now supported by most clients.
|
||||||
|
* Existing configs will still need to enable `caption_in_message` manually.
|
||||||
|
* Changed new room creation to hardcode room v11 to avoid v12 rooms being
|
||||||
|
created before proper support for them can be added.
|
||||||
|
* Fixed bridging sticker messages with partial quote replies from Telegram.
|
||||||
|
* Fixed text in poll bridging.
|
||||||
|
* Disabled kicking unauthenticated users from portals.
|
||||||
|
|
||||||
|
# v0.15.2 (2024-07-16)
|
||||||
|
|
||||||
|
* Dropped support for Python 3.9.
|
||||||
|
* Updated Telegram API to layer 183.
|
||||||
|
* Added support for authenticated media downloads.
|
||||||
|
* Added support for receiving reactions when using a bot account.
|
||||||
|
* Added option to limit file size by chat type.
|
||||||
|
* Fixed reply bridging breaking in some cases.
|
||||||
|
|
||||||
|
# v0.15.1 (2023-12-26)
|
||||||
|
|
||||||
|
* Updated Telegram API to layer 169.
|
||||||
|
* Updated Docker image to Alpine 3.19.
|
||||||
|
* Fixed some potential cases where a portal room would be created for the
|
||||||
|
relaybot even if `ignore_unbridged_group_chat` was enabled.
|
||||||
|
* Fixed member sync in groups with hidden members causing puppeted Matrix users
|
||||||
|
to be kicked even if they're still in the group.
|
||||||
|
|
||||||
|
# v0.15.0 (2023-11-26)
|
||||||
|
|
||||||
|
* Removed support for MSC2716 backfilling.
|
||||||
|
* Added `add-contact` and `delete-contact` commands.
|
||||||
|
* Updated Telegram API layer to 166.
|
||||||
|
* Includes receiving view-once media, blockquotes, quote replies and other
|
||||||
|
such things
|
||||||
|
* Fixed AuthKeyNotFound errors not being handled and causing users to get stuck
|
||||||
|
in a non-logged-in state.
|
||||||
|
|
||||||
|
# v0.14.2 (2023-09-19)
|
||||||
|
|
||||||
|
* **Security:** Updated Pillow to 10.0.1.
|
||||||
|
* Added support for double puppeting with arbitrary `as_token`s.
|
||||||
|
See [docs](https://docs.mau.fi/bridges/general/double-puppeting.html#appservice-method-new) for more info.
|
||||||
|
* Added support for sending webm and tgs files as stickers.
|
||||||
|
* Updated to Telegram API layer 161.
|
||||||
|
* Fixed cached usernames for Telegram users being cleared incorrectly, leading
|
||||||
|
to mentions not being bridged as usernames.
|
||||||
|
* Fixed reaction bridging failing if the server running the bridge was rebooted
|
||||||
|
less than 12 hours ago.
|
||||||
|
|
||||||
|
# v0.14.1 (2023-06-26)
|
||||||
|
|
||||||
|
### Added
|
||||||
|
* Added option to delete megolm sessions that were received before the
|
||||||
|
automatic ratcheting options were introduced.
|
||||||
|
* Added config option to use IPv6 for Telegram connection
|
||||||
|
(thanks to [@exciler] in [#920]).
|
||||||
|
|
||||||
|
### Improved
|
||||||
|
* Dropped support for Python 3.8.
|
||||||
|
* Updated Docker image to Alpine 3.18.
|
||||||
|
* Added timeout for forward backfills to prevent it from getting stuck
|
||||||
|
permanently.
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
* Fixed `bridge.filter.users` config option not being read correctly.
|
||||||
|
* Fixed proxy support to use python-socks instead of pysocks.
|
||||||
|
|
||||||
|
[@exciler]: https://github.com/exciler
|
||||||
|
[#920]: https://github.com/mautrix/telegram/pull/920
|
||||||
|
|
||||||
|
# v0.14.0 (2023-05-26)
|
||||||
|
|
||||||
|
### Added
|
||||||
|
* Added fallback messages for calls and premium gifts.
|
||||||
|
* Added options to automatically ratchet/delete megolm sessions to minimize
|
||||||
|
access to old messages.
|
||||||
|
* Added option to not set room name/avatar even in encrypted rooms.
|
||||||
|
* Implemented appservice pinging using MSC2659.
|
||||||
|
* Added option to disable or filter bridging direct chats
|
||||||
|
(thanks to [@Steffo99] in [#892]).
|
||||||
|
* Added options to specify different limits for forward and catchup backfilling
|
||||||
|
depending on chat type.
|
||||||
|
|
||||||
|
### Improved
|
||||||
|
* Improved handling logouts and certain connection errors.
|
||||||
|
* Changed reaction bridging to preserve timestamps.
|
||||||
|
* Disabled creating portals for DMs that don't have any messages when
|
||||||
|
`sync_direct_chats` is enabled.
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
* Fixed syncing mute status when portal is created through incoming message
|
||||||
|
rather than in startup sync.
|
||||||
|
* Fixed bridge incorrectly trusting member list and kicking users when
|
||||||
|
supergroup has member list hidden.
|
||||||
|
* Fixed sending messages after creating groups from Matrix using relaybot
|
||||||
|
instead of puppet (thanks to [@maltee1] in [#902]).
|
||||||
|
|
||||||
|
[@Steffo99]: https://github.com/Steffo99
|
||||||
|
[@maltee1]: https://github.com/maltee1
|
||||||
|
[#892]: https://github.com/mautrix/telegram/pull/892
|
||||||
|
[#902]: https://github.com/mautrix/telegram/pull/902
|
||||||
|
|
||||||
# v0.13.0 (2023-02-26)
|
# v0.13.0 (2023-02-26)
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|||||||
+8
-6
@@ -1,9 +1,11 @@
|
|||||||
FROM dock.mau.dev/tulir/lottieconverter:alpine-3.17
|
FROM dock.mau.dev/tulir/lottieconverter:alpine-3.22
|
||||||
|
|
||||||
RUN apk add --no-cache \
|
RUN apk add --no-cache \
|
||||||
python3 py3-pip py3-setuptools py3-wheel \
|
python3 py3-pip py3-setuptools py3-wheel \
|
||||||
py3-pillow \
|
py3-pillow \
|
||||||
py3-aiohttp \
|
py3-aiohttp \
|
||||||
|
py3-asyncpg \
|
||||||
|
py3-aiosqlite \
|
||||||
py3-magic \
|
py3-magic \
|
||||||
py3-ruamel.yaml \
|
py3-ruamel.yaml \
|
||||||
py3-commonmark \
|
py3-commonmark \
|
||||||
@@ -14,9 +16,9 @@ RUN apk add --no-cache \
|
|||||||
py3-idna \
|
py3-idna \
|
||||||
py3-rsa \
|
py3-rsa \
|
||||||
#py3-telethon \ (outdated)
|
#py3-telethon \ (outdated)
|
||||||
# Optional for socks proxies
|
|
||||||
py3-pysocks \
|
|
||||||
py3-pyaes \
|
py3-pyaes \
|
||||||
|
py3-aiodns \
|
||||||
|
py3-python-socks \
|
||||||
# cryptg
|
# cryptg
|
||||||
py3-cffi \
|
py3-cffi \
|
||||||
py3-qrcode \
|
py3-qrcode \
|
||||||
@@ -40,13 +42,13 @@ COPY requirements.txt /opt/mautrix-telegram/requirements.txt
|
|||||||
COPY optional-requirements.txt /opt/mautrix-telegram/optional-requirements.txt
|
COPY optional-requirements.txt /opt/mautrix-telegram/optional-requirements.txt
|
||||||
WORKDIR /opt/mautrix-telegram
|
WORKDIR /opt/mautrix-telegram
|
||||||
RUN apk add --virtual .build-deps python3-dev libffi-dev build-base \
|
RUN apk add --virtual .build-deps python3-dev libffi-dev build-base \
|
||||||
&& pip3 install /cryptg-*.whl \
|
&& pip3 install --break-system-packages /cryptg-*.whl \
|
||||||
&& pip3 install --no-cache-dir -r requirements.txt -r optional-requirements.txt \
|
&& pip3 install --break-system-packages --no-cache-dir -r requirements.txt -r optional-requirements.txt \
|
||||||
&& apk del .build-deps \
|
&& apk del .build-deps \
|
||||||
&& rm -f /cryptg-*.whl
|
&& rm -f /cryptg-*.whl
|
||||||
|
|
||||||
COPY . /opt/mautrix-telegram
|
COPY . /opt/mautrix-telegram
|
||||||
RUN apk add git && pip3 install --no-cache-dir .[all] && apk del git \
|
RUN apk add git && pip3 install --break-system-packages --no-cache-dir .[all] && apk del git \
|
||||||
# This doesn't make the image smaller, but it's needed so that the `version` command works properly
|
# 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 .git build
|
&& cp mautrix_telegram/example-config.yaml . && rm -rf mautrix_telegram .git build
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
pre-commit>=2.10.1,<3
|
pre-commit>=2.10.1,<3
|
||||||
isort>=5.10.1,<6
|
isort>=5.10.1,<6
|
||||||
black>=23,<24
|
black>=24,<25
|
||||||
|
|||||||
@@ -1,2 +1,2 @@
|
|||||||
__version__ = "0.13.0"
|
__version__ = "0.15.3"
|
||||||
__author__ = "Tulir Asokan <tulir@maunium.net>"
|
__author__ = "Tulir Asokan <tulir@maunium.net>"
|
||||||
|
|||||||
@@ -39,6 +39,8 @@ from .abstract_user import AbstractUser # isort: skip
|
|||||||
class TelegramBridge(Bridge):
|
class TelegramBridge(Bridge):
|
||||||
module = "mautrix_telegram"
|
module = "mautrix_telegram"
|
||||||
name = "mautrix-telegram"
|
name = "mautrix-telegram"
|
||||||
|
beeper_service_name = "telegram"
|
||||||
|
beeper_network_name = "telegram"
|
||||||
command = "python -m mautrix-telegram"
|
command = "python -m mautrix-telegram"
|
||||||
description = "A Matrix-Telegram puppeting bridge."
|
description = "A Matrix-Telegram puppeting bridge."
|
||||||
repo_url = "https://github.com/mautrix/telegram"
|
repo_url = "https://github.com/mautrix/telegram"
|
||||||
@@ -50,6 +52,7 @@ class TelegramBridge(Bridge):
|
|||||||
|
|
||||||
config: Config
|
config: Config
|
||||||
bot: Bot | None
|
bot: Bot | None
|
||||||
|
matrix: MatrixHandler
|
||||||
public_website: PublicBridgeWebsite | None
|
public_website: PublicBridgeWebsite | None
|
||||||
provisioning_api: ProvisioningAPI | None
|
provisioning_api: ProvisioningAPI | None
|
||||||
|
|
||||||
@@ -101,8 +104,6 @@ class TelegramBridge(Bridge):
|
|||||||
self.log.info("Finished re-sending bridge info state events")
|
self.log.info("Finished re-sending bridge info state events")
|
||||||
|
|
||||||
def prepare_stop(self) -> None:
|
def prepare_stop(self) -> None:
|
||||||
for puppet in Puppet.by_custom_mxid.values():
|
|
||||||
puppet.stop()
|
|
||||||
self.add_shutdown_actions(user.stop() for user in User.by_tgid.values())
|
self.add_shutdown_actions(user.stop() for user in User.by_tgid.values())
|
||||||
if self.bot:
|
if self.bot:
|
||||||
self.add_shutdown_actions(self.bot.stop())
|
self.add_shutdown_actions(self.bot.stop())
|
||||||
|
|||||||
@@ -38,7 +38,9 @@ from telethon.tl.types import (
|
|||||||
PeerChannel,
|
PeerChannel,
|
||||||
PeerChat,
|
PeerChat,
|
||||||
PeerUser,
|
PeerUser,
|
||||||
|
PhoneCallRequested,
|
||||||
TypeUpdate,
|
TypeUpdate,
|
||||||
|
UpdateBotMessageReaction,
|
||||||
UpdateChannel,
|
UpdateChannel,
|
||||||
UpdateChannelUserTyping,
|
UpdateChannelUserTyping,
|
||||||
UpdateChatDefaultBannedRights,
|
UpdateChatDefaultBannedRights,
|
||||||
@@ -54,6 +56,7 @@ from telethon.tl.types import (
|
|||||||
UpdateNewChannelMessage,
|
UpdateNewChannelMessage,
|
||||||
UpdateNewMessage,
|
UpdateNewMessage,
|
||||||
UpdateNotifySettings,
|
UpdateNotifySettings,
|
||||||
|
UpdatePhoneCall,
|
||||||
UpdatePinnedChannelMessages,
|
UpdatePinnedChannelMessages,
|
||||||
UpdatePinnedDialogs,
|
UpdatePinnedDialogs,
|
||||||
UpdatePinnedMessages,
|
UpdatePinnedMessages,
|
||||||
@@ -206,6 +209,8 @@ class AbstractUser(ABC):
|
|||||||
sysversion = self.config["telegram.device_info.system_version"]
|
sysversion = self.config["telegram.device_info.system_version"]
|
||||||
appversion = self.config["telegram.device_info.app_version"]
|
appversion = self.config["telegram.device_info.app_version"]
|
||||||
connection, proxy = self._proxy_settings
|
connection, proxy = self._proxy_settings
|
||||||
|
if proxy:
|
||||||
|
self.log.debug(f"Using proxy setting: {proxy}")
|
||||||
|
|
||||||
assert isinstance(session, Session)
|
assert isinstance(session, Session)
|
||||||
|
|
||||||
@@ -233,8 +238,27 @@ class AbstractUser(ABC):
|
|||||||
loop=self.loop,
|
loop=self.loop,
|
||||||
base_logger=base_logger,
|
base_logger=base_logger,
|
||||||
update_error_callback=self._telethon_update_error_callback,
|
update_error_callback=self._telethon_update_error_callback,
|
||||||
|
use_ipv6=self.config["telegram.connection.use_ipv6"],
|
||||||
)
|
)
|
||||||
self.client.add_event_handler(self._update_catch)
|
self.client.add_event_handler(self._update_catch)
|
||||||
|
self._schedule_reconnect()
|
||||||
|
|
||||||
|
def _schedule_reconnect(self) -> None:
|
||||||
|
reconnect_interval = self.config["telegram.force_refresh_interval_seconds"]
|
||||||
|
if not reconnect_interval or reconnect_interval == 0:
|
||||||
|
return
|
||||||
|
refresh_time = time.time() + reconnect_interval
|
||||||
|
self.log.info(
|
||||||
|
"Scheduling forced reconnect in %d seconds. Connection will be refreshed at %s",
|
||||||
|
reconnect_interval,
|
||||||
|
time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(refresh_time)),
|
||||||
|
)
|
||||||
|
self.loop.call_later(reconnect_interval, lambda: background_task.create(self._reconnect()))
|
||||||
|
|
||||||
|
async def _reconnect(self) -> None:
|
||||||
|
self.log.info("Reconnecting to Telegram...")
|
||||||
|
await self.stop()
|
||||||
|
await self.start()
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
async def on_signed_out(self, err: UnauthorizedError | AuthKeyError) -> None:
|
async def on_signed_out(self, err: UnauthorizedError | AuthKeyError) -> None:
|
||||||
@@ -301,7 +325,18 @@ class AbstractUser(ABC):
|
|||||||
async def start(self, delete_unless_authenticated: bool = False) -> AbstractUser:
|
async def start(self, delete_unless_authenticated: bool = False) -> AbstractUser:
|
||||||
if not self.client:
|
if not self.client:
|
||||||
await self._init_client()
|
await self._init_client()
|
||||||
|
attempts = 1
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
await self.client.connect()
|
await self.client.connect()
|
||||||
|
except Exception:
|
||||||
|
attempts += 1
|
||||||
|
if attempts > 10:
|
||||||
|
raise
|
||||||
|
self.log.exception("Exception connecting to Telegram, retrying in 5s...")
|
||||||
|
await asyncio.sleep(5)
|
||||||
|
else:
|
||||||
|
break
|
||||||
self.log.debug(f"{'Bot' if self.is_relaybot else self.mxid} connected: {self.connected}")
|
self.log.debug(f"{'Bot' if self.is_relaybot else self.mxid} connected: {self.connected}")
|
||||||
return self
|
return self
|
||||||
|
|
||||||
@@ -343,8 +378,12 @@ class AbstractUser(ABC):
|
|||||||
await self.delete_message(update)
|
await self.delete_message(update)
|
||||||
elif isinstance(update, UpdateDeleteChannelMessages):
|
elif isinstance(update, UpdateDeleteChannelMessages):
|
||||||
await self.delete_channel_message(update)
|
await self.delete_channel_message(update)
|
||||||
|
elif isinstance(update, UpdatePhoneCall):
|
||||||
|
await self.update_phone_call(update)
|
||||||
elif isinstance(update, UpdateMessageReactions):
|
elif isinstance(update, UpdateMessageReactions):
|
||||||
await self.update_reactions(update)
|
await self.update_reactions(update)
|
||||||
|
elif isinstance(update, UpdateBotMessageReaction):
|
||||||
|
await self.update_bot_reactions(update)
|
||||||
elif isinstance(update, (UpdateChatUserTyping, UpdateChannelUserTyping, UpdateUserTyping)):
|
elif isinstance(update, (UpdateChatUserTyping, UpdateChannelUserTyping, UpdateUserTyping)):
|
||||||
await self.update_typing(update)
|
await self.update_typing(update)
|
||||||
elif isinstance(update, UpdateUserStatus):
|
elif isinstance(update, UpdateUserStatus):
|
||||||
@@ -449,6 +488,7 @@ class AbstractUser(ABC):
|
|||||||
return
|
return
|
||||||
|
|
||||||
if not portal or not portal.mxid:
|
if not portal or not portal.mxid:
|
||||||
|
# TODO This explodes on channels because the field is channel_id
|
||||||
self.log.debug(f"Dropping own read receipt in unknown chat ({update.peer})")
|
self.log.debug(f"Dropping own read receipt in unknown chat ({update.peer})")
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -617,6 +657,25 @@ class AbstractUser(ABC):
|
|||||||
return
|
return
|
||||||
await portal.handle_telegram_reactions(self, TelegramID(update.msg_id), update.reactions)
|
await portal.handle_telegram_reactions(self, TelegramID(update.msg_id), update.reactions)
|
||||||
|
|
||||||
|
async def update_bot_reactions(self, update: UpdateBotMessageReaction) -> None:
|
||||||
|
portal = await po.Portal.get_by_entity(update.peer, tg_receiver=self.tgid)
|
||||||
|
if not portal or not portal.mxid or not portal.allow_bridging:
|
||||||
|
return
|
||||||
|
await portal.handle_telegram_bot_reactions(self, update)
|
||||||
|
|
||||||
|
async def update_phone_call(self, update: UpdatePhoneCall) -> None:
|
||||||
|
self.log.debug("Phone call update %s", update)
|
||||||
|
if not isinstance(update.phone_call, PhoneCallRequested):
|
||||||
|
return
|
||||||
|
tgid = TelegramID(update.phone_call.participant_id)
|
||||||
|
if tgid == self.tgid:
|
||||||
|
tgid = update.phone_call.admin_id
|
||||||
|
portal = await po.Portal.get_by_tgid(tgid, tg_receiver=self.tgid, peer_type="user")
|
||||||
|
if not portal or not portal.mxid or not portal.allow_bridging:
|
||||||
|
return
|
||||||
|
sender = await pu.Puppet.get_by_tgid(TelegramID(update.phone_call.admin_id))
|
||||||
|
await portal.handle_telegram_direct_call(self, sender, update)
|
||||||
|
|
||||||
async def update_channel(self, update: UpdateChannel) -> None:
|
async def update_channel(self, update: UpdateChannel) -> None:
|
||||||
portal = await po.Portal.get_by_tgid(TelegramID(update.channel_id))
|
portal = await po.Portal.get_by_tgid(TelegramID(update.channel_id))
|
||||||
if not portal:
|
if not portal:
|
||||||
@@ -626,6 +685,10 @@ class AbstractUser(ABC):
|
|||||||
await portal.delete_telegram_user(self.tgid, sender=None)
|
await portal.delete_telegram_user(self.tgid, sender=None)
|
||||||
elif chan := getattr(update, "mau_channel", None):
|
elif chan := getattr(update, "mau_channel", None):
|
||||||
if not portal.mxid:
|
if not portal.mxid:
|
||||||
|
if (
|
||||||
|
not self.is_relaybot
|
||||||
|
or not self.config["bridge.relaybot.ignore_unbridged_group_chat"]
|
||||||
|
):
|
||||||
background_task.create(self._delayed_create_channel(chan))
|
background_task.create(self._delayed_create_channel(chan))
|
||||||
else:
|
else:
|
||||||
self.log.debug("Updating channel info with data fetched by Telethon")
|
self.log.debug("Updating channel info with data fetched by Telethon")
|
||||||
@@ -659,7 +722,15 @@ class AbstractUser(ABC):
|
|||||||
if not portal:
|
if not portal:
|
||||||
return
|
return
|
||||||
elif portal and not portal.allow_bridging:
|
elif portal and not portal.allow_bridging:
|
||||||
self.log.debug(f"Ignoring message in portal {portal.tgid_log} (bridging disallowed)")
|
self.log.debug(
|
||||||
|
f"Ignoring message {update.id} in portal {portal.tgid_log} (bridging disallowed)"
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
if not portal.mxid and getattr(original_update, "mau_left_channel", False):
|
||||||
|
self.log.debug(
|
||||||
|
f"Ignoring message {update.id} in portal {portal.tgid_log} because user isn't in the chat"
|
||||||
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
if self.is_relaybot:
|
if self.is_relaybot:
|
||||||
@@ -683,6 +754,22 @@ class AbstractUser(ABC):
|
|||||||
self.log.debug("Ignoring relaybot-sent message %s to %s", update.id, portal.tgid_log)
|
self.log.debug("Ignoring relaybot-sent message %s to %s", update.id, portal.tgid_log)
|
||||||
return
|
return
|
||||||
|
|
||||||
|
task = self._call_portal_message_handler(update, original_update, portal, sender)
|
||||||
|
if portal.backfill_lock.locked:
|
||||||
|
self.log.debug(
|
||||||
|
f"{portal.tgid_log} is backfill locked, moving incoming message to async task"
|
||||||
|
)
|
||||||
|
background_task.create(task)
|
||||||
|
else:
|
||||||
|
await task
|
||||||
|
|
||||||
|
async def _call_portal_message_handler(
|
||||||
|
self,
|
||||||
|
update: UpdateMessageContent,
|
||||||
|
original_update: UpdateMessage,
|
||||||
|
portal: po.Portal,
|
||||||
|
sender: pu.Puppet,
|
||||||
|
) -> None:
|
||||||
await portal.backfill_lock.wait(f"update {update.id}")
|
await portal.backfill_lock.wait(f"update {update.id}")
|
||||||
|
|
||||||
if isinstance(update, MessageService):
|
if isinstance(update, MessageService):
|
||||||
|
|||||||
@@ -395,7 +395,7 @@ class Bot(AbstractUser):
|
|||||||
def reply(reply_text: str) -> Awaitable[Message]:
|
def reply(reply_text: str) -> Awaitable[Message]:
|
||||||
return self.client.send_message(message.chat_id, reply_text, reply_to=message.id)
|
return self.client.send_message(message.chat_id, reply_text, reply_to=message.id)
|
||||||
|
|
||||||
if command == "start":
|
if command == "start" and message.is_private:
|
||||||
pcm = self.config["bridge.relaybot.private_chat.message"]
|
pcm = self.config["bridge.relaybot.private_chat.message"]
|
||||||
if pcm:
|
if pcm:
|
||||||
await reply(pcm)
|
await reply(pcm)
|
||||||
|
|||||||
@@ -159,6 +159,7 @@ def command_handler(
|
|||||||
needs_admin: bool = False,
|
needs_admin: bool = False,
|
||||||
management_only: bool = False,
|
management_only: bool = False,
|
||||||
name: str | None = None,
|
name: str | None = None,
|
||||||
|
aliases: list[str] | None = None,
|
||||||
help_text: str = "",
|
help_text: str = "",
|
||||||
help_args: str = "",
|
help_args: str = "",
|
||||||
help_section: HelpSection = None,
|
help_section: HelpSection = None,
|
||||||
@@ -167,6 +168,7 @@ def command_handler(
|
|||||||
_func,
|
_func,
|
||||||
_handler_class=CommandHandler,
|
_handler_class=CommandHandler,
|
||||||
name=name,
|
name=name,
|
||||||
|
aliases=aliases,
|
||||||
help_text=help_text,
|
help_text=help_text,
|
||||||
help_args=help_args,
|
help_args=help_args,
|
||||||
help_section=help_section,
|
help_section=help_section,
|
||||||
|
|||||||
@@ -39,8 +39,6 @@ async def clear_db_cache(evt: CommandEvent) -> EventID:
|
|||||||
await evt.reply("Cleared portal cache")
|
await evt.reply("Cleared portal cache")
|
||||||
elif section == "puppet":
|
elif section == "puppet":
|
||||||
pu.Puppet.by_tgid = {}
|
pu.Puppet.by_tgid = {}
|
||||||
for puppet in pu.Puppet.by_custom_mxid.values():
|
|
||||||
puppet.stop()
|
|
||||||
pu.Puppet.by_custom_mxid = {}
|
pu.Puppet.by_custom_mxid = {}
|
||||||
await asyncio.gather(
|
await asyncio.gather(
|
||||||
*[puppet.try_start() async for puppet in pu.Puppet.all_with_custom_mxid()]
|
*[puppet.try_start() async for puppet in pu.Puppet.all_with_custom_mxid()]
|
||||||
@@ -69,8 +67,6 @@ async def reload_user(evt: CommandEvent) -> EventID:
|
|||||||
if not user:
|
if not user:
|
||||||
return await evt.reply("User not found")
|
return await evt.reply("User not found")
|
||||||
puppet = await pu.Puppet.get_by_custom_mxid(mxid)
|
puppet = await pu.Puppet.get_by_custom_mxid(mxid)
|
||||||
if puppet:
|
|
||||||
puppet.stop()
|
|
||||||
await user.stop()
|
await user.stop()
|
||||||
del u.User.by_tgid[user.tgid]
|
del u.User.by_tgid[user.tgid]
|
||||||
del u.User.by_mxid[user.mxid]
|
del u.User.by_mxid[user.mxid]
|
||||||
|
|||||||
@@ -56,7 +56,9 @@ async def bridge(evt: CommandEvent) -> EventID:
|
|||||||
return await evt.reply(f"{that_this} room is already a portal room.")
|
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"):
|
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.")
|
return await evt.reply(
|
||||||
|
f"You do not have the permissions to bridge {that_this.lower()} room."
|
||||||
|
)
|
||||||
|
|
||||||
# The /id bot command provides the prefixed ID, so we assume
|
# The /id bot command provides the prefixed ID, so we assume
|
||||||
tgid_str = evt.args[0]
|
tgid_str = evt.args[0]
|
||||||
|
|||||||
@@ -65,19 +65,11 @@ async def create(evt: CommandEvent) -> EventID:
|
|||||||
about=about,
|
about=about,
|
||||||
encrypted=encrypted,
|
encrypted=encrypted,
|
||||||
)
|
)
|
||||||
invites, errors = await portal.get_telegram_users_in_matrix_room(evt.sender, pre_create=True)
|
|
||||||
if len(errors) > 0:
|
|
||||||
error_list = "\n".join(f"* [{mxid}](https://matrix.to/#/{mxid})" for mxid in errors)
|
|
||||||
await evt.reply(
|
|
||||||
f"Failed to add the following users to the chat:\n\n{error_list}\n\n"
|
|
||||||
"You can try `$cmdprefix+sp search -r <username>` to help the bridge find "
|
|
||||||
"those users."
|
|
||||||
)
|
|
||||||
|
|
||||||
await warn_missing_power(levels, evt)
|
await warn_missing_power(levels, evt)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
await portal.create_telegram_chat(evt.sender, invites=invites, supergroup=supergroup)
|
await portal.create_telegram_chat(evt.sender, supergroup=supergroup)
|
||||||
except ValueError as e:
|
except ValueError as e:
|
||||||
await portal.delete()
|
await portal.delete()
|
||||||
return await evt.reply(e.args[0])
|
return await evt.reply(e.args[0])
|
||||||
|
|||||||
@@ -121,6 +121,7 @@ async def login_qr(evt: CommandEvent) -> EventID:
|
|||||||
mxc = await evt.az.intent.upload_media(qr, "image/png", "login-qr.png", len(qr))
|
mxc = await evt.az.intent.upload_media(qr, "image/png", "login-qr.png", len(qr))
|
||||||
content = MediaMessageEventContent(
|
content = MediaMessageEventContent(
|
||||||
body=qr_login.url,
|
body=qr_login.url,
|
||||||
|
filename="login-qr.png",
|
||||||
url=mxc,
|
url=mxc,
|
||||||
msgtype=MessageType.IMAGE,
|
msgtype=MessageType.IMAGE,
|
||||||
info=ImageInfo(mimetype="image/png", size=len(qr), width=size, height=size),
|
info=ImageInfo(mimetype="image/png", size=len(qr), width=size, height=size),
|
||||||
|
|||||||
@@ -18,8 +18,8 @@ from __future__ import annotations
|
|||||||
from typing import cast
|
from typing import cast
|
||||||
import base64
|
import base64
|
||||||
import codecs
|
import codecs
|
||||||
import math
|
|
||||||
import re
|
import re
|
||||||
|
import shlex
|
||||||
|
|
||||||
from aiohttp import ClientSession, InvalidURL
|
from aiohttp import ClientSession, InvalidURL
|
||||||
from telethon.errors import (
|
from telethon.errors import (
|
||||||
@@ -29,10 +29,10 @@ from telethon.errors import (
|
|||||||
InviteHashInvalidError,
|
InviteHashInvalidError,
|
||||||
InviteRequestSentError,
|
InviteRequestSentError,
|
||||||
OptionsTooMuchError,
|
OptionsTooMuchError,
|
||||||
TakeoutInitDelayError,
|
|
||||||
UserAlreadyParticipantError,
|
UserAlreadyParticipantError,
|
||||||
)
|
)
|
||||||
from telethon.tl.functions.channels import JoinChannelRequest
|
from telethon.tl.functions.channels import JoinChannelRequest
|
||||||
|
from telethon.tl.functions.contacts import DeleteByPhonesRequest, ImportContactsRequest
|
||||||
from telethon.tl.functions.messages import (
|
from telethon.tl.functions.messages import (
|
||||||
CheckChatInviteRequest,
|
CheckChatInviteRequest,
|
||||||
GetBotCallbackAnswerRequest,
|
GetBotCallbackAnswerRequest,
|
||||||
@@ -42,12 +42,14 @@ from telethon.tl.functions.messages import (
|
|||||||
from telethon.tl.patched import Message
|
from telethon.tl.patched import Message
|
||||||
from telethon.tl.types import (
|
from telethon.tl.types import (
|
||||||
InputMediaDice,
|
InputMediaDice,
|
||||||
|
InputPhoneContact,
|
||||||
MessageMediaGame,
|
MessageMediaGame,
|
||||||
MessageMediaPoll,
|
MessageMediaPoll,
|
||||||
TypeInputPeer,
|
TypeInputPeer,
|
||||||
TypeUpdates,
|
TypeUpdates,
|
||||||
User as TLUser,
|
User as TLUser,
|
||||||
)
|
)
|
||||||
|
from telethon.tl.types.contacts import ImportedContacts
|
||||||
from telethon.tl.types.messages import BotCallbackAnswer
|
from telethon.tl.types.messages import BotCallbackAnswer
|
||||||
|
|
||||||
from mautrix.types import EventID, Format
|
from mautrix.types import EventID, Format
|
||||||
@@ -161,6 +163,76 @@ async def pm(evt: CommandEvent) -> EventID:
|
|||||||
return await evt.reply(f"Created private chat room with {displayname}")
|
return await evt.reply(f"Created private chat room with {displayname}")
|
||||||
|
|
||||||
|
|
||||||
|
async def _handle_contact(source: AbstractUser, user: TLUser) -> str:
|
||||||
|
puppet: pu.Puppet = await pu.Puppet.get_by_tgid(user.id)
|
||||||
|
await puppet.update_info(source, user)
|
||||||
|
|
||||||
|
params = []
|
||||||
|
if user.username:
|
||||||
|
params.append(f"[@{user.username}](https://t.me/{user.username})")
|
||||||
|
if user.phone:
|
||||||
|
params.append(f"+{user.phone}")
|
||||||
|
params.append(f"ID `{user.id}`")
|
||||||
|
params_str = " / ".join(params)
|
||||||
|
return f"[{puppet.displayname}](https://matrix.to/#/{puppet.mxid}): {params_str}"
|
||||||
|
|
||||||
|
|
||||||
|
@command_handler(
|
||||||
|
help_section=SECTION_CREATING_PORTALS,
|
||||||
|
help_args="<_phone_> <_first name_> <_last name_>",
|
||||||
|
help_text="Add a phone number to your contacts on Telegram",
|
||||||
|
)
|
||||||
|
async def add_contact(evt: CommandEvent) -> EventID:
|
||||||
|
if len(evt.args) < 3:
|
||||||
|
return await evt.reply(
|
||||||
|
"**Usage:** `$cmdprefix+sp add-contact <phone> <first name> <last name>`"
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
names = shlex.split(" ".join(evt.args[1:]))
|
||||||
|
except ValueError as e:
|
||||||
|
return await evt.reply(
|
||||||
|
f"Failed to parse names (use shell quoting for names with spaces): {e}"
|
||||||
|
)
|
||||||
|
if len(names) != 2:
|
||||||
|
return await evt.reply(
|
||||||
|
"Wrong number of names, must have first and last name "
|
||||||
|
"(use shell quoting for names with spaces)"
|
||||||
|
)
|
||||||
|
res: ImportedContacts = await evt.sender.client(
|
||||||
|
ImportContactsRequest(
|
||||||
|
contacts=[
|
||||||
|
InputPhoneContact(
|
||||||
|
client_id=1, phone=evt.args[0], first_name=names[0], last_name=names[1]
|
||||||
|
)
|
||||||
|
]
|
||||||
|
)
|
||||||
|
)
|
||||||
|
if res.retry_contacts:
|
||||||
|
return await evt.reply("Failed to import contacts")
|
||||||
|
elif not res.users:
|
||||||
|
return await evt.reply("Contact imported, but user not found on Telegram")
|
||||||
|
imported_str = "\n".join(
|
||||||
|
[f"* {await _handle_contact(evt.sender, user)}" for user in res.users]
|
||||||
|
)
|
||||||
|
return await evt.reply(f"Imported contacts:\n\n{imported_str}")
|
||||||
|
|
||||||
|
|
||||||
|
@command_handler(
|
||||||
|
help_section=SECTION_CREATING_PORTALS,
|
||||||
|
help_args="<_phones..._>",
|
||||||
|
help_text="Remove phone numbers from your contacts on Telegram.",
|
||||||
|
aliases=["remove-contact", "delete-contacts", "remove-contacts"],
|
||||||
|
)
|
||||||
|
async def delete_contact(evt: CommandEvent) -> EventID:
|
||||||
|
if len(evt.args) == 0:
|
||||||
|
return await evt.reply("**Usage:** `$cmdprefix+sp delete-contact <phones...>`")
|
||||||
|
ok = await evt.sender.client(DeleteByPhonesRequest(phones=evt.args))
|
||||||
|
if ok:
|
||||||
|
return await evt.reply("Contacts deleted")
|
||||||
|
else:
|
||||||
|
return await evt.reply("Contacts not deleted?")
|
||||||
|
|
||||||
|
|
||||||
async def _join(
|
async def _join(
|
||||||
evt: CommandEvent, identifier: str, link_type: str
|
evt: CommandEvent, identifier: str, link_type: str
|
||||||
) -> tuple[TypeUpdates | None, EventID | None]:
|
) -> tuple[TypeUpdates | None, EventID | None]:
|
||||||
@@ -440,14 +512,5 @@ async def backfill(evt: CommandEvent) -> None:
|
|||||||
if not evt.config["bridge.backfill.normal_groups"] and portal.peer_type == "chat":
|
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")
|
await evt.reply("Backfilling normal groups is disabled in the bridge config")
|
||||||
return
|
return
|
||||||
if portal.backfill_msc2716:
|
|
||||||
messages_per_batch = evt.config["bridge.backfill.incremental.messages_per_batch"]
|
|
||||||
batches = math.ceil(limit / messages_per_batch)
|
|
||||||
rounded = ""
|
|
||||||
if batches * messages_per_batch != limit:
|
|
||||||
rounded = f" (rounded message limit to {batches}*{messages_per_batch})"
|
|
||||||
await portal.enqueue_backfill(evt.sender, priority=0, max_batches=batches)
|
|
||||||
await evt.reply(f"Backfill queued{rounded}")
|
|
||||||
else:
|
|
||||||
output = await portal.forward_backfill(evt.sender, initial=False, override_limit=limit)
|
output = await portal.forward_backfill(evt.sender, initial=False, override_limit=limit)
|
||||||
await evt.reply(output)
|
await evt.reply(output)
|
||||||
|
|||||||
@@ -136,6 +136,8 @@ class Config(BaseBridgeConfig):
|
|||||||
copy("bridge.caption_in_message")
|
copy("bridge.caption_in_message")
|
||||||
copy("bridge.image_as_file_size")
|
copy("bridge.image_as_file_size")
|
||||||
copy("bridge.image_as_file_pixels")
|
copy("bridge.image_as_file_pixels")
|
||||||
|
copy("bridge.document_as_link_size.bot")
|
||||||
|
copy("bridge.document_as_link_size.channel")
|
||||||
copy("bridge.parallel_file_transfer")
|
copy("bridge.parallel_file_transfer")
|
||||||
copy("bridge.federate_rooms")
|
copy("bridge.federate_rooms")
|
||||||
copy("bridge.always_custom_emoji_reaction")
|
copy("bridge.always_custom_emoji_reaction")
|
||||||
@@ -148,7 +150,16 @@ class Config(BaseBridgeConfig):
|
|||||||
copy("bridge.animated_emoji.args.width")
|
copy("bridge.animated_emoji.args.width")
|
||||||
copy("bridge.animated_emoji.args.height")
|
copy("bridge.animated_emoji.args.height")
|
||||||
copy("bridge.animated_emoji.args.fps")
|
copy("bridge.animated_emoji.args.fps")
|
||||||
|
if isinstance(self.get("bridge.private_chat_portal_meta", "default"), bool):
|
||||||
|
base["bridge.private_chat_portal_meta"] = (
|
||||||
|
"always" if self["bridge.private_chat_portal_meta"] else "default"
|
||||||
|
)
|
||||||
|
else:
|
||||||
copy("bridge.private_chat_portal_meta")
|
copy("bridge.private_chat_portal_meta")
|
||||||
|
if base["bridge.private_chat_portal_meta"] not in ("default", "always", "never"):
|
||||||
|
base["bridge.private_chat_portal_meta"] = "default"
|
||||||
|
copy("bridge.disable_reply_fallbacks")
|
||||||
|
copy("bridge.cross_room_replies")
|
||||||
copy("bridge.delivery_receipts")
|
copy("bridge.delivery_receipts")
|
||||||
copy("bridge.delivery_error_reports")
|
copy("bridge.delivery_error_reports")
|
||||||
copy("bridge.incoming_bridge_error_reports")
|
copy("bridge.incoming_bridge_error_reports")
|
||||||
@@ -162,12 +173,29 @@ class Config(BaseBridgeConfig):
|
|||||||
copy("bridge.kick_on_logout")
|
copy("bridge.kick_on_logout")
|
||||||
copy("bridge.always_read_joined_telegram_notice")
|
copy("bridge.always_read_joined_telegram_notice")
|
||||||
copy("bridge.backfill.enable")
|
copy("bridge.backfill.enable")
|
||||||
copy("bridge.backfill.msc2716")
|
|
||||||
copy("bridge.backfill.double_puppet_backfill")
|
|
||||||
copy("bridge.backfill.normal_groups")
|
copy("bridge.backfill.normal_groups")
|
||||||
copy("bridge.backfill.unread_hours_threshold")
|
copy("bridge.backfill.unread_hours_threshold")
|
||||||
copy("bridge.backfill.forward.initial_limit")
|
if "bridge.backfill.forward" in self:
|
||||||
copy("bridge.backfill.forward.sync_limit")
|
initial_limit = self.get("bridge.backfill.forward.initial_limit", 10)
|
||||||
|
sync_limit = self.get("bridge.backfill.forward.sync_limit", 100)
|
||||||
|
base["bridge.backfill.forward_limits.initial.user"] = initial_limit
|
||||||
|
base["bridge.backfill.forward_limits.initial.normal_group"] = initial_limit
|
||||||
|
base["bridge.backfill.forward_limits.initial.supergroup"] = initial_limit
|
||||||
|
base["bridge.backfill.forward_limits.initial.channel"] = initial_limit
|
||||||
|
base["bridge.backfill.forward_limits.sync.user"] = sync_limit
|
||||||
|
base["bridge.backfill.forward_limits.sync.normal_group"] = sync_limit
|
||||||
|
base["bridge.backfill.forward_limits.sync.supergroup"] = sync_limit
|
||||||
|
base["bridge.backfill.forward_limits.sync.channel"] = sync_limit
|
||||||
|
else:
|
||||||
|
copy("bridge.backfill.forward_limits.initial.user")
|
||||||
|
copy("bridge.backfill.forward_limits.initial.normal_group")
|
||||||
|
copy("bridge.backfill.forward_limits.initial.supergroup")
|
||||||
|
copy("bridge.backfill.forward_limits.initial.channel")
|
||||||
|
copy("bridge.backfill.forward_limits.sync.user")
|
||||||
|
copy("bridge.backfill.forward_limits.sync.normal_group")
|
||||||
|
copy("bridge.backfill.forward_limits.sync.supergroup")
|
||||||
|
copy("bridge.backfill.forward_limits.sync.channel")
|
||||||
|
copy("bridge.backfill.forward_timeout")
|
||||||
copy("bridge.backfill.incremental.messages_per_batch")
|
copy("bridge.backfill.incremental.messages_per_batch")
|
||||||
copy("bridge.backfill.incremental.post_batch_delay")
|
copy("bridge.backfill.incremental.post_batch_delay")
|
||||||
copy("bridge.backfill.incremental.max_batches.user")
|
copy("bridge.backfill.incremental.max_batches.user")
|
||||||
@@ -197,6 +225,7 @@ class Config(BaseBridgeConfig):
|
|||||||
|
|
||||||
copy("bridge.filter.mode")
|
copy("bridge.filter.mode")
|
||||||
copy("bridge.filter.list")
|
copy("bridge.filter.list")
|
||||||
|
copy("bridge.filter.users")
|
||||||
|
|
||||||
copy("bridge.command_prefix")
|
copy("bridge.command_prefix")
|
||||||
|
|
||||||
@@ -235,12 +264,14 @@ class Config(BaseBridgeConfig):
|
|||||||
copy("telegram.catch_up")
|
copy("telegram.catch_up")
|
||||||
copy("telegram.sequential_updates")
|
copy("telegram.sequential_updates")
|
||||||
copy("telegram.exit_on_update_error")
|
copy("telegram.exit_on_update_error")
|
||||||
|
copy("telegram.force_refresh_interval_seconds")
|
||||||
|
|
||||||
copy("telegram.connection.timeout")
|
copy("telegram.connection.timeout")
|
||||||
copy("telegram.connection.retries")
|
copy("telegram.connection.retries")
|
||||||
copy("telegram.connection.retry_delay")
|
copy("telegram.connection.retry_delay")
|
||||||
copy("telegram.connection.flood_sleep_threshold")
|
copy("telegram.connection.flood_sleep_threshold")
|
||||||
copy("telegram.connection.request_retries")
|
copy("telegram.connection.request_retries")
|
||||||
|
copy("telegram.connection.use_ipv6")
|
||||||
|
|
||||||
copy("telegram.device_info.device_model")
|
copy("telegram.device_info.device_model")
|
||||||
copy("telegram.device_info.system_version")
|
copy("telegram.device_info.system_version")
|
||||||
|
|||||||
@@ -48,6 +48,7 @@ class Puppet:
|
|||||||
avatar_url: ContentURI | None
|
avatar_url: ContentURI | None
|
||||||
name_set: bool
|
name_set: bool
|
||||||
avatar_set: bool
|
avatar_set: bool
|
||||||
|
contact_info_set: bool
|
||||||
is_bot: bool | None
|
is_bot: bool | None
|
||||||
is_channel: bool
|
is_channel: bool
|
||||||
is_premium: bool
|
is_premium: bool
|
||||||
@@ -68,7 +69,7 @@ class Puppet:
|
|||||||
columns: ClassVar[str] = (
|
columns: ClassVar[str] = (
|
||||||
"id, is_registered, displayname, displayname_source, displayname_contact, "
|
"id, is_registered, displayname, displayname_source, displayname_contact, "
|
||||||
"displayname_quality, disable_updates, username, phone, photo_id, avatar_url, "
|
"displayname_quality, disable_updates, username, phone, photo_id, avatar_url, "
|
||||||
"name_set, avatar_set, is_bot, is_channel, is_premium, "
|
"name_set, avatar_set, contact_info_set, is_bot, is_channel, is_premium, "
|
||||||
"custom_mxid, access_token, next_batch, base_url"
|
"custom_mxid, access_token, next_batch, base_url"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -108,6 +109,7 @@ class Puppet:
|
|||||||
self.avatar_url,
|
self.avatar_url,
|
||||||
self.name_set,
|
self.name_set,
|
||||||
self.avatar_set,
|
self.avatar_set,
|
||||||
|
self.contact_info_set,
|
||||||
self.is_bot,
|
self.is_bot,
|
||||||
self.is_channel,
|
self.is_channel,
|
||||||
self.is_premium,
|
self.is_premium,
|
||||||
@@ -122,8 +124,9 @@ class Puppet:
|
|||||||
UPDATE puppet
|
UPDATE puppet
|
||||||
SET is_registered=$2, displayname=$3, displayname_source=$4, displayname_contact=$5,
|
SET is_registered=$2, displayname=$3, displayname_source=$4, displayname_contact=$5,
|
||||||
displayname_quality=$6, disable_updates=$7, username=$8, phone=$9, photo_id=$10,
|
displayname_quality=$6, disable_updates=$7, username=$8, phone=$9, photo_id=$10,
|
||||||
avatar_url=$11, name_set=$12, avatar_set=$13, is_bot=$14, is_channel=$15,
|
avatar_url=$11, name_set=$12, avatar_set=$13, contact_info_set=$14, is_bot=$15,
|
||||||
is_premium=$16, custom_mxid=$17, access_token=$18, next_batch=$19, base_url=$20
|
is_channel=$16, is_premium=$17, custom_mxid=$18, access_token=$19, next_batch=$20,
|
||||||
|
base_url=$21
|
||||||
WHERE id=$1
|
WHERE id=$1
|
||||||
"""
|
"""
|
||||||
await self.db.execute(q, *self._values)
|
await self.db.execute(q, *self._values)
|
||||||
@@ -133,9 +136,9 @@ class Puppet:
|
|||||||
INSERT INTO puppet (
|
INSERT INTO puppet (
|
||||||
id, is_registered, displayname, displayname_source, displayname_contact,
|
id, is_registered, displayname, displayname_source, displayname_contact,
|
||||||
displayname_quality, disable_updates, username, phone, photo_id, avatar_url, name_set,
|
displayname_quality, disable_updates, username, phone, photo_id, avatar_url, name_set,
|
||||||
avatar_set, is_bot, is_channel, is_premium, custom_mxid, access_token, next_batch,
|
avatar_set, contact_info_set, is_bot, is_channel, is_premium, custom_mxid,
|
||||||
base_url
|
access_token, next_batch, base_url
|
||||||
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18,
|
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18,
|
||||||
$19, $20)
|
$19, $20, $21)
|
||||||
"""
|
"""
|
||||||
await self.db.execute(q, *self._values)
|
await self.db.execute(q, *self._values)
|
||||||
|
|||||||
@@ -208,9 +208,9 @@ class PgSession(MemorySession):
|
|||||||
await self._locked_process_entities(tlo)
|
await self._locked_process_entities(tlo)
|
||||||
|
|
||||||
async def _locked_process_entities(self, tlo) -> None:
|
async def _locked_process_entities(self, tlo) -> None:
|
||||||
rows: list[
|
rows: list[tuple[str, int, int, str | None, str | None, str | None]] = (
|
||||||
tuple[str, int, int, str | None, str | None, str | None]
|
self._entities_to_rows(tlo)
|
||||||
] = self._entities_to_rows(tlo)
|
)
|
||||||
if not rows:
|
if not rows:
|
||||||
return
|
return
|
||||||
if self.db.scheme == Scheme.POSTGRES:
|
if self.db.scheme == Scheme.POSTGRES:
|
||||||
|
|||||||
@@ -20,4 +20,5 @@ from . import (
|
|||||||
v15_backfill_anchor_id,
|
v15_backfill_anchor_id,
|
||||||
v16_backfill_type,
|
v16_backfill_type,
|
||||||
v17_message_find_recent,
|
v17_message_find_recent,
|
||||||
|
v18_puppet_contact_info_set,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -15,7 +15,7 @@
|
|||||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
from mautrix.util.async_db import Connection, Scheme
|
from mautrix.util.async_db import Connection, Scheme
|
||||||
|
|
||||||
latest_version = 17
|
latest_version = 18
|
||||||
|
|
||||||
|
|
||||||
async def create_latest_tables(conn: Connection, scheme: Scheme) -> int:
|
async def create_latest_tables(conn: Connection, scheme: Scheme) -> int:
|
||||||
@@ -113,6 +113,7 @@ async def create_latest_tables(conn: Connection, scheme: Scheme) -> int:
|
|||||||
avatar_url TEXT,
|
avatar_url TEXT,
|
||||||
name_set BOOLEAN NOT NULL DEFAULT false,
|
name_set BOOLEAN NOT NULL DEFAULT false,
|
||||||
avatar_set BOOLEAN NOT NULL DEFAULT false,
|
avatar_set BOOLEAN NOT NULL DEFAULT false,
|
||||||
|
contact_info_set BOOLEAN NOT NULL DEFAULT false,
|
||||||
is_bot BOOLEAN,
|
is_bot BOOLEAN,
|
||||||
is_channel BOOLEAN NOT NULL DEFAULT false,
|
is_channel BOOLEAN NOT NULL DEFAULT false,
|
||||||
is_premium BOOLEAN NOT NULL DEFAULT false,
|
is_premium BOOLEAN NOT NULL DEFAULT false,
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
# mautrix-telegram - A Matrix-Telegram puppeting bridge
|
||||||
|
# Copyright (C) 2022 Tulir Asokan
|
||||||
|
#
|
||||||
|
# This program is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU Affero General Public License as published by
|
||||||
|
# the Free Software Foundation, either version 3 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU Affero General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU Affero General Public License
|
||||||
|
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
from mautrix.util.async_db import Connection
|
||||||
|
|
||||||
|
from . import upgrade_table
|
||||||
|
|
||||||
|
|
||||||
|
@upgrade_table.register(description="Add contact_info_set column to puppet table")
|
||||||
|
async def upgrade_v18(conn: Connection) -> None:
|
||||||
|
await conn.execute(
|
||||||
|
"ALTER TABLE puppet ADD COLUMN contact_info_set BOOLEAN NOT NULL DEFAULT false"
|
||||||
|
)
|
||||||
@@ -40,7 +40,7 @@ appservice:
|
|||||||
|
|
||||||
# The full URI to the database. SQLite and Postgres are supported.
|
# The full URI to the database. SQLite and Postgres are supported.
|
||||||
# Format examples:
|
# Format examples:
|
||||||
# SQLite: sqlite:///filename.db
|
# SQLite: sqlite:filename.db
|
||||||
# Postgres: postgres://username:password@hostname/dbname
|
# Postgres: postgres://username:password@hostname/dbname
|
||||||
database: postgres://username:password@hostname/dbname
|
database: postgres://username:password@hostname/dbname
|
||||||
# Additional arguments for asyncpg.create_pool() or sqlite3.connect()
|
# Additional arguments for asyncpg.create_pool() or sqlite3.connect()
|
||||||
@@ -216,12 +216,16 @@ bridge:
|
|||||||
# to resolve redirects in invite links.
|
# to resolve redirects in invite links.
|
||||||
invite_link_resolve: false
|
invite_link_resolve: false
|
||||||
# Send captions in the same message as images. This will send data compatible with both MSC2530 and MSC3552.
|
# Send captions in the same message as images. This will send data compatible with both MSC2530 and MSC3552.
|
||||||
# This is currently not supported in most clients.
|
caption_in_message: true
|
||||||
caption_in_message: false
|
|
||||||
# Maximum size of image in megabytes before sending to Telegram as a document.
|
# Maximum size of image in megabytes before sending to Telegram as a document.
|
||||||
image_as_file_size: 10
|
image_as_file_size: 10
|
||||||
# Maximum number of pixels in an image before sending to Telegram as a document. Defaults to 4096x4096 = 16777216.
|
# Maximum number of pixels in an image before sending to Telegram as a document. Defaults to 4096x4096 = 16777216.
|
||||||
image_as_file_pixels: 16777216
|
image_as_file_pixels: 16777216
|
||||||
|
# Maximum size of Telegram documents before linking to Telegrm instead of bridge
|
||||||
|
# to Matrix media.
|
||||||
|
document_as_link_size:
|
||||||
|
channel:
|
||||||
|
bot:
|
||||||
# Enable experimental parallel file transfer, which makes uploads/downloads much faster by
|
# Enable experimental parallel file transfer, which makes uploads/downloads much faster by
|
||||||
# streaming from/to Matrix and using many connections for Telegram.
|
# streaming from/to Matrix and using many connections for Telegram.
|
||||||
# Note that generating HQ thumbnails for videos is not possible with streamed transfers.
|
# Note that generating HQ thumbnails for videos is not possible with streamed transfers.
|
||||||
@@ -267,13 +271,45 @@ bridge:
|
|||||||
# Default to encryption, force-enable encryption in all portals the bridge creates
|
# Default to encryption, force-enable encryption in all portals the bridge creates
|
||||||
# This will cause the bridge bot to be in private chats for the encryption to work properly.
|
# This will cause the bridge bot to be in private chats for the encryption to work properly.
|
||||||
default: false
|
default: false
|
||||||
# Whether to use MSC2409/MSC3202 instead of /sync long polling for receiving encryption-related data.
|
# Whether to use MSC3202/MSC4203 instead of /sync long polling for receiving encryption-related data.
|
||||||
|
# This option is not yet compatible with standard Matrix servers like Synapse and should not be used.
|
||||||
|
# Changing this option requires updating the appservice registration file.
|
||||||
appservice: false
|
appservice: false
|
||||||
|
# Whether to use MSC4190 instead of appservice login to create the bridge bot device.
|
||||||
|
# Requires the homeserver to support MSC4190 and the device masquerading parts of MSC3202.
|
||||||
|
# Only relevant when using end-to-bridge encryption, required when using encryption with next-gen auth (MSC3861).
|
||||||
|
# Changing this option requires updating the appservice registration file.
|
||||||
|
msc4190: false
|
||||||
|
# Should the bridge bot generate a recovery key and cross-signing keys and verify itself?
|
||||||
|
# Note that without the latest version of MSC4190, this will fail if you reset the bridge database.
|
||||||
|
# The generated recovery key will be printed in logs.
|
||||||
|
self_sign: false
|
||||||
# Require encryption, drop any unencrypted messages.
|
# Require encryption, drop any unencrypted messages.
|
||||||
require: false
|
require: false
|
||||||
# Enable key sharing? If enabled, key requests for rooms where users are in will be fulfilled.
|
# Enable key sharing? If enabled, key requests for rooms where users are in will be fulfilled.
|
||||||
# You must use a client that supports requesting keys from other users to use this feature.
|
# You must use a client that supports requesting keys from other users to use this feature.
|
||||||
allow_key_sharing: false
|
allow_key_sharing: false
|
||||||
|
# Options for deleting megolm sessions from the bridge.
|
||||||
|
delete_keys:
|
||||||
|
# Beeper-specific: delete outbound sessions when hungryserv confirms
|
||||||
|
# that the user has uploaded the key to key backup.
|
||||||
|
delete_outbound_on_ack: false
|
||||||
|
# Don't store outbound sessions in the inbound table.
|
||||||
|
dont_store_outbound: false
|
||||||
|
# Ratchet megolm sessions forward after decrypting messages.
|
||||||
|
ratchet_on_decrypt: false
|
||||||
|
# Delete fully used keys (index >= max_messages) after decrypting messages.
|
||||||
|
delete_fully_used_on_decrypt: false
|
||||||
|
# Delete previous megolm sessions from same device when receiving a new one.
|
||||||
|
delete_prev_on_new_session: false
|
||||||
|
# Delete megolm sessions received from a device when the device is deleted.
|
||||||
|
delete_on_device_delete: false
|
||||||
|
# Periodically delete megolm sessions when 2x max_age has passed since receiving the session.
|
||||||
|
periodically_delete_expired: false
|
||||||
|
# Delete inbound megolm sessions that don't have the received_at field used for
|
||||||
|
# automatic ratcheting and expired session deletion. This is meant as a migration
|
||||||
|
# to delete old keys prior to the bridge update.
|
||||||
|
delete_outdated_inbound: false
|
||||||
# What level of device verification should be required from users?
|
# What level of device verification should be required from users?
|
||||||
#
|
#
|
||||||
# Valid levels:
|
# Valid levels:
|
||||||
@@ -309,9 +345,20 @@ bridge:
|
|||||||
# default.
|
# default.
|
||||||
messages: 100
|
messages: 100
|
||||||
|
|
||||||
# Whether or not to explicitly set the avatar and room name for private
|
# Disable rotating keys when a user's devices change?
|
||||||
# chat portal rooms. This will be implicitly enabled if encryption.default is true.
|
# You should not enable this option unless you understand all the implications.
|
||||||
private_chat_portal_meta: false
|
disable_device_change_key_rotation: false
|
||||||
|
|
||||||
|
# Whether to explicitly set the avatar and room name for private chat portal rooms.
|
||||||
|
# If set to `default`, this will be enabled in encrypted rooms and disabled in unencrypted rooms.
|
||||||
|
# If set to `always`, all DM rooms will have explicit names and avatars set.
|
||||||
|
# If set to `never`, DM rooms will never have names and avatars set.
|
||||||
|
private_chat_portal_meta: default
|
||||||
|
# Disable generating reply fallbacks? Some extremely bad clients still rely on them,
|
||||||
|
# but they're being phased out and will be completely removed in the future.
|
||||||
|
disable_reply_fallbacks: true
|
||||||
|
# Should cross-chat replies from Telegram be bridged? Most servers and clients don't support this.
|
||||||
|
cross_room_replies: false
|
||||||
# Whether or not the bridge should send a read receipt from the bridge bot when a message has
|
# Whether or not the bridge should send a read receipt from the bridge bot when a message has
|
||||||
# been sent to Telegram.
|
# been sent to Telegram.
|
||||||
delivery_receipts: false
|
delivery_receipts: false
|
||||||
@@ -347,20 +394,6 @@ bridge:
|
|||||||
backfill:
|
backfill:
|
||||||
# Allow backfilling at all?
|
# Allow backfilling at all?
|
||||||
enable: true
|
enable: true
|
||||||
# Use MSC2716 for backfilling?
|
|
||||||
#
|
|
||||||
# This requires a server with MSC2716 support, which is currently an experimental feature in Synapse.
|
|
||||||
# It can be enabled by setting experimental_features -> msc2716_enabled to true in homeserver.yaml.
|
|
||||||
msc2716: false
|
|
||||||
# Use double puppets for backfilling?
|
|
||||||
#
|
|
||||||
# If using MSC2716, the double puppets must be in the appservice's user ID namespace
|
|
||||||
# (because the bridge can't use the double puppet access token with batch sending).
|
|
||||||
#
|
|
||||||
# Even without MSC2716, bridging old messages with correct timestamps requires the double
|
|
||||||
# puppets to be in an appservice namespace, or the server to be modified to allow
|
|
||||||
# overriding timestamps anyway.
|
|
||||||
double_puppet_backfill: false
|
|
||||||
# Whether or not to enable backfilling in normal groups.
|
# Whether or not to enable backfilling in normal groups.
|
||||||
# Normal groups have numerous technical problems in Telegram, and backfilling normal groups
|
# Normal groups have numerous technical problems in Telegram, and backfilling normal groups
|
||||||
# will likely cause problems if there are multiple Matrix users in the group.
|
# will likely cause problems if there are multiple Matrix users in the group.
|
||||||
@@ -370,17 +403,26 @@ bridge:
|
|||||||
# Set to -1 to let any chat be unread.
|
# Set to -1 to let any chat be unread.
|
||||||
unread_hours_threshold: 720
|
unread_hours_threshold: 720
|
||||||
|
|
||||||
# Forward backfilling limits. These apply to both MSC2716 and legacy backfill.
|
# Forward backfilling limits.
|
||||||
#
|
#
|
||||||
# Using a negative initial limit is not recommended, as it would try to backfill everything in a single batch.
|
# Using a negative initial limit is not recommended, as it would try to backfill everything in a single batch.
|
||||||
# MSC2716 and the incremental settings are meant for backfilling everything incrementally rather than at once.
|
forward_limits:
|
||||||
forward:
|
|
||||||
# Number of messages to backfill immediately after creating a portal.
|
# Number of messages to backfill immediately after creating a portal.
|
||||||
initial_limit: 10
|
initial:
|
||||||
|
user: 50
|
||||||
|
normal_group: 100
|
||||||
|
supergroup: 10
|
||||||
|
channel: 10
|
||||||
# Number of messages to backfill when syncing chats.
|
# Number of messages to backfill when syncing chats.
|
||||||
sync_limit: 100
|
sync:
|
||||||
|
user: 100
|
||||||
|
normal_group: 100
|
||||||
|
supergroup: 100
|
||||||
|
channel: 100
|
||||||
|
# Timeout for forward backfills in seconds. If you have a high limit, you'll have to increase this too.
|
||||||
|
forward_timeout: 900
|
||||||
|
|
||||||
# Settings for incremental backfill of history. These only apply when using MSC2716.
|
# Settings for incremental backfill of history. These only apply to Beeper, as upstream abandoned MSC2716.
|
||||||
incremental:
|
incremental:
|
||||||
# Maximum number of messages to backfill per batch.
|
# Maximum number of messages to backfill per batch.
|
||||||
messages_per_batch: 100
|
messages_per_batch: 100
|
||||||
@@ -458,7 +500,6 @@ bridge:
|
|||||||
# Filter rooms that can/can't be bridged. Can also be managed using the `filter` and
|
# Filter rooms that can/can't be bridged. Can also be managed using the `filter` and
|
||||||
# `filter-mode` management commands.
|
# `filter-mode` management commands.
|
||||||
#
|
#
|
||||||
# Filters do not affect direct chats.
|
|
||||||
# An empty blacklist will essentially disable the filter.
|
# An empty blacklist will essentially disable the filter.
|
||||||
filter:
|
filter:
|
||||||
# Filter mode to use. Either "blacklist" or "whitelist".
|
# Filter mode to use. Either "blacklist" or "whitelist".
|
||||||
@@ -467,6 +508,11 @@ bridge:
|
|||||||
mode: blacklist
|
mode: blacklist
|
||||||
# The list of group/channel IDs to filter.
|
# The list of group/channel IDs to filter.
|
||||||
list: []
|
list: []
|
||||||
|
# How to handle direct chats:
|
||||||
|
# If users is "null", direct chats will follow the previous settings.
|
||||||
|
# If users is "true", direct chats will always be bridged.
|
||||||
|
# If users is "false", direct chats will never be bridged.
|
||||||
|
users: true
|
||||||
|
|
||||||
# The prefix for commands. Only required in non-management rooms.
|
# The prefix for commands. Only required in non-management rooms.
|
||||||
command_prefix: "!tg"
|
command_prefix: "!tg"
|
||||||
@@ -545,6 +591,8 @@ telegram:
|
|||||||
# Should incoming updates be handled sequentially to make sure order is preserved on Matrix?
|
# Should incoming updates be handled sequentially to make sure order is preserved on Matrix?
|
||||||
sequential_updates: true
|
sequential_updates: true
|
||||||
exit_on_update_error: false
|
exit_on_update_error: false
|
||||||
|
# Interval to force refresh the connection (full reconnect). 0 disables it.
|
||||||
|
force_refresh_interval_seconds: 0
|
||||||
|
|
||||||
# Telethon connection options.
|
# Telethon connection options.
|
||||||
connection:
|
connection:
|
||||||
@@ -567,6 +615,8 @@ telegram:
|
|||||||
# is not recommended, since some requests can always trigger a call fail (such as searching
|
# is not recommended, since some requests can always trigger a call fail (such as searching
|
||||||
# for messages).
|
# for messages).
|
||||||
request_retries: 5
|
request_retries: 5
|
||||||
|
# Use IPv6 for Telethon connection
|
||||||
|
use_ipv6: false
|
||||||
|
|
||||||
# Device info sent to Telegram.
|
# Device info sent to Telegram.
|
||||||
device_info:
|
device_info:
|
||||||
|
|||||||
@@ -1,2 +1,2 @@
|
|||||||
from .from_matrix import matrix_reply_to_telegram, matrix_to_telegram
|
from .from_matrix import matrix_reply_to_telegram, matrix_to_telegram
|
||||||
from .from_telegram import telegram_to_matrix
|
from .from_telegram import telegram_text_to_matrix_html, telegram_to_matrix
|
||||||
|
|||||||
@@ -82,14 +82,6 @@ class MatrixParser(BaseMatrixParser[TelegramMessage]):
|
|||||||
prefix = "#" * length + " "
|
prefix = "#" * length + " "
|
||||||
return TelegramMessage.join(children, "").prepend(prefix).format(TelegramEntityType.BOLD)
|
return TelegramMessage.join(children, "").prepend(prefix).format(TelegramEntityType.BOLD)
|
||||||
|
|
||||||
async def blockquote_to_fstring(
|
|
||||||
self, node: HTMLNode, ctx: RecursionContext
|
|
||||||
) -> TelegramMessage:
|
|
||||||
msg = await self.tag_aware_parse_node(node, ctx)
|
|
||||||
children = msg.trim().split("\n")
|
|
||||||
children = [child.prepend("> ") for child in children]
|
|
||||||
return TelegramMessage.join(children, "\n")
|
|
||||||
|
|
||||||
async def color_to_fstring(self, msg: TelegramMessage, color: str) -> TelegramMessage:
|
async def color_to_fstring(self, msg: TelegramMessage, color: str) -> TelegramMessage:
|
||||||
return msg
|
return msg
|
||||||
|
|
||||||
|
|||||||
@@ -176,6 +176,21 @@ async def _convert_custom_emoji(
|
|||||||
entities[i] = ReuploadedCustomEmoji(entity, custom_emojis[entity.document_id])
|
entities[i] = ReuploadedCustomEmoji(entity, custom_emojis[entity.document_id])
|
||||||
|
|
||||||
|
|
||||||
|
async def telegram_text_to_matrix_html(
|
||||||
|
source: au.AbstractUser,
|
||||||
|
text: str,
|
||||||
|
entities: list[TypeMessageEntity],
|
||||||
|
client: MautrixTelegramClient | None = None,
|
||||||
|
) -> str:
|
||||||
|
if not entities:
|
||||||
|
return escape(text).replace("\n", "<br/>")
|
||||||
|
await _convert_custom_emoji(source, entities, client=client)
|
||||||
|
text = add_surrogate(text)
|
||||||
|
html = await _telegram_entities_to_matrix_catch(text, entities)
|
||||||
|
html = del_surrogate(html)
|
||||||
|
return html
|
||||||
|
|
||||||
|
|
||||||
async def telegram_to_matrix(
|
async def telegram_to_matrix(
|
||||||
evt: Message | SponsoredMessage,
|
evt: Message | SponsoredMessage,
|
||||||
source: au.AbstractUser,
|
source: au.AbstractUser,
|
||||||
@@ -192,10 +207,10 @@ async def telegram_to_matrix(
|
|||||||
)
|
)
|
||||||
entities = override_entities or evt.entities
|
entities = override_entities or evt.entities
|
||||||
if entities:
|
if entities:
|
||||||
await _convert_custom_emoji(source, entities, client=client)
|
|
||||||
content.format = Format.HTML
|
content.format = Format.HTML
|
||||||
html = await _telegram_entities_to_matrix_catch(add_surrogate(content.body), entities)
|
content.formatted_body = await telegram_text_to_matrix_html(
|
||||||
content.formatted_body = del_surrogate(html)
|
source, content.body, entities, client=client
|
||||||
|
)
|
||||||
|
|
||||||
if require_html:
|
if require_html:
|
||||||
content.ensure_has_html()
|
content.ensure_has_html()
|
||||||
@@ -333,7 +348,11 @@ async def _telegram_entities_to_matrix(
|
|||||||
last_offset = relative_offset + (0 if skip_entity else entity.length)
|
last_offset = relative_offset + (0 if skip_entity else entity.length)
|
||||||
html.append(text_to_html(text[last_offset:]))
|
html.append(text_to_html(text[last_offset:]))
|
||||||
|
|
||||||
return "".join(html)
|
html_string = "".join(html)
|
||||||
|
# Remove redundant <br>'s after block tags
|
||||||
|
html_string = html_string.replace("</blockquote><br/>", "</blockquote>")
|
||||||
|
html_string = html_string.replace("</pre><br/>", "</pre>")
|
||||||
|
return html_string
|
||||||
|
|
||||||
|
|
||||||
def _parse_pre(html: list[str], entity_text: str, language: str) -> bool:
|
def _parse_pre(html: list[str], entity_text: str, language: str) -> bool:
|
||||||
|
|||||||
@@ -61,21 +61,6 @@ class MatrixHandler(BaseMatrixHandler):
|
|||||||
|
|
||||||
self._previously_typing = {}
|
self._previously_typing = {}
|
||||||
|
|
||||||
async def check_versions(self) -> None:
|
|
||||||
await super().check_versions()
|
|
||||||
if self.config["bridge.backfill.msc2716"] and not (
|
|
||||||
support := self.versions.supports("org.matrix.msc2716")
|
|
||||||
):
|
|
||||||
self.log.fatal(
|
|
||||||
"Backfilling with MSC2716 is enabled in bridge config, but "
|
|
||||||
+ (
|
|
||||||
"batch sending is not enabled on homeserver"
|
|
||||||
if support is False
|
|
||||||
else "homeserver does not support batch sending"
|
|
||||||
)
|
|
||||||
)
|
|
||||||
sys.exit(18)
|
|
||||||
|
|
||||||
async def handle_puppet_group_invite(
|
async def handle_puppet_group_invite(
|
||||||
self,
|
self,
|
||||||
room_id: RoomID,
|
room_id: RoomID,
|
||||||
@@ -135,20 +120,8 @@ class MatrixHandler(BaseMatrixHandler):
|
|||||||
levels.users[self.az.bot_mxid] = 100 if invited_by_level >= 100 else invited_by_level
|
levels.users[self.az.bot_mxid] = 100 if invited_by_level >= 100 else invited_by_level
|
||||||
await double_puppet.intent.set_power_levels(room_id, levels)
|
await double_puppet.intent.set_power_levels(room_id, levels)
|
||||||
|
|
||||||
invites, errors = await portal.get_telegram_users_in_matrix_room(
|
|
||||||
invited_by, pre_create=True
|
|
||||||
)
|
|
||||||
if len(errors) > 0:
|
|
||||||
error_list = "\n".join(f"* [{mxid}](https://matrix.to/#/{mxid})" for mxid in errors)
|
|
||||||
await portal.az.intent.send_notice(
|
|
||||||
room_id,
|
|
||||||
f"Failed to add the following users to the chat:\n\n{error_list}\n\n"
|
|
||||||
"You can try `$cmdprefix+sp search -r <username>` to help the bridge find "
|
|
||||||
"those users.",
|
|
||||||
)
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
await portal.create_telegram_chat(invited_by, invites=invites, supergroup=True)
|
await portal.create_telegram_chat(invited_by, supergroup=True)
|
||||||
except ValueError as e:
|
except ValueError as e:
|
||||||
await portal.delete()
|
await portal.delete()
|
||||||
await portal.az.intent.send_notice(room_id, e.args[0])
|
await portal.az.intent.send_notice(room_id, e.args[0])
|
||||||
@@ -182,13 +155,6 @@ class MatrixHandler(BaseMatrixHandler):
|
|||||||
room_id, user.mxid, "You are not whitelisted on this Telegram bridge."
|
room_id, user.mxid, "You are not whitelisted on this Telegram bridge."
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
elif not await user.is_logged_in() and not portal.has_bot:
|
|
||||||
await portal.main_intent.kick_user(
|
|
||||||
room_id,
|
|
||||||
user.mxid,
|
|
||||||
"This chat does not have a bot relaying messages for unauthenticated users.",
|
|
||||||
)
|
|
||||||
return
|
|
||||||
|
|
||||||
self.log.debug(f"{user.mxid} joined {room_id}")
|
self.log.debug(f"{user.mxid} joined {room_id}")
|
||||||
if await user.is_logged_in() or portal.has_bot:
|
if await user.is_logged_in() or portal.has_bot:
|
||||||
|
|||||||
+426
-208
File diff suppressed because it is too large
Load Diff
@@ -34,6 +34,8 @@ from telethon.tl.types import (
|
|||||||
DocumentAttributeVideo,
|
DocumentAttributeVideo,
|
||||||
Game,
|
Game,
|
||||||
InputPhotoFileLocation,
|
InputPhotoFileLocation,
|
||||||
|
InputStickerSetID,
|
||||||
|
InputStickerSetShortName,
|
||||||
Message,
|
Message,
|
||||||
MessageEntityPre,
|
MessageEntityPre,
|
||||||
MessageMediaContact,
|
MessageMediaContact,
|
||||||
@@ -42,12 +44,16 @@ from telethon.tl.types import (
|
|||||||
MessageMediaGame,
|
MessageMediaGame,
|
||||||
MessageMediaGeo,
|
MessageMediaGeo,
|
||||||
MessageMediaGeoLive,
|
MessageMediaGeoLive,
|
||||||
|
MessageMediaInvoice,
|
||||||
MessageMediaPhoto,
|
MessageMediaPhoto,
|
||||||
MessageMediaPoll,
|
MessageMediaPoll,
|
||||||
|
MessageMediaStory,
|
||||||
MessageMediaUnsupported,
|
MessageMediaUnsupported,
|
||||||
MessageMediaVenue,
|
MessageMediaVenue,
|
||||||
MessageMediaWebPage,
|
MessageMediaWebPage,
|
||||||
|
MessageReplyStoryHeader,
|
||||||
PeerChannel,
|
PeerChannel,
|
||||||
|
PeerChat,
|
||||||
PeerUser,
|
PeerUser,
|
||||||
Photo,
|
Photo,
|
||||||
PhotoCachedSize,
|
PhotoCachedSize,
|
||||||
@@ -58,6 +64,8 @@ from telethon.tl.types import (
|
|||||||
Poll,
|
Poll,
|
||||||
TypeDocumentAttribute,
|
TypeDocumentAttribute,
|
||||||
TypePhotoSize,
|
TypePhotoSize,
|
||||||
|
UpdateShortChatMessage,
|
||||||
|
UpdateShortMessage,
|
||||||
WebPage,
|
WebPage,
|
||||||
)
|
)
|
||||||
from telethon.utils import decode_waveform
|
from telethon.utils import decode_waveform
|
||||||
@@ -104,6 +112,7 @@ class DocAttrs(NamedTuple):
|
|||||||
mime_type: str | None
|
mime_type: str | None
|
||||||
is_sticker: bool
|
is_sticker: bool
|
||||||
sticker_alt: str | None
|
sticker_alt: str | None
|
||||||
|
sticker_pack_ref: dict | None
|
||||||
width: int
|
width: int
|
||||||
height: int
|
height: int
|
||||||
is_gif: bool
|
is_gif: bool
|
||||||
@@ -142,6 +151,8 @@ class TelegramMessageConverter:
|
|||||||
MessageMediaUnsupported: self._convert_unsupported,
|
MessageMediaUnsupported: self._convert_unsupported,
|
||||||
MessageMediaGame: self._convert_game,
|
MessageMediaGame: self._convert_game,
|
||||||
MessageMediaContact: self._convert_contact,
|
MessageMediaContact: self._convert_contact,
|
||||||
|
MessageMediaStory: self._convert_story,
|
||||||
|
MessageMediaInvoice: self._convert_invoice,
|
||||||
}
|
}
|
||||||
self._allowed_media = tuple(self._media_converters.keys())
|
self._allowed_media = tuple(self._media_converters.keys())
|
||||||
|
|
||||||
@@ -150,6 +161,7 @@ class TelegramMessageConverter:
|
|||||||
source: au.AbstractUser,
|
source: au.AbstractUser,
|
||||||
intent: IntentAPI,
|
intent: IntentAPI,
|
||||||
is_bot: bool,
|
is_bot: bool,
|
||||||
|
is_channel: bool,
|
||||||
evt: Message,
|
evt: Message,
|
||||||
no_reply_fallback: bool = False,
|
no_reply_fallback: bool = False,
|
||||||
deterministic_reply_id: bool = False,
|
deterministic_reply_id: bool = False,
|
||||||
@@ -158,8 +170,13 @@ class TelegramMessageConverter:
|
|||||||
if not client:
|
if not client:
|
||||||
client = source.client
|
client = source.client
|
||||||
if hasattr(evt, "media") and isinstance(evt.media, self._allowed_media):
|
if hasattr(evt, "media") and isinstance(evt.media, self._allowed_media):
|
||||||
|
if self._should_convert_full_document(evt.media, is_bot, is_channel):
|
||||||
convert_media = self._media_converters[type(evt.media)]
|
convert_media = self._media_converters[type(evt.media)]
|
||||||
converted = await convert_media(source=source, intent=intent, evt=evt, client=client)
|
converted = await convert_media(
|
||||||
|
source=source, intent=intent, evt=evt, client=client
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
converted = await self._convert_document_thumb_only(source, intent, evt, client)
|
||||||
elif evt.message:
|
elif evt.message:
|
||||||
converted = await self._convert_text(source, intent, is_bot, evt, client)
|
converted = await self._convert_text(source, intent, is_bot, evt, client)
|
||||||
else:
|
else:
|
||||||
@@ -192,6 +209,16 @@ class TelegramMessageConverter:
|
|||||||
)
|
)
|
||||||
return converted
|
return converted
|
||||||
|
|
||||||
|
def _should_convert_full_document(self, media, is_bot: bool, is_channel: bool) -> bool:
|
||||||
|
if not isinstance(media, MessageMediaDocument):
|
||||||
|
return True
|
||||||
|
size = media.document.size
|
||||||
|
if is_bot and self.config["bridge.document_as_link_size.bot"]:
|
||||||
|
return size < self.config["bridge.document_as_link_size.bot"] * 1000**2
|
||||||
|
if is_channel and self.config["bridge.document_as_link_size.channel"]:
|
||||||
|
return size < self.config["bridge.document_as_link_size.channel"] * 1000**2
|
||||||
|
return True
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _caption_to_message(converted: ConvertedMessage) -> None:
|
def _caption_to_message(converted: ConvertedMessage) -> None:
|
||||||
content, caption = converted.content, converted.caption
|
content, caption = converted.content, converted.caption
|
||||||
@@ -252,20 +279,56 @@ class TelegramMessageConverter:
|
|||||||
) -> None:
|
) -> None:
|
||||||
if not evt.reply_to:
|
if not evt.reply_to:
|
||||||
return
|
return
|
||||||
|
elif isinstance(evt.reply_to, MessageReplyStoryHeader):
|
||||||
|
return
|
||||||
|
|
||||||
|
if evt.reply_to.quote and content.msgtype and content.msgtype.is_text:
|
||||||
|
content.ensure_has_html()
|
||||||
|
quote_html = await formatter.telegram_text_to_matrix_html(
|
||||||
|
source, evt.reply_to.quote_text, evt.reply_to.quote_entities
|
||||||
|
)
|
||||||
|
content.formatted_body = (
|
||||||
|
f"<blockquote data-telegram-partial-reply>{quote_html}</blockquote>"
|
||||||
|
f"{content.formatted_body}"
|
||||||
|
)
|
||||||
|
|
||||||
space = (
|
space = (
|
||||||
evt.peer_id.channel_id
|
evt.peer_id.channel_id
|
||||||
if isinstance(evt, Message) and isinstance(evt.peer_id, PeerChannel)
|
if isinstance(evt, Message) and isinstance(evt.peer_id, PeerChannel)
|
||||||
else source.tgid
|
else source.tgid
|
||||||
)
|
)
|
||||||
|
if isinstance(evt, Message):
|
||||||
|
evt_peer_id = evt.peer_id
|
||||||
|
elif isinstance(evt, UpdateShortMessage):
|
||||||
|
evt_peer_id = PeerUser(evt.user_id)
|
||||||
|
elif isinstance(evt, UpdateShortChatMessage):
|
||||||
|
evt_peer_id = PeerChat(evt.chat_id)
|
||||||
|
else:
|
||||||
|
evt_peer_id = None
|
||||||
|
if evt.reply_to.reply_to_peer_id and evt.reply_to.reply_to_peer_id != evt_peer_id:
|
||||||
|
if not self.config["bridge.cross_room_replies"]:
|
||||||
|
return
|
||||||
|
space = (
|
||||||
|
evt.reply_to.reply_to_peer_id.channel_id
|
||||||
|
if isinstance(evt.reply_to.reply_to_peer_id, PeerChannel)
|
||||||
|
else source.tgid
|
||||||
|
)
|
||||||
|
|
||||||
reply_to_id = TelegramID(evt.reply_to.reply_to_msg_id)
|
reply_to_id = TelegramID(evt.reply_to.reply_to_msg_id)
|
||||||
msg = await DBMessage.get_one_by_tgid(reply_to_id, space)
|
msg = await DBMessage.get_one_by_tgid(reply_to_id, space)
|
||||||
if not msg or msg.mx_room != self.portal.mxid:
|
no_fallback = no_fallback or self.config["bridge.disable_reply_fallbacks"]
|
||||||
|
if not msg:
|
||||||
|
# TODO try to find room ID when generating deterministic ID for cross-room reply
|
||||||
if deterministic_id:
|
if deterministic_id:
|
||||||
content.set_reply(self.deterministic_event_id(space, reply_to_id))
|
content.set_reply(self.deterministic_event_id(space, reply_to_id))
|
||||||
return
|
return
|
||||||
|
elif msg.mx_room != self.portal.mxid and not self.config["bridge.cross_room_replies"]:
|
||||||
|
return
|
||||||
elif not isinstance(content, TextMessageEventContent) or no_fallback:
|
elif not isinstance(content, TextMessageEventContent) or no_fallback:
|
||||||
# Not a text message, just set the reply metadata and return
|
# Not a text message, just set the reply metadata and return
|
||||||
content.set_reply(msg.mxid)
|
content.set_reply(msg.mxid)
|
||||||
|
if msg.mx_room != self.portal.mxid:
|
||||||
|
content.relates_to.in_reply_to["room_id"] = msg.mx_room
|
||||||
return
|
return
|
||||||
|
|
||||||
# Text message, try to fetch original message to generate reply fallback.
|
# Text message, try to fetch original message to generate reply fallback.
|
||||||
@@ -280,6 +343,8 @@ class TelegramMessageConverter:
|
|||||||
except Exception:
|
except Exception:
|
||||||
self.log.exception("Failed to get event to add reply fallback")
|
self.log.exception("Failed to get event to add reply fallback")
|
||||||
content.set_reply(msg.mxid)
|
content.set_reply(msg.mxid)
|
||||||
|
if msg.mx_room != self.portal.mxid:
|
||||||
|
content.relates_to.in_reply_to["room_id"] = msg.mx_room
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _photo_size_key(photo: TypePhotoSize) -> int:
|
def _photo_size_key(photo: TypePhotoSize) -> int:
|
||||||
@@ -428,7 +493,104 @@ class TelegramMessageConverter:
|
|||||||
return ConvertedMessage(
|
return ConvertedMessage(
|
||||||
content=content,
|
content=content,
|
||||||
caption=caption_content,
|
caption=caption_content,
|
||||||
disappear_seconds=media.ttl_seconds,
|
disappear_seconds=self._adjust_ttl(media.ttl_seconds),
|
||||||
|
)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _adjust_ttl(ttl: int | None) -> int | None:
|
||||||
|
if not ttl:
|
||||||
|
return None
|
||||||
|
elif ttl == 2147483647:
|
||||||
|
# View-once media, set low TTL
|
||||||
|
return 15
|
||||||
|
else:
|
||||||
|
# Increase media TTL because it's supposed to be counted from opening the media,
|
||||||
|
# but we can only count it from read receipt.
|
||||||
|
return ttl * 5
|
||||||
|
|
||||||
|
async def _convert_document_thumb_only(
|
||||||
|
self,
|
||||||
|
source: au.AbstractUser,
|
||||||
|
intent: IntentAPI,
|
||||||
|
evt: Message,
|
||||||
|
client: MautrixTelegramClient,
|
||||||
|
) -> ConvertedMessage | None:
|
||||||
|
document = evt.media.document
|
||||||
|
|
||||||
|
if not document:
|
||||||
|
return None
|
||||||
|
|
||||||
|
external_link_content = "Unsupported file, please access directly on Telegram"
|
||||||
|
|
||||||
|
external_url = self._get_external_url(evt)
|
||||||
|
# We don't generate external URLs for bot users so only set if known
|
||||||
|
if external_url is not None:
|
||||||
|
external_link_content = (
|
||||||
|
f"Unsupported file, please access directly on Telegram here: {external_url}"
|
||||||
|
)
|
||||||
|
|
||||||
|
attrs = _parse_document_attributes(document.attributes)
|
||||||
|
file = None
|
||||||
|
|
||||||
|
thumb_loc, thumb_size = self.get_largest_photo_size(document)
|
||||||
|
if thumb_size and not isinstance(thumb_size, (PhotoSize, PhotoCachedSize)):
|
||||||
|
self.log.debug(f"Unsupported thumbnail type {type(thumb_size)}")
|
||||||
|
thumb_loc = None
|
||||||
|
thumb_size = None
|
||||||
|
if thumb_loc:
|
||||||
|
try:
|
||||||
|
file = await util.transfer_thumbnail_to_matrix(
|
||||||
|
client,
|
||||||
|
intent,
|
||||||
|
thumb_loc,
|
||||||
|
video=None,
|
||||||
|
mime_type=document.mime_type,
|
||||||
|
encrypt=self.portal.encrypted,
|
||||||
|
async_upload=self.config["homeserver.async_media"],
|
||||||
|
)
|
||||||
|
except Exception:
|
||||||
|
self.log.exception("Failed to transfer thumbnail")
|
||||||
|
if not file:
|
||||||
|
name = attrs.name or ""
|
||||||
|
caption = f"\n{evt.message}" if evt.message else ""
|
||||||
|
return ConvertedMessage(
|
||||||
|
content=TextMessageEventContent(
|
||||||
|
msgtype=MessageType.NOTICE,
|
||||||
|
body=f"{name}{caption}\n{external_link_content}",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
info, name = _parse_document_meta(evt, file, attrs, thumb_size)
|
||||||
|
|
||||||
|
event_type = EventType.ROOM_MESSAGE
|
||||||
|
if not name:
|
||||||
|
ext = sane_mimetypes.guess_extension(file.mime_type) or ""
|
||||||
|
name = "unnamed_file" + ext
|
||||||
|
|
||||||
|
content = MediaMessageEventContent(
|
||||||
|
body=name,
|
||||||
|
info=info,
|
||||||
|
msgtype={
|
||||||
|
"video/": MessageType.VIDEO,
|
||||||
|
"audio/": MessageType.AUDIO,
|
||||||
|
"image/": MessageType.IMAGE,
|
||||||
|
}.get(info.mimetype[:6], MessageType.FILE),
|
||||||
|
)
|
||||||
|
if file.decryption_info:
|
||||||
|
content.file = file.decryption_info
|
||||||
|
else:
|
||||||
|
content.url = file.mxc
|
||||||
|
|
||||||
|
caption_content = (
|
||||||
|
await formatter.telegram_to_matrix(evt, source, client) if evt.message else None
|
||||||
|
)
|
||||||
|
caption_content = f"{caption_content}\n{external_link_content}"
|
||||||
|
|
||||||
|
return ConvertedMessage(
|
||||||
|
type=event_type,
|
||||||
|
content=content,
|
||||||
|
caption=caption_content,
|
||||||
|
disappear_seconds=self._adjust_ttl(evt.media.ttl_seconds),
|
||||||
)
|
)
|
||||||
|
|
||||||
async def _convert_document(
|
async def _convert_document(
|
||||||
@@ -440,6 +602,9 @@ class TelegramMessageConverter:
|
|||||||
) -> ConvertedMessage | None:
|
) -> ConvertedMessage | None:
|
||||||
document = evt.media.document
|
document = evt.media.document
|
||||||
|
|
||||||
|
if not document:
|
||||||
|
return None
|
||||||
|
|
||||||
attrs = _parse_document_attributes(document.attributes)
|
attrs = _parse_document_attributes(document.attributes)
|
||||||
|
|
||||||
if document.size > self.matrix.media_config.upload_size:
|
if document.size > self.matrix.media_config.upload_size:
|
||||||
@@ -496,6 +661,7 @@ class TelegramMessageConverter:
|
|||||||
info["fi.mau.telegram.gif"] = True
|
info["fi.mau.telegram.gif"] = True
|
||||||
else:
|
else:
|
||||||
info["fi.mau.telegram.animated_sticker"] = True
|
info["fi.mau.telegram.animated_sticker"] = True
|
||||||
|
info["fi.mau.gif"] = True
|
||||||
info["fi.mau.loop"] = True
|
info["fi.mau.loop"] = True
|
||||||
info["fi.mau.autoplay"] = True
|
info["fi.mau.autoplay"] = True
|
||||||
info["fi.mau.hide_controls"] = True
|
info["fi.mau.hide_controls"] = True
|
||||||
@@ -536,7 +702,7 @@ class TelegramMessageConverter:
|
|||||||
type=event_type,
|
type=event_type,
|
||||||
content=content,
|
content=content,
|
||||||
caption=caption_content,
|
caption=caption_content,
|
||||||
disappear_seconds=evt.media.ttl_seconds,
|
disappear_seconds=self._adjust_ttl(evt.media.ttl_seconds),
|
||||||
)
|
)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
@@ -597,18 +763,18 @@ class TelegramMessageConverter:
|
|||||||
_n += 1
|
_n += 1
|
||||||
return _n
|
return _n
|
||||||
|
|
||||||
text_answers = "\n".join(f"{n()}. {answer.text}" for answer in poll.answers)
|
text_answers = "\n".join(f"{n()}. {answer.text.text}" for answer in poll.answers)
|
||||||
html_answers = "\n".join(f"<li>{answer.text}</li>" for answer in poll.answers)
|
html_answers = "\n".join(f"<li>{answer.text.text}</li>" for answer in poll.answers)
|
||||||
vote_command = f"{self.command_prefix} vote {poll_id}"
|
vote_command = f"{self.command_prefix} vote {poll_id}"
|
||||||
content = TextMessageEventContent(
|
content = TextMessageEventContent(
|
||||||
msgtype=MessageType.TEXT,
|
msgtype=MessageType.TEXT,
|
||||||
format=Format.HTML,
|
format=Format.HTML,
|
||||||
body=(
|
body=(
|
||||||
f"Poll: {poll.question}\n{text_answers}\n"
|
f"Poll: {poll.question.text}\n{text_answers}\n"
|
||||||
f"Vote with {vote_command} <choice number>"
|
f"Vote with {vote_command} <choice number>"
|
||||||
),
|
),
|
||||||
formatted_body=(
|
formatted_body=(
|
||||||
f"<strong>Poll</strong>: {poll.question}<br/>\n"
|
f"<strong>Poll</strong>: {poll.question.text}<br/>\n"
|
||||||
f"<ol>{html_answers}</ol>\n"
|
f"<ol>{html_answers}</ol>\n"
|
||||||
f"Vote with <code>{vote_command} <choice number></code>"
|
f"Vote with <code>{vote_command} <choice number></code>"
|
||||||
),
|
),
|
||||||
@@ -698,10 +864,33 @@ class TelegramMessageConverter:
|
|||||||
)
|
)
|
||||||
return ConvertedMessage(content=content)
|
return ConvertedMessage(content=content)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
async def _convert_story(
|
||||||
|
source: au.AbstractUser, evt: Message, client: MautrixTelegramClient, **_
|
||||||
|
) -> ConvertedMessage:
|
||||||
|
content = await formatter.telegram_to_matrix(
|
||||||
|
evt, source, client, override_text="Stories are not yet supported"
|
||||||
|
)
|
||||||
|
content.msgtype = MessageType.NOTICE
|
||||||
|
content["fi.mau.telegram.unsupported"] = True
|
||||||
|
return ConvertedMessage(content=content)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
async def _convert_invoice(
|
||||||
|
source: au.AbstractUser, evt: Message, client: MautrixTelegramClient, **_
|
||||||
|
) -> ConvertedMessage:
|
||||||
|
content = await formatter.telegram_to_matrix(
|
||||||
|
evt, source, client, override_text="Invoices are not yet supported"
|
||||||
|
)
|
||||||
|
content.msgtype = MessageType.NOTICE
|
||||||
|
content["fi.mau.telegram.unsupported"] = True
|
||||||
|
return ConvertedMessage(content=content)
|
||||||
|
|
||||||
|
|
||||||
def _parse_document_attributes(attributes: list[TypeDocumentAttribute]) -> DocAttrs:
|
def _parse_document_attributes(attributes: list[TypeDocumentAttribute]) -> DocAttrs:
|
||||||
name, mime_type, is_sticker, sticker_alt, width, height = None, None, False, None, 0, 0
|
name, mime_type, is_sticker, sticker_alt, width, height = None, None, False, None, 0, 0
|
||||||
is_gif, is_audio, is_voice, duration, waveform = False, False, False, 0, bytes()
|
is_gif, is_audio, is_voice, duration, waveform = False, False, False, 0, bytes()
|
||||||
|
sticker_pack_ref = None
|
||||||
for attr in attributes:
|
for attr in attributes:
|
||||||
if isinstance(attr, DocumentAttributeFilename):
|
if isinstance(attr, DocumentAttributeFilename):
|
||||||
name = name or attr.file_name
|
name = name or attr.file_name
|
||||||
@@ -709,6 +898,13 @@ def _parse_document_attributes(attributes: list[TypeDocumentAttribute]) -> DocAt
|
|||||||
elif isinstance(attr, DocumentAttributeSticker):
|
elif isinstance(attr, DocumentAttributeSticker):
|
||||||
is_sticker = True
|
is_sticker = True
|
||||||
sticker_alt = attr.alt
|
sticker_alt = attr.alt
|
||||||
|
if isinstance(attr.stickerset, InputStickerSetID):
|
||||||
|
sticker_pack_ref = {
|
||||||
|
"id": str(attr.stickerset.id),
|
||||||
|
"access_hash": str(attr.stickerset.access_hash),
|
||||||
|
}
|
||||||
|
elif isinstance(attr.stickerset, InputStickerSetShortName):
|
||||||
|
sticker_pack_ref = {"short_name": attr.stickerset.short_name}
|
||||||
elif isinstance(attr, DocumentAttributeAnimated):
|
elif isinstance(attr, DocumentAttributeAnimated):
|
||||||
is_gif = True
|
is_gif = True
|
||||||
elif isinstance(attr, DocumentAttributeVideo):
|
elif isinstance(attr, DocumentAttributeVideo):
|
||||||
@@ -722,17 +918,18 @@ def _parse_document_attributes(attributes: list[TypeDocumentAttribute]) -> DocAt
|
|||||||
waveform = decode_waveform(attr.waveform) if attr.waveform else b""
|
waveform = decode_waveform(attr.waveform) if attr.waveform else b""
|
||||||
|
|
||||||
return DocAttrs(
|
return DocAttrs(
|
||||||
name,
|
name=name,
|
||||||
mime_type,
|
mime_type=mime_type,
|
||||||
is_sticker,
|
is_sticker=is_sticker,
|
||||||
sticker_alt,
|
sticker_alt=sticker_alt,
|
||||||
width,
|
sticker_pack_ref=sticker_pack_ref,
|
||||||
height,
|
width=width,
|
||||||
is_gif,
|
height=height,
|
||||||
is_audio,
|
is_gif=is_gif,
|
||||||
is_voice,
|
is_audio=is_audio,
|
||||||
duration,
|
is_voice=is_voice,
|
||||||
waveform,
|
duration=duration,
|
||||||
|
waveform=waveform,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -758,6 +955,13 @@ def _parse_document_meta(
|
|||||||
mime_type = file.mime_type or document.mime_type
|
mime_type = file.mime_type or document.mime_type
|
||||||
info = ImageInfo(size=file.size, mimetype=mime_type)
|
info = ImageInfo(size=file.size, mimetype=mime_type)
|
||||||
|
|
||||||
|
if attrs.is_sticker:
|
||||||
|
info["fi.mau.telegram.sticker"] = {
|
||||||
|
"alt": attrs.sticker_alt,
|
||||||
|
"id": str(document.id),
|
||||||
|
"pack": attrs.sticker_pack_ref,
|
||||||
|
}
|
||||||
|
|
||||||
if attrs.mime_type and not file.was_converted:
|
if attrs.mime_type and not file.was_converted:
|
||||||
file.mime_type = attrs.mime_type or file.mime_type
|
file.mime_type = attrs.mime_type or file.mime_type
|
||||||
if file.width and file.height:
|
if file.width and file.height:
|
||||||
@@ -777,6 +981,10 @@ def _parse_document_meta(
|
|||||||
size=file.thumbnail.size,
|
size=file.thumbnail.size,
|
||||||
)
|
)
|
||||||
elif attrs.is_sticker:
|
elif attrs.is_sticker:
|
||||||
|
if not info.width or not info.height:
|
||||||
|
info.width = 256
|
||||||
|
info.height = 256
|
||||||
|
|
||||||
# This is a hack for bad clients like Element iOS that require a thumbnail
|
# This is a hack for bad clients like Element iOS that require a thumbnail
|
||||||
info.thumbnail_info = ImageInfo.deserialize(info.serialize())
|
info.thumbnail_info = ImageInfo.deserialize(info.serialize())
|
||||||
if file.decryption_info:
|
if file.decryption_info:
|
||||||
|
|||||||
@@ -83,9 +83,11 @@ def get_base_power_levels(
|
|||||||
levels.users_default = overrides.get("users_default", 0)
|
levels.users_default = overrides.get("users_default", 0)
|
||||||
levels.events_default = overrides.get(
|
levels.events_default = overrides.get(
|
||||||
"events_default",
|
"events_default",
|
||||||
|
(
|
||||||
50
|
50
|
||||||
if portal.peer_type == "channel" and not portal.megagroup or dbr.send_messages
|
if portal.peer_type == "channel" and not portal.megagroup or dbr.send_messages
|
||||||
else 0,
|
else 0
|
||||||
|
),
|
||||||
)
|
)
|
||||||
for evt_type, value in overrides.get("events", {}).items():
|
for evt_type, value in overrides.get("events", {}).items():
|
||||||
levels.events[EventType.find(evt_type)] = value
|
levels.events[EventType.find(evt_type)] = value
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ from __future__ import annotations
|
|||||||
import base64
|
import base64
|
||||||
import html
|
import html
|
||||||
|
|
||||||
from telethon.tl.functions.channels import GetSponsoredMessagesRequest
|
from telethon.tl.functions.messages import GetSponsoredMessagesRequest
|
||||||
from telethon.tl.types import Channel, InputChannel, PeerChannel, PeerUser, SponsoredMessage, User
|
from telethon.tl.types import Channel, InputChannel, PeerChannel, PeerUser, SponsoredMessage, User
|
||||||
from telethon.tl.types.messages import SponsoredMessages, SponsoredMessagesEmpty
|
from telethon.tl.types.messages import SponsoredMessages, SponsoredMessagesEmpty
|
||||||
|
|
||||||
|
|||||||
@@ -55,6 +55,7 @@ if TYPE_CHECKING:
|
|||||||
|
|
||||||
|
|
||||||
class Puppet(DBPuppet, BasePuppet):
|
class Puppet(DBPuppet, BasePuppet):
|
||||||
|
bridge: TelegramBridge
|
||||||
config: Config
|
config: Config
|
||||||
hs_domain: str
|
hs_domain: str
|
||||||
mxid_template: SimpleTemplate[TelegramID]
|
mxid_template: SimpleTemplate[TelegramID]
|
||||||
@@ -78,6 +79,7 @@ class Puppet(DBPuppet, BasePuppet):
|
|||||||
avatar_url: ContentURI | None = None,
|
avatar_url: ContentURI | None = None,
|
||||||
name_set: bool = False,
|
name_set: bool = False,
|
||||||
avatar_set: bool = False,
|
avatar_set: bool = False,
|
||||||
|
contact_info_set: bool = False,
|
||||||
is_bot: bool = False,
|
is_bot: bool = False,
|
||||||
is_channel: bool = False,
|
is_channel: bool = False,
|
||||||
is_premium: bool = False,
|
is_premium: bool = False,
|
||||||
@@ -100,6 +102,7 @@ class Puppet(DBPuppet, BasePuppet):
|
|||||||
avatar_url=avatar_url,
|
avatar_url=avatar_url,
|
||||||
name_set=name_set,
|
name_set=name_set,
|
||||||
avatar_set=avatar_set,
|
avatar_set=avatar_set,
|
||||||
|
contact_info_set=contact_info_set,
|
||||||
is_bot=is_bot,
|
is_bot=is_bot,
|
||||||
is_channel=is_channel,
|
is_channel=is_channel,
|
||||||
is_premium=is_premium,
|
is_premium=is_premium,
|
||||||
@@ -154,6 +157,7 @@ class Puppet(DBPuppet, BasePuppet):
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def init_cls(cls, bridge: "TelegramBridge") -> AsyncIterable[Awaitable[None]]:
|
def init_cls(cls, bridge: "TelegramBridge") -> AsyncIterable[Awaitable[None]]:
|
||||||
|
cls.bridge = bridge
|
||||||
cls.config = bridge.config
|
cls.config = bridge.config
|
||||||
cls.loop = bridge.loop
|
cls.loop = bridge.loop
|
||||||
cls.mx = bridge.matrix
|
cls.mx = bridge.matrix
|
||||||
@@ -265,11 +269,14 @@ class Puppet(DBPuppet, BasePuppet):
|
|||||||
is_bot != self.is_bot or is_channel != self.is_channel or is_premium != self.is_premium
|
is_bot != self.is_bot or is_channel != self.is_channel or is_premium != self.is_premium
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if is_bot is not None:
|
||||||
self.is_bot = is_bot
|
self.is_bot = is_bot
|
||||||
self.is_channel = is_channel
|
self.is_channel = is_channel
|
||||||
|
if is_premium is not None:
|
||||||
self.is_premium = is_premium
|
self.is_premium = is_premium
|
||||||
|
|
||||||
if self.username != info.username:
|
if self.username != info.username and (info.username or not info.min):
|
||||||
|
self.log.debug(f"Updating username {self.username} -> {info.username}")
|
||||||
self.username = info.username
|
self.username = info.username
|
||||||
changed = True
|
changed = True
|
||||||
|
|
||||||
@@ -279,6 +286,8 @@ class Puppet(DBPuppet, BasePuppet):
|
|||||||
|
|
||||||
if not self.disable_updates:
|
if not self.disable_updates:
|
||||||
try:
|
try:
|
||||||
|
changed = await self._update_contact_info(force=changed) or changed
|
||||||
|
|
||||||
changed = (
|
changed = (
|
||||||
await self.update_displayname(source, info, client_override=client_override)
|
await self.update_displayname(source, info, client_override=client_override)
|
||||||
or changed
|
or changed
|
||||||
@@ -296,8 +305,37 @@ class Puppet(DBPuppet, BasePuppet):
|
|||||||
await self.update_portals_meta()
|
await self.update_portals_meta()
|
||||||
await self.save()
|
await self.save()
|
||||||
|
|
||||||
|
async def _update_contact_info(self, force: bool = False) -> bool:
|
||||||
|
if not self.bridge.homeserver_software.is_hungry:
|
||||||
|
return False
|
||||||
|
|
||||||
|
if self.contact_info_set and not force:
|
||||||
|
return False
|
||||||
|
|
||||||
|
try:
|
||||||
|
identifiers = []
|
||||||
|
if self.username:
|
||||||
|
identifiers.append(f"telegram:{self.username}")
|
||||||
|
if self.phone:
|
||||||
|
phone = "+" + self.phone.lstrip("+")
|
||||||
|
identifiers.append(f"tel:{phone}")
|
||||||
|
await self.default_mxid_intent.beeper_update_profile(
|
||||||
|
{
|
||||||
|
"com.beeper.bridge.identifiers": identifiers,
|
||||||
|
"com.beeper.bridge.remote_id": str(self.tgid),
|
||||||
|
"com.beeper.bridge.service": "telegram",
|
||||||
|
"com.beeper.bridge.network": "telegram",
|
||||||
|
"com.beeper.bridge.is_network_bot": self.is_bot,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
self.contact_info_set = True
|
||||||
|
except Exception:
|
||||||
|
self.log.exception("Error updating contact info")
|
||||||
|
self.contact_info_set = False
|
||||||
|
return True
|
||||||
|
|
||||||
async def update_portals_meta(self) -> None:
|
async def update_portals_meta(self) -> None:
|
||||||
if not p.Portal.private_chat_portal_meta and not self.mx.e2ee:
|
if p.Portal.private_chat_portal_meta != "always" and not self.mx.e2ee:
|
||||||
return
|
return
|
||||||
async for portal in p.Portal.find_private_chats_with(self.tgid):
|
async for portal in p.Portal.find_private_chats_with(self.tgid):
|
||||||
await portal.update_info_from_puppet(self)
|
await portal.update_info_from_puppet(self)
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ from telethon.tl.patched import Message
|
|||||||
from telethon.tl.types import (
|
from telethon.tl.types import (
|
||||||
InputMediaUploadedDocument,
|
InputMediaUploadedDocument,
|
||||||
InputMediaUploadedPhoto,
|
InputMediaUploadedPhoto,
|
||||||
|
InputReplyToMessage,
|
||||||
TypeDocumentAttribute,
|
TypeDocumentAttribute,
|
||||||
TypeInputMedia,
|
TypeInputMedia,
|
||||||
TypeInputPeer,
|
TypeInputPeer,
|
||||||
@@ -67,6 +68,10 @@ class MautrixTelegramClient(TelegramClient):
|
|||||||
entity = await self.get_input_entity(entity)
|
entity = await self.get_input_entity(entity)
|
||||||
reply_to = utils.get_message_id(reply_to)
|
reply_to = utils.get_message_id(reply_to)
|
||||||
request = SendMediaRequest(
|
request = SendMediaRequest(
|
||||||
entity, media, message=caption or "", entities=entities or [], reply_to_msg_id=reply_to
|
entity,
|
||||||
|
media,
|
||||||
|
message=caption or "",
|
||||||
|
entities=entities or [],
|
||||||
|
reply_to=InputReplyToMessage(reply_to_msg_id=reply_to) if reply_to else None,
|
||||||
)
|
)
|
||||||
return self._get_response_message(request, await self(request), entity)
|
return self._get_response_message(request, await self(request), entity)
|
||||||
|
|||||||
+75
-23
@@ -23,6 +23,7 @@ import time
|
|||||||
from telethon.errors import (
|
from telethon.errors import (
|
||||||
AuthKeyDuplicatedError,
|
AuthKeyDuplicatedError,
|
||||||
AuthKeyError,
|
AuthKeyError,
|
||||||
|
AuthKeyNotFound,
|
||||||
RPCError,
|
RPCError,
|
||||||
TakeoutInitDelayError,
|
TakeoutInitDelayError,
|
||||||
UnauthorizedError,
|
UnauthorizedError,
|
||||||
@@ -39,6 +40,9 @@ from telethon.tl.types import (
|
|||||||
ChatForbidden,
|
ChatForbidden,
|
||||||
InputUserSelf,
|
InputUserSelf,
|
||||||
Message,
|
Message,
|
||||||
|
MessageActionContactSignUp,
|
||||||
|
MessageActionHistoryClear,
|
||||||
|
MessageService,
|
||||||
NotifyPeer,
|
NotifyPeer,
|
||||||
PeerUser,
|
PeerUser,
|
||||||
TypeUpdate,
|
TypeUpdate,
|
||||||
@@ -52,6 +56,7 @@ from telethon.tl.types import (
|
|||||||
User as TLUser,
|
User as TLUser,
|
||||||
)
|
)
|
||||||
from telethon.tl.types.contacts import ContactsNotModified
|
from telethon.tl.types.contacts import ContactsNotModified
|
||||||
|
from telethon.tl.types.help import AppConfig
|
||||||
from telethon.tl.types.messages import AvailableReactions
|
from telethon.tl.types.messages import AvailableReactions
|
||||||
|
|
||||||
from mautrix.appservice import DOUBLE_PUPPET_SOURCE_KEY
|
from mautrix.appservice import DOUBLE_PUPPET_SOURCE_KEY
|
||||||
@@ -106,6 +111,7 @@ class User(DBUser, AbstractUser, BaseUser):
|
|||||||
_available_emoji_reactions_fetched: float
|
_available_emoji_reactions_fetched: float
|
||||||
_available_emoji_reactions_lock: asyncio.Lock
|
_available_emoji_reactions_lock: asyncio.Lock
|
||||||
_app_config: dict[str, Any] | None
|
_app_config: dict[str, Any] | None
|
||||||
|
_app_config_hash: int
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
@@ -143,6 +149,7 @@ class User(DBUser, AbstractUser, BaseUser):
|
|||||||
self._available_emoji_reactions_fetched = 0
|
self._available_emoji_reactions_fetched = 0
|
||||||
self._available_emoji_reactions_lock = asyncio.Lock()
|
self._available_emoji_reactions_lock = asyncio.Lock()
|
||||||
self._app_config = None
|
self._app_config = None
|
||||||
|
self._app_config_hash = 0
|
||||||
|
|
||||||
(
|
(
|
||||||
self.relaybot_whitelisted,
|
self.relaybot_whitelisted,
|
||||||
@@ -209,7 +216,7 @@ class User(DBUser, AbstractUser, BaseUser):
|
|||||||
async with self._ensure_started_lock:
|
async with self._ensure_started_lock:
|
||||||
return cast(User, await super().ensure_started(even_if_no_session))
|
return cast(User, await super().ensure_started(even_if_no_session))
|
||||||
|
|
||||||
async def on_signed_out(self, err: UnauthorizedError | AuthKeyError) -> None:
|
async def on_signed_out(self, err: UnauthorizedError | AuthKeyError | AuthKeyNotFound) -> None:
|
||||||
error_code = "tg-auth-error"
|
error_code = "tg-auth-error"
|
||||||
if isinstance(err, AuthKeyDuplicatedError):
|
if isinstance(err, AuthKeyDuplicatedError):
|
||||||
error_code = "tg-auth-key-duplicated"
|
error_code = "tg-auth-key-duplicated"
|
||||||
@@ -230,8 +237,8 @@ class User(DBUser, AbstractUser, BaseUser):
|
|||||||
async def start(self, delete_unless_authenticated: bool = False) -> User:
|
async def start(self, delete_unless_authenticated: bool = False) -> User:
|
||||||
try:
|
try:
|
||||||
await super().start()
|
await super().start()
|
||||||
except AuthKeyDuplicatedError as e:
|
except (AuthKeyDuplicatedError, AuthKeyNotFound) as e:
|
||||||
self.log.warning("Got AuthKeyDuplicatedError in start()")
|
self.log.warning(f"Got {type(e).__name__} in start()")
|
||||||
await self.on_signed_out(e)
|
await self.on_signed_out(e)
|
||||||
if not delete_unless_authenticated:
|
if not delete_unless_authenticated:
|
||||||
# The caller wants the client to be connected, so restart the connection.
|
# The caller wants the client to be connected, so restart the connection.
|
||||||
@@ -291,9 +298,11 @@ class User(DBUser, AbstractUser, BaseUser):
|
|||||||
self._track_metric(METRIC_CONNECTED, connected)
|
self._track_metric(METRIC_CONNECTED, connected)
|
||||||
if connected:
|
if connected:
|
||||||
await self.push_bridge_state(
|
await self.push_bridge_state(
|
||||||
|
(
|
||||||
BridgeStateEvent.BACKFILLING
|
BridgeStateEvent.BACKFILLING
|
||||||
if self._is_backfilling
|
if self._is_backfilling
|
||||||
else BridgeStateEvent.CONNECTED,
|
else BridgeStateEvent.CONNECTED
|
||||||
|
),
|
||||||
info=self._bridge_state_info,
|
info=self._bridge_state_info,
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
@@ -487,13 +496,16 @@ class User(DBUser, AbstractUser, BaseUser):
|
|||||||
self.log.info(f"Creating portal for {portal.tgid_log} as part of backfill loop")
|
self.log.info(f"Creating portal for {portal.tgid_log} as part of backfill loop")
|
||||||
try:
|
try:
|
||||||
await portal.create_matrix_room(
|
await portal.create_matrix_room(
|
||||||
self, client=client, update_if_exists=False, invites=[self.mxid]
|
self,
|
||||||
|
client=client,
|
||||||
|
update_if_exists=False,
|
||||||
|
invites=[self.mxid],
|
||||||
|
from_dialog_sync=True,
|
||||||
)
|
)
|
||||||
except Exception:
|
except Exception:
|
||||||
self.log.exception(f"Error while creating {portal.tgid_log}")
|
self.log.exception(f"Error while creating {portal.tgid_log}")
|
||||||
else:
|
else:
|
||||||
puppet = await pu.Puppet.get_by_custom_mxid(self.mxid)
|
await self.post_sync_dialog(portal, puppet=None, was_created=True, **post_sync_args)
|
||||||
await self._post_sync_dialog(portal, puppet, was_created=True, **post_sync_args)
|
|
||||||
|
|
||||||
async def update(self, update: TypeUpdate) -> bool:
|
async def update(self, update: TypeUpdate) -> bool:
|
||||||
if not self.is_bot:
|
if not self.is_bot:
|
||||||
@@ -535,7 +547,7 @@ class User(DBUser, AbstractUser, BaseUser):
|
|||||||
await self.stop()
|
await self.stop()
|
||||||
return None
|
return None
|
||||||
|
|
||||||
async def update_info(self, info: TLUser = None) -> None:
|
async def update_info(self, info: TLUser | None = None) -> None:
|
||||||
if not info:
|
if not info:
|
||||||
info = await self.get_me()
|
info = await self.get_me()
|
||||||
if not info:
|
if not info:
|
||||||
@@ -608,7 +620,10 @@ class User(DBUser, AbstractUser, BaseUser):
|
|||||||
await self.stop()
|
await self.stop()
|
||||||
await sess.delete()
|
await sess.delete()
|
||||||
|
|
||||||
|
# Drop LOGGED_OUT states if the user was already logged out previously
|
||||||
|
# and doesn't have a remote ID anymore
|
||||||
# TODO send a management room notice for non-manual logouts?
|
# TODO send a management room notice for non-manual logouts?
|
||||||
|
if self.tgid or state != BridgeStateEvent.LOGGED_OUT:
|
||||||
await self.push_bridge_state(state, error=error, message=message)
|
await self.push_bridge_state(state, error=error, message=message)
|
||||||
if delete:
|
if delete:
|
||||||
await self.delete()
|
await self.delete()
|
||||||
@@ -743,12 +758,12 @@ class User(DBUser, AbstractUser, BaseUser):
|
|||||||
)
|
)
|
||||||
await self._mute_room(puppet, portal, update.notify_settings.mute_until.timestamp())
|
await self._mute_room(puppet, portal, update.notify_settings.mute_until.timestamp())
|
||||||
|
|
||||||
async def _sync_dialog(
|
@staticmethod
|
||||||
self, portal: po.Portal, dialog: Dialog, should_create: bool, puppet: pu.Puppet | None
|
def dialog_to_sync_args(dialog: Dialog) -> dict:
|
||||||
) -> None:
|
return {
|
||||||
was_created = False
|
"last_message_ts": (
|
||||||
post_sync_args = {
|
cast(datetime, dialog.date).timestamp() if dialog.date else time.time()
|
||||||
"last_message_ts": cast(datetime, dialog.date).timestamp(),
|
),
|
||||||
"unread_count": dialog.unread_count,
|
"unread_count": dialog.unread_count,
|
||||||
"max_read_id": dialog.dialog.read_inbox_max_id,
|
"max_read_id": dialog.dialog.read_inbox_max_id,
|
||||||
"mute_until": (
|
"mute_until": (
|
||||||
@@ -759,6 +774,24 @@ class User(DBUser, AbstractUser, BaseUser):
|
|||||||
"pinned": dialog.pinned,
|
"pinned": dialog.pinned,
|
||||||
"archived": dialog.archived,
|
"archived": dialog.archived,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async def _sync_dialog(
|
||||||
|
self, portal: po.Portal, dialog: Dialog, should_create: bool, puppet: pu.Puppet | None
|
||||||
|
) -> None:
|
||||||
|
if (
|
||||||
|
not portal.mxid
|
||||||
|
and isinstance(dialog.message, MessageService)
|
||||||
|
and isinstance(
|
||||||
|
dialog.message.action, (MessageActionContactSignUp, MessageActionHistoryClear)
|
||||||
|
)
|
||||||
|
):
|
||||||
|
self.log.debug(
|
||||||
|
f"Not syncing {portal.tgid_log} "
|
||||||
|
f"(last message is a {type(dialog.message.action).__name__})"
|
||||||
|
)
|
||||||
|
return
|
||||||
|
was_created = False
|
||||||
|
post_sync_args = self.dialog_to_sync_args(dialog)
|
||||||
if portal.mxid:
|
if portal.mxid:
|
||||||
self.log.debug(f"Backfilling and updating {portal.tgid_log} (dialog sync)")
|
self.log.debug(f"Backfilling and updating {portal.tgid_log} (dialog sync)")
|
||||||
try:
|
try:
|
||||||
@@ -772,7 +805,9 @@ class User(DBUser, AbstractUser, BaseUser):
|
|||||||
elif should_create:
|
elif should_create:
|
||||||
self.log.debug(f"Creating portal for {portal.tgid_log} immediately (dialog sync)")
|
self.log.debug(f"Creating portal for {portal.tgid_log} immediately (dialog sync)")
|
||||||
try:
|
try:
|
||||||
await portal.create_matrix_room(self, dialog.entity, invites=[self.mxid])
|
await portal.create_matrix_room(
|
||||||
|
self, dialog.entity, invites=[self.mxid], from_dialog_sync=True
|
||||||
|
)
|
||||||
was_created = True
|
was_created = True
|
||||||
except Exception:
|
except Exception:
|
||||||
self.log.exception(f"Error while creating {portal.tgid_log}")
|
self.log.exception(f"Error while creating {portal.tgid_log}")
|
||||||
@@ -785,7 +820,7 @@ class User(DBUser, AbstractUser, BaseUser):
|
|||||||
extra_data=post_sync_args,
|
extra_data=post_sync_args,
|
||||||
)
|
)
|
||||||
if portal.mxid and puppet and puppet.is_real_user:
|
if portal.mxid and puppet and puppet.is_real_user:
|
||||||
await self._post_sync_dialog(
|
await self.post_sync_dialog(
|
||||||
portal=portal,
|
portal=portal,
|
||||||
puppet=puppet,
|
puppet=puppet,
|
||||||
was_created=was_created,
|
was_created=was_created,
|
||||||
@@ -793,10 +828,10 @@ class User(DBUser, AbstractUser, BaseUser):
|
|||||||
)
|
)
|
||||||
self.log.debug(f"_sync_dialog finished for {portal.tgid_log}")
|
self.log.debug(f"_sync_dialog finished for {portal.tgid_log}")
|
||||||
|
|
||||||
async def _post_sync_dialog(
|
async def post_sync_dialog(
|
||||||
self,
|
self,
|
||||||
portal: po.Portal,
|
portal: po.Portal,
|
||||||
puppet: pu.Puppet,
|
puppet: pu.Puppet | None,
|
||||||
was_created: bool,
|
was_created: bool,
|
||||||
max_read_id: int,
|
max_read_id: int,
|
||||||
last_message_ts: float,
|
last_message_ts: float,
|
||||||
@@ -805,6 +840,10 @@ class User(DBUser, AbstractUser, BaseUser):
|
|||||||
pinned: bool,
|
pinned: bool,
|
||||||
archived: bool,
|
archived: bool,
|
||||||
) -> None:
|
) -> None:
|
||||||
|
if puppet is None:
|
||||||
|
puppet = await pu.Puppet.get_by_custom_mxid(self.mxid)
|
||||||
|
if not puppet or not puppet.is_real_user:
|
||||||
|
return
|
||||||
self.log.debug(
|
self.log.debug(
|
||||||
f"Running dialog post-sync for {portal.tgid_log} with args "
|
f"Running dialog post-sync for {portal.tgid_log} with args "
|
||||||
f"{was_created=}, {max_read_id=}, {last_message_ts=}, {unread_count=}, "
|
f"{was_created=}, {max_read_id=}, {last_message_ts=}, {unread_count=}, "
|
||||||
@@ -941,11 +980,18 @@ class User(DBUser, AbstractUser, BaseUser):
|
|||||||
self.log.debug("Contact syncing complete")
|
self.log.debug("Contact syncing complete")
|
||||||
return contacts
|
return contacts
|
||||||
|
|
||||||
|
@property
|
||||||
|
def _available_reactions_up_to_date(self) -> bool:
|
||||||
|
return (
|
||||||
|
bool(self._available_emoji_reactions)
|
||||||
|
and self._available_emoji_reactions_fetched + 12 * 60 * 60 > time.monotonic()
|
||||||
|
)
|
||||||
|
|
||||||
async def get_available_reactions(self) -> set[str]:
|
async def get_available_reactions(self) -> set[str]:
|
||||||
if self._available_emoji_reactions_fetched + 12 * 60 * 60 > time.monotonic():
|
if self._available_reactions_up_to_date:
|
||||||
return self._available_emoji_reactions
|
return self._available_emoji_reactions
|
||||||
async with self._available_emoji_reactions_lock:
|
async with self._available_emoji_reactions_lock:
|
||||||
if self._available_emoji_reactions_fetched + 12 * 60 * 60 > time.monotonic():
|
if self._available_reactions_up_to_date:
|
||||||
return self._available_emoji_reactions
|
return self._available_emoji_reactions
|
||||||
self.log.debug("Fetching available emoji reactions")
|
self.log.debug("Fetching available emoji reactions")
|
||||||
available_reactions = await self.client(
|
available_reactions = await self.client(
|
||||||
@@ -955,13 +1001,18 @@ class User(DBUser, AbstractUser, BaseUser):
|
|||||||
self._available_emoji_reactions = {
|
self._available_emoji_reactions = {
|
||||||
react.reaction
|
react.reaction
|
||||||
for react in available_reactions.reactions
|
for react in available_reactions.reactions
|
||||||
if self.is_premium or not react.premium
|
if not react.inactive and (self.is_premium or not react.premium)
|
||||||
}
|
}
|
||||||
self._available_emoji_reactions_hash = available_reactions.hash
|
self._available_emoji_reactions_hash = available_reactions.hash
|
||||||
self._available_emoji_reactions_fetched = time.monotonic()
|
self._available_emoji_reactions_fetched = time.monotonic()
|
||||||
self.log.debug(
|
self.log.debug(
|
||||||
"Got available emoji reactions: %s", self._available_emoji_reactions
|
"Got available emoji reactions: %s", self._available_emoji_reactions
|
||||||
)
|
)
|
||||||
|
elif self._available_emoji_reactions is None:
|
||||||
|
self.log.warning(
|
||||||
|
f"Got {available_reactions} in response to available reactions request"
|
||||||
|
" even though nothing is cached"
|
||||||
|
)
|
||||||
return self._available_emoji_reactions
|
return self._available_emoji_reactions
|
||||||
|
|
||||||
def tl_to_json(self) -> Any:
|
def tl_to_json(self) -> Any:
|
||||||
@@ -969,8 +1020,9 @@ class User(DBUser, AbstractUser, BaseUser):
|
|||||||
|
|
||||||
async def get_app_config(self) -> dict[str, Any]:
|
async def get_app_config(self) -> dict[str, Any]:
|
||||||
if not self._app_config:
|
if not self._app_config:
|
||||||
cfg = await self.client(GetAppConfigRequest())
|
cfg: AppConfig = await self.client(GetAppConfigRequest(hash=self._app_config_hash))
|
||||||
self._app_config = util.parse_tl_json(cfg)
|
self._app_config = util.parse_tl_json(cfg.config)
|
||||||
|
self._app_config_hash = cfg.hash
|
||||||
return self._app_config
|
return self._app_config
|
||||||
|
|
||||||
async def get_max_reactions(self, is_premium: bool | None = None) -> int:
|
async def get_max_reactions(self, is_premium: bool | None = None) -> int:
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ from .file_transfer import (
|
|||||||
convert_image,
|
convert_image,
|
||||||
transfer_custom_emojis_to_matrix,
|
transfer_custom_emojis_to_matrix,
|
||||||
transfer_file_to_matrix,
|
transfer_file_to_matrix,
|
||||||
|
transfer_thumbnail_to_matrix,
|
||||||
unicode_custom_emoji_map,
|
unicode_custom_emoji_map,
|
||||||
)
|
)
|
||||||
from .parallel_file_transfer import parallel_transfer_to_telegram
|
from .parallel_file_transfer import parallel_transfer_to_telegram
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ from telethon.errors import (
|
|||||||
PhoneNumberInvalidError,
|
PhoneNumberInvalidError,
|
||||||
PhoneNumberUnoccupiedError,
|
PhoneNumberUnoccupiedError,
|
||||||
SessionPasswordNeededError,
|
SessionPasswordNeededError,
|
||||||
|
SessionRevokedError,
|
||||||
)
|
)
|
||||||
|
|
||||||
from mautrix.bridge import InvalidAccessToken, OnlyLoginSelf
|
from mautrix.bridge import InvalidAccessToken, OnlyLoginSelf
|
||||||
@@ -288,6 +289,17 @@ class AuthAPI(abc.ABC):
|
|||||||
errcode="phone_number_unoccupied",
|
errcode="phone_number_unoccupied",
|
||||||
error="That phone number has not been registered.",
|
error="That phone number has not been registered.",
|
||||||
)
|
)
|
||||||
|
except FloodWaitError as e:
|
||||||
|
return self.get_login_response(
|
||||||
|
mxid=user.mxid,
|
||||||
|
state="code",
|
||||||
|
status=429,
|
||||||
|
errcode="flood_wait",
|
||||||
|
error=(
|
||||||
|
"You tried to enter your phone code too many times. "
|
||||||
|
f"Please wait for {format_duration(e.seconds)} before trying again."
|
||||||
|
),
|
||||||
|
)
|
||||||
except SessionPasswordNeededError:
|
except SessionPasswordNeededError:
|
||||||
if not password_in_data:
|
if not password_in_data:
|
||||||
if user.command_status and user.command_status["action"] == "Login":
|
if user.command_status and user.command_status["action"] == "Login":
|
||||||
@@ -342,6 +354,28 @@ class AuthAPI(abc.ABC):
|
|||||||
errcode="password_invalid",
|
errcode="password_invalid",
|
||||||
error="Incorrect password.",
|
error="Incorrect password.",
|
||||||
)
|
)
|
||||||
|
except SessionRevokedError:
|
||||||
|
return self.get_login_response(
|
||||||
|
mxid=user.mxid,
|
||||||
|
state="request",
|
||||||
|
status=401,
|
||||||
|
errcode="session_revoked",
|
||||||
|
error=(
|
||||||
|
"Please try again. Login cancelled because your other sessions were "
|
||||||
|
"terminated via the Telegram app."
|
||||||
|
),
|
||||||
|
)
|
||||||
|
except FloodWaitError as e:
|
||||||
|
return self.get_login_response(
|
||||||
|
mxid=user.mxid,
|
||||||
|
state="password",
|
||||||
|
status=429,
|
||||||
|
errcode="flood_wait",
|
||||||
|
error=(
|
||||||
|
"You tried to enter your password too many times. "
|
||||||
|
f"Please wait for {format_duration(e.seconds)} before trying again."
|
||||||
|
),
|
||||||
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.log.exception("Error sending password")
|
self.log.exception("Error sending password")
|
||||||
if isinstance(e, ValueError) and "You must provide a phone and a code" in str(e):
|
if isinstance(e, ValueError) and "You must provide a phone and a code" in str(e):
|
||||||
@@ -357,5 +391,5 @@ class AuthAPI(abc.ABC):
|
|||||||
state="password",
|
state="password",
|
||||||
status=500,
|
status=500,
|
||||||
errcode="unknown_error",
|
errcode="unknown_error",
|
||||||
error="Internal server error while sending password.",
|
error=f"Internal server error while sending password. {e}",
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -130,9 +130,9 @@ class ProvisioningAPI(AuthAPI):
|
|||||||
"about": portal.about,
|
"about": portal.about,
|
||||||
"username": portal.username,
|
"username": portal.username,
|
||||||
"megagroup": portal.megagroup,
|
"megagroup": portal.megagroup,
|
||||||
"can_unbridge": (await portal.can_user_perform(user, "unbridge"))
|
"can_unbridge": (
|
||||||
if user
|
(await portal.can_user_perform(user, "unbridge")) if user else False
|
||||||
else False,
|
),
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -188,9 +188,11 @@ class ProvisioningAPI(AuthAPI):
|
|||||||
if force in ("delete", "unbridge"):
|
if force in ("delete", "unbridge"):
|
||||||
delete = force == "delete"
|
delete = force == "delete"
|
||||||
await portal.cleanup_portal(
|
await portal.cleanup_portal(
|
||||||
|
(
|
||||||
"Portal deleted (moving to another room)"
|
"Portal deleted (moving to another room)"
|
||||||
if delete
|
if delete
|
||||||
else "Room unbridged (portal moving to another room)",
|
else "Room unbridged (portal moving to another room)"
|
||||||
|
),
|
||||||
puppets_only=not delete,
|
puppets_only=not delete,
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
|
|||||||
@@ -2,25 +2,28 @@
|
|||||||
# Uncommented lines after the group definition insert things into that group.
|
# Uncommented lines after the group definition insert things into that group.
|
||||||
|
|
||||||
#/speedups
|
#/speedups
|
||||||
cryptg>=0.1,<0.5
|
cryptg>=0.1,<0.6
|
||||||
aiodns
|
aiodns
|
||||||
brotli
|
brotli
|
||||||
|
|
||||||
#/qr_login
|
#/qr_login
|
||||||
pillow>=4,<10
|
pillow>=10.0.1,<12
|
||||||
qrcode>=6,<8
|
qrcode>=6,<9
|
||||||
|
|
||||||
|
|
||||||
#/formattednumbers
|
#/formattednumbers
|
||||||
phonenumbers>=8,<9
|
phonenumbers>=8,<10
|
||||||
|
|
||||||
#/metrics
|
#/metrics
|
||||||
prometheus_client>=0.6,<0.17
|
prometheus_client>=0.6,<0.23
|
||||||
|
|
||||||
#/e2be
|
#/e2be
|
||||||
python-olm>=3,<4
|
python-olm>=3,<4
|
||||||
pycryptodome>=3,<4
|
pycryptodome>=3,<4
|
||||||
unpaddedbase64>=1,<3
|
unpaddedbase64>=1,<3
|
||||||
|
base58>=2,<3
|
||||||
|
|
||||||
#/sqlite
|
#/sqlite
|
||||||
aiosqlite>=0.16,<0.19
|
aiosqlite>=0.16,<0.22
|
||||||
|
|
||||||
|
#/proxy
|
||||||
|
python-socks[asyncio]
|
||||||
|
|||||||
+1
-1
@@ -9,4 +9,4 @@ line_length = 99
|
|||||||
|
|
||||||
[tool.black]
|
[tool.black]
|
||||||
line-length = 99
|
line-length = 99
|
||||||
target-version = ["py38"]
|
target-version = ["py310"]
|
||||||
|
|||||||
+5
-6
@@ -1,11 +1,10 @@
|
|||||||
ruamel.yaml>=0.15.35,<0.18
|
ruamel.yaml>=0.15.35,<0.19
|
||||||
python-magic>=0.4,<0.5
|
python-magic>=0.4,<0.5
|
||||||
commonmark>=0.8,<0.10
|
commonmark>=0.8,<0.10
|
||||||
aiohttp>=3,<4
|
aiohttp>=3,<4
|
||||||
yarl>=1,<2
|
yarl>=1,<2
|
||||||
mautrix>=0.19.4,<0.20
|
mautrix>=0.21.0b5,<0.22
|
||||||
#telethon>=1.25.4,<1.27
|
tulir-telethon==1.99.0a6
|
||||||
tulir-telethon==1.28.0a3
|
asyncpg>=0.20,<1
|
||||||
asyncpg>=0.20,<0.28
|
|
||||||
mako>=1,<2
|
mako>=1,<2
|
||||||
setuptools
|
setuptools<82
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ setuptools.setup(
|
|||||||
|
|
||||||
install_requires=install_requires,
|
install_requires=install_requires,
|
||||||
extras_require=extras_require,
|
extras_require=extras_require,
|
||||||
python_requires="~=3.8",
|
python_requires="~=3.10",
|
||||||
|
|
||||||
classifiers=[
|
classifiers=[
|
||||||
"Development Status :: 4 - Beta",
|
"Development Status :: 4 - Beta",
|
||||||
@@ -60,9 +60,10 @@ setuptools.setup(
|
|||||||
"Framework :: AsyncIO",
|
"Framework :: AsyncIO",
|
||||||
"Programming Language :: Python",
|
"Programming Language :: Python",
|
||||||
"Programming Language :: Python :: 3",
|
"Programming Language :: Python :: 3",
|
||||||
"Programming Language :: Python :: 3.8",
|
|
||||||
"Programming Language :: Python :: 3.9",
|
|
||||||
"Programming Language :: Python :: 3.10",
|
"Programming Language :: Python :: 3.10",
|
||||||
|
"Programming Language :: Python :: 3.11",
|
||||||
|
"Programming Language :: Python :: 3.12",
|
||||||
|
"Programming Language :: Python :: 3.13",
|
||||||
],
|
],
|
||||||
package_data={"mautrix_telegram": [
|
package_data={"mautrix_telegram": [
|
||||||
"web/public/*.mako", "web/public/*.png", "web/public/*.css",
|
"web/public/*.mako", "web/public/*.png", "web/public/*.css",
|
||||||
|
|||||||
Reference in New Issue
Block a user