def send_followup_reminder_and_mark_chat_as_completed(): """This sends out reminders for any chat that is about to happen and also marks a chat as COMPLETED. Handled in background_tasks.py """ slack_client = get_slack_client() now = datetime.now().astimezone(timezone(settings.TIME_ZONE)) completed_chats = PennyChatSlackInvitation.objects.filter( status__gte=PennyChatSlackInvitation.SHARED, status__lt=PennyChatSlackInvitation.COMPLETED, date__lt=now - timedelta(minutes=settings.FOLLOWUP_REMINDER_AFTER_PENNY_CHAT_MINUTES), ) for penny_chat_invitation in completed_chats: penny_chat_invitation.status = PennyChatSlackInvitation.COMPLETED penny_chat_invitation.save() reminder_blocks = _followup_reminder_blocks(penny_chat_invitation) participants = penny_chat_invitation.get_participants() for user in participants: profile = SocialProfile.objects.get( user=user, slack_team_id=penny_chat_invitation.created_from_slack_team_id, ) slack_client.chat_postMessage( channel=profile.slack_id, blocks=reminder_blocks, )
def make_matches(slack_team_id, emails, topic_channel_name): """Match profiles (corresponding to emails) to meet for Penny Chats for in a given topic channel.""" logging.info( f'make_matches for {slack_team_id}: {emails} in {topic_channel_name}') profiles = SocialProfile.objects.filter(slack_team_id=slack_team_id, email__in=emails) if len(profiles) < len(emails): arg_set = set(emails) result_set = set([profile.email for profile in profiles]) raise RuntimeError( f'Could not find profiles for all emails. {arg_set.difference(result_set)} not found.' ) # fetch topic channel to make sure it exists topic_channel = TopicChannel.objects.get(name=topic_channel_name) slack_client = get_slack_client(slack_team_id) conversation = slack_client.conversations_open( users=[profile.slack_id for profile in profiles]) conversation_id = conversation['channel']['id'] match, created = Match.objects.get_or_create( topic_channel=topic_channel, conversation_id=conversation_id, ) match.profiles.add(*profiles) blocks = create_match_blocks(topic_channel.channel_id, conversation_id) slack_client.chat_postMessage(channel=conversation_id, blocks=blocks)
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. Handled in background_tasks.py """ slack_client = get_slack_client() now = datetime.now().astimezone(timezone(settings.TIME_ZONE)) imminent_chats = PennyChatSlackInvitation.objects.filter( status__gte=PennyChatSlackInvitation.SHARED, status__lt=PennyChatSlackInvitation.REMINDED, date__gte=now, date__lt=now + timedelta(minutes=settings.CHAT_REMINDER_BEFORE_PENNY_CHAT_MINUTES), ) for penny_chat_invitation in imminent_chats: penny_chat_invitation.status = PennyChatSlackInvitation.REMINDED penny_chat_invitation.save() reminder_blocks = _penny_chat_details_blocks(penny_chat_invitation, mode=REMIND) participants = penny_chat_invitation.get_participants() for user in participants: profile = SocialProfile.objects.get( user=user, slack_team_id=penny_chat_invitation.created_from_slack_team_id, ) slack_client.chat_postMessage( channel=profile.slack_id, blocks=reminder_blocks, )
def get_or_create_social_profile_from_slack_ids(slack_user_ids, slack_client=None, ignore_user_not_found=True): """Gets or creates SocialProfile from the slack users associated with user ids. :return dict of SocialProfiles keyed by slack_user_id, if a user can not be found in slack, they will not have an entry in the dict """ if not slack_client: slack_client = get_slack_client() profiles = {} for slack_user_id in set(slack_user_ids): try: # call slack every time rather than just seeing if the user is in the database just in case slack contains # updated information # TODO get the profile out of the db and only check slack if the update_at is older than some cutoff slack_user = slack_client.users_info(user=slack_user_id).data['user'] except SlackApiError as e: capture_exception(e) if ignore_user_not_found and "'error': 'user_not_found'" in str(e): continue raise profile, create = update_social_profile_from_slack_user(slack_user) profiles[slack_user_id] = profile return profiles
def _followup_reminder_blocks(penny_chat_invitation): organizer = get_or_create_social_profile_from_slack_id( penny_chat_invitation.organizer_slack_id, slack_client=get_slack_client(), ) return [ { 'type': 'section', 'text': { 'type': 'mrkdwn', 'text': f'{organizer.real_name}\'s Penny Chat *"{penny_chat_invitation.title}"* has completed. ' 'How did it go?\n\nIf you learned something new, then consider writing a Penny Chat ' 'Follow-Up and sharing what you learned with the community. This is also a great way ' 'to say _*thank you*_ to all those that attended the Penny Chat.' } }, { 'type': 'actions', 'elements': [ { 'type': 'button', 'text': { 'type': 'plain_text', 'text': ':pencil2: Write a Follow-Up', 'emoji': True }, 'style': 'primary', 'url': f'https://www.pennyuniversity.org/chats/{penny_chat_invitation.id}', 'action_id': GO_TO_FOLLOWUP } ] } ]
def post_organizer_edit_after_share_blocks(penny_chat_view_id): slack_client = get_slack_client() penny_chat_invitation = PennyChatSlackInvitation.objects.get(view=penny_chat_view_id) slack_client.chat_postMessage( channel=penny_chat_invitation.organizer_slack_id, blocks=organizer_edit_after_share_blocks(slack_client, penny_chat_invitation), )
def notify_about_activity(user_data, live_run=False): """Notifies users in slack about recent activity in Penny Chat's they've participated in. :param user_data: expected form is { 'user_id': ..., 'first_name': ..., 'slack_team_id': ..., 'slack_id': ..., 'penny_chats': [{ 'id': ..., 'title': ..., 'date': ..., 'followups': [{ 'user_id': ..., 'first_name': ..., }, ...], }, ...], } :return: Nothing. Notifies users as side effect. """ chat_data = [ ] # each item will be a tuple of (explanatory text, penny_chat.id) for penny_chat in user_data['penny_chats']: people = { followup['first_name']: followup['user_id'] for followup in penny_chat['followups'] if followup['user_id'] != user_data['user_id'] } if not people: continue people_string = get_people_string(people) date_string = ( f'<!date^{int(penny_chat["date"].timestamp())}^{{date_short}}|{penny_chat["date"].strftime("%b %d, %Y")}>' ) if people_string: chat_data.append([ f'Your {date_string} chat _"{penny_chat["title"]}"_ has follow-ups from {people_string}.', penny_chat["id"], ]) else: chat_data.append([ f'Your {date_string} chat _"{penny_chat["title"]}"_ has follow-ups.', penny_chat["id"] ]) print( f'\n\nTo be sent to {user_data["first_name"]} ' f'({user_data["slack_team_id"]} @{user_data["slack_id"]}):\n{chat_data}' ) if chat_data and live_run: slack_client = get_slack_client(team_id=user_data['slack_team_id']) slack_client.chat_postMessage( channel=user_data['slack_id'], blocks=generate_blocks(user_data["first_name"], chat_data), )
def channel_lookup(name): global _CHANNEL_NAME__ID if _CHANNEL_NAME__ID is None: slack_client = get_slack_client() resp = slack_client.conversations_list() _CHANNEL_NAME__ID = { chan['name']: chan['id'] for chan in resp.data['channels'] } return _CHANNEL_NAME__ID.get(name)
def handle(self, *args, **options): slack_client = get_slack_client() matches_without_penny_chats = Match.objects.filter( penny_chat__isnull=True) for match in matches_without_penny_chats: blocks = create_match_blocks(match.topic_channel.channel_id, match.conversation_id, reminder=True) slack_client.chat_postMessage(channel=match.conversation_id, blocks=blocks)
def request_matches(slack_team_id, channel_names=None): """Contact all topic channels (or only those specified) and allow users to sign up to be matched for chats.""" logging.info(f'request_matches for {slack_team_id}') topic_channels = TopicChannel.objects.filter(slack_team_id=slack_team_id) if channel_names is not None: topic_channels = TopicChannel.objects.filter(name__in=channel_names) if len(topic_channels) == 0: raise Exception('No topic channels found for provided arguments') slack_client = get_slack_client(slack_team_id) for channel in topic_channels: blocks = request_match_blocks(channel.channel_id) slack_client.chat_postMessage(channel=channel.channel_id, blocks=blocks)
def remind_matches(slack_team_id): """Find all people that were recently scheduled to meet but haven't yet, and encourage them to meet.""" logging.info(f'remind_matches for {slack_team_id}') slack_client = get_slack_client(slack_team_id) since = datetime.now().astimezone( timezone.utc) - timedelta(days=settings.REMIND_MATCHES_SINCE_DAYS) matches_without_penny_chats = Match.objects.filter(penny_chat__isnull=True, date__gte=since) for match in matches_without_penny_chats: blocks = create_match_blocks(match.topic_channel.channel_id, match.conversation_id, reminder=True) slack_client.chat_postMessage(channel=match.conversation_id, blocks=blocks)
def handle(self, *args, **options): num_user_args = len(options['users']) if num_user_args < 2 or num_user_args > 8: raise RuntimeError(f'Between 2 and 8 users are required. You provided {num_user_args}.') profiles = SocialProfile.objects.filter(email__in=options['users']) if len(profiles) < num_user_args: arg_set = set(options['users']) result_set = set([profile.email for profile in profiles]) raise RuntimeError(f'Could not find profiles for all emails. {arg_set.difference(result_set)} not found.') # fetch topic channel topic_channel = TopicChannel.objects.get(name=options['topic']) slack_client = get_slack_client() make_matches(slack_client, profiles, topic_channel)
def update_social_profile_from_slack(slack_client=None): if not slack_client: slack_client = get_slack_client() resp = slack_client.users_list() new_profiles = [] updated_profiles = [] for slack_user in resp.data['members']: if not slack_user.get('profile', {}).get('email'): continue user, created = update_social_profile_from_slack_user(slack_user) if created: new_profiles.append(user) else: updated_profiles.append(user) return new_profiles, updated_profiles
def handle(self, *args, **options): channels_string = options['topic_channels'] slack_team = options['slack_team'] if channels_string == '*': topic_channels = TopicChannel.objects.all() else: topic_channels = TopicChannel.objects.filter(name__in=channels_string.split()) if slack_team is not None: topic_channels = topic_channels.filter(slack_team_id=slack_team) if len(topic_channels) == 0: raise Exception('No topic channels found for provided arguments') slack_client = get_slack_client() for channel in topic_channels: blocks = request_match_blocks(channel.channel_id) slack_client.chat_postMessage(channel=channel.channel_id, blocks=blocks)
def share_penny_chat_invitation(penny_chat_id): """Shares penny chat invitations with people and channels in the invitee list.""" penny_chat_invitation = PennyChatSlackInvitation.objects.get( id=penny_chat_id) slack_client = get_slack_client() # unshare the old shares old_shares = json.loads(penny_chat_invitation.shares or '{}') for channel, ts in old_shares.items(): # TODO https://github.com/penny-university/penny_university/issues/140 # until this is resolved we will not be able to remove shared messages in private channels try: slack_client.chat_delete(channel=channel, ts=ts) except Exception as e: # noqa capture_exception(e) # can't do anything about it anyway... might as well continue pass invitation_blocks = _penny_chat_details_blocks(penny_chat_invitation, mode=INVITE) shares = {} channel_ids = comma_split(penny_chat_invitation.channels) invitee_ids = comma_split(penny_chat_invitation.invitees) participant_ids = [] for p in penny_chat_invitation.participants.all(): if p.role != Participant.ORGANIZER: profile = SocialProfile.objects.get( user=p.user, slack_team_id=penny_chat_invitation.created_from_slack_team_id, ) participant_ids.append(profile.slack_id) for share_to in set(channel_ids + invitee_ids + participant_ids): resp = slack_client.chat_postMessage( channel=share_to, blocks=invitation_blocks, ) shares[resp.data['channel']] = resp.data['ts'] penny_chat_invitation.shares = json.dumps(shares) penny_chat_invitation.save()
def handle(self, *args, **options): slack_client = get_slack_client() slack_client.chat_postMessage(channel=options['channel'], text=options['message']) exit(options['exit_code'])
from django.http import ( HttpResponse, JsonResponse, ) from django.views.decorators.csrf import csrf_exempt from django.views.decorators.clickjacking import xframe_options_exempt from bot.processors.greeting import GreetingBotModule from bot.processors.pennychat import PennyChatBotModule from bot.processors.matchmaking import MatchMakingBotModule from bot.processors.base import Bot from bot.utils import chat_postEphemeral_with_fallback from common.utils import get_slack_client slack_client = get_slack_client() bot = Bot(event_processors=[ GreetingBotModule(slack_client), PennyChatBotModule(slack_client), MatchMakingBotModule(slack_client), ]) def index(request): # We've come a long way haven't we? return HttpResponse("At least something works!!!!") @xframe_options_exempt @csrf_exempt def hook(request):
def handle(self, *args, **options): # process arguments slack_names = options['slack_handles'] if len(slack_names) == 1: slack_names = slack_names[0].replace(' ', '').split(',') slack_names = [slack_name.strip(', ') for slack_name in slack_names] # collect appropriate ids for onboarding slack_client = get_slack_client() resp = slack_client.users_list() if 'ok' not in resp.data or not resp.data['ok']: raise RuntimeError(f'something is wrong: {resp.data}') user_id__slack_name = { member['id']: member['name'] for member in resp.data['members'][1:] if not (member['is_bot'] or member['id'] == 'USLACKBOT') } slack_name__user_id = {v: k for k, v in user_id__slack_name.items()} not_found_users = set() user_ids = set() if slack_names: for slack_name in slack_names: print(slack_name) user_id = slack_name__user_id.get(slack_name) if user_id: user_ids.add(user_id) else: not_found_users.add(slack_name) else: user_ids = set(user_id__slack_name) # notify developer of send status if not_found_users: print(f'WARNING: These users not found: {", ".join(not_found_users)}\n') print( 'Preparing to send messages to: ' f'{", ".join([user_id__slack_name[uid] for uid in user_ids])}.' ) print() # double check that it's ok to send start_sending = '' while True: start_sending = input('Start sending? [NO,yes] >>> ').lower() if not start_sending: start_sending = 'no' if start_sending not in ['no', 'yes']: print('Type "yes" or "no".') else: break if start_sending != 'yes': print('Exiting') exit(0) # send print('Sending:') for user_id in user_ids: slack_client.chat_postMessage(channel=user_id, blocks=greeting_blocks(user_id)) print(user_id) time.sleep(1.0) # attempting to avoid rate limiting
def _penny_chat_details_blocks(penny_chat_invitation, mode=None): """Creates blocks for penny chat details""" assert mode in PENNY_CHAT_DETAILS_BLOCKS_MODES include_calendar_link = mode in {PREVIEW, INVITE, UPDATE} include_rsvp = mode in {INVITE, UPDATE} include_participant_pictures = mode in {REMIND} and \ len(penny_chat_invitation.get_participants()) > 0 organizer = get_or_create_social_profile_from_slack_id( penny_chat_invitation.organizer_slack_id, slack_client=get_slack_client(), ) header_text = '' if mode in {PREVIEW, INVITE}: header_text = f'_*{organizer.real_name}* invited you to a new Penny Chat!_' elif mode == UPDATE: header_text = f'_*{organizer.real_name}* has updated their Penny Chat._' elif mode == REMIND: header_text = f'_*{organizer.real_name}\'s* Penny Chat is coming up soon! We hope you can still make it._' date_text = f'<!date^{int(penny_chat_invitation.date.astimezone(utc).timestamp())}^{{date}} at {{time}}|{penny_chat_invitation.date}>' # noqa date_time_block = { 'type': 'section', 'text': { 'type': 'mrkdwn', 'text': f'*Date and Time*\n{date_text}' }, } 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 } body = [ { 'type': 'section', 'text': { 'type': 'mrkdwn', 'text': header_text, } }, { 'type': 'section', 'text': { 'type': 'mrkdwn', 'text': f'*Title*\n{penny_chat_invitation.title} ' f'(<{settings.FRONT_END_HOST}/chats/{penny_chat_invitation.id}|sharable link>)' # noqa } }, { 'type': 'section', 'text': { 'type': 'mrkdwn', 'text': f'*Description*\n{penny_chat_invitation.description}' } }, date_time_block ] if include_rsvp: body.append( { 'type': 'actions', 'elements': [ { 'type': 'button', 'text': { 'type': 'plain_text', 'text': 'Count me in :thumbsup:', 'emoji': True, }, 'action_id': PENNY_CHAT_CAN_ATTEND, 'value': json.dumps({PENNY_CHAT_ID: penny_chat_invitation.id}), # TODO should this be a helper function? # noqa 'style': 'primary', }, { 'type': 'button', 'text': { 'type': 'plain_text', 'text': 'I can\'t make it :thumbsdown:', 'emoji': True, }, 'action_id': PENNY_CHAT_CAN_NOT_ATTEND, 'value': json.dumps({PENNY_CHAT_ID: penny_chat_invitation.id}), 'style': 'primary', } ] } ) if include_participant_pictures: participants = penny_chat_invitation.get_participants() default = "https://avatars.slack-edge.com/2020-05-25/1144415980914_c8a4ff7c783de54f72e5_512.png" size = 40 profiles = [] # issue-349: set max profile images to avoid too many blocks profile_overage = 0 if len(participants) > MAX_PARTICIPANT_PROFILES: profile_overage = len(participants) - MAX_PARTICIPANT_PROFILES participants = participants[0:MAX_PARTICIPANT_PROFILES] for participant in participants: # get the (gravatar) profile image of a participant email = participant.email # https://en.gravatar.com/site/implement/images/python/ gravatar_url = "https://www.gravatar.com/avatar/" +\ hashlib.md5(email.lower().encode('utf-8')).hexdigest() + "?" +\ urllib.parse.urlencode({'d': default, 's': str(size)}) if participant.first_name != "" and participant.last_name != "": name = participant.first_name + " " + participant.last_name else: name = participant.username profiles.append({ 'name': name, 'image_url': gravatar_url }) profiles_text = f"{len(profiles)} attending" if profile_overage == 0 else f"& {profile_overage} more attending" attendee_images = [ { 'type': 'image', 'image_url': profile['image_url'], 'alt_text': profile['name'] } for profile in profiles ] elements = attendee_images + [ { 'type': 'plain_text', 'emoji': True, 'text': profiles_text } ] body.append( { 'type': 'context', 'elements': elements } ) return body