From 069977ffaf12321d924ff7bdde060e994234eeaa Mon Sep 17 00:00:00 2001 From: David Date: Wed, 21 Aug 2019 21:55:09 +1000 Subject: [PATCH] include basic options to relay messages from Bots, as well as basic Docker deployment for quick and dirty setups --- .gitignore | 1 + Dockerfile | 10 +++++ README.md | 6 +++ docker-compose.yml | 25 ++++++++++++ entrypoint.sh | 14 +++++++ requirements.txt | 4 ++ slackrelay.py | 94 ++++++++++++++++++++++++++++++++++------------ 7 files changed, 131 insertions(+), 23 deletions(-) create mode 100644 Dockerfile create mode 100644 docker-compose.yml create mode 100644 entrypoint.sh create mode 100644 requirements.txt diff --git a/.gitignore b/.gitignore index e0f558a..beaa0a7 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ slackrelay.json +.DS_Store diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..f8153cc --- /dev/null +++ b/Dockerfile @@ -0,0 +1,10 @@ +FROM python:3 + +WORKDIR /opt/slack-relay + +COPY entrypoint.sh requirements.txt ./ +RUN pip install --no-cache-dir -r requirements.txt + +COPY . . + +CMD [ "bash", "/opt/slack-relay/entrypoint.sh" ] diff --git a/README.md b/README.md index 5a402fb..1ba35a2 100644 --- a/README.md +++ b/README.md @@ -29,6 +29,7 @@ optional arguments: configuration -f CONFIG_FILE, --config-file CONFIG_FILE Configuration file + -mb, --mirror-bots True|False Option to relay messages from other Bots (such as your Build server Notifications) -e EMOJI_TO_CONFIRM, --emoji-to-confirm EMOJI_TO_CONFIRM Emoji that relayed messages will be updated with (reacted to) to show confirmation to humans, e.g. @@ -66,6 +67,11 @@ Test message2 Test message3 ``` +Docker Usage / Running in Production +------------------------------------ + +A Dockerfile and docker-compose file has been included for ease of deployment... Simply `vi docker-compose.yml` and then run `docker-compose up -d` to start the container on a server. + Contribute ---------- diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..ab3dee9 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,25 @@ +version: '3' +services: + + slack-relay: + container_name: slack-relay + build: . + + restart: always + privileged: true + + volumes: + - ./conf/:/opt/slack-relay/conf + + environment: + - CONFIG_FILE=/opt/slack-relay/conf/slackrelay.json + + ## You MUST set these variables at a minimum to run the application. + #- BOT_TOKEN= + #- BOT_NAME=name_of_your_bot + + ## Uncomment/set the following variables as required: + #- LOG_LEVEL=debug + #- MIRROR_BOTS=True + #- EMOJI_TO_CONFIRM= + #- SLEEP_MS= \ No newline at end of file diff --git a/entrypoint.sh b/entrypoint.sh new file mode 100644 index 0000000..6f2242b --- /dev/null +++ b/entrypoint.sh @@ -0,0 +1,14 @@ +#!/bin/bash + +OPTARGS="" + +[[ ! -z "$LOG_LEVEL" ]] && OPTARGS="$OPTARGS -l $LOG_LEVEL" +[[ ! -z "$BOT_NAME" ]] && OPTARGS="$OPTARGS --bot $BOT_NAME" +[[ ! -z "$CONFIG_FILE" ]] && OPTARGS="$OPTARGS --config-file $CONFIG_FILE" +[[ ! -z "$EMOJI_TO_CONFIRM" ]] && OPTARGS="$OPTARGS --emoji-to-confirm $EMOJI_TO_CONFIRM" +[[ ! -z "$SLEEP_MS" ]] && OPTARGS="$OPTARGS --sleep-ms $SLEEP_MS" +[[ ! -z "$MIRROR_BOTS" ]] && OPTARGS="$OPTARGS --mirror-bots $MIRROR_BOTS" + +[[ ! -z "$BOT_TOKEN" ]] && OPTARGS="$OPTARGS $BOT_TOKEN" + +python ./slackrelay.py $OPTARGS \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..ff8e580 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,4 @@ +slackclient==1.3.2 +jason==0.1.7 +simplejson==3.16.0 +Django==2.2.4 \ No newline at end of file diff --git a/slackrelay.py b/slackrelay.py index b78d808..aa44e37 100755 --- a/slackrelay.py +++ b/slackrelay.py @@ -1,14 +1,14 @@ #!/usr/bin/env python - + # Module: slackrelay # Date: 5th January 2017 # Author: Cristiano Giuffrida, giuffrida at cs dot vu dot nl - + """slackrelay - + Slack Relay Bot. """ - + __desc__ = "Slack Relay Bot" __version__ = "0.1" __author__ = "Cristiano Giuffrida" @@ -132,6 +132,16 @@ def lookup(sc, team, name): return Bot(user.id, user.name, user.image) err_exit(3, "Unable to find bot identity") + def lookupBot(sc, botId): + bots = sc.api_call( + "bots.info", + bot=botId + ) + + logging.debug("BOT INFO: %s" % bots) + return bots + + class Rule: def __init__(self, name, fTeam, fChannel, backend='echo', bURL=None): self.name=name @@ -212,7 +222,8 @@ def handleCommand(self, team, channel, cmd, prefix): syntax = "Syntax: %s [rule-add json] [rule-del name] [rule-del-all] [rule-list] [help]" % prefix try: if cmd.startswith(' rule-add '): - args = cmd[10:].strip() + args = cmd[10:].strip().replace(u"“", "\"").replace(u"”", "\"") + logging.debug(args) ruleDict = json.loads(args) ruleDict['frontend-team'] = team.name ruleDict['frontend-channel'] = channel.name @@ -246,7 +257,7 @@ def load(self): if not os.path.isfile(self.file): self.store() with open(self.file, mode='rb') as f: - ruleSet = json.load(f) + ruleSet = json.load(f) for d in ruleSet: r = Rule.fromDict(d) if not self.addRule(r): @@ -255,7 +266,7 @@ def load(self): def store(self): ruleSet = self.getRuleSet() - with open(self.file, mode='wb') as f: + with open(self.file, mode='w') as f: json.dump(ruleSet, f, indent=4, sort_keys=True) def err_exit(status, message): @@ -264,12 +275,12 @@ def err_exit(status, message): def parse_args(): """parse_args() -> args - + Parse any command-line arguments.. """ parser = argparse.ArgumentParser(description=__desc__) - + parser.add_argument("-l", "--log", default="warning", choices=['debug', 'info', 'warning', 'error'], help="Log level") @@ -293,11 +304,15 @@ def parse_args(): default=100, help="Polling interval (ms)") + parser.add_argument("-mb", "--mirror-bots", + default=False, + help="Mirrors messages from bots, as well as users when relaying messages with the 'slack-iwh' rule.") + parser.add_argument("-v", "--version", action='version', version=__version__) - + parser.add_argument("bot_user_token") - + args = parser.parse_args() return args @@ -353,29 +368,44 @@ def main(): logging.warning("Reconnecting to bot..") (bot,team,sc) = connect_to_bot(args.bot_user_token, args.bot) for part in response: + logging.debug(part) # Skip nonmessages and bot messages if len(part) == 0: + logging.debug("part length is 0!") continue if 'type' not in part: logging.warning("Type not in part: %s" % str(part)) if 'type' in part and part['type'] != 'message': + logging.debug("'type' in part and part['type'] != 'message'") continue - if 'bot_id' in part: - continue + if args.mirror_bots == False: + if 'bot_id' in part: + logging.debug("'bot_id' in part") + continue if 'previous_message' in part and 'bot_id' in part['previous_message']: + logging.debug("'previous_message' in part and 'bot_id' in part['previous_message']") continue + logging.debug(response) + # Lookup event channel logging.debug("New event: %s" % part) channel = Channel.lookup(sc, team, part['channel']) # Handle @slackrelay commands - if not 'subtype' in part and part['text'].startswith(bot.commandPrefix): - if args.slave: - logging.warning('Skipping command "%s" (slave mode)' % part['text']) - else: - ret = config.handleCommand(team, channel, part['text'], bot.commandPrefix) - sc.api_call("chat.postMessage", channel=part['channel'], text=ret, username=bot.name, icon_url=bot.image) + try: + if not 'subtype' in part and part['text'].startswith(bot.commandPrefix): + if args.slave: + logging.warning('Skipping command "%s" (slave mode)' % part['text']) + else: + ret = config.handleCommand(team, channel, part['text'], bot.commandPrefix) + sc.api_call("chat.postMessage", channel=part['channel'], text=ret, username=bot.name, icon_url=bot.image) + continue + except: + continue + + ret = config.handleCommand(team, channel, part['text'], bot.commandPrefix) + sc.api_call("chat.postMessage", channel=part['channel'], text=ret, username=bot.name, icon_url=bot.image) continue # See if we have any matching rules @@ -391,8 +421,12 @@ def main(): # Determine user and text user = None text = None + isOtherBot = False if 'subtype' in part: mtype = part['subtype'] + + logging.debug("TYPE: %s" % mtype) + if mtype == 'message_deleted': user = User.lookup(sc, team, part['previous_message']['user']) text = '[DELETED] %s' % part['previous_message']['text'] @@ -401,12 +435,26 @@ def main(): text = '[EDITED] %s -> %s' % (part['previous_message']['text'], part['message']['text']) elif mtype == 'me_message': part['text'] = "/me %s" % part['text'] + elif args.mirror_bots != False and mtype == 'bot_message': + part['text'] = part['text'] + isOtherBot = True else: logging.warning("Unhandled message, skipping") continue if not user: - user = User.lookup(sc, team, part['user']) - text = part['text'] + if isOtherBot == False: + user = User.lookup(sc, team, part['user']) + text = part['text'] + if isOtherBot == True: + CurrentBot = Bot.lookupBot(sc, part['bot_id']) + class botUser: + fullName = CurrentBot['bot']['name'] + "@" + team.name + image = CurrentBot['bot']['icons']['image_72'] + user = botUser + try: + text = part['attachments'][0]['fallback'] + except: + continue for p in usernamePattern.findall(text): src = p @@ -428,9 +476,9 @@ def main(): "icon_url": user.image } req = requests.post(r.bURL, json.dumps(payload), headers={'content-type': 'application/json'}) - req = req.ok + req = req.ok else: - req = sc.api_call("chat.postMessage", channel=part['channel'], text=text, username=user.fullName, icon_url=user.image) + req = sc.api_call("chat.postMessage", channel=part['channel'], text=text, username=user.fullName, icon_url=user.image) req = req['ok'] except Exception: print(traceback.format_exc())