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()
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()
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()
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()
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()
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()
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
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()
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()