Mercurial > libervia-backend
comparison libervia/backend/plugins/plugin_xep_0050.py @ 4184:50c919dfe61b
plugin XEP-0050: small code quality improvements + add `C.ENTITY_ADMINS` magic key to allow administrators profiles
author | Goffi <goffi@goffi.org> |
---|---|
date | Sun, 10 Dec 2023 15:08:08 +0100 |
parents | 4b842c1fb686 |
children |
comparison
equal
deleted
inserted
replaced
4183:6784d07b99c8 | 4184:50c919dfe61b |
---|---|
1 #!/usr/bin/env python3 | 1 #!/usr/bin/env python3 |
2 | 2 |
3 # SàT plugin for Ad-Hoc Commands (XEP-0050) | 3 # Libervia plugin for Ad-Hoc Commands (XEP-0050) |
4 # Copyright (C) 2009-2021 Jérôme Poisson (goffi@goffi.org) | 4 # Copyright (C) 2009-2023 Jérôme Poisson (goffi@goffi.org) |
5 | 5 |
6 # This program is free software: you can redistribute it and/or modify | 6 # This program is free software: you can redistribute it and/or modify |
7 # it under the terms of the GNU Affero General Public License as published by | 7 # it under the terms of the GNU Affero General Public License as published by |
8 # the Free Software Foundation, either version 3 of the License, or | 8 # the Free Software Foundation, either version 3 of the License, or |
9 # (at your option) any later version. | 9 # (at your option) any later version. |
17 # along with this program. If not, see <http://www.gnu.org/licenses/>. | 17 # along with this program. If not, see <http://www.gnu.org/licenses/>. |
18 | 18 |
19 | 19 |
20 from collections import namedtuple | 20 from collections import namedtuple |
21 from uuid import uuid4 | 21 from uuid import uuid4 |
22 from typing import List, Optional | 22 from typing import Callable |
23 | 23 |
24 from zope.interface import implementer | 24 from zope.interface import implementer |
25 from twisted.words.protocols.jabber import jid | 25 from twisted.words.protocols.jabber import jid |
26 from twisted.words.protocols import jabber | 26 from twisted.words.protocols.jabber.error import StanzaError |
27 from twisted.words.protocols.jabber.xmlstream import XMPPHandler | 27 from twisted.words.protocols.jabber.xmlstream import XMPPHandler |
28 from twisted.words.xish import domish | 28 from twisted.words.xish import domish |
29 from twisted.internet import defer | 29 from twisted.internet import defer |
30 from wokkel import disco, iwokkel, data_form | 30 from wokkel import disco, iwokkel, data_form |
31 from libervia.backend.core.i18n import _, D_ | 31 from libervia.backend.core.i18n import _, D_ |
32 from libervia.backend.core.constants import Const as C | 32 from libervia.backend.core.constants import Const as C |
33 from libervia.backend.core.log import getLogger | 33 from libervia.backend.core.log import getLogger |
34 from libervia.backend.core.xmpp import SatXMPPEntity | 34 from libervia.backend.core.xmpp import SatXMPPClient, SatXMPPEntity |
35 from libervia.backend.core import exceptions | 35 from libervia.backend.core import exceptions |
36 from libervia.backend.memory.memory import Sessions | 36 from libervia.backend.memory.memory import Sessions |
37 from libervia.backend.tools import xml_tools, utils | 37 from libervia.backend.tools import xml_tools, utils |
38 from libervia.backend.tools.common import data_format | 38 from libervia.backend.tools.common import data_format |
39 | 39 |
66 C.PI_HANDLER: "yes", | 66 C.PI_HANDLER: "yes", |
67 C.PI_DESCRIPTION: _("""Implementation of Ad-Hoc Commands"""), | 67 C.PI_DESCRIPTION: _("""Implementation of Ad-Hoc Commands"""), |
68 } | 68 } |
69 | 69 |
70 | 70 |
71 Status = namedtuple("Status", ("EXECUTING", "COMPLETED", "CANCELED")) | |
72 Action = namedtuple("Action", ("EXECUTE", "CANCEL", "NEXT", "PREV")) | |
73 Note = namedtuple("Note", ("INFO", "WARN", "ERROR")) | |
74 Error = namedtuple( | |
75 "Error", | |
76 ( | |
77 "MALFORMED_ACTION", | |
78 "BAD_ACTION", | |
79 "BAD_LOCALE", | |
80 "BAD_PAYLOAD", | |
81 "BAD_SESSIONID", | |
82 "SESSION_EXPIRED", | |
83 "FORBIDDEN", | |
84 "ITEM_NOT_FOUND", | |
85 "FEATURE_NOT_IMPLEMENTED", | |
86 "INTERNAL", | |
87 ), | |
88 ) | |
89 | |
90 | |
71 class AdHocError(Exception): | 91 class AdHocError(Exception): |
72 def __init__(self, error_const): | 92 def __init__(self, error_const): |
73 """ Error to be used from callback | 93 """Error to be used from callback |
74 @param error_const: one of XEP_0050.ERROR | 94 @param error_const: one of XEP_0050.ERROR |
75 """ | 95 """ |
76 assert error_const in XEP_0050.ERROR | 96 assert error_const in XEP_0050.ERROR |
77 self.callback_error = error_const | 97 self.callback_error = error_const |
78 | 98 |
79 | 99 |
80 @implementer(iwokkel.IDisco) | 100 @implementer(iwokkel.IDisco) |
81 class AdHocCommand(XMPPHandler): | 101 class AdHocCommand(XMPPHandler): |
82 | 102 def __init__( |
83 def __init__(self, callback, label, node, features, timeout, | 103 self, |
84 allowed_jids, allowed_groups, allowed_magics, forbidden_jids, | 104 callback, |
85 forbidden_groups): | 105 label, |
106 node, | |
107 features, | |
108 timeout, | |
109 allowed_jids, | |
110 allowed_groups, | |
111 allowed_magics, | |
112 forbidden_jids, | |
113 forbidden_groups, | |
114 ): | |
86 XMPPHandler.__init__(self) | 115 XMPPHandler.__init__(self) |
87 self.callback = callback | 116 self.callback = callback |
88 self.label = label | 117 self.label = label |
89 self.node = node | 118 self.node = node |
90 self.features = [disco.DiscoFeature(feature) for feature in features] | 119 self.features = [disco.DiscoFeature(feature) for feature in features] |
101 | 130 |
102 def getName(self, xml_lang=None): | 131 def getName(self, xml_lang=None): |
103 return self.label | 132 return self.label |
104 | 133 |
105 def is_authorised(self, requestor): | 134 def is_authorised(self, requestor): |
106 if "@ALL@" in self.allowed_magics: | 135 if C.ENTITY_ALL in self.allowed_magics: |
107 return True | 136 return True |
108 forbidden = set(self.forbidden_jids) | 137 forbidden = set(self.forbidden_jids) |
109 for group in self.forbidden_groups: | 138 for group in self.forbidden_groups: |
110 forbidden.update(self.client.roster.get_jids_from_group(group)) | 139 forbidden.update(self.client.roster.get_jids_from_group(group)) |
111 if requestor.userhostJID() in forbidden: | 140 if requestor.userhostJID() in forbidden: |
113 allowed = set(self.allowed_jids) | 142 allowed = set(self.allowed_jids) |
114 for group in self.allowed_groups: | 143 for group in self.allowed_groups: |
115 try: | 144 try: |
116 allowed.update(self.client.roster.get_jids_from_group(group)) | 145 allowed.update(self.client.roster.get_jids_from_group(group)) |
117 except exceptions.UnknownGroupError: | 146 except exceptions.UnknownGroupError: |
118 log.warning(_("The groups [{group}] is unknown for profile [{profile}])") | 147 log.warning( |
119 .format(group=group, profile=self.client.profile)) | 148 _("The groups [{group}] is unknown for profile [{profile}])").format( |
149 group=group, profile=self.client.profile | |
150 ) | |
151 ) | |
120 if requestor.userhostJID() in allowed: | 152 if requestor.userhostJID() in allowed: |
121 return True | 153 return True |
122 return False | 154 return False |
123 | 155 |
124 def getDiscoInfo(self, requestor, target, nodeIdentifier=""): | 156 def getDiscoInfo(self, requestor, target, nodeIdentifier=""): |
130 return [disco.DiscoFeature(NS_COMMANDS)] + self.features | 162 return [disco.DiscoFeature(NS_COMMANDS)] + self.features |
131 | 163 |
132 def getDiscoItems(self, requestor, target, nodeIdentifier=""): | 164 def getDiscoItems(self, requestor, target, nodeIdentifier=""): |
133 return [] | 165 return [] |
134 | 166 |
135 def _sendAnswer(self, callback_data, session_id, request): | 167 def _sendAnswer( |
136 """ Send result of the command | 168 self, |
169 callback_data: tuple[ | |
170 tuple[domish.Element, None], | |
171 Status, | |
172 list[str] | None, | |
173 tuple[Note, str] | None, | |
174 ], | |
175 session_id, | |
176 request, | |
177 ) -> None: | |
178 """Send result of the command | |
137 | 179 |
138 @param callback_data: tuple (payload, status, actions, note) with: | 180 @param callback_data: tuple (payload, status, actions, note) with: |
139 - payload (domish.Element, None) usualy containing data form | 181 - payload: usualy containing data form |
140 - status: current status, see XEP_0050.STATUS | 182 - status: current status |
141 - actions(list[str], None): list of allowed actions (see XEP_0050.ACTION). | 183 - actions: list of allowed actions (see XEP_0050.ACTION). |
142 First action is the default one. Default to EXECUTE | 184 First action is the default one. Default to EXECUTE |
143 - note(tuple[str, unicode]): optional additional note: either None or a | 185 - note: optional additional note: either None or a tuple with (note type, |
144 tuple with (note type, human readable string), "note type" being in | 186 human readable string) |
145 XEP_0050.NOTE | |
146 @param session_id: current session id | 187 @param session_id: current session id |
147 @param request: original request (domish.Element) | 188 @param request: original request (domish.Element) |
148 @return: deferred | 189 @return: deferred |
149 """ | 190 """ |
150 payload, status, actions, note = callback_data | 191 payload, status, actions, note = callback_data |
179 self.client.send(result) | 220 self.client.send(result) |
180 if status in (XEP_0050.STATUS.COMPLETED, XEP_0050.STATUS.CANCELED): | 221 if status in (XEP_0050.STATUS.COMPLETED, XEP_0050.STATUS.CANCELED): |
181 del self.sessions[session_id] | 222 del self.sessions[session_id] |
182 | 223 |
183 def _sendError(self, error_constant, session_id, request): | 224 def _sendError(self, error_constant, session_id, request): |
184 """ Send error stanza | 225 """Send error stanza |
185 | 226 |
186 @param error_constant: one of XEP_OO50.ERROR | 227 @param error_constant: one of XEP_OO50.ERROR |
187 @param request: original request (domish.Element) | 228 @param request: original request (domish.Element) |
188 """ | 229 """ |
189 xmpp_condition, cmd_condition = error_constant | 230 xmpp_condition, cmd_condition = error_constant |
190 iq_elt = jabber.error.StanzaError(xmpp_condition).toResponse(request) | 231 iq_elt = StanzaError(xmpp_condition).toResponse(request) |
191 if cmd_condition: | 232 if cmd_condition: |
192 error_elt = next(iq_elt.elements(None, "error")) | 233 error_elt = next(iq_elt.elements(None, "error")) |
193 error_elt.addElement(cmd_condition, NS_COMMANDS) | 234 error_elt.addElement(cmd_condition, NS_COMMANDS) |
194 self.client.send(iq_elt) | 235 self.client.send(iq_elt) |
195 del self.sessions[session_id] | 236 del self.sessions[session_id] |
235 ) | 276 ) |
236 d.addCallback(self._sendAnswer, session_id, command_elt.parent) | 277 d.addCallback(self._sendAnswer, session_id, command_elt.parent) |
237 d.addErrback(self._request_eb, command_elt.parent, session_id) | 278 d.addErrback(self._request_eb, command_elt.parent, session_id) |
238 | 279 |
239 | 280 |
240 class XEP_0050(object): | 281 class XEP_0050: |
241 STATUS = namedtuple("Status", ("EXECUTING", "COMPLETED", "CANCELED"))( | 282 STATUS = Status("executing", "completed", "canceled") |
242 "executing", "completed", "canceled" | 283 ACTION = Action("execute", "cancel", "next", "prev") |
243 ) | 284 NOTE = Note("info", "warn", "error") |
244 ACTION = namedtuple("Action", ("EXECUTE", "CANCEL", "NEXT", "PREV"))( | 285 ERROR = Error( |
245 "execute", "cancel", "next", "prev" | |
246 ) | |
247 NOTE = namedtuple("Note", ("INFO", "WARN", "ERROR"))("info", "warn", "error") | |
248 ERROR = namedtuple( | |
249 "Error", | |
250 ( | |
251 "MALFORMED_ACTION", | |
252 "BAD_ACTION", | |
253 "BAD_LOCALE", | |
254 "BAD_PAYLOAD", | |
255 "BAD_SESSIONID", | |
256 "SESSION_EXPIRED", | |
257 "FORBIDDEN", | |
258 "ITEM_NOT_FOUND", | |
259 "FEATURE_NOT_IMPLEMENTED", | |
260 "INTERNAL", | |
261 ), | |
262 )( | |
263 ("bad-request", "malformed-action"), | 286 ("bad-request", "malformed-action"), |
264 ("bad-request", "bad-action"), | 287 ("bad-request", "bad-action"), |
265 ("bad-request", "bad-locale"), | 288 ("bad-request", "bad-locale"), |
266 ("bad-request", "bad-payload"), | 289 ("bad-request", "bad-payload"), |
267 ("bad-request", "bad-sessionid"), | 290 ("bad-request", "bad-sessionid"), |
307 (D_("Service"), D_("Commands")), | 330 (D_("Service"), D_("Commands")), |
308 self._commands_menu, | 331 self._commands_menu, |
309 security_limit=2, | 332 security_limit=2, |
310 help_string=D_("Execute ad-hoc commands"), | 333 help_string=D_("Execute ad-hoc commands"), |
311 ) | 334 ) |
312 host.register_namespace('commands', NS_COMMANDS) | 335 host.register_namespace("commands", NS_COMMANDS) |
313 | 336 |
314 def get_handler(self, client): | 337 def get_handler(self, client): |
315 return XEP_0050_handler(self) | 338 return XEP_0050_handler(self) |
316 | 339 |
317 def profile_connected(self, client): | 340 def profile_connected(self, client): |
318 # map from node to AdHocCommand instance | 341 # map from node to AdHocCommand instance |
319 client._XEP_0050_commands = {} | 342 client._XEP_0050_commands = {} |
320 if not client.is_component: | 343 if not client.is_component: |
321 self.add_ad_hoc_command(client, self._status_callback, _("Status")) | 344 self.add_ad_hoc_command(client, self._status_callback, _("Status")) |
322 | 345 |
323 def do(self, client, entity, node, action=ACTION.EXECUTE, session_id=None, | 346 def do( |
324 form_values=None, timeout=30): | 347 self, |
348 client, | |
349 entity, | |
350 node, | |
351 action=ACTION.EXECUTE, | |
352 session_id=None, | |
353 form_values=None, | |
354 timeout=30, | |
355 ): | |
325 """Do an Ad-Hoc Command | 356 """Do an Ad-Hoc Command |
326 | 357 |
327 @param entity(jid.JID): entity which will execture the command | 358 @param entity(jid.JID): entity which will execture the command |
328 @param node(unicode): node of the command | 359 @param node(unicode): node of the command |
329 @param action(unicode): one of XEP_0050.ACTION | 360 @param action(unicode): one of XEP_0050.ACTION |
361 @param error_type(unicode): one of XEP_0050.ERROR | 392 @param error_type(unicode): one of XEP_0050.ERROR |
362 """ | 393 """ |
363 raise AdHocError(error_type) | 394 raise AdHocError(error_type) |
364 | 395 |
365 def _items_2_xmlui(self, items, no_instructions): | 396 def _items_2_xmlui(self, items, no_instructions): |
366 """Convert discovery items to XMLUI dialog """ | 397 """Convert discovery items to XMLUI dialog""" |
367 # TODO: manage items on different jids | 398 # TODO: manage items on different jids |
368 form_ui = xml_tools.XMLUI("form", submit_id=self.__requesting_id) | 399 form_ui = xml_tools.XMLUI("form", submit_id=self.__requesting_id) |
369 | 400 |
370 if not no_instructions: | 401 if not no_instructions: |
371 form_ui.addText(_("Please select a command"), "instructions") | 402 form_ui.addText(_("Please select a command"), "instructions") |
415 str(note_elt), | 446 str(note_elt), |
416 ) | 447 ) |
417 ) | 448 ) |
418 | 449 |
419 return command_elt, data | 450 return command_elt, data |
420 | |
421 | 451 |
422 def _commands_answer_2_xmlui(self, iq_elt, session_id, session_data): | 452 def _commands_answer_2_xmlui(self, iq_elt, session_id, session_data): |
423 """Convert command answer to an ui for frontend | 453 """Convert command answer to an ui for frontend |
424 | 454 |
425 @param iq_elt: command result | 455 @param iq_elt: command result |
484 return xmlui | 514 return xmlui |
485 | 515 |
486 form = data_form.Form.fromElement(data_elt) | 516 form = data_form.Form.fromElement(data_elt) |
487 # we add any present note to the instructions | 517 # we add any present note to the instructions |
488 form.instructions.extend(self._merge_notes(notes)) | 518 form.instructions.extend(self._merge_notes(notes)) |
489 return xml_tools.data_form_2_xmlui(form, self.__requesting_id, session_id=session_id) | 519 return xml_tools.data_form_2_xmlui( |
520 form, self.__requesting_id, session_id=session_id | |
521 ) | |
490 | 522 |
491 def _requesting_entity(self, data, profile): | 523 def _requesting_entity(self, data, profile): |
492 def serialise(ret_data): | 524 def serialise(ret_data): |
493 if "xmlui" in ret_data: | 525 if "xmlui" in ret_data: |
494 ret_data["xmlui"] = ret_data["xmlui"].toXml() | 526 ret_data["xmlui"] = ret_data["xmlui"].toXml() |
548 # remote_id is the XEP_0050 sessionid used by answering command | 580 # remote_id is the XEP_0050 sessionid used by answering command |
549 # while session_id is our own session id used with the frontend | 581 # while session_id is our own session id used with the frontend |
550 remote_id = session_data.get("remote_id") | 582 remote_id = session_data.get("remote_id") |
551 | 583 |
552 # we request execute node's command | 584 # we request execute node's command |
553 d = self.do(client, entity, session_data["node"], action=XEP_0050.ACTION.EXECUTE, | 585 d = self.do( |
554 session_id=remote_id, form_values=data_form_values) | 586 client, |
587 entity, | |
588 session_data["node"], | |
589 action=XEP_0050.ACTION.EXECUTE, | |
590 session_id=remote_id, | |
591 form_values=data_form_values, | |
592 ) | |
555 d.addCallback(self._commands_answer_2_xmlui, session_id, session_data) | 593 d.addCallback(self._commands_answer_2_xmlui, session_id, session_data) |
556 d.addCallback(lambda xmlui: {"xmlui": xmlui} if xmlui is not None else {}) | 594 d.addCallback(lambda xmlui: {"xmlui": xmlui} if xmlui is not None else {}) |
557 | 595 |
558 return d | 596 return d |
559 | 597 |
643 xmlui = cb_data["xmlui"] | 681 xmlui = cb_data["xmlui"] |
644 | 682 |
645 xmlui.session_id = session_id | 683 xmlui.session_id = session_id |
646 return xmlui | 684 return xmlui |
647 | 685 |
648 def list(self, client, to_jid): | 686 def list_commands(self, client, to_jid): |
649 """Request available commands | 687 """Request available commands |
650 | 688 |
651 @param to_jid(jid.JID, None): the entity answering the commands | 689 @param to_jid(jid.JID, None): the entity answering the commands |
652 None to use profile's server | 690 None to use profile's server |
653 @return D(disco.DiscoItems): found commands | 691 @return D(disco.DiscoItems): found commands |
668 @param to_jid(jid.JID, None): the entity answering the commands | 706 @param to_jid(jid.JID, None): the entity answering the commands |
669 None to use profile's server | 707 None to use profile's server |
670 @param no_instructions(bool): if True, don't add instructions widget | 708 @param no_instructions(bool): if True, don't add instructions widget |
671 @return D(xml_tools.XMLUI): UI with the commands | 709 @return D(xml_tools.XMLUI): UI with the commands |
672 """ | 710 """ |
673 d = self.list(client, to_jid) | 711 d = self.list_commands(client, to_jid) |
674 d.addCallback(self._items_2_xmlui, no_instructions) | 712 d.addCallback(self._items_2_xmlui, no_instructions) |
675 return d | 713 return d |
676 | 714 |
677 def _sequence(self, sequence, node, service_jid_s="", profile_key=C.PROF_KEY_NONE): | 715 def _sequence(self, sequence, node, service_jid_s="", profile_key=C.PROF_KEY_NONE): |
678 sequence = data_format.deserialise(sequence, type_check=list) | 716 sequence = data_format.deserialise(sequence, type_check=list) |
683 return d | 721 return d |
684 | 722 |
685 async def sequence( | 723 async def sequence( |
686 self, | 724 self, |
687 client: SatXMPPEntity, | 725 client: SatXMPPEntity, |
688 sequence: List[dict], | 726 sequence: list[dict], |
689 node: str, | 727 node: str, |
690 service_jid: Optional[jid.JID] = None, | 728 service_jid: jid.JID | None = None, |
691 ) -> dict: | 729 ) -> dict: |
692 """Send a series of data to an ad-hoc service | 730 """Send a series of data to an ad-hoc service |
693 | 731 |
694 @param sequence: list of values to send | 732 @param sequence: list of values to send |
695 value are specified by a dict mapping var name to value. | 733 value are specified by a dict mapping var name to value. |
696 @param node: node of the ad-hoc commnad | 734 @param node: node of the ad-hoc commnad |
697 @param service_jid: jid of the ad-hoc service | 735 @param service_jid: jid of the ad-hoc service |
698 None to use profile's server | 736 None to use profile's server |
699 @return: data received in final answer | 737 @return: data received in final answer |
700 """ | 738 """ |
739 assert sequence | |
740 answer_data = None | |
701 if service_jid is None: | 741 if service_jid is None: |
702 service_jid = jid.JID(client.jid.host) | 742 service_jid = jid.JID(client.jid.host) |
703 | 743 |
704 session_id = None | 744 session_id = None |
705 | 745 |
712 form_values=data_to_send, | 752 form_values=data_to_send, |
713 ) | 753 ) |
714 __, answer_data = self.parse_command_answer(iq_result_elt) | 754 __, answer_data = self.parse_command_answer(iq_result_elt) |
715 session_id = answer_data.pop("session_id") | 755 session_id = answer_data.pop("session_id") |
716 | 756 |
757 assert answer_data is not None | |
758 | |
717 return answer_data | 759 return answer_data |
718 | 760 |
719 def add_ad_hoc_command(self, client, callback, label, node=None, features=None, | 761 def add_ad_hoc_command( |
720 timeout=600, allowed_jids=None, allowed_groups=None, | 762 self, |
721 allowed_magics=None, forbidden_jids=None, forbidden_groups=None, | 763 client: SatXMPPClient, |
722 ): | 764 callback: Callable, |
765 label: str, | |
766 node: str | None = None, | |
767 features: list[str] | None = None, | |
768 timeout: int = 600, | |
769 allowed_jids: list[jid.JID] | None = None, | |
770 allowed_groups: list[str] | None = None, | |
771 allowed_magics: list[str] | None = None, | |
772 forbidden_jids: list[jid.JID] | None = None, | |
773 forbidden_groups: list[str] | None = None, | |
774 ) -> str: | |
723 """Add an ad-hoc command for the current profile | 775 """Add an ad-hoc command for the current profile |
724 | 776 |
725 @param callback: method associated with this ad-hoc command which return the | 777 @param callback: method associated with this ad-hoc command which return the |
726 payload data (see AdHocCommand._sendAnswer), can return a | 778 payload data (see AdHocCommand._sendAnswer), can return a deferred |
727 deferred | |
728 @param label: label associated with this command on the main menu | 779 @param label: label associated with this command on the main menu |
729 @param node: disco item node associated with this command. None to use | 780 @param node: disco item node associated with this command. None to use |
730 autogenerated node | 781 autogenerated node |
731 @param features: features associated with the payload (list of strings), usualy | 782 @param features: features associated with the payload (list of strings), usualy |
732 data form | 783 data form |
733 @param timeout: delay between two requests before canceling the session (in | 784 @param timeout: delay between two requests before canceling the session (in |
734 seconds) | 785 seconds) |
735 @param allowed_jids: list of allowed entities | 786 @param allowed_jids: list of allowed entities |
736 @param allowed_groups: list of allowed roster groups | 787 @param allowed_groups: list of allowed roster groups |
737 @param allowed_magics: list of allowed magic keys, can be: | 788 @param allowed_magics: list of allowed magic keys, can be: |
738 @ALL@: allow everybody | 789 C.ENTITY_ALL: allow everybody |
739 @PROFILE_BAREJID@: allow only the jid of the profile | 790 C.ENTITY_PROFILE_BARE: allow only the jid of the profile |
791 C.ENTITY_ADMINS: any administrator user | |
740 @param forbidden_jids: black list of entities which can't access this command | 792 @param forbidden_jids: black list of entities which can't access this command |
741 @param forbidden_groups: black list of groups which can't access this command | 793 @param forbidden_groups: black list of groups which can't access this command |
742 @return: node of the added command, useful to remove the command later | 794 @return: node of the added command, useful to remove the command later |
743 """ | 795 """ |
744 # FIXME: "@ALL@" for profile_key seems useless and dangerous | 796 commands = client._XEP_0050_commands |
745 | 797 |
746 if node is None: | 798 if node is None: |
747 node = "%s_%s" % ("COMMANDS", uuid4()) | 799 node = label.lower().replace(" ", "_") |
800 if not node: | |
801 node = f"COMMANDS_{uuid4()}" | |
802 elif node in commands: | |
803 node = f"{node}_{uuid4()}" | |
748 | 804 |
749 if features is None: | 805 if features is None: |
750 features = [data_form.NS_X_DATA] | 806 features = [data_form.NS_X_DATA] |
751 | 807 |
752 if allowed_jids is None: | 808 if allowed_jids is None: |
753 allowed_jids = [] | 809 allowed_jids = [] |
810 else: | |
811 # we don't want to modify the initial list | |
812 allowed_jids = allowed_jids.copy() | |
754 if allowed_groups is None: | 813 if allowed_groups is None: |
755 allowed_groups = [] | 814 allowed_groups = [] |
756 if allowed_magics is None: | 815 if allowed_magics is None: |
757 allowed_magics = ["@PROFILE_BAREJID@"] | 816 allowed_magics = [C.ENTITY_PROFILE_BARE] |
758 if forbidden_jids is None: | 817 if forbidden_jids is None: |
759 forbidden_jids = [] | 818 forbidden_jids = [] |
760 if forbidden_groups is None: | 819 if forbidden_groups is None: |
761 forbidden_groups = [] | 820 forbidden_groups = [] |
762 | 821 |
763 # TODO: manage newly created/removed profiles | 822 # TODO: manage newly created/removed profiles |
764 _allowed_jids = ( | 823 if C.ENTITY_PROFILE_BARE in allowed_magics: |
765 (allowed_jids + [client.jid.userhostJID()]) | 824 allowed_jids += [client.jid.userhostJID()] |
766 if "@PROFILE_BAREJID@" in allowed_magics | 825 # TODO: manage dynamic addition/removal of admin status once possible |
767 else allowed_jids | 826 if C.ENTITY_ADMINS in allowed_magics: |
768 ) | 827 allowed_jids += list(self.host.memory.admin_jids) |
828 | |
769 ad_hoc_command = AdHocCommand( | 829 ad_hoc_command = AdHocCommand( |
770 callback, | 830 callback, |
771 label, | 831 label, |
772 node, | 832 node, |
773 features, | 833 features, |
774 timeout, | 834 timeout, |
775 _allowed_jids, | 835 allowed_jids, |
776 allowed_groups, | 836 allowed_groups, |
777 allowed_magics, | 837 allowed_magics, |
778 forbidden_jids, | 838 forbidden_jids, |
779 forbidden_groups, | 839 forbidden_groups, |
780 ) | 840 ) |
781 ad_hoc_command.setHandlerParent(client) | 841 ad_hoc_command.setHandlerParent(client) |
782 commands = client._XEP_0050_commands | |
783 commands[node] = ad_hoc_command | 842 commands[node] = ad_hoc_command |
843 return node | |
784 | 844 |
785 def on_cmd_request(self, request, client): | 845 def on_cmd_request(self, request, client): |
786 request.handled = True | 846 request.handled = True |
787 requestor = jid.JID(request["from"]) | 847 requestor = jid.JID(request["from"]) |
788 command_elt = next(request.elements(NS_COMMANDS, "command")) | 848 command_elt = next(request.elements(NS_COMMANDS, "command")) |
801 command.on_request(command_elt, requestor, action, sessionid) | 861 command.on_request(command_elt, requestor, action, sessionid) |
802 | 862 |
803 | 863 |
804 @implementer(iwokkel.IDisco) | 864 @implementer(iwokkel.IDisco) |
805 class XEP_0050_handler(XMPPHandler): | 865 class XEP_0050_handler(XMPPHandler): |
806 | |
807 def __init__(self, plugin_parent): | 866 def __init__(self, plugin_parent): |
808 self.plugin_parent = plugin_parent | 867 self.plugin_parent = plugin_parent |
809 | 868 |
810 @property | 869 @property |
811 def client(self): | 870 def client(self): |