Mercurial > libervia-backend
comparison libervia/backend/plugins/plugin_xep_0048.py @ 4071:4b842c1fb686
refactoring: renamed `sat` package to `libervia.backend`
author | Goffi <goffi@goffi.org> |
---|---|
date | Fri, 02 Jun 2023 11:49:51 +0200 |
parents | sat/plugins/plugin_xep_0048.py@524856bd7b19 |
children | 5f2d496c633f |
comparison
equal
deleted
inserted
replaced
4070:d10748475025 | 4071:4b842c1fb686 |
---|---|
1 #!/usr/bin/env python3 | |
2 | |
3 | |
4 # SAT plugin for Bookmarks (xep-0048) | |
5 # Copyright (C) 2009-2021 Jérôme Poisson (goffi@goffi.org) | |
6 | |
7 # This program is free software: you can redistribute it and/or modify | |
8 # it under the terms of the GNU Affero General Public License as published by | |
9 # the Free Software Foundation, either version 3 of the License, or | |
10 # (at your option) any later version. | |
11 | |
12 # This program is distributed in the hope that it will be useful, | |
13 # but WITHOUT ANY WARRANTY; without even the implied warranty of | |
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |
15 # GNU Affero General Public License for more details. | |
16 | |
17 # You should have received a copy of the GNU Affero General Public License | |
18 # along with this program. If not, see <http://www.gnu.org/licenses/>. | |
19 | |
20 from libervia.backend.core.i18n import _, D_ | |
21 from libervia.backend.core import exceptions | |
22 from libervia.backend.core.constants import Const as C | |
23 from libervia.backend.memory.persistent import PersistentBinaryDict | |
24 from libervia.backend.tools import xml_tools | |
25 from libervia.backend.core.log import getLogger | |
26 | |
27 log = getLogger(__name__) | |
28 from twisted.words.xish import domish | |
29 from twisted.words.protocols.jabber import jid | |
30 from twisted.words.protocols.jabber.error import StanzaError | |
31 | |
32 from twisted.internet import defer | |
33 | |
34 NS_BOOKMARKS = "storage:bookmarks" | |
35 | |
36 PLUGIN_INFO = { | |
37 C.PI_NAME: "Bookmarks", | |
38 C.PI_IMPORT_NAME: "XEP-0048", | |
39 C.PI_TYPE: "XEP", | |
40 C.PI_PROTOCOLS: ["XEP-0048"], | |
41 C.PI_DEPENDENCIES: ["XEP-0045"], | |
42 C.PI_RECOMMENDATIONS: ["XEP-0049"], | |
43 C.PI_MAIN: "XEP_0048", | |
44 C.PI_HANDLER: "no", | |
45 C.PI_DESCRIPTION: _("""Implementation of bookmarks"""), | |
46 } | |
47 | |
48 | |
49 class XEP_0048(object): | |
50 MUC_TYPE = "muc" | |
51 URL_TYPE = "url" | |
52 MUC_KEY = "jid" | |
53 URL_KEY = "url" | |
54 MUC_ATTRS = ("autojoin", "name") | |
55 URL_ATTRS = ("name",) | |
56 | |
57 def __init__(self, host): | |
58 log.info(_("Bookmarks plugin initialization")) | |
59 self.host = host | |
60 # self.__menu_id = host.register_callback(self._bookmarks_menu, with_data=True) | |
61 self.__bm_save_id = host.register_callback(self._bookmarks_save_cb, with_data=True) | |
62 host.import_menu( | |
63 (D_("Groups"), D_("Bookmarks")), | |
64 self._bookmarks_menu, | |
65 security_limit=0, | |
66 help_string=D_("Use and manage bookmarks"), | |
67 ) | |
68 self.__selected_id = host.register_callback( | |
69 self._bookmark_selected_cb, with_data=True | |
70 ) | |
71 host.bridge.add_method( | |
72 "bookmarks_list", | |
73 ".plugin", | |
74 in_sign="sss", | |
75 out_sign="a{sa{sa{ss}}}", | |
76 method=self._bookmarks_list, | |
77 async_=True, | |
78 ) | |
79 host.bridge.add_method( | |
80 "bookmarks_remove", | |
81 ".plugin", | |
82 in_sign="ssss", | |
83 out_sign="", | |
84 method=self._bookmarks_remove, | |
85 async_=True, | |
86 ) | |
87 host.bridge.add_method( | |
88 "bookmarks_add", | |
89 ".plugin", | |
90 in_sign="ssa{ss}ss", | |
91 out_sign="", | |
92 method=self._bookmarks_add, | |
93 async_=True, | |
94 ) | |
95 try: | |
96 self.private_plg = self.host.plugins["XEP-0049"] | |
97 except KeyError: | |
98 self.private_plg = None | |
99 try: | |
100 self.host.plugins[C.TEXT_CMDS].register_text_commands(self) | |
101 except KeyError: | |
102 log.info(_("Text commands not available")) | |
103 | |
104 async def profile_connected(self, client): | |
105 local = client.bookmarks_local = PersistentBinaryDict( | |
106 NS_BOOKMARKS, client.profile | |
107 ) | |
108 await local.load() | |
109 if not local: | |
110 local[XEP_0048.MUC_TYPE] = dict() | |
111 local[XEP_0048.URL_TYPE] = dict() | |
112 private = await self._get_server_bookmarks("private", client.profile) | |
113 pubsub = client.bookmarks_pubsub = None | |
114 | |
115 for bookmarks in (local, private, pubsub): | |
116 if bookmarks is not None: | |
117 for (room_jid, data) in list(bookmarks[XEP_0048.MUC_TYPE].items()): | |
118 if data.get("autojoin", "false") == "true": | |
119 nick = data.get("nick", client.jid.user) | |
120 defer.ensureDeferred( | |
121 self.host.plugins["XEP-0045"].join(client, room_jid, nick, {}) | |
122 ) | |
123 | |
124 # we don't use a DeferredList to gather result here, as waiting for all room would | |
125 # slow down a lot the connection process, and result in a bad user experience. | |
126 | |
127 @defer.inlineCallbacks | |
128 def _get_server_bookmarks(self, storage_type, profile): | |
129 """Get distants bookmarks | |
130 | |
131 update also the client.bookmarks_[type] key, with None if service is not available | |
132 @param storage_type: storage type, can be: | |
133 - 'private': XEP-0049 storage | |
134 - 'pubsub': XEP-0223 storage | |
135 @param profile: %(doc_profile)s | |
136 @return: data dictionary, or None if feature is not available | |
137 """ | |
138 client = self.host.get_client(profile) | |
139 if storage_type == "private": | |
140 try: | |
141 bookmarks_private_xml = yield self.private_plg.private_xml_get( | |
142 "storage", NS_BOOKMARKS, profile | |
143 ) | |
144 data = client.bookmarks_private = self._bookmark_elt_2_dict( | |
145 bookmarks_private_xml | |
146 ) | |
147 except (StanzaError, AttributeError): | |
148 log.info(_("Private XML storage not available")) | |
149 data = client.bookmarks_private = None | |
150 elif storage_type == "pubsub": | |
151 raise NotImplementedError | |
152 else: | |
153 raise ValueError("storage_type must be 'private' or 'pubsub'") | |
154 defer.returnValue(data) | |
155 | |
156 @defer.inlineCallbacks | |
157 def _set_server_bookmarks(self, storage_type, bookmarks_elt, profile): | |
158 """Save bookmarks on server | |
159 | |
160 @param storage_type: storage type, can be: | |
161 - 'private': XEP-0049 storage | |
162 - 'pubsub': XEP-0223 storage | |
163 @param bookmarks_elt (domish.Element): bookmarks XML | |
164 @param profile: %(doc_profile)s | |
165 """ | |
166 if storage_type == "private": | |
167 yield self.private_plg.private_xml_store(bookmarks_elt, profile) | |
168 elif storage_type == "pubsub": | |
169 raise NotImplementedError | |
170 else: | |
171 raise ValueError("storage_type must be 'private' or 'pubsub'") | |
172 | |
173 def _bookmark_elt_2_dict(self, storage_elt): | |
174 """Parse bookmarks to get dictionary | |
175 @param storage_elt (domish.Element): bookmarks storage | |
176 @return (dict): bookmark data (key: bookmark type, value: list) where key can be: | |
177 - XEP_0048.MUC_TYPE | |
178 - XEP_0048.URL_TYPE | |
179 - value (dict): data as for add_bookmark | |
180 """ | |
181 conf_data = {} | |
182 url_data = {} | |
183 | |
184 conference_elts = storage_elt.elements(NS_BOOKMARKS, "conference") | |
185 for conference_elt in conference_elts: | |
186 try: | |
187 room_jid = jid.JID(conference_elt[XEP_0048.MUC_KEY]) | |
188 except KeyError: | |
189 log.warning( | |
190 "invalid bookmark found, igoring it:\n%s" % conference_elt.toXml() | |
191 ) | |
192 continue | |
193 | |
194 data = conf_data[room_jid] = {} | |
195 | |
196 for attr in XEP_0048.MUC_ATTRS: | |
197 if conference_elt.hasAttribute(attr): | |
198 data[attr] = conference_elt[attr] | |
199 try: | |
200 data["nick"] = str( | |
201 next(conference_elt.elements(NS_BOOKMARKS, "nick")) | |
202 ) | |
203 except StopIteration: | |
204 pass | |
205 # TODO: manage password (need to be secured, see XEP-0049 §4) | |
206 | |
207 url_elts = storage_elt.elements(NS_BOOKMARKS, "url") | |
208 for url_elt in url_elts: | |
209 try: | |
210 url = url_elt[XEP_0048.URL_KEY] | |
211 except KeyError: | |
212 log.warning("invalid bookmark found, igoring it:\n%s" % url_elt.toXml()) | |
213 continue | |
214 data = url_data[url] = {} | |
215 for attr in XEP_0048.URL_ATTRS: | |
216 if url_elt.hasAttribute(attr): | |
217 data[attr] = url_elt[attr] | |
218 | |
219 return {XEP_0048.MUC_TYPE: conf_data, XEP_0048.URL_TYPE: url_data} | |
220 | |
221 def _dict_2_bookmark_elt(self, type_, data): | |
222 """Construct a bookmark element from a data dict | |
223 @param data (dict): bookmark data (key: bookmark type, value: list) where key can be: | |
224 - XEP_0048.MUC_TYPE | |
225 - XEP_0048.URL_TYPE | |
226 - value (dict): data as for add_bookmark | |
227 @return (domish.Element): bookmark element | |
228 """ | |
229 rooms_data = data.get(XEP_0048.MUC_TYPE, {}) | |
230 urls_data = data.get(XEP_0048.URL_TYPE, {}) | |
231 storage_elt = domish.Element((NS_BOOKMARKS, "storage")) | |
232 for room_jid in rooms_data: | |
233 conference_elt = storage_elt.addElement("conference") | |
234 conference_elt[XEP_0048.MUC_KEY] = room_jid.full() | |
235 for attr in XEP_0048.MUC_ATTRS: | |
236 try: | |
237 conference_elt[attr] = rooms_data[room_jid][attr] | |
238 except KeyError: | |
239 pass | |
240 try: | |
241 conference_elt.addElement("nick", content=rooms_data[room_jid]["nick"]) | |
242 except KeyError: | |
243 pass | |
244 | |
245 for url, url_data in urls_data.items(): | |
246 url_elt = storage_elt.addElement("url") | |
247 url_elt[XEP_0048.URL_KEY] = url | |
248 for attr in XEP_0048.URL_ATTRS: | |
249 try: | |
250 url_elt[attr] = url_data[attr] | |
251 except KeyError: | |
252 pass | |
253 | |
254 return storage_elt | |
255 | |
256 def _bookmark_selected_cb(self, data, profile): | |
257 try: | |
258 room_jid_s, nick = data["index"].split(" ", 1) | |
259 room_jid = jid.JID(room_jid_s) | |
260 except (KeyError, RuntimeError): | |
261 log.warning(_("No room jid selected")) | |
262 return {} | |
263 | |
264 client = self.host.get_client(profile) | |
265 d = self.host.plugins["XEP-0045"].join(client, room_jid, nick, {}) | |
266 | |
267 def join_eb(failure): | |
268 log.warning("Error while trying to join room: {}".format(failure)) | |
269 # FIXME: failure are badly managed in plugin XEP-0045. Plugin XEP-0045 need to be fixed before managing errors correctly here | |
270 return {} | |
271 | |
272 d.addCallbacks(lambda __: {}, join_eb) | |
273 return d | |
274 | |
275 def _bookmarks_menu(self, data, profile): | |
276 """ XMLUI activated by menu: return Gateways UI | |
277 @param profile: %(doc_profile)s | |
278 | |
279 """ | |
280 client = self.host.get_client(profile) | |
281 xmlui = xml_tools.XMLUI(title=_("Bookmarks manager")) | |
282 adv_list = xmlui.change_container( | |
283 "advanced_list", | |
284 columns=3, | |
285 selectable="single", | |
286 callback_id=self.__selected_id, | |
287 ) | |
288 for bookmarks in ( | |
289 client.bookmarks_local, | |
290 client.bookmarks_private, | |
291 client.bookmarks_pubsub, | |
292 ): | |
293 if bookmarks is None: | |
294 continue | |
295 for (room_jid, data) in sorted( | |
296 list(bookmarks[XEP_0048.MUC_TYPE].items()), | |
297 key=lambda item: item[1].get("name", item[0].user), | |
298 ): | |
299 room_jid_s = room_jid.full() | |
300 adv_list.set_row_index( | |
301 "%s %s" % (room_jid_s, data.get("nick") or client.jid.user) | |
302 ) | |
303 xmlui.addText(data.get("name", "")) | |
304 xmlui.addJid(room_jid) | |
305 if C.bool(data.get("autojoin", C.BOOL_FALSE)): | |
306 xmlui.addText("autojoin") | |
307 else: | |
308 xmlui.addEmpty() | |
309 adv_list.end() | |
310 xmlui.addDivider("dash") | |
311 xmlui.addText(_("add a bookmark")) | |
312 xmlui.change_container("pairs") | |
313 xmlui.addLabel(_("Name")) | |
314 xmlui.addString("name") | |
315 xmlui.addLabel(_("jid")) | |
316 xmlui.addString("jid") | |
317 xmlui.addLabel(_("Nickname")) | |
318 xmlui.addString("nick", client.jid.user) | |
319 xmlui.addLabel(_("Autojoin")) | |
320 xmlui.addBool("autojoin") | |
321 xmlui.change_container("vertical") | |
322 xmlui.addButton(self.__bm_save_id, _("Save"), ("name", "jid", "nick", "autojoin")) | |
323 return {"xmlui": xmlui.toXml()} | |
324 | |
325 def _bookmarks_save_cb(self, data, profile): | |
326 bm_data = xml_tools.xmlui_result_2_data_form_result(data) | |
327 try: | |
328 location = jid.JID(bm_data.pop("jid")) | |
329 except KeyError: | |
330 raise exceptions.InternalError("Can't find mandatory key") | |
331 d = self.add_bookmark(XEP_0048.MUC_TYPE, location, bm_data, profile_key=profile) | |
332 d.addCallback(lambda __: {}) | |
333 return d | |
334 | |
335 @defer.inlineCallbacks | |
336 def add_bookmark( | |
337 self, type_, location, data, storage_type="auto", profile_key=C.PROF_KEY_NONE | |
338 ): | |
339 """Store a new bookmark | |
340 | |
341 @param type_: bookmark type, one of: | |
342 - XEP_0048.MUC_TYPE: Multi-User chat room | |
343 - XEP_0048.URL_TYPE: web page URL | |
344 @param location: dependeding on type_, can be a MUC room jid or an url | |
345 @param data (dict): depending on type_, can contains the following keys: | |
346 - name: human readable name of the bookmark | |
347 - nick: user preferred room nick (default to user part of profile's jid) | |
348 - autojoin: "true" if room must be automatically joined on connection | |
349 - password: unused yet TODO | |
350 @param storage_type: where the bookmark will be stored, can be: | |
351 - "auto": find best available option: pubsub, private, local in that order | |
352 - "pubsub": PubSub private storage (XEP-0223) | |
353 - "private": Private XML storage (XEP-0049) | |
354 - "local": Store in SàT database | |
355 @param profile_key: %(doc_profile_key)s | |
356 """ | |
357 assert storage_type in ("auto", "pubsub", "private", "local") | |
358 if type_ == XEP_0048.URL_TYPE and {"autojoin", "nick"}.intersection(list(data.keys())): | |
359 raise ValueError("autojoin or nick can't be used with URLs") | |
360 client = self.host.get_client(profile_key) | |
361 if storage_type == "auto": | |
362 if client.bookmarks_pubsub is not None: | |
363 storage_type = "pubsub" | |
364 elif client.bookmarks_private is not None: | |
365 storage_type = "private" | |
366 else: | |
367 storage_type = "local" | |
368 log.warning(_("Bookmarks will be local only")) | |
369 log.info(_('Type selected for "auto" storage: %s') % storage_type) | |
370 | |
371 if storage_type == "local": | |
372 client.bookmarks_local[type_][location] = data | |
373 yield client.bookmarks_local.force(type_) | |
374 else: | |
375 bookmarks = yield self._get_server_bookmarks(storage_type, client.profile) | |
376 bookmarks[type_][location] = data | |
377 bookmark_elt = self._dict_2_bookmark_elt(type_, bookmarks) | |
378 yield self._set_server_bookmarks(storage_type, bookmark_elt, client.profile) | |
379 | |
380 @defer.inlineCallbacks | |
381 def remove_bookmark( | |
382 self, type_, location, storage_type="all", profile_key=C.PROF_KEY_NONE | |
383 ): | |
384 """Remove a stored bookmark | |
385 | |
386 @param type_: bookmark type, one of: | |
387 - XEP_0048.MUC_TYPE: Multi-User chat room | |
388 - XEP_0048.URL_TYPE: web page URL | |
389 @param location: dependeding on type_, can be a MUC room jid or an url | |
390 @param storage_type: where the bookmark is stored, can be: | |
391 - "all": remove from everywhere | |
392 - "pubsub": PubSub private storage (XEP-0223) | |
393 - "private": Private XML storage (XEP-0049) | |
394 - "local": Store in SàT database | |
395 @param profile_key: %(doc_profile_key)s | |
396 """ | |
397 assert storage_type in ("all", "pubsub", "private", "local") | |
398 client = self.host.get_client(profile_key) | |
399 | |
400 if storage_type in ("all", "local"): | |
401 try: | |
402 del client.bookmarks_local[type_][location] | |
403 yield client.bookmarks_local.force(type_) | |
404 except KeyError: | |
405 log.debug("Bookmark is not present in local storage") | |
406 | |
407 if storage_type in ("all", "private"): | |
408 bookmarks = yield self._get_server_bookmarks("private", client.profile) | |
409 try: | |
410 del bookmarks[type_][location] | |
411 bookmark_elt = self._dict_2_bookmark_elt(type_, bookmarks) | |
412 yield self._set_server_bookmarks("private", bookmark_elt, client.profile) | |
413 except KeyError: | |
414 log.debug("Bookmark is not present in private storage") | |
415 | |
416 if storage_type == "pubsub": | |
417 raise NotImplementedError | |
418 | |
419 def _bookmarks_list(self, type_, storage_location, profile_key=C.PROF_KEY_NONE): | |
420 """Return stored bookmarks | |
421 | |
422 @param type_: bookmark type, one of: | |
423 - XEP_0048.MUC_TYPE: Multi-User chat room | |
424 - XEP_0048.URL_TYPE: web page URL | |
425 @param storage_location: can be: | |
426 - 'all' | |
427 - 'local' | |
428 - 'private' | |
429 - 'pubsub' | |
430 @param profile_key: %(doc_profile_key)s | |
431 @param return (dict): (key: storage_location, value dict) with: | |
432 - value (dict): (key: bookmark_location, value: bookmark data) | |
433 """ | |
434 client = self.host.get_client(profile_key) | |
435 ret = {} | |
436 ret_d = defer.succeed(ret) | |
437 | |
438 def fill_bookmarks(__, _storage_location): | |
439 bookmarks_ori = getattr(client, "bookmarks_" + _storage_location) | |
440 if bookmarks_ori is None: | |
441 return ret | |
442 data = bookmarks_ori[type_] | |
443 for bookmark in data: | |
444 if type_ == XEP_0048.MUC_TYPE: | |
445 ret[_storage_location][bookmark.full()] = data[bookmark].copy() | |
446 else: | |
447 ret[_storage_location][bookmark] = data[bookmark].copy() | |
448 return ret | |
449 | |
450 for _storage_location in ("local", "private", "pubsub"): | |
451 if storage_location in ("all", _storage_location): | |
452 ret[_storage_location] = {} | |
453 if _storage_location in ("private",): | |
454 # we update distant bookmarks, just in case an other client added something | |
455 d = self._get_server_bookmarks(_storage_location, client.profile) | |
456 else: | |
457 d = defer.succeed(None) | |
458 d.addCallback(fill_bookmarks, _storage_location) | |
459 ret_d.addCallback(lambda __: d) | |
460 | |
461 return ret_d | |
462 | |
463 def _bookmarks_remove( | |
464 self, type_, location, storage_location, profile_key=C.PROF_KEY_NONE | |
465 ): | |
466 """Return stored bookmarks | |
467 | |
468 @param type_: bookmark type, one of: | |
469 - XEP_0048.MUC_TYPE: Multi-User chat room | |
470 - XEP_0048.URL_TYPE: web page URL | |
471 @param location: dependeding on type_, can be a MUC room jid or an url | |
472 @param storage_location: can be: | |
473 - "all": remove from everywhere | |
474 - "pubsub": PubSub private storage (XEP-0223) | |
475 - "private": Private XML storage (XEP-0049) | |
476 - "local": Store in SàT database | |
477 @param profile_key: %(doc_profile_key)s | |
478 """ | |
479 if type_ == XEP_0048.MUC_TYPE: | |
480 location = jid.JID(location) | |
481 return self.remove_bookmark(type_, location, storage_location, profile_key) | |
482 | |
483 def _bookmarks_add( | |
484 self, type_, location, data, storage_type="auto", profile_key=C.PROF_KEY_NONE | |
485 ): | |
486 if type_ == XEP_0048.MUC_TYPE: | |
487 location = jid.JID(location) | |
488 return self.add_bookmark(type_, location, data, storage_type, profile_key) | |
489 | |
490 def cmd_bookmark(self, client, mess_data): | |
491 """(Un)bookmark a MUC room | |
492 | |
493 @command (group): [autojoin | remove] | |
494 - autojoin: join room automatically on connection | |
495 - remove: remove bookmark(s) for this room | |
496 """ | |
497 txt_cmd = self.host.plugins[C.TEXT_CMDS] | |
498 | |
499 options = mess_data["unparsed"].strip().split() | |
500 if options and options[0] not in ("autojoin", "remove"): | |
501 txt_cmd.feed_back(client, _("Bad arguments"), mess_data) | |
502 return False | |
503 | |
504 room_jid = mess_data["to"].userhostJID() | |
505 | |
506 if "remove" in options: | |
507 self.remove_bookmark(XEP_0048.MUC_TYPE, room_jid, profile_key=client.profile) | |
508 txt_cmd.feed_back( | |
509 client, | |
510 _("All [%s] bookmarks are being removed") % room_jid.full(), | |
511 mess_data, | |
512 ) | |
513 return False | |
514 | |
515 data = { | |
516 "name": room_jid.user, | |
517 "nick": client.jid.user, | |
518 "autojoin": "true" if "autojoin" in options else "false", | |
519 } | |
520 self.add_bookmark(XEP_0048.MUC_TYPE, room_jid, data, profile_key=client.profile) | |
521 txt_cmd.feed_back(client, _("Bookmark added"), mess_data) | |
522 | |
523 return False |