From bfaeb32d330a2ada7adc405b069369061f5818aa Mon Sep 17 00:00:00 2001 From: Tarik Eshaq Date: Sun, 11 Oct 2020 20:03:54 -0700 Subject: [PATCH] Adds donut feature to rocket --- app/scheduler/__init__.py | 7 +++- app/scheduler/modules/donut.py | 63 ++++++++++++++++++++++++++++++++++ app/server.py | 7 ++-- config/__init__.py | 4 +-- docker-compose.yml | 1 + factory/__init__.py | 8 +++++ interface/slack.py | 37 ++++++++++++++++++++ 7 files changed, 119 insertions(+), 8 deletions(-) create mode 100644 app/scheduler/modules/donut.py diff --git a/app/scheduler/__init__.py b/app/scheduler/__init__.py index 8a003fac..783c814e 100644 --- a/app/scheduler/__init__.py +++ b/app/scheduler/__init__.py @@ -3,7 +3,9 @@ from flask import Flask from apscheduler.schedulers.background import BackgroundScheduler from .modules.random_channel import RandomChannelPromoter +from .modules.donut import Donut from .modules.base import ModuleBase +from db.facade import DBFacade from typing import Tuple, List from config import Config @@ -13,10 +15,12 @@ class Scheduler: def __init__(self, scheduler: BackgroundScheduler, - args: Tuple[Flask, Config]): + args: Tuple[Flask, Config], + facade: DBFacade): """Initialize scheduler class.""" self.scheduler = scheduler self.args = args + self.facade = facade self.modules: List[ModuleBase] = [] self.__init_periodic_tasks() @@ -35,3 +39,4 @@ def __add_job(self, module: ModuleBase): def __init_periodic_tasks(self): """Add jobs that fire every interval.""" self.__add_job(RandomChannelPromoter(*self.args)) + self.__add_job(Donut(*self.args, self.facade)) diff --git a/app/scheduler/modules/donut.py b/app/scheduler/modules/donut.py new file mode 100644 index 00000000..83caedde --- /dev/null +++ b/app/scheduler/modules/donut.py @@ -0,0 +1,63 @@ +"""Match two Launchpad member for a private conversation""" +from slack import WebClient +from interface.slack import Bot +from random import shuffle +from .base import ModuleBase +from typing import Dict, List, Tuple, Any +from flask import Flask +from config import Config +from db.facade import DBFacade +import logging + + +class Donut(ModuleBase): + """Module that matches 2 launchpad members each week""" + + NAME = 'Match launch pad members randomly' + + def __init__(self, + flask_app: Flask, + config: Config, + facade: DBFacade): + """Initialize the object.""" + self.bot = Bot(WebClient(config.slack_api_token), + config.slack_notification_channel) + self.channel_id = self.bot.get_channel_id(config.slack_donut_channel) + self.facade = facade + + + def get_job_args(self) -> Dict[str, Any]: + """Get job configuration arguments for apscheduler.""" + return {'trigger': 'cron', + 'minute': '*', + 'name': self.NAME} + + def do_it(self): + """Pair users together, and create a private chat for them""" + users = self.bot.get_channel_users(self.channel_id) + logging.debug(f"users of the donut channel are {users}") + matched_user_pairs = self.__pair_users(users) + for pair in matched_user_pairs: + group_name = self.bot.create_private_chat(pair) + logging.info(f"The name of the created group name is {group_name}") + self.bot.send_to_channel("Hello! You have been matched by Rocket. " + + "please use this channel to get to know each other!", group_name) + + def __pair_users(self, users: List[str]) -> List[List[str]]: + """ + Creates pairs of users that haven't been matched before + """ + shuffle(users) + pairs = [] + pair = [] + for i, user in enumerate(users): + pair.append(user) + if i % 2 != 0: + pairs.append(pair) + pair = [] + # If we have an odd number of people that is not 1 + # We put the odd person out in one of the groups + # So we might have a group of 3 + if len(pair) == 1 and len(pairs) > 0: + pairs[len(pairs) - 1].append(pair[0]) + return pairs diff --git a/app/server.py b/app/server.py index 0052239d..613537b0 100644 --- a/app/server.py +++ b/app/server.py @@ -1,15 +1,13 @@ """Flask server instance.""" from factory import make_command_parser, make_github_webhook_handler, \ - make_slack_events_handler, make_github_interface + make_slack_events_handler, make_github_interface, make_event_scheduler from flask import Flask, request from logging.config import dictConfig from slackeventsapi import SlackEventAdapter -from apscheduler.schedulers.background import BackgroundScheduler import logging import structlog from flask_talisman import Talisman from config import Config -from app.scheduler import Scheduler from interface.slack import Bot from slack import WebClient from boto3.session import Session @@ -81,8 +79,7 @@ slack_events_adapter = SlackEventAdapter(config.slack_signing_secret, "/slack/events", app) -sched = Scheduler(BackgroundScheduler(timezone="America/Los_Angeles"), - (app, config)) +sched = make_event_scheduler(app, config) sched.start() bot = Bot(WebClient(config.slack_api_token), diff --git a/config/__init__.py b/config/__init__.py index 866abb91..c2c7396e 100644 --- a/config/__init__.py +++ b/config/__init__.py @@ -17,7 +17,7 @@ class Config: 'SLACK_API_TOKEN': 'slack_api_token', 'SLACK_NOTIFICATION_CHANNEL': 'slack_notification_channel', 'SLACK_ANNOUNCEMENT_CHANNEL': 'slack_announcement_channel', - + 'SLACK_DONUT_CHANNEL': 'slack_donut_channel', 'GITHUB_APP_ID': 'github_app_id', 'GITHUB_ORG_NAME': 'github_org_name', 'GITHUB_DEFAULT_TEAM_NAME': 'github_team_all', @@ -89,7 +89,7 @@ def _set_attrs(self): self.slack_api_token = '' self.slack_notification_channel = '' self.slack_announcement_channel = '' - + self.slack_donut_channel = '' self.github_app_id = '' self.github_org_name = '' self.github_team_all = '' diff --git a/docker-compose.yml b/docker-compose.yml index b4176389..7ff83f84 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -22,6 +22,7 @@ services: environment: - SLACK_NOTIFICATION_CHANNEL=${SLACK_NOTIFICATION_CHANNEL} - SLACK_ANNOUNCEMENT_CHANNEL=${SLACK_ANNOUNCEMENT_CHANNEL} + - SLACK_DONUT_CHANNEL=${SLACK_DONUT_CHANNEL} - SLACK_SIGNING_SECRET=${SLACK_SIGNING_SECRET} - SLACK_API_TOKEN=${SLACK_API_TOKEN} - GITHUB_APP_ID=${GITHUB_APP_ID} diff --git a/factory/__init__.py b/factory/__init__.py index caf61563..a305ce29 100644 --- a/factory/__init__.py +++ b/factory/__init__.py @@ -20,6 +20,9 @@ from google.oauth2 import service_account as gcp_service_account from googleapiclient.discovery import build as gcp_build from typing import Optional +from flask import Flask +from app.scheduler import Scheduler +from apscheduler.schedulers.background import BackgroundScheduler def make_dbfacade(config: Config) -> DBFacade: @@ -85,6 +88,11 @@ def make_gcp_client(config: Config) -> Optional[GCPInterface]: drive = gcp_build('drive', 'v3', credentials=credentials) return GCPInterface(drive, subject=config.gcp_service_account_subject) +def make_event_scheduler(app: Flask, config: Config) -> Scheduler: + background_scheduler = BackgroundScheduler(timezone="America/Los_Angeles") + facade = make_dbfacade(config) + return Scheduler(background_scheduler, (app, config), facade) + def create_signing_token() -> str: """Create a new, random signing token.""" diff --git a/interface/slack.py b/interface/slack.py index 63aad99a..da7e02b9 100644 --- a/interface/slack.py +++ b/interface/slack.py @@ -110,6 +110,43 @@ def send_event_notif(self, message): logging.error("Webhook notif failed to send due to {} error.". format(se.error)) + def create_private_chat(self, users: List[str]) -> str: + """ + Create a private chat with the given users + + :param users: the list of users to add to the private chat + + :raise SlackAPIError if the slack API returned error openning the chat + + :return The name of of the private chat created + """ + logging.debug(f"Attempting to open a private conversation with users {users}") + response = self.sc.conversations_open(users = users) + if response['ok']: + logging.debug(f"Successfly opened a converation with the name {response['channel']['name']}") + return response['channel']['name'] + raise SlackAPIError(response['error']) + + def get_channel_id(self, channel_name: str) -> str: + """ + Retrieves a channel's id given it's name + + :param channel_name: The name of the channel + + :raise SlackAPIError if no channels were found with the name `channel_name` + + :return the slack id of the channel + """ + # We strip away the "#" in case it was provided with the channel name + channel_name = channel_name.replace("#", "") + logging.debug(f"Attempting to get the id of channel {channel_name}") + channels = list(filter(lambda c: c['name'] == channel_name, self.get_channels())) + if len(channels) == 0: + raise SlackAPIError(f"No channels found with the name{channel_name}") + if len(channels) != 1: + logging.warning(f"Somehow there is more than one channel with the name {channel_name}") + return channels[0]['id'] + class SlackAPIError(Exception): """Exception representing an error while calling Slack API."""