import asyncio from typing import Iterable, Optional, Union from typing_extensions import override import aioxmpp import aioxmpp.im import aioxmpp.muc.xso from platypush.message.event.xmpp import ( XmppRoomAffiliationChangedEvent, XmppRoomInviteEvent, XmppRoomInviteAcceptedEvent, XmppRoomInviteRejectedEvent, XmppRoomEnterEvent, XmppRoomExitEvent, XmppRoomJoinEvent, XmppRoomLeaveEvent, XmppRoomMessageReceivedEvent, XmppRoomNickChangedEvent, XmppRoomPresenceChangedEvent, XmppRoomRoleChangedEvent, XmppRoomTopicChangedEvent, ) from .._types import Errors, XmppPresence from ._base import XmppBaseHandler class XmppRoomHandler(XmppBaseHandler): """ Handler for XMPP room events. """ def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.muc_client: aioxmpp.MUCClient = self._client.summon(aioxmpp.MUCClient) self.muc_client.on_muc_invitation.connect(self._on_muc_invitation) # type: ignore async def _restore_state(self): if self._loaded_state.rooms: await asyncio.gather( *[ self.join( room_id, nick=room.nick if room.nick else self._jid.localpart, password=room.password, ) for room_id, room in self._loaded_state.rooms.items() ] ) @override def restore_state(self): self._async_run(self._restore_state, wait_result=False) def _on_muc_invitation( self, _: aioxmpp.stanza.Message, room_id: aioxmpp.JID, inviter: aioxmpp.JID, mode: aioxmpp.im.InviteMode, password: Optional[str] = None, reason: Optional[str] = None, **__, ): def join(): assert self._loop, Errors.LOOP nick = self._jid.localpart self._async_run( self.join, room_id=jid, nick=nick, password=password, timeout=self.DEFAULT_TIMEOUT, ) self._state.pending_rooms.add(jid) self._state.room_invites.pop(jid, None) self._post_event(XmppRoomInviteAcceptedEvent, room_id=jid) def reject(): self._state.room_invites.pop(jid, None) self._post_event(XmppRoomInviteRejectedEvent, room_id=jid) jid = str(room_id) invite = self._state.room_invites[jid] self._post_user_event( XmppRoomInviteEvent, room_id=jid, user_id=inviter, mode=mode.name, password=password, reason=reason, ) invite.on_accepted = join invite.on_rejected = reject if self._config.auto_accept_invites: invite.accept() def _get_occupant_by_jid( self, user_id: str, room: aioxmpp.muc.Room ) -> aioxmpp.muc.service.Occupant: occupant = next( iter( m for m in room.members if str(m.conversation_jid) == user_id or str(m.direct_jid) == user_id ), None, ) assert occupant, Errors.NO_USER return occupant async def join( self, room_id: str, nick: Optional[str] = None, password: Optional[str] = None, auto_rejoin: bool = True, ): address = aioxmpp.JID.fromstr(room_id) room, future = self.muc_client.join( address, nick=nick, password=password, autorejoin=auto_rejoin, ) await future await self._register_room(room) return room async def leave(self, room_id: str): room = self._state.rooms.get(room_id) assert room, Errors.ROOM_NOT_JOINED await room.leave() self._unregister_room(room) async def invite( self, user_id: aioxmpp.JID, room_id: str, mode: aioxmpp.im.InviteMode = aioxmpp.im.InviteMode.DIRECT, text: Optional[str] = None, ): room = self._state.rooms.get(room_id) assert room, Errors.ROOM_NOT_JOINED await room.invite(user_id, text=text, mode=mode) async def kick( self, user_id: aioxmpp.JID, room_id: str, reason: Optional[str] = None, ): room = self._state.rooms.get(room_id) assert room, Errors.ROOM_NOT_JOINED occupant = self._get_occupant_by_jid(user_id=str(user_id), room=room) await room.kick(occupant, reason=reason) async def ban( self, user_id: aioxmpp.JID, room_id: str, reason: Optional[str] = None, ): room = self._state.rooms.get(room_id) assert room, Errors.ROOM_NOT_JOINED occupant = self._get_occupant_by_jid(user_id=str(user_id), room=room) await room.ban(occupant, reason=reason) async def set_affiliation( self, user_id: aioxmpp.JID, room_id: str, affiliation: str, reason: Optional[str] = None, ): room = self._state.rooms.get(room_id) assert room, Errors.ROOM_NOT_JOINED occupant = self._get_occupant_by_jid(user_id=str(user_id), room=room) await room.muc_set_affiliation( occupant.direct_jid or occupant.conversation_jid, affiliation, reason=reason ) async def set_role( self, user_id: aioxmpp.JID, room_id: str, role: str, reason: Optional[str] = None, ): room = self._state.rooms.get(room_id) assert room, Errors.ROOM_NOT_JOINED occupant = self._get_occupant_by_jid(user_id=str(user_id), room=room) await room.muc_set_role(occupant.nick, role, reason=reason) async def set_topic(self, room_id: str, topic: str): room = self._state.rooms.get(room_id) assert room, Errors.ROOM_NOT_JOINED await room.set_topic(topic) async def set_nick(self, room_id: str, nick: str): room = self._state.rooms.get(room_id) assert room, Errors.ROOM_NOT_JOINED await room.set_nick(nick) # pylint: disable=too-many-branches async def set_room_config( self, room_id: str, name: Optional[bool] = None, description: Optional[bool] = None, members_only: Optional[bool] = None, persistent: Optional[bool] = None, moderated: Optional[bool] = None, allow_invites: Optional[bool] = None, allow_private_messages: Optional[bool] = None, allow_change_subject: Optional[bool] = None, enable_logging: Optional[bool] = None, max_history_fetch: Optional[int] = None, max_users: Optional[int] = None, password_protected: Optional[bool] = None, public: Optional[bool] = None, room_admins: Optional[Iterable[Union[str, aioxmpp.JID]]] = None, room_owners: Optional[Iterable[Union[str, aioxmpp.JID]]] = None, password: Optional[str] = None, language: Optional[str] = None, ): room = self._state.rooms.get(room_id) assert room, Errors.ROOM_NOT_JOINED config = await self.muc_client.get_room_config(room.jid) form = aioxmpp.muc.xso.ConfigurationForm.from_xso(config) if members_only is not None: form.membersonly.value = members_only if persistent is not None: form.persistentroom.value = persistent if moderated is not None: form.moderatedroom.value = moderated if description is not None: form.roomdesc.value = description if name is not None: form.roomname.value = name if allow_invites is not None: form.allowinvites.value = allow_invites if allow_private_messages is not None: form.allowpm.value = allow_private_messages if allow_change_subject is not None: form.changesubject.value = allow_change_subject if enable_logging is not None: form.enablelogging.value = enable_logging if max_history_fetch is not None: form.maxhistoryfetch.value = max_history_fetch if max_users is not None: form.maxusers.value = max_users if password_protected is not None: form.passwordprotectedroom.value = max_users if public is not None: form.publicroom.value = public if password is not None: form.roomsecret.value = password if language is not None: form.lang.value = language if room_admins is not None: form.roomadmins.value = [ aioxmpp.JID.fromstr(user_id) if isinstance(user_id, str) else user_id for user_id in room_admins ] if room_owners is not None: form.roomowners.value = [ aioxmpp.JID.fromstr(user_id) if isinstance(user_id, str) else user_id for user_id in room_owners ] await self.muc_client.set_room_config(room.jid, form.render_reply()) async def _register_room(self, room: aioxmpp.muc.Room): room_id = str(room.jid) if not self._state.rooms.get(room_id): self._register_room_events(room) if room_id in self._state.pending_rooms: self._state.pending_rooms.remove(room_id) self._state.rooms[room_id] = room self._post_user_room_event( XmppRoomJoinEvent, room=room, user_id=self._jid, is_self=True, members=[ str(m.direct_jid if m.direct_jid else m.conversation_jid) for m in room.members ], ) await self._configure_room_on_join(room) async def _configure_room_on_join(self, room: aioxmpp.muc.Room): # Check if I'm the owner of the room and there's only me here. # If that's the case, odds are that the room has been newly created. # Newly created rooms have public_room set to False by default if len(room.members) != 1: return member = room.members[0] if not (member.is_self and member.affiliation == "owner"): return config = await self.muc_client.get_room_config(room.jid) form = aioxmpp.muc.xso.ConfigurationForm.from_xso(config) # If it's already a persistent room, then it's probably not a room that # has just been created if form.persistentroom.value: return form.publicroom.value = True form.allowinvites.value = True await self.muc_client.set_room_config(room.jid, form.render_reply()) def _unregister_room(self, room: aioxmpp.muc.Room): stored_room = self._state.rooms.pop(self._jid_to_str(room.jid), None) if stored_room: self._post_user_room_event( XmppRoomLeaveEvent, room=room, user_id=self._jid, is_self=True, ) def _register_room_events(self, room: aioxmpp.muc.Room): room.on_enter.connect(self._on_room_enter(room)) # type: ignore room.on_exit.connect(self._on_room_exit(room)) # type: ignore room.on_join.connect(self._on_room_join(room)) # type: ignore room.on_leave.connect(self._on_room_leave(room)) # type: ignore room.on_message.connect(self._on_msg_received(room)) # type: ignore room.on_nick_changed.connect(self._on_room_nick_changed(room)) # type: ignore room.on_presence_changed.connect(self._on_room_presence_changed(room)) # type: ignore room.on_topic_changed.connect(self._on_room_topic_changed(room)) # type: ignore room.on_muc_affiliation_changed.connect(self._on_room_muc_affiliation_changed(room)) # type: ignore room.on_muc_role_changed.connect(self._on_room_muc_role_changed(room)) # type: ignore def _on_msg_received(self, room: aioxmpp.muc.Room): def callback(msg, occupant: aioxmpp.muc.service.Occupant, *_, **__): if not msg.body: return if msg.error: self.logger.warning( 'Error on message from %s: %s', msg.from_, msg.error ) body = msg.body.lookup([aioxmpp.structs.LanguageRange.fromstr('*')]) self._post_room_occupant_event( XmppRoomMessageReceivedEvent, room=room, occupant=occupant, body=body.rstrip(), ) return callback def _on_room_join(self, room: aioxmpp.muc.Room): def callback(occupant: aioxmpp.muc.service.Occupant, *_, **__): self._post_room_occupant_event( XmppRoomJoinEvent, room=room, occupant=occupant ) return callback def _on_room_leave(self, room: aioxmpp.muc.Room): def callback(occupant: aioxmpp.muc.service.Occupant, *_, **__): if occupant.is_self: self._unregister_room(room) else: self._post_room_occupant_event( XmppRoomLeaveEvent, room=room, occupant=occupant ) return callback def _on_room_enter(self, room: aioxmpp.muc.Room): def callback(*args, **__): if args: occupant = args[0] self._post_room_occupant_event( XmppRoomEnterEvent, room=room, occupant=occupant ) else: self._async_run(self._register_room, room) self._post_user_room_event( XmppRoomEnterEvent, room=room, user_id=self._jid, is_self=True, ) return callback def _on_room_exit(self, room: aioxmpp.muc.Room): def callback( *args, reason: Optional[str] = None, **__, ): if args: occupant = args[0] self._post_room_occupant_event( XmppRoomExitEvent, room=room, occupant=occupant, reason=reason ) else: self._post_user_room_event( XmppRoomExitEvent, room=room, user_id=self._jid, is_self=True, reason=reason, ) return callback def _on_room_nick_changed(self, room: aioxmpp.muc.Room): def callback( member: aioxmpp.muc.service.Occupant, old_nick: Optional[str], new_nick: Optional[str], *_, **__, ): self._post_room_occupant_event( XmppRoomNickChangedEvent, room=room, occupant=member, old_nick=old_nick, new_nick=new_nick, ) return callback def _on_room_presence_changed(self, room: aioxmpp.muc.Room): def callback( occupant: aioxmpp.muc.service.Occupant, _, presence: aioxmpp.stanza.Presence, **__, ): self._post_room_occupant_event( XmppRoomPresenceChangedEvent, room=room, occupant=occupant, status=aioxmpp.PresenceShow(presence.show).value or XmppPresence.AVAILABLE.value, ) return callback def _on_room_muc_affiliation_changed(self, room: aioxmpp.muc.Room): def callback( presence: aioxmpp.stanza.Presence, *_, actor: Optional[aioxmpp.muc.xso.UserActor] = None, reason: Optional[str] = None, **__, ): occupant = self._get_occupant_by_jid(room=room, user_id=str(presence.from_)) self._post_room_occupant_event( XmppRoomAffiliationChangedEvent, room=room, occupant=occupant, affiliation=occupant.affiliation, changed_by=str(actor.jid) if actor else None, reason=reason, ) return callback def _on_room_muc_role_changed(self, room: aioxmpp.muc.Room): def callback( presence: aioxmpp.stanza.Presence, *_, actor: Optional[aioxmpp.muc.xso.UserActor] = None, reason: Optional[str] = None, **__, ): occupant = self._get_occupant_by_jid(room=room, user_id=str(presence.from_)) self._post_room_occupant_event( XmppRoomRoleChangedEvent, room=room, occupant=occupant, role=occupant.role, changed_by=str(actor.jid) if actor else None, reason=reason, ) return callback def _on_room_topic_changed(self, room: aioxmpp.muc.Room): def callback( member: aioxmpp.muc.service.ServiceMember, topic_map: aioxmpp.structs.LanguageMap, **_, ): topic = topic_map.lookup([aioxmpp.structs.LanguageRange.fromstr('*')]) self._post_room_event( XmppRoomTopicChangedEvent, room=room, topic=topic, changed_by=member.nick, ) return callback def send_message( self, room_id: str, body: str, language: Optional[str] = None, ): room = self._state.rooms.get(room_id) assert room, Errors.ROOM_NOT_JOINED target = room.jid msg_type = aioxmpp.MessageType.GROUPCHAT lang = language or self._lang msg = aioxmpp.Message(type_=msg_type, to=target) msg.body.update({lang: body}) self._client.enqueue(msg) def accept_invite(self, room_id: str): invite = self._state.room_invites.get(room_id) assert invite, Errors.NO_INVITE invite.accept() def reject_invite(self, room_id: str): invite = self._state.room_invites.get(room_id) assert invite, Errors.NO_INVITE invite.reject() async def request_voice(self, room_id: str): room = self._state.rooms.get(room_id) assert room, Errors.ROOM_NOT_JOINED await room.muc_request_voice()