Start migrating to mautrix-python

This commit is contained in:
Tulir Asokan
2019-07-13 00:23:46 +03:00
parent e0d3c940f8
commit 8d4a9dc231
14 changed files with 263 additions and 797 deletions
+128 -246
View File
@@ -13,66 +13,59 @@
#
# 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 Dict, List, Match, Optional, Set, Tuple, TYPE_CHECKING
import logging
import asyncio
from typing import Dict, Match, Optional, Set, Tuple, Union, Iterable, TYPE_CHECKING
import time
import re
from mautrix_appservice import MatrixRequestError, IntentError
from mautrix.bridge import BaseMatrixHandler
from mautrix.types import (Event, EventType, RoomID, UserID, EventID, ReceiptEvent, ReceiptType,
ReceiptEventContent, PresenceEvent, PresenceState, TypingEvent,
MessageEvent, StateEvent, RedactionEvent, RoomNameStateEventContent,
RoomAvatarStateEventContent, RoomTopicStateEventContent,
MemberStateEventContent)
from mautrix.errors import MatrixError
from .types import MatrixEvent, MatrixEventID, MatrixRoomID, MatrixUserID
from . import user as u, portal as po, puppet as pu, commands as com
if TYPE_CHECKING:
from .context import Context
from .config import Config
from .bot import Bot
from mautrix_appservice import AppService
try:
from prometheus_client import Histogram
EVENT_TIME = Histogram("matrix_event", "Time spent processing Matrix events",
["event_type"])
EVENT_TIME = Histogram("matrix_event", "Time spent processing Matrix events", ["event_type"])
except ImportError:
Histogram = None
EVENT_TIME = None
class MatrixHandler:
log: logging.Logger = logging.getLogger("mau.mx")
az: 'AppService'
config: 'Config'
RoomMetaStateEventContent = Union[RoomNameStateEventContent, RoomAvatarStateEventContent,
RoomTopicStateEventContent]
class MatrixHandler(BaseMatrixHandler):
bot: 'Bot'
commands: 'com.CommandProcessor'
previously_typing: Dict[MatrixRoomID, Set[MatrixUserID]]
previously_typing: Dict[RoomID, Set[UserID]]
def __init__(self, context: 'Context') -> None:
self.az, self.config, _, self.tgbot = context.core
self.commands = com.CommandProcessor(context)
super(MatrixHandler, self).__init__(context.az, context.config, loop=context.loop,
command_processor=com.CommandProcessor(context))
self.bot = context.bot
self.previously_typing = {}
self.az.matrix_event_handler(self.handle_event)
async def get_user(self, user_id: UserID) -> 'u.User':
return await u.User.get_by_mxid(user_id).ensure_started()
async def init_as_bot(self) -> None:
displayname = self.config["appservice.bot_displayname"]
if displayname:
try:
await self.az.intent.set_display_name(
displayname if displayname != "remove" else "")
except asyncio.TimeoutError:
self.log.exception("TimeoutError when trying to set displayname")
async def get_portal(self, room_id: RoomID) -> 'po.Portal':
return po.Portal.get_by_mxid(room_id)
avatar = self.config["appservice.bot_avatar"]
if avatar:
try:
await self.az.intent.set_avatar(avatar if avatar != "remove" else "")
except asyncio.TimeoutError:
self.log.exception("TimeoutError when trying to set avatar")
async def get_puppet(self, user_id: UserID) -> 'pu.Puppet':
return pu.Puppet.get_by_mxid(user_id)
async def handle_puppet_invite(self, room_id: MatrixRoomID, puppet: pu.Puppet, inviter: u.User
) -> None:
async def handle_puppet_invite(self, room_id: RoomID, puppet: pu.Puppet, inviter: u.User,
event_id: EventID) -> None:
intent = puppet.default_mxid_intent
self.log.debug(f"{inviter} invited puppet for {puppet.tgid} to {room_id}")
if not await inviter.is_logged_in():
@@ -90,7 +83,7 @@ class MatrixHandler:
return
try:
members = await self.az.intent.get_room_members(room_id)
except MatrixRequestError:
except MatrixError:
members = []
if self.az.bot_mxid not in members:
if len(members) > 1:
@@ -113,7 +106,7 @@ class MatrixHandler:
"</a>"))
await intent.leave_room(room_id)
return
except MatrixRequestError:
except MatrixError:
pass
portal.mxid = room_id
portal.save()
@@ -124,67 +117,25 @@ class MatrixHandler:
await intent.send_notice(room_id, "This puppet will remain inactive until a "
"Telegram chat is created for this room.")
async def accept_bot_invite(self, room_id: MatrixRoomID, inviter: u.User) -> None:
tries = 0
while tries < 5:
try:
await self.az.intent.join_room(room_id)
break
except (IntentError, MatrixRequestError):
tries += 1
wait_for_seconds = (tries + 1) * 10
if tries < 5:
self.log.exception(f"Failed to join room {room_id} with bridge bot, "
f"retrying in {wait_for_seconds} seconds...")
await asyncio.sleep(wait_for_seconds)
else:
self.log.exception("Failed to join room {room}, giving up.")
return
if not inviter.whitelisted:
await self.az.intent.send_notice(
room_id,
text="You are not whitelisted to use this bridge.\n\n"
"If you are the owner of this bridge, see the "
"`bridge.permissions` section in your config file.",
html="<p>You are not whitelisted to use this bridge.</p>"
"<p>If you are the owner of this bridge, see the "
"<code>bridge.permissions</code> section in your config file.</p>")
await self.az.intent.leave_room(room_id)
async def send_welcome_message(self, room_id: RoomID, inviter: 'u.User', event_id: EventID
) -> None:
try:
is_management = len(await self.az.intent.get_room_members(room_id)) == 2
except MatrixRequestError:
is_management = False
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.")
pass
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: MatrixRoomID, user_id: MatrixUserID,
inviter_mxid: MatrixUserID) -> None:
self.log.debug(f"{inviter_mxid} invited {user_id} to {room_id}")
inviter = u.User.get_by_mxid(inviter_mxid)
if inviter is None:
self.log.exception("Failed to find user with Matrix ID {inviter_mxid}")
await inviter.ensure_started()
if user_id == self.az.bot_mxid:
return await self.accept_bot_invite(room_id, inviter)
elif not inviter.whitelisted:
return
puppet = pu.Puppet.get_by_mxid(user_id)
if puppet:
await self.handle_puppet_invite(room_id, puppet, inviter)
return
async def handle_invite(self, room_id: RoomID, user_id: UserID, inviter: 'u.User') -> None:
user = u.User.get_by_mxid(user_id, create=False)
if not user:
return
@@ -194,10 +145,8 @@ class MatrixHandler:
await portal.invite_telegram(inviter, user)
return
# The rest can probably be ignored
async def handle_join(self, room_id: MatrixRoomID, user_id: MatrixUserID,
event_id: MatrixEventID) -> None:
async def handle_join(self, room_id: RoomID, user_id: UserID,
event_id: EventID) -> None:
user = await u.User.get_by_mxid(user_id).ensure_started()
portal = po.Portal.get_by_mxid(room_id)
@@ -218,11 +167,11 @@ class MatrixHandler:
if await user.is_logged_in() or portal.has_bot:
await portal.join_matrix(user, event_id)
async def handle_part(self, room_id: MatrixRoomID, user_id: MatrixUserID,
sender_mxid: MatrixUserID, event_id: MatrixEventID) -> None:
async def handle_raw_leave(self, room_id: RoomID, user_id: UserID, sender_id: UserID,
reason: str, event_id: EventID) -> None:
self.log.debug(f"{user_id} left {room_id}")
sender = u.User.get_by_mxid(sender_mxid, create=False)
sender = u.User.get_by_mxid(sender_id, create=False)
if not sender:
return
await sender.ensure_started()
@@ -233,98 +182,67 @@ class MatrixHandler:
puppet = pu.Puppet.get_by_mxid(user_id)
if puppet:
if sender:
await portal.kick_matrix(puppet, sender)
await portal.kick_matrix(puppet, sender)
return
user = u.User.get_by_mxid(user_id, create=False)
if not user:
return
await user.ensure_started()
if await user.is_logged_in() or portal.has_bot:
await portal.leave_matrix(user, sender, event_id)
def is_command(self, message: Dict) -> Tuple[bool, str]:
text = message.get("body", "")
prefix = self.config["bridge.command_prefix"]
is_command = text.startswith(prefix)
if is_command:
text = text[len(prefix) + 1:].lstrip()
return is_command, text
async def handle_message(self, room: MatrixRoomID, sender_id: MatrixUserID, message: Dict,
event_id: MatrixEventID) -> None:
is_command, text = self.is_command(message)
sender = await u.User.get_by_mxid(sender_id).ensure_started()
if not sender.relaybot_whitelisted:
self.log.debug(f"Ignoring message \"{message}\" from {sender} to {room}:"
" User is not whitelisted.")
return
self.log.debug(f"Received Matrix event \"{message}\" from {sender} in {room}")
portal = po.Portal.get_by_mxid(room)
if not is_command and portal and (await sender.is_logged_in() or portal.has_bot):
await portal.handle_matrix_message(sender, message, event_id)
return
if not sender.whitelisted or message.get("msgtype", "m.unknown") != "m.text":
return
try:
is_management = len(await self.az.intent.get_room_members(room)) == 2
except MatrixRequestError:
# The AS bot is not in the room.
return
if is_command or is_management:
try:
command, arguments = text.split(" ", 1)
args = arguments.split(" ")
except ValueError:
# Not enough values to unpack, i.e. no arguments
command = text
args = []
await self.commands.handle(room, event_id, sender, command, args, is_management,
is_portal=portal is not None)
if sender_id != user_id:
await portal.kick_matrix(user, sender)
else:
await portal.leave_matrix(user, event_id)
@staticmethod
async def handle_redaction(room_id: MatrixRoomID, sender_mxid: MatrixUserID,
event_id: MatrixEventID) -> None:
sender = await u.User.get_by_mxid(sender_mxid).ensure_started()
async def allow_message(user: 'u.User') -> bool:
return user.relaybot_whitelisted
@staticmethod
async def allow_command(user: 'u.User') -> bool:
return user.whitelisted
@staticmethod
async def allow_bridging_message(user: 'u.User', portal: 'po.Portal') -> bool:
return await user.is_logged_in() or portal.has_bot
@staticmethod
async def handle_redaction(evt: RedactionEvent) -> None:
sender = await u.User.get_by_mxid(evt.sender).ensure_started()
if not sender.relaybot_whitelisted:
return
portal = po.Portal.get_by_mxid(room_id)
portal = po.Portal.get_by_mxid(evt.room_id)
if not portal:
return
await portal.handle_matrix_deletion(sender, event_id)
await portal.handle_matrix_deletion(sender, evt.redacts)
@staticmethod
async def handle_power_levels(room_id: MatrixRoomID, sender_mxid: MatrixUserID,
new: Dict, old: Dict) -> None:
portal = po.Portal.get_by_mxid(room_id)
sender = await u.User.get_by_mxid(sender_mxid).ensure_started()
async def handle_power_levels(evt: StateEvent) -> None:
portal = po.Portal.get_by_mxid(evt.event_id)
sender = await u.User.get_by_mxid(evt.sender).ensure_started()
if await sender.has_full_access(allow_bot=True) and portal:
await portal.handle_matrix_power_levels(sender, new["users"], old["users"])
await portal.handle_matrix_power_levels(sender, evt.content.users,
evt.unsigned.prev_content.users)
@staticmethod
async def handle_room_meta(evt_type: str, room_id: MatrixRoomID, sender_mxid: MatrixUserID,
content: dict) -> None:
async def handle_room_meta(evt_type: EventType, room_id: RoomID, sender_mxid: UserID,
content: RoomMetaStateEventContent) -> None:
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:
handler, content_key = {
"m.room.name": (portal.handle_matrix_title, "name"),
"m.room.topic": (portal.handle_matrix_about, "topic"),
"m.room.avatar": (portal.handle_matrix_avatar, "url"),
EventType.ROOM_NAME: (portal.handle_matrix_title, "name"),
EventType.ROOM_TOPIC: (portal.handle_matrix_about, "topic"),
EventType.ROOM_AVATAR: (portal.handle_matrix_avatar, "url"),
}[evt_type]
if content_key not in content:
return
await handler(sender, content[content_key])
@staticmethod
async def handle_room_pin(room_id: MatrixRoomID, sender_mxid: MatrixUserID,
async def handle_room_pin(room_id: RoomID, sender_mxid: UserID,
new_events: Set[str], old_events: Set[str]) -> None:
portal = po.Portal.get_by_mxid(room_id)
sender = await u.User.get_by_mxid(sender_mxid).ensure_started()
@@ -332,55 +250,61 @@ class MatrixHandler:
events = new_events - old_events
if len(events) > 0:
# New event pinned, set that as pinned in Telegram.
await portal.handle_matrix_pin(sender, MatrixEventID(events.pop()))
await portal.handle_matrix_pin(sender, EventID(events.pop()))
elif len(new_events) == 0:
# All pinned events removed, remove pinned event in Telegram.
await portal.handle_matrix_pin(sender, None)
@staticmethod
async def handle_room_upgrade(room_id: MatrixRoomID, new_room_id: MatrixRoomID) -> None:
async def handle_room_upgrade(room_id: RoomID, new_room_id: RoomID) -> None:
portal = po.Portal.get_by_mxid(room_id)
if portal:
await portal.handle_matrix_upgrade(new_room_id)
@staticmethod
async def handle_name_change(room_id: MatrixRoomID, user_id: MatrixUserID, displayname: str,
prev_displayname: str, event_id: MatrixEventID) -> None:
async def handle_member_info_change(room_id: RoomID, user_id: UserID,
profile: MemberStateEventContent,
prev_profile: MemberStateEventContent,
event_id: EventID) -> None:
if profile.displayname == prev_profile.displayname:
return
portal = po.Portal.get_by_mxid(room_id)
if not portal or not portal.has_bot:
return
user = await u.User.get_by_mxid(user_id).ensure_started()
if await user.needs_relaybot(portal):
await portal.name_change_matrix(user, displayname, prev_displayname, event_id)
await portal.name_change_matrix(user, profile.displayname, prev_profile.displayname,
event_id)
@staticmethod
def parse_read_receipts(content: Dict) -> Dict[MatrixUserID, MatrixEventID]:
return {user_id: event_id
def parse_read_receipts(content: ReceiptEventContent) -> Iterable[Tuple[UserID, EventID]]:
return ((user_id, event_id)
for event_id, receipts in content.items()
for user_id in receipts.get("m.read", {})}
for user_id in receipts.get(ReceiptType.READ, {}))
@staticmethod
async def handle_read_receipts(room_id: MatrixRoomID,
receipts: Dict[MatrixUserID, MatrixEventID]) -> None:
async def handle_read_receipts(room_id: RoomID, receipts: Iterable[Tuple[UserID, EventID]]
) -> None:
portal = po.Portal.get_by_mxid(room_id)
if not portal:
return
for user_id, event_id in receipts.items():
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)
@staticmethod
async def handle_presence(user_id: MatrixUserID, presence: str) -> None:
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 == "online")
await user.set_presence(presence == PresenceState.ONLINE)
async def handle_typing(self, room_id: MatrixRoomID, now_typing: Set[MatrixUserID]) -> None:
async def handle_typing(self, room_id: RoomID, now_typing: Set[UserID]) -> None:
portal = po.Portal.get_by_mxid(room_id)
if not portal:
return
@@ -401,86 +325,44 @@ class MatrixHandler:
self.previously_typing[room_id] = now_typing
def filter_matrix_event(self, event: MatrixEvent) -> bool:
sender = event.get("sender", None)
if not sender:
return False
return (sender == self.az.bot_mxid
or pu.Puppet.get_id_from_mxid(sender) is not None)
def filter_matrix_event(self, evt: Event) -> bool:
if not isinstance(evt, (MessageEvent, StateEvent)):
return True
return evt.sender and (evt.sender == self.az.bot_mxid
or pu.Puppet.get_id_from_mxid(evt.sender) is not None)
async def try_handle_ephemeral_event(self, evt: MatrixEvent) -> None:
try:
await self.handle_ephemeral_event(evt)
except Exception:
self.log.exception("Error handling manually received Matrix event")
async def handle_ephemeral_event(self, evt: Union[ReceiptEvent, PresenceEvent, TypingEvent]
) -> None:
if evt.type == EventType.RECEIPT:
await self.handle_read_receipts(evt.room_id, self.parse_read_receipts(evt.content))
elif evt.type == EventType.PRESENCE:
await self.handle_presence(evt.sender, evt.content.presence)
elif evt.type == EventType.TYPING:
await self.handle_typing(evt.room_id, set(evt.content.user_ids))
async def handle_ephemeral_event(self, evt: MatrixEvent) -> None:
evt_type: str = evt.get("type", "m.unknown")
room_id: Optional[MatrixRoomID] = evt.get("room_id", None)
sender: Optional[MatrixUserID] = evt.get("sender", None)
content: Dict = evt.get("content", {})
if evt_type == "m.receipt":
await self.handle_read_receipts(room_id, self.parse_read_receipts(content))
elif evt_type == "m.presence":
await self.handle_presence(sender, content.get("presence", "offline"))
elif evt_type == "m.typing":
await self.handle_typing(room_id, set(content.get("user_ids", [])))
async def handle_event(self, evt: Event) -> None:
if evt.type == EventType.ROOM_REDACTION:
await self.handle_redaction(evt)
async def handle_event(self, evt: MatrixEvent) -> None:
if self.filter_matrix_event(evt):
return
start_time = time.time()
self.log.debug("Received event: %s", evt)
evt_type: str = evt.get("type", "m.unknown")
room_id: Optional[MatrixRoomID] = evt.get("room_id", None)
event_id: Optional[MatrixEventID] = evt.get("event_id", None)
sender: Optional[MatrixUserID] = evt.get("sender", None)
state_key = evt.get("state_key", None)
content: Dict = evt.get("content", {})
if state_key is not None:
if evt_type == "m.room.member":
prev_content: Dict = evt.get("unsigned", {}).get("prev_content", {})
membership: str = content.get("membership", "")
prev_membership: str = prev_content.get("membership", "leave")
if membership == prev_membership:
match: Match = re.compile("@(.+):(.+)").match(state_key)
mxid: str = match.group(0)
displayname: str = content.get("displayname", None) or mxid
prev_displayname: str = prev_content.get("displayname", None) or mxid
if displayname != prev_displayname:
await self.handle_name_change(room_id, state_key, displayname,
prev_displayname, event_id)
elif membership == "invite":
await self.handle_invite(room_id, state_key, sender)
elif prev_membership == "join" and membership == "leave":
await self.handle_part(room_id, state_key, sender, event_id)
elif membership == "join":
await self.handle_join(room_id, state_key, event_id)
elif evt_type == "m.room.power_levels":
prev_content = evt.get("unsigned", {}).get("prev_content", {})
await self.handle_power_levels(room_id, sender, evt["content"], prev_content)
elif evt_type in ("m.room.name", "m.room.avatar", "m.room.topic"):
await self.handle_room_meta(evt_type, room_id, sender, evt["content"])
elif evt_type == "m.room.pinned_events":
new_events = set(evt["content"]["pinned"])
try:
old_events = set(evt["unsigned"]["prev_content"]["pinned"])
except KeyError:
old_events = set()
await self.handle_room_pin(room_id, sender, new_events, old_events)
elif evt_type == "m.room.tombstone":
await self.handle_room_upgrade(room_id, evt["content"]["replacement_room"])
else:
return
else:
if evt_type in ("m.room.message", "m.sticker"):
if evt_type != "m.room.message":
content["msgtype"] = evt_type
await self.handle_message(room_id, sender, content, event_id)
elif evt_type == "m.room.redaction":
await self.handle_redaction(room_id, sender, evt["redacts"])
else:
return
async def handle_state_event(self, evt: StateEvent) -> None:
if evt.type == EventType.ROOM_POWER_LEVELS:
await self.handle_power_levels(evt)
elif evt.type in (EventType.ROOM_NAME, EventType.ROOM_AVATAR, EventType.ROOM_TOPIC):
await self.handle_room_meta(evt.type, evt.room_id, evt.sender, evt.content)
elif evt.type == EventType.ROOM_PINNED_EVENTS:
new_events = set(evt.content.pinned)
try:
old_events = set(evt.unsigned.prev_content.pinned)
except (KeyError, ValueError, TypeError, AttributeError):
old_events = set()
await self.handle_room_pin(evt.room_id, evt.sender, new_events, old_events)
elif evt.type == EventType.ROOM_TOMBSTONE:
await self.handle_room_upgrade(evt.room_id, evt.content.replacement_room)
if EVENT_TIME:
EVENT_TIME.labels(event_type=evt_type).observe(time.time() - start_time)
# async def handle_event(self, evt: MatrixEvent) -> None:
# if self.filter_matrix_event(evt):
# return
# start_time = time.time()
#
# if EVENT_TIME:
# EVENT_TIME.labels(event_type=evt_type).observe(time.time() - start_time)