Compare commits
53 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 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 |
+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,3 +1,46 @@
|
|||||||
|
# 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)
|
# v0.14.0 (2023-05-26)
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|||||||
+5
-5
@@ -1,8 +1,8 @@
|
|||||||
FROM dock.mau.dev/tulir/lottieconverter:alpine-3.17
|
FROM dock.mau.dev/tulir/lottieconverter:alpine-3.18
|
||||||
|
|
||||||
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-magic \
|
py3-magic \
|
||||||
py3-ruamel.yaml \
|
py3-ruamel.yaml \
|
||||||
@@ -14,8 +14,6 @@ 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 \
|
||||||
# cryptg
|
# cryptg
|
||||||
py3-cffi \
|
py3-cffi \
|
||||||
@@ -34,7 +32,9 @@ RUN apk add --no-cache \
|
|||||||
bash \
|
bash \
|
||||||
curl \
|
curl \
|
||||||
jq \
|
jq \
|
||||||
yq
|
yq \
|
||||||
|
# Temporarily install pillow from edge repo to get up-to-date version
|
||||||
|
&& apk add --no-cache py3-pillow --repository=https://dl-cdn.alpinelinux.org/alpine/edge/community
|
||||||
|
|
||||||
COPY requirements.txt /opt/mautrix-telegram/requirements.txt
|
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
|
||||||
|
|||||||
@@ -1,2 +1,2 @@
|
|||||||
__version__ = "0.14.0"
|
__version__ = "0.15.0"
|
||||||
__author__ = "Tulir Asokan <tulir@maunium.net>"
|
__author__ = "Tulir Asokan <tulir@maunium.net>"
|
||||||
|
|||||||
@@ -104,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())
|
||||||
|
|||||||
@@ -208,6 +208,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)
|
||||||
|
|
||||||
@@ -235,6 +237,7 @@ 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)
|
||||||
|
|
||||||
@@ -303,7 +306,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()
|
||||||
await self.client.connect()
|
attempts = 1
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
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
|
||||||
|
|
||||||
@@ -453,6 +467,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
|
||||||
|
|
||||||
@@ -708,6 +723,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):
|
||||||
|
|||||||
@@ -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]
|
||||||
|
|||||||
@@ -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:
|
output = await portal.forward_backfill(evt.sender, initial=False, override_limit=limit)
|
||||||
messages_per_batch = evt.config["bridge.backfill.incremental.messages_per_batch"]
|
await evt.reply(output)
|
||||||
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)
|
|
||||||
await evt.reply(output)
|
|
||||||
|
|||||||
@@ -157,6 +157,7 @@ class Config(BaseBridgeConfig):
|
|||||||
if base["bridge.private_chat_portal_meta"] not in ("default", "always", "never"):
|
if base["bridge.private_chat_portal_meta"] not in ("default", "always", "never"):
|
||||||
base["bridge.private_chat_portal_meta"] = "default"
|
base["bridge.private_chat_portal_meta"] = "default"
|
||||||
copy("bridge.disable_reply_fallbacks")
|
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")
|
||||||
@@ -170,8 +171,6 @@ 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")
|
||||||
if "bridge.backfill.forward" in self:
|
if "bridge.backfill.forward" in self:
|
||||||
@@ -194,6 +193,7 @@ class Config(BaseBridgeConfig):
|
|||||||
copy("bridge.backfill.forward_limits.sync.normal_group")
|
copy("bridge.backfill.forward_limits.sync.normal_group")
|
||||||
copy("bridge.backfill.forward_limits.sync.supergroup")
|
copy("bridge.backfill.forward_limits.sync.supergroup")
|
||||||
copy("bridge.backfill.forward_limits.sync.channel")
|
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")
|
||||||
@@ -268,6 +268,7 @@ class Config(BaseBridgeConfig):
|
|||||||
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")
|
||||||
|
|||||||
@@ -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()
|
||||||
@@ -291,6 +291,10 @@ bridge:
|
|||||||
delete_on_device_delete: false
|
delete_on_device_delete: false
|
||||||
# Periodically delete megolm sessions when 2x max_age has passed since receiving the session.
|
# Periodically delete megolm sessions when 2x max_age has passed since receiving the session.
|
||||||
periodically_delete_expired: false
|
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:
|
||||||
@@ -326,6 +330,10 @@ bridge:
|
|||||||
# default.
|
# default.
|
||||||
messages: 100
|
messages: 100
|
||||||
|
|
||||||
|
# Disable rotating keys when a user's devices change?
|
||||||
|
# You should not enable this option unless you understand all the implications.
|
||||||
|
disable_device_change_key_rotation: false
|
||||||
|
|
||||||
# Whether to explicitly set the avatar and room name for private chat portal rooms.
|
# 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 `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 `always`, all DM rooms will have explicit names and avatars set.
|
||||||
@@ -334,6 +342,8 @@ bridge:
|
|||||||
# Disable generating reply fallbacks? Some extremely bad clients still rely on them,
|
# 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.
|
# but they're being phased out and will be completely removed in the future.
|
||||||
disable_reply_fallbacks: false
|
disable_reply_fallbacks: false
|
||||||
|
# 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
|
||||||
@@ -369,23 +379,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.
|
|
||||||
#
|
|
||||||
# Also note that adding users to the appservice namespace may have unexpected side effects,
|
|
||||||
# as described in https://docs.mau.fi/bridges/general/double-puppeting.html#appservice-method
|
|
||||||
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.
|
||||||
@@ -395,10 +388,9 @@ 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_limits:
|
||||||
# Number of messages to backfill immediately after creating a portal.
|
# Number of messages to backfill immediately after creating a portal.
|
||||||
initial:
|
initial:
|
||||||
@@ -412,8 +404,10 @@ bridge:
|
|||||||
normal_group: 100
|
normal_group: 100
|
||||||
supergroup: 100
|
supergroup: 100
|
||||||
channel: 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
|
||||||
@@ -604,6 +598,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,
|
||||||
@@ -174,7 +159,8 @@ class MatrixHandler(BaseMatrixHandler):
|
|||||||
await portal.main_intent.kick_user(
|
await portal.main_intent.kick_user(
|
||||||
room_id,
|
room_id,
|
||||||
user.mxid,
|
user.mxid,
|
||||||
"This chat does not have a bot relaying messages for unauthenticated users.",
|
"This chat does not have a bot on the Telegram side for relaying messages sent by"
|
||||||
|
" unauthenticated Matrix users.",
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
|
|||||||
+79
-138
@@ -99,6 +99,7 @@ from telethon.tl.types import (
|
|||||||
DocumentAttributeAudio,
|
DocumentAttributeAudio,
|
||||||
DocumentAttributeFilename,
|
DocumentAttributeFilename,
|
||||||
DocumentAttributeImageSize,
|
DocumentAttributeImageSize,
|
||||||
|
DocumentAttributeSticker,
|
||||||
DocumentAttributeVideo,
|
DocumentAttributeVideo,
|
||||||
GeoPoint,
|
GeoPoint,
|
||||||
InputChannel,
|
InputChannel,
|
||||||
@@ -110,6 +111,7 @@ from telethon.tl.types import (
|
|||||||
InputPeerChat,
|
InputPeerChat,
|
||||||
InputPeerPhotoFileLocation,
|
InputPeerPhotoFileLocation,
|
||||||
InputPeerUser,
|
InputPeerUser,
|
||||||
|
InputStickerSetEmpty,
|
||||||
InputUser,
|
InputUser,
|
||||||
MessageActionChannelCreate,
|
MessageActionChannelCreate,
|
||||||
MessageActionChatAddUser,
|
MessageActionChatAddUser,
|
||||||
@@ -250,7 +252,6 @@ if TYPE_CHECKING:
|
|||||||
StateBridge = EventType.find("m.bridge", EventType.Class.STATE)
|
StateBridge = EventType.find("m.bridge", EventType.Class.STATE)
|
||||||
StateHalfShotBridge = EventType.find("uk.half-shot.bridge", EventType.Class.STATE)
|
StateHalfShotBridge = EventType.find("uk.half-shot.bridge", EventType.Class.STATE)
|
||||||
DummyPortalCreated = EventType.find("fi.mau.dummy.portal_created", EventType.Class.MESSAGE)
|
DummyPortalCreated = EventType.find("fi.mau.dummy.portal_created", EventType.Class.MESSAGE)
|
||||||
StateMarker = EventType.find("org.matrix.msc2716.marker", EventType.Class.STATE)
|
|
||||||
|
|
||||||
InviteList = Union[UserID, List[UserID]]
|
InviteList = Union[UserID, List[UserID]]
|
||||||
UpdateTyping = Union[UpdateUserTyping, UpdateChatUserTyping, UpdateChannelUserTyping]
|
UpdateTyping = Union[UpdateUserTyping, UpdateChatUserTyping, UpdateChannelUserTyping]
|
||||||
@@ -297,8 +298,6 @@ class Portal(DBPortal, BasePortal):
|
|||||||
|
|
||||||
backfill_lock: SimpleLock
|
backfill_lock: SimpleLock
|
||||||
backfill_method_lock: asyncio.Lock
|
backfill_method_lock: asyncio.Lock
|
||||||
backfill_leave: set[IntentAPI] | None
|
|
||||||
backfill_msc2716: bool
|
|
||||||
backfill_enable: bool
|
backfill_enable: bool
|
||||||
|
|
||||||
alias: RoomAlias | None
|
alias: RoomAlias | None
|
||||||
@@ -319,7 +318,6 @@ class Portal(DBPortal, BasePortal):
|
|||||||
_sponsored_seen: dict[UserID, bool]
|
_sponsored_seen: dict[UserID, bool]
|
||||||
_new_messages_after_sponsored: bool
|
_new_messages_after_sponsored: bool
|
||||||
|
|
||||||
_member_list_cache: dict[EventID, set[UserID]]
|
|
||||||
_prev_reaction_poll: dict[UserID, float]
|
_prev_reaction_poll: dict[UserID, float]
|
||||||
|
|
||||||
_msg_conv: putil.TelegramMessageConverter
|
_msg_conv: putil.TelegramMessageConverter
|
||||||
@@ -378,7 +376,6 @@ class Portal(DBPortal, BasePortal):
|
|||||||
"Waiting for backfilling to finish before handling %s", log=self.log
|
"Waiting for backfilling to finish before handling %s", log=self.log
|
||||||
)
|
)
|
||||||
self.backfill_method_lock = asyncio.Lock()
|
self.backfill_method_lock = asyncio.Lock()
|
||||||
self.backfill_leave = None
|
|
||||||
|
|
||||||
self.dedup = putil.PortalDedup(self)
|
self.dedup = putil.PortalDedup(self)
|
||||||
self.send_lock = putil.PortalSendLock()
|
self.send_lock = putil.PortalSendLock()
|
||||||
@@ -393,7 +390,6 @@ class Portal(DBPortal, BasePortal):
|
|||||||
self._new_messages_after_sponsored = True
|
self._new_messages_after_sponsored = True
|
||||||
self._bridging_blocked_at_runtime = False
|
self._bridging_blocked_at_runtime = False
|
||||||
|
|
||||||
self._member_list_cache = {}
|
|
||||||
self._prev_reaction_poll = defaultdict(lambda: 0.0)
|
self._prev_reaction_poll = defaultdict(lambda: 0.0)
|
||||||
|
|
||||||
self._msg_conv = putil.TelegramMessageConverter(self)
|
self._msg_conv = putil.TelegramMessageConverter(self)
|
||||||
@@ -488,9 +484,8 @@ class Portal(DBPortal, BasePortal):
|
|||||||
cls.private_chat_portal_meta = cls.config["bridge.private_chat_portal_meta"]
|
cls.private_chat_portal_meta = cls.config["bridge.private_chat_portal_meta"]
|
||||||
cls.filter_mode = cls.config["bridge.filter.mode"]
|
cls.filter_mode = cls.config["bridge.filter.mode"]
|
||||||
cls.filter_list = cls.config["bridge.filter.list"]
|
cls.filter_list = cls.config["bridge.filter.list"]
|
||||||
cls.filter_users = cls.config["bridge.filter.filter_users"]
|
cls.filter_users = cls.config["bridge.filter.users"]
|
||||||
cls.hs_domain = cls.config["homeserver.domain"]
|
cls.hs_domain = cls.config["homeserver.domain"]
|
||||||
cls.backfill_msc2716 = cls.config["bridge.backfill.msc2716"]
|
|
||||||
cls.backfill_enable = cls.config["bridge.backfill.enable"]
|
cls.backfill_enable = cls.config["bridge.backfill.enable"]
|
||||||
cls.alias_template = SimpleTemplate(
|
cls.alias_template = SimpleTemplate(
|
||||||
cls.config["bridge.alias_template"],
|
cls.config["bridge.alias_template"],
|
||||||
@@ -1063,18 +1058,14 @@ class Portal(DBPortal, BasePortal):
|
|||||||
self.first_event_id = await self.main_intent.send_message_event(
|
self.first_event_id = await self.main_intent.send_message_event(
|
||||||
self.mxid, DummyPortalCreated, {}
|
self.mxid, DummyPortalCreated, {}
|
||||||
)
|
)
|
||||||
if not self.bridge.homeserver_software.is_hungry:
|
|
||||||
self._member_list_cache[self.first_event_id] = set(
|
|
||||||
(await self.main_intent.get_joined_members(self.mxid)).keys()
|
|
||||||
)
|
|
||||||
await self.save()
|
await self.save()
|
||||||
|
|
||||||
if self.backfill_enable and (isinstance(user, u.User) or not self.backfill_msc2716):
|
if self.backfill_enable:
|
||||||
try:
|
try:
|
||||||
await self.forward_backfill(user, initial=True, client=client)
|
await self.forward_backfill(user, initial=True, client=client)
|
||||||
except Exception:
|
except Exception:
|
||||||
self.log.exception("Error in initial backfill")
|
self.log.exception("Error in initial backfill")
|
||||||
if self.backfill_msc2716:
|
if self._enable_batch_sending:
|
||||||
await self.enqueue_backfill(user, priority=50)
|
await self.enqueue_backfill(user, priority=50)
|
||||||
|
|
||||||
return self.mxid
|
return self.mxid
|
||||||
@@ -1841,6 +1832,7 @@ class Portal(DBPortal, BasePortal):
|
|||||||
max_image_size = self.config["bridge.image_as_file_size"] * 1000**2
|
max_image_size = self.config["bridge.image_as_file_size"] * 1000**2
|
||||||
max_image_pixels = self.config["bridge.image_as_file_pixels"]
|
max_image_pixels = self.config["bridge.image_as_file_pixels"]
|
||||||
|
|
||||||
|
attributes = []
|
||||||
if self.config["bridge.parallel_file_transfer"] and content.url:
|
if self.config["bridge.parallel_file_transfer"] and content.url:
|
||||||
file_handle, file_size = await util.parallel_transfer_to_telegram(
|
file_handle, file_size = await util.parallel_transfer_to_telegram(
|
||||||
client, self.main_intent, content.url, sender_id
|
client, self.main_intent, content.url, sender_id
|
||||||
@@ -1860,21 +1852,27 @@ class Portal(DBPortal, BasePortal):
|
|||||||
file = await self.main_intent.download_media(content.url)
|
file = await self.main_intent.download_media(content.url)
|
||||||
|
|
||||||
if content.msgtype == MessageType.STICKER:
|
if content.msgtype == MessageType.STICKER:
|
||||||
if mime != "image/gif":
|
if mime == "image/gif":
|
||||||
mime, file, w, h = util.convert_image(
|
|
||||||
file, source_mime=mime, target_type="webp"
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
# Remove sticker description
|
# Remove sticker description
|
||||||
file_name = "sticker.gif"
|
file_name = "sticker.gif"
|
||||||
|
else:
|
||||||
|
if mime not in ("video/webm", "application/x-tgsticker"):
|
||||||
|
mime, file, w, h = util.convert_image(
|
||||||
|
file, source_mime=mime, target_type="webp"
|
||||||
|
)
|
||||||
|
attributes.append(
|
||||||
|
DocumentAttributeSticker(
|
||||||
|
alt=content.body, stickerset=InputStickerSetEmpty()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
file_handle = await client.upload_file(file)
|
file_handle = await client.upload_file(file)
|
||||||
file_size = len(file)
|
file_size = len(file)
|
||||||
|
|
||||||
file_handle.name = file_name
|
file_handle.name = file_name
|
||||||
force_document = file_size >= max_image_size
|
force_document = file_size >= max_image_size
|
||||||
|
attributes.append(DocumentAttributeFilename(file_name=file_name))
|
||||||
|
|
||||||
attributes = [DocumentAttributeFilename(file_name=file_name)]
|
|
||||||
if content.msgtype == MessageType.VIDEO:
|
if content.msgtype == MessageType.VIDEO:
|
||||||
attributes.append(
|
attributes.append(
|
||||||
DocumentAttributeVideo(
|
DocumentAttributeVideo(
|
||||||
@@ -2047,7 +2045,7 @@ class Portal(DBPortal, BasePortal):
|
|||||||
response: TypeMessage,
|
response: TypeMessage,
|
||||||
msgtype: MessageType | None = None,
|
msgtype: MessageType | None = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
self.log.trace("Handled Matrix message: %s", response)
|
self.log.trace("Raw event handling response for %s: %s", event_id, response)
|
||||||
event_hash, _ = self.dedup.check(response, (event_id, space), force_hash=edit_index != 0)
|
event_hash, _ = self.dedup.check(response, (event_id, space), force_hash=edit_index != 0)
|
||||||
if edit_index < 0:
|
if edit_index < 0:
|
||||||
prev_edit = await DBMessage.get_one_by_tgid(TelegramID(response.id), space, -1)
|
prev_edit = await DBMessage.get_one_by_tgid(TelegramID(response.id), space, -1)
|
||||||
@@ -2077,6 +2075,9 @@ class Portal(DBPortal, BasePortal):
|
|||||||
seconds=response.ttl_period,
|
seconds=response.ttl_period,
|
||||||
expires_at=int(response.date.timestamp()) + response.ttl_period,
|
expires_at=int(response.date.timestamp()) + response.ttl_period,
|
||||||
)
|
)
|
||||||
|
self.log.debug(
|
||||||
|
f"Handled Matrix message {event_id} -> {response.id} (edit index {edit_index})"
|
||||||
|
)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _error_to_human_message(err: Exception) -> str | None:
|
def _error_to_human_message(err: Exception) -> str | None:
|
||||||
@@ -2420,6 +2421,10 @@ class Portal(DBPortal, BasePortal):
|
|||||||
await deleter.client(
|
await deleter.client(
|
||||||
SendReactionRequest(peer=self.peer, msg_id=msg.tgid, reaction=new_reactions)
|
SendReactionRequest(peer=self.peer, msg_id=msg.tgid, reaction=new_reactions)
|
||||||
)
|
)
|
||||||
|
self.log.debug(
|
||||||
|
f"Handled Matrix deletion of reaction {event_id} to {msg.tgid} "
|
||||||
|
f"(new reaction count: {len(new_reactions) if new_reactions else 0})"
|
||||||
|
)
|
||||||
|
|
||||||
async def _handle_matrix_deletion(self, deleter: u.User, event_id: EventID) -> None:
|
async def _handle_matrix_deletion(self, deleter: u.User, event_id: EventID) -> None:
|
||||||
real_deleter = deleter if not await deleter.needs_relaybot(self) else self.bot
|
real_deleter = deleter if not await deleter.needs_relaybot(self) else self.bot
|
||||||
@@ -2440,6 +2445,7 @@ class Portal(DBPortal, BasePortal):
|
|||||||
else:
|
else:
|
||||||
await message.mark_redacted()
|
await message.mark_redacted()
|
||||||
await real_deleter.client.delete_messages(self.peer, [message.tgid])
|
await real_deleter.client.delete_messages(self.peer, [message.tgid])
|
||||||
|
self.log.debug(f"Handled Matrix redaction of {event_id} / {message.tgid}")
|
||||||
|
|
||||||
async def handle_matrix_reaction(
|
async def handle_matrix_reaction(
|
||||||
self, user: u.User, target_event_id: EventID, emoji: str, reaction_event_id: EventID
|
self, user: u.User, target_event_id: EventID, emoji: str, reaction_event_id: EventID
|
||||||
@@ -2546,9 +2552,15 @@ class Portal(DBPortal, BasePortal):
|
|||||||
SendReactionRequest(peer=self.peer, msg_id=msg.tgid, reaction=new_tg_reactions)
|
SendReactionRequest(peer=self.peer, msg_id=msg.tgid, reaction=new_tg_reactions)
|
||||||
)
|
)
|
||||||
puppet = await user.get_puppet()
|
puppet = await user.get_puppet()
|
||||||
|
removed = 0
|
||||||
for db_reaction in reactions_to_remove:
|
for db_reaction in reactions_to_remove:
|
||||||
|
removed += 1
|
||||||
await db_reaction.delete()
|
await db_reaction.delete()
|
||||||
await puppet.intent_for(self).redact(db_reaction.mx_room, db_reaction.mxid)
|
await puppet.intent_for(self).redact(db_reaction.mx_room, db_reaction.mxid)
|
||||||
|
self.log.debug(
|
||||||
|
f"Handled Matrix reaction {reaction_event_id} to {msg.tgid} "
|
||||||
|
f"(new reaction count: {len(new_tg_reactions)}, removed {removed} old reactions)"
|
||||||
|
)
|
||||||
await DBReaction(
|
await DBReaction(
|
||||||
mxid=reaction_event_id,
|
mxid=reaction_event_id,
|
||||||
mx_room=self.mxid,
|
mx_room=self.mxid,
|
||||||
@@ -2814,6 +2826,10 @@ class Portal(DBPortal, BasePortal):
|
|||||||
def _default_max_batches(self) -> int:
|
def _default_max_batches(self) -> int:
|
||||||
return self.config[f"bridge.backfill.incremental.max_batches.{self._backfill_config_type}"]
|
return self.config[f"bridge.backfill.incremental.max_batches.{self._backfill_config_type}"]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def _enable_batch_sending(self) -> bool:
|
||||||
|
return self.bridge.matrix.versions.supports("com.beeper.batch_sending")
|
||||||
|
|
||||||
async def enqueue_backfill(
|
async def enqueue_backfill(
|
||||||
self,
|
self,
|
||||||
source: u.User,
|
source: u.User,
|
||||||
@@ -2862,10 +2878,15 @@ class Portal(DBPortal, BasePortal):
|
|||||||
)
|
)
|
||||||
if limit == 0:
|
if limit == 0:
|
||||||
return "Limit is zero, not backfilling"
|
return "Limit is zero, not backfilling"
|
||||||
|
timeout = self.config["bridge.backfill.forward_timeout"]
|
||||||
with self.backfill_lock:
|
with self.backfill_lock:
|
||||||
output = await self.backfill(
|
task = self.backfill(
|
||||||
source, client, forward=True, forward_limit=limit, last_tgid=last_tgid
|
source, client, forward=True, forward_limit=limit, last_tgid=last_tgid
|
||||||
)
|
)
|
||||||
|
if timeout > 0:
|
||||||
|
output = await asyncio.wait_for(task, timeout=timeout)
|
||||||
|
else:
|
||||||
|
output = await task
|
||||||
self.log.debug(f"Forward backfill complete, status: {output}")
|
self.log.debug(f"Forward backfill complete, status: {output}")
|
||||||
return output
|
return output
|
||||||
|
|
||||||
@@ -2898,14 +2919,9 @@ class Portal(DBPortal, BasePortal):
|
|||||||
if not self.config["bridge.backfill.normal_groups"] and self.peer_type == "chat":
|
if not self.config["bridge.backfill.normal_groups"] and self.peer_type == "chat":
|
||||||
return "Backfilling normal groups is disabled in the bridge config"
|
return "Backfilling normal groups is disabled in the bridge config"
|
||||||
tg_space = source.tgid if self.peer_type != "channel" else self.tgid
|
tg_space = source.tgid if self.peer_type != "channel" else self.tgid
|
||||||
prev_event_id = self.first_event_id
|
|
||||||
if forward:
|
if forward:
|
||||||
last_in_room = await DBMessage.find_last(self.mxid, tg_space)
|
last_in_room = await DBMessage.find_last(self.mxid, tg_space)
|
||||||
if last_in_room:
|
min_id = last_in_room.tgid if last_in_room else 0
|
||||||
prev_event_id = last_in_room.mxid
|
|
||||||
min_id = last_in_room.tgid
|
|
||||||
else:
|
|
||||||
min_id = 0
|
|
||||||
if last_tgid is None:
|
if last_tgid is None:
|
||||||
messages = await source.client.get_messages(self.peer, limit=1)
|
messages = await source.client.get_messages(self.peer, limit=1)
|
||||||
if not messages:
|
if not messages:
|
||||||
@@ -2936,29 +2952,10 @@ class Portal(DBPortal, BasePortal):
|
|||||||
f"Backfilling up to {req.messages_per_batch} historical messages "
|
f"Backfilling up to {req.messages_per_batch} historical messages "
|
||||||
f"before {anchor_id} ({anchor_source}) through {source.mxid}"
|
f"before {anchor_id} ({anchor_source}) through {source.mxid}"
|
||||||
)
|
)
|
||||||
insertion_id, event_count, message_count, lowest_id = await self._backfill_messages(
|
event_count, message_count, lowest_id = await self._backfill_messages(
|
||||||
source, client, forward, anchor_id, limit, prev_event_id
|
source, client, forward, anchor_id, limit
|
||||||
)
|
)
|
||||||
if prev_event_id == self.first_event_id:
|
|
||||||
if insertion_id and not self.base_insertion_id:
|
|
||||||
self.base_insertion_id = insertion_id
|
|
||||||
elif not insertion_id:
|
|
||||||
insertion_id = self.base_insertion_id
|
|
||||||
await self.save()
|
await self.save()
|
||||||
if (
|
|
||||||
event_count > 0
|
|
||||||
and self.backfill_msc2716
|
|
||||||
and (not forward or not self.bridge.homeserver_software.is_hungry)
|
|
||||||
):
|
|
||||||
await self.main_intent.send_state_event(
|
|
||||||
self.mxid,
|
|
||||||
StateMarker,
|
|
||||||
{
|
|
||||||
"org.matrix.msc2716.marker.insertion": insertion_id,
|
|
||||||
"com.beeper.timestamp": int(time.time() * 1000),
|
|
||||||
},
|
|
||||||
state_key=insertion_id,
|
|
||||||
)
|
|
||||||
if forward:
|
if forward:
|
||||||
self.log.debug(f"Forward backfill finished with {event_count}/{message_count} events")
|
self.log.debug(f"Forward backfill finished with {event_count}/{message_count} events")
|
||||||
elif message_count > 0 and lowest_id and lowest_id > 1:
|
elif message_count > 0 and lowest_id and lowest_id > 1:
|
||||||
@@ -2977,42 +2974,11 @@ class Portal(DBPortal, BasePortal):
|
|||||||
self.log.debug("No more messages to backfill")
|
self.log.debug("No more messages to backfill")
|
||||||
return f"Backfilled {event_count} messages"
|
return f"Backfilled {event_count} messages"
|
||||||
|
|
||||||
def _can_double_puppet_backfill(self, custom_mxid: UserID) -> bool:
|
|
||||||
if not self.backfill_msc2716:
|
|
||||||
return True
|
|
||||||
if not self.config["bridge.backfill.double_puppet_backfill"]:
|
|
||||||
return False
|
|
||||||
if self.bridge.homeserver_software.is_hungry:
|
|
||||||
return True
|
|
||||||
|
|
||||||
# Batch sending can only use local users, so don't allow double puppets on other servers.
|
|
||||||
if custom_mxid[custom_mxid.index(":") + 1 :] != self.config["homeserver.domain"]:
|
|
||||||
return False
|
|
||||||
return True
|
|
||||||
|
|
||||||
async def _get_members_at(self, event_id: EventID) -> set[UserID]:
|
|
||||||
try:
|
|
||||||
return self._member_list_cache[event_id]
|
|
||||||
except KeyError:
|
|
||||||
pass
|
|
||||||
# TODO cache the list in db?
|
|
||||||
self.log.debug(f"Fetching member list at {event_id}")
|
|
||||||
ctx = await self.main_intent.get_event_context(self.mxid, event_id, limit=0)
|
|
||||||
members = {
|
|
||||||
evt.state_key
|
|
||||||
for evt in ctx.state
|
|
||||||
if evt.type == EventType.ROOM_MEMBER and evt.content.membership == Membership.JOIN
|
|
||||||
}
|
|
||||||
self.log.debug(f"Found {len(members)} members at {event_id}")
|
|
||||||
self._member_list_cache[event_id] = members
|
|
||||||
return members
|
|
||||||
|
|
||||||
async def _convert_batch_msg(
|
async def _convert_batch_msg(
|
||||||
self,
|
self,
|
||||||
source: u.User,
|
source: u.User,
|
||||||
client: MautrixTelegramClient,
|
client: MautrixTelegramClient,
|
||||||
msg: Message,
|
msg: Message,
|
||||||
add_member: Callable[[IntentAPI, str, ContentURI], Awaitable[None]],
|
|
||||||
) -> tuple[putil.ConvertedMessage, IntentAPI]:
|
) -> tuple[putil.ConvertedMessage, IntentAPI]:
|
||||||
if msg.from_id and isinstance(msg.from_id, (PeerUser, PeerChannel)):
|
if msg.from_id and isinstance(msg.from_id, (PeerUser, PeerChannel)):
|
||||||
sender = await p.Puppet.get_by_peer(msg.from_id)
|
sender = await p.Puppet.get_by_peer(msg.from_id)
|
||||||
@@ -3030,10 +2996,12 @@ class Portal(DBPortal, BasePortal):
|
|||||||
await sender.update_info(source, entity, client_override=client)
|
await sender.update_info(source, entity, client_override=client)
|
||||||
else:
|
else:
|
||||||
intent = self.main_intent
|
intent = self.main_intent
|
||||||
if intent.api.is_real_user and not self._can_double_puppet_backfill(intent.mxid):
|
if (
|
||||||
|
intent.api.is_real_user
|
||||||
|
and not intent.api.is_real_user_as_token
|
||||||
|
and not self._enable_batch_sending
|
||||||
|
):
|
||||||
intent = sender.default_mxid_intent
|
intent = sender.default_mxid_intent
|
||||||
if sender:
|
|
||||||
await add_member(intent, sender.displayname, sender.avatar_url)
|
|
||||||
is_bot = sender.is_bot if sender else False
|
is_bot = sender.is_bot if sender else False
|
||||||
converted = await self._msg_conv.convert(
|
converted = await self._msg_conv.convert(
|
||||||
source,
|
source,
|
||||||
@@ -3078,50 +3046,15 @@ class Portal(DBPortal, BasePortal):
|
|||||||
forward: bool,
|
forward: bool,
|
||||||
anchor_id: int,
|
anchor_id: int,
|
||||||
limit: int,
|
limit: int,
|
||||||
prev_event_id: EventID,
|
) -> tuple[int, int, TelegramID]:
|
||||||
) -> tuple[EventID | None, int, int, TelegramID]:
|
|
||||||
entity = await self.get_input_entity(source)
|
entity = await self.get_input_entity(source)
|
||||||
events = []
|
events = []
|
||||||
intents = []
|
intents = []
|
||||||
metas = []
|
metas = []
|
||||||
state_events = []
|
|
||||||
do_batch_send = self.backfill_msc2716
|
|
||||||
added_members = (
|
|
||||||
await self._get_members_at(prev_event_id)
|
|
||||||
if not self.bridge.homeserver_software.is_hungry and do_batch_send
|
|
||||||
else set()
|
|
||||||
)
|
|
||||||
before_first_msg_timestamp = 0
|
|
||||||
tg_space = self.tgid if self.peer_type == "channel" else source.tgid
|
tg_space = self.tgid if self.peer_type == "channel" else source.tgid
|
||||||
|
|
||||||
async def add_member(intent: IntentAPI, displayname: str, avatar_url: ContentURI) -> None:
|
|
||||||
if self.bridge.homeserver_software.is_hungry or intent.mxid in added_members:
|
|
||||||
return
|
|
||||||
added_members.add(intent.mxid)
|
|
||||||
if not do_batch_send:
|
|
||||||
# TODO leave these members?
|
|
||||||
await intent.ensure_joined(self.mxid)
|
|
||||||
return
|
|
||||||
invite_event = BatchSendStateEvent(
|
|
||||||
type=EventType.ROOM_MEMBER,
|
|
||||||
state_key=intent.mxid,
|
|
||||||
sender=self.main_intent.mxid,
|
|
||||||
timestamp=before_first_msg_timestamp,
|
|
||||||
content=MemberStateEventContent(
|
|
||||||
membership=Membership.INVITE,
|
|
||||||
displayname=displayname,
|
|
||||||
avatar_url=avatar_url,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
join_event = attr.evolve(
|
|
||||||
invite_event,
|
|
||||||
content=attr.evolve(invite_event.content, membership=Membership.JOIN),
|
|
||||||
sender=intent.mxid,
|
|
||||||
)
|
|
||||||
state_events.append(invite_event)
|
|
||||||
state_events.append(join_event)
|
|
||||||
|
|
||||||
lowest_id = 0
|
lowest_id = 0
|
||||||
|
first_id_found = False
|
||||||
first_id = anchor_id
|
first_id = anchor_id
|
||||||
message_count = 0
|
message_count = 0
|
||||||
minmax = {"min_id": anchor_id} if forward else {"max_id": anchor_id}
|
minmax = {"min_id": anchor_id} if forward else {"max_id": anchor_id}
|
||||||
@@ -3129,9 +3062,16 @@ class Portal(DBPortal, BasePortal):
|
|||||||
anchor_id = 2**31 - 1
|
anchor_id = 2**31 - 1
|
||||||
minmax = {}
|
minmax = {}
|
||||||
self.log.debug(f"Iterating messages through {source.tgid} with {limit=}, {minmax}")
|
self.log.debug(f"Iterating messages through {source.tgid} with {limit=}, {minmax}")
|
||||||
|
delay_warn_handle = self.loop.call_later(
|
||||||
|
5 * 60, lambda: self.log.warning("Iterating messages is taking long")
|
||||||
|
)
|
||||||
# Iterate messages newest to oldest and collect the results
|
# Iterate messages newest to oldest and collect the results
|
||||||
async for msg in client.iter_messages(entity, limit=limit, **minmax):
|
async for msg in client.iter_messages(entity, limit=limit, **minmax):
|
||||||
message_count += 1
|
message_count += 1
|
||||||
|
if message_count == 1:
|
||||||
|
self.log.debug(f"Backfill iter: got first message {msg.id}")
|
||||||
|
elif message_count % 50 == 0:
|
||||||
|
self.log.debug(f"Backfill iter: got {message_count} messages so far (at {msg.id})")
|
||||||
if (forward and msg.id <= anchor_id) or (not forward and msg.id >= anchor_id):
|
if (forward and msg.id <= anchor_id) or (not forward and msg.id >= anchor_id):
|
||||||
continue
|
continue
|
||||||
elif isinstance(msg, MessageService):
|
elif isinstance(msg, MessageService):
|
||||||
@@ -3139,11 +3079,11 @@ class Portal(DBPortal, BasePortal):
|
|||||||
continue
|
continue
|
||||||
if not lowest_id or msg.id < lowest_id:
|
if not lowest_id or msg.id < lowest_id:
|
||||||
lowest_id = msg.id
|
lowest_id = msg.id
|
||||||
if not before_first_msg_timestamp:
|
if not first_id_found:
|
||||||
first_id = msg.id
|
first_id = msg.id
|
||||||
before_first_msg_timestamp = int(msg.date.timestamp() * 1000) - 1
|
first_id_found = True
|
||||||
|
|
||||||
converted, intent = await self._convert_batch_msg(source, client, msg, add_member)
|
converted, intent = await self._convert_batch_msg(source, client, msg)
|
||||||
if converted is None:
|
if converted is None:
|
||||||
continue
|
continue
|
||||||
d_event_id = None
|
d_event_id = None
|
||||||
@@ -3156,33 +3096,27 @@ class Portal(DBPortal, BasePortal):
|
|||||||
events.append(await self._wrap_batch_msg(intent, msg, converted, caption=True))
|
events.append(await self._wrap_batch_msg(intent, msg, converted, caption=True))
|
||||||
intents.append(intent)
|
intents.append(intent)
|
||||||
metas.append(None)
|
metas.append(None)
|
||||||
|
delay_warn_handle.cancel()
|
||||||
if len(events) == 0:
|
if len(events) == 0:
|
||||||
self.log.debug(
|
self.log.debug(
|
||||||
f"Didn't get any events to send out of {message_count} messages fetched "
|
f"Didn't get any events to send out of {message_count} messages fetched "
|
||||||
f"(first received ID: {first_id}, lowest: {lowest_id})"
|
f"(first received ID: {first_id}, lowest: {lowest_id})"
|
||||||
)
|
)
|
||||||
return None, 0, message_count, lowest_id
|
return 0, message_count, lowest_id
|
||||||
self.log.debug(
|
self.log.debug(
|
||||||
f"Got {len(events)} events to send out of {message_count} messages fetched "
|
f"Got {len(events)} events to send out of {message_count} messages fetched "
|
||||||
f"(first received ID: {first_id}, lowest: {lowest_id})"
|
f"(first received ID: {first_id}, lowest: {lowest_id})"
|
||||||
)
|
)
|
||||||
if do_batch_send:
|
if self._enable_batch_sending:
|
||||||
resp = await self.main_intent.batch_send(
|
resp = await self.main_intent.beeper_batch_send(
|
||||||
self.mxid,
|
self.mxid,
|
||||||
prev_event_id,
|
|
||||||
batch_id=self.next_batch_id if not forward else None,
|
|
||||||
# We iterated the events in reverse chronological order,
|
# We iterated the events in reverse chronological order,
|
||||||
# so reverse them before sending
|
# so reverse them before sending
|
||||||
events=list(reversed(events)),
|
events=list(reversed(events)),
|
||||||
state_events_at_start=state_events,
|
forward=forward,
|
||||||
beeper_new_messages=forward,
|
|
||||||
)
|
)
|
||||||
if prev_event_id == self.first_event_id and resp.next_batch_id:
|
|
||||||
self.next_batch_id = resp.next_batch_id
|
|
||||||
base_insertion_event_id = resp.base_insertion_event_id
|
|
||||||
event_ids = resp.event_ids
|
event_ids = resp.event_ids
|
||||||
else:
|
else:
|
||||||
base_insertion_event_id = None
|
|
||||||
event_ids = [
|
event_ids = [
|
||||||
await intent.send_message_event(
|
await intent.send_message_event(
|
||||||
self.mxid, evt.type, evt.content, timestamp=evt.timestamp
|
self.mxid, evt.type, evt.content, timestamp=evt.timestamp
|
||||||
@@ -3207,16 +3141,20 @@ class Portal(DBPortal, BasePortal):
|
|||||||
if msg is not None
|
if msg is not None
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
return base_insertion_event_id, len(events), message_count, lowest_id
|
return len(events), message_count, lowest_id
|
||||||
|
|
||||||
def _split_dm_reaction_counts(self, counts: list[ReactionCount]) -> list[MessagePeerReaction]:
|
def _split_dm_reaction_counts(self, counts: list[ReactionCount]) -> list[MessagePeerReaction]:
|
||||||
reactions = []
|
reactions = []
|
||||||
for item in counts:
|
for item in counts:
|
||||||
if item.count == 2:
|
if item.count == 2:
|
||||||
reactions += [
|
reactions += [
|
||||||
MessagePeerReaction(reaction=item.reaction, peer_id=PeerUser(self.tgid)),
|
|
||||||
MessagePeerReaction(
|
MessagePeerReaction(
|
||||||
reaction=item.reaction, peer_id=PeerUser(self.tg_receiver)
|
reaction=item.reaction, peer_id=PeerUser(self.tgid), date=None
|
||||||
|
),
|
||||||
|
MessagePeerReaction(
|
||||||
|
reaction=item.reaction,
|
||||||
|
peer_id=PeerUser(self.tg_receiver),
|
||||||
|
date=None,
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
elif item.count == 1:
|
elif item.count == 1:
|
||||||
@@ -3224,6 +3162,7 @@ class Portal(DBPortal, BasePortal):
|
|||||||
MessagePeerReaction(
|
MessagePeerReaction(
|
||||||
reaction=item.reaction,
|
reaction=item.reaction,
|
||||||
peer_id=PeerUser(self.tg_receiver if item.chosen_order else self.tgid),
|
peer_id=PeerUser(self.tg_receiver if item.chosen_order else self.tgid),
|
||||||
|
date=None,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
return reactions
|
return reactions
|
||||||
@@ -3587,7 +3526,9 @@ class Portal(DBPortal, BasePortal):
|
|||||||
async def _mark_disappearing(
|
async def _mark_disappearing(
|
||||||
self, event_id: EventID, seconds: int, expires_at: int | None
|
self, event_id: EventID, seconds: int, expires_at: int | None
|
||||||
) -> None:
|
) -> None:
|
||||||
dm = DisappearingMessage(self.mxid, event_id, seconds, expiration_ts=expires_at * 1000)
|
dm = DisappearingMessage(
|
||||||
|
self.mxid, event_id, seconds, expiration_ts=expires_at * 1000 if expires_at else None
|
||||||
|
)
|
||||||
await dm.insert()
|
await dm.insert()
|
||||||
if expires_at:
|
if expires_at:
|
||||||
background_task.create(self._disappear_event(dm))
|
background_task.create(self._disappear_event(dm))
|
||||||
|
|||||||
@@ -34,6 +34,8 @@ from telethon.tl.types import (
|
|||||||
DocumentAttributeVideo,
|
DocumentAttributeVideo,
|
||||||
Game,
|
Game,
|
||||||
InputPhotoFileLocation,
|
InputPhotoFileLocation,
|
||||||
|
InputStickerSetID,
|
||||||
|
InputStickerSetShortName,
|
||||||
Message,
|
Message,
|
||||||
MessageEntityPre,
|
MessageEntityPre,
|
||||||
MessageMediaContact,
|
MessageMediaContact,
|
||||||
@@ -42,11 +44,14 @@ 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,
|
||||||
PeerUser,
|
PeerUser,
|
||||||
Photo,
|
Photo,
|
||||||
@@ -104,6 +109,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 +148,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())
|
||||||
|
|
||||||
@@ -252,21 +260,48 @@ 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.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 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)
|
||||||
no_fallback = no_fallback or self.config["bridge.disable_reply_fallbacks"]
|
no_fallback = no_fallback or self.config["bridge.disable_reply_fallbacks"]
|
||||||
if not msg or msg.mx_room != self.portal.mxid:
|
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.
|
||||||
@@ -281,6 +316,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:
|
||||||
@@ -429,9 +466,21 @@ 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(
|
async def _convert_document(
|
||||||
self,
|
self,
|
||||||
source: au.AbstractUser,
|
source: au.AbstractUser,
|
||||||
@@ -538,7 +587,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
|
||||||
@@ -700,10 +749,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
|
||||||
@@ -711,6 +783,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):
|
||||||
@@ -724,17 +803,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,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -760,6 +840,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:
|
||||||
@@ -779,6 +866,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:
|
||||||
|
|||||||
@@ -269,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
|
||||||
)
|
)
|
||||||
|
|
||||||
self.is_bot = is_bot
|
if is_bot is not None:
|
||||||
|
self.is_bot = is_bot
|
||||||
self.is_channel = is_channel
|
self.is_channel = is_channel
|
||||||
self.is_premium = is_premium
|
if is_premium is not None:
|
||||||
|
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
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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,
|
||||||
@@ -215,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"
|
||||||
@@ -236,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.
|
||||||
@@ -617,8 +618,11 @@ 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?
|
||||||
await self.push_bridge_state(state, error=error, message=message)
|
if self.tgid or state != BridgeStateEvent.LOGGED_OUT:
|
||||||
|
await self.push_bridge_state(state, error=error, message=message)
|
||||||
if delete:
|
if delete:
|
||||||
await self.delete()
|
await self.delete()
|
||||||
self.by_mxid.pop(self.mxid, None)
|
self.by_mxid.pop(self.mxid, None)
|
||||||
@@ -974,11 +978,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(
|
||||||
@@ -988,13 +999,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:
|
||||||
|
|||||||
@@ -7,14 +7,14 @@ aiodns
|
|||||||
brotli
|
brotli
|
||||||
|
|
||||||
#/qr_login
|
#/qr_login
|
||||||
pillow>=4,<10
|
pillow>=10.0.1,<11
|
||||||
qrcode>=6,<8
|
qrcode>=6,<8
|
||||||
|
|
||||||
#/formattednumbers
|
#/formattednumbers
|
||||||
phonenumbers>=8,<9
|
phonenumbers>=8,<9
|
||||||
|
|
||||||
#/metrics
|
#/metrics
|
||||||
prometheus_client>=0.6,<0.17
|
prometheus_client>=0.6,<0.19
|
||||||
|
|
||||||
#/e2be
|
#/e2be
|
||||||
python-olm>=3,<4
|
python-olm>=3,<4
|
||||||
@@ -23,3 +23,6 @@ unpaddedbase64>=1,<3
|
|||||||
|
|
||||||
#/sqlite
|
#/sqlite
|
||||||
aiosqlite>=0.16,<0.20
|
aiosqlite>=0.16,<0.20
|
||||||
|
|
||||||
|
#/proxy
|
||||||
|
python-socks[asyncio]
|
||||||
|
|||||||
+4
-4
@@ -1,10 +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.14,<0.20
|
mautrix>=0.20.3,<0.21
|
||||||
tulir-telethon==1.28.0a9
|
tulir-telethon==1.33.0a1
|
||||||
asyncpg>=0.20,<0.28
|
asyncpg>=0.20,<0.30
|
||||||
mako>=1,<2
|
mako>=1,<2
|
||||||
setuptools
|
setuptools
|
||||||
|
|||||||
@@ -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.9",
|
||||||
|
|
||||||
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.9",
|
||||||
"Programming Language :: Python :: 3.10",
|
"Programming Language :: Python :: 3.10",
|
||||||
|
"Programming Language :: Python :: 3.11",
|
||||||
|
"Programming Language :: Python :: 3.12",
|
||||||
],
|
],
|
||||||
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