comparison sat/plugins/plugin_misc_forums.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_misc_forums.py@785b6a1cef0a
children 003b8b4b56a7
comparison
equal deleted inserted replaced
2561:bd30dc3ffe5a 2562:26edcf3a30eb
1 #!/usr/bin/env python2
2 # -*- coding: utf-8 -*-
3
4 # SAT plugin for Pubsub Schemas
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 _
21 from sat.core.constants import Const as C
22 from sat.core import exceptions
23 from sat.core.log import getLogger
24 from sat.tools.common import uri
25 from twisted.words.protocols.jabber import jid
26 from twisted.words.xish import domish
27 from twisted.internet import defer
28 import shortuuid
29 import json
30 log = getLogger(__name__)
31
32 NS_FORUMS = u'org.salut-a-toi.forums:0'
33 NS_FORUMS_TOPICS = NS_FORUMS + u'#topics'
34
35 PLUGIN_INFO = {
36 C.PI_NAME: _("forums management"),
37 C.PI_IMPORT_NAME: "forums",
38 C.PI_TYPE: "EXP",
39 C.PI_PROTOCOLS: [],
40 C.PI_DEPENDENCIES: ["XEP-0060", "XEP-0277"],
41 C.PI_MAIN: "forums",
42 C.PI_HANDLER: "no",
43 C.PI_DESCRIPTION: _("""forums management plugin""")
44 }
45 FORUM_ATTR = {u'title', u'name', u'main-language', u'uri'}
46 FORUM_SUB_ELTS = (u'short-desc', u'desc')
47 FORUM_TOPICS_NODE_TPL = u'{node}#topics_{uuid}'
48 FORUM_TOPIC_NODE_TPL = u'{node}_{uuid}'
49
50
51 class forums(object):
52
53 def __init__(self, host):
54 log.info(_(u"forums plugin initialization"))
55 self.host = host
56 self._m = self.host.plugins['XEP-0277']
57 self._p = self.host.plugins['XEP-0060']
58 self._node_options = {
59 self._p.OPT_ACCESS_MODEL: self._p.ACCESS_OPEN,
60 self._p.OPT_PERSIST_ITEMS: 1,
61 self._p.OPT_MAX_ITEMS: -1,
62 self._p.OPT_DELIVER_PAYLOADS: 1,
63 self._p.OPT_SEND_ITEM_SUBSCRIBE: 1,
64 self._p.OPT_PUBLISH_MODEL: self._p.ACCESS_OPEN,
65 }
66 host.registerNamespace('forums', NS_FORUMS)
67 host.bridge.addMethod("forumsGet", ".plugin",
68 in_sign='ssss', out_sign='s',
69 method=self._get,
70 async=True)
71 host.bridge.addMethod("forumsSet", ".plugin",
72 in_sign='sssss', out_sign='',
73 method=self._set,
74 async=True)
75 host.bridge.addMethod("forumTopicsGet", ".plugin",
76 in_sign='ssa{ss}s', out_sign='(aa{ss}a{ss})',
77 method=self._getTopics,
78 async=True)
79 host.bridge.addMethod("forumTopicCreate", ".plugin",
80 in_sign='ssa{ss}s', out_sign='',
81 method=self._createTopic,
82 async=True)
83
84 @defer.inlineCallbacks
85 def _createForums(self, client, forums, service, node, forums_elt=None, names=None):
86 """recursively create <forums> element(s)
87
88 @param forums(list): forums which may have subforums
89 @param service(jid.JID): service where the new nodes will be created
90 @param node(unicode): node of the forums
91 will be used as basis for the newly created nodes
92 @param parent_elt(domish.Element, None): element where the forum must be added
93 if None, the root <forums> element will be created
94 @return (domish.Element): created forums
95 """
96 if not isinstance(forums, list):
97 raise ValueError(_(u"forums arguments must be a list of forums"))
98 if forums_elt is None:
99 forums_elt = domish.Element((NS_FORUMS, u'forums'))
100 assert names is None
101 names = set()
102 else:
103 if names is None or forums_elt.name != u'forums':
104 raise exceptions.InternalError(u'invalid forums or names')
105 assert names is not None
106
107 for forum in forums:
108 if not isinstance(forum, dict):
109 raise ValueError(_(u"A forum item must be a dictionary"))
110 forum_elt = forums_elt.addElement('forum')
111
112 for key, value in forum.iteritems():
113 if key == u'name' and key in names:
114 raise exceptions.ConflictError(_(u"following forum name is not unique: {name}").format(name=key))
115 if key == u'uri' and not value.strip():
116 log.info(_(u"creating missing forum node"))
117 forum_node = FORUM_TOPICS_NODE_TPL.format(node=node, uuid=shortuuid.uuid())
118 yield self._p.createNode(client, service, forum_node, self._node_options)
119 value = uri.buildXMPPUri(u'pubsub',
120 path=service.full(),
121 node=forum_node)
122 if key in FORUM_ATTR:
123 forum_elt[key] = value.strip()
124 elif key in FORUM_SUB_ELTS:
125 forum_elt.addElement(key, content=value)
126 elif key == u'sub-forums':
127 sub_forums_elt = forum_elt.addElement(u'forums')
128 yield self._createForums(client, value, service, node, sub_forums_elt, names=names)
129 else:
130 log.warning(_(u"Unknown forum attribute: {key}").format(key=key))
131 if not forum_elt.getAttribute(u'title'):
132 name = forum_elt.getAttribute(u'name')
133 if name:
134 forum_elt[u'title'] = name
135 else:
136 raise ValueError(_(u"forum need a title or a name"))
137 if not forum_elt.getAttribute(u'uri') and not forum_elt.children:
138 raise ValueError(_(u"forum need uri or sub-forums"))
139 defer.returnValue(forums_elt)
140
141 def _parseForums(self, parent_elt=None, forums=None):
142 """recursivly parse a <forums> elements and return corresponding forums data
143
144 @param item(domish.Element): item with <forums> element
145 @param parent_elt(domish.Element, None): element to parse
146 @return (list): parsed data
147 @raise ValueError: item is invalid
148 """
149 if parent_elt.name == u'item':
150 forums = []
151 try:
152 forums_elt = next(parent_elt.elements(NS_FORUMS, u'forums'))
153 except StopIteration:
154 raise ValueError(_(u"missing <forums> element"))
155 else:
156 forums_elt = parent_elt
157 if forums is None:
158 raise exceptions.InternalError(u'expected forums')
159 if forums_elt.name != 'forums':
160 raise ValueError(_(u'Unexpected element: {xml}').format(xml=forums_elt.toXml()))
161 for forum_elt in forums_elt.elements():
162 if forum_elt.name == 'forum':
163 data = {}
164 for attrib in FORUM_ATTR.intersection(forum_elt.attributes):
165 data[attrib] = forum_elt[attrib]
166 unknown = set(forum_elt.attributes).difference(FORUM_ATTR)
167 if unknown:
168 log.warning(_(u"Following attributes are unknown: {unknown}").format(unknown=unknown))
169 for elt in forum_elt.elements():
170 if elt.name in FORUM_SUB_ELTS:
171 data[elt.name] = unicode(elt)
172 elif elt.name == u'forums':
173 sub_forums = data[u'sub-forums'] = []
174 self._parseForums(elt, sub_forums)
175 if not u'title' in data or not {u'uri', u'sub-forums'}.intersection(data):
176 log.warning(_(u"invalid forum, ignoring: {xml}").format(xml=forum_elt.toXml()))
177 else:
178 forums.append(data)
179 else:
180 log.warning(_(u"unkown forums sub element: {xml}").format(xml=forum_elt))
181
182 return forums
183
184 def _get(self, service=None, node=None, forums_key=None, profile_key=C.PROF_KEY_NONE):
185 client = self.host.getClient(profile_key)
186 if service.strip():
187 service = jid.JID(service)
188 else:
189 service = None
190 if not node.strip():
191 node = None
192 d=self.get(client, service, node, forums_key or None)
193 d.addCallback(lambda data: json.dumps(data))
194 return d
195
196 @defer.inlineCallbacks
197 def get(self, client, service=None, node=None, forums_key=None):
198 if service is None:
199 service = client.pubsub_service
200 if node is None:
201 node = NS_FORUMS
202 if forums_key is None:
203 forums_key = u'default'
204 items_data = yield self._p.getItems(client, service, node, item_ids=[forums_key])
205 item = items_data[0][0]
206 # we have the item and need to convert it to json
207 forums = self._parseForums(item)
208 defer.returnValue(forums)
209
210 def _set(self, forums, service=None, node=None, forums_key=None, profile_key=C.PROF_KEY_NONE):
211 client = self.host.getClient(profile_key)
212 forums = json.loads(forums)
213 if service.strip():
214 service = jid.JID(service)
215 else:
216 service = None
217 if not node.strip():
218 node = None
219 return self.set(client, forums, service, node, forums_key or None)
220
221 @defer.inlineCallbacks
222 def set(self, client, forums, service=None, node=None, forums_key=None):
223 """create or replace forums structure
224
225 @param forums(list): list of dictionary as follow:
226 a dictionary represent a forum metadata, with the following keys:
227 - title: title of the forum
228 - name: short name (unique in those forums) for the forum
229 - main-language: main language to be use in the forums
230 - uri: XMPP uri to the microblog node hosting the forum
231 - short-desc: short description of the forum (in main-language)
232 - desc: long description of the forum (in main-language)
233 - sub-forums: a list of sub-forums with the same structure
234 title or name is needed, and uri or sub-forums
235 @param forums_key(unicode, None): key (i.e. item id) of the forums
236 may be used to store different forums structures for different languages
237 None to use "default"
238 """
239 if service is None:
240 service = client.pubsub_service
241 if node is None:
242 node = NS_FORUMS
243 if forums_key is None:
244 forums_key = u'default'
245 forums_elt = yield self._createForums(client, forums, service, node)
246 yield self._p.sendItem(client, service, node, forums_elt, item_id=forums_key)
247
248 def _getTopics(self, service, node, extra=None, profile_key=C.PROF_KEY_NONE):
249 client = self.host.getClient(profile_key)
250 extra = self._p.parseExtra(extra)
251 d = self.getTopics(client, jid.JID(service), node, rsm_request=extra.rsm_request, extra=extra.extra)
252 d.addCallback(lambda(topics, metadata): (topics, {k: unicode(v) for k,v in metadata.iteritems()}))
253 return d
254
255 @defer.inlineCallbacks
256 def getTopics(self, client, service, node, rsm_request=None, extra=None):
257 """retrieve topics data
258
259 Topics are simple microblog URIs with some metadata duplicated from first post
260 """
261 topics_data = yield self._p.getItems(client, service, node, rsm_request=rsm_request, extra=extra)
262 topics = []
263 item_elts, metadata = topics_data
264 for item_elt in item_elts:
265 topic_elt = next(item_elt.elements(NS_FORUMS, u'topic'))
266 title_elt = next(topic_elt.elements(NS_FORUMS, u'title'))
267 topic = {u'uri': topic_elt[u'uri'],
268 u'author': topic_elt[u'author'],
269 u'title': unicode(title_elt)}
270 topics.append(topic)
271 defer.returnValue((topics, metadata))
272
273 def _createTopic(self, service, node, mb_data, profile_key):
274 client = self.host.getClient(profile_key)
275 return self.createTopic(client, jid.JID(service), node, mb_data)
276
277 @defer.inlineCallbacks
278 def createTopic(self, client, service, node, mb_data):
279 try:
280 title = mb_data[u'title']
281 if not u'content' in mb_data:
282 raise KeyError(u'content')
283 except KeyError as e:
284 raise exceptions.DataError(u"missing mandatory data: {key}".format(key=e.args[0]))
285
286 topic_node = FORUM_TOPIC_NODE_TPL.format(node=node, uuid=shortuuid.uuid())
287 yield self._p.createNode(client, service, topic_node, self._node_options)
288 self._m.send(client, mb_data, service, topic_node)
289 topic_uri = uri.buildXMPPUri(u'pubsub',
290 subtype=u'microblog',
291 path=service.full(),
292 node=topic_node)
293 topic_elt = domish.Element((NS_FORUMS, 'topic'))
294 topic_elt[u'uri'] = topic_uri
295 topic_elt[u'author'] = client.jid.userhost()
296 topic_elt.addElement(u'title', content = title)
297 yield self._p.sendItem(client, service, node, topic_elt)