def get_user(session, tg_user): """Get the user from the event.""" user = session.query(User).get(tg_user.id) if user is not None and user.banned: return user, None if user is None: user = User(tg_user.id, tg_user.username) session.add(user) try: session.commit() increase_stat(session, "new_users") # Handle race condition for parallel user addition # Return the user that has already been created # in another session except IntegrityError as e: session.rollback() user = session.query(User).get(tg_user.id) if user is None: raise e if tg_user.username is not None: user.username = tg_user.username.lower() name = get_name_from_tg_user(tg_user) user.name = name # Ensure user statistics exist for this user # We need to track at least some user activity, since there seem to be some users which # abuse the bot by creating polls and spamming up to 1 million votes per day. # # I really hate doing this, but I don't see another way to prevent DOS attacks # without tracking at least some numbers. user_statistic = session.query(UserStatistic).get((date.today(), user.id)) if user_statistic is None: user_statistic = UserStatistic(user) session.add(user_statistic) try: session.commit() # Handle race condition for parallel user statistic creation # Return the statistic that has already been created in another session except IntegrityError as e: session.rollback() user_statistic = session.query(UserStatistic).get( (date.today(), user.id)) if user_statistic is None: raise e return user, user_statistic
def handle_async_callback_query(bot, update, session, user): """Handle asynchronous callback queries. Most callback queries are unproblematic in terms of causing race-conditions. Thereby they can be handled asynchronously. However, we do handle votes asynchronously as an edge-case, since we want those calls to be handled as fast as possible. The race condition handling for votes is handled in the respective `handle_vote` function. """ context = get_context(bot, update, session, user) # Vote logic needs some special handling if context.callback_type == CallbackType.vote: option = session.query(Option).get(context.payload) if option is None: return poll = option.poll # Ensure user statistics exist for this poll owner # We need to track at least some user activity, since there seem to be some users which # abuse the bot by creating polls and spamming up to 1 million votes per day. # # I really hate doing this, but I don't see another way to prevent DOS attacks # without tracking at least some numbers. user_statistic = session.query(UserStatistic).get( (date.today(), poll.user.id)) if user_statistic is None: user_statistic = UserStatistic(poll.user) session.add(user_statistic) try: session.commit() # Handle race condition for parallel user statistic creation # Return the statistic that has already been created in another session except IntegrityError as e: session.rollback() user_statistic = session.query(UserStatistic).get( (date.today(), poll.user.id)) if user_statistic is None: raise e # Increase stats before we do the voting logic # Otherwise the user might dos the bot by triggering flood exceptions # before actually being able to increase the stats increase_user_stat(session, context.user, "votes") increase_user_stat(session, poll.user, "poll_callback_calls") session.commit() response = handle_vote(session, context, option) else: increase_user_stat(session, context.user, "callback_calls") session.commit() response = async_callback_mapping[context.callback_type](session, context) # Callback handler functions always return the callback answer # The only exception is the vote function, which is way too complicated and # implements its own callback query answer logic. if response is not None and context.callback_type != CallbackType.vote: context.query.answer(response) else: context.query.answer("") increase_stat(session, "callback_calls") return