Exemplo n.º 1
0
def create_admin_reply_markup(suggestion: Suggestion):
    """
    Создает кнопки для сообщения предложки

    :return: Кнопки
    """
    markup = InlineKeyboardMarkup()
    # В каждой кноке сохраняем информацию о действии, запоминаем идентификатор пользователя отправителя
    # и идентификатор сообщения отправителя в чате с ботом
    markup.add(
        InlineKeyboardButton(t("app.admin.suggestion.button.accept"),
                             callback_data=json.dumps({
                                 'a': ACTION_ACCEPT,
                                 's': suggestion.id,
                             })))
    markup.add(
        InlineKeyboardButton(t("app.admin.suggestion.button.accept_with_poll"),
                             callback_data=json.dumps({
                                 'a': ACTION_ACCEPT_WITH_POLL,
                                 's': suggestion.id,
                             })))
    markup.add(
        InlineKeyboardButton(t("app.admin.suggestion.button.decline"),
                             callback_data=json.dumps({
                                 'a': ACTION_DECLINE,
                                 's': suggestion.id,
                             })))
    return markup
Exemplo n.º 2
0
def on_cancel_command(message: TelegramMessage, session: Session = None):
    chat = repo.chat_get_by_telegram_id(message.chat.id)
    if chat.state == Chat.STATE_WAIT_TITLE:
        data = chat.get_state_data()
        if "voice_id" not in data or data["voice_id"] is None:
            raise Exception(t("app.error.state_wait_title.no_voice_id"))
        voice_id = data["voice_id"]
        voice = repo.get_voice_by_id(voice_id)
        session.delete(voice)
        chat.clear_state()
        bot.send_message(message.chat.id, t("app.message.operation_cancelled"))
Exemplo n.º 3
0
def callback_handler(call: CallbackQuery, session=None):
    """
    Обработка нажатий на кнопках опроса

    :param call:
    :param session:
    """
    callback_data = json.loads(call.data)
    callback_action = callback_data['a']
    if callback_action == ACTION_VOTE:
        # Сценарии голосования:
        # 1) Первое нажание на кнопку любую кнопку - добавляем голос
        # 2) Повтороное нажание на кнопку, по которой уже отдан голос - снимаем голос
        # 3) Нажание на другую кнопку, отличную от той по которой отдан лолос - снимаем голос, добавляем новый
        user_id = call.from_user.id
        callback_poll_id = callback_data['p']
        callback_option_id = callback_data['o']
        option = repo.get_option(callback_option_id)
        if option is None:
            # Ошибка: вариант ответа опроса не найден
            bot.answer_callback_query(call.id, t("app.poll.vote.error"))
            return
        vote = repo.get_vote(callback_poll_id, user_id)
        poll = option.poll
        if vote is None:
            # Пользователь сделал первый голос
            repo.add_vote(poll.id, callback_option_id, user_id)
            bot.answer_callback_query(
                call.id, t("app.poll.vote.voted", emoji=option.text))
            rerender_post_votes(poll.id)
            return
        elif vote.option.id == option.id:
            # Пользователь повторно нажал на кнопку - голос отменен
            repo.clear_vote(poll.id, user_id)
            bot.answer_callback_query(
                call.id, t("app.poll.vote.canceled", emoji=option.text))
            rerender_post_votes(poll.id)
            return
        elif vote.option.id != option.id:
            # Пользователь проголосовал за другой вариант
            repo.clear_vote(poll.id, user_id)
            repo.add_vote(poll.id, option.id, user_id)
            bot.answer_callback_query(
                call.id, t("app.poll.vote.voted", emoji=option.text))
            rerender_post_votes(poll.id)
            return
        else:
            # Ошибка: что-то пошло не так
            bot.answer_callback_query(call.id, t("app.poll.vote.error"))
            return
        pass
