Commit 897dfb64 authored by Martin's avatar Martin

import upstream alpha release 0.3.0a1

parent da422336
v. 0.2 (02/12/2015):
- schema updated (unversioned => version 1)
- sync with Idavoll
- use of sat.tmp.wokkel as a workaround for change not merged upstream
- RSM (XEP-0059) implementation
- MAM (XEP-0313) implementation
- Namespace Delegation (XEP-0355) implementation
- Privileged Entity (XEP-0356) implementation
- removed remote-roster hack
- SàT PubSub can be used as a PEP service (PEP implementation not complete yet)
- item publisher is enforced
- categories handling (not complete)
V 0.3 (NOT RELEASED YET)
- PEP: presence and +notify initial support
- PEP: notifications for auto subscribers
- added "presence" access model
- implemented affiliations
- items are now ordered by default using last update date. This is actually the order corresponding to the XEP
- item creation and modification are now saved in 2 separate fields
- "roster" access model has been renamed to "publisher-roster"
- re-implemented feature which allows an entity to retract an item from somebody else node (i.e. a node from which he is neither owner or publisher) if he is the publisher of the item.
- implemented "Order-By" protoXEP
- node schema experimental features (use a data form as a schema for a node)
- serial ids experimental feature (ids in series like 1, 2, 3, etc.)
- pubsub admin experimental feature (publish item and specify the publisher, only for admin users)
- SàT Pubsub can now be configured using the same config file as SàT itself, i.e. "sat.conf"
- new "admins_jids_list" setting, to specify a comma separated list of admin bare jids.
- MAM: filtering by categories
- MAM and RSM various improvments
- replaced sat.tmp by new independent sat_tmp module, so now SàT Pubsub can be used independently of SàT
- item id is returned on publish if it is not provided already
- (bug fix) fixed security check which was rejecting all delegations from external servers
- (bug fix) fixed publisher check on item publishing
- various other fixes/improvments, checks the mercurial logs for details
v 0.2 (02/12/2015):
- schema updated (unversioned => version 1)
- sync with Idavoll
- use of sat.tmp.wokkel as a workaround for change not merged upstream
- RSM (XEP-0059) implementation
- MAM (XEP-0313) implementation
- Namespace Delegation (XEP-0355) implementation
- Privileged Entity (XEP-0356) implementation
- removed remote-roster hack
- SàT PubSub can be used as a PEP service (PEP implementation not complete yet)
- item publisher is enforced
- categories handling (not complete)
- pgsql: schema version is checked, and a message asking to upgrade is displayed if needed
v 0.1.1 (09/09/2014):
bugfix release
......
SàT PubSub component v0.1.1
SàT PubSub component v0.3.0a1
This program is heavily based on Idavoll (0.9.1), which was written by Ralph Meijer
Copyright (c) 2012-2018 Jérôme Poisson
Copyright (c) 2012-2019 Jérôme Poisson
Copyright (c) 2014-2016 Adrien Cossa
Copyright (c) 2003-2011 Ralph Meijer
......
#!/usr/bin/python
#-*- coding: utf-8 -*-
# Copyright (c) 2012-2018 Jérôme Poisson
# Copyright (c) 2012-2019 Jérôme Poisson
# Copyright (c) 2013-2016 Adrien Cossa
# Copyright (c) 2003-2011 Ralph Meijer
#
......@@ -54,11 +54,8 @@
SàT PubSub, a generic XMPP publish-subscribe service.
"""
__version__ = '0.2.0'
__version__ = '0.3.0a1'
# TODO: remove this when RSM and MAM are in wokkel
import wokkel
from sat_tmp.wokkel import pubsub as tmp_pubsub, rsm as tmp_rsm, mam as tmp_mam
wokkel.pubsub = tmp_pubsub
wokkel.rsm = tmp_rsm
wokkel.mam = tmp_mam
# TODO: remove this when changes are merged in Wokkel
from sat_tmp.wokkel import install
install()
#!/usr/bin/python
#-*- coding: utf-8 -*-
#
# Copyright (c) 2012-2018 Jérôme Poisson
# Copyright (c) 2012-2019 Jérôme Poisson
# Copyright (c) 2013-2016 Adrien Cossa
# Copyright (c) 2003-2011 Ralph Meijer
......@@ -70,13 +70,14 @@ from twisted.python import components, log
from twisted.internet import defer, reactor
from twisted.words.protocols.jabber.error import StanzaError
# from twisted.words.protocols.jabber.jid import JID, InvalidFormat
from twisted.words.xish import utility
from twisted.words.xish import domish, utility
from wokkel import disco
from wokkel import data_form
from wokkel import rsm
from wokkel import iwokkel
from wokkel import pubsub
from wokkel.subprotocols import XMPPHandler
from sat_pubsub import error
from sat_pubsub import iidavoll
......@@ -162,10 +163,11 @@ class BackendService(service.Service, utility.EventDispatcher):
},
}
def __init__(self, storage):
def __init__(self, storage, config):
utility.EventDispatcher.__init__(self)
self.storage = storage
self._callbackList = []
self.config = config
def supportsPublishOptions(self):
return True
......@@ -378,6 +380,7 @@ class BackendService(service.Service, utility.EventDispatcher):
items_data = []
check_overwrite = False
ret_payload = None # payload returned, None or domish.Element
for item in items:
# we enforce publisher (cf XEP-0060 §7.1.2.3)
item['publisher'] = requestor.full()
......@@ -387,6 +390,12 @@ class BackendService(service.Service, utility.EventDispatcher):
if not item.getAttribute("id"):
item["id"] = yield node.getNextId()
new_item = True
ret_pubsub_elt = domish.Element((pubsub.NS_PUBSUB, u'pubsub'))
ret_publish_elt = ret_pubsub_elt.addElement(u'publish')
ret_publish_elt[u'node'] = node.nodeIdentifier
ret_item_elt = ret_publish_elt.addElement(u'item')
ret_item_elt["id"] = item[u"id"]
ret_payload = ret_pubsub_elt
else:
check_overwrite = True
new_item = False
......@@ -408,6 +417,7 @@ class BackendService(service.Service, utility.EventDispatcher):
yield node.storeItems(items_data, requestor)
yield self._doNotify(node, items_data, deliverPayloads, pep, recipient)
defer.returnValue(ret_payload)
def _doNotify(self, node, items_data, deliverPayloads, pep, recipient):
if items_data and not deliverPayloads:
......@@ -1658,11 +1668,14 @@ class PubSubResourceFromBackend(pubsub.PubSubResource):
def items(self, request):
ext_data = {}
if const.FLAG_ENABLE_RSM and request.rsm is not None:
if request.rsm.max < 0:
raise pubsub.BadRequest(text="max can't be negative")
ext_data['rsm'] = request.rsm
try:
ext_data['pep'] = request.delegated
except AttributeError:
pass
ext_data['order_by'] = request.orderBy or []
d = self.backend.getItems(request.nodeIdentifier,
request.sender,
request.recipient,
......@@ -1697,3 +1710,16 @@ class PubSubResourceFromBackend(pubsub.PubSubResource):
components.registerAdapter(PubSubResourceFromBackend,
iidavoll.IBackendService,
iwokkel.IPubSubResource)
class ExtraDiscoHandler(XMPPHandler):
implements(iwokkel.IDisco)
# see comment in twisted/plugins/pubsub.py
# FIXME: upstream must be fixed so we can use custom (non pubsub#) disco features
def getDiscoInfo(self, requestor, service, nodeIdentifier=''):
return [disco.DiscoFeature(pubsub.NS_ORDER_BY)]
def getDiscoItems(self, requestor, service, nodeIdentifier=''):
return []
#!/usr/bin/python
#-*- coding: utf-8 -*-
# Copyright (c) 2012-2018 Jérôme Poisson
# Copyright (c) 2012-2019 Jérôme Poisson
# Copyright (c) 2013-2016 Adrien Cossa
# Copyright (c) 2003-2011 Ralph Meijer
......
#!/usr/bin/python
#-*- coding: utf-8 -*-
# Copyright (c) 2012-2018 Jérôme Poisson
# Copyright (c) 2012-2019 Jérôme Poisson
# Copyright (c) 2013-2016 Adrien Cossa
# Copyright (c) 2003-2011 Ralph Meijer
......
#!/usr/bin/python
#-*- coding: utf-8 -*-
# Copyright (c) 2012-2018 Jérôme Poisson
# Copyright (c) 2012-2019 Jérôme Poisson
# Copyright (c) 2013-2016 Adrien Cossa
# Copyright (c) 2003-2011 Ralph Meijer
......
......@@ -2,7 +2,7 @@
#-*- coding: utf-8 -*-
# Copyright (c) 2003-2011 Ralph Meijer
# Copyright (c) 2012-2018 Jérôme Poisson
# Copyright (c) 2012-2019 Jérôme Poisson
# This program is free software: you can redistribute it and/or modify
......
......@@ -2,7 +2,7 @@
#-*- coding: utf-8 -*-
# Copyright (c) 2003-2011 Ralph Meijer
# Copyright (c) 2012-2018 Jérôme Poisson
# Copyright (c) 2012-2019 Jérôme Poisson
# This program is free software: you can redistribute it and/or modify
......
......@@ -85,6 +85,9 @@ class MAMResource(object):
else:
ext_data['rsm'] = mam_request.rsm
if mam_request.orderBy:
ext_data['order_by'] = mam_request.orderBy
d = self.backend.getItemsData(mam_request.node, mam_request.sender, mam_request.recipient, None, None, ext_data)
def make_message(elt):
......@@ -99,10 +102,20 @@ class MAMResource(object):
def cb(items_data):
msg_data = []
rsm_elt = None
attributes = {}
for item_data in items_data:
if item_data.item.name == 'set' and item_data.item.uri == rsm.NS_RSM:
assert rsm_elt is None
rsm_elt = item_data.item
# XXX: we check if it is the last page using initial request data
# and RSM element data. In this case, we must have the "complete"
# attribute set to "true".
page_max = (int(rsm_elt.first['index']) + 1) * mam_request.rsm.max
count = int(unicode(rsm_elt.count))
if page_max >= count:
# the maximum items which can be displayed is equal to or above
# the total number of items, which means we are complete
attributes['complete'] = "true"
elif item_data.item.name == 'item':
msg_data.append([item_data.item['id'], make_message(item_data.item), item_data.created])
else:
......@@ -114,7 +127,7 @@ class MAMResource(object):
for data in msg_data:
self.forwardPEPMessage(mam_request, *data)
msg_data = []
return (msg_data, rsm_elt)
return (msg_data, rsm_elt, attributes)
d.addErrback(self._mapErrors)
d.addCallback(cb)
......
......@@ -2,7 +2,7 @@
#-*- coding: utf-8 -*-
# Copyright (c) 2003-2011 Ralph Meijer
# Copyright (c) 2012-2018 Jérôme Poisson
# Copyright (c) 2012-2019 Jérôme Poisson
# This program is free software: you can redistribute it and/or modify
......
#!/usr/bin/python
#-*- coding: utf-8 -*-
# Copyright (c) 2012-2018 Jérôme Poisson
# Copyright (c) 2012-2019 Jérôme Poisson
# Copyright (c) 2013-2016 Adrien Cossa
# Copyright (c) 2003-2011 Ralph Meijer
......@@ -437,7 +437,7 @@ class Storage:
AND node IN %s
AND nodes.access_model in %s
AND items.access_model in %s
ORDER BY node_id DESC, item_id DESC""",
ORDER BY node_id DESC, items.updated DESC""",
(tuple([e.userhost() for e in entities]),
nodes,
node_accesses,
......@@ -834,6 +834,29 @@ class LeafNode(Node):
nodeType = 'leaf'
def getOrderBy(self, ext_data, direction='DESC'):
"""Return ORDER BY clause corresponding to Order By key in ext_data
@param ext_data (dict): extra data as used in getItems
@param direction (unicode): ORDER BY direction (ASC or DESC)
@return (unicode): ORDER BY clause to use
"""
keys = ext_data.get('order_by')
if not keys:
return u'ORDER BY updated ' + direction
cols_statmnt = []
for key in keys:
if key == 'creation':
column = 'item_id' # could work with items.created too
elif key == 'modification':
column = 'updated'
else:
log.msg(u"WARNING: Unknown order by key: {key}".format(key=key))
column = 'updated'
cols_statmnt.append(column + u' ' + direction)
return u"ORDER BY " + u",".join([col for col in cols_statmnt])
def storeItems(self, item_data, publisher):
return self.dbpool.runInteraction(self._storeItems, item_data, publisher)
......@@ -1031,7 +1054,7 @@ class LeafNode(Node):
query.extend(query_filters)
return "ORDER BY item_id DESC"
return self.getOrderBy(ext_data)
def _getItems(self, cursor, authorized_groups, unrestricted, maxItems, ext_data, ids_only):
self._checkNodeExists(cursor)
......@@ -1075,7 +1098,8 @@ class LeafNode(Node):
# if we have maxItems (i.e. a limit), we need to reverse order
# in a first query to get the right items
query.insert(0,"SELECT * from (")
query.append("ORDER BY item_id ASC LIMIT %s) as x")
query.append(self.getOrderBy(ext_data, direction='ASC'))
query.append("LIMIT %s) as x")
args.append(maxItems)
elif rsm.after:
query.append("AND item_id<(SELECT item_id FROM items WHERE item=%s LIMIT 1)")
......
#!/usr/bin/python
#-*- coding: utf-8 -*-
# Copyright (c) 2019 Jérôme Poisson
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# 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/>.
"""
Pubsub Admin experimental protocol implementation
"""
from zope.interface import implements
from twisted.python import log
from twisted.internet import defer
from twisted.words.protocols.jabber import jid, error, xmlstream
from wokkel.subprotocols import XMPPHandler
from wokkel import disco, iwokkel, pubsub
NS_PUBSUB_ADMIN = u"https://salut-a-toi.org/spec/pubsub_admin:0"
ADMIN_REQUEST = '/iq[@type="set"]/admin[@xmlns="{}"]'.format(NS_PUBSUB_ADMIN)
class PubsubAdminHandler(XMPPHandler):
implements(iwokkel.IDisco)
def __init__(self, backend):
super(PubsubAdminHandler, self).__init__()
self.backend = backend
def connectionInitialized(self):
self.xmlstream.addObserver(ADMIN_REQUEST, self.onAdminRequest)
def sendError(self, iq_elt, condition=u'bad-request'):
stanza_error = error.StanzaError(u'bad-request')
iq_error = stanza_error.toResponse(iq_elt)
self.parent.xmlstream.send(iq_error)
@defer.inlineCallbacks
def onAdminRequest(self, iq_elt):
"""Pubsub Admin request received"""
iq_elt.handled = True
try:
pep = bool(iq_elt.delegated)
except AttributeError:
pep = False
# is the sender really an admin?
admins = self.backend.config[u'admins_jids_list']
from_jid = jid.JID(iq_elt[u'from'])
if from_jid.userhostJID() not in admins:
log.msg("WARNING: admin request done by non admin entity {from_jid}"
.format(from_jid=from_jid.full()))
self.sendError(iq_elt, u'forbidden')
return
# alright, we can proceed
recipient = jid.JID(iq_elt[u'to'])
admin_elt = iq_elt.admin
try:
pubsub_elt = next(admin_elt.elements(pubsub.NS_PUBSUB, u'pubsub'))
publish_elt = next(pubsub_elt.elements(pubsub.NS_PUBSUB, u'publish'))
except StopIteration:
self.sendError(iq_elt)
return
try:
node = publish_elt[u'node']
except KeyError:
self.sendError(iq_elt)
return
# we prepare the result IQ request, we will fill it with item ids
iq_result_elt = xmlstream.toResponse(iq_elt, u'result')
result_admin_elt = iq_result_elt.addElement((NS_PUBSUB_ADMIN, u'admin'))
result_pubsub_elt = result_admin_elt.addElement((pubsub.NS_PUBSUB, u'pubsub'))
result_publish_elt = result_pubsub_elt.addElement(u'publish')
result_publish_elt[u'node'] = node
# now we can send the items
for item in publish_elt.elements(pubsub.NS_PUBSUB, u'item'):
try:
requestor = jid.JID(item.attributes.pop(u'publisher'))
except Exception as e:
log.msg(u"WARNING: invalid jid in publisher ({requestor}): {msg}"
.format(requestor=requestor, msg=e))
self.sendError(iq_elt)
return
except KeyError:
requestor = from_jid
# we don't use a DeferredList because we want to be sure that
# each request is done in order
payload = yield self.backend.publish(
nodeIdentifier=node,
items=[item],
requestor=requestor,
pep=pep,
recipient=recipient)
result_item_elt = result_publish_elt.addElement(u'item')
# either the id was given and it is available in item
# either it's a new item, and we can retrieve it from return payload
try:
result_item_elt[u'id'] = item[u'id']
except KeyError:
result_item_elt = payload.publish.item[u'id']
self.xmlstream.send(iq_result_elt)
def getDiscoInfo(self, requestor, service, nodeIdentifier=''):
return [disco.DiscoFeature(NS_PUBSUB_ADMIN)]
def getDiscoItems(self, requestor, service, nodeIdentifier=''):
return []
......@@ -2,7 +2,7 @@
#-*- coding: utf-8 -*-
# Copyright (c) 2003-2011 Ralph Meijer
# Copyright (c) 2012-2018 Jérôme Poisson
# Copyright (c) 2012-2019 Jérôme Poisson
# This program is free software: you can redistribute it and/or modify
......
......@@ -2,7 +2,7 @@
#-*- coding: utf-8 -*-
# Copyright (c) 2003-2011 Ralph Meijer
# Copyright (c) 2012-2018 Jérôme Poisson
# Copyright (c) 2012-2019 Jérôme Poisson
# This program is free software: you can redistribute it and/or modify
......
......@@ -2,7 +2,7 @@
#-*- coding: utf-8 -*-
# Copyright (c) 2003-2011 Ralph Meijer
# Copyright (c) 2012-2018 Jérôme Poisson
# Copyright (c) 2012-2019 Jérôme Poisson
# This program is free software: you can redistribute it and/or modify
......
......@@ -2,7 +2,7 @@
#-*- coding: utf-8 -*-
# Copyright (c) 2003-2011 Ralph Meijer
# Copyright (c) 2012-2018 Jérôme Poisson
# Copyright (c) 2012-2019 Jérôme Poisson
# This program is free software: you can redistribute it and/or modify
......
#!/usr/bin/python
#-*- coding: utf-8 -*-
# Copyright (c) 2012-2018 Jérôme Poisson
# Copyright (c) 2012-2019 Jérôme Poisson
# Copyright (c) 2003-2011 Ralph Meijer
......@@ -50,10 +50,12 @@
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
import csv
import sat_pubsub
import sys
from twisted.application.service import IServiceMaker
from twisted.application import service
from twisted.python import usage
from twisted.python import usage, log
from twisted.words.protocols.jabber.jid import JID
from twisted.plugin import IPlugin
......@@ -69,7 +71,8 @@ from zope.interface import implements
from sat_pubsub import const
from sat_pubsub import mam as pubsub_mam
from sat_pubsub.backend import BackendService
from sat_pubsub import pubsub_admin
from sat_pubsub.backend import BackendService, ExtraDiscoHandler
from sat_pubsub.schema import SchemaHandler
from sat_pubsub.privilege import PrivilegesHandler
from sat_pubsub.delegation import DelegationsHandler
......@@ -77,6 +80,20 @@ from os.path import expanduser, realpath
import ConfigParser
def coerceListType(value):
return csv.reader(
[value], delimiter=",", quotechar='"', skipinitialspace=True
).next()
def coerceJidListType(value):
values = [JID(v) for v in coerceListType(value)]
if any((j.resource for j in values)):
raise ValueError(u"you must use bare jids")
return values
OPT_PARAMETERS_BOTH = [
['jid', None, None, 'JID this component will be available at'],
['xmpp_pwd', None, None, 'XMPP server component password'],
......@@ -90,7 +107,10 @@ OPT_PARAMETERS_BOTH = [
['db_port', None, None, 'Database port (pgsql backend)'],
]
# here for future use
OPT_PARAMETERS_CFG = []
OPT_PARAMETERS_CFG = [
["admins_jids_list", None, [], "List of administrators' bare jids",
coerceJidListType]
]
CONFIG_FILENAME = u'sat'
# List of the configuration filenames sorted by ascending priority
......@@ -134,6 +154,10 @@ class Options(usage.Options):
param[2] = param[4](value)
except IndexError: # the coerce method is optional
param[2] = value
except Exception as e:
log.err(u'Invalid value for setting "{name}": {msg}'.format(
name=name, msg=e))
sys.exit(1)
except (ConfigParser.NoSectionError, ConfigParser.NoOptionError):
pass
usage.Options.__init__(self)
......@@ -190,7 +214,7 @@ class SatPubsubMaker(object):
from sat_pubsub.memory_storage import Storage
st = Storage()
bs = BackendService(st)
bs = BackendService(st, config)
bs.setName('backend')
bs.setServiceParent(s)
......@@ -228,9 +252,19 @@ class SatPubsubMaker(object):
mam_s.addFilter(data_form.Field(var=const.MAM_FILTER_CATEGORY))
mam_s.setHandlerParent(cs)
pa = pubsub_admin.PubsubAdminHandler(bs)
pa.setHandlerParent(cs)
sh = SchemaHandler()
sh.setHandlerParent(cs)
# wokkel.pubsub doesn't handle non pubsub# disco
# and we need to announce other feature, so this is a workaround
# to add them
# FIXME: propose a patch upstream to fix this situation
ed = ExtraDiscoHandler()
ed.setHandlerParent(cs)
# XXX: delegation must be instancied at the end,
# because it does some MonkeyPatching on handlers
dh = DelegationsHandler()
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment