plugin XEP-0313: Sync status tracking + resume workflow:

- Add sync status tracking with bridge signals (`mam_sync_started`, `mam_sync_completed`,
  `mam_sync_error`), `mam_sync_statuses_get` method and helper methods that are called
  from `plugin_xep_0045`.
- Update "resume" workflow:
    - fix settings of last stanza ID, so it's always set (was not set for our own messages)
    - own message now use `onMessage` full workflow instead of just store and signal.
    - add `flush_archive_batch` at the end of the process.
    - Sync status are sent during resumt
- Modernisation (type hints, use of `get_plugin_data`)
This commit is contained in:
Goffi 2026-05-01 20:31:49 +02:00
commit 766d49fb38
2 changed files with 383 additions and 193 deletions

View file

@ -923,6 +923,7 @@ class XEP_0045:
room_jid = room.roomJID
total_messages = 0
self._mam.sync_started(room_jid, client.profile)
try:
last_mess = await self.host.memory.history_get(
room_jid,
@ -1015,9 +1016,11 @@ class XEP_0045:
f"Retrieved {total_messages} message(s) from {room_jid} while"
" offline.",
)
self._mam.sync_completed(room_jid, client.profile, total_messages)
except Exception as e:
log.exception(f"MAM history sync failed for {room_jid}")
self._mam.sync_error(room_jid, client.profile, str(e))
finally:
client.messageProt.flush_archive_batch()

View file