Exemplo n.º 4
0
def render_decision(decision: str) -> str:
    """
    Создает отображение решениения с предложкой

    :param decision: Решение (см. константы)
    :return:
    """
    if decision == DECISION_ACCEPT:
        return t("app.admin.decision.accepted.rich")
    elif decision == DECISION_ACCEPT_WITH_POLL:
        return t("app.admin.decision.accepted_with_poll.rich")
    elif decision == DECISION_DECLINE:
        return t("app.admin.decision.declined.rich")
    else:
        raise Exception("Unknown decision")
Exemplo n.º 5
0
def catch_any_message(message: TelebotMessage, session=None):
    """
    Сообщение на случай, если отправлен неподдерживаемый контент

    :param message:
    """
    bot.send_message(message.chat.id, t("app.bot.user.wrong_content"))
Exemplo n.º 6
0
def on_text(message: TelegramMessage, session=None):
    """
    Бот получает текст.

    Сценарии:

    1) Пользователь записывает новый войс в чате с ботом. Бот уже получил войс и предложил
    пользователю ввести название войса. При этом состояние чата `STATE_WAIT_TITLE`. После
    получения фразы, бот записывает название в войс и отправляет сообщение, что войс
    успешно добавлен

    :param message: Сообщение телеграм
    :param session: Сессия БД
    :return:
    """
    chat = repo.chat_get_by_telegram_id(message.chat.id)
    if chat.state == Chat.STATE_WAIT_TITLE:
        state_data = chat.get_state_data()
        if state_data is None or "voice_id" not in state_data or state_data[
                "voice_id"] is None:
            return
        if len(message.text) == 0:
            return
        voice_id = state_data["voice_id"]
        voice = repo.get_voice_by_id(voice_id)
        voice.title = message.text
        voice.search_title = utils.create_search_title(voice.title)
        # Помечаем войс как активный
        voice.status = Voice.STATUS_ACTIVE
        chat.clear_state()
        bot.send_message(message.chat.id,
                         t("app.message.voice_successfully_saved"))
Exemplo n.º 7
0
def catch_cancel_command(message: TelebotMessage, session=None):
    """
    Обработка команды Отмена (`\\\\cancel`)

    Сценарии:
     1) Бот ждет от админа список эмодзи для опроса - Отменяем состояние
     и возвращаем предложке кнопки выбора решения

    """
    admin_id = get_admin_id()
    # Принимаем команду только в чате админа
    if message.chat.id != admin_id:
        return
    admin_state = repo.get_admin_state()
    # Если состояние чата админа пустое, ничего не делаем
    if admin_state is None:
        return
    # Если состояние чата - ожидает кнопки, то отменяем это состояние
    # и возвращаем предложке кнопки выбора решения
    if admin_state['state'] == AdminState.STATE_WAIT_BUTTONS:
        suggestion = repo.get_suggestion(admin_state['data']['suggestion_id'])
        if suggestion is None:
            raise Exception("Suggestion not found")
        repo.clear_admin_state()
        reset_suggestion(suggestion)
        # Отправляем сообщение, что операция отменена и удаляем клавиатуру, если есть
        bot.send_message(admin_id,
                         t("app.bot.admin.cancel"),
                         reply_markup=ReplyKeyboardRemove())
Exemplo n.º 8
0
def catch_photo(message: TelebotMessage, session=None):
    # Проверяем, что сообщение содержит изображение
    if len(message.photo) == 0:
        bot.send_message(message.chat.id, t("app.bot.user.wrong_content"))
        return
    # Создаем новую предложку
    suggestion = Suggestion()
    suggestion.state = Suggestion.STATE_NEW
    # Используем последнее фото с конца (наибольшее разрешение)
    suggestion.file_id = message.photo[-1].file_id
    # Сохраняем отправителя
    suggestion.user_title = get_full_name(message.from_user)
    suggestion.user_id = message.from_user.id
    suggestion.user_username = message.from_user.username
    # Сохраняем идентификатор сообщения в чате пользователя с ботом - понадобится чтобы бот
    # реплайнул на сообщение если его опубликуют
    suggestion.user_message_id = message.message_id
    # Проверяем, пользователь создал сообщение сам или переслал его
    if message.forward_from is not None or message.forward_from_chat is not None:
        if message.forward_from_chat is None:
            # Переслано от пользователя
            suggestion.forwarded_from_id = message.forward_from.id
            suggestion.forwarded_from_username = message.forward_from.username
            suggestion.forwarded_from_title = get_full_name(
                message.forward_from)
        else:
            # Переслано из канала
            suggestion.forwarded_from_id = message.forward_from_chat.id
            suggestion.forwarded_from_username = message.forward_from_chat.username
            suggestion.forwarded_from_title = message.forward_from_chat.title
    # Сохраняем предложку в базе
    session.add(suggestion)
    session.flush()
    # Создаем текст и кнопки для предложки
    admin_message = render_suggestion_text(suggestion)
    markup = create_admin_reply_markup(suggestion)
    # Отправляем предложку админу
    suggestion_message = bot.send_photo(config.APP_BOT_ADMIN_ID,
                                        suggestion.file_id,
                                        admin_message,
                                        reply_markup=markup,
                                        parse_mode="HTML")
    # Сохраняем идентификатор сообщения с предложкой
    suggestion.admin_message_id = suggestion_message.message_id
    # Отправляем пользователю сообщение о том, что его предложка отправлена
    bot.send_message(message.chat.id, t("app.bot.user.posted"))
Exemplo n.º 9
0
def unpublish_from_channel(voice):
    if voice.post_message_id is None:
        raise Exception(t("app.error.channel.voice_not_published"))
    channel = get_channel()
    bot.delete_message(channel.id, voice.post_message_id)
    bot.delete_message(channel.id, voice.title_message_id)
    voice.post_message_id = None
    voice.title_message_id = None
Exemplo n.º 10
0
def on_voice(message: TelegramMessage, session=None):
    """
    Бот получает войс.

    Сценарии:

    1) Пользователь записывает новый войс в чате с ботом. При этом
    состояние чата должно быть пустым. После получения войса, бот
    сохраняет войс в базе и предлагает пользователю отправить название
    для войса.

    :param message: Сообщение телеграм с войсом
    :param session: Сессия БД
    :return:
    """
    chat = repo.chat_get_by_telegram_id(message.chat.id)
    if chat.state is None:
        # Состояние чата пусто, сохраняем войс как новый
        file_id = message.voice.file_id
        # Проверяем был ли войс добавлен ранее. Уникальность войса определяется по file_id
        # TODO проверять уникальность войса по file_id и id пользователя, чтобы
        # можно было добавлять один и тот же войс разным пользователями без публикации
        check_voice = repo.get_voice_by_file_id(file_id)
        if check_voice is not None:
            bot.send_message(message.chat.id,
                             t("app.error.voice.voice_already_exists"))
            return
        voice = Voice()
        voice.file_id = file_id
        voice.sender = chat
        author_id = deduct_voice_author(message)
        author = bot.get_chat(author_id)
        voice.author_id = author_id
        # Если у автора сообщения нет ни имени ни фамилии, то
        # считаем его пересланным от канала или бота - в этом
        # случае просто игнорируем войс
        if author.first_name is None and author.last_name is None:
            return
        voice.author_first_name = author.first_name
        voice.author_last_name = author.last_name
        # Устанавливаем состояние войса в новый, чтобы он пока был недоступен в поиске
        voice.status = Voice.STATUS_NEW
        session.add(voice)
        session.flush()
        chat.set_state_wait_title(voice.id)
        bot.send_message(message.chat.id, t("app.message.send_title"))
Exemplo n.º 11
0
def render_post_url(channel_message_id: int):
    """
    Создает отображение ссылки на пост

    :param channel_message_id: Идентификатор поста в канале
    """
    post_url = generate_post_link(channel_message_id)
    post_link_caption = t("app.admin.suggestion.post_url", url=post_url)
    return post_link_caption
