Skip to content

LOG: Romutchio

Romutchio edited this page Jun 10, 2019 · 1 revision

Plan

  1. Постановка задачи / проблема на весь проект. (Может быть общая у всех из команды)
  2. Постановка своей собственной задачи (индивидуально).
  3. Описание архитектуры решения (может быть общая у всей команды).
  4. Подробное описание своего участка работы.
  5. Скриншоты или что-то подобное, что демонстрирует, как вся эта штуковина работает в сборе. (общая для всей команды).
  6. Литература - может быть общая, может быть нет.

Если у вас вдруг не было четкого разделения на области ответственности, то разделите постфактум, кто про что пишет. Общие части можете вместе написать. В отчете явно укажите, что делали проект вместе, а в п.2 и 4 явно укажите, что это > > ваша индивидуальная часть в команде.

1. Проблема

  1. В обучении есть темы, которые нужно закреплять практикой. Каждому студенту требуется свое количество практики. Одна из таких тем - это оценка сложности алгоритмов.
  2. Студенты не всегда готовы выделить время за компьютером для дополнительной практики, но готовы тренироваться по несколько минут в день на телефоне, в моменты, когда есть свободное время без доступа к полноценному компьютеру. Сейчас они тратят это время на соц-сети, а могут тратить на обучение.

Цель проекта

  1. Сделать бота для телеграмма, который позволит тренироваться в оценке сложности алгоритмов в свободное время прямо на своем телефоне. Для этого разработать механику, плавного увеличения сложности задач, с учетом успеваемости пользователя. Задания должны быть каждый раз уникальными и генерироваться на лету, чтобы исключить возможность запоминания уже увиденных вариантов.
  2. Обобщить бота так, чтобы его можно было использовать для других тем, отличных от оценки сложности алгоритмов.
  3. Обобщить код так, чтобы незначительными доработками можно было сделать его доступным через другие платформы для чат-ботов.

2. Описание архитектуры решения

Грамотное создание диалогов -- весьма нетривиальная задача. Архитектурным решением данной задачи является конечный трансдьюсер.

Конечный трансдьюссер - шестерка (Q, Σ, ∆, δ, q0, F), где:

  • Q — конечное множество состояний (в данном проекте - Сущность State),
  • Σ —конечное множество входных символов (сообщения пользователя, колбеки кнопок),
  • ∆ —конечное множество выходных символов (заготовленные ответы бота),
  • δ ⊂ Q×Σ∗×∆∗×Q —конечное множество переходов (сущность Transition),
  • q0 ∈ Q —начальное состояние,
  • F ⊂ Q —множество заключительных состояний.

Ниже приведен автомат, визуализирующий данный трансдьюссер.

FSM

Общаться с серверами Telegram бот может двумя способами:

  • getUpdates - pull: ваш бот постоянно дёргает сервер Telegram и проверяет есть ли новые сообщения;
  • setWebhook - push: по мере поступления новых сообщений сервер Telegram отправляет их вашему боту. Разницу можно изобразить следующим образом:

    Очевидно, что второй способ setWebhook рациональнее для всех участников процесса.

3. Постановка задачи

Наш проект довольно объемный, уже на данный момент написана большая кодовая база. Моя основная задача в данном проекте - создание слоя визуальной части сервиса - Telegram бот.

4. Решение.

Механика

  • Все задания разбиты на уровни. Уровень определяет тему заданий. Чтобы пройти уровень, нужно заполнить прогрессбар уровня до конца. Level 1️⃣. Progress: 🔘🔘🔘🔘⚫️
  • Внутри уровня много генераторов. Каждый генератор объявляет сколько ячеек прогресс бара он добавляет. Корректный ответ — +1, Ошибка — -1, но только если правильных ответов на задачи этого генератора больше, чем неправильных.
  • Прохождение уровня может открывать возможность прохождения других уровней.
  • В любой момент можно перейти на любой из открытых уровней и продолжить упражняться там.
  • При неудачном ответе можно запросить объяснения.

На данном этапе существуют такие ключевые сущности:

  • State - Состояние - тот же смысл, что и в Теории Автоматов
  • Transition - Переход
  • Command - Команда - действие, совершаемое при осуществлении перехода в следующее состояние. Ниже представлен граф наследования, демонстрирующий существующие Состояния, Переходы и Команды:

Сердцем телеграм бота служит StateMachine, в котором есть метод GetNextState:

 public interface IStateMachine<TCommand>
    {
        (State state, TCommand command) GetNextState<TState, TTransition>(TState currentState, TTransition transition)
            where TState : State where TTransition : Transition;
    }

Данный метод принимает текущее состояние и переход, по которому мы в него попали. На основе этого, StateMachine выдает новое состояние в ДКА.

