Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 570372fa83 | |||
| 5ed09ad783 | |||
| c385aa0b8d | |||
| ec152cbd9d | |||
| b36fc35e04 | |||
| 198e77cae9 | |||
| 9c4beb29a5 | |||
| 6accb530c6 | |||
| 1a77ba5fcd | |||
| 7e9dd8b895 |
+1
-6
@@ -2,11 +2,6 @@ FROM dock.mau.dev/tulir/lottieconverter:alpine-3.13
|
|||||||
|
|
||||||
ARG TARGETARCH=amd64
|
ARG TARGETARCH=amd64
|
||||||
|
|
||||||
#RUN echo $'\
|
|
||||||
#@edge http://dl-cdn.alpinelinux.org/alpine/edge/main\n\
|
|
||||||
#@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 \
|
python3 py3-pip py3-setuptools py3-wheel \
|
||||||
py3-virtualenv \
|
py3-virtualenv \
|
||||||
@@ -27,7 +22,7 @@ RUN apk add --no-cache \
|
|||||||
py3-requests \
|
py3-requests \
|
||||||
#imageio
|
#imageio
|
||||||
py3-numpy \
|
py3-numpy \
|
||||||
#py3-telethon@edge \ (outdated)
|
#py3-telethon \ (outdated)
|
||||||
# Optional for socks proxies
|
# Optional for socks proxies
|
||||||
py3-pysocks \
|
py3-pysocks \
|
||||||
# cryptg
|
# cryptg
|
||||||
|
|||||||
@@ -1,2 +1,2 @@
|
|||||||
__version__ = "0.10.0rc1"
|
__version__ = "0.10.0"
|
||||||
__author__ = "Tulir Asokan <tulir@maunium.net>"
|
__author__ = "Tulir Asokan <tulir@maunium.net>"
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
# mautrix-telegram - A Matrix-Telegram puppeting bridge
|
# mautrix-telegram - A Matrix-Telegram puppeting bridge
|
||||||
# Copyright (C) 2020 Tulir Asokan
|
# Copyright (C) 2021 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
|
||||||
@@ -15,9 +15,9 @@
|
|||||||
# 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 Tuple, Optional, Union, Dict, Type, Any, TYPE_CHECKING
|
from typing import Tuple, Optional, Union, Dict, Type, Any, TYPE_CHECKING
|
||||||
from abc import ABC, abstractmethod
|
from abc import ABC, abstractmethod
|
||||||
|
import platform
|
||||||
import asyncio
|
import asyncio
|
||||||
import logging
|
import logging
|
||||||
import platform
|
|
||||||
import time
|
import time
|
||||||
|
|
||||||
from telethon.sessions import Session
|
from telethon.sessions import Session
|
||||||
@@ -31,7 +31,8 @@ from telethon.tl.types import (
|
|||||||
UpdateEditChannelMessage, UpdateEditMessage, UpdateNewChannelMessage, UpdateReadHistoryOutbox,
|
UpdateEditChannelMessage, UpdateEditMessage, UpdateNewChannelMessage, UpdateReadHistoryOutbox,
|
||||||
UpdateShortChatMessage, UpdateShortMessage, UpdateUserName, UpdateUserPhoto, UpdateUserStatus,
|
UpdateShortChatMessage, UpdateShortMessage, UpdateUserName, UpdateUserPhoto, UpdateUserStatus,
|
||||||
UpdateUserTyping, User, UserStatusOffline, UserStatusOnline, UpdateReadHistoryInbox,
|
UpdateUserTyping, User, UserStatusOffline, UserStatusOnline, UpdateReadHistoryInbox,
|
||||||
UpdateReadChannelInbox, MessageEmpty)
|
UpdateReadChannelInbox, MessageEmpty, UpdateFolderPeers, UpdatePinnedDialogs,
|
||||||
|
UpdateNotifySettings, UpdateChannelUserTyping)
|
||||||
|
|
||||||
from mautrix.types import UserID, PresenceState
|
from mautrix.types import UserID, PresenceState
|
||||||
from mautrix.errors import MatrixError
|
from mautrix.errors import MatrixError
|
||||||
@@ -57,6 +58,7 @@ MAX_DELETIONS: int = 10
|
|||||||
UpdateMessage = Union[UpdateShortChatMessage, UpdateShortMessage, UpdateNewChannelMessage,
|
UpdateMessage = Union[UpdateShortChatMessage, UpdateShortMessage, UpdateNewChannelMessage,
|
||||||
UpdateNewMessage, UpdateEditMessage, UpdateEditChannelMessage]
|
UpdateNewMessage, UpdateEditMessage, UpdateEditChannelMessage]
|
||||||
UpdateMessageContent = Union[UpdateShortMessage, UpdateShortChatMessage, Message, MessageService]
|
UpdateMessageContent = Union[UpdateShortMessage, UpdateShortChatMessage, Message, MessageService]
|
||||||
|
UpdateTyping = Union[UpdateUserTyping, UpdateChatUserTyping, UpdateChannelUserTyping]
|
||||||
|
|
||||||
UPDATE_TIME = Histogram("bridge_telegram_update", "Time spent processing Telegram updates",
|
UPDATE_TIME = Histogram("bridge_telegram_update", "Time spent processing Telegram updates",
|
||||||
("update_type",))
|
("update_type",))
|
||||||
@@ -235,8 +237,7 @@ class AbstractUser(ABC):
|
|||||||
# region Telegram update handling
|
# region Telegram update handling
|
||||||
|
|
||||||
async def _update(self, update: TypeUpdate) -> None:
|
async def _update(self, update: TypeUpdate) -> None:
|
||||||
asyncio.ensure_future(self._handle_entity_updates(getattr(update, "_entities", {})),
|
asyncio.create_task(self._handle_entity_updates(getattr(update, "_entities", {})))
|
||||||
loop=self.loop)
|
|
||||||
if isinstance(update, (UpdateShortChatMessage, UpdateShortMessage, UpdateNewChannelMessage,
|
if isinstance(update, (UpdateShortChatMessage, UpdateShortMessage, UpdateNewChannelMessage,
|
||||||
UpdateNewMessage, UpdateEditMessage, UpdateEditChannelMessage)):
|
UpdateNewMessage, UpdateEditMessage, UpdateEditChannelMessage)):
|
||||||
await self.update_message(update)
|
await self.update_message(update)
|
||||||
@@ -244,7 +245,7 @@ class AbstractUser(ABC):
|
|||||||
await self.delete_message(update)
|
await self.delete_message(update)
|
||||||
elif isinstance(update, UpdateDeleteChannelMessages):
|
elif isinstance(update, UpdateDeleteChannelMessages):
|
||||||
await self.delete_channel_message(update)
|
await self.delete_channel_message(update)
|
||||||
elif isinstance(update, (UpdateChatUserTyping, 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):
|
||||||
await self.update_status(update)
|
await self.update_status(update)
|
||||||
@@ -260,9 +261,24 @@ class AbstractUser(ABC):
|
|||||||
await self.update_read_receipt(update)
|
await self.update_read_receipt(update)
|
||||||
elif isinstance(update, (UpdateReadHistoryInbox, UpdateReadChannelInbox)):
|
elif isinstance(update, (UpdateReadHistoryInbox, UpdateReadChannelInbox)):
|
||||||
await self.update_own_read_receipt(update)
|
await self.update_own_read_receipt(update)
|
||||||
|
elif isinstance(update, UpdateFolderPeers):
|
||||||
|
await self.update_folder_peers(update)
|
||||||
|
elif isinstance(update, UpdatePinnedDialogs):
|
||||||
|
await self.update_pinned_dialogs(update)
|
||||||
|
elif isinstance(update, UpdateNotifySettings):
|
||||||
|
await self.update_notify_settings(update)
|
||||||
else:
|
else:
|
||||||
self.log.trace("Unhandled update: %s", update)
|
self.log.trace("Unhandled update: %s", update)
|
||||||
|
|
||||||
|
async def update_folder_peers(self, update: UpdateFolderPeers) -> None:
|
||||||
|
pass
|
||||||
|
|
||||||
|
async def update_pinned_dialogs(self, update: UpdatePinnedDialogs) -> None:
|
||||||
|
pass
|
||||||
|
|
||||||
|
async def update_notify_settings(self, update: UpdateNotifySettings) -> None:
|
||||||
|
pass
|
||||||
|
|
||||||
async def update_pinned_messages(self, update: Union[UpdatePinnedMessages,
|
async def update_pinned_messages(self, update: Union[UpdatePinnedMessages,
|
||||||
UpdatePinnedChannelMessages]) -> None:
|
UpdatePinnedChannelMessages]) -> None:
|
||||||
if isinstance(update, UpdatePinnedMessages):
|
if isinstance(update, UpdatePinnedMessages):
|
||||||
@@ -330,16 +346,27 @@ class AbstractUser(ABC):
|
|||||||
|
|
||||||
await portal.set_telegram_admin(TelegramID(update.user_id))
|
await portal.set_telegram_admin(TelegramID(update.user_id))
|
||||||
|
|
||||||
async def update_typing(self, update: Union[UpdateUserTyping, UpdateChatUserTyping]) -> None:
|
async def update_typing(self, update: UpdateTyping) -> None:
|
||||||
|
sender = None
|
||||||
if isinstance(update, UpdateUserTyping):
|
if isinstance(update, UpdateUserTyping):
|
||||||
portal = po.Portal.get_by_tgid(TelegramID(update.user_id), self.tgid, "user")
|
portal = po.Portal.get_by_tgid(TelegramID(update.user_id), self.tgid, "user")
|
||||||
else:
|
sender = pu.Puppet.get(TelegramID(update.user_id))
|
||||||
|
elif isinstance(update, UpdateChannelUserTyping):
|
||||||
|
portal = po.Portal.get_by_tgid(TelegramID(update.channel_id))
|
||||||
|
elif isinstance(update, UpdateChatUserTyping):
|
||||||
portal = po.Portal.get_by_tgid(TelegramID(update.chat_id))
|
portal = po.Portal.get_by_tgid(TelegramID(update.chat_id))
|
||||||
|
else:
|
||||||
if not portal or not portal.mxid:
|
return
|
||||||
|
|
||||||
|
if isinstance(update, (UpdateChannelUserTyping, UpdateChatUserTyping)):
|
||||||
|
# Can typing notifications come from non-user peers?
|
||||||
|
if not update.from_id.user_id:
|
||||||
|
return
|
||||||
|
sender = pu.Puppet.get(TelegramID(update.from_id.user_id))
|
||||||
|
|
||||||
|
if not sender or not portal or not portal.mxid:
|
||||||
return
|
return
|
||||||
|
|
||||||
sender = pu.Puppet.get(TelegramID(update.user_id))
|
|
||||||
await portal.handle_telegram_typing(sender, update)
|
await portal.handle_telegram_typing(sender, update)
|
||||||
|
|
||||||
async def _handle_entity_updates(self, entities: Dict[int, Union[User, Chat, Channel]]
|
async def _handle_entity_updates(self, entities: Dict[int, Union[User, Chat, Channel]]
|
||||||
|
|||||||
@@ -186,6 +186,7 @@ async def _locked_confirm_bridge(evt: CommandEvent, portal: 'po.Portal', room_id
|
|||||||
portal.encrypted) = await get_initial_state(evt.az.intent, evt.room_id)
|
portal.encrypted) = await get_initial_state(evt.az.intent, evt.room_id)
|
||||||
portal.photo_id = ""
|
portal.photo_id = ""
|
||||||
await portal.save()
|
await portal.save()
|
||||||
|
await portal.update_bridge_info()
|
||||||
|
|
||||||
asyncio.ensure_future(portal.update_matrix_room(user, entity, direct=False, levels=levels),
|
asyncio.ensure_future(portal.update_matrix_room(user, entity, direct=False, levels=levels),
|
||||||
loop=evt.loop)
|
loop=evt.loop)
|
||||||
|
|||||||
@@ -132,6 +132,10 @@ class Config(BaseBridgeConfig):
|
|||||||
copy("bridge.delivery_receipts")
|
copy("bridge.delivery_receipts")
|
||||||
copy("bridge.delivery_error_reports")
|
copy("bridge.delivery_error_reports")
|
||||||
copy("bridge.resend_bridge_info")
|
copy("bridge.resend_bridge_info")
|
||||||
|
copy("bridge.mute_bridging")
|
||||||
|
copy("bridge.pinned_tag")
|
||||||
|
copy("bridge.archive_tag")
|
||||||
|
copy("bridge.tag_only_on_create")
|
||||||
copy("bridge.backfill.invite_own_puppet")
|
copy("bridge.backfill.invite_own_puppet")
|
||||||
copy("bridge.backfill.takeout_limit")
|
copy("bridge.backfill.takeout_limit")
|
||||||
copy("bridge.backfill.initial_limit")
|
copy("bridge.backfill.initial_limit")
|
||||||
|
|||||||
@@ -8,6 +8,12 @@ homeserver:
|
|||||||
# Only applies if address starts with https://
|
# Only applies if address starts with https://
|
||||||
verify_ssl: true
|
verify_ssl: true
|
||||||
asmux: false
|
asmux: false
|
||||||
|
# Number of retries for all HTTP requests if the homeserver isn't reachable.
|
||||||
|
http_retry_count: 4
|
||||||
|
# The URL to push real-time bridge status to.
|
||||||
|
# If set, the bridge will make POST requests to this URL whenever a user's Telegram connection state changes.
|
||||||
|
# The bridge will use the appservice as_token to authorize requests.
|
||||||
|
status_endpoint: null
|
||||||
|
|
||||||
# Application service host/registration related details
|
# Application service host/registration related details
|
||||||
# Changing these values requires regeneration of the registration.
|
# Changing these values requires regeneration of the registration.
|
||||||
@@ -271,6 +277,15 @@ bridge:
|
|||||||
# This field will automatically be changed back to false after it,
|
# This field will automatically be changed back to false after it,
|
||||||
# except if the config file is not writable.
|
# except if the config file is not writable.
|
||||||
resend_bridge_info: false
|
resend_bridge_info: false
|
||||||
|
# When using double puppeting, should muted chats be muted in Matrix?
|
||||||
|
mute_bridging: false
|
||||||
|
# When using double puppeting, should pinned chats be moved to a specific tag in Matrix?
|
||||||
|
# The favorites tag is `m.favourite`.
|
||||||
|
pinned_tag: null
|
||||||
|
# Same as above for archived chats, the low priority tag is `m.lowpriority`.
|
||||||
|
archive_tag: null
|
||||||
|
# Whether or not mute status and tags should only be bridged when the portal room is created.
|
||||||
|
tag_only_on_create: true
|
||||||
# Settings for backfilling messages from Telegram.
|
# Settings for backfilling messages from Telegram.
|
||||||
backfill:
|
backfill:
|
||||||
# Whether or not the Telegram ghosts of logged in Matrix users should be
|
# Whether or not the Telegram ghosts of logged in Matrix users should be
|
||||||
|
|||||||
@@ -109,6 +109,7 @@ class MatrixHandler(BaseMatrixHandler):
|
|||||||
if e2be_ok is False:
|
if e2be_ok is False:
|
||||||
message += "\n\nWarning: Failed to enable end-to-bridge encryption"
|
message += "\n\nWarning: Failed to enable end-to-bridge encryption"
|
||||||
await intent.send_notice(room_id, message)
|
await intent.send_notice(room_id, message)
|
||||||
|
await portal.update_bridge_info()
|
||||||
else:
|
else:
|
||||||
await intent.join_room(room_id)
|
await intent.join_room(room_id)
|
||||||
await intent.send_notice(room_id, "This puppet will remain inactive until a "
|
await intent.send_notice(room_id, "This puppet will remain inactive until a "
|
||||||
|
|||||||
@@ -170,6 +170,7 @@ class PortalMetadata(BasePortal, ABC):
|
|||||||
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, {}, None)
|
await self.handle_matrix_power_levels(source, levels.users, {}, None)
|
||||||
|
await self.update_bridge_info()
|
||||||
|
|
||||||
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:
|
||||||
|
|||||||
@@ -34,7 +34,8 @@ from telethon.tl.types import (
|
|||||||
MessageMediaPhoto, MessageMediaDice, MessageMediaGame, MessageMediaUnsupported, PeerUser,
|
MessageMediaPhoto, MessageMediaDice, MessageMediaGame, MessageMediaUnsupported, PeerUser,
|
||||||
PhotoCachedSize, TypeChannelParticipant, TypeChatParticipant, TypeDocumentAttribute,
|
PhotoCachedSize, TypeChannelParticipant, TypeChatParticipant, TypeDocumentAttribute,
|
||||||
TypeMessageAction, TypePhotoSize, PhotoSize, UpdateChatUserTyping, UpdateUserTyping,
|
TypeMessageAction, TypePhotoSize, PhotoSize, UpdateChatUserTyping, UpdateUserTyping,
|
||||||
MessageEntityPre, ChatPhotoEmpty, DocumentAttributeImageSize)
|
MessageEntityPre, ChatPhotoEmpty, DocumentAttributeImageSize, DocumentAttributeAnimated,
|
||||||
|
UpdateChannelUserTyping, SendMessageTypingAction)
|
||||||
|
|
||||||
from mautrix.appservice import IntentAPI
|
from mautrix.appservice import IntentAPI
|
||||||
from mautrix.types import (EventID, UserID, ImageInfo, ThumbnailInfo, RelatesTo, MessageType,
|
from mautrix.types import (EventID, UserID, ImageInfo, ThumbnailInfo, RelatesTo, MessageType,
|
||||||
@@ -56,16 +57,18 @@ if TYPE_CHECKING:
|
|||||||
|
|
||||||
InviteList = Union[UserID, List[UserID]]
|
InviteList = Union[UserID, List[UserID]]
|
||||||
TypeParticipant = Union[TypeChatParticipant, TypeChannelParticipant]
|
TypeParticipant = Union[TypeChatParticipant, TypeChannelParticipant]
|
||||||
|
UpdateTyping = Union[UpdateUserTyping, UpdateChatUserTyping, UpdateChannelUserTyping]
|
||||||
DocAttrs = NamedTuple("DocAttrs", name=Optional[str], mime_type=Optional[str], is_sticker=bool,
|
DocAttrs = NamedTuple("DocAttrs", name=Optional[str], mime_type=Optional[str], is_sticker=bool,
|
||||||
sticker_alt=Optional[str], width=int, height=int)
|
sticker_alt=Optional[str], width=int, height=int, is_gif=bool)
|
||||||
|
|
||||||
config: Optional['Config'] = None
|
config: Optional['Config'] = None
|
||||||
|
|
||||||
|
|
||||||
class PortalTelegram(BasePortal, ABC):
|
class PortalTelegram(BasePortal, ABC):
|
||||||
async def handle_telegram_typing(self, user: p.Puppet,
|
async def handle_telegram_typing(self, user: p.Puppet, update: UpdateTyping) -> None:
|
||||||
_: Union[UpdateUserTyping, UpdateChatUserTyping]) -> None:
|
is_typing = isinstance(update.action, SendMessageTypingAction)
|
||||||
await user.intent_for(self).set_typing(self.mxid, is_typing=True)
|
# Always use the default puppet here to avoid any problems with echoing
|
||||||
|
await user.default_mxid_intent.set_typing(self.mxid, is_typing=is_typing)
|
||||||
|
|
||||||
def _get_external_url(self, evt: Message) -> Optional[str]:
|
def _get_external_url(self, evt: Message) -> Optional[str]:
|
||||||
if self.peer_type == "channel" and self.username is not None:
|
if self.peer_type == "channel" and self.username is not None:
|
||||||
@@ -134,6 +137,7 @@ class PortalTelegram(BasePortal, ABC):
|
|||||||
@staticmethod
|
@staticmethod
|
||||||
def _parse_telegram_document_attributes(attributes: List[TypeDocumentAttribute]) -> DocAttrs:
|
def _parse_telegram_document_attributes(attributes: List[TypeDocumentAttribute]) -> DocAttrs:
|
||||||
name, mime_type, is_sticker, sticker_alt, width, height = None, None, False, None, 0, 0
|
name, mime_type, is_sticker, sticker_alt, width, height = None, None, False, None, 0, 0
|
||||||
|
is_gif = False
|
||||||
for attr in attributes:
|
for attr in attributes:
|
||||||
if isinstance(attr, DocumentAttributeFilename):
|
if isinstance(attr, DocumentAttributeFilename):
|
||||||
name = name or attr.file_name
|
name = name or attr.file_name
|
||||||
@@ -141,11 +145,13 @@ class PortalTelegram(BasePortal, ABC):
|
|||||||
elif isinstance(attr, DocumentAttributeSticker):
|
elif isinstance(attr, DocumentAttributeSticker):
|
||||||
is_sticker = True
|
is_sticker = True
|
||||||
sticker_alt = attr.alt
|
sticker_alt = attr.alt
|
||||||
|
elif isinstance(attr, DocumentAttributeAnimated):
|
||||||
|
is_gif = True
|
||||||
elif isinstance(attr, DocumentAttributeVideo):
|
elif isinstance(attr, DocumentAttributeVideo):
|
||||||
width, height = attr.w, attr.h
|
width, height = attr.w, attr.h
|
||||||
elif isinstance(attr, DocumentAttributeImageSize):
|
elif isinstance(attr, DocumentAttributeImageSize):
|
||||||
width, height = attr.w, attr.h
|
width, height = attr.w, attr.h
|
||||||
return DocAttrs(name, mime_type, is_sticker, sticker_alt, width, height)
|
return DocAttrs(name, mime_type, is_sticker, sticker_alt, width, height, is_gif)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _parse_telegram_document_meta(evt: Message, file: DBTelegramFile, attrs: DocAttrs,
|
def _parse_telegram_document_meta(evt: Message, file: DBTelegramFile, attrs: DocAttrs,
|
||||||
@@ -241,6 +247,11 @@ class PortalTelegram(BasePortal, ABC):
|
|||||||
if info.thumbnail_info:
|
if info.thumbnail_info:
|
||||||
info.thumbnail_info.width = info.width
|
info.thumbnail_info.width = info.width
|
||||||
info.thumbnail_info.height = info.height
|
info.thumbnail_info.height = info.height
|
||||||
|
if attrs.is_gif:
|
||||||
|
info["fi.mau.telegram.gif"] = True
|
||||||
|
info["fi.mau.loop"] = True
|
||||||
|
info["fi.mau.autoplay"] = True
|
||||||
|
info["fi.mau.no_audio"] = True
|
||||||
|
|
||||||
content = MediaMessageEventContent(
|
content = MediaMessageEventContent(
|
||||||
body=name or "unnamed file", info=info, relates_to=relates_to,
|
body=name or "unnamed file", info=info, relates_to=relates_to,
|
||||||
|
|||||||
@@ -209,7 +209,9 @@ class Puppet(BasePuppet):
|
|||||||
whitespace = ("\t\n\r\v\f \u00a0\u034f\u180e\u2063\u202f\u205f\u2800\u3000\u3164\ufeff"
|
whitespace = ("\t\n\r\v\f \u00a0\u034f\u180e\u2063\u202f\u205f\u2800\u3000\u3164\ufeff"
|
||||||
"\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200a\u200b"
|
"\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200a\u200b"
|
||||||
"\u200c\u200d\u200e\u200f\ufe0f")
|
"\u200c\u200d\u200e\u200f\ufe0f")
|
||||||
name = "".join(c for c in name.strip(whitespace) if unicodedata.category(c) != 'Cf')
|
allowed_other_format = ("\u200d", "\u200c")
|
||||||
|
name = "".join(c for c in name.strip(whitespace) if unicodedata.category(c) != 'Cf'
|
||||||
|
or c in allowed_other_format)
|
||||||
return name
|
return name
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
|
|||||||
+98
-13
@@ -1,5 +1,5 @@
|
|||||||
# mautrix-telegram - A Matrix-Telegram puppeting bridge
|
# mautrix-telegram - A Matrix-Telegram puppeting bridge
|
||||||
# Copyright (C) 2020 Tulir Asokan
|
# Copyright (C) 2021 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
|
||||||
@@ -15,22 +15,23 @@
|
|||||||
# 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 (Awaitable, Dict, List, Iterable, NamedTuple, Optional, Tuple, Any, cast,
|
from typing import (Awaitable, Dict, List, Iterable, NamedTuple, Optional, Tuple, Any, cast,
|
||||||
TYPE_CHECKING)
|
TYPE_CHECKING)
|
||||||
from collections import defaultdict
|
from datetime import datetime, timezone
|
||||||
import logging
|
import logging
|
||||||
import asyncio
|
import asyncio
|
||||||
|
|
||||||
from telethon.tl.types import (TypeUpdate, UpdateNewMessage, UpdateNewChannelMessage,
|
from telethon.tl.types import (TypeUpdate, UpdateNewMessage, UpdateNewChannelMessage,
|
||||||
UpdateShortChatMessage, UpdateShortMessage, User as TLUser, Chat,
|
UpdateShortChatMessage, UpdateShortMessage, User as TLUser, Chat,
|
||||||
ChatForbidden)
|
ChatForbidden, UpdateFolderPeers, UpdatePinnedDialogs,
|
||||||
|
UpdateNotifySettings, NotifyPeer)
|
||||||
from telethon.tl.custom import Dialog
|
from telethon.tl.custom import Dialog
|
||||||
from telethon.tl.types.contacts import ContactsNotModified
|
from telethon.tl.types.contacts import ContactsNotModified
|
||||||
from telethon.tl.functions.contacts import GetContactsRequest, SearchRequest
|
from telethon.tl.functions.contacts import GetContactsRequest, SearchRequest
|
||||||
from telethon.tl.functions.account import UpdateStatusRequest
|
from telethon.tl.functions.account import UpdateStatusRequest
|
||||||
|
|
||||||
from mautrix.client import Client
|
from mautrix.client import Client
|
||||||
from mautrix.errors import MatrixRequestError
|
from mautrix.errors import MatrixRequestError, MNotFound
|
||||||
from mautrix.types import UserID, RoomID
|
from mautrix.types import UserID, RoomID, PushRuleScope, PushRuleKind, PushActionType, RoomTagInfo
|
||||||
from mautrix.bridge import BaseUser
|
from mautrix.bridge import BaseUser, BridgeState
|
||||||
from mautrix.util.logging import TraceLogger
|
from mautrix.util.logging import TraceLogger
|
||||||
from mautrix.util.opt_prometheus import Gauge
|
from mautrix.util.opt_prometheus import Gauge
|
||||||
|
|
||||||
@@ -50,6 +51,11 @@ SearchResult = NamedTuple('SearchResult', puppet='pu.Puppet', similarity=int)
|
|||||||
METRIC_LOGGED_IN = Gauge('bridge_logged_in', 'Users logged into bridge')
|
METRIC_LOGGED_IN = Gauge('bridge_logged_in', 'Users logged into bridge')
|
||||||
METRIC_CONNECTED = Gauge('bridge_connected', 'Users connected to Telegram')
|
METRIC_CONNECTED = Gauge('bridge_connected', 'Users connected to Telegram')
|
||||||
|
|
||||||
|
BridgeState.human_readable_errors.update({
|
||||||
|
"tg-not-connected": "Your Telegram connection failed",
|
||||||
|
"logged-out": "You're not logged into Telegram",
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
class User(AbstractUser, BaseUser):
|
class User(AbstractUser, BaseUser):
|
||||||
log: TraceLogger = logging.getLogger("mau.user")
|
log: TraceLogger = logging.getLogger("mau.user")
|
||||||
@@ -72,8 +78,9 @@ class User(AbstractUser, BaseUser):
|
|||||||
saved_contacts: int = 0, is_bot: bool = False,
|
saved_contacts: int = 0, is_bot: bool = False,
|
||||||
db_portals: Optional[Iterable[Tuple[TelegramID, TelegramID]]] = None,
|
db_portals: Optional[Iterable[Tuple[TelegramID, TelegramID]]] = None,
|
||||||
db_instance: Optional[DBUser] = None) -> None:
|
db_instance: Optional[DBUser] = None) -> None:
|
||||||
super().__init__()
|
AbstractUser.__init__(self)
|
||||||
self.mxid = mxid
|
self.mxid = mxid
|
||||||
|
BaseUser.__init__(self)
|
||||||
self.tgid = tgid
|
self.tgid = tgid
|
||||||
self.is_bot = is_bot
|
self.is_bot = is_bot
|
||||||
self.username = username
|
self.username = username
|
||||||
@@ -85,12 +92,8 @@ class User(AbstractUser, BaseUser):
|
|||||||
self.db_portals = db_portals or []
|
self.db_portals = db_portals or []
|
||||||
self._db_instance = db_instance
|
self._db_instance = db_instance
|
||||||
self._ensure_started_lock = asyncio.Lock()
|
self._ensure_started_lock = asyncio.Lock()
|
||||||
self.dm_update_lock = asyncio.Lock()
|
|
||||||
self._metric_value = defaultdict(lambda: False)
|
|
||||||
self._track_connection_task = None
|
self._track_connection_task = None
|
||||||
|
|
||||||
self.command_status = None
|
|
||||||
|
|
||||||
(self.relaybot_whitelisted,
|
(self.relaybot_whitelisted,
|
||||||
self.whitelisted,
|
self.whitelisted,
|
||||||
self.puppet_whitelisted,
|
self.puppet_whitelisted,
|
||||||
@@ -102,8 +105,6 @@ class User(AbstractUser, BaseUser):
|
|||||||
if tgid:
|
if tgid:
|
||||||
self.by_tgid[tgid] = self
|
self.by_tgid[tgid] = self
|
||||||
|
|
||||||
self.log = self.log.getChild(self.mxid)
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def name(self) -> str:
|
def name(self) -> str:
|
||||||
return self.mxid
|
return self.mxid
|
||||||
@@ -217,6 +218,21 @@ class User(AbstractUser, BaseUser):
|
|||||||
connected = bool(self.client._sender._transport_connected
|
connected = bool(self.client._sender._transport_connected
|
||||||
if self.client and self.client._sender else False)
|
if self.client and self.client._sender else False)
|
||||||
self._track_metric(METRIC_CONNECTED, connected)
|
self._track_metric(METRIC_CONNECTED, connected)
|
||||||
|
await self.push_bridge_state(ok=connected, ttl=3600 if connected else 240,
|
||||||
|
error="tg-not-connected" if not connected else None)
|
||||||
|
|
||||||
|
async def fill_bridge_state(self, state: BridgeState) -> None:
|
||||||
|
await super().fill_bridge_state(state)
|
||||||
|
state.remote_id = str(self.tgid)
|
||||||
|
state.remote_name = self.human_tg_id
|
||||||
|
|
||||||
|
async def get_bridge_state(self) -> BridgeState:
|
||||||
|
if not self.client:
|
||||||
|
return BridgeState(ok=False, error="logged-out")
|
||||||
|
elif not self.client._sender or not self.client._sender._transport_connected:
|
||||||
|
return BridgeState(ok=False, error="tg-not-connected")
|
||||||
|
else:
|
||||||
|
return BridgeState(ok=True)
|
||||||
|
|
||||||
async def stop(self) -> None:
|
async def stop(self) -> None:
|
||||||
await super().stop()
|
await super().stop()
|
||||||
@@ -224,6 +240,7 @@ class User(AbstractUser, BaseUser):
|
|||||||
self._track_connection_task.cancel()
|
self._track_connection_task.cancel()
|
||||||
self._track_connection_task = None
|
self._track_connection_task = None
|
||||||
self._track_metric(METRIC_CONNECTED, False)
|
self._track_metric(METRIC_CONNECTED, False)
|
||||||
|
await self.push_bridge_state(ok=False, error="tg-not-connected")
|
||||||
|
|
||||||
async def post_login(self, info: TLUser = None, first_login: bool = False) -> None:
|
async def post_login(self, info: TLUser = None, first_login: bool = False) -> None:
|
||||||
if config["metrics.enabled"] and not self._track_connection_task:
|
if config["metrics.enabled"] and not self._track_connection_task:
|
||||||
@@ -328,6 +345,7 @@ class User(AbstractUser, BaseUser):
|
|||||||
self.delete()
|
self.delete()
|
||||||
await self.stop()
|
await self.stop()
|
||||||
self._track_metric(METRIC_LOGGED_IN, False)
|
self._track_metric(METRIC_LOGGED_IN, False)
|
||||||
|
await self.push_bridge_state(ok=False, error="logged-out")
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def _search_local(self, query: str, max_results: int = 5, min_similarity: int = 45
|
def _search_local(self, query: str, max_results: int = 5, min_similarity: int = 45
|
||||||
@@ -376,8 +394,70 @@ class User(AbstractUser, BaseUser):
|
|||||||
if portal.mxid
|
if portal.mxid
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async def _tag_room(self, puppet: pu.Puppet, portal: po.Portal, tag: str, active: bool
|
||||||
|
) -> None:
|
||||||
|
if not tag or not portal or not portal.mxid:
|
||||||
|
return
|
||||||
|
tag_info = await puppet.intent.get_room_tag(portal.mxid, tag)
|
||||||
|
if active and tag_info is None:
|
||||||
|
tag_info = RoomTagInfo(order=0.5)
|
||||||
|
tag_info[self.bridge.real_user_content_key] = True
|
||||||
|
await puppet.intent.set_room_tag(portal.mxid, tag, tag_info)
|
||||||
|
elif not active and tag_info and tag_info.get(self.bridge.real_user_content_key, False):
|
||||||
|
await puppet.intent.remove_room_tag(portal.mxid, tag)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
async def _mute_room(puppet: pu.Puppet, portal: po.Portal, mute_until: datetime) -> None:
|
||||||
|
if not config["bridge.mute_bridging"] or not portal or not portal.mxid:
|
||||||
|
return
|
||||||
|
now = datetime.utcnow().replace(tzinfo=timezone.utc)
|
||||||
|
if mute_until is not None and mute_until > now:
|
||||||
|
await puppet.intent.set_push_rule(PushRuleScope.GLOBAL, PushRuleKind.ROOM, portal.mxid,
|
||||||
|
actions=[PushActionType.DONT_NOTIFY])
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
await puppet.intent.remove_push_rule(PushRuleScope.GLOBAL, PushRuleKind.ROOM,
|
||||||
|
portal.mxid)
|
||||||
|
except MNotFound:
|
||||||
|
pass
|
||||||
|
|
||||||
|
async def update_folder_peers(self, update: UpdateFolderPeers) -> None:
|
||||||
|
if config["bridge.tag_only_on_create"]:
|
||||||
|
return
|
||||||
|
puppet = await pu.Puppet.get_by_custom_mxid(self.mxid)
|
||||||
|
if not puppet or not puppet.is_real_user:
|
||||||
|
return
|
||||||
|
for peer in update.folder_peers:
|
||||||
|
portal = po.Portal.get_by_entity(peer.peer, receiver_id=self.tgid, create=False)
|
||||||
|
await self._tag_room(puppet, portal, config["bridge.archive_tag"],
|
||||||
|
peer.folder_id == 1)
|
||||||
|
|
||||||
|
async def update_pinned_dialogs(self, update: UpdatePinnedDialogs) -> None:
|
||||||
|
if config["bridge.tag_only_on_create"]:
|
||||||
|
return
|
||||||
|
puppet = await pu.Puppet.get_by_custom_mxid(self.mxid)
|
||||||
|
if not puppet or not puppet.is_real_user:
|
||||||
|
return
|
||||||
|
# TODO bridge unpinning properly
|
||||||
|
for pinned in update.order:
|
||||||
|
portal = po.Portal.get_by_entity(pinned.peer, receiver_id=self.tgid, create=False)
|
||||||
|
await self._tag_room(puppet, portal, config["bridge.pinned_tag"], True)
|
||||||
|
|
||||||
|
async def update_notify_settings(self, update: UpdateNotifySettings) -> None:
|
||||||
|
if config["bridge.tag_only_on_create"]:
|
||||||
|
return
|
||||||
|
elif not isinstance(update.peer, NotifyPeer):
|
||||||
|
# TODO handle global notification setting changes?
|
||||||
|
return
|
||||||
|
puppet = await pu.Puppet.get_by_custom_mxid(self.mxid)
|
||||||
|
if not puppet or not puppet.is_real_user:
|
||||||
|
return
|
||||||
|
portal = po.Portal.get_by_entity(update.peer.peer, receiver_id=self.tgid, create=False)
|
||||||
|
await self._mute_room(puppet, portal, update.notify_settings.mute_until)
|
||||||
|
|
||||||
async def _sync_dialog(self, portal: po.Portal, dialog: Dialog, should_create: bool,
|
async def _sync_dialog(self, portal: po.Portal, dialog: Dialog, should_create: bool,
|
||||||
puppet: Optional[pu.Puppet]) -> None:
|
puppet: Optional[pu.Puppet]) -> None:
|
||||||
|
was_created = False
|
||||||
if portal.mxid:
|
if portal.mxid:
|
||||||
try:
|
try:
|
||||||
await portal.backfill(self, last_id=dialog.message.id)
|
await portal.backfill(self, last_id=dialog.message.id)
|
||||||
@@ -390,6 +470,7 @@ class User(AbstractUser, BaseUser):
|
|||||||
elif should_create:
|
elif should_create:
|
||||||
try:
|
try:
|
||||||
await portal.create_matrix_room(self, dialog.entity, invites=[self.mxid])
|
await portal.create_matrix_room(self, dialog.entity, invites=[self.mxid])
|
||||||
|
was_created = True
|
||||||
except Exception:
|
except Exception:
|
||||||
self.log.exception(f"Error while creating {portal.tgid_log}")
|
self.log.exception(f"Error while creating {portal.tgid_log}")
|
||||||
if portal.mxid and puppet and puppet.is_real_user:
|
if portal.mxid and puppet and puppet.is_real_user:
|
||||||
@@ -403,6 +484,10 @@ class User(AbstractUser, BaseUser):
|
|||||||
dialog.dialog.read_inbox_max_id)
|
dialog.dialog.read_inbox_max_id)
|
||||||
if last_read:
|
if last_read:
|
||||||
await puppet.intent.mark_read(last_read.mx_room, last_read.mxid)
|
await puppet.intent.mark_read(last_read.mx_room, last_read.mxid)
|
||||||
|
if was_created or not config["bridge.tag_only_on_create"]:
|
||||||
|
await self._mute_room(puppet, portal, dialog.dialog.notify_settings.mute_until)
|
||||||
|
await self._tag_room(puppet, portal, config["bridge.pinned_tag"], dialog.pinned)
|
||||||
|
await self._tag_room(puppet, portal, config["bridge.archive_tag"], dialog.archived)
|
||||||
|
|
||||||
async def sync_dialogs(self) -> None:
|
async def sync_dialogs(self) -> None:
|
||||||
if self.is_bot:
|
if self.is_bot:
|
||||||
|
|||||||
@@ -30,7 +30,6 @@ from telethon.errors import (AuthBytesInvalidError, AuthKeyInvalidError, Locatio
|
|||||||
SecurityError, FileIdInvalidError)
|
SecurityError, FileIdInvalidError)
|
||||||
|
|
||||||
from mautrix.appservice import IntentAPI
|
from mautrix.appservice import IntentAPI
|
||||||
from mautrix.util.network_retry import call_with_net_retry
|
|
||||||
|
|
||||||
from ..tgclient import MautrixTelegramClient
|
from ..tgclient import MautrixTelegramClient
|
||||||
from ..db import TelegramFile as DBTelegramFile
|
from ..db import TelegramFile as DBTelegramFile
|
||||||
@@ -145,8 +144,7 @@ async def transfer_thumbnail_to_matrix(client: MautrixTelegramClient, intent: In
|
|||||||
if encrypt:
|
if encrypt:
|
||||||
file, decryption_info = encrypt_attachment(file)
|
file, decryption_info = encrypt_attachment(file)
|
||||||
upload_mime_type = "application/octet-stream"
|
upload_mime_type = "application/octet-stream"
|
||||||
content_uri = await call_with_net_retry(intent.upload_media, file, upload_mime_type,
|
content_uri = await intent.upload_media(file, upload_mime_type)
|
||||||
_action="upload media")
|
|
||||||
if decryption_info:
|
if decryption_info:
|
||||||
decryption_info.url = content_uri
|
decryption_info.url = content_uri
|
||||||
|
|
||||||
@@ -239,8 +237,7 @@ async def _unlocked_transfer_file_to_matrix(client: MautrixTelegramClient, inten
|
|||||||
if encrypt and encrypt_attachment:
|
if encrypt and encrypt_attachment:
|
||||||
file, decryption_info = encrypt_attachment(file)
|
file, decryption_info = encrypt_attachment(file)
|
||||||
upload_mime_type = "application/octet-stream"
|
upload_mime_type = "application/octet-stream"
|
||||||
content_uri = await call_with_net_retry(intent.upload_media, file, upload_mime_type,
|
content_uri = await intent.upload_media(file, upload_mime_type)
|
||||||
_action="upload media")
|
|
||||||
if decryption_info:
|
if decryption_info:
|
||||||
decryption_info.url = content_uri
|
decryption_info.url = content_uri
|
||||||
|
|
||||||
|
|||||||
@@ -15,13 +15,13 @@ qrcode>=6,<7
|
|||||||
moviepy>=1,<2
|
moviepy>=1,<2
|
||||||
|
|
||||||
#/metrics
|
#/metrics
|
||||||
prometheus_client>=0.6,<0.11
|
prometheus_client>=0.6,<0.12
|
||||||
|
|
||||||
#/postgres
|
#/postgres
|
||||||
psycopg2-binary>=2,<3
|
psycopg2-binary>=2,<3
|
||||||
|
|
||||||
#/e2be
|
#/e2be
|
||||||
asyncpg>=0.20,<0.23
|
asyncpg>=0.20,<0.24
|
||||||
python-olm>=3,<4
|
python-olm>=3,<4
|
||||||
pycryptodome>=3,<4
|
pycryptodome>=3,<4
|
||||||
unpaddedbase64>=1,<2
|
unpaddedbase64>=1,<2
|
||||||
|
|||||||
+1
-1
@@ -5,6 +5,6 @@ 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.8.11,<0.9
|
mautrix>=0.9.3,<0.10
|
||||||
telethon>=1.20,<1.22
|
telethon>=1.20,<1.22
|
||||||
telethon-session-sqlalchemy>=0.2.14,<0.3
|
telethon-session-sqlalchemy>=0.2.14,<0.3
|
||||||
|
|||||||
Reference in New Issue
Block a user