Exemplo n.º 12
0
def render_suggestion_text(suggestion: Suggestion):
    """
    Создает текст предложки

    :param suggestion: Предложка
    :return: Тест предложки
    """
    # Основа: заголовок и отправитель
    text = t("app.admin.suggestion.head")
    text += "\n\n"
    # Проверить будет ли работать ссылка на пользователя без юзернейма
    # if not suggestion.user_is_public():
    #     # Отправитель не имеет юзернейма
    #     text += messages.SUGGESTION_FROM % {
    #         "user_title": suggestion.user_title
    #     }
    # else:
    # # Отправитель публичен
    text += t("app.admin.suggestion.from.url",
              user_id=suggestion.user_id,
              user_title=suggestion.user_title)
    # Если отправитель переслал пост из другого канала или от другого пользователя,
    # добавляем информацию об этом в предложку
    if suggestion.is_forward():
        text += "\n"
        if not suggestion.forwarded_is_public():
            # Автор исходного поста не имеет юзернейма
            text += t("app.admin.suggestion.forwarded_from.plain",
                      forwarded_user_title=suggestion.forwarded_from_title)
        else:
            # Автор исходного поста публичен
            text += t("app.admin.suggestion.forwarded_from.url",
                      forwarded_user_id=suggestion.forwarded_from_username,
                      forwarded_user_title=suggestion.forwarded_from_title)
    # Если по предложке уже есть решение, то отображаем его
    if suggestion.decision is not None:
        text += "\n\n"
        text += render_decision(suggestion.decision)
        # Если есть ссылка на пост, то добавляем ее
        # Ссылка на пост может быть только если есть решение
        if suggestion.channel_post_id is not None:
            text += "\n"
            text += render_post_url(suggestion.channel_post_id)
    return text
Exemplo n.º 13
0
def answer_callback_decision(call: CallbackQuery, decision: str):
    """
    Отвечает на сообщение-колбек клика по кнопке админу

    Отправляется при нажатии по кнопке Запостить и Отклонить.

    :param call:
    :param decision: Решение
    """
    if decision == DECISION_DECLINE:
        resolution_text = t("app.admin.decision.declined.plain")
    elif decision == DECISION_ACCEPT:
        resolution_text = t("app.admin.decision.accepted.plain")
    elif decision == DECISION_ACCEPT_WITH_POLL:
        resolution_text = t(
            "app.admin.decision.accepted_with_poll.callback_answer")
    else:
        raise Exception("Ошибка выбора решения")
    bot.answer_callback_query(call.id, resolution_text)
Exemplo n.º 14
0
def send_help(message: TelebotMessage, session=None):
    """
    Отправка помощи. Отправляется на команды `\\\\start` и `\\\\help`
    """
    channel = get_channel()
    bot.send_message(message.chat.id,
                     t("app.bot.message.start",
                       channel_title=channel.title,
                       channel_username=channel.username),
                     parse_mode="HTML")
Exemplo n.º 15
0
def catch_text_message(message: TelebotMessage, session=None):
    admin_id = get_admin_id()
    if message.chat.id != admin_id:
        bot.send_message(message.chat.id, t("app.bot.user.wrong_content"))
    admin_state = repo.get_admin_state()
    if admin_state is None:
        # Когда админ в пустом состоянии считаем его обычным пользователем
        bot.send_message(message.chat.id, t("app.bot.user.wrong_content"))
        return
    if admin_state['state'] == AdminState.STATE_WAIT_BUTTONS:
        suggestion = repo.get_suggestion(admin_state['data']['suggestion_id'])
        if suggestion is None:
            raise Exception("Suggestion not found")
        # Находим эмодзи в тексте
        emoji_chars = utils.find_emoji_in_text(message.text)
        # Не было найдено ниодного или более 6 эмодзи
        if len(emoji_chars) > 6 or len(emoji_chars) == 0:
            bot.send_message(message.chat.id,
                             t("app.bot.admin.error.poll.emoji_restrictions"))
            return
        # Создаем опрос в БД
        poll = repo.create_poll(emoji_chars)
        poll_markup = create_post_votes_markup(poll.id)
        # Публикуем пост
        channel_post = publish_post(suggestion.file_id, poll_markup)
        # Записываем в опрос идентификатор сообщения в канале
        poll.message_id = channel_post.message_id
        # Очищаем состояние админа
        repo.clear_admin_state()
        # Отправляем админу отбивку, что пост опубликован + очищаем клавиатуру (предложенные наборы эмодзи)
        bot.send_message(admin_id,
                         t("app.bot.admin.poll_posted"),
                         reply_to_message_id=suggestion.admin_message_id,
                         reply_markup=ReplyKeyboardRemove())
        # Обновляем предложку
        suggestion.decision = DECISION_ACCEPT_WITH_POLL
        suggestion.channel_post_id = channel_post.message_id
        rerender_suggestion(suggestion)
        # Отправляем пользователю информацию, что пост опубликован
        notify_user_about_publish(suggestion)
        # После вынесения решения удаляем предложку из базы
        session.delete(suggestion)
