diff --git a/nbxmpp/dispatcher.py b/nbxmpp/dispatcher.py index 678f26fd0cd6997b6352701badf8fac5207eda81..f2dc86abd8c00209b04731decea54777a51671a0 100644 --- a/nbxmpp/dispatcher.py +++ b/nbxmpp/dispatcher.py @@ -76,6 +76,7 @@ from nbxmpp.modules.register import Register from nbxmpp.modules.http_upload import HTTPUpload from nbxmpp.modules.mam import MAM from nbxmpp.modules.vcard_temp import VCardTemp +from nbxmpp.modules.vcard4 import VCard4 from nbxmpp.modules.misc import unwrap_carbon from nbxmpp.modules.misc import unwrap_mam from nbxmpp.structs import StanzaTimeoutError @@ -182,6 +183,7 @@ class StanzaDispatcher(Observable): self._modules['HTTPUpload'] = HTTPUpload(self._client) self._modules['MAM'] = MAM(self._client) self._modules['VCardTemp'] = VCardTemp(self._client) + self._modules['VCard4'] = VCard4(self._client) for instance in self._modules.values(): for handler in instance.handlers: diff --git a/nbxmpp/modules/base.py b/nbxmpp/modules/base.py index 952cf1bb39f1037f214934f846433bd8c8bc4536..4ea6bc6c76d8e28292df37543d3043e838812ba6 100644 --- a/nbxmpp/modules/base.py +++ b/nbxmpp/modules/base.py @@ -31,7 +31,7 @@ class BaseModule: def __getattr__(self, name): if name not in self._depends: - raise AttributeError + raise AttributeError('Unknown method: %s' % name) module = self._client.get_module(self._depends[name]) return getattr(module, name) diff --git a/nbxmpp/modules/pubsub.py b/nbxmpp/modules/pubsub.py index 5bac95df7e9a04ad9b80a7635df4cda1b0e9b9ec..f4d9be757994ecb13bda397b4d57b8b8a76fd666 100644 --- a/nbxmpp/modules/pubsub.py +++ b/nbxmpp/modules/pubsub.py @@ -23,11 +23,14 @@ from nbxmpp.errors import PubSubStanzaError from nbxmpp.errors import MalformedStanzaError from nbxmpp.structs import StanzaHandler from nbxmpp.structs import PubSubEventData +from nbxmpp.structs import CommonResult from nbxmpp.protocol import Iq from nbxmpp.protocol import Node from nbxmpp.namespaces import Namespace from nbxmpp.modules.base import BaseModule from nbxmpp.modules.util import process_response +from nbxmpp.modules.util import raise_if_error +from nbxmpp.modules.util import finalize from nbxmpp.modules.dataforms import extend_form @@ -141,6 +144,47 @@ class PubSub(BaseModule): item_id = _get_published_item_id(response, node, id_) yield PubSubPublishResult(jid, node, item_id) + @iq_request_task + def get_access_model(self, node): + _task = yield + + self._log.info('Request access model') + + result = yield self.get_node_configuration(node) + + raise_if_error(result) + + yield result.form['pubsub#access_model'].value + + @iq_request_task + def set_access_model(self, node, model): + task = yield + + if model not in ('open', 'presence'): + raise ValueError('Invalid access model') + + result = yield self.get_node_configuration(node) + + raise_if_error(result) + + try: + access_model = result.form['pubsub#access_model'].value + except Exception: + yield task.set_error('warning', + condition='access-model-not-supported') + + if access_model == model: + jid = self._client.get_bound_jid().new_as_bare() + yield CommonResult(jid=jid) + + result.form['pubsub#access_model'].value = model + + self._log.info('Set access model %s', model) + + result = yield self.set_node_configuration(node, result.form) + + yield finalize(task, result) + @iq_request_task def retract(self, node, id_, jid=None, notify=True): _task = yield diff --git a/nbxmpp/modules/user_avatar.py b/nbxmpp/modules/user_avatar.py index 1e976c4aa449cb25705a608274fa20e3b942f5c0..a42c964857718ac18f4feedf67878ac13d555c96 100644 --- a/nbxmpp/modules/user_avatar.py +++ b/nbxmpp/modules/user_avatar.py @@ -27,7 +27,6 @@ from nbxmpp.namespaces import Namespace from nbxmpp.protocol import NodeProcessed from nbxmpp.protocol import Node from nbxmpp.structs import StanzaHandler -from nbxmpp.structs import CommonResult from nbxmpp.util import b64encode from nbxmpp.util import b64decode from nbxmpp.errors import MalformedStanzaError @@ -40,8 +39,6 @@ from nbxmpp.modules.util import finalize class UserAvatar(BaseModule): _depends = { - 'get_node_configuration': 'PubSub', - 'set_node_configuration': 'PubSub', 'publish': 'PubSub', 'request_item': 'PubSub', } @@ -160,48 +157,6 @@ class UserAvatar(BaseModule): yield finalize(task, result) - @iq_request_task - def get_access_model(self): - _task = yield - - self._log.info('Request access model') - - result = yield self.get_node_configuration(Namespace.AVATAR_DATA) - - raise_if_error(result) - - yield result.form['pubsub#access_model'].value - - @iq_request_task - def set_access_model(self, model): - task = yield - - if model not in ('open', 'presence'): - raise ValueError('Invalid access model') - - result = yield self.get_node_configuration(Namespace.AVATAR_DATA) - - raise_if_error(result) - - try: - access_model = result.form['pubsub#access_model'].value - except Exception: - yield task.set_error('warning', - condition='access-model-not-supported') - - if access_model == model: - jid = self._client.get_bound_jid().new_as_bare() - yield CommonResult(jid=jid) - - result.form['pubsub#access_model'].value = model - - self._log.info('Set access model %s', model) - - result = yield self.set_node_configuration(Namespace.AVATAR_DATA, - result.form) - - yield finalize(task, result) - def _get_avatar_data(item, id_): data_node = item.getTag('data', namespace=Namespace.AVATAR_DATA) diff --git a/nbxmpp/modules/vcard4.py b/nbxmpp/modules/vcard4.py new file mode 100644 index 0000000000000000000000000000000000000000..cf72e33563b8c9d7d29fe331dc8c34c80f50c222 --- /dev/null +++ b/nbxmpp/modules/vcard4.py @@ -0,0 +1,971 @@ + +from typing import List +from typing import Optional + +from dataclasses import dataclass +from dataclasses import field + +from nbxmpp.simplexml import Node +from nbxmpp.namespaces import Namespace + +from nbxmpp.errors import MalformedStanzaError +from nbxmpp.task import iq_request_task +from nbxmpp.modules.base import BaseModule +from nbxmpp.modules.util import raise_if_error +from nbxmpp.modules.util import finalize + + +ALLOWED_SEX_VALUES = ['M', 'F', 'O', 'N', 'U'] +ALLOWED_KIND_VALUES = ['individual', 'group', 'org', 'location'] + +# Cardinality +# 1 Exactly one instance per vCard MUST be present. +# *1 Exactly one instance per vCard MAY be present. +# 1* One or more instances per vCard MUST be present. +# * One or more instances per vCard MAY be present. + +PROPERTY_DEFINITION = { + 'source': (['altid', 'pid', 'pref', 'mediatype'], '*'), + 'kind': ([], '*1'), + 'fn': (['language', 'altid', 'pid', 'pref', 'type'], '1*'), + 'n': (['language', 'altid', 'sort-as'], '*1'), + 'nickname': (['language', 'altid', 'pid', 'pref', 'type'], '*'), + 'photo': (['altid', 'pid', 'pref', 'type', 'mediatype'], '*'), + 'bday': (['altid', 'calscale'], '*1'), + 'anniversary': (['altid', 'calscale'], '*1'), + 'gender': ([], '*1'), + 'adr': (['language', 'altid', 'pid', 'pref', 'type', 'geo', 'tz', 'label'], '*'), + 'tel': (['altid', 'pid', 'pref', 'type', 'mediatype'], '*'), + 'email': (['altid', 'pid', 'pref', 'type'], '*'), + 'impp': (['altid', 'pid', 'pref', 'type', 'mediatype'], '*'), + 'lang': (['altid', 'pid', 'pref', 'type'], '*'), + 'tz': (['altid', 'pid', 'pref', 'type', 'mediatype'], '*'), + 'geo': (['altid', 'pid', 'pref', 'type', 'mediatype'], '*'), + 'title': (['language', 'altid', 'pid', 'pref', 'type'], '*'), + 'role': (['language', 'altid', 'pid', 'pref', 'type'], '*'), + 'logo': (['language', 'altid', 'pid', 'pref', 'type', 'mediatype'], '*'), + 'org': (['language', 'altid', 'pid', 'pref', 'type', 'sort-as'], '*'), + 'member': (['altid', 'pid', 'pref', 'mediatype'], '*'), + 'related': (['altid', 'pid', 'pref', 'type', 'mediatype'], '*'), + 'categories': (['altid', 'pid', 'pref', 'type'], '*'), + 'note': (['language', 'altid', 'pid', 'pref', 'type'], '*'), + 'prodid': ([], '*1'), + 'rev': ([], '*1'), + 'sound': (['language', 'altid', 'pid', 'pref', 'type', 'mediatype'], '*'), + 'uid': ([], '*1'), + 'clientpidmap': ([], '*'), + 'url': (['altid', 'pid', 'pref', 'type', 'mediatype'], '*'), + 'key': (['altid', 'pid', 'pref', 'type', 'mediatype'], '*'), + 'fburl': (['altid', 'pid', 'pref', 'type', 'mediatype'], '*'), + 'caladruri': (['altid', 'pid', 'pref', 'type', 'mediatype'], '*'), + 'caluri': (['altid', 'pid', 'pref', 'type', 'mediatype'], '*'), +} + + +PROPERTY_VALUE_TYPES = { + 'bday': ['date', 'time', 'date-time', 'text'], + 'anniversary': ['date', 'time', 'date-time', 'text'], + 'key': ['text', 'uri'], + 'tel': ['uri', 'text'], + 'tz': ['text', 'uri', 'utc-offset'], + 'related': ['text', 'uri'], +} + + +def get_data_from_children(node, child_name): + values = [] + child_nodes = node.getTags(child_name) + for child_node in child_nodes: + child_value = child_node.getData() + if child_value: + values.append(child_value) + return values + + +def add_children(node, child_name, values): + for value in values: + node.addchild(child_name, payload=value) + + +def get_multiple_type_value(node, types): + for type_ in types: + value = node.getTagData(type_) + if value: + return type_, value + + raise ValueError('no value found') + + +def make_parameters(parameters): + parameters_node = Node('parameters') + for parameter in parameters: + parameters_node.addChild(node=parameter.to_node()) + return parameters_node + + +def get_parameters(node): + name = node.getName() + definition = PROPERTY_DEFINITION[name] + allowed_parameters = definition[0] + parameters_node = node.getTag('parameters') + if parameters_node is None: + return [] + + parameters = [] + for parameter in allowed_parameters: + parameter_node = parameters_node.getTag(parameter) + if parameter_node is None: + continue + + parameter_class = PARAMETER_CLASSES.get(parameter_node.getName()) + if parameter_class is None: + continue + + parameter = parameter_class.from_node(parameter_node) + parameters.append(parameter) + + return parameters + + +@dataclass +class Parameter: + + name: str + type: str + value: str + + @classmethod + def from_node(cls, node): + name = node.getName() + if name != cls.name: + raise ValueError(f'invalid parameter name: {name}') + + value = node.getTagData(cls.type) + if not value: + raise ValueError('no parameter value found') + + return cls(value) + + def to_node(self): + node = Node(self.name) + node.addChild(self.type, payload=self.value) + return node + + +@dataclass +class MultiParameter: + + name: str + type: str + values: str + + @classmethod + def from_node(cls, node): + name = node.getName() + if name != cls.name: + raise ValueError(f'invalid parameter name: {name}') + + value_nodes = node.getTags(cls.type) + if not value_nodes: + raise ValueError('no parameter value found') + + values = [] + for value_node in value_nodes: + value = value_node.getData() + if value: + values.append(value) + + if not values: + raise ValueError('no parameter value found') + + return cls(values) + + def to_node(self): + node = Node(self.name) + for value in self.values: + node.addChild(self.type, payload=value) + return node + + +@dataclass +class LanguageParameter(Parameter): + + name: str = field(default='language', init=False) + type: str = field(default='language-tag', init=False) + + +@dataclass +class PrefParameter(Parameter): + + name: str = field(default='pref', init=False) + type: str = field(default='integer', init=False) + + +@dataclass +class AltidParameter(Parameter): + + name: str = field(default='altid', init=False) + type: str = field(default='text', init=False) + + +@dataclass +class PidParameter(MultiParameter): + + name: str = field(default='pid', init=False) + type: str = field(default='text', init=False) + + +@dataclass +class TypeParameter(MultiParameter): + + name: str = field(default='type', init=False) + type: str = field(default='text', init=False) + + +@dataclass +class MediatypeParameter(Parameter): + + name: str = field(default='mediatype', init=False) + type: str = field(default='text', init=False) + + +@dataclass +class CalscaleParameter(Parameter): + + name: str = field(default='calscale', init=False) + type: str = field(default='text', init=False) + + +@dataclass +class SortasParameter(MultiParameter): + + name: str = field(default='sort-as', init=False) + type: str = field(default='text', init=False) + + +@dataclass +class GeoParameter(Parameter): + + name: str = field(default='geo', init=False) + type: str = field(default='uri', init=False) + + +@dataclass +class TzParameter: + + name: str = field(default='tz', init=False) + value_type: str + value: str + + @classmethod + def from_node(cls, node): + name = node.getName() + if name != cls.name: + raise ValueError(f'invalid property name: {name}') + + value_type, value = get_multiple_type_value(node, ['text', 'uri']) + return cls(value_type, value) + + def to_node(self): + node = Node(self.name) + node.addChild(self.value_type, payload=self.value) + return node + + +PARAMETER_CLASSES = { + 'language': LanguageParameter, + 'pref': PrefParameter, + 'altid': AltidParameter, + 'pid': PidParameter, + 'type': TypeParameter, + 'mediatype': MediatypeParameter, + 'calscale': CalscaleParameter, + 'sort-as': SortasParameter, + 'geo': GeoParameter, + 'tz': TzParameter +} + + +@dataclass +class UriProperty: + + name: str + value: str + parameters: List[Parameter] = field(default_factory=list) + + @classmethod + def from_node(cls, node): + name = node.getName() + if name != cls.name: + raise ValueError(f'invalid property name: {name}') + + value = node.getTagData('uri') + if not value: + raise ValueError('no value found') + + parameters = get_parameters(node) + + return cls(value, parameters) + + def to_node(self): + node = Node(self.name) + if self.parameters: + node.addChild(node=make_parameters(self.parameters)) + node.addChild('uri', payload=self.value) + return node + + +@dataclass +class TextProperty: + + name: str + value: str + parameters: List[Parameter] = field(default_factory=list) + + @classmethod + def from_node(cls, node): + name = node.getName() + if name != cls.name: + raise ValueError(f'invalid property name: {name}') + + text = node.getTagData('text') + if not text: + raise ValueError('no value found') + + parameters = get_parameters(node) + + return cls(text, parameters) + + def to_node(self): + node = Node(self.name) + if self.parameters: + node.addChild(node=make_parameters(self.parameters)) + node.addChild('text', payload=self.value) + return node + + +@dataclass +class TextListProperty: + + name: str + values: List[str] + parameters: List[Parameter] = field(default_factory=list) + + @classmethod + def from_node(cls, node): + name = node.getName() + if name != cls.name: + raise ValueError(f'invalid property name: {name}') + + text_nodes = node.getTags('text') + if not text_nodes: + raise ValueError('no value found') + + values = get_data_from_children(node, 'text') + + parameters = get_parameters(node) + + return cls(values, parameters) + + def to_node(self): + node = Node(self.name) + if self.parameters: + node.addChild(node=make_parameters(self.parameters)) + add_children(node, 'text', self.values) + return node + + +@dataclass +class MultipleValueProperty: + + name: str + value_type: str + value: str + parameters: List[Parameter] = field(default_factory=list) + + @classmethod + def from_node(cls, node): + name = node.getName() + if name != cls.name: + raise ValueError(f'invalid property name: {name}') + + types = PROPERTY_VALUE_TYPES[cls.name] + value_type, value = get_multiple_type_value(node, types) + + parameters = get_parameters(node) + + return cls(value_type, value, parameters) + + def to_node(self): + node = Node(self.name) + if self.parameters: + node.addChild(node=make_parameters(self.parameters)) + node.addChild(self.value_type, payload=self.value) + return node + + +@dataclass +class SourceProperty(UriProperty): + + name: str = field(default='source', init=False) + + +@dataclass +class KindProperty(TextProperty): + + name: str = field(default='kind', init=False) + + @classmethod + def from_node(cls, node): + name = node.getName() + if name != cls.name: + raise ValueError(f'invalid property name: {name}') + + text = node.getTagData('text') + if not text: + raise ValueError('no value found') + + if text not in ALLOWED_KIND_VALUES: + text = 'individual' + + parameters = get_parameters(node) + + return cls(text, parameters) + + +@dataclass +class FnProperty(TextProperty): + + name: str = field(default='fn', init=False) + + +@dataclass +class NProperty: + + name: str = field(default='n', init=False) + surname: List[str] = field(default_factory=list) + given: List[str] = field(default_factory=list) + additional: List[str] = field(default_factory=list) + prefix: List[str] = field(default_factory=list) + suffix: List[str] = field(default_factory=list) + parameters: List[Parameter] = field(default_factory=list) + + @classmethod + def from_node(cls, node): + name = node.getName() + if name != cls.name: + raise ValueError(f'invalid property name: {name}') + + surname = get_data_from_children(node, 'surname') + given = get_data_from_children(node, 'given') + additional = get_data_from_children(node, 'additional') + prefix = get_data_from_children(node, 'prefix') + suffix = get_data_from_children(node, 'suffix') + + parameters = get_parameters(node) + + return cls(surname, given, additional, prefix, suffix, parameters) + + def to_node(self): + node = Node(self.name) + if self.parameters: + node.addChild(node=make_parameters(self.parameters)) + add_children(node, 'surname', self.surname) + add_children(node, 'given', self.given) + add_children(node, 'additional', self.additional) + add_children(node, 'prefix', self.prefix) + add_children(node, 'suffix', self.suffix) + return node + + +@dataclass +class NicknameProperty(TextListProperty): + + name: str = field(default='nickname', init=False) + + +@dataclass +class PhotoProperty(UriProperty): + + name: str = field(default='photo', init=False) + + +@dataclass +class BDayProperty(MultipleValueProperty): + + name: str = field(default='bday', init=False) + + +@dataclass +class AnniversaryProperty(MultipleValueProperty): + + name: str = field(default='anniversary', init=False) + + +@dataclass +class GenderProperty: + + name: str = field(default='gender', init=False) + sex: Optional[str] = None + identity: Optional[str] = None + parameters: List[Parameter] = field(default_factory=list) + + @classmethod + def from_node(cls, node): + name = node.getName() + if name != cls.name: + raise ValueError(f'invalid property name: {name}') + + sex = node.getTagData('sex') + if sex not in ALLOWED_SEX_VALUES: + sex = None + + identity = node.getTagData('identity') + if not identity: + identity = None + + parameters = get_parameters(node) + + return cls(sex, identity, parameters) + + def to_node(self): + node = Node(self.name) + if self.parameters: + node.addChild(node=make_parameters(self.parameters)) + if self.sex: + node.addChild('sex', payload=self.sex) + if self.identity: + node.addChild('identity', payload=self.sex) + return node + + +@dataclass +class AdrProperty: + + name: str = field(default='adr', init=False) + pobox: List[str] = field(default_factory=list) + ext: List[str] = field(default_factory=list) + street: List[str] = field(default_factory=list) + locality: List[str] = field(default_factory=list) + region: List[str] = field(default_factory=list) + code: List[str] = field(default_factory=list) + country: List[str] = field(default_factory=list) + parameters: List[Parameter] = field(default_factory=list) + + @classmethod + def from_node(cls, node): + name = node.getName() + if name != cls.name: + raise ValueError(f'invalid property name: {name}') + + pobox = get_data_from_children(node, 'pobox') + ext = get_data_from_children(node, 'ext') + street = get_data_from_children(node, 'street') + locality = get_data_from_children(node, 'locality') + region = get_data_from_children(node, 'region') + code = get_data_from_children(node, 'code') + country = get_data_from_children(node, 'country') + + parameters = get_parameters(node) + + return cls(pobox, ext, street, locality, + region, code, country, parameters) + + def to_node(self): + node = Node(self.name) + if self.parameters: + node.addChild(node=make_parameters(self.parameters)) + add_children(node, 'pobox', self.pobox) + add_children(node, 'ext', self.ext) + add_children(node, 'street', self.street) + add_children(node, 'locality', self.locality) + add_children(node, 'region', self.region) + add_children(node, 'code', self.code) + add_children(node, 'country', self.country) + return node + + +@dataclass +class TelProperty(MultipleValueProperty): + + name: str = field(default='tel', init=False) + + +@dataclass +class EmailProperty(TextProperty): + + name: str = field(default='email', init=False) + + +@dataclass +class ImppProperty(UriProperty): + + name: str = field(default='impp', init=False) + + +@dataclass +class LangProperty: + + name: str = field(default='lang', init=False) + value: str + parameters: List[Parameter] = field(default_factory=list) + + @classmethod + def from_node(cls, node): + name = node.getName() + if name != cls.name: + raise ValueError(f'invalid property name: {name}') + + value = node.getTagData('language-tag') + if not value: + raise ValueError('no value found') + + parameters = get_parameters(node) + + return cls(value, parameters) + + def to_node(self): + node = Node(self.name) + if self.parameters: + node.addChild(node=make_parameters(self.parameters)) + node.addChild('language-tag', payload=self.value) + return node + + +@dataclass +class TzProperty(MultipleValueProperty): + + name: str = field(default='tz', init=False) + + +@dataclass +class GeoProperty(UriProperty): + + name: str = field(default='geo', init=False) + + +@dataclass +class TitleProperty(TextProperty): + + name: str = field(default='title', init=False) + + +@dataclass +class RoleProperty(TextProperty): + + name: str = field(default='role', init=False) + + +@dataclass +class LogoProperty(UriProperty): + + name: str = field(default='logo', init=False) + + +@dataclass +class OrgProperty(TextListProperty): + + name: str = field(default='org', init=False) + + +@dataclass +class MemberProperty(UriProperty): + + name: str = field(default='member', init=False) + + +@dataclass +class RelatedProperty(MultipleValueProperty): + + name: str = field(default='related', init=False) + + +@dataclass +class CategoriesProperty(TextListProperty): + + name: str = field(default='org', init=False) + + +@dataclass +class NoteProperty(TextProperty): + + name: str = field(default='note', init=False) + + +@dataclass +class ProdidProperty(TextProperty): + + name: str = field(default='prodid', init=False) + + +@dataclass +class RevProperty(TextProperty): + + name: str = field(default='rev', init=False) + + @classmethod + def from_node(cls, node): + name = node.getName() + if name != cls.name: + raise ValueError(f'invalid property name: {name}') + + timestamp = node.getTagData('timestamp') + if not timestamp: + raise ValueError('no value found') + + parameters = get_parameters(node) + + return cls(timestamp, parameters) + + def to_node(self): + node = Node(self.name) + if self.parameters: + node.addChild(node=make_parameters(self.parameters)) + node.addChild('timestamp', payload=self.value) + return node + + +@dataclass +class SoundProperty(UriProperty): + + name: str = field(default='sound', init=False) + + +@dataclass +class UidProperty(UriProperty): + + name: str = field(default='uid', init=False) + + +@dataclass +class ClientpidmapProperty: + + name: str = field(default='clientpidmap', init=False) + sourceid: int + uri: str + parameters: List[Parameter] = field(default_factory=list) + + @classmethod + def from_node(cls, node): + name = node.getName() + if name != cls.name: + raise ValueError(f'invalid property name: {name}') + + sourceid = node.getTagData('sourceid') + if not sourceid: + raise ValueError('no value found') + + uri = node.getTagData('uri') + if not uri: + raise ValueError('no value found') + + parameters = get_parameters(node) + + return cls(sourceid, uri, parameters) + + def to_node(self): + node = Node(self.name) + if self.parameters: + node.addChild(node=make_parameters(self.parameters)) + node.addChild('sourceid', payload=self.sourceid) + node.addChild('uri', payload=self.uri) + return node + + +@dataclass +class UrlProperty(UriProperty): + + name: str = field(default='url', init=False) + + +@dataclass +class KeyProperty(MultipleValueProperty): + + name: str = field(default='key', init=False) + + +@dataclass +class FBurlProperty(UriProperty): + + name: str = field(default='fburl', init=False) + + +@dataclass +class CaladruriProperty(UriProperty): + + name: str = field(default='caladruri', init=False) + + +@dataclass +class CaluriProperty(UriProperty): + + name: str = field(default='calurl', init=False) + + +PROPERTY_CLASSES = { + 'source': SourceProperty, + 'kind': KindProperty, + 'fn': FnProperty, + 'n': NProperty, + 'nickname': NicknameProperty, + 'photo': PhotoProperty, + 'bday': BDayProperty, + 'anniversary': AnniversaryProperty, + 'gender': GenderProperty, + 'adr': AdrProperty, + 'tel': TelProperty, + 'email': EmailProperty, + 'impp': ImppProperty, + 'lang': LangProperty, + 'tz': TzProperty, + 'geo': GeoProperty, + 'title': TitleProperty, + 'role': RoleProperty, + 'logo': LogoProperty, + 'org': OrgProperty, + 'member': MemberProperty, + 'related': RelatedProperty, + 'categories': CategoriesProperty, + 'note': NoteProperty, + 'prodid': ProdidProperty, + 'rev': RevProperty, + 'sound': SoundProperty, + 'uid': UidProperty, + 'clientpidmap': ClientpidmapProperty, + 'url': UrlProperty, + 'key': KeyProperty, + 'fburl': FBurlProperty, + 'caladruri': CaladruriProperty, + 'caluri': CaluriProperty, +} + + +def get_property_from_name(name, node): + property_class = PROPERTY_CLASSES.get(name) + if property_class is None: + return None + return property_class.from_node(node) + + +class VCard: + def __init__(self, properties=None): + if properties is None: + properties = [] + self._properties = properties + + @classmethod + def from_node(cls, node): + properties = [] + for child in node.getChildren(): + child_name = child.getName() + + if child_name == 'group': + group_name = child.getAttr('name') + if not group_name: + continue + + group_properties = [] + for group_child in child.getChildren(): + group_child_name = group_child.getName() + property_ = get_property_from_name(group_child_name, + group_child) + if property_ is None: + continue + group_properties.append(property_) + + properties.append((group_name, group_properties)) + + else: + + property_ = get_property_from_name(child_name, child) + if property_ is None: + continue + properties.append((None, property_)) + + return cls(properties) + + def to_node(self): + vcard = Node(f'{Namespace.VCARD4} vcard') + for group, props in self._properties: + if group is None: + vcard.addChild(node=props.to_node()) + else: + group = Node(group) + for prop in props: + group.addChild(node=prop.to_node()) + vcard.addChild(node=group) + return vcard + + def get_properties(self): + properties = [] + for group, props in self._properties: + if group is None: + properties.append(props) + else: + properties.extend(props) + return properties + + def add_property(self, name, *args, **kwargs): + prop = PROPERTY_CLASSES.get(name)(*args, **kwargs) + self._properties.append((None, prop)) + + +class VCard4(BaseModule): + + _depends = { + 'publish': 'PubSub', + 'request_items': 'PubSub', + } + + def __init__(self, client): + BaseModule.__init__(self, client) + + self._client = client + self.handlers = [] + + @iq_request_task + def request_vcard(self, jid=None): + task = yield + + items = yield self.request_items(Namespace.VCARD4_PUBSUB, + jid=jid, + max_items=1) + + raise_if_error(items) + + if not items: + yield task.set_result(None) + + yield _get_vcard(items[0]) + + @iq_request_task + def set_vcard(self, vcard, public=False): + task = yield + + access_model = 'open' if public else 'presence' + + options = { + 'pubsub#persist_items': 'true', + 'pubsub#access_model': access_model, + } + + result = yield self.publish(Namespace.VCARD4_PUBSUB, + vcard.to_node(), + id_='current', + options=options, + force_node_options=True) + + yield finalize(task, result) + + +def _get_vcard(item): + vcard = item.getTag('vcard', namespace=Namespace.VCARD4) + if vcard is None: + raise MalformedStanzaError('vcard node missing', item) + + try: + vcard = VCard.from_node(vcard) + except Exception: + raise MalformedStanzaError('invalid vcard', item) + + return vcard diff --git a/nbxmpp/namespaces.py b/nbxmpp/namespaces.py index 046717db07e9b77c5401259d920de21454d47b48..bdd62624b34d4a30a958d7fb3615b3d7e5dda102 100644 --- a/nbxmpp/namespaces.py +++ b/nbxmpp/namespaces.py @@ -154,6 +154,8 @@ class _Namespaces: VCARD: str = 'vcard-temp' VCARD_UPDATE: str = 'vcard-temp:x:update' VCARD_CONVERSION: str = 'urn:xmpp:pep-vcard-conversion:0' + VCARD4: str = 'urn:ietf:params:xml:ns:vcard-4.0' + VCARD4_PUBSUB: str = 'urn:xmpp:vcard4' VERSION: str = 'jabber:iq:version' XHTML_IM: str = 'http://jabber.org/protocol/xhtml-im' XHTML: str = 'http://www.w3.org/1999/xhtml'