550 lines
18 KiB
Python
550 lines
18 KiB
Python
|
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()
|