def test_reply_with_cmdprefix(self, command_processor: CommandProcessor, mocker: MockFixture) -> None: mocker.patch("mautrix_telegram.user.config", self.config) evt = CommandEvent( processor=command_processor, room_id=RoomID("#mock_room:example.org"), event_id=EventID("$H45H:example.org"), sender=u.User(UserID("@sender:example.org")), command="help", args=[], is_management=False, is_portal=False, ) mock_az = command_processor.az evt.reply("$cmdprefix+sp ....$cmdprefix+sp...$cmdprefix $cmdprefix", allow_html=False, render_markdown=False) mock_az.intent.send_notice.assert_called_with( RoomID("#mock_room:example.org"), "tg ....tg+sp...tg tg", html=None, )
async def test_handle_delegated_handler( self, command_processor: CommandProcessor, boolean2: Tuple[bool, bool], mocker: MockFixture) -> None: mocker.patch('mautrix_telegram.user.config', self.config) mocker.patch('mautrix.bridge.commands.handler.command_handlers', { "help": AsyncMock(), "unknown-command": AsyncMock() }) sender = u.User(UserID("@sender:example.org")) sender.command_status = {"foo": AsyncMock(), "next": AsyncMock()} result = await command_processor.handle( room_id=RoomID("#mock_room:example.org"), event_id=EventID("$H45H:example.org"), sender=sender, # u.User command="foo", args=[], is_management=boolean2[0], is_portal=boolean2[1]) assert result is None command_handlers = mautrix.bridge.commands.handler.command_handlers command_handlers["help"].mock.assert_not_called() # type: ignore command_handlers["unknown-command"].mock.assert_not_called( ) # type: ignore sender.command_status["foo"].mock.assert_not_called() # type: ignore sender.command_status["next"].mock.assert_called_once() # type: ignore
async def handle_telegram_edit(self, source: 'AbstractUser', sender: p.Puppet, evt: Message ) -> None: if not self.mxid: return elif hasattr(evt, "media") and isinstance(evt.media, MessageMediaGame): self.log.debug("Ignoring game message edit event") return async with self.send_lock(sender.tgid if sender else None, required=False): tg_space = self.tgid if self.peer_type == "channel" else source.tgid temporary_identifier = EventID( f"${random.randint(1000000000000, 9999999999999)}TGBRIDGEDITEMP") duplicate_found = self.dedup.check(evt, (temporary_identifier, tg_space), force_hash=True) if duplicate_found: mxid, other_tg_space = duplicate_found if tg_space != other_tg_space: prev_edit_msg = DBMessage.get_one_by_tgid(TelegramID(evt.id), tg_space, -1) if not prev_edit_msg: return DBMessage(mxid=mxid, mx_room=self.mxid, tg_space=tg_space, tgid=TelegramID(evt.id), edit_index=prev_edit_msg.edit_index + 1 ).insert() return content = await formatter.telegram_to_matrix(evt, source, self.main_intent, no_reply_fallback=True) editing_msg = DBMessage.get_one_by_tgid(TelegramID(evt.id), tg_space) if not editing_msg: self.log.info(f"Didn't find edited message {evt.id}@{tg_space} (src {source.tgid}) " "in database.") return content.msgtype = (MessageType.NOTICE if (sender and sender.is_bot and self.get_config("bot_messages_as_notices")) else MessageType.TEXT) content.external_url = self._get_external_url(evt) content.set_edit(editing_msg.mxid) # TODO remove this stuff once mautrix-python generates m.new_content new_content = content.serialize() del new_content["m.relates_to"] content["m.new_content"] = new_content content.body = f"Edit: {content.body}" content.format = Format.HTML content.formatted_body = (f"<a href=\"https://matrix.to/#/{editing_msg.mx_room}/" f"{editing_msg.mxid}\">Edit</a>: " f"{content.formatted_body or escape_html(content.body)}") intent = sender.intent_for(self) if sender else self.main_intent await intent.set_typing(self.mxid, is_typing=False) event_id = await intent.send_message(self.mxid, content) prev_edit_msg = DBMessage.get_one_by_tgid(TelegramID(evt.id), tg_space, -1) or editing_msg DBMessage(mxid=event_id, mx_room=self.mxid, tg_space=tg_space, tgid=TelegramID(evt.id), edit_index=prev_edit_msg.edit_index + 1).insert() DBMessage.update_by_mxid(temporary_identifier, self.mxid, mxid=event_id)
async def test_permission_granted( self, is_management: bool, puppet_whitelisted: bool, matrix_puppet_whitelisted: bool, is_admin: bool, is_logged_in: bool, command_processor: CommandProcessor, boolean: bool, mocker: MockFixture, ) -> None: mocker.patch("mautrix_telegram.user.config", self.config) command = "testcmd" mock_handler = Mock() command_handler = CommandHandler( handler=mock_handler, needs_auth=False, needs_puppeting=False, needs_matrix_puppeting=False, needs_admin=False, management_only=False, name=command, help_text="No real command", help_args="mock mockmock", help_section=HelpSection("Mock Section", 42, ""), ) sender = u.User(UserID("@sender:example.org")) sender.puppet_whitelisted = puppet_whitelisted sender.matrix_puppet_whitelisted = matrix_puppet_whitelisted sender.is_admin = is_admin mocker.patch.object(u.User, 'is_logged_in', return_value=is_logged_in) event = CommandEvent( processor=command_processor, room_id=RoomID("#mock_room:example.org"), event_id=EventID("$H45H:example.org"), sender=sender, command=command, args=[], content=Mock(), is_management=is_management, is_portal=boolean, ) assert not await command_handler.get_permission_error(event) assert command_handler.has_permission( HelpCacheKey(is_management=is_management, puppet_whitelisted=puppet_whitelisted, matrix_puppet_whitelisted=matrix_puppet_whitelisted, is_admin=is_admin, is_logged_in=is_logged_in, is_portal=boolean))
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() if await sender.has_full_access(allow_bot=True) and portal: events = new_events - old_events if len(events) > 0: # New event pinned, set that as pinned in Telegram. await portal.handle_matrix_pin(sender, EventID(events.pop())) elif len(new_events) == 0: # All pinned events removed, remove pinned event in Telegram. await portal.handle_matrix_pin(sender, None)
def test_reply(self, command_processor: CommandProcessor, mocker: MockFixture) -> None: mocker.patch("mautrix_telegram.user.config", self.config) evt = CommandEvent( processor=command_processor, room_id=RoomID("#mock_room:example.org"), event_id=EventID("$H45H:example.org"), sender=u.User(UserID("@sender:example.org")), command="help", args=[], content=Mock(), is_management=True, is_portal=False, ) mock_az = command_processor.az message = "**This** <i>was</i><br/><strong>all</strong>fun*!" # html, no markdown evt.reply(message, allow_html=True, render_markdown=False) mock_az.intent.send_notice.assert_called_with( RoomID("#mock_room:example.org"), "**This** <i>was</i><br/><strong>all</strong>fun*!", html="**This** <i>was</i><br/><strong>all</strong>fun*!\n", ) # html, markdown (default) evt.reply(message, allow_html=True, render_markdown=True) mock_az.intent.send_notice.assert_called_with( RoomID("#mock_room:example.org"), "**This** <i>was</i><br/><strong>all</strong>fun*!", html=("<p><strong>This</strong> <i>was</i><br/>" "<strong>all</strong>fun*!</p>\n"), ) # no html, no markdown evt.reply(message, allow_html=False, render_markdown=False) mock_az.intent.send_notice.assert_called_with( RoomID("#mock_room:example.org"), "**This** <i>was</i><br/><strong>all</strong>fun*!", html=None, ) # no html, markdown evt.reply(message, allow_html=False, render_markdown=True) mock_az.intent.send_notice.assert_called_with( RoomID("#mock_room:example.org"), "**This** <i>was</i><br/><strong>all</strong>fun*!", html="<p><strong>This</strong> <i>was</i><br/>" "<strong>all</strong>fun*!</p>\n")
async def test_permissions_denied( self, needs_auth: bool, needs_puppeting: bool, needs_matrix_puppeting: bool, needs_admin: bool, management_only: bool, command_processor: CommandProcessor, boolean: bool, mocker: MockFixture, ) -> None: mocker.patch("mautrix_telegram.user.config", self.config) command = "testcmd" mock_handler = Mock() command_handler = CommandHandler( handler=mock_handler, needs_auth=needs_auth, needs_puppeting=needs_puppeting, needs_matrix_puppeting=needs_matrix_puppeting, needs_admin=needs_admin, management_only=management_only, name=command, help_text="No real command", help_args="mock mockmock", help_section=HelpSection("Mock Section", 42, ""), ) sender = u.User(UserID("@sender:example.org")) sender.puppet_whitelisted = False sender.matrix_puppet_whitelisted = False sender.is_admin = False event = CommandEvent( processor=command_processor, room_id=RoomID("#mock_room:example.org"), event_id=EventID("$H45H:example.org"), sender=sender, command=command, args=[], content=Mock(), is_management=False, is_portal=boolean, ) assert await command_handler.get_permission_error(event) assert not command_handler.has_permission( HelpCacheKey(False, False, False, False, False, False))
async def handle_telegram_edit(self, source: 'AbstractUser', sender: p.Puppet, evt: Message ) -> None: if not self.mxid: self.log.trace("Ignoring edit to %d as chat has no Matrix room", evt.id) return elif hasattr(evt, "media") and isinstance(evt.media, MessageMediaGame): self.log.debug("Ignoring game message edit event") return async with self.send_lock(sender.tgid if sender else None, required=False): tg_space = self.tgid if self.peer_type == "channel" else source.tgid temporary_identifier = EventID( f"${random.randint(1000000000000, 9999999999999)}TGBRIDGEDITEMP") duplicate_found = self.dedup.check(evt, (temporary_identifier, tg_space), force_hash=True) if duplicate_found: mxid, other_tg_space = duplicate_found if tg_space != other_tg_space: prev_edit_msg = DBMessage.get_one_by_tgid(TelegramID(evt.id), tg_space, -1) if not prev_edit_msg: return DBMessage(mxid=mxid, mx_room=self.mxid, tg_space=tg_space, tgid=TelegramID(evt.id), edit_index=prev_edit_msg.edit_index + 1 ).insert() return content = await formatter.telegram_to_matrix(evt, source, self.main_intent, no_reply_fallback=True) editing_msg = DBMessage.get_one_by_tgid(TelegramID(evt.id), tg_space) if not editing_msg: self.log.info(f"Didn't find edited message {evt.id}@{tg_space} (src {source.tgid}) " "in database.") return content.msgtype = (MessageType.NOTICE if (sender and sender.is_bot and self.get_config("bot_messages_as_notices")) else MessageType.TEXT) content.external_url = self._get_external_url(evt) content.set_edit(editing_msg.mxid) intent = sender.intent_for(self) if sender else self.main_intent await intent.set_typing(self.mxid, is_typing=False) event_id = await self._send_message(intent, content) prev_edit_msg = DBMessage.get_one_by_tgid(TelegramID(evt.id), tg_space, -1) or editing_msg DBMessage(mxid=event_id, mx_room=self.mxid, tg_space=tg_space, tgid=TelegramID(evt.id), edit_index=prev_edit_msg.edit_index + 1).insert() DBMessage.update_by_mxid(temporary_identifier, self.mxid, mxid=event_id)
def update(self, event: TypeMessage, mxid: DedupMXID = None, expected_mxid: Optional[DedupMXID] = None, force_hash: bool = False) -> Optional[DedupMXID]: evt_hash = self._hash_event( event) if self._always_force_hash or force_hash else event.id try: found_mxid = self._dedup_mxid[evt_hash] except KeyError: return EventID("None"), TelegramID(0) if found_mxid != expected_mxid: return found_mxid self._dedup_mxid[evt_hash] = mxid return None
async def get_state_event( self, room_id: RoomID, event_type: EventType, state_key: Optional[str] = None) -> StateEventContent: event = await super().get_state_event(room_id, event_type, state_key) if self.state_store: fake_event = StateEvent(type=event_type, room_id=room_id, event_id=EventID(""), sender=UserID(""), state_key=state_key, timestamp=0, content=event) await self.state_store.update_state(fake_event) return event
async def handle_telegram_message(self, source: 'AbstractUser', sender: p.Puppet, evt: Message) -> None: if not self.mxid: await self.create_matrix_room(source, invites=[source.mxid], update_if_exists=False) if (self.peer_type == "user" and sender.tgid == self.tg_receiver and not sender.is_real_user and not self.az.state_store.is_joined(self.mxid, sender.mxid)): self.log.debug( f"Ignoring private chat message {evt.id}@{source.tgid} as receiver does" " not have matrix puppeting and their default puppet isn't in the room" ) return async with self.send_lock(sender.tgid if sender else None, required=False): tg_space = self.tgid if self.peer_type == "channel" else source.tgid temporary_identifier = EventID( f"${random.randint(1000000000000, 9999999999999)}TGBRIDGETEMP") duplicate_found = self.dedup.check( evt, (temporary_identifier, tg_space)) if duplicate_found: mxid, other_tg_space = duplicate_found self.log.debug( f"Ignoring message {evt.id}@{tg_space} (src {source.tgid}) " f"as it was already handled (in space {other_tg_space})") if tg_space != other_tg_space: DBMessage(tgid=TelegramID(evt.id), mx_room=self.mxid, mxid=mxid, tg_space=tg_space, edit_index=0).insert() return if self.backfilling or (self.dedup.pre_db_check and self.peer_type == "channel"): msg = DBMessage.get_one_by_tgid(TelegramID(evt.id), tg_space) if msg: self.log.debug( f"Ignoring message {evt.id} (src {source.tgid}) as it was already" f"handled into {msg.mxid}. This duplicate was catched in the db " "check. If you get this message often, consider increasing" "bridge.deduplication.cache_queue_length in the config.") return if sender and not sender.displayname: self.log.debug( f"Telegram user {sender.tgid} sent a message, but doesn't have a " "displayname, updating info...") entity = await source.client.get_entity(PeerUser(sender.tgid)) await sender.update_info(source, entity) allowed_media = (MessageMediaPhoto, MessageMediaDocument, MessageMediaGeo, MessageMediaGame, MessageMediaPoll, MessageMediaUnsupported) media = evt.media if hasattr(evt, "media") and isinstance( evt.media, allowed_media) else None if sender: intent = sender.intent_for(self) if self.backfilling and intent != sender.default_mxid_intent: intent = sender.default_mxid_intent self.backfill_leave.add(intent) else: intent = self.main_intent if not media and evt.message: is_bot = sender.is_bot if sender else False event_id = await self.handle_telegram_text(source, intent, is_bot, evt) elif media: event_id = await { MessageMediaPhoto: self.handle_telegram_photo, MessageMediaDocument: self.handle_telegram_document, MessageMediaGeo: self.handle_telegram_location, MessageMediaPoll: self.handle_telegram_poll, MessageMediaUnsupported: self.handle_telegram_unsupported, MessageMediaGame: self.handle_telegram_game, }[type(media) ](source, intent, evt, relates_to=formatter.telegram_reply_to_matrix(evt, source)) else: self.log.debug("Unhandled Telegram message: %s", evt) return if not event_id: return prev_id = self.dedup.update(evt, (event_id, tg_space), (temporary_identifier, tg_space)) if prev_id: self.log.debug( f"Sent message {evt.id}@{tg_space} to Matrix as {event_id}. " f"Temporary dedup identifier was {temporary_identifier}, " f"but dedup map contained {prev_id[1]} instead! -- " "This was probably a race condition caused by Telegram sending updates" "to other clients before responding to the sender. I'll just redact " "the likely duplicate message now.") await intent.redact(self.mxid, event_id) return self.log.debug("Handled Telegram message: %s", evt) try: DBMessage(tgid=TelegramID(evt.id), mx_room=self.mxid, mxid=event_id, tg_space=tg_space, edit_index=0).insert() DBMessage.update_by_mxid(temporary_identifier, self.mxid, mxid=event_id) except IntegrityError as e: self.log.exception( f"{e.__class__.__name__} while saving message mapping. " "This might mean that an update was handled after it left the " "dedup cache queue. You can try enabling bridge.deduplication." "pre_db_check in the config.") await intent.redact(self.mxid, event_id)