plugin XEP-0050: some modernisation + adHocSequence:

improved code by reordering imports, adding some type hints, using standard dict instead
of OrderedDict, etc.

New sequence method/adHocSequence bridge method allow to easily send a sequence of data to
a well known ad-hoc node.
This commit is contained in:
Goffi 2020-06-19 15:35:45 +02:00
commit 762f96acb2

View file

@ -17,34 +17,30 @@
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from sat.core.i18n import _, D_
from sat.core.constants import Const as C
from sat.core.log import getLogger
log = getLogger(__name__)
from collections import namedtuple
from uuid import uuid4
from typing import List, Optional
from zope.interface import implementer
from twisted.words.protocols.jabber import jid
from twisted.words.protocols import jabber
from twisted.words.protocols.jabber.xmlstream import XMPPHandler
from twisted.words.xish import domish
from twisted.internet import defer
from wokkel import disco, iwokkel, data_form
from sat.core.i18n import _, D_
from sat.core.constants import Const as C
from sat.core.log import getLogger
from sat.core.xmpp import SatXMPPEntity
from sat.core import exceptions
from sat.memory.memory import Sessions
from uuid import uuid4
from sat.tools import xml_tools
from sat.tools.common import data_format
from zope.interface import implementer
try:
from twisted.words.protocols.xmlstream import XMPPHandler
except ImportError:
from wokkel.subprotocols import XMPPHandler
log = getLogger(__name__)
from collections import namedtuple
try:
from collections import OrderedDict # only available from python 2.7
except ImportError:
from ordereddict import OrderedDict
IQ_SET = '/iq[@type="set"]'
NS_COMMANDS = "http://jabber.org/protocol/commands"
@ -52,16 +48,14 @@ ID_CMD_LIST = disco.DiscoIdentity("automation", "command-list")
ID_CMD_NODE = disco.DiscoIdentity("automation", "command-node")
CMD_REQUEST = IQ_SET + '/command[@xmlns="' + NS_COMMANDS + '"]'
SHOWS = OrderedDict(
[
("default", _("Online")),
("away", _("Away")),
("chat", _("Free for chat")),
("dnd", _("Do not disturb")),
("xa", _("Left")),
("disconnect", _("Disconnect")),
]
)
SHOWS = {
"default": _("Online"),
"away": _("Away"),
"chat": _("Free for chat"),
"dnd": _("Do not disturb"),
"xa": _("Left"),
"disconnect": _("Disconnect"),
}
PLUGIN_INFO = {
C.PI_NAME: "Ad-Hoc Commands",
@ -209,6 +203,15 @@ class AdHocCommand(XMPPHandler):
self.client.send(iq_elt)
del self.sessions[session_id]
def _requestEb(self, failure_, request, session_id):
if failure_.check(AdHocError):
error_constant = failure.value.callback_error
else:
log.error(f"unexpected error while handling request: {failure_}")
error_constant = XEP_0050.ERROR.INTERNAL
self._sendError(error_constant, session_id, request)
def onRequest(self, command_elt, requestor, action, session_id):
if not self.isAuthorised(requestor):
return self._sendError(
@ -240,12 +243,7 @@ class AdHocCommand(XMPPHandler):
self.node,
)
d.addCallback(self._sendAnswer, session_id, command_elt.parent)
d.addErrback(
lambda failure, request: self._sendError(
failure.value.callback_error, session_id, request
),
command_elt.parent,
)
d.addErrback(self._requestEb, command_elt.parent, session_id)
class XEP_0050(object):
@ -303,6 +301,14 @@ class XEP_0050(object):
method=self._listUI,
async_=True,
)
host.bridge.addMethod(
"adHocSequence",
".plugin",
in_sign="ssss",
out_sign="s",
method=self._sequence,
async_=True,
)
self.__requesting_id = host.registerCallback(
self._requestingEntity, with_data=True
)
@ -334,7 +340,7 @@ class XEP_0050(object):
None if no session is involved
@param form_values(dict, None): values to use to create command form
values will be passed to data_form.Form.makeFields
@return
@return: iq result element
"""
iq_elt = client.IQ(timeout=timeout)
iq_elt["to"] = entity.full()
@ -405,26 +411,12 @@ class XEP_0050(object):
}
return ["%s%s" % (lvl_map[lvl], msg) for lvl, msg in notes]
def _commandsAnswer2XMLUI(self, iq_elt, session_id, session_data):
"""Convert command answer to an ui for frontend
@param iq_elt: command result
@param session_id: id of the session used with the frontend
@param profile_key: %(doc_profile_key)s
"""
def parseCommandAnswer(self, iq_elt):
command_elt = self.getCommandElt(iq_elt)
status = command_elt.getAttribute("status", XEP_0050.STATUS.EXECUTING)
if status in [XEP_0050.STATUS.COMPLETED, XEP_0050.STATUS.CANCELED]:
# the command session is finished, we purge our session
del self.requesting[session_id]
if status == XEP_0050.STATUS.COMPLETED:
session_id = None
else:
return None
remote_session_id = command_elt.getAttribute("sessionid")
if remote_session_id:
session_data["remote_id"] = remote_session_id
notes = []
data = {}
data["status"] = command_elt.getAttribute("status", XEP_0050.STATUS.EXECUTING)
data["session_id"] = command_elt.getAttribute("sessionid")
data["notes"] = notes = []
for note_elt in command_elt.elements(NS_COMMANDS, "note"):
notes.append(
(
@ -432,6 +424,30 @@ class XEP_0050(object):
str(note_elt),
)
)
return command_elt, data
def _commandsAnswer2XMLUI(self, iq_elt, session_id, session_data):
"""Convert command answer to an ui for frontend
@param iq_elt: command result
@param session_id: id of the session used with the frontend
@param profile_key: %(doc_profile_key)s
"""
command_elt, answer_data = self.parseCommandAnswer(iq_elt)
status = answer_data["status"]
if status in [XEP_0050.STATUS.COMPLETED, XEP_0050.STATUS.CANCELED]:
# the command session is finished, we purge our session
del self.requesting[session_id]
if status == XEP_0050.STATUS.COMPLETED:
session_id = None
else:
return None
remote_session_id = answer_data["session_id"]
if remote_session_id:
session_data["remote_id"] = remote_session_id
notes = answer_data["notes"]
for data_elt in command_elt.elements(data_form.NS_X_DATA, "x"):
if data_elt["type"] in ("form", "result"):
break
@ -608,7 +624,7 @@ class XEP_0050(object):
@defer.inlineCallbacks
def run(self, client, service_jid=None, node=None):
"""run an ad-hoc command
"""Run an ad-hoc command
@param service_jid(jid.JID, None): jid of the ad-hoc service
None to use profile's server
@ -661,6 +677,47 @@ class XEP_0050(object):
d.addCallback(self._items2XMLUI, no_instructions)
return d
def _sequence(self, sequence, node, service_jid_s="", profile_key=C.PROF_KEY_NONE):
sequence = data_format.deserialise(sequence, type_check=list)
client = self.host.getClient(profile_key)
service_jid = jid.JID(service_jid_s) if service_jid_s else None
d = defer.ensureDeferred(self.sequence(client, sequence, node, service_jid))
d.addCallback(lambda data: data_format.serialise(data))
return d
async def sequence(
self,
client: SatXMPPEntity,
sequence: List[dict],
node: str,
service_jid: Optional[jid.JID] = None,
) -> dict:
"""Send a series of data to an ad-hoc service
@param sequence: list of
@param node: node of the ad-hoc commnad
@param service_jid: jid of the ad-hoc service
None to use profile's server
@return: data received in final answer
"""
if service_jid is None:
service_jid = jid.JID(client.jid.host)
session_id = None
for data_to_send in sequence:
iq_result_elt = await self.do(
client,
service_jid,
node,
session_id=session_id,
form_values=data_to_send,
)
__, answer_data = self.parseCommandAnswer(iq_result_elt)
session_id = answer_data.pop("session_id")
return answer_data
def addAdHocCommand(self, client, callback, label, node=None, features=None,
timeout=600, allowed_jids=None, allowed_groups=None,
allowed_magics=None, forbidden_jids=None, forbidden_groups=None,