예제 #1
0
def update_poll_messages(session, bot, poll):
    """Logic for handling updates."""
    now = datetime.now()
    # Check whether we have a new window
    current_update = session.query(Update) \
        .filter(Update.poll == poll) \
        .one_or_none()

    # Don't handle it in here, it's already handled in the job
    if current_update is not None:
        return

    try:
        # Try to send updates
        send_updates(session, bot, poll)
    except RetryAfter as e:
        # Schedule an update after the RetryAfter timeout + 1 second buffer
        try:
            update = Update(poll,
                            now + timedelta(seconds=int(e.retry_after) + 1))
            session.add(update)
            session.commit()
        except (UniqueViolation, IntegrityError):
            session.rollback()

    except Exception:
        # Something happened
        # Schedule an update after two secondsj
        try:
            update = Update(poll, now + timedelta(seconds=3))
            session.add(update)
            session.commit()
        except (UniqueViolation, IntegrityError):
            # The update has already been added
            session.rollback()
예제 #2
0
def try_update_reference(session, bot, poll, reference, first_try=False):
    try:
        update_reference(session, bot, poll, reference, first_try)
    except RetryAfter as e:
        session.rollback()
        # Handle a flood control exception on initial reference update.
        retry_after_seconds = int(e.retry_after) + 1
        retry_after = datetime.now() + timedelta(seconds=retry_after_seconds)
        update = Update(poll, retry_after)
        try:
            session.add(update)
            session.commit()
        except IntegrityError:
            # There's already a scheduled update for this poll.
            session.rollback()

    except IntegrityError:
        # There's already a scheduled update for this poll.
        session.rollback()
예제 #3
0
def update_poll_messages(session, bot, poll):
    """Logic for handling updates."""
    now = datetime.now()
    # Check whether we have a new window
    current_update = session.query(Update) \
        .filter(Update.poll == poll) \
        .one_or_none()

    # Don't handle it in here, it's already handled in the job
    if current_update is not None:
        return

    try:
        # Try to send updates
        send_updates(session, bot, poll)
    except (TimedOut, RetryAfter) as e:
        # Schedule an update after the RetryAfter timeout + 1 second buffer
        if isinstance(e, RetryAfter):
            retry_after = int(e.retry_after) + 1
        else:
            retry_after = 2

        try:
            update = Update(poll, now + timedelta(seconds=retry_after))
            session.add(update)
            session.commit()
        except (UniqueViolation, IntegrityError):
            session.rollback()

    except Exception as e:
        # We encountered an unknown error
        # Since we don't want to continuously tro to send this update, and spam sentry, delete the update
        if current_update is not None:
            session.delete(current_update)
        session.commit()

        raise e
예제 #4
0
def update_poll_messages(session,
                         bot,
                         poll,
                         message_id=None,
                         user=None,
                         inline_message_id=None):
    """Logic for handling updates.

    The message the original call has been made from will be updated instantly.
    The updates on all other messages will be scheduled in the background.
    """
    now = datetime.now()
    reference = None
    if message_id is not None:
        reference = (session.query(Reference).filter(
            Reference.message_id == message_id).filter(
                Reference.user == user).filter(
                    Reference.poll == poll).one_or_none())
    elif inline_message_id is not None:
        reference = (session.query(Reference).filter(
            Reference.bot_inline_message_id == inline_message_id).filter(
                Reference.poll == poll).one_or_none())

    # Check whether there already is a scheduled update
    new_update = False
    retry_after = None
    update = session.query(Update).filter(Update.poll == poll).one_or_none()

    if reference is not None:
        try:
            update_reference(session, bot, poll, reference)
        except RetryAfter as e:
            retry_after = int(e.retry_after) + 1
            retry_after = datetime.now() + timedelta(seconds=retry_after)
            pass

    # If there's no update yet, create a new one
    if update is None:
        try:
            update = Update(poll, now)
            session.add(update)
            if retry_after is not None:
                update.next_update = retry_after

            session.commit()
            new_update = True
        except (UniqueViolation, IntegrityError):
            # Some other function already created the update. Try again
            session.rollback()
            update_poll_messages(session, bot, poll)

    if not new_update:
        # In case there already is an update increase the counter and set the next_update date
        # This will result in a new update in the background job and ensures
        # currently (right now) running updates will be scheduled again.
        if retry_after is not None:
            next_update = retry_after
        else:
            next_update = datetime.now()

        try:
            session.query(Update).filter(Update.poll == poll).update({
                "count":
                Update.count + 1,
                "next_update":
                next_update
            })
        except ObjectDeletedError:
            # This is a hard edge-case
            # This occurs, if the update we got a few microseconds ago
            # just got deleted by the background job. It happens maybe
            # once every 10000 requests and fixes itself as soon as somebody
            # votes on the poll once more
            #
            # The result of this MAY be, that polls in other chats have a
            # desync of a single vote. But it may also be the case, that
            # everything is already in sync.
            session.rollback()
            # Anyway just try again
            update_poll_messages(session, bot, poll)
            pass
