comparison libervia/backend/plugins/plugin_exp_pubsub_hook.py @ 4071:4b842c1fb686

refactoring: renamed `sat` package to `libervia.backend`
author Goffi <goffi@goffi.org>
date Fri, 02 Jun 2023 11:49:51 +0200
parents sat/plugins/plugin_exp_pubsub_hook.py@524856bd7b19
children 0d7bb4df2343
comparison
equal deleted inserted replaced
4070:d10748475025 4071:4b842c1fb686
1 #!/usr/bin/env python3
2
3
4 # SAT plugin for Pubsub Hooks
5 # Copyright (C) 2009-2021 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 libervia.backend.core.i18n import _
21 from libervia.backend.core.constants import Const as C
22 from libervia.backend.core import exceptions
23 from libervia.backend.core.log import getLogger
24 from libervia.backend.memory import persistent
25 from twisted.words.protocols.jabber import jid
26 from twisted.internet import defer
27
28 log = getLogger(__name__)
29
30 NS_PUBSUB_HOOK = "PUBSUB_HOOK"
31
32 PLUGIN_INFO = {
33 C.PI_NAME: "PubSub Hook",
34 C.PI_IMPORT_NAME: NS_PUBSUB_HOOK,
35 C.PI_TYPE: "EXP",
36 C.PI_PROTOCOLS: [],
37 C.PI_DEPENDENCIES: ["XEP-0060"],
38 C.PI_MAIN: "PubsubHook",
39 C.PI_HANDLER: "no",
40 C.PI_DESCRIPTION: _(
41 """Experimental plugin to launch on action on Pubsub notifications"""
42 ),
43 }
44
45 #  python module
46 HOOK_TYPE_PYTHON = "python"
47 # python file path
48 HOOK_TYPE_PYTHON_FILE = "python_file"
49 # python code directly
50 HOOK_TYPE_PYTHON_CODE = "python_code"
51 HOOK_TYPES = (HOOK_TYPE_PYTHON, HOOK_TYPE_PYTHON_FILE, HOOK_TYPE_PYTHON_CODE)
52
53
54 class PubsubHook(object):
55 def __init__(self, host):
56 log.info(_("PubSub Hook initialization"))
57 self.host = host
58 self.node_hooks = {} # keep track of the number of hooks per node (for all profiles)
59 host.bridge.add_method(
60 "ps_hook_add", ".plugin", in_sign="ssssbs", out_sign="", method=self._addHook
61 )
62 host.bridge.add_method(
63 "ps_hook_remove",
64 ".plugin",
65 in_sign="sssss",
66 out_sign="i",
67 method=self._removeHook,
68 )
69 host.bridge.add_method(
70 "ps_hook_list",
71 ".plugin",
72 in_sign="s",
73 out_sign="aa{ss}",
74 method=self._list_hooks,
75 )
76
77 @defer.inlineCallbacks
78 def profile_connected(self, client):
79 hooks = client._hooks = persistent.PersistentBinaryDict(
80 NS_PUBSUB_HOOK, client.profile
81 )
82 client._hooks_temporary = {}
83 yield hooks.load()
84 for node in hooks:
85 self._install_node_manager(client, node)
86
87 def profile_disconnected(self, client):
88 for node in client._hooks:
89 self._remove_node_manager(client, node)
90
91 def _install_node_manager(self, client, node):
92 if node in self.node_hooks:
93 log.debug(_("node manager already set for {node}").format(node=node))
94 self.node_hooks[node] += 1
95 else:
96 # first hook on this node
97 self.host.plugins["XEP-0060"].add_managed_node(
98 node, items_cb=self._items_received
99 )
100 self.node_hooks[node] = 0
101 log.info(_("node manager installed on {node}").format(node=node))
102
103 def _remove_node_manager(self, client, node):
104 try:
105 self.node_hooks[node] -= 1
106 except KeyError:
107 log.error(_("trying to remove a {node} without hook").format(node=node))
108 else:
109 if self.node_hooks[node] == 0:
110 del self.node_hooks[node]
111 self.host.plugins["XEP-0060"].remove_managed_node(node, self._items_received)
112 log.debug(_("hook removed"))
113 else:
114 log.debug(_("node still needed for an other hook"))
115
116 def install_hook(self, client, service, node, hook_type, hook_arg, persistent):
117 if hook_type not in HOOK_TYPES:
118 raise exceptions.DataError(
119 _("{hook_type} is not handled").format(hook_type=hook_type)
120 )
121 if hook_type != HOOK_TYPE_PYTHON_FILE:
122 raise NotImplementedError(
123 _("{hook_type} hook type not implemented yet").format(
124 hook_type=hook_type
125 )
126 )
127 self._install_node_manager(client, node)
128 hook_data = {"service": service, "type": hook_type, "arg": hook_arg}
129
130 if persistent:
131 hooks_list = client._hooks.setdefault(node, [])
132 hooks_list.append(hook_data)
133 client._hooks.force(node)
134 else:
135 hooks_list = client._hooks_temporary.setdefault(node, [])
136 hooks_list.append(hook_data)
137
138 log.info(
139 _("{persistent} hook installed on {node} for {profile}").format(
140 persistent=_("persistent") if persistent else _("temporary"),
141 node=node,
142 profile=client.profile,
143 )
144 )
145
146 def _items_received(self, client, itemsEvent):
147 node = itemsEvent.nodeIdentifier
148 for hooks in (client._hooks, client._hooks_temporary):
149 if node not in hooks:
150 continue
151 hooks_list = hooks[node]
152 for hook_data in hooks_list[:]:
153 if hook_data["service"] != itemsEvent.sender.userhostJID():
154 continue
155 try:
156 callback = hook_data["callback"]
157 except KeyError:
158 # first time we get this hook, we create the callback
159 hook_type = hook_data["type"]
160 try:
161 if hook_type == HOOK_TYPE_PYTHON_FILE:
162 hook_globals = {}
163 exec(compile(open(hook_data["arg"], "rb").read(), hook_data["arg"], 'exec'), hook_globals)
164 callback = hook_globals["hook"]
165 else:
166 raise NotImplementedError(
167 _("{hook_type} hook type not implemented yet").format(
168 hook_type=hook_type
169 )
170 )
171 except Exception as e:
172 log.warning(
173 _(
174 "Can't load Pubsub hook at node {node}, it will be removed: {reason}"
175 ).format(node=node, reason=e)
176 )
177 hooks_list.remove(hook_data)
178 continue
179
180 for item in itemsEvent.items:
181 try:
182 callback(self.host, client, item)
183 except Exception as e:
184 log.warning(
185 _(
186 "Error while running Pubsub hook for node {node}: {msg}"
187 ).format(node=node, msg=e)
188 )
189
190 def _addHook(self, service, node, hook_type, hook_arg, persistent, profile):
191 client = self.host.get_client(profile)
192 service = jid.JID(service) if service else client.jid.userhostJID()
193 return self.add_hook(
194 client,
195 service,
196 str(node),
197 str(hook_type),
198 str(hook_arg),
199 persistent,
200 )
201
202 def add_hook(self, client, service, node, hook_type, hook_arg, persistent):
203 r"""Add a hook which will be triggered on a pubsub notification
204
205 @param service(jid.JID): service of the node
206 @param node(unicode): Pubsub node
207 @param hook_type(unicode): type of the hook, one of:
208 - HOOK_TYPE_PYTHON: a python module (must be in path)
209 module must have a "hook" method which will be called
210 - HOOK_TYPE_PYTHON_FILE: a python file
211 file must have a "hook" method which will be called
212 - HOOK_TYPE_PYTHON_CODE: direct python code
213 /!\ Python hooks will be executed in SàT context,
214 with host, client and item as arguments, it means that:
215 - they can do whatever they wants, so don't run untrusted hooks
216 - they MUST NOT BLOCK, they are run in Twisted async environment and blocking would block whole SàT process
217 - item are domish.Element
218 @param hook_arg(unicode): argument of the hook, depending on the hook_type
219 can be a module path, file path, python code
220 """
221 assert service is not None
222 return self.install_hook(client, service, node, hook_type, hook_arg, persistent)
223
224 def _removeHook(self, service, node, hook_type, hook_arg, profile):
225 client = self.host.get_client(profile)
226 service = jid.JID(service) if service else client.jid.userhostJID()
227 return self.remove_hook(client, service, node, hook_type or None, hook_arg or None)
228
229 def remove_hook(self, client, service, node, hook_type=None, hook_arg=None):
230 """Remove a persistent or temporaty root
231
232 @param service(jid.JID): service of the node
233 @param node(unicode): Pubsub node
234 @param hook_type(unicode, None): same as for [add_hook]
235 match all if None
236 @param hook_arg(unicode, None): same as for [add_hook]
237 match all if None
238 @return(int): number of hooks removed
239 """
240 removed = 0
241 for hooks in (client._hooks, client._hooks_temporary):
242 if node in hooks:
243 for hook_data in hooks[node]:
244 if (
245 service != hook_data["service"]
246 or hook_type is not None
247 and hook_type != hook_data["type"]
248 or hook_arg is not None
249 and hook_arg != hook_data["arg"]
250 ):
251 continue
252 hooks[node].remove(hook_data)
253 removed += 1
254 if not hooks[node]:
255 #  no more hooks, we can remove the node
256 del hooks[node]
257 self._remove_node_manager(client, node)
258 else:
259 if hooks == client._hooks:
260 hooks.force(node)
261 return removed
262
263 def _list_hooks(self, profile):
264 hooks_list = self.list_hooks(self.host.get_client(profile))
265 for hook in hooks_list:
266 hook["service"] = hook["service"].full()
267 hook["persistent"] = C.bool_const(hook["persistent"])
268 return hooks_list
269
270 def list_hooks(self, client):
271 """return list of registered hooks"""
272 hooks_list = []
273 for hooks in (client._hooks, client._hooks_temporary):
274 persistent = hooks is client._hooks
275 for node, hooks_data in hooks.items():
276 for hook_data in hooks_data:
277 hooks_list.append(
278 {
279 "service": hook_data["service"],
280 "node": node,
281 "type": hook_data["type"],
282 "arg": hook_data["arg"],
283 "persistent": persistent,
284 }
285 )
286 return hooks_list