2484
+ − 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 )