Compare commits

...

50 Commits

Author SHA1 Message Date
Tulir Asokan 78fcacf7aa Bump version to 0.10.0rc1 2021-04-05 12:47:11 +03:00
Tulir Asokan 077f5d588b Update dependencies 2021-04-05 12:28:39 +03:00
Tulir Asokan 8b73c67836 Mark chat as fully read on Telegram if read receipt target is unknown 2021-03-31 16:42:35 +03:00
Tulir Asokan 92fa05cb06 Fix handling forwarded messages from known chats without a cached title 2021-03-25 19:40:33 +02:00
Tulir Asokan 18f5a33279 Add some logs when bridging read receipts 2021-03-25 19:12:33 +02:00
Tulir Asokan f9a6e9c4fb Fix other usages of Puppet.get_displayname 2021-03-23 20:22:05 +02:00
Tulir Asokan abfefab545 Store puppet displayname quality and don't allow it to decrease 2021-03-23 20:13:06 +02:00
Tulir Asokan 79f8c520bd Move RowProxy import into type checking 2021-03-22 13:51:49 +02:00
Tulir Asokan fa35ed1cb6 Sync own read marker to Matrix when backfilling chats 2021-03-22 13:51:22 +02:00
Tulir Asokan 2e8d612078 Merge remote-tracking branch 'MadhuranS/master'
Fixes #375
2021-03-18 20:33:31 +02:00
Madhu Sivapragasam 4801b0f323 Added about section update bot command 2021-03-18 13:52:02 -04:00
Tulir Asokan 783c94dadd Pin SQLAlchemy to <1.4. Fixes #595 2021-03-15 23:23:26 +02:00
Tulir Asokan c8cf662ad0 Catch network errors when setting puppet displayname/avatar 2021-03-14 12:34:31 +02:00
Tulir Asokan cd70e6b836 Switch to BIGINT for Telegram IDs in database 2021-03-09 22:03:23 +02:00
Tulir Asokan 72cfbf71f8 Fix finding largest photo size. Fixes #586 2021-02-28 14:22:17 +02:00
Tulir Asokan cb36800c75 Maybe fix parallel transfer. Fixes #587 2021-02-28 14:13:07 +02:00
Tulir Asokan 559c504e8b Improve formatting of dice messages 2021-02-28 13:53:50 +02:00
Tulir Asokan de3a37f40c Update Telethon and add support for invite link customization 2021-02-28 13:16:07 +02:00
Tulir Asokan 6020cdf8bf Let mautrix-python handle registration generation message 2021-02-21 17:24:35 +02:00
Tulir Asokan 429cb07b79 Handle missing input entities better when creating groups. Fixes #379 2021-02-14 16:36:21 +02:00
Tulir Asokan 2cf93c5765 Replace wiki with docs.mau.fi 2021-02-13 21:27:34 +02:00
Tulir Asokan db41c8d806 Bump maximum Telethon version again 2021-02-06 13:53:47 +02:00
Tulir Asokan 5313369d85 Revert "Bump maximum Telethon version". Fixes #582
This reverts commit c8c17dac01.
2021-02-06 13:00:12 +02:00
Tulir Asokan c8c17dac01 Bump maximum Telethon version 2021-02-05 19:49:12 +02:00
Tulir Asokan bbb864773f Update Docker image to Alpine 3.13 2021-02-05 19:47:26 +02:00
Tulir Asokan 4767fec86e Update mautrix-python 2021-01-23 01:21:32 +02:00
Tulir Asokan 6d57f070f9 Fix updating names of contact users. Fixes #570 2021-01-21 21:24:16 +02:00
Tulir Asokan 97d47d80ee Allow displayname updates if ghost user has no name 2021-01-21 16:34:10 +02:00
Steffen Deusch 35f59b5f95 fix async puppet default leave 2021-01-16 02:42:55 +02:00
Tulir Asokan 697fb06909 Try to fix displayname changing between contact and non-contact name. Fixes #533 2021-01-01 12:02:21 +02:00
Tulir Asokan efd536357c Fix sticker bridging. Fixes #566 2020-12-28 13:06:59 +02:00
Tulir Asokan 2c917a559c Log raw event that caused displayname updates 2020-12-28 13:06:59 +02:00
Rafaeltheraven b97c1a1b59 Allow enabling room encryption with PL 50 if end-to-bridge encryption is enabled (#550) 2020-12-23 13:18:03 +02:00
Tulir Asokan 9237046b96 Install yq from alpine repos 2020-12-19 14:14:46 +02:00
Tulir Asokan 646bbceb99 Remove webp conversion 2020-12-19 14:14:33 +02:00
Tulir Asokan e9e164c679 Stringify URL when following redirects 2020-12-19 13:36:04 +02:00
Tulir Asokan 033c6c698a Rename Riot to Element in comments about how bad they are 2020-12-19 13:28:49 +02:00
Tulir Asokan 3d403c2471 Add option to resolve redirects in invite links. Fixes #559 2020-12-19 13:15:27 +02:00
Tulir Asokan b22e3d2573 Improve invite link regex
Fixes #554
Fixes #555
2020-12-19 13:10:19 +02:00
Tulir Asokan 7d20c5b732 Fix deduplicating forwarded messages. Fixes #549 2020-12-19 12:54:58 +02:00
Tulir Asokan 2ce2337674 Stringify base_url before inserting to db. Fixes #546 2020-12-19 12:52:10 +02:00
Tulir Asokan 3fe26ae4dd Strip spaces around messages when hashing for deduplication. Fixes #553 2020-12-19 12:49:48 +02:00
Tulir Asokan 6f4faf7a58 Store Matrix redaction state and ignore deletions of redacted messages 2020-12-19 12:48:08 +02:00
Tulir Asokan e1dcfb76f4 Update dependencies and python_requires 2020-12-12 14:01:54 +02:00
Tulir Asokan f658f2c5b7 Fix bugs 2020-12-02 12:11:11 +02:00
Tulir Asokan dd7eed834c Update telethon 2020-12-02 12:01:20 +02:00
Tulir Asokan e4f8b22bc6 Merge branch 'telethon-1.18' 2020-12-02 11:59:39 +02:00
Tulir Asokan 0b8fa5ea06 Update mautrix-python. Fixes #472 2020-12-02 00:34:13 +02:00
Tulir Asokan 140fcae403 Fix Matrix->Telegram location message bridging 2020-11-22 13:47:20 +02:00
Tulir Asokan 2e27e85ac5 Add support for multiple pins 2020-11-06 18:57:22 +02:00
35 changed files with 631 additions and 213 deletions
+11 -12
View File
@@ -1,11 +1,11 @@
FROM dock.mau.dev/tulir/lottieconverter:alpine-3.12
FROM dock.mau.dev/tulir/lottieconverter:alpine-3.13
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 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 \
@@ -14,11 +14,11 @@ RUN apk add --no-cache \
py3-aiohttp \
py3-magic \
py3-sqlalchemy \
py3-telethon-session-sqlalchemy@edge \
py3-alembic@edge \
py3-telethon-session-sqlalchemy \
py3-alembic \
py3-psycopg2 \
py3-ruamel.yaml \
py3-commonmark@edge \
py3-commonmark \
# Indirect dependencies
py3-idna \
#moviepy
@@ -32,7 +32,7 @@ RUN apk add --no-cache \
py3-pysocks \
# cryptg
py3-cffi \
py3-qrcode@edge \
py3-qrcode \
py3-brotli \
# Other dependencies
ffmpeg \
@@ -46,9 +46,8 @@ RUN apk add --no-cache \
py3-future \
bash \
curl \
jq && \
curl -sLo yq https://github.com/mikefarah/yq/releases/download/3.3.2/yq_linux_${TARGETARCH} && \
chmod +x yq && mv yq /usr/bin/yq
jq \
yq
COPY requirements.txt /opt/mautrix-telegram/requirements.txt
COPY optional-requirements.txt /opt/mautrix-telegram/optional-requirements.txt
+9 -8
View File
@@ -10,15 +10,16 @@ A Matrix-Telegram hybrid puppeting/relaybot bridge.
## Sponsors
* [Joel Lehtonen / Zouppen](https://github.com/zouppen)
### Wiki
All setup and usage instructions are located in the GitHub
[wiki](https://github.com/tulir/mautrix-telegram/wiki). Some quick links:
### Documentation
All setup and usage instructions are located on
[docs.mau.fi](https://docs.mau.fi/bridges/python/telegram/index.html).
Some quick links:
* [Bridge setup](https://github.com/tulir/mautrix-telegram/wiki/Bridge-setup)
(or [with Docker](https://github.com/tulir/mautrix-telegram/wiki/Bridge-setup-with-Docker))
* Basic usage: [Authentication](https://github.com/tulir/mautrix-telegram/wiki/Authentication),
[Creating chats](https://github.com/tulir/mautrix-telegram/wiki/Creating-and-managing-chats),
[Relaybot setup](https://github.com/tulir/mautrix-telegram/wiki/Relay-bot)
* [Bridge setup](https://docs.mau.fi/bridges/python/setup/index.html?bridge=telegram)
(or [with Docker](https://docs.mau.fi/bridges/python/setup/docker.html?bridge=telegram))
* Basic usage: [Authentication](https://docs.mau.fi/bridges/python/telegram/authentication.html),
[Creating chats](https://docs.mau.fi/bridges/python/telegram/creating-and-managing-chats.html),
[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)
@@ -0,0 +1,25 @@
"""Add Matrix redaction state to message table
Revision ID: 7de69cf5809e
Revises: 888275d58e57
Create Date: 2020-12-19 12:39:57.368568
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '7de69cf5809e'
down_revision = '888275d58e57'
branch_labels = None
depends_on = None
def upgrade():
with op.batch_alter_table('message', schema=None) as batch_op:
batch_op.add_column(sa.Column('redacted', sa.Boolean(), server_default=sa.false(), nullable=True))
def downgrade():
with op.batch_alter_table('message', schema=None) as batch_op:
batch_op.drop_column('redacted')
@@ -0,0 +1,32 @@
"""Store displayname contact status in puppet table
Revision ID: 990f4395afc6
Revises: 7de69cf5809e
Create Date: 2021-01-01 11:56:54.610681
"""
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import postgresql
# revision identifiers, used by Alembic.
revision = '990f4395afc6'
down_revision = '7de69cf5809e'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('puppet', schema=None) as batch_op:
batch_op.add_column(sa.Column('displayname_contact', sa.Boolean(), server_default=sa.true(), nullable=False))
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('puppet', schema=None) as batch_op:
batch_op.drop_column('displayname_contact')
# ### end Alembic commands ###
@@ -0,0 +1,32 @@
"""Store displayname quality in puppet table
Revision ID: bfc0a39bfe02
Revises: ec1d3dcc77e9
Create Date: 2021-03-23 20:03:08.825333
"""
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import postgresql
# revision identifiers, used by Alembic.
revision = 'bfc0a39bfe02'
down_revision = 'ec1d3dcc77e9'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('puppet', schema=None) as batch_op:
batch_op.add_column(sa.Column('displayname_quality', sa.Integer(), server_default='0', nullable=False))
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('puppet', schema=None) as batch_op:
batch_op.drop_column('displayname_quality')
# ### end Alembic commands ###
@@ -0,0 +1,44 @@
"""Switch Telegram IDs to bigints
Revision ID: ec1d3dcc77e9
Revises: 990f4395afc6
Create Date: 2021-03-09 21:36:58.443727
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = 'ec1d3dcc77e9'
down_revision = '990f4395afc6'
branch_labels = None
depends_on = None
columns_to_upgrade = (
("bot_chat", "id"),
("message", "tgid"),
("message", "tg_space"),
("portal", "tgid"),
("portal", "tg_receiver"),
("puppet", "id"),
("puppet", "displayname_source"),
("user", "tgid"),
("user_portal", "user"),
("user_portal", "portal"),
("user_portal", "portal_receiver"),
("contact", "user"),
("contact", "contact"),
)
def upgrade():
if op.get_context().dialect.name == "postgresql":
for table, column in columns_to_upgrade:
op.alter_column(table, column, existing_type=sa.Integer, type_=sa.BigInteger)
def downgrade():
if op.get_context().dialect.name == "postgresql":
for table, column in columns_to_upgrade:
op.alter_column(table, column, existing_type=sa.BigInteger, type_=sa.Integer)
-3
View File
@@ -26,9 +26,6 @@ fi
if [ ! -f /data/registration.yaml ]; then
python3 -m mautrix_telegram -g -c /data/config.yaml -r /data/registration.yaml
echo "Didn't find a registration file."
echo "Generated one for you."
echo "Copy that over to synapses app service directory."
fixperms
exit
fi
+1 -1
View File
@@ -1,2 +1,2 @@
__version__ = "0.9.0"
__version__ = "0.10.0rc1"
__author__ = "Tulir Asokan <tulir@maunium.net>"
+14 -9
View File
@@ -25,8 +25,8 @@ from telethon.network import (ConnectionTcpMTProxyRandomizedIntermediate, Connec
Connection)
from telethon.tl.patched import MessageService, Message
from telethon.tl.types import (
Channel, Chat, MessageActionChannelMigrateFrom, PeerUser, TypeUpdate, UpdateChatPinnedMessage,
UpdateChannelPinnedMessage, UpdateChatParticipantAdmin, UpdateChatParticipants, PeerChat,
Channel, Chat, MessageActionChannelMigrateFrom, PeerUser, TypeUpdate, UpdatePinnedMessages,
UpdatePinnedChannelMessages, UpdateChatParticipantAdmin, UpdateChatParticipants, PeerChat,
UpdateChatUserTyping, UpdateDeleteChannelMessages, UpdateNewMessage, UpdateDeleteMessages,
UpdateEditChannelMessage, UpdateEditMessage, UpdateNewChannelMessage, UpdateReadHistoryOutbox,
UpdateShortChatMessage, UpdateShortMessage, UpdateUserName, UpdateUserPhoto, UpdateUserStatus,
@@ -252,7 +252,7 @@ class AbstractUser(ABC):
await self.update_admin(update)
elif isinstance(update, UpdateChatParticipants):
await self.update_participants(update)
elif isinstance(update, (UpdateChannelPinnedMessage, UpdateChatPinnedMessage)):
elif isinstance(update, (UpdatePinnedMessages, UpdatePinnedChannelMessages)):
await self.update_pinned_messages(update)
elif isinstance(update, (UpdateUserName, UpdateUserPhoto)):
await self.update_others_info(update)
@@ -263,14 +263,15 @@ class AbstractUser(ABC):
else:
self.log.trace("Unhandled update: %s", update)
async def update_pinned_messages(self, update: Union[UpdateChannelPinnedMessage,
UpdateChatPinnedMessage]) -> None:
if isinstance(update, UpdateChatPinnedMessage):
portal = po.Portal.get_by_tgid(TelegramID(update.chat_id))
async def update_pinned_messages(self, update: Union[UpdatePinnedMessages,
UpdatePinnedChannelMessages]) -> None:
if isinstance(update, UpdatePinnedMessages):
portal = po.Portal.get_by_entity(update.peer, receiver_id=self.tgid)
else:
portal = po.Portal.get_by_tgid(TelegramID(update.channel_id))
if portal and portal.mxid:
await portal.receive_telegram_pin_id(update.id, self.tgid)
await portal.receive_telegram_pin_ids(update.messages, self.tgid,
remove=not update.pinned)
@staticmethod
async def update_participants(update: UpdateChatParticipants) -> None:
@@ -419,6 +420,8 @@ class AbstractUser(ABC):
for message_id in update.messages:
for message in DBMessage.get_all_by_tgid(TelegramID(message_id), self.tgid):
if message.redacted:
continue
message.delete()
number_left = DBMessage.count_spaces_by_mxid(message.mxid, message.mx_room)
if number_left == 0:
@@ -432,6 +435,8 @@ class AbstractUser(ABC):
for message_id in update.messages:
for message in DBMessage.get_all_by_tgid(TelegramID(message_id), channel_id):
if message.redacted:
continue
message.delete()
await self._try_redact(message)
@@ -468,7 +473,7 @@ class AbstractUser(ABC):
await self.register_portal(portal)
return
self.log.trace("Handling action %s to %s by %d", update.action, portal.tgid_log,
sender.id)
(sender.id if sender else 0))
return await portal.handle_telegram_action(self, sender, update)
if isinstance(original_update, (UpdateEditMessage, UpdateEditChannelMessage)):
@@ -52,8 +52,14 @@ async def create(evt: CommandEvent) -> EventID:
portal = po.Portal(tgid=TelegramID(0), peer_type=type, mxid=evt.room_id,
title=title, about=about, encrypted=encrypted)
invites, errors = await portal.get_telegram_users_in_matrix_room(evt.sender)
if len(errors) > 0:
error_list = "\n".join(f"* [{mxid}](https://matrix.to/#/{mxid})" for mxid in errors)
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.")
try:
await portal.create_telegram_chat(evt.sender, supergroup=supergroup)
await portal.create_telegram_chat(evt.sender, invites=invites, supergroup=supergroup)
except ValueError as e:
await portal.delete()
return await evt.reply(e.args[0])
+78 -2
View File
@@ -13,6 +13,10 @@
#
# 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, List, Tuple
from datetime import timedelta, datetime
import re
from telethon.tl.functions.channels import GetFullChannelRequest
from telethon.tl.functions.messages import GetFullChatRequest
from telethon.errors import (ChatAdminRequiredError, UsernameInvalidError,
@@ -80,9 +84,81 @@ async def get_id(evt: CommandEvent) -> EventID:
await evt.reply(f"This room is bridged to Telegram chat ID `{tgid}`.")
invite_link_usage = ("**Usage:** `$cmdprefix+sp invite-link [--uses=<amount>] [--expire=<delta>]`"
"\n\n"
"* `--uses`: the number of times the invite link can be used."
" Defaults to unlimited.\n"
"* `--expire`: the duration after which the link will expire."
" A number suffixed with d(ay), h(our), m(inute) or s(econd)")
def _parse_flag(args: List[str]) -> Tuple[str, str]:
arg = args.pop(0).lower()
if arg.startswith("--"):
value_start = arg.index("=")
if value_start:
flag = arg[2:value_start]
value = arg[value_start+1:]
else:
flag = arg[2:]
value = args.pop(0).lower()
elif arg.startswith("-"):
flag = arg[1]
if len(arg) > 3 and arg[2] == "=":
value = arg[3:]
else:
value = args.pop(0).lower()
else:
raise ValueError("invalid flag")
return flag, value
delta_regex = re.compile("([0-9]+)(w(?:eek)?|d(?:ay)?|h(?:our)?|m(?:in(?:ute)?)?|s(?:ec(?:ond)?)?)")
def _parse_delta(value: str) -> Optional[timedelta]:
match = delta_regex.fullmatch(value)
if not match:
return None
number = int(match.group(1))
unit = match.group(2)[0]
if unit == "w":
return timedelta(weeks=number)
elif unit == "d":
return timedelta(days=number)
elif unit == "h":
return timedelta(hours=number)
elif unit == "m":
return timedelta(minutes=number)
elif unit == "s":
return timedelta(seconds=number)
else:
return None
@command_handler(help_section=SECTION_PORTAL_MANAGEMENT,
help_text="Get a Telegram invite link to the current chat.")
help_text="Get a Telegram invite link to the current chat.",
help_args="[--uses=<amount>] [--expire=<time delta, e.g. 1d>]")
async def invite_link(evt: CommandEvent) -> EventID:
# TODO once we switch to Python 3.9 minimum, use argparse with exit_on_error=False
uses = None
expire = None
while evt.args:
try:
flag, value = _parse_flag(evt.args)
except (ValueError, IndexError):
return await evt.reply(invite_link_usage)
if flag in ("uses", "u"):
try:
uses = int(value)
except ValueError:
await evt.reply("The number of uses must be an integer")
elif flag in ("expire", "e"):
expire_delta = _parse_delta(value)
if not expire_delta:
await evt.reply("Invalid format for expiry time delta")
expire = datetime.now() + expire_delta
portal = po.Portal.get_by_mxid(evt.room_id)
if not portal:
return await evt.reply("This is not a portal room.")
@@ -91,7 +167,7 @@ async def invite_link(evt: CommandEvent) -> EventID:
return await evt.reply("You can't invite users to private chats.")
try:
link = await portal.get_invite_link(evt.sender)
link = await portal.get_invite_link(evt.sender, uses=uses, expire=expire)
return await evt.reply(f"Invite link to {portal.title}: {link}")
except ValueError as e:
return await evt.reply(e.args[0])
+18 -1
View File
@@ -16,7 +16,7 @@
from typing import Optional
from telethon.errors import (UsernameInvalidError, UsernameNotModifiedError, UsernameOccupiedError,
HashInvalidError, AuthKeyError, FirstNameInvalidError)
HashInvalidError, AuthKeyError, FirstNameInvalidError, AboutTooLongError)
from telethon.tl.types import Authorization
from telethon.tl.functions.account import (UpdateUsernameRequest, GetAuthorizationsRequest,
ResetAuthorizationRequest, UpdateProfileRequest)
@@ -53,6 +53,23 @@ async def username(evt: CommandEvent) -> EventID:
else:
await evt.reply(f"Username changed to {evt.sender.username}")
@command_handler(needs_auth=True,
help_section=SECTION_AUTH,
help_args="<_new about_>",
help_text="Change your Telegram about section.")
async def about(evt: CommandEvent) -> EventID:
if len(evt.args) == 0:
return await evt.reply("**Usage:** `$cmdprefix+sp about <new about>`")
if evt.sender.is_bot:
return await evt.reply("Bots can't set their own about section.")
new_about = " ".join(evt.args)
if new_about == "-":
new_about = ""
try:
await evt.sender.client(UpdateProfileRequest(about=new_about))
except AboutTooLongError:
return await evt.reply("The provided about section is too long")
return await evt.reply("About section updated")
@command_handler(needs_auth=True, help_section=SECTION_AUTH, help_args="<_new displayname_>",
help_text="Change your Telegram displayname.")
+29 -15
View File
@@ -14,11 +14,12 @@
# 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 List, Optional, Tuple, cast
import logging
import codecs
import base64
import re
from aiohttp import ClientSession, InvalidURL
from telethon.errors import (InviteHashInvalidError, InviteHashExpiredError, OptionsTooMuchError,
UserAlreadyParticipantError, ChatIdInvalidError,
TakeoutInitDelayError, EmoticonInvalidError)
@@ -115,25 +116,25 @@ async def pm(evt: CommandEvent) -> EventID:
return await evt.reply("That doesn't seem to be a user.")
portal = po.Portal.get_by_entity(user, evt.sender.tgid)
await portal.create_matrix_room(evt.sender, user, [evt.sender.mxid])
return await evt.reply("Created private chat room with "
f"{pu.Puppet.get_displayname(user, False)}")
displayname, _ = pu.Puppet.get_displayname(user, False)
return await evt.reply(f"Created private chat room with {displayname}")
async def _join(evt: CommandEvent, arg: str) -> Tuple[Optional[TypeUpdates], Optional[EventID]]:
if arg.startswith("joinchat/"):
invite_hash = arg[len("joinchat/"):]
async def _join(evt: CommandEvent, identifier: str, link_type: str
) -> Tuple[Optional[TypeUpdates], Optional[EventID]]:
if link_type == "joinchat":
try:
await evt.sender.client(CheckChatInviteRequest(invite_hash))
await evt.sender.client(CheckChatInviteRequest(identifier))
except InviteHashInvalidError:
return None, await evt.reply("Invalid invite link.")
except InviteHashExpiredError:
return None, await evt.reply("Invite link expired.")
try:
return (await evt.sender.client(ImportChatInviteRequest(invite_hash))), None
return (await evt.sender.client(ImportChatInviteRequest(identifier))), None
except UserAlreadyParticipantError:
return None, await evt.reply("You are already in that chat.")
else:
channel = await evt.sender.client.get_entity(arg)
channel = await evt.sender.client.get_entity(identifier)
if not channel:
return None, await evt.reply("Channel/supergroup not found.")
return await evt.sender.client(JoinChannelRequest(channel)), None
@@ -146,12 +147,26 @@ async def join(evt: CommandEvent) -> Optional[EventID]:
if len(evt.args) == 0:
return await evt.reply("**Usage:** `$cmdprefix+sp join <invite link>`")
regex = re.compile(r"(?:https?://)?t(?:elegram)?\.(?:dog|me)(?:joinchat/)?/(.+)")
arg = regex.match(evt.args[0])
url = evt.args[0]
if evt.config["bridge.invite_link_resolve"]:
try:
async with ClientSession() as sess, sess.get(url) as resp:
url = str(resp.url)
except InvalidURL:
return await evt.reply("That doesn't look like a Telegram invite link.")
regex = re.compile(r"(?:https?://)?t(?:elegram)?\.(?:dog|me)"
r"(?:/(?P<type>joinchat|s))?/(?P<id>[^/]+)/?", flags=re.IGNORECASE)
arg = regex.match(url)
if not arg:
return await evt.reply("That doesn't look like a Telegram invite link.")
updates, _ = await _join(evt, arg.group(1))
data = arg.groupdict()
identifier = data["id"]
link_type = data["type"]
if link_type:
link_type = link_type.lower()
updates, _ = await _join(evt, identifier, link_type)
if not updates:
return None
@@ -165,9 +180,8 @@ async def join(evt: CommandEvent) -> Optional[EventID]:
try:
await portal.create_matrix_room(evt.sender, chat, [evt.sender.mxid])
except ChatIdInvalidError as e:
logging.getLogger("mau.commands").trace("ChatIdInvalidError while creating portal "
"from !tg join command: %s",
updates.stringify())
evt.log.trace("ChatIdInvalidError while creating portal from !tg join command: %s",
updates.stringify())
raise e
return await evt.reply(f"Created room for {portal.title}")
return None
+1
View File
@@ -114,6 +114,7 @@ class Config(BaseBridgeConfig):
else:
copy("bridge.login_shared_secret_map")
copy("bridge.telegram_link_preview")
copy("bridge.invite_link_resolve")
copy("bridge.inline_images")
copy("bridge.image_as_file_size")
copy("bridge.max_document_size")
+2 -2
View File
@@ -15,7 +15,7 @@
# along with this program. If not, see <https://www.gnu.org/licenses/>.
from typing import Iterable
from sqlalchemy import Column, Integer, String
from sqlalchemy import Column, BigInteger, String
from mautrix.util.db import Base
@@ -25,7 +25,7 @@ from ..types import TelegramID
# Fucking Telegram not telling bots what chats they are in 3:<
class BotChat(Base):
__tablename__ = "bot_chat"
id: TelegramID = Column(Integer, primary_key=True)
id: TelegramID = Column(BigInteger, primary_key=True)
type: str = Column(String, nullable=False)
@classmethod
+18 -4
View File
@@ -13,9 +13,10 @@
#
# 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, Iterator
from typing import Optional, Iterator, List
from sqlalchemy import Column, UniqueConstraint, Integer, String, and_, func, desc, select
from sqlalchemy import (Column, UniqueConstraint, BigInteger, Integer, String, Boolean, and_, func,
desc, select, false)
from mautrix.types import RoomID, EventID
from mautrix.util.db import Base
@@ -28,9 +29,10 @@ class Message(Base):
mxid: EventID = Column(String)
mx_room: RoomID = Column(String)
tgid: TelegramID = Column(Integer, primary_key=True)
tg_space: TelegramID = Column(Integer, primary_key=True)
tgid: TelegramID = Column(BigInteger, primary_key=True)
tg_space: TelegramID = Column(BigInteger, primary_key=True)
edit_index: int = Column(Integer, primary_key=True)
redacted: bool = Column(Boolean, server_default=false())
__table_args__ = (UniqueConstraint("mxid", "mx_room", "tg_space", name="_mx_id_room_2"),)
@@ -51,6 +53,12 @@ class Message(Base):
return cls._select_one_or_none(cls.c.tgid == tgid, cls.c.tg_space == tg_space,
cls.c.edit_index == edit_index)
@classmethod
def get_first_by_tgids(cls, tgids: List[TelegramID], tg_space: TelegramID
) -> Iterator['Message']:
return cls._select_all(cls.c.tgid.in_(tgids), cls.c.tg_space == tg_space,
cls.c.edit_index == 0)
@classmethod
def count_spaces_by_mxid(cls, mxid: EventID, mx_room: RoomID) -> int:
rows = cls.db.execute(select([func.count(cls.c.tg_space)])
@@ -77,6 +85,12 @@ class Message(Base):
return cls._select_one_or_none(cls.c.mxid == mxid, cls.c.mx_room == mx_room,
cls.c.tg_space == tg_space)
@classmethod
def get_by_mxids(cls, mxids: List[EventID], mx_room: RoomID, tg_space: TelegramID
) -> Iterator['Message']:
return cls._select_all(cls.c.mxid.in_(mxids), cls.c.mx_room == mx_room,
cls.c.tg_space == tg_space)
@classmethod
def update_by_tgid(cls, s_tgid: TelegramID, s_tg_space: TelegramID, s_edit_index: int,
**values) -> None:
+3 -3
View File
@@ -15,7 +15,7 @@
# along with this program. If not, see <https://www.gnu.org/licenses/>.
from typing import Optional, Iterable
from sqlalchemy import Column, Integer, String, Boolean, Text, func, sql
from sqlalchemy import Column, BigInteger, String, Boolean, Text, func, sql
from mautrix.types import RoomID, ContentURI
from mautrix.util.db import Base
@@ -27,8 +27,8 @@ class Portal(Base):
__tablename__ = "portal"
# Telegram chat information
tgid: TelegramID = Column(Integer, primary_key=True)
tg_receiver: TelegramID = Column(Integer, primary_key=True)
tgid: TelegramID = Column(BigInteger, primary_key=True)
tg_receiver: TelegramID = Column(BigInteger, primary_key=True)
peer_type: str = Column(String, nullable=False)
megagroup: bool = Column(Boolean)
+5 -3
View File
@@ -15,7 +15,7 @@
# along with this program. If not, see <https://www.gnu.org/licenses/>.
from typing import Optional, Iterable
from sqlalchemy import Column, Integer, String, Text, Boolean
from sqlalchemy import Column, Integer, BigInteger, String, Text, Boolean
from sqlalchemy.sql import expression, func
from mautrix.types import UserID, SyncToken
@@ -27,13 +27,15 @@ from ..types import TelegramID
class Puppet(Base):
__tablename__ = "puppet"
id: TelegramID = Column(Integer, primary_key=True)
id: TelegramID = Column(BigInteger, primary_key=True)
custom_mxid: UserID = Column(String, nullable=True)
access_token: str = Column(String, nullable=True)
next_batch: SyncToken = Column(String, nullable=True)
base_url: str = Column(Text, nullable=True)
displayname: str = Column(String, nullable=True)
displayname_source: TelegramID = Column(Integer, nullable=True)
displayname_source: TelegramID = Column(BigInteger, nullable=True)
displayname_contact: bool = Column(Boolean, nullable=False, server_default=expression.true())
displayname_quality: int = Column(Integer, nullable=False, server_default="0")
username: str = Column(String, nullable=True)
photo_id: str = Column(String, nullable=True)
is_bot: bool = Column(Boolean, nullable=True)
+5 -3
View File
@@ -13,15 +13,17 @@
#
# 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, cast, Dict, Any
from typing import Optional, cast, Dict, Any, TYPE_CHECKING
from sqlalchemy import (Column, ForeignKey, Integer, BigInteger, String, Boolean, Text,
TypeDecorator)
from sqlalchemy.engine.result import RowProxy
from mautrix.types import ContentURI, EncryptedFile
from mautrix.util.db import Base
if TYPE_CHECKING:
from sqlalchemy.engine.result import RowProxy
class DBEncryptedFile(TypeDecorator):
impl = Text
@@ -60,7 +62,7 @@ class TelegramFile(Base):
thumbnail: Optional['TelegramFile'] = None
@classmethod
def scan(cls, row: RowProxy) -> 'TelegramFile':
def scan(cls, row: 'RowProxy') -> 'TelegramFile':
telegram_file = cast(TelegramFile, super().scan(row))
if isinstance(telegram_file.thumbnail, str):
telegram_file.thumbnail = cls.get(telegram_file.thumbnail)
+7 -7
View File
@@ -15,7 +15,7 @@
# along with this program. If not, see <https://www.gnu.org/licenses/>.
from typing import Optional, Iterable, Tuple
from sqlalchemy import Column, ForeignKey, ForeignKeyConstraint, Integer, String, func
from sqlalchemy import Column, ForeignKey, ForeignKeyConstraint, BigInteger, Integer, String, func
from mautrix.types import UserID
from mautrix.util.db import Base
@@ -27,7 +27,7 @@ class User(Base):
__tablename__ = "user"
mxid: UserID = Column(String, primary_key=True)
tgid: Optional[TelegramID] = Column(Integer, nullable=True, unique=True)
tgid: Optional[TelegramID] = Column(BigInteger, nullable=True, unique=True)
tg_username: str = Column(String, nullable=True)
tg_phone: str = Column(String, nullable=True)
saved_contacts: int = Column(Integer, default=0, nullable=False)
@@ -91,10 +91,10 @@ class User(Base):
class UserPortal(Base):
__tablename__ = "user_portal"
user: TelegramID = Column(Integer, ForeignKey("user.tgid", onupdate="CASCADE",
user: TelegramID = Column(BigInteger, ForeignKey("user.tgid", onupdate="CASCADE",
ondelete="CASCADE"), primary_key=True)
portal: TelegramID = Column(Integer, primary_key=True)
portal_receiver: TelegramID = Column(Integer, primary_key=True)
portal: TelegramID = Column(BigInteger, primary_key=True)
portal_receiver: TelegramID = Column(BigInteger, primary_key=True)
__table_args__ = (ForeignKeyConstraint(("portal", "portal_receiver"),
("portal.tgid", "portal.tg_receiver"),
@@ -104,5 +104,5 @@ class UserPortal(Base):
class Contact(Base):
__tablename__ = "contact"
user: TelegramID = Column(Integer, ForeignKey("user.tgid"), primary_key=True)
contact: TelegramID = Column(Integer, ForeignKey("puppet.id"), primary_key=True)
user: TelegramID = Column(BigInteger, ForeignKey("user.tgid"), primary_key=True)
contact: TelegramID = Column(BigInteger, ForeignKey("puppet.id"), primary_key=True)
+5 -1
View File
@@ -194,8 +194,11 @@ bridge:
example.com: foobar
# Set to false to disable link previews in messages sent to Telegram.
telegram_link_preview: true
# Whether or not the !tg join command should do a HTTP request
# to resolve redirects in invite links.
invite_link_resolve: false
# Use inline images instead of a separate message for the caption.
# N.B. Inline images are not supported on all clients (e.g. Riot iOS).
# N.B. Inline images are not supported on all clients (e.g. Element iOS/Android).
inline_images: false
# Maximum size of image in megabytes before sending to Telegram as a document.
image_as_file_size: 10
@@ -204,6 +207,7 @@ bridge:
# Enable experimental parallel file transfer, which makes uploads/downloads much faster by
# streaming from/to Matrix and using many connections for Telegram.
# Note that generating HQ thumbnails for videos is not possible with streamed transfers.
# This option uses internal Telethon implementation details and may break with minor updates.
parallel_file_transfer: false
# Whether or not created rooms should have federation enabled.
# If false, created portal rooms will never be federated.
+2 -2
View File
@@ -79,7 +79,7 @@ async def _add_forward_header(source: 'AbstractUser', content: TextMessageEventC
try:
user = await source.client.get_entity(fwd_from.from_id)
if user:
fwd_from_text = pu.Puppet.get_displayname(user, False)
fwd_from_text, _ = pu.Puppet.get_displayname(user, False)
fwd_from_html = f"<b>{escape(fwd_from_text)}</b>"
except (ValueError, RPCError):
fwd_from_text = fwd_from_html = "unknown user"
@@ -87,7 +87,7 @@ async def _add_forward_header(source: 'AbstractUser', content: TextMessageEventC
from_id = (fwd_from.from_id.chat_id if isinstance(fwd_from.from_id, PeerChat)
else fwd_from.from_id.channel_id)
portal = po.Portal.get_by_tgid(TelegramID(from_id))
if portal:
if portal and portal.title:
fwd_from_text = portal.title
if portal.alias:
fwd_from_html = (f"<a href='https://matrix.to/#/{portal.alias}'>"
+7 -10
View File
@@ -94,9 +94,7 @@ class MatrixHandler(BaseMatrixHandler):
except MatrixError:
pass
portal.mxid = room_id
e2be_ok = None
if self.config["bridge.encryption.default"] and self.e2ee:
e2be_ok = await portal.enable_dm_encryption()
e2be_ok = await portal.check_dm_encryption()
await portal.save()
await inviter.register_portal(portal)
if e2be_ok is True:
@@ -283,13 +281,12 @@ class MatrixHandler(BaseMatrixHandler):
portal = po.Portal.get_by_mxid(room_id)
sender = await u.User.get_by_mxid(sender_mxid).ensure_started()
if await sender.has_full_access(allow_bot=True) and portal:
events = new_events - old_events
if len(events) > 0:
# New event pinned, set that as pinned in Telegram.
await portal.handle_matrix_pin(sender, EventID(events.pop()), event_id)
elif len(new_events) == 0:
# All pinned events removed, remove pinned event in Telegram.
await portal.handle_matrix_pin(sender, None, event_id)
if not new_events:
await portal.handle_matrix_unpin_all(sender, event_id)
else:
changes = {event_id: event_id in new_events
for event_id in new_events ^ old_events}
await portal.handle_matrix_pin(sender, changes, event_id)
@staticmethod
async def handle_room_upgrade(room_id: RoomID, sender: UserID, new_room_id: RoomID,
+27 -11
View File
@@ -15,22 +15,23 @@
# along with this program. If not, see <https://www.gnu.org/licenses/>.
from typing import Awaitable, Dict, List, Optional, Tuple, Union, Any, Set, Iterable, TYPE_CHECKING
from abc import ABC, abstractmethod
from datetime import datetime
import asyncio
import logging
import json
from telethon.tl.functions.messages import ExportChatInviteRequest
from telethon.tl.types import (Channel, ChannelFull, Chat, ChatFull, ChatInviteEmpty, InputChannel,
from telethon.tl.types import (Channel, ChannelFull, Chat, ChatFull, InputChannel,
InputPeerChannel, InputPeerChat, InputPeerUser, InputUser,
PeerChannel, PeerChat, PeerUser, TypeChat, TypeInputPeer, TypePeer,
TypeUser, TypeUserFull, User, UserFull, TypeInputChannel, Photo,
Document, TypePhotoSize, PhotoSize, InputPhotoFileLocation,
TypeChatParticipant, TypeChannelParticipant, PhotoEmpty, ChatPhoto,
ChatPhotoEmpty)
ChatPhotoEmpty, PhotoSizeProgressive, PhotoSizeEmpty)
from mautrix.errors import MatrixRequestError, IntentError
from mautrix.appservice import AppService, IntentAPI
from mautrix.types import (RoomID, RoomAlias, UserID, EventID, EventType, MessageEventContent,
from mautrix.types import (RoomID, RoomAlias, UserID, EventID, EventType,
PowerLevelStateEventContent, ContentURI)
from mautrix.util.simple_template import SimpleTemplate
from mautrix.util.simple_lock import SimpleLock
@@ -104,6 +105,7 @@ class BasePortal(MautrixBasePortal, ABC):
dedup: PortalDedup
send_lock: PortalSendLock
_pin_lock: asyncio.Lock
_db_instance: DBPortal
_main_intent: Optional[IntentAPI]
@@ -138,6 +140,7 @@ class BasePortal(MautrixBasePortal, ABC):
self.dedup = PortalDedup(self)
self.send_lock = PortalSendLock()
self._pin_lock = asyncio.Lock()
if tgid:
self.by_tgid[self.tgid_full] = self
@@ -181,6 +184,10 @@ class BasePortal(MautrixBasePortal, ABC):
elif self.peer_type == "channel":
return PeerChannel(channel_id=self.tgid)
@property
def is_direct(self) -> bool:
return self.peer_type == "user"
@property
def has_bot(self) -> bool:
return (bool(self.bot)
@@ -215,7 +222,18 @@ class BasePortal(MautrixBasePortal, ABC):
return config[f"bridge.{key}"]
@staticmethod
def _get_largest_photo_size(photo: Union[Photo, Document]
def _photo_size_key(photo: TypePhotoSize) -> int:
if isinstance(photo, PhotoSize):
return photo.size
elif isinstance(photo, PhotoSizeProgressive):
return max(photo.sizes)
elif isinstance(photo, PhotoSizeEmpty):
return 0
else:
return len(photo.bytes)
@classmethod
def _get_largest_photo_size(cls, photo: Union[Photo, Document]
) -> Tuple[Optional[InputPhotoFileLocation],
Optional[TypePhotoSize]]:
if not photo or isinstance(photo, PhotoEmpty) or (isinstance(photo, Document)
@@ -223,9 +241,7 @@ class BasePortal(MautrixBasePortal, ABC):
return None, None
largest = max(photo.thumbs if isinstance(photo, Document) else photo.sizes,
key=(lambda photo2: (len(photo2.bytes)
if not isinstance(photo2, PhotoSize)
else photo2.size)))
key=cls._photo_size_key)
return InputPhotoFileLocation(
id=photo.id,
access_hash=photo.access_hash,
@@ -264,14 +280,14 @@ class BasePortal(MautrixBasePortal, ABC):
return dialog.entity
raise
async def get_invite_link(self, user: 'u.User') -> str:
async def get_invite_link(self, user: 'u.User', uses: Optional[int] = None,
expire: Optional[datetime] = None) -> str:
if self.peer_type == "user":
raise ValueError("You can't invite users to private chats.")
if self.username:
return f"https://t.me/{self.username}"
link = await user.client(ExportChatInviteRequest(peer=await self.get_input_entity(user)))
if isinstance(link, ChatInviteEmpty):
raise ValueError("Failed to get invite link.")
link = await user.client(ExportChatInviteRequest(peer=await self.get_input_entity(user),
expire_date=expire, usage_limit=uses))
return link.link
# endregion
+2 -2
View File
@@ -61,9 +61,9 @@ class PortalDedup:
if isinstance(event, MessageService):
hash_content = [event.date.timestamp(), event.from_id, event.action]
else:
hash_content = [event.date.timestamp(), event.message]
hash_content = [event.date.timestamp(), event.message.strip()]
if event.fwd_from:
hash_content += [event.fwd_from.from_id, event.fwd_from.channel_id]
hash_content += [event.fwd_from.from_id]
elif isinstance(event, Message) and event.media:
try:
hash_content += {
+43 -26
View File
@@ -22,11 +22,10 @@ import magic
from telethon.tl.functions.messages import (EditChatPhotoRequest, EditChatTitleRequest,
UpdatePinnedMessageRequest, SetTypingRequest,
EditChatAboutRequest)
EditChatAboutRequest, UnpinAllMessagesRequest)
from telethon.tl.functions.channels import EditPhotoRequest, EditTitleRequest, JoinChannelRequest
from telethon.errors import (ChatNotModifiedError, PhotoExtInvalidError,
PhotoInvalidDimensionsError, PhotoSaveFileInvalidError,
RPCError)
from telethon.errors import (ChatNotModifiedError, PhotoExtInvalidError, MessageIdInvalidError,
PhotoInvalidDimensionsError, PhotoSaveFileInvalidError, RPCError)
from telethon.tl.patched import Message, MessageService
from telethon.tl.types import (DocumentAttributeFilename, DocumentAttributeImageSize, GeoPoint,
InputChatUploadedPhoto, MessageActionChatEditPhoto, MessageMediaGeo,
@@ -113,7 +112,19 @@ class PortalMatrix(BasePortal, ABC):
space = self.tgid if self.peer_type == "channel" else user.tgid
message = DBMessage.get_by_mxid(event_id, self.mxid, space)
if not message:
return
message = DBMessage.find_last(self.mxid, space)
if not message:
self.log.debug(f"Dropping Matrix read receipt from {user.mxid}: "
f"target message {event_id} not known and last message"
" in chat not found")
return
else:
self.log.debug(f"Matrix read receipt target {event_id} not known, marking "
f"messages up to most recent ({message.mxid}/{message.tgid}) "
f"as read by {user.mxid}/{user.tgid}")
else:
self.log.debug("Handling Matrix read receipt: marking messages up to "
f"{message.mxid}/{message.tgid} as read by {user.mxid}/{user.tgid}")
await user.client.send_read_acknowledge(self.peer, max_id=message.tgid,
clear_mentions=True)
@@ -327,7 +338,7 @@ class PortalMatrix(BasePortal, ABC):
self.log.exception("Failed to parse location")
return None
caption, entities = await formatter.matrix_to_telegram(client, text=content.body)
media = MessageMediaGeo(geo=GeoPoint(lat, long, access_hash=0))
media = MessageMediaGeo(geo=GeoPoint(lat=lat, long=long, access_hash=0))
async with self.send_lock(sender_id):
if await self._matrix_document_edit(client, content, space, caption, media, event_id):
@@ -412,23 +423,23 @@ class PortalMatrix(BasePortal, ABC):
else:
self.log.trace("Unhandled Matrix event: %s", content)
async def handle_matrix_pin(self, sender: 'u.User', pinned_message: Optional[EventID],
async def handle_matrix_unpin_all(self, sender: 'u.User', pin_event_id: EventID) -> None:
await sender.client(UnpinAllMessagesRequest(peer=self.peer))
await self._send_delivery_receipt(pin_event_id)
async def handle_matrix_pin(self, sender: 'u.User', changes: Dict[EventID, bool],
pin_event_id: EventID) -> None:
if self.peer_type != "chat" and self.peer_type != "channel":
return
try:
if not pinned_message:
await sender.client(UpdatePinnedMessageRequest(peer=self.peer, id=0))
else:
tg_space = self.tgid if self.peer_type == "channel" else sender.tgid
message = DBMessage.get_by_mxid(pinned_message, self.mxid, tg_space)
if message is None:
self.log.warning(f"Could not find pinned {pinned_message} in {self.mxid}")
return
await sender.client(UpdatePinnedMessageRequest(peer=self.peer, id=message.tgid))
await self._send_delivery_receipt(pin_event_id)
except ChatNotModifiedError:
pass
tg_space = self.tgid if self.peer_type == "channel" else sender.tgid
ids = {msg.mxid: msg.tgid
for msg in DBMessage.get_by_mxids(list(changes.keys()),
mx_room=self.mxid, tg_space=tg_space)}
for event_id, pinned in changes.items():
try:
await sender.client(UpdatePinnedMessageRequest(peer=self.peer, id=ids[event_id],
unpin=not pinned))
except (ChatNotModifiedError, MessageIdInvalidError, KeyError):
pass
await self._send_delivery_receipt(pin_event_id)
async def handle_matrix_deletion(self, deleter: 'u.User', event_id: EventID,
redaction_event_id: EventID) -> None:
@@ -436,12 +447,18 @@ class PortalMatrix(BasePortal, ABC):
space = self.tgid if self.peer_type == "channel" else real_deleter.tgid
message = DBMessage.get_by_mxid(event_id, self.mxid, space)
if not message:
return
if message.edit_index == 0:
self.log.trace(f"Ignoring Matrix redaction of unknown event {event_id}")
elif message.redacted:
self.log.debug("Ignoring Matrix redaction of already redacted event "
f"{message.mxid} in {message.mx_room}")
elif message.edit_index != 0:
message.edit(redacted=True)
self.log.debug("Ignoring Matrix redaction of edit event "
f"{message.mxid} in {message.mx_room}")
else:
message.edit(redacted=True)
await real_deleter.client.delete_messages(self.peer, [message.tgid])
await self._send_delivery_receipt(redaction_event_id)
else:
self.log.debug(f"Ignoring deletion of edit event {message.mxid} in {message.mx_room}")
async def _update_telegram_power_level(self, sender: 'u.User', user_id: TelegramID,
level: int) -> None:
+25 -15
View File
@@ -13,7 +13,7 @@
#
# 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 List, Optional, Iterable, Union, Dict, Any, TYPE_CHECKING
from typing import List, Optional, Iterable, Union, Dict, Any, Tuple, TYPE_CHECKING
from abc import ABC
import asyncio
@@ -26,7 +26,8 @@ from telethon.tl.types import (
Channel, ChatBannedRights, ChannelParticipantsRecent, ChannelParticipantsSearch, ChatPhoto,
PhotoEmpty, InputChannel, InputUser, ChatPhotoEmpty, PeerUser, Photo, TypeChat, TypeInputPeer,
TypeUser, User, InputPeerPhotoFileLocation, ChatParticipantAdmin, ChannelParticipantAdmin,
ChatParticipantCreator, ChannelParticipantCreator, UserProfilePhoto, UserProfilePhotoEmpty)
ChatParticipantCreator, ChannelParticipantCreator, UserProfilePhoto, UserProfilePhotoEmpty,
InputPeerUser)
from mautrix.errors import MForbidden
from mautrix.types import (RoomID, UserID, RoomCreatePreset, EventType, Membership,
@@ -58,21 +59,30 @@ class PortalMetadata(BasePortal, ABC):
# region Matrix -> Telegram
async def _get_telegram_users_in_matrix_room(self) -> List[Union[InputUser, PeerUser]]:
user_tgids = set()
async def get_telegram_users_in_matrix_room(self, source: 'u.User'
) -> Tuple[List[InputPeerUser], List[UserID]]:
user_tgids = {}
user_mxids = await self.main_intent.get_room_members(self.mxid, (Membership.JOIN,
Membership.INVITE))
for user_str in user_mxids:
user = UserID(user_str)
if user == self.az.bot_mxid:
for mxid in user_mxids:
if mxid == self.az.bot_mxid:
continue
mx_user = u.User.get_by_mxid(user, create=False)
mx_user = u.User.get_by_mxid(mxid, create=False)
if mx_user and mx_user.tgid:
user_tgids.add(mx_user.tgid)
puppet_id = p.Puppet.get_id_from_mxid(user)
user_tgids[mx_user.tgid] = mxid
puppet_id = p.Puppet.get_id_from_mxid(mxid)
if puppet_id:
user_tgids.add(puppet_id)
return [PeerUser(user_id) for user_id in user_tgids]
user_tgids[puppet_id] = mxid
input_users = []
errors = []
for tgid, mxid in user_tgids.items():
try:
input_users.append(await source.client.get_input_entity(tgid))
except ValueError as e:
source.log.debug(f"Failed to find the input entity for {tgid} ({mxid}) for "
f"creating a group: {e}")
errors.append(mxid)
return input_users, errors
async def upgrade_telegram_chat(self, source: 'u.User') -> None:
if self.peer_type != "chat":
@@ -116,13 +126,13 @@ class PortalMetadata(BasePortal, ABC):
if await self._update_username(username):
await self.save()
async def create_telegram_chat(self, source: 'u.User', supergroup: bool = False) -> None:
async def create_telegram_chat(self, source: 'u.User', invites: List[InputUser],
supergroup: bool = False) -> None:
if not self.mxid:
raise ValueError("Can't create Telegram chat for portal without Matrix room.")
elif self.tgid:
raise ValueError("Can't create Telegram chat for portal with existing Telegram chat.")
invites = await self._get_telegram_users_in_matrix_room()
if len(invites) < 2:
if self.bot is not None:
info, mxid = await self.bot.get_me()
@@ -450,7 +460,7 @@ class PortalMetadata(BasePortal, ABC):
levels.kick = overrides.get("kick", 50)
levels.redact = overrides.get("redact", 50)
levels.invite = overrides.get("invite", 50 if dbr.invite_users else 0)
levels.events[EventType.ROOM_ENCRYPTION] = 99
levels.events[EventType.ROOM_ENCRYPTION] = 50 if self.matrix.e2ee else 99
levels.events[EventType.ROOM_TOMBSTONE] = 99
levels.events[EventType.ROOM_NAME] = 50 if dbr.change_info else 0
levels.events[EventType.ROOM_AVATAR] = 50 if dbr.change_info else 0
+83 -13
View File
@@ -34,7 +34,7 @@ from telethon.tl.types import (
MessageMediaPhoto, MessageMediaDice, MessageMediaGame, MessageMediaUnsupported, PeerUser,
PhotoCachedSize, TypeChannelParticipant, TypeChatParticipant, TypeDocumentAttribute,
TypeMessageAction, TypePhotoSize, PhotoSize, UpdateChatUserTyping, UpdateUserTyping,
MessageEntityPre, ChatPhotoEmpty)
MessageEntityPre, ChatPhotoEmpty, DocumentAttributeImageSize)
from mautrix.appservice import IntentAPI
from mautrix.types import (EventID, UserID, ImageInfo, ThumbnailInfo, RelatesTo, MessageType,
@@ -109,8 +109,7 @@ class PortalTelegram(BasePortal, ABC):
return await self._send_message(intent, content, timestamp=evt.date)
info = ImageInfo(
height=largest_size.h, width=largest_size.w, orientation=0, mimetype=file.mime_type,
size=(len(largest_size.bytes) if (isinstance(largest_size, PhotoCachedSize))
else largest_size.size))
size=self._photo_size_key(largest_size))
ext = sane_mimetypes.guess_extension(file.mime_type)
name = f"disappearing_image{ext}" if media.ttl_seconds else f"image{ext}"
await intent.set_typing(self.mxid, is_typing=False)
@@ -144,6 +143,8 @@ class PortalTelegram(BasePortal, ABC):
sticker_alt = attr.alt
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)
@staticmethod
@@ -185,7 +186,7 @@ class PortalTelegram(BasePortal, ABC):
width=file.thumbnail.width or thumb_size.w,
size=file.thumbnail.size)
else:
# This is a hack for bad clients like Riot iOS that require a thumbnail
# This is a hack for bad clients like Element iOS that require a thumbnail
if file.decryption_info:
info.thumbnail_file = file.decryption_info
else:
@@ -226,9 +227,21 @@ class PortalTelegram(BasePortal, ABC):
await intent.set_typing(self.mxid, is_typing=False)
event_type = EventType.ROOM_MESSAGE
# Riot only supports images as stickers, so send animated webm stickers as m.video
# Elements only support images as stickers, so send animated webm stickers as m.video
if attrs.is_sticker and file.mime_type.startswith("image/"):
event_type = EventType.STICKER
# Tell clients to render the stickers as 256x256 if they're bigger
if info.width > 256 or info.height > 256:
if info.width > info.height:
info.height = int(info.height / (info.width / 256))
info.width = 256
else:
info.width = int(info.width / (info.height / 256))
info.height = 256
if info.thumbnail_info:
info.thumbnail_info.width = info.width
info.thumbnail_info.height = info.height
content = MediaMessageEventContent(
body=name or "unnamed file", info=info, relates_to=relates_to,
external_url=self._get_external_url(evt),
@@ -318,16 +331,63 @@ class PortalTelegram(BasePortal, ABC):
await intent.set_typing(self.mxid, is_typing=False)
return await self._send_message(intent, content, timestamp=evt.date)
@staticmethod
def _format_dice(roll: MessageMediaDice) -> str:
if roll.emoticon == "\U0001F3B0":
emojis = {
0: "\U0001F36B", # "🍫",
1: "\U0001F352", # "🍒",
2: "\U0001F34B", # "🍋",
3: "7\ufe0f\u20e3" # "7️⃣",
}
res = roll.value - 1
slot1, slot2, slot3 = emojis[res % 4], emojis[res // 4 % 4], emojis[res // 16]
return f"{slot1} {slot2} {slot3} ({roll.value})"
elif roll.emoticon == "\u26BD":
results = {
1: "miss",
2: "hit the woodwork",
3: "goal", # seems to go in through the center
4: "goal",
5: "goal 🎉", # seems to go in through the top right corner, includes confetti
}
elif roll.emoticon == "\U0001F3B3":
results = {
1: "miss",
2: "1 pin down",
3: "3 pins down, split",
4: "4 pins down, split",
5: "5 pins down",
6: "strike 🎉",
}
# elif roll.emoticon == "\U0001F3C0":
# results = {
# 2: "rolled off",
# 3: "stuck",
# }
# elif roll.emoticon == "\U0001F3AF":
# results = {
# 1: "bounced off",
# 2: "outer rim",
#
# 6: "bullseye",
# }
else:
return str(roll.value)
return f"{results[roll.value]} ({roll.value})"
async def handle_telegram_dice(self, source: 'AbstractUser', intent: IntentAPI, evt: Message,
relates_to: RelatesTo) -> EventID:
emoji_text = {
"\U0001F3AF": " Dart throw",
"\U0001F3B2": " Dice roll",
"\U0001F3C0": " Basketball throw",
"\U0001F3B0": " Slot machine",
"\U0001F3B3": " Bowling",
"\u26BD": " Football kick"
}
roll: MessageMediaDice = evt.media
text = f"{roll.emoticon}{emoji_text.get(roll.emoticon, '')} result: {roll.value}"
text = f"{roll.emoticon}{emoji_text.get(roll.emoticon, '')} result: {self._format_dice(roll)}"
content = TextMessageEventContent(msgtype=MessageType.TEXT, format=Format.HTML, body=text,
formatted_body=f"<h4>{text}</h4>", relates_to=relates_to,
external_url=self._get_external_url(evt))
@@ -575,6 +635,9 @@ class PortalTelegram(BasePortal, ABC):
"displayname, updating info...")
entity = await source.client.get_entity(PeerUser(sender.tgid))
await sender.update_info(source, entity)
if not sender.displayname:
self.log.debug(f"Telegram user {sender.tgid} doesn't have a displayname even after"
f" updating with data {entity!s}")
allowed_media = (MessageMediaPhoto, MessageMediaDocument, MessageMediaGeo,
MessageMediaGame, MessageMediaDice, MessageMediaPoll,
@@ -694,13 +757,20 @@ class PortalTelegram(BasePortal, ABC):
levels.users[puppet.mxid] = 50
await self.main_intent.set_power_levels(self.mxid, levels)
async def receive_telegram_pin_id(self, msg_id: TelegramID, receiver: TelegramID) -> None:
tg_space = receiver if self.peer_type != "channel" else self.tgid
message = DBMessage.get_one_by_tgid(msg_id, tg_space) if msg_id != 0 else None
if message:
await self.main_intent.set_pinned_messages(self.mxid, [message.mxid])
else:
await self.main_intent.set_pinned_messages(self.mxid, [])
async def receive_telegram_pin_ids(self, msg_ids: List[TelegramID], receiver: TelegramID,
remove: bool) -> None:
async with self._pin_lock:
tg_space = receiver if self.peer_type != "channel" else self.tgid
previously_pinned = await self.main_intent.get_pinned_messages(self.mxid)
currently_pinned_dict = {event_id: True for event_id in previously_pinned}
for message in DBMessage.get_first_by_tgids(msg_ids, tg_space):
if remove:
currently_pinned_dict.pop(message.mxid, None)
else:
currently_pinned_dict[message.mxid] = True
currently_pinned = list(currently_pinned_dict.keys())
if currently_pinned != previously_pinned:
await self.main_intent.set_pinned_messages(self.mxid, currently_pinned)
async def set_telegram_admins_enabled(self, enabled: bool) -> None:
level = 50 if enabled else 10
+46 -21
View File
@@ -13,7 +13,7 @@
#
# 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 Awaitable, Any, Dict, Iterable, Optional, Union, TYPE_CHECKING
from typing import Awaitable, Any, Dict, Iterable, Optional, Union, Tuple, TYPE_CHECKING
from difflib import SequenceMatcher
import unicodedata
import asyncio
@@ -24,10 +24,11 @@ from telethon.tl.types import (UserProfilePhoto, User, UpdateUserName, PeerUser,
from yarl import URL
from mautrix.appservice import AppService, IntentAPI
from mautrix.errors import MatrixRequestError
from mautrix.errors import MatrixRequestError, MatrixError
from mautrix.bridge import BasePuppet
from mautrix.types import UserID, SyncToken, RoomID
from mautrix.types import UserID, SyncToken, RoomID, ContentURI
from mautrix.util.simple_template import SimpleTemplate
from mautrix.util.logging import TraceLogger
from .types import TelegramID
from .db import Puppet as DBPuppet
@@ -43,7 +44,7 @@ config: Optional['Config'] = None
class Puppet(BasePuppet):
log: logging.Logger = logging.getLogger("mau.puppet")
log: TraceLogger = logging.getLogger("mau.puppet")
az: AppService
mx: 'MatrixHandler'
loop: asyncio.AbstractEventLoop
@@ -64,6 +65,8 @@ class Puppet(BasePuppet):
username: Optional[str]
displayname: Optional[str]
displayname_source: Optional[TelegramID]
displayname_contact: bool
displayname_quality: int
photo_id: Optional[str]
is_bot: bool
is_registered: bool
@@ -85,6 +88,8 @@ class Puppet(BasePuppet):
username: Optional[str] = None,
displayname: Optional[str] = None,
displayname_source: Optional[TelegramID] = None,
displayname_contact: bool = True,
displayname_quality: int = 0,
photo_id: Optional[str] = None,
is_bot: bool = False,
is_registered: bool = False,
@@ -100,6 +105,8 @@ class Puppet(BasePuppet):
self.username = username
self.displayname = displayname
self.displayname_source = displayname_source
self.displayname_contact = displayname_contact
self.displayname_quality = displayname_quality
self.photo_id = photo_id
self.is_bot = is_bot
self.is_registered = is_registered
@@ -164,8 +171,10 @@ class Puppet(BasePuppet):
return dict(access_token=self.access_token, next_batch=self._next_batch,
custom_mxid=self.custom_mxid, username=self.username, is_bot=self.is_bot,
displayname=self.displayname, displayname_source=self.displayname_source,
photo_id=self.photo_id, matrix_registered=self.is_registered,
disable_updates=self.disable_updates, base_url=self.base_url)
displayname_contact=self.displayname_contact,
displayname_quality=self.displayname_quality, photo_id=self.photo_id,
matrix_registered=self.is_registered, disable_updates=self.disable_updates,
base_url=str(self.base_url) if self.base_url else None)
def new_db_instance(self) -> DBPuppet:
return DBPuppet(id=self.id, **self._fields)
@@ -177,9 +186,10 @@ class Puppet(BasePuppet):
def from_db(cls, db_puppet: DBPuppet) -> 'Puppet':
return Puppet(db_puppet.id, db_puppet.access_token, db_puppet.custom_mxid,
db_puppet.next_batch, db_puppet.base_url, db_puppet.username,
db_puppet.displayname, db_puppet.displayname_source, db_puppet.photo_id,
db_puppet.is_bot, db_puppet.matrix_registered, db_puppet.disable_updates,
db_instance=db_puppet)
db_puppet.displayname, db_puppet.displayname_source,
db_puppet.displayname_contact, db_puppet.displayname_quality,
db_puppet.photo_id, db_puppet.is_bot, db_puppet.matrix_registered,
db_puppet.disable_updates, db_instance=db_puppet)
# endregion
# region Info updating
@@ -203,7 +213,7 @@ class Puppet(BasePuppet):
return name
@classmethod
def get_displayname(cls, info: User, enable_format: bool = True) -> str:
def get_displayname(cls, info: User, enable_format: bool = True) -> Tuple[str, int]:
fn = cls._filter_name(info.first_name)
ln = cls._filter_name(info.last_name)
data = {
@@ -216,19 +226,21 @@ class Puppet(BasePuppet):
}
preferences = config["bridge.displayname_preference"]
name = None
quality = 99
for preference in preferences:
name = data[preference]
if name:
break
quality -= 1
if isinstance(info, User) and info.deleted:
name = f"Deleted account {info.id}"
quality = 99
elif not name:
name = str(info.id)
quality = 0
if not enable_format:
return name
return cls.displayname_template.format_full(name)
return (cls.displayname_template.format_full(name) if enable_format else name), quality
async def try_update_info(self, source: 'AbstractUser', info: User) -> None:
try:
@@ -264,27 +276,40 @@ class Puppet(BasePuppet):
allow_because = "user is the primary source"
elif not isinstance(info, UpdateUserName) and not info.contact:
allow_because = "user is not a contact"
elif self.displayname_source is None:
elif not self.displayname_source:
allow_because = "no primary source set"
elif not self.displayname:
allow_because = "user has no name"
else:
return False
if isinstance(info, UpdateUserName):
info = await source.client.get_entity(PeerUser(self.tgid))
if not info.contact:
self.displayname_contact = False
elif not self.displayname_contact:
if not self.displayname:
self.displayname_contact = True
else:
return False
displayname = self.get_displayname(info)
if displayname != self.displayname:
displayname, quality = self.get_displayname(info)
if displayname != self.displayname and quality >= self.displayname_quality:
allow_because = f"{allow_because} and quality {quality} >= {self.displayname_quality}"
self.log.debug(f"Updating displayname of {self.id} (src: {source.tgid}, allowed "
f"because {allow_because}) from {self.displayname} to {displayname}")
self.log.trace("Displayname source data: %s", info)
self.displayname = displayname
self.displayname_source = source.tgid
self.displayname_quality = quality
try:
await self.default_mxid_intent.set_displayname(
displayname[:config["bridge.displayname_max_length"]])
except MatrixRequestError:
except MatrixError:
self.log.exception("Failed to set displayname")
self.displayname = ""
self.displayname_source = None
self.displayname_quality = 0
return True
elif source.is_relaybot or self.displayname_source is None:
self.displayname_source = source.tgid
@@ -309,8 +334,8 @@ class Puppet(BasePuppet):
if not photo_id:
self.photo_id = ""
try:
await self.default_mxid_intent.set_avatar_url("")
except MatrixRequestError:
await self.default_mxid_intent.set_avatar_url(ContentURI(""))
except MatrixError:
self.log.exception("Failed to set avatar")
self.photo_id = ""
return True
@@ -326,13 +351,13 @@ class Puppet(BasePuppet):
self.photo_id = photo_id
try:
await self.default_mxid_intent.set_avatar_url(file.mxc)
except MatrixRequestError:
except MatrixError:
self.log.exception("Failed to set avatar")
self.photo_id = ""
return True
return False
def default_puppet_should_leave_room(self, room_id: RoomID) -> bool:
async def default_puppet_should_leave_room(self, room_id: RoomID) -> bool:
portal: p.Portal = p.Portal.get_by_mxid(room_id)
return portal and not portal.backfill_lock.locked and portal.peer_type != "user"
+34 -13
View File
@@ -19,7 +19,7 @@ from collections import defaultdict
import logging
import asyncio
from telethon.tl.types import (TypeUpdate, UpdateNewMessage, UpdateNewChannelMessage, PeerUser,
from telethon.tl.types import (TypeUpdate, UpdateNewMessage, UpdateNewChannelMessage,
UpdateShortChatMessage, UpdateShortMessage, User as TLUser, Chat,
ChatForbidden)
from telethon.tl.custom import Dialog
@@ -35,7 +35,7 @@ from mautrix.util.logging import TraceLogger
from mautrix.util.opt_prometheus import Gauge
from .types import TelegramID
from .db import User as DBUser, Portal as DBPortal
from .db import User as DBUser, Portal as DBPortal, Message as DBMessage
from .abstract_user import AbstractUser
from . import portal as po, puppet as pu
@@ -376,6 +376,34 @@ class User(AbstractUser, BaseUser):
if portal.mxid
}
async def _sync_dialog(self, portal: po.Portal, dialog: Dialog, should_create: bool,
puppet: Optional[pu.Puppet]) -> None:
if portal.mxid:
try:
await portal.backfill(self, last_id=dialog.message.id)
except Exception:
self.log.exception(f"Error while backfilling {portal.tgid_log}")
try:
await portal.update_matrix_room(self, dialog.entity)
except Exception:
self.log.exception(f"Error while updating {portal.tgid_log}")
elif should_create:
try:
await portal.create_matrix_room(self, dialog.entity, invites=[self.mxid])
except Exception:
self.log.exception(f"Error while creating {portal.tgid_log}")
if portal.mxid and puppet and puppet.is_real_user:
tg_space = portal.tgid if portal.peer_type == "channel" else self.tgid
if dialog.unread_count == 0:
# This is usually more reliable than finding a specific message
# e.g. if the last read message is a service message that isn't in the message db
last_read = DBMessage.find_last(portal.mxid, tg_space)
else:
last_read = DBMessage.get_one_by_tgid(portal.tgid, tg_space,
dialog.dialog.read_inbox_max_id)
if last_read:
await puppet.intent.mark_read(last_read.mx_room, last_read.mxid)
async def sync_dialogs(self) -> None:
if self.is_bot:
return
@@ -385,6 +413,7 @@ class User(AbstractUser, BaseUser):
index = 0
self.log.debug(f"Syncing dialogs (update_limit={update_limit}, "
f"create_limit={create_limit})")
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,
archived=False):
@@ -400,17 +429,9 @@ class User(AbstractUser, BaseUser):
continue
portal = po.Portal.get_by_entity(entity, receiver_id=self.tgid)
self.portals[portal.tgid_full] = portal
if portal.mxid:
update_task = portal.update_matrix_room(self, entity)
backfill_task = portal.backfill(self, last_id=dialog.message.id)
creators.append(self._catch(f"updating {portal.tgid_log}",
self.loop.create_task(update_task)))
creators.append(self._catch(f"backfilling {portal.tgid_log}",
self.loop.create_task(backfill_task)))
elif not create_limit or index < create_limit:
create_task = portal.create_matrix_room(self, entity, invites=[self.mxid])
creators.append(self._catch(f"creating {portal.tgid_log}",
self.loop.create_task(create_task)))
coro = self._sync_dialog(portal=portal, dialog=dialog, puppet=puppet,
should_create=not create_limit or index < create_limit)
creators.append(self.loop.create_task(coro))
index += 1
await self.save(portals=True)
await asyncio.gather(*creators)
+3 -11
View File
@@ -222,9 +222,9 @@ async def _unlocked_transfer_file_to_matrix(client: MautrixTelegramClient, inten
image_converted = False
# A weird bug in alpine/magic makes it return application/octet-stream for gzips...
is_tgs = (mime_type == "application/gzip" or (mime_type == "application/octet-stream"
and magic.from_buffer(file).startswith(
"gzip")))
is_tgs = (mime_type == "application/gzip"
or (mime_type == "application/octet-stream"
and magic.from_buffer(file).startswith("gzip")))
if is_sticker and tgs_convert and is_tgs:
converted_anim = await convert_tgs_to(file, tgs_convert["target"],
**tgs_convert["args"])
@@ -234,14 +234,6 @@ async def _unlocked_transfer_file_to_matrix(client: MautrixTelegramClient, inten
image_converted = mime_type != "application/gzip"
thumbnail = None
if mime_type == "image/webp":
new_mime_type, file, width, height = convert_image(
file, source_mime="image/webp", target_type="png",
thumbnail_to=(256, 256) if is_sticker else None)
image_converted = new_mime_type != mime_type
mime_type = new_mime_type
thumbnail = None
decryption_info = None
upload_mime_type = mime_type
if encrypt and encrypt_attachment:
@@ -27,8 +27,10 @@ from telethon.tl.types import (Document, InputFileLocation, InputDocumentFileLoc
InputPhotoFileLocation, InputPeerPhotoFileLocation, TypeInputFile,
InputFileBig, InputFile)
from telethon.tl.functions.auth import ExportAuthorizationRequest, ImportAuthorizationRequest
from telethon.tl.functions import InvokeWithLayerRequest
from telethon.tl.functions.upload import (GetFileRequest, SaveFilePartRequest,
SaveBigFilePartRequest)
from telethon.tl.alltlobjects import LAYER
from telethon.network import MTProtoSender
from telethon.crypto import AuthKey
from telethon import utils, helpers
@@ -193,9 +195,9 @@ class ParallelTransferrer:
if not self.auth_key:
log.debug(f"Exporting auth to DC {self.dc_id}")
auth = await self.client(ExportAuthorizationRequest(self.dc_id))
req = self.client._init_with(ImportAuthorizationRequest(
id=auth.id, bytes=auth.bytes
))
self.client._init_request.query = ImportAuthorizationRequest(id=auth.id,
bytes=auth.bytes)
req = InvokeWithLayerRequest(LAYER, self.client._init_request)
await sender.send(req)
self.auth_key = sender.auth_key
return sender
+3 -6
View File
@@ -7,24 +7,21 @@ cchardet
aiodns
brotli
#/webp_convert
pillow>=4,<8
#/qr_login
pillow>=4,<8
pillow>=4,<9
qrcode>=6,<7
#/hq_thumbnails
moviepy>=1,<2
#/metrics
prometheus_client>=0.6,<0.9
prometheus_client>=0.6,<0.11
#/postgres
psycopg2-binary>=2,<3
#/e2be
asyncpg>=0.20,<0.22
asyncpg>=0.20,<0.23
python-olm>=3,<4
pycryptodome>=3,<4
unpaddedbase64>=1,<2
+4 -4
View File
@@ -1,10 +1,10 @@
SQLAlchemy>=1.2,<2
SQLAlchemy>=1.2,<1.4
alembic>=1,<2
ruamel.yaml>=0.15.35,<0.17
ruamel.yaml>=0.15.35,<0.18
python-magic>=0.4,<0.5
commonmark>=0.8,<0.10
aiohttp>=3,<4
yarl>=1,<2
mautrix>=0.8.3,<0.9
telethon>=1.17,<1.18
mautrix>=0.8.11,<0.9
telethon>=1.20,<1.22
telethon-session-sqlalchemy>=0.2.14,<0.3
+1 -1
View File
@@ -49,7 +49,7 @@ setuptools.setup(
install_requires=install_requires,
extras_require=extras_require,
python_requires="~=3.6",
python_requires="~=3.7",
setup_requires=["pytest-runner"],
tests_require=["pytest", "pytest-asyncio", "pytest-mock"],