Commit bcceae50 authored by Philipp Hörist's avatar Philipp Hörist

Add Annotations (XEP-0145) module

parent 54ea05ea
......@@ -65,6 +65,7 @@ from nbxmpp.modules.user_avatar import UserAvatar
from nbxmpp.modules.bookmarks import Bookmarks
from nbxmpp.modules.openpgp import OpenPGP
from nbxmpp.modules.omemo import OMEMO
from nbxmpp.modules.annotations import Annotations
from nbxmpp.modules.misc import unwrap_carbon
from nbxmpp.modules.misc import unwrap_mam
from nbxmpp.util import get_properties_struct
......@@ -197,6 +198,7 @@ class XMPPDispatcher(PlugIn):
self._modules['Bookmarks'] = Bookmarks(self._owner)
self._modules['OpenPGP'] = OpenPGP(self._owner)
self._modules['OMEMO'] = OMEMO(self._owner)
self._modules['Annotations'] = Annotations(self._owner)
for instance in self._modules.values():
for handler in instance.handlers:
# Copyright (C) 2019 Philipp Hörist <philipp AT>
# 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
# 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 <>.
import logging
from nbxmpp.protocol import NS_PRIVATE
from nbxmpp.protocol import NS_ROSTERNOTES
from nbxmpp.protocol import Iq
from nbxmpp.protocol import Node
from nbxmpp.protocol import isResultNode
from nbxmpp.structs import AnnotationNote
from nbxmpp.structs import CommonResult
from nbxmpp.modules.date_and_time import parse_datetime
from nbxmpp.util import validate_jid
from nbxmpp.util import call_on_response
from nbxmpp.util import callback
from nbxmpp.util import raise_error
log = logging.getLogger('nbxmpp.m.annotations')
class Annotations:
def __init__(self, client):
self._client = client
self.handlers = []
def domain(self):
return self._client.get_bound_jid().getDomain()
def request_annotations(self):'Request annotations for %s', self.domain)
payload = Node('storage', attrs={'xmlns': NS_ROSTERNOTES})
return Iq(typ='get', queryNS=NS_PRIVATE, payload=payload)
def _annotations_received(self, stanza):
if not isResultNode(stanza):
return raise_error(, stanza)
storage = stanza.getQueryChild('storage')
if storage is None:
return raise_error(log.warning, stanza, 'stanza-malformed',
'No annotations found')
nodes = storage.getTags('note')
if not nodes:
return raise_error(, stanza, 'is-empty',
'No annotations found')
notes = []
for note in nodes:
jid = validate_jid(note.getAttr('jid'))
except Exception:
log.warning('Invalid JID: %s, ignoring it',
cdate = note.getAttr('cdate')
if cdate is not None:
cdate = parse_datetime(cdate, epoch=True)
mdate = note.getAttr('mdate')
if mdate is not None:
mdate = parse_datetime(mdate, epoch=True)
data = note.getData()
notes.append(AnnotationNote(jid=jid, cdate=cdate,
mdate=mdate, data=data))'Received annotations from %s:', self.domain)
for note in notes:
return notes
def set_annotations(self, notes):'Set annotations for %s:', self.domain)
for note in notes:
storage = Node('storage', attrs={'xmlns': NS_ROSTERNOTES})
for note in notes:
node = Node('note', attrs={'jid': note.jid})
if note.cdate is not None:
node.setAttr('cdate', note.cdate)
if note.mdate is not None:
node.setAttr('mdate', note.mdate)
return Iq(typ='set', queryNS=NS_PRIVATE, payload=storage)
def _default_response(self, stanza):
if not isResultNode(stanza):
return raise_error(, stanza)
return CommonResult(jid=stanza.getFrom())
......@@ -95,6 +95,9 @@ PGPKeyMetadata = namedtuple('PGPKeyMetadata', 'jid fingerprint date')
OMEMOMessage = namedtuple('OMEMOMessage', 'sid iv keys payload')
AnnotationNote = namedtuple('AnnotationNote', 'jid data cdate mdate')
AnnotationNote.__new__.__defaults__ = (None, None)
class OMEMOBundle(namedtuple('OMEMOBundle', 'spk spk_signature ik otpks')):
def pick_prekey(self):
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment