Skip to content

Commit

Permalink
Refactor slackbridge.py to wsgi.py. Add config loader classes.
Browse files Browse the repository at this point in the history
If you use the PORTAL_n_SIDE_n env config or the slackbridgeconf.py
style config, you need only replace your reference to slackbridge.py to
wsgi.py.

However, you may now start using an inifile for configuration. See
sample.ini for an example.

Configuration loading is done in the following order:

- environment (PORTAL_n_SIDE_n_...)
- if that was not found, load inifile (./slackbridge.ini or the
  filename found in SLACKBRIDGE_INIFILE env)

If slackbridgeconf.py is found, it is loaded as well. Expect this to
change in future commits.

If you're replacing your CONFIG-style slackbridge conf with either the
INI or ENV style config, you can use testconfig.py to compare the
output. The old CONFIG-style config was write only, so a double-check to
see if you made any mistakes in the new INI or ENV config is highly
recommended.

Touches issue #12.
  • Loading branch information
wdoekes committed Aug 28, 2017
1 parent 2dc08b1 commit 8b7eff6
Show file tree
Hide file tree
Showing 11 changed files with 250 additions and 42 deletions.
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,11 @@ __pycache__

*.orig
*.rej
*.swp

# Config files
/slackbridge.ini
/slackbridgeconf.py

# Heroku stuff
/.env
15 changes: 9 additions & 6 deletions sample.env
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,19 @@ [email protected]
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...
32 changes: 32 additions & 0 deletions sample.ini
Original file line number Diff line number Diff line change
@@ -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}
Empty file added slackbridge/__init__.py
Empty file.
Empty file added slackbridge/config/__init__.py
Empty file.
14 changes: 14 additions & 0 deletions slackbridge/config/auto.py
Original file line number Diff line number Diff line change
@@ -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()
72 changes: 72 additions & 0 deletions slackbridge/config/data.py
Original file line number Diff line number Diff line change
@@ -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
37 changes: 37 additions & 0 deletions slackbridge/config/env.py
Original file line number Diff line number Diff line change
@@ -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
60 changes: 60 additions & 0 deletions slackbridge/config/ini.py
Original file line number Diff line number Diff line change
@@ -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)
9 changes: 9 additions & 0 deletions testconfig.py
Original file line number Diff line number Diff line change
@@ -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)
48 changes: 12 additions & 36 deletions slackbridge.py → wsgi.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,8 +60,6 @@
'owh_linked': '<outgoing_token_from_team_2>',
# Web Api token, optional, see https://api.slack.com/web.
'wa_token': '<token_from_team1_user>',
# Optional team 2 name to notify @team2 from team1.
'other_name': '<team2_name>',
},
'<outgoing_token_from_team_2>': {
# The next two settings are for the TEAM1-side.
Expand Down Expand Up @@ -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
Expand Down

0 comments on commit 8b7eff6

Please sign in to comment.