async def _add_instagram_reply(self, content: MessageEventContent, reply_to: Optional[ThreadItem]) -> None: if not reply_to: return message = await DBMessage.get_by_item_id(reply_to.item_id, self.receiver) if not message: return content.set_reply(message.mxid) if not isinstance(content, TextMessageEventContent): return try: evt = await self.main_intent.get_event(message.mx_room, message.mxid) except (MNotFound, MForbidden): evt = None if not evt: return if evt.type == EventType.ROOM_ENCRYPTED: try: evt = await self.matrix.e2ee.decrypt(evt, wait_session_timeout=0) except SessionNotFound: return if isinstance(evt.content, TextMessageEventContent): evt.content.trim_reply_fallback() content.set_reply(evt)
async def handle_message(self, room_id: RoomID, user_id: UserID, message: MessageEventContent, event_id: EventID) -> None: sender = await self.bridge.get_user(user_id) if not sender or not await self.allow_message(sender): self.log.debug(f"Ignoring message {event_id} from {user_id} to {room_id}:" " User is not whitelisted.") return self.log.debug(f"Received Matrix event {event_id} from {sender.mxid} in {room_id}") self.log.trace("Event %s content: %s", event_id, message) if isinstance(message, TextMessageEventContent): message.trim_reply_fallback() is_command, text = self.is_command(message) portal = await self.bridge.get_portal(room_id) if not is_command and portal and await self.allow_bridging_message(sender, portal): await portal.handle_matrix_message(sender, message, event_id) return if message.msgtype != MessageType.TEXT or not await self.allow_command(sender): return is_management = await self.is_management(room_id) 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_id, event_id, sender, command, args, message, is_management, is_portal=portal is not None)
async def apply_relay_message_format( self, sender: br.BaseUser, content: MessageEventContent ) -> None: if self.relay_formatted_body and content.get("format", None) != Format.HTML: content["format"] = Format.HTML content["formatted_body"] = html.escape(content.body).replace("\n", "<br/>") tpl = self.bridge.config["bridge.relay.message_formats"].get( content.msgtype.value, "$sender_displayname: $message" ) displayname = await self.get_displayname(sender) username, _ = self.az.intent.parse_user_id(sender.mxid) tpl_args = { "sender_mxid": sender.mxid, "sender_username": username, "sender_displayname": html.escape(displayname), "formatted_body": content["formatted_body"], "body": content.body, "message": content.body, } content.body = Template(tpl).safe_substitute(tpl_args) if self.relay_formatted_body and "formatted_body" in content: tpl_args["message"] = content["formatted_body"] content["formatted_body"] = Template(tpl).safe_substitute(tpl_args) if self.relay_emote_to_text and content.msgtype == MessageType.EMOTE: content.msgtype = MessageType.TEXT
async def _matrix_document_edit(self, client: 'MautrixTelegramClient', content: MessageEventContent, space: TelegramID, caption: str, media: Any, event_id: EventID) -> bool: if content.get_edit(): orig_msg = DBMessage.get_by_mxid(content.get_edit(), self.mxid, space) if orig_msg: response = await client.edit_message(self.peer, orig_msg.tgid, caption, file=media) self._add_telegram_message_to_db(event_id, space, -1, response) return True return False
async def handle_matrix_message(self, sender: 'u.User', message: MessageEventContent, event_id: EventID) -> None: if ((message.get(self.bridge.real_user_content_key, False) and await p.Puppet.get_by_custom_mxid(sender.mxid))): self.log.debug( f"Ignoring puppet-sent message by confirmed puppet user {sender.mxid}" ) return request_id = int(time.time() * 1000) self._msgts_dedup.appendleft((sender.uuid, request_id)) quote = None if message.get_reply_to(): reply = await DBMessage.get_by_mxid(message.get_reply_to(), self.mxid) # TODO include actual text? either store in db or fetch event from homeserver quote = Quote(id=reply.timestamp, author=Address(uuid=reply.sender), text="") text = message.body attachments: Optional[List[Attachment]] = None attachment_path: Optional[str] = None if message.msgtype == MessageType.EMOTE: text = f"/me {text}" elif message.msgtype.is_media: attachment_path = await self._download_matrix_media(message) attachment = self._make_attachment(message, attachment_path) attachments = [attachment] text = None self.log.trace("Formed outgoing attachment %s", attachment) await self.signal.send(username=sender.username, recipient=self.recipient, body=text, quote=quote, attachments=attachments, timestamp=request_id) msg = DBMessage(mxid=event_id, mx_room=self.mxid, sender=sender.uuid, timestamp=request_id, signal_chat_id=self.chat_id, signal_receiver=self.receiver) await msg.insert() await self._send_delivery_receipt(event_id) self.log.debug(f"Handled Matrix message {event_id} -> {request_id}") if attachment_path and self.config["signal.remove_file_after_handling"]: try: os.remove(attachment_path) except FileNotFoundError: pass
async def matrix_reply_to_telegram( content: MessageEventContent, tg_space: TelegramID, room_id: RoomID | None = None) -> TelegramID | None: event_id = content.get_reply_to() if not event_id: return content.trim_reply_fallback() message = await DBMessage.get_by_mxid(event_id, room_id, tg_space) if message: return message.tgid return None
def matrix_reply_to_telegram( content: MessageEventContent, tg_space: TelegramID, room_id: Optional[RoomID] = None) -> Optional[TelegramID]: event_id = content.get_reply_to() if not event_id: return content.trim_reply_fallback() message = DBMessage.get_by_mxid(event_id, room_id, tg_space) if message: return message.tgid return None
async def _apply_msg_format(self, sender: 'u.User', content: MessageEventContent ) -> None: if not isinstance(content, TextMessageEventContent) or content.format != Format.HTML: content.format = Format.HTML content.formatted_body = escape_html(content.body).replace("\n", "<br/>") tpl = (self.get_config(f"message_formats.[{content.msgtype.value}]") or "<b>$sender_displayname</b>: $message") displayname = await self.get_displayname(sender) tpl_args = dict(sender_mxid=sender.mxid, sender_username=sender.mxid_localpart, sender_displayname=escape_html(displayname), message=content.formatted_body, body=content.body, formatted_body=content.formatted_body) content.formatted_body = Template(tpl).safe_substitute(tpl_args)
async def handle_matrix_message(self, sender: 'u.User', message: MessageEventContent, event_id: EventID) -> None: puppet = p.Puppet.get_by_custom_mxid(sender.mxid) if puppet and message.get("net.maunium.hangouts.puppet", False): self.log.debug( f"Ignoring puppet-sent message by confirmed puppet user {sender.mxid}" ) return # TODO this probably isn't nice for bridging images, it really only needs to lock the # actual message send call and dedup queue append. async with self.require_send_lock(sender.gid): if message.msgtype == MessageType.TEXT or message.msgtype == MessageType.NOTICE: gid = await self._handle_matrix_text(sender, message) elif message.msgtype == MessageType.EMOTE: gid = await self._handle_matrix_emote(sender, message) elif message.msgtype == MessageType.IMAGE: gid = await self._handle_matrix_image(sender, message) # elif message.msgtype == MessageType.LOCATION: # gid = await self._handle_matrix_location(sender, message) else: self.log.warning(f"Unsupported msgtype in {message}") return if not gid: return self._dedup.appendleft(gid) DBMessage(mxid=event_id, mx_room=self.mxid, gid=gid, receiver=self.receiver, index=0).insert() self._last_bridged_mxid = event_id await self._send_delivery_receipt(event_id)
async def _pre_process_matrix_message( self, sender: 'u.User', use_relaybot: bool, content: MessageEventContent) -> None: if content.msgtype == MessageType.EMOTE: await self._apply_msg_format(sender, content) content.msgtype = MessageType.TEXT elif use_relaybot: await self._apply_msg_format(sender, content)
async def handle_matrix_message(self, sender: 'u.User', content: MessageEventContent, event_id: EventID) -> None: if not content.body or not content.msgtype: self.log.debug(f"Ignoring message {event_id} in {self.mxid} without body or msgtype") return puppet = p.Puppet.get_by_custom_mxid(sender.mxid) if puppet and content.get("net.maunium.telegram.puppet", False): self.log.debug("Ignoring puppet-sent message by confirmed puppet user %s", sender.mxid) return logged_in = not await sender.needs_relaybot(self) client = sender.client if logged_in else self.bot.client sender_id = sender.tgid if logged_in else self.bot.tgid space = (self.tgid if self.peer_type == "channel" # Channels have their own ID space else (sender.tgid if logged_in else self.bot.tgid)) reply_to = formatter.matrix_reply_to_telegram(content, space, room_id=self.mxid) media = (MessageType.STICKER, MessageType.IMAGE, MessageType.FILE, MessageType.AUDIO, MessageType.VIDEO) if content.msgtype == MessageType.NOTICE: bridge_notices = self.get_config("bridge_notices.default") excepted = sender.mxid in self.get_config("bridge_notices.exceptions") if not bridge_notices and not excepted: return if content.msgtype in (MessageType.TEXT, MessageType.EMOTE, MessageType.NOTICE): await self._pre_process_matrix_message(sender, not logged_in, content) await self._handle_matrix_text(sender_id, event_id, space, client, content, reply_to) elif content.msgtype == MessageType.LOCATION: await self._pre_process_matrix_message(sender, not logged_in, content) await self._handle_matrix_location(sender_id, event_id, space, client, content, reply_to) elif content.msgtype in media: content["net.maunium.telegram.internal.filename"] = content.body try: caption_content: MessageEventContent = sender.command_status["caption"] reply_to = reply_to or formatter.matrix_reply_to_telegram(caption_content, space, room_id=self.mxid) sender.command_status = None except (KeyError, TypeError): caption_content = None if logged_in else TextMessageEventContent(body=content.body) if caption_content: caption_content.msgtype = content.msgtype await self._pre_process_matrix_message(sender, not logged_in, caption_content) await self._handle_matrix_file(sender_id, event_id, space, client, content, reply_to, caption_content) else: self.log.debug(f"Unhandled Matrix event: {content}")
async def handle_matrix_message(self, sender: 'u.User', message: MessageEventContent, event_id: EventID) -> None: if not sender.client: self.log.debug( f"Ignoring message {event_id} as user is not connected") return elif ((message.get(self.bridge.real_user_content_key, False) and await p.Puppet.get_by_custom_mxid(sender.mxid))): self.log.debug( f"Ignoring puppet-sent message by confirmed puppet user {sender.mxid}" ) return request_id = str(sender.client.new_request_id()) self._reqid_dedup.add(request_id) text = message.body media_id = None if message.msgtype == MessageType.EMOTE: text = f"/me {text}" elif message.msgtype.is_media: if message.file and decrypt_attachment: data = await self.main_intent.download_media(message.file.url) data = decrypt_attachment(data, message.file.key.key, message.file.hashes.get("sha256"), message.file.iv) else: data = await self.main_intent.download_media(message.url) mime_type = message.info.mimetype or magic.from_buffer(data, mime=True) # TODO this will throw errors if the mime type is not supported # those errors should be sent back to the client upload_resp = await sender.client.upload(data, mime_type=mime_type) media_id = upload_resp.media_id text = "" resp = await sender.client.conversation(self.twid ).send(text, media_id=media_id, request_id=request_id) resp_msg_id = int(resp.entries[0].message.id) self._msgid_dedup.appendleft(resp_msg_id) msg = DBMessage(mxid=event_id, mx_room=self.mxid, twid=resp_msg_id, receiver=self.receiver) await msg.insert() self._reqid_dedup.remove(request_id) await self._send_delivery_receipt(event_id) self.log.debug(f"Handled Matrix message {event_id} -> {resp_msg_id}")
async def _handle_matrix_message(self, sender: 'u.User', message: MessageEventContent, event_id: EventID) -> None: if ((message.get(self.bridge.real_user_content_key, False) and await p.Puppet.get_by_custom_mxid(sender.mxid))): self.log.debug(f"Ignoring puppet-sent message by confirmed puppet user {sender.mxid}") return elif not sender.is_connected: await self._send_bridge_error("You're not connected to Instagram", confirmed=True) return else: self.log.debug(f"Handling Matrix message {event_id} from {sender.mxid}/{sender.igpk}") request_id = str(uuid4()) self._reqid_dedup.add(request_id) if message.msgtype in (MessageType.EMOTE, MessageType.TEXT): text = message.body if message.msgtype == MessageType.EMOTE: text = f"/me {text}" self.log.trace(f"Sending Matrix text from {event_id} with request ID {request_id}") resp = await sender.mqtt.send_text(self.thread_id, text=text, client_context=request_id) elif message.msgtype.is_media: if message.file and decrypt_attachment: data = await self.main_intent.download_media(message.file.url) data = decrypt_attachment(data, message.file.key.key, message.file.hashes.get("sha256"), message.file.iv) else: data = await self.main_intent.download_media(message.url) mime_type = message.info.mimetype or magic.from_buffer(data, mime=True) if mime_type != "image/jpeg" and mime_type.startswith("image/"): with BytesIO(data) as inp: img = Image.open(inp) with BytesIO() as out: img.convert("RGB").save(out, format="JPEG", quality=80) data = out.getvalue() mime_type = "image/jpeg" if mime_type == "image/jpeg": self.log.trace(f"Uploading photo from {event_id}") upload_resp = await sender.client.upload_jpeg_photo(data) self.log.trace(f"Broadcasting uploaded photo with request ID {request_id}") # TODO is it possible to do this with MQTT? resp = await sender.client.broadcast(self.thread_id, ThreadItemType.CONFIGURE_PHOTO, client_context=request_id, upload_id=upload_resp.upload_id, allow_full_aspect_ratio="1") else: await self._send_bridge_error("Non-image files are currently not supported", confirmed=True) return else: self.log.debug(f"Unhandled Matrix message {event_id}: " f"unknown msgtype {message.msgtype}") return self.log.trace(f"Got response to message send {request_id}: {resp}") if resp.status != "ok": self.log.warning(f"Failed to handle {event_id}: {resp}") await self._send_bridge_error(resp.payload.message) else: self._msgid_dedup.appendleft(resp.payload.item_id) await DBMessage(mxid=event_id, mx_room=self.mxid, item_id=resp.payload.item_id, receiver=self.receiver, sender=sender.igpk).insert() self._reqid_dedup.remove(request_id) await self._send_delivery_receipt(event_id) self.log.debug(f"Handled Matrix message {event_id} -> {resp.payload.item_id}")
async def handle_message( self, room_id: RoomID, user_id: UserID, message: MessageEventContent, event_id: EventID ) -> None: async def bail(error_text: str, step=MessageSendCheckpointStep.REMOTE) -> None: self.log.debug(error_text) await MessageSendCheckpoint( event_id=event_id, room_id=room_id, step=step, timestamp=int(time.time() * 1000), status=MessageSendCheckpointStatus.PERM_FAILURE, reported_by=MessageSendCheckpointReportedBy.BRIDGE, event_type=EventType.ROOM_MESSAGE, message_type=message.msgtype, info=error_text, ).send( self.bridge.config["homeserver.message_send_checkpoint_endpoint"], self.az.as_token, self.log, ) sender = await self.bridge.get_user(user_id) if not sender or not await self.allow_message(sender): await bail( f"Ignoring message {event_id} from {user_id} to {room_id}:" " user is not whitelisted." ) return self.log.debug(f"Received Matrix event {event_id} from {sender.mxid} in {room_id}") self.log.trace("Event %s content: %s", event_id, message) if isinstance(message, TextMessageEventContent): message.trim_reply_fallback() is_command, text = self.is_command(message) portal = await self.bridge.get_portal(room_id) if not is_command and portal: if await self.allow_bridging_message(sender, portal): await portal.handle_matrix_message(sender, message, event_id) else: await bail( f"Ignoring event {event_id} from {sender.mxid}:" " not allowed to send to portal" ) return if message.msgtype != MessageType.TEXT: await bail(f"Ignoring event {event_id}: not a portal room and not a m.text message") return elif not await self.allow_command(sender): await bail( f"Ignoring command {event_id} from {sender.mxid}: not allowed to perform command", step=MessageSendCheckpointStep.COMMAND, ) return has_two_members, bridge_bot_in_room = await self._is_direct_chat(room_id) is_management = has_two_members and bridge_bot_in_room 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 = [] try: await self.commands.handle( room_id, event_id, sender, command, args, message, portal, is_management, bridge_bot_in_room, ) except Exception as e: await bail(repr(e), step=MessageSendCheckpointStep.COMMAND) else: await MessageSendCheckpoint( event_id=event_id, room_id=room_id, step=MessageSendCheckpointStep.COMMAND, timestamp=int(time.time() * 1000), status=MessageSendCheckpointStatus.SUCCESS, reported_by=MessageSendCheckpointReportedBy.BRIDGE, event_type=EventType.ROOM_MESSAGE, message_type=message.msgtype, ).send( self.bridge.config["homeserver.message_send_checkpoint_endpoint"], self.az.as_token, self.log, ) else: await bail( f"Ignoring event {event_id} from {sender.mxid}:" " not a command and not a portal room" )