1# Copyright (C) 2020 Philipp Hörist <philipp AT hoerist.com> 2# 3# This file is part of nbxmpp. 4# 5# This program is free software; you can redistribute it and/or 6# modify it under the terms of the GNU General Public License 7# as published by the Free Software Foundation; either version 3 8# of the License, or (at your option) any later version. 9# 10# This program is distributed in the hope that it will be useful, 11# but WITHOUT ANY WARRANTY; without even the implied warranty of 12# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13# GNU General Public License for more details. 14# 15# You should have received a copy of the GNU General Public License 16# along with this program; If not, see <http://www.gnu.org/licenses/>. 17 18from collections import namedtuple 19 20from nbxmpp.task import iq_request_task 21from nbxmpp.errors import is_error 22from nbxmpp.errors import PubSubStanzaError 23from nbxmpp.errors import MalformedStanzaError 24from nbxmpp.structs import StanzaHandler 25from nbxmpp.structs import PubSubEventData 26from nbxmpp.structs import CommonResult 27from nbxmpp.protocol import Iq 28from nbxmpp.protocol import Node 29from nbxmpp.namespaces import Namespace 30from nbxmpp.modules.base import BaseModule 31from nbxmpp.modules.util import process_response 32from nbxmpp.modules.util import raise_if_error 33from nbxmpp.modules.util import finalize 34from nbxmpp.modules.dataforms import extend_form 35 36 37class PubSub(BaseModule): 38 def __init__(self, client): 39 BaseModule.__init__(self, client) 40 41 self._client = client 42 self.handlers = [ 43 StanzaHandler(name='message', 44 callback=self._process_pubsub_base, 45 ns=Namespace.PUBSUB_EVENT, 46 priority=15), 47 ] 48 49 def _process_pubsub_base(self, _client, stanza, properties): 50 properties.pubsub = True 51 event = stanza.getTag('event', namespace=Namespace.PUBSUB_EVENT) 52 53 delete = event.getTag('delete') 54 if delete is not None: 55 node = delete.getAttr('node') 56 properties.pubsub_event = PubSubEventData( 57 node, deleted=True) 58 return 59 60 purge = event.getTag('purge') 61 if purge is not None: 62 node = purge.getAttr('node') 63 properties.pubsub_event = PubSubEventData(node, purged=True) 64 return 65 66 items = event.getTag('items') 67 if items is not None: 68 node = items.getAttr('node') 69 70 retract = items.getTag('retract') 71 if retract is not None: 72 id_ = retract.getAttr('id') 73 properties.pubsub_event = PubSubEventData( 74 node, id_, retracted=True) 75 return 76 77 if len(items.getChildren()) != 1: 78 self._log.warning('PubSub event with != 1 item') 79 self._log.warning(stanza) 80 return 81 82 item = items.getTag('item') 83 if item is None: 84 self._log.warning('No item node found') 85 self._log.warning(stanza) 86 return 87 id_ = item.getAttr('id') 88 properties.pubsub_event = PubSubEventData(node, id_, item) 89 90 @iq_request_task 91 def request_item(self, node, id_, jid=None): 92 task = yield 93 94 response = yield _make_pubsub_request(node, id_=id_, jid=jid) 95 96 if response.isError(): 97 raise PubSubStanzaError(response) 98 99 item = _get_pubsub_item(response, node, id_) 100 yield task.set_result(item) 101 102 @iq_request_task 103 def request_items(self, node, max_items=None, jid=None): 104 _task = yield 105 106 response = yield _make_pubsub_request(node, 107 max_items=max_items, 108 jid=jid) 109 110 if response.isError(): 111 raise PubSubStanzaError(response) 112 113 yield _get_pubsub_items(response, node) 114 115 @iq_request_task 116 def publish(self, 117 node, 118 item, 119 id_=None, 120 options=None, 121 jid=None, 122 force_node_options=False): 123 124 _task = yield 125 126 request = _make_publish_request(node, item, id_, options, jid) 127 response = yield request 128 129 if response.isError(): 130 error = PubSubStanzaError(response) 131 if (not force_node_options or 132 error.app_condition != 'precondition-not-met'): 133 raise error 134 135 result = yield self.reconfigure_node(node, options, jid) 136 if is_error(result): 137 raise result 138 139 response = yield request 140 if response.isError(): 141 raise PubSubStanzaError(response) 142 143 jid = response.getFrom() 144 item_id = _get_published_item_id(response, node, id_) 145 yield PubSubPublishResult(jid, node, item_id) 146 147 @iq_request_task 148 def get_access_model(self, node): 149 _task = yield 150 151 self._log.info('Request access model') 152 153 result = yield self.get_node_configuration(node) 154 155 raise_if_error(result) 156 157 yield result.form['pubsub#access_model'].value 158 159 @iq_request_task 160 def set_access_model(self, node, model): 161 task = yield 162 163 if model not in ('open', 'presence'): 164 raise ValueError('Invalid access model') 165 166 result = yield self.get_node_configuration(node) 167 168 raise_if_error(result) 169 170 try: 171 access_model = result.form['pubsub#access_model'].value 172 except Exception: 173 yield task.set_error('warning', 174 condition='access-model-not-supported') 175 176 if access_model == model: 177 jid = self._client.get_bound_jid().new_as_bare() 178 yield CommonResult(jid=jid) 179 180 result.form['pubsub#access_model'].value = model 181 182 self._log.info('Set access model %s', model) 183 184 result = yield self.set_node_configuration(node, result.form) 185 186 yield finalize(task, result) 187 188 @iq_request_task 189 def retract(self, node, id_, jid=None, notify=True): 190 _task = yield 191 192 response = yield _make_retract_request(node, id_, jid, notify) 193 yield process_response(response) 194 195 @iq_request_task 196 def delete(self, node, jid=None): 197 _task = yield 198 199 response = yield _make_delete_request(node, jid) 200 yield process_response(response) 201 202 @iq_request_task 203 def reconfigure_node(self, node, options, jid=None): 204 _task = yield 205 206 result = yield self.get_node_configuration(node, jid) 207 if is_error(result): 208 raise result 209 210 _apply_options(result.form, options) 211 result = yield self.set_node_configuration(node, result.form, jid) 212 yield result 213 214 @iq_request_task 215 def set_node_configuration(self, node, form, jid=None): 216 _task = yield 217 218 response = yield _make_node_configuration(node, form, jid) 219 yield process_response(response) 220 221 @iq_request_task 222 def get_node_configuration(self, node, jid=None): 223 _task = yield 224 225 response = yield _make_node_configuration_request(node, jid) 226 227 if response.isError(): 228 raise PubSubStanzaError(response) 229 230 jid = response.getFrom() 231 form = _get_configure_form(response, node) 232 yield PubSubNodeConfigurationResult(jid=jid, node=node, form=form) 233 234 235def get_pubsub_request(jid, node, id_=None, max_items=None): 236 query = Iq('get', to=jid) 237 pubsub = query.addChild('pubsub', namespace=Namespace.PUBSUB) 238 items = pubsub.addChild('items', {'node': node}) 239 if max_items is not None: 240 items.setAttr('max_items', max_items) 241 if id_ is not None: 242 items.addChild('item', {'id': id_}) 243 return query 244 245 246def get_pubsub_item(stanza): 247 pubsub_node = stanza.getTag('pubsub') 248 items_node = pubsub_node.getTag('items') 249 return items_node.getTag('item') 250 251 252def get_pubsub_items(stanza, node=None): 253 pubsub_node = stanza.getTag('pubsub') 254 items_node = pubsub_node.getTag('items') 255 if node is not None and items_node.getAttr('node') != node: 256 return None 257 258 if items_node is not None: 259 return items_node.getTags('item') 260 return None 261 262 263def get_publish_options(config): 264 options = Node(Namespace.DATA + ' x', attrs={'type': 'submit'}) 265 field = options.addChild('field', 266 attrs={'var': 'FORM_TYPE', 'type': 'hidden'}) 267 field.setTagData('value', Namespace.PUBSUB_PUBLISH_OPTIONS) 268 269 for var, value in config.items(): 270 field = options.addChild('field', attrs={'var': var}) 271 field.setTagData('value', value) 272 return options 273 274 275def _get_pubsub_items(response, node): 276 pubsub_node = response.getTag('pubsub', namespace=Namespace.PUBSUB) 277 if pubsub_node is None: 278 raise MalformedStanzaError('pubsub node missing', response) 279 280 items_node = pubsub_node.getTag('items') 281 if items_node is None: 282 raise MalformedStanzaError('items node missing', response) 283 284 if items_node.getAttr('node') != node: 285 raise MalformedStanzaError('invalid node attr', response) 286 287 return items_node.getTags('item') 288 289 290def _get_pubsub_item(response, node, id_): 291 items = _get_pubsub_items(response, node) 292 293 if len(items) > 1: 294 raise MalformedStanzaError('multiple items found', response) 295 296 if not items: 297 return None 298 299 item = items[0] 300 if item.getAttr('id') != id_: 301 raise MalformedStanzaError('invalid item id', response) 302 303 return item 304 305 306def _make_pubsub_request(node, id_=None, max_items=None, jid=None): 307 query = Iq('get', to=jid) 308 pubsub = query.addChild('pubsub', namespace=Namespace.PUBSUB) 309 items = pubsub.addChild('items', {'node': node}) 310 if max_items is not None: 311 items.setAttr('max_items', max_items) 312 if id_ is not None: 313 items.addChild('item', {'id': id_}) 314 return query 315 316 317def _get_configure_form(response, node): 318 pubsub = response.getTag('pubsub', namespace=Namespace.PUBSUB_OWNER) 319 if pubsub is None: 320 raise MalformedStanzaError('pubsub node missing', response) 321 322 configure = pubsub.getTag('configure') 323 if configure is None: 324 raise MalformedStanzaError('configure node missing', response) 325 326 if node != configure.getAttr('node'): 327 raise MalformedStanzaError('invalid node attribute', response) 328 329 forms = configure.getTags('x', namespace=Namespace.DATA) 330 for form in forms: 331 dataform = extend_form(node=form) 332 form_type = dataform.vars.get('FORM_TYPE') 333 if form_type is None or form_type.value != Namespace.PUBSUB_CONFIG: 334 continue 335 336 return dataform 337 338 raise MalformedStanzaError('no valid form type found', response) 339 340 341def _get_published_item_id(response, node, id_): 342 pubsub = response.getTag('pubsub', namespace=Namespace.PUBSUB) 343 if pubsub is None: 344 # https://xmpp.org/extensions/xep-0060.html#publisher-publish-success 345 # If the publish request did not include an ItemID, 346 # the IQ-result SHOULD include an empty <item/> element 347 # that specifies the ItemID of the published item. 348 # 349 # If the server did not add a payload we assume the item was 350 # published with the id we requested 351 return id_ 352 353 publish = pubsub.getTag('publish') 354 if publish is None: 355 raise MalformedStanzaError('publish node missing', response) 356 357 if node != publish.getAttr('node'): 358 raise MalformedStanzaError('invalid node attribute', response) 359 360 item = publish.getTag('item') 361 if item is None: 362 raise MalformedStanzaError('item node missing', response) 363 364 item_id = item.getAttr('id') 365 if id_ is not None and item_id != id_: 366 raise MalformedStanzaError('invalid item id', response) 367 368 return item_id 369 370 371def _make_publish_request(node, item, id_, options, jid): 372 query = Iq('set', to=jid) 373 pubsub = query.addChild('pubsub', namespace=Namespace.PUBSUB) 374 publish = pubsub.addChild('publish', {'node': node}) 375 attrs = {} 376 if id_ is not None: 377 attrs = {'id': id_} 378 publish.addChild('item', attrs, [item]) 379 if options: 380 publish = pubsub.addChild('publish-options') 381 publish.addChild(node=_make_publish_options(options)) 382 return query 383 384 385def _make_publish_options(options): 386 data = Node(Namespace.DATA + ' x', attrs={'type': 'submit'}) 387 field = data.addChild('field', attrs={'var': 'FORM_TYPE', 'type': 'hidden'}) 388 field.setTagData('value', Namespace.PUBSUB_PUBLISH_OPTIONS) 389 390 for var, value in options.items(): 391 field = data.addChild('field', attrs={'var': var}) 392 field.setTagData('value', value) 393 return data 394 395 396def _make_retract_request(node, id_, jid, notify): 397 query = Iq('set', to=jid) 398 pubsub = query.addChild('pubsub', namespace=Namespace.PUBSUB) 399 attrs = {'node': node} 400 if notify: 401 attrs['notify'] = 'true' 402 retract = pubsub.addChild('retract', attrs=attrs) 403 retract.addChild('item', {'id': id_}) 404 return query 405 406 407def _make_delete_request(node, jid): 408 query = Iq('set', to=jid) 409 pubsub = query.addChild('pubsub', namespace=Namespace.PUBSUB_OWNER) 410 411 pubsub.addChild('delete', attrs={'node': node}) 412 return query 413 414 415def _make_node_configuration(node, form, jid): 416 query = Iq('set', to=jid) 417 pubsub = query.addChild('pubsub', namespace=Namespace.PUBSUB_OWNER) 418 configure = pubsub.addChild('configure', {'node': node}) 419 form.setAttr('type', 'submit') 420 configure.addChild(node=form) 421 return query 422 423 424def _make_node_configuration_request(node, jid): 425 query = Iq('get', to=jid) 426 pubsub = query.addChild('pubsub', namespace=Namespace.PUBSUB_OWNER) 427 pubsub.addChild('configure', {'node': node}) 428 return query 429 430 431def _apply_options(form, options): 432 for var, value in options.items(): 433 try: 434 field = form[var] 435 except KeyError: 436 pass 437 else: 438 field.value = value 439 440 441PubSubNodeConfigurationResult = namedtuple('PubSubConfigResult', 442 'jid node form') 443 444PubSubConfigResult = namedtuple('PubSubConfigResult', 445 'jid node form') 446 447PubSubPublishResult = namedtuple('PubSubPublishResult', 448 'jid node id') 449