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