Compare commits

...

27 Commits

Author SHA1 Message Date
Tulir Asokan 4b03cddde8 Bump version to 0.8.2 2020-07-27 13:55:09 +03:00
Tulir Asokan 6eec096501 Bump minimum mautrix-python version to v0.5.8 2020-07-27 13:55:09 +03:00
Tulir Asokan 78cdb43c65 Fix using edge repos in docker image
Backported from 482a52cb5e
2020-07-27 13:54:54 +03:00
Tulir Asokan 4b57be3917 Bump version to 0.8.1 2020-06-08 17:45:19 +03:00
Tulir Asokan 9383e5eed2 Allow any 0.5.x version of mautrix-python
Fixes #479
2020-06-08 12:36:18 +03:00
Tulir Asokan a3b4a5e30e Update Docker image to Alpine 3.12 2020-06-06 20:10:14 +03:00
Tulir Asokan 72a45d7d80 Bump version to 0.8.0 2020-06-03 15:37:07 +03:00
Tulir Asokan bcf464428a Bump version to 0.8.0rc5 2020-05-30 13:18:58 +03:00
Tulir Asokan f3b9f4bf73 Bump maximum Telethon version 2020-05-29 15:28:53 +03:00
Tulir Asokan 10e54ed789 Add option to send delivery error notices 2020-05-29 15:28:41 +03:00
Tulir Asokan 35da8df526 Add option to disable removing avatars from Telegram ghosts
There's no way to determine whether an avatar is removed or just hidden
from some users, so avatars are not removed by default.
2020-05-29 15:27:38 +03:00
Tulir Asokan fb1ab220ff Update ROADMAP.md 2020-05-28 12:56:56 +03:00
Tulir Asokan 2dd39fddf0 Try to prevent infinite loop of state changes with double puppeting
Fixes #464
2020-05-27 12:36:51 +03:00
Tulir Asokan 7f69e9f329 Bump mautrix-python version 2020-05-25 14:11:03 +03:00
Tulir Asokan 3f6a4237ad Add option to send read receipt on confirmed delivery to Telegram 2020-05-25 13:25:37 +03:00
Tulir Asokan ee04e8c17f Bump mautrix-python req to rc1 2020-05-22 22:19:36 +03:00
Tulir Asokan 7d75c15027 Actually fix branch/tag condition in CI 2020-05-22 22:19:27 +03:00
Tulir Asokan 312a44d361 Add sponsors section to README.md 2020-05-22 22:00:23 +03:00
Tulir Asokan 85d38e3db6 Bump version to 0.8.0rc3 2020-05-22 20:49:47 +03:00
Tulir Asokan 3a25ee2c93 Merge pull request #468 from davidmehren/fix-peerchannel-admin
Fix admin detection in _can_use_commands
2020-05-22 20:02:53 +03:00
Tulir Asokan a4d49a41e0 Maybe fix branch condition in CI 2020-05-21 19:35:04 +03:00
David Mehren 7ba9e10f0f Fix admin detection in _can_use_commands 2020-05-21 09:44:27 +02:00
Tulir Asokan 05e966011e Fix error syncing private chat portals with no avatar 2020-05-20 23:29:36 +03:00
Tulir Asokan 9081f6bce4 Bump mautrix-python requirement 2020-05-20 23:17:42 +03:00
Tulir Asokan c126e8b615 Actually ignore ChatForbidden when syncing. Fixes #446 2020-05-20 22:45:22 +03:00
Tulir Asokan f454803ef7 Move private information to trace log level. Fixes #321 2020-05-20 22:40:20 +03:00
Tulir Asokan 40beb8f752 Add private_chat_portal_meta option and fix bugs
* The new option is implicitly enabled when encryption is default
* Private chat metadata is now updated after creating the room too
* The puppet metadata is updated before creating the room, to make sure their
  name is available locally
