Mercurial > libervia-pubsub
view twisted/plugins/pubsub.py @ 433:920440200570
PostgreSQL: don't use `regconfig` and `GENERATED` column anymore
/!\ pgsql schema needs to be updated /!\
/!\ Minimal PostgreSQL required version is back to 9.5 /!\
`regconfig` is using system table, and `pg_upgrade` can't handle that, causing trouble
when moving to a new major version of PostgreSQL.
To work around this, the `data_fts_cfg` column type in `items` has been changed from
`regconfig` to `text`.
GENERATED column can't be used with type casting to `regconfig`, so the data_fts column is
now generated with a trigger. As a result, the minimal requirement of PostgreSQL 12 is not
necessary anymore.
author | Goffi <goffi@goffi.org> |
---|---|
date | Thu, 14 Jan 2021 17:59:23 +0100 |
parents | 5e8b8ef5c862 |
children | 36c9fb677f1d |
line wrap: on
line source
#!/usr/bin/env python3 #-*- coding: utf-8 -*- # Copyright (c) 2012-2019 Jérôme Poisson # Copyright (c) 2003-2011 Ralph Meijer # 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/>. # -- # This program is based on Idavoll (http://idavoll.ik.nu/), # originaly written by Ralph Meijer (http://ralphm.net/blog/) # It is sublicensed under AGPL v3 (or any later version) as allowed by the original # license. # -- # Here is a copy of the original license: # Copyright (c) 2003-2011 Ralph Meijer # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the # "Software"), to deal in the Software without restriction, including # without limitation the rights to use, copy, modify, merge, publish, # distribute, sublicense, and/or sell copies of the Software, and to # permit persons to whom the Software is furnished to do so, subject to # the following conditions: # The above copyright notice and this permission notice shall be # included in all copies or substantial portions of the Software. # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. import sys import csv import os from os.path import expanduser, realpath import configparser # patch for Python 3.8 compatibility from sat_tmp.twisted import install as install_twisted_patches install_twisted_patches() from zope.interface import implementer from twisted.application.service import IServiceMaker from twisted.application import service from twisted.python import usage, log from twisted.plugin import IPlugin from twisted.words.protocols.jabber.jid import JID import sat_pubsub def coerceListType(value): return next(csv.reader( [value], delimiter=",", quotechar='"', skipinitialspace=True )) def coerceJidListType(value): values = [JID(v) for v in coerceListType(value)] if any((j.resource for j in values)): raise ValueError("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'], ['rhost', None, '127.0.0.1', 'XMPP server host'], ['rport', None, '5347', 'XMPP server port'], ['backend', None, 'pgsql', 'Choice of storage backend'], ['db_user', None, None, 'Database user (pgsql backend)'], ['db_name', None, 'pubsub', 'Database name (pgsql backend)'], ['db_pass', None, None, 'Database password (pgsql backend)'], ['db_host', None, None, 'Database host (pgsql backend)'], ['db_port', None, None, 'Database port (pgsql backend)'], ] OPT_PARAMETERS_CFG = [ ["admins_jids_list", None, [], "List of administrators' bare jids", coerceJidListType] ] # prefix used for environment variables ENV_PREFIX = "SAT_PUBSUB_" # mapping from option name to environment variables to use # each parameter name links to a list of variable environment name # if an environment variable of one of the names exists it will be used # as default value, with priority over config file ENV_OPT_MAP = { # we use the same environment variables as PostgreSQL 'db_user': ['PGUSER'], 'db_name': ['PGDATABASE'], 'db_pass': ['PGPASSWORD'], 'db_host': ['PGHOST'], 'db_port': ['PGPORT'], } for opt in OPT_PARAMETERS_BOTH + OPT_PARAMETERS_CFG: name = opt[0] env_name = f"{ENV_PREFIX}{name.upper()}" ENV_OPT_MAP.setdefault(name, []).append(env_name) CONFIG_FILENAME = 'sat' # List of the configuration filenames sorted by ascending priority CONFIG_FILES = [realpath(expanduser(path) + CONFIG_FILENAME + '.conf') for path in ( '/etc/', '/etc/{}/'.format(CONFIG_FILENAME), '~/', '~/.', '.config/', '.config/.', '.config/{}/'.format(CONFIG_FILENAME), '', '.')] CONFIG_SECTION = 'pubsub' class Options(usage.Options): optParameters = OPT_PARAMETERS_BOTH optFlags = [ ('verbose', 'v', 'Show traffic'), ('hide-nodes', None, 'Hide all nodes for disco') ] def __init__(self): """Read SàT Pubsub configuration file in order to overwrite the hard-coded default values. Priority for the usage of the values is (from lowest to highest): - hard-coded default values - values from SàT configuration files - values passed on the command line """ # If we do it the reading later: after the command line options have been parsed, there's no good way to know # if the options values are the hard-coded ones or if they have been passed on the command line. # FIXME: must be refactored + code can be factorised with backend config_parser = configparser.ConfigParser() config_parser.read(CONFIG_FILES) for param in self.optParameters + OPT_PARAMETERS_CFG: name = param[0] for env_name in ENV_OPT_MAP[name]: # we first check if value is set as an environment variable value = os.getenv(env_name) if value is not None: self.setDefaultOption(param, value) break else: # no environment variable set, let's try with configuration try: value = config_parser.get(CONFIG_SECTION, name) self.setDefaultOption(param, value) except (configparser.NoSectionError, configparser.NoOptionError): pass usage.Options.__init__(self) for opt_data in OPT_PARAMETERS_CFG: self[opt_data[0]] = opt_data[2] def setDefaultOption(self, param, value): """Set default option value using coerce method when needed If the value is invalid, we quit the program with exit code 1 """ try: param[2] = param[4](value) except IndexError: # the coerce method is optional param[2] = value except Exception as e: log.err('Invalid value for setting "{name}": {msg}'.format( name=name, msg=e)) sys.exit(1) def postOptions(self): if self['backend'] not in ['pgsql', 'memory']: raise usage.UsageError("Unknown backend!") if self['backend'] == 'memory': raise NotImplementedError('memory backend is not available at the moment') self['jid'] = JID(self['jid']) if self['jid'] else None @implementer(IServiceMaker, IPlugin) class SatPubsubMaker(object): tapname = "sat-pubsub" description = "Salut à Toi Publish-Subscribe Service Component" options = Options def makeService(self, config): from wokkel.component import Component from wokkel.disco import DiscoHandler from wokkel.generic import FallbackHandler, VersionHandler from wokkel.iwokkel import IPubSubResource from wokkel import data_form from wokkel import pubsub from wokkel import rsm from wokkel import mam from sat_pubsub import const from sat_pubsub import mam as pubsub_mam from sat_pubsub import pubsub_admin from sat_pubsub.backend import BackendService, ExtraDiscoHandler from sat_pubsub.privilege import PrivilegesHandler from sat_pubsub.delegation import DelegationsHandler if not config['jid'] or not config['xmpp_pwd']: raise usage.UsageError("You must specify jid and xmpp_pwd") s = service.MultiService() # Create backend service with storage if config['backend'] == 'pgsql': from twisted.enterprise import adbapi from sat_pubsub.pgsql_storage import Storage from psycopg2.extras import NamedTupleConnection keys_map = { 'db_user': 'user', 'db_pass': 'password', 'db_name': 'database', 'db_host': 'host', 'db_port': 'port', } kwargs = {} for config_k, k in keys_map.items(): v = config.get(config_k) if v is None: continue kwargs[k] = v dbpool = adbapi.ConnectionPool('psycopg2', cp_reconnect=True, client_encoding='utf-8', connection_factory=NamedTupleConnection, **kwargs ) st = Storage(dbpool) elif config['backend'] == 'memory': raise NotImplementedError('memory backend is not available at the moment') bs = BackendService(st, config) bs.setName('backend') bs.setServiceParent(s) # Set up XMPP server-side component with publish-subscribe capabilities cs = Component(config["rhost"], int(config["rport"]), config["jid"].full(), config["xmpp_pwd"]) cs.setName('component') cs.setServiceParent(s) cs.factory.maxDelay = 900 if config["verbose"]: cs.logTraffic = True FallbackHandler().setHandlerParent(cs) VersionHandler('SàT Pubsub', sat_pubsub.__version__).setHandlerParent(cs) DiscoHandler().setHandlerParent(cs) ph = PrivilegesHandler(config['jid']) ph.setHandlerParent(cs) bs.privilege = ph resource = IPubSubResource(bs) resource.hideNodes = config["hide-nodes"] resource.serviceJID = config["jid"] ps = (rsm if const.FLAG_ENABLE_RSM else pubsub).PubSubService(resource) ps.setHandlerParent(cs) resource.pubsubService = ps if const.FLAG_ENABLE_MAM: mam_resource = pubsub_mam.MAMResource(bs) mam_s = mam.MAMService(mam_resource) mam_s.addFilter(data_form.Field(var=const.MAM_FILTER_CATEGORY)) mam_s.addFilter(data_form.Field(var=const.MAM_FILTER_FTS)) mam_s.setHandlerParent(cs) pa = pubsub_admin.PubsubAdminHandler(bs) pa.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() dh.setHandlerParent(cs) bs.delegation = dh return s serviceMaker = SatPubsubMaker()