diff --git a/CodeChallenge/__init__.py b/CodeChallenge/__init__.py index 2e0b6f4..39a3add 100644 --- a/CodeChallenge/__init__.py +++ b/CodeChallenge/__init__.py @@ -11,6 +11,7 @@ from . import core from .api.eb import bp as eb_bp from .api.questions import bp as questions_bp +from .api.slack import bp as slack_bp from .api.users import bp as users_bp from .api.vote import bp as vote_bp from .auth import jwt @@ -62,6 +63,7 @@ def create_app(config): app.register_blueprint(q_cli_bp) app.register_blueprint(clock_cli_bp) app.register_blueprint(vote_bp) + app.register_blueprint(slack_bp) @app.errorhandler(429) def ratelimit_handler(e): diff --git a/CodeChallenge/api/eb.py b/CodeChallenge/api/eb.py index 9ea0c3f..fb0196f 100644 --- a/CodeChallenge/api/eb.py +++ b/CodeChallenge/api/eb.py @@ -1,5 +1,6 @@ from hmac import compare_digest +import requests from flask import Blueprint, request, current_app, render_template from flask_mail import Message @@ -41,4 +42,10 @@ def worker(): mail.send(msg) + webhook = current_app.config.get("SLACK_WEBHOOK") + if webhook is not None: + requests.post(webhook, json=dict( + text=f"*NEW RANK* {core.current_rank()}" + )) + return "", 200 diff --git a/CodeChallenge/api/slack.py b/CodeChallenge/api/slack.py new file mode 100644 index 0000000..43689b5 --- /dev/null +++ b/CodeChallenge/api/slack.py @@ -0,0 +1,75 @@ +import hmac +import re +import time + +import requests +from flask import Blueprint, request, jsonify, current_app, abort + +from .. import core + +bp = Blueprint("slackapi", __name__, url_prefix="/api/v1/slack") + +CMDRE = re.compile(r"^!([^ ]+)", re.I) + + +@bp.before_request +def slack_verify(): + ts = request.headers.get("X-Slack-Request-Timestamp") + secret = bytes(current_app.config.get("SLACK_SIGNING_SECRET"), "utf8") + body = request.data + + if abs(time.time() - int(ts)) > 60 * 5: + abort(401) + + sig_basestring = bytes(f"v0:{ts}:", "utf8") + sig_basestring += body + + my_sig = "v0=" + hmac.new(secret, sig_basestring, digestmod="SHA256").hexdigest() + slack_sig = request.headers.get("X-Slack-Signature") + if not hmac.compare_digest(my_sig, slack_sig): + abort(401) + + +def post_message(channel, text): + rv = requests.post("https://slack.com/api/chat.postMessage", + headers=dict( + Authorization="Bearer " + current_app.config["SLACK_OAUTH_TOKEN"] + ), + json=dict( + channel=channel, + text=text + )) + + rv.raise_for_status() + + +def handle_message(text, channel): + match = CMDRE.search(text) + + if match is None: + return + + command = match.group(1) + + if command == "status": + rank = core.current_rank() + resp = f"*Current Rank:* {rank if rank != -1 else '(challenge not started)'}\n" + resp += f"*Max Rank:* {core.max_rank()}\n" + resp += f"*Next Rank:* {core.time_until_next_rank()}\n" + resp += f"*Total Users:* {core.user_count()}" + + post_message(channel, resp) + + +@bp.route("/event", methods=["POST"]) +def slack_event(): + data = request.get_json() + + if "challenge" in data: + return jsonify(challenge=data["challenge"]) + + event = data["event"] + if event["type"] == "message": + handle_message(event["text"], event["channel"]) + + return "", 200 diff --git a/CodeChallenge/config.py b/CodeChallenge/config.py index 651a0f5..f52d8b6 100644 --- a/CodeChallenge/config.py +++ b/CodeChallenge/config.py @@ -31,6 +31,9 @@ class DefaultConfig: MG_LIST = "codechallenge@school.codewizardshq.com" WORKER_PASSWORD = os.getenv("WORKER_PASSWORD") SLACK_WEBHOOK = os.getenv("SLACK_WEBHOOK") + SLACK_SIGNING_SECRET = os.getenv("SLACK_SIGNING_SECRET") + SLACK_OAUTH_TOKEN = os.getenv("SLACK_OAUTH_TOKEN") + SLACK_CHANNEL = os.getenv("SLACK_CHANNEL") # no trailing / EXTERNAL_URL = "https://challenge.codewizardshq.com" diff --git a/CodeChallenge/core.py b/CodeChallenge/core.py index 81c746c..a1b4f6a 100644 --- a/CodeChallenge/core.py +++ b/CodeChallenge/core.py @@ -3,6 +3,7 @@ from flask import current_app from sqlalchemy import func +from .auth import Users from .models import Question, db @@ -68,3 +69,7 @@ def challenge_ended() -> bool: return True return False + + +def user_count() -> int: + return db.session.query(func.count(Users)).scalar()