Compare commits

...

28 Commits

Author SHA1 Message Date
Tulir Asokan 95920728f4 Bump version to 0.9.0 2020-11-17 18:01:14 +02:00
Tulir Asokan e85be95d2d Fix cleaning unidentified rooms. Fixes #541 2020-11-17 18:01:06 +02:00
Tulir Asokan 3006b3ab3b Update mautrix-python 2020-11-17 17:57:29 +02:00
Tulir Asokan d4d6cfa87d Bump version to 0.9.0rc3 2020-11-12 01:41:44 +02:00
Tulir Asokan b8cfcbe5ee Set nova nightly image hash in CI 2020-11-11 23:19:19 +02:00
Tulir Asokan 9875833c90 Use correct relation type for replies 2020-11-10 12:31:03 +02:00
Tulir Asokan 38d94484bb Use mautrix utility function for file upload retry 2020-11-10 00:21:36 +02:00
Tulir Asokan 0b3014ff88 Retry sending messages if server returns 502 2020-11-09 21:01:30 +02:00
Tulir Asokan 04c64949e7 Update mautrix-python 2020-11-07 16:01:38 +02:00
Tulir Asokan be59d50678 Fix Matrix->Telegram name mentions 2020-11-07 16:01:21 +02:00
Tulir Asokan 04e2497dd3 Bump version to 0.9.0rc2 2020-11-06 21:30:07 +02:00
Tulir Asokan 2c59cb4871 Fix sending plaintext captions to Telegram 2020-11-06 18:14:20 +02:00
Tulir Asokan 64ddd07171 Update mautrix-python 2020-11-05 22:19:09 +02:00
Tulir Asokan 1b91fbc806 Check room encryption status when bridging portal 2020-10-30 20:16:02 +02:00
Tulir Asokan 2b6cffc8ef Fix bugs in manual bridging that were added by the previous fix 2020-10-30 19:55:43 +02:00
Tulir Asokan 5cc0afef85 Let mautrix-python handle generating namespaces for the registration 2020-10-30 19:46:37 +02:00
Tulir Asokan 52adbb7335 Fix potential bugs in manual bridging 2020-10-30 19:46:02 +02:00
Tulir Asokan dd3bdd2846 Allow unbridging direct chat portals. Fixes #495 2020-10-29 23:02:37 +02:00
Tulir Asokan f088599dec Disconnect from Telegram after logging out 2020-10-29 22:38:54 +02:00
Tulir Asokan fe573865aa Completely delete private chat portals when user logs out
If it just kicks the user, logging in again later would cause the
bridge to think there's a portal, but fail to invite the user again.