public (State state, ICommand command) GetNextState<TState, TTransition>(
            TState currentState,
            TTransition transition) where TState : State where TTransition : Transition
        {
            switch ((state: currentState, transition: transition))
            {
                case var t when t.transition is HelpTransition:
                    return (new TopicSelectionState(), new SelectTopicCommand());
                case var t when t.transition is FeedbackTransition:
                    return (new TopicSelectionState(), new FeedBackCommand());
                case var t when t.state is UnknownUserState:
                    return (new TopicSelectionState(), new SelectTopicCommand());
                case var t when t.state is ReportState reportState:
                    return ProcessReportState(reportState, t.transition);
                case var t when t.state is TopicSelectionState topicSelectionState:
                    return ProcessTopicSelectionState(topicSelectionState, t.transition);
                case var t when t.state is LevelSelectionState levelSelectionState:
                    return ProcessLevelSelectionState(levelSelectionState, t.transition);
                case var t when t.state is TaskState taskState:
                    return ProcessTaskState(taskState, t.transition);
            }
            return default;
        }

MVC Контроллер, принимающий обновления с Telegram по URI: api/update

[Route("api/[controller]")]
    public class UpdateController : Controller
    {
        // POST api/update
        [HttpPost]
        public async Task<IActionResult> Post([FromBody] Update update)
        {
            Chat chat = null;
            switch (update.Type)
            {
                case UpdateType.Message:
                    chat = update.Message.Chat;
                    break;
                case UpdateType.CallbackQuery:
                    chat = update.CallbackQuery.Message.Chat;
                    await botService.Client.AnswerCallbackQueryAsync(update.CallbackQuery.Id);
                    break;
                default:
                    return Ok();
            }
            var userCommand = updateService.ProcessMessage(update);
            var serviceManager = new ServiceManager(quizService, userRepository, logger);
            await userCommand.ExecuteAsync(chat, botService.Client, serviceManager);
            return Ok();
        }
    }

Было принято решение писать бота на основе WebHook, поэтому по мере поступления новых сообщений сервер Telegram отправляет их нашему боту.

public interface IUpdateService
    {
        ICommand ProcessMessage(Update update);
    }

 public ICommand ProcessMessage(Update update)
        {       
            var userEntity = userRepository.FindByTelegramId(userId) ??
                            userRepository.Insert(new UserEntity(new UnknownUserState(), userId, Guid.NewGuid(), 0));
            var state = userEntity.CurrentState;
            var transition = parser.Parse(state, update, quizService, logger);
            var (currentState, currentCommand) = stateMachine.GetNextState(state, transition);         
            userRepository.Update(new UserEntity(currentState, userId, userEntity.Id, userEntity.MessageId));
            return currentCommand;
        }

Поскольку пользователей телеграм необходимо где-то хранить, была выбрана база данных MongoDB. Для удобного использования, был написан репозиторий, реализующий работу с ней:

using System;
using MongoDB.Driver;

namespace QuizBotCore.Database
{
    public class MongoUserRepository : IUserRepository
    {
        public const string CollectionName = "telegramUsers";

        private readonly IMongoCollection<UserEntity> userCollection;

        public MongoUserRepository(IMongoDatabase database)
        {
            userCollection = database.GetCollection<UserEntity>(CollectionName);
        }

        public UserEntity Insert(UserEntity user)
        {
            userCollection.InsertOne(user);
            return user;
        }

        public UserEntity FindById(Guid id)
        {
            return userCollection.Find(u => u.Id == id).SingleOrDefault();
        }

        public UserEntity FindByTelegramId(long telegramId)
        {
            return userCollection.Find(u => u.TelegramId == telegramId).SingleOrDefault();
        }

        public void Update(UserEntity user)
        {
            userCollection.ReplaceOne(u => u.Id == user.Id, user);
        }

        public void Delete(Guid id)
        {
            userCollection.DeleteOne(u => u.Id == id);
        }

        public UserEntity UpdateOrInsert(UserEntity user)
        {
            userCollection.ReplaceOne(u => u.Id == user.Id, user, new UpdateOptions {IsUpsert = true});
            return user;
        }
    }
}

5. Скриншоты, демонстрирующие работу.

  • Приветственное сообщение
  • Обратная связь (вызов посредством команды /feedback)
  • Выбор уровня
  • Отображение задачи
  • Сообщение после прохождение уровня
  • Пользователь нажал на кнопку "Пожаловаться на задачу" и сообщил об ошибке:
  • После репорта, сообщение прилетает в специальный чат в таком виде:

6. Литература:

http://kadm.imkn.urfu.ru/files/shurzam.pdf
https://telegrambots.github.io/book/

Clone this wiki locally