@ -1,7 +1,6 @@
#!/usr/bin/env python3
# SAT plugin for Message Archive Management (XEP-0313)
# Libervia plugin for Message Archive Management (XEP-0313)
# Copyright (C) 2009-2026 Jérôme Poisson (goffi@goffi.org)
# Copyright (C) 2013-2016 Adrien Cossa (souliane@mailoo.org)
@ -18,6 +17,7 @@
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from collections.abc import Callable
from datetime import datetime
import uuid
@ -25,24 +25,33 @@ from dateutil import tz
from twisted.internet import defer
from twisted.words.protocols.jabber import jid
from twisted.words.protocols.jabber.error import StanzaError
from libervia.backend import G
from tx_xmpp import disco
from tx_xmpp import data_form
from tx_xmpp import rsm
from tx_xmpp import mam
from twisted.words.xish import domish
from zope.interface import implementer
from libervia.backend.core import exceptions
from libervia.backend.core.constants import Const as C
from libervia.backend.core.core_types import SatXMPPEntity
from libervia.backend.core.i18n import _
from libervia.backend.core.log import getLogger
from libervia.backend.tools import xml_tools
from libervia.backend.tools.common import data_format
from typing import Any, Literal, TYPE_CHECKING
if TYPE_CHECKING:
from libervia.backend.core.main import LiberviaBackend
log = getLogger(__name__)
IMPORT_NAME = "XEP-0313"
PLUGIN_INFO = {
C.PI_NAME: "Message Archive Management",
C.PI_IMPORT_NAME: "XEP-0313",
C.PI_IMPORT_NAME: IMPORT_NAME,
C.PI_TYPE: "XEP",
# XEP-0431 only defines a namespace, so we register it here
C.PI_PROTOCOLS: ["XEP-0313", "XEP-0431"],
@ -60,8 +69,8 @@ MESSAGE_STANZA_ID = '/message/stanza-id[@xmlns="{ns_stanza_id}"]'
NS_FTS = "urn:xmpp:fulltext:0"
class XEP_0313(object):
def __init__(self, host):
class XEP_0313:
def __init__(self, host: "LiberviaBackend") -> None:
log.info(_("Message Archive Management plugin initialization"))
self.host = host
self.host.register_namespace("mam", mam.NS_MAM)
@ -71,6 +80,7 @@ class XEP_0313(object):
# Deferred used to store last stanza id in order of reception
self._last_stanza_id_d = defer.Deferred()
self._last_stanza_id_d.callback(None)
self._sync_status: dict[jid.JID, bool] = {}
host.bridge.add_method(
"mam_get",
".plugin",
@ -79,137 +89,195 @@ class XEP_0313(object):
method=self._get_archives,
async_=True,
)
async def resume(self, client):
"""Retrieve one2one messages received since the last we have in local storage"""
stanza_id_data = await self.host.memory.storage.get_privates(
mam.NS_MAM, [KEY_LAST_STANZA_ID], profile=client.profile
host.bridge.add_method(
"mam_sync_statuses_get",
".plugin",
in_sign="s",
out_sign="a{sb}",
method=self._sync_statuses_get,
)
stanza_id = stanza_id_data.get(KEY_LAST_STANZA_ID)
rsm_req = None
if stanza_id is None:
log.info("can't retrieve last stanza ID, checking history")
last_mess = await self.host.memory.history_get(
None,
None,
limit=1,
filters={"not_types": C.MESS_TYPE_GROUPCHAT, "last_stanza_id": True},
profile=client.profile,
# args: entity_jid, profile
host.bridge.add_signal("mam_sync_started", ".plugin", signature="ss")
# args: entity_jid, profile, message_count
host.bridge.add_signal("mam_sync_completed", ".plugin", signature="ssi")
# args: entity_jid, profile, error_message
host.bridge.add_signal("mam_sync_error", ".plugin", signature="sss")
def _sync_statuses_get(self, profile: str) -> dict[str, bool]:
"""Get MAM sync status for all entities.
@param profile: The profile name.
@return: Mapping of entity_jid to syncing status.
"""
return {str(jid): status for jid, status in self._sync_status.items()}
def sync_started(self, entity_jid: jid.JID, profile: str) -> None:
"""Set sync status and emit signal.
@param entity_jid: The entity JID.
@param profile: The profile name.
"""
self._sync_status[entity_jid] = True
self.host.bridge.mam_sync_started(str(entity_jid), profile)
def sync_completed(self, entity_jid: jid.JID, profile: str, count: int) -> None:
"""Clear sync status and emit signal.
@param entity_jid: The entity JID (jid.JID or str).
@param profile: The profile name.
@param count: Number of messages synced.
"""
self._sync_status.pop(entity_jid, None)
self.host.bridge.mam_sync_completed(str(entity_jid), profile, count)
def sync_error(self, entity_jid: jid.JID, profile: str, error: str) -> None:
"""Clear sync status and emit signal.
@param entity_jid: The entity JID (jid.JID or str).
@param profile: The profile name.
@param error: Error message.
"""
self._sync_status.pop(entity_jid, None)
self.host.bridge.mam_sync_error(str(entity_jid), profile, error)
async def resume(self, client: SatXMPPEntity) -> None:
"""Retrieve one2one messages received since the last we have in local storage"""
entity_jid = client.jid.userhostJID()
self.sync_started(entity_jid, client.profile)
try:
stanza_id_data = await self.host.memory.storage.get_privates(
mam.NS_MAM, [KEY_LAST_STANZA_ID], profile=client.profile
)
if not last_mess:
log.info(_("It seems that we have no MAM history yet"))
stanza_id = None
rsm_req = rsm.RSMRequest(max_=50, before="")
else:
stanza_id = last_mess[0][-1]["stanza_id"]
if rsm_req is None:
rsm_req = rsm.RSMRequest(max_=100, after=stanza_id)
mam_req = mam.MAMRequest(rsm_=rsm_req)
complete = False
count = 0
while not complete:
try:
mam_data = await self.get_archives(
client, mam_req, service=client.jid.userhostJID()
stanza_id = stanza_id_data.get(KEY_LAST_STANZA_ID)
rsm_req = None
if stanza_id is None:
log.info("can't retrieve last stanza ID, checking history")
last_mess = await self.host.memory.history_get(
None,
None,
limit=1,
filters={"not_types": C.MESS_TYPE_GROUPCHAT, "last_stanza_id": True},
profile=client.profile,
)
except StanzaError as e:
log.warning(
f"Can't retrieve MAM archives: {e}\n"
f"{xml_tools.pp_elt(mam_req.toElement())}\n"
f"{xml_tools.pp_elt(e.stanza)}"
)
return
except Exception as e:
log.exception(f"Can't retrieve retrieve MAM archive")
return
elt_list, rsm_response, mam_response = mam_data
complete = mam_response["complete"]
# we update MAM request for next iteration
mam_req.rsm.after = rsm_response.last
# before may be set if we had no previous history
mam_req.rsm.before = None
if not elt_list:
break
else:
count += len(elt_list)
for idx, mess_elt in enumerate(elt_list):
try:
fwd_message_elt = self.get_message_from_result(
client, mess_elt, mam_req
)
except exceptions.DataError:
continue
try:
destinee = jid.JID(fwd_message_elt["to"])
except KeyError:
log.warning(_('missing "to" attribute in forwarded message'))
destinee = client.jid
if destinee.userhostJID() == client.jid.userhostJID():
# message to use, we insert the forwarded message in the normal
# workflow
client.xmlstream.dispatch(fwd_message_elt)
if not last_mess:
log.info(_("It seems that we have no MAM history yet"))
stanza_id = None
rsm_req = rsm.RSMRequest(max_=50, before="")
else:
# this message should be from us, we just add it to history
stanza_id = last_mess[0][-1]["stanza_id"]
if rsm_req is None:
rsm_req = rsm.RSMRequest(max_=100, after=stanza_id)
mam_req = mam.MAMRequest(rsm_=rsm_req)
assert mam_req.rsm is not None
complete = False
count = 0
while not complete:
try:
mam_data = await self.get_archives(
client, mam_req, service=client.jid.userhostJID()
)
except StanzaError as e:
log.warning(
f"Can't retrieve MAM archives: {e}\n"
f"{xml_tools.pp_elt(mam_req.toElement())}\n"
f"{xml_tools.pp_elt(e.iq.toElement()) if e.iq else ''}"
)
return
except Exception as e:
log.exception(f"Can't retrieve retrieve MAM archive")
return
elt_list, rsm_response, mam_response = mam_data
assert rsm_response is not None
complete = mam_response["complete"]
# we update MAM request for next iteration
mam_req.rsm.after = rsm_response.last
# before may be set if we had no previous history
mam_req.rsm.before = None
if not elt_list:
break
else:
count += len(elt_list)
for idx, mess_elt in enumerate(elt_list):
try:
from_jid = jid.JID(fwd_message_elt["from"])
except KeyError:
log.warning(_('missing "from" attribute in forwarded message'))
from_jid = client.jid
if from_jid.userhostJID() != client.jid.userhostJID():
log.warning(
_(
"was expecting a message sent by our jid, but this one if "
"from {from_jid}, ignoring\n{xml}"
).format(from_jid=from_jid.full(), xml=mess_elt.toXml())
fwd_message_elt = self.get_message_from_result(
client, mess_elt, mam_req
)
except exceptions.DataError:
continue
# adding message to history
mess_data = client.messageProt.parse_message(fwd_message_elt)
try:
await client.messageProt.add_to_history(mess_data)
except exceptions.CancelError as e:
log.warning(
"message has not been added to history: {e}".format(e=e)
)
except Exception as e:
log.error(
"can't add message to history: {e}\n{xml}".format(
e=e, xml=mess_elt.toXml()
)
)
if complete and idx == len(elt_list) - 1:
# We are at the last message from archive, we store the ID to not
# ask again the same messages next time.
destinee = jid.JID(fwd_message_elt["to"])
except KeyError:
log.warning(_('missing "to" attribute in forwarded message'))
destinee = client.jid
if destinee.userhostJID() == client.jid.userhostJID():
# message to use, we insert the forwarded message in the normal
# workflow
client.xmlstream.dispatch(fwd_message_elt)
else:
# this message should be from us, we just add it to history
try:
stanza_id = mess_elt.result["id"]
await self.host.memory.storage.set_private_value(
namespace=mam.NS_MAM,
key=KEY_LAST_STANZA_ID,
value=stanza_id,
profile=client.profile,
from_jid = jid.JID(fwd_message_elt["from"])
except KeyError:
log.warning(
_('missing "from" attribute in forwarded message')
)
except Exception:
log.exception("Can't store last stanza ID")
from_jid = client.jid
if from_jid.userhostJID() != client.jid.userhostJID():
log.warning(
_(
"was expecting a message sent by our jid, but this "
"one if from {from_jid}, ignoring\n{xml}"
).format(from_jid=from_jid.full(), xml=mess_elt.toXml())
)
continue
# use normal message flow for consistency
client.messageProt.onMessage(fwd_message_elt)
if not count:
log.info(_("We have received no message while offline"))
else:
log.info(
_("We have received {num_mess} message(s) while offline.").format(
num_mess=count
# We are at the last page, we store the ID to not ask again
# the same messages next time.
if complete and elt_list:
try:
last_result_elt = elt_list[-1].result
assert last_result_elt is not None
stanza_id = last_result_elt["id"]
await self.host.memory.storage.set_private_value(
namespace=mam.NS_MAM,
key=KEY_LAST_STANZA_ID,
value=stanza_id,
profile=client.profile,
)
except Exception:
log.exception("Can't store last stanza ID")
if not count:
log.info(_("We have received no message while offline"))
else:
log.info(
_("We have received {num_mess} message(s) while offline.").format(
num_mess=count
)
)
)
self.sync_completed(entity_jid, client.profile, count)
def profile_connected(self, client):
except Exception as e:
log.exception(f"MAM history sync failed for {entity_jid}")
self.sync_error(entity_jid, client.profile, str(e))
finally:
client.messageProt.flush_archive_batch()
def profile_connected(self, client: SatXMPPEntity) -> None:
defer.ensureDeferred(self.resume(client))
def get_handler(self, client):
mam_client = client._mam = LiberviaMAMClient(self)
def get_handler(self, client: SatXMPPEntity) -> "LiberviaMAMClient":
mam_client = LiberviaMAMClient(self)
client.set_plugin_data(IMPORT_NAME, mam_client)
return mam_client
def parse_extra(self, extra, with_rsm=True):
def parse_extra(
self, extra: dict[str, Any], with_rsm: bool = True
) -> mam.MAMRequest | None:
"""Parse extra dictionnary to retrieve MAM arguments
@param extra(dict): data for parse
@ -269,16 +337,21 @@ class XEP_0313(object):
return mam.MAMRequest(**mam_args) if mam_args else None
def get_message_from_result(self, client, mess_elt, mam_req, service=None):
def get_message_from_result(
self,
client: SatXMPPEntity,
mess_elt: domish.Element,
mam_req: mam.MAMRequest,
service: jid.JID | None = None,
) -> domish.Element:
"""Extract usable <message/> from MAM query result
The message will be validated, and stanza-id/delay will be added if necessary.
@param mess_elt(domish.Element): result <message/> element wrapping the message
to retrieve
@param mam_req(mam.MAMRequest): request used (needed to get query_id)
@param service(jid.JID, None): MAM service where the request has been sent
@param mess_elt: result <message/> element wrapping the message to retrieve
@param mam_req: request used (needed to get query_id)
@param service: MAM service where the request has been sent
None if it's user server
@return domish.Element): <message/> that can be used directly with onMessage
@return: <message/> that can be used directly with onMessage
"""
if mess_elt.name != "message":
log.warning(
@ -315,9 +388,8 @@ class XEP_0313(object):
else:
if not result_elt["queryid"] == mam_req.query_id:
log.error(
"Unexpected query id (was expecting {query_id}): {xml}".format(
query_id=mam.query_id, xml=mess_elt.toXml()
)
f"Unexpected query id (was expecting {mam_req.query_id}): "
f"{mess_elt.toXml()}"
)
raise exceptions.DataError("Invalid element")
stanza_id = self._sid.get_stanza_id(fwd_message_elt, service_jid)
@ -345,31 +417,69 @@ class XEP_0313(object):
return fwd_message_elt
def queryFields(self, client, service=None):
def queryFields(
self,
client: SatXMPPEntity,
service: jid.JID | None = None,
) -> defer.Deferred[data_form.Form | None]:
"""Ask the server about supported fields.
@param client: The client session.
@param service: entity offering the MAM service (None for user archives)
@return (D(data_form.Form)): form with the implemented fields (cf XEP-0313 §4.1.5)
@return: data Form with the implemented fields (cf XEP-0313 §4.1.5), or None
"""
return client._mam.queryFields(service)
mam_client = client.get_plugin_data(IMPORT_NAME, LiberviaMAMClient)
return mam_client.queryFields(service)
def queryArchive(self, client, mam_req, service=None):
def queryArchive(
self,
client: SatXMPPEntity,
mam_req: mam.MAMRequest,
service: jid.JID | None = None,
) -> defer.Deferred[domish.Element]:
"""Query a user, MUC or pubsub archive.
@param mam_req(mam.MAMRequest): MAM query instance
@param service(jid.JID, None): entity offering the MAM service
@param client: The client session.
@param mam_req: MAM query instance
@param service: entity offering the MAM service
None for user server
@return (D(domish.Element)): <IQ/> result
@return: <IQ/> result
"""
return client._mam.queryArchive(mam_req, service)
mam_client = client.get_plugin_data(IMPORT_NAME, LiberviaMAMClient)
return mam_client.queryArchive(mam_req, service)
def _append_message(self, elt_list, message_cb, message_elt):
def _append_message(
self,
elt_list: list[domish.Element],
message_cb: Callable[[domish.Element], domish.Element] | None,
message_elt: domish.Element,
) -> None:
"""Append a message element to the list, optionally transforming it via callback.
@param elt_list: List to append elements to.
@param message_cb: Optional callback to transform the element before appending.
@param message_elt: The message element to append.
"""
if message_cb is not None:
elt_list.append(message_cb(message_elt))
else:
elt_list.append(message_elt)
def _query_finished(self, iq_result, client, elt_list, event):
def _query_finished(
self,
iq_result: domish.Element,
client: SatXMPPEntity,
elt_list: list[domish.Element],
event: str,
) -> tuple[list[domish.Element], rsm.RSMResponse | None, dict[str, bool]]:
"""Handle the completion of a MAM query.
@param iq_result: The IQ result element.
@param client: The client session.
@param elt_list: List of collected message elements.
@param event: The event xpath string.
@return: Tuple of element list, RSM response, and MAM response dict.
"""
client.xmlstream.removeObserver(event, self._append_message)
try:
fin_elt = next(iq_result.elements(mam.NS_MAM, "fin"))
@ -388,20 +498,43 @@ class XEP_0313(object):
return (elt_list, rsm_response, mam_response)
def serialize_archive_result(self, data, client, mam_req, service):
async def serialize_archive_result(
self,
client: SatXMPPEntity,
data: tuple[list[domish.Element], rsm.RSMResponse | None, dict[str, bool]],
mam_req: mam.MAMRequest,
service: jid.JID | None,
) -> tuple[list[dict[str, Any]], str, str]:
"""Serialize MAM archive result for bridge communication.
@param client: The client session.
@param data: Tuple of element list, RSM response, and MAM response.
@param mam_req: The MAM request used.
@param service: The MAM service JID.
@return: Tuple of message list, serialized metadata, and profile key.
"""
elt_list, rsm_response, mam_response = data
mess_list = []
mess_list: list[dict[str, Any]] = []
for elt in elt_list:
fwd_message_elt = self.get_message_from_result(
client, elt, mam_req, service=service
)
mess_data = client.messageProt.parse_message(fwd_message_elt)
mess_data = await client.messageProt.parse_message(fwd_message_elt)
mess_list.append(client.message_get_bridge_args(mess_data))
metadata = {"rsm": self._rsm.response2dict(rsm_response), "mam": mam_response}
metadata: dict[str, Any] = {
"rsm": self._rsm.response2dict(rsm_response),
"mam": mam_response,
}
return mess_list, data_format.serialise(metadata), client.profile
def _get_archives(self, service, extra_ser, profile_key):
"""
def _get_archives(
self, service: str, extra_ser: str, profile_key: str
) -> defer.Deferred[tuple[list[dict[str, Any]], str, str]]:
"""Retrieve MAM archives via bridge.
@param service: Service JID string or empty string for user server.
@param extra_ser: Serialized extra parameters dict.
@param profile_key: Profile key string.
@return: tuple with:
- list of message with same data as in bridge.message_new
- response metadata with:
@ -410,23 +543,37 @@ class XEP_0313(object):
- profile
"""
client = self.host.get_client(profile_key)
service = jid.JID(service) if service else None
extra = data_format.deserialise(extra_ser, {})
service_jid: jid.JID | None = jid.JID(service) if service else None
extra: dict[str, Any] = data_format.deserialise(extra_ser, {})
mam_req = self.parse_extra(extra)
assert mam_req is not None
d = self.get_archives(client, mam_req, service=service)
d.addCallback(self.serialize_archive_result, client, mam_req, service)
d = self.get_archives(client, mam_req, service=service_jid)
d.addCallback(
lambda data: defer.ensureDeferred(
self.serialize_archive_result(client, data, mam_req, service_jid)
)
)
return d
def get_archives(self, client, query, service=None, message_cb=None):
"""Query archive and gather page result
def get_archives(
self,
client: SatXMPPEntity,
query: mam.MAMRequest,
service: jid.JID | None = None,
message_cb: Callable[[domish.Element], domish.Element] | None = None,
) -> defer.Deferred[
tuple[list[domish.Element], rsm.RSMResponse | None, dict[str, bool]]
]:
"""Query archive and gather page result.
@param query(mam.MAMRequest): MAM request
@param service(jid.JID, None): MAM service to use
@param client: The client session.
@param query: MAM request
@param service: MAM service to use
None to use our own server
@param message_cb(callable, None): callback to use on each message
@param message_cb: callback to use on each message
this method can be used to unwrap messages
@return (tuple[list[domish.Element], rsm.RSMResponse, dict): result data with:
@return: Deferred firing with result data:
- list of found elements
- RSM response
- MAM response, which is a dict with following value:
@ -435,55 +582,82 @@ class XEP_0313(object):
"""
if query.query_id is None:
query.query_id = str(uuid.uuid4())
elt_list = []
elt_list: list[domish.Element] = []
event = MESSAGE_RESULT.format(mam_ns=mam.NS_MAM, query_id=query.query_id)
client.xmlstream.addObserver(event, self._append_message, 0, elt_list, message_cb)
d = self.queryArchive(client, query, service)
d.addCallback(self._query_finished, client, elt_list, event)
return d
def get_prefs(self, client, service=None):
def get_prefs(
self,
client: SatXMPPEntity,
service: jid.JID | None = None,
) -> defer.Deferred[domish.Element]:
"""Retrieve the current user preferences.
@param client: The client session.
@param service: entity offering the MAM service (None for user archives)
@return: the server response as a Deferred domish.Element
@return: server response
"""
# http://xmpp.org/extensions/xep-0313.html#prefs
return client._mam.queryPrefs(service)
mam_client = client.get_plugin_data(IMPORT_NAME, LiberviaMAMClient)
return mam_client.queryPrefs(service)
def _set_prefs(
self,
service_s=None,
default="roster",
always=None,
never=None,
profile_key=C.PROF_KEY_NONE,
):
service = jid.JID(service_s) if service_s else None
always_jid = [jid.JID(entity) for entity in always]
never_jid = [jid.JID(entity) for entity in never]
service_s: str | None = None,
default: Literal["always", "never", "roster"] = "roster",
always: list[str] | None = None,
never: list[str] | None = None,
profile_key: str = C.PROF_KEY_NONE,
) -> defer.Deferred[domish.Element]:
"""Set new user preferences via bridge.
@param service_s: Service JID string or None for user server.
@param default: A value in ('always', 'never', 'roster').
@param always: List of JID strings to always archive.
@param never: List of JID strings to never archive.
@param profile_key: Profile key string.
@return: server response
"""
client = G.host.get_client(profile_key)
service: jid.JID | None = jid.JID(service_s) if service_s else None
always_jid: list[jid.JID] = [jid.JID(entity) for entity in (always or [])]
never_jid: list[jid.JID] = [jid.JID(entity) for entity in (never or [])]
# TODO: why not build here a MAMPrefs object instead of passing the args separately?
return self.setPrefs(service, default, always_jid, never_jid, profile_key)
return self.setPrefs(client, service, default, always_jid, never_jid)
def setPrefs(self, client, service=None, default="roster", always=None, never=None):
"""Set news user preferences.
def setPrefs(
self,
client: SatXMPPEntity,
service: jid.JID | None = None,
default: Literal["always", "never", "roster"] = "roster",
always: list[jid.JID] | None = None,
never: list[jid.JID] | None = None,
) -> defer.Deferred[domish.Element]:
"""Set new user preferences.
@param client: The client session.
@param service: entity offering the MAM service (None for user archives)
@param default (unicode): a value in ('always', 'never', 'roster')
@param always (list): a list of JID instances
@param never (list): a list of JID instances
@param profile_key (unicode): %(doc_profile_key)s
@return: the server response as a Deferred domish.Element
@param default: a value in ('always', 'never', 'roster')
@param always: a list of JID instances
@param never: a list of JID instances
@return: server response
"""
# http://xmpp.org/extensions/xep-0313.html#prefs
return client._mam.setPrefs(service, default, always, never)
mam_client = client.get_plugin_data(IMPORT_NAME, LiberviaMAMClient)
return mam_client.setPrefs(service, default, always, never)
def on_message_stanza_id(self, message_elt, client):
"""Called when a message with a stanza-id is received
def on_message_stanza_id(
self, message_elt: domish.Element, client: SatXMPPEntity
) -> None:
"""Called when a message with a stanza-id is received.
the messages' stanza ids are stored when received, so the last one can be used
to retrieve missing history on next connection
@param message_elt(domish.Element): <message> with a stanza-id
The messages' stanza ids are stored when received, so the last one can be used
to retrieve missing history on next connection.
@param message_elt: <message> with a stanza-id
@param client: The client session.
"""
if message_elt.getAttribute("type") == C.MESS_TYPE_GROUPCHAT:
# groupchat message MAM is handled by XEP-0045
@ -508,23 +682,36 @@ class XEP_0313(object):
@implementer(disco.IDisco)
class LiberviaMAMClient(mam.MAMClient):
def __init__(self, plugin_parent):
def __init__(self, plugin_parent: XEP_0313) -> None:
self.plugin_parent = plugin_parent
@property
def host(self):
return self.parent.host_app
def connectionInitialized(self):
observer_xpath = MESSAGE_STANZA_ID.format(
ns_stanza_id=self.host.ns_map["stanza_id"]
)
def connectionInitialized(self) -> None:
"""Set up observers when the XMPP connection is initialized."""
observer_xpath = MESSAGE_STANZA_ID.format(ns_stanza_id=G.host.ns_map["stanza_id"])
self.xmlstream.addObserver(
observer_xpath, self.plugin_parent.on_message_stanza_id, client=self.parent
)
def getDiscoInfo(self, requestor, target, nodeIdentifier=""):
def getDiscoInfo(
self, requestor: jid.JID, target: jid.JID, nodeIdentifier: str = ""
) -> list[disco.DiscoFeature]:
"""Get disco info for MAM
@param requestor: JID of the requesting entity
@param target: JID of the target entity
@param nodeIdentifier: optional node identifier
@return: list of disco features
"""
return [disco.DiscoFeature(mam.NS_MAM)]
def getDiscoItems(self, requestor, target, nodeIdentifier=""):
def getDiscoItems(
self, requestor: jid.JID, target: jid.JID, nodeIdentifier: str = ""
) -> list[disco.DiscoItem]:
"""Get disco items for MAM
@param requestor: JID of the requesting entity
@param target: JID of the target entity
@param nodeIdentifier: optional node identifier
@return: empty list (MAM doesn't have items)
"""
return []