Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Slack pairings #553

Open
wants to merge 14 commits into
base: master
Choose a base branch
from
7 changes: 6 additions & 1 deletion app/scheduler/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@
from flask import Flask
from apscheduler.schedulers.background import BackgroundScheduler
from .modules.random_channel import RandomChannelPromoter
from .modules.pairing import Pairing
from .modules.base import ModuleBase
from db.facade import DBFacade
from typing import Tuple, List
from config import Config

Expand All @@ -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()
Expand All @@ -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(Pairing(*self.args, self.facade))
63 changes: 63 additions & 0 deletions app/scheduler/modules/pairing.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
"""Match two Launchpad member for a private conversation"""
tarikeshaq marked this conversation as resolved.
Show resolved Hide resolved
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 Pairing(ModuleBase):
"""Module that matches 2 launchpad members each week"""

NAME = 'Match launch pad members randomly'
tarikeshaq marked this conversation as resolved.
Show resolved Hide resolved

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_pairing_channel)
self.facade = facade


def get_job_args(self) -> Dict[str, Any]:
"""Get job configuration arguments for apscheduler."""
return {'trigger': 'cron',
'minute': '*',
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm guessing this is just temporary for testing, but could also just make this a configuration option! SLACK_PAIRING_FREQUENCY for example

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Alrighty made it an env variable as a cron job string... not the cleanest so happy for recommendations!

'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)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Probably want to stop execution when we find out that there are no users in the channel (it also removes a check in __pair_users() that checks to see if there are any users in the list).

logging.debug(f"users of the pairing 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
Comment on lines +85 to +87
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
# 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 we have an odd number of people that is not 1,
# or if there is 1 person left unpaired:
# 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
7 changes: 2 additions & 5 deletions app/server.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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),
Expand Down
4 changes: 2 additions & 2 deletions config/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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_PAIRING_CHANNEL': 'slack_pairing_channel',
tarikeshaq marked this conversation as resolved.
Show resolved Hide resolved
'GITHUB_APP_ID': 'github_app_id',
'GITHUB_ORG_NAME': 'github_org_name',
'GITHUB_DEFAULT_TEAM_NAME': 'github_team_all',
Expand Down Expand Up @@ -89,7 +89,7 @@ def _set_attrs(self):
self.slack_api_token = ''
self.slack_notification_channel = ''
self.slack_announcement_channel = ''

self.slack_pairing_channel = ''
self.github_app_id = ''
self.github_org_name = ''
self.github_team_all = ''
Expand Down
1 change: 1 addition & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ services:
environment:
- SLACK_NOTIFICATION_CHANNEL=${SLACK_NOTIFICATION_CHANNEL}
- SLACK_ANNOUNCEMENT_CHANNEL=${SLACK_ANNOUNCEMENT_CHANNEL}
- SLACK_PAIRING_CHANNEL=${SLACK_PAIRING_CHANNEL}
- SLACK_SIGNING_SECRET=${SLACK_SIGNING_SECRET}
- SLACK_API_TOKEN=${SLACK_API_TOKEN}
- GITHUB_APP_ID=${GITHUB_APP_ID}
Expand Down
8 changes: 8 additions & 0 deletions factory/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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."""
Expand Down
37 changes: 37 additions & 0 deletions interface/slack.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
:raise SlackAPIError if the slack API returned error openning the chat
:raise SlackAPIError if the slack API returned error opening 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("#", "")
tarikeshaq marked this conversation as resolved.
Show resolved Hide resolved
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."""
Expand Down