Exemplo n.º 16
0
def get_channel():
    """
    Получить канал с топ войсами

    :return: Канал с топ войсами
    :rtype: TelegramChat
    """
    channel = bot.get_chat(config.CHANNEL_ID)
    if channel is None:
        raise Exception(t("app.error.channel.not_found"))
    return channel
Exemplo n.º 17
0
def on_publish_command(message: TelegramMessage, session: Session = None):
    chat = repo.chat_get_by_telegram_id(message.chat.id)
    if chat.state is None:
        if message.reply_to_message is None:
            return
        if message.reply_to_message.voice is None:
            return
        file_id = message.reply_to_message.voice.file_id
        voice = repo.get_voice_by_file_id(file_id)
        if voice is None or not voice.is_active():
            bot.send_message(message.chat.id,
                             t("app.error.voice.voice_not_found"))
            return
        if not voice.can_edit(chat):
            bot.send_message(message.chat.id, t("app.error.voice.no_access"))
            return
        if voice.is_public:
            bot.send_message(message.chat.id,
                             t("app.error.voice.already_published"))
            return
        voice.is_public = True
        publish_to_channel(voice)
        bot.send_message(message.chat.id, t("app.message.voice_published"))
Exemplo n.º 18
0
def notify_user_about_publish(suggestion: Suggestion):
    """
    Отправляет пользователю уведомление о том, что его предложка опубликована

    :param suggestion:
    :return:
    """
    # Выключаем превью ссылок, чтобы не прикрепилось превью поста
    post_url = generate_post_link(suggestion.channel_post_id)
    bot.send_message(suggestion.user_id,
                     t("app.bot.user.published", post_url=post_url),
                     reply_to_message_id=suggestion.user_message_id,
                     parse_mode="HTML",
                     disable_web_page_preview=True)
Exemplo n.º 19
0
def publish_post(file_id: str,
                 reply_markup: InlineKeyboardMarkup = None) -> TelebotMessage:
    """
    Публикует пост в канале

    :param file_id: Идентификатор файла
    :param reply_markup: Кнопки опроса или None если не нужны
    :return: Пост
    """
    me = bot.get_me()
    channel_post = bot.send_photo(config.APP_CHANNEL_ID,
                                  file_id,
                                  caption=t("app.bot.sign",
                                            bot_username=me.username),
                                  parse_mode="HTML",
                                  reply_markup=reply_markup)
    return channel_post
Exemplo n.º 20
0
def on_help_command(message: TelegramMessage, session=None):
    chat = repo.chat_get_by_telegram_id(message.chat.id)
    bot.send_message(chat.telegram_chat_id,
                     t("app.message.help"),
                     parse_mode="Markdown")
