# Slixmpp: The Slick XMPP Library
# Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout
# This file is part of Slixmpp.
# See the file LICENSE for copying permission.
import logging
from asyncio import Future
from datetime import datetime, timedelta
from slixmpp.plugins import BasePlugin
from slixmpp import JID
from slixmpp.stanza import Iq
from slixmpp.exceptions import XMPPError
from slixmpp.xmlstream import register_stanza_plugin
from slixmpp.xmlstream.handler import CoroutineCallback
from slixmpp.xmlstream.matcher import StanzaPath
from slixmpp.plugins.xep_0012 import stanza, LastActivity
log = logging.getLogger(__name__)
[docs]
class XEP_0012(BasePlugin):
"""
XEP-0012 Last Activity
"""
name = 'xep_0012'
description = 'XEP-0012: Last Activity'
dependencies = {'xep_0030'}
stanza = stanza
def plugin_init(self):
register_stanza_plugin(Iq, LastActivity)
self._last_activities = {}
self.xmpp.register_handler(
CoroutineCallback('Last Activity',
StanzaPath('iq@type=get/last_activity'),
self._handle_get_last_activity))
self.api.register(self._default_get_last_activity,
'get_last_activity',
default=True)
self.api.register(self._default_set_last_activity,
'set_last_activity',
default=True)
self.api.register(self._default_del_last_activity,
'del_last_activity',
default=True)
def plugin_end(self):
self.xmpp.remove_handler('Last Activity')
self.xmpp['xep_0030'].del_feature(feature='jabber:iq:last')
def session_bind(self, jid):
self.xmpp['xep_0030'].add_feature('jabber:iq:last')
[docs]
def begin_idle(self, jid: JID | None = None, status: str | None = None) -> Future:
"""Reset the last activity for the given JID.
.. versionchanged:: 1.8.0
This function now returns a Future.
:param status: Optional status.
"""
return self.set_last_activity(jid, 0, status)
[docs]
def end_idle(self, jid: JID | None = None) -> Future:
"""Remove the last activity of a JID.
.. versionchanged:: 1.8.0
This function now returns a Future.
"""
return self.del_last_activity(jid)
[docs]
def start_uptime(self, status: str | None = None) -> Future:
"""
.. versionchanged:: 1.8.0
This function now returns a Future.
"""
return self.set_last_activity(None, 0, status)
[docs]
def set_last_activity(self, jid=None, seconds=None, status=None) -> Future:
"""Set last activity for a JID.
.. versionchanged:: 1.8.0
This function now returns a Future.
"""
return self.api['set_last_activity'](jid, args={
'seconds': seconds,
'status': status
})
[docs]
def del_last_activity(self, jid: JID) -> Future:
"""Remove the last activity of a JID.
.. versionchanged:: 1.8.0
This function now returns a Future.
"""
return self.api['del_last_activity'](jid)
[docs]
def get_last_activity(self, jid: JID, local: bool = False,
ifrom: JID | None = None, **iqkwargs) -> Future:
"""Get last activity for a specific JID.
:param local: Fetch the value from the local cache.
"""
if jid is not None and not isinstance(jid, JID):
jid = JID(jid)
if self.xmpp.is_component:
if jid.domain == self.xmpp.boundjid.domain:
local = True
else:
if str(jid) == str(self.xmpp.boundjid):
local = True
jid = jid.full
if local or jid in (None, ''):
log.debug("Looking up local last activity data for %s", jid)
return self.api['get_last_activity'](jid, None, ifrom, None)
iq = self.xmpp.make_iq_get(ito=jid, ifrom=ifrom)
iq.enable('last_activity')
return iq.send(**iqkwargs)
async def _handle_get_last_activity(self, iq: Iq):
log.debug("Received last activity query from " + \
"<%s> to <%s>.", iq['from'], iq['to'])
reply = await self.api['get_last_activity'](iq['to'], None, iq['from'], iq)
reply.send()
# =================================================================
# Default in-memory implementations for storing last activity data.
# =================================================================
def _default_set_last_activity(self, jid: JID, node: str, ifrom: JID, data: dict):
seconds = data.get('seconds', None)
if seconds is None:
seconds = 0
status = data.get('status', None)
if status is None:
status = ''
self._last_activities[jid] = {
'seconds': datetime.now() - timedelta(seconds=seconds),
'status': status}
def _default_del_last_activity(self, jid: JID, node: str, ifrom: JID, data: dict):
if jid in self._last_activities:
del self._last_activities[jid]
def _default_get_last_activity(self, jid: JID, node: str, ifrom: JID, iq: Iq) -> Iq:
if not isinstance(iq, Iq):
reply = self.xmpp.Iq()
else:
reply = iq.reply()
if jid not in self._last_activities:
raise XMPPError('service-unavailable')
bare = JID(jid).bare
if bare != self.xmpp.boundjid.bare:
if bare in self.xmpp.roster[jid]:
sub = self.xmpp.roster[jid][bare]['subscription']
if sub not in ('from', 'both'):
raise XMPPError('forbidden')
td = datetime.now() - self._last_activities[jid]['seconds']
seconds = td.seconds + td.days * 24 * 3600
status = self._last_activities[jid]['status']
reply['last_activity']['seconds'] = seconds
reply['last_activity']['status'] = status
return reply