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
+ )