예제 #1
0
    def wrapper(update, context):
        user = None
        if context.user_data.get("ban"):
            return

        temp_ban_time = context.user_data.get("temporary-ban-time")
        if temp_ban_time is not None and temp_ban_time == date.today():
            update.callback_query.answer(i18n.t("callback.spam"))
            return

        session = get_session()
        try:
            user, statistic = get_user(session,
                                       update.callback_query.from_user)
            # Cache ban value, so we don't have to lookup the value in our database
            if user.banned:
                context.user_data["ban"] = True
                return

            # Cache temporary-ban time, so we don't have to create a connection to our database
            if statistic.votes > config["telegram"]["max_user_votes_per_day"]:
                update.callback_query.answer(
                    i18n.t("callback.spam", locale=user.locale))
                context.user_data["temporary-ban-time"] = date.today()
                return

            func(context.bot, update, session, user)

            session.commit()

        except RollbackException as e:
            session.rollback()

            update.callback_query.answer(e.message)

        except Exception as e:
            if not ignore_exception(e):
                if config["logging"]["debug"]:
                    traceback.print_exc()

                with configure_scope() as scope:
                    scope.set_tag("handler", "callback_query")
                    sentry.capture_exception()

                locale = "English"
                if user is not None:
                    locale = user.locale
                update.callback_query.answer(
                    i18n.t("callback.error", locale=locale))

        finally:
            session.close()
예제 #2
0
    def wrapper(context):
        session = get_session()
        try:
            func(context, session)

            session.commit()
        except Exception as e:
            # Capture all exceptions from jobs. We need to handle those inside the jobs
            if not ignore_job_exception(e):
                if config["logging"]["debug"]:
                    traceback.print_exc()

                sentry.capture_exception(tags={"handler": "job"})

        finally:
            session.close()
예제 #3
0
    def wrapper(update, context):
        session = get_session()
        try:
            user, _ = get_user(session, update.chosen_inline_result.from_user)
            if user.banned:
                return

            func(context.bot, update, session, user)

            session.commit()
        except Exception as e:
            if not ignore_exception(e):
                if config["logging"]["debug"]:
                    traceback.print_exc()

                sentry.capture_exception(tags={"handler": "inline_query_result"})

        finally:
            session.close()
예제 #4
0
    def wrapper(update, context):
        session = get_session()
        try:
            user, statistic = get_user(session, update.inline_query.from_user)
            if user.banned:
                return

            func(context.bot, update, session, user)

            session.commit()
        except Exception as e:
            if not ignore_exception(e):
                if config["logging"]["debug"]:
                    traceback.print_exc()

                with configure_scope() as scope:
                    scope.set_tag("handler", "inline_query")
                    sentry.capture_exception()

        finally:
            session.close()
예제 #5
0
        def wrapper(update: Update, context: CallbackContext):
            # The user has been banned and already got a message regarding this issue
            if context.user_data.get("banned-message-sent"):
                return

            user = None
            session = get_session()
            try:
                if hasattr(update, "message") and update.message:
                    message = update.message
                elif hasattr(update, "edited_message") and update.edited_message:
                    message = update.edited_message

                user, _ = get_user(session, message.from_user)

                # Send a message explaining the user, why they cannot use the bot.
                # Also set a flag, which prevents sending this messages multiple times and thereby prevents DOS attacks.
                if user.banned:
                    if not context.user_data.get("banned-message-sent"):
                        context.user_data["banned-message-sent"] = True

                    message.chat.send_message(
                        "You have been permanently banned from using this bot, either due to spamming or inappropriate behavior."
                        "Please refrain from asking questions in the support group or on Github. There's nothing we can do about this."
                    )
                    return

                # Show an error message, if the users uses the bot in a public chat,
                # when he shouldn't. Also check if we're even allowed to send a message.
                if private and message.chat.type != "private":
                    chat = context.bot.getChat(message.chat.id)
                    if chat.permissions.can_send_messages:
                        message.chat.send_message(
                            "Please do this in a direct conversation with me."
                        )
                    return

                response = func(context.bot, update, session, user)

                session.commit()

                # Respond to user
                if response is not None:
                    message.chat.send_message(response)

            except RollbackException as e:
                session.rollback()

                message.chat.send_message(
                    e.message, parse_mode="markdown", disable_web_page_preview=True,
                )

            except Exception as e:
                if not ignore_exception(e):
                    if config["logging"]["debug"]:
                        traceback.print_exc()

                    sentry.capture_exception(
                        tags={"handler": "message",},
                        extra={"update": update.to_dict(), "function": func.__name__},
                    )

                    locale = "English"
                    if user is not None:
                        locale = user.locale

                    message.chat.send_message(
                        i18n.t("misc.error", locale=locale),
                        parse_mode="markdown",
                        disable_web_page_preview=True,
                    )

            finally:
                # The session might not be there yet
                # We're checking for bans inside this try/catch, which has to
                # happen before session initialization due to performance reasons
                if "session" in locals():
                    session.close()
