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 all 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
18 changes: 18 additions & 0 deletions bot/processors/pennychat.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,12 @@
from bot.tasks import (
post_organizer_edit_after_share_blocks,
share_penny_chat_invitation,
add_google_meet,
add_google_integration_blocks,
update_google_meet,
)
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 +35,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 +307,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 +414,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
247 changes: 202 additions & 45 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,60 @@ 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)


if calendar is None:
return

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 +159,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 @@ -195,23 +280,7 @@ 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')
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)}'

date_time_block['accessory'] = {
'type': 'button',
'text': {
'type': 'plain_text',
'text': 'Add to Google Calendar :calendar:',
'emoji': True
},
'url': google_cal_url
}
date_time_block['accessory'] = _google_calendar_link_block(penny_chat_invitation)

body = [
{
Expand Down Expand Up @@ -240,6 +309,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 @@ -332,6 +438,26 @@ def _penny_chat_details_blocks(penny_chat_invitation, mode=None):
return body


def _google_calendar_link_block(penny_chat_invitation):
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(description)}'
return {
'type': 'button',
'text': {
'type': 'plain_text',
'text': 'Add to Google Calendar :calendar:',
'emoji': True
},
'url': google_cal_url
}


def _followup_reminder_blocks(penny_chat_invitation):
organizer = get_or_create_social_profile_from_slack_id(
penny_chat_invitation.organizer_slack_id,
Expand Down Expand Up @@ -368,25 +494,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 +505,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 +517,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 +533,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
Loading