comparison sat_pubsub/memory_storage.py @ 405:c56a728412f1

file organisation + setup refactoring: - `/src` has been renamed to `/sat_pubsub`, this is the recommended naming convention - revamped `setup.py` on the basis of SàT's `setup.py` - added a `VERSION` which is the unique place where version number will now be set - use same trick as in SàT to specify dev version (`D` at the end) - use setuptools_scm to retrieve Mercurial hash when in dev version
author Goffi <goffi@goffi.org>
date Fri, 16 Aug 2019 12:00:02 +0200
parents src/memory_storage.py@aa3a464df605
children
comparison
equal deleted inserted replaced
404:105a0772eedd 405:c56a728412f1
1 #!/usr/bin/python
2 #-*- coding: utf-8 -*-
3
4 # Copyright (c) 2003-2011 Ralph Meijer
5 # Copyright (c) 2012-2019 Jérôme Poisson
6
7
8 # This program is free software: you can redistribute it and/or modify
9 # it under the terms of the GNU Affero General Public License as published by
10 # the Free Software Foundation, either version 3 of the License, or
11 # (at your option) any later version.
12
13 # This program is distributed in the hope that it will be useful,
14 # but WITHOUT ANY WARRANTY; without even the implied warranty of
15 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16 # GNU Affero General Public License for more details.
17
18 # You should have received a copy of the GNU Affero General Public License
19 # along with this program. If not, see <http://www.gnu.org/licenses/>.
20 # --
21
22 # This program is based on Idavoll (http://idavoll.ik.nu/),
23 # originaly written by Ralph Meijer (http://ralphm.net/blog/)
24 # It is sublicensed under AGPL v3 (or any later version) as allowed by the original
25 # license.
26
27 # --
28
29 # Here is a copy of the original license:
30
31 # Copyright (c) 2003-2011 Ralph Meijer
32
33 # Permission is hereby granted, free of charge, to any person obtaining
34 # a copy of this software and associated documentation files (the
35 # "Software"), to deal in the Software without restriction, including
36 # without limitation the rights to use, copy, modify, merge, publish,
37 # distribute, sublicense, and/or sell copies of the Software, and to
38 # permit persons to whom the Software is furnished to do so, subject to
39 # the following conditions:
40
41 # The above copyright notice and this permission notice shall be
42 # included in all copies or substantial portions of the Software.
43
44 # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
45 # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
46 # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
47 # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
48 # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
49 # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
50 # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
51
52
53 import copy
54 from zope.interface import implements
55 from twisted.internet import defer
56 from twisted.words.protocols.jabber import jid
57
58 from wokkel.pubsub import Subscription
59
60 from sat_pubsub import error, iidavoll
61
62 class Storage:
63
64 implements(iidavoll.IStorage)
65
66 defaultConfig = {
67 'leaf': {
68 "pubsub#persist_items": True,
69 "pubsub#deliver_payloads": True,
70 "pubsub#send_last_published_item": 'on_sub',
71 },
72 'collection': {
73 "pubsub#deliver_payloads": True,
74 "pubsub#send_last_published_item": 'on_sub',
75 }
76 }
77
78 def __init__(self):
79 rootNode = CollectionNode('', jid.JID('localhost'),
80 copy.copy(self.defaultConfig['collection']))
81 self._nodes = {'': rootNode}
82
83
84 def getNode(self, nodeIdentifier):
85 try:
86 node = self._nodes[nodeIdentifier]
87 except KeyError:
88 return defer.fail(error.NodeNotFound())
89
90 return defer.succeed(node)
91
92
93 def getNodeIds(self):
94 return defer.succeed(self._nodes.keys())
95
96
97 def createNode(self, nodeIdentifier, owner, config):
98 if nodeIdentifier in self._nodes:
99 return defer.fail(error.NodeExists())
100
101 if config['pubsub#node_type'] != 'leaf':
102 raise error.NoCollections()
103
104 node = LeafNode(nodeIdentifier, owner, config)
105 self._nodes[nodeIdentifier] = node
106
107 return defer.succeed(None)
108
109
110 def deleteNode(self, nodeIdentifier):
111 try:
112 del self._nodes[nodeIdentifier]
113 except KeyError:
114 return defer.fail(error.NodeNotFound())
115
116 return defer.succeed(None)
117
118
119 def getAffiliations(self, entity):
120 entity = entity.userhost()
121 return defer.succeed([(node.nodeIdentifier, node._affiliations[entity])
122 for name, node in self._nodes.iteritems()
123 if entity in node._affiliations])
124
125
126 def getSubscriptions(self, entity):
127 subscriptions = []
128 for node in self._nodes.itervalues():
129 for subscriber, subscription in node._subscriptions.iteritems():
130 subscriber = jid.internJID(subscriber)
131 if subscriber.userhostJID() == entity.userhostJID():
132 subscriptions.append(subscription)
133
134 return defer.succeed(subscriptions)
135
136
137 def getDefaultConfiguration(self, nodeType):
138 if nodeType == 'collection':
139 raise error.NoCollections()
140
141 return self.defaultConfig[nodeType]
142
143
144 class Node:
145
146 implements(iidavoll.INode)
147
148 def __init__(self, nodeIdentifier, owner, config):
149 self.nodeIdentifier = nodeIdentifier
150 self._affiliations = {owner.userhost(): 'owner'}
151 self._subscriptions = {}
152 self._config = copy.copy(config)
153
154
155 def getType(self):
156 return self.nodeType
157
158
159 def getConfiguration(self):
160 return self._config
161
162
163 def getMetaData(self):
164 config = copy.copy(self._config)
165 config["pubsub#node_type"] = self.nodeType
166 return config
167
168
169 def setConfiguration(self, options):
170 for option in options:
171 if option in self._config:
172 self._config[option] = options[option]
173
174 return defer.succeed(None)
175
176
177 def getAffiliation(self, entity):
178 return defer.succeed(self._affiliations.get(entity.userhost()))
179
180
181 def getSubscription(self, subscriber):
182 try:
183 subscription = self._subscriptions[subscriber.full()]
184 except KeyError:
185 return defer.succeed(None)
186 else:
187 return defer.succeed(subscription)
188
189
190 def getSubscriptions(self, state=None):
191 return defer.succeed(
192 [subscription
193 for subscription in self._subscriptions.itervalues()
194 if state is None or subscription.state == state])
195
196
197
198 def addSubscription(self, subscriber, state, options):
199 if self._subscriptions.get(subscriber.full()):
200 return defer.fail(error.SubscriptionExists())
201
202 subscription = Subscription(self.nodeIdentifier, subscriber, state,
203 options)
204 self._subscriptions[subscriber.full()] = subscription
205 return defer.succeed(None)
206
207
208 def removeSubscription(self, subscriber):
209 try:
210 del self._subscriptions[subscriber.full()]
211 except KeyError:
212 return defer.fail(error.NotSubscribed())
213
214 return defer.succeed(None)
215
216
217 def isSubscribed(self, entity):
218 for subscriber, subscription in self._subscriptions.iteritems():
219 if jid.internJID(subscriber).userhost() == entity.userhost() and \
220 subscription.state == 'subscribed':
221 return defer.succeed(True)
222
223 return defer.succeed(False)
224
225
226 def getAffiliations(self):
227 affiliations = [(jid.internJID(entity), affiliation) for entity, affiliation
228 in self._affiliations.iteritems()]
229
230 return defer.succeed(affiliations)
231
232
233
234 class PublishedItem(object):
235 """
236 A published item.
237
238 This represent an item as it was published by an entity.
239
240 @ivar element: The DOM representation of the item that was published.
241 @type element: L{Element<twisted.words.xish.domish.Element>}
242 @ivar publisher: The entity that published the item.
243 @type publisher: L{JID<twisted.words.protocols.jabber.jid.JID>}
244 """
245
246 def __init__(self, element, publisher):
247 self.element = element
248 self.publisher = publisher
249
250
251
252 class LeafNode(Node):
253
254 implements(iidavoll.ILeafNode)
255
256 nodeType = 'leaf'
257
258 def __init__(self, nodeIdentifier, owner, config):
259 Node.__init__(self, nodeIdentifier, owner, config)
260 self._items = {}
261 self._itemlist = []
262
263
264 def storeItems(self, item_data, publisher):
265 for access_model, item_config, element in item_data:
266 item = PublishedItem(element, publisher)
267 itemIdentifier = element["id"]
268 if itemIdentifier in self._items:
269 self._itemlist.remove(self._items[itemIdentifier])
270 self._items[itemIdentifier] = item
271 self._itemlist.append(item)
272
273 return defer.succeed(None)
274
275
276 def removeItems(self, itemIdentifiers):
277 deleted = []
278
279 for itemIdentifier in itemIdentifiers:
280 try:
281 item = self._items[itemIdentifier]
282 except KeyError:
283 pass
284 else:
285 self._itemlist.remove(item)
286 del self._items[itemIdentifier]
287 deleted.append(itemIdentifier)
288
289 return defer.succeed(deleted)
290
291
292 def getItems(self, authorized_groups, unrestricted, maxItems=None):
293 if maxItems is not None:
294 itemList = self._itemlist[-maxItems:]
295 else:
296 itemList = self._itemlist
297 return defer.succeed([item.element for item in itemList])
298
299
300 def getItemsById(self, authorized_groups, unrestricted, itemIdentifiers):
301 items = []
302 for itemIdentifier in itemIdentifiers:
303 try:
304 item = self._items[itemIdentifier]
305 except KeyError:
306 pass
307 else:
308 items.append(item.element)
309 return defer.succeed(items)
310
311
312 def purge(self):
313 self._items = {}
314 self._itemlist = []
315
316 return defer.succeed(None)
317
318
319 def filterItemsWithPublisher(self, itemIdentifiers, requestor):
320 filteredItems = []
321 for itemIdentifier in itemIdentifiers:
322 try:
323 if self._items[itemIdentifier].publisher.userhost() == requestor.userhost():
324 filteredItems.append(self.items[itemIdentifier])
325 except KeyError, AttributeError:
326 pass
327 return defer.succeed(filteredItems)
328
329
330 class CollectionNode(Node):
331 nodeType = 'collection'
332
333
334
335 class GatewayStorage(object):
336 """
337 Memory based storage facility for the XMPP-HTTP gateway.
338 """
339
340 def __init__(self):
341 self.callbacks = {}
342
343
344 def addCallback(self, service, nodeIdentifier, callback):
345 try:
346 callbacks = self.callbacks[service, nodeIdentifier]
347 except KeyError:
348 callbacks = {callback}
349 self.callbacks[service, nodeIdentifier] = callbacks
350 else:
351 callbacks.add(callback)
352 pass
353
354 return defer.succeed(None)
355
356
357 def removeCallback(self, service, nodeIdentifier, callback):
358 try:
359 callbacks = self.callbacks[service, nodeIdentifier]
360 callbacks.remove(callback)
361 except KeyError:
362 return defer.fail(error.NotSubscribed())
363 else:
364 if not callbacks:
365 del self.callbacks[service, nodeIdentifier]
366
367 return defer.succeed(not callbacks)
368
369
370 def getCallbacks(self, service, nodeIdentifier):
371 try:
372 callbacks = self.callbacks[service, nodeIdentifier]
373 except KeyError:
374 return defer.fail(error.NoCallbacks())
375 else:
376 return defer.succeed(callbacks)
377
378
379 def hasCallbacks(self, service, nodeIdentifier):
380 return defer.succeed((service, nodeIdentifier) in self.callbacks)