Philipp Hörist pushed to branch master at gajim / python-nbxmpp
Commits: 520beb18 by lovetox at 2020-03-27T16:02:11+01:00 Add RSM (XEP-0059) support
- - - - - b0038b28 by lovetox at 2020-03-28T07:44:16+01:00 Add MAM (XEP-0313) support
- - - - - 80ec5753 by lovetox at 2020-03-28T08:08:57+01:00 TCP: Initiate new read before data-received signal
- - - - -
5 changed files:
- nbxmpp/dispatcher.py - + nbxmpp/modules/mam.py - + nbxmpp/modules/rsm.py - nbxmpp/structs.py - nbxmpp/tcp.py
Changes:
===================================== nbxmpp/dispatcher.py ===================================== @@ -76,6 +76,7 @@ from nbxmpp.modules.security_labels import SecurityLabels from nbxmpp.modules.chatstates import Chatstates from nbxmpp.modules.register import Register from nbxmpp.modules.http_upload import HTTPUpload +from nbxmpp.modules.mam import MAM from nbxmpp.modules.misc import unwrap_carbon from nbxmpp.modules.misc import unwrap_mam from nbxmpp.structs import StanzaTimeoutError @@ -180,6 +181,7 @@ class StanzaDispatcher(Observable): self._modules['Chatstates'] = Chatstates(self._client) self._modules['Register'] = Register(self._client) self._modules['HTTPUpload'] = HTTPUpload(self._client) + self._modules['MAM'] = MAM(self._client)
for instance in self._modules.values(): for handler in instance.handlers:
===================================== nbxmpp/modules/mam.py ===================================== @@ -0,0 +1,209 @@ +# Copyright (C) 2020 Philipp Hörist <philipp AT hoerist.com> +# +# This file is part of nbxmpp. +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 3 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; If not, see http://www.gnu.org/licenses/. + + +from nbxmpp.protocol import JID +from nbxmpp.protocol import Iq +from nbxmpp.protocol import isResultNode +from nbxmpp.protocol import Node +from nbxmpp.protocol import NS_MAM_2 +from nbxmpp.protocol import NS_RSM +from nbxmpp.structs import MAMQueryData +from nbxmpp.structs import MAMPreferencesData +from nbxmpp.structs import CommonResult +from nbxmpp.util import call_on_response +from nbxmpp.util import callback +from nbxmpp.util import raise_error +from nbxmpp.modules.rsm import parse_rsm +from nbxmpp.modules.dataforms import SimpleDataForm +from nbxmpp.modules.dataforms import create_field +from nbxmpp.modules.base import BaseModule + + +class MAM(BaseModule): + def __init__(self, client): + BaseModule.__init__(self, client) + + self._client = client + self.handlers = [] + + @call_on_response('_query_result') + def make_query(self, + jid, + queryid, + start=None, + end=None, + with_=None, + after=None, + max_=70): + + iq = Iq(typ='set', to=jid, queryNS=NS_MAM_2) + iq.getQuery().setAttr('queryid', queryid) + + payload = [ + self._make_query_form(start, end, with_), + self._make_rsm_query(max_, after) + ] + + iq.setQueryPayload(payload) + return iq + + @staticmethod + def _make_query_form(start, end, with_): + fields = [ + create_field(typ='hidden', var='FORM_TYPE', value=NS_MAM_2) + ] + + if start: + fields.append(create_field( + typ='text-single', + var='start', + value=start.strftime('%Y-%m-%dT%H:%M:%SZ'))) + + if end: + fields.append(create_field( + typ='text-single', + var='end', + value=end.strftime('%Y-%m-%dT%H:%M:%SZ'))) + + if with_: + fields.append(create_field( + typ='jid-single', + var='with', + value=with_)) + + return SimpleDataForm(type_='submit', fields=fields) + + @staticmethod + def _make_rsm_query(max_, after): + rsm_set = Node('set', attrs={'xmlns': NS_RSM}) + if max_ is not None: + rsm_set.setTagData('max', max_) + if after is not None: + rsm_set.setTagData('after', after) + return rsm_set + + @callback + def _query_result(self, stanza): + if not isResultNode(stanza): + return raise_error(self._log.info, stanza) + + jid = stanza.getFrom() + fin = stanza.getTag('fin', namespace=NS_MAM_2) + if fin is None: + return raise_error(self._log.warning, + stanza, + 'stanza-malformed', + 'No fin node found') + + rsm = parse_rsm(fin) + if rsm is None: + return raise_error(self._log.warning, + stanza, + 'stanza-malformed', + 'rsm set missing') + + complete = bool(fin.getAttr('complete')) + + return MAMQueryData(jid=jid, + complete=complete, + rsm=rsm) + + @call_on_response('_preferences_result') + def request_preferences(self): + iq = Iq('get', queryNS=NS_MAM_2) + iq.setQuery('prefs') + return iq + + @callback + def _preferences_result(self, stanza): + if not isResultNode(stanza): + return raise_error(self._log.info, stanza) + + prefs = stanza.getTag('prefs', namespace=NS_MAM_2) + if prefs is None: + return raise_error(self._log.warning, + stanza, + 'stanza-malformed', + 'No prefs node found') + + default = prefs.getAttr('default') + if default is None: + return raise_error(self._log.warning, + stanza, + 'stanza-malformed', + 'No default attr found') + + always_node = prefs.getTag('always') + if always_node is None: + return raise_error(self._log.warning, + stanza, + 'stanza-malformed', + 'No always node found') + + always = self._get_preference_jids(always_node) + + never_node = prefs.getTag('never') + if never_node is None: + return raise_error(self._log.warning, + stanza, + 'stanza-malformed', + 'No never node found') + + never = self._get_preference_jids(never_node) + return MAMPreferencesData(default=default, + always=always, + never=never) + + def _get_preference_jids(self, node): + jids = [] + for item in node.getTags('jid'): + jid = item.getData() + if not jid: + continue + + try: + jid = JID(jid) + except Exception: + self._log.warning('Invalid jid found in preferences: %s', + jid) + jids.append(jid) + return jids + + @call_on_response('_default_response') + def set_preferences(self, default, always, never): + if default not in ('always', 'never', 'roster'): + raise ValueError('Wrong default preferences type') + + iq = Iq(typ='set') + prefs = iq.addChild(name='prefs', + namespace=NS_MAM_2, + attrs={'default': default}) + always_node = prefs.addChild(name='always') + never_node = prefs.addChild(name='never') + for jid in always: + always_node.addChild(name='jid').setData(jid) + + for jid in never: + never_node.addChild(name='jid').setData(jid) + return iq + + @callback + def _default_response(self, stanza): + if not isResultNode(stanza): + return raise_error(self._log.info, stanza) + return CommonResult(jid=stanza.getFrom())
===================================== nbxmpp/modules/rsm.py ===================================== @@ -0,0 +1,62 @@ +# Copyright (C) 2020 Philipp Hörist <philipp AT hoerist.com> +# +# This file is part of nbxmpp. +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 3 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; If not, see http://www.gnu.org/licenses/. + +from nbxmpp.protocol import NS_RSM + +from nbxmpp.structs import RSMData + + +def parse_rsm(stanza): + set_ = stanza.getTag('set', namespace=NS_RSM) + if set_ is None: + return None + + after = stanza.getTagData('after') or None + before = stanza.getTagData('before') or None + last = stanza.getTagData('last') or None + + first_index = None + first = stanza.getTagData('first') or None + if first is not None: + try: + first_index = int(first.getAttr('index')) + except Exception: + pass + + try: + count = int(stanza.getTagData('count')) + except Exception: + count = None + + try: + max_ = int(stanza.getTagData('max')) + except Exception: + max_ = None + + try: + index = int(stanza.getTagData('index')) + except Exception: + index = None + + return RSMData(after=after, + before=before, + last=last, + first=first, + first_index=first_index, + count=count, + max=max_, + index=index)
===================================== nbxmpp/structs.py ===================================== @@ -144,6 +144,12 @@ ChangePasswordResult.__new__.__defaults__ = (None,) HTTPUploadData = namedtuple('HTTPUploadData', 'put_uri get_uri headers') HTTPUploadData.__new__.__defaults__ = (None,)
+RSMData = namedtuple('RSMData', 'after before last first first_index count max index') + +MAMQueryData = namedtuple('MAMQueryData', 'jid rsm complete') + +MAMPreferencesData = namedtuple('MAMPreferencesData', 'default always never') +
class DiscoInfo(namedtuple('DiscoInfo', 'stanza identities features dataforms timestamp')):
===================================== nbxmpp/tcp.py ===================================== @@ -236,13 +236,13 @@ class TCPConnection(Connection):
self._log_stanza(data, received=True)
+ self._read_async() + try: self.notify('data-received', data) except Exception: self._log.exception('Error while executing data-received:')
- self._read_async() - def _write_stanzas(self): self._write_stanza_buffer = self._write_queue self._write_queue = deque([])
View it on GitLab: https://dev.gajim.org/gajim/python-nbxmpp/-/compare/ea902b3b0e7afe0bf16910ff...