From 65636b8cce3cfc3d2bae7e5156819c8ad9500293 Mon Sep 17 00:00:00 2001 From: nicoco Date: Fri, 4 Nov 2022 06:20:11 +0100 Subject: [PATCH] implements XEP-0292 (vCard4 over XMPP) --- docs/api/plugins/xep_0292.rst | 17 +++ slixmpp/plugins/__init__.py | 1 + slixmpp/plugins/xep_0292/__init__.py | 5 + slixmpp/plugins/xep_0292/stanza.py | 167 +++++++++++++++++++++++++++ slixmpp/plugins/xep_0292/vcard4.py | 111 ++++++++++++++++++ tests/test_stanza_xep_0292.py | 118 +++++++++++++++++++ 6 files changed, 419 insertions(+) create mode 100644 docs/api/plugins/xep_0292.rst create mode 100644 slixmpp/plugins/xep_0292/__init__.py create mode 100644 slixmpp/plugins/xep_0292/stanza.py create mode 100644 slixmpp/plugins/xep_0292/vcard4.py create mode 100644 tests/test_stanza_xep_0292.py diff --git a/docs/api/plugins/xep_0292.rst b/docs/api/plugins/xep_0292.rst new file mode 100644 index 00000000..3c6502f3 --- /dev/null +++ b/docs/api/plugins/xep_0292.rst @@ -0,0 +1,17 @@ + +XEP-0292: vCard4 Over XMPP +========================== + +.. module:: slixmpp.plugins.xep_0292 + +.. autoclass:: XEP_0292 + :members: + :exclude-members: plugin_init, plugin_end + + +Stanza elements +--------------- + +.. automodule:: slixmpp.plugins.xep_0292.stanza + :members: + :undoc-members: diff --git a/slixmpp/plugins/__init__.py b/slixmpp/plugins/__init__.py index ac7482ee..325cc1c4 100644 --- a/slixmpp/plugins/__init__.py +++ b/slixmpp/plugins/__init__.py @@ -79,6 +79,7 @@ __all__ = [ # 'xep_0270', # XMPP Compliance Suites 2010. Don’t automatically load 'xep_0279', # Server IP Check 'xep_0280', # Message Carbons + 'xep_0292', # vCard4 Over XMPP 'xep_0297', # Stanza Forwarding 'xep_0300', # Use of Cryptographic Hash Functions in XMPP # 'xep_0302', # XMPP Compliance Suites 2012. Don’t automatically load diff --git a/slixmpp/plugins/xep_0292/__init__.py b/slixmpp/plugins/xep_0292/__init__.py new file mode 100644 index 00000000..b2fc85d8 --- /dev/null +++ b/slixmpp/plugins/xep_0292/__init__.py @@ -0,0 +1,5 @@ +from slixmpp.plugins.base import register_plugin + +from . import stanza, vcard4 + +register_plugin(vcard4.XEP_0292) diff --git a/slixmpp/plugins/xep_0292/stanza.py b/slixmpp/plugins/xep_0292/stanza.py new file mode 100644 index 00000000..e7f2fd98 --- /dev/null +++ b/slixmpp/plugins/xep_0292/stanza.py @@ -0,0 +1,167 @@ +import datetime +from typing import Optional + +from slixmpp import ElementBase, Iq, register_stanza_plugin + +NS = "urn:ietf:params:xml:ns:vcard-4.0" + + +class _VCardElementBase(ElementBase): + namespace = NS + + +class VCard4(_VCardElementBase): + name = plugin_attrib = "vcard" + interfaces = {"full_name", "given", "surname", "birthday"} + + def set_full_name(self, full_name: str): + self["fn"]["text"] = full_name + + def get_full_name(self): + return self["fn"]["text"] + + def set_given(self, given: str): + self["n"]["given"] = given + + def get_given(self): + return self["n"]["given"] + + def set_surname(self, surname: str): + self["n"]["surname"] = surname + + def get_surname(self): + return self["n"]["surname"] + + def set_birthday(self, birthday: datetime.date): + self["bday"]["date"] = birthday + + def get_birthday(self): + return self["bday"]["date"] + + def add_tel(self, number: str, name: Optional[str] = None): + tel = Tel() + if name: + tel["parameters"]["type_"]["text"] = name + tel["uri"] = f"tel:{number}" + self.append(tel) + + def add_address( + self, country: Optional[str] = None, locality: Optional[str] = None + ): + adr = Adr() + if locality: + adr["locality"] = locality + if country: + adr["country"] = country + self.append(adr) + + def add_nickname(self, nick: str): + el = Nickname() + el["text"] = nick + self.append(el) + + def add_note(self, note: str): + el = Note() + el["text"] = note + self.append(el) + + def add_impp(self, impp: str): + el = Impp() + el["uri"] = impp + self.append(el) + + def add_url(self, url: str): + el = Url() + el["uri"] = url + self.append(el) + + def add_email(self, email: str): + el = Email() + el["text"] = email + self.append(el) + + +class _VCardTextElementBase(_VCardElementBase): + interfaces = {"text"} + sub_interfaces = {"text"} + + +class Fn(_VCardTextElementBase): + name = plugin_attrib = "fn" + + +class Nickname(_VCardTextElementBase): + name = plugin_attrib = "nickname" + + +class Note(_VCardTextElementBase): + name = plugin_attrib = "note" + + +class _VCardUriElementBase(_VCardElementBase): + interfaces = {"uri"} + sub_interfaces = {"uri"} + + +class Url(_VCardUriElementBase): + name = plugin_attrib = "url" + + +class Impp(_VCardUriElementBase): + name = plugin_attrib = "impp" + + +class Email(_VCardTextElementBase): + name = plugin_attrib = "email" + + +class N(_VCardElementBase): + name = "n" + plugin_attrib = "n" + interfaces = sub_interfaces = {"given", "surname", "additional"} + + +class BDay(_VCardElementBase): + name = plugin_attrib = "bday" + interfaces = {"date"} + + def set_date(self, date: datetime.date): + d = Date() + d.xml.text = date.strftime("%Y-%m-%d") + self.append(d) + + def get_date(self): + for elem in self.xml: + try: + return datetime.date.fromisoformat(elem.text) + except ValueError: + return None + + +class Date(_VCardElementBase): + name = "date" + + +class Tel(_VCardUriElementBase): + name = plugin_attrib = "tel" + + +class Parameters(_VCardElementBase): + name = plugin_attrib = "parameters" + + +class Type(_VCardTextElementBase): + name = "type" + plugin_attrib = "type_" + + +class Adr(_VCardElementBase): + name = plugin_attrib = "adr" + interfaces = sub_interfaces = {"locality", "country"} + + +register_stanza_plugin(Parameters, Type) +register_stanza_plugin(Tel, Parameters) +for p in N, Fn, Nickname, Note, Url, Impp, Email, BDay, Tel, Adr: + register_stanza_plugin(VCard4, p, iterable=True) +register_stanza_plugin(Iq, VCard4) diff --git a/slixmpp/plugins/xep_0292/vcard4.py b/slixmpp/plugins/xep_0292/vcard4.py new file mode 100644 index 00000000..123d66f7 --- /dev/null +++ b/slixmpp/plugins/xep_0292/vcard4.py @@ -0,0 +1,111 @@ +import logging +from datetime import date +from typing import Optional + +from slixmpp import ( + JID, + ComponentXMPP, + register_stanza_plugin, +) +from slixmpp.plugins.base import BasePlugin + +from . import stanza + + +class XEP_0292(BasePlugin): + """ + vCard4 over XMPP + + Does not implement the IQ semantics that neither movim does gajim implement, + cf https://xmpp.org/extensions/xep-0292.html#self-iq-retrieval and + https://xmpp.org/extensions/xep-0292.html#self-iq-publication + + Does not implement the "empty pubsub event item" as a notification mechanism, + that neither gajim nor movim implement + https://xmpp.org/extensions/xep-0292.html#sect-idm45744791178720 + + Relies on classic pubsub semantics instead. + """ + xmpp: ComponentXMPP + + name = "xep_0292" + description = "vCard4 Over XMPP" + dependencies = {"xep_0163", "xep_0060", "xep_0030"} + stanza = stanza + + def plugin_init(self): + pubsub_stanza = self.xmpp["xep_0060"].stanza + + register_stanza_plugin(pubsub_stanza.Item, stanza.VCard4) + register_stanza_plugin(pubsub_stanza.EventItem, stanza.VCard4) + + self.xmpp['xep_0060'].map_node_event(stanza.NS, 'vcard4') + + def plugin_end(self): + self.xmpp['xep_0030'].del_feature(feature=stanza.NS) + self.xmpp['xep_0163'].remove_interest(stanza.NS) + + def session_bind(self, jid): + self.xmpp['xep_0163'].register_pep('vcard4', stanza.VCard4) + + def publish_vcard( + self, + full_name: Optional[str] = None, + given: Optional[str] = None, + surname: Optional[str] = None, + birthday: Optional[date] = None, + nickname: Optional[str] = None, + phone: Optional[str] = None, + note: Optional[str] = None, + url: Optional[str] = None, + email: Optional[str] = None, + country: Optional[str] = None, + locality: Optional[str] = None, + impp: Optional[str] = None, + **pubsubkwargs, + ): + """ + Publish a vcard using PEP + """ + vcard = stanza.VCard4() + + if impp: + vcard.add_impp(impp) + + if nickname: + vcard.add_nickname(nickname) + if full_name: + vcard["full_name"] = full_name + + if given: + vcard["given"] = given + if surname: + vcard["surname"] = surname + if birthday: + vcard["birthday"] = birthday + + if note: + vcard.add_note(note) + if url: + vcard.add_url(url) + if email: + vcard.add_email(email) + if phone: + vcard.add_tel(phone) + if country and locality: + vcard.add_address(country, locality) + elif country: + vcard.add_address(country, locality) + + return self.xmpp["xep_0163"].publish(vcard, id="current", **pubsubkwargs) + + def retrieve_vcard(self, jid: JID, **pubsubkwargs): + """ + Retrieve a vcard using PEP + """ + return self.xmpp["xep_0060"].get_item( + jid, stanza.VCard4.namespace, "current", **pubsubkwargs + ) + + +log = logging.getLogger(__name__) diff --git a/tests/test_stanza_xep_0292.py b/tests/test_stanza_xep_0292.py new file mode 100644 index 00000000..8b2467fb --- /dev/null +++ b/tests/test_stanza_xep_0292.py @@ -0,0 +1,118 @@ +import datetime + +from slixmpp import Iq +from slixmpp.test import SlixTest + +from slixmpp.plugins.xep_0292 import stanza + + +REF = """ + + + + Full Name + + FullName + + some nick + + + 1984-05-21 + + + https://nicoco.fr + + + About me + + + xmpp:test@localhost + + + test@gmail.com + + + + work + + tel:+555 + + + Nice + France + + + +""" + + +class TestVcard(SlixTest): + def test_basic_interfaces(self): + iq = Iq() + x = iq["vcard"] + + x["fn"]["text"] = "Full Name" + x["nickname"]["text"] = "some nick" + x["n"]["given"] = "Full" + x["n"]["surname"] = "Name" + x["bday"]["date"] = datetime.date(1984, 5, 21) + x["note"]["text"] = "About me" + x["url"]["uri"] = "https://nicoco.fr" + x["impp"]["uri"] = "xmpp:test@localhost" + x["email"]["text"] = "test@gmail.com" + + x["tel"]["uri"] = "tel:+555" + x["tel"]["parameters"]["type_"]["text"] = "work" + x["adr"]["locality"] = "Nice" + x["adr"]["country"] = "France" + + self.check(iq, REF, use_values=False) + + def test_easy_interface(self): + iq = Iq() + x: stanza.VCard4 = iq["vcard"] + + x["full_name"] = "Full Name" + x["given"] = "Full" + x["surname"] = "Name" + x["birthday"] = datetime.date(1984, 5, 21) + x.add_nickname("some nick") + x.add_note("About me") + x.add_url("https://nicoco.fr") + x.add_impp("xmpp:test@localhost") + x.add_email("test@gmail.com") + x.add_tel("+555", "work") + x.add_address("France", "Nice") + + self.check(iq, REF, use_values=False) + + def test_2_phones(self): + vcard = stanza.VCard4() + tel1 = stanza.Tel() + tel1["parameters"]["type_"]["text"] = "work" + tel1["uri"] = "tel:+555" + tel2 = stanza.Tel() + tel2["parameters"]["type_"]["text"] = "devil" + tel2["uri"] = "tel:+666" + vcard.append(tel1) + vcard.append(tel2) + self.check( + vcard, + """ + + + + work + + tel:+555 + + + + devil + + tel:+666 + + + """, + use_values=False + )