From: Carl Worth Date: Tue, 29 Sep 2020 19:44:58 +0000 (-0700) Subject: Break out rot.py and slack.py from turbot.py X-Git-Url: https://git.cworth.org/git?p=turbot;a=commitdiff_plain;h=ff26920dffe100e3aaad8cbab829eef0b58b617b Break out rot.py and slack.py from turbot.py Using separate modules to make the code more maintainable. --- diff --git a/turbot.wsgi.in b/turbot.wsgi.in index 7817355..5a260bf 100644 --- a/turbot.wsgi.in +++ b/turbot.wsgi.in @@ -4,4 +4,4 @@ sys.path.insert(0, '${DEPLOY_DIR}') from dotenv import load_dotenv load_dotenv('${DEPLOY_DIR}/.turbot.env') -from turbot import app as application +from turbot.turbot import app as application diff --git a/turbot/rot.py b/turbot/rot.py new file mode 100644 index 0000000..e991941 --- /dev/null +++ b/turbot/rot.py @@ -0,0 +1,71 @@ +from flask import Blueprint, request, make_response +from turbot.slack import slack_is_valid_request, slack_send_reply +import re + +rot_route = Blueprint('rot_route', __name__) + +def rot_string(str, n=13): + """Return a rotated version of a string + + Specifically, this functions returns a version of the input string + where each uppercase letter has been advanced 'n' positions in the + alphabet (wrapping around). Lowercase letters and any non-alphabetic + characters will be unchanged.""" + + result = '' + for letter in str: + if letter.isupper(): + result += chr(ord("A") + (ord(letter) - ord("A") + n) % 26) + else: + result += letter + return result + +@rot_route.route('/rot', methods = ['POST']) +def rot(): + """Implements the /rot route for the /rot slash command in Slack + + This implements the /rot command of our Slack bot. The format of this + command is as follows: + + /rot [count|*] String to be rotated + + The optional count indicates an amount to rotate each character in the + string. If the count is '*' or is not present, then the string will + be rotated through all possible 25 values. + + The result of the rotation is provided as a message in Slack. If the + slash command was issued in a direct message, the response is made by + using the "response_url" from the request. This allows the bot to reply + in a direct message that it is not a member of. Otherwise, if the slash + command was issued in a channel, the bot will reply in that channel.""" + + if not slack_is_valid_request(request): + return make_response("invalid request", 403) + + query = request.form.get('text') + match = re.match(r'^([0-9]+|\*) (.*)$', query) + if (match): + try: + count = int(match.group(1)) + except ValueError: + count = None + text = match.group(2) + else: + count = None + text = query + + text = text.upper() + + reply = "```/rot {} {}\n".format(count if count else '*', text) + + if count: + reply += rot_string(text, count) + else: + reply += "\n".join(["{:02d} ".format(count) + rot_string(text, count) + for count in range(1, 26)]) + + reply += "```" + + slack_send_reply(request, reply) + + return "" diff --git a/turbot/slack.py b/turbot/slack.py new file mode 100644 index 0000000..9982822 --- /dev/null +++ b/turbot/slack.py @@ -0,0 +1,59 @@ +from flask import current_app +from slack import WebClient +from slack.errors import SlackApiError +from slack.signature import SignatureVerifier +import os +import requests + +slack_signing_secret = os.environ['SLACK_SIGNING_SECRET'] +slack_bot_token = os.environ['SLACK_BOT_TOKEN'] + +signature_verifier = SignatureVerifier(slack_signing_secret) +slack_client = WebClient(slack_bot_token) + +def slack_is_valid_request(request): + """Returns true if request actually came from Slack. + + By means of checking the requests signature together with the slack + signing key. + + Note: If flask is in debug mode, this function will always return true.""" + + if current_app.debug: + return True + + data = request.get_data() + headers = request.headers + + return signature_verifier.is_valid_request(data, headers) + +def slack_send_reply(request, text): + """Send a Slack message as a reply to a specified request. + + If the request is associated with a direct message, the reply is + made by using the "response_url" from the request. Otherwise, the + reply will be sent to the channel associated with the request. + + Note: If flask is in debug mode, this function will just print the + text to stdout.""" + + app = current_app + channel_name = request.form.get('channel_name') + response_url = request.form.get('response_url') + channel = request.form.get('channel_id') + + if (app.debug): + print("Sending message to channel '{}': {}".format(channel, text)) + return + + if (channel_name == "directmessage"): + resp = requests.post(response_url, + json = {"text": text}, + headers = {"Content-type": "application/json"}) + if (resp.status_code != 200): + app.logger.error("Error posting request to Slack: " + resp.text) + else: + try: + slack_client.chat_postMessage(channel=channel, text=text) + except SlackApiError as e: + app.logger.error("Slack API error: " + e.response["error"]) diff --git a/turbot/turbot.py b/turbot/turbot.py index 2b9462f..fa37804 100755 --- a/turbot/turbot.py +++ b/turbot/turbot.py @@ -1,135 +1,15 @@ #!/usr/bin/env python3 -from flask import Flask, request, make_response - +from flask import Flask from slackeventsapi import SlackEventAdapter -from slack import WebClient -from slack.errors import SlackApiError -from slack.signature import SignatureVerifier import os -import requests -import re +from turbot.rot import rot_route app = Flask(__name__) +app.register_blueprint(rot_route) slack_signing_secret = os.environ['SLACK_SIGNING_SECRET'] -slack_bot_token = os.environ['SLACK_BOT_TOKEN'] - slack_events = SlackEventAdapter(slack_signing_secret, "/slack/events", app) -signature_verifier = SignatureVerifier(slack_signing_secret) -slack_client = WebClient(slack_bot_token) - -def slack_is_valid_request(request): - """Returns true if request actually came from Slack. - - By means of checking the requests signature together with the slack - signing key. - - Note: If flask is in debug mode, this function will always return true.""" - - if app.debug: - return True - - data = request.get_data() - headers = request.headers - - return signature_verifier.is_valid_request(data, headers) - -def slack_send_reply(request, text): - """Send a Slack message as a reply to a specified request. - - If the request is associated with a direct message, the reply is - made by using the "response_url" from the request. Otherwise, the - reply will be sent to the channel associated with the request. - - Note: If flask is in debug mode, this function will just print the - text to stdout.""" - - channel_name = request.form.get('channel_name') - response_url = request.form.get('response_url') - channel = request.form.get('channel_id') - - if (app.debug): - print("Sending message to channel '{}': {}".format(channel, text)) - return - - if (channel_name == "directmessage"): - resp = requests.post(response_url, - json = {"text": text}, - headers = {"Content-type": "application/json"}) - if (resp.status_code != 200): - app.logger.error("Error posting request to Slack: " + resp.text) - else: - try: - slack_client.chat_postMessage(channel=channel, text=text) - except SlackApiError as e: - app.logger.error("Slack API error: " + e.response["error"]) - -def rot_string(str, n=13): - """Return a rotated version of a string - - Specifically, this functions returns a version of the input string - where each uppercase letter has been advanced 'n' positions in the - alphabet (wrapping around). Lowercase letters and any non-alphabetic - characters will be unchanged.""" - - result = '' - for letter in str: - if letter.isupper(): - result += chr(ord("A") + (ord(letter) - ord("A") + n) % 26) - else: - result += letter - return result - -@app.route('/rot', methods = ['POST']) -def rot(): - """Implements the /rot route for the /rot slash command in Slack - - This implements the /rot command of our Slack bot. The format of this - command is as follows: - - /rot [count|*] String to be rotated - - The optional count indicates an amount to rotate each character in the - string. If the count is '*' or is not present, then the string will - be rotated through all possible 25 values. - - The result of the rotation is provided as a message in Slack. If the - slash command was issued in a direct message, the response is made by - using the "response_url" from the request. This allows the bot to reply - in a direct message that it is not a member of. Otherwise, if the slash - command was issued in a channel, the bot will reply in that channel.""" - - if not slack_is_valid_request(request): - return make_response("invalid request", 403) - - query = request.form.get('text') - match = re.match(r'^([0-9]+|\*) (.*)$', query) - if (match): - try: - count = int(match.group(1)) - except ValueError: - count = None - text = match.group(2) - else: - count = None - text = query - - text = text.upper() - - reply = "```/rot {} {}\n".format(count if count else '*', text) - - if count: - reply += rot_string(text, count) - else: - reply += "\n".join(["{:02d} ".format(count) + rot_string(text, count) - for count in range(1, 26)]) - - reply += "```" - - slack_send_reply(request, reply) - - return "" @slack_events.on("error") def handle_error(error):