Fixes #397
2020-10-29 22:33:22 +02:00
Tulir Asokan 5316ed57af Send link to Telegram ToS when signing up 2020-10-28 18:54:12 +02:00
Tulir Asokan 1567239ae6 Update connection metric after logging in 2020-10-28 18:44:50 +02:00
Tulir Asokan 24c65f8942 Don't set bridge_connected metric for non-logged-in users 2020-10-28 18:14:12 +02:00
Tulir Asokan 213e63830d Update mautrix-python and unpin yarl/aiohttp 2020-10-28 12:34:11 +02:00
Tulir Asokan efe532e4d0 Don't check user database when handling ephemeral events 2020-10-27 16:49:54 +02:00
Tulir Asokan 8392f46db9 Fix bugs in left member check 2020-10-27 15:37:38 +02:00
Tulir Asokan 87cacc9b20 Update mautrix-python 2020-10-27 15:19:19 +02:00
Tulir Asokan d808893274 Move clean-rooms command to mautrix-python 2020-10-26 19:56:20 +02:00
21 changed files with 256 additions and 457 deletions
+7
View File
@@ -17,6 +17,13 @@ build amd64:
- docker build --pull --cache-from $CI_REGISTRY_IMAGE:latest --build-arg TARGETARCH=amd64 --tag $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA-amd64 .
- docker push $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA-amd64
- docker rmi $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA-amd64
after_script:
- |
if [ "$CI_COMMIT_BRANCH" = "master" ]; then
apk add --update curl
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"}'
fi
build arm64:
stage: build
+1 -1
View File
@@ -1,2 +1,2 @@
__version__ = "0.9.0rc1"
__version__ = "0.9.0"
__author__ = "Tulir Asokan <tulir@maunium.net>"
+1 -1
View File
@@ -1,7 +1,7 @@
from .handler import (command_handler, CommandHandler, CommandProcessor, CommandEvent,
SECTION_AUTH, SECTION_CREATING_PORTALS, SECTION_PORTAL_MANAGEMENT,
SECTION_MISC, SECTION_ADMIN)
from . import portal, telegram, clean_rooms, matrix_auth, manhole
from . import portal, telegram, matrix_auth, manhole
__all__ = ["command_handler", "CommandHandler", "CommandProcessor", "CommandEvent",
"SECTION_AUTH", "SECTION_MISC", "SECTION_ADMIN", "SECTION_CREATING_PORTALS",
-195
View File
@@ -1,195 +0,0 @@
# mautrix-telegram - A Matrix-Telegram puppeting bridge
# Copyright (C) 2019 Tulir Asokan
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
from typing import List, NamedTuple, Tuple, Union
from mautrix.appservice import IntentAPI
from mautrix.errors import MatrixRequestError
from mautrix.types import RoomID, UserID, EventID, EventType
from . import command_handler, CommandEvent, SECTION_ADMIN
from .. import puppet as pu, portal as po
ManagementRoom = NamedTuple('ManagementRoom', room_id=RoomID, user_id=UserID)
async def _find_rooms(intent: IntentAPI) -> Tuple[List[ManagementRoom], List[RoomID], List[RoomID],
List['po.Portal'], List['po.Portal']]:
management_rooms: List[ManagementRoom] = []
unidentified_rooms: List[RoomID] = []
tombstoned_rooms: List[RoomID] = []
portals: List[po.Portal] = []
empty_portals: List[po.Portal] = []
rooms = await intent.get_joined_rooms()
for room_id in rooms:
portal = po.Portal.get_by_mxid(room_id)
if not portal:
try:
tombstone = await intent.get_state_event(room_id, EventType.ROOM_TOMBSTONE)
if tombstone and tombstone.replacement_room:
tombstoned_rooms.append(room_id)
continue
except MatrixRequestError:
pass
try:
members = await intent.get_room_members(room_id)
except MatrixRequestError:
members = []
if len(members) == 2:
other_member = members[0] if members[0] != intent.mxid else members[1]
if pu.Puppet.get_id_from_mxid(other_member):
unidentified_rooms.append(room_id)
else:
management_rooms.append(ManagementRoom(room_id, other_member))
else:
unidentified_rooms.append(room_id)
else:
members = await portal.get_authenticated_matrix_users()
if len(members) == 0:
empty_portals.append(portal)
else:
portals.append(portal)
return management_rooms, unidentified_rooms, tombstoned_rooms, portals, empty_portals
@command_handler(needs_admin=True, needs_auth=False, management_only=True, name="clean-rooms",
help_section=SECTION_ADMIN,
help_text="Clean up unused portal/management rooms.")
async def clean_rooms(evt: CommandEvent) -> EventID:
(management_rooms, unidentified_rooms, tombstoned_rooms,
portals, empty_portals) = await _find_rooms(evt.az.intent)
reply = ["#### Management rooms (M)"]
reply += ([f"{n+1}. [M{n+1}](https://matrix.to/#/{room}) (with {other_member}"
for n, (room, other_member) in enumerate(management_rooms)]
or ["No management rooms found."])
reply.append("#### Active portal rooms (A)")
reply += ([f"{n+1}. [A{n+1}](https://matrix.to/#/{portal.mxid}) "
f"(to Telegram chat \"{portal.title}\")"
for n, portal in enumerate(portals)]
or ["No active portal rooms found."])
reply.append("#### Unidentified rooms (U)")
reply += ([f"{n+1}. [U{n+1}](https://matrix.to/#/{room})"
for n, room in enumerate(unidentified_rooms)]
or ["No unidentified rooms found."])
reply.append("#### Tombstoned rooms (T)")
reply += ([f"{n+1}. [T{n+1}](https://matrix.to/#/{room})"
for n, room in enumerate(tombstoned_rooms)]
or ["No tombstoned rooms found."])
reply.append("#### Inactive portal rooms (I)")
reply += ([f"{n}. [I{n}](https://matrix.to/#/{portal.mxid}) "
f"(to Telegram chat \"{portal.title}\")"
for n, portal in enumerate(empty_portals)]
or ["No inactive portal rooms found."])
reply += ["#### Usage",
("To clean the recommended set of rooms (unidentified & inactive portals), "
"type `$cmdprefix+sp clean-recommended`"),
"",
("To clean other groups of rooms, type `$cmdprefix+sp clean-groups <letters>` "
"where `letters` are the first letters of the group names (M, A, U, I, T)"),
"",
("To clean specific rooms, type `$cmdprefix+sp clean-range <range>` "
"where `range` is the range (e.g. `5-21`) prefixed with the first letter of"
"the group name. (e.g. `I2-6`)"),
"",
("Please note that you will have to re-run `$cmdprefix+sp clean-rooms` "
"between each use of the commands above.")]
evt.sender.command_status = {
"next": lambda clean_evt: set_rooms_to_clean(clean_evt, management_rooms,
unidentified_rooms, tombstoned_rooms, portals,
empty_portals),
"action": "Room cleaning",
}
return await evt.reply("\n".join(reply))
async def set_rooms_to_clean(evt, management_rooms: List[ManagementRoom],
unidentified_rooms: List[RoomID], tombstoned_rooms: List[RoomID],
portals: List["po.Portal"], empty_portals: List["po.Portal"]) -> None:
command = evt.args[0]
rooms_to_clean: List[Union[po.Portal, RoomID]] = []
if command == "clean-recommended":
rooms_to_clean += empty_portals
rooms_to_clean += unidentified_rooms
elif command == "clean-groups":
if len(evt.args) < 2:
return await evt.reply("**Usage:** `$cmdprefix+sp clean-groups [M][A][U][I]")
groups_to_clean = evt.args[1].upper()
if "M" in groups_to_clean:
rooms_to_clean += [room_id for (room_id, user_id) in management_rooms]
if "A" in groups_to_clean:
rooms_to_clean += portals
if "U" in groups_to_clean:
rooms_to_clean += unidentified_rooms
if "I" in groups_to_clean:
rooms_to_clean += empty_portals
if "T" in groups_to_clean:
rooms_to_clean += tombstoned_rooms
elif command == "clean-range":
try:
clean_range = evt.args[1]
group, clean_range = clean_range[0], clean_range[1:]
start, end = clean_range.split("-")
start, end = int(start), int(end)
if group == "M":
group = [room_id for (room_id, user_id) in management_rooms]
elif group == "A":
group = portals
elif group == "U":
group = unidentified_rooms
elif group == "I":
group = empty_portals
elif group == "T":
group = tombstoned_rooms
else:
raise ValueError("Unknown group")
rooms_to_clean = group[start - 1:end]
except (KeyError, ValueError):
return await evt.reply(
"**Usage:** `$cmdprefix+sp clean-groups <_M|A|U|I_><range>")
else:
return await evt.reply(f"Unknown room cleaning action `{command}`. "
"Use `$cmdprefix+sp cancel` to cancel room "
"cleaning.")
evt.sender.command_status = {
"next": lambda confirm: execute_room_cleanup(confirm, rooms_to_clean),
"action": "Room cleaning",
}
await evt.reply(f"To confirm cleaning up {len(rooms_to_clean)} rooms, type "
"`$cmdprefix+sp confirm-clean`.")
async def execute_room_cleanup(evt, rooms_to_clean: List[Union[po.Portal, RoomID]]) -> None:
if len(evt.args) > 0 and evt.args[0] == "confirm-clean":
await evt.reply(f"Cleaning {len(rooms_to_clean)} rooms. "
"This might take a while.")
cleaned = 0
for room in rooms_to_clean:
if isinstance(room, po.Portal):
await room.cleanup_and_delete()
cleaned += 1
else:
await po.Portal.cleanup_room(evt.az.intent, room, "Room deleted")
cleaned += 1
evt.sender.command_status = None
await evt.reply(f"{cleaned} rooms cleaned up successfully.")
else:
await evt.reply("Room cleaning cancelled.")
+19 -11
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 Optional, Tuple, Coroutine
from typing import Optional, Tuple, Awaitable
import asyncio
from telethon.tl.types import ChatForbidden, ChannelForbidden
@@ -105,18 +105,17 @@ async def bridge(evt: CommandEvent) -> EventID:
async def cleanup_old_portal_while_bridging(evt: CommandEvent, portal: "po.Portal"
) -> Tuple[
bool, Optional[Coroutine[None, None, None]]]:
) -> Tuple[bool, Optional[Awaitable[None]]]:
if not portal.mxid:
await evt.reply("The portal seems to have lost its Matrix room between you"
"calling `$cmdprefix+sp bridge` and this command.\n\n"
"Continuing without touching previous Matrix room...")
return True, None
elif evt.args[0] == "delete-and-continue":
return True, portal.cleanup_portal("Portal deleted (moving to another room)")
return True, portal.cleanup_portal("Portal deleted (moving to another room)", delete=False)
elif evt.args[0] == "unbridge-and-continue":
return True, portal.cleanup_portal("Room unbridged (portal moving to another room)",
puppets_only=True)
puppets_only=True, delete=False)
else:
await evt.reply(
"The chat you were trying to bridge already has a Matrix portal room.\n\n"
@@ -137,6 +136,9 @@ async def confirm_bridge(evt: CommandEvent) -> Optional[EventID]:
return await evt.reply("Fatal error: tgid or peer_type missing from command_status. "
"This shouldn't happen unless you're messing with the command "
"handler code.")
is_logged_in = await evt.sender.is_logged_in() and not status["force_use_bot"]
if "mxid" in status:
ok, coro = await cleanup_old_portal_while_bridging(evt, portal)
if not ok:
@@ -154,7 +156,13 @@ async def confirm_bridge(evt: CommandEvent) -> Optional[EventID]:
"`$cmdprefix+sp cancel` to cancel.")
evt.sender.command_status = None
is_logged_in = await evt.sender.is_logged_in() and not status["force_use_bot"]
async with portal._room_create_lock:
await _locked_confirm_bridge(evt, portal=portal, room_id=bridge_to_mxid,
is_logged_in=is_logged_in)
async def _locked_confirm_bridge(evt: CommandEvent, portal: 'po.Portal', room_id: RoomID,
is_logged_in: bool) -> Optional[EventID]:
user = evt.sender if is_logged_in else evt.tgbot
try:
entity = await user.client.get_entity(portal.peer)
@@ -172,14 +180,14 @@ async def confirm_bridge(evt: CommandEvent) -> Optional[EventID]:
else:
return await evt.reply("The bot doesn't seem to be in that chat.")
direct = False
portal.mxid = bridge_to_mxid
portal.title, portal.about, levels = await get_initial_state(evt.az.intent, evt.room_id)
portal.mxid = room_id
portal.by_mxid[portal.mxid] = portal
(portal.title, portal.about, levels,
portal.encrypted) = await get_initial_state(evt.az.intent, evt.room_id)
portal.photo_id = ""
await portal.save()
asyncio.ensure_future(portal.update_matrix_room(user, entity, direct, levels=levels),
asyncio.ensure_future(portal.update_matrix_room(user, entity, direct=False, levels=levels),
loop=evt.loop)
return await evt.reply("Bridging complete. Portal synchronization should begin momentarily.")
@@ -38,7 +38,7 @@ async def create(evt: CommandEvent) -> EventID:
if not await user_has_power_level(evt.room_id, evt.az.intent, evt.sender, "bridge"):
return await evt.reply("You do not have the permissions to bridge this room.")
title, about, levels = await get_initial_state(evt.az.intent, evt.room_id)
title, about, levels, encrypted = await get_initial_state(evt.az.intent, evt.room_id)
if not title:
return await evt.reply("Please set a title before creating a Telegram chat.")
@@ -50,11 +50,11 @@ async def create(evt: CommandEvent) -> EventID:
"group": "chat",
}[type]
portal = po.Portal(tgid=TelegramID(0), peer_type=type,
mxid=evt.room_id, title=title, about=about)
portal = po.Portal(tgid=TelegramID(0), peer_type=type, mxid=evt.room_id,
title=title, about=about, encrypted=encrypted)
try:
await portal.create_telegram_chat(evt.sender, supergroup=supergroup)
except ValueError as e:
portal.delete()
await portal.delete()
return await evt.reply(e.args[0])
return await evt.reply(f"Telegram chat created. ID: {portal.tgid}")
@@ -31,6 +31,12 @@ async def _get_portal_and_check_permission(evt: CommandEvent) -> Optional[po.Por
await evt.reply(f"{that_this} is not a portal room.")
return None
if portal.peer_type == "user":
if portal.tg_receiver != evt.sender.tgid:
await evt.reply("You do not have the permissions to unbridge that portal.")
return None
return portal
if not await user_has_power_level(portal.mxid, evt.az.intent, evt.sender, "unbridge"):
await evt.reply("You do not have the permissions to unbridge that portal.")
return None
+5 -2
View File
@@ -25,11 +25,12 @@ OptStr = Optional[str]
async def get_initial_state(intent: IntentAPI, room_id: RoomID
) -> Tuple[OptStr, OptStr, Optional[PowerLevelStateEventContent]]:
) -> Tuple[OptStr, OptStr, Optional[PowerLevelStateEventContent], bool]:
state = await intent.get_state(room_id)
title: OptStr = None
about: OptStr = None
levels: Optional[PowerLevelStateEventContent] = None
encrypted: bool = False
for event in state:
try:
if event.type == EventType.ROOM_NAME:
@@ -40,10 +41,12 @@ async def get_initial_state(intent: IntentAPI, room_id: RoomID
levels = event.content
elif event.type == EventType.ROOM_CANONICAL_ALIAS:
title = title or event.content.canonical_alias
elif event.type == EventType.ROOM_ENCRYPTION:
encrypted = True
except KeyError:
# Some state event probably has empty content
pass
return title, about, levels
return title, about, levels, encrypted
async def user_has_power_level(room_id: RoomID, intent: IntentAPI, sender: u.User,
+12 -14
View File
@@ -31,7 +31,7 @@ from mautrix.types import (EventID, UserID, MediaMessageEventContent, ImageInfo,
from ... import user as u
from ...types import TelegramID
from ...commands import command_handler, CommandEvent, SECTION_AUTH
from ...util import format_duration
from ...util import format_duration as fmt_duration
try:
import qrcode
@@ -70,7 +70,7 @@ async def ping_bot(evt: CommandEvent) -> EventID:
help_section=SECTION_AUTH,
help_args="<_phone_> <_full name_>",
help_text="Register to Telegram")
async def register(evt: CommandEvent) -> Optional[EventID]:
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:
@@ -87,7 +87,8 @@ async def register(evt: CommandEvent) -> Optional[EventID]:
"action": "Register",
"full_name": full_name,
})
return None
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:
@@ -222,21 +223,18 @@ async def _request_code(evt: CommandEvent, phone_number: str, next_status: Dict[
ok = True
return await evt.reply(f"Login code sent to {phone_number}. Please send the code here.")
except PhoneNumberAppSignupForbiddenError:
return await evt.reply(
"Your phone number does not allow 3rd party apps to sign in.")
return await evt.reply("Your phone number does not allow 3rd party apps to sign in.")
except PhoneNumberFloodError:
return await evt.reply(
"Your phone number has been temporarily blocked for flooding. "
"The ban is usually applied for around a day.")
return await evt.reply("Your phone number has been temporarily blocked for flooding. "
"The ban is usually applied for around a day.")
except FloodWaitError as e:
return await evt.reply(
"Your phone number has been temporarily blocked for flooding. "
f"Please wait for {format_duration(e.seconds)} before trying again.")
return await evt.reply("Your phone number has been temporarily blocked for flooding. "
f"Please wait for {fmt_duration(e.seconds)} before trying again.")
except PhoneNumberBannedError:
return await evt.reply("Your phone number has been banned from Telegram.")
return await evt.reply("Your phone number has been banned from Telegram.")
except PhoneNumberUnoccupiedError:
return await evt.reply("That phone number has not been registered. "
"Please register with `$cmdprefix+sp register <phone>`.")
return await evt.reply("That phone number has not been registered. "
"Please register with `$cmdprefix+sp register <phone>`.")
except PhoneNumberInvalidError:
return await evt.reply("That phone number is not valid.")
except Exception:
+3 -24
View File
@@ -13,14 +13,14 @@
#
# 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 Any, Dict, List, NamedTuple
from typing import Any, List, NamedTuple
from ruamel.yaml.comments import CommentedMap
import os
from mautrix.types import UserID
from mautrix.client import Client
from mautrix.bridge.config import (BaseBridgeConfig, ConfigUpdateHelper, ForbiddenKey,
ForbiddenDefault)
from mautrix.bridge.config import BaseBridgeConfig
from mautrix.util.config import ForbiddenKey, ForbiddenDefault, ConfigUpdateHelper
Permissions = NamedTuple("Permissions", relaybot=bool, user=bool, puppeting=bool,
matrix_puppeting=bool, admin=bool, level=str)
@@ -240,24 +240,3 @@ class Config(BaseBridgeConfig):
return self._get_permissions(homeserver)
return self._get_permissions("*")
@property
def namespaces(self) -> Dict[str, List[Dict[str, Any]]]:
homeserver = self["homeserver.domain"]
username_format = self["bridge.username_template"].format(userid=".+")
alias_format = self["bridge.alias_template"].format(groupname=".+")
group_id = ({"group_id": self["appservice.community_id"]}
if self["appservice.community_id"] else {})
return {
"users": [{
"exclusive": True,
"regex": f"@{username_format}:{homeserver}",
**group_id,
}],
"aliases": [{
"exclusive": True,
"regex": f"#{alias_format}:{homeserver}",
}]
}
+1 -2
View File
@@ -1,5 +1,4 @@
from .from_matrix import (matrix_reply_to_telegram, matrix_to_telegram, matrix_text_to_telegram,
init_mx)
from .from_matrix import matrix_reply_to_telegram, matrix_to_telegram, init_mx
from .from_telegram import telegram_reply_to_matrix, telegram_to_matrix
from .. import context as c
@@ -18,10 +18,12 @@ import re
import logging
from telethon.tl.types import (MessageEntityMention, MessageEntityMentionName, MessageEntityItalic,
TypeMessageEntity)
TypeMessageEntity, InputMessageEntityMentionName)
from telethon.helpers import add_surrogate, del_surrogate
from telethon import TelegramClient
from mautrix.types import RoomID, MessageEventContent
from mautrix.util.logging import TraceLogger
from ... import puppet as pu
from ...types import TelegramID
@@ -31,30 +33,19 @@ from .parser import ParsedMessage, parse_html
if TYPE_CHECKING:
from ...context import Context
log: logging.Logger = logging.getLogger("mau.fmt.mx")
log: TraceLogger = logging.getLogger("mau.fmt.mx")
should_bridge_plaintext_highlights: bool = False
command_regex: Pattern = re.compile(r"^!([A-Za-z0-9@]+)")
not_command_regex: Pattern = re.compile(r"^\\(![A-Za-z0-9@]+)")
plain_mention_regex: Optional[Pattern] = None
def plain_mention_to_html(match: Match) -> str:
puppet = pu.Puppet.find_by_displayname(match.group(2))
if puppet:
return (f"{match.group(1)}"
f"<a href='https://matrix.to/#/{puppet.mxid}'>"
f"{puppet.displayname}"
"</a>")
return "".join(match.groups())
MAX_LENGTH = 4096
CUTOFF_TEXT = " [message cut]"
CUT_MAX_LENGTH = MAX_LENGTH - len(CUTOFF_TEXT)
def cut_long_message(message: str, entities: List[TypeMessageEntity]) -> ParsedMessage:
def _cut_long_message(message: str, entities: List[TypeMessageEntity]) -> ParsedMessage:
if len(message) > MAX_LENGTH:
message = message[0:CUT_MAX_LENGTH] + CUTOFF_TEXT
new_entities = []
@@ -73,23 +64,6 @@ class FormatError(Exception):
pass
def matrix_to_telegram(html: str) -> ParsedMessage:
try:
html = command_regex.sub(r"<command>\1</command>", html)
html = html.replace("\t", " " * 4)
html = not_command_regex.sub(r"\1", html)
if should_bridge_plaintext_highlights:
html = plain_mention_regex.sub(plain_mention_to_html, html)
text, entities = parse_html(add_surrogate(html))
text = del_surrogate(text.strip())
text, entities = cut_long_message(text, entities)
return text, entities
except Exception as e:
raise FormatError(f"Failed to convert Matrix format: {html}") from e
def matrix_reply_to_telegram(content: MessageEventContent, tg_space: TelegramID,
room_id: Optional[RoomID] = None) -> Optional[TelegramID]:
event_id = content.get_reply_to()
@@ -103,19 +77,61 @@ def matrix_reply_to_telegram(content: MessageEventContent, tg_space: TelegramID,
return None
def matrix_text_to_telegram(text: str) -> ParsedMessage:
async def matrix_to_telegram(client: TelegramClient, *, text: Optional[str] = None,
html: Optional[str] = None) -> ParsedMessage:
if html is not None:
text, entities = _matrix_html_to_telegram(html)
elif text is not None:
text, entities = _matrix_text_to_telegram(text)
else:
raise ValueError("text or html must be provided to convert formatting")
await _fix_name_mentions(client, entities)
return text, entities
def _matrix_html_to_telegram(html: str) -> ParsedMessage:
try:
html = command_regex.sub(r"<command>\1</command>", html)
html = html.replace("\t", " " * 4)
html = not_command_regex.sub(r"\1", html)
if should_bridge_plaintext_highlights:
html = plain_mention_regex.sub(_plain_mention_to_html, html)
text, entities = parse_html(add_surrogate(html))
text = del_surrogate(text.strip())
text, entities = _cut_long_message(text, entities)
return text, entities
except Exception as e:
raise FormatError(f"Failed to convert Matrix format: {html}") from e
def _matrix_text_to_telegram(text: str) -> ParsedMessage:
text = command_regex.sub(r"/\1", text)
text = text.replace("\t", " " * 4)
text = not_command_regex.sub(r"\1", text)
if should_bridge_plaintext_highlights:
entities, pmr_replacer = plain_mention_to_text()
entities, pmr_replacer = _plain_mention_to_text()
text = plain_mention_regex.sub(pmr_replacer, text)
else:
entities = []
return text, entities
def plain_mention_to_text() -> Tuple[List[TypeMessageEntity], Callable[[Match], str]]:
async def _fix_name_mentions(client: TelegramClient, entities: List[TypeMessageEntity]) -> None:
for index in reversed(range(len(entities))):
entity = entities[index]
if isinstance(entity, (MessageEntityMentionName, InputMessageEntityMentionName)):
try:
user = await client.get_input_entity(entity.user_id)
except (ValueError, TypeError) as e:
log.trace(f"Dropping mention of {entity.user_id}: {e}")
del entities[index]
else:
entities[index] = InputMessageEntityMentionName(entity.offset, entity.length, user)
def _plain_mention_to_text() -> Tuple[List[TypeMessageEntity], Callable[[Match], str]]:
entities = []
def replacer(match: Match) -> str:
@@ -136,6 +152,16 @@ def plain_mention_to_text() -> Tuple[List[TypeMessageEntity], Callable[[Match],
return entities, replacer
def _plain_mention_to_html(match: Match) -> str:
puppet = pu.Puppet.find_by_displayname(match.group(2))
if puppet:
return (f"{match.group(1)}"
f"<a href='https://matrix.to/#/{puppet.mxid}'>"
f"{puppet.displayname}"
"</a>")
return "".join(match.groups())
def init_mx(context: "Context") -> None:
global plain_mention_regex, should_bridge_plaintext_highlights
config = context.config
+2 -2
View File
@@ -51,7 +51,7 @@ def telegram_reply_to_matrix(evt: Message, source: 'AbstractUser') -> Optional[R
else source.tgid)
msg = DBMessage.get_one_by_tgid(TelegramID(evt.reply_to.reply_to_msg_id), space)
if msg:
return RelatesTo(rel_type=RelationType.REFERENCE, event_id=msg.mxid)
return RelatesTo(rel_type=RelationType.REPLY, event_id=msg.mxid)
return None
@@ -126,7 +126,7 @@ async def _add_reply_header(source: 'AbstractUser', content: TextMessageEventCon
if not msg:
return
content.relates_to = RelatesTo(rel_type=RelationType.REFERENCE, event_id=msg.mxid)
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)
+9 -13
View File
@@ -328,17 +328,15 @@ class MatrixHandler(BaseMatrixHandler):
return
for user_id, event_id in receipts:
user = await u.User.get_by_mxid(user_id).ensure_started()
if not await user.is_logged_in():
continue
await portal.mark_read(user, event_id)
user = u.User.get_by_mxid(user_id, check_db=False, create=False)
if user and await user.is_logged_in():
await portal.mark_read(user, event_id)
@staticmethod
async def handle_presence(user_id: UserID, presence: PresenceState) -> None:
user = await u.User.get_by_mxid(user_id).ensure_started()
if not await user.is_logged_in():
return
await user.set_presence(presence == PresenceState.ONLINE)
user = u.User.get_by_mxid(user_id, check_db=False, create=False)
if user and await user.is_logged_in():
await user.set_presence(presence == PresenceState.ONLINE)
async def handle_typing(self, room_id: RoomID, now_typing: Set[UserID]) -> None:
portal = po.Portal.get_by_mxid(room_id)
@@ -353,11 +351,9 @@ class MatrixHandler(BaseMatrixHandler):
if is_typing and was_typing:
continue
user = await u.User.get_by_mxid(user_id).ensure_started()
if not await user.is_logged_in():
continue
await portal.set_typing(user, is_typing)
user = u.User.get_by_mxid(user_id, check_db=False, create=False)
if user and await user.is_logged_in():
await portal.set_typing(user, is_typing)
self.previously_typing[room_id] = now_typing
+19 -39
View File
@@ -107,6 +107,7 @@ class BasePortal(MautrixBasePortal, ABC):
_db_instance: DBPortal
_main_intent: Optional[IntentAPI]
_room_create_lock: asyncio.Lock
def __init__(self, tgid: TelegramID, peer_type: str, tg_receiver: Optional[TelegramID] = None,
mxid: Optional[RoomID] = None, username: Optional[str] = None,
@@ -155,6 +156,10 @@ class BasePortal(MautrixBasePortal, ABC):
return str(self.tgid)
return f"{self.tg_receiver}<->{self.tgid}"
@property
def name(self) -> str:
return self.title
@property
def alias(self) -> Optional[RoomAlias]:
if not self.username:
@@ -272,61 +277,32 @@ class BasePortal(MautrixBasePortal, ABC):
# endregion
# region Matrix room cleanup
async def get_authenticated_matrix_users(self) -> List['u.User']:
async def get_authenticated_matrix_users(self) -> List[UserID]:
try:
members = await self.main_intent.get_room_members(self.mxid)
except MatrixRequestError:
return []
authenticated: List[u.User] = []
authenticated: List[UserID] = []
has_bot = self.has_bot
for member_str in members:
member = UserID(member_str)
if p.Puppet.get_id_from_mxid(member) or member == self.main_intent.mxid:
for member in members:
if p.Puppet.get_id_from_mxid(member) or member == self.az.bot_mxid:
continue
user = await u.User.get_by_mxid(member).ensure_started()
authenticated_through_bot = has_bot and user.relaybot_whitelisted
if authenticated_through_bot or await user.has_full_access(allow_bot=True):
authenticated.append(user)
authenticated.append(user.mxid)
return authenticated
@classmethod
async def cleanup_room(cls, intent: IntentAPI, room_id: RoomID, message: str,
puppets_only: bool = False) -> None:
# TODO use the cleanup_room from BasePortal instead of this
try:
members = await intent.get_room_members(room_id)
except MatrixRequestError:
members = []
for user in members:
puppet = await p.Puppet.get_by_mxid(UserID(user), create=False)
if user != intent.mxid and (not puppets_only or puppet):
try:
if puppet:
await puppet.default_mxid_intent.leave_room(room_id)
else:
await intent.kick_user(room_id, user, message)
except (MatrixRequestError, IntentError):
pass
try:
await intent.leave_room(room_id)
except (MatrixRequestError, IntentError):
cls.log.warning(f"Failed to leave room {room_id} when cleaning up room", exc_info=True)
async def cleanup_portal(self, message: str, puppets_only: bool = False) -> None:
async def cleanup_portal(self, message: str, puppets_only: bool = False, delete: bool = True
) -> None:
if self.username:
try:
await self.main_intent.remove_room_alias(self.alias_localpart)
except (MatrixRequestError, IntentError):
self.log.warning("Failed to remove alias when cleaning up room", exc_info=True)
await self.cleanup_room(self.main_intent, self.mxid, message, puppets_only)
async def unbridge(self) -> None:
await self.cleanup_portal("Room unbridged", puppets_only=True)
self.delete()
async def cleanup_and_delete(self) -> None:
await self.cleanup_portal("Portal deleted")
self.delete()
if delete:
await self.delete()
# endregion
# region Database conversion
@@ -350,7 +326,10 @@ class BasePortal(MautrixBasePortal, ABC):
config=json.dumps(self.local_config), avatar_url=self.avatar_url,
encrypted=self.encrypted)
def delete(self) -> None:
async def delete(self) -> None:
self.delete_sync()
def delete_sync(self) -> None:
try:
del self.by_tgid[self.tgid_full]
except KeyError:
@@ -544,6 +523,7 @@ def init(context: Context) -> None:
global config
BasePortal.az, config, BasePortal.loop, BasePortal.bot = context.core
BasePortal.matrix = context.mx
MautrixBasePortal.bridge = context.bridge
BasePortal.max_initial_member_sync = config["bridge.max_initial_member_sync"]
BasePortal.sync_channel_members = config["bridge.sync_channel_members"]
BasePortal.sync_matrix_state = config["bridge.sync_matrix_state"]
+18 -38
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, Dict, List, Optional, Tuple, Union, Any, TYPE_CHECKING
from typing import Awaitable, Dict, Optional, Union, Any, TYPE_CHECKING
from html import escape as escape_html
from string import Template
from abc import ABC
@@ -28,11 +28,11 @@ from telethon.errors import (ChatNotModifiedError, PhotoExtInvalidError,
PhotoInvalidDimensionsError, PhotoSaveFileInvalidError,
RPCError)
from telethon.tl.patched import Message, MessageService
from telethon.tl.types import (
DocumentAttributeFilename, DocumentAttributeImageSize, GeoPoint,
InputChatUploadedPhoto, MessageActionChatEditPhoto, MessageMediaGeo,
SendMessageCancelAction, SendMessageTypingAction, TypeInputPeer, TypeMessageEntity,
UpdateNewMessage, InputMediaUploadedDocument, InputMediaUploadedPhoto)
from telethon.tl.types import (DocumentAttributeFilename, DocumentAttributeImageSize, GeoPoint,
InputChatUploadedPhoto, MessageActionChatEditPhoto, MessageMediaGeo,
SendMessageCancelAction, SendMessageTypingAction, TypeInputPeer,
UpdateNewMessage, InputMediaUploadedDocument,
InputMediaUploadedPhoto)
from mautrix.types import (EventID, RoomID, UserID, ContentURI, MessageType, MessageEventContent,
TextMessageEventContent, MediaMessageEventContent, Format,
@@ -87,7 +87,7 @@ class PortalMatrix(BasePortal, ABC):
message = await self._get_state_change_message(event, user, **kwargs)
if not message:
return
message, entities = formatter.matrix_to_telegram(message)
message, entities = await formatter.matrix_to_telegram(self.bot.client, html=message)
response = await self.bot.client.send_message(self.peer, message,
formatting_entities=entities)
space = self.tgid if self.peer_type == "channel" else self.bot.tgid
@@ -122,7 +122,7 @@ class PortalMatrix(BasePortal, ABC):
if user.tgid == source.tgid:
return None
if self.peer_type == "user" and user.tgid == self.tgid:
self.delete()
await self.delete()
return None
if isinstance(user, u.User) and await user.needs_relaybot(self):
if not self.bot:
@@ -152,7 +152,7 @@ class PortalMatrix(BasePortal, ABC):
if self.peer_type == "user":
await self.main_intent.leave_room(self.mxid)
self.delete()
await self.delete()
try:
del self.by_tgid[self.tgid_full]
del self.by_mxid[self.mxid]
@@ -214,27 +214,11 @@ class PortalMatrix(BasePortal, ABC):
elif content.msgtype == MessageType.EMOTE:
await self._apply_emote_format(sender, content)
@staticmethod
def _matrix_event_to_entities(event: Union[str, MessageEventContent]
) -> Tuple[str, Optional[List[TypeMessageEntity]]]:
try:
if isinstance(event, str):
message, entities = formatter.matrix_to_telegram(event)
elif isinstance(event, TextMessageEventContent) and event.format == Format.HTML:
message, entities = formatter.matrix_to_telegram(event.formatted_body)
else:
message, entities = formatter.matrix_text_to_telegram(event.body)
except KeyError:
message, entities = None, None
return message, entities
async def _handle_matrix_text(self, sender_id: TelegramID, event_id: EventID,
space: TelegramID, client: 'MautrixTelegramClient',
content: TextMessageEventContent, reply_to: TelegramID) -> None:
if content.formatted_body and content.format == Format.HTML:
message, entities = formatter.matrix_to_telegram(content.formatted_body)
else:
message, entities = formatter.matrix_text_to_telegram(content.body)
message, entities = await formatter.matrix_to_telegram(client, text=content.body,
html=content.formatted(Format.HTML))
async with self.send_lock(sender_id):
lp = self.get_config("telegram_link_preview")
if content.get_edit():
@@ -301,25 +285,21 @@ class PortalMatrix(BasePortal, ABC):
media = InputMediaUploadedDocument(file=file_handle, attributes=attributes,
mime_type=mime or "application/octet-stream")
if caption:
if caption.formatted_body and caption.format == Format.HTML:
caption, entities = formatter.matrix_to_telegram(caption.formatted_body)
else:
caption, entities = formatter.matrix_text_to_telegram(content.body)
else:
caption, entities = None, None
capt, entities = (await formatter.matrix_to_telegram(client, text=caption.body,
html=caption.formatted(Format.HTML))
if caption else (None, None))
async with self.send_lock(sender_id):
if await self._matrix_document_edit(client, content, space, caption, media, event_id):
if await self._matrix_document_edit(client, content, space, capt, media, event_id):
return
try:
response = await client.send_media(self.peer, media, reply_to=reply_to,
caption=caption, entities=entities)
caption=capt, entities=entities)
except (PhotoInvalidDimensionsError, PhotoSaveFileInvalidError, PhotoExtInvalidError):
media = InputMediaUploadedDocument(file=media.file, mime_type=mime,
attributes=attributes)
response = await client.send_media(self.peer, media, reply_to=reply_to,
caption=caption, entities=entities)
caption=capt, entities=entities)
self._add_telegram_message_to_db(event_id, space, 0, response)
await self._send_delivery_receipt(event_id)
@@ -346,7 +326,7 @@ class PortalMatrix(BasePortal, ABC):
except (KeyError, ValueError):
self.log.exception("Failed to parse location")
return None
caption, entities = formatter.matrix_text_to_telegram(content.body)
caption, entities = await formatter.matrix_to_telegram(client, text=content.body)
media = MessageMediaGeo(geo=GeoPoint(lat, long, access_hash=0))
async with self.send_lock(sender_id):
+31 -28
View File
@@ -97,7 +97,7 @@ class PortalMetadata(BasePortal, ABC):
pass
try:
existing = self.by_tgid[(new_id, new_id)]
existing.delete()
existing.delete_sync()
except KeyError:
pass
self.db_instance.edit(tgid=new_id, tg_receiver=new_id, peer_type=self.peer_type)
@@ -295,17 +295,14 @@ class PortalMetadata(BasePortal, ABC):
async def _create_matrix_room(self, user: 'AbstractUser', entity: Union[TypeChat, User],
invites: InviteList) -> Optional[RoomID]:
direct = self.peer_type == "user"
if invites is None:
invites = []
if self.mxid:
return self.mxid
if not self.allow_bridging:
elif not self.allow_bridging:
return None
direct = self.peer_type == "user"
invites = invites or []
if not entity:
entity = await self.get_entity(user)
self.log.trace("Fetched data: %s", entity)
@@ -576,32 +573,38 @@ class PortalMetadata(BasePortal, ABC):
if self.max_initial_member_sync < 0
else len(allowed_tgids) < self.max_initial_member_sync - 10)
and (self.megagroup or self.peer_type != "channel"))
if trust_member_list:
joined_mxids = await self.main_intent.get_room_members(self.mxid)
for user_mxid in joined_mxids:
if user_mxid == self.az.bot_mxid:
continue
puppet_id = p.Puppet.get_id_from_mxid(user_mxid)
if puppet_id and puppet_id not in allowed_tgids:
if self.bot and puppet_id == self.bot.tgid:
self.bot.remove_chat(self.tgid)
try:
await self.main_intent.kick_user(self.mxid, user_mxid,
"User had left this Telegram chat.")
except MForbidden:
pass
continue
mx_user = u.User.get_by_mxid(user_mxid, create=False)
if mx_user and mx_user.is_bot and mx_user.tgid not in allowed_tgids:
await mx_user.unregister_portal(*self.tgid_full)
if not trust_member_list:
return
if mx_user and not self.has_bot and mx_user.tgid not in allowed_tgids:
for user_mxid in await self.main_intent.get_room_members(self.mxid):
if user_mxid == self.az.bot_mxid:
continue
puppet_id = p.Puppet.get_id_from_mxid(user_mxid)
if puppet_id:
if puppet_id in allowed_tgids:
continue
if self.bot and puppet_id == self.bot.tgid:
self.bot.remove_chat(self.tgid)
try:
await self.main_intent.kick_user(self.mxid, user_mxid,
"User had left this Telegram chat.")
except MForbidden:
pass
continue
mx_user = u.User.get_by_mxid(user_mxid, create=False)
if mx_user:
if mx_user.tgid in allowed_tgids:
continue
if mx_user.is_bot:
await mx_user.unregister_portal(*self.tgid_full)
if not self.has_bot:
try:
await self.main_intent.kick_user(self.mxid, mx_user.mxid,
"You had left this Telegram chat.")
except MForbidden:
pass
continue
async def _add_telegram_user(self, user_id: TelegramID, source: Optional['AbstractUser'] = None
) -> None:
+20 -15
View File
@@ -48,7 +48,7 @@ config: Optional['Config'] = None
SearchResult = NamedTuple('SearchResult', puppet='pu.Puppet', similarity=int)
METRIC_LOGGED_IN = Gauge('bridge_logged_in', 'Users logged into bridge')
METRIC_CONNECTED = Gauge('bridge_connected', 'Users connected')
METRIC_CONNECTED = Gauge('bridge_connected', 'Users connected to Telegram')
class User(AbstractUser, BaseUser):
@@ -201,16 +201,12 @@ class User(AbstractUser, BaseUser):
async def start(self, delete_unless_authenticated: bool = False) -> 'User':
await super().start()
self._track_metric(METRIC_CONNECTED, True)
if await self.is_logged_in():
self.log.debug(f"Ensuring post_login() for {self.name}")
self.loop.create_task(self.post_login())
if config["metrics.enabled"]:
self._track_connection_task = self.loop.create_task(self._track_connection())
elif delete_unless_authenticated:
self.log.debug(f"Unauthenticated user {self.name} start()ed, deleting session...")
await self.client.disconnect()
self._track_metric(METRIC_CONNECTED, False)
self.client.session.delete()
return self
@@ -230,6 +226,9 @@ class User(AbstractUser, BaseUser):
self._track_metric(METRIC_CONNECTED, False)
async def post_login(self, info: TLUser = None, first_login: bool = False) -> None:
if config["metrics.enabled"] and not self._track_connection_task:
self._track_connection_task = self.loop.create_task(self._track_connection())
try:
await self.update_info(info)
except Exception:
@@ -305,11 +304,14 @@ class User(AbstractUser, BaseUser):
for _, portal in self.portals.items():
if not portal or portal.deleted or not portal.mxid or portal.has_bot:
continue
try:
await portal.main_intent.kick_user(portal.mxid, self.mxid,
"Logged out of Telegram.")
except MatrixRequestError:
pass
if portal.peer_type == "user":
await portal.cleanup_portal("Logged out of Telegram")
else:
try:
await portal.main_intent.kick_user(portal.mxid, self.mxid,
"Logged out of Telegram.")
except MatrixRequestError:
pass
self.portals = {}
self.contacts = []
await self.save(portals=True, contacts=True)
@@ -324,6 +326,7 @@ class User(AbstractUser, BaseUser):
if not ok:
return False
self.delete()
await self.stop()
self._track_metric(METRIC_LOGGED_IN, False)
return True
@@ -459,7 +462,8 @@ class User(AbstractUser, BaseUser):
# region Class instance lookup
@classmethod
def get_by_mxid(cls, mxid: UserID, create: bool = True) -> Optional['User']:
def get_by_mxid(cls, mxid: UserID, create: bool = True, check_db: bool = True
) -> Optional['User']:
if not mxid:
raise ValueError("Matrix ID can't be empty")
@@ -468,10 +472,11 @@ class User(AbstractUser, BaseUser):
except KeyError:
pass
user = DBUser.get_by_mxid(mxid)
if user:
user = cls.from_db(user)
return user
if check_db:
user = DBUser.get_by_mxid(mxid)
if user:
user = cls.from_db(user)
return user
if create:
user = cls(mxid)
+5 -3
View File
@@ -30,7 +30,7 @@ from telethon.errors import (AuthBytesInvalidError, AuthKeyInvalidError, Locatio
SecurityError, FileIdInvalidError)
from mautrix.appservice import IntentAPI
from mautrix.types import EncryptedFile
from mautrix.util.network_retry import call_with_net_retry
from ..tgclient import MautrixTelegramClient
from ..db import TelegramFile as DBTelegramFile
@@ -145,7 +145,8 @@ async def transfer_thumbnail_to_matrix(client: MautrixTelegramClient, intent: In
if encrypt:
file, decryption_info = encrypt_attachment(file)
upload_mime_type = "application/octet-stream"
content_uri = await intent.upload_media(file, upload_mime_type)
content_uri = await call_with_net_retry(intent.upload_media, file, upload_mime_type,
_action="upload media")
if decryption_info:
decryption_info.url = content_uri
@@ -246,7 +247,8 @@ async def _unlocked_transfer_file_to_matrix(client: MautrixTelegramClient, inten
if encrypt and encrypt_attachment:
file, decryption_info = encrypt_attachment(file)
upload_mime_type = "application/octet-stream"
content_uri = await intent.upload_media(file, upload_mime_type)
content_uri = await call_with_net_retry(intent.upload_media, file, upload_mime_type,
_action="upload media")
if decryption_info:
decryption_info.url = content_uri
+30 -28
View File
@@ -141,6 +141,12 @@ class ProvisioningAPI(AuthAPI):
return self.get_error_response(403, "not_enough_permissions",
"You do not have the permissions to bridge that room.")
is_logged_in = user is not None and await user.is_logged_in()
acting_user = user if is_logged_in else self.context.bot
if not acting_user:
return self.get_login_response(status=403, errcode="not_logged_in",
error="You are not logged in and there is no relay bot.")
portal = Portal.get_by_tgid(tgid, peer_type=peer_type)
if portal.mxid == room_id:
return self.get_error_response(200, "bridge_exists",
@@ -157,35 +163,30 @@ class ProvisioningAPI(AuthAPI):
"Telegram chat is already bridged to another "
"Matrix room.")
is_logged_in = user is not None and await user.is_logged_in()
acting_user = user if is_logged_in else self.context.bot
if not acting_user:
return self.get_login_response(status=403, errcode="not_logged_in",
error="You are not logged in and there is no relay bot.")
async with portal._room_create_lock:
entity: Optional[TypeChat] = None
try:
entity = await acting_user.client.get_entity(portal.peer)
except Exception:
self.log.exception("Failed to get_entity(%s) for manual bridging.", portal.peer)
entity: Optional[TypeChat] = None
try:
entity = await acting_user.client.get_entity(portal.peer)
except Exception:
self.log.exception("Failed to get_entity(%s) for manual bridging.", portal.peer)
if not entity or isinstance(entity, (ChatForbidden, ChannelForbidden)):
if is_logged_in:
return self.get_error_response(403, "user_not_in_chat",
if not entity or isinstance(entity, (ChatForbidden, ChannelForbidden)):
if is_logged_in:
return self.get_error_response(403, "user_not_in_chat",
"Failed to get info of Telegram chat. "
"Are you in the chat?")
return self.get_error_response(403, "bot_not_in_chat",
"Failed to get info of Telegram chat. "
"Are you in the chat?")
return self.get_error_response(403, "bot_not_in_chat",
"Failed to get info of Telegram chat. "
"Is the relay bot in the chat?")
"Is the relay bot in the chat?")
direct = False
portal.mxid = room_id
portal.by_mxid[portal.mxid] = portal
(portal.title, portal.about, levels,
portal.encrypted) = await get_initial_state(self.az.intent, room_id)
portal.photo_id = ""
await portal.save()
portal.mxid = room_id
portal.title, portal.about, levels = await get_initial_state(self.az.intent, room_id)
portal.photo_id = ""
await portal.save()
asyncio.ensure_future(portal.update_matrix_room(user, entity, direct, levels=levels),
asyncio.ensure_future(portal.update_matrix_room(user, entity, direct=False, levels=levels),
loop=self.loop)
return web.Response(status=202, body="{}")
@@ -216,7 +217,7 @@ class ProvisioningAPI(AuthAPI):
"You do not have the permissions to bridge that room.")
try:
title, about, _ = await get_initial_state(self.az.intent, room_id)
title, about, _, encrypted = await get_initial_state(self.az.intent, room_id)
except (MatrixRequestError, IntentError):
return self.get_error_response(403, "bot_not_in_room",
"The bridge bot is not in the given room.")
@@ -240,11 +241,12 @@ class ProvisioningAPI(AuthAPI):
"group": "chat",
}[type]
portal = Portal(tgid=TelegramID(0), mxid=room_id, title=title, about=about, peer_type=type)
portal = Portal(tgid=TelegramID(0), mxid=room_id, title=title, about=about, peer_type=type,
encrypted=encrypted)
try:
await portal.create_telegram_chat(user, supergroup=supergroup)
except ValueError as e:
portal.delete()
await portal.delete()
return self.get_error_response(500, "unknown_error", e.args[0])
return web.json_response({
+3 -3
View File
@@ -3,8 +3,8 @@ alembic>=1,<2
ruamel.yaml>=0.15.35,<0.17
python-magic>=0.4,<0.5
commonmark>=0.8,<0.10
aiohttp>=3,<3.7
yarl<1.6
mautrix==0.8.0rc1
aiohttp>=3,<4
yarl>=1,<2
mautrix>=0.8.3,<0.9
telethon>=1.17,<1.18
telethon-session-sqlalchemy>=0.2.14,<0.3