Ejemplo n.º 1
0
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,
            )
Ejemplo n.º 2
0
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)
Ejemplo n.º 3
0
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,
            )
Ejemplo n.º 4
0
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
Ejemplo n.º 5
0
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
                }
            ]
        }
    ]
Ejemplo n.º 6
0
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),
        )
Ejemplo n.º 8
0
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)
Ejemplo n.º 9
0
 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)
Ejemplo n.º 10
0
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)
Ejemplo n.º 11
0
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)
Ejemplo n.º 12
0
    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)
Ejemplo n.º 13
0
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
Ejemplo n.º 14
0
    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)
Ejemplo n.º 15
0
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()
Ejemplo n.º 16
0
 def handle(self, *args, **options):
     slack_client = get_slack_client()
     slack_client.chat_postMessage(channel=options['channel'], text=options['message'])
     exit(options['exit_code'])
Ejemplo n.º 17
0
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):
Ejemplo n.º 18
0
    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
Ejemplo n.º 19
0
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