예제 #5
0
def update_reference(
    session, bot, poll, reference, show_warning=False, first_try=False
):
    try:
        # Admin poll management interface
        if reference.type == ReferenceType.admin.name and not poll.in_settings:
            text, keyboard = get_poll_text_and_vote_keyboard(
                session, poll, user=poll.user, show_warning=show_warning, show_back=True
            )

            if poll.user.expected_input != ExpectedInput.votes.name:
                keyboard = get_management_keyboard(poll)

            bot.edit_message_text(
                text,
                chat_id=reference.user.id,
                message_id=reference.message_id,
                reply_markup=keyboard,
                parse_mode="markdown",
                disable_web_page_preview=True,
            )

        # User that votes in private chat (priority vote)
        elif reference.type == ReferenceType.private_vote.name:
            text, keyboard = get_poll_text_and_vote_keyboard(
                session,
                poll,
                user=reference.user,
                show_warning=show_warning,
            )

            bot.edit_message_text(
                text,
                chat_id=reference.user.id,
                message_id=reference.message_id,
                reply_markup=keyboard,
                parse_mode="markdown",
                disable_web_page_preview=True,
            )

        # Edit message created via inline query
        elif reference.type == ReferenceType.inline.name:
            # Create text and keyboard
            text, keyboard = get_poll_text_and_vote_keyboard(
                session, poll, show_warning=show_warning
            )

            bot.edit_message_text(
                text,
                inline_message_id=reference.bot_inline_message_id,
                reply_markup=keyboard,
                parse_mode="markdown",
                disable_web_page_preview=True,
            )

    except BadRequest as e:
        if (
            e.message.startswith("Message_id_invalid")
            or e.message.startswith("Message can't be edited")
            or e.message.startswith("Message to edit not found")
            or e.message.startswith("Chat not found")
            or e.message.startswith("Can't access the chat")
        ):
            # This is just a hunch
            # It feels like we're too fast and the message isn't synced between Telegram's servers yet.
            # If this happens, allow the first try to fail and schedule an update
            # If it happens again, we'll fail on the second try
            if first_try:
                update = Update(poll, datetime.now() + timedelta(seconds=2))
                session.add(update)
                sentry.capture_exception(
                    extra={
                        "context": "update_reference",
                    },
                )

                return

            session.delete(reference)
            session.commit()
        elif e.message.startswith("Message is not modified"):
            pass
        else:
            raise e

    except Unauthorized:
        session.delete(reference)
        session.commit()
    except TimedOut:
        # Ignore timeouts during updates for now
        pass
