diff --git a/libervia/backend/core/xmpp.py b/libervia/backend/core/xmpp.py
index d4629ff4..339be7b8 100644
--- a/libervia/backend/core/xmpp.py
+++ b/libervia/backend/core/xmpp.py
@@ -1645,6 +1645,9 @@ class SatMessageProtocol(xmppim.MessageProtocol):
else:
client.mess_id2uid[(data["from"], message_id)] = data["uid"]
+ if getattr(message_elt, "_is_from_archive", False):
+ extra["is_from_archive"] = True
+
# origin
if not from_jid_bare in (client.jid.userhostJID(), client.server_jid):
disco_infos = await G.host.memory.disco.get_infos(client, from_jid_bare)
diff --git a/libervia/backend/plugins/plugin_xep_0045.py b/libervia/backend/plugins/plugin_xep_0045.py
index 291f1ebc..465108e9 100644
--- a/libervia/backend/plugins/plugin_xep_0045.py
+++ b/libervia/backend/plugins/plugin_xep_0045.py
@@ -1,7 +1,7 @@
#!/usr/bin/env python3
-# SAT plugin for managing xep-0045
+# Libervia implementation of XEP-0045 (Multi-User Chat)
# Copyright (C) 2009-2026 Jérôme Poisson (goffi@goffi.org)
# This program is free software: you can redistribute it and/or modify
@@ -17,39 +17,49 @@
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see .
+from dataclasses import dataclass
import difflib
+from enum import StrEnum
import time
-from typing import Optional, cast
+from typing import TYPE_CHECKING, cast
+from collections.abc import Sequence
+
+from twisted.python.failure import Failure
import uuid
+from zope.interface import implementer
from pydantic import Field
import shortuuid
from twisted.internet import defer
from twisted.python import failure
from twisted.words.protocols.jabber import jid
-from twisted.words.protocols.jabber import error as xmpp_error
-from tx_xmpp import disco, itx_xmpp, muc
-from tx_xmpp import rsm
-from tx_xmpp import mam
-from zope.interface import implementer
+from twisted.words.protocols.jabber.error import StanzaError
+from tx_xmpp import disco, itx_xmpp, mam, muc, rsm
from libervia.backend import G
from libervia.backend.core import exceptions
-from libervia.backend.core.core_types import SatXMPPEntity
from libervia.backend.core.constants import Const as C
+from libervia.backend.core.core_types import SatXMPPEntity
from libervia.backend.core.i18n import D_, _
from libervia.backend.core.log import getLogger
from libervia.backend.memory import memory
from libervia.backend.plugins import plugin_misc_jid_search
-from libervia.backend.tools import xml_tools, utils
+from libervia.backend.plugins.plugin_xep_0313 import XEP_0313
+from libervia.backend.plugins.plugin_xep_0359 import XEP_0359
+from libervia.backend.tools import utils
+from libervia.backend.tools import xml_tools
from libervia.backend.tools.config import PluginSettings
+if TYPE_CHECKING:
+ from libervia.backend.core.main import LiberviaBackend
+
log = getLogger(__name__)
+IMPORT_NAME = "XEP-0045"
PLUGIN_INFO = {
C.PI_NAME: "XEP-0045 Plugin",
- C.PI_IMPORT_NAME: "XEP-0045",
+ C.PI_IMPORT_NAME: IMPORT_NAME,
C.PI_TYPE: "XEP",
C.PI_MODES: C.PLUG_MODE_BOTH,
C.PI_PROTOCOLS: ["XEP-0045"],
@@ -57,45 +67,69 @@ PLUGIN_INFO = {
C.PI_RECOMMENDATIONS: [C.TEXT_CMDS, "XEP-0313"],
C.PI_MAIN: "XEP_0045",
C.PI_HANDLER: "yes",
- C.PI_DESCRIPTION: _("""Implementation of Multi-User Chat"""),
+ C.PI_DESCRIPTION: _("Implementation of Multi-User Chat"),
}
NS_MUC = "http://jabber.org/protocol/muc"
-AFFILIATIONS = ("owner", "admin", "member", "none", "outcast")
ROOM_USER_JOINED = "ROOM_USER_JOINED"
ROOM_USER_LEFT = "ROOM_USER_LEFT"
OCCUPANT_KEYS = ("nick", "entity", "affiliation", "role")
-ROOM_STATE_OCCUPANTS = "occupants"
-ROOM_STATE_SELF_PRESENCE = "self-presence"
-ROOM_STATE_LIVE = "live"
-ROOM_STATES = (ROOM_STATE_OCCUPANTS, ROOM_STATE_SELF_PRESENCE, ROOM_STATE_LIVE)
-HISTORY_LEGACY = "legacy"
-HISTORY_MAM = "mam"
+DEFAULT_HISTORY_SYNC_LIMIT = 50
+
+
+@dataclass
+class MUCClientData:
+ _muc_client: "LiberviaMUCClient | None" = None
+ muc_service: jid.JID | None = None
+
+ @property
+ def muc_client(self) -> "LiberviaMUCClient":
+ """Get the MUC client, asserting it exists."""
+ assert self._muc_client is not None
+ return self._muc_client
+
+
+class Affiliation(StrEnum):
+ OWNER = "owner"
+ ADMIN = "admin"
+ MEMBER = "member"
+ NONE = "none"
+ OUTCAST = "outcast"
+
+
+@dataclass
+class OccupantInfo:
+ nick: str
+ entity: str
+ affiliation: str
+ role: str
class PluginsMucSettings(PluginSettings, plugin_info=PLUGIN_INFO):
- default_muc: str = Field(default="sat@chat.jabberfr.org")
+ default_muc: str = Field(default="libervia@chat.jabberfr.org")
class AlreadyJoined(exceptions.ConflictError):
+ """Raised when attempting to join an already joined room."""
- def __init__(self, room):
- super(AlreadyJoined, self).__init__()
+ def __init__(self, room: muc.Room) -> None:
+ super().__init__()
self.room = room
class XEP_0045:
# TODO: handle invitations
- # FIXME: this plugin need a good cleaning, join method is messy
- def __init__(self, host):
+ def __init__(self, host: "LiberviaBackend") -> None:
log.info(_("Plugin XEP_0045 initialization"))
self.host = host
+
+ self.settings = G.config.get_settings(PluginsMucSettings)
self._sessions = memory.Sessions()
self._jid_search = cast(
plugin_misc_jid_search.JidSearch, host.plugins["JID_SEARCH"]
)
- # return same arguments as muc_room_joined + a boolean set to True is the room was
+ # return same arguments as muc_room_joined + a boolean set to True if the room was
# already joined (first argument)
host.bridge.add_method(
"muc_join",
@@ -170,10 +204,6 @@ class XEP_0045:
method=self._get_muc_service,
async_=True,
)
- # called when a room will be joined but must be locked until join is received
- # (room is prepared, history is getting retrieved)
- # args: room_jid, profile
- host.bridge.add_signal("muc_room_prepare_join", ".plugin", signature="ss")
# args: room_jid, occupants, user_nick, subject, profile
host.bridge.add_signal("muc_room_joined", ".plugin", signature="sa{sa{ss}}ssass")
# args: room_jid, profile
@@ -201,8 +231,8 @@ class XEP_0045:
self.text_cmds.register_text_commands(self)
self.text_cmds.add_who_is_cb(self._whois, 100)
- self._mam = self.host.plugins.get("XEP-0313")
- self._si = self.host.plugins["XEP-0359"]
+ self._mam = cast(XEP_0313, self.host.plugins.get("XEP-0313"))
+ self._si = cast(XEP_0359, self.host.plugins["XEP-0359"])
host.trigger.add("presence_available", self.presence_trigger)
host.trigger.add("presence_received", self.presence_received_trigger)
@@ -214,49 +244,46 @@ class XEP_0045:
"JID_SEARCH_perform_search", self.jid_search_perform_search_trigger
)
- async def profile_connected(self, client):
- client.muc_service = await self.get_muc_service(client)
+ async def profile_connected(self, client: SatXMPPEntity) -> None:
+ """Called when a profile connects."""
+ muc_data = client.get_plugin_data(IMPORT_NAME, MUCClientData)
+ muc_data.muc_service = await self.get_muc_service(client)
- def _message_parse_trigger(self, client, message_elt, data):
- """Add stanza-id from the room if present"""
+ def _message_parse_trigger(
+ self, client: SatXMPPEntity, message_elt, data: dict
+ ) -> bool:
+ """Add stanza-id from the room if present."""
+ # stanza_id will not be filled by parse_message because the emitter
+ # is the room and not our server, so we have to parse it here
if message_elt.getAttribute("type") == C.MESS_TYPE_GROUPCHAT:
- # stanza_id will not be filled by parse_message because the emitter
- # is the room and not our server, so we have to parse it here
room_jid = data["from"].userhostJID()
stanza_id = self._si.get_stanza_id(message_elt, room_jid)
if stanza_id:
data["extra"]["stanza_id"] = stanza_id
-
return True
- def message_received_trigger(self, client, message_elt, post_treat):
- if message_elt.getAttribute("type") == C.MESS_TYPE_GROUPCHAT:
- if message_elt.subject:
- return False
- from_jid = jid.JID(message_elt["from"])
- room_jid = from_jid.userhostJID()
- if room_jid in client._muc_client.joined_rooms:
- room = client._muc_client.joined_rooms[room_jid]
- if room.state != ROOM_STATE_LIVE:
- if getattr(room, "_history_type", HISTORY_LEGACY) == HISTORY_LEGACY:
- # With MAM history, order is different, and we can get live
- # messages before history is complete, so this is not a warning
- # but an expected case.
- # On the other hand, with legacy history, it's not normal.
- log.warning(
- _(
- "Received non delayed message in a room before its "
- "initialisation: state={state}, msg={msg}"
- ).format(state=room.state, msg=message_elt.toXml())
- )
- room._cache.append(message_elt)
- return False
- else:
- log.warning(
- "Received groupchat message for a room which has not been "
- "joined, ignoring it: {}".format(message_elt.toXml())
- )
- return False
+ def message_received_trigger(
+ self, client: SatXMPPEntity, message_elt, post_treat: bool
+ ) -> bool:
+ """Handle incoming groupchat messages."""
+ if message_elt.getAttribute("type") != C.MESS_TYPE_GROUPCHAT:
+ return True
+
+ from_jid = jid.JID(message_elt["from"])
+ room_jid = from_jid.userhostJID()
+
+ if (
+ room_jid
+ not in client.get_plugin_data(
+ IMPORT_NAME, MUCClientData
+ ).muc_client.joined_rooms
+ ):
+ log.warning(
+ "Received groupchat message for unjoined room, ignoring: %s",
+ message_elt.toXml(),
+ )
+ return False
+
return True
async def jid_search_perform_search_trigger(
@@ -267,72 +294,83 @@ class XEP_0045:
sequence_matcher: difflib.SequenceMatcher,
matches: plugin_misc_jid_search.SearchItems,
) -> bool:
- if options.groupchat:
- if client.muc_service is not None:
- # FIXME: RSM is not handled, we need proper RSM handling as number of
- # rooms may be large.
- disco_items = await self.host.memory.disco.get_items(
- client, client.muc_service
- )
- for disco_item in disco_items:
- search_item = plugin_misc_jid_search.RoomSearchItem(
- entity=disco_item.entity, name=disco_item.name or "", local=True
- )
- self._jid_search.process_matching(
- search_term, sequence_matcher, matches, search_item
- )
+ """Search MUC rooms if groupchat option is enabled."""
+ # FIXME: RSM is not handled, we need proper RSM handling as number of
+ # rooms may be large.
+ muc_data = client.get_plugin_data(IMPORT_NAME, MUCClientData)
+ if not options.groupchat or muc_data.muc_service is None:
+ return True
+
+ disco_items = await self.host.memory.disco.get_items(client, muc_data.muc_service)
+ for disco_item in disco_items:
+ search_item = plugin_misc_jid_search.RoomSearchItem(
+ entity=disco_item.entity, name=disco_item.name or "", local=True
+ )
+ self._jid_search.process_matching(
+ search_term, sequence_matcher, matches, search_item
+ )
return True
def get_room(self, client: SatXMPPEntity, room_jid: jid.JID) -> muc.Room:
- """Retrieve Room instance from its jid
+ """Retrieve Room instance from its jid.
- @param room_jid: jid of the room
- @raise exceptions.NotFound: the room has not been joined
+ @param client: The client instance.
+ @param room_jid: JID of the room.
+ @return: The joined room.
"""
try:
- return client._muc_client.joined_rooms[room_jid]
+ return client.get_plugin_data(
+ IMPORT_NAME, MUCClientData
+ ).muc_client.joined_rooms[room_jid]
except KeyError:
raise exceptions.NotFound(_("This room has not been joined"))
- def check_room_joined(self, client, room_jid):
- """Check that given room has been joined in current session
+ def check_room_joined(self, client: SatXMPPEntity, room_jid: jid.JID) -> None:
+ """Check that given room has been joined in current session.
- @param room_jid (JID): room JID
+ @param client: The client instance.
+ @param room_jid: Room JID.
"""
- if room_jid not in client._muc_client.joined_rooms:
+ if (
+ room_jid
+ not in client.get_plugin_data(
+ IMPORT_NAME, MUCClientData
+ ).muc_client.joined_rooms
+ ):
raise exceptions.NotFound(_("This room has not been joined"))
def is_joined_room(self, client: SatXMPPEntity, room_jid: jid.JID) -> bool:
- """Tell if a jid is a known and joined room
+ """Check if a JID is a known and joined room.
- @room_jid: jid of the room
+ @param client: The client instance.
+ @param room_jid: JID of the room.
+ @return: True if the room is joined.
"""
try:
self.check_room_joined(client, room_jid)
except exceptions.NotFound:
return False
- else:
- return True
+ return True
- def is_room(self, client, entity_jid):
- """Tell if a jid is a joined MUC
+ def is_room(self, client: SatXMPPEntity, entity_jid: jid.JID) -> bool:
+ """Check if a JID is a joined MUC.
- similar to is_joined_room but returns a boolean
- @param entity_jid(jid.JID): full or bare jid of the entity check
- @return (bool): True if the bare jid of the entity is a room jid
+ @param client: The client instance.
+ @param entity_jid: JID of the entity to check.
+ @return: True if the JID is a joined MUC.
"""
try:
self.check_room_joined(client, entity_jid.userhostJID())
except exceptions.NotFound:
return False
- else:
- return True
+ return True
- def get_bare_or_full(self, client, peer_jid):
- """use full jid if peer_jid is an occupant of a room, bare jid else
+ def get_bare_or_full(self, client: SatXMPPEntity, peer_jid: jid.JID) -> jid.JID:
+ """Use full JID if peer is an occupant of a room, bare JID else.
- @param peer_jid(jid.JID): entity to test
- @return (jid.JID): bare or full jid
+ @param client: The client instance.
+ @param peer_jid: JID of the entity to test.
+ @return: Bare or full JID.
"""
if peer_jid.resource:
if not self.is_room(client, peer_jid):
@@ -340,57 +378,84 @@ class XEP_0045:
return peer_jid
def get_room_user_jid(self, client: SatXMPPEntity, room_jid: jid.JID) -> jid.JID:
- """Get the nick used in the room
+ """Get the nick used in the room.
- @param room_jid: jid of the room to use
- @return: JID used in room
-
- @raise exceptions.NotFound: this room has not been joined
- @raise exceptions.InternalError: invalid room_jid
+ @param client: The client instance.
+ @param room_jid: Room JID (bare, no resource).
+ @return: Occupant JID with nick as resource.
"""
# FIXME: doesn't check if room is anonymous or not
# TODO: check if room is (semi)anonymous
if room_jid.resource:
- raise exceptions.InternalError(f"{room_jid} should not have a ressource.")
+ raise exceptions.InternalError(f"{room_jid} should not have a resource")
room = self.get_room(client, room_jid)
return jid.JID(tuple=(room_jid.user, room_jid.host, room.nick))
- def _get_room_joined_args(self, room, profile):
+ def _get_room_joined_bridge_args(
+ self, client: SatXMPPEntity, room: muc.Room
+ ) -> Sequence[str | dict | list[str]]:
+ """Build arguments for muc_room_joined bridge signal.
+
+ @param client: The client instance.
+ @param room: The joined room.
+ @return: Sequence of arguments for the bridge signal.
+ """
+ occupants = self.get_room_occupants(client, room.roomJID)
return [
room.roomJID.userhost(),
- XEP_0045._get_occupants(room),
+ occupants,
room.nick,
- # FIXME: getattr below is a Q&D fix as `subject` may not be set in early call
- # of _get_rooms_joined (during `join` call). The whole workflow of this
- # plugin should be refactored.
+ # getattr because subject arrives after self-presence (XEP-0045 §7.1)
getattr(room, "subject", ""),
- [s.name for s in room.statuses],
- profile,
+ [s.name for s in getattr(room, "statuses", [])],
+ client.profile,
]
- def _ui_room_join_cb(self, data, profile):
+ def _ui_room_join_cb(self, data: dict, profile: str) -> dict:
+ """Handle UI room join callback.
+
+ @param data: UI data dict.
+ @param profile: Profile key.
+ @return: Result dict.
+ """
room_jid = jid.JID(data["index"])
client = self.host.get_client(profile)
defer.ensureDeferred(self.join(client, room_jid))
return {}
- def _password_ui_cb(self, data, client, room_jid, nick):
- """Called when the user has given room password (or cancelled)"""
+ def _password_ui_cb(
+ self,
+ data: dict,
+ client: SatXMPPEntity,
+ room_jid: jid.JID,
+ nick: str,
+ ) -> defer.Deferred:
+ """Handle password UI callback.
+
+ @param data: UI data dict.
+ @param client: The client instance.
+ @param room_jid: Room JID.
+ @param nick: Nickname.
+ @return: Deferred.
+ """
if C.bool(data.get(C.XMLUI_DATA_CANCELLED, "false")):
- log.info("room join for {} is cancelled".format(room_jid.userhost()))
- raise failure.Failure(
- exceptions.CancelError(D_("Room joining cancelled by user"))
+ log.info("Room join for %s cancelled", room_jid.userhost())
+ return defer.fail(
+ failure.Failure(exceptions.CancelError("Room joining cancelled"))
)
password = data[xml_tools.form_escape("password")]
- return client._muc_client.join(room_jid, nick, password).addCallbacks(
- self._join_cb,
- self._join_eb,
- (client, room_jid, nick),
- errbackArgs=(client, room_jid, nick, password),
+ return (
+ client.get_plugin_data(IMPORT_NAME, MUCClientData)
+ .muc_client.join(room_jid, nick, password)
+ .addCallbacks(
+ lambda room: room,
+ lambda f: self._join_eb(f, client, room_jid, nick, password),
+ )
)
- def _show_list_ui(self, items, client, service):
- xmlui = xml_tools.XMLUI(title=D_("Rooms in {}".format(service.full())))
+ def _show_list_ui(self, items: list, client: SatXMPPEntity, service: jid.JID) -> None:
+ """Show room list UI."""
+ xmlui = xml_tools.XMLUI(title=f"Rooms in {service.full()}")
adv_list = xmlui.change_container(
"advanced_list",
columns=1,
@@ -404,22 +469,24 @@ class XEP_0045:
adv_list.end()
self.host.action_new({"xmlui": xmlui.toXml()}, profile=client.profile)
- def _join_cb(self, room, client, room_jid, nick):
- """Called when the user is in the requested room"""
- if room.locked:
- # FIXME: the current behaviour is to create an instant room
- # and send the signal only when the room is unlocked
- # a proper configuration management should be done
- log.debug(_("room locked !"))
- d = client._muc_client.configure(room.roomJID, {})
- d.addErrback(
- self.host.log_errback,
- msg=_("Error while configuring the room: {failure_}"),
- )
- return room.fully_joined
+ def _join_eb(
+ self,
+ failure_: failure.Failure,
+ client: SatXMPPEntity,
+ room_jid: jid.JID,
+ nick: str,
+ password: str | None,
+ ) -> defer.Deferred:
+ """Handle join errors.
- def _join_eb(self, failure_, client, room_jid, nick, password):
- """Called when something is going wrong when joining the room"""
+ @param failure_: The failure object.
+ @param client: The client instance.
+ @param room_jid: Room JID.
+ @param nick: Nickname.
+ @param password: Password or None.
+ @return: Deferred.
+ """
+ # TODO: error management + signal in bridge
try:
condition = failure_.value.condition
except AttributeError:
@@ -428,110 +495,143 @@ class XEP_0045:
if condition == "conflict":
# we have a nickname conflict, we try again with "_" suffixed to current nickname
nick += "_"
- return client._muc_client.join(room_jid, nick, password).addCallbacks(
- self._join_cb,
- self._join_eb,
- (client, room_jid, nick),
- errbackArgs=(client, room_jid, nick, password),
+ return (
+ client.get_plugin_data(IMPORT_NAME, MUCClientData)
+ .muc_client.join(room_jid, nick, password)
+ .addCallbacks(
+ lambda room: room,
+ lambda f: self._join_eb(f, client, room_jid, nick, password),
+ )
)
elif condition == "not-allowed":
# room is restricted, we need a password
password_ui = xml_tools.XMLUI(
"form",
- title=D_("Room {} is restricted").format(room_jid.userhost()),
+ title=f"Room {room_jid.userhost()} is restricted",
submit_id="",
)
- password_ui.addText(
- D_("This room is restricted, please enter the password")
- )
+ password_ui.addText("This room is restricted, please enter the password")
password_ui.addPassword("password")
d = xml_tools.defer_xmlui(self.host, password_ui, profile=client.profile)
- d.addCallback(self._password_ui_cb, client, room_jid, nick)
+ d.addCallback(
+ lambda data: self._password_ui_cb(data, client, room_jid, nick)
+ )
return d
- msg_suffix = ' with condition "{}"'.format(failure_.value.condition)
+ msg_suffix = f' with condition "{condition}"'
- mess = D_(
- "Error while joining the room {room}{suffix}".format(
- room=room_jid.userhost(), suffix=msg_suffix
- )
- )
+ mess = f"Error while joining room {room_jid.userhost()}{msg_suffix}"
log.warning(mess)
- xmlui = xml_tools.note(mess, D_("Group chat error"), level=C.XMLUI_DATA_LVL_ERROR)
+ xmlui = xml_tools.note(mess, "Group chat error", level=C.XMLUI_DATA_LVL_ERROR)
self.host.action_new({"xmlui": xmlui.toXml()}, profile=client.profile)
+ return defer.fail(failure_)
- @staticmethod
- def _get_occupants(room):
- """Get occupants of a room in a form suitable for bridge"""
- return {
- u.nick: {k: str(getattr(u, k) or "") for k in OCCUPANT_KEYS}
- for u in list(room.roster.values())
- }
-
- def _get_room_occupants(self, room_jid_s, profile_key):
+ def _get_room_occupants(
+ self, room_jid_s: str, profile_key: str
+ ) -> dict[str, dict[str, str]]:
+ """Get room occupants."""
client = self.host.get_client(profile_key)
room_jid = jid.JID(room_jid_s)
return self.get_room_occupants(client, room_jid)
- def get_room_occupants(self, client, room_jid):
+ def get_room_occupants(
+ self, client: SatXMPPEntity, room_jid: jid.JID
+ ) -> dict[str, dict[str, str]]:
+ """Get room occupants as dict suitable for bridge."""
room = self.get_room(client, room_jid)
- return self._get_occupants(room)
+ return {
+ u.nick: {k: str(getattr(u, k, "")) for k in OCCUPANT_KEYS}
+ for u in room.roster.values()
+ }
- def _get_rooms_joined(self, profile_key=C.PROF_KEY_NONE):
+ def _get_rooms_joined(self, profile_key: str = C.PROF_KEY_NONE) -> list[tuple]:
+ """Get joined rooms.
+
+ @param profile_key: Profile key.
+ @return: List of joined room tuples.
+ """
client = self.host.get_client(profile_key)
return self.get_rooms_joined(client)
- def get_rooms_joined(self, client):
- """Return rooms where user is"""
+ def get_rooms_joined(self, client: SatXMPPEntity) -> list[tuple]:
+ """Return rooms where user is.
+
+ @param client: The client instance.
+ @return: List of joined room tuples.
+ """
result = []
- for room in list(client._muc_client.joined_rooms.values()):
- if room.state == ROOM_STATE_LIVE:
- result.append(
- (
- room.roomJID.userhost(),
- self._get_occupants(room),
- room.nick,
- room.subject,
- [s.name for s in room.statuses],
- )
+ for room in client.get_plugin_data(
+ IMPORT_NAME, MUCClientData
+ ).muc_client.joined_rooms.values():
+ occupants = {
+ u.nick: {k: str(getattr(u, k, "")) for k in OCCUPANT_KEYS}
+ for u in room.roster.values()
+ }
+ result.append(
+ (
+ room.roomJID.userhost(),
+ occupants,
+ room.nick,
+ # getattr because subject arrives after self-presence (XEP-0045 §7.1)
+ getattr(room, "subject", ""),
+ [s.name for s in getattr(room, "statuses", [])],
)
+ )
return result
- def _get_room_nick(self, room_jid_s, profile_key=C.PROF_KEY_NONE):
+ def _get_room_nick(self, room_jid_s: str, profile_key: str = C.PROF_KEY_NONE) -> str:
+ """Get room nick.
+
+ @param room_jid_s: Room JID string.
+ @param profile_key: Profile key.
+ @return: Room nick string.
+ """
client = self.host.get_client(profile_key)
return self.get_room_nick(client, jid.JID(room_jid_s))
- def get_room_nick(self, client, room_jid):
- """return nick used in room by user
+ def get_room_nick(self, client: SatXMPPEntity, room_jid: jid.JID) -> str:
+ """Return nick used in room by user.
- @param room_jid (jid.JID): JID of the room
- @profile_key: profile
- @return: nick or empty string in case of error
- @raise exceptions.Notfound: use has not joined the room
+ @param client: The client instance.
+ @param room_jid: Room JID.
+ @return: Nick or empty string in case of error.
"""
self.check_room_joined(client, room_jid)
- return client._muc_client.joined_rooms[room_jid].nick
+ return (
+ client.get_plugin_data(IMPORT_NAME, MUCClientData)
+ .muc_client.joined_rooms[room_jid]
+ .nick
+ )
- def _configure_room(self, room_jid_s, profile_key=C.PROF_KEY_NONE):
+ def _configure_room(
+ self, room_jid_s: str, profile_key: str = C.PROF_KEY_NONE
+ ) -> defer.Deferred:
+ """Configure room.
+
+ @param room_jid_s: Room JID string.
+ @param profile_key: Profile key.
+ @return: Deferred with XMLUI form string.
+ """
client = self.host.get_client(profile_key)
d = self.configure_room(client, jid.JID(room_jid_s))
- d.addCallback(lambda xmlui: xmlui.toXml())
+ d.addCallback(lambda xmlui: xmlui.toXml() if xmlui else "")
return d
- def _configure_room_menu(self, menu_data, profile):
- """Return room configuration form
+ def _configure_room_menu(self, menu_data: dict, profile: str) -> defer.Deferred:
+ """Return room configuration form.
- @param menu_data: %(menu_data)s
- @param profile: %(doc_profile)s
+ @param menu_data: Menu data dict.
+ @param profile: Profile key.
+ @return: Deferred with XMLUI form string.
"""
client = self.host.get_client(profile)
try:
room_jid = jid.JID(menu_data["room_jid"])
except KeyError:
- log.error(_("room_jid key is not present !"))
- return defer.fail(exceptions.DataError)
+ log.error("room_jid key is not present!")
+ return defer.fail(exceptions.DataError())
- def xmlui_received(xmlui):
+ def xmlui_received(xmlui: xml_tools.XMLUI | None) -> dict:
if not xmlui:
msg = D_("No configuration available for this room")
return {"xmlui": xml_tools.note(msg).toXml()}
@@ -539,77 +639,110 @@ class XEP_0045:
return self.configure_room(client, room_jid).addCallback(xmlui_received)
- def configure_room(self, client, room_jid):
- """return the room configuration form
+ def configure_room(self, client: SatXMPPEntity, room_jid: jid.JID) -> defer.Deferred:
+ """Return the room configuration form.
- @param room: jid of the room to configure
- @return: configuration form as XMLUI
+ @param client: The client instance.
+ @param room_jid: Room JID to configure.
+ @return: Deferred firing with configuration form as XMLUI.
"""
self.check_room_joined(client, room_jid)
- def config_2_xmlui(result):
+ def config_2_xmlui(result) -> xml_tools.XMLUI | None:
if not result:
- return ""
+ return None
session_id, session_data = self._sessions.new_session(profile=client.profile)
session_data["room_jid"] = room_jid
xmlui = xml_tools.data_form_2_xmlui(result, submit_id=self.__submit_conf_id)
xmlui.session_id = session_id
return xmlui
- d = client._muc_client.getConfiguration(room_jid)
+ d = client.get_plugin_data(
+ IMPORT_NAME, MUCClientData
+ ).muc_client.getConfiguration(room_jid)
d.addCallback(config_2_xmlui)
return d
- def _submit_configuration(self, raw_data, profile):
+ def _submit_configuration(self, raw_data: dict, profile: str) -> defer.Deferred:
+ """Submit room configuration."""
cancelled = C.bool(raw_data.get("cancelled", C.BOOL_FALSE))
if cancelled:
return defer.succeed({})
+
client = self.host.get_client(profile)
try:
session_data = self._sessions.profile_get(raw_data["session_id"], profile)
except KeyError:
- log.warning(D_("Session ID doesn't exist, session has probably expired."))
- _dialog = xml_tools.XMLUI("popup", title=D_("Room configuration failed"))
- _dialog.addText(D_("Session ID doesn't exist, session has probably expired."))
- return defer.succeed({"xmlui": _dialog.toXml()})
+ log.warning("Session ID doesn't exist, session has probably expired.")
+ dialog = xml_tools.XMLUI("popup", title=D_("Room configuration failed"))
+ dialog.addText(D_("Session ID doesn't exist, session has probably expired."))
+ return defer.succeed({"xmlui": dialog.toXml()})
data = xml_tools.xmlui_result_2_data_form_result(raw_data)
- d = client._muc_client.configure(session_data["room_jid"], data)
- _dialog = xml_tools.XMLUI("popup", title=D_("Room configuration succeed"))
- _dialog.addText(D_("The new settings have been saved."))
- d.addCallback(lambda ignore: {"xmlui": _dialog.toXml()})
+ d = client.get_plugin_data(IMPORT_NAME, MUCClientData).muc_client.configure(
+ session_data["room_jid"], data
+ )
+
+ def on_success(_) -> dict:
+ dialog = xml_tools.XMLUI("popup", title=D_("Room configuration succeed"))
+ dialog.addText(D_("The new settings have been saved."))
+ return {"xmlui": dialog.toXml()}
+
+ d.addCallback(on_success)
del self._sessions[raw_data["session_id"]]
return d
- def is_nick_in_room(self, client, room_jid, nick):
- """Tell if a nick is currently present in a room"""
+ def is_nick_in_room(
+ self, client: SatXMPPEntity, room_jid: jid.JID, nick: str
+ ) -> bool:
+ """Tell if a nick is currently present in a room."""
self.check_room_joined(client, room_jid)
- return client._muc_client.joined_rooms[room_jid].inRoster(muc.User(nick))
+ return (
+ client.get_plugin_data(IMPORT_NAME, MUCClientData)
+ .muc_client.joined_rooms[room_jid]
+ .inRoster(muc.User(nick))
+ )
- def _get_muc_service(self, jid_=None, profile=C.PROF_KEY_NONE):
+ # Text commands
+
+ def _get_muc_service(
+ self, jid_: str | None = None, profile: str = C.PROF_KEY_NONE
+ ) -> defer.Deferred:
+ """Get MUC service JID.
+
+ @param jid_: JID string or None.
+ @param profile: Profile key.
+ @return: Deferred with MUC service JID string.
+ """
client = self.host.get_client(profile)
- d = defer.ensureDeferred(self.get_muc_service(client, jid_ or None))
+ d = defer.ensureDeferred(
+ self.get_muc_service(client, jid_.userhostJID() if jid_ else None)
+ )
d.addCallback(
lambda service_jid: service_jid.full() if service_jid is not None else ""
)
return d
async def get_muc_service(
- self, client: SatXMPPEntity, jid_: Optional[jid.JID] = None
- ) -> Optional[jid.JID]:
- """Return first found MUC service of an entity
+ self, client: SatXMPPEntity, jid_: jid.JID | None = None
+ ) -> jid.JID | None:
+ """Return first found MUC service of an entity.
- @param jid_: entity which may have a MUC service, or None for our own server
- @return: found service jid or None
+ @param client: The client instance.
+ @param jid_: Entity which may have a MUC service, or None for our own server.
+ @return: Found service JID or None.
"""
if jid_ is None:
try:
- muc_service = client.muc_service
+ muc_service = client.get_plugin_data(
+ IMPORT_NAME, MUCClientData
+ ).muc_service
except AttributeError:
pass
else:
- # we have a cached value, we return it
- return muc_service
+ if muc_service is not None:
+ return muc_service
+
services = await self.host.find_service_entities(
client, "conference", "text", jid_
)
@@ -618,213 +751,429 @@ class XEP_0045:
# FIXME:
# This ugly hack is here to avoid an issue with openfire: the IRC gateway
# use "conference/text" identity (instead of "conference/irc")
- muc_service = service
- break
- else:
- muc_service = None
- return muc_service
+ return service
+ return None
- def _get_unique_name(self, muc_service="", profile_key=C.PROF_KEY_NONE):
+ def _get_unique_name(
+ self, muc_service: str = "", profile_key: str = C.PROF_KEY_NONE
+ ) -> str:
+ """Get unique room name.
+
+ @param muc_service: MUC service JID string.
+ @param profile_key: Profile key.
+ @return: Unique room name JID string.
+ """
client = self.host.get_client(profile_key)
return self.get_unique_name(client, muc_service or None).full()
def get_unique_name(
- self, client: SatXMPPEntity, muc_service: jid.JID | None = None, prefix: str = ""
+ self,
+ client: SatXMPPEntity,
+ muc_service: jid.JID | None = None,
+ prefix: str = "",
) -> jid.JID:
- """Return unique name for a room, avoiding collision
+ """Return unique name for a room, avoiding collision.
- @param client: Client instance.
- @param muc_service: leave empty string to use the default service
- @param prefix: prefix to use in room name.
- @return: unique room bare JID
+ @param client: The client instance.
+ @param muc_service: Leave empty to use the default service.
+ @param prefix: Optional prefix for room name.
+ @return: Unique room bare JID.
"""
room_name = f"{prefix}{shortuuid.uuid()}"
if muc_service is None:
try:
- muc_service = client.muc_service
+ muc_service = client.get_plugin_data(
+ IMPORT_NAME, MUCClientData
+ ).muc_service
except AttributeError:
raise exceptions.NotReady(
"Main server MUC service has not been checked yet"
)
if muc_service is None:
- log.warning(_("No MUC service found on main server"))
+ log.warning("No MUC service found on main server")
raise exceptions.FeatureNotFound
- muc_service = muc_service.userhost()
- return jid.JID(f"{room_name}@{muc_service}")
+ return jid.JID(f"{room_name}@{muc_service.userhost()}")
- def get_default_muc(self):
- """Return the default MUC."""
- plugin_settings = G.config.get_settings(PluginsMucSettings)
- return plugin_settings.default_muc
+ def get_default_muc(self) -> str:
+ """Return the default MUC.
- def _bridge_join_eb(self, failure_, client):
- failure_.trap(AlreadyJoined)
- room = failure_.value.room
- return [True] + self._get_room_joined_args(room, client.profile)
+ @return: Default MUC JID string.
+ """
+ return self.settings.default_muc
- def _join(self, room_jid_s, nick, options, profile_key=C.PROF_KEY_NONE):
- """join method used by bridge
+ @utils.ensure_deferred
+ async def _join(
+ self,
+ room_jid_s: str,
+ nick: str,
+ options: dict,
+ profile_key: str = C.PROF_KEY_NONE,
+ ) -> list[str | dict | list[str] | bool]:
+ """Join method used by bridge.
- @return (tuple): already_joined boolean + room joined arguments (see [_get_room_joined_args])
+ @param room_jid_s: Room JID string (bare or full).
+ @param nick: Nickname to use.
+ @param options: Join options dict.
+ @param profile_key: Profile identifier.
+ @return: Tuple of (already_joined, room_joined_args).
"""
client = self.host.get_client(profile_key)
+
if room_jid_s:
- muc_service = client.muc_service
+ muc_service = client.get_plugin_data(IMPORT_NAME, MUCClientData).muc_service
try:
room_jid = jid.JID(room_jid_s)
except (RuntimeError, jid.InvalidFormat, AttributeError):
- return defer.fail(
- jid.InvalidFormat(
- _(
- "Invalid room identifier: {room_id}'. Please give a room short or full identifier like 'room' or 'room@{muc_service}'."
- ).format(room_id=room_jid_s, muc_service=str(muc_service))
- )
+ raise jid.InvalidFormat(
+ _(
+ "Invalid room identifier: {room_id}. "
+ "Use 'room' or 'room@{muc_service}'"
+ ).format(room_id=room_jid_s, muc_service=str(muc_service))
)
if not room_jid.user:
- room_jid.user, room_jid.host = room_jid.host, muc_service
+ assert muc_service is not None
+ room_jid.user, room_jid.host = room_jid.host, str(muc_service)
else:
room_jid = self.get_unique_name(client)
- # TODO: error management + signal in bridge
- d = defer.ensureDeferred(self.join(client, room_jid, nick, options or None))
- d.addCallback(
- lambda room: [False] + self._get_room_joined_args(room, client.profile)
- )
- d.addErrback(self._bridge_join_eb, client)
- return d
+ # TODO: error management + signal in bridge
+ try:
+ room = await self.join(client, room_jid, nick, options or None)
+ except AlreadyJoined as e:
+ return [True] + list(self._get_room_joined_bridge_args(client, e.room))
+ else:
+ return [False] + list(self._get_room_joined_bridge_args(client, room))
async def join(
self,
client: SatXMPPEntity,
room_jid: jid.JID,
- nick: Optional[str] = None,
- options: Optional[dict] = None,
- ) -> Optional[muc.Room]:
- if not nick:
+ nick: str | None = None,
+ options: dict | None = None,
+ ) -> muc.Room:
+ """Join a MUC room.
+
+ History sync will run in background to join as quick as possible.
+
+ @param client: The client instance.
+ @param room_jid: Room JID to join.
+ @param nick: Nickname to use (defaults to username).
+ @param options: Optional dict with 'password' key.
+ @return: The joined room.
+ @raise AlreadyJoined: The room is already joined by this profile.
+ """
+ if nick is None:
nick = client.jid.user
if options is None:
options = {}
- if room_jid in client._muc_client.joined_rooms:
- room = client._muc_client.joined_rooms[room_jid]
+
+ muc_data = client.get_plugin_data(IMPORT_NAME, MUCClientData)
+ muc_client = muc_data.muc_client
+
+ if room_jid in muc_client.joined_rooms:
+ room = muc_client.joined_rooms[room_jid]
log.info(
- _("{profile} is already in room {room_jid}").format(
- profile=client.profile, room_jid=room_jid.userhost()
- )
+ "{profile} is already in room {room_jid}",
+ profile=client.profile,
+ room_jid=room_jid.userhost(),
)
raise AlreadyJoined(room)
+
log.info(
- _("[{profile}] is joining room {room} with nick {nick}").format(
- profile=client.profile, room=room_jid.userhost(), nick=nick
- )
+ "[{profile}] joining room {room} as {nick}",
+ profile=client.profile,
+ room=room_jid.userhost(),
+ nick=nick,
)
- self.host.bridge.muc_room_prepare_join(room_jid.userhost(), client.profile)
password = options.get("password")
try:
- room = await client._muc_client.join(room_jid, nick, password)
+ room = await muc_client.join(
+ room_jid, nick, muc.HistoryOptions(maxStanzas=0), password
+ )
except Exception as e:
- room = await utils.as_deferred(
- self._join_eb(failure.Failure(e), client, room_jid, nick, password)
+ room = await self._join_eb(
+ failure.Failure(e), client, room_jid, nick, password
)
else:
- room.on_joined_callbacks = []
- room.on_left_callbacks = []
- await defer.ensureDeferred(self._join_cb(room, client, room_jid, nick))
+ if room.locked:
+ log.debug("Room locked, configuring it")
+ try:
+ await muc_client.configure(room.roomJID, {})
+ except Exception as e:
+ log.exception(f"Can't configure MUC room {room_jid}")
+
+ if self._mam is not None:
+ # We use `ensureDeferred` to run the archive sync in background
+ defer.ensureDeferred(self._sync_mam_history(client, room))
+
return room
- def pop_rooms(self, client):
- """Remove rooms and return data needed to re-join them
+ async def _sync_mam_history(self, client: SatXMPPEntity, room: muc.Room) -> None:
+ """Retrieve MAM history and send messages to bridge progressively.
- This methods is to be called before a hot reconnection
- @return (list[(jid.JID, unicode)]): arguments needed to re-join the rooms
- This list can be used directly (unpacked) with self.join
+ @param client: The client instance.
+ @param room: The joined room.
+ """
+ if self._mam is None:
+ return
+
+ room_jid = room.roomJID
+ total_messages = 0
+
+ try:
+ last_mess = await self.host.memory.history_get(
+ room_jid,
+ None,
+ limit=1,
+ between=False,
+ filters={"types": C.MESS_TYPE_GROUPCHAT, "last_stanza_id": True},
+ profile=client.profile,
+ )
+
+ if last_mess:
+ stanza_id = last_mess[0][-1].get("stanza_id")
+ rsm_req = rsm.RSMRequest(max_=100, after=stanza_id)
+ else:
+ log.info(
+ "We have no MAM archive for room {room_jid}.".format(
+ room_jid=room_jid
+ )
+ )
+ stanza_id = None
+ rsm_req = rsm.RSMRequest(max_=DEFAULT_HISTORY_SYNC_LIMIT, before="")
+
+ mam_req = mam.MAMRequest(rsm_=rsm_req)
+
+ while True:
+ try:
+ mam_data = await self._mam.get_archives(
+ client, mam_req, service=room_jid
+ )
+ except StanzaError as e:
+ if last_mess and e.condition == "item-not-found":
+ log.warning(
+ f"Requested item (with stanza_id {stanza_id!r}) can't be "
+ f"found in history of {room_jid}, history has probably been "
+ "purged on server. Falling back to last N items."
+ )
+ # Fall back to retrieving last N items like for a new room
+ rsm_req = rsm.RSMRequest(
+ max_=DEFAULT_HISTORY_SYNC_LIMIT, before=""
+ )
+ mam_req = mam.MAMRequest(rsm_=rsm_req)
+ # Clear last_mess to prevent infinite loop if server keeps failing
+ last_mess = None
+ continue
+ else:
+ raise
+
+ elt_list, rsm_response, mam_response = mam_data
+
+ if not elt_list:
+ break
+
+ for mess_elt in elt_list:
+ try:
+ fwd_message_elt = self._mam.get_message_from_result(
+ client, mess_elt, mam_req, service=room_jid
+ )
+ except exceptions.DataError:
+ log.exception("Invalid element received")
+ continue
+
+ if to_ := fwd_message_elt.getAttribute("to"):
+ log.warning(
+ f'Forwarded message element has a "to" attribute ({to_!r})'
+ " while it is forbidden by specifications"
+ )
+ fwd_message_elt["to"] = client.jid.full()
+
+ client.messageProt.onMessage(fwd_message_elt)
+ total_messages += 1
+
+ if mam_response.get("complete", False):
+ break
+
+ if rsm_response and rsm_response.last:
+ # Create new MAMRequest with updated rsm (rsm is a property)
+ rsm_req = rsm.RSMRequest(max_=100, after=rsm_response.last)
+ mam_req = mam.MAMRequest(rsm_=rsm_req)
+ else:
+ break
+
+ if total_messages == 0:
+ log.info(
+ _("No message received while offline in {room_jid}").format(
+ room_jid=room_jid
+ )
+ )
+ else:
+ log.info(
+ f"Retrieved {total_messages} message(s) from {room_jid} while"
+ " offline.",
+ )
+
+ except Exception as e:
+ log.exception(f"MAM history sync failed for {room_jid}")
+
+ def pop_rooms(self, client: SatXMPPEntity) -> list[tuple]:
+ """Remove rooms and return data needed to re-join them.
+
+ This method is to be called before a hot reconnection.
+
+ @param client: The client instance.
+ @return: List of tuples (client, room_jid, nick) for re-joining.
"""
args_list = []
- for room in list(client._muc_client.joined_rooms.values()):
- client._muc_client._removeRoom(room.roomJID)
+ muc_client = client.get_plugin_data(IMPORT_NAME, MUCClientData).muc_client
+ for room in list(muc_client.joined_rooms.values()):
+ muc_client._removeRoom(room.roomJID)
args_list.append((client, room.roomJID, room.nick))
return args_list
- def _nick(self, room_jid_s, nick, profile_key=C.PROF_KEY_NONE):
+ def _nick(
+ self, room_jid_s: str, nick: str, profile_key: str = C.PROF_KEY_NONE
+ ) -> None:
+ """Change nickname."""
client = self.host.get_client(profile_key)
- return self.nick(client, jid.JID(room_jid_s), nick)
+ self.nick(client, jid.JID(room_jid_s), nick)
- def nick(self, client, room_jid, nick):
- """Change nickname in a room"""
+ def nick(self, client: SatXMPPEntity, room_jid: jid.JID, nick: str) -> None:
+ """Change nickname in a room.
+
+ @param client: The client instance.
+ @param room_jid: Room JID.
+ @param nick: New nickname.
+ """
self.check_room_joined(client, room_jid)
- return client._muc_client.nick(room_jid, nick)
+ client.get_plugin_data(IMPORT_NAME, MUCClientData).muc_client.nick(room_jid, nick)
- def _leave(self, room_jid, profile_key):
+ def _leave(self, room_jid: str, profile_key: str) -> defer.Deferred:
+ """Leave room.
+
+ @param room_jid: Room JID string.
+ @param profile_key: Profile key.
+ @return: Deferred.
+ """
client = self.host.get_client(profile_key)
return self.leave(client, jid.JID(room_jid))
- def leave(self, client, room_jid):
- self.check_room_joined(client, room_jid)
- return client._muc_client.leave(room_jid)
+ def leave(self, client: SatXMPPEntity, room_jid: jid.JID) -> defer.Deferred:
+ """Leave a room.
- def _subject(self, room_jid_s, new_subject, profile_key):
+ @param client: The client instance.
+ @param room_jid: Room JID.
+ @return: Deferred firing when leave is complete.
+ """
+ self.check_room_joined(client, room_jid)
+ return client.get_plugin_data(IMPORT_NAME, MUCClientData).muc_client.leave(
+ room_jid
+ )
+
+ def _subject(self, room_jid_s: str, new_subject: str, profile_key: str) -> None:
+ """Set room subject."""
client = self.host.get_client(profile_key)
- return self.subject(client, jid.JID(room_jid_s), new_subject)
+ self.subject(client, jid.JID(room_jid_s), new_subject)
- def subject(self, client, room_jid, subject):
+ def subject(self, client: SatXMPPEntity, room_jid: jid.JID, subject: str) -> None:
+ """Set room subject.
+
+ @param client: The client instance.
+ @param room_jid: Room JID.
+ @param subject: New subject.
+ """
self.check_room_joined(client, room_jid)
- return client._muc_client.subject(room_jid, subject)
+ client.get_plugin_data(IMPORT_NAME, MUCClientData).muc_client.subject(
+ room_jid, subject
+ )
- def get_handler(self, client):
+ def get_handler(self, client: SatXMPPEntity) -> "LiberviaMUCClient":
+ """Create a MUC client and associate it with profile's session.
+
+ @param client: The client instance.
+ @return: The MUC client.
+ """
# create a MUC client and associate it with profile' session
- muc_client = client._muc_client = LiberviaMUCClient(self)
+ muc_data = client.get_plugin_data(IMPORT_NAME, MUCClientData)
+ muc_client = muc_data._muc_client = LiberviaMUCClient(self)
return muc_client
- def kick(self, client, nick, room_jid, options=None):
- """Kick a participant from the room
+ def kick(
+ self,
+ client: SatXMPPEntity,
+ nick: str,
+ room_jid: jid.JID,
+ options: dict | None = None,
+ ) -> defer.Deferred:
+ """Kick a participant from the room.
- @param nick (str): nick of the user to kick
- @param room_jid_s (JID): jid of the room
- @param options (dict): attribute with extra info (reason, password) as in #XEP-0045
+ @param client: The client instance.
+ @param nick: Nick of the user to kick.
+ @param room_jid: Room JID.
+ @param options: Dict with 'reason' key.
+ @return: Deferred firing when kick is complete.
"""
if options is None:
options = {}
self.check_room_joined(client, room_jid)
- return client._muc_client.kick(room_jid, nick, reason=options.get("reason", None))
+ return client.get_plugin_data(IMPORT_NAME, MUCClientData).muc_client.kick(
+ room_jid, nick, reason=options.get("reason", None)
+ )
- def ban(self, client, entity_jid, room_jid, options=None):
- """Ban an entity from the room
+ def ban(
+ self,
+ client: SatXMPPEntity,
+ entity_jid: jid.JID,
+ room_jid: jid.JID,
+ options: dict | None = None,
+ ) -> defer.Deferred:
+ """Ban an entity from the room.
- @param entity_jid (JID): bare jid of the entity to be banned
- @param room_jid (JID): jid of the room
- @param options: attribute with extra info (reason, password) as in #XEP-0045
+ @param client: The client instance.
+ @param entity_jid: Bare JID of the entity to be banned.
+ @param room_jid: Room JID.
+ @param options: Dict with 'reason' key.
+ @return: Deferred firing when ban is complete.
"""
self.check_room_joined(client, room_jid)
if options is None:
options = {}
assert not entity_jid.resource
assert not room_jid.resource
- return client._muc_client.ban(
+ return client.get_plugin_data(IMPORT_NAME, MUCClientData).muc_client.ban(
room_jid, entity_jid, reason=options.get("reason", None)
)
- def affiliate(self, client, entity_jid, room_jid, options):
- """Change the affiliation of an entity
+ def affiliate(
+ self,
+ client: SatXMPPEntity,
+ entity_jid: jid.JID,
+ room_jid: jid.JID,
+ options: dict,
+ ) -> defer.Deferred:
+ """Change the affiliation of an entity.
- @param entity_jid (JID): bare jid of the entity
- @param room_jid_s (JID): jid of the room
- @param options: attribute with extra info (reason, nick) as in #XEP-0045
+ @param client: The client instance.
+ @param entity_jid: Bare JID of the entity.
+ @param room_jid: Room JID.
+ @param options: Dict with 'affiliation' key.
+ @return:
+ Deferred firing when affiliation change is complete.
"""
self.check_room_joined(client, room_jid)
assert not entity_jid.resource
assert not room_jid.resource
assert "affiliation" in options
# TODO: handles reason and nick
- return client._muc_client.modifyAffiliationList(
- room_jid, [entity_jid], options["affiliation"]
- )
+ return client.get_plugin_data(
+ IMPORT_NAME, MUCClientData
+ ).muc_client.modifyAffiliationList(room_jid, [entity_jid], options["affiliation"])
- # Text commands #
-
- def cmd_nick(self, client, mess_data):
- """change nickname
+ def cmd_nick(self, client: SatXMPPEntity, mess_data: dict) -> bool:
+ """Change nickname.
@command (group): new_nick
- new_nick: new nick to use
@@ -833,61 +1182,59 @@ class XEP_0045:
if nick:
room = mess_data["to"]
self.nick(client, room, nick)
-
return False
- def cmd_join(self, client, mess_data):
- """join a new room
+ def cmd_join(self, client: SatXMPPEntity, mess_data: dict) -> bool:
+ """Join a new room.
@command (all): JID
- JID: room to join (on the same service if full jid is not specified)
"""
room_raw = mess_data["unparsed"].strip()
- if room_raw:
- if self.is_joined_room(client, mess_data["to"]):
- # we use the same service as the one from the room where the command has
- # been entered if full jid is not entered
- muc_service = mess_data["to"].host
- nick = self.get_room_nick(client, mess_data["to"]) or client.jid.user
- else:
- # the command has been entered in a one2one conversation, so we use
- # our server MUC service as default service
- muc_service = client.muc_service or ""
- nick = client.jid.user
- room_jid = self.text_cmds.get_room_jid(room_raw, muc_service)
- defer.ensureDeferred(self.join(client, room_jid, nick, {}))
+ if not room_raw:
+ return False
+ if self.is_joined_room(client, mess_data["to"]):
+ # we use the same service as the one from the room where the command has
+ # been entered if full jid is not entered
+ muc_service = mess_data["to"].host
+ nick = self.get_room_nick(client, mess_data["to"]) or client.jid.user
+ else:
+ # the command has been entered in a one2one conversation, so we use
+ # our server MUC service as default service
+ muc_service = (
+ client.get_plugin_data(IMPORT_NAME, MUCClientData).muc_service or ""
+ )
+ nick = client.jid.user
+
+ room_jid = self.text_cmds.get_room_jid(room_raw, muc_service)
+ defer.ensureDeferred(self.join(client, room_jid, nick, {}))
return False
- def cmd_leave(self, client, mess_data):
- """quit a room
+ def cmd_leave(self, client: SatXMPPEntity, mess_data: dict) -> bool:
+ """Quit a room.
@command (group): [ROOM_JID]
- - ROOM_JID: jid of the room to live (current room if not specified)
+ - ROOM_JID: jid of the room to leave (current room if not specified)
"""
room_raw = mess_data["unparsed"].strip()
if room_raw:
room = self.text_cmds.get_room_jid(room_raw, mess_data["to"].host)
else:
room = mess_data["to"]
-
self.leave(client, room)
-
return False
- def cmd_part(self, client, mess_data):
- """just a synonym of /leave
-
- @command (group): [ROOM_JID]
- - ROOM_JID: jid of the room to live (current room if not specified)
- """
+ def cmd_part(self, client: SatXMPPEntity, mess_data: dict) -> bool:
+ """Synonym of /leave."""
return self.cmd_leave(client, mess_data)
- def cmd_kick(self, client, mess_data):
- """kick a room member
+ def cmd_kick(self, client: SatXMPPEntity, mess_data: dict) -> bool | defer.Deferred:
+ """Kick a room member.
- @command (group): ROOM_NICK
+ @command (group): ROOM_NICK [reason]
- ROOM_NICK: the nick of the person to kick
+ - reason: reason for the kick
"""
options = mess_data["unparsed"].strip().split()
try:
@@ -899,10 +1246,9 @@ class XEP_0045:
return False
reason = " ".join(options[1:]) if len(options) > 1 else None
-
d = self.kick(client, nick, mess_data["to"], {"reason": reason})
- def cb(__):
+ def cb(_) -> bool:
feedback_msg = _("You have kicked {}").format(nick)
if reason is not None:
feedback_msg += _(" for the following reason: {reason}").format(
@@ -914,8 +1260,8 @@ class XEP_0045:
d.addCallback(cb)
return d
- def cmd_ban(self, client, mess_data):
- """ban an entity from the room
+ def cmd_ban(self, client: SatXMPPEntity, mess_data: dict) -> bool | defer.Deferred:
+ """Ban an entity from the room.
@command (group): (JID) [reason]
- JID: the JID of the entity to ban
@@ -941,10 +1287,9 @@ class XEP_0045:
return False
reason = " ".join(options[1:]) if len(options) > 1 else None
-
d = self.ban(client, entity_jid, mess_data["to"], {"reason": reason})
- def cb(__):
+ def cb(_) -> bool:
feedback_msg = _("You have banned {}").format(entity_jid)
if reason is not None:
feedback_msg += _(" for the following reason: {reason}").format(
@@ -956,8 +1301,10 @@ class XEP_0045:
d.addCallback(cb)
return d
- def cmd_affiliate(self, client, mess_data):
- """affiliate an entity to the room
+ def cmd_affiliate(
+ self, client: SatXMPPEntity, mess_data: dict
+ ) -> bool | defer.Deferred:
+ """Affiliate an entity to the room.
@command (group): (JID) [owner|admin|member|none|outcast]
- JID: the JID of the entity to affiliate
@@ -981,15 +1328,16 @@ class XEP_0045:
AssertionError,
):
feedback = _(
- "You must provide a valid JID to affiliate, like in '/affiliate contact@example.net member'"
+ "You must provide a valid JID to affiliate, like in '/affiliate"
+ " contact@example.net member'"
)
self.text_cmds.feed_back(client, feedback, mess_data)
return False
affiliation = options[1] if len(options) > 1 else "none"
- if affiliation not in AFFILIATIONS:
- feedback = _("You must provide a valid affiliation: %s") % " ".join(
- AFFILIATIONS
+ if affiliation not in Affiliation:
+ feedback = _("You must provide a valid affiliation: {}").format(
+ " ".join(a.value for a in Affiliation)
)
self.text_cmds.feed_back(client, feedback, mess_data)
return False
@@ -998,7 +1346,7 @@ class XEP_0045:
client, entity_jid, mess_data["to"], {"affiliation": affiliation}
)
- def cb(__):
+ def cb(_) -> bool:
feedback_msg = _("New affiliation for {entity}: {affiliation}").format(
entity=entity_jid, affiliation=affiliation
)
@@ -1008,30 +1356,24 @@ class XEP_0045:
d.addCallback(cb)
return d
- def cmd_title(self, client, mess_data):
- """change room's subject
+ def cmd_title(self, client: SatXMPPEntity, mess_data: dict) -> bool:
+ """Change room's subject.
@command (group): title
- title: new room subject
"""
subject = mess_data["unparsed"].strip()
-
if subject:
room = mess_data["to"]
self.subject(client, room, subject)
-
return False
- def cmd_topic(self, client, mess_data):
- """just a synonym of /title
-
- @command (group): title
- - title: new room subject
- """
+ def cmd_topic(self, client: SatXMPPEntity, mess_data: dict) -> bool:
+ """Synonym of /title."""
return self.cmd_title(client, mess_data)
- def cmd_list(self, client, mess_data):
- """list available rooms in a muc server
+ def cmd_list(self, client: SatXMPPEntity, mess_data: dict) -> bool | defer.Deferred:
+ """List available rooms in a MUC server.
@command (all): [MUC_SERVICE]
- MUC_SERVICE: service to request
@@ -1042,65 +1384,86 @@ class XEP_0045:
try:
service = jid.JID(unparsed)
except RuntimeError:
+ muc_data = client.get_plugin_data(IMPORT_NAME, MUCClientData)
if mess_data["type"] == C.MESS_TYPE_GROUPCHAT:
room_jid = mess_data["to"]
service = jid.JID(room_jid.host)
- elif client.muc_service is not None:
- service = client.muc_service
+ elif muc_data.muc_service is not None:
+ service = muc_data.muc_service
else:
- msg = D_("No known default MUC service {unparsed}").format(
- unparsed=unparsed
- )
+ msg = D_("No known default MUC service")
self.text_cmds.feed_back(client, msg, mess_data)
return False
except jid.InvalidFormat:
- msg = D_("{} is not a valid JID!".format(unparsed))
+ msg = D_("{} is not a valid JID!").format(unparsed)
self.text_cmds.feed_back(client, msg, mess_data)
return False
+
d = self.host.getDiscoItems(client, service)
d.addCallback(self._show_list_ui, client, service)
+ return d
- return False
-
- def _whois(self, client, whois_msg, mess_data, target_jid):
- """Add MUC user information to whois"""
+ def _whois(
+ self,
+ client: SatXMPPEntity,
+ whois_msg: list,
+ mess_data: dict,
+ target_jid: jid.JID,
+ ) -> None:
+ """Add MUC user information to whois."""
if mess_data["type"] != "groupchat":
return
- if target_jid.userhostJID() not in client._muc_client.joined_rooms:
- log.warning(_("This room has not been joined"))
+ if (
+ target_jid.userhostJID()
+ not in client.get_plugin_data(
+ IMPORT_NAME, MUCClientData
+ ).muc_client.joined_rooms
+ ):
+ log.warning("This room has not been joined")
return
if not target_jid.resource:
return
- user = client._muc_client.joined_rooms[target_jid.userhostJID()].getUser(
+
+ muc_data = client.get_plugin_data(IMPORT_NAME, MUCClientData)
+ user = muc_data.muc_client.joined_rooms[target_jid.userhostJID()].getUser(
target_jid.resource
)
- whois_msg.append(_("Nickname: %s") % user.nick)
+ whois_msg.append(_("Nickname: {}").format(user.nick))
if user.entity:
- whois_msg.append(_("Entity: %s") % user.entity)
+ whois_msg.append(_("Entity: {}").format(user.entity))
if user.affiliation != "none":
- whois_msg.append(_("Affiliation: %s") % user.affiliation)
+ whois_msg.append(_("Affiliation: {}").format(user.affiliation))
if user.role != "none":
- whois_msg.append(_("Role: %s") % user.role)
+ whois_msg.append(_("Role: {}").format(user.role))
if user.status:
- whois_msg.append(_("Status: %s") % user.status)
+ whois_msg.append(_("Status: {}").format(user.status))
if user.show:
- whois_msg.append(_("Show: %s") % user.show)
+ whois_msg.append(_("Show: {}").format(user.show))
- def presence_trigger(self, presence_elt, client):
+ def presence_trigger(self, presence_elt, client: SatXMPPEntity) -> bool:
+ """Broadcast presence to all joined rooms."""
# FIXME: should we add a privacy parameters in settings to activate before
# broadcasting the presence to all MUC rooms ?
- muc_client = client._muc_client
+ muc_client = client.get_plugin_data(IMPORT_NAME, MUCClientData).muc_client
for room_jid, room in muc_client.joined_rooms.items():
elt = xml_tools.element_copy(presence_elt)
elt["to"] = room_jid.userhost() + "/" + room.nick
client.presence.send(elt)
return True
- def presence_received_trigger(self, client, entity, show, priority, statuses):
+ def presence_received_trigger(
+ self,
+ client: SatXMPPEntity,
+ entity: jid.JID,
+ show: str,
+ priority: int,
+ statuses: dict,
+ ) -> bool:
+ """Handle presence received trigger."""
+ # presence is already handled in (un)availableReceived
entity_bare = entity.userhostJID()
- muc_client = client._muc_client
+ muc_client = client.get_plugin_data(IMPORT_NAME, MUCClientData).muc_client
if entity_bare in muc_client.joined_rooms:
- # presence is already handled in (un)availableReceived
return False
return True
@@ -1108,287 +1471,52 @@ class XEP_0045:
@implementer(itx_xmpp.IDisco)
class LiberviaMUCClient(muc.MUCClient):
- def __init__(self, plugin_parent):
+ ## presence/roster ##
+
+ def __init__(self, plugin_parent: XEP_0045) -> None:
self.plugin_parent = plugin_parent
- muc.MUCClient.__init__(self)
- self._changing_nicks = set() # used to keep trace of who is changing nick,
- # and to discard userJoinedRoom signal in this case
- print("init SatMUCClient OK")
+ super().__init__()
@property
- def joined_rooms(self):
+ def joined_rooms(self) -> dict:
+ """Get joined rooms."""
return self._rooms
@property
- def host(self):
+ def host(self) -> "LiberviaBackend":
+ """Get host."""
return self.plugin_parent.host
@property
- def client(self):
+ def client(self) -> SatXMPPEntity:
+ """Get client."""
+ assert self.parent is not None
return self.parent
- @property
- def _mam(self):
- return self.plugin_parent._mam
-
- @property
- def _si(self):
- return self.plugin_parent._si
-
- def change_room_state(self, room, new_state):
- """Check that room is in expected state, and change it
-
- @param new_state: one of ROOM_STATE_*
- """
- new_state_idx = ROOM_STATES.index(new_state)
- if new_state_idx == -1:
- raise exceptions.InternalError("unknown room state")
- if new_state_idx < 1:
- raise exceptions.InternalError(
- "unexpected new room state ({room}): {state}".format(
- room=room.userhost(), state=new_state
- )
- )
- expected_state = ROOM_STATES[new_state_idx - 1]
- if room.state != expected_state:
- log.error(
- _(
- "room {room} is not in expected state: room is in state {current_state} "
- "while we were expecting {expected_state}"
- ).format(
- room=room.roomJID.userhost(),
- current_state=room.state,
- expected_state=expected_state,
- )
- )
- room.state = new_state
-
- def _addRoom(self, room):
- super(LiberviaMUCClient, self)._addRoom(room)
- room._roster_ok = False # True when occupants list has been fully received
- room.state = ROOM_STATE_OCCUPANTS
- # FIXME: check if history_d is not redundant with fully_joined
- room.fully_joined = defer.Deferred() # called when everything is OK
- # cache data until room is ready
- # list of elements which will be re-injected in stream
- room._cache = []
- # we only need to keep last presence status for each jid, so a dict is suitable
- room._cache_presence = {}
-
- async def _join_legacy(
- self, client: SatXMPPEntity, room_jid: jid.JID, nick: str, password: Optional[str]
- ) -> muc.Room:
- """Join room an retrieve history with legacy method"""
- mess_data_list = await self.host.memory.history_get(
- room_jid,
- client.jid.userhostJID(),
- limit=1,
- between=True,
- profile=client.profile,
- )
- if mess_data_list:
- timestamp = mess_data_list[0][1]
- # we use seconds since last message to get backlog without duplicates
- # and we remove 1 second to avoid getting the last message again
- seconds = int(time.time() - timestamp) - 1
- else:
- seconds = None
-
- room = await super(LiberviaMUCClient, self).join(
- room_jid, nick, muc.HistoryOptions(seconds=seconds), password
- )
- # used to send bridge signal once backlog are written in history
- room._history_type = HISTORY_LEGACY
- room._history_d = defer.Deferred()
- room._history_d.callback(None)
- return room
-
- async def _get_mam_history(
- self, client: SatXMPPEntity, room: muc.Room, room_jid: jid.JID
- ) -> None:
- """Retrieve history for rooms handling MAM"""
- history_d = room._history_d = defer.Deferred()
- # we trigger now the deferred so all callback are processed as soon as possible
- # and in order
- history_d.callback(None)
-
- last_mess = await self.host.memory.history_get(
- room_jid,
- None,
- limit=1,
- between=False,
- filters={"types": C.MESS_TYPE_GROUPCHAT, "last_stanza_id": True},
- profile=client.profile,
- )
- if last_mess:
- stanza_id = last_mess[0][-1]["stanza_id"]
- rsm_req = rsm.RSMRequest(max_=20, after=stanza_id)
- no_loop = False
- else:
- log.info(
- "We have no MAM archive for room {room_jid}.".format(room_jid=room_jid)
- )
- # we don't want the whole archive if we have no archive yet
- # as it can be huge
- rsm_req = rsm.RSMRequest(max_=50, before="")
- no_loop = True
-
- mam_req = mam.MAMRequest(rsm_=rsm_req)
- complete = False
- count = 0
- while not complete:
- try:
- mam_data = await self._mam.get_archives(client, mam_req, service=room_jid)
- except xmpp_error.StanzaError as e:
- if last_mess and e.condition == "item-not-found":
- log.warning(
- f"requested item (with id {stanza_id!r}) can't be found in "
- f"history of {room_jid}, history has probably been purged on "
- f"server."
- )
- # we get last items like for a new room
- rsm_req = rsm.RSMRequest(max_=50, before="")
- mam_req = mam.MAMRequest(rsm_=rsm_req)
- no_loop = True
- continue
- else:
- raise e
- elt_list, rsm_response, mam_response = mam_data
- complete = True if no_loop else mam_response["complete"]
- # we update MAM request for next iteration
- mam_req.rsm.after = rsm_response.last
-
- if not elt_list:
- break
- else:
- count += len(elt_list)
-
- for mess_elt in elt_list:
- try:
- fwd_message_elt = self._mam.get_message_from_result(
- client, mess_elt, mam_req, service=room_jid
- )
- except exceptions.DataError:
- continue
- if fwd_message_elt.getAttribute("to"):
- log.warning(
- 'Forwarded message element has a "to" attribute while it is '
- "forbidden by specifications"
- )
- fwd_message_elt["to"] = client.jid.full()
- client.messageProt.onMessage(fwd_message_elt)
- client._muc_client._onGroupChat(fwd_message_elt)
-
- if not count:
- log.info(
- _(
- "No message received while offline in {room_jid}".format(
- room_jid=room_jid
- )
- )
- )
- else:
- log.info(
- _(
- "We have received {num_mess} message(s) in {room_jid} while "
- "offline."
- ).format(num_mess=count, room_jid=room_jid)
- )
-
- # for legacy history, the following steps are done in receivedSubject but for MAM
- # the order is different (we have to join then get MAM archive, so subject
- # is received before archive), so we change state and add the callbacks here.
- self.change_room_state(room, ROOM_STATE_LIVE)
- history_d.addCallbacks(
- self._history_cb, self._history_eb, [room], errbackArgs=[room]
- )
-
- # we wait for all callbacks to be processed
- await history_d
-
- async def _join_mam(
- self, client: SatXMPPEntity, room_jid: jid.JID, nick: str, password: Optional[str]
- ) -> muc.Room:
- """Join room and retrieve history using MAM"""
- room = await super(LiberviaMUCClient, self).join(
- # we don't want any history from room as we'll get it with MAM
- room_jid,
- nick,
- muc.HistoryOptions(maxStanzas=0),
- password=password,
- )
- room._history_type = HISTORY_MAM
- # MAM history retrieval can be very long, and doesn't need to be sync, so we don't
- # wait for it
- defer.ensureDeferred(self._get_mam_history(client, room, room_jid))
- room.fully_joined.callback(room)
-
- return room
-
- async def join(self, room_jid, nick, password=None):
- room_service = jid.JID(room_jid.host)
- has_mam = await self.host.has_feature(self.client, mam.NS_MAM, room_service)
- if not self._mam or not has_mam:
- return await self._join_legacy(self.client, room_jid, nick, password)
- else:
- return await self._join_mam(self.client, room_jid, nick, password)
-
- ## presence/roster ##
-
- def availableReceived(self, presence):
- """
- Available presence was received.
- """
- # XXX: we override MUCClient.availableReceived to fix bugs
- # (affiliation and role are not set)
-
+ def availableReceived(self, presence) -> None:
+ """Handle available presence."""
room, user = self._getRoomUser(presence)
- if room is None:
+ if room is None or user is None:
return
- if user is None:
- nick = presence.sender.resource
- if not nick:
- log.warning(
- _("missing nick in presence: {xml}").format(
- xml=presence.toElement().toXml()
- )
- )
- return
- user = muc.User(nick, presence.entity)
-
- # we want to keep statuses with room
- # XXX: presence if broadcasted, and we won't have status code
- # like 110 (REALJID_PUBLIC) after first received
- # so we keep only the initial (with SELF_PRESENCE),
- # thus we check if attribute already exists
+ # we want to keep statuses with room so they can be sent to the frontend via the
+ # muc_room_joined bridge signal
+ # XXX: after the first received, subsequent broadcasts won't include
+ # status codes like 110 (SELF_PRESENCE) or 100 (REALJID_PUBLIC), so we only
+ # keep the statuses from the initial presence (identified by SELF_PRESENCE);
+ # thus we check if the attribute already exists.
if (
not hasattr(room, "statuses")
and muc.STATUS_CODE.SELF_PRESENCE in presence.mucStatuses
):
room.statuses = presence.mucStatuses
- # Update user data
- user.role = presence.role
- user.affiliation = presence.affiliation
- user.status = presence.status
- user.show = presence.show
+ super().availableReceived(presence)
- if room.inRoster(user):
- self.userUpdatedStatus(room, user, presence.show, presence.status)
- else:
- room.addUser(user)
- self.userJoinedRoom(room, user)
-
- def unavailableReceived(self, presence):
+ def unavailableReceived(self, presence) -> None:
+ """Handle unavailable presence."""
# XXX: we override this method to manage nickname change
- """
- Unavailable presence was received.
-
- If this was received from a MUC room occupant JID, that occupant has
- left the room.
- """
room, user = self._getRoomUser(presence)
if room is None or user is None:
@@ -1397,256 +1525,152 @@ class LiberviaMUCClient(muc.MUCClient):
room.removeUser(user)
if muc.STATUS_CODE.NEW_NICK in presence.mucStatuses:
- self._changing_nicks.add(presence.nick)
self.user_changed_nick(room, user, presence.nick)
else:
- self._changing_nicks.discard(presence.nick)
+ # we left the room
self.userLeftRoom(room, user)
- def userJoinedRoom(self, room, user):
+ def userJoinedRoom(self, room: muc.Room, user: muc.User) -> None:
+ """Handle user joined room event."""
if user.nick == room.nick:
- # we have received our own nick,
- # this mean that the full room roster was received
- self.change_room_state(room, ROOM_STATE_SELF_PRESENCE)
+ # we have received our own nick, this mean that the full room roster was
+ # received
log.debug(
- "room {room} joined with nick {nick}".format(
- room=room.occupantJID.userhost(), nick=user.nick
- )
+ "room {room} joined with nick {nick}",
+ room=room.occupantJID.userhost(),
+ nick=user.nick,
)
- # we set type so we don't have to use a deferred
- # with disco to check entity type
+ args = self.plugin_parent._get_room_joined_bridge_args(self.client, room)
+ self.plugin_parent.host.bridge.muc_room_joined(*args)
+
+ # we set type so we don't have to use a deferred with disco to check entity
+ # type
self.host.memory.update_entity_data(
self.client, room.roomJID, C.ENTITY_TYPE, C.ENTITY_TYPE_MUC
)
- elif room.state not in (ROOM_STATE_OCCUPANTS, ROOM_STATE_LIVE):
- log.warning(
- "Received user presence data in a room before its initialisation "
- "(current state: {state}),"
- "this is not standard! Ignoring it: {room} ({nick})".format(
- state=room.state, room=room.roomJID.userhost(), nick=user.nick
- )
- )
return
- else:
- if not room.fully_joined.called:
- return
- for cb in room.on_joined_callbacks:
- defer.ensureDeferred(cb(room, user))
- try:
- self._changing_nicks.remove(user.nick)
- except KeyError:
- # this is a new user
- log.debug(
- _("user {nick} has joined room {room_id}").format(
- nick=user.nick, room_id=room.occupantJID.userhost()
- )
- )
- if not self.host.trigger.point(
- "MUC user joined", room, user, self.client.profile
- ):
- return
- extra = {
- "info_type": ROOM_USER_JOINED,
- "user_affiliation": user.affiliation,
- "user_role": user.role,
- "user_nick": user.nick,
- }
- if user.entity is not None:
- extra["user_entity"] = user.entity.full()
- mess_data = { # dict is similar to the one used in client.onMessage
- "from": room.roomJID,
- "to": self.client.jid,
- "uid": str(uuid.uuid4()),
- "message": {"": D_("=> {} has joined the room").format(user.nick)},
- "subject": {},
- "type": C.MESS_TYPE_INFO,
- "extra": extra,
- "timestamp": time.time(),
- }
- # FIXME: we disable presence in history as it's taking a lot of space
- # while not indispensable. In the future an option may allow
- # to re-enable it
- # self.client.message_add_to_history(mess_data)
- self.client.message_send_to_bridge(mess_data)
+ log.debug(
+ "user {nick} has joined room {room_id}",
+ nick=user.nick,
+ room_id=room.occupantJID.userhost(),
+ )
+ if not self.host.trigger.point(
+ "MUC user joined", room, user, self.client.profile
+ ):
+ return
- def userLeftRoom(self, room, user):
+ extra = {
+ "info_type": ROOM_USER_JOINED,
+ "user_affiliation": user.affiliation,
+ "user_role": user.role,
+ "user_nick": user.nick,
+ }
+ if user.entity is not None:
+ extra["user_entity"] = user.entity.full()
+
+ mess_data = {
+ "from": room.roomJID,
+ "to": self.client.jid,
+ "uid": str(uuid.uuid4()),
+ "message": {"": _("=> {} has joined the room").format(user.nick)},
+ "subject": {},
+ "type": C.MESS_TYPE_INFO,
+ "extra": extra,
+ "timestamp": time.time(),
+ }
+ self.client.message_send_to_bridge(mess_data)
+
+ def userLeftRoom(self, room: muc.Room, user: muc.User) -> None:
+ """Handle user left room event."""
if not self.host.trigger.point("MUC user left", room, user, self.client.profile):
return
+
if user.nick == room.nick:
- # we left the room
room_jid_s = room.roomJID.userhost()
log.info(
- _("Room ({room}) left ({profile})").format(
- room=room_jid_s, profile=self.client.profile
- )
+ "Room ({room}) left ({profile})",
+ room=room_jid_s,
+ profile=self.client.profile,
)
self.host.memory.del_entity_cache(
room.roomJID, profile_key=self.client.profile
)
- self.host.bridge.muc_room_left(room.roomJID.userhost(), self.client.profile)
- elif room.state != ROOM_STATE_LIVE:
- log.warning(
- "Received user presence data in a room before its initialisation (current state: {state}),"
- "this is not standard! Ignoring it: {room} ({nick})".format(
- state=room.state, room=room.roomJID.userhost(), nick=user.nick
- )
- )
+ self.host.bridge.muc_room_left(room_jid_s, self.client.profile)
return
- else:
- if not room.fully_joined.called:
- return
- log.debug(
- _("user {nick} left room {room_id}").format(
- nick=user.nick, room_id=room.occupantJID.userhost()
- )
- )
- for cb in room.on_left_callbacks:
- defer.ensureDeferred(cb(room, user))
- extra = {
- "info_type": ROOM_USER_LEFT,
- "user_affiliation": user.affiliation,
- "user_role": user.role,
- "user_nick": user.nick,
- }
- if user.entity is not None:
- extra["user_entity"] = user.entity.full()
- mess_data = { # dict is similar to the one used in client.onMessage
- "from": room.roomJID,
- "to": self.client.jid,
- "uid": str(uuid.uuid4()),
- "message": {"": D_("<= {} has left the room").format(user.nick)},
- "subject": {},
- "type": C.MESS_TYPE_INFO,
- "extra": extra,
- "timestamp": time.time(),
- }
- # FIXME: disable history, see userJoinRoom comment
- # self.client.message_add_to_history(mess_data)
- self.client.message_send_to_bridge(mess_data)
- def user_changed_nick(self, room, user, new_nick):
+ log.debug(
+ "user {nick} left room {room_id}",
+ nick=user.nick,
+ room_id=room.occupantJID.userhost(),
+ )
+
+ extra = {
+ "info_type": ROOM_USER_LEFT,
+ "user_affiliation": user.affiliation,
+ "user_role": user.role,
+ "user_nick": user.nick,
+ }
+ if user.entity is not None:
+ extra["user_entity"] = user.entity.full()
+
+ mess_data = {
+ "from": room.roomJID,
+ "to": self.client.jid,
+ "uid": str(uuid.uuid4()),
+ "message": {"": _("<= {} has left the room").format(user.nick)},
+ "subject": {},
+ "type": C.MESS_TYPE_INFO,
+ "extra": extra,
+ "timestamp": time.time(),
+ }
+ self.client.message_send_to_bridge(mess_data)
+
+ def user_changed_nick(self, room: muc.Room, user: muc.User, new_nick: str) -> None:
+ """Handle user nick change."""
self.host.bridge.muc_room_user_changed_nick(
room.roomJID.userhost(), user.nick, new_nick, self.client.profile
)
- def userUpdatedStatus(self, room, user, show, status):
+ def userUpdatedStatus(
+ self, room: muc.Room, user: muc.User, show: str, status: str
+ ) -> None:
+ """Handle user status update."""
entity = jid.JID(tuple=(room.roomJID.user, room.roomJID.host, user.nick))
- if hasattr(room, "_cache_presence"):
- # room has a cache for presence, meaning it has not been fully
- # joined yet. So we put presence in cache, and stop workflow.
- # Or delete current presence and continue workflow if it's an
- # "unavailable" presence
- cache = room._cache_presence
- cache[entity] = {
- "room": room,
- "user": user,
- "show": show,
- "status": status,
- }
- return
statuses = {C.PRESENCE_STATUSES_DEFAULT: status or ""}
self.host.bridge.presence_update(
entity.full(), show or "", 0, statuses, self.client.profile
)
- ## messages ##
-
- def receivedGroupChat(self, room, user, body):
- log.debug(
- "receivedGroupChat: room=%s user=%s body=%s"
- % (room.roomJID.full(), user, body)
- )
-
- ## subject ##
-
- def groupChatReceived(self, message):
- """
- A group chat message has been received from a MUC room.
-
- There are a few event methods that may get called here.
- L{receivedGroupChat}, L{receivedSubject} or L{receivedHistory}.
- """
+ def receivedSubject(
+ self, room: muc.Room, user: muc.User | None, subject: str
+ ) -> None:
+ """Handle room subject."""
# We override this method to fix subject handling (empty strings were discarded)
# FIXME: remove this once fixed upstream
- room, user = self._getRoomUser(message)
+ room.subject = subject
- if room is None:
- log.warning(
- "No room found for message: {message}".format(
- message=message.toElement().toXml()
- )
- )
+ if user is not None and user.nick == room.nick:
return
- if message.subject is not None:
- self.receivedSubject(room, user, message.subject)
- elif message.delay is None:
- self.receivedGroupChat(room, user, message)
-
- def subject(self, room, subject):
- return muc.MUCClientProtocol.subject(self, room, subject)
-
- def _history_cb(self, __, room):
- """Called when history have been written to database and subject is received
-
- this method will finish joining by:
- - sending message to bridge
- - calling fully_joined deferred (for legacy history)
- - sending stanza put in cache
- - cleaning variables not needed anymore
- """
- args = self.plugin_parent._get_room_joined_args(room, self.client.profile)
- self.host.bridge.muc_room_joined(*args)
- if room._history_type == HISTORY_LEGACY:
- room.fully_joined.callback(room)
- del room._history_d
- del room._history_type
- cache = room._cache
- del room._cache
- cache_presence = room._cache_presence
- del room._cache_presence
- for elem in cache:
- self.client.xmlstream.dispatch(elem)
- for presence_data in cache_presence.values():
- if not presence_data["show"] and not presence_data["status"]:
- # occupants are already sent in muc_room_joined, so if we don't have
- # extra information like show or statuses, we can discard the signal
- continue
- else:
- self.userUpdatedStatus(**presence_data)
-
- def _history_eb(self, failure_, room):
- log.error("Error while managing history: {}".format(failure_))
- self._history_cb(None, room)
-
- def receivedSubject(self, room, user, subject):
- # when subject is received, we know that we have whole roster and history
- # cf. http://xmpp.org/extensions/xep-0045.html#enter-subject
- room.subject = subject # FIXME: subject doesn't handle xml:lang
- if room.state != ROOM_STATE_LIVE:
- if room._history_type == HISTORY_LEGACY:
- self.change_room_state(room, ROOM_STATE_LIVE)
- room._history_d.addCallbacks(
- self._history_cb, self._history_eb, [room], errbackArgs=[room]
- )
- else:
- # the subject has been changed
- log.debug(
- _("New subject for room ({room_id}): {subject}").format(
- room_id=room.roomJID.full(), subject=subject
- )
- )
- self.host.bridge.muc_room_new_subject(
- room.roomJID.userhost(), subject, self.client.profile
- )
+ log.debug(
+ "New subject for room ({room_id}): {subject}",
+ room_id=room.occupantJID.full(),
+ subject=subject,
+ )
+ # the subject has been changed
+ self.host.bridge.muc_room_new_subject(
+ room.roomJID.userhost(), subject, self.client.profile
+ )
## disco ##
- def getDiscoInfo(self, requestor, target, nodeIdentifier=""):
+ def getDiscoInfo(self, requestor, target: jid.JID, nodeIdentifier: str = "") -> list:
+ """Get disco info."""
return [disco.DiscoFeature(NS_MUC)]
- def getDiscoItems(self, requestor, target, nodeIdentifier=""):
+ def getDiscoItems(self, requestor, target: jid.JID, nodeIdentifier: str = "") -> list:
+ """Get disco items."""
# TODO: manage room queries ? Bad for privacy, must be disabled by default
# see XEP-0045 § 6.7
return []
diff --git a/libervia/backend/plugins/plugin_xep_0313.py b/libervia/backend/plugins/plugin_xep_0313.py
index fff12918..ee8c2ed7 100644
--- a/libervia/backend/plugins/plugin_xep_0313.py
+++ b/libervia/backend/plugins/plugin_xep_0313.py
@@ -341,6 +341,8 @@ class XEP_0313(object):
if delay_elt is not None:
fwd_message_elt.addChild(delay_elt)
+ fwd_message_elt._is_from_archive = True
+
return fwd_message_elt
def queryFields(self, client, service=None):