comparison sat/plugins/plugin_xep_0292.py @ 3819:4f02e339d184

plugin XEP-0292: vCard4 Over XMPP implementation: implementation of vCard4, the dedicated <IQ/> protocol is not implemented because it's obsoleted (see comments). Fow now, only `fn`, `nickname` and `note` are handled. rel 368
author Goffi <goffi@goffi.org>
date Wed, 29 Jun 2022 11:58:00 +0200 (2022-06-29)
parents
children 524856bd7b19
comparison
equal deleted inserted replaced
3818:2863345c9bbb 3819:4f02e339d184
1 #!/usr/bin/env python3
2
3 # Libervia plugin for XEP-0292
4 # Copyright (C) 2009-2022 Jérôme Poisson (goffi@goffi.org)
5
6 # This program is free software: you can redistribute it and/or modify
7 # it under the terms of the GNU Affero General Public License as published by
8 # the Free Software Foundation, either version 3 of the License, or
9 # (at your option) any later version.
10
11 # This program is distributed in the hope that it will be useful,
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 # GNU Affero General Public License for more details.
15
16 # You should have received a copy of the GNU Affero General Public License
17 # along with this program. If not, see <http://www.gnu.org/licenses/>.
18
19 from typing import List, Dict, Union, Any, Optional
20 from functools import partial
21
22 from twisted.words.protocols.jabber.xmlstream import XMPPHandler
23 from twisted.words.protocols.jabber import jid
24 from twisted.words.xish import domish
25 from zope.interface import implementer
26 from wokkel import disco, iwokkel
27
28 from sat.core.constants import Const as C
29 from sat.core.i18n import _
30 from sat.core.log import getLogger
31 from sat.core.core_types import SatXMPPEntity
32 from sat.core import exceptions
33 from sat.tools.common.async_utils import async_lru
34
35
36 log = getLogger(__name__)
37
38 IMPORT_NAME = "XEP-0292"
39
40 PLUGIN_INFO = {
41 C.PI_NAME: "vCard4 Over XMPP",
42 C.PI_IMPORT_NAME: IMPORT_NAME,
43 C.PI_TYPE: C.PLUG_TYPE_XEP,
44 C.PI_MODES: C.PLUG_MODE_BOTH,
45 C.PI_PROTOCOLS: ["XEP-0292"],
46 C.PI_DEPENDENCIES: ["IDENTITY", "XEP-0060", "XEP-0163"],
47 C.PI_MAIN: "XEP_0292",
48 C.PI_HANDLER: "yes",
49 C.PI_DESCRIPTION: _("""XEP-0292 (vCard4 Over XMPP) implementation"""),
50 }
51
52 NS_VCARD4 = "urn:ietf:params:xml:ns:vcard-4.0"
53 VCARD4_NODE = "urn:xmpp:vcard4"
54 text_fields = {
55 "fn": "name",
56 "nickname": "nicknames",
57 "note": "description"
58 }
59 text_fields_inv = {v: k for k,v in text_fields.items()}
60
61
62 class XEP_0292:
63 namespace = NS_VCARD4
64 node = VCARD4_NODE
65
66 def __init__(self, host):
67 # XXX: as of XEP-0292 v0.11, there is a dedicated <IQ/> protocol in this XEP which
68 # should be used according to the XEP. Hovewer it feels like an outdated behaviour
69 # and other clients don't seem to use it. After discussing it on xsf@ MUC, it
70 # seems that implemeting the dedicated <IQ/> protocol is a waste of time, and thus
71 # it is not done here. It is expected that this dedicated protocol will be removed
72 # from a future version of the XEP.
73 log.info(_("vCard4 Over XMPP initialization"))
74 host.registerNamespace("vcard4", NS_VCARD4)
75 self.host = host
76 self._p = host.plugins["XEP-0060"]
77 self._i = host.plugins['IDENTITY']
78 self._i.register(
79 IMPORT_NAME,
80 'nicknames',
81 partial(self.getValue, field="nicknames"),
82 partial(self.setValue, field="nicknames"),
83 priority=1000
84 )
85 self._i.register(
86 IMPORT_NAME,
87 'description',
88 partial(self.getValue, field="description"),
89 partial(self.setValue, field="description"),
90 priority=1000
91 )
92
93 def getHandler(self, client):
94 return XEP_0292_Handler()
95
96 def vcard2Dict(self, vcard_elt: domish.Element) -> Dict[str, Any]:
97 """Convert vcard element to equivalent identity metadata"""
98 vcard: Dict[str, Any] = {}
99
100 for metadata_elt in vcard_elt.elements():
101 # Text values
102 for source_field, dest_field in text_fields.items():
103 if metadata_elt.name == source_field:
104 if metadata_elt.text is not None:
105 dest_type = self._i.getFieldType(dest_field)
106 value = str(metadata_elt.text)
107 if dest_type is str:
108 if dest_field in vcard:
109 vcard[dest_field] += value
110 else:
111 vcard[dest_field] = value
112 elif dest_type is list:
113 vcard.setdefault(dest_field, []).append(value)
114 else:
115 raise exceptions.InternalError(
116 f"unexpected dest_type: {dest_type!r}"
117 )
118 break
119 else:
120 log.debug(
121 f"Following element is currently unmanaged: {metadata_elt.toXml()}"
122 )
123 return vcard
124
125 def dict2VCard(self, vcard: dict[str, Any]) -> domish.Element:
126 """Convert vcard metadata to vCard4 element"""
127 vcard_elt = domish.Element((NS_VCARD4, "vcard"))
128 for field, elt_name in text_fields_inv.items():
129 value = vcard.get(field)
130 if value:
131 if isinstance(value, str):
132 value = [value]
133 if isinstance(value, list):
134 for v in value:
135 field_elt = vcard_elt.addElement(elt_name)
136 field_elt.addElement("text", content=v)
137 else:
138 log.warning(
139 f"ignoring unexpected value: {value!r}"
140 )
141
142 return vcard_elt
143
144 @async_lru(5)
145 async def getCard(self, client: SatXMPPEntity, entity: jid.JID) -> dict:
146 try:
147 items, metadata = await self._p.getItems(
148 client, entity, VCARD4_NODE, item_ids=["current"]
149 )
150 except exceptions.NotFound:
151 log.info(f"No vCard node found for {entity}")
152 return {}
153 item_elt = items[0]
154 try:
155 vcard_elt = next(item_elt.elements(NS_VCARD4, "vcard"))
156 except StopIteration:
157 log.info(f"vCard element is not present for {entity}")
158 return {}
159
160 return self.vcard2Dict(vcard_elt)
161
162 async def updateVCardElt(
163 self,
164 client: SatXMPPEntity,
165 vcard_elt: domish.Element,
166 entity: Optional[jid.JID] = None
167 ) -> None:
168 """Update VCard 4 of given entity, create node if doesn't already exist
169
170 @param vcard_elt: whole vCard element to update
171 @param entity: entity for which the vCard must be updated
172 None to update profile's own vCard
173 """
174 service = entity or client.jid.userhostJID()
175 node_options = {
176 self._p.OPT_ACCESS_MODEL: self._p.ACCESS_OPEN,
177 self._p.OPT_PUBLISH_MODEL: self._p.PUBLISH_MODEL_PUBLISHERS
178 }
179 await self._p.createIfNewNode(client, service, VCARD4_NODE, node_options)
180 await self._p.sendItem(
181 client, service, VCARD4_NODE, vcard_elt, item_id=self._p.ID_SINGLETON
182 )
183
184 async def updateVCard(
185 self,
186 client: SatXMPPEntity,
187 vcard: Dict[str, Any],
188 entity: Optional[jid.JID] = None,
189 update: bool = True,
190 ) -> None:
191 """Update VCard 4 of given entity, create node if doesn't already exist
192
193 @param vcard: identity metadata
194 @param entity: entity for which the vCard must be updated
195 None to update profile's own vCard
196 @param update: if True, current vCard will be retrieved and updated with given
197 vcard (thus if False, `vcard` data will fully replace previous one).
198 """
199 service = entity or client.jid.userhostJID()
200 if update:
201 current_vcard = await self.getCard(client, service)
202 current_vcard.update(vcard)
203 vcard = current_vcard
204 vcard_elt = self.dict2VCard(vcard)
205 await self.updateVCardElt(client, vcard_elt, service)
206
207 async def getValue(
208 self,
209 client: SatXMPPEntity,
210 entity: jid.JID,
211 field: str,
212 ) -> Optional[Union[str, List[str]]]:
213 """Return generic value
214
215 @param entity: entity from who the vCard comes
216 @param field: name of the field to get
217 This has to be a string field
218 @return request value
219 """
220 vcard_data = await self.getCard(client, entity)
221 return vcard_data.get(field)
222
223 async def setValue(
224 self,
225 client: SatXMPPEntity,
226 value: Union[str, List[str]],
227 entity: jid.JID,
228 field: str
229 ) -> None:
230 """Set generic value
231
232 @param entity: entity from who the vCard comes
233 @param field: name of the field to get
234 This has to be a string field
235 """
236 await self.updateVCard(client, {field: value}, entity)
237
238
239 @implementer(iwokkel.IDisco)
240 class XEP_0292_Handler(XMPPHandler):
241
242 def getDiscoInfo(self, requestor, service, nodeIdentifier=""):
243 return [disco.DiscoFeature(NS_VCARD4)]
244
245 def getDiscoItems(self, requestor, service, nodeIdentifier=""):
246 return []