Compare commits

...

60 Commits

Author SHA1 Message Date
Tulir Asokan 2cc439853f Bump version to 0.10.2 2021-11-13 14:40:39 +02:00
Tulir Asokan 76b2937c18 Update mautrix-python and stop supporting pickle for crypto store
SQLite is now supported for the crypto store instead of pickle (via aiosqlite)
2021-11-13 14:15:32 +02:00
Tulir Asokan f2a9f4ab33 Merge pull request #639 from olmari/patch-1
Add linebreak to "legend"
2021-11-12 23:06:26 +02:00
Tulir Asokan ec375e79d7 Merge pull request #680 from tadzik/tadzik/fix-max-initial-sync-for-chats
Make max_initial_member_sync work for Chats as well as Channels
2021-11-12 23:04:22 +02:00
Tulir Asokan 338a4d9761 Pin Pillow version in dockerfile to same as alpine. Fixes #683 2021-11-01 18:56:08 +02:00
Tulir Asokan 83d457f2b3 Ignore ChannelParticipantBanned in participant list. Fixes #635 2021-10-29 20:24:42 +03:00
Sumner Evans 3507095572 Merge pull request #681 from justinbot/justinbot/dont-log-messages
Don't log entire message contents on exception
2021-10-29 11:24:15 -06:00
Justin Carlson 4e7cf481fd Don't log entire messagecontents on exception. 2021-10-29 12:30:49 -04:00
Tadeusz Sośnierz 0915bb9402 Make max_initial_member_sync work for Chats as well as Channels 2021-10-27 14:46:12 +02:00
Sumner Evans 7c5d1c2959 Merge pull request #676 from justinbot/justinbot/welcome-text-config
Add example config for custom welcome messages
2021-10-26 09:51:08 -06:00
Justin Carlson 8aecf1f84b Update example config. 2021-10-23 12:10:36 -04:00
Justin Carlson 2c45d8dd5b Remove send_welcome_message override 2021-10-23 12:09:16 -04:00
Justin Carlson fac337eaf1 Add example config for welcome messages. 2021-10-22 12:17:25 -04:00
Tulir Asokan e7d8948334 Bump Telethon to update to latest version of layer 133 2021-10-20 21:20:05 +03:00
Tulir Asokan 6b8831872c Allow logout even if session isn't authorized 2021-10-20 20:55:11 +03:00
Tulir Asokan 4e8c373d1b Delete session on log_out() even if telegram logout fails 2021-10-20 20:24:47 +03:00
Tulir Asokan 8865dab6b0 Push bad credentials state if session isn't valid in start() 2021-10-20 20:12:23 +03:00
Tulir Asokan e4a2bd2f69 Catch authorization errors in get_me() 2021-10-20 20:02:09 +03:00
Tulir Asokan a132916525 Update Telethon
The upstream dev doesn't want to make new releases anymore before 2.0,
so this is temporarily using a fork. The main change is API layer 133,
which updates all user/chat IDs to be 64-bit.
2021-10-19 12:40:34 +03:00
Tulir Asokan a9dcb34b2d Use existing power levels as base for user levels instead of hardcoded values 2021-10-19 12:40:34 +03:00
Tulir Asokan 74c43355e4 Decrypt fetched messages to generate reply fallback 2021-10-19 12:40:34 +03:00
Sumner Evans 7255e86595 Merge pull request #670 from mautrix/ci-update-container-versions-on-success
ci: only update container versions on success
2021-10-14 12:30:36 -06:00
Sumner Evans e4098a226e ci: only update container versions on success 2021-10-14 09:45:39 -06:00
Sumner Evans 5dea5977ad Merge pull request #662 from mautrix/ci-auto-update-version
ci: deploy to dev stable and internal automatically
2021-09-17 19:04:14 -04:00
Sumner Evans 1c9a30773e ci: deploy to dev stable and internal automatically 2021-09-17 12:19:14 -04:00
Tulir Asokan e276944b40 Implement get_bridge_states 2021-08-25 16:04:50 +03:00
Tulir Asokan 2e14991815 Remove element ios hack from non-sticker documents 2021-08-20 14:00:42 +03:00
Tulir Asokan 3083727aff Add extension to unnamed file names. Fixes #646 2021-08-20 14:00:25 +03:00
Tulir Asokan d778c639dc Bump maximum Telethon version 2021-08-19 15:08:20 +03:00
Tulir Asokan 10de186598 Bump version to 0.10.1 2021-08-19 14:57:33 +03:00
Tulir Asokan 64107fab17 Add video flags for animated stickers 2021-08-19 14:43:57 +03:00
Tulir Asokan 52bfbddcca Add flag to invite events that will be auto-accepted 2021-08-18 20:48:11 +03:00
Tulir Asokan 5d9cc490d7 Fix public_portals setting not being respected on portal creation 2021-08-17 00:17:21 +03:00
Tulir Asokan 13cac8db9a Reset TelegramClient completely on AuthKeyDuplicatedError 2021-08-16 20:54:02 +03:00
Tulir Asokan 3ab5e4d8cc Move remaining manhole stuff to mautrix-python 2021-08-14 16:22:31 +03:00
Tulir Asokan 7e728dd5af Fix incorrect states being sent when stopping client 2021-08-13 16:48:19 +03:00
Tulir Asokan 597d2e3282 Bump asyncpg version 2021-08-13 13:00:07 +03:00
Tulir Asokan 57611a3f30 Catch AuthKeyDuplicatedError in start() 2021-08-13 12:59:24 +03:00
Tulir Asokan ec64c83cb0 Merge pull request #652 from mautrix/new-bridge-state
Upgrade mautrix to 0.10.2 and use new BridgeStateEvents
2021-08-10 19:53:59 +03:00
Tulir Asokan ecdaaea3b9 Don't send connected state when sync is in progress 2021-08-10 18:14:32 +03:00
Tulir Asokan bda41417aa Update repo path 2021-08-06 17:46:24 +03:00
Sumner Evans 5a76b5bcdc Upgrade mautrix to 0.10.2 and use new BridgeStateEvents 2021-08-04 16:49:56 -06:00
Tulir Asokan 4edd8eaa7b Ignore everything after ; in Matrix location events 2021-08-02 12:52:21 +03:00
Tulir Asokan 742a925040 Change some things 2021-08-02 12:52:05 +03:00
Sami Olmari bcede7710f Add linebreak to "legend"
Signed-off-by: Sami Olmari <sami@olmari.fi>
2021-07-06 09:57:33 +03:00
Tulir Asokan c02f67e0d1 Send warning if bridge bot doesn't have redaction permissions 2021-06-23 18:44:32 +03:00
Tulir Asokan 31650aac96 Update Docker image to Alpine 3.14 2021-06-23 17:50:48 +03:00
Tulir Asokan 730f6bab6f Update to Telethon 1.22 2021-06-22 19:42:36 +03:00
Tulir Asokan f923552f86 Update mautrix-python (ref #623) 2021-06-22 19:25:27 +03:00
Tulir Asokan eca1032d16 Ignore typing notifications from double puppeted users. Fixes #631 2021-06-19 22:48:43 +03:00
Tulir Asokan 570372fa83 Bump version to 0.10.0 2021-06-14 19:45:00 +03:00
Tulir Asokan 5ed09ad783 Fix Telegram->Matrix typing notifications 2021-06-10 15:44:12 +03:00
Tulir Asokan c385aa0b8d Add real-time bridge status push option 2021-06-09 20:04:17 +03:00
Tulir Asokan ec152cbd9d Pass through Telegram gif meta as custom fields 2021-05-24 16:04:53 +03:00
Tulir Asokan b36fc35e04 Don't remove zero-width joiners from middle of displaynames 2021-05-23 16:28:26 +03:00
Tulir Asokan 198e77cae9 Remove commented edge things from dockerfile 2021-05-13 14:56:09 +03:00
Tulir Asokan 9c4beb29a5 Send m.bridge data when bridging existing room to Telegram 2021-05-12 19:21:37 +03:00
Tulir Asokan 6accb530c6 Add option to only bridge mute status and tags when creating portal 2021-04-29 12:09:54 +03:00
Tulir Asokan 1a77ba5fcd Add option to bridge archive, pin and mute status from Telegram 2021-04-20 14:52:19 +03:00
Tulir Asokan 7e9dd8b895 Update mautrix-python 2021-04-16 15:27:56 +03:00
31 changed files with 463 additions and 323 deletions
+21 -3
View File
@@ -19,10 +19,28 @@ build amd64:
- docker rmi $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA-amd64
after_script:
- |
if [ "$CI_COMMIT_BRANCH" = "master" ]; then
apk add --update curl
if [[ "$CI_COMMIT_BRANCH" == "master" && "$CI_JOB_STATUS" == "success" ]]; then
apk add --update curl jq
rm -rf /var/cache/apk/*
curl "$NOVA_ADMIN_API_URL" -H "Content-Type: application/json" -d '{"password":"'"$NOVA_ADMIN_NIGHTLY_PASS"'","bridge":"'$NOVA_BRIDGE_TYPE'","image":"'$CI_REGISTRY_IMAGE':'$CI_COMMIT_SHA'-amd64"}'
jq -n '
{
password: env.BEEPER_DEV_ADMIN_NIGHTLY_PASS,
bridge: env.BEEPER_BRIDGE_TYPE,
image: "\(env.CI_REGISTRY_IMAGE):\(env.CI_COMMIT_SHA)-amd64",
channel: "STABLE"
}
' | curl "$BEEPER_DEV_ADMIN_API_URL" -H "Content-Type: application/json" -d @-
jq -n '
{
password: env.BEEPER_PROD_ADMIN_NIGHTLY_PASS,
bridge: env.BEEPER_BRIDGE_TYPE,
image: "\(env.CI_REGISTRY_IMAGE):\(env.CI_COMMIT_SHA)-amd64",
channel: "INTERNAL",
deployNext: true
}
' | curl "$BEEPER_PROD_ADMIN_API_URL" -H "Content-Type: application/json" -d @-
fi
build arm64:
+7 -9
View File
@@ -1,12 +1,7 @@
FROM dock.mau.dev/tulir/lottieconverter:alpine-3.13
FROM dock.mau.dev/tulir/lottieconverter:alpine-3.14
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 \
python3 py3-pip py3-setuptools py3-wheel \
py3-virtualenv \
@@ -19,6 +14,7 @@ RUN apk add --no-cache \
py3-psycopg2 \
py3-ruamel.yaml \
py3-commonmark \
py3-prometheus-client \
# Indirect dependencies
py3-idna \
#moviepy
@@ -27,9 +23,10 @@ RUN apk add --no-cache \
py3-requests \
#imageio
py3-numpy \
#py3-telethon@edge \ (outdated)
#py3-telethon \ (outdated)
# Optional for socks proxies
py3-pysocks \
py3-pyaes \
# cryptg
py3-cffi \
py3-qrcode \
@@ -40,7 +37,7 @@ RUN apk add --no-cache \
su-exec \
netcat-openbsd \
# encryption
olm-dev \
py3-olm \
py3-pycryptodome \
py3-unpaddedbase64 \
py3-future \
@@ -57,7 +54,8 @@ RUN apk add --virtual .build-deps \
libffi-dev \
build-base \
&& sed -Ei 's/psycopg2-binary.+//' optional-requirements.txt \
&& pip3 install -r requirements.txt -r optional-requirements.txt \
# TODO: unpin Pillow here after it's updated in Alpine
&& pip3 install -r requirements.txt -r optional-requirements.txt 'pillow==8.2' \
&& apk del .build-deps
COPY . /opt/mautrix-telegram
+5 -6
View File
@@ -1,9 +1,8 @@
# mautrix-telegram
![Languages](https://img.shields.io/github/languages/top/tulir/mautrix-telegram.svg)
[![License](https://img.shields.io/github/license/tulir/mautrix-telegram.svg)](LICENSE)
[![Release](https://img.shields.io/github/release/tulir/mautrix-telegram/all.svg)](https://github.com/tulir/mautrix-telegram/releases)
[![GitLab CI](https://mau.dev/tulir/mautrix-telegram/badges/master/pipeline.svg)](https://mau.dev/tulir/mautrix-telegram/container_registry)
[![Maintainability](https://img.shields.io/codeclimate/maintainability/tulir/mautrix-telegram.svg)](https://codeclimate.com/github/tulir/mautrix-telegram)
![Languages](https://img.shields.io/github/languages/top/mautrix/telegram.svg)
[![License](https://img.shields.io/github/license/mautrix/telegram.svg)](LICENSE)
[![Release](https://img.shields.io/github/release/mautrix/telegram/all.svg)](https://github.com/mautrix/telegram/releases)
[![GitLab CI](https://mau.dev/mautrix/telegram/badges/master/pipeline.svg)](https://mau.dev/mautrix/telegram/container_registry)
A Matrix-Telegram hybrid puppeting/relaybot bridge.
@@ -22,7 +21,7 @@ Some quick links:
[Relaybot setup](https://docs.mau.fi/bridges/python/telegram/relay-bot.html)
### Features & Roadmap
[ROADMAP.md](https://github.com/tulir/mautrix-telegram/blob/master/ROADMAP.md)
[ROADMAP.md](https://github.com/mautrix/telegram/blob/master/ROADMAP.md)
contains a general overview of what is supported by the bridge.
## Discussion
+1 -1
View File
@@ -59,5 +59,5 @@
* [ ] ‡ Secret chats (i.e. End-to-bridge encryption on Telegram)
* [x] End-to-bridge encryption in Matrix rooms (see [wiki](https://github.com/tulir/mautrix-telegram/wiki/End%E2%80%90to%E2%80%90bridge-encryption))
† Information not automatically sent from source, i.e. implementation may not be possible
† Information not automatically sent from source, i.e. implementation may not be possible
‡ Maybe, i.e. this feature may or may not be implemented at some point
+1 -1
View File
@@ -1,2 +1,2 @@
__version__ = "0.10.0rc1"
__version__ = "0.10.2"
__author__ = "Tulir Asokan <tulir@maunium.net>"
+18 -8
View File
@@ -13,8 +13,9 @@
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
from typing import Optional
from typing import Dict, Any
from telethon import __version__ as __telethon_version__
from alchemysession import AlchemySessionContainer
from mautrix.types import UserID, RoomID
@@ -23,7 +24,6 @@ from mautrix.util.db import Base
from .web.provisioning import ProvisioningAPI
from .web.public import PublicBridgeWebsite
from .commands.manhole import ManholeState
from .abstract_user import init as init_abstract_user
from .bot import Bot, init as init_bot
from .config import Config
@@ -47,7 +47,7 @@ class TelegramBridge(Bridge):
name = "mautrix-telegram"
command = "python -m mautrix-telegram"
description = "A Matrix-Telegram puppeting bridge."
repo_url = "https://github.com/tulir/mautrix-telegram"
repo_url = "https://github.com/mautrix/telegram"
real_user_content_key = "net.maunium.telegram.puppet"
version = version
markdown_version = linkified_version
@@ -57,7 +57,6 @@ class TelegramBridge(Bridge):
config: Config
session_container: AlchemySessionContainer
bot: Bot
manhole: Optional[ManholeState]
def prepare_db(self) -> None:
super().prepare_db()
@@ -83,7 +82,6 @@ class TelegramBridge(Bridge):
context = Context(self.az, self.config, self.loop, self.session_container, self, self.bot)
self._prepare_website(context)
self.matrix = context.mx = MatrixHandler(context)
self.manhole = None
init_abstract_user(context)
init_formatter(context)
@@ -107,9 +105,6 @@ class TelegramBridge(Bridge):
for puppet in Puppet.by_custom_mxid.values():
puppet.stop()
self.shutdown_actions = (user.stop() for user in User.by_tgid.values())
if self.manhole:
self.manhole.close()
self.manhole = None
async def get_user(self, user_id: UserID, create: bool = True) -> User:
user = User.get_by_mxid(user_id, create=create)
@@ -129,5 +124,20 @@ class TelegramBridge(Bridge):
def is_bridge_ghost(self, user_id: UserID) -> bool:
return bool(Puppet.get_id_from_mxid(user_id))
async def count_logged_in_users(self) -> int:
return len([user for user in User.by_tgid.values() if user.tgid])
async def manhole_global_namespace(self, user_id: UserID) -> Dict[str, Any]:
return {
**await super().manhole_global_namespace(user_id),
"User": User,
"Portal": Portal,
"Puppet": Puppet,
}
@property
def manhole_banner_program_version(self) -> str:
return f"{super().manhole_banner_program_version} and Telethon {__telethon_version__}"
TelegramBridge().run()
+46 -16
View File
@@ -1,5 +1,5 @@
# 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
# 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/>.
from typing import Tuple, Optional, Union, Dict, Type, Any, TYPE_CHECKING
from abc import ABC, abstractmethod
import platform
import asyncio
import logging
import platform
import time
from telethon.sessions import Session
@@ -31,7 +31,8 @@ from telethon.tl.types import (
UpdateEditChannelMessage, UpdateEditMessage, UpdateNewChannelMessage, UpdateReadHistoryOutbox,
UpdateShortChatMessage, UpdateShortMessage, UpdateUserName, UpdateUserPhoto, UpdateUserStatus,
UpdateUserTyping, User, UserStatusOffline, UserStatusOnline, UpdateReadHistoryInbox,
UpdateReadChannelInbox, MessageEmpty)
UpdateReadChannelInbox, MessageEmpty, UpdateFolderPeers, UpdatePinnedDialogs,
UpdateNotifySettings, UpdateChannelUserTyping)
from mautrix.types import UserID, PresenceState
from mautrix.errors import MatrixError
@@ -49,6 +50,7 @@ if TYPE_CHECKING:
from .context import Context
from .config import Config
from .bot import Bot
from .__main__ import TelegramBridge
config: Optional['Config'] = None
# Value updated from config in init()
@@ -57,6 +59,7 @@ MAX_DELETIONS: int = 10
UpdateMessage = Union[UpdateShortChatMessage, UpdateShortMessage, UpdateNewChannelMessage,
UpdateNewMessage, UpdateEditMessage, UpdateEditChannelMessage]
UpdateMessageContent = Union[UpdateShortMessage, UpdateShortChatMessage, Message, MessageService]
UpdateTyping = Union[UpdateUserTyping, UpdateChatUserTyping, UpdateChannelUserTyping]
UPDATE_TIME = Histogram("bridge_telegram_update", "Time spent processing Telegram updates",
("update_type",))
@@ -69,6 +72,7 @@ class AbstractUser(ABC):
loop: asyncio.AbstractEventLoop = None
log: TraceLogger
az: AppService
bridge: 'TelegramBridge'
relaybot: Optional['Bot']
ignore_incoming_bot_events: bool = True
@@ -127,9 +131,9 @@ class AbstractUser(ABC):
def _init_client(self) -> None:
self.log.debug(f"Initializing client for {self.name}")
self.session = self.session_container.new_session(self.name)
session = self.session_container.new_session(self.name)
if config["telegram.server.enabled"]:
self.session.set_dc(config["telegram.server.dc"],
session.set_dc(config["telegram.server.dc"],
config["telegram.server.ip"],
config["telegram.server.port"])
@@ -143,10 +147,10 @@ class AbstractUser(ABC):
appversion = config["telegram.device_info.app_version"]
connection, proxy = self._proxy_settings
assert isinstance(self.session, Session)
assert isinstance(session, Session)
self.client = MautrixTelegramClient(
session=self.session,
session=session,
api_id=config["telegram.api_id"],
api_hash=config["telegram.api_hash"],
@@ -194,7 +198,7 @@ class AbstractUser(ABC):
if not await self.update(update):
await self._update(update)
except Exception:
self.log.exception(f"Failed to handle Telegram update {update}")
self.log.exception("Failed to handle Telegram update")
UPDATE_ERRORS.labels(update_type=update_type).inc()
UPDATE_TIME.labels(update_type=update_type).observe(time.time() - start_time)
@@ -235,8 +239,7 @@ class AbstractUser(ABC):
# region Telegram update handling
async def _update(self, update: TypeUpdate) -> None:
asyncio.ensure_future(self._handle_entity_updates(getattr(update, "_entities", {})),
loop=self.loop)
asyncio.create_task(self._handle_entity_updates(getattr(update, "_entities", {})))
if isinstance(update, (UpdateShortChatMessage, UpdateShortMessage, UpdateNewChannelMessage,
UpdateNewMessage, UpdateEditMessage, UpdateEditChannelMessage)):
await self.update_message(update)
@@ -244,7 +247,7 @@ class AbstractUser(ABC):
await self.delete_message(update)
elif isinstance(update, UpdateDeleteChannelMessages):
await self.delete_channel_message(update)
elif isinstance(update, (UpdateChatUserTyping, UpdateUserTyping)):
elif isinstance(update, (UpdateChatUserTyping, UpdateChannelUserTyping, UpdateUserTyping)):
await self.update_typing(update)
elif isinstance(update, UpdateUserStatus):
await self.update_status(update)
@@ -260,9 +263,24 @@ class AbstractUser(ABC):
await self.update_read_receipt(update)
elif isinstance(update, (UpdateReadHistoryInbox, UpdateReadChannelInbox)):
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:
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,
UpdatePinnedChannelMessages]) -> None:
if isinstance(update, UpdatePinnedMessages):
@@ -330,16 +348,27 @@ class AbstractUser(ABC):
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):
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))
if not portal or not portal.mxid:
else:
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
sender = pu.Puppet.get(TelegramID(update.user_id))
await portal.handle_telegram_typing(sender, update)
async def _handle_entity_updates(self, entities: Dict[int, Union[User, Chat, Channel]]
@@ -486,6 +515,7 @@ class AbstractUser(ABC):
def init(context: 'Context') -> None:
global config, MAX_DELETIONS
AbstractUser.az, config, AbstractUser.loop, AbstractUser.relaybot = context.core
AbstractUser.bridge = context.bridge
AbstractUser.ignore_incoming_bot_events = config["bridge.relaybot.ignore_own_incoming_events"]
AbstractUser.session_container = context.session_container
MAX_DELETIONS = config.get("bridge.max_telegram_delete", 10)
+1 -1
View File
@@ -195,7 +195,7 @@ class Bot(AbstractUser):
return await reply("That user seems to be logged in. "
f"Just invite [{displayname}](tg://user?id={user.tgid})")
else:
await portal.main_intent.invite_user(portal.mxid, user.mxid)
await portal.invite_to_matrix(user.mxid)
return await reply(f"Invited `{user.mxid}` to the portal.")
@staticmethod
+1 -1
View File
@@ -1,7 +1,7 @@
from .handler import (command_handler, CommandHandler, CommandProcessor, CommandEvent,
SECTION_AUTH, SECTION_CREATING_PORTALS, SECTION_PORTAL_MANAGEMENT,
SECTION_MISC, SECTION_ADMIN)
from . import portal, telegram, matrix_auth, manhole
from . import portal, telegram, matrix_auth
__all__ = ["command_handler", "CommandHandler", "CommandProcessor", "CommandEvent",
"SECTION_AUTH", "SECTION_MISC", "SECTION_ADMIN", "SECTION_CREATING_PORTALS",
-128
View File
@@ -1,128 +0,0 @@
# mautrix-telegram - A Matrix-Telegram puppeting bridge
# Copyright (C) 2019 Tulir Asokan
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
from typing import Set, Callable
import asyncio
import sys
import os
from attr import dataclass
from telethon import __version__ as __telethon_version__
from mautrix import __version__ as __mautrix_version__
from mautrix.types import UserID
from mautrix.errors import MatrixConnectionError
from mautrix.util.manhole import start_manhole
from .. import __version__
from . import command_handler, CommandEvent, SECTION_ADMIN
@dataclass
class ManholeState:
server: asyncio.AbstractServer
opened_by: UserID
close: Callable[[], None]
whitelist: Set[int]
@command_handler(needs_auth=False, needs_admin=True, help_section=SECTION_ADMIN,
help_text="Open a manhole into the bridge.", help_args="<_uid..._>")
async def open_manhole(evt: CommandEvent) -> None:
if not evt.config["manhole.enabled"]:
await evt.reply("The manhole has been disabled in the config.")
return
elif len(evt.args) == 0:
await evt.reply("**Usage:** `$cmdprefix+sp open-manhole <uid...>`")
return
whitelist = set()
whitelist_whitelist = evt.config["manhole.whitelist"]
for arg in evt.args:
try:
uid = int(arg)
except ValueError:
await evt.reply(f"{arg} is not an integer.")
return
if whitelist_whitelist and uid not in whitelist_whitelist:
await evt.reply(f"{uid} is not in the list of allowed UIDs.")
return
whitelist.add(uid)
if evt.bridge.manhole:
added = [uid for uid in whitelist
if uid not in evt.bridge.manhole.whitelist]
evt.bridge.manhole.whitelist |= set(added)
if len(added) == 0:
await evt.reply(f"There's an existing manhole opened by {evt.bridge.manhole.opened_by}"
" and all the given UIDs are already whitelisted.")
else:
added_str = (f"{', '.join(str(uid) for uid in added[:-1])} and {added[-1]}"
if len(added) > 1 else added[0])
await evt.reply(f"There's an existing manhole opened by {evt.bridge.manhole.opened_by}"
f". Added {added_str} to the whitelist.")
evt.log.info(f"{evt.sender.mxid} added {added_str} to the manhole whitelist.")
return
from ..portal import Portal
from ..puppet import Puppet
from ..user import User
namespace = {
"bridge": evt.bridge,
"User": User,
"Portal": Portal,
"Puppet": Puppet,
}
banner = (f"Python {sys.version} on {sys.platform}\n"
f"mautrix-telegram {__version__} with mautrix-python {__mautrix_version__} "
f"and Telethon {__telethon_version__}\n\nManhole opened by {evt.sender.mxid}\n")
path = evt.config["manhole.path"]
wl_list = list(whitelist)
whitelist_str = (f"{', '.join(str(uid) for uid in wl_list[:-1])} and {wl_list[-1]}"
if len(wl_list) > 1 else wl_list[0])
evt.log.info(f"{evt.sender.mxid} opened a manhole with {whitelist_str} whitelisted.")
server, close = await start_manhole(path=path, banner=banner, namespace=namespace,
loop=evt.loop, whitelist=whitelist)
evt.bridge.manhole = ManholeState(server=server, opened_by=evt.sender.mxid, close=close,
whitelist=whitelist)
plrl = "s" if len(whitelist) != 1 else ""
await evt.reply(f"Opened manhole at unix://{path} with UID{plrl} {whitelist_str} whitelisted")
await server.wait_closed()
evt.bridge.manhole = None
try:
os.unlink(path)
except FileNotFoundError:
pass
evt.log.info(f"{evt.sender.mxid}'s manhole was closed.")
try:
await evt.reply("Your manhole was closed.")
except (AttributeError, MatrixConnectionError) as e:
evt.log.warning(f"Failed to send manhole close notification: {e}")
@command_handler(needs_auth=False, needs_admin=True, help_section=SECTION_ADMIN,
help_text="Close an open manhole.")
async def close_manhole(evt: CommandEvent) -> None:
if not evt.bridge.manhole:
await evt.reply("There is no open manhole.")
return
opened_by = evt.bridge.manhole.opened_by
evt.bridge.manhole.close()
evt.bridge.manhole = None
if opened_by != evt.sender.mxid:
await evt.reply(f"Closed manhole opened by {opened_by}")
+4 -1
View File
@@ -23,7 +23,7 @@ from mautrix.types import EventID, RoomID
from ...types import TelegramID
from ... import portal as po
from .. import command_handler, CommandEvent, SECTION_CREATING_PORTALS
from .util import user_has_power_level, get_initial_state
from .util import user_has_power_level, get_initial_state, warn_missing_power
@command_handler(needs_auth=False, needs_puppeting=False,
@@ -186,8 +186,11 @@ 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.photo_id = ""
await portal.save()
await portal.update_bridge_info()
asyncio.ensure_future(portal.update_matrix_room(user, entity, direct=False, levels=levels),
loop=evt.loop)
await warn_missing_power(levels, evt)
return await evt.reply("Bridging complete. Portal synchronization should begin momentarily.")
@@ -18,7 +18,7 @@ from mautrix.types import EventID
from ... import portal as po
from ...types import TelegramID
from .. import command_handler, CommandEvent, SECTION_CREATING_PORTALS
from .util import user_has_power_level, get_initial_state
from .util import user_has_power_level, get_initial_state, warn_missing_power
@command_handler(help_section=SECTION_CREATING_PORTALS,
@@ -58,6 +58,9 @@ async def create(evt: CommandEvent) -> EventID:
await evt.reply(f"Failed to add the following users to the chat:\n\n{error_list}\n\n"
"You can try `$cmdprefix+sp search -r <username>` to help the bridge find "
"those users.")
await warn_missing_power(levels, evt)
try:
await portal.create_telegram_chat(evt.sender, invites=invites, supergroup=supergroup)
except ValueError as e:
+12 -2
View File
@@ -18,14 +18,16 @@ from typing import Tuple, Optional
from mautrix.errors import MatrixRequestError
from mautrix.appservice import IntentAPI
from mautrix.types import RoomID, EventType, PowerLevelStateEventContent
from .. import CommandEvent
from ... import user as u
OptStr = Optional[str]
async def get_initial_state(intent: IntentAPI, room_id: RoomID
) -> Tuple[OptStr, OptStr, Optional[PowerLevelStateEventContent], bool]:
async def get_initial_state(
intent: IntentAPI, room_id: RoomID
) -> Tuple[OptStr, OptStr, Optional[PowerLevelStateEventContent], bool]:
state = await intent.get_state(room_id)
title: OptStr = None
about: OptStr = None
@@ -49,6 +51,14 @@ async def get_initial_state(intent: IntentAPI, room_id: RoomID
return title, about, levels, encrypted
async def warn_missing_power(levels: PowerLevelStateEventContent, evt: CommandEvent) -> None:
if levels.get_user_level(evt.az.bot_mxid) < levels.redact:
await evt.reply("Warning: The bot does not have privileges to redact messages on Matrix. "
"Message deletions from Telegram will not be bridged unless you give "
"redaction permissions to "
f"[{evt.az.bot_mxid}](https://matrix.to/#/{evt.az.bot_mxid})")
async def user_has_power_level(room_id: RoomID, intent: IntentAPI, sender: u.User,
event: str) -> bool:
if sender.is_admin:
+10 -5
View File
@@ -46,10 +46,13 @@ except ImportError:
help_section=SECTION_AUTH,
help_text="Check if you're logged into Telegram.")
async def ping(evt: CommandEvent) -> EventID:
me = await evt.sender.client.get_me() if await evt.sender.is_logged_in() else None
if me:
human_tg_id = f"@{me.username}" if me.username else f"+{me.phone}"
return await evt.reply(f"You're logged in as {human_tg_id}")
if await evt.sender.is_logged_in():
me = await evt.sender.get_me()
if me:
human_tg_id = f"@{me.username}" if me.username else f"+{me.phone}"
return await evt.reply(f"You're logged in as {human_tg_id}")
else:
return await evt.reply("You were logged in, but there appears to have been an error.")
else:
return await evt.reply("You're not logged in.")
@@ -346,10 +349,12 @@ async def _finish_sign_in(evt: CommandEvent, user: User, login_as: 'u.User' = No
return await evt.reply(msg)
@command_handler(needs_auth=True,
@command_handler(needs_auth=False,
help_section=SECTION_AUTH,
help_text="Log out from Telegram.")
async def logout(evt: CommandEvent) -> EventID:
if not evt.sender.tgid:
return await evt.reply("You're not logged in")
if await evt.sender.log_out():
return await evt.reply("Logged out successfully.")
return await evt.reply("Failed to log out.")
+4 -4
View File
@@ -75,10 +75,6 @@ class Config(BaseBridgeConfig):
copy("metrics.enabled")
copy("metrics.listen_port")
copy("manhole.enabled")
copy("manhole.path")
copy("manhole.whitelist")
copy("bridge.username_template")
copy("bridge.alias_template")
copy("bridge.displayname_template")
@@ -132,6 +128,10 @@ class Config(BaseBridgeConfig):
copy("bridge.delivery_receipts")
copy("bridge.delivery_error_reports")
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.takeout_limit")
copy("bridge.backfill.initial_limit")
+31 -8
View File
@@ -8,6 +8,12 @@ homeserver:
# Only applies if address starts with https://
verify_ssl: true
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
# Changing these values requires regeneration of the registration.
@@ -238,14 +244,7 @@ bridge:
# 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.
default: false
# Database for the encryption data. Currently only supports Postgres and an in-memory
# store that's persisted as a pickle.
# If set to `default`, will use the appservice postgres database
# or a pickle file if the appservice database is sqlite.
#
# Format examples:
# Pickle: pickle:///filename.pickle
# Postgres: postgres://username:password@hostname/dbname
# Database for the encryption data. If set to `default`, will use the appservice database.
database: default
# Options for automatic key sharing.
key_sharing:
@@ -271,6 +270,15 @@ bridge:
# This field will automatically be changed back to false after it,
# except if the config file is not writable.
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.
backfill:
# Whether or not the Telegram ghosts of logged in Matrix users should be
@@ -379,6 +387,21 @@ bridge:
# The prefix for commands. Only required in non-management rooms.
command_prefix: "!tg"
# Messages sent upon joining a management room.
# Markdown is supported. The defaults are listed below.
management_room_text:
# Sent when joining a room.
welcome: "Hello, I'm a Telegram bridge bot."
# Sent when joining a management room and the user is already logged in.
welcome_connected: "Use `help` for help."
# Sent when joining a management room and the user is not logged in.
welcome_unconnected: "Use `help` for help or `login` to log in."
# Optional extra text sent when joining a management room.
additional_help: ""
# Send each message separately (for readability in some clients)
management_room_multiple_messages: false
# Permissions for using the bridge.
# Permitted values:
# relaybot - Only use the bridge via the relaybot, no access to commands.
+5 -3
View File
@@ -32,7 +32,7 @@ from telethon.helpers import add_surrogate, del_surrogate
from mautrix.errors import MatrixRequestError
from mautrix.appservice import IntentAPI
from mautrix.types import (TextMessageEventContent, RelatesTo, RelationType, Format, MessageType,
MessageEvent)
MessageEvent, EventType)
from .. import user as u, puppet as pu, portal as po
from ..types import TelegramID
@@ -129,12 +129,14 @@ async def _add_reply_header(source: 'AbstractUser', content: TextMessageEventCon
content.relates_to = RelatesTo(rel_type=RelationType.REPLY, event_id=msg.mxid)
try:
event: MessageEvent = await main_intent.get_event(msg.mx_room, msg.mxid)
event = await main_intent.get_event(msg.mx_room, msg.mxid)
if event.type == EventType.ROOM_ENCRYPTED and source.bridge.matrix.e2ee:
event = await source.bridge.matrix.e2ee.decrypt(event)
if isinstance(event.content, TextMessageEventContent):
event.content.trim_reply_fallback()
puppet = await pu.Puppet.get_by_mxid(event.sender, create=False)
content.set_reply(event, displayname=puppet.displayname if puppet else event.sender)
except MatrixRequestError:
except Exception:
log.exception("Failed to get event to add reply fallback")
+2 -2
View File
@@ -18,7 +18,7 @@ def run(cmd):
if os.path.exists(".git") and shutil.which("git"):
try:
git_revision = run(["git", "rev-parse", "HEAD"]).strip().decode("ascii")
git_revision_url = f"https://github.com/tulir/mautrix-telegram/commit/{git_revision}"
git_revision_url = f"https://github.com/mautrix/telegram/commit/{git_revision}"
git_revision = git_revision[:8]
except (subprocess.SubprocessError, OSError):
git_revision = "unknown"
@@ -33,7 +33,7 @@ else:
git_revision_url = None
git_tag = None
git_tag_url = (f"https://github.com/tulir/mautrix-telegram/releases/tag/{git_tag}"
git_tag_url = (f"https://github.com/mautrix/telegram/releases/tag/{git_tag}"
if git_tag else None)
if git_tag and __version__ == git_tag[1:].replace("-", ""):
+2 -18
View File
@@ -84,7 +84,7 @@ class MatrixHandler(BaseMatrixHandler):
portal = po.Portal.get_by_tgid(puppet.tgid, inviter.tgid, "user")
if portal.mxid:
try:
await intent.invite_user(portal.mxid, inviter.mxid)
await portal.invite_to_matrix(inviter.mxid)
await intent.send_notice(
room_id, text=f"You already have a private chat with me: {portal.mxid}",
html=("You already have a private chat with me: "
@@ -109,28 +109,12 @@ class MatrixHandler(BaseMatrixHandler):
if e2be_ok is False:
message += "\n\nWarning: Failed to enable end-to-bridge encryption"
await intent.send_notice(room_id, message)
await portal.update_bridge_info()
else:
await intent.join_room(room_id)
await intent.send_notice(room_id, "This puppet will remain inactive until a "
"Telegram chat is created for this room.")
async def send_welcome_message(self, room_id: RoomID, inviter: 'u.User') -> None:
try:
is_management = len(await self.az.intent.get_room_members(room_id)) == 2
except MatrixError:
# The AS bot is not in the room.
return
cmd_prefix = self.commands.command_prefix
text = html = "Hello, I'm a Telegram bridge bot. "
if is_management and inviter.puppet_whitelisted and not await inviter.is_logged_in():
text += f"Use `{cmd_prefix} help` for help or `{cmd_prefix} login` to log in."
html += (f"Use <code>{cmd_prefix} help</code> for help"
f" or <code>{cmd_prefix} login</code> to log in.")
else:
text += f"Use `{cmd_prefix} help` for help."
html += f"Use <code>{cmd_prefix} help</code> for help."
await self.az.intent.send_notice(room_id, text=text, html=html)
async def handle_invite(self, room_id: RoomID, user_id: UserID, inviter: 'u.User',
event_id: EventID) -> None:
user = u.User.get_by_mxid(user_id, create=False)
+3 -2
View File
@@ -332,7 +332,7 @@ class PortalMatrix(BasePortal, ABC):
content: LocationMessageEventContent, reply_to: TelegramID
) -> None:
try:
lat, long = content.geo_uri[len("geo:"):].split(",")
lat, long = content.geo_uri[len("geo:"):].split(";")[0].split(",")
lat, long = float(lat), float(long)
except (KeyError, ValueError):
self.log.exception("Failed to parse location")
@@ -421,7 +421,8 @@ class PortalMatrix(BasePortal, ABC):
await self._handle_matrix_file(sender_id, event_id, space, client, content, reply_to,
caption_content)
else:
self.log.trace("Unhandled Matrix event: %s", content)
self.log.debug("Didn't handle Matrix event {event_id} due to unknown msgtype {content.msgtype}")
self.log.trace("Unhandled Matrix event content: %s", content)
async def handle_matrix_unpin_all(self, sender: 'u.User', pin_event_id: EventID) -> None:
await sender.client(UnpinAllMessagesRequest(peer=self.peer))
+44 -30
View File
@@ -27,7 +27,7 @@ from telethon.tl.types import (
PhotoEmpty, InputChannel, InputUser, ChatPhotoEmpty, PeerUser, Photo, TypeChat, TypeInputPeer,
TypeUser, User, InputPeerPhotoFileLocation, ChatParticipantAdmin, ChannelParticipantAdmin,
ChatParticipantCreator, ChannelParticipantCreator, UserProfilePhoto, UserProfilePhotoEmpty,
InputPeerUser)
InputPeerUser, ChannelParticipantBanned)
from mautrix.errors import MForbidden
from mautrix.types import (RoomID, UserID, RoomCreatePreset, EventType, Membership,
@@ -170,6 +170,7 @@ class PortalMetadata(BasePortal, ABC):
levels = self._get_base_power_levels(levels, entity)
await self.main_intent.set_power_levels(self.mxid, levels)
await self.handle_matrix_power_levels(source, levels.users, {}, None)
await self.update_bridge_info()
async def invite_telegram(self, source: 'u.User',
puppet: Union[p.Puppet, 'AbstractUser']) -> None:
@@ -185,12 +186,27 @@ class PortalMetadata(BasePortal, ABC):
# endregion
# region Telegram -> Matrix
def _get_invite_content(self, double_puppet: Optional['p.Puppet']) -> Dict[str, Any]:
invite_content = {}
if double_puppet:
invite_content["fi.mau.will_auto_accept"] = True
if self.is_direct:
invite_content["is_direct"] = True
return invite_content
async def invite_to_matrix(self, users: InviteList) -> None:
if isinstance(users, list):
for user in users:
await self.main_intent.invite_user(self.mxid, user, check_cache=True)
await self.invite_to_matrix(user)
else:
await self.main_intent.invite_user(self.mxid, users, check_cache=True)
puppet = await p.Puppet.get_by_custom_mxid(users)
await self.main_intent.invite_user(self.mxid, users, check_cache=True,
extra_content=self._get_invite_content(puppet))
if puppet:
try:
await puppet.intent.ensure_joined(self.mxid)
except Exception:
self.log.exception("Failed to ensure %s is joined to portal", users)
async def update_matrix_room(self, user: 'AbstractUser', entity: Union[TypeChat, User],
direct: bool = None, puppet: p.Puppet = None,
@@ -336,12 +352,13 @@ class PortalMetadata(BasePortal, ABC):
if self.peer_type == "channel":
self.megagroup = entity.megagroup
preset = RoomCreatePreset.PRIVATE
if self.peer_type == "channel" and entity.username:
preset = RoomCreatePreset.PUBLIC
if self.public_portals:
preset = RoomCreatePreset.PUBLIC
self.username = entity.username
alias = self.alias_localpart
else:
preset = RoomCreatePreset.PRIVATE
# TODO invite link alias?
alias = None
@@ -378,6 +395,7 @@ class PortalMetadata(BasePortal, ABC):
"state_key": self.bridge_info_state_key,
"content": self.bridge_info,
}]
create_invites = []
if config["bridge.encryption.default"] and self.matrix.e2ee:
self.encrypted = True
initial_state.append({
@@ -385,7 +403,7 @@ class PortalMetadata(BasePortal, ABC):
"content": {"algorithm": "m.megolm.v1.aes-sha2"},
})
if direct:
invites.append(self.az.bot_mxid)
create_invites.append(self.az.bot_mxid)
if direct and (self.encrypted or self.private_chat_portal_meta):
self.title = puppet.displayname
if config["appservice.community_id"]:
@@ -399,7 +417,7 @@ class PortalMetadata(BasePortal, ABC):
with self.backfill_lock:
room_id = await self.main_intent.create_room(alias_localpart=alias, preset=preset,
is_direct=direct, invitees=invites or [],
is_direct=direct, invitees=create_invites,
name=self.title, topic=self.about,
initial_state=initial_state,
creation_content=creation_content)
@@ -418,6 +436,8 @@ class PortalMetadata(BasePortal, ABC):
await self.az.state_store.set_power_levels(self.mxid, power_levels)
await user.register_portal(self)
await self.invite_to_matrix(invites)
update_room = self.loop.create_task(self.update_matrix_room(
user, entity, direct, puppet,
levels=power_levels, users=users))
@@ -483,14 +503,15 @@ class PortalMetadata(BasePortal, ABC):
levels.users[self.main_intent.mxid] = 100
return levels
@staticmethod
def _get_level_from_participant(participant: TypeParticipant) -> int:
@classmethod
def _get_level_from_participant(cls, participant: TypeParticipant,
levels: PowerLevelStateEventContent) -> int:
# TODO use the power level requirements to get better precision in channels
if isinstance(participant, (ChatParticipantAdmin, ChannelParticipantAdmin)):
return 50
return levels.state_default or 50
elif isinstance(participant, (ChatParticipantCreator, ChannelParticipantCreator)):
return 95
return 0
return levels.get_user_level(cls.az.bot_mxid) - 5
return levels.users_default or 0
@staticmethod
def _participant_to_power_levels(levels: PowerLevelStateEventContent,
@@ -521,7 +542,7 @@ class PortalMetadata(BasePortal, ABC):
puppet = p.Puppet.get(TelegramID(participant.user_id))
user = u.User.get_by_tgid(TelegramID(participant.user_id))
new_level = self._get_level_from_participant(participant)
new_level = self._get_level_from_participant(participant, levels)
if user:
await user.register_portal(self)
@@ -568,13 +589,6 @@ class PortalMetadata(BasePortal, ABC):
if user:
await self.invite_to_matrix(user.mxid)
puppet = await p.Puppet.get_by_custom_mxid(user.mxid)
if puppet:
try:
await puppet.intent.ensure_joined(self.mxid)
except Exception:
self.log.exception("Failed to ensure %s is joined to portal", user.mxid)
# We can't trust the member list if any of the following cases is true:
# * There are close to 10 000 users, because Telegram might not be sending all members.
# * The member sync count is limited, because then we might ignore some members.
@@ -746,15 +760,13 @@ class PortalMetadata(BasePortal, ABC):
if isinstance(photo, (ChatPhoto, UserProfilePhoto)):
loc = InputPeerPhotoFileLocation(
peer=await self.get_input_entity(user),
local_id=photo.photo_big.local_id,
volume_id=photo.photo_big.volume_id,
photo_id=photo.photo_id,
big=True
)
photo_id = (f"{loc.volume_id}-{loc.local_id}" if isinstance(photo, ChatPhoto)
else photo.photo_id)
photo_id = str(photo.photo_id)
elif isinstance(photo, Photo):
loc, largest = self._get_largest_photo_size(photo)
photo_id = f"{largest.location.volume_id}-{largest.location.local_id}"
loc, _ = self._get_largest_photo_size(photo)
photo_id = str(loc.id)
elif isinstance(photo, (UserProfilePhotoEmpty, ChatPhotoEmpty, PhotoEmpty, type(None))):
photo_id = ""
loc = None
@@ -785,7 +797,8 @@ class PortalMetadata(BasePortal, ABC):
@staticmethod
def _filter_participants(users: List[TypeUser], participants: List[TypeParticipant]
) -> Iterable[TypeUser]:
participant_map = {part.user_id: part for part in participants}
participant_map = {part.user_id: part for part in participants
if not isinstance(part, ChannelParticipantBanned)}
for user in users:
try:
user.participant = participant_map[user.id]
@@ -821,15 +834,16 @@ class PortalMetadata(BasePortal, ABC):
async def _get_users(self, user: 'AbstractUser',
entity: Union[TypeInputPeer, InputUser, TypeChat, TypeUser, InputChannel]
) -> List[TypeUser]:
limit = self.max_initial_member_sync
if self.peer_type == "chat":
chat = await user.client(GetFullChatRequest(chat_id=self.tgid))
return list(self._filter_participants(chat.users,
chat.full_chat.participants.participants))
return list(
self._filter_participants(chat.users, chat.full_chat.participants.participants)
)[:limit]
elif self.peer_type == "channel":
if not self.megagroup and not self.sync_channel_members:
return []
limit = self.max_initial_member_sync
if limit == 0:
return []
+31 -11
View File
@@ -34,7 +34,8 @@ from telethon.tl.types import (
MessageMediaPhoto, MessageMediaDice, MessageMediaGame, MessageMediaUnsupported, PeerUser,
PhotoCachedSize, TypeChannelParticipant, TypeChatParticipant, TypeDocumentAttribute,
TypeMessageAction, TypePhotoSize, PhotoSize, UpdateChatUserTyping, UpdateUserTyping,
MessageEntityPre, ChatPhotoEmpty, DocumentAttributeImageSize)
MessageEntityPre, ChatPhotoEmpty, DocumentAttributeImageSize, DocumentAttributeAnimated,
UpdateChannelUserTyping, SendMessageTypingAction)
from mautrix.appservice import IntentAPI
from mautrix.types import (EventID, UserID, ImageInfo, ThumbnailInfo, RelatesTo, MessageType,
@@ -56,16 +57,20 @@ if TYPE_CHECKING:
InviteList = Union[UserID, List[UserID]]
TypeParticipant = Union[TypeChatParticipant, TypeChannelParticipant]
UpdateTyping = Union[UpdateUserTyping, UpdateChatUserTyping, UpdateChannelUserTyping]
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
class PortalTelegram(BasePortal, ABC):
async def handle_telegram_typing(self, user: p.Puppet,
_: Union[UpdateUserTyping, UpdateChatUserTyping]) -> None:
await user.intent_for(self).set_typing(self.mxid, is_typing=True)
async def handle_telegram_typing(self, user: p.Puppet, update: UpdateTyping) -> None:
if user.is_real_user:
# Ignore typing notifications from double puppeted users to avoid echoing
return
is_typing = isinstance(update.action, SendMessageTypingAction)
await user.default_mxid_intent.set_typing(self.mxid, is_typing=is_typing)
def _get_external_url(self, evt: Message) -> Optional[str]:
if self.peer_type == "channel" and self.username is not None:
@@ -134,6 +139,7 @@ class PortalTelegram(BasePortal, ABC):
@staticmethod
def _parse_telegram_document_attributes(attributes: List[TypeDocumentAttribute]) -> DocAttrs:
name, mime_type, is_sticker, sticker_alt, width, height = None, None, False, None, 0, 0
is_gif = False
for attr in attributes:
if isinstance(attr, DocumentAttributeFilename):
name = name or attr.file_name
@@ -141,11 +147,13 @@ class PortalTelegram(BasePortal, ABC):
elif isinstance(attr, DocumentAttributeSticker):
is_sticker = True
sticker_alt = attr.alt
elif isinstance(attr, DocumentAttributeAnimated):
is_gif = True
elif isinstance(attr, DocumentAttributeVideo):
width, height = attr.w, attr.h
elif isinstance(attr, DocumentAttributeImageSize):
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
def _parse_telegram_document_meta(evt: Message, file: DBTelegramFile, attrs: DocAttrs,
@@ -185,13 +193,13 @@ class PortalTelegram(BasePortal, ABC):
height=file.thumbnail.height or thumb_size.h,
width=file.thumbnail.width or thumb_size.w,
size=file.thumbnail.size)
else:
elif attrs.is_sticker:
# This is a hack for bad clients like Element iOS that require a thumbnail
info.thumbnail_info = ImageInfo.deserialize(info.serialize())
if file.decryption_info:
info.thumbnail_file = file.decryption_info
else:
info.thumbnail_url = file.mxc
info.thumbnail_info = ImageInfo.deserialize(info.serialize())
return info, name
@@ -241,9 +249,21 @@ class PortalTelegram(BasePortal, ABC):
if info.thumbnail_info:
info.thumbnail_info.width = info.width
info.thumbnail_info.height = info.height
if attrs.is_gif or (attrs.is_sticker and info.mimetype == "video/webm"):
if attrs.is_gif:
info["fi.mau.telegram.gif"] = True
else:
info["fi.mau.telegram.animated_sticker"] = True
info["fi.mau.loop"] = True
info["fi.mau.autoplay"] = True
info["fi.mau.hide_controls"] = True
info["fi.mau.no_audio"] = True
if not name:
ext = sane_mimetypes.guess_extension(file.mime_type)
name = "unnamed_file" + ext
content = MediaMessageEventContent(
body=name or "unnamed file", info=info, relates_to=relates_to,
body=name, info=info, relates_to=relates_to,
external_url=self._get_external_url(evt),
msgtype={
"video/": MessageType.VIDEO,
@@ -295,7 +315,7 @@ class PortalTelegram(BasePortal, ABC):
async def handle_telegram_unsupported(self, source: 'AbstractUser', intent: IntentAPI,
evt: Message, relates_to: RelatesTo = None) -> EventID:
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/mautrix/telegram or ask your "
"bridge administrator about possible updates.")
content = await formatter.telegram_to_matrix(
evt, source, self.main_intent, override_text=override_text)
@@ -397,7 +417,7 @@ class PortalTelegram(BasePortal, ABC):
@staticmethod
def _int_to_bytes(i: int) -> bytes:
hex_value = "{0:010x}".format(i).encode("utf-8")
hex_value = f"{i:010x}".encode("utf-8")
return codecs.decode(hex_value, "hex_codec")
def _encode_msgid(self, source: 'AbstractUser', evt: Message) -> str:
+4 -3
View File
@@ -209,7 +209,9 @@ class Puppet(BasePuppet):
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"
"\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
@classmethod
@@ -342,8 +344,7 @@ class Puppet(BasePuppet):
loc = InputPeerPhotoFileLocation(
peer=await self.get_input_entity(source),
local_id=photo.photo_big.local_id,
volume_id=photo.photo_big.volume_id,
photo_id=photo.photo_id,
big=True
)
file = await util.transfer_file_to_matrix(source.client, self.default_mxid_intent, loc)
+163 -27
View File
@@ -1,5 +1,5 @@
# 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
# it under the terms of the GNU Affero General Public License as published by
@@ -15,22 +15,27 @@
# along with this program. If not, see <https://www.gnu.org/licenses/>.
from typing import (Awaitable, Dict, List, Iterable, NamedTuple, Optional, Tuple, Any, cast,
TYPE_CHECKING)
from collections import defaultdict
from datetime import datetime, timezone
import logging
import asyncio
from telethon.tl.types import (TypeUpdate, UpdateNewMessage, UpdateNewChannelMessage,
UpdateShortChatMessage, UpdateShortMessage, User as TLUser, Chat,
ChatForbidden)
ChatForbidden, UpdateFolderPeers, UpdatePinnedDialogs,
UpdateNotifySettings, NotifyPeer, InputUserSelf)
from telethon.tl.custom import Dialog
from telethon.tl.types.contacts import ContactsNotModified
from telethon.tl.functions.contacts import GetContactsRequest, SearchRequest
from telethon.tl.functions.account import UpdateStatusRequest
from telethon.tl.functions.users import GetUsersRequest
from telethon.errors import (AuthKeyDuplicatedError, UserDeactivatedError, UserDeactivatedBanError,
SessionRevokedError, UnauthorizedError)
from mautrix.client import Client
from mautrix.errors import MatrixRequestError
from mautrix.types import UserID, RoomID
from mautrix.bridge import BaseUser
from mautrix.errors import MatrixRequestError, MNotFound
from mautrix.types import UserID, RoomID, PushRuleScope, PushRuleKind, PushActionType, RoomTagInfo
from mautrix.bridge import BaseUser, BridgeState
from mautrix.util.bridge_state import BridgeStateEvent
from mautrix.util.logging import TraceLogger
from mautrix.util.opt_prometheus import Gauge
@@ -50,6 +55,12 @@ SearchResult = NamedTuple('SearchResult', puppet='pu.Puppet', similarity=int)
METRIC_LOGGED_IN = Gauge('bridge_logged_in', 'Users logged into bridge')
METRIC_CONNECTED = Gauge('bridge_connected', 'Users connected to Telegram')
BridgeState.human_readable_errors.update({
"tg-not-connected": "Your Telegram connection failed",
"tg-auth-key-duplicated": "The bridge accidentally logged you out",
"tg-not-authenticated": "The stored auth token did not work",
})
class User(AbstractUser, BaseUser):
log: TraceLogger = logging.getLogger("mau.user")
@@ -72,8 +83,9 @@ class User(AbstractUser, BaseUser):
saved_contacts: int = 0, is_bot: bool = False,
db_portals: Optional[Iterable[Tuple[TelegramID, TelegramID]]] = None,
db_instance: Optional[DBUser] = None) -> None:
super().__init__()
AbstractUser.__init__(self)
self.mxid = mxid
BaseUser.__init__(self)
self.tgid = tgid
self.is_bot = is_bot
self.username = username
@@ -85,11 +97,8 @@ class User(AbstractUser, BaseUser):
self.db_portals = db_portals or []
self._db_instance = db_instance
self._ensure_started_lock = asyncio.Lock()
self.dm_update_lock = asyncio.Lock()
self._metric_value = defaultdict(lambda: False)
self._track_connection_task = None
self.command_status = None
self._is_backfilling = False
(self.relaybot_whitelisted,
self.whitelisted,
@@ -102,8 +111,6 @@ class User(AbstractUser, BaseUser):
if tgid:
self.by_tgid[tgid] = self
self.log = self.log.getChild(self.mxid)
@property
def name(self) -> str:
return self.mxid
@@ -200,29 +207,79 @@ class User(AbstractUser, BaseUser):
return cast(User, await super().ensure_started(even_if_no_session))
async def start(self, delete_unless_authenticated: bool = False) -> 'User':
await super().start()
try:
await super().start()
except AuthKeyDuplicatedError:
self.log.warning("Got AuthKeyDuplicatedError in start()")
await self.push_bridge_state(BridgeStateEvent.BAD_CREDENTIALS,
error="tg-auth-key-duplicated")
await self.client.disconnect()
self.client.session.delete()
self.client = None
if not delete_unless_authenticated:
# The caller wants the client to be connected, so restart the connection.
await super().start()
return self
except Exception:
await self.push_bridge_state(BridgeStateEvent.UNKNOWN_ERROR)
raise
if await self.is_logged_in():
self.log.debug(f"Ensuring post_login() for {self.name}")
self.loop.create_task(self.post_login())
elif delete_unless_authenticated:
self.log.debug(f"Unauthenticated user {self.name} start()ed, deleting session...")
await self.client.disconnect()
if self.tgid:
await self.push_bridge_state(BridgeStateEvent.BAD_CREDENTIALS,
error="tg-not-authenticated")
self.client.session.delete()
return self
@property
def _is_connected(self) -> bool:
return bool(self.client and self.client._sender
and self.client._sender._transport_connected())
async def _track_connection(self) -> None:
self.log.debug("Starting loop to track connection state")
while True:
await asyncio.sleep(3)
connected = bool(self.client._sender._transport_connected
if self.client and self.client._sender else False)
connected = self._is_connected
self._track_metric(METRIC_CONNECTED, connected)
if connected:
await self.push_bridge_state(BridgeStateEvent.BACKFILLING if self._is_backfilling
else BridgeStateEvent.CONNECTED, ttl=3600)
else:
await self.push_bridge_state(BridgeStateEvent.UNKNOWN_ERROR, ttl=240,
error="tg-not-connected")
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_states(self) -> List[BridgeState]:
if not self.tgid:
return []
if self._is_connected and await self.is_logged_in():
state_event = (BridgeStateEvent.BACKFILLING if self._is_backfilling
else BridgeStateEvent.CONNECTED)
ttl = 3600
else:
state_event = BridgeStateEvent.UNKNOWN_ERROR
ttl = 240
return [BridgeState(state_event=state_event, ttl=ttl)]
async def get_puppet(self) -> Optional['pu.Puppet']:
if not self.tgid:
return None
return pu.Puppet.get(self.tgid)
async def stop(self) -> None:
await super().stop()
if self._track_connection_task:
self._track_connection_task.cancel()
self._track_connection_task = None
await super().stop()
self._track_metric(METRIC_CONNECTED, False)
async def post_login(self, info: TLUser = None, first_login: bool = False) -> None:
@@ -247,10 +304,13 @@ class User(AbstractUser, BaseUser):
if not self.is_bot and config["bridge.startup_sync"]:
try:
self._is_backfilling = True
await self.sync_dialogs()
await self.sync_contacts()
except Exception:
self.log.exception("Failed to run post-login sync")
finally:
self._is_backfilling = False
async def update(self, update: TypeUpdate) -> bool:
if not self.is_bot:
@@ -279,8 +339,22 @@ class User(AbstractUser, BaseUser):
if not self.is_bot:
await self.client(UpdateStatusRequest(offline=not online))
async def get_me(self) -> Optional[TLUser]:
try:
return (await self.client(GetUsersRequest([InputUserSelf()])))[0]
except UnauthorizedError as e:
self.log.error(f"Authorization error in get_me(): {e}")
await self.push_bridge_state(BridgeStateEvent.BAD_CREDENTIALS, error="tg-auth-error",
message=str(e), ttl=3600)
await self.stop()
return None
async def update_info(self, info: TLUser = None) -> None:
info = info or await self.client.get_me()
if not info:
info = await self.get_me()
if not info:
self.log.warning("get_me() returned None, aborting update_info()")
return
changed = False
if self.is_bot != info.bot:
self.is_bot = info.bot
@@ -315,6 +389,7 @@ class User(AbstractUser, BaseUser):
self.portals = {}
self.contacts = []
await self.save(portals=True, contacts=True)
await self.push_bridge_state(BridgeStateEvent.LOGGED_OUT)
if self.tgid:
try:
del self.by_tgid[self.tgid]
@@ -323,12 +398,11 @@ class User(AbstractUser, BaseUser):
self.tgid = None
await self.save()
ok = await self.client.log_out()
if not ok:
return False
self.client.session.delete()
self.delete()
await self.stop()
self._track_metric(METRIC_LOGGED_IN, False)
return True
return ok
def _search_local(self, query: str, max_results: int = 5, min_similarity: int = 45
) -> List[SearchResult]:
@@ -363,12 +437,6 @@ class User(AbstractUser, BaseUser):
return await self._search_remote(query), True
async def _catch(self, action: str, task: asyncio.Task) -> None:
try:
await task
except Exception:
self.log.exception(f"Error while {action}")
async def get_direct_chats(self) -> Dict[UserID, List[RoomID]]:
return {
pu.Puppet.get_mxid_from_id(portal.tgid): [portal.mxid]
@@ -376,8 +444,70 @@ class User(AbstractUser, BaseUser):
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,
puppet: Optional[pu.Puppet]) -> None:
was_created = False
if portal.mxid:
try:
await portal.backfill(self, last_id=dialog.message.id)
@@ -390,6 +520,7 @@ class User(AbstractUser, BaseUser):
elif should_create:
try:
await portal.create_matrix_room(self, dialog.entity, invites=[self.mxid])
was_created = True
except Exception:
self.log.exception(f"Error while creating {portal.tgid_log}")
if portal.mxid and puppet and puppet.is_real_user:
@@ -403,6 +534,10 @@ class User(AbstractUser, BaseUser):
dialog.dialog.read_inbox_max_id)
if last_read:
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:
if self.is_bot:
@@ -413,6 +548,7 @@ class User(AbstractUser, BaseUser):
index = 0
self.log.debug(f"Syncing dialogs (update_limit={update_limit}, "
f"create_limit={create_limit})")
await self.push_bridge_state(BridgeStateEvent.BACKFILLING)
puppet = await pu.Puppet.get_by_custom_mxid(self.mxid)
dialog: Dialog
async for dialog in self.client.iter_dialogs(limit=update_limit, ignore_migrated=True,
+5 -6
View File
@@ -30,7 +30,6 @@ from telethon.errors import (AuthBytesInvalidError, AuthKeyInvalidError, Locatio
SecurityError, FileIdInvalidError)
from mautrix.appservice import IntentAPI
from mautrix.util.network_retry import call_with_net_retry
from ..tgclient import MautrixTelegramClient
from ..db import TelegramFile as DBTelegramFile
@@ -103,8 +102,10 @@ def _location_to_id(location: TypeLocation) -> str:
return f"{location.id}-{location.access_hash}"
elif isinstance(location, (InputDocumentFileLocation, InputPhotoFileLocation)):
return f"{location.id}-{location.access_hash}-{location.thumb_size}"
elif isinstance(location, (InputFileLocation, InputPeerPhotoFileLocation)):
elif isinstance(location, InputFileLocation):
return f"{location.volume_id}-{location.local_id}"
elif isinstance(location, InputPeerPhotoFileLocation):
return str(location.photo_id)
async def transfer_thumbnail_to_matrix(client: MautrixTelegramClient, intent: IntentAPI,
@@ -145,8 +146,7 @@ async def transfer_thumbnail_to_matrix(client: MautrixTelegramClient, intent: In
if encrypt:
file, decryption_info = encrypt_attachment(file)
upload_mime_type = "application/octet-stream"
content_uri = await call_with_net_retry(intent.upload_media, file, upload_mime_type,
_action="upload media")
content_uri = await intent.upload_media(file, upload_mime_type)
if decryption_info:
decryption_info.url = content_uri
@@ -239,8 +239,7 @@ async def _unlocked_transfer_file_to_matrix(client: MautrixTelegramClient, inten
if encrypt and encrypt_attachment:
file, decryption_info = encrypt_attachment(file)
upload_mime_type = "application/octet-stream"
content_uri = await call_with_net_retry(intent.upload_media, file, upload_mime_type,
_action="upload media")
content_uri = await intent.upload_media(file, upload_mime_type)
if decryption_info:
decryption_info.url = content_uri
@@ -134,7 +134,7 @@ class ParallelTransferrer:
self.upload_ticker = 0
async def _cleanup(self) -> None:
await asyncio.gather(*[sender.disconnect() for sender in self.senders])
await asyncio.gather(*(sender.disconnect() for sender in self.senders))
self.senders = None
@staticmethod
@@ -161,9 +161,9 @@ class ParallelTransferrer:
await self._create_download_sender(file, 0, part_size, connections * part_size,
get_part_count()),
*await asyncio.gather(
*[self._create_download_sender(file, i, part_size, connections * part_size,
*(self._create_download_sender(file, i, part_size, connections * part_size,
get_part_count())
for i in range(1, connections)])
for i in range(1, connections)))
]
async def _create_download_sender(self, file: TypeLocation, index: int, part_size: int,
@@ -177,8 +177,8 @@ class ParallelTransferrer:
self.senders = [
await self._create_upload_sender(file_id, part_count, big, 0, connections),
*await asyncio.gather(
*[self._create_upload_sender(file_id, part_count, big, i, connections)
for i in range(1, connections)])
*(self._create_upload_sender(file_id, part_count, big, i, connections)
for i in range(1, connections)))
]
async def _create_upload_sender(self, file_id: int, part_count: int, big: bool, index: int,
+17 -13
View File
@@ -21,7 +21,10 @@ import json
from aiohttp import web
from telethon.utils import get_peer_id, resolve_id
from telethon.tl.types import ChatForbidden, ChannelForbidden, TypeChat
from telethon.tl.types import ChatForbidden, ChannelForbidden, TypeChat, InputUserSelf
from telethon.tl.functions.users import GetUsersRequest
from telethon.errors import (UserDeactivatedError, UserDeactivatedBanError, SessionRevokedError,
UnauthorizedError)
from mautrix.appservice import AppService
from mautrix.errors import MatrixRequestError, IntentError
@@ -294,16 +297,17 @@ class ProvisioningAPI(AuthAPI):
user_data = None
if await user.is_logged_in():
me = await user.client.get_me()
await user.update_info(me)
user_data = {
"id": user.tgid,
"username": user.username,
"first_name": me.first_name,
"last_name": me.last_name,
"phone": me.phone,
"is_bot": user.is_bot,
}
me = await user.get_me()
if me:
await user.update_info(me)
user_data = {
"id": user.tgid,
"username": user.username,
"first_name": me.first_name,
"last_name": me.last_name,
"phone": me.phone,
"is_bot": user.is_bot,
}
return web.json_response({
"telegram": user_data,
"mxid": user.mxid,
@@ -351,7 +355,7 @@ class ProvisioningAPI(AuthAPI):
return await self.post_login_password(user, data.get("password", ""))
async def logout(self, request: web.Request) -> web.Response:
_, user, err = await self.get_user_request_info(request, expect_logged_in=True,
_, user, err = await self.get_user_request_info(request, expect_logged_in=None,
require_puppeting=False,
want_data=False)
if err is not None:
@@ -461,7 +465,7 @@ class ProvisioningAPI(AuthAPI):
Optional[web.Response]]):
err = self.check_authorization(request)
if err is not None:
return err
return None, None, err
data = None
if want_data and (request.method == "POST" or request.method == "PUT"):
+3 -3
View File
@@ -6,11 +6,11 @@ info:
description: The provisioning API for Mautrix-Telegram, the Matrix-Telegram puppeting/relaybot bridge.
license:
name: AGPLv3
url: https://github.com/tulir/mautrix-telegram/blob/master/LICENSE
url: https://github.com/mautrix/telegram/blob/master/LICENSE
externalDocs:
description: Provisioning API wiki page on GitHub
url: https://github.com/tulir/mautrix-telegram/wiki/Provisioning-API
description: Provisioning API docs on docs.mau.fi
url: https://docs.mau.fi/bridges/python/telegram/provisioning-api.html
basePath: /_matrix/provision/v1
+5 -2
View File
@@ -15,13 +15,16 @@ qrcode>=6,<7
moviepy>=1,<2
#/metrics
prometheus_client>=0.6,<0.11
prometheus_client>=0.6,<0.13
#/postgres
psycopg2-binary>=2,<3
asyncpg>=0.20,<0.25
#/sqlite
aiosqlite>=0.17,<0.18
#/e2be
asyncpg>=0.20,<0.23
python-olm>=3,<4
pycryptodome>=3,<4
unpaddedbase64>=1,<2
+7 -2
View File
@@ -5,6 +5,11 @@ python-magic>=0.4,<0.5
commonmark>=0.8,<0.10
aiohttp>=3,<4
yarl>=1,<2
mautrix>=0.8.11,<0.9
telethon>=1.20,<1.22
mautrix>=0.11.3,<0.12
#telethon>=1.22,<1.24
# Temporary patch for 64-bit IDs until upstream telethon 2.0 is ready
tulir-telethon==1.24.0a2
telethon-session-sqlalchemy>=0.2.14,<0.3
# Temporarily always depend on aiosqlite to prevent breaking old installs
# Will be removed in v0.12 (after which you need to choose the [sqlite] optional dependency)
aiosqlite>=0.17,<0.18
+1 -1
View File
@@ -36,7 +36,7 @@ linkified_version = {linkified_version!r}
setuptools.setup(
name="mautrix-telegram",
version=version,
url="https://github.com/tulir/mautrix-telegram",
url="https://github.com/mautrix/telegram",
author="Tulir Asokan",
author_email="tulir@maunium.net",