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

Gcal Integration #378

Open
wants to merge 11 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ db.sqlite3
venv
staticfiles/*
!staticfiles/.gitignore
.env
.envrc
.direnv

Expand Down
17 changes: 17 additions & 0 deletions bot/processors/pennychat.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,11 @@
from bot.tasks import (
post_organizer_edit_after_share_blocks,
share_penny_chat_invitation,
add_google_meet,
add_google_integration_blocks, update_google_meet,
Copy link
Collaborator

Choose a reason for hiding this comment

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

nit, new line

)
from bot.utils import chat_postEphemeral_with_fallback
from integrations.google import get_authorization_url
from pennychat.models import (
PennyChat,
PennyChatSlackInvitation,
Expand All @@ -31,6 +34,7 @@
PENNY_CHAT_USER_SELECT = 'penny_chat_user_select'
PENNY_CHAT_CHANNEL_SELECT = 'penny_chat_channel_select'
PENNY_CHAT_DETAILS = 'penny_chat_details'
PENNY_CHAT_REVIEW_DETAILS = 'penny_chat_review_details'
PENNY_CHAT_EDIT = 'penny_chat_edit'
PENNY_CHAT_SHARE = 'penny_chat_share'
PENNY_CHAT_CAN_ATTEND = 'penny_chat_can_attend'
Expand Down Expand Up @@ -302,6 +306,12 @@ def create_penny_chat(cls, slack, event):
penny_chat_invitation.view = response.data['view']['id']
penny_chat_invitation.save()

@classmethod
def integrate_google_calendar(cls, slack, event):
user = get_or_create_social_profile_from_slack_id(event['user_id'])
blocks = add_google_integration_blocks(authorization_url=get_authorization_url(user.email))
chat_postEphemeral_with_fallback(slack, channel=event['channel_id'], user=event['user_id'], blocks=blocks)

@is_block_interaction_event
@has_action_id(PENNY_CHAT_SCHEDULE_MATCH)
def schedule_match(self, event):
Expand Down Expand Up @@ -403,12 +413,19 @@ def submit_details_and_share(self, event):
}
}

penny_chat_invitation.save_organizer_from_slack_id(penny_chat_invitation.organizer_slack_id)

# Ready to share
penny_chat_invitation.status = PennyChatSlackInvitation.SHARED
penny_chat_invitation.save()

post_organizer_edit_after_share_blocks.now(view['id'])
penny_chat_invitation.save_organizer_from_slack_id(penny_chat_invitation.organizer_slack_id)

if not penny_chat_invitation.video_conference_link:
add_google_meet(penny_chat_invitation.id)
else:
update_google_meet(penny_chat_invitation.id)
share_penny_chat_invitation(penny_chat_invitation.id)

@is_block_interaction_event
Expand Down
214 changes: 183 additions & 31 deletions bot/tasks/pennychat.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,16 +9,18 @@
from sentry_sdk import capture_exception

from common.utils import get_slack_client
from integrations.google import build_credentials, get_authorization_url, GoogleCalendar
from integrations.models import GoogleCredentials
from pennychat.models import PennyChatSlackInvitation, Participant
from users.models import (
SocialProfile,
get_or_create_social_profile_from_slack_id,
get_or_create_social_profile_from_slack_ids,
get_or_create_social_profile_from_slack_id, get_or_create_social_profile_from_slack_ids,
)

VIEW_SUBMISSION = 'view_submission'
VIEW_CLOSED = 'view_closed'

ADD_GOOGLE_INTEGRATION = 'add_google_integration'
PENNY_CHAT_DATE = 'penny_chat_date'
PENNY_CHAT_TIME = 'penny_chat_time'
PENNY_CHAT_USER_SELECT = 'penny_chat_user_select'
Expand All @@ -32,7 +34,6 @@

PENNY_CHAT_ID = 'penny_chat_id'


PREVIEW, INVITE, UPDATE, REMIND = 'review', 'invite', 'update', 'remind'
PENNY_CHAT_DETAILS_BLOCKS_MODES = {PREVIEW, INVITE, UPDATE, REMIND}

Expand Down Expand Up @@ -65,6 +66,57 @@ def post_organizer_edit_after_share_blocks(penny_chat_view_id):
)


def get_user_google_calendar_from_slack_id(slack_id):
user = get_or_create_social_profile_from_slack_id(slack_id).user
try:
google_credentials = GoogleCredentials.objects.get(user=user)
except GoogleCredentials.DoesNotExist:
authorization_url = get_authorization_url(user)
slack_client = get_slack_client()
slack_client.chat_postMessage(
channel=slack_id,
blocks=add_google_integration_blocks(authorization_url, from_penny_chat=True),
)
return

credentials = build_credentials(google_credentials)
return GoogleCalendar(credentials=credentials)


@background
def add_google_meet(penny_chat_id):
penny_chat_invitation = PennyChatSlackInvitation.objects.get(id=penny_chat_id)

calendar = get_user_google_calendar_from_slack_id(penny_chat_invitation.organizer_slack_id)

if calendar is None:
return

meet = calendar.create_event(
summary=penny_chat_invitation.title,
description=penny_chat_invitation.description,
start=penny_chat_invitation.date
)

penny_chat_invitation.video_conference_link = meet.get('hangoutLink')
penny_chat_invitation.google_event_id = meet.get('id')
penny_chat_invitation.save()


@background
def update_google_meet(penny_chat_id):
penny_chat_invitation = PennyChatSlackInvitation.objects.get(id=penny_chat_id)

calendar = get_user_google_calendar_from_slack_id(penny_chat_invitation.organizer_slack_id)
Copy link
Collaborator

Choose a reason for hiding this comment

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

if the user retracts google permissions then calendar will be None here, add an if calendar is None here or we'll error on line 112. In this case I guess we just keep the calendar invite as is? (I presume it will still work)


calendar.update_event(
event_id=penny_chat_invitation.google_event_id,
summary=penny_chat_invitation.title,
description=penny_chat_invitation.description,
start=penny_chat_invitation.date
)


@background
def share_penny_chat_invitation(penny_chat_id):
"""Shares penny chat invitations with people and channels in the invitee list."""
Expand Down Expand Up @@ -104,6 +156,36 @@ def share_penny_chat_invitation(penny_chat_id):
penny_chat_invitation.save()


def comma_split(comma_delimited_string):
"""normal string split for ''.split(',') returns [''], so using this instead"""
return [x for x in comma_delimited_string.split(',') if x]


def build_share_string(slack_client, penny_chat_invitation):
shares = []
users = get_or_create_social_profile_from_slack_ids(
comma_split(penny_chat_invitation.invitees),
slack_client=slack_client,
)
for slack_user_id in comma_split(penny_chat_invitation.invitees):
shares.append(users[slack_user_id].real_name)

if len(penny_chat_invitation.channels) > 0:
for channel in comma_split(penny_chat_invitation.channels):
shares.append(f'<#{channel}>')

share_string = ''
if len(shares) == 1:
share_string = shares[0]
elif len(shares) == 2:
share_string = ' and '.join(shares)
elif len(shares) > 2:
shares[-1] = f'and {shares[-1]}'
share_string = ', '.join(shares)

return share_string


def send_penny_chat_reminders_and_mark_chat_as_reminded():
"""This sends out reminders for any chat that is about to happen and also marks a chat as REMINDED.

Expand Down Expand Up @@ -197,11 +279,13 @@ def _penny_chat_details_blocks(penny_chat_invitation, mode=None):
if include_calendar_link:
start_date = penny_chat_invitation.date.astimezone(utc).strftime('%Y%m%dT%H%M%SZ')
end_date = (penny_chat_invitation.date.astimezone(utc) + timedelta(hours=1)).strftime('%Y%m%dT%H%M%SZ')

description = f'{penny_chat_invitation.description}\nVideo Link: {penny_chat_invitation.video_conference_link}'
google_cal_url = 'https://calendar.google.com/calendar/render?' \
'action=TEMPLATE&text=' \
f'{urllib.parse.quote(penny_chat_invitation.title)}&dates=' \
f'{start_date}/{end_date}&details=' \
f'{urllib.parse.quote(penny_chat_invitation.description)}'
f'{urllib.parse.quote(penny_chat_invitation.title)}&dates=' \
f'{start_date}/{end_date}&details=' \
f'{urllib.parse.quote(description)}'
Copy link
Collaborator

Choose a reason for hiding this comment

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

this function has grown in complexity, can you pull out the contents of this if statement into it's own helpful func that lives in this file?


date_time_block['accessory'] = {
'type': 'button',
Expand Down Expand Up @@ -240,6 +324,43 @@ def _penny_chat_details_blocks(penny_chat_invitation, mode=None):
date_time_block
]

if penny_chat_invitation.video_conference_link:
Copy link
Collaborator

Choose a reason for hiding this comment

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

probably a nit pick, but rather than have 3 sections here, I'd just have a {PREVIEW, INVITE, UPDATE} section with a parenthetical (A video link will be provided shortly before the chat starts) and a REMIND section with the full details.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

So just remove the Video Link header from the invite and add parentheses?

Copy link
Collaborator

Choose a reason for hiding this comment

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

yup, just like you did - looks good

if mode in {PREVIEW, INVITE, UPDATE}:
body.append(
{
'type': 'section',
'text': {
'type': 'mrkdwn',
'text': '_(A video link will be provided shortly before the chat starts)_'
}
}
)
elif mode in {REMIND}:
body += [
{
'type': 'section',
'text': {
'type': 'mrkdwn',
'text': '*Video Call Link*'
}
},
{
'type': 'actions',
'elements': [
{
'type': 'button',
'text': {
'type': 'plain_text',
'text': ':call_me_hand: Join Video Call',
'emoji': True,
},
'url': penny_chat_invitation.video_conference_link,
'style': 'primary',
}
]
}
]

if include_rsvp:
body.append(
{
Expand Down Expand Up @@ -368,25 +489,7 @@ def _followup_reminder_blocks(penny_chat_invitation):


def organizer_edit_after_share_blocks(slack_client, penny_chat_invitation):
shares = []
users = get_or_create_social_profile_from_slack_ids(
comma_split(penny_chat_invitation.invitees),
slack_client=slack_client,
)
for slack_user_id in comma_split(penny_chat_invitation.invitees):
shares.append(users[slack_user_id].real_name)

if len(penny_chat_invitation.channels) > 0:
for channel in comma_split(penny_chat_invitation.channels):
shares.append(f'<#{channel}>')

if len(shares) == 1:
share_string = shares[0]
elif len(shares) == 2:
share_string = ' and '.join(shares)
elif len(shares) > 2:
shares[-1] = f'and {shares[-1]}'
share_string = ', '.join(shares)
share_string = build_share_string(slack_client, penny_chat_invitation)

shared_message_preview_blocks = _penny_chat_details_blocks(penny_chat_invitation, mode=PREVIEW) + [
{
Expand All @@ -397,8 +500,9 @@ def organizer_edit_after_share_blocks(slack_client, penny_chat_invitation):
'text': {
'type': 'mrkdwn',
'text': f'*:point_up: You just shared this invitation with:* {share_string}. '
'We will notify you as invitees respond.\n\n'
'In the meantime if you need to update the event, click the button below.'
'We will notify you as invitees respond.\n\n'
'In the meantime if you need to update the event, click the button below.\n\n'
'*If you have enabled Google Calendar, a video link will be provided automatically.*'
}
},
{
Expand All @@ -408,7 +512,7 @@ def organizer_edit_after_share_blocks(slack_client, penny_chat_invitation):
'type': 'button',
'text': {
'type': 'plain_text',
'text': 'Edit Details :pencil2:',
'text': ':pencil2: Edit Details',
'emoji': True,
},
# TODO should this be a helper function?
Expand All @@ -424,6 +528,54 @@ def organizer_edit_after_share_blocks(slack_client, penny_chat_invitation):
return shared_message_preview_blocks


def comma_split(comma_delimited_string):
"""normal string split for ''.split(',') returns [''], so using this instead"""
return [x for x in comma_delimited_string.split(',') if x]
def missing_google_auth_blocks():
blocks = [
{
'type': 'section',
'text': {
'type': 'mrkdwn',
'text': 'Awesome, it looks like you just shared a Penny Chat!'
}
},
{
'type': 'section',
'text': {
'type': 'mrkdwn',
'text': 'If you want to make the Penny Chat experience even better, consider adding our Google Calendar integration so that we can automatically add video conference links to your Penny Chat.' # noqa
}
},
]

return blocks


def add_google_integration_blocks(authorization_url, from_penny_chat=False):
pre_add_button_blocks = missing_google_auth_blocks() if from_penny_chat else []
blocks = pre_add_button_blocks + [
{
'type': 'section',
'text': {
'type': 'mrkdwn',
'text': 'Click the button below to activate the Google Calendar integration.'
}
},
{
'type': 'actions',
'elements': [
{
'type': 'button',
'text': {
'type': 'plain_text',
'text': 'Add Google Integration',
'emoji': True
},
'value': 'add_integration',
'style': 'primary',
'url': authorization_url,
'action_id': ADD_GOOGLE_INTEGRATION
},
]
},
]

return blocks
3 changes: 3 additions & 0 deletions bot/utils.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
import logging

from django.conf import settings

from common.utils import get_slack_client
from sentry_sdk import capture_exception
from slack.errors import SlackApiError

from users.models import get_or_create_social_profile_from_slack_ids

_CHANNEL_NAME__ID = None


Expand Down
2 changes: 2 additions & 0 deletions bot/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,8 @@ def command(request):
command = event['text'].split(' ', 1)[0]
if command == 'chat':
PennyChatBotModule.create_penny_chat(slack_client, event)
elif command == 'gcal':
PennyChatBotModule.integrate_google_calendar(slack_client, event)
elif command == 'set-topic':
MatchMakingBotModule.set_topic_channel(slack_client, event)
else:
Expand Down
11 changes: 11 additions & 0 deletions docs/integrations.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
Go to https://developers.google.com/calendar/quickstart/python?authuser=1 and enable the calendar API in order to get credentials for your dev account

Use: http://localhost:8000/integrations/google/auth-callback for your redirect uri

Copy credentials to these respective settings:
* export GOOGLE_CLIENT_ID
* export GOOGLE_CLIENT_SECRET
* export GOOGLE_REDIRECT_URI

Scopes = 'https://www.googleapis.com/auth/calendar.events'
Access type = offline
1 change: 1 addition & 0 deletions integrations/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
default_app_config = 'integrations.apps.IntegrationsConfig'
3 changes: 3 additions & 0 deletions integrations/admin.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
from django.contrib import admin

# Register your models here.
Loading