Mercurial > libervia-backend
annotate sat/plugins/plugin_exp_pubsub_hook.py @ 2738:eb58f26ed236
plugin XEP-0384: update to last python-omemo + trust management:
- Plugin has been updated to use last version of python-omemo (10.0.3).
- A temporary method remove all storage data if they are found, this method must be removed before 0.7 release (only people using dev version should have old omemo data in there storage).
- Trust management is not implemented, using new encryptionTrustUIGet method (an UI is also displayed when trust handling is needed before sending a message).
- omemo.DefaultOTPKPolicy is now used, instead of previous test policy of always deleting.
OMEMO e2e encryption is now functional for one2one conversations, including fingerprint management.
author | Goffi <goffi@goffi.org> |
---|---|
date | Wed, 02 Jan 2019 18:50:28 +0100 |
parents | 56f94936df1e |
children | 003b8b4b56a7 |
rev | line source |
---|---|
2307 | 1 #!/usr/bin/env python2 |
2 # -*- coding: utf-8 -*- | |
3 | |
4 # SAT plugin for Pubsub Hooks | |
2483 | 5 # Copyright (C) 2009-2018 Jérôme Poisson (goffi@goffi.org) |
2307 | 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 | |
2624
56f94936df1e
code style reformatting using black
Goffi <goffi@goffi.org>
parents:
2562
diff
changeset
|
27 |
2307 | 28 log = getLogger(__name__) |
29 | |
2624
56f94936df1e
code style reformatting using black
Goffi <goffi@goffi.org>
parents:
2562
diff
changeset
|
30 NS_PUBSUB_HOOK = "PUBSUB_HOOK" |
2307 | 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", | |
2624
56f94936df1e
code style reformatting using black
Goffi <goffi@goffi.org>
parents:
2562
diff
changeset
|
40 C.PI_DESCRIPTION: _( |
56f94936df1e
code style reformatting using black
Goffi <goffi@goffi.org>
parents:
2562
diff
changeset
|
41 """Experimental plugin to launch on action on Pubsub notifications""" |
56f94936df1e
code style reformatting using black
Goffi <goffi@goffi.org>
parents:
2562
diff
changeset
|
42 ), |
2307 | 43 } |
44 | |
2624
56f94936df1e
code style reformatting using black
Goffi <goffi@goffi.org>
parents:
2562
diff
changeset
|
45 # python module |
56f94936df1e
code style reformatting using black
Goffi <goffi@goffi.org>
parents:
2562
diff
changeset
|
46 HOOK_TYPE_PYTHON = u"python" |
2307 | 47 # python file path |
2624
56f94936df1e
code style reformatting using black
Goffi <goffi@goffi.org>
parents:
2562
diff
changeset
|
48 HOOK_TYPE_PYTHON_FILE = u"python_file" |
2307 | 49 # python code directly |
2624
56f94936df1e
code style reformatting using black
Goffi <goffi@goffi.org>
parents:
2562
diff
changeset
|
50 HOOK_TYPE_PYTHON_CODE = u"python_code" |
2307 | 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(_(u"PubSub Hook initialization")) | |
57 self.host = host | |
58 self.node_hooks = {} # keep track of the number of hooks per node (for all profiles) | |
2624
56f94936df1e
code style reformatting using black
Goffi <goffi@goffi.org>
parents:
2562
diff
changeset
|
59 host.bridge.addMethod( |
56f94936df1e
code style reformatting using black
Goffi <goffi@goffi.org>
parents:
2562
diff
changeset
|
60 "psHookAdd", ".plugin", in_sign="ssssbs", out_sign="", method=self._addHook |
56f94936df1e
code style reformatting using black
Goffi <goffi@goffi.org>
parents:
2562
diff
changeset
|
61 ) |
56f94936df1e
code style reformatting using black
Goffi <goffi@goffi.org>
parents:
2562
diff
changeset
|
62 host.bridge.addMethod( |
56f94936df1e
code style reformatting using black
Goffi <goffi@goffi.org>
parents:
2562
diff
changeset
|
63 "psHookRemove", |
56f94936df1e
code style reformatting using black
Goffi <goffi@goffi.org>
parents:
2562
diff
changeset
|
64 ".plugin", |
56f94936df1e
code style reformatting using black
Goffi <goffi@goffi.org>
parents:
2562
diff
changeset
|
65 in_sign="sssss", |
56f94936df1e
code style reformatting using black
Goffi <goffi@goffi.org>
parents:
2562
diff
changeset
|
66 out_sign="i", |
56f94936df1e
code style reformatting using black
Goffi <goffi@goffi.org>
parents:
2562
diff
changeset
|
67 method=self._removeHook, |
56f94936df1e
code style reformatting using black
Goffi <goffi@goffi.org>
parents:
2562
diff
changeset
|
68 ) |
56f94936df1e
code style reformatting using black
Goffi <goffi@goffi.org>
parents:
2562
diff
changeset
|
69 host.bridge.addMethod( |
56f94936df1e
code style reformatting using black
Goffi <goffi@goffi.org>
parents:
2562
diff
changeset
|
70 "psHookList", |
56f94936df1e
code style reformatting using black
Goffi <goffi@goffi.org>
parents:
2562
diff
changeset
|
71 ".plugin", |
56f94936df1e
code style reformatting using black
Goffi <goffi@goffi.org>
parents:
2562
diff
changeset
|
72 in_sign="s", |
56f94936df1e
code style reformatting using black
Goffi <goffi@goffi.org>
parents:
2562
diff
changeset
|
73 out_sign="aa{ss}", |
56f94936df1e
code style reformatting using black
Goffi <goffi@goffi.org>
parents:
2562
diff
changeset
|
74 method=self._listHooks, |
56f94936df1e
code style reformatting using black
Goffi <goffi@goffi.org>
parents:
2562
diff
changeset
|
75 ) |
2307 | 76 |
77 @defer.inlineCallbacks | |
78 def profileConnected(self, client): | |
2624
56f94936df1e
code style reformatting using black
Goffi <goffi@goffi.org>
parents:
2562
diff
changeset
|
79 hooks = client._hooks = persistent.PersistentBinaryDict( |
56f94936df1e
code style reformatting using black
Goffi <goffi@goffi.org>
parents:
2562
diff
changeset
|
80 NS_PUBSUB_HOOK, client.profile |
56f94936df1e
code style reformatting using black
Goffi <goffi@goffi.org>
parents:
2562
diff
changeset
|
81 ) |
2307 | 82 client._hooks_temporary = {} |
83 yield hooks.load() | |
84 for node in hooks: | |
85 self._installNodeManager(client, node) | |
86 | |
87 def profileDisconnected(self, client): | |
88 for node in client._hooks: | |
89 self._removeNodeManager(client, node) | |
90 | |
91 def _installNodeManager(self, client, node): | |
92 if node in self.node_hooks: | |
93 log.debug(_(u"node manager already set for {node}").format(node=node)) | |
94 self.node_hooks[node] += 1 | |
95 else: | |
96 # first hook on this node | |
2624
56f94936df1e
code style reformatting using black
Goffi <goffi@goffi.org>
parents:
2562
diff
changeset
|
97 self.host.plugins["XEP-0060"].addManagedNode( |
56f94936df1e
code style reformatting using black
Goffi <goffi@goffi.org>
parents:
2562
diff
changeset
|
98 node, items_cb=self._itemsReceived |
56f94936df1e
code style reformatting using black
Goffi <goffi@goffi.org>
parents:
2562
diff
changeset
|
99 ) |
2307 | 100 self.node_hooks[node] = 0 |
2624
56f94936df1e
code style reformatting using black
Goffi <goffi@goffi.org>
parents:
2562
diff
changeset
|
101 log.info(_(u"node manager installed on {node}").format(node=node)) |
2307 | 102 |
103 def _removeNodeManager(self, client, node): | |
104 try: | |
105 self.node_hooks[node] -= 1 | |
106 except KeyError: | |
107 log.error(_(u"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] | |
2624
56f94936df1e
code style reformatting using black
Goffi <goffi@goffi.org>
parents:
2562
diff
changeset
|
111 self.host.plugins["XEP-0060"].removeManagedNode(node, self._itemsReceived) |
2307 | 112 log.debug(_(u"hook removed")) |
113 else: | |
114 log.debug(_(u"node still needed for an other hook")) | |
115 | |
116 def installHook(self, client, service, node, hook_type, hook_arg, persistent): | |
117 if hook_type not in HOOK_TYPES: | |
2624
56f94936df1e
code style reformatting using black
Goffi <goffi@goffi.org>
parents:
2562
diff
changeset
|
118 raise exceptions.DataError( |
56f94936df1e
code style reformatting using black
Goffi <goffi@goffi.org>
parents:
2562
diff
changeset
|
119 _(u"{hook_type} is not handled").format(hook_type=hook_type) |
56f94936df1e
code style reformatting using black
Goffi <goffi@goffi.org>
parents:
2562
diff
changeset
|
120 ) |
2307 | 121 if hook_type != HOOK_TYPE_PYTHON_FILE: |
2624
56f94936df1e
code style reformatting using black
Goffi <goffi@goffi.org>
parents:
2562
diff
changeset
|
122 raise NotImplementedError( |
56f94936df1e
code style reformatting using black
Goffi <goffi@goffi.org>
parents:
2562
diff
changeset
|
123 _(u"{hook_type} hook type not implemented yet").format( |
56f94936df1e
code style reformatting using black
Goffi <goffi@goffi.org>
parents:
2562
diff
changeset
|
124 hook_type=hook_type |
56f94936df1e
code style reformatting using black
Goffi <goffi@goffi.org>
parents:
2562
diff
changeset
|
125 ) |
56f94936df1e
code style reformatting using black
Goffi <goffi@goffi.org>
parents:
2562
diff
changeset
|
126 ) |
2307 | 127 self._installNodeManager(client, node) |
2624
56f94936df1e
code style reformatting using black
Goffi <goffi@goffi.org>
parents:
2562
diff
changeset
|
128 hook_data = {"service": service, "type": hook_type, "arg": hook_arg} |
2307 | 129 |
130 if persistent: | |
2624
56f94936df1e
code style reformatting using black
Goffi <goffi@goffi.org>
parents:
2562
diff
changeset
|
131 hooks_list = client._hooks.setdefault(node, []) |
2307 | 132 hooks_list.append(hook_data) |
133 client._hooks.force(node) | |
134 else: | |
2624
56f94936df1e
code style reformatting using black
Goffi <goffi@goffi.org>
parents:
2562
diff
changeset
|
135 hooks_list = client._hooks_temporary.setdefault(node, []) |
2307 | 136 hooks_list.append(hook_data) |
137 | |
2624
56f94936df1e
code style reformatting using black
Goffi <goffi@goffi.org>
parents:
2562
diff
changeset
|
138 log.info( |
56f94936df1e
code style reformatting using black
Goffi <goffi@goffi.org>
parents:
2562
diff
changeset
|
139 _(u"{persistent} hook installed on {node} for {profile}").format( |
56f94936df1e
code style reformatting using black
Goffi <goffi@goffi.org>
parents:
2562
diff
changeset
|
140 persistent=_(u"persistent") if persistent else _(u"temporary"), |
56f94936df1e
code style reformatting using black
Goffi <goffi@goffi.org>
parents:
2562
diff
changeset
|
141 node=node, |
56f94936df1e
code style reformatting using black
Goffi <goffi@goffi.org>
parents:
2562
diff
changeset
|
142 profile=client.profile, |
56f94936df1e
code style reformatting using black
Goffi <goffi@goffi.org>
parents:
2562
diff
changeset
|
143 ) |
56f94936df1e
code style reformatting using black
Goffi <goffi@goffi.org>
parents:
2562
diff
changeset
|
144 ) |
2307 | 145 |
146 def _itemsReceived(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[:]: | |
2624
56f94936df1e
code style reformatting using black
Goffi <goffi@goffi.org>
parents:
2562
diff
changeset
|
153 if hook_data["service"] != itemsEvent.sender.userhostJID(): |
2307 | 154 continue |
155 try: | |
2624
56f94936df1e
code style reformatting using black
Goffi <goffi@goffi.org>
parents:
2562
diff
changeset
|
156 callback = hook_data["callback"] |
2307 | 157 except KeyError: |
158 # first time we get this hook, we create the callback | |
2624
56f94936df1e
code style reformatting using black
Goffi <goffi@goffi.org>
parents:
2562
diff
changeset
|
159 hook_type = hook_data["type"] |
2307 | 160 try: |
161 if hook_type == HOOK_TYPE_PYTHON_FILE: | |
162 hook_globals = {} | |
2624
56f94936df1e
code style reformatting using black
Goffi <goffi@goffi.org>
parents:
2562
diff
changeset
|
163 execfile(hook_data["arg"], hook_globals) |
56f94936df1e
code style reformatting using black
Goffi <goffi@goffi.org>
parents:
2562
diff
changeset
|
164 callback = hook_globals["hook"] |
2307 | 165 else: |
2624
56f94936df1e
code style reformatting using black
Goffi <goffi@goffi.org>
parents:
2562
diff
changeset
|
166 raise NotImplementedError( |
56f94936df1e
code style reformatting using black
Goffi <goffi@goffi.org>
parents:
2562
diff
changeset
|
167 _(u"{hook_type} hook type not implemented yet").format( |
56f94936df1e
code style reformatting using black
Goffi <goffi@goffi.org>
parents:
2562
diff
changeset
|
168 hook_type=hook_type |
56f94936df1e
code style reformatting using black
Goffi <goffi@goffi.org>
parents:
2562
diff
changeset
|
169 ) |
56f94936df1e
code style reformatting using black
Goffi <goffi@goffi.org>
parents:
2562
diff
changeset
|
170 ) |
2307 | 171 except Exception as e: |
2624
56f94936df1e
code style reformatting using black
Goffi <goffi@goffi.org>
parents:
2562
diff
changeset
|
172 log.warning( |
56f94936df1e
code style reformatting using black
Goffi <goffi@goffi.org>
parents:
2562
diff
changeset
|
173 _( |
56f94936df1e
code style reformatting using black
Goffi <goffi@goffi.org>
parents:
2562
diff
changeset
|
174 u"Can't load Pubsub hook at node {node}, it will be removed: {reason}" |
56f94936df1e
code style reformatting using black
Goffi <goffi@goffi.org>
parents:
2562
diff
changeset
|
175 ).format(node=node, reason=e) |
56f94936df1e
code style reformatting using black
Goffi <goffi@goffi.org>
parents:
2562
diff
changeset
|
176 ) |
2307 | 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: | |
2624
56f94936df1e
code style reformatting using black
Goffi <goffi@goffi.org>
parents:
2562
diff
changeset
|
184 log.warning( |
56f94936df1e
code style reformatting using black
Goffi <goffi@goffi.org>
parents:
2562
diff
changeset
|
185 _( |
56f94936df1e
code style reformatting using black
Goffi <goffi@goffi.org>
parents:
2562
diff
changeset
|
186 u"Error while running Pubsub hook for node {node}: {msg}" |
56f94936df1e
code style reformatting using black
Goffi <goffi@goffi.org>
parents:
2562
diff
changeset
|
187 ).format(node=node, msg=e) |
56f94936df1e
code style reformatting using black
Goffi <goffi@goffi.org>
parents:
2562
diff
changeset
|
188 ) |
2307 | 189 |
190 def _addHook(self, service, node, hook_type, hook_arg, persistent, profile): | |
191 client = self.host.getClient(profile) | |
192 service = jid.JID(service) if service else client.jid.userhostJID() | |
2624
56f94936df1e
code style reformatting using black
Goffi <goffi@goffi.org>
parents:
2562
diff
changeset
|
193 return self.addHook( |
56f94936df1e
code style reformatting using black
Goffi <goffi@goffi.org>
parents:
2562
diff
changeset
|
194 client, |
56f94936df1e
code style reformatting using black
Goffi <goffi@goffi.org>
parents:
2562
diff
changeset
|
195 service, |
56f94936df1e
code style reformatting using black
Goffi <goffi@goffi.org>
parents:
2562
diff
changeset
|
196 unicode(node), |
56f94936df1e
code style reformatting using black
Goffi <goffi@goffi.org>
parents:
2562
diff
changeset
|
197 unicode(hook_type), |
56f94936df1e
code style reformatting using black
Goffi <goffi@goffi.org>
parents:
2562
diff
changeset
|
198 unicode(hook_arg), |
56f94936df1e
code style reformatting using black
Goffi <goffi@goffi.org>
parents:
2562
diff
changeset
|
199 persistent, |
56f94936df1e
code style reformatting using black
Goffi <goffi@goffi.org>
parents:
2562
diff
changeset
|
200 ) |
2307 | 201 |
202 def addHook(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 | |
2444
30278ea1ca7c
plugin XEP-0060: added node watching methods to bridge:
Goffi <goffi@goffi.org>
parents:
2307
diff
changeset
|
213 /!\ Python hooks will be executed in SàT context, |
2307 | 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.installHook(client, service, node, hook_type, hook_arg, persistent) | |
223 | |
224 def _removeHook(self, service, node, hook_type, hook_arg, profile): | |
225 client = self.host.getClient(profile) | |
226 service = jid.JID(service) if service else client.jid.userhostJID() | |
227 return self.removeHook(client, service, node, hook_type or None, hook_arg or None) | |
228 | |
229 def removeHook(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 [addHook] | |
235 match all if None | |
236 @param hook_arg(unicode, None): same as for [addHook] | |
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]: | |
2624
56f94936df1e
code style reformatting using black
Goffi <goffi@goffi.org>
parents:
2562
diff
changeset
|
244 if ( |
56f94936df1e
code style reformatting using black
Goffi <goffi@goffi.org>
parents:
2562
diff
changeset
|
245 service != hook_data[u"service"] |
56f94936df1e
code style reformatting using black
Goffi <goffi@goffi.org>
parents:
2562
diff
changeset
|
246 or hook_type is not None |
56f94936df1e
code style reformatting using black
Goffi <goffi@goffi.org>
parents:
2562
diff
changeset
|
247 and hook_type != hook_data[u"type"] |
56f94936df1e
code style reformatting using black
Goffi <goffi@goffi.org>
parents:
2562
diff
changeset
|
248 or hook_arg is not None |
56f94936df1e
code style reformatting using black
Goffi <goffi@goffi.org>
parents:
2562
diff
changeset
|
249 and hook_arg != hook_data[u"arg"] |
56f94936df1e
code style reformatting using black
Goffi <goffi@goffi.org>
parents:
2562
diff
changeset
|
250 ): |
2307 | 251 continue |
252 hooks[node].remove(hook_data) | |
253 removed += 1 | |
254 if not hooks[node]: | |
2624
56f94936df1e
code style reformatting using black
Goffi <goffi@goffi.org>
parents:
2562
diff
changeset
|
255 # no more hooks, we can remove the node |
2307 | 256 del hooks[node] |
257 self._removeNodeManager(client, node) | |
258 else: | |
259 if hooks == client._hooks: | |
260 hooks.force(node) | |
261 return removed | |
262 | |
263 def _listHooks(self, profile): | |
264 hooks_list = self.listHooks(self.host.getClient(profile)) | |
265 for hook in hooks_list: | |
2624
56f94936df1e
code style reformatting using black
Goffi <goffi@goffi.org>
parents:
2562
diff
changeset
|
266 hook[u"service"] = hook[u"service"].full() |
56f94936df1e
code style reformatting using black
Goffi <goffi@goffi.org>
parents:
2562
diff
changeset
|
267 hook[u"persistent"] = C.boolConst(hook[u"persistent"]) |
2307 | 268 return hooks_list |
269 | |
270 def listHooks(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.iteritems(): | |
276 for hook_data in hooks_data: | |
2624
56f94936df1e
code style reformatting using black
Goffi <goffi@goffi.org>
parents:
2562
diff
changeset
|
277 hooks_list.append( |
56f94936df1e
code style reformatting using black
Goffi <goffi@goffi.org>
parents:
2562
diff
changeset
|
278 { |
56f94936df1e
code style reformatting using black
Goffi <goffi@goffi.org>
parents:
2562
diff
changeset
|
279 u"service": hook_data[u"service"], |
56f94936df1e
code style reformatting using black
Goffi <goffi@goffi.org>
parents:
2562
diff
changeset
|
280 u"node": node, |
56f94936df1e
code style reformatting using black
Goffi <goffi@goffi.org>
parents:
2562
diff
changeset
|
281 u"type": hook_data[u"type"], |
56f94936df1e
code style reformatting using black
Goffi <goffi@goffi.org>
parents:
2562
diff
changeset
|
282 u"arg": hook_data[u"arg"], |
56f94936df1e
code style reformatting using black
Goffi <goffi@goffi.org>
parents:
2562
diff
changeset
|
283 u"persistent": persistent, |
56f94936df1e
code style reformatting using black
Goffi <goffi@goffi.org>
parents:
2562
diff
changeset
|
284 } |
56f94936df1e
code style reformatting using black
Goffi <goffi@goffi.org>
parents:
2562
diff
changeset
|
285 ) |
2307 | 286 return hooks_list |