2020-05-20 21:19:42 +03:00
21 changed files with 243 additions and 161 deletions
+2 -2
View File
@@ -36,6 +36,6 @@ manifest:
script: script:
- docker pull $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA-amd64 - docker pull $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA-amd64
- docker pull $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA-arm64 - docker pull $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA-arm64
- if [ $CI_COMMIT_BRANCH == "master" ]; then docker manifest create $CI_REGISTRY_IMAGE:latest $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA-amd64 $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA-arm64 && docker manifest push $CI_REGISTRY_IMAGE:latest; fi - if [ "$CI_COMMIT_BRANCH" = "master" ]; then docker manifest create $CI_REGISTRY_IMAGE:latest $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA-amd64 $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA-arm64 && docker manifest push $CI_REGISTRY_IMAGE:latest; fi
- if [ $CI_COMMIT_BRANCH != "master" ]; then docker manifest create $CI_REGISTRY_IMAGE:$CI_COMMIT_REF_NAME $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA-amd64 $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA-arm64 && docker manifest push $CI_REGISTRY_IMAGE:$CI_COMMIT_REF_NAME; fi - if [ "$CI_COMMIT_BRANCH" != "master" ]; then docker manifest create $CI_REGISTRY_IMAGE:$CI_COMMIT_REF_NAME $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA-amd64 $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA-arm64 && docker manifest push $CI_REGISTRY_IMAGE:$CI_COMMIT_REF_NAME; fi
- docker rmi $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA-amd64 $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA-arm64 - docker rmi $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA-amd64 $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA-arm64
+18 -17
View File
@@ -1,19 +1,22 @@
FROM dock.mau.dev/tulir/lottieconverter:alpine-3.11 FROM dock.mau.dev/tulir/lottieconverter:alpine-3.12
RUN echo "@edge_main http://dl-cdn.alpinelinux.org/alpine/edge/main" >> /etc/apk/repositories RUN echo $'\
RUN echo "@edge_testing http://dl-cdn.alpinelinux.org/alpine/edge/testing" >> /etc/apk/repositories @edge http://dl-cdn.alpinelinux.org/alpine/edge/main\n\
RUN echo "@edge_community http://dl-cdn.alpinelinux.org/alpine/edge/community" >> /etc/apk/repositories @edge http://dl-cdn.alpinelinux.org/alpine/edge/testing\n\
@edge http://dl-cdn.alpinelinux.org/alpine/edge/community' >> /etc/apk/repositories
RUN apk add --no-cache \ RUN apk add --no-cache \
python3 py3-pip py3-setuptools py3-wheel \
py3-virtualenv \ py3-virtualenv \
py3-pillow \ py3-pillow \
py3-aiohttp \ py3-aiohttp \
py3-magic \ py3-magic \
py3-sqlalchemy \ py3-sqlalchemy \
py3-alembic@edge_testing \ py3-telethon-session-sqlalchemy@edge \
py3-alembic@edge \
py3-psycopg2 \ py3-psycopg2 \
py3-ruamel.yaml \ py3-ruamel.yaml \
py3-commonmark@edge_testing \ py3-commonmark@edge \
# Indirect dependencies # Indirect dependencies
py3-idna \ py3-idna \
#moviepy #moviepy
@@ -22,8 +25,7 @@ RUN apk add --no-cache \
py3-requests \ py3-requests \
#imageio #imageio
py3-numpy \ py3-numpy \
#telethon py3-telethon@edge \
py3-rsa \
# Optional for socks proxies # Optional for socks proxies
py3-pysocks \ py3-pysocks \
# cryptg # cryptg
@@ -35,20 +37,19 @@ RUN apk add --no-cache \
su-exec \ su-exec \
netcat-openbsd \ netcat-openbsd \
# olm # olm
olm-dev@edge_community \ olm-dev \
# matrix-nio? # matrix-nio?
py3-future \ py3-future \
py3-atomicwrites \ py3-atomicwrites \
py3-pycryptodome@edge_main \ py3-pycryptodome \
py3-peewee@edge_community \ py3-peewee \
py3-pyrsistent@edge_community \ py3-pyrsistent \
py3-jsonschema \ py3-jsonschema \
py3-aiofiles \ #py3-aiofiles \ # (too new)
py3-cachetools@edge_community \ py3-cachetools \
py3-prometheus-client@edge_community \
py3-unpaddedbase64 \ py3-unpaddedbase64 \
py3-pyaes@edge_testing \ py3-h2@edge \
py3-logbook@edge_testing py3-logbook@edge
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
+3
View File
@@ -7,6 +7,9 @@
A Matrix-Telegram hybrid puppeting/relaybot bridge. A Matrix-Telegram hybrid puppeting/relaybot bridge.
## Sponsors
* [Joel Lehtonen / Zouppen](https://github.com/zouppen)
### [Wiki](https://github.com/tulir/mautrix-telegram/wiki) ### [Wiki](https://github.com/tulir/mautrix-telegram/wiki)
### [Features & Roadmap](https://github.com/tulir/mautrix-telegram/blob/master/ROADMAP.md) ### [Features & Roadmap](https://github.com/tulir/mautrix-telegram/blob/master/ROADMAP.md)
+3
View File
@@ -29,6 +29,9 @@
* [x] Message deletions * [x] Message deletions
* [x] Message edits * [x] Message edits
* [ ] Message history * [ ] Message history
* [x] Manually (`!tg backfill`)
* [ ] Automatically when creating portal
* [ ] Automatically for missed messages
* [x] Avatars * [x] Avatars
* [x] Presence * [x] Presence
* [x] Typing notifications * [x] Typing notifications
+1 -1
View File
@@ -1,2 +1,2 @@
__version__ = "0.8.0rc2" __version__ = "0.8.2"
__author__ = "Tulir Asokan <tulir@maunium.net>" __author__ = "Tulir Asokan <tulir@maunium.net>"
+8 -9
View File
@@ -35,6 +35,7 @@ from telethon.tl.types import (
from mautrix.types import UserID, PresenceState from mautrix.types import UserID, PresenceState
from mautrix.errors import MatrixError from mautrix.errors import MatrixError
from mautrix.appservice import AppService from mautrix.appservice import AppService
from mautrix.util.logging import TraceLogger
from alchemysession import AlchemySessionContainer from alchemysession import AlchemySessionContainer
from . import portal as po, puppet as pu, __version__ from . import portal as po, puppet as pu, __version__
@@ -68,7 +69,7 @@ except ImportError:
class AbstractUser(ABC): class AbstractUser(ABC):
session_container: AlchemySessionContainer = None session_container: AlchemySessionContainer = None
loop: asyncio.AbstractEventLoop = None loop: asyncio.AbstractEventLoop = None
log: logging.Logger log: TraceLogger
az: AppService az: AppService
relaybot: Optional['Bot'] relaybot: Optional['Bot']
ignore_incoming_bot_events: bool = True ignore_incoming_bot_events: bool = True
@@ -258,7 +259,7 @@ class AbstractUser(ABC):
elif isinstance(update, UpdateReadHistoryOutbox): elif isinstance(update, UpdateReadHistoryOutbox):
await self.update_read_receipt(update) await self.update_read_receipt(update)
else: else:
self.log.debug("Unhandled update: %s", update) self.log.trace("Unhandled update: %s", update)
async def update_pinned_messages(self, update: Union[UpdateChannelPinnedMessage, async def update_pinned_messages(self, update: Union[UpdateChannelPinnedMessage,
UpdateChatPinnedMessage]) -> None: UpdateChatPinnedMessage]) -> None:
@@ -333,7 +334,7 @@ class AbstractUser(ABC):
if await puppet.update_avatar(self, update.photo): if await puppet.update_avatar(self, update.photo):
puppet.save() puppet.save()
else: else:
self.log.warning("Unexpected other user info update: %s", update) self.log.warning(f"Unexpected other user info update: {type(update)}")
async def update_status(self, update: UpdateUserStatus) -> None: async def update_status(self, update: UpdateUserStatus) -> None:
puppet = pu.Puppet.get(TelegramID(update.user_id)) puppet = pu.Puppet.get(TelegramID(update.user_id))
@@ -342,7 +343,7 @@ class AbstractUser(ABC):
elif isinstance(update.status, UserStatusOffline): elif isinstance(update.status, UserStatusOffline):
await puppet.default_mxid_intent.set_presence(PresenceState.OFFLINE) await puppet.default_mxid_intent.set_presence(PresenceState.OFFLINE)
else: else:
self.log.warning("Unexpected user status update: %s", update) self.log.warning(f"Unexpected user status update: type({update})")
return return
def get_message_details(self, update: UpdateMessage) -> Tuple[UpdateMessageContent, def get_message_details(self, update: UpdateMessage) -> Tuple[UpdateMessageContent,
@@ -366,8 +367,7 @@ class AbstractUser(ABC):
portal = po.Portal.get_by_entity(update.to_id, receiver_id=self.tgid) portal = po.Portal.get_by_entity(update.to_id, receiver_id=self.tgid)
sender = pu.Puppet.get(update.from_id) if update.from_id else None sender = pu.Puppet.get(update.from_id) if update.from_id else None
else: else:
self.log.warning( self.log.warning(f"Unexpected message type in User#get_message_details: {type(update)}")
f"Unexpected message type in User#get_message_details: {type(update)}")
return update, None, None return update, None, None
return update, sender, portal return update, sender, portal
@@ -428,11 +428,10 @@ class AbstractUser(ABC):
if isinstance(update, MessageService): if isinstance(update, MessageService):
if isinstance(update.action, MessageActionChannelMigrateFrom): if isinstance(update.action, MessageActionChannelMigrateFrom):
self.log.debug(f"Ignoring action %s to %s by %d", update.action, self.log.trace(f"Ignoring action %s to %s by %d", update.action, portal.tgid_log,
portal.tgid_log,
sender.id) sender.id)
return return
self.log.debug("Handling action %s to %s by %d", update.action, portal.tgid_log, self.log.trace("Handling action %s to %s by %d", update.action, portal.tgid_log,
sender.id) sender.id)
return await portal.handle_telegram_action(self, sender, update) return await portal.handle_telegram_action(self, sender, update)
+1 -1
View File
@@ -147,7 +147,7 @@ class Bot(AbstractUser):
if self.whitelist_group_admins: if self.whitelist_group_admins:
if isinstance(chat, PeerChannel): if isinstance(chat, PeerChannel):
p = await self.client(GetParticipantRequest(chat, tgid)) p = await self.client(GetParticipantRequest(chat, tgid))
return isinstance(p, (ChannelParticipantCreator, ChannelParticipantAdmin)) return isinstance(p.participant, (ChannelParticipantCreator, ChannelParticipantAdmin))
elif isinstance(chat, PeerChat): elif isinstance(chat, PeerChat):
chat = await self.client(GetFullChatRequest(chat.chat_id)) chat = await self.client(GetFullChatRequest(chat.chat_id))
participants = chat.full_chat.participants.participants participants = chat.full_chat.participants.participants
+3 -1
View File
@@ -165,7 +165,9 @@ async def join(evt: CommandEvent) -> Optional[EventID]:
try: try:
await portal.create_matrix_room(evt.sender, chat, [evt.sender.mxid]) await portal.create_matrix_room(evt.sender, chat, [evt.sender.mxid])
except ChatIdInvalidError as e: except ChatIdInvalidError as e:
logging.getLogger("mau.commands").info(updates.stringify()) logging.getLogger("mau.commands").trace("ChatIdInvalidError while creating portal "
"from !tg join command: %s",
updates.stringify())
raise e raise e
return await evt.reply(f"Created room for {portal.title}") return await evt.reply(f"Created room for {portal.title}")
return None return None
+10 -30
View File
@@ -45,26 +45,18 @@ class Config(BaseBridgeConfig):
] ]
def do_update(self, helper: ConfigUpdateHelper) -> None: def do_update(self, helper: ConfigUpdateHelper) -> None:
super().do_update(helper)
copy, copy_dict, base = helper copy, copy_dict, base = helper
copy("homeserver.address")
copy("homeserver.domain")
copy("homeserver.verify_ssl")
if "appservice.protocol" in self and "appservice.address" not in self: if "appservice.protocol" in self and "appservice.address" not in self:
protocol, hostname, port = (self["appservice.protocol"], self["appservice.hostname"], protocol, hostname, port = (self["appservice.protocol"], self["appservice.hostname"],
self["appservice.port"]) self["appservice.port"])
base["appservice.address"] = f"{protocol}://{hostname}:{port}" base["appservice.address"] = f"{protocol}://{hostname}:{port}"
else: if "appservice.debug" in self and "logging" not in self:
copy("appservice.address") level = "DEBUG" if self["appservice.debug"] else "INFO"
base["logging.root.level"] = level
copy("appservice.tls_cert") base["logging.loggers.mau.level"] = level
copy("appservice.tls_key") base["logging.loggers.telethon.level"] = level
copy("appservice.hostname")
copy("appservice.port")
copy("appservice.max_body_size")
copy("appservice.database")
copy("appservice.public.enabled") copy("appservice.public.enabled")
copy("appservice.public.prefix") copy("appservice.public.prefix")
@@ -76,16 +68,8 @@ class Config(BaseBridgeConfig):
if base["appservice.provisioning.shared_secret"] == "generate": if base["appservice.provisioning.shared_secret"] == "generate":
base["appservice.provisioning.shared_secret"] = self._new_token() base["appservice.provisioning.shared_secret"] = self._new_token()
copy("appservice.id")
copy("appservice.bot_username")
copy("appservice.bot_displayname")
copy("appservice.bot_avatar")
copy("appservice.community_id") copy("appservice.community_id")
copy("appservice.as_token")
copy("appservice.hs_token")
copy("metrics.enabled") copy("metrics.enabled")
copy("metrics.listen_port") copy("metrics.listen_port")
@@ -99,6 +83,7 @@ class Config(BaseBridgeConfig):
copy("bridge.displayname_preference") copy("bridge.displayname_preference")
copy("bridge.displayname_max_length") copy("bridge.displayname_max_length")
copy("bridge.allow_avatar_remove")
copy("bridge.max_initial_member_sync") copy("bridge.max_initial_member_sync")
copy("bridge.sync_channel_members") copy("bridge.sync_channel_members")
@@ -123,6 +108,9 @@ class Config(BaseBridgeConfig):
copy("bridge.animated_sticker.args") copy("bridge.animated_sticker.args")
copy("bridge.encryption.allow") copy("bridge.encryption.allow")
copy("bridge.encryption.default") copy("bridge.encryption.default")
copy("bridge.private_chat_portal_meta")
copy("bridge.delivery_receipts")
copy("bridge.delivery_error_reports")
copy("bridge.initial_power_level_overrides.group") copy("bridge.initial_power_level_overrides.group")
copy("bridge.initial_power_level_overrides.user") copy("bridge.initial_power_level_overrides.user")
@@ -207,14 +195,6 @@ class Config(BaseBridgeConfig):
copy("telegram.proxy.username") copy("telegram.proxy.username")
copy("telegram.proxy.password") copy("telegram.proxy.password")
if "appservice.debug" in self and "logging" not in self:
level = "DEBUG" if self["appservice.debug"] else "INFO"
base["logging.root.level"] = level
base["logging.loggers.mau.level"] = level
base["logging.loggers.telethon.level"] = level
else:
copy("logging")
def _get_permissions(self, key: str) -> Permissions: def _get_permissions(self, key: str) -> Permissions:
level = self["bridge.permissions"].get(key, "") level = self["bridge.permissions"].get(key, "")
admin = level == "admin" admin = level == "admin"
+12
View File
@@ -121,6 +121,10 @@ bridge:
- phone number - phone number
# Maximum length of displayname # Maximum length of displayname
displayname_max_length: 100 displayname_max_length: 100
# Remove avatars from Telegram ghost users when removed on Telegram. This is disabled by default
# as there's no way to determine whether an avatar is removed or just hidden from some users. If
# you're on a single-user instance, this should be safe to enable.
allow_avatar_remove: false
# Maximum number of members to sync per portal when starting up. Other members will be # Maximum number of members to sync per portal when starting up. Other members will be
# synced when they send messages. The maximum is 10000, after which the Telegram server # synced when they send messages. The maximum is 10000, after which the Telegram server
@@ -207,6 +211,14 @@ 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 or not to explicitly set the avatar and room name for private
# chat portal rooms. This will be implicitly enabled if encryption.default is true.
private_chat_portal_meta: false
# Whether or not the bridge should send a read receipt from the bridge bot when a message has
# been sent to Telegram.
delivery_receipts: false
# Whether or not delivery errors should be reported as messages in the Matrix room.
delivery_error_reports: false
# Overrides for base power levels. # Overrides for base power levels.
initial_power_level_overrides: initial_power_level_overrides:
+23 -12
View File
@@ -278,7 +278,7 @@ class MatrixHandler(BaseMatrixHandler):
if not portal: if not portal:
return return
await portal.handle_matrix_deletion(sender, evt.redacts) await portal.handle_matrix_deletion(sender, evt.redacts, evt.event_id)
@staticmethod @staticmethod
async def handle_power_levels(evt: StateEvent) -> None: async def handle_power_levels(evt: StateEvent) -> None:
@@ -286,11 +286,12 @@ class MatrixHandler(BaseMatrixHandler):
sender = await u.User.get_by_mxid(evt.sender).ensure_started() sender = await u.User.get_by_mxid(evt.sender).ensure_started()
if await sender.has_full_access(allow_bot=True) and portal: if await sender.has_full_access(allow_bot=True) and portal:
await portal.handle_matrix_power_levels(sender, evt.content.users, await portal.handle_matrix_power_levels(sender, evt.content.users,
evt.unsigned.prev_content.users) evt.unsigned.prev_content.users,
evt.event_id)
@staticmethod @staticmethod
async def handle_room_meta(evt_type: EventType, room_id: RoomID, sender_mxid: UserID, async def handle_room_meta(evt_type: EventType, room_id: RoomID, sender_mxid: UserID,
content: RoomMetaStateEventContent) -> None: content: RoomMetaStateEventContent, event_id: EventID) -> None:
portal = po.Portal.get_by_mxid(room_id) portal = po.Portal.get_by_mxid(room_id)
sender = await u.User.get_by_mxid(sender_mxid).ensure_started() sender = await u.User.get_by_mxid(sender_mxid).ensure_started()
if await sender.has_full_access(allow_bot=True) and portal: if await sender.has_full_access(allow_bot=True) and portal:
@@ -301,27 +302,29 @@ class MatrixHandler(BaseMatrixHandler):
}[evt_type] }[evt_type]
if not isinstance(content, content_type): if not isinstance(content, content_type):
return return
await handler(sender, content[content_key]) await handler(sender, content[content_key], event_id)
@staticmethod @staticmethod
async def handle_room_pin(room_id: RoomID, sender_mxid: UserID, async def handle_room_pin(room_id: RoomID, sender_mxid: UserID,
new_events: Set[str], old_events: Set[str]) -> None: new_events: Set[str], old_events: Set[str],
event_id: EventID) -> None:
portal = po.Portal.get_by_mxid(room_id) portal = po.Portal.get_by_mxid(room_id)
sender = await u.User.get_by_mxid(sender_mxid).ensure_started() sender = await u.User.get_by_mxid(sender_mxid).ensure_started()
if await sender.has_full_access(allow_bot=True) and portal: if await sender.has_full_access(allow_bot=True) and portal:
events = new_events - old_events events = new_events - old_events
if len(events) > 0: if len(events) > 0:
# New event pinned, set that as pinned in Telegram. # New event pinned, set that as pinned in Telegram.
await portal.handle_matrix_pin(sender, EventID(events.pop())) await portal.handle_matrix_pin(sender, EventID(events.pop()), event_id)
elif len(new_events) == 0: elif len(new_events) == 0:
# All pinned events removed, remove pinned event in Telegram. # All pinned events removed, remove pinned event in Telegram.
await portal.handle_matrix_pin(sender, None) await portal.handle_matrix_pin(sender, None, event_id)
@staticmethod @staticmethod
async def handle_room_upgrade(room_id: RoomID, sender: UserID, new_room_id: RoomID) -> None: async def handle_room_upgrade(room_id: RoomID, sender: UserID, new_room_id: RoomID,
event_id: EventID) -> None:
portal = po.Portal.get_by_mxid(room_id) portal = po.Portal.get_by_mxid(room_id)
if portal: if portal:
await portal.handle_matrix_upgrade(sender, new_room_id) await portal.handle_matrix_upgrade(sender, new_room_id, event_id)
async def handle_member_info_change(self, room_id: RoomID, user_id: UserID, async def handle_member_info_change(self, room_id: RoomID, user_id: UserID,
profile: MemberStateEventContent, profile: MemberStateEventContent,
@@ -389,6 +392,11 @@ class MatrixHandler(BaseMatrixHandler):
def filter_matrix_event(self, evt: Event) -> bool: def filter_matrix_event(self, evt: Event) -> bool:
if not isinstance(evt, (RedactionEvent, MessageEvent, StateEvent, EncryptedEvent)): if not isinstance(evt, (RedactionEvent, MessageEvent, StateEvent, EncryptedEvent)):
return True return True
if evt.content.get("net.maunium.telegram.puppet", False):
puppet = pu.Puppet.get_by_custom_mxid(evt.sender)
if puppet:
self.log.debug("Ignoring puppet-sent event %s", evt.event_id)
return True
return evt.sender and (evt.sender == self.az.bot_mxid return evt.sender and (evt.sender == self.az.bot_mxid
or pu.Puppet.get_id_from_mxid(evt.sender) is not None) or pu.Puppet.get_id_from_mxid(evt.sender) is not None)
@@ -409,16 +417,19 @@ class MatrixHandler(BaseMatrixHandler):
if evt.type == EventType.ROOM_POWER_LEVELS: if evt.type == EventType.ROOM_POWER_LEVELS:
await self.handle_power_levels(evt) await self.handle_power_levels(evt)
elif evt.type in (EventType.ROOM_NAME, EventType.ROOM_AVATAR, EventType.ROOM_TOPIC): elif evt.type in (EventType.ROOM_NAME, EventType.ROOM_AVATAR, EventType.ROOM_TOPIC):
await self.handle_room_meta(evt.type, evt.room_id, evt.sender, evt.content) await self.handle_room_meta(evt.type, evt.room_id, evt.sender, evt.content,
evt.event_id)
elif evt.type == EventType.ROOM_PINNED_EVENTS: elif evt.type == EventType.ROOM_PINNED_EVENTS:
new_events = set(evt.content.pinned) new_events = set(evt.content.pinned)
try: try:
old_events = set(evt.unsigned.prev_content.pinned) old_events = set(evt.unsigned.prev_content.pinned)
except (KeyError, ValueError, TypeError, AttributeError): except (KeyError, ValueError, TypeError, AttributeError):
old_events = set() old_events = set()
await self.handle_room_pin(evt.room_id, evt.sender, new_events, old_events) await self.handle_room_pin(evt.room_id, evt.sender, new_events, old_events,
evt.event_id)
elif evt.type == EventType.ROOM_TOMBSTONE: elif evt.type == EventType.ROOM_TOMBSTONE:
await self.handle_room_upgrade(evt.room_id, evt.sender, evt.content.replacement_room) await self.handle_room_upgrade(evt.room_id, evt.sender, evt.content.replacement_room,
evt.event_id)
elif evt.type == EventType.ROOM_ENCRYPTION: elif evt.type == EventType.ROOM_ENCRYPTION:
portal = po.Portal.get_by_mxid(evt.room_id) portal = po.Portal.get_by_mxid(evt.room_id)
if portal: if portal:
+21 -8
View File
@@ -1,5 +1,5 @@
# mautrix-telegram - A Matrix-Telegram puppeting bridge # mautrix-telegram - A Matrix-Telegram puppeting bridge
# Copyright (C) 2019 Tulir Asokan # Copyright (C) 2020 Tulir Asokan
# #
# This program is free software: you can redistribute it and/or modify # This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by # it under the terms of the GNU Affero General Public License as published by
@@ -30,8 +30,10 @@ from telethon.tl.types import (Channel, ChannelFull, Chat, ChatFull, ChatInviteE
from mautrix.errors import MatrixRequestError, IntentError from mautrix.errors import MatrixRequestError, IntentError
from mautrix.appservice import AppService, IntentAPI from mautrix.appservice import AppService, IntentAPI
from mautrix.types import RoomID, RoomAlias, UserID, EventType, PowerLevelStateEventContent from mautrix.types import (RoomID, RoomAlias, UserID, EventID, EventType, MessageEventContent,
PowerLevelStateEventContent)
from mautrix.util.simple_template import SimpleTemplate from mautrix.util.simple_template import SimpleTemplate
from mautrix.util.logging import TraceLogger
from ..types import TelegramID from ..types import TelegramID
from ..context import Context from ..context import Context
@@ -55,7 +57,7 @@ config: Optional['Config'] = None
class BasePortal(ABC): class BasePortal(ABC):
base_log: logging.Logger = logging.getLogger("mau.portal") base_log: TraceLogger = logging.getLogger("mau.portal")
az: AppService = None az: AppService = None
bot: 'Bot' = None bot: 'Bot' = None
loop: asyncio.AbstractEventLoop = None loop: asyncio.AbstractEventLoop = None
@@ -69,6 +71,7 @@ class BasePortal(ABC):
sync_channel_members: bool = True sync_channel_members: bool = True
sync_matrix_state: bool = True sync_matrix_state: bool = True
public_portals: bool = False public_portals: bool = False
private_chat_portal_meta: bool = False
alias_template: SimpleTemplate[str] alias_template: SimpleTemplate[str]
hs_domain: str hs_domain: str
@@ -91,7 +94,7 @@ class BasePortal(ABC):
deleted: bool deleted: bool
backfilling: bool backfilling: bool
backfill_leave: Optional[Set[IntentAPI]] backfill_leave: Optional[Set[IntentAPI]]
log: logging.Logger log: TraceLogger
alias: Optional[RoomAlias] alias: Optional[RoomAlias]
@@ -133,7 +136,7 @@ class BasePortal(ABC):
if mxid: if mxid:
self.by_mxid[mxid] = self self.by_mxid[mxid] = self
# region Propegrties # region Properties
@property @property
def tgid_full(self) -> Tuple[TelegramID, TelegramID]: def tgid_full(self) -> Tuple[TelegramID, TelegramID]:
@@ -242,8 +245,7 @@ class BasePortal(ABC):
return await user.client.get_entity(self.peer) return await user.client.get_entity(self.peer)
except ValueError: except ValueError:
if user.is_bot: if user.is_bot:
self.log.warning(f"Could not find entity with bot {user.tgid}. " self.log.warning(f"Could not find entity with bot {user.tgid}. Failing...")
"Failing...")
raise raise
self.log.warning(f"Could not find entity with user {user.tgid}. " self.log.warning(f"Could not find entity with user {user.tgid}. "
"falling back to get_dialogs.") "falling back to get_dialogs.")
@@ -459,6 +461,15 @@ class BasePortal(ABC):
type_name if create else None) type_name if create else None)
# endregion # endregion
async def _send_message(self, intent: IntentAPI, content: MessageEventContent,
event_type: EventType = EventType.ROOM_MESSAGE, **kwargs) -> EventID:
if self.encrypted and self.matrix.e2ee:
if intent.api.is_real_user:
content[intent.api.real_user_content_key] = True
event_type, content = await self.matrix.e2ee.encrypt(self.mxid, event_type, content)
return await intent.send_message_event(self.mxid, event_type, content, **kwargs)
# region Abstract methods (cross-called in matrix/metadata/telegram classes) # region Abstract methods (cross-called in matrix/metadata/telegram classes)
@abstractmethod @abstractmethod
@@ -500,7 +511,8 @@ class BasePortal(ABC):
@abstractmethod @abstractmethod
def handle_matrix_power_levels(self, sender: 'u.User', new_levels: Dict[UserID, int], def handle_matrix_power_levels(self, sender: 'u.User', new_levels: Dict[UserID, int],
old_levels: Dict[UserID, int]) -> Awaitable[None]: old_levels: Dict[UserID, int], event_id: Optional[EventID]
) -> Awaitable[None]:
pass pass
@abstractmethod @abstractmethod
@@ -518,6 +530,7 @@ def init(context: Context) -> None:
BasePortal.sync_channel_members = config["bridge.sync_channel_members"] BasePortal.sync_channel_members = config["bridge.sync_channel_members"]
BasePortal.sync_matrix_state = config["bridge.sync_matrix_state"] BasePortal.sync_matrix_state = config["bridge.sync_matrix_state"]
BasePortal.public_portals = config["bridge.public_portals"] BasePortal.public_portals = config["bridge.public_portals"]
BasePortal.private_chat_portal_meta = config["bridge.private_chat_portal_meta"]
BasePortal.filter_mode = config["bridge.filter.mode"] BasePortal.filter_mode = config["bridge.filter.mode"]
BasePortal.filter_list = config["bridge.filter.list"] BasePortal.filter_list = config["bridge.filter.list"]
BasePortal.hs_domain = config["homeserver.domain"] BasePortal.hs_domain = config["homeserver.domain"]
+48 -17
View File
@@ -1,5 +1,5 @@
# mautrix-telegram - A Matrix-Telegram puppeting bridge # mautrix-telegram - A Matrix-Telegram puppeting bridge
# Copyright (C) 2019 Tulir Asokan # Copyright (C) 2020 Tulir Asokan
# #
# This program is free software: you can redistribute it and/or modify # This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by # it under the terms of the GNU Affero General Public License as published by
@@ -25,7 +25,8 @@ from telethon.tl.functions.messages import (EditChatPhotoRequest, EditChatTitleR
EditChatAboutRequest) EditChatAboutRequest)
from telethon.tl.functions.channels import EditPhotoRequest, EditTitleRequest, JoinChannelRequest from telethon.tl.functions.channels import EditPhotoRequest, EditTitleRequest, JoinChannelRequest
from telethon.errors import (ChatNotModifiedError, PhotoExtInvalidError, from telethon.errors import (ChatNotModifiedError, PhotoExtInvalidError,
PhotoInvalidDimensionsError, PhotoSaveFileInvalidError) PhotoInvalidDimensionsError, PhotoSaveFileInvalidError,
RPCError)
from telethon.tl.patched import Message, MessageService from telethon.tl.patched import Message, MessageService
from telethon.tl.types import ( from telethon.tl.types import (
DocumentAttributeFilename, DocumentAttributeImageSize, GeoPoint, DocumentAttributeFilename, DocumentAttributeImageSize, GeoPoint,
@@ -228,6 +229,13 @@ class PortalMatrix(BasePortal, MautrixBasePortal, ABC):
message, entities = None, None message, entities = None, None
return message, entities return message, entities
async def _send_delivery_receipt(self, event_id: EventID) -> None:
if event_id and config["bridge.delivery_receipts"]:
try:
await self.az.intent.mark_read(self.mxid, event_id)
except Exception:
self.log.exception("Failed to send delivery receipt for %s", event_id)
async def _handle_matrix_text(self, sender_id: TelegramID, event_id: EventID, async def _handle_matrix_text(self, sender_id: TelegramID, event_id: EventID,
space: TelegramID, client: 'MautrixTelegramClient', space: TelegramID, client: 'MautrixTelegramClient',
content: TextMessageEventContent, reply_to: TelegramID) -> None: content: TextMessageEventContent, reply_to: TelegramID) -> None:
@@ -245,6 +253,7 @@ class PortalMatrix(BasePortal, MautrixBasePortal, ABC):
parse_mode=self._matrix_event_to_entities, parse_mode=self._matrix_event_to_entities,
link_preview=lp) link_preview=lp)
self._add_telegram_message_to_db(event_id, space, 0, response) self._add_telegram_message_to_db(event_id, space, 0, response)
await self._send_delivery_receipt(event_id)
async def _handle_matrix_file(self, sender_id: TelegramID, event_id: EventID, async def _handle_matrix_file(self, sender_id: TelegramID, event_id: EventID,
space: TelegramID, client: 'MautrixTelegramClient', space: TelegramID, client: 'MautrixTelegramClient',
@@ -307,6 +316,7 @@ class PortalMatrix(BasePortal, MautrixBasePortal, ABC):
response = await client.send_media(self.peer, media, reply_to=reply_to, response = await client.send_media(self.peer, media, reply_to=reply_to,
caption=caption, entities=entities) caption=caption, entities=entities)
self._add_telegram_message_to_db(event_id, space, 0, response) self._add_telegram_message_to_db(event_id, space, 0, response)
await self._send_delivery_receipt(event_id)
async def _matrix_document_edit(self, client: 'MautrixTelegramClient', async def _matrix_document_edit(self, client: 'MautrixTelegramClient',
content: MessageEventContent, space: TelegramID, content: MessageEventContent, space: TelegramID,
@@ -317,6 +327,7 @@ class PortalMatrix(BasePortal, MautrixBasePortal, ABC):
response = await client.edit_message(self.peer, orig_msg.tgid, response = await client.edit_message(self.peer, orig_msg.tgid,
caption, file=media) caption, file=media)
self._add_telegram_message_to_db(event_id, space, -1, response) self._add_telegram_message_to_db(event_id, space, -1, response)
await self._send_delivery_receipt(event_id)
return True return True
return False return False
@@ -339,10 +350,11 @@ class PortalMatrix(BasePortal, MautrixBasePortal, ABC):
response = await client.send_media(self.peer, media, reply_to=reply_to, response = await client.send_media(self.peer, media, reply_to=reply_to,
caption=caption, entities=entities) caption=caption, entities=entities)
self._add_telegram_message_to_db(event_id, space, 0, response) self._add_telegram_message_to_db(event_id, space, 0, response)
await self._send_delivery_receipt(event_id)
def _add_telegram_message_to_db(self, event_id: EventID, space: TelegramID, def _add_telegram_message_to_db(self, event_id: EventID, space: TelegramID,
edit_index: int, response: TypeMessage) -> None: edit_index: int, response: TypeMessage) -> None:
self.log.debug("Handled Matrix message: %s", response) self.log.trace("Handled Matrix message: %s", response)
self.dedup.check(response, (event_id, space), force_hash=edit_index != 0) self.dedup.check(response, (event_id, space), force_hash=edit_index != 0)
if edit_index < 0: if edit_index < 0:
prev_edit = DBMessage.get_one_by_tgid(TelegramID(response.id), space, -1) prev_edit = DBMessage.get_one_by_tgid(TelegramID(response.id), space, -1)
@@ -354,17 +366,26 @@ class PortalMatrix(BasePortal, MautrixBasePortal, ABC):
mxid=event_id, mxid=event_id,
edit_index=edit_index).insert() edit_index=edit_index).insert()
async def _send_bridge_error(self, msg: str) -> None:
if config["bridge.delivery_error_reports"]:
await self._send_message(self.main_intent,
TextMessageEventContent(msgtype=MessageType.NOTICE, body=msg))
async def handle_matrix_message(self, sender: 'u.User', content: MessageEventContent, async def handle_matrix_message(self, sender: 'u.User', content: MessageEventContent,
event_id: EventID) -> None: event_id: EventID) -> None:
try:
await self._handle_matrix_message(sender, content, event_id)
except RPCError as e:
if config["bridge.delivery_error_reports"]:
await self._send_bridge_error(f"\u26a0 Your message may not have been bridged: {e}")
raise
async def _handle_matrix_message(self, sender: 'u.User', content: MessageEventContent,
event_id: EventID) -> None:
if not content.body or not content.msgtype: if not content.body or not content.msgtype:
self.log.debug(f"Ignoring message {event_id} in {self.mxid} without body or msgtype") self.log.debug(f"Ignoring message {event_id} in {self.mxid} without body or msgtype")
return return
puppet = p.Puppet.get_by_custom_mxid(sender.mxid)
if puppet and content.get("net.maunium.telegram.puppet", False):
self.log.debug("Ignoring puppet-sent message by confirmed puppet user %s", sender.mxid)
return
logged_in = not await sender.needs_relaybot(self) logged_in = not await sender.needs_relaybot(self)
client = sender.client if logged_in else self.bot.client client = sender.client if logged_in else self.bot.client
sender_id = sender.tgid if logged_in else self.bot.tgid sender_id = sender.tgid if logged_in else self.bot.tgid
@@ -403,10 +424,10 @@ class PortalMatrix(BasePortal, MautrixBasePortal, ABC):
await self._handle_matrix_file(sender_id, event_id, space, client, content, reply_to, await self._handle_matrix_file(sender_id, event_id, space, client, content, reply_to,
caption_content) caption_content)
else: else:
self.log.debug(f"Unhandled Matrix event: {content}") self.log.trace("Unhandled Matrix event: %s", content)
async def handle_matrix_pin(self, sender: 'u.User', async def handle_matrix_pin(self, sender: 'u.User', pinned_message: Optional[EventID],
pinned_message: Optional[EventID]) -> None: pin_event_id: EventID) -> None:
if self.peer_type != "chat" and self.peer_type != "channel": if self.peer_type != "chat" and self.peer_type != "channel":
return return
try: try:
@@ -419,10 +440,12 @@ class PortalMatrix(BasePortal, MautrixBasePortal, ABC):
self.log.warning(f"Could not find pinned {pinned_message} in {self.mxid}") self.log.warning(f"Could not find pinned {pinned_message} in {self.mxid}")
return return
await sender.client(UpdatePinnedMessageRequest(peer=self.peer, id=message.tgid)) await sender.client(UpdatePinnedMessageRequest(peer=self.peer, id=message.tgid))
await self._send_delivery_receipt(pin_event_id)
except ChatNotModifiedError: except ChatNotModifiedError:
pass pass
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,
redaction_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
space = self.tgid if self.peer_type == "channel" else real_deleter.tgid space = self.tgid if self.peer_type == "channel" else real_deleter.tgid
message = DBMessage.get_by_mxid(event_id, self.mxid, space) message = DBMessage.get_by_mxid(event_id, self.mxid, space)
@@ -430,6 +453,7 @@ class PortalMatrix(BasePortal, MautrixBasePortal, ABC):
return return
if message.edit_index == 0: if message.edit_index == 0:
await real_deleter.client.delete_messages(self.peer, [message.tgid]) await real_deleter.client.delete_messages(self.peer, [message.tgid])
await self._send_delivery_receipt(redaction_event_id)
else: else:
self.log.debug(f"Ignoring deletion of edit event {message.mxid} in {message.mx_room}") self.log.debug(f"Ignoring deletion of edit event {message.mxid} in {message.mx_room}")
@@ -444,7 +468,8 @@ class PortalMatrix(BasePortal, MautrixBasePortal, ABC):
pin_messages=moderator, add_admins=admin) pin_messages=moderator, add_admins=admin)
async def handle_matrix_power_levels(self, sender: 'u.User', new_users: Dict[UserID, int], async def handle_matrix_power_levels(self, sender: 'u.User', new_users: Dict[UserID, int],
old_users: Dict[UserID, int]) -> None: old_users: Dict[UserID, int], event_id: Optional[EventID]
) -> None:
# TODO handle all power level changes and bridge exact admin rights to supergroups/channels # TODO handle all power level changes and bridge exact admin rights to supergroups/channels
for user, level in new_users.items(): for user, level in new_users.items():
if not user or user == self.main_intent.mxid or user == sender.mxid: if not user or user == self.main_intent.mxid or user == sender.mxid:
@@ -460,15 +485,16 @@ class PortalMatrix(BasePortal, MautrixBasePortal, ABC):
if user not in old_users or level != old_users[user]: if user not in old_users or level != old_users[user]:
await self._update_telegram_power_level(sender, user_id, level) await self._update_telegram_power_level(sender, user_id, level)
async def handle_matrix_about(self, sender: 'u.User', about: str) -> None: async def handle_matrix_about(self, sender: 'u.User', about: str, event_id: EventID) -> None:
if self.peer_type not in ("chat", "channel"): if self.peer_type not in ("chat", "channel"):
return return
peer = await self.get_input_entity(sender) peer = await self.get_input_entity(sender)
await sender.client(EditChatAboutRequest(peer=peer, about=about)) await sender.client(EditChatAboutRequest(peer=peer, about=about))
self.about = about self.about = about
self.save() self.save()
await self._send_delivery_receipt(event_id)
async def handle_matrix_title(self, sender: 'u.User', title: str) -> None: async def handle_matrix_title(self, sender: 'u.User', title: str, event_id: EventID) -> None:
if self.peer_type not in ("chat", "channel"): if self.peer_type not in ("chat", "channel"):
return return
@@ -480,8 +506,10 @@ class PortalMatrix(BasePortal, MautrixBasePortal, ABC):
self.dedup.register_outgoing_actions(response) self.dedup.register_outgoing_actions(response)
self.title = title self.title = title
self.save() self.save()
await self._send_delivery_receipt(event_id)
async def handle_matrix_avatar(self, sender: 'u.User', url: ContentURI) -> None: async def handle_matrix_avatar(self, sender: 'u.User', url: ContentURI, event_id: EventID
) -> None:
if self.peer_type not in ("chat", "channel"): if self.peer_type not in ("chat", "channel"):
# Invalid peer type # Invalid peer type
return return
@@ -507,8 +535,10 @@ class PortalMatrix(BasePortal, MautrixBasePortal, ABC):
self.photo_id = f"{size.location.volume_id}-{size.location.local_id}" self.photo_id = f"{size.location.volume_id}-{size.location.local_id}"
self.save() self.save()
break break
await self._send_delivery_receipt(event_id)
async def handle_matrix_upgrade(self, sender: UserID, new_room: RoomID) -> None: async def handle_matrix_upgrade(self, sender: UserID, new_room: RoomID, event_id: EventID
) -> None:
_, server = self.main_intent.parse_user_id(sender) _, server = self.main_intent.parse_user_id(sender)
old_room = self.mxid old_room = self.mxid
self.migrate_and_save_matrix(new_room) self.migrate_and_save_matrix(new_room)
@@ -535,6 +565,7 @@ class PortalMatrix(BasePortal, MautrixBasePortal, ABC):
return return
await self.update_matrix_room(user, entity, direct=self.peer_type == "user") await self.update_matrix_room(user, entity, direct=self.peer_type == "user")
self.log.info(f"{sender} upgraded room from {old_room} to {self.mxid}") self.log.info(f"{sender} upgraded room from {old_room} to {self.mxid}")
await self._send_delivery_receipt(event_id)
def migrate_and_save_matrix(self, new_id: RoomID) -> None: def migrate_and_save_matrix(self, new_id: RoomID) -> None:
try: try:
+46 -31
View File
@@ -1,5 +1,5 @@
# mautrix-telegram - A Matrix-Telegram puppeting bridge # mautrix-telegram - A Matrix-Telegram puppeting bridge
# Copyright (C) 2019 Tulir Asokan # Copyright (C) 2020 Tulir Asokan
# #
# This program is free software: you can redistribute it and/or modify # This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by # it under the terms of the GNU Affero General Public License as published by
@@ -13,7 +13,7 @@
# #
# You should have received a copy of the GNU Affero General Public License # You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>. # along with this program. If not, see <https://www.gnu.org/licenses/>.
from typing import List, Optional, Tuple, Union, Callable, TYPE_CHECKING from typing import List, Optional, Tuple, Union, Callable, Awaitable, TYPE_CHECKING
from abc import ABC from abc import ABC
import asyncio import asyncio
@@ -26,12 +26,13 @@ from telethon.tl.types import (
Channel, ChatBannedRights, ChannelParticipantsRecent, ChannelParticipantsSearch, ChatPhoto, Channel, ChatBannedRights, ChannelParticipantsRecent, ChannelParticipantsSearch, ChatPhoto,
PhotoEmpty, InputChannel, InputUser, ChatPhotoEmpty, PeerUser, Photo, TypeChat, TypeInputPeer, PhotoEmpty, InputChannel, InputUser, ChatPhotoEmpty, PeerUser, Photo, TypeChat, TypeInputPeer,
TypeUser, User, InputPeerPhotoFileLocation, ChatParticipantAdmin, ChannelParticipantAdmin, TypeUser, User, InputPeerPhotoFileLocation, ChatParticipantAdmin, ChannelParticipantAdmin,
ChatParticipantCreator, ChannelParticipantCreator) ChatParticipantCreator, ChannelParticipantCreator, UserProfilePhoto, UserProfilePhotoEmpty)
from mautrix.errors import MForbidden from mautrix.errors import MForbidden
from mautrix.types import (RoomID, UserID, RoomCreatePreset, EventType, Membership, Member, from mautrix.types import (RoomID, UserID, RoomCreatePreset, EventType, Membership, Member,
PowerLevelStateEventContent) PowerLevelStateEventContent, RoomTopicStateEventContent,
from mautrix.appservice import IntentAPI RoomNameStateEventContent, RoomAvatarStateEventContent,
StateEventContent)
from ..types import TelegramID from ..types import TelegramID
from ..context import Context from ..context import Context
@@ -155,7 +156,7 @@ class PortalMetadata(BasePortal, ABC):
if levels.get_user_level(self.main_intent.mxid) == 100: if levels.get_user_level(self.main_intent.mxid) == 100:
levels = self._get_base_power_levels(levels, entity) levels = self._get_base_power_levels(levels, entity)
await self.main_intent.set_power_levels(self.mxid, levels) await self.main_intent.set_power_levels(self.mxid, levels)
await self.handle_matrix_power_levels(source, levels.users, {}) await self.handle_matrix_power_levels(source, levels.users, {}, None)
async def invite_telegram(self, source: 'u.User', async def invite_telegram(self, source: 'u.User',
puppet: Union[p.Puppet, 'AbstractUser']) -> None: puppet: Union[p.Puppet, 'AbstractUser']) -> None:
@@ -218,10 +219,17 @@ class PortalMetadata(BasePortal, ABC):
puppet = p.Puppet.get(self.tgid) puppet = p.Puppet.get(self.tgid)
await puppet.update_info(user, entity) await puppet.update_info(user, entity)
await puppet.intent_for(self).join_room(self.mxid) await puppet.intent_for(self).join_room(self.mxid)
if self.encrypted or self.private_chat_portal_meta:
# The bridge bot needs to join for e2ee, but that messes up the default name
# generation. If/when canonical DMs happen, this might not be necessary anymore.
changed = await self._update_title(puppet.displayname)
changed = await self._update_avatar(user, entity.photo) or changed
if changed:
self.save()
if self.sync_matrix_state: if self.sync_matrix_state:
await self.sync_matrix_members() await self.sync_matrix_members()
async def create_matrix_room(self, user: 'AbstractUser', entity: TypeChat = None, async def create_matrix_room(self, user: 'AbstractUser', entity: Union[TypeChat, User] = None,
invites: InviteList = None, update_if_exists: bool = True, invites: InviteList = None, update_if_exists: bool = True,
synchronous: bool = False) -> Optional[str]: synchronous: bool = False) -> Optional[str]:
if self.mxid: if self.mxid:
@@ -245,8 +253,8 @@ class PortalMetadata(BasePortal, ABC):
except Exception: except Exception:
self.log.exception("Fatal error creating Matrix room") self.log.exception("Fatal error creating Matrix room")
async def _create_matrix_room(self, user: 'AbstractUser', entity: TypeChat, invites: InviteList async def _create_matrix_room(self, user: 'AbstractUser', entity: Union[TypeChat, User],
) -> Optional[RoomID]: invites: InviteList) -> Optional[RoomID]:
direct = self.peer_type == "user" direct = self.peer_type == "user"
if invites is None: if invites is None:
@@ -260,7 +268,7 @@ class PortalMetadata(BasePortal, ABC):
if not entity: if not entity:
entity = await self.get_entity(user) entity = await self.get_entity(user)
self.log.debug(f"Fetched data: {entity}") self.log.trace("Fetched data: %s", entity)
self.log.debug("Creating room") self.log.debug("Creating room")
@@ -274,6 +282,8 @@ class PortalMetadata(BasePortal, ABC):
self.about = "Your Telegram cloud storage chat" self.about = "Your Telegram cloud storage chat"
puppet = p.Puppet.get(self.tgid) if direct else None puppet = p.Puppet.get(self.tgid) if direct else None
if puppet:
await puppet.update_info(user, entity)
self._main_intent = puppet.intent_for(self) if direct else self.az.intent self._main_intent = puppet.intent_for(self) if direct else self.az.intent
if self.peer_type == "channel": if self.peer_type == "channel":
@@ -340,9 +350,8 @@ class PortalMetadata(BasePortal, ABC):
}) })
if direct: if direct:
invites.append(self.az.bot_mxid) invites.append(self.az.bot_mxid)
# The bridge bot needs to join for e2ee, but that messes up the default name if direct and (self.encrypted or self.private_chat_portal_meta):
# generation. If/when canonical DMs happen, this might not be necessary anymore. self.title = puppet.displayname
self.title = puppet.displayname
if config["appservice.community_id"]: if config["appservice.community_id"]:
initial_state.append({ initial_state.append({
"type": "m.room.related_groups", "type": "m.room.related_groups",
@@ -587,12 +596,12 @@ class PortalMetadata(BasePortal, ABC):
self.log.warning("Called update_info() for direct chat portal") self.log.warning("Called update_info() for direct chat portal")
return return
changed = False
self.log.debug("Updating info") self.log.debug("Updating info")
try: try:
if not entity: if not entity:
entity = await self.get_entity(user) entity = await self.get_entity(user)
self.log.debug(f"Fetched data: {entity}") self.log.trace("Fetched data: %s", entity)
changed = False
if self.peer_type == "channel": if self.peer_type == "channel":
changed = self.megagroup != entity.megagroup or changed changed = self.megagroup != entity.megagroup or changed
@@ -630,15 +639,18 @@ class PortalMetadata(BasePortal, ABC):
self.save() self.save()
return True return True
async def _try_use_intent(self, sender: Optional['p.Puppet'], async def _try_set_state(self, sender: Optional['p.Puppet'], evt_type: EventType,
action: Callable[[IntentAPI], None]) -> None: content: StateEventContent) -> None:
if sender: if sender:
try: try:
await action(sender.intent_for(self)) intent = sender.intent_for(self)
if sender.is_real_user:
content[self.az.real_user_content_key] = True
await intent.send_state_event(self.mxid, evt_type, content)
except MForbidden: except MForbidden:
await action(self.main_intent) await self.main_intent.send_state_event(self.mxid, evt_type, content)
else: else:
await action(self.main_intent) await self.main_intent.send_state_event(self.mxid, evt_type, content)
async def _update_about(self, about: str, sender: Optional['p.Puppet'] = None, async def _update_about(self, about: str, sender: Optional['p.Puppet'] = None,
save: bool = False) -> bool: save: bool = False) -> bool:
@@ -646,8 +658,8 @@ class PortalMetadata(BasePortal, ABC):
return False return False
self.about = about self.about = about
await self._try_use_intent(sender, await self._try_set_state(sender, EventType.ROOM_TOPIC,
lambda intent: intent.set_room_topic(self.mxid, self.about)) RoomTopicStateEventContent(topic=self.about))
if save: if save:
self.save() self.save()
return True return True
@@ -658,42 +670,45 @@ class PortalMetadata(BasePortal, ABC):
return False return False
self.title = title self.title = title
await self._try_use_intent(sender, await self._try_set_state(sender, EventType.ROOM_NAME,
lambda intent: intent.set_room_name(self.mxid, self.title)) RoomNameStateEventContent(name=self.title))
if save: if save:
self.save() self.save()
return True return True
async def _update_avatar(self, user: 'AbstractUser', photo: TypeChatPhoto, async def _update_avatar(self, user: 'AbstractUser', photo: TypeChatPhoto,
sender: Optional['p.Puppet'] = None, save: bool = False) -> bool: sender: Optional['p.Puppet'] = None, save: bool = False) -> bool:
if isinstance(photo, ChatPhoto): if isinstance(photo, (ChatPhoto, UserProfilePhoto)):
loc = InputPeerPhotoFileLocation( loc = InputPeerPhotoFileLocation(
peer=await self.get_input_entity(user), peer=await self.get_input_entity(user),
local_id=photo.photo_big.local_id, local_id=photo.photo_big.local_id,
volume_id=photo.photo_big.volume_id, volume_id=photo.photo_big.volume_id,
big=True big=True
) )
photo_id = f"{loc.volume_id}-{loc.local_id}" photo_id = (f"{loc.volume_id}-{loc.local_id}" if isinstance(photo, ChatPhoto)
else photo.photo_id)
elif isinstance(photo, Photo): elif isinstance(photo, Photo):
loc, largest = self._get_largest_photo_size(photo) loc, largest = self._get_largest_photo_size(photo)
photo_id = f"{largest.location.volume_id}-{largest.location.local_id}" photo_id = f"{largest.location.volume_id}-{largest.location.local_id}"
elif isinstance(photo, (ChatPhotoEmpty, PhotoEmpty)): elif isinstance(photo, (UserProfilePhotoEmpty, ChatPhotoEmpty, PhotoEmpty, type(None))):
photo_id = "" photo_id = ""
loc = None loc = None
else: else:
raise ValueError(f"Unknown photo type {type(photo)}") raise ValueError(f"Unknown photo type {type(photo)}")
if self.peer_type == "user" and not photo_id and not config["bridge.allow_avatar_remove"]:
return False
if self.photo_id != photo_id: if self.photo_id != photo_id:
if not photo_id: if not photo_id:
await self._try_use_intent(sender, await self._try_set_state(sender, EventType.ROOM_AVATAR,
lambda intent: intent.set_room_avatar(self.mxid, None)) RoomAvatarStateEventContent(url=None))
self.photo_id = "" self.photo_id = ""
if save: if save:
self.save() self.save()
return True return True
file = await util.transfer_file_to_matrix(user.client, self.main_intent, loc) file = await util.transfer_file_to_matrix(user.client, self.main_intent, loc)
if file: if file:
await self._try_use_intent(sender, lambda intent: intent.set_room_avatar(self.mxid, await self._try_set_state(sender, EventType.ROOM_AVATAR,
file.mxc)) RoomAvatarStateEventContent(url=file.mxc))
self.photo_id = photo_id self.photo_id = photo_id
if save: if save:
self.save() self.save()
+20 -17
View File
@@ -1,5 +1,5 @@
# mautrix-telegram - A Matrix-Telegram puppeting bridge # mautrix-telegram - A Matrix-Telegram puppeting bridge
# Copyright (C) 2019 Tulir Asokan # Copyright (C) 2020 Tulir Asokan
# #
# This program is free software: you can redistribute it and/or modify # This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by # it under the terms of the GNU Affero General Public License as published by
@@ -71,16 +71,8 @@ class PortalTelegram(BasePortal, ABC):
return f"https://t.me/c/{self.tgid}/{evt.id}" return f"https://t.me/c/{self.tgid}/{evt.id}"
return None return None
async def _send_message(self, intent: IntentAPI, content: MessageEventContent,
event_type: EventType = EventType.ROOM_MESSAGE, **kwargs) -> EventID:
if self.encrypted and self.matrix.e2ee:
if intent.api.is_real_user:
content[intent.api.real_user_content_key] = True
event_type, content = await self.matrix.e2ee.encrypt(self.mxid, event_type, content)
return await intent.send_message_event(self.mxid, event_type, content, **kwargs)
async def handle_telegram_photo(self, source: 'AbstractUser', intent: IntentAPI, evt: Message, async def handle_telegram_photo(self, source: 'AbstractUser', intent: IntentAPI, evt: Message,
relates_to: Dict = None) -> Optional[EventID]: relates_to: RelatesTo = None) -> Optional[EventID]:
loc, largest_size = self._get_largest_photo_size(evt.media.photo) loc, largest_size = self._get_largest_photo_size(evt.media.photo)
file = await util.transfer_file_to_matrix(source.client, intent, loc, file = await util.transfer_file_to_matrix(source.client, intent, loc,
encrypt=self.encrypted) encrypt=self.encrypted)
@@ -227,8 +219,8 @@ class PortalTelegram(BasePortal, ABC):
content.url = file.mxc content.url = file.mxc
return await self._send_message(intent, content, event_type=event_type, timestamp=evt.date) return await self._send_message(intent, content, event_type=event_type, timestamp=evt.date)
def handle_telegram_location(self, _: 'AbstractUser', intent: IntentAPI, evt: Message, def handle_telegram_location(self, source: 'AbstractUser', intent: IntentAPI, evt: Message,
relates_to: dict = None) -> Awaitable[EventID]: relates_to: RelatesTo = None) -> Awaitable[EventID]:
long = evt.media.geo.long long = evt.media.geo.long
lat = evt.media.geo.lat lat = evt.media.geo.lat
long_char = "E" if long > 0 else "W" long_char = "E" if long > 0 else "W"
@@ -257,7 +249,7 @@ class PortalTelegram(BasePortal, ABC):
return await self._send_message(intent, content, timestamp=evt.date) return await self._send_message(intent, content, timestamp=evt.date)
async def handle_telegram_unsupported(self, source: 'AbstractUser', intent: IntentAPI, async def handle_telegram_unsupported(self, source: 'AbstractUser', intent: IntentAPI,
evt: Message, relates_to: dict = None) -> EventID: evt: Message, relates_to: RelatesTo = None) -> EventID:
override_text = ("This message is not supported on your version of Mautrix-Telegram. " override_text = ("This message is not supported on your version of Mautrix-Telegram. "
"Please check https://github.com/tulir/mautrix-telegram or ask your " "Please check https://github.com/tulir/mautrix-telegram or ask your "
"bridge administrator about possible updates.") "bridge administrator about possible updates.")
@@ -312,7 +304,7 @@ class PortalTelegram(BasePortal, ABC):
@staticmethod @staticmethod
def _int_to_bytes(i: int) -> bytes: def _int_to_bytes(i: int) -> bytes:
hex_value = "{0:010x}".format(i) hex_value = "{0:010x}".format(i).encode("utf-8")
return codecs.decode(hex_value, "hex_codec") return codecs.decode(hex_value, "hex_codec")
def _encode_msgid(self, source: 'AbstractUser', evt: Message) -> str: def _encode_msgid(self, source: 'AbstractUser', evt: Message) -> str:
@@ -355,6 +347,7 @@ class PortalTelegram(BasePortal, ABC):
async def handle_telegram_edit(self, source: 'AbstractUser', sender: p.Puppet, evt: Message async def handle_telegram_edit(self, source: 'AbstractUser', sender: p.Puppet, evt: Message
) -> None: ) -> None:
if not self.mxid: if not self.mxid:
self.log.trace("Ignoring edit to %d as chat has no Matrix room", evt.id)
return return
elif hasattr(evt, "media") and isinstance(evt.media, MessageMediaGame): elif hasattr(evt, "media") and isinstance(evt.media, MessageMediaGame):
self.log.debug("Ignoring game message edit event") self.log.debug("Ignoring game message edit event")
@@ -402,17 +395,21 @@ class PortalTelegram(BasePortal, ABC):
DBMessage.update_by_mxid(temporary_identifier, self.mxid, mxid=event_id) DBMessage.update_by_mxid(temporary_identifier, self.mxid, mxid=event_id)
async def backfill(self, source: 'AbstractUser') -> None: async def backfill(self, source: 'AbstractUser') -> None:
self.log.debug("Backfilling history through %s", source.mxid)
last = DBMessage.find_last(self.mxid, (source.tgid if self.peer_type != "channel" last = DBMessage.find_last(self.mxid, (source.tgid if self.peer_type != "channel"
else self.tgid)) else self.tgid))
min_id = last.tgid if last else 0 min_id = last.tgid if last else 0
self.backfilling = True self.backfilling = True
self.backfill_leave = set() self.backfill_leave = set()
if self.peer_type == "user": if self.peer_type == "user":
self.log.debug("Adding %s's default puppet to room for backfilling", source.mxid)
sender = p.Puppet.get(source.tgid) sender = p.Puppet.get(source.tgid)
await self.main_intent.invite_user(self.mxid, sender.default_mxid) await self.main_intent.invite_user(self.mxid, sender.default_mxid)
await sender.default_mxid_intent.join_room_by_id(self.mxid) await sender.default_mxid_intent.join_room_by_id(self.mxid)
self.backfill_leave.add(sender.default_mxid_intent) self.backfill_leave.add(sender.default_mxid_intent)
max_file_size = min(config["bridge.max_document_size"], 1500) * 1024 * 1024 max_file_size = min(config["bridge.max_document_size"], 1500) * 1024 * 1024
self.log.trace("Opening takeout client for %d, message ID %d->", source.tgid, min_id)
count = 0
async with source.client.takeout(files=True, megagroups=self.megagroup, async with source.client.takeout(files=True, megagroups=self.megagroup,
chats=self.peer_type == "chat", chats=self.peer_type == "chat",
users=self.peer_type == "user", users=self.peer_type == "user",
@@ -426,14 +423,18 @@ class PortalTelegram(BasePortal, ABC):
# if isinstance(message, MessageService): # if isinstance(message, MessageService):
# await self.handle_telegram_action(source, sender, message) # await self.handle_telegram_action(source, sender, message)
await self.handle_telegram_message(source, sender, message) await self.handle_telegram_message(source, sender, message)
count += 1
for intent in self.backfill_leave: for intent in self.backfill_leave:
self.log.trace("Leaving room with %s post-backfill", intent.mxid)
await intent.leave_room(self.mxid) await intent.leave_room(self.mxid)
self.backfilling = False self.backfilling = False
self.backfill_leave = None self.backfill_leave = None
self.log.info("Backfilled %d messages through %s", count, source.mxid)
async def handle_telegram_message(self, source: 'AbstractUser', sender: p.Puppet, async def handle_telegram_message(self, source: 'AbstractUser', sender: p.Puppet,
evt: Message) -> None: evt: Message) -> None:
if not self.mxid: if not self.mxid:
self.log.trace("Got telegram message %d, but no room exists, creating...", evt.id)
await self.create_matrix_room(source, invites=[source.mxid], update_if_exists=False) await self.create_matrix_room(source, invites=[source.mxid], update_if_exists=False)
if (self.peer_type == "user" and sender.tgid == self.tg_receiver if (self.peer_type == "user" and sender.tgid == self.tg_receiver
@@ -467,6 +468,8 @@ class PortalTelegram(BasePortal, ABC):
"bridge.deduplication.cache_queue_length in the config.") "bridge.deduplication.cache_queue_length in the config.")
return return
self.log.trace("Handling Telegram message %s", evt)
if sender and not sender.displayname: if sender and not sender.displayname:
self.log.debug(f"Telegram user {sender.tgid} sent a message, but doesn't have a " self.log.debug(f"Telegram user {sender.tgid} sent a message, but doesn't have a "
"displayname, updating info...") "displayname, updating info...")
@@ -500,7 +503,7 @@ class PortalTelegram(BasePortal, ABC):
}[type(media)](source, intent, evt, }[type(media)](source, intent, evt,
relates_to=formatter.telegram_reply_to_matrix(evt, source)) relates_to=formatter.telegram_reply_to_matrix(evt, source))
else: else:
self.log.debug("Unhandled Telegram message: %s", evt) self.log.debug("Unhandled Telegram message %d", evt.id)
return return
if not event_id: if not event_id:
@@ -517,7 +520,7 @@ class PortalTelegram(BasePortal, ABC):
await intent.redact(self.mxid, event_id) await intent.redact(self.mxid, event_id)
return return
self.log.debug("Handled Telegram message: %s", evt) self.log.debug("Handled telegram message %d -> %s", evt.id, event_id)
try: try:
DBMessage(tgid=TelegramID(evt.id), mx_room=self.mxid, mxid=event_id, DBMessage(tgid=TelegramID(evt.id), mx_room=self.mxid, mxid=event_id,
tg_space=tg_space, edit_index=0).insert() tg_space=tg_space, edit_index=0).insert()
@@ -572,7 +575,7 @@ class PortalTelegram(BasePortal, ABC):
# TODO handle game score # TODO handle game score
pass pass
else: else:
self.log.debug("Unhandled Telegram action in %s: %s", self.title, action) self.log.trace("Unhandled Telegram action in %s: %s", self.title, action)
async def set_telegram_admin(self, user_id: TelegramID) -> None: async def set_telegram_admin(self, user_id: TelegramID) -> None:
puppet = p.Puppet.get(user_id) puppet = p.Puppet.get(user_id)
+9 -5
View File
@@ -1,5 +1,5 @@
# mautrix-telegram - A Matrix-Telegram puppeting bridge # mautrix-telegram - A Matrix-Telegram puppeting bridge
# Copyright (C) 2019 Tulir Asokan # Copyright (C) 2020 Tulir Asokan
# #
# This program is free software: you can redistribute it and/or modify # This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by # it under the terms of the GNU Affero General Public License as published by
@@ -242,8 +242,7 @@ class Puppet(CustomPuppetMixin):
try: try:
changed = await self.update_displayname(source, info) or changed changed = await self.update_displayname(source, info) or changed
if isinstance(info.photo, UserProfilePhoto): changed = await self.update_avatar(source, info.photo) or changed
changed = await self.update_avatar(source, info.photo) or changed
except Exception: except Exception:
self.log.exception(f"Failed to update info from source {source.tgid}") self.log.exception(f"Failed to update info from source {source.tgid}")
@@ -294,10 +293,15 @@ class Puppet(CustomPuppetMixin):
if self.disable_updates: if self.disable_updates:
return False return False
if isinstance(photo, UserProfilePhotoEmpty): if photo is None or isinstance(photo, UserProfilePhotoEmpty):
photo_id = "" photo_id = ""
else: elif isinstance(photo, UserProfilePhoto):
photo_id = str(photo.photo_id) photo_id = str(photo.photo_id)
else:
self.log.warning(f"Unknown user profile photo type: {type(photo)}")
return False
if not photo_id and not config["bridge.allow_avatar_remove"]:
return False
if self.photo_id != photo_id: if self.photo_id != photo_id:
if not photo_id: if not photo_id:
self.photo_id = "" self.photo_id = ""
+4 -1
View File
@@ -29,6 +29,7 @@ from mautrix.client import Client
from mautrix.errors import MatrixRequestError from mautrix.errors import MatrixRequestError
from mautrix.types import UserID from mautrix.types import UserID
from mautrix.bridge import BaseUser from mautrix.bridge import BaseUser
from mautrix.util.logging import TraceLogger
from .types import TelegramID from .types import TelegramID
from .db import User as DBUser from .db import User as DBUser
@@ -45,7 +46,7 @@ SearchResult = NewType('SearchResult', Tuple['pu.Puppet', int])
class User(AbstractUser, BaseUser): class User(AbstractUser, BaseUser):
log: logging.Logger = logging.getLogger("mau.user") log: TraceLogger = logging.getLogger("mau.user")
by_mxid: Dict[str, 'User'] = {} by_mxid: Dict[str, 'User'] = {}
by_tgid: Dict[int, 'User'] = {} by_tgid: Dict[int, 'User'] = {}
@@ -343,10 +344,12 @@ class User(AbstractUser, BaseUser):
entity = dialog.entity entity = dialog.entity
if isinstance(entity, ChatForbidden): if isinstance(entity, ChatForbidden):
self.log.warning(f"Ignoring forbidden chat {entity} while syncing") self.log.warning(f"Ignoring forbidden chat {entity} while syncing")
continue
elif isinstance(entity, Chat) and (entity.deactivated or entity.left): elif isinstance(entity, Chat) and (entity.deactivated or entity.left):
self.log.warning(f"Ignoring deactivated or left chat {entity} while syncing") self.log.warning(f"Ignoring deactivated or left chat {entity} while syncing")
continue continue
elif isinstance(entity, TLUser) and not config["bridge.sync_direct_chats"]: elif isinstance(entity, TLUser) and not config["bridge.sync_direct_chats"]:
self.log.trace(f"Ignoring user {entity.id} while syncing")
continue continue
portal = po.Portal.get_by_entity(entity, receiver_id=self.tgid) portal = po.Portal.get_by_entity(entity, receiver_id=self.tgid)
self.portals[portal.tgid_full] = portal self.portals[portal.tgid_full] = portal
+2 -1
View File
@@ -13,7 +13,8 @@
# #
# You should have received a copy of the GNU Affero General Public License # You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>. # along with this program. If not, see <https://www.gnu.org/licenses/>.
from mautrix.util.color_log import ColorFormatter as BaseColorFormatter, PREFIX, MXID_COLOR, RESET from mautrix.util.logging.color import (ColorFormatter as BaseColorFormatter,
PREFIX, MXID_COLOR, RESET)
TELETHON_COLOR = PREFIX + "35;1m" # magenta TELETHON_COLOR = PREFIX + "35;1m" # magenta
TELETHON_MODULE_COLOR = PREFIX + "35m" TELETHON_MODULE_COLOR = PREFIX + "35m"
@@ -13,7 +13,7 @@
# #
# You should have received a copy of the GNU Affero General Public License # You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>. # along with this program. If not, see <https://www.gnu.org/licenses/>.
from typing import Optional, List, AsyncGenerator, Union, Awaitable, DefaultDict, Tuple from typing import Optional, List, AsyncGenerator, Union, Awaitable, DefaultDict, Tuple, cast
from collections import defaultdict from collections import defaultdict
import hashlib import hashlib
import asyncio import asyncio
@@ -35,6 +35,7 @@ from telethon import utils, helpers
from mautrix.appservice import IntentAPI from mautrix.appservice import IntentAPI
from mautrix.types import ContentURI, EncryptedFile from mautrix.types import ContentURI, EncryptedFile
from mautrix.util.logging import TraceLogger
from ..tgclient import MautrixTelegramClient from ..tgclient import MautrixTelegramClient
from ..db import TelegramFile as DBTelegramFile from ..db import TelegramFile as DBTelegramFile
@@ -44,7 +45,7 @@ try:
except ImportError: except ImportError:
async_encrypt_attachment = None async_encrypt_attachment = None
log: logging.Logger = logging.getLogger("mau.util") log: TraceLogger = cast(TraceLogger, logging.getLogger("mau.util"))
TypeLocation = Union[Document, InputDocumentFileLocation, InputPeerPhotoFileLocation, TypeLocation = Union[Document, InputDocumentFileLocation, InputPeerPhotoFileLocation,
InputFileLocation, InputPhotoFileLocation] InputFileLocation, InputPhotoFileLocation]
@@ -102,7 +103,7 @@ class UploadSender:
async def _next(self, data: bytes) -> None: async def _next(self, data: bytes) -> None:
self.request.bytes = data self.request.bytes = data
log.debug(f"Sending file part {self.request.file_part}/{self.part_count}" log.trace(f"Sending file part {self.request.file_part}/{self.part_count}"
f" with {len(data)} bytes") f" with {len(data)} bytes")
await self.sender.send(self.request) await self.sender.send(self.request)
self.request.file_part += self.stride self.request.file_part += self.stride
@@ -236,7 +237,7 @@ class ParallelTransferrer:
break break
yield data yield data
part += 1 part += 1
log.debug(f"Part {part} downloaded") log.trace(f"Part {part} downloaded")
log.debug("Parallel download finished, cleaning up connections") log.debug("Parallel download finished, cleaning up connections")
await self._cleanup() await self._cleanup()
+2 -2
View File
@@ -14,10 +14,10 @@ pillow>=4.3,<8
moviepy>=1,<2 moviepy>=1,<2
#/metrics #/metrics
prometheus_client>=0.6,<0.8 prometheus_client>=0.6,<0.9
#/postgres #/postgres
psycopg2-binary>=2,<3 psycopg2-binary>=2,<3
#/e2be #/e2be
matrix-nio[e2e]>=0.9,<0.11 matrix-nio[e2e]>=0.9,<0.13
+2 -2
View File
@@ -4,6 +4,6 @@ ruamel.yaml>=0.15.35,<0.17
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
mautrix==0.5.0.beta15 mautrix>=0.5.8,<0.6
telethon>=1.13,<1.14 telethon>=1.13,<1.15
telethon-session-sqlalchemy>=0.2.14,<0.3 telethon-session-sqlalchemy>=0.2.14,<0.3