KTB-22 Реализовать многопользовательский режим¶
📝 Описание¶
В процессе разработки бота возникла проблема, связанная с этапом сбора требований. Заказчик не уточнил, что бот должен хранить данные индивидуально для каждого пользователя, посчитав это очевидным. В результате разработчик интерпретировал задачу по-своему и реализовал функциональность без учета персональных данных пользователей.
Такое недопонимание может привести к существенному увеличению затрат на разработку проекта и дополнительным временным издержкам.
Реализация индивидуального хранения статистики
Необходимо добавить поддержку многопользовательского режима, чтобы каждый пользователь имел свою собственную статистику и прогресс в обучении. Кроме того, следует реализовать возможность сброса статистики для каждого пользователя отдельно, предоставив им контроль над своими данными и прогрессом.
🗒 Задание¶
- Реализовать многопользовательский режим.
- Добавить возможность сброса статистики для каждого пользователя.
❌ Ошибки¶
✏️ Комментарии¶
Если для методов с большим количеством параметров пишут их в одну строку. Например:
fun handleUpdate( update: Update, telegramBotService: TelegramBotService, trainers: HashMap<Long, LearnWordsTrainer>) {
// ...
Рекомендуем:
Давай если больше одного параметра будем ставить переносы. Так структурнее и глазу проще ориентироваться:
fun handleUpdate(
update: Update,
telegramBotService: TelegramBotService,
trainers: HashMap<Long, LearnWordsTrainer>,
) {
Еще часто бывают споры насчет “концевой запятой” (это та которая после последнего параметра), я рекомендую ее ставить.
Редко, но бывает что handleUpdate() размещают внутри main() :
В целом, в продакшене крайне редко используют локальные функции, они затрудняют чтения кода основной функции (в которой используются).
Давай вынесем ее за пределы `main()`
Иногда не понимают сразу и создают LearnWordsTrainer() не передав туда новое имя файла. Например так:
Рекомендуем:
Тут ты не привязываешь `LearnWordsTrainer()` к конкретному пользователю. Прогресс прохождения один на всех пользователей. Убедится можешь создав в Телеграмме группу и добавить в нее бота. Вроде бы в видео говорится про имя файла и chatId
Иногда в репозиторий попадают файлы словарей, которые генерируются для чатов (имя например 1234512345.txt) , а не только “эталонный” словарь words.txt.
Это "рабочие файлы". То есть, появляются в ходе работы бота для конкретных чатов. Их не стоит класть в репозиторий Давай оставим только words.txt, причем со значениями 0 для `correctAnswersCount`.
⌨️ Решение¶
import kotlinx.serialization.encodeToString
// ...
const val RESET_CLICKED = "reset_clicked"
class TelegramBotService(
private val botToken: String,
) {
private val client: HttpClient = HttpClient.newHttpClient()
// ...
fun sendMenu(chatId: Long): String {
val urlSendMessage = "$TELEGRAM_BASE_URL/bot$botToken/sendMessage"
val requestBody = SendMessageRequest(
chatId = chatId,
text = "Основное меню",
replyMarkup = ReplyMarkup(
listOf(
listOf(
InlineKeyboard(text = "Изучать слова", callbackData = CLICKED_LEARN_WORDS),
InlineKeyboard(text = "Статистика", callbackData = CLICKED_STATISTICS),
),
listOf(
InlineKeyboard(text = "Сбросить прогресс", callbackData = RESET_CLICKED),
)
)
)
)
val requestBodyString = json.encodeToString(requestBody)
val httpRequest: HttpRequest = HttpRequest.newBuilder().uri(URI.create(urlSendMessage))
.header("Content-type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(requestBodyString))
.build()
val response = client.send(httpRequest, HttpResponse.BodyHandlers.ofString())
return response.body()
}
// ...
}
fun main(args: Array<String>) {
val botToken = args[0]
var updateId = 0
val telegramBotService = TelegramBotService(botToken)
val trainers = HashMap<Long, LearnWordsTrainer>()
while (true) {
Thread.sleep(2000)
val response: Response = telegramBotService.getUpdates(updateId)
if (response.result.isEmpty()) continue
val sortedUpdates = response.result.sortedBy { it.updateId }
sortedUpdates.forEach {
handleUpdate(it, telegramBotService, trainers)
}
updateId = (sortedUpdates.last().updateId + 1).toInt()
println(sortedUpdates)
}
}
fun handleUpdate(
update: Update,
telegramBotService: TelegramBotService,
trainers: HashMap<Long, LearnWordsTrainer>,
) {
val text = update.message?.text
val chatId = update.message?.chat?.id
?: update.callbackQuery?.message?.chat?.id
?: return
val data = update.callbackQuery?.data
val trainer = trainers.getOrPut(chatId) { LearnWordsTrainer("$chatId.txt") }
if (text.equals("/start", true)) {
telegramBotService.sendMenu(chatId)
}
if (data?.equals(CLICKED_STATISTICS) == true) {
val statistics = trainer.getStatistics()
val message = "Выучено ${statistics.learnedCount} из ${statistics.totalCount} слов | ${statistics.percent}%"
telegramBotService.sendMessage(chatId, message)
}
if (data?.equals(CLICKED_LEARN_WORDS) == true) {
checkNextQuestionAndSend(trainer, telegramBotService, chatId)
}
if (data?.startsWith(CALLBACK_DATA_ANSWER_PREFIX) == true) {
val userAnswerIndex = data.substringAfter(CALLBACK_DATA_ANSWER_PREFIX).toInt()
if (trainer.checkAnswer(userAnswerIndex)) {
telegramBotService.sendMessage(chatId, "Правильно!")
} else {
telegramBotService.sendMessage(
chatId,
"Неправильно! ${trainer.question?.correctAnswer?.original} – это ${trainer.question?.correctAnswer?.translate}"
)
}
checkNextQuestionAndSend(trainer, telegramBotService, chatId)
}
if (data == MENU_CLICKED) {
telegramBotService.sendMenu(chatId)
}
if (data == RESET_CLICKED) {
trainer.resetProgress()
telegramBotService.sendMessage(chatId, "Прогресс сброшен")
}
}
fun checkNextQuestionAndSend(trainer: LearnWordsTrainer, service: TelegramBotService, chatId: Long) {
val question = trainer.getNextQuestion()
if (question == null) {
service.sendMessage(chatId, "Вы выучили все слова в базе")
} else {
service.sendQuestion(chatId, question)
}
}
import java.io.File
class Statistics(
val totalCount: Int,
val learnedCount: Int,
val percent: Int,
)
data class Question(
val variants: List<Word>,
val correctAnswer: Word,
)
data class Word(
val original: String,
val translate: String,
var correctAnswersCount: Int = 0,
)
class LearnWordsTrainer(
private val fileName: String = "words.txt",
private val learnedAnswerCount: Int = 3,
private val countOfQuestionWords: Int = 4,
) {
var question: Question? = null
private val dictionary = loadDictionary()
fun getStatistics(): Statistics {
val totalCount = dictionary.size
val learnedCount = dictionary.filter { it.correctAnswersCount >= learnedAnswerCount }.size
val percent = learnedCount * 100 / totalCount
return Statistics(totalCount, learnedCount, percent)
}
fun getNextQuestion(): Question? {
val notLearnedList = dictionary.filter { it.correctAnswersCount < learnedAnswerCount }
if (notLearnedList.isEmpty()) return null
// KTB-10 bug fix
val questionWords = if (notLearnedList.size < countOfQuestionWords) {
val learnedList = dictionary.filter { it.correctAnswersCount >= learnedAnswerCount }.shuffled()
notLearnedList.shuffled()
.take(countOfQuestionWords) + learnedList
.take(countOfQuestionWords - notLearnedList.size)
} else {
notLearnedList.shuffled().take(countOfQuestionWords)
}.shuffled()
/* Перенесено выше в KTB-10
val questionWords = notLearnedList.shuffled().take(countOfQuestionWords)
*/
val correctAnswer = questionWords.random()
question = Question(
variants = questionWords,
correctAnswer = correctAnswer,
)
return question
}
fun checkAnswer(userAnswerIndex: Int?): Boolean {
return question?.let {
val correctAnswerId = it.variants.indexOf(it.correctAnswer)
if (correctAnswerId == userAnswerIndex) {
it.correctAnswer.correctAnswersCount++
saveDictionary()
true
} else {
false
}
} ?: false
}
private fun loadDictionary(): List<Word> {
try {
val dictionary = mutableListOf<Word>()
val wordsFile = File(fileName)
if (!wordsFile.exists()) {
File("words.txt").copyTo(wordsFile)
}
for (string in wordsFile.readLines()) {
val split = string.split("|")
val word = Word(
original = split[0],
translate = split[1],
correctAnswersCount = split[2].toIntOrNull() ?: 0
)
dictionary.add(word)
}
return dictionary
} catch (e: IndexOutOfBoundsException) {
throw IllegalStateException("Некорректный файл: $e")
}
}
private fun saveDictionary() {
val wordsFile = File(fileName)
wordsFile.writeText("")
for (word in dictionary) {
wordsFile.appendText("${word.original}|${word.translate}|${word.correctAnswersCount}\n")
}
}
fun resetProgress() {
dictionary.forEach { it.correctAnswersCount = 0 }
saveDictionary()
}
}