Compare commits

..

30 Commits

Author SHA1 Message Date
Tulir Asokan 2cc439853f Bump version to 0.10.2 2021-11-13 14:40:39 +02:00
Tulir Asokan 76b2937c18 Update mautrix-python and stop supporting pickle for crypto store
SQLite is now supported for the crypto store instead of pickle (via aiosqlite)
2021-11-13 14:15:32 +02:00
Tulir Asokan f2a9f4ab33 Merge pull request #639 from olmari/patch-1
Add linebreak to "legend"
2021-11-12 23:06:26 +02:00
Tulir Asokan ec375e79d7 Merge pull request #680 from tadzik/tadzik/fix-max-initial-sync-for-chats
Make max_initial_member_sync work for Chats as well as Channels
2021-11-12 23:04:22 +02:00
Tulir Asokan 338a4d9761 Pin Pillow version in dockerfile to same as alpine. Fixes #683 2021-11-01 18:56:08 +02:00
Tulir Asokan 83d457f2b3 Ignore ChannelParticipantBanned in participant list. Fixes #635 2021-10-29 20:24:42 +03:00
Sumner Evans 3507095572 Merge pull request #681 from justinbot/justinbot/dont-log-messages
Don't log entire message contents on exception
2021-10-29 11:24:15 -06:00
Justin Carlson 4e7cf481fd Don't log entire messagecontents on exception. 2021-10-29 12:30:49 -04:00
Tadeusz Sośnierz 0915bb9402 Make max_initial_member_sync work for Chats as well as Channels 2021-10-27 14:46:12 +02:00
Sumner Evans 7c5d1c2959 Merge pull request #676 from justinbot/justinbot/welcome-text-config
Add example config for custom welcome messages
2021-10-26 09:51:08 -06:00
Justin Carlson 8aecf1f84b Update example config. 2021-10-23 12:10:36 -04:00
Justin Carlson 2c45d8dd5b Remove send_welcome_message override 2021-10-23 12:09:16 -04:00
Justin Carlson fac337eaf1 Add example config for welcome messages. 2021-10-22 12:17:25 -04:00
Tulir Asokan e7d8948334 Bump Telethon to update to latest version of layer 133 2021-10-20 21:20:05 +03:00
Tulir Asokan 6b8831872c Allow logout even if session isn't authorized 2021-10-20 20:55:11 +03:00
Tulir Asokan 4e8c373d1b Delete session on log_out() even if telegram logout fails 2021-10-20 20:24:47 +03:00
Tulir Asokan 8865dab6b0 Push bad credentials state if session isn't valid in start() 2021-10-20 20:12:23 +03:00
Tulir Asokan e4a2bd2f69 Catch authorization errors in get_me() 2021-10-20 20:02:09 +03:00
Tulir Asokan a132916525 Update Telethon
The upstream dev doesn't want to make new releases anymore before 2.0,
so this is temporarily using a fork. The main change is API layer 133,
which updates all user/chat IDs to be 64-bit.
2021-10-19 12:40:34 +03:00
Tulir Asokan a9dcb34b2d Use existing power levels as base for user levels instead of hardcoded values 2021-10-19 12:40:34 +03:00
Tulir Asokan 74c43355e4 Decrypt fetched messages to generate reply fallback 2021-10-19 12:40:34 +03:00
Sumner Evans 7255e86595 Merge pull request #670 from mautrix/ci-update-container-versions-on-success
ci: only update container versions on success
2021-10-14 12:30:36 -06:00
Sumner Evans e4098a226e ci: only update container versions on success 2021-10-14 09:45:39 -06:00
Sumner Evans 5dea5977ad Merge pull request #662 from mautrix/ci-auto-update-version
ci: deploy to dev stable and internal automatically
2021-09-17 19:04:14 -04:00
Sumner Evans 1c9a30773e ci: deploy to dev stable and internal automatically 2021-09-17 12:19:14 -04:00
Tulir Asokan e276944b40 Implement get_bridge_states 2021-08-25 16:04:50 +03:00
Tulir Asokan 2e14991815 Remove element ios hack from non-sticker documents 2021-08-20 14:00:42 +03:00
Tulir Asokan 3083727aff Add extension to unnamed file names. Fixes #646 2021-08-20 14:00:25 +03:00
Tulir Asokan d778c639dc Bump maximum Telethon version 2021-08-19 15:08:20 +03:00
Sami Olmari bcede7710f Add linebreak to "legend"
Signed-off-by: Sami Olmari <sami@olmari.fi>
2021-07-06 09:57:33 +03:00
16 changed files with 149 additions and 79 deletions
+21 -3
View File
@@ -19,10 +19,28 @@ build amd64:
- docker rmi $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA-amd64
after_script:
- |
if [ "$CI_COMMIT_BRANCH" = "master" ]; then
apk add --update curl
if [[ "$CI_COMMIT_BRANCH" == "master" && "$CI_JOB_STATUS" == "success" ]]; then
apk add --update curl jq
rm -rf /var/cache/apk/*
curl "$NOVA_ADMIN_API_URL" -H "Content-Type: application/json" -d '{"password":"'"$NOVA_ADMIN_NIGHTLY_PASS"'","bridge":"'$NOVA_BRIDGE_TYPE'","image":"'$CI_REGISTRY_IMAGE':'$CI_COMMIT_SHA'-amd64"}'
jq -n '
{
password: env.BEEPER_DEV_ADMIN_NIGHTLY_PASS,
bridge: env.BEEPER_BRIDGE_TYPE,
image: "\(env.CI_REGISTRY_IMAGE):\(env.CI_COMMIT_SHA)-amd64",
channel: "STABLE"
}
' | curl "$BEEPER_DEV_ADMIN_API_URL" -H "Content-Type: application/json" -d @-
jq -n '
{
password: env.BEEPER_PROD_ADMIN_NIGHTLY_PASS,
bridge: env.BEEPER_BRIDGE_TYPE,
image: "\(env.CI_REGISTRY_IMAGE):\(env.CI_COMMIT_SHA)-amd64",
channel: "INTERNAL",
deployNext: true
}
' | curl "$BEEPER_PROD_ADMIN_API_URL" -H "Content-Type: application/json" -d @-
fi
build arm64:
+2 -1
View File
@@ -54,7 +54,8 @@ RUN apk add --virtual .build-deps \
libffi-dev \
build-base \
&& sed -Ei 's/psycopg2-binary.+//' optional-requirements.txt \
&& pip3 install -r requirements.txt -r optional-requirements.txt \
# TODO: unpin Pillow here after it's updated in Alpine
&& pip3 install -r requirements.txt -r optional-requirements.txt 'pillow==8.2' \
&& apk del .build-deps
COPY . /opt/mautrix-telegram
+1 -1
View File
@@ -59,5 +59,5 @@
* [ ] ‡ Secret chats (i.e. End-to-bridge encryption on Telegram)
* [x] End-to-bridge encryption in Matrix rooms (see [wiki](https://github.com/tulir/mautrix-telegram/wiki/End%E2%80%90to%E2%80%90bridge-encryption))
† Information not automatically sent from source, i.e. implementation may not be possible
† Information not automatically sent from source, i.e. implementation may not be possible
‡ Maybe, i.e. this feature may or may not be implemented at some point
+1 -1
View File
@@ -1,2 +1,2 @@
__version__ = "0.10.1"
__version__ = "0.10.2"
__author__ = "Tulir Asokan <tulir@maunium.net>"
+4 -1
View File
@@ -50,6 +50,7 @@ if TYPE_CHECKING:
from .context import Context
from .config import Config
from .bot import Bot
from .__main__ import TelegramBridge
config: Optional['Config'] = None
# Value updated from config in init()
@@ -71,6 +72,7 @@ class AbstractUser(ABC):
loop: asyncio.AbstractEventLoop = None
log: TraceLogger
az: AppService
bridge: 'TelegramBridge'
relaybot: Optional['Bot']
ignore_incoming_bot_events: bool = True
@@ -196,7 +198,7 @@ class AbstractUser(ABC):
if not await self.update(update):
await self._update(update)
except Exception:
self.log.exception(f"Failed to handle Telegram update {update}")
self.log.exception("Failed to handle Telegram update")
UPDATE_ERRORS.labels(update_type=update_type).inc()
UPDATE_TIME.labels(update_type=update_type).observe(time.time() - start_time)
@@ -513,6 +515,7 @@ class AbstractUser(ABC):
def init(context: 'Context') -> None:
global config, MAX_DELETIONS
AbstractUser.az, config, AbstractUser.loop, AbstractUser.relaybot = context.core
AbstractUser.bridge = context.bridge
AbstractUser.ignore_incoming_bot_events = config["bridge.relaybot.ignore_own_incoming_events"]
AbstractUser.session_container = context.session_container
MAX_DELETIONS = config.get("bridge.max_telegram_delete", 10)
+10 -5
View File
@@ -46,10 +46,13 @@ except ImportError:
help_section=SECTION_AUTH,
help_text="Check if you're logged into Telegram.")
async def ping(evt: CommandEvent) -> EventID:
me = await evt.sender.client.get_me() if await evt.sender.is_logged_in() else None
if me:
human_tg_id = f"@{me.username}" if me.username else f"+{me.phone}"
return await evt.reply(f"You're logged in as {human_tg_id}")
if await evt.sender.is_logged_in():
me = await evt.sender.get_me()
if me:
human_tg_id = f"@{me.username}" if me.username else f"+{me.phone}"
return await evt.reply(f"You're logged in as {human_tg_id}")
else:
return await evt.reply("You were logged in, but there appears to have been an error.")
else:
return await evt.reply("You're not logged in.")
@@ -346,10 +349,12 @@ async def _finish_sign_in(evt: CommandEvent, user: User, login_as: 'u.User' = No
return await evt.reply(msg)
@command_handler(needs_auth=True,
@command_handler(needs_auth=False,
help_section=SECTION_AUTH,
help_text="Log out from Telegram.")
async def logout(evt: CommandEvent) -> EventID:
if not evt.sender.tgid:
return await evt.reply("You're not logged in")
if await evt.sender.log_out():
return await evt.reply("Logged out successfully.")
return await evt.reply("Failed to log out.")
+16 -8
View File
@@ -244,14 +244,7 @@ bridge:
# Default to encryption, force-enable encryption in all portals the bridge creates
# This will cause the bridge bot to be in private chats for the encryption to work properly.
default: false
# Database for the encryption data. Currently only supports Postgres and an in-memory
# store that's persisted as a pickle.
# If set to `default`, will use the appservice postgres database
# or a pickle file if the appservice database is sqlite.
#
# Format examples:
# Pickle: pickle:///filename.pickle
# Postgres: postgres://username:password@hostname/dbname
# Database for the encryption data. If set to `default`, will use the appservice database.
database: default
# Options for automatic key sharing.
key_sharing:
@@ -394,6 +387,21 @@ bridge:
# The prefix for commands. Only required in non-management rooms.
command_prefix: "!tg"
# Messages sent upon joining a management room.
# Markdown is supported. The defaults are listed below.
management_room_text:
# Sent when joining a room.
welcome: "Hello, I'm a Telegram bridge bot."
# Sent when joining a management room and the user is already logged in.
welcome_connected: "Use `help` for help."
# Sent when joining a management room and the user is not logged in.
welcome_unconnected: "Use `help` for help or `login` to log in."
# Optional extra text sent when joining a management room.
additional_help: ""
# Send each message separately (for readability in some clients)
management_room_multiple_messages: false
# Permissions for using the bridge.
# Permitted values:
# relaybot - Only use the bridge via the relaybot, no access to commands.
+5 -3
View File
@@ -32,7 +32,7 @@ from telethon.helpers import add_surrogate, del_surrogate
from mautrix.errors import MatrixRequestError
from mautrix.appservice import IntentAPI
from mautrix.types import (TextMessageEventContent, RelatesTo, RelationType, Format, MessageType,
MessageEvent)
MessageEvent, EventType)
from .. import user as u, puppet as pu, portal as po
from ..types import TelegramID
@@ -129,12 +129,14 @@ async def _add_reply_header(source: 'AbstractUser', content: TextMessageEventCon
content.relates_to = RelatesTo(rel_type=RelationType.REPLY, event_id=msg.mxid)
try:
event: MessageEvent = await main_intent.get_event(msg.mx_room, msg.mxid)
event = await main_intent.get_event(msg.mx_room, msg.mxid)
if event.type == EventType.ROOM_ENCRYPTED and source.bridge.matrix.e2ee:
event = await source.bridge.matrix.e2ee.decrypt(event)
if isinstance(event.content, TextMessageEventContent):
event.content.trim_reply_fallback()
puppet = await pu.Puppet.get_by_mxid(event.sender, create=False)
content.set_reply(event, displayname=puppet.displayname if puppet else event.sender)
except MatrixRequestError:
except Exception:
log.exception("Failed to get event to add reply fallback")
-17
View File
@@ -115,23 +115,6 @@ class MatrixHandler(BaseMatrixHandler):
await intent.send_notice(room_id, "This puppet will remain inactive until a "
"Telegram chat is created for this room.")
async def send_welcome_message(self, room_id: RoomID, inviter: 'u.User') -> None:
try:
is_management = len(await self.az.intent.get_room_members(room_id)) == 2
except MatrixError:
# The AS bot is not in the room.
return
cmd_prefix = self.commands.command_prefix
text = html = "Hello, I'm a Telegram bridge bot. "
if is_management and inviter.puppet_whitelisted and not await inviter.is_logged_in():
text += f"Use `{cmd_prefix} help` for help or `{cmd_prefix} login` to log in."
html += (f"Use <code>{cmd_prefix} help</code> for help"
f" or <code>{cmd_prefix} login</code> to log in.")
else:
text += f"Use `{cmd_prefix} help` for help."
html += f"Use <code>{cmd_prefix} help</code> for help."
await self.az.intent.send_notice(room_id, text=text, html=html)
async def handle_invite(self, room_id: RoomID, user_id: UserID, inviter: 'u.User',
event_id: EventID) -> None:
user = u.User.get_by_mxid(user_id, create=False)
+2 -1
View File
@@ -421,7 +421,8 @@ class PortalMatrix(BasePortal, ABC):
await self._handle_matrix_file(sender_id, event_id, space, client, content, reply_to,
caption_content)
else:
self.log.trace("Unhandled Matrix event: %s", content)
self.log.debug("Didn't handle Matrix event {event_id} due to unknown msgtype {content.msgtype}")
self.log.trace("Unhandled Matrix event content: %s", content)
async def handle_matrix_unpin_all(self, sender: 'u.User', pin_event_id: EventID) -> None:
await sender.client(UnpinAllMessagesRequest(peer=self.peer))
+14 -11
View File
@@ -27,7 +27,7 @@ from telethon.tl.types import (
PhotoEmpty, InputChannel, InputUser, ChatPhotoEmpty, PeerUser, Photo, TypeChat, TypeInputPeer,
TypeUser, User, InputPeerPhotoFileLocation, ChatParticipantAdmin, ChannelParticipantAdmin,
ChatParticipantCreator, ChannelParticipantCreator, UserProfilePhoto, UserProfilePhotoEmpty,
InputPeerUser)
InputPeerUser, ChannelParticipantBanned)
from mautrix.errors import MForbidden
from mautrix.types import (RoomID, UserID, RoomCreatePreset, EventType, Membership,
@@ -503,14 +503,15 @@ class PortalMetadata(BasePortal, ABC):
levels.users[self.main_intent.mxid] = 100
return levels
@staticmethod
def _get_level_from_participant(participant: TypeParticipant) -> int:
@classmethod
def _get_level_from_participant(cls, participant: TypeParticipant,
levels: PowerLevelStateEventContent) -> int:
# TODO use the power level requirements to get better precision in channels
if isinstance(participant, (ChatParticipantAdmin, ChannelParticipantAdmin)):
return 50
return levels.state_default or 50
elif isinstance(participant, (ChatParticipantCreator, ChannelParticipantCreator)):
return 95
return 0
return levels.get_user_level(cls.az.bot_mxid) - 5
return levels.users_default or 0
@staticmethod
def _participant_to_power_levels(levels: PowerLevelStateEventContent,
@@ -541,7 +542,7 @@ class PortalMetadata(BasePortal, ABC):
puppet = p.Puppet.get(TelegramID(participant.user_id))
user = u.User.get_by_tgid(TelegramID(participant.user_id))
new_level = self._get_level_from_participant(participant)
new_level = self._get_level_from_participant(participant, levels)
if user:
await user.register_portal(self)
@@ -796,7 +797,8 @@ class PortalMetadata(BasePortal, ABC):
@staticmethod
def _filter_participants(users: List[TypeUser], participants: List[TypeParticipant]
) -> Iterable[TypeUser]:
participant_map = {part.user_id: part for part in participants}
participant_map = {part.user_id: part for part in participants
if not isinstance(part, ChannelParticipantBanned)}
for user in users:
try:
user.participant = participant_map[user.id]
@@ -832,15 +834,16 @@ class PortalMetadata(BasePortal, ABC):
async def _get_users(self, user: 'AbstractUser',
entity: Union[TypeInputPeer, InputUser, TypeChat, TypeUser, InputChannel]
) -> List[TypeUser]:
limit = self.max_initial_member_sync
if self.peer_type == "chat":
chat = await user.client(GetFullChatRequest(chat_id=self.tgid))
return list(self._filter_participants(chat.users,
chat.full_chat.participants.participants))
return list(
self._filter_participants(chat.users, chat.full_chat.participants.participants)
)[:limit]
elif self.peer_type == "channel":
if not self.megagroup and not self.sync_channel_members:
return []
limit = self.max_initial_member_sync
if limit == 0:
return []
+6 -3
View File
@@ -193,13 +193,13 @@ class PortalTelegram(BasePortal, ABC):
height=file.thumbnail.height or thumb_size.h,
width=file.thumbnail.width or thumb_size.w,
size=file.thumbnail.size)
else:
elif attrs.is_sticker:
# This is a hack for bad clients like Element iOS that require a thumbnail
info.thumbnail_info = ImageInfo.deserialize(info.serialize())
if file.decryption_info:
info.thumbnail_file = file.decryption_info
else:
info.thumbnail_url = file.mxc
info.thumbnail_info = ImageInfo.deserialize(info.serialize())
return info, name
@@ -258,9 +258,12 @@ class PortalTelegram(BasePortal, ABC):
info["fi.mau.autoplay"] = True
info["fi.mau.hide_controls"] = True
info["fi.mau.no_audio"] = True
if not name:
ext = sane_mimetypes.guess_extension(file.mime_type)
name = "unnamed_file" + ext
content = MediaMessageEventContent(
body=name or "unnamed file", info=info, relates_to=relates_to,
body=name, info=info, relates_to=relates_to,
external_url=self._get_external_url(evt),
msgtype={
"video/": MessageType.VIDEO,
+38 -7
View File
@@ -22,12 +22,14 @@ import asyncio
from telethon.tl.types import (TypeUpdate, UpdateNewMessage, UpdateNewChannelMessage,
UpdateShortChatMessage, UpdateShortMessage, User as TLUser, Chat,
ChatForbidden, UpdateFolderPeers, UpdatePinnedDialogs,
UpdateNotifySettings, NotifyPeer)
UpdateNotifySettings, NotifyPeer, InputUserSelf)
from telethon.tl.custom import Dialog
from telethon.tl.types.contacts import ContactsNotModified
from telethon.tl.functions.contacts import GetContactsRequest, SearchRequest
from telethon.tl.functions.account import UpdateStatusRequest
from telethon.errors import AuthKeyDuplicatedError
from telethon.tl.functions.users import GetUsersRequest
from telethon.errors import (AuthKeyDuplicatedError, UserDeactivatedError, UserDeactivatedBanError,
SessionRevokedError, UnauthorizedError)
from mautrix.client import Client
from mautrix.errors import MatrixRequestError, MNotFound
@@ -56,6 +58,7 @@ METRIC_CONNECTED = Gauge('bridge_connected', 'Users connected to Telegram')
BridgeState.human_readable_errors.update({
"tg-not-connected": "Your Telegram connection failed",
"tg-auth-key-duplicated": "The bridge accidentally logged you out",
"tg-not-authenticated": "The stored auth token did not work",
})
@@ -226,13 +229,16 @@ class User(AbstractUser, BaseUser):
elif delete_unless_authenticated:
self.log.debug(f"Unauthenticated user {self.name} start()ed, deleting session...")
await self.client.disconnect()
if self.tgid:
await self.push_bridge_state(BridgeStateEvent.BAD_CREDENTIALS,
error="tg-not-authenticated")
self.client.session.delete()
return self
@property
def _is_connected(self) -> bool:
return bool(self.client and self.client._sender
and self.client._sender._transport_connected)
and self.client._sender._transport_connected())
async def _track_connection(self) -> None:
self.log.debug("Starting loop to track connection state")
@@ -252,6 +258,18 @@ class User(AbstractUser, BaseUser):
state.remote_id = str(self.tgid)
state.remote_name = self.human_tg_id
async def get_bridge_states(self) -> List[BridgeState]:
if not self.tgid:
return []
if self._is_connected and await self.is_logged_in():
state_event = (BridgeStateEvent.BACKFILLING if self._is_backfilling
else BridgeStateEvent.CONNECTED)
ttl = 3600
else:
state_event = BridgeStateEvent.UNKNOWN_ERROR
ttl = 240
return [BridgeState(state_event=state_event, ttl=ttl)]
async def get_puppet(self) -> Optional['pu.Puppet']:
if not self.tgid:
return None
@@ -321,8 +339,22 @@ class User(AbstractUser, BaseUser):
if not self.is_bot:
await self.client(UpdateStatusRequest(offline=not online))
async def get_me(self) -> Optional[TLUser]:
try:
return (await self.client(GetUsersRequest([InputUserSelf()])))[0]
except UnauthorizedError as e:
self.log.error(f"Authorization error in get_me(): {e}")
await self.push_bridge_state(BridgeStateEvent.BAD_CREDENTIALS, error="tg-auth-error",
message=str(e), ttl=3600)
await self.stop()
return None
async def update_info(self, info: TLUser = None) -> None:
info = info or await self.client.get_me()
if not info:
info = await self.get_me()
if not info:
self.log.warning("get_me() returned None, aborting update_info()")
return
changed = False
if self.is_bot != info.bot:
self.is_bot = info.bot
@@ -366,12 +398,11 @@ class User(AbstractUser, BaseUser):
self.tgid = None
await self.save()
ok = await self.client.log_out()
if not ok:
return False
self.client.session.delete()
self.delete()
await self.stop()
self._track_metric(METRIC_LOGGED_IN, False)
return True
return ok
def _search_local(self, query: str, max_results: int = 5, min_similarity: int = 45
) -> List[SearchResult]:
+17 -13
View File
@@ -21,7 +21,10 @@ import json
from aiohttp import web
from telethon.utils import get_peer_id, resolve_id
from telethon.tl.types import ChatForbidden, ChannelForbidden, TypeChat
from telethon.tl.types import ChatForbidden, ChannelForbidden, TypeChat, InputUserSelf
from telethon.tl.functions.users import GetUsersRequest
from telethon.errors import (UserDeactivatedError, UserDeactivatedBanError, SessionRevokedError,
UnauthorizedError)
from mautrix.appservice import AppService
from mautrix.errors import MatrixRequestError, IntentError
@@ -294,16 +297,17 @@ class ProvisioningAPI(AuthAPI):
user_data = None
if await user.is_logged_in():
me = await user.client.get_me()
await user.update_info(me)
user_data = {
"id": user.tgid,
"username": user.username,
"first_name": me.first_name,
"last_name": me.last_name,
"phone": me.phone,
"is_bot": user.is_bot,
}
me = await user.get_me()
if me:
await user.update_info(me)
user_data = {
"id": user.tgid,
"username": user.username,
"first_name": me.first_name,
"last_name": me.last_name,
"phone": me.phone,
"is_bot": user.is_bot,
}
return web.json_response({
"telegram": user_data,
"mxid": user.mxid,
@@ -351,7 +355,7 @@ class ProvisioningAPI(AuthAPI):
return await self.post_login_password(user, data.get("password", ""))
async def logout(self, request: web.Request) -> web.Response:
_, user, err = await self.get_user_request_info(request, expect_logged_in=True,
_, user, err = await self.get_user_request_info(request, expect_logged_in=None,
require_puppeting=False,
want_data=False)
if err is not None:
@@ -461,7 +465,7 @@ class ProvisioningAPI(AuthAPI):
Optional[web.Response]]):
err = self.check_authorization(request)
if err is not None:
return err
return None, None, err
data = None
if want_data and (request.method == "POST" or request.method == "PUT"):
+5 -2
View File
@@ -15,13 +15,16 @@ qrcode>=6,<7
moviepy>=1,<2
#/metrics
prometheus_client>=0.6,<0.12
prometheus_client>=0.6,<0.13
#/postgres
psycopg2-binary>=2,<3
asyncpg>=0.20,<0.25
#/sqlite
aiosqlite>=0.17,<0.18
#/e2be
asyncpg>=0.20,<0.25
python-olm>=3,<4
pycryptodome>=3,<4
unpaddedbase64>=1,<2
+7 -2
View File
@@ -5,6 +5,11 @@ python-magic>=0.4,<0.5
commonmark>=0.8,<0.10
aiohttp>=3,<4
yarl>=1,<2
mautrix>=0.10.4,<0.11
telethon>=1.22,<1.23
mautrix>=0.11.3,<0.12
#telethon>=1.22,<1.24
# Temporary patch for 64-bit IDs until upstream telethon 2.0 is ready
tulir-telethon==1.24.0a2
telethon-session-sqlalchemy>=0.2.14,<0.3
# Temporarily always depend on aiosqlite to prevent breaking old installs
# Will be removed in v0.12 (after which you need to choose the [sqlite] optional dependency)
aiosqlite>=0.17,<0.18