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):