Exemplo n.º 21
0
def call_on_admin_suggestion(call: CallbackQuery, session=None):
    """
    Обработка действий нажатия на кнопки предложки у админа

    :param call:
    :param session:
    """
    callback_data = json.loads(call.data)
    callback_action = callback_data['a']
    if callback_action == ACTION_DECLINE:
        callback_suggestion_id = callback_data['s']
        suggestion = repo.get_suggestion(callback_suggestion_id)
        if suggestion is None:
            raise Exception("Suggestion not found")
        if not suggestion.is_new():
            # Для это действия предложка должна быть только что опубликованной
            bot.answer_callback_query(
                call.id, t("bot.admin.error.decision.wrong_state"))
            return
        # Обновляем сообщение предложки и удаляем кнопки
        suggestion.decision = DECISION_DECLINE
        rerender_suggestion(suggestion)
        # Отображаем плашку с отменой
        answer_callback_decision(call, DECISION_DECLINE)
        # После вынесения решения удаляем предложку из базы
        session.delete(suggestion)
        return
    elif callback_action == ACTION_ACCEPT:
        callback_suggestion_id = callback_data['s']
        suggestion = repo.get_suggestion(callback_suggestion_id)
        if suggestion is None:
            raise Exception("Suggestion not found")
        if not suggestion.is_new():
            # Для это действия предложка должна быть только что опубликованной
            return
        # Отображаем плашку с ответом
        answer_callback_decision(call, DECISION_ACCEPT)
        # Публикуем пост в канале
        channel_post = publish_post(suggestion.file_id)
        # Обновляем предложку
        suggestion.decision = DECISION_ACCEPT
        suggestion.channel_post_id = channel_post.message_id
        # Обновляем сообщение предложки и удаляем кнопки
        rerender_suggestion(suggestion)
        # Отправляем пользователю информацию, что пост одобрен
        notify_user_about_publish(suggestion)
        # После вынесения решения удаляем предложку из базы
        session.delete(suggestion)
    elif callback_action == ACTION_ACCEPT_WITH_POLL:
        callback_suggestion_id = callback_data['s']
        suggestion = repo.get_suggestion(callback_suggestion_id)
        if suggestion is None:
            raise Exception("Suggestion not found")
        # Сохраняем состояние чата админа с ботом
        admin_state = repo.get_admin_state()
        if admin_state is not None and admin_state[
                'state'] == AdminState.STATE_WAIT_BUTTONS:
            # Если другое сообщение уже ожидает эмодзи от пользователя, то возобновляем
            # кнопки в том сообщении
            other_suggestion = repo.get_suggestion(
                admin_state['data']['suggestion_id'])
            if other_suggestion is None:
                raise Exception("Suggestion not found")
            reset_suggestion(other_suggestion)
            admin_id = get_admin_id()
            # Отправляем сообщение о том, что предыдущая операция отменена + удаляем кнопки если есть
            # TODO попробовать совместить с /cancel
            bot.send_message(
                admin_id,
                t("app.bot.admin.previous_action_canceled"),
                reply_to_message_id=other_suggestion.admin_message_id,
                reply_markup=ReplyKeyboardRemove())
        repo.set_admin_state(AdminState.STATE_WAIT_BUTTONS,
                             {"suggestion_id": suggestion.id})
        # Показываем плашку о том, что решение принято
        answer_callback_decision(call, DECISION_ACCEPT_WITH_POLL)
        # Удаляем кнопки
        rerender_suggestion(suggestion)
        # Отправляем сообщение о том, что ждем эмодзи
        suggested_emoji_set_markup = ReplyKeyboardMarkup()
        # К сообщению прикрепляем последние 5 использованных уникальных наборов эмодзи
        previous_emoji_sets = repo.get_previous_emoji_sets()
        for emoji_set in previous_emoji_sets:
            suggested_emoji_set_markup.add(KeyboardButton(emoji_set))
        bot.send_message(call.message.chat.id,
                         t("app.bot.admin.wait_buttons"),
                         reply_to_message_id=suggestion.admin_message_id,
                         reply_markup=suggested_emoji_set_markup
                         if len(previous_emoji_sets) > 0 else None)
        # Устанавливаем состояние предложки в состояние ожидания
        suggestion.state = Suggestion.STATE_WAIT