예제 #6
0
def update_poll_messages(session, bot, poll):
    """Logic for handling updates."""
    # Round the current time to the nearest time window
    now = datetime.now()
    time_window = now - timedelta(seconds=now.second % window_size,
                                  microseconds=now.microsecond)
    one_minute_ago = time_window - timedelta(minutes=1)

    # Check whether we have a new window
    current_update = session.query(Update) \
        .filter(Update.poll == poll) \
        .filter(Update.time_window == time_window) \
        .one_or_none()

    updates_in_last_minute = session.query(func.sum(Update.count)) \
        .filter(Update.poll == poll) \
        .filter(Update.time_window >= one_minute_ago) \
        .one()[0]

    if updates_in_last_minute is None:
        updates_in_last_minute = 0

    # No window yet, we need to create it
    if current_update is None:
        try:
            # Create and commit update.
            # This automatically schedules the update and might result in a double
            # update, if the job runs at practically the same time.
            # The worst case scenario is a Message is not modified exception.
            current_update = Update(poll, time_window)
            session.add(current_update)
            session.commit()

            # We are below the flood_limit, just update it
            if updates_in_last_minute <= flood_threshold:
                # Try to send updates
                send_updates(session, bot, poll)

                # If that succeeded, set updated to true and increase count
                # Update inside of mysql to avoid race conditions between threads
                session.query(Update) \
                    .filter(Update.id == current_update.id) \
                    .update({
                        'count': Update.count + 1,
                        'updated': True,
                    })

        except (IntegrityError, UniqueViolation):
            # The update has been already created in another thread
            # Get the update and work with this instance
            session.rollback()
            current_update = session.query(Update) \
                .filter(Update.poll == poll) \
                .filter(Update.time_window == time_window) \
                .one()

    # The update should be updated again
    elif current_update and current_update.updated:
        try:
            # We are still below the flood_threshold, update directrly
            if updates_in_last_minute <= flood_threshold:
                if updates_in_last_minute == flood_threshold:
                    send_updates(session, bot, poll, show_warning=True)
                else:
                    send_updates(session, bot, poll)

                # Update inside of mysql to avoid race conditions between threads
                session.query(Update) \
                    .filter(Update.id == current_update.id) \
                    .update({'count': Update.count + 1})

            # Reschedule the update, the job will increment the count
            else:
                current_update.updated = False
        except Exception as e:
            # Some error occurred during updating of the message.
            # Set the updated flag to False to reschedule the update!
            current_update.updated = False
            # Commit here for now and raise e. Just for temporary debugging and monitoring purposes
            session.commit()
            raise e

    # The next update is already scheduled
    elif current_update and not current_update.updated:
        pass

    session.commit()
예제 #7
0
def update_poll_messages(session, bot, poll):
    """Logic for handling updates."""
    # Round the current time to the nearest time window
    now = datetime.now()
    time_window = now - timedelta(seconds=now.second % window_size,
                                  microseconds=now.microsecond)
    one_minute_ago = time_window - timedelta(minutes=1)

    # Check whether we have a new window
    current_update = session.query(Update) \
        .filter(Update.poll == poll) \
        .filter(Update.time_window == time_window) \
        .one_or_none()

    updates_in_last_minute = session.query(func.sum(Update.count)) \
        .filter(Update.poll == poll) \
        .filter(Update.time_window >= one_minute_ago) \
        .one()[0]

    if updates_in_last_minute is None:
        updates_in_last_minute = 0

    # No window yet, we need to create it
    if current_update is None:
        try:
            update = Update(poll, time_window)
            session.add(update)

            # We are below the flood_limit, just update it
            if updates_in_last_minute <= flood_threshold:
                update.count = 1
                update.updated = True
                # Set the count and updated to true to avoid racing conditions with other threads
                session.commit()
                send_updates(session, bot, poll)

            # We are above the flood limit. Simply commit and thereby schedule the update.
            else:
                session.commit()

        except (IntegrityError, UniqueViolation):
            # The update has been already created in another thread
            # Get the update and work with this instance
            session.rollback()
            current_update = session.query(Update) \
                .filter(Update.time_window == time_window) \
                .one()

    # The update should be updated again
    elif current_update and current_update.updated:
        # We are still below the flood_threshold, update directrly
        if updates_in_last_minute <= flood_threshold:
            if updates_in_last_minute == flood_threshold:
                send_updates(session, bot, poll, show_warning=True)
            else:
                send_updates(session, bot, poll)

            # Update inside of mysql to avoid race conditions between threads
            session.query(Update) \
                .filter(Update.id == current_update.id) \
                .update({'count': Update.count + 1})

        # Reschedule the update, the job will increment the count
        else:
            current_update.updated = False

    # The next update is already scheduled
    elif current_update and not current_update.updated:
        pass