From: Carl Worth Date: Tue, 29 Sep 2020 17:07:15 +0000 (-0700) Subject: Make a top-level turbot package X-Git-Url: https://git.cworth.org/git?p=turbot;a=commitdiff_plain;h=34d9614d79faf1740b7f9c250e723de441a192ed Make a top-level turbot package This will be useful as we soon start splitting up the single turbot.py file into multiple modules that want to include things from each other. --- diff --git a/Makefile b/Makefile index d38a9a9..d0719d8 100644 --- a/Makefile +++ b/Makefile @@ -52,7 +52,7 @@ endif pip-compile --no-index --generate-hashes --allow-unsafe run: require-venv - FLASK_APP=turbot FLASK_DEBUG=true flask run + FLASK_APP=turbot.turbot FLASK_DEBUG=true flask run turbot.wsgi: turbot.wsgi.in Makefile envsubst < turbot.wsgi.in > turbot.wsgi diff --git a/turbot.py b/turbot.py deleted file mode 100755 index 2b9462f..0000000 --- a/turbot.py +++ /dev/null @@ -1,136 +0,0 @@ -#!/usr/bin/env python3 - -from flask import Flask, request, make_response - -from slackeventsapi import SlackEventAdapter -from slack import WebClient -from slack.errors import SlackApiError -from slack.signature import SignatureVerifier -import os -import requests -import re - -app = Flask(__name__) - -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): - app.logger.error("Error from Slack: " + str(error)) diff --git a/turbot/__init__.py b/turbot/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/turbot/turbot.py b/turbot/turbot.py new file mode 100755 index 0000000..2b9462f --- /dev/null +++ b/turbot/turbot.py @@ -0,0 +1,136 @@ +#!/usr/bin/env python3 + +from flask import Flask, request, make_response + +from slackeventsapi import SlackEventAdapter +from slack import WebClient +from slack.errors import SlackApiError +from slack.signature import SignatureVerifier +import os +import requests +import re + +app = Flask(__name__) + +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): + app.logger.error("Error from Slack: " + str(error))