diff --git a/.gitignore b/.gitignore index aeddaff..e2337dc 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,11 @@ __pycache__ *.orig *.rej +*.swp + +# Config files +/slackbridge.ini +/slackbridgeconf.py # Heroku stuff /.env diff --git a/sample.env b/sample.env index 8476fc0..cae4fdd 100644 --- a/sample.env +++ b/sample.env @@ -2,16 +2,19 @@ ADMIN_EMAIL=admin@example.com WEB_CONCURRENCY=3 # CivicTechTO Slack -PORTAL_1_SIDE_A_WEBHOOK_OUT_TOKEN=xxxxxxxxxxxx -PORTAL_1_SIDE_A_WEBHOOK_IN_URL=https://hooks.slack.com/services/xxxxxxxxxxxx +# (GROUP_NAME is what we call them using @GROUP_NAME) +PORTAL_1_SIDE_A_WEBHOOK_OUT_TOKEN=xxxxxxxxxxx1 +PORTAL_1_SIDE_A_WEBHOOK_IN_URL=https://hooks.slack.com/services/xxxxxxxxxxx1 PORTAL_1_SIDE_A_CHANNEL_NAME=portal-yowcivictech PORTAL_1_SIDE_A_GROUP_NAME=yowcivictech -PORTAL_1_SIDE_A_WEB_API_TOKEN=xxxxxxxxxxxx +PORTAL_1_SIDE_A_WEB_API_TOKEN=xoxp-xxxxxxxxxxx1 + # YOWCivicTech Slack -PORTAL_1_SIDE_B_WEBHOOK_OUT_TOKEN=xxxxxxxxxxxx -PORTAL_1_SIDE_B_WEBHOOK_IN_URL=https://hooks.slack.com/services/xxxxxxxxxxxx +PORTAL_1_SIDE_B_WEBHOOK_OUT_TOKEN=xxxxxxxxxxx2 +PORTAL_1_SIDE_B_WEBHOOK_IN_URL=https://hooks.slack.com/services/xxxxxxxxxxx2 PORTAL_1_SIDE_B_CHANNEL_NAME=portal-civictechto PORTAL_1_SIDE_B_GROUP_NAME=civictechto -PORTAL_1_SIDE_B_WEB_API_TOKEN=xxxxxxxxxxxx +# (WEB_API_TOKEN is allowed to be empty) +PORTAL_1_SIDE_B_WEB_API_TOKEN= # Can increment portal number and add another set... diff --git a/sample.ini b/sample.ini new file mode 100644 index 0000000..e5196be --- /dev/null +++ b/sample.ini @@ -0,0 +1,32 @@ +; SlackBridge slackbridge.ini configuration file. +; +; ONLY WORKS WITH PYTHON3! +; +; Comments are valid after ';', even on the same line. Values may span +; multiple lines, as long as indentation is used. Value reuse: [DEFAULT] +; VAL=X [section1] key = ${VAL}YZ. +; +; Sections should contain L.webhook_in_url, L.webhook_out_token, +; L.channel, L.peername and optionally L.webapi_token; twice, for L +; being A and B. +; +; Example: +; +; [DEFAULT] +; WEBHOOK_IN_URL = https://hooks.slack.com/services +; WEBHOOK_IN_URL_OSSO = ${WEBHOOK_IN_URL}/X/Y/Z +; WEBAPI_TOKEN_OSSO = xoxp-token-token-token +; +; [company1-osso] +; A.webhook_in_url = +; ${WEBHOOK_IN_URL}/AAAAAAAAA/BBBBBBBBB/cccccccccccccccccccccccc +; A.webhook_out_token = dddddddddddddddddddddddd +; A.channel = CXXXXXXXX +; A.peername = osso +; A.webapi_token = +; +; B.webhook_in_url = ${WEBHOOK_IN_URL_OSSO} +; B.webhook_out_token = eeeeeeeeeeeeeeeeeeeeeeee +; B.channel = #shared-company1 +; B.peername = company1 +; B.webapi_token = ${WEBAPI_TOKEN_OSSO} diff --git a/slackbridge/__init__.py b/slackbridge/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/slackbridge/config/__init__.py b/slackbridge/config/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/slackbridge/config/auto.py b/slackbridge/config/auto.py new file mode 100644 index 0000000..9fccb1e --- /dev/null +++ b/slackbridge/config/auto.py @@ -0,0 +1,14 @@ +from . import env, ini +from .ini import load as ini_load + + +def load(): + for mod in (env, ini): + try: + configs = mod.load() + except StopIteration: + pass + else: + return configs + + raise StopIteration() diff --git a/slackbridge/config/data.py b/slackbridge/config/data.py new file mode 100644 index 0000000..57f893b --- /dev/null +++ b/slackbridge/config/data.py @@ -0,0 +1,72 @@ +class BridgeEndConfig(object): + """ + All settings are mandatory, except the WEBAPI_TOKEN. However, it is + preferred to have access to a limited user to supply info about + @USERS, @CHANNELS, Avatars and the list of people online. + + Use a "Bot user" (single channel guest) in your #shared-peername + channel for limited access. + """ + # From Slack to us: + # '0123ABCDabcdefghijklmnop' + WEBHOOK_OUT_TOKEN = None + # From us to Slack: + # 'https://hooks.slack.com/services/xxxxxxxxxxx' + WEBHOOK_IN_URL = None + # What channel name we use when writing to Slack: + # 'C012345ZYX' or '#shared-peername' + CHANNEL = None + # The "@mention" to @channel only the other side: + # 'othercompany' + PEERNAME = None + # WebAPI token for information gathering: + # 'xoxp-0123456789-0123456789-0123456789-abcdef' + WEBAPI_TOKEN = None + + +class BridgeConfig(object): + def __init__(self, name, side_a, side_b): + self.NAME = name + self.SIDE_A = side_a + self.SIDE_B = side_b + + def __str__(self): + return '<{}: {}->@{}, {}->@{}>'.format( + self.NAME, self.SIDE_A.CHANNEL, self.SIDE_A.PEERNAME, + self.SIDE_B.CHANNEL, self.SIDE_B.PEERNAME) + + +class BridgeConfigs(object): + """ + Settings object holding all of the settings to be consumed by actual + constructors of the actual slack communication. + """ + def __init__(self): + self._pairs = [] + + def __len__(self): + return len(self._pairs) # also for boolean check + + def add_config(self, bridgeconfig): + self._pairs.append(bridgeconfig) + + def to_config_dict(self): + """ + Convert settings into old-style CONFIG dict. + """ + ret = {} + for pair in self._pairs: + for our, their in ( + (pair.SIDE_A, pair.SIDE_B), (pair.SIDE_B, pair.SIDE_A)): + ret[our.WEBHOOK_OUT_TOKEN] = { + 'iwh_url': their.WEBHOOK_IN_URL, + 'iwh_update': { + 'channel': their.CHANNEL, + '_atchannel': our.PEERNAME, + }, + 'owh_linked': their.WEBHOOK_OUT_TOKEN, + } + if our.WEBAPI_TOKEN: + ret[our.WEBHOOK_OUT_TOKEN]['wa_token'] = our.WEBAPI_TOKEN + + return ret diff --git a/slackbridge/config/env.py b/slackbridge/config/env.py new file mode 100644 index 0000000..b5690d8 --- /dev/null +++ b/slackbridge/config/env.py @@ -0,0 +1,37 @@ +from os import environ as env + +from .data import BridgeConfig, BridgeConfigs, BridgeEndConfig + + +def load(): + configs = BridgeConfigs() + try: + idx = 1 + + while True: + pair = [] + for L in 'AB': + end = BridgeEndConfig() + end.WEBHOOK_OUT_TOKEN = env[ + 'PORTAL_{}_SIDE_{}_WEBHOOK_OUT_TOKEN'.format(idx, L)] + end.WEBHOOK_IN_URL = env[ + 'PORTAL_{}_SIDE_{}_WEBHOOK_IN_URL'.format(idx, L)] + end.CHANNEL = env[ + 'PORTAL_{}_SIDE_{}_CHANNEL_NAME'.format(idx, L)] + end.PEERNAME = env[ # GROUP_NAME is "them", i.e. peername + 'PORTAL_{}_SIDE_{}_GROUP_NAME'.format(idx, L)] + end.WEBAPI_TOKEN = env[ + 'PORTAL_{}_SIDE_{}_WEB_API_TOKEN'.format(idx, L)] + pair.append(end) + + name = '{}-{}'.format(pair[1].PEERNAME, pair[0].PEERNAME) + configs.add_config(BridgeConfig(name, *pair)) + idx += 1 + except KeyError: + # Stop at first keyerror. + pass + + if not len(configs): + raise StopIteration('No SlackBridge config found in ENV') + + return configs diff --git a/slackbridge/config/ini.py b/slackbridge/config/ini.py new file mode 100644 index 0000000..86631dc --- /dev/null +++ b/slackbridge/config/ini.py @@ -0,0 +1,60 @@ +from os import environ + +from .data import BridgeConfig, BridgeConfigs, BridgeEndConfig + + +class BridgeConfigsFromIni(BridgeConfigs): + def __init__(self, inifile): + self._res = BridgeConfigs() + self._ini = inifile + self._load() + + def get(self): + return self._res + + def _load(self): + for section in self._ini.sections(): + self._load_section(section, self._ini[section]) + + def _load_section(self, name, section): + pair = [] + for L in 'AB': + def get(key): + return section['{}.{}'.format(L, key)].strip() + + end = BridgeEndConfig() + end.WEBHOOK_IN_URL = get('webhook_in_url') + end.WEBHOOK_OUT_TOKEN = get('webhook_out_token') + end.CHANNEL = get('channel') + end.PEERNAME = get('peername') + end.WEBAPI_TOKEN = get('webapi_token') + pair.append(end) + + self._res.add_config(BridgeConfig(name, *pair)) + + +def configs_from_inifile(inifile): + try: + from configparser import ConfigParser, ExtendedInterpolation + except ImportError: + import errno + raise IOError( + errno.ENOENT, + 'Missing ConfigParser and ExtendedInterpolation: ' + 'please use python3+') + else: + parser = ConfigParser( + allow_no_value=False, # don't allow "key" without "=" + delimiters=('=',), # inifile "=" between key and value + comment_prefixes=(';',), # only ';' for comments (fixes #channel) + inline_comment_prefixes=(';',), # comments after lines + interpolation=ExtendedInterpolation(), + empty_lines_in_values=False) # empty line means new key + parser.read_file(inifile) + return BridgeConfigsFromIni(parser).get() + + +def load(): + filename = environ.get('SLACKBRIDGE_INIFILE', './slackbridge.ini') + with open(filename) as inifile: + return configs_from_inifile(inifile) diff --git a/testconfig.py b/testconfig.py new file mode 100644 index 0000000..1e7f96c --- /dev/null +++ b/testconfig.py @@ -0,0 +1,9 @@ +# Example script to check that your config still looks like it did +# before upgrading. Compare it with the output of your COFIG dict before +# updating to the new-style config. +from pprint import pprint +from slackbridge.config.auto import load + +configs = load() +CONFIG = configs.to_config_dict() +pprint(CONFIG) diff --git a/slackbridge.py b/wsgi.py similarity index 94% rename from slackbridge.py rename to wsgi.py index 76ebbd6..d2815af 100644 --- a/slackbridge.py +++ b/wsgi.py @@ -60,8 +60,6 @@ 'owh_linked': '', # Web Api token, optional, see https://api.slack.com/web. 'wa_token': '', - # Optional team 2 name to notify @team2 from team1. - 'other_name': '', }, '': { # The next two settings are for the TEAM1-side. @@ -132,48 +130,26 @@ from email.header import Header from email.mime.text import MIMEText from multiprocessing import Process, Pipe -from os import environ as env from pprint import pformat +from slackbridge.config import auto # BASE_PATH needs to be set to the path prefix (location) as configured # in the web server. BASE_PATH = '/' -# CONFIG is a dictionary indexed by "Outgoing WebHooks" token. -# The subdictionaries contain 'iwh_url' for "Incoming WebHooks" post and -# a dictionary with payload updates ({'channel': '#new_chan'}). -# TODO: should we index it by "service_id" instead of "(owh)token"? + +# Load CONFIG: autodetect which kind of config we use. try: - CONFIG = {} - i = 1 - - while True: - portal_config = { - env['PORTAL_{}_SIDE_A_WEBHOOK_OUT_TOKEN'.format(i)]: { - 'iwh_url': env['PORTAL_{}_SIDE_B_WEBHOOK_IN_URL'.format(i)], - 'iwh_update': { - 'channel': env['PORTAL_{}_SIDE_B_CHANNEL_NAME'.format(i)], - '_atchannel': env['PORTAL_{}_SIDE_A_GROUP_NAME'.format(i)], - }, - 'owh_linked': ( - env['PORTAL_{}_SIDE_B_WEBHOOK_OUT_TOKEN'.format(i)]), - 'wa_token': env['PORTAL_{}_SIDE_A_WEB_API_TOKEN'.format(i)], - }, - env['PORTAL_{}_SIDE_B_WEBHOOK_OUT_TOKEN'.format(i)]: { - 'iwh_url': env['PORTAL_{}_SIDE_A_WEBHOOK_IN_URL'.format(i)], - 'iwh_update': { - 'channel': env['PORTAL_{}_SIDE_A_CHANNEL_NAME'.format(i)], - '_atchannel': env['PORTAL_{}_SIDE_B_GROUP_NAME'.format(i)], - }, - 'owh_linked': ( - env['PORTAL_{}_SIDE_A_WEBHOOK_OUT_TOKEN'.format(i)]), - 'wa_token': env['PORTAL_{}_SIDE_B_WEB_API_TOKEN'.format(i)], - }, - } - CONFIG.update(portal_config) - i += 1 -except KeyError: + bridgeconfigs = auto.load() +except StopIteration: + # Hope you have the CONFIG in slackbridgeconf like before. pass +else: + # CONFIG is a dictionary indexed by "Outgoing WebHooks" token. The + # subdictionaries contain 'iwh_url' for "Incoming WebHooks" post and + # a dictionary with payload updates ({'channel': '#new_chan'}). + # NOTE: This is about to change. Expect the CONFIG dict to be removed. + CONFIG = bridgeconfigs.to_config_dict() # Lazy initialization of workers? LAZY_INITIALIZATION = True # use, unless you have uwsgi-lazy-apps