예제 #6
0
    def wrapper(update, context):
        user = None
        if context.user_data.get("ban"):
            return

        try:
            # Check if the user is temporarily banned and send a message.
            # The check is done via the local telegram cache. This way we can prevent
            # opening a new DB connection for each spam request. (lots of performance)
            temp_ban_time = context.user_data.get("temporary-ban-time")
            if temp_ban_time is not None and temp_ban_time == date.today():
                update.callback_query.answer(i18n.t("callback.spam"))
                return

            session = get_session()

            user, statistic = get_user(session, update.callback_query.from_user)
            # Cache ban value, so we don't have to lookup the value in our database on each request
            if user.banned:
                context.user_data["ban"] = True
                return

            # Cache temporary-ban time, so we don't have to create a connection to our database
            if statistic.votes > config["telegram"]["max_user_votes_per_day"]:
                update.callback_query.answer(
                    i18n.t("callback.spam", locale=user.locale)
                )
                context.user_data["temporary-ban-time"] = date.today()
                return

            func(context.bot, update, session, user)

            session.commit()

        except RollbackException as e:
            session.rollback()

            update.callback_query.answer(e.message)

        except Exception as e:
            if not ignore_exception(e):
                if config["logging"]["debug"]:
                    traceback.print_exc()

                sentry.capture_exception(
                    tags={"handler": "callback_query",},
                    extra={"query": update.callback_query,},
                )

                locale = "English"
                if user is not None:
                    locale = user.locale
                try:
                    update.callback_query.answer(
                        i18n.t("callback.error", locale=locale)
                    )
                except BadRequest as e:
                    # Check if this is a simple query timeout exception
                    if not ignore_exception(e):
                        raise e

        finally:
            # The session might not be there yet
            # We're checking for bans inside this try/catch, which has to
            # happen before session initialization due to performance reasons
            if "session" in locals():
                session.close()
예제 #7
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
예제 #8
0
        def wrapper(update: Update, context: CallbackContext):
            user = None
            session = get_session()
            try:
                if hasattr(update, "message") and update.message:
                    message = update.message
                elif hasattr(update,
                             "edited_message") and update.edited_message:
                    message = update.edited_message
                else:
                    with configure_scope() as scope:
                        scope.set_extra("calling_function", func.__name__)
                        scope.set_extra("update", update.to_dict())
                        sentry.capture_message(
                            "Got an update without a message")
                    return

                user, _ = get_user(session, message.from_user)
                if user.banned:
                    return

                if private and message.chat.type != "private":
                    message.chat.send_message(
                        "Please do this in a direct conversation with me.")
                    return

                response = func(context.bot, update, session, user)

                session.commit()

                # Respond to user
                if response is not None:
                    message.chat.send_message(response)

            except RollbackException as e:
                session.rollback()

                message.chat.send_message(
                    e.message,
                    parse_mode="markdown",
                    disable_web_page_preview=True,
                )

            except Exception as e:
                if not ignore_exception(e):
                    if config["logging"]["debug"]:
                        traceback.print_exc()

                    with configure_scope() as scope:
                        scope.set_tag("handler", "message")
                        sentry.capture_exception()

                    locale = "English"
                    if user is not None:
                        locale = user.locale

                    message.chat.send_message(
                        i18n.t("misc.error", locale=locale),
                        parse_mode="markdown",
                        disable_web_page_preview=True,
                    )

            finally:
                session.close()
예제 #9
0
def delete_poll(session, context, poll, remove_all=False):
    """Delete a poll.

    By default, the poll will simply be deleted from the database.
    If the poll wasn't closed yet, it will be closed and all messages will be
    updated once, to indicate that the poll is now closed.

    If remove_all is set to true , ALL poll messages will be removed.
    """
    # In case the poll messages should not be removed,
    # only close the poll and update the message, if necessary.
    if not remove_all:
        if not poll.closed:
            poll.closed = True
            try:
                send_updates(session, context.bot, poll)
            except RetryAfter as e:
                # In case we get an flood control error, wait a little longer
                # than the specified time. Afterwards, just try again.
                retry_after = int(e.retry_after) + 5
                sleep(retry_after)

                send_updates(session, context.bot, poll)

        session.delete(poll)
        return

    for reference in poll.references:
        try:
            # 1. Admin poll management interface
            # 2. User that votes in private chat (priority vote)
            if reference.type in [
                ReferenceType.admin.name,
                ReferenceType.private_vote.name,
            ]:
                context.bot.edit_message_text(
                    i18n.t("deleted.poll", locale=poll.locale),
                    chat_id=reference.user.id,
                    message_id=reference.message_id,
                )

            # Remove message created via inline_message_id
            else:
                context.bot.edit_message_text(
                    i18n.t("deleted.poll", locale=poll.locale),
                    inline_message_id=reference.bot_inline_message_id,
                )

            # Early delete of the reference
            # If something critical, such as a flood control error, happens in the middle
            # of a delete, we don't have to update all previous references again.
            session.delete(reference)
            session.commit()
        except RetryAfter as e:
            # In case we get an flood control error, wait for the specified time
            # Then rollback the transaction, and return. The reference will then be updated
            # the next time the job runs.
            retry_after = int(e.retry_after) + 5
            sleep(retry_after)
            session.rollback()
            return

        except BadRequest as e:
            if (
                e.message.startswith("Message_id_invalid")
                or e.message.startswith("Have no rights to send a message")
                or e.message.startswith("Message is not modified")
                or e.message.startswith("Message to edit not found")
                or e.message.startswith("Message identifier is not specified")
                or e.message.startswith("Chat_write_forbidden")
                or e.message.startswith("Chat not found")
            ):
                pass
            else:
                # Don't die if a single poll fails.
                # Otherwise all other users get stuck as well.
                if should_report_exception(context, e):
                    sentry.capture_exception(tags={"handler": "job"})
                return
        except Unauthorized:
            pass
        except ObjectDeletedError:
            # This reference has already been deleted somewhere else. Just ignore this.
            pass

    session.delete(poll)
    session.commit()