Mercurial > libervia-backend
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 |
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 [] |