comparison sat/plugins/plugin_xep_0048.py @ 2562:26edcf3a30eb

core, setup: huge cleaning: - moved directories from src and frontends/src to sat and sat_frontends, which is the recommanded naming convention - move twisted directory to root - removed all hacks from setup.py, and added missing dependencies, it is now clean - use https URL for website in setup.py - removed "Environment :: X11 Applications :: GTK", as wix is deprecated and removed - renamed sat.sh to sat and fixed its installation - added python_requires to specify Python version needed - replaced glib2reactor which use deprecated code by gtk3reactor sat can now be installed directly from virtualenv without using --system-site-packages anymore \o/
author Goffi <goffi@goffi.org>
date Mon, 02 Apr 2018 19:44:50 +0200
parents src/plugins/plugin_xep_0048.py@0046283a285d
children 56f94936df1e
comparison
equal deleted inserted replaced
2561:bd30dc3ffe5a 2562:26edcf3a30eb
1 #!/usr/bin/env python2
2 # -*- coding: utf-8 -*-
3
4 # SAT plugin for Bookmarks (xep-0048)
5 # Copyright (C) 2009-2018 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 sat.core.i18n import _, D_
21 from sat.core import exceptions
22 from sat.core.constants import Const as C
23 from sat.memory.persistent import PersistentBinaryDict
24 from sat.tools import xml_tools
25 from sat.core.log import getLogger
26 log = getLogger(__name__)
27 from twisted.words.xish import domish
28 from twisted.words.protocols.jabber import jid
29 from twisted.words.protocols.jabber.error import StanzaError
30
31 from twisted.internet import defer
32
33 NS_BOOKMARKS = 'storage:bookmarks'
34
35 PLUGIN_INFO = {
36 C.PI_NAME: "Bookmarks",
37 C.PI_IMPORT_NAME: "XEP-0048",
38 C.PI_TYPE: "XEP",
39 C.PI_PROTOCOLS: ["XEP-0048"],
40 C.PI_DEPENDENCIES: ["XEP-0045"],
41 C.PI_RECOMMENDATIONS: ["XEP-0049"],
42 C.PI_MAIN: "XEP_0048",
43 C.PI_HANDLER: "no",
44 C.PI_DESCRIPTION: _("""Implementation of bookmarks""")
45 }
46
47
48 class XEP_0048(object):
49 MUC_TYPE = 'muc'
50 URL_TYPE = 'url'
51 MUC_KEY = 'jid'
52 URL_KEY = 'url'
53 MUC_ATTRS = ('autojoin', 'name')
54 URL_ATTRS = ('name',)
55
56 def __init__(self, host):
57 log.info(_("Bookmarks plugin initialization"))
58 self.host = host
59 # self.__menu_id = host.registerCallback(self._bookmarksMenu, with_data=True)
60 self.__bm_save_id = host.registerCallback(self._bookmarksSaveCb, with_data=True)
61 host.importMenu((D_("Groups"), D_("Bookmarks")), self._bookmarksMenu, security_limit=0, help_string=D_("Use and manage bookmarks"))
62 self.__selected_id = host.registerCallback(self._bookmarkSelectedCb, with_data=True)
63 host.bridge.addMethod("bookmarksList", ".plugin", in_sign='sss', out_sign='a{sa{sa{ss}}}', method=self._bookmarksList, async=True)
64 host.bridge.addMethod("bookmarksRemove", ".plugin", in_sign='ssss', out_sign='', method=self._bookmarksRemove, async=True)
65 host.bridge.addMethod("bookmarksAdd", ".plugin", in_sign='ssa{ss}ss', out_sign='', method=self._bookmarksAdd, async=True)
66 try:
67 self.private_plg = self.host.plugins["XEP-0049"]
68 except KeyError:
69 self.private_plg = None
70 try:
71 self.host.plugins[C.TEXT_CMDS].registerTextCommands(self)
72 except KeyError:
73 log.info(_("Text commands not available"))
74
75 @defer.inlineCallbacks
76 def profileConnected(self, client):
77 local = client.bookmarks_local = PersistentBinaryDict(NS_BOOKMARKS, client.profile)
78 yield local.load()
79 if not local:
80 local[XEP_0048.MUC_TYPE] = dict()
81 local[XEP_0048.URL_TYPE] = dict()
82 private = yield self._getServerBookmarks('private', client.profile)
83 pubsub = client.bookmarks_pubsub = None
84
85 for bookmarks in (local, private, pubsub):
86 if bookmarks is not None:
87 for (room_jid, data) in bookmarks[XEP_0048.MUC_TYPE].items():
88 if data.get('autojoin', 'false') == 'true':
89 nick = data.get('nick', client.jid.user)
90 self.host.plugins['XEP-0045'].join(client, room_jid, nick, {})
91
92 @defer.inlineCallbacks
93 def _getServerBookmarks(self, storage_type, profile):
94 """Get distants bookmarks
95
96 update also the client.bookmarks_[type] key, with None if service is not available
97 @param storage_type: storage type, can be:
98 - 'private': XEP-0049 storage
99 - 'pubsub': XEP-0223 storage
100 @param profile: %(doc_profile)s
101 @return: data dictionary, or None if feature is not available
102 """
103 client = self.host.getClient(profile)
104 if storage_type == 'private':
105 try:
106 bookmarks_private_xml = yield self.private_plg.privateXMLGet('storage', NS_BOOKMARKS, profile)
107 data = client.bookmarks_private = self._bookmarkElt2Dict(bookmarks_private_xml)
108 except (StanzaError, AttributeError):
109 log.info(_("Private XML storage not available"))
110 data = client.bookmarks_private = None
111 elif storage_type == 'pubsub':
112 raise NotImplementedError
113 else:
114 raise ValueError("storage_type must be 'private' or 'pubsub'")
115 defer.returnValue(data)
116
117 @defer.inlineCallbacks
118 def _setServerBookmarks(self, storage_type, bookmarks_elt, profile):
119 """Save bookmarks on server
120
121 @param storage_type: storage type, can be:
122 - 'private': XEP-0049 storage
123 - 'pubsub': XEP-0223 storage
124 @param bookmarks_elt (domish.Element): bookmarks XML
125 @param profile: %(doc_profile)s
126 """
127 if storage_type == 'private':
128 yield self.private_plg.privateXMLStore(bookmarks_elt, profile)
129 elif storage_type == 'pubsub':
130 raise NotImplementedError
131 else:
132 raise ValueError("storage_type must be 'private' or 'pubsub'")
133
134 def _bookmarkElt2Dict(self, storage_elt):
135 """Parse bookmarks to get dictionary
136 @param storage_elt (domish.Element): bookmarks storage
137 @return (dict): bookmark data (key: bookmark type, value: list) where key can be:
138 - XEP_0048.MUC_TYPE
139 - XEP_0048.URL_TYPE
140 - value (dict): data as for addBookmark
141 """
142 conf_data = {}
143 url_data = {}
144
145 conference_elts = storage_elt.elements(NS_BOOKMARKS, 'conference')
146 for conference_elt in conference_elts:
147 try:
148 room_jid = jid.JID(conference_elt[XEP_0048.MUC_KEY])
149 except KeyError:
150 log.warning ("invalid bookmark found, igoring it:\n%s" % conference_elt.toXml())
151 continue
152
153 data = conf_data[room_jid] = {}
154
155 for attr in XEP_0048.MUC_ATTRS:
156 if conference_elt.hasAttribute(attr):
157 data[attr] = conference_elt[attr]
158 try:
159 data['nick'] = unicode(conference_elt.elements(NS_BOOKMARKS, 'nick').next())
160 except StopIteration:
161 pass
162 # TODO: manage password (need to be secured, see XEP-0049 §4)
163
164 url_elts = storage_elt.elements(NS_BOOKMARKS, 'url')
165 for url_elt in url_elts:
166 try:
167 url = url_elt[XEP_0048.URL_KEY]
168 except KeyError:
169 log.warning ("invalid bookmark found, igoring it:\n%s" % url_elt.toXml())
170 continue
171 data = url_data[url] = {}
172 for attr in XEP_0048.URL_ATTRS:
173 if url_elt.hasAttribute(attr):
174 data[attr] = url_elt[attr]
175
176 return {XEP_0048.MUC_TYPE: conf_data, XEP_0048.URL_TYPE: url_data}
177
178 def _dict2BookmarkElt(self, type_, data):
179 """Construct a bookmark element from a data dict
180 @param data (dict): bookmark data (key: bookmark type, value: list) where key can be:
181 - XEP_0048.MUC_TYPE
182 - XEP_0048.URL_TYPE
183 - value (dict): data as for addBookmark
184 @return (domish.Element): bookmark element
185 """
186 rooms_data = data.get(XEP_0048.MUC_TYPE, {})
187 urls_data = data.get(XEP_0048.URL_TYPE, {})
188 storage_elt = domish.Element((NS_BOOKMARKS, 'storage'))
189 for room_jid in rooms_data:
190 conference_elt = storage_elt.addElement('conference')
191 conference_elt[XEP_0048.MUC_KEY] = room_jid.full()
192 for attr in XEP_0048.MUC_ATTRS:
193 try:
194 conference_elt[attr] = rooms_data[room_jid][attr]
195 except KeyError:
196 pass
197 try:
198 conference_elt.addElement('nick', content=rooms_data[room_jid]['nick'])
199 except KeyError:
200 pass
201
202 for url in urls_data:
203 url_elt = storage_elt.addElement('url')
204 url_elt[XEP_0048.URL_KEY] = url
205 for attr in XEP_0048.URL_ATTRS:
206 try:
207 url_elt[attr] = url[attr]
208 except KeyError:
209 pass
210
211 return storage_elt
212
213 def _bookmarkSelectedCb(self, data, profile):
214 try:
215 room_jid_s, nick = data['index'].split(' ', 1)
216 room_jid = jid.JID(room_jid_s)
217 except (KeyError, RuntimeError):
218 log.warning(_("No room jid selected"))
219 return {}
220
221 client = self.host.getClient(profile)
222 d = self.host.plugins['XEP-0045'].join(client, room_jid, nick, {})
223 def join_eb(failure):
224 log.warning(u"Error while trying to join room: {}".format(failure))
225 # FIXME: failure are badly managed in plugin XEP-0045. Plugin XEP-0045 need to be fixed before managing errors correctly here
226 return {}
227 d.addCallbacks(lambda dummy: {}, join_eb)
228 return d
229
230 def _bookmarksMenu(self, data, profile):
231 """ XMLUI activated by menu: return Gateways UI
232 @param profile: %(doc_profile)s
233
234 """
235 client = self.host.getClient(profile)
236 xmlui = xml_tools.XMLUI(title=_('Bookmarks manager'))
237 adv_list = xmlui.changeContainer('advanced_list', columns=3, selectable='single', callback_id=self.__selected_id)
238 for bookmarks in (client.bookmarks_local, client.bookmarks_private, client.bookmarks_pubsub):
239 if bookmarks is None:
240 continue
241 for (room_jid, data) in sorted(bookmarks[XEP_0048.MUC_TYPE].items(), key=lambda item: item[1].get('name',item[0].user)):
242 room_jid_s = room_jid.full()
243 adv_list.setRowIndex(u'%s %s' % (room_jid_s, data.get('nick') or client.jid.user))
244 xmlui.addText(data.get('name',''))
245 xmlui.addJid(room_jid)
246 if data.get('autojoin', 'false') == 'true':
247 xmlui.addText('autojoin')
248 else:
249 xmlui.addEmpty()
250 adv_list.end()
251 xmlui.addDivider('dash')
252 xmlui.addText(_("add a bookmark"))
253 xmlui.changeContainer("pairs")
254 xmlui.addLabel(_('Name'))
255 xmlui.addString('name')
256 xmlui.addLabel(_('jid'))
257 xmlui.addString('jid')
258 xmlui.addLabel(_('Nickname'))
259 xmlui.addString('nick', client.jid.user)
260 xmlui.addLabel(_('Autojoin'))
261 xmlui.addBool('autojoin')
262 xmlui.changeContainer("vertical")
263 xmlui.addButton(self.__bm_save_id, _("Save"), ('name', 'jid', 'nick', 'autojoin'))
264 return {'xmlui': xmlui.toXml()}
265
266 def _bookmarksSaveCb(self, data, profile):
267 bm_data = xml_tools.XMLUIResult2DataFormResult(data)
268 try:
269 location = jid.JID(bm_data.pop('jid'))
270 except KeyError:
271 raise exceptions.InternalError("Can't find mandatory key")
272 d = self.addBookmark(XEP_0048.MUC_TYPE, location, bm_data, profile_key=profile)
273 d.addCallback(lambda dummy: {})
274 return d
275
276 @defer.inlineCallbacks
277 def addBookmark(self, type_, location, data, storage_type="auto", profile_key=C.PROF_KEY_NONE):
278 """Store a new bookmark
279
280 @param type_: bookmark type, one of:
281 - XEP_0048.MUC_TYPE: Multi-User chat room
282 - XEP_0048.URL_TYPE: web page URL
283 @param location: dependeding on type_, can be a MUC room jid or an url
284 @param data (dict): depending on type_, can contains the following keys:
285 - name: human readable name of the bookmark
286 - nick: user preferred room nick (default to user part of profile's jid)
287 - autojoin: "true" if room must be automatically joined on connection
288 - password: unused yet TODO
289 @param storage_type: where the bookmark will be stored, can be:
290 - "auto": find best available option: pubsub, private, local in that order
291 - "pubsub": PubSub private storage (XEP-0223)
292 - "private": Private XML storage (XEP-0049)
293 - "local": Store in SàT database
294 @param profile_key: %(doc_profile_key)s
295 """
296 assert storage_type in ('auto', 'pubsub', 'private', 'local')
297 if type_ == XEP_0048.URL_TYPE and {'autojoin', 'nick'}.intersection(data.keys()):
298 raise ValueError("autojoin or nick can't be used with URLs")
299 client = self.host.getClient(profile_key)
300 if storage_type == 'auto':
301 if client.bookmarks_pubsub is not None:
302 storage_type = 'pubsub'
303 elif client.bookmarks_private is not None:
304 storage_type = 'private'
305 else:
306 storage_type = 'local'
307 log.warning(_("Bookmarks will be local only"))
308 log.info(_('Type selected for "auto" storage: %s') % storage_type)
309
310 if storage_type == 'local':
311 client.bookmarks_local[type_][location] = data
312 yield client.bookmarks_local.force(type_)
313 else:
314 bookmarks = yield self._getServerBookmarks(storage_type, client.profile)
315 bookmarks[type_][location] = data
316 bookmark_elt = self._dict2BookmarkElt(type_, bookmarks)
317 yield self._setServerBookmarks(storage_type, bookmark_elt, client.profile)
318
319 @defer.inlineCallbacks
320 def removeBookmark(self, type_, location, storage_type="all", profile_key=C.PROF_KEY_NONE):
321 """Remove a stored bookmark
322
323 @param type_: bookmark type, one of:
324 - XEP_0048.MUC_TYPE: Multi-User chat room
325 - XEP_0048.URL_TYPE: web page URL
326 @param location: dependeding on type_, can be a MUC room jid or an url
327 @param storage_type: where the bookmark is stored, can be:
328 - "all": remove from everywhere
329 - "pubsub": PubSub private storage (XEP-0223)
330 - "private": Private XML storage (XEP-0049)
331 - "local": Store in SàT database
332 @param profile_key: %(doc_profile_key)s
333 """
334 assert storage_type in ('all', 'pubsub', 'private', 'local')
335 client = self.host.getClient(profile_key)
336
337 if storage_type in ('all', 'local'):
338 try:
339 del client.bookmarks_local[type_][location]
340 yield client.bookmarks_local.force(type_)
341 except KeyError:
342 log.debug("Bookmark is not present in local storage")
343
344 if storage_type in ('all', 'private'):
345 bookmarks = yield self._getServerBookmarks('private', client.profile)
346 try:
347 del bookmarks[type_][location]
348 bookmark_elt = self._dict2BookmarkElt(type_, bookmarks)
349 yield self._setServerBookmarks('private', bookmark_elt, client.profile)
350 except KeyError:
351 log.debug("Bookmark is not present in private storage")
352
353 if storage_type == 'pubsub':
354 raise NotImplementedError
355
356 def _bookmarksList(self, type_, storage_location, profile_key=C.PROF_KEY_NONE):
357 """Return stored bookmarks
358
359 @param type_: bookmark type, one of:
360 - XEP_0048.MUC_TYPE: Multi-User chat room
361 - XEP_0048.URL_TYPE: web page URL
362 @param storage_location: can be:
363 - 'all'
364 - 'local'
365 - 'private'
366 - 'pubsub'
367 @param profile_key: %(doc_profile_key)s
368 @param return (dict): (key: storage_location, value dict) with:
369 - value (dict): (key: bookmark_location, value: bookmark data)
370 """
371 client = self.host.getClient(profile_key)
372 ret = {}
373 ret_d = defer.succeed(ret)
374
375 def fillBookmarks(dummy, _storage_location):
376 bookmarks_ori = getattr(client, "bookmarks_" + _storage_location)
377 if bookmarks_ori is None:
378 return ret
379 data = bookmarks_ori[type_]
380 for bookmark in data:
381 ret[_storage_location][bookmark.full()] = data[bookmark].copy()
382 return ret
383
384 for _storage_location in ('local', 'private', 'pubsub'):
385 if storage_location in ('all', _storage_location):
386 ret[_storage_location] = {}
387 if _storage_location in ('private',):
388 # we update distant bookmarks, just in case an other client added something
389 d = self._getServerBookmarks(_storage_location, client.profile)
390 else:
391 d = defer.succeed(None)
392 d.addCallback(fillBookmarks, _storage_location)
393 ret_d.addCallback(lambda dummy: d)
394
395 return ret_d
396
397 def _bookmarksRemove(self, type_, location, storage_location, profile_key=C.PROF_KEY_NONE):
398 """Return stored bookmarks
399
400 @param type_: bookmark type, one of:
401 - XEP_0048.MUC_TYPE: Multi-User chat room
402 - XEP_0048.URL_TYPE: web page URL
403 @param location: dependeding on type_, can be a MUC room jid or an url
404 @param storage_location: can be:
405 - "all": remove from everywhere
406 - "pubsub": PubSub private storage (XEP-0223)
407 - "private": Private XML storage (XEP-0049)
408 - "local": Store in SàT database
409 @param profile_key: %(doc_profile_key)s
410 """
411 if type_ == XEP_0048.MUC_TYPE:
412 location = jid.JID(location)
413 return self.removeBookmark(type_, location, storage_location, profile_key)
414
415 def _bookmarksAdd(self, type_, location, data, storage_type="auto", profile_key=C.PROF_KEY_NONE):
416 if type_ == XEP_0048.MUC_TYPE:
417 location = jid.JID(location)
418 return self.addBookmark(type_, location, data, storage_type, profile_key)
419
420 def cmd_bookmark(self, client, mess_data):
421 """(Un)bookmark a MUC room
422
423 @command (group): [autojoin | remove]
424 - autojoin: join room automatically on connection
425 - remove: remove bookmark(s) for this room
426 """
427 txt_cmd = self.host.plugins[C.TEXT_CMDS]
428
429 options = mess_data["unparsed"].strip().split()
430 if options and options[0] not in ('autojoin', 'remove'):
431 txt_cmd.feedBack(client, _("Bad arguments"), mess_data)
432 return False
433
434 room_jid = mess_data["to"].userhostJID()
435
436 if "remove" in options:
437 self.removeBookmark(XEP_0048.MUC_TYPE, room_jid, profile_key = client.profile)
438 txt_cmd.feedBack(client, _("All [%s] bookmarks are being removed") % room_jid.full(), mess_data)
439 return False
440
441 data = { "name": room_jid.user,
442 "nick": client.jid.user,
443 "autojoin": "true" if "autojoin" in options else "false",
444 }
445 self.addBookmark(XEP_0048.MUC_TYPE, room_jid, data, profile_key=client.profile)
446 txt_cmd.feedBack(client, _("Bookmark added"), mess_data)
447
448 return False