Compare commits

...

46 Commits

Author SHA1 Message Date
Tulir Asokan 60b1573386 Bump version to 0.13.0 2023-02-26 17:24:11 +02:00
Tulir Asokan f4695d8395 Update changelog 2023-02-26 15:05:49 +02:00
Tulir Asokan f63c679d3e Catch errors updating initial profile. Fixes #860 2023-02-22 01:31:32 +02:00
Tulir Asokan 4e5305c91b Update Telethon to save update state more actively (ref #894) 2023-02-22 01:02:47 +02:00
Tulir Asokan f30c03a727 Block creating rooms for deactivated chats (ref #894) 2023-02-21 22:34:21 +02:00
Tulir Asokan 354b49d9e5 Remove unnecessary dependencies in dockerfile and update changelog 2023-02-15 23:01:09 +02:00
Tulir Asokan 7b60ee1337 Actually save timestamps for telegram_file 2023-02-15 21:51:49 +02:00
Tulir Asokan ab1d9b246e Replace moviepy with directly using ffmpeg for video thumbnails
Fixes #809
2023-02-15 21:51:44 +02:00
Tulir Asokan f7b694c9e4 Use new wrapper for creating background tasks 2023-02-11 22:41:15 +02:00
Tulir Asokan be6f6bbfac Update linters 2023-02-11 22:40:50 +02:00
Tulir Asokan a32f797b0b Remove support for registering accounts 2023-02-10 21:20:51 +02:00
vurpo f12abbe038 Merge pull request #887 from mautrix/vurpo/qr-websocket
Add websocket for QR login to provisioning API
2023-01-27 18:40:35 +02:00
Max Sandholm ad2b49928a Sort imports 2023-01-27 17:40:12 +02:00
Max Sandholm 67f75796fa Correct retry and timeout for QR websocket 2023-01-27 17:37:48 +02:00
Tulir Asokan c235ced030 Update dependencies 2023-01-27 15:11:15 +02:00
Tulir Asokan d53764fd84 Remove custom TTLs in bridge states 2023-01-27 15:11:15 +02:00
Tulir Asokan 529d8ae3ba Recreate whole connection instead of only update loop on error 2023-01-27 15:11:15 +02:00
Max Sandholm f864f66e62 Add websocket for QR login to provisioning API 2023-01-26 23:43:44 +02:00
Tulir Asokan b1b633bcf9 Add option to notify portal if incoming message bridging fails 2023-01-26 16:01:59 +02:00
Tulir Asokan e655e0a882 Only send marker for backwards backfills on hungryserv 2023-01-18 14:28:12 +02:00
Tulir Asokan db88fbb694 Remove internal ID from pm command help (ref #882) 2023-01-15 19:05:24 +02:00
Tulir Asokan ace3e42281 Update mautrix-python 2023-01-14 14:28:45 +02:00
Tulir Asokan a40000e6b7 Only fill bridge state if tgid is set 2023-01-14 14:28:22 +02:00
Tulir Asokan 21d2d7dfea Update telethon 2023-01-11 12:13:59 +02:00
Tulir Asokan a61731a289 Update changelog 2023-01-10 16:03:50 +02:00
Tulir Asokan c250076032 Update mautrix-python 2023-01-10 16:03:39 +02:00
vurpo c6d35b103a Merge pull request #880 from mautrix/max/bri-5580
Fix remaining reconnect bug in provision API
2023-01-04 18:49:03 +02:00
Max Sandholm 596c9a5055 None check puppet on logout call 2023-01-04 18:21:25 +02:00
Tulir Asokan 9fae4f14d2 Handle getting logged out the same way in all cases 2023-01-03 21:45:25 +02:00
Tulir Asokan f1f0b86696 Fix deleting existing backfill queue items 2023-01-03 20:45:55 +02:00
Tulir Asokan e3d2a1fcef Catch ValueErrors in 2fa login step 2023-01-02 17:46:54 +02:00
Tulir Asokan 2303622475 Update changelog 2023-01-02 17:16:24 +02:00
vurpo 732277be5e Merge pull request #879 from mautrix/stickersets
Add provisioning API function to get list of user's sticker sets
2023-01-02 16:27:40 +02:00
Max Sandholm 28f205057f Lint imports after enabling linting 2023-01-02 15:11:27 +02:00
Max Sandholm 9e32ec3e39 Add provisioning API function to get list of user's sticker sets 2023-01-02 15:04:49 +02:00
Tulir Asokan 1fa86cbb52 Fix handling username updates 2022-12-31 12:24:33 +02:00
Tulir Asokan 9d8a4d4269 Use allow_contact_info flag for names too 2022-12-30 20:29:35 +02:00
Tulir Asokan cb22615bb5 Update Telethon 2022-12-30 20:17:25 +02:00
Tulir Asokan 989dc32481 Don't fail on unnamed files with unknown mime types 2022-12-28 13:15:13 +02:00
Tulir Asokan 02dd44ad63 Update Telethon 2022-12-22 22:50:21 +02:00
Tulir Asokan d6517959d8 Update dependencies 2022-12-21 18:31:18 +02:00
Tulir Asokan d9d539c4b8 Don't fail file transfer entirely if thumbnailing fails 2022-12-21 18:23:21 +02:00
Tulir Asokan 5b18ffb7ec Fix handling UpdateUserName 2022-12-11 13:37:08 +02:00
Tulir Asokan cf70efb6a2 Clear backfill queue when chat is upgraded 2022-12-02 16:53:58 +02:00
Tulir Asokan a42699e1fb Fix cryptg version range 2022-11-28 12:00:03 +02:00
Tulir Asokan 597e82a33b Update Docker image to Alpine 3.17 2022-11-26 22:02:34 +02:00
27 changed files with 461 additions and 264 deletions
+2 -2
View File
@@ -9,14 +9,14 @@ jobs:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
- uses: actions/setup-python@v2 - uses: actions/setup-python@v2
with: with:
python-version: "3.10" python-version: "3.11"
- uses: isort/isort-action@master - uses: isort/isort-action@master
with: with:
sortPaths: "./mautrix_telegram" sortPaths: "./mautrix_telegram"
- uses: psf/black@stable - uses: psf/black@stable
with: with:
src: "./mautrix_telegram" src: "./mautrix_telegram"
version: "22.3.0" version: "23.1.0"
- name: pre-commit - name: pre-commit
run: | run: |
pip install pre-commit pip install pre-commit
+3 -3
View File
@@ -1,6 +1,6 @@
repos: repos:
- repo: https://github.com/pre-commit/pre-commit-hooks - repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.1.0 rev: v4.4.0
hooks: hooks:
- id: trailing-whitespace - id: trailing-whitespace
exclude_types: [markdown] exclude_types: [markdown]
@@ -8,13 +8,13 @@ repos:
- id: check-yaml - id: check-yaml
- id: check-added-large-files - id: check-added-large-files
- repo: https://github.com/psf/black - repo: https://github.com/psf/black
rev: 22.3.0 rev: 23.1.0
hooks: hooks:
- id: black - id: black
language_version: python3 language_version: python3
files: ^mautrix_telegram/.*\.pyi?$ files: ^mautrix_telegram/.*\.pyi?$
- repo: https://github.com/PyCQA/isort - repo: https://github.com/PyCQA/isort
rev: 5.10.1 rev: 5.12.0
hooks: hooks:
- id: isort - id: isort
files: ^mautrix_telegram/.*\.pyi?$ files: ^mautrix_telegram/.*\.pyi?$
+28
View File
@@ -1,3 +1,31 @@
# v0.13.0 (2023-02-26)
### Added
* Added `allow_contact_info` config option to specify whether personal names
and avatars for other users should be bridged.
* The option is only safe to enable on single-user instances, using it
anywhere else will cause ghost user profiles to flip back and forth between
personal and default ones.
* Added config option to notify Matrix room if bridging an incoming message
fails.
### Improved
* Updated Docker image to Alpine 3.17.
* Updated to Telegram API layer 152.
* Improved handling users getting logged out.
* Removed support for creating accounts, as Telegram only allows requesting SMS
login codes on the official mobile clients now.
* Replaced moviepy with calling ffmpeg directly for generating video thumbnails.
### Fixed
* Fixed handling Telegram chat upgrades when backfilling is enabled.
* Fixed file transfers failing if transfering the thumbnail fails.
* Fixed bridging unnamed files with unrecognized mime types.
* Fixed enqueueing more backfill.
* Fixed timestamps not being saved in `telegram_file` table.
* Fixed issues with old events being replayed if the bridge was shut down
uncleanly.
# v0.12.2 (2022-11-26) # v0.12.2 (2022-11-26)
### Added ### Added
+1 -8
View File
@@ -1,4 +1,4 @@
FROM dock.mau.dev/tulir/lottieconverter:alpine-3.16 FROM dock.mau.dev/tulir/lottieconverter:alpine-3.17
RUN apk add --no-cache \ RUN apk add --no-cache \
python3 py3-pip py3-setuptools py3-wheel \ python3 py3-pip py3-setuptools py3-wheel \
@@ -13,13 +13,6 @@ RUN apk add --no-cache \
# Indirect dependencies # Indirect dependencies
py3-idna \ py3-idna \
py3-rsa \ py3-rsa \
#moviepy
py3-decorator \
py3-tqdm \
py3-requests \
#py3-proglog \
#imageio
py3-numpy \
#py3-telethon \ (outdated) #py3-telethon \ (outdated)
# Optional for socks proxies # Optional for socks proxies
py3-pysocks \ py3-pysocks \
+1 -1
View File
@@ -1,3 +1,3 @@
pre-commit>=2.10.1,<3 pre-commit>=2.10.1,<3
isort>=5.10.1,<6 isort>=5.10.1,<6
black>=22.3,<23 black>=23,<24
+1 -1
View File
@@ -1,2 +1,2 @@
__version__ = "0.12.2" __version__ = "0.13.0"
__author__ = "Tulir Asokan <tulir@maunium.net>" __author__ = "Tulir Asokan <tulir@maunium.net>"
+36 -21
View File
@@ -22,7 +22,7 @@ import logging
import platform import platform
import time import time
from telethon.errors import UnauthorizedError from telethon.errors import AuthKeyError, UnauthorizedError
from telethon.network import ( from telethon.network import (
Connection, Connection,
ConnectionTcpFull, ConnectionTcpFull,
@@ -63,8 +63,8 @@ from telethon.tl.types import (
UpdateShort, UpdateShort,
UpdateShortChatMessage, UpdateShortChatMessage,
UpdateShortMessage, UpdateShortMessage,
UpdateUser,
UpdateUserName, UpdateUserName,
UpdateUserPhoto,
UpdateUserStatus, UpdateUserStatus,
UpdateUserTyping, UpdateUserTyping,
User, User,
@@ -75,6 +75,7 @@ from telethon.tl.types import (
from mautrix.appservice import AppService from mautrix.appservice import AppService
from mautrix.errors import MatrixError from mautrix.errors import MatrixError
from mautrix.types import PresenceState, UserID from mautrix.types import PresenceState, UserID
from mautrix.util import background_task
from mautrix.util.logging import TraceLogger from mautrix.util.logging import TraceLogger
from mautrix.util.opt_prometheus import Counter, Histogram from mautrix.util.opt_prometheus import Counter, Histogram
@@ -235,18 +236,23 @@ class AbstractUser(ABC):
) )
self.client.add_event_handler(self._update_catch) self.client.add_event_handler(self._update_catch)
@abstractmethod
async def on_signed_out(self, err: UnauthorizedError | AuthKeyError) -> None:
pass
async def _telethon_update_error_callback(self, err: Exception) -> None: async def _telethon_update_error_callback(self, err: Exception) -> None:
if isinstance(err, (UnauthorizedError, AuthKeyError)):
background_task.create(self.on_signed_out(err))
return
if self.config["telegram.exit_on_update_error"]: if self.config["telegram.exit_on_update_error"]:
self.log.critical(f"Stopping due to update handling error {type(err).__name__}") self.log.critical(f"Stopping due to update handling error {type(err).__name__}")
self.bridge.manual_stop(50) self.bridge.manual_stop(50)
else: else:
if isinstance(err, UnauthorizedError): self.log.info("Recreating Telethon connection in 60 seconds")
self.log.warning("Not recreating Telethon update loop")
return
self.log.info("Recreating Telethon update loop in 60 seconds")
await asyncio.sleep(60) await asyncio.sleep(60)
self.log.debug("Now recreating Telethon update loop") self.log.debug("Now recreating Telethon connection")
self.client._updates_handle = self.loop.create_task(self.client._update_loop()) await self.stop()
await self.start()
@abstractmethod @abstractmethod
async def update(self, update: TypeUpdate) -> bool: async def update(self, update: TypeUpdate) -> bool:
@@ -320,7 +326,7 @@ class AbstractUser(ABC):
async def _update(self, update: TypeUpdate) -> None: async def _update(self, update: TypeUpdate) -> None:
if isinstance(update, UpdateShort): if isinstance(update, UpdateShort):
update = update.update update = update.update
asyncio.create_task(self._handle_entity_updates(getattr(update, "_entities", {}))) background_task.create(self._handle_entity_updates(getattr(update, "_entities", {})))
if isinstance( if isinstance(
update, update,
( (
@@ -351,7 +357,7 @@ class AbstractUser(ABC):
await self.update_default_banned_rights(update) await self.update_default_banned_rights(update)
elif isinstance(update, (UpdatePinnedMessages, UpdatePinnedChannelMessages)): elif isinstance(update, (UpdatePinnedMessages, UpdatePinnedChannelMessages)):
await self.update_pinned_messages(update) await self.update_pinned_messages(update)
elif isinstance(update, (UpdateUserName, UpdateUserPhoto)): elif isinstance(update, (UpdateUserName, UpdateUser)):
await self.update_others_info(update) await self.update_others_info(update)
elif isinstance(update, UpdateReadHistoryOutbox): elif isinstance(update, UpdateReadHistoryOutbox):
await self.update_read_receipt(update) await self.update_read_receipt(update)
@@ -500,18 +506,23 @@ class AbstractUser(ABC):
except Exception: except Exception:
self.log.exception("Failed to handle entity updates") self.log.exception("Failed to handle entity updates")
async def update_others_info(self, update: UpdateUserName | UpdateUserPhoto) -> None: async def update_others_info(self, update: UpdateUserName | UpdateUser) -> None:
# TODO duplication not checked # TODO duplication not checked
puppet = await pu.Puppet.get_by_tgid(TelegramID(update.user_id)) puppet = await pu.Puppet.get_by_tgid(TelegramID(update.user_id))
if isinstance(update, UpdateUserName): if isinstance(update, UpdateUserName):
puppet.username = update.username if len(update.usernames) > 1:
self.log.warning(
"Got update with multiple usernames (%s) for %s, only saving first one",
update.usernames,
update.user_id,
)
puppet.username = update.usernames[0].username if update.usernames else None
if await puppet.update_displayname(self, update): if await puppet.update_displayname(self, update):
await puppet.save() await puppet.save()
await puppet.update_portals_meta() await puppet.update_portals_meta()
elif isinstance(update, UpdateUserPhoto): elif isinstance(update, UpdateUser):
if await puppet.update_avatar(self, update.photo): info = await self.client.get_entity(puppet.peer)
await puppet.save() await puppet.update_info(self, info)
await puppet.update_portals_meta()
else: else:
self.log.warning(f"Unexpected other user info update: {type(update)}") self.log.warning(f"Unexpected other user info update: {type(update)}")
@@ -615,24 +626,28 @@ class AbstractUser(ABC):
await portal.delete_telegram_user(self.tgid, sender=None) await portal.delete_telegram_user(self.tgid, sender=None)
elif chan := getattr(update, "mau_channel", None): elif chan := getattr(update, "mau_channel", None):
if not portal.mxid: if not portal.mxid:
asyncio.create_task(self._delayed_create_channel(chan)) background_task.create(self._delayed_create_channel(chan))
else: else:
self.log.debug("Updating channel info with data fetched by Telethon") self.log.debug("Updating channel info with data fetched by Telethon")
await portal.update_info(self, chan) await portal.update_info(self, chan)
await portal.invite_to_matrix(self.mxid) await portal.invite_to_matrix(self.mxid)
async def _delayed_create_channel(self, chan: Channel) -> None: async def _delayed_create_channel(self, chan: Channel) -> None:
self.log.debug("Waiting 5 seconds before handling UpdateChannel for non-existent portal") self.log.debug(
f"Waiting 5 seconds before handling UpdateChannel for non-existent portal {chan.id}"
)
await asyncio.sleep(5) await asyncio.sleep(5)
portal = await po.Portal.get_by_tgid(TelegramID(chan.id)) portal = await po.Portal.get_by_tgid(TelegramID(chan.id))
if portal.mxid: if portal.mxid:
self.log.debug( self.log.debug(
"Portal started existing after waiting 5 seconds, dropping UpdateChannel" "Portal started existing after waiting 5 seconds, "
f"dropping UpdateChannel for {portal.tgid}"
) )
return return
else: else:
self.log.info( self.log.info(
"Creating Matrix room with data fetched by Telethon due to UpdateChannel" f"Creating Matrix room for {portal.tgid}"
" with data fetched by Telethon due to UpdateChannel"
) )
await portal.create_matrix_room(self, chan, invites=[self.mxid]) await portal.create_matrix_room(self, chan, invites=[self.mxid])
@@ -681,7 +696,7 @@ class AbstractUser(ABC):
await self.unregister_portal(update.action.chat_id, update.action.chat_id) await self.unregister_portal(update.action.chat_id, update.action.chat_id)
await self.register_portal(portal) await self.register_portal(portal)
return return
self.log.trace( self.log.debug(
"Handling action %s to %s by %d", "Handling action %s to %s by %d",
update.action, update.action,
portal.tgid_log, portal.tgid_log,
+10 -1
View File
@@ -19,7 +19,12 @@ from typing import TYPE_CHECKING, Awaitable, Callable, Literal
import logging import logging
import time import time
from telethon.errors import ChannelInvalidError, ChannelPrivateError from telethon.errors import (
AuthKeyError,
ChannelInvalidError,
ChannelPrivateError,
UnauthorizedError,
)
from telethon.tl.functions.channels import GetChannelsRequest, GetParticipantRequest from telethon.tl.functions.channels import GetChannelsRequest, GetParticipantRequest
from telethon.tl.functions.messages import GetChatsRequest, GetFullChatRequest from telethon.tl.functions.messages import GetChatsRequest, GetFullChatRequest
from telethon.tl.patched import Message, MessageService from telethon.tl.patched import Message, MessageService
@@ -145,6 +150,10 @@ class Bot(AbstractUser):
await self.post_login() await self.post_login()
return self return self
async def on_signed_out(self, err: UnauthorizedError | AuthKeyError) -> None:
self.log.fatal("Relay bot got signed out, crashing bridge", exc_info=err)
self.bridge.manual_stop(51)
async def post_login(self) -> None: async def post_login(self) -> None:
await self.init_permissions() await self.init_permissions()
info = await self.client.get_me() info = await self.client.get_me()
+3 -2
View File
@@ -21,6 +21,7 @@ import asyncio
from telethon.tl.types import ChannelForbidden, ChatForbidden from telethon.tl.types import ChannelForbidden, ChatForbidden
from mautrix.types import EventID, RoomID from mautrix.types import EventID, RoomID
from mautrix.util import background_task
from ... import portal as po from ... import portal as po
from ...types import TelegramID from ...types import TelegramID
@@ -184,7 +185,7 @@ async def confirm_bridge(evt: CommandEvent) -> EventID | None:
if not ok: if not ok:
return None return None
elif coro: elif coro:
asyncio.create_task(coro) background_task.create(coro)
await evt.reply("Cleaning up previous portal room...") await evt.reply("Cleaning up previous portal room...")
elif portal.mxid: elif portal.mxid:
evt.sender.command_status = None evt.sender.command_status = None
@@ -251,7 +252,7 @@ async def _locked_confirm_bridge(
await portal.save() await portal.save()
await portal.update_bridge_info() await portal.update_bridge_info()
asyncio.create_task(portal.update_matrix_room(user, entity, levels=levels)) background_task.create(portal.update_matrix_room(user, entity, levels=levels))
await warn_missing_power(levels, evt) await warn_missing_power(levels, evt)
+3 -69
View File
@@ -22,7 +22,6 @@ import io
from telethon.errors import ( from telethon.errors import (
AccessTokenExpiredError, AccessTokenExpiredError,
AccessTokenInvalidError, AccessTokenInvalidError,
FirstNameInvalidError,
FloodWaitError, FloodWaitError,
PasswordHashInvalidError, PasswordHashInvalidError,
PhoneCodeExpiredError, PhoneCodeExpiredError,
@@ -31,14 +30,12 @@ from telethon.errors import (
PhoneNumberBannedError, PhoneNumberBannedError,
PhoneNumberFloodError, PhoneNumberFloodError,
PhoneNumberInvalidError, PhoneNumberInvalidError,
PhoneNumberOccupiedError,
PhoneNumberUnoccupiedError, PhoneNumberUnoccupiedError,
SessionPasswordNeededError, SessionPasswordNeededError,
) )
from telethon.tl.types import User from telethon.tl.types import User
from mautrix.client import Client from mautrix.client import Client
from mautrix.errors import MForbidden
from mautrix.types import ( from mautrix.types import (
EventID, EventID,
ImageInfo, ImageInfo,
@@ -47,6 +44,7 @@ from mautrix.types import (
TextMessageEventContent, TextMessageEventContent,
UserID, UserID,
) )
from mautrix.util import background_task
from mautrix.util.format_duration import format_duration as fmt_duration from mautrix.util.format_duration import format_duration as fmt_duration
from ... import user as u from ... import user as u
@@ -94,70 +92,6 @@ async def ping_bot(evt: CommandEvent) -> EventID:
) )
@command_handler(
needs_auth=False,
management_only=True,
help_section=SECTION_AUTH,
help_args="<_phone_> <_full name_>",
help_text="Register to Telegram",
)
async def register(evt: CommandEvent) -> EventID:
if await evt.sender.is_logged_in():
return await evt.reply("You are already logged in.")
elif len(evt.args) < 1:
return await evt.reply("**Usage:** `$cmdprefix+sp register <phone> <full name>`")
phone_number = evt.args[0]
if len(evt.args) == 2:
full_name = evt.args[1], ""
else:
full_name = " ".join(evt.args[1:-1]), evt.args[-1]
await _request_code(
evt,
phone_number,
{
"next": enter_code_register,
"action": "Register",
"full_name": full_name,
},
)
return await evt.reply(
"By signing up for Telegram, you agree to "
"the terms of service: https://telegram.org/tos"
)
async def enter_code_register(evt: CommandEvent) -> EventID:
if len(evt.args) == 0:
return await evt.reply("**Usage:** `$cmdprefix+sp <code>`")
try:
await evt.sender.ensure_started(even_if_no_session=True)
first_name, last_name = evt.sender.command_status["full_name"]
user = await evt.sender.client.sign_up(evt.args[0], first_name, last_name)
asyncio.create_task(evt.sender.post_login(user, first_login=True))
evt.sender.command_status = None
return await evt.reply(f"Successfully registered to Telegram.")
except PhoneNumberOccupiedError:
return await evt.reply(
"That phone number has already been registered. "
"You can log in with `$cmdprefix+sp login`."
)
except FirstNameInvalidError:
return await evt.reply("Invalid name. Please set a Matrix displayname before registering.")
except PhoneCodeExpiredError:
return await evt.reply(
"Phone code expired. Try again with `$cmdprefix+sp register <phone>`."
)
except PhoneCodeInvalidError:
return await evt.reply("Invalid phone code.")
except Exception:
evt.log.exception("Error sending phone code")
return await evt.reply(
"Unhandled exception while sending code. Check console for more details."
)
@command_handler( @command_handler(
needs_auth=False, needs_auth=False,
management_only=True, management_only=True,
@@ -317,7 +251,7 @@ async def _request_code(
except PhoneNumberUnoccupiedError: except PhoneNumberUnoccupiedError:
return await evt.reply( return await evt.reply(
"That phone number has not been registered. " "That phone number has not been registered. "
"Please register with `$cmdprefix+sp register <phone>`." "Please sign up to Telegram using an official mobile client first."
) )
except PhoneNumberInvalidError: except PhoneNumberInvalidError:
return await evt.reply("That phone number is not valid.") return await evt.reply("That phone number is not valid.")
@@ -432,7 +366,7 @@ async def _finish_sign_in(evt: CommandEvent, user: User, login_as: u.User = None
f"[{existing_user.displayname}] (https://matrix.to/#/{existing_user.mxid})" f"[{existing_user.displayname}] (https://matrix.to/#/{existing_user.mxid})"
" was logged out from the account." " was logged out from the account."
) )
asyncio.create_task(login_as.post_login(user, first_login=True)) background_task.create(login_as.post_login(user, first_login=True))
evt.sender.command_status = None evt.sender.command_status = None
name = f"@{user.username}" if user.username else f"+{user.phone}" name = f"@{user.username}" if user.username else f"+{user.phone}"
if login_as != evt.sender: if login_as != evt.sender:
+7 -6
View File
@@ -134,15 +134,16 @@ async def search(evt: CommandEvent) -> EventID:
@command_handler( @command_handler(
help_section=SECTION_CREATING_PORTALS, help_section=SECTION_CREATING_PORTALS,
help_args="<_identifier_>", help_args="<_username_>",
help_text="Open a private chat with the given Telegram user. The identifier is " help_text=(
"either the internal user ID, the username or the phone number. " "Open a private chat with the given Telegram user. You can also use a "
"**N.B.** The phone numbers you start chats with must already be in " "phone number instead of username, but you must have the number in "
"your contacts.", "your Telegram contacts for that to work."
),
) )
async def pm(evt: CommandEvent) -> EventID: async def pm(evt: CommandEvent) -> EventID:
if len(evt.args) == 0: if len(evt.args) == 0:
return await evt.reply("**Usage:** `$cmdprefix+sp pm <user identifier>`") return await evt.reply("**Usage:** `$cmdprefix+sp pm <username>`")
try: try:
id = "".join(evt.args).translate({ord(c): None for c in "+()- "}) id = "".join(evt.args).translate({ord(c): None for c in "+()- "})
+2
View File
@@ -101,6 +101,7 @@ class Config(BaseBridgeConfig):
copy("bridge.displayname_preference") copy("bridge.displayname_preference")
copy("bridge.displayname_max_length") copy("bridge.displayname_max_length")
copy("bridge.allow_avatar_remove") copy("bridge.allow_avatar_remove")
copy("bridge.allow_contact_info")
copy("bridge.max_initial_member_sync") copy("bridge.max_initial_member_sync")
copy("bridge.max_member_count") copy("bridge.max_member_count")
@@ -150,6 +151,7 @@ class Config(BaseBridgeConfig):
copy("bridge.private_chat_portal_meta") copy("bridge.private_chat_portal_meta")
copy("bridge.delivery_receipts") copy("bridge.delivery_receipts")
copy("bridge.delivery_error_reports") copy("bridge.delivery_error_reports")
copy("bridge.incoming_bridge_error_reports")
copy("bridge.message_status_events") copy("bridge.message_status_events")
copy("bridge.resend_bridge_info") copy("bridge.resend_bridge_info")
copy("bridge.mute_bridging") copy("bridge.mute_bridging")
+4 -4
View File
@@ -24,7 +24,7 @@ from asyncpg import Record
from attr import dataclass from attr import dataclass
from mautrix.types import UserID from mautrix.types import UserID
from mautrix.util.async_db import Database from mautrix.util.async_db import Connection, Database
from ..types import TelegramID from ..types import TelegramID
@@ -169,8 +169,8 @@ class Backfill:
) )
@classmethod @classmethod
async def delete_all(cls, user_mxid: UserID) -> None: async def delete_all(cls, user_mxid: UserID, conn: Connection | None = None) -> None:
await cls.db.execute("DELETE FROM backfill_queue WHERE user_mxid=$1", user_mxid) await (conn or cls.db).execute("DELETE FROM backfill_queue WHERE user_mxid=$1", user_mxid)
@classmethod @classmethod
async def delete_for_portal(cls, tgid: int, tg_receiver: int) -> None: async def delete_for_portal(cls, tgid: int, tg_receiver: int) -> None:
@@ -186,7 +186,7 @@ class Backfill:
AND type=$4 AND type=$4
AND dispatch_time IS NULL AND dispatch_time IS NULL
AND completed_at IS NULL AND completed_at IS NULL
RETURNING {self.columns_str} RETURNING queue_id, {self.columns_str}
""" """
q = f""" q = f"""
INSERT INTO backfill_queue ({self.columns_str}) INSERT INTO backfill_queue ({self.columns_str})
+4 -1
View File
@@ -167,7 +167,10 @@ class Portal:
"UPDATE portal SET tgid=$1, tg_receiver=$1, peer_type=$2 " "UPDATE portal SET tgid=$1, tg_receiver=$1, peer_type=$2 "
"WHERE tgid=$3 AND tg_receiver=$3" "WHERE tgid=$3 AND tg_receiver=$3"
) )
await self.db.execute(q, id, peer_type, self.tgid) clear_queue = "DELETE FROM backfill_queue WHERE portal_tgid=$1 AND portal_tg_receiver=$2"
async with self.db.acquire() as conn, conn.transaction():
await conn.execute(clear_queue, self.tgid, self.tg_receiver)
await conn.execute(q, id, peer_type, self.tgid)
self.tgid = id self.tgid = id
self.tg_receiver = id self.tg_receiver = id
self.peer_type = peer_type self.peer_type = peer_type
+4 -3
View File
@@ -92,9 +92,9 @@ class TelegramFile:
async def insert(self) -> None: async def insert(self) -> None:
q = ( q = (
"INSERT INTO telegram_file (id, mxc, mime_type, was_converted, size, width, height, " "INSERT INTO telegram_file (id, mxc, mime_type, was_converted, timestamp,"
" thumbnail, decryption_info) " " size, width, height, thumbnail, decryption_info) "
"VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)" "VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)"
) )
await self.db.execute( await self.db.execute(
q, q,
@@ -102,6 +102,7 @@ class TelegramFile:
self.mxc, self.mxc,
self.mime_type, self.mime_type,
self.was_converted, self.was_converted,
self.timestamp,
self.size, self.size,
self.width, self.width,
self.height, self.height,
+43 -7
View File
@@ -123,19 +123,55 @@ class PgSession(MemorySession):
date = datetime.datetime.utcfromtimestamp(row["date"]) date = datetime.datetime.utcfromtimestamp(row["date"])
return updates.State(row["pts"], row["qts"], date, row["seq"], row["unread_count"]) return updates.State(row["pts"], row["qts"], date, row["seq"], row["unread_count"])
_set_update_state_q = """
INSERT INTO telethon_update_state (session_id, entity_id, pts, qts, date, seq, unread_count)
VALUES ($1, $2, $3, $4, $5, $6, $7)
ON CONFLICT (session_id, entity_id) DO UPDATE SET
pts=excluded.pts, qts=excluded.qts, date=excluded.date, seq=excluded.seq,
unread_count=excluded.unread_count
"""
async def set_update_state(self, entity_id: int, row: updates.State) -> None: async def set_update_state(self, entity_id: int, row: updates.State) -> None:
q = """ q = self._set_update_state_q
INSERT INTO telethon_update_state(session_id, entity_id, pts, qts, date, seq, unread_count)
VALUES ($1, $2, $3, $4, $5, $6, $7)
ON CONFLICT (session_id, entity_id) DO UPDATE SET
pts=excluded.pts, qts=excluded.qts, date=excluded.date, seq=excluded.seq,
unread_count=excluded.unread_count
"""
ts = row.date.timestamp() ts = row.date.timestamp()
await self.db.execute( await self.db.execute(
q, self.session_id, entity_id, row.pts, row.qts, ts, row.seq, row.unread_count q, self.session_id, entity_id, row.pts, row.qts, ts, row.seq, row.unread_count
) )
async def set_update_states(self, rows: list[tuple[int, updates.State]]) -> None:
rows = [
(
self.session_id,
entity_id,
row.pts,
row.qts,
row.date.timestamp(),
row.seq,
row.unread_count,
)
for entity_id, row in rows
]
if self.db.scheme == Scheme.POSTGRES:
q = """
INSERT INTO telethon_update_state (
session_id, entity_id, pts, qts, date, seq, unread_count
)
VALUES (
$1,
unnest($2::bigint[]), unnest($3::bigint[]), unnest($4::bigint[]),
unnest($5::bigint[]), unnest($6::bigint[]), unnest($7::integer[])
)
ON CONFLICT (session_id, entity_id) DO UPDATE SET
pts=excluded.pts, qts=excluded.qts, date=excluded.date, seq=excluded.seq,
unread_count=excluded.unread_count
"""
_, entity_ids, ptses, qtses, timestamps, seqs, unread_counts = zip(*rows)
await self.db.execute(
q, self.session_id, entity_ids, ptses, qtses, timestamps, seqs, unread_counts
)
else:
await self.db.executemany(self._set_update_state_q, rows)
async def delete_update_state(self, entity_id: int) -> None: async def delete_update_state(self, entity_id: int) -> None:
q = "DELETE FROM telethon_update_state WHERE session_id=$1 AND entity_id=$2" q = "DELETE FROM telethon_update_state WHERE session_id=$1 AND entity_id=$2"
await self.db.execute(q, self.session_id, entity_id) await self.db.execute(q, self.session_id, entity_id)
+18 -3
View File
@@ -21,9 +21,10 @@ from asyncpg import Record
from attr import dataclass from attr import dataclass
from mautrix.types import UserID from mautrix.types import UserID
from mautrix.util.async_db import Database, Scheme from mautrix.util.async_db import Connection, Database, Scheme
from ..types import TelegramID from ..types import TelegramID
from .backfill_queue import Backfill
fake_db = Database.create("") if TYPE_CHECKING else None fake_db = Database.create("") if TYPE_CHECKING else None
@@ -73,6 +74,20 @@ class User:
async def delete(self) -> None: async def delete(self) -> None:
await self.db.execute('DELETE FROM "user" WHERE mxid=$1', self.mxid) await self.db.execute('DELETE FROM "user" WHERE mxid=$1', self.mxid)
async def remove_tgid(self) -> None:
async with self.db.acquire() as conn, conn.transaction():
if self.tgid:
await conn.execute('DELETE FROM contact WHERE "user"=$1', self.tgid)
await conn.execute('DELETE FROM user_portal WHERE "user"=$1', self.tgid)
await Backfill.delete_all(self.mxid, conn=conn)
self.tgid = None
self.tg_username = None
self.tg_phone = None
self.is_bot = False
self.is_premium = False
self.saved_contacts = 0
await self.save(conn=conn)
@property @property
def _values(self): def _values(self):
return ( return (
@@ -85,13 +100,13 @@ class User:
self.saved_contacts, self.saved_contacts,
) )
async def save(self) -> None: async def save(self, conn: Connection | None = None) -> None:
q = """ q = """
UPDATE "user" SET tgid=$2, tg_username=$3, tg_phone=$4, is_bot=$5, is_premium=$6, UPDATE "user" SET tgid=$2, tg_username=$3, tg_phone=$4, is_bot=$5, is_premium=$6,
saved_contacts=$7 saved_contacts=$7
WHERE mxid=$1 WHERE mxid=$1
""" """
await self.db.execute(q, *self._values) await (conn or self.db).execute(q, *self._values)
async def insert(self) -> None: async def insert(self) -> None:
q = """ q = """
+5
View File
@@ -145,6 +145,9 @@ bridge:
# as there's no way to determine whether an avatar is removed or just hidden from some users. If # as there's no way to determine whether an avatar is removed or just hidden from some users. If
# you're on a single-user instance, this should be safe to enable. # you're on a single-user instance, this should be safe to enable.
allow_avatar_remove: false allow_avatar_remove: false
# Should contact names and profile pictures be allowed?
# This is only safe to enable on single-user instances.
allow_contact_info: false
# Maximum number of members to sync per portal when starting up. Other members will be # Maximum number of members to sync per portal when starting up. Other members will be
# synced when they send messages. The maximum is 10000, after which the Telegram server # synced when they send messages. The maximum is 10000, after which the Telegram server
@@ -314,6 +317,8 @@ bridge:
delivery_receipts: false delivery_receipts: false
# Whether or not delivery errors should be reported as messages in the Matrix room. # Whether or not delivery errors should be reported as messages in the Matrix room.
delivery_error_reports: false delivery_error_reports: false
# Should errors in incoming message handling send a message to the Matrix room?
incoming_bridge_error_reports: false
# Whether the bridge should send the message status as a custom com.beeper.message_send_status event. # Whether the bridge should send the message status as a custom com.beeper.message_send_status event.
message_status_events: false message_status_events: false
# Set this to true to tell the bridge to re-send m.bridge events to all rooms on the next run. # Set this to true to tell the bridge to re-send m.bridge events to all rooms on the next run.
+60 -23
View File
@@ -171,7 +171,7 @@ from mautrix.types import (
UserID, UserID,
VideoInfo, VideoInfo,
) )
from mautrix.util import magic, variation_selector from mautrix.util import background_task, magic, variation_selector
from mautrix.util.message_send_checkpoint import MessageSendCheckpointStatus from mautrix.util.message_send_checkpoint import MessageSendCheckpointStatus
from mautrix.util.simple_lock import SimpleLock from mautrix.util.simple_lock import SimpleLock
from mautrix.util.simple_template import SimpleTemplate from mautrix.util.simple_template import SimpleTemplate
@@ -727,7 +727,7 @@ class Portal(DBPortal, BasePortal):
self.log.exception(f"Failed to get entity through {user.tgid} for update") self.log.exception(f"Failed to get entity through {user.tgid} for update")
return self.mxid return self.mxid
update = self.update_matrix_room(user, entity) update = self.update_matrix_room(user, entity)
asyncio.create_task(update) background_task.create(update)
await self.invite_to_matrix(invites or []) await self.invite_to_matrix(invites or [])
return self.mxid return self.mxid
async with self._room_create_lock: async with self._room_create_lock:
@@ -805,6 +805,12 @@ class Portal(DBPortal, BasePortal):
participants_count = 2 participants_count = 2
if isinstance(entity, Chat): if isinstance(entity, Chat):
participants_count = entity.participants_count participants_count = entity.participants_count
if entity.deactivated or entity.migrated_to:
self.log.error(
"Throwing error for attempted portal creation "
f"({entity.deactivated=}, {entity.migrated_to=})"
)
raise RuntimeError("Tried to create portal for deactivated chat")
elif isinstance(entity, Channel) and not entity.broadcast: elif isinstance(entity, Channel) and not entity.broadcast:
participants_count = entity.participants_count participants_count = entity.participants_count
if participants_count is None and self.config["bridge.max_member_count"] > 0: if participants_count is None and self.config["bridge.max_member_count"] > 0:
@@ -1525,11 +1531,11 @@ class Portal(DBPortal, BasePortal):
) )
if self.peer_type == "channel": if self.peer_type == "channel":
if not self.megagroup: if not self.megagroup:
asyncio.create_task( background_task.create(
self._try_handle_read_for_sponsored_msg(user, event_id, timestamp) self._try_handle_read_for_sponsored_msg(user, event_id, timestamp)
) )
else: else:
asyncio.create_task(self._poll_telegram_reactions(user)) background_task.create(self._poll_telegram_reactions(user))
async def _preproc_kick_ban( async def _preproc_kick_ban(
self, user: u.User | p.Puppet, source: u.User self, user: u.User | p.Puppet, source: u.User
@@ -1964,7 +1970,7 @@ class Portal(DBPortal, BasePortal):
message_type=msgtype, message_type=msgtype,
) )
await self._send_delivery_receipt(event_id) await self._send_delivery_receipt(event_id)
asyncio.create_task(self._send_message_status(event_id, err=None)) background_task.create(self._send_message_status(event_id, err=None))
if response.ttl_period: if response.ttl_period:
await self._mark_disappearing( await self._mark_disappearing(
event_id=event_id, event_id=event_id,
@@ -1993,7 +1999,6 @@ class Portal(DBPortal, BasePortal):
status.status = MessageStatus.RETRIABLE status.status = MessageStatus.RETRIABLE
else: else:
status.status = MessageStatus.SUCCESS status.status = MessageStatus.SUCCESS
status.fill_legacy_booleans()
await intent.send_message_event( await intent.send_message_event(
room_id=self.mxid, room_id=self.mxid,
@@ -2258,7 +2263,7 @@ class Portal(DBPortal, BasePortal):
EventType.ROOM_REDACTION, EventType.ROOM_REDACTION,
) )
await self._send_delivery_receipt(redaction_event_id) await self._send_delivery_receipt(redaction_event_id)
asyncio.create_task(self._send_message_status(redaction_event_id, err=None)) background_task.create(self._send_message_status(redaction_event_id, err=None))
async def _handle_matrix_reaction_deletion( async def _handle_matrix_reaction_deletion(
self, deleter: u.User, event_id: EventID, tg_space: TelegramID self, deleter: u.User, event_id: EventID, tg_space: TelegramID
@@ -2371,7 +2376,7 @@ class Portal(DBPortal, BasePortal):
EventType.REACTION, EventType.REACTION,
) )
await self._send_delivery_receipt(reaction_event_id) await self._send_delivery_receipt(reaction_event_id)
asyncio.create_task(self._send_message_status(reaction_event_id, err=None)) background_task.create(self._send_message_status(reaction_event_id, err=None))
async def _handle_matrix_reaction( async def _handle_matrix_reaction(
self, self,
@@ -2574,7 +2579,7 @@ class Portal(DBPortal, BasePortal):
# Ignore typing notifications from double puppeted users to avoid echoing # Ignore typing notifications from double puppeted users to avoid echoing
return return
is_typing = isinstance(update.action, SendMessageTypingAction) is_typing = isinstance(update.action, SendMessageTypingAction)
await user.default_mxid_intent.set_typing(self.mxid, is_typing=is_typing) await user.default_mxid_intent.set_typing(self.mxid, timeout=5000 if is_typing else 0)
async def handle_telegram_edit( async def handle_telegram_edit(
self, source: au.AbstractUser, sender: p.Puppet | None, evt: Message self, source: au.AbstractUser, sender: p.Puppet | None, evt: Message
@@ -2587,7 +2592,7 @@ class Portal(DBPortal, BasePortal):
return return
if self.peer_type != "channel" and isinstance(evt, Message) and evt.reactions is not None: if self.peer_type != "channel" and isinstance(evt, Message) and evt.reactions is not None:
asyncio.create_task( background_task.create(
self.try_handle_telegram_reactions(source, TelegramID(evt.id), evt.reactions) self.try_handle_telegram_reactions(source, TelegramID(evt.id), evt.reactions)
) )
sender_id = sender.tgid if sender else self.tgid sender_id = sender.tgid if sender else self.tgid
@@ -2648,7 +2653,7 @@ class Portal(DBPortal, BasePortal):
source, intent, is_bot, evt, no_reply_fallback=True source, intent, is_bot, evt, no_reply_fallback=True
) )
converted.content.set_edit(editing_msg.mxid) converted.content.set_edit(editing_msg.mxid)
await intent.set_typing(self.mxid, is_typing=False) await intent.set_typing(self.mxid, timeout=0)
timestamp = evt.edit_date if evt.edit_date != evt.date else None timestamp = evt.edit_date if evt.edit_date != evt.date else None
event_id = await self._send_message( event_id = await self._send_message(
intent, converted.content, timestamp=timestamp, event_type=converted.type intent, converted.content, timestamp=timestamp, event_type=converted.type
@@ -2805,8 +2810,11 @@ class Portal(DBPortal, BasePortal):
elif not insertion_id: elif not insertion_id:
insertion_id = self.base_insertion_id insertion_id = self.base_insertion_id
await self.save() await self.save()
# TODO this should probably check actual event count instead of message count if (
if event_count > 0 and self.backfill_msc2716: event_count > 0
and self.backfill_msc2716
and (not forward or not self.bridge.homeserver_software.is_hungry)
):
await self.main_intent.send_state_event( await self.main_intent.send_state_event(
self.mxid, self.mxid,
StateMarker, StateMarker,
@@ -3264,9 +3272,29 @@ class Portal(DBPortal, BasePortal):
async def handle_telegram_message( async def handle_telegram_message(
self, source: au.AbstractUser, sender: p.Puppet | None, evt: Message self, source: au.AbstractUser, sender: p.Puppet | None, evt: Message
) -> None:
try:
await self._handle_telegram_message(source, sender, evt)
except Exception:
sender_id = sender.tgid if sender else None
self.log.exception(
f"Failed to handle Telegram message {evt.id} from {sender_id} via {source.tgid}"
)
if self.config["bridge.incoming_bridge_error_reports"]:
intent = sender.intent_for(self) if sender else self.main_intent
await self._send_message(
intent,
TextMessageEventContent(
msgtype=MessageType.NOTICE,
body="Error processing message from Telegram",
),
)
async def _handle_telegram_message(
self, source: au.AbstractUser, sender: p.Puppet | None, evt: Message
) -> None: ) -> None:
if not self.mxid: if not self.mxid:
self.log.trace("Got telegram message %d, but no room exists, creating...", evt.id) self.log.debug("Got telegram message %d, but no room exists, creating...", evt.id)
await self.create_matrix_room(source, invites=[source.mxid], update_if_exists=False) await self.create_matrix_room(source, invites=[source.mxid], update_if_exists=False)
if not self.mxid: if not self.mxid:
self.log.warning("Room doesn't exist even after creating, dropping %d", evt.id) self.log.warning("Room doesn't exist even after creating, dropping %d", evt.id)
@@ -3333,12 +3361,17 @@ class Portal(DBPortal, BasePortal):
f"Telegram user {sender.tgid} sent a message, but doesn't have a displayname," f"Telegram user {sender.tgid} sent a message, but doesn't have a displayname,"
" updating info..." " updating info..."
) )
entity = await source.client.get_entity(sender.peer) try:
await sender.update_info(source, entity) entity = await source.client.get_entity(sender.peer)
if not sender.displayname: await sender.update_info(source, entity)
self.log.debug( if not sender.displayname:
f"Telegram user {sender.tgid} doesn't have a displayname even after" self.log.debug(
f" updating with data {entity!s}" f"Telegram user {sender.tgid} doesn't have a displayname even after"
f" updating with data {entity!s}"
)
except ValueError as e:
self.log.warning(
f"Couldn't find entity to update profile of {sender.tgid}", exc_info=True
) )
if sender: if sender:
@@ -3350,7 +3383,7 @@ class Portal(DBPortal, BasePortal):
converted = await self._msg_conv.convert(source, intent, is_bot, evt) converted = await self._msg_conv.convert(source, intent, is_bot, evt)
if not converted: if not converted:
return return
await intent.set_typing(self.mxid, is_typing=False) await intent.set_typing(self.mxid, timeout=0)
event_id = await self._send_message( event_id = await self._send_message(
intent, converted.content, timestamp=evt.date, event_type=converted.type intent, converted.content, timestamp=evt.date, event_type=converted.type
) )
@@ -3397,7 +3430,7 @@ class Portal(DBPortal, BasePortal):
await intent.redact(self.mxid, event_id) await intent.redact(self.mxid, event_id)
return return
if isinstance(evt, Message) and evt.reactions: if isinstance(evt, Message) and evt.reactions:
asyncio.create_task( background_task.create(
self.try_handle_telegram_reactions( self.try_handle_telegram_reactions(
source, dbm.tgid, evt.reactions, dbm=dbm, timestamp=evt.date source, dbm.tgid, evt.reactions, dbm=dbm, timestamp=evt.date
) )
@@ -3418,7 +3451,7 @@ class Portal(DBPortal, BasePortal):
dm = DisappearingMessage(self.mxid, event_id, seconds, expiration_ts=expires_at * 1000) dm = DisappearingMessage(self.mxid, event_id, seconds, expiration_ts=expires_at * 1000)
await dm.insert() await dm.insert()
if expires_at: if expires_at:
asyncio.create_task(self._disappear_event(dm)) background_task.create(self._disappear_event(dm))
async def _create_room_on_action( async def _create_room_on_action(
self, source: au.AbstractUser, action: TypeMessageAction self, source: au.AbstractUser, action: TypeMessageAction
@@ -3432,6 +3465,10 @@ class Portal(DBPortal, BasePortal):
MessageActionChatJoinedByRequest, MessageActionChatJoinedByRequest,
) )
if isinstance(action, create_and_exit) or isinstance(action, create_and_continue): if isinstance(action, create_and_exit) or isinstance(action, create_and_continue):
self.log.debug(
f"Got telegram action of type {type(action).__name__},"
" but no room exists, creating..."
)
await self.create_matrix_room( await self.create_matrix_room(
source, invites=[source.mxid], update_if_exists=isinstance(action, create_and_exit) source, invites=[source.mxid], update_if_exists=isinstance(action, create_and_exit)
) )
@@ -409,6 +409,8 @@ class TelegramMessageConverter:
mimetype=file.mime_type, mimetype=file.mime_type,
size=self._photo_size_key(largest_size), size=self._photo_size_key(largest_size),
) )
if media.spoiler:
info["fi.mau.telegram.spoiler"] = True
ext = sane_mimetypes.guess_extension(file.mime_type) ext = sane_mimetypes.guess_extension(file.mime_type)
name = f"disappearing_image{ext}" if media.ttl_seconds else f"image{ext}" name = f"disappearing_image{ext}" if media.ttl_seconds else f"image{ext}"
content = MediaMessageEventContent( content = MediaMessageEventContent(
@@ -498,8 +500,10 @@ class TelegramMessageConverter:
info["fi.mau.autoplay"] = True info["fi.mau.autoplay"] = True
info["fi.mau.hide_controls"] = True info["fi.mau.hide_controls"] = True
info["fi.mau.no_audio"] = True info["fi.mau.no_audio"] = True
if evt.media.spoiler:
info["fi.mau.telegram.spoiler"] = True
if not name: if not name:
ext = sane_mimetypes.guess_extension(file.mime_type) ext = sane_mimetypes.guess_extension(file.mime_type) or ""
name = "unnamed_file" + ext name = "unnamed_file" + ext
content = MediaMessageEventContent( content = MediaMessageEventContent(
+23 -9
View File
@@ -334,13 +334,15 @@ class Puppet(DBPuppet, BasePuppet):
if isinstance(info, UpdateUserName): if isinstance(info, UpdateUserName):
info = await (client_override or source.client).get_entity(self.peer) info = await (client_override or source.client).get_entity(self.peer)
if isinstance(info, Channel) or not info.contact: is_contact_name = not isinstance(info, Channel) and info.contact
self.displayname_contact = False # Reject name change if the contact status is moving in an unwanted direction,
elif not self.displayname_contact: # and we already have a name for the ghost.
if not self.displayname: if (
self.displayname_contact = True is_contact_name != self.displayname_contact
else: and is_contact_name != self.config["bridge.allow_contact_info"]
return False and self.displayname
):
return False
displayname, quality = self.get_displayname(info) displayname, quality = self.get_displayname(info)
needs_reset = displayname != self.displayname or not self.name_set needs_reset = displayname != self.displayname or not self.name_set
@@ -348,12 +350,14 @@ class Puppet(DBPuppet, BasePuppet):
if needs_reset and is_high_quality: if needs_reset and is_high_quality:
allow_because = f"{allow_because} and quality {quality} >= {self.displayname_quality}" allow_because = f"{allow_because} and quality {quality} >= {self.displayname_quality}"
self.log.debug( self.log.debug(
f"Updating displayname of {self.id} (src: {source.tgid}, allowed " f"Updating displayname of {self.id} (src: {source.tgid}, "
f"because {allow_because}) from {self.displayname} to {displayname}" f"contact: {is_contact_name}, allowed because {allow_because}) "
f"from {self.displayname} to {displayname}"
) )
self.log.trace("Displayname source data: %s", info) self.log.trace("Displayname source data: %s", info)
self.displayname = displayname self.displayname = displayname
self.displayname_source = source.tgid self.displayname_source = source.tgid
self.displayname_contact = is_contact_name
self.displayname_quality = quality self.displayname_quality = quality
try: try:
await self.default_mxid_intent.set_displayname( await self.default_mxid_intent.set_displayname(
@@ -378,6 +382,16 @@ class Puppet(DBPuppet, BasePuppet):
) -> bool: ) -> bool:
if self.disable_updates: if self.disable_updates:
return False return False
if (
isinstance(photo, UserProfilePhoto)
and photo.personal
and not self.config["bridge.allow_contact_info"]
):
self.log.trace(
"Dropping user avatar as it's personal "
"and contact info is disabled in bridge config"
)
return False
if photo is None or isinstance(photo, (UserProfilePhotoEmpty, ChatPhotoEmpty)): if photo is None or isinstance(photo, (UserProfilePhotoEmpty, ChatPhotoEmpty)):
photo_id = "" photo_id = ""
+58 -29
View File
@@ -22,6 +22,7 @@ import time
from telethon.errors import ( from telethon.errors import (
AuthKeyDuplicatedError, AuthKeyDuplicatedError,
AuthKeyError,
RPCError, RPCError,
TakeoutInitDelayError, TakeoutInitDelayError,
UnauthorizedError, UnauthorizedError,
@@ -58,6 +59,7 @@ from mautrix.bridge import BaseUser, async_getter_lock
from mautrix.client import Client from mautrix.client import Client
from mautrix.errors import MatrixRequestError, MNotFound from mautrix.errors import MatrixRequestError, MNotFound
from mautrix.types import PushActionType, PushRuleKind, PushRuleScope, RoomID, RoomTagInfo, UserID from mautrix.types import PushActionType, PushRuleKind, PushRuleScope, RoomID, RoomTagInfo, UserID
from mautrix.util import background_task
from mautrix.util.bridge_state import BridgeState, BridgeStateEvent from mautrix.util.bridge_state import BridgeState, BridgeStateEvent
from mautrix.util.opt_prometheus import Gauge from mautrix.util.opt_prometheus import Gauge
@@ -207,17 +209,30 @@ class User(DBUser, AbstractUser, BaseUser):
async with self._ensure_started_lock: async with self._ensure_started_lock:
return cast(User, await super().ensure_started(even_if_no_session)) return cast(User, await super().ensure_started(even_if_no_session))
async def on_signed_out(self, err: UnauthorizedError | AuthKeyError) -> None:
error_code = "tg-auth-error"
if isinstance(err, AuthKeyDuplicatedError):
error_code = "tg-auth-key-duplicated"
message = None
else:
message = str(err)
self.log.warning(f"User got signed out with {err}, deleting data...")
try:
await self.log_out(
state=BridgeStateEvent.BAD_CREDENTIALS,
error=error_code,
message=message,
delete=False,
)
except Exception:
self.log.exception("Error handling external logout")
async def start(self, delete_unless_authenticated: bool = False) -> User: async def start(self, delete_unless_authenticated: bool = False) -> User:
try: try:
await super().start() await super().start()
except AuthKeyDuplicatedError: except AuthKeyDuplicatedError as e:
self.log.warning("Got AuthKeyDuplicatedError in start()") self.log.warning("Got AuthKeyDuplicatedError in start()")
await self.push_bridge_state( await self.on_signed_out(e)
BridgeStateEvent.BAD_CREDENTIALS, error="tg-auth-key-duplicated"
)
await self.client.disconnect()
await self.client.session.delete()
self.client = None
if not delete_unless_authenticated: if not delete_unless_authenticated:
# The caller wants the client to be connected, so restart the connection. # The caller wants the client to be connected, so restart the connection.
await super().start() await super().start()
@@ -237,12 +252,7 @@ class User(DBUser, AbstractUser, BaseUser):
if delete_unless_authenticated or self.tgid: if delete_unless_authenticated or self.tgid:
self.log.error(f"Authorization error in start(): {type(e)}: {e}") self.log.error(f"Authorization error in start(): {type(e)}: {e}")
if self.tgid: if self.tgid:
await self.push_bridge_state( await self.on_signed_out(e)
BridgeStateEvent.BAD_CREDENTIALS,
error="tg-auth-error",
message=str(e),
ttl=3600,
)
except RPCError as e: except RPCError as e:
self.log.error(f"Unknown RPC error in start(): {type(e)}: {e}") self.log.error(f"Unknown RPC error in start(): {type(e)}: {e}")
if self.tgid: if self.tgid:
@@ -250,10 +260,10 @@ class User(DBUser, AbstractUser, BaseUser):
else: else:
# Authenticated, run post login # Authenticated, run post login
self.log.debug(f"Ensuring post_login() for {self.name}") self.log.debug(f"Ensuring post_login() for {self.name}")
asyncio.create_task(self.post_login()) background_task.create(self.post_login())
return self return self
# Not authenticated, delete data if necessary # Not authenticated, delete data if necessary
if delete_unless_authenticated: if delete_unless_authenticated and self.client is not None:
self.log.debug(f"Unauthenticated user {self.name} start()ed, deleting session...") self.log.debug(f"Unauthenticated user {self.name} start()ed, deleting session...")
await self.client.disconnect() await self.client.disconnect()
await self.client.session.delete() await self.client.session.delete()
@@ -284,18 +294,18 @@ class User(DBUser, AbstractUser, BaseUser):
BridgeStateEvent.BACKFILLING BridgeStateEvent.BACKFILLING
if self._is_backfilling if self._is_backfilling
else BridgeStateEvent.CONNECTED, else BridgeStateEvent.CONNECTED,
ttl=3600,
info=self._bridge_state_info, info=self._bridge_state_info,
) )
else: else:
await self.push_bridge_state( await self.push_bridge_state(
BridgeStateEvent.TRANSIENT_DISCONNECT, ttl=240, error="tg-not-connected" BridgeStateEvent.TRANSIENT_DISCONNECT, error="tg-not-connected"
) )
async def fill_bridge_state(self, state: BridgeState) -> None: async def fill_bridge_state(self, state: BridgeState) -> None:
await super().fill_bridge_state(state) await super().fill_bridge_state(state)
state.remote_id = str(self.tgid) if self.tgid:
state.remote_name = self.human_tg_id state.remote_id = str(self.tgid)
state.remote_name = self.human_tg_id
async def get_bridge_states(self) -> list[BridgeState]: async def get_bridge_states(self) -> list[BridgeState]:
if not self.tgid: if not self.tgid:
@@ -567,27 +577,46 @@ class User(DBUser, AbstractUser, BaseUser):
except MatrixRequestError: except MatrixRequestError:
pass pass
async def log_out(self) -> bool: async def log_out(
self,
delete: bool = True,
do_logout: bool = True,
state: BridgeStateEvent = BridgeStateEvent.LOGGED_OUT,
error: str | None = None,
message: str | None = None,
) -> bool:
puppet = await pu.Puppet.get_by_tgid(self.tgid) puppet = await pu.Puppet.get_by_tgid(self.tgid)
if puppet.is_real_user: if puppet is not None and puppet.is_real_user:
await puppet.switch_mxid(None, None) await puppet.switch_mxid(None, None)
try: try:
await self.kick_from_portals() await self.kick_from_portals()
except Exception: except Exception:
self.log.exception("Failed to kick user from portals on logout") self.log.exception("Failed to kick user from portals on logout")
await self.push_bridge_state(BridgeStateEvent.LOGGED_OUT)
if self.tgid: if self.tgid:
try: try:
del self.by_tgid[self.tgid] del self.by_tgid[self.tgid]
except KeyError: except KeyError:
pass pass
self.tgid = None ok = False
ok = await self.client.log_out() if self.client is not None:
sess = self.client.session sess = self.client.session
await self.stop() # Try to send a logout request. If it succeeds, this also disconnects the client and
await sess.delete() # deletes the session, but we do those again later just to be safe.
await self.delete() if do_logout:
self.by_mxid.pop(self.mxid, None) ok = await self.client.log_out()
# Force-disconnect the client and set it to None
await self.stop()
await sess.delete()
# TODO send a management room notice for non-manual logouts?
await self.push_bridge_state(state, error=error, message=message)
if delete:
await self.delete()
self.by_mxid.pop(self.mxid, None)
self.log.info("User deleted")
else:
await self.remove_tgid()
self.log.info("User telegram ID cleared")
self._track_metric(METRIC_LOGGED_IN, False) self._track_metric(METRIC_LOGGED_IN, False)
return ok return ok
+44 -58
View File
@@ -22,7 +22,6 @@ import asyncio
import logging import logging
import pickle import pickle
import pkgutil import pkgutil
import tempfile
import time import time
from asyncpg import UniqueViolationError from asyncpg import UniqueViolationError
@@ -46,7 +45,7 @@ from telethon.tl.types import (
) )
from mautrix.appservice import IntentAPI from mautrix.appservice import IntentAPI
from mautrix.util import magic, variation_selector from mautrix.util import ffmpeg, magic, variation_selector
from .. import abstract_user as au from .. import abstract_user as au
from ..db import TelegramFile as DBTelegramFile from ..db import TelegramFile as DBTelegramFile
@@ -61,11 +60,6 @@ try:
except ImportError: except ImportError:
Image = None Image = None
try:
from moviepy.editor import VideoFileClip
except ImportError:
VideoFileClip = None
try: try:
from mautrix.crypto.attachments import encrypt_attachment from mautrix.crypto.attachments import encrypt_attachment
except ImportError: except ImportError:
@@ -103,29 +97,16 @@ def convert_image(
return source_mime, file, None, None return source_mime, file, None, None
def _read_video_thumbnail( async def _read_video_thumbnail(data: bytes, mime_type: str) -> tuple[bytes, int, int]:
data: bytes, first_frame = await ffmpeg.convert_bytes(
video_ext: str = "mp4", data,
frame_ext: str = "png", output_extension=".png",
max_size: tuple[int, int] = (1024, 720), output_args=("-update", "1", "-frames:v", "1"),
) -> tuple[bytes, int, int]: input_mime=mime_type,
with tempfile.NamedTemporaryFile(prefix="mxtg_video_", suffix=f".{video_ext}") as file: logger=log,
# We don't have any way to read the video from memory, so save it to disk. )
file.write(data) width, height = Image.open(BytesIO(first_frame)).size
return first_frame, width, height
# Read temp file and get frame
frame = VideoFileClip(file.name).get_frame(0)
# Convert to png and save to BytesIO
image = Image.fromarray(frame).convert("RGBA")
thumbnail_file = BytesIO()
if max_size:
image.thumbnail(max_size, Image.ANTIALIAS)
image.save(thumbnail_file, frame_ext)
w, h = image.size
return thumbnail_file.getvalue(), w, h
def _location_to_id(location: TypeLocation) -> str: def _location_to_id(location: TypeLocation) -> str:
@@ -151,7 +132,7 @@ async def transfer_thumbnail_to_matrix(
height: int | None = None, height: int | None = None,
async_upload: bool = False, async_upload: bool = False,
) -> DBTelegramFile | None: ) -> DBTelegramFile | None:
if not Image or not VideoFileClip: if not Image or not ffmpeg.ffmpeg_path:
return None return None
loc_id = _location_to_id(thumbnail_loc) loc_id = _location_to_id(thumbnail_loc)
@@ -170,10 +151,12 @@ async def transfer_thumbnail_to_matrix(
video_ext = sane_mimetypes.guess_extension(mime_type) video_ext = sane_mimetypes.guess_extension(mime_type)
if custom_data: if custom_data:
file = custom_data file = custom_data
elif VideoFileClip and video_ext and video: elif video_ext and video:
log.debug(f"Generating thumbnail for video {loc_id} with ffmpeg")
try: try:
file, width, height = _read_video_thumbnail(video, video_ext, frame_ext="png") file, width, height = await _read_video_thumbnail(video, mime_type=mime_type)
except OSError: except Exception:
log.warning(f"Failed to generate thumbnail for {loc_id}", exc_info=True)
return None return None
mime_type = "image/png" mime_type = "image/png"
else: else:
@@ -350,10 +333,10 @@ async def _unlocked_transfer_file_to_matrix(
client, intent, loc_id, location, filename, encrypt, parallel_id client, intent, loc_id, location, filename, encrypt, parallel_id
) )
mime_type = location.mime_type mime_type = location.mime_type
file = None unencrypted_file = None
else: else:
try: try:
file = await client.download_file(location) unencrypted_file = file = await client.download_file(location)
except (LocationInvalidError, FileIdInvalidError): except (LocationInvalidError, FileIdInvalidError):
return None return None
except (AuthBytesInvalidError, AuthKeyInvalidError, SecurityError) as e: except (AuthBytesInvalidError, AuthKeyInvalidError, SecurityError) as e:
@@ -401,34 +384,37 @@ async def _unlocked_transfer_file_to_matrix(
width=width, width=width,
height=height, height=height,
) )
if thumbnail and (mime_type.startswith("video/") or mime_type == "image/gif"): try:
if isinstance(thumbnail, (PhotoSize, PhotoCachedSize)): if thumbnail and (mime_type.startswith("video/") or mime_type == "image/gif"):
thumbnail = thumbnail.location if isinstance(thumbnail, (PhotoSize, PhotoCachedSize)):
try: thumbnail = thumbnail.location
try:
db_file.thumbnail = await transfer_thumbnail_to_matrix(
client,
intent,
thumbnail,
video=unencrypted_file,
mime_type=mime_type,
encrypt=encrypt,
async_upload=async_upload,
)
except FileIdInvalidError:
log.warning(f"Failed to transfer thumbnail {thumbnail!s}", exc_info=True)
elif converted_anim and converted_anim.thumbnail_data:
db_file.thumbnail = await transfer_thumbnail_to_matrix( db_file.thumbnail = await transfer_thumbnail_to_matrix(
client, client,
intent, intent,
thumbnail, location,
video=file, video=None,
mime_type=mime_type,
encrypt=encrypt, encrypt=encrypt,
custom_data=converted_anim.thumbnail_data,
mime_type=converted_anim.thumbnail_mime,
width=converted_anim.width,
height=converted_anim.height,
async_upload=async_upload, async_upload=async_upload,
) )
except FileIdInvalidError: except Exception:
log.warning(f"Failed to transfer thumbnail for {thumbnail!s}", exc_info=True) log.exception(f"Failed to transfer thumbnail for {loc_id}")
elif converted_anim and converted_anim.thumbnail_data:
db_file.thumbnail = await transfer_thumbnail_to_matrix(
client,
intent,
location,
video=None,
encrypt=encrypt,
custom_data=converted_anim.thumbnail_data,
mime_type=converted_anim.thumbnail_mime,
width=converted_anim.width,
height=converted_anim.height,
async_upload=async_upload,
)
try: try:
await db_file.insert() await db_file.insert()
+11 -2
View File
@@ -38,6 +38,7 @@ from telethon.errors import (
) )
from mautrix.bridge import InvalidAccessToken, OnlyLoginSelf from mautrix.bridge import InvalidAccessToken, OnlyLoginSelf
from mautrix.util import background_task
from mautrix.util.format_duration import format_duration from mautrix.util.format_duration import format_duration
from ...commands.telegram.auth import enter_password from ...commands.telegram.auth import enter_password
@@ -199,7 +200,7 @@ class AuthAPI(abc.ABC):
existing_user = await User.get_by_tgid(user_info.id) existing_user = await User.get_by_tgid(user_info.id)
if existing_user and existing_user != user: if existing_user and existing_user != user:
await existing_user.log_out() await existing_user.log_out()
asyncio.create_task(user.post_login(user_info, first_login=True)) background_task.create(user.post_login(user_info, first_login=True))
if user.command_status and user.command_status["action"] == "Login": if user.command_status and user.command_status["action"] == "Login":
user.command_status = None user.command_status = None
@@ -341,8 +342,16 @@ class AuthAPI(abc.ABC):
errcode="password_invalid", errcode="password_invalid",
error="Incorrect password.", error="Incorrect password.",
) )
except Exception: except Exception as e:
self.log.exception("Error sending password") self.log.exception("Error sending password")
if isinstance(e, ValueError) and "You must provide a phone and a code" in str(e):
return self.get_login_response(
mxid=user.mxid,
state="request",
status=400,
errcode="phone_code_not_entered",
error="Please request a new phone code and enter it first.",
)
return self.get_login_response( return self.get_login_response(
mxid=user.mxid, mxid=user.mxid,
state="password", state="password",
+80 -3
View File
@@ -17,10 +17,14 @@ from __future__ import annotations
from typing import TYPE_CHECKING, Awaitable, Callable from typing import TYPE_CHECKING, Awaitable, Callable
import asyncio import asyncio
import datetime
import json import json
import logging import logging
from aiohttp import web from aiohttp import web
from telethon.errors import SessionPasswordNeededError
from telethon.tl.custom import QRLogin
from telethon.tl.functions.messages import GetAllStickersRequest
from telethon.tl.types import ChannelForbidden, ChatForbidden, TypeChat, User as TLUser from telethon.tl.types import ChannelForbidden, ChatForbidden, TypeChat, User as TLUser
from telethon.utils import get_peer_id, resolve_id from telethon.utils import get_peer_id, resolve_id
@@ -28,6 +32,7 @@ from mautrix.appservice import AppService
from mautrix.client import Client from mautrix.client import Client
from mautrix.errors import IntentError, MatrixRequestError from mautrix.errors import IntentError, MatrixRequestError
from mautrix.types import UserID from mautrix.types import UserID
from mautrix.util import background_task
from ...commands.portal.util import get_initial_state, user_has_power_level from ...commands.portal.util import get_initial_state, user_has_power_level
from ...portal import Portal from ...portal import Portal
@@ -72,9 +77,12 @@ class ProvisioningAPI(AuthAPI):
) )
self.app.router.add_route("POST", f"{user_prefix}/pm/{{identifier}}", self.start_dm) self.app.router.add_route("POST", f"{user_prefix}/pm/{{identifier}}", self.start_dm)
self.app.router.add_route("GET", f"{user_prefix}/stickersets", self.get_stickersets)
self.app.router.add_route("POST", f"{user_prefix}/retry_takeout", self.retry_takeout) self.app.router.add_route("POST", f"{user_prefix}/retry_takeout", self.retry_takeout)
self.app.router.add_route("POST", f"{user_prefix}/logout", self.logout) self.app.router.add_route("POST", f"{user_prefix}/logout", self.logout)
self.app.router.add_route("GET", f"{user_prefix}/login/qr", self.login_qr)
self.app.router.add_route("POST", f"{user_prefix}/login/bot_token", self.send_bot_token) self.app.router.add_route("POST", f"{user_prefix}/login/bot_token", self.send_bot_token)
self.app.router.add_route("POST", f"{user_prefix}/login/request_code", self.request_code) self.app.router.add_route("POST", f"{user_prefix}/login/request_code", self.request_code)
self.app.router.add_route("POST", f"{user_prefix}/login/send_code", self.send_code) self.app.router.add_route("POST", f"{user_prefix}/login/send_code", self.send_code)
@@ -220,7 +228,7 @@ class ProvisioningAPI(AuthAPI):
portal.photo_id = "" portal.photo_id = ""
await portal.save() await portal.save()
asyncio.create_task(portal.update_matrix_room(user, entity, levels=levels)) background_task.create(portal.update_matrix_room(user, entity, levels=levels))
return web.Response(status=202, body="{}") return web.Response(status=202, body="{}")
@@ -341,7 +349,7 @@ class ProvisioningAPI(AuthAPI):
self.log.exception("Failed to disconnect chat") self.log.exception("Failed to disconnect chat")
return self.get_error_response(500, "exception", "Failed to disconnect chat") return self.get_error_response(500, "exception", "Failed to disconnect chat")
else: else:
asyncio.create_task(coro) background_task.create(coro)
return web.json_response({}, status=200 if sync else 202) return web.json_response({}, status=200 if sync else 202)
async def get_user_info(self, request: web.Request) -> web.Response: async def get_user_info(self, request: web.Request) -> web.Response:
@@ -497,6 +505,18 @@ class ProvisioningAPI(AuthAPI):
status=201 if just_created else 200, status=201 if just_created else 200,
) )
async def get_stickersets(self, request: web.Request) -> web.Response:
_, user, err = await self.get_user_request_info(
request, expect_logged_in=True, want_data=False
)
if err is not None:
return err
result = await user.client(GetAllStickersRequest(0))
resp = []
for stickerset in result.sets:
resp.append(stickerset.short_name)
return web.json_response(resp, status=200)
async def retry_takeout(self, request: web.Request) -> web.Response: async def retry_takeout(self, request: web.Request) -> web.Response:
data, user, err = await self.get_user_request_info( data, user, err = await self.get_user_request_info(
request, expect_logged_in=True, want_data=False request, expect_logged_in=True, want_data=False
@@ -513,6 +533,50 @@ class ProvisioningAPI(AuthAPI):
user.takeout_retry_immediate.set() user.takeout_retry_immediate.set()
return web.json_response({}, status=200) return web.json_response({}, status=200)
async def login_qr(self, request: web.Request) -> web.Response:
_, user, err = await self.get_user_request_info(request, websocket=True)
if err is not None:
return err
await user.ensure_started(even_if_no_session=True)
qr_login = QRLogin(user.client, ignored_ids=[])
ws = web.WebSocketResponse(protocols=["net.maunium.telegram.login"])
await ws.prepare(request)
retries = 0
user_info = None
while retries < 4:
try:
await qr_login.recreate()
await ws.send_json(
{
"code": qr_login.url,
"timeout": int(
(
qr_login.expires - datetime.datetime.now(tz=datetime.timezone.utc)
).total_seconds()
),
}
)
user_info = await qr_login.wait()
break
except asyncio.TimeoutError:
retries += 1
except SessionPasswordNeededError:
await ws.send_json({"success": False, "error": "password-needed"})
await ws.close()
return ws
else:
await ws.send_json({"success": False, "error": "timeout"})
await ws.close()
return ws
await self.postprocess_login(user, user_info)
await ws.send_json({"success": True})
await ws.close()
return ws
async def send_bot_token(self, request: web.Request) -> web.Response: async def send_bot_token(self, request: web.Request) -> web.Response:
data, user, err = await self.get_user_request_info(request) data, user, err = await self.get_user_request_info(request)
if err is not None: if err is not None:
@@ -638,6 +702,15 @@ class ProvisioningAPI(AuthAPI):
) )
return None return None
def check_websocket_authorization(self, request: web.Request) -> web.Response | None:
auth_parts = request.headers.get("Sec-WebSocket-Protocol").split(",")
for part in auth_parts:
if part.strip() == f"net.maunium.telegram.auth-{self.secret}":
return None
return self.get_error_response(
error="Shared secret is not valid.", errcode="shared_secret_invalid", status=401
)
@staticmethod @staticmethod
async def get_data(request: web.Request) -> dict | None: async def get_data(request: web.Request) -> dict | None:
try: try:
@@ -692,8 +765,12 @@ class ProvisioningAPI(AuthAPI):
expect_logged_in: bool | None = False, expect_logged_in: bool | None = False,
require_puppeting: bool = False, require_puppeting: bool = False,
want_data: bool = True, want_data: bool = True,
websocket: bool = False,
) -> tuple[dict | None, User | None, web.Response | None]: ) -> tuple[dict | None, User | None, web.Response | None]:
err = self.check_authorization(request) if not websocket:
err = self.check_authorization(request)
else:
err = self.check_websocket_authorization(request)
if err is not None: if err is not None:
return None, None, err return None, None, err
+3 -5
View File
@@ -2,7 +2,7 @@
# Uncommented lines after the group definition insert things into that group. # Uncommented lines after the group definition insert things into that group.
#/speedups #/speedups
cryptg>=0.1,<0.4 cryptg>=0.1,<0.5
aiodns aiodns
brotli brotli
@@ -10,14 +10,12 @@ brotli
pillow>=4,<10 pillow>=4,<10
qrcode>=6,<8 qrcode>=6,<8
#/hq_thumbnails
moviepy>=1,<2
#/formattednumbers #/formattednumbers
phonenumbers>=8,<9 phonenumbers>=8,<9
#/metrics #/metrics
prometheus_client>=0.6,<0.16 prometheus_client>=0.6,<0.17
#/e2be #/e2be
python-olm>=3,<4 python-olm>=3,<4
@@ -25,4 +23,4 @@ pycryptodome>=3,<4
unpaddedbase64>=1,<3 unpaddedbase64>=1,<3
#/sqlite #/sqlite
aiosqlite>=0.16,<0.18 aiosqlite>=0.16,<0.19
+2 -2
View File
@@ -3,9 +3,9 @@ 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.18.8,<0.19 mautrix>=0.19.4,<0.20
#telethon>=1.25.4,<1.27 #telethon>=1.25.4,<1.27
tulir-telethon==1.27.0a1 tulir-telethon==1.28.0a3
asyncpg>=0.20,<0.28 asyncpg>=0.20,<0.28
mako>=1,<2 mako>=1,<2
setuptools setuptools