Exemplo n.º 1
0
    def _find_and_respond_to_twitter_events(self) -> None:
        """Looks for TwitterEvent objects that have not yet been responded to and
        begins the process of creating a response. Additionally, failed events are
        rerun to provide a correct response, particularly useful in cases where
        external apis are down for maintenance.
        """

        interval = (
            self.PRODUCTION_APP_RATE_LIMITING_INTERVAL_IN_SECONDS if self._is_production()
            else self.DEVELOPMENT_CLIENT_RATE_LIMITING_INTERVAL_IN_SECONDS)

        self._events_iteration += 1
        LOG.debug(
            f'Looking up twitter events on iteration {self._events_iteration}')

        # set up timer
        twitter_events_thread = threading.Timer(
            interval, self._find_and_respond_to_twitter_events)
        self._lookup_threads.append(twitter_events_thread)

        # start timer
        twitter_events_thread.start()

        try:
            new_events: list[TwitterEvent] = TwitterEvent.get_all_by(
                is_duplicate=False,
                responded_to=False,
                response_in_progress=False)

            LOG.debug(f'new events: {new_events}')

            failed_events: list[TwitterEvent] = TwitterEvent.get_all_by(
                is_duplicate=False,
                error_on_lookup=True,
                responded_to=True,
                response_in_progress=False)

            failed_events_that_need_response: list[TwitterEvent] = self._filter_failed_twitter_events(failed_events)

            events_to_respond_to: [list[TwitterEvent]] = new_events + failed_events_that_need_response

            LOG.debug(f'events to respond to: {events_to_respond_to}')

            for event in events_to_respond_to:
                self._process_twitter_event(event=event)

        except Exception as e:

            LOG.error(e)
            LOG.error(str(e))
            LOG.error(e.args)
            logging.exception("stack trace")

        finally:
            TwitterEvent.query.session.close()
Exemplo n.º 2
0
    def test_find_and_respond_to_twitter_events(self, twitter_event_mock):
        db_id = 1
        random_id = random.randint(10000000000000000000, 20000000000000000000)
        event_type = 'status'
        user_handle = 'bdhowald'
        user_id = random.randint(1000000000, 2000000000)
        event_text = '@HowsMyDrivingNY abc1234:ny'
        timestamp = random.randint(1500000000000, 1700000000000)
        in_reply_to_message_id = random.randint(
            10000000000000000000, 20000000000000000000)
        location = 'Queens, NY'
        responded_to = 0
        user_mentions = '@HowsMyDrivingNY'

        twitter_event = TwitterEvent(
            id=db_id,
            created_at=timestamp,
            event_id=random_id,
            event_text=event_text,
            event_type=event_type,
            in_reply_to_message_id=in_reply_to_message_id,
            location=location,
            responded_to=responded_to,
            user_handle=user_handle,
            user_id=user_id,
            user_mentions=user_mentions)

        lookup_request = AccountActivityAPIDirectMessage(
            message=TwitterEvent(
                id=1,
                created_at=timestamp,
                event_id=random_id,
                event_text='@howsmydrivingny ny:hme6483',
                event_type='direct_message',
                user_handle=user_handle,
                user_id=30139847),
            message_source='api')

        twitter_event_mock.get_all_by.return_value = [twitter_event]
        twitter_event_mock.query.filter_by().filter().count.return_value = 0

        initiate_reply_mock = MagicMock(name='initiate_reply')
        self.tweeter.aggregator.initiate_reply = initiate_reply_mock

        build_reply_data_mock = MagicMock(name='build_reply_data')
        build_reply_data_mock.return_value = lookup_request
        self.tweeter.reply_argument_builder.build_reply_data = build_reply_data_mock

        self.tweeter._find_and_respond_to_twitter_events()

        self.tweeter.aggregator.initiate_reply.assert_called_with(
            lookup_request=lookup_request)
Exemplo n.º 3
0
    def _add_twitter_events_for_missed_statuses(self, messages: List[tweepy.Status]):
        """Creates TwitterEvent objects when the Account Activity API fails to send us
        status events via webhooks to have them created by the HowsMyDrivingNY API.

        :param messages: List[tweepy.Status]: The statuses returned via the Twitter
                                              Search API via Tweepy.
        """

        undetected_messages = 0

        for message in messages:
            existing_event: Optional[TwitterEvent] = TwitterEvent.query.filter(TwitterEvent.event_id == message.id).first()

            if not existing_event and message.user.id != HMDNY_TWITTER_USER_ID:
                undetected_messages += 1

                event: TwitterEvent = TwitterEvent(
                    event_type=TwitterMessageType.STATUS.value,
                    event_id=message.id,
                    user_handle=message.user.screen_name,
                    user_id=message.user.id,
                    event_text=message.full_text,
                    created_at=message.created_at.replace(tzinfo=pytz.timezone('UTC')).timestamp() * MILLISECONDS_PER_SECOND,
                    in_reply_to_message_id=message.in_reply_to_status_id,
                    location=message.place and message.place.full_name,
                    user_mentions=' '.join([user['screen_name'] for user in message.entities['user_mentions']]),
                    detected_via_account_activity_api=False)

                TwitterEvent.query.session.add(event)

        TwitterEvent.query.session.commit()

        LOG.debug(
            f"Found {undetected_messages} status{'' if undetected_messages == 1 else 'es'} that "
            f"{'was' if undetected_messages == 1 else 'were'} previously undetected.")
    def test_process_response_direct_message(self, mocked_is_production):
        """ Test direct message and new format """

        username = '******'
        message_id = random.randint(1000000000000000000, 2000000000000000000)

        lookup_request = AccountActivityAPIDirectMessage(
            message=TwitterEvent(
                id=1,
                created_at=random.randint(1_000_000_000_000, 2_000_000_000_000),
                event_id=message_id,
                event_text='@howsmydrivingny ny:hme6483',
                event_type='direct_message',
                user_handle=username,
                user_id=30139847),
            message_source='direct_message')

        combined_message = "@bdhowald #NY_HME6483 has been queried 1 time.\n\nTotal parking and camera violation tickets: 15\n\n4 | No Standing - Day/Time Limits\n3 | No Parking - Street Cleaning\n1 | Failure To Display Meter Receipt\n1 | No Violation Description Available\n1 | Bus Lane Violation\n\n@bdhowald Parking and camera violation tickets for #NY_HME6483, cont'd:\n\n1 | Failure To Stop At Red Light\n1 | No Standing - Commercial Meter Zone\n1 | Expired Meter\n1 | Double Parking\n1 | No Angle Parking\n\n@bdhowald Violations by year for #NY_HME6483:\n\n10 | 2017\n15 | 2018\n\n@bdhowald Known fines for #NY_HME6483:\n\n$200.00 | Fined\n$125.00 | Outstanding\n$75.00   | Paid\n"

        mocked_is_production.return_value = True

        send_direct_message_mock = MagicMock('send_direct_message_mock')

        application_api_mock = MagicMock(name='application_api')
        application_api_mock.send_direct_message = send_direct_message_mock
        self.tweeter._app_api = application_api_mock

        self.tweeter._process_response(
            request_object=lookup_request,
            response_parts=[[combined_message]],
            successful_lookup=True)

        send_direct_message_mock.assert_called_with(
          recipient_id=30139847,
          text=combined_message)
    def test_process_response_campaign_only_lookup(self,
                                                   recursively_process_status_updates_mock):
        """ Test campaign-only lookup """

        username = '******'
        message_id = random.randint(1000000000000000000, 2000000000000000000)
        campaign_hashtag = '#SaferSkillman'
        campaign_tickets = random.randint(1000, 2000)
        campaign_vehicles = random.randint(100, 200)

        response_parts = [[(f"@{username} {'{:,}'.format(campaign_vehicles)} vehicles with a total of "
            f"{'{:,}'.format(campaign_tickets)} tickets have been tagged with {campaign_hashtag}.\n\n")]]

        lookup_request = AccountActivityAPIStatus(
            message=TwitterEvent(
                id=1,
                created_at=random.randint(1_000_000_000_000, 2_000_000_000_000),
                event_id=message_id,
                event_text=f'@howsmydrivingny {campaign_hashtag}',
                event_type='status',
                user_handle=username,
                user_id=30139847,
                user_mention_ids='813286,19834403,1230933768342528001,37687633,66379182',
                user_mentions='@BarackObama @NYCMayor @GoodNYCMayor @NYC_DOT @NYCTSubway'
            ),
            message_source='status')

        reply_event_args = {
            'error_on_lookup': False,
            'request_object': lookup_request,
            'response_parts': response_parts,
            'success': True,
            'successful_lookup': True,
            'username': username
        }

        reply_event_args['username'] = username

        self.tweeter._process_response(
            request_object=lookup_request,
            response_parts=response_parts,
            successful_lookup=True)

        recursively_process_status_updates_mock.assert_called_with(
            response_parts=response_parts,
            message_id=message_id,
            user_mention_ids=[
                '813286',
                '19834403',
                '1230933768342528001',
                '37687633',
                '66379182'
            ])
Exemplo n.º 6
0
    def test_process_response_status_legacy_format(
            self, recursively_process_status_updates_mock):
        """ Test status and old format """

        username = '******'
        message_id = random.randint(1000000000000000000, 2000000000000000000)

        response_parts = [[
            '@BarackObama #PA_GLF7467 has been queried 1 time.\n\nTotal parking and camera violation tickets: 49\n\n17 | No Parking - Street Cleaning\n6   | Expired Meter\n5   | No Violation Description Available\n3   | Fire Hydrant\n3   | No Parking - Day/Time Limits\n',
            "@BarackObama Parking and camera violation tickets for #PA_GLF7467, cont'd:\n\n3   | Failure To Display Meter Receipt\n3   | School Zone Speed Camera Violation\n2   | No Parking - Except Authorized Vehicles\n2   | Bus Lane Violation\n1   | Failure To Stop At Red Light\n",
            "@BarackObama Parking and camera violation tickets for #PA_GLF7467, cont'd:\n\n1   | No Standing - Day/Time Limits\n1   | No Standing - Except Authorized Vehicle\n1   | Obstructing Traffic Or Intersection\n1   | Double Parking\n",
            '@BarackObama Known fines for #PA_GLF7467:\n\n$1,000.00 | Fined\n$225.00     | Outstanding\n$775.00     | Paid\n'
        ]]

        lookup_request = AccountActivityAPIStatus(message=TwitterEvent(
            id=1,
            created_at=random.randint(1_000_000_000_000, 2_000_000_000_000),
            event_id=message_id,
            event_text='@howsmydrivingny plate:glf7467 state:pa',
            event_type='status',
            user_handle=username,
            user_id=30139847),
                                                  message_source='status')

        reply_event_args = {
            'error_on_lookup': False,
            'request_object': lookup_request,
            'response_parts': response_parts,
            'success': True,
            'successful_lookup': True,
            'username': username
        }

        is_production_mock = MagicMock(name='is_production')
        is_production_mock.return_value = True

        create_favorite_mock = MagicMock(name='is_production')
        create_favorite_mock.return_value = True

        application_api_mock = MagicMock(name='application_api')
        application_api_mock.create_favorite = create_favorite_mock

        self.tweeter._is_production = is_production_mock
        self.tweeter._app_api = application_api_mock

        reply_event_args['username'] = username

        self.tweeter._process_response(request_object=lookup_request,
                                       response_parts=response_parts,
                                       successful_lookup=True)

        recursively_process_status_updates_mock.assert_called_with(
            response_parts=response_parts, message_id=message_id)
    def test_process_response_with_error(self,
                                         recursively_process_status_updates_mock):
        """ Test error handling """

        username = '******'
        message_id = random.randint(1000000000000000000, 2000000000000000000)

        lookup_request = AccountActivityAPIStatus(
            message=TwitterEvent(
                id=1,
                created_at=random.randint(1_000_000_000_000, 2_000_000_000_000),
                event_id=message_id,
                event_text='@howsmydrivingny plate:glf7467 state:pa',
                event_type='status',
                user_handle=username,
                user_id=30139847,
                user_mention_ids='813286,19834403,1230933768342528001,37687633,66379182',
                user_mentions='@BarackObama @NYCMayor @GoodNYCMayor @NYC_DOT @NYCTSubway'
            ),
            message_source='status'
        )

        response_parts = [
            ['@' + username + " Sorry, I encountered an error. Tagging @bdhowald."]]

        reply_event_args = {
            'error_on_lookup': False,
            'request_object': lookup_request,
            'response_parts': response_parts,
            'success': True,
            'successful_lookup': True,
            'username': username
        }

        reply_event_args['username'] = username

        self.tweeter._process_response(
            request_object=lookup_request,
            response_parts=response_parts,
            successful_lookup=True)

        recursively_process_status_updates_mock.assert_called_with(
            response_parts=response_parts,
            message_id=message_id,
            user_mention_ids=[
                '813286',
                '19834403',
                '1230933768342528001',
                '37687633',
                '66379182'
            ])
    def test_process_response_with_direct_message_api_direct_message(self,
                                                                     recursively_process_status_updates_mock):
        """ Test plateless lookup """

        username = '******'
        message_id = random.randint(1000000000000000000, 2000000000000000000)

        response_parts = [
            ['@' + username + " I think you're trying to look up a plate, but can't be sure.\n\nJust a reminder, the format is <state|province|territory>:<plate>, e.g. NY:abc1234"]]

        lookup_request = AccountActivityAPIStatus(
            message=TwitterEvent(
                id=1,
                created_at=random.randint(1_000_000_000_000, 2_000_000_000_000),
                event_id=message_id,
                event_text='@howsmydrivingny the state is ny',
                event_type='status',
                user_handle=username,
                user_id=30139847,
                user_mention_ids='813286,19834403,1230933768342528001,37687633,66379182',
                user_mentions='@BarackObama @NYCMayor @GoodNYCMayor @NYC_DOT @NYCTSubway'
            ),
            message_source='status')

        reply_event_args = {
            'error_on_lookup': False,
            'request_object': lookup_request,
            'response_parts': response_parts,
            'success': True,
            'successful_lookup': True,
            'username': username
        }

        reply_event_args['username'] = username

        self.tweeter._process_response(
            request_object=lookup_request,
            response_parts=response_parts,
            successful_lookup=True)

        recursively_process_status_updates_mock.assert_called_with(
            response_parts=response_parts,
            message_id=message_id,
            user_mention_ids=[
                '813286',
                '19834403',
                '1230933768342528001',
                '37687633',
                '66379182'
            ])
Exemplo n.º 9
0
    def test_build_account_activity_api_status(self):
        account_activity_api_status_twitter_event = TwitterEvent(
            id=1,
            created_at=random.randint(self.CREATED_AT_MIN,
                                      self.CREATED_AT_MAX),
            event_id=random.randint(self.EVENT_ID_MIN, self.EVENT_ID_MAX),
            event_text='hi there',
            event_type='status',
            user_handle=self.OTHER_TWITTER_HANDLE,
            user_id=random.randint(self.USER_ID_MIN, self.USER_ID_MAX),
            user_mentions=self.HMDNY_TWITTER_HANDLE)

        req = self.reply_argument_builder.build_reply_data(
            account_activity_api_status_twitter_event, LookupSource.STATUS)

        self.assertIsInstance(req, AccountActivityAPIStatus)
Exemplo n.º 10
0
    def _add_twitter_events_for_missed_direct_messages(self, messages: list[tweepy.models.Status]) -> None:
        """Creates TwitterEvent objects when the Account Activity API fails to send us
        direct message events via webhooks to have them created by the HowsMyDrivingNY API.

        :param messages: list[tweepy.DirectMessage]: The direct messages returned via the
                                                     Twitter Search API via Tweepy.
        """

        undetected_messages = 0

        sender_ids = set(int(message.message_create['sender_id']) for message in messages)

        sender_objects = self._get_twitter_client_api().lookup_users(user_id=sender_ids)
        senders = {sender.id_str:sender for sender in sender_objects}

        for message in messages:

            existing_event: Optional[TwitterEvent] = TwitterEvent.query.filter(TwitterEvent.event_id == message.id).first()

            if not existing_event and int(message.message_create['sender_id']) != HMDNY_TWITTER_USER_ID:
                undetected_messages += 1

                sender = senders[message.message_create['sender_id']]

                event: TwitterEvent = TwitterEvent(
                    event_type=TwitterMessageType.DIRECT_MESSAGE.value,
                    event_id=message.id,
                    user_handle=sender.screen_name,
                    user_id=sender.id,
                    event_text=message.message_create['message_data']['text'],
                    created_at=message.created_timestamp,
                    in_reply_to_message_id=None,
                    location=None,
                    user_mention_ids=','.join([user['id_str'] for user in message.message_create['message_data']['entities']['user_mentions']]),
                    user_mentions=' '.join([user['screen_name'] for user in message.message_create['message_data']['entities']['user_mentions']]),
                    detected_via_account_activity_api=False)

                TwitterEvent.query.session.add(event)

        TwitterEvent.query.session.commit()

        LOG.debug(
            f"Found {undetected_messages} direct message{'' if undetected_messages == 1 else 's'} that "
            f"{'was' if undetected_messages == 1 else 'were'} previously undetected.")
Exemplo n.º 11
0
    def test_process_response_with_error(
            self, recursively_process_status_updates_mock):
        """ Test error handling """

        username = '******'
        message_id = random.randint(1000000000000000000, 2000000000000000000)

        lookup_request = AccountActivityAPIStatus(message=TwitterEvent(
            id=1,
            created_at=random.randint(1_000_000_000_000, 2_000_000_000_000),
            event_id=message_id,
            event_text='@howsmydrivingny plate:glf7467 state:pa',
            event_type='status',
            user_handle=username,
            user_id=30139847),
                                                  message_source='status')

        response_parts = [[
            '@' + username +
            " Sorry, I encountered an error. Tagging @bdhowald."
        ]]

        reply_event_args = {
            'error_on_lookup': False,
            'request_object': lookup_request,
            'response_parts': response_parts,
            'success': True,
            'successful_lookup': True,
            'username': username
        }

        reply_event_args['username'] = username

        self.tweeter._process_response(request_object=lookup_request,
                                       response_parts=response_parts,
                                       successful_lookup=True)

        recursively_process_status_updates_mock.assert_called_with(
            response_parts=response_parts, message_id=message_id)
Exemplo n.º 12
0
    def test_process_response_campaign_only_lookup(self,
                                                   recursively_process_status_updates_mock):
        """ Test campaign-only lookup """

        username = '******'
        message_id = random.randint(1000000000000000000, 2000000000000000000)
        campaign_hashtag = '#SaferSkillman'
        campaign_tickets = random.randint(1000, 2000)
        campaign_vehicles = random.randint(100, 200)

        response_parts = [[(f"@{username} {'{:,}'.format(campaign_vehicles)} vehicles with a total of "
            f"{'{:,}'.format(campaign_tickets)} tickets have been tagged with {campaign_hashtag}.\n\n")]]

        lookup_request = AccountActivityAPIStatus(
            message=TwitterEvent(
                id=1,
                created_at=random.randint(1_000_000_000_000, 2_000_000_000_000),
                event_id=message_id,
                event_text=f'@howsmydrivingny {campaign_hashtag}',
                event_type='status',
                user_handle=username,
                user_id=30139847),
            message_source='status')

        reply_event_args = {
            'error_on_lookup': False,
            'request_object': lookup_request,
            'response_parts': response_parts,
            'success': True,
            'successful_lookup': True,
            'username': username
        }

        reply_event_args['username'] = username

        self.tweeter._process_response(reply_event_args)

        recursively_process_status_updates_mock.assert_called_with(
            response_parts, message_id, username)
Exemplo n.º 13
0
    def test_process_response_with_search_status(
            self, recursively_process_status_updates_mock):
        """ Test plateless lookup """

        username = '******'
        message_id = random.randint(1000000000000000000, 2000000000000000000)

        response_parts = [[
            '@' + username +
            ' I’d be happy to look that up for you!\n\nJust a reminder, the format is <state|province|territory>:<plate>, e.g. NY:abc1234'
        ]]

        lookup_request = AccountActivityAPIStatus(message=TwitterEvent(
            id=1,
            created_at=random.randint(1_000_000_000_000, 2_000_000_000_000),
            event_id=message_id,
            event_text='@howsmydrivingny plate dkr9364 state ny',
            event_type='status',
            user_handle=username,
            user_id=30139847),
                                                  message_source='status')

        reply_event_args = {
            'error_on_lookup': False,
            'request_object': lookup_request,
            'response_parts': response_parts,
            'success': True,
            'successful_lookup': True,
            'username': username
        }

        reply_event_args['username'] = username

        self.tweeter._process_response(request_object=lookup_request,
                                       response_parts=response_parts,
                                       successful_lookup=True)

        recursively_process_status_updates_mock.assert_called_with(
            response_parts=response_parts, message_id=message_id)
Exemplo n.º 14
0
    def test_process_response_with_direct_message_api_direct_message(self,
                                                                     recursively_process_status_updates_mock):
        """ Test plateless lookup """

        username = '******'
        message_id = random.randint(1000000000000000000, 2000000000000000000)

        response_parts = [
            ['@' + username + " I think you're trying to look up a plate, but can't be sure.\n\nJust a reminder, the format is <state|province|territory>:<plate>, e.g. NY:abc1234"]]

        lookup_request = AccountActivityAPIStatus(
            message=TwitterEvent(
                id=1,
                created_at=random.randint(1_000_000_000_000, 2_000_000_000_000),
                event_id=message_id,
                event_text='@howsmydrivingny the state is ny',
                event_type='status',
                user_handle=username,
                user_id=30139847),
            message_source='status')

        reply_event_args = {
            'error_on_lookup': False,
            'request_object': lookup_request,
            'response_parts': response_parts,
            'success': True,
            'successful_lookup': True,
            'username': username
        }

        reply_event_args['username'] = username

        self.tweeter._process_response(reply_event_args)

        recursively_process_status_updates_mock.assert_called_with(
            response_parts, message_id, username)
Exemplo n.º 15
0
    def _process_twitter_event(self, event: TwitterEvent):
        LOG.debug(f'Beginning response for event: {event.id}')

        # search for duplicates
        is_event_duplicate: bool = TwitterEvent.query.filter_by(
            event_type=event.event_type,
            event_id=event.event_id,
            user_handle=event.user_handle,
            responded_to=True
        ).filter(
            TwitterEvent.id != event.id).count() > 0

        if is_event_duplicate:
            event.is_duplicate = True
            TwitterEvent.query.session.commit()

            LOG.info(f'Event {event.id} is a duplicate, skipping.')

        else:
            event.response_in_progress = True
            TwitterEvent.query.session.commit()

            try:
                message_source: str = LookupSource(event.event_type)

                # build request
                lookup_request: Type[BaseLookupRequest] = self.reply_argument_builder.build_reply_data(
                    message=event,
                    message_source=message_source)

                user_is_follower = lookup_request.requesting_user_is_follower(
                    follower_ids=self._get_follower_ids())

                perform_lookup_for_user: bool = (user_is_follower or
                    event.user_favorited_non_follower_reply)

                if self.aggregator.lookup_has_valid_plates(
                    lookup_request=lookup_request) and not perform_lookup_for_user:

                    response_parts: List[Any]

                    if lookup_request.is_direct_message():
                        response_parts = [L10N.NON_FOLLOWER_DIRECT_MESSAGE_REPLY_STRING]
                    elif lookup_request.is_status():
                        response_parts = [L10N.NON_FOLLOWER_TWEET_REPLY_STRING]

                    try:
                        reply_message_id = self._process_response(
                            request_object=lookup_request,
                            response_parts=response_parts)

                        # Save the reply id, so that when the user favorites it,
                        # we can trigger the search.
                        non_follower_reply = NonFollowerReply(
                            created_at=(int(datetime.utcnow().timestamp() *
                                MILLISECONDS_PER_SECOND)),
                            event_type=event.event_type,
                            event_id=reply_message_id,
                            in_reply_to_message_id=event.event_id,
                            user_handle=event.user_handle,
                            user_id=event.user_id)

                        NonFollowerReply.query.session.add(
                            non_follower_reply)
                        NonFollowerReply.query.session.commit()

                    except tweepy.error.TweepError as e:
                        event.error_on_lookup = True
                else:
                    # Reply to the event.
                    reply_to_event = self.aggregator.initiate_reply(
                        lookup_request=lookup_request)

                    success = reply_to_event['success']

                    if success:
                        # There's no need to tell people that
                        # there was an error more than once.
                        if not (reply_to_event[
                            'error_on_lookup'] and event.error_on_lookup):

                            try:
                                self._process_response(
                                    request_object=reply_to_event['request_object'],
                                    response_parts=reply_to_event['response_parts'],
                                    successful_lookup=reply_to_event.get('successful_lookup'))
                            except tweepy.error.TweepError as e:
                                reply_to_event['error_on_lookup'] = True

                    # Update error status
                    if reply_to_event['error_on_lookup']:
                        event.error_on_lookup = True
                    else:
                        event.error_on_lookup = False

                # We've responded!
                event.response_in_progress = False
                event.responded_to = True

                TwitterEvent.query.session.commit()

            except ValueError as e:
                LOG.error(
                    f'Encountered unknown event type. '
                    f'Response is not possible.')
Exemplo n.º 16
0
    def _find_and_respond_to_twitter_events(self):
        """Looks for TwitterEvent objects that have not yet been responded to and
        begins the process of creating a response. Additionally, failed events are
        rerun to provide a correct response, particularly useful in cases where
        external apis are down for maintenance.
        """

        interval = 3.0 if self._is_production() else self.DEVELOPMENT_TIME_INTERVAL

        self.events_iteration += 1
        LOG.debug(
            f'Looking up twitter events on iteration {self.events_iteration}')

        # start timer
        threading.Timer(
            interval, self._find_and_respond_to_twitter_events).start()

        try:
            new_events: [List[TwitterEvent]] = TwitterEvent.get_all_by(
                is_duplicate=False,
                responded_to=False,
                response_in_progress=False)

            LOG.debug(f'new events: {new_events}')

            failed_events: [List[TwitterEvent]] = TwitterEvent.get_all_by(
                is_duplicate=False,
                error_on_lookup=True,
                responded_to=True,
                response_in_progress=False)

            LOG.debug(f'failed events: {failed_events}')

            events_to_respond_to: [List[TwitterEvent]] = new_events + failed_events

            LOG.debug(f'events to respond to: {events_to_respond_to}')

            for event in events_to_respond_to:

                LOG.debug(f'Beginning response for event: {event.id}')

                # search for duplicates
                is_event_duplicate: bool = TwitterEvent.query.filter_by(
                    event_type=event.event_type,
                    event_id=event.event_id,
                    user_handle=event.user_handle,
                    responded_to=True
                ).filter(
                    TwitterEvent.id != event.id).count() > 0

                if is_event_duplicate:
                    event.is_duplicate = True

                    TwitterEvent.query.session.commit()

                    LOG.info(f'Event {event.id} is a duplicate, skipping.')

                else:

                    event.response_in_progress = True
                    TwitterEvent.query.session.commit()

                    try:
                        message_source = LookupSource(event.event_type)

                        # build request
                        lookup_request: Type[BaseLookupRequest] = self.reply_argument_builder.build_reply_data(
                            message=event,
                            message_source=message_source)

                        # Reply to the event.
                        reply_event = self.aggregator.initiate_reply(
                            lookup_request=lookup_request)
                        success = reply_event.get('success', False)

                        if success:
                            # Need username for statuses
                            reply_event['username'] = event.user_handle

                            # There's need to tell people that there was an error more than once
                            if not (reply_event.get(
                                    'error_on_lookup') and event.error_on_lookup):

                                try:
                                    self._process_response(reply_event)
                                except tweepy.error.TweepError as e:
                                    reply_event['error_on_lookup'] = True

                            # We've responded!
                            event.response_in_progress = False
                            event.responded_to = True

                            # Update error status
                            if reply_event.get('error_on_lookup'):
                                event.error_on_lookup = True
                            else:
                                event.error_on_lookup = False

                        TwitterEvent.query.session.commit()

                    except ValueError as e:
                        LOG.error(
                            f'Encountered unknown event type. '
                            f'Response is not possible.')

        except Exception as e:

            LOG.error(e)
            LOG.error(str(e))
            LOG.error(e.args)
            logging.exception("stack trace")

        finally:
            TwitterEvent.query.session.close()
    def test_find_and_respond_to_missed_direct_messages(self, twitter_event_mock):
        db_id = 1
        event_id = random.randint(10000000000000000000, 20000000000000000000)
        event_text = 'abc1234:ny'
        event_type = 'direct_message'
        timestamp = random.randint(1500000000000, 1700000000000)
        user_handle = 'bdhowald'
        user_id = random.randint(1000000000, 2000000000)

        new_twitter_event = TwitterEvent(
            created_at=timestamp + 1,
            detected_via_account_activity_api=False,
            event_id=event_id + 1,
            event_text=event_text + '!',
            event_type=event_type,
            in_reply_to_message_id=None,
            location=None,
            responded_to=False,
            user_handle=user_handle,
            user_id=user_id,
            user_mentions=[])

        message_needing_event = MagicMock(
            id=event_id + 1,
            created_timestamp = timestamp + 1,
            message_create={
              'message_data': {
                'entities': {
                  'user_mentions': [
                    {
                      'id': 123,
                      'id_str': '123',
                      'screen_name': user_handle
                    },
                    {
                      'id': 456,
                      'id_str': '456',
                      'screen_name': 'OtherUser'
                    },
                    {
                      'id': 789,
                      'id_str': '456',
                      'screen_name': 'SomeOtherUser'
                    }
                  ]
                },
                'text': event_text + '!'
              },
              'sender_id': f'{user_id}'
            },
            name='message_needing_event')

        message_not_needing_event = MagicMock(
            id=event_id - 1,
            created_timestamp = timestamp - 1,
            message_create={
                'message_data': {
                    'entities': {
                        'user_mentions': [
                            {
                                'id': 123,
                                'id_str': '123',
                                'screen_name': user_handle
                            },
                            {
                                'id': 456,
                                'id_str': '456',
                                'screen_name': 'OtherUser'
                            },
                            {
                                'id': 789,
                                'id_str': '456',
                                'screen_name': 'SomeOtherUser'
                            }
                        ]
                    },
                  'text': event_text,
                },
                'sender_id': f'{user_id}'
            },
            name='message_not_needing_event')

        sender = MagicMock(
            id=user_id,
            id_str=f'{user_id}',
            name='sender',
            screen_name=user_handle)

        twitter_event_mock.return_value = new_twitter_event
        twitter_event_mock.query.filter.return_value.first.side_effect = [
            None, message_not_needing_event]

        client_api_mock = MagicMock(name='client_api')
        client_api_mock.get_direct_messages.return_value = [
          message_needing_event, message_not_needing_event]
        client_api_mock.lookup_users.return_value = [sender]
        self.tweeter._client_api = client_api_mock

        self.tweeter._find_and_respond_to_missed_direct_messages()

        twitter_event_mock.query.session.add.assert_called_once_with(new_twitter_event)
        twitter_event_mock.query.session.commit.assert_called_once_with()

        self.mocked_log.debug.assert_called_with('Found 1 direct message that was previously undetected.')

        self.tweeter.terminate_lookups()
    def test_find_and_respond_to_missed_statuses(self, twitter_event_mock):
        db_id = 1
        event_id = random.randint(10000000000000000000, 20000000000000000000)
        event_text = '@HowsMyDrivingNY @bdhowald abc1234:ny'
        event_type = 'status'
        in_reply_to_message_id = random.randint(
            10000000000000000000, 20000000000000000000)
        location = 'Queens, NY'
        now = datetime.utcnow()
        place = MagicMock(
            full_name=location,
            name='place')

        user_id = random.randint(1000000000, 2000000000)
        user = MagicMock(
            id=user_id,
            name='user')
        user_handle = 'bdhowald'

        older_twitter_event = TwitterEvent(
            id=db_id,
            created_at=now.replace(tzinfo=pytz.timezone('UTC')).timestamp() * 1000,
            detected_via_account_activity_api=False,
            event_id=event_id,
            event_text=event_text,
            event_type=event_type,
            in_reply_to_message_id=in_reply_to_message_id,
            location=location,
            responded_to=True,
            user_handle=user_handle,
            user_id=user_id,
            user_mention_ids=[user_id],
            user_mentions=[
                {
                    'id': 123,
                    'id_str': '123',
                    'screen_name': user_handle
                },
                {
                    'id': 456,
                    'id_str': '456',
                    'screen_name': 'OtherUser'
                },
                {
                    'id': 789,
                    'id_str': '456',
                    'screen_name': 'SomeOtherUser'
                }
            ])

        new_twitter_event = TwitterEvent(
            created_at=now.replace(tzinfo=pytz.timezone('UTC')).timestamp() * 1000,
            detected_via_account_activity_api=False,
            event_id=event_id + 1,
            event_text=event_text + '!',
            event_type=event_type,
            in_reply_to_message_id=in_reply_to_message_id,
            location=location,
            responded_to=False,
            user_handle=user_handle,
            user_id=user_id,
            user_mention_ids=[user_id],
            user_mentions=user_handle)

        status_needing_event = MagicMock(
            id=event_id + 1,
            created_at = now,
            entities={
                'user_mentions': [
                    {
                        'id': 123,
                        'id_str': '123',
                        'screen_name': user_handle
                    },
                    {
                        'id': 456,
                        'id_str': '456',
                        'screen_name': 'OtherUser'
                    },
                    {
                        'id': 789,
                        'id_str': '456',
                        'screen_name': 'SomeOtherUser'
                    }
                ]
            },
            full_text=event_text + '!',
            in_reply_to_message_id=in_reply_to_message_id,
            name='status_needing_event',
            place=place,
            user=user)

        status_not_needing_event = MagicMock(
            id=event_id - 1,
            created_at = now - relativedelta(minutes=1),
            entities={
                'user_mentions': [
                    {
                        'id': 123,
                        'id_str': '123',
                        'screen_name': user_handle
                    },
                    {
                        'id': 456,
                        'id_str': '456',
                        'screen_name': 'OtherUser'
                    },
                    {
                        'id': 789,
                        'id_str': '456',
                        'screen_name': 'SomeOtherUser'
                    }
                ]
            },
            full_text=event_text,
            in_reply_to_message_id=in_reply_to_message_id,
            name='status_not_needing_event',
            place=place,
            user=user)

        twitter_event_mock.return_value = new_twitter_event
        twitter_event_mock.query.filter().order_by().first.return_value = older_twitter_event
        twitter_event_mock.query.filter().first.side_effect = [
            None, status_not_needing_event]

        client_api_mock = MagicMock(name='client_api')
        client_api_mock.mentions_timeline.side_effect = [[
            status_needing_event, status_not_needing_event], []]
        self.tweeter._client_api = client_api_mock

        self.tweeter._find_and_respond_to_missed_statuses()

        twitter_event_mock.query.session.add.assert_called_once_with(new_twitter_event)
        twitter_event_mock.query.session.commit.assert_called_once_with()

        self.mocked_log.debug.assert_called_with('Found 1 status that was previously undetected.')

        self.tweeter.terminate_lookups()
    def test_find_and_respond_to_failed_twitter_events(self,
                                                       twitter_event_mock: MagicMock,
                                                       event_type: str,
                                                       expect_called: bool,
                                                       num_times_failed: int,
                                                       last_failed_time: timedelta = timedelta(minutes=0),
                                                       tweet_exists: bool = True):
        db_id = 1
        random_id = random.randint(1000000000000000000, 2000000000000000000)
        user_handle = 'bdhowald'
        user_id = random.randint(1000000000, 2000000000)
        event_text = '@HowsMyDrivingNY abc1234:ny'
        timestamp = random.randint(1500000000000, 1700000000000)
        in_reply_to_message_id = random.randint(
            10000000000000000000, 20000000000000000000)
        location = 'Queens, NY'
        responded_to = 0
        user_mentions = [
            {
                'id': 123,
                'id_str': '123',
                'screen_name': 'HowsMyDrivingNY'
            },
            {
                'id': 456,
                'id_str': '456',
                'screen_name': 'OtherUser'
            },
            {
                'id': 789,
                'id_str': '456',
                'screen_name': 'SomeOtherUser'
            }
        ]


        twitter_event = TwitterEvent(
            id=db_id,
            created_at=timestamp,
            event_id=random_id,
            event_text=event_text,
            event_type=event_type,
            in_reply_to_message_id=in_reply_to_message_id,
            last_failed_at_time=(datetime.utcnow() - last_failed_time),
            location=location,
            num_times_failed=num_times_failed,
            responded_to=responded_to,
            user_handle=user_handle,
            user_id=user_id,
            user_mentions=user_mentions)

        direct_message_lookup_request = AccountActivityAPIDirectMessage(
            message=TwitterEvent(
                id=1,
                created_at=timestamp,
                event_id=random_id,
                event_text=event_text,
                event_type=event_type,
                user_handle=user_handle,
                user_id=user_id,
                user_favorited_non_follower_reply=False),
            message_source=event_type)

        status_lookup_request = AccountActivityAPIStatus(
            message=TwitterEvent(
                id=1,
                created_at=timestamp,
                event_id=random_id,
                event_text=event_text,
                event_type=event_type,
                user_handle=user_handle,
                user_id=user_id,
                user_favorited_non_follower_reply=False),
            message_source=event_type)

        lookup_request = (direct_message_lookup_request if
            event_type == 'direct_message' else status_lookup_request)

        twitter_event_mock.get_all_by.side_effect = [[], [twitter_event]]
        twitter_event_mock.query.filter_by().filter().count.return_value = 0

        tweet_exists_mock = MagicMock(name='tweet_exists')
        tweet_exists_mock.return_value = True if tweet_exists else False
        self.tweeter.tweet_detection_service.tweet_exists = tweet_exists_mock

        initiate_reply_mock = MagicMock(name='initiate_reply')
        self.tweeter.aggregator.initiate_reply = initiate_reply_mock

        build_reply_data_mock = MagicMock(name='build_reply_data')
        build_reply_data_mock.return_value = lookup_request
        self.tweeter.reply_argument_builder.build_reply_data = build_reply_data_mock

        application_api_mock = MagicMock(name='application_api')
        application_api_mock.get_follower_ids.return_value = ([user_id], (123, 0))
        self.tweeter._app_api = application_api_mock

        process_response_mock = MagicMock(name='process_response')
        process_response_mock.return_value = random_id
        self.tweeter._process_response = process_response_mock

        self.tweeter._find_and_respond_to_twitter_events()

        if expect_called:
            self.tweeter.aggregator.initiate_reply.assert_called_with(
                lookup_request=lookup_request)
        else:
            self.tweeter.aggregator.initiate_reply.assert_not_called()

        self.tweeter.terminate_lookups()
    def test_find_and_respond_to_twitter_events(self,
                                                twitter_event_mock: MagicMock,
                                                event_type: str,
                                                is_follower: bool,
                                                response_parts: Optional[list[str]] = None):
        db_id = 1
        random_id = random.randint(1000000000000000000, 2000000000000000000)
        user_handle = 'bdhowald'
        user_id = random.randint(1000000000, 2000000000)
        event_text = '@HowsMyDrivingNY abc1234:ny'
        timestamp = random.randint(1500000000000, 1700000000000)
        in_reply_to_message_id = random.randint(
            10000000000000000000, 20000000000000000000)
        location = 'Queens, NY'
        responded_to = 0
        user_mentions = [
            {
                'id': 123,
                'id_str': '123',
                'screen_name': 'HowsMyDrivingNY'
            },
            {
                'id': 456,
                'id_str': '456',
                'screen_name': 'OtherUser'
            },
            {
                'id': 789,
                'id_str': '456',
                'screen_name': 'SomeOtherUser'
            }
        ]

        twitter_event = TwitterEvent(
            id=db_id,
            created_at=timestamp,
            event_id=random_id,
            event_text=event_text,
            event_type=event_type,
            in_reply_to_message_id=in_reply_to_message_id,
            last_failed_at_time=None,
            location=location,
            num_times_failed=0,
            responded_to=responded_to,
            user_handle=user_handle,
            user_id=user_id,
            user_mentions=user_mentions)

        lookup_request = AccountActivityAPIDirectMessage(
            message=TwitterEvent(
                id=1,
                created_at=timestamp,
                event_id=random_id,
                event_text=event_text,
                event_type=event_type,
                user_handle=user_handle,
                user_id=user_id,
                user_favorited_non_follower_reply=False),
            message_source=event_type)

        twitter_event_mock.get_all_by.return_value = [twitter_event]
        twitter_event_mock.query.filter_by().filter().count.return_value = 0

        initiate_reply_mock = MagicMock(name='initiate_reply')
        self.tweeter.aggregator.initiate_reply = initiate_reply_mock

        build_reply_data_mock = MagicMock(name='build_reply_data')
        build_reply_data_mock.return_value = lookup_request
        self.tweeter.reply_argument_builder.build_reply_data = build_reply_data_mock

        application_api_mock = MagicMock(name='application_api')
        application_api_mock.get_follower_ids.return_value = ([
            user_id if is_follower else (user_id + 1)], (123, 0))
        self.tweeter._app_api = application_api_mock

        process_response_mock = MagicMock(name='process_response')
        process_response_mock.return_value = random_id
        self.tweeter._process_response = process_response_mock

        self.tweeter._find_and_respond_to_twitter_events()

        if is_follower:
            self.tweeter.aggregator.initiate_reply.assert_called_with(
                lookup_request=lookup_request)
        else:
            self.tweeter.aggregator.initiate_reply.assert_not_called()
            self.tweeter._process_response.assert_called_with(
                request_object=lookup_request,
                response_parts=response_parts)

        self.tweeter.terminate_lookups()