Compare commits
46 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 |
@@ -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
|
||||||
|
|||||||
@@ -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?$
|
||||||
|
|||||||
@@ -1,3 +1,24 @@
|
|||||||
|
# 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)
|
# v0.15.1 (2023-12-26)
|
||||||
|
|
||||||
* Updated Telegram API to layer 169.
|
* Updated Telegram API to layer 169.
|
||||||
|
|||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
FROM dock.mau.dev/tulir/lottieconverter:alpine-3.19
|
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 \
|
||||||
|
|||||||
@@ -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.15.1"
|
__version__ = "0.15.3"
|
||||||
__author__ = "Tulir Asokan <tulir@maunium.net>"
|
__author__ = "Tulir Asokan <tulir@maunium.net>"
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ from telethon.tl.types import (
|
|||||||
PeerUser,
|
PeerUser,
|
||||||
PhoneCallRequested,
|
PhoneCallRequested,
|
||||||
TypeUpdate,
|
TypeUpdate,
|
||||||
|
UpdateBotMessageReaction,
|
||||||
UpdateChannel,
|
UpdateChannel,
|
||||||
UpdateChannelUserTyping,
|
UpdateChannelUserTyping,
|
||||||
UpdateChatDefaultBannedRights,
|
UpdateChatDefaultBannedRights,
|
||||||
@@ -240,6 +241,24 @@ class AbstractUser(ABC):
|
|||||||
use_ipv6=self.config["telegram.connection.use_ipv6"],
|
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:
|
||||||
@@ -363,6 +382,8 @@ class AbstractUser(ABC):
|
|||||||
await self.update_phone_call(update)
|
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):
|
||||||
@@ -636,6 +657,12 @@ 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:
|
async def update_phone_call(self, update: UpdatePhoneCall) -> None:
|
||||||
self.log.debug("Phone call update %s", update)
|
self.log.debug("Phone call update %s", update)
|
||||||
if not isinstance(update.phone_call, PhoneCallRequested):
|
if not isinstance(update.phone_call, PhoneCallRequested):
|
||||||
|
|||||||
@@ -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),
|
||||||
|
|||||||
@@ -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")
|
||||||
@@ -262,6 +264,7 @@ 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")
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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,8 +271,19 @@ 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.
|
||||||
@@ -341,7 +356,7 @@ bridge:
|
|||||||
private_chat_portal_meta: default
|
private_chat_portal_meta: default
|
||||||
# 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: true
|
||||||
# Should cross-chat replies from Telegram be bridged? Most servers and clients don't support this.
|
# Should cross-chat replies from Telegram be bridged? Most servers and clients don't support this.
|
||||||
cross_room_replies: false
|
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
|
||||||
@@ -576,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:
|
||||||
|
|||||||
@@ -155,14 +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 on the Telegram side for relaying messages sent by"
|
|
||||||
" unauthenticated Matrix 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:
|
||||||
|
|||||||
+99
-24
@@ -23,6 +23,7 @@ from typing import (
|
|||||||
Callable,
|
Callable,
|
||||||
List,
|
List,
|
||||||
Literal,
|
Literal,
|
||||||
|
NamedTuple,
|
||||||
Union,
|
Union,
|
||||||
cast,
|
cast,
|
||||||
)
|
)
|
||||||
@@ -49,6 +50,7 @@ from telethon.errors import (
|
|||||||
InputUserDeactivatedError,
|
InputUserDeactivatedError,
|
||||||
MessageEmptyError,
|
MessageEmptyError,
|
||||||
MessageIdInvalidError,
|
MessageIdInvalidError,
|
||||||
|
MessageNotModifiedError,
|
||||||
MessageTooLongError,
|
MessageTooLongError,
|
||||||
PhotoExtInvalidError,
|
PhotoExtInvalidError,
|
||||||
PhotoInvalidDimensionsError,
|
PhotoInvalidDimensionsError,
|
||||||
@@ -68,7 +70,6 @@ from telethon.tl.functions.channels import (
|
|||||||
InviteToChannelRequest,
|
InviteToChannelRequest,
|
||||||
JoinChannelRequest,
|
JoinChannelRequest,
|
||||||
UpdateUsernameRequest,
|
UpdateUsernameRequest,
|
||||||
ViewSponsoredMessageRequest,
|
|
||||||
)
|
)
|
||||||
from telethon.tl.functions.messages import (
|
from telethon.tl.functions.messages import (
|
||||||
AddChatUserRequest,
|
AddChatUserRequest,
|
||||||
@@ -85,6 +86,7 @@ from telethon.tl.functions.messages import (
|
|||||||
SetTypingRequest,
|
SetTypingRequest,
|
||||||
UnpinAllMessagesRequest,
|
UnpinAllMessagesRequest,
|
||||||
UpdatePinnedMessageRequest,
|
UpdatePinnedMessageRequest,
|
||||||
|
ViewSponsoredMessageRequest,
|
||||||
)
|
)
|
||||||
from telethon.tl.patched import Message, MessageService
|
from telethon.tl.patched import Message, MessageService
|
||||||
from telethon.tl.types import (
|
from telethon.tl.types import (
|
||||||
@@ -113,6 +115,7 @@ from telethon.tl.types import (
|
|||||||
InputPeerUser,
|
InputPeerUser,
|
||||||
InputStickerSetEmpty,
|
InputStickerSetEmpty,
|
||||||
InputUser,
|
InputUser,
|
||||||
|
MessageActionBoostApply,
|
||||||
MessageActionChannelCreate,
|
MessageActionChannelCreate,
|
||||||
MessageActionChatAddUser,
|
MessageActionChatAddUser,
|
||||||
MessageActionChatCreate,
|
MessageActionChatCreate,
|
||||||
@@ -159,6 +162,7 @@ from telethon.tl.types import (
|
|||||||
TypeUser,
|
TypeUser,
|
||||||
TypeUserFull,
|
TypeUserFull,
|
||||||
TypeUserProfilePhoto,
|
TypeUserProfilePhoto,
|
||||||
|
UpdateBotMessageReaction,
|
||||||
UpdateChannelUserTyping,
|
UpdateChannelUserTyping,
|
||||||
UpdateChatUserTyping,
|
UpdateChatUserTyping,
|
||||||
UpdateMessageReactions,
|
UpdateMessageReactions,
|
||||||
@@ -269,6 +273,11 @@ class IgnoredMessageError(Exception):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class WrappedReaction(NamedTuple):
|
||||||
|
reaction: ReactionEmoji | ReactionCustomEmoji
|
||||||
|
date: datetime | None
|
||||||
|
|
||||||
|
|
||||||
class Portal(DBPortal, BasePortal):
|
class Portal(DBPortal, BasePortal):
|
||||||
bot: "Bot"
|
bot: "Bot"
|
||||||
config: Config
|
config: Config
|
||||||
@@ -435,6 +444,10 @@ class Portal(DBPortal, BasePortal):
|
|||||||
def is_direct(self) -> bool:
|
def is_direct(self) -> bool:
|
||||||
return self.peer_type == "user"
|
return self.peer_type == "user"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_channel(self) -> bool:
|
||||||
|
return self.peer_type == "channel"
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def has_bot(self) -> bool:
|
def has_bot(self) -> bool:
|
||||||
return bool(self.bot) and (
|
return bool(self.bot) and (
|
||||||
@@ -1025,6 +1038,7 @@ class Portal(DBPortal, BasePortal):
|
|||||||
initial_state=initial_state,
|
initial_state=initial_state,
|
||||||
creation_content=creation_content,
|
creation_content=creation_content,
|
||||||
beeper_auto_join_invites=autojoin_invites,
|
beeper_auto_join_invites=autojoin_invites,
|
||||||
|
room_version="11",
|
||||||
)
|
)
|
||||||
if not room_id:
|
if not room_id:
|
||||||
raise Exception(f"Failed to create room")
|
raise Exception(f"Failed to create room")
|
||||||
@@ -1187,7 +1201,7 @@ class Portal(DBPortal, BasePortal):
|
|||||||
continue
|
continue
|
||||||
if mx_user.is_bot:
|
if mx_user.is_bot:
|
||||||
await mx_user.unregister_portal(*self.tgid_full)
|
await mx_user.unregister_portal(*self.tgid_full)
|
||||||
if not self.has_bot:
|
if not self.has_bot and mx_user.tgid:
|
||||||
try:
|
try:
|
||||||
await self.main_intent.kick_user(
|
await self.main_intent.kick_user(
|
||||||
self.mxid, mx_user.mxid, "You had left this Telegram chat."
|
self.mxid, mx_user.mxid, "You had left this Telegram chat."
|
||||||
@@ -2133,7 +2147,7 @@ class Portal(DBPortal, BasePortal):
|
|||||||
status.status = MessageStatus.FAIL
|
status.status = MessageStatus.FAIL
|
||||||
elif err:
|
elif err:
|
||||||
status.reason = MessageStatusReason.GENERIC_ERROR
|
status.reason = MessageStatusReason.GENERIC_ERROR
|
||||||
status.error = f"{type(err)}: {err}"
|
status.error = f"{type(err).__name__}: {err}"
|
||||||
status.status = MessageStatus.RETRIABLE
|
status.status = MessageStatus.RETRIABLE
|
||||||
status.message = self._error_to_human_message(err)
|
status.message = self._error_to_human_message(err)
|
||||||
else:
|
else:
|
||||||
@@ -2164,9 +2178,10 @@ class Portal(DBPortal, BasePortal):
|
|||||||
)
|
)
|
||||||
|
|
||||||
if msg and self.config["bridge.delivery_error_reports"]:
|
if msg and self.config["bridge.delivery_error_reports"]:
|
||||||
await self._send_message(
|
if not isinstance(err, MessageNotModifiedError):
|
||||||
self.main_intent, TextMessageEventContent(msgtype=MessageType.NOTICE, body=msg)
|
await self._send_message(
|
||||||
)
|
self.main_intent, TextMessageEventContent(msgtype=MessageType.NOTICE, body=msg)
|
||||||
|
)
|
||||||
await self._send_message_status(event_id, err)
|
await self._send_message_status(event_id, err)
|
||||||
|
|
||||||
async def handle_matrix_message(
|
async def handle_matrix_message(
|
||||||
@@ -2184,7 +2199,6 @@ class Portal(DBPortal, BasePortal):
|
|||||||
message_type=content.msgtype,
|
message_type=content.msgtype,
|
||||||
msg=f"\u26a0 Your message may not have been bridged: {e}",
|
msg=f"\u26a0 Your message may not have been bridged: {e}",
|
||||||
)
|
)
|
||||||
raise
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
if isinstance(e, IgnoredMessageError):
|
if isinstance(e, IgnoredMessageError):
|
||||||
self.log.debug(f"Ignored {event_id}: {e}")
|
self.log.debug(f"Ignored {event_id}: {e}")
|
||||||
@@ -2323,20 +2337,17 @@ class Portal(DBPortal, BasePortal):
|
|||||||
sender.command_status = None
|
sender.command_status = None
|
||||||
except (KeyError, TypeError):
|
except (KeyError, TypeError):
|
||||||
if not logged_in or (
|
if not logged_in or (
|
||||||
"filename" in content and content["filename"] != content.body
|
content.filename is not None and content.filename != content.body
|
||||||
):
|
):
|
||||||
if "filename" in content:
|
if content.filename:
|
||||||
file_name = content["filename"]
|
file_name = content.filename
|
||||||
caption_content = TextMessageEventContent(
|
caption_content = TextMessageEventContent(
|
||||||
msgtype=MessageType.TEXT,
|
msgtype=MessageType.TEXT,
|
||||||
body=content.body,
|
body=content.body,
|
||||||
)
|
)
|
||||||
if (
|
if content.formatted_body and content.format == Format.HTML:
|
||||||
"formatted_body" in content
|
caption_content.formatted_body = content.formatted_body
|
||||||
and str(content.get("format")) == Format.HTML.value
|
caption_content.format = Format.HTML
|
||||||
):
|
|
||||||
caption_content["formatted_body"] = content["formatted_body"]
|
|
||||||
caption_content["format"] = Format.HTML
|
|
||||||
else:
|
else:
|
||||||
caption_content = None
|
caption_content = None
|
||||||
if caption_content:
|
if caption_content:
|
||||||
@@ -2800,7 +2811,7 @@ class Portal(DBPortal, BasePortal):
|
|||||||
intent = sender.intent_for(self) if sender else self.main_intent
|
intent = sender.intent_for(self) if sender else self.main_intent
|
||||||
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, intent, is_bot, evt, no_reply_fallback=True
|
source, intent, is_bot, self.is_channel, evt, no_reply_fallback=True
|
||||||
)
|
)
|
||||||
converted.content.set_edit(editing_msg.mxid)
|
converted.content.set_edit(editing_msg.mxid)
|
||||||
await intent.set_typing(self.mxid, timeout=0)
|
await intent.set_typing(self.mxid, timeout=0)
|
||||||
@@ -3016,6 +3027,7 @@ class Portal(DBPortal, BasePortal):
|
|||||||
source,
|
source,
|
||||||
intent,
|
intent,
|
||||||
is_bot,
|
is_bot,
|
||||||
|
self.is_channel,
|
||||||
msg,
|
msg,
|
||||||
client=client,
|
client=client,
|
||||||
deterministic_reply_id=self.bridge.homeserver_software.is_hungry,
|
deterministic_reply_id=self.bridge.homeserver_software.is_hungry,
|
||||||
@@ -3250,12 +3262,40 @@ class Portal(DBPortal, BasePortal):
|
|||||||
recent_reactions = resp.reactions
|
recent_reactions = resp.reactions
|
||||||
|
|
||||||
async with self.reaction_lock(dbm.mxid):
|
async with self.reaction_lock(dbm.mxid):
|
||||||
await self._handle_telegram_reactions_locked(
|
await self._handle_telegram_user_reactions_locked(
|
||||||
source, dbm, recent_reactions, total_count, timestamp=timestamp
|
source, dbm, recent_reactions, total_count, timestamp=timestamp
|
||||||
)
|
)
|
||||||
|
|
||||||
|
async def handle_telegram_bot_reactions(
|
||||||
|
self, source: au.AbstractUser, update: UpdateBotMessageReaction
|
||||||
|
) -> None:
|
||||||
|
tg_space = self.tgid if self.peer_type == "channel" else source.tgid
|
||||||
|
dbm = await DBMessage.get_one_by_tgid(TelegramID(update.msg_id), tg_space)
|
||||||
|
if dbm is None:
|
||||||
|
return
|
||||||
|
reactions: dict[TelegramID, list[WrappedReaction]] = {}
|
||||||
|
custom_emoji_ids: list[int] = []
|
||||||
|
if isinstance(update.actor, PeerUser):
|
||||||
|
user_id = TelegramID(update.actor.user_id)
|
||||||
|
elif isinstance(update.actor, PeerChannel):
|
||||||
|
user_id = TelegramID(update.actor.channel_id)
|
||||||
|
else:
|
||||||
|
return
|
||||||
|
for reaction in update.new_reactions:
|
||||||
|
reactions.setdefault(user_id, []).append(WrappedReaction(reaction=reaction, date=None))
|
||||||
|
async with self.reaction_lock(dbm.mxid):
|
||||||
|
await self._handle_telegram_parsed_reactions_locked(
|
||||||
|
source,
|
||||||
|
dbm,
|
||||||
|
reactions,
|
||||||
|
custom_emoji_ids,
|
||||||
|
is_full=True,
|
||||||
|
only_user_id=user_id,
|
||||||
|
timestamp=update.date,
|
||||||
|
)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _reactions_filter(lst: list[MessagePeerReaction], existing: DBReaction) -> bool:
|
def _reactions_filter(lst: list[WrappedReaction], existing: DBReaction) -> bool:
|
||||||
if not lst:
|
if not lst:
|
||||||
return False
|
return False
|
||||||
for wrapped_reaction in lst:
|
for wrapped_reaction in lst:
|
||||||
@@ -3278,7 +3318,7 @@ class Portal(DBPortal, BasePortal):
|
|||||||
return await source.get_max_reactions(is_premium)
|
return await source.get_max_reactions(is_premium)
|
||||||
return 3 if is_premium else 1
|
return 3 if is_premium else 1
|
||||||
|
|
||||||
async def _handle_telegram_reactions_locked(
|
async def _handle_telegram_user_reactions_locked(
|
||||||
self,
|
self,
|
||||||
source: au.AbstractUser,
|
source: au.AbstractUser,
|
||||||
msg: DBMessage,
|
msg: DBMessage,
|
||||||
@@ -3286,17 +3326,38 @@ class Portal(DBPortal, BasePortal):
|
|||||||
total_count: int,
|
total_count: int,
|
||||||
timestamp: datetime | None = None,
|
timestamp: datetime | None = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
reactions: dict[TelegramID, list[MessagePeerReaction]] = {}
|
reactions: dict[TelegramID, list[WrappedReaction]] = {}
|
||||||
custom_emoji_ids: list[int] = []
|
custom_emoji_ids: list[int] = []
|
||||||
for reaction in reaction_list:
|
for reaction in reaction_list:
|
||||||
if isinstance(reaction.peer_id, (PeerUser, PeerChannel)) and isinstance(
|
if isinstance(reaction.peer_id, (PeerUser, PeerChannel)) and isinstance(
|
||||||
reaction.reaction, (ReactionEmoji, ReactionCustomEmoji)
|
reaction.reaction, (ReactionEmoji, ReactionCustomEmoji)
|
||||||
):
|
):
|
||||||
sender_user_id = p.Puppet.get_id_from_peer(reaction.peer_id)
|
sender_user_id = p.Puppet.get_id_from_peer(reaction.peer_id)
|
||||||
reactions.setdefault(sender_user_id, []).append(reaction)
|
reactions.setdefault(sender_user_id, []).append(
|
||||||
|
WrappedReaction(reaction.reaction, reaction.date)
|
||||||
|
)
|
||||||
if isinstance(reaction.reaction, ReactionCustomEmoji):
|
if isinstance(reaction.reaction, ReactionCustomEmoji):
|
||||||
custom_emoji_ids.append(reaction.reaction.document_id)
|
custom_emoji_ids.append(reaction.reaction.document_id)
|
||||||
is_full = len(reaction_list) == total_count
|
is_full = len(reaction_list) == total_count
|
||||||
|
await self._handle_telegram_parsed_reactions_locked(
|
||||||
|
source,
|
||||||
|
msg,
|
||||||
|
reactions,
|
||||||
|
custom_emoji_ids,
|
||||||
|
is_full=is_full,
|
||||||
|
timestamp=timestamp,
|
||||||
|
)
|
||||||
|
|
||||||
|
async def _handle_telegram_parsed_reactions_locked(
|
||||||
|
self,
|
||||||
|
source: au.AbstractUser,
|
||||||
|
msg: DBMessage,
|
||||||
|
reactions: dict[TelegramID, list[WrappedReaction]],
|
||||||
|
custom_emoji_ids: list[int],
|
||||||
|
is_full: bool,
|
||||||
|
only_user_id: TelegramID | None = None,
|
||||||
|
timestamp: datetime | None = None,
|
||||||
|
) -> None:
|
||||||
custom_emojis = await util.transfer_custom_emojis_to_matrix(source, custom_emoji_ids)
|
custom_emojis = await util.transfer_custom_emojis_to_matrix(source, custom_emoji_ids)
|
||||||
|
|
||||||
existing_reactions = await DBReaction.get_all_by_message(msg.mxid, msg.mx_room)
|
existing_reactions = await DBReaction.get_all_by_message(msg.mxid, msg.mx_room)
|
||||||
@@ -3304,6 +3365,8 @@ class Portal(DBPortal, BasePortal):
|
|||||||
removed: list[DBReaction] = []
|
removed: list[DBReaction] = []
|
||||||
for existing_reaction in existing_reactions:
|
for existing_reaction in existing_reactions:
|
||||||
sender_id = existing_reaction.tg_sender
|
sender_id = existing_reaction.tg_sender
|
||||||
|
if only_user_id is not None and sender_id != only_user_id:
|
||||||
|
continue
|
||||||
new_reactions = reactions.get(sender_id)
|
new_reactions = reactions.get(sender_id)
|
||||||
if self._reactions_filter(new_reactions, existing_reaction):
|
if self._reactions_filter(new_reactions, existing_reaction):
|
||||||
if new_reactions is not None and len(new_reactions) == 0:
|
if new_reactions is not None and len(new_reactions) == 0:
|
||||||
@@ -3469,7 +3532,7 @@ class Portal(DBPortal, BasePortal):
|
|||||||
else:
|
else:
|
||||||
intent = self.main_intent
|
intent = self.main_intent
|
||||||
is_bot = sender.is_bot if sender else False
|
is_bot = sender.is_bot if sender else False
|
||||||
converted = await self._msg_conv.convert(source, intent, is_bot, evt)
|
converted = await self._msg_conv.convert(source, intent, is_bot, self.is_channel, evt)
|
||||||
if not converted:
|
if not converted:
|
||||||
return
|
return
|
||||||
await intent.set_typing(self.mxid, timeout=0)
|
await intent.set_typing(self.mxid, timeout=0)
|
||||||
@@ -3623,7 +3686,7 @@ class Portal(DBPortal, BasePortal):
|
|||||||
end_reason = "disconnected"
|
end_reason = "disconnected"
|
||||||
body = f"{call_type} {end_reason}"
|
body = f"{call_type} {end_reason}"
|
||||||
if action.duration:
|
if action.duration:
|
||||||
body += f" ({format_duration(action.duration)}"
|
body += f" ({format_duration(action.duration)})"
|
||||||
await self._send_message(
|
await self._send_message(
|
||||||
sender.intent_for(self),
|
sender.intent_for(self),
|
||||||
TextMessageEventContent(msgtype=MessageType.NOTICE, body=body),
|
TextMessageEventContent(msgtype=MessageType.NOTICE, body=body),
|
||||||
@@ -3651,6 +3714,18 @@ class Portal(DBPortal, BasePortal):
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
elif isinstance(action, MessageActionBoostApply):
|
||||||
|
await self._send_message(
|
||||||
|
sender.intent_for(self),
|
||||||
|
TextMessageEventContent(
|
||||||
|
msgtype=MessageType.EMOTE,
|
||||||
|
body=(
|
||||||
|
"boosted the group"
|
||||||
|
if action.boosts == 1
|
||||||
|
else f"boosted the group {action.boosts} times"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
elif isinstance(action, MessageActionGameScore):
|
elif isinstance(action, MessageActionGameScore):
|
||||||
# TODO handle game score
|
# TODO handle game score
|
||||||
pass
|
pass
|
||||||
|
|||||||
@@ -53,6 +53,7 @@ from telethon.tl.types import (
|
|||||||
MessageMediaWebPage,
|
MessageMediaWebPage,
|
||||||
MessageReplyStoryHeader,
|
MessageReplyStoryHeader,
|
||||||
PeerChannel,
|
PeerChannel,
|
||||||
|
PeerChat,
|
||||||
PeerUser,
|
PeerUser,
|
||||||
Photo,
|
Photo,
|
||||||
PhotoCachedSize,
|
PhotoCachedSize,
|
||||||
@@ -63,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
|
||||||
@@ -158,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,
|
||||||
@@ -166,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):
|
||||||
convert_media = self._media_converters[type(evt.media)]
|
if self._should_convert_full_document(evt.media, is_bot, is_channel):
|
||||||
converted = await convert_media(source=source, intent=intent, evt=evt, client=client)
|
convert_media = self._media_converters[type(evt.media)]
|
||||||
|
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:
|
||||||
@@ -200,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
|
||||||
@@ -263,7 +282,7 @@ class TelegramMessageConverter:
|
|||||||
elif isinstance(evt.reply_to, MessageReplyStoryHeader):
|
elif isinstance(evt.reply_to, MessageReplyStoryHeader):
|
||||||
return
|
return
|
||||||
|
|
||||||
if evt.reply_to.quote and content.msgtype.is_text:
|
if evt.reply_to.quote and content.msgtype and content.msgtype.is_text:
|
||||||
content.ensure_has_html()
|
content.ensure_has_html()
|
||||||
quote_html = await formatter.telegram_text_to_matrix_html(
|
quote_html = await formatter.telegram_text_to_matrix_html(
|
||||||
source, evt.reply_to.quote_text, evt.reply_to.quote_entities
|
source, evt.reply_to.quote_text, evt.reply_to.quote_entities
|
||||||
@@ -278,7 +297,15 @@ class TelegramMessageConverter:
|
|||||||
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 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"]:
|
if not self.config["bridge.cross_room_replies"]:
|
||||||
return
|
return
|
||||||
space = (
|
space = (
|
||||||
@@ -481,6 +508,91 @@ class TelegramMessageConverter:
|
|||||||
# but we can only count it from read receipt.
|
# but we can only count it from read receipt.
|
||||||
return ttl * 5
|
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(
|
||||||
self,
|
self,
|
||||||
source: au.AbstractUser,
|
source: au.AbstractUser,
|
||||||
@@ -490,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:
|
||||||
@@ -648,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>"
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -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
|
(
|
||||||
if portal.peer_type == "channel" and not portal.megagroup or dbr.send_messages
|
50
|
||||||
else 0,
|
if portal.peer_type == "channel" and not portal.megagroup or dbr.send_messages
|
||||||
|
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
|
||||||
|
|
||||||
|
|||||||
@@ -298,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
|
(
|
||||||
if self._is_backfilling
|
BridgeStateEvent.BACKFILLING
|
||||||
else BridgeStateEvent.CONNECTED,
|
if self._is_backfilling
|
||||||
|
else BridgeStateEvent.CONNECTED
|
||||||
|
),
|
||||||
info=self._bridge_state_info,
|
info=self._bridge_state_info,
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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)"
|
(
|
||||||
if delete
|
"Portal deleted (moving to another room)"
|
||||||
else "Room unbridged (portal moving to another room)",
|
if delete
|
||||||
|
else "Room unbridged (portal moving to another room)"
|
||||||
|
),
|
||||||
puppets_only=not delete,
|
puppets_only=not delete,
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
|
|||||||
@@ -2,27 +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>=10.0.1,<11
|
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.20
|
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.20
|
aiosqlite>=0.16,<0.22
|
||||||
|
|
||||||
#/proxy
|
#/proxy
|
||||||
python-socks[asyncio]
|
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"]
|
||||||
|
|||||||
+4
-4
@@ -3,8 +3,8 @@ 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.20.3,<0.21
|
mautrix>=0.21.0b5,<0.22
|
||||||
tulir-telethon==1.34.0a2
|
tulir-telethon==1.99.0a6
|
||||||
asyncpg>=0.20,<0.30
|
asyncpg>=0.20,<1
|
||||||
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.9",
|
python_requires="~=3.10",
|
||||||
|
|
||||||
classifiers=[
|
classifiers=[
|
||||||
"Development Status :: 4 - Beta",
|
"Development Status :: 4 - Beta",
|
||||||
@@ -60,10 +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.9",
|
|
||||||
"Programming Language :: Python :: 3.10",
|
"Programming Language :: Python :: 3.10",
|
||||||
"Programming Language :: Python :: 3.11",
|
"Programming Language :: Python :: 3.11",
|
||||||
"Programming Language :: Python :: 3.12",
|
"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