From c0b129aac69db1afc9b2bdd6220e7c1cc3a4db70 Mon Sep 17 00:00:00 2001 From: johnspade Date: Sun, 21 Jul 2024 12:43:12 +0200 Subject: [PATCH] Add Taskobot URL button to task messages --- project/Dependencies.scala | 2 +- src/main/resources/reference.conf | 2 + .../ru/johnspade/taskobot/BotService.scala | 2 +- .../johnspade/taskobot/KeyboardService.scala | 18 +- .../ru/johnspade/taskobot/Taskobot.scala | 17 +- .../johnspade/taskobot/UserMiddleware.scala | 4 +- .../ru/johnspade/taskobot/configuration.scala | 7 +- .../ru/johnspade/taskobot/constants.scala | 12 + .../ru/johnspade/taskobot/core/Page.scala | 2 +- .../taskobot/messages/MessageService.scala | 4 +- .../taskobot/scheduled/ReminderService.scala | 2 +- .../settings/SettingsController.scala | 4 +- .../taskobot/task/TaskController.scala | 368 +++++++++--------- src/test/resources/reference.conf | 1 + .../johnspade/taskobot/BotServiceSpec.scala | 4 +- .../taskobot/CleanupRepository.scala | 17 +- .../taskobot/CommandControllerSpec.scala | 3 +- .../ru/johnspade/taskobot/TaskobotISpec.scala | 132 ++++--- .../ru/johnspade/taskobot/TestBotApi.scala | 66 ++-- .../ru/johnspade/taskobot/TestHelpers.scala | 2 +- .../ru/johnspade/taskobot/TestUsers.scala | 4 +- .../johnspade/taskobot/core/CbDataSpec.scala | 2 +- .../ru/johnspade/taskobot/core/PageSpec.scala | 4 +- .../datetime/DatePickerServiceSpec.scala | 4 +- .../datetime/TimePickerServiceSpec.scala | 2 +- .../ReminderNotificationServiceSpec.scala | 17 +- .../scheduled/ReminderServiceSpec.scala | 24 +- .../settings/SettingsControllerSpec.scala | 6 +- .../taskobot/task/TaskControllerSpec.scala | 22 +- 29 files changed, 391 insertions(+), 363 deletions(-) create mode 100644 src/main/scala/ru/johnspade/taskobot/constants.scala diff --git a/project/Dependencies.scala b/project/Dependencies.scala index b9bfd69..51e99cd 100644 --- a/project/Dependencies.scala +++ b/project/Dependencies.scala @@ -2,7 +2,7 @@ import sbt.librarymanagement.syntax._ object Dependencies { object V { - val telegramium = "9.74.0" + val telegramium = "9.77.0" val tgbotUtils = "0.8.1" val zio = "2.1.4" val zioCats = "23.1.0.2" diff --git a/src/main/resources/reference.conf b/src/main/resources/reference.conf index 87b247f..f2a891b 100644 --- a/src/main/resources/reference.conf +++ b/src/main/resources/reference.conf @@ -10,4 +10,6 @@ bot { port = ${?PORT} token = ${?BOT_TOKEN} url = ${BOT_EXTERNAL_URL} + username = "tasko_bot" + username = ${?BOT_USERNAME} } diff --git a/src/main/scala/ru/johnspade/taskobot/BotService.scala b/src/main/scala/ru/johnspade/taskobot/BotService.scala index f30cf8b..01c7fd0 100644 --- a/src/main/scala/ru/johnspade/taskobot/BotService.scala +++ b/src/main/scala/ru/johnspade/taskobot/BotService.scala @@ -9,8 +9,8 @@ import zio.interop.catz.* import telegramium.bots import telegramium.bots.high.messageentities.MessageEntities import telegramium.bots.high.messageentities.MessageEntityFormat -import telegramium.bots.high.messageentities.MessageEntityFormat.Plain.lineBreak import telegramium.bots.high.messageentities.MessageEntityFormat.* +import telegramium.bots.high.messageentities.MessageEntityFormat.Plain.lineBreak import ru.johnspade.taskobot.core.Page import ru.johnspade.taskobot.core.TelegramOps.toUser diff --git a/src/main/scala/ru/johnspade/taskobot/KeyboardService.scala b/src/main/scala/ru/johnspade/taskobot/KeyboardService.scala index 5230382..5ffaeb1 100644 --- a/src/main/scala/ru/johnspade/taskobot/KeyboardService.scala +++ b/src/main/scala/ru/johnspade/taskobot/KeyboardService.scala @@ -3,6 +3,7 @@ package ru.johnspade.taskobot import zio.* import cats.syntax.option.* +import telegramium.bots.InlineKeyboardButton import telegramium.bots.InlineKeyboardMarkup import telegramium.bots.KeyboardButton import telegramium.bots.ReplyKeyboardMarkup @@ -11,8 +12,8 @@ import telegramium.bots.high.keyboards.InlineKeyboardButtons import telegramium.bots.high.keyboards.InlineKeyboardMarkups import telegramium.bots.high.keyboards.KeyboardButtons -import ru.johnspade.taskobot.core.TelegramOps.inlineKeyboardButton import ru.johnspade.taskobot.core.* +import ru.johnspade.taskobot.core.TelegramOps.inlineKeyboardButton import ru.johnspade.taskobot.messages.Language import ru.johnspade.taskobot.messages.MessageService import ru.johnspade.taskobot.messages.MsgId @@ -38,7 +39,9 @@ trait KeyboardService: def standardReminders(taskId: Long, pageNumber: Int, language: Language): InlineKeyboardMarkup -final class KeyboardServiceLive(msgService: MessageService) extends KeyboardService: + def taskobotUrlButton: InlineKeyboardButton + +final class KeyboardServiceLive(msgService: MessageService, botConfig: BotConfig) extends KeyboardService: def chats(page: Page[User], `for`: User): InlineKeyboardMarkup = { lazy val prevButton = inlineKeyboardButton(msgService.previousPage(`for`.language), Chats(page.number - 1)) lazy val nextButton = inlineKeyboardButton(msgService.nextPage(`for`.language), Chats(page.number + 1)) @@ -53,7 +56,7 @@ final class KeyboardServiceLive(msgService: MessageService) extends KeyboardServ List( InlineKeyboardButtons.url( msgService.getMessage(`buy-coffee`, `for`.language) + " โ˜•", - "https://buymeacoff.ee/johnspade" + DonateUrl ) ) ) @@ -105,7 +108,7 @@ final class KeyboardServiceLive(msgService: MessageService) extends KeyboardServ KeyboardButtons.text("โš™๏ธ " + msgService.getMessage(`settings`, language)), KeyboardButton( text = "๐ŸŒ " + msgService.getMessage(`timezone`, language), - webApp = Some(WebAppInfo("https://timezones.johnspade.ru")) + webApp = Some(WebAppInfo(TimezonesAppUrl)) ) ) ), @@ -154,8 +157,11 @@ final class KeyboardServiceLive(msgService: MessageService) extends KeyboardServ inlineKeyboardButton(msgService.remindersDaysBefore(3, language), CreateReminder(taskId, 60 * 24 * 3)), inlineKeyboardButton("๐Ÿ”™", Reminders(taskId, pageNumber)) ) + + override val taskobotUrlButton: InlineKeyboardButton = + InlineKeyboardButtons.url("\uD83D\uDE80 Taskobot", s"https://t.me/${botConfig.username}") end KeyboardServiceLive object KeyboardServiceLive: - val layer: URLayer[MessageService, KeyboardService] = - ZLayer(ZIO.service[MessageService].map(new KeyboardServiceLive(_))) + val layer: URLayer[MessageService & BotConfig, KeyboardService] = + ZLayer.fromFunction(new KeyboardServiceLive(_, _)) diff --git a/src/main/scala/ru/johnspade/taskobot/Taskobot.scala b/src/main/scala/ru/johnspade/taskobot/Taskobot.scala index 5e3481b..d3e0083 100644 --- a/src/main/scala/ru/johnspade/taskobot/Taskobot.scala +++ b/src/main/scala/ru/johnspade/taskobot/Taskobot.scala @@ -2,8 +2,8 @@ package ru.johnspade.taskobot import java.time.ZoneId -import zio.Task import zio.* +import zio.Task import zio.interop.catz.* import zio.json.* @@ -32,11 +32,6 @@ import ru.johnspade.taskobot.task.TaskController import ru.johnspade.taskobot.task.TaskRepository import ru.johnspade.taskobot.user.User -val DefaultPageSize: Int = 5 -val MessageLimit = 4096 - -val UTC = ZoneId.of("UTC") - type CbDataRoutes[F[_]] = CallbackQueryRoutes[CbData, Option[Method[_]], F] type CbDataUserRoutes[F[_]] = CallbackQueryContextRoutes[CbData, User, Option[Method[_]], F] @@ -74,8 +69,9 @@ final class Taskobot( entities = messageEntities.toTelegramEntities().map(OpenEnum(_)) ), replyMarkup = InlineKeyboardMarkups - .singleButton( - inlineKeyboardButton("Confirm task", ConfirmTask(id = None, senderId = query.from.id.some)) + .singleColumn( + inlineKeyboardButton("Confirm task", ConfirmTask(id = None, senderId = query.from.id.some)), + kbService.taskobotUrlButton ) .some, description = text.some @@ -93,8 +89,9 @@ final class Taskobot( method = editMessageReplyMarkup( inlineMessageId = inlineResult.inlineMessageId, replyMarkup = InlineKeyboardMarkups - .singleButton( - inlineKeyboardButton("Confirm task", ConfirmTask(task.id.some, user.id.some)) + .singleColumn( + inlineKeyboardButton("Confirm task", ConfirmTask(task.id.some, user.id.some)), + kbService.taskobotUrlButton ) .some ) diff --git a/src/main/scala/ru/johnspade/taskobot/UserMiddleware.scala b/src/main/scala/ru/johnspade/taskobot/UserMiddleware.scala index 5926b8e..2412991 100644 --- a/src/main/scala/ru/johnspade/taskobot/UserMiddleware.scala +++ b/src/main/scala/ru/johnspade/taskobot/UserMiddleware.scala @@ -1,7 +1,7 @@ package ru.johnspade.taskobot -import zio._ -import zio.interop.catz._ +import zio.* +import zio.interop.catz.* import cats.data.Kleisli import cats.data.OptionT diff --git a/src/main/scala/ru/johnspade/taskobot/configuration.scala b/src/main/scala/ru/johnspade/taskobot/configuration.scala index 57638ba..a28e3bb 100644 --- a/src/main/scala/ru/johnspade/taskobot/configuration.scala +++ b/src/main/scala/ru/johnspade/taskobot/configuration.scala @@ -31,11 +31,12 @@ object DbConfig: }.orDie ) -final case class BotConfig(port: Int, url: String, token: String) +final case class BotConfig(port: Int, url: String, token: String, username: String) object BotConfig: implicit val botConfigReader: ConfigReader[BotConfig] = - ConfigReader.forProduct3[BotConfig, Int, String, String]("port", "url", "token") { case (port, url, token) => - BotConfig(port, url, token) + ConfigReader.forProduct4[BotConfig, Int, String, String, String]("port", "url", "token", "username") { + case (port, url, token, username) => + BotConfig(port, url, token, username) } val live: ULayer[BotConfig] = ZLayer( ZIO.attempt { diff --git a/src/main/scala/ru/johnspade/taskobot/constants.scala b/src/main/scala/ru/johnspade/taskobot/constants.scala new file mode 100644 index 0000000..3993fa3 --- /dev/null +++ b/src/main/scala/ru/johnspade/taskobot/constants.scala @@ -0,0 +1,12 @@ +package ru.johnspade.taskobot + +import java.time.ZoneId + +val DefaultPageSize: Int = 5 +val MessageLimit = 4096 + +val UTC = ZoneId.of("UTC") + +val DonateUrl = "https://buymeacoff.ee/johnspade" + +val TimezonesAppUrl = "https://timezones.johnspade.ru" diff --git a/src/main/scala/ru/johnspade/taskobot/core/Page.scala b/src/main/scala/ru/johnspade/taskobot/core/Page.scala index 21994c3..ddff13c 100644 --- a/src/main/scala/ru/johnspade/taskobot/core/Page.scala +++ b/src/main/scala/ru/johnspade/taskobot/core/Page.scala @@ -1,7 +1,7 @@ package ru.johnspade.taskobot.core import cats.Functor -import cats.syntax.functor._ +import cats.syntax.functor.* final case class Page[T]( items: List[T], diff --git a/src/main/scala/ru/johnspade/taskobot/messages/MessageService.scala b/src/main/scala/ru/johnspade/taskobot/messages/MessageService.scala index 09014af..a6d72ac 100644 --- a/src/main/scala/ru/johnspade/taskobot/messages/MessageService.scala +++ b/src/main/scala/ru/johnspade/taskobot/messages/MessageService.scala @@ -6,6 +6,8 @@ import zio.URLayer import zio.ZIO import zio.ZLayer +import ru.johnspade.taskobot.DonateUrl + import MsgId.* trait MessageService: @@ -45,7 +47,7 @@ final class MessageServiceLive(msgConfig: MsgConfig) extends MessageService: getMessage(`help-due-date`, lang) + "\n\n" + getMessage(`help-task-complete`, lang) + "\n\n" + switchLanguage(lang) + ": /settings" + "\n" + - getMessage(`support-creator`, lang) + ": https://buymeacoff.ee/johnspade โ˜•" + getMessage(`support-creator`, lang) + s": $DonateUrl โ˜•" def taskCreated(task: String, language: Language): String = getMessage(`tasks-personal-created`, language, task) diff --git a/src/main/scala/ru/johnspade/taskobot/scheduled/ReminderService.scala b/src/main/scala/ru/johnspade/taskobot/scheduled/ReminderService.scala index 02208d7..7f23445 100644 --- a/src/main/scala/ru/johnspade/taskobot/scheduled/ReminderService.scala +++ b/src/main/scala/ru/johnspade/taskobot/scheduled/ReminderService.scala @@ -6,8 +6,8 @@ import zio.* import cats.data.NonEmptyList import telegramium.bots.* -import telegramium.bots.high.Api import telegramium.bots.high.* +import telegramium.bots.high.Api import telegramium.bots.high.implicits.* import ru.johnspade.taskobot.BotService diff --git a/src/main/scala/ru/johnspade/taskobot/settings/SettingsController.scala b/src/main/scala/ru/johnspade/taskobot/settings/SettingsController.scala index 3effafe..e39cdf2 100644 --- a/src/main/scala/ru/johnspade/taskobot/settings/SettingsController.scala +++ b/src/main/scala/ru/johnspade/taskobot/settings/SettingsController.scala @@ -61,8 +61,8 @@ final class SettingsControllerLive( execDiscardWithHandling( editMessageText( msgService.currentLanguage(language), - ChatIntId(msg.chat.id).some, - msg.messageId.some, + chatId = ChatIntId(msg.chat.id).some, + messageId = msg.messageId.some, replyMarkup = kbService.languages(language).some ) ) diff --git a/src/main/scala/ru/johnspade/taskobot/task/TaskController.scala b/src/main/scala/ru/johnspade/taskobot/task/TaskController.scala index 0232a21..4c2a640 100644 --- a/src/main/scala/ru/johnspade/taskobot/task/TaskController.scala +++ b/src/main/scala/ru/johnspade/taskobot/task/TaskController.scala @@ -12,9 +12,10 @@ import telegramium.bots.ChatIntId import telegramium.bots.InlineKeyboardMarkup import telegramium.bots.Message import telegramium.bots.client.Method -import telegramium.bots.high.Methods.* import telegramium.bots.high.* +import telegramium.bots.high.Methods.* import telegramium.bots.high.implicits.* +import telegramium.bots.high.keyboards.InlineKeyboardMarkups import telegramium.bots.high.messageentities.MessageEntities import ru.johnspade.taskobot.BotService @@ -25,9 +26,9 @@ import ru.johnspade.taskobot.Errors import ru.johnspade.taskobot.Errors.MaxRemindersExceeded import ru.johnspade.taskobot.KeyboardService import ru.johnspade.taskobot.TelegramBotApi.TelegramBotApi +import ru.johnspade.taskobot.core.* import ru.johnspade.taskobot.core.TelegramOps.* import ru.johnspade.taskobot.core.TimePicker -import ru.johnspade.taskobot.core.* import ru.johnspade.taskobot.messages.Language import ru.johnspade.taskobot.messages.MessageService import ru.johnspade.taskobot.messages.MsgId @@ -48,206 +49,207 @@ final class TaskControllerLive( kbService: KeyboardService )(using api: Api[Task]) extends TaskController: - override val routes: CbDataRoutes[Task] = CallbackQueryRoutes.of { case ConfirmTask(taskIdOpt, senderIdOpt) in cb => - def confirm(task: BotTask, from: User): Task[Option[Method[_]]] = - for - _ <- taskRepo.setReceiver(task.id, senderIdOpt, from.id) - _ <- execDiscardWithHandling( - editMessageReplyMarkup(inlineMessageId = cb.inlineMessageId, replyMarkup = Option.empty) - ) - yield answerCallbackQuery(cb.id).some - - def mustBeConfirmedByReceiver(from: User): UIO[Option[Method[_]]] = { - ZIO.succeed( - answerCallbackQuery( - cb.id, - msgService.getMessage(MsgId.`tasks-must-be-confirmed`, from.language).some - ).some - ) - } - - for - id <- ZIO.fromOption(taskIdOpt).orElseFail(new RuntimeException(Errors.Default)) - taskOpt <- taskRepo.findByIdUnsafe(id) - task <- ZIO.fromOption(taskOpt).orElseFail(new RuntimeException(Errors.NotFound)) - user <- botService.updateUser(cb.from) - answerOpt <- if (task.sender == cb.from.id) mustBeConfirmedByReceiver(user) else confirm(task, user) - yield answerOpt - - } - - override val userRoutes: CbDataUserRoutes[Task] = CallbackQueryContextRoutes.of { - case Chats(pageNumber) in cb as user => - ackCb(cb) { msg => + override val routes: CbDataRoutes[Task] = CallbackQueryRoutes.of: + case ConfirmTask(taskIdOpt, senderIdOpt) in cb => + def confirm(task: BotTask, from: User): Task[Option[Method[_]]] = for - page <- Page.request[User, Task](pageNumber, DefaultPageSize, userRepo.findUsersWithSharedTasks(user.id)) + _ <- taskRepo.setReceiver(task.id, senderIdOpt, from.id) _ <- execDiscardWithHandling( - editMessageText( - msgService.chatsWithTasks(user.language), - ChatIntId(msg.chat.id).some, - msg.messageId.some, - replyMarkup = kbService.chats(page, user).some + editMessageReplyMarkup( + inlineMessageId = cb.inlineMessageId, + replyMarkup = InlineKeyboardMarkups.singleButton(kbService.taskobotUrlButton).some ) ) - yield () - } + yield answerCallbackQuery(cb.id).some + + def mustBeConfirmedByReceiver(from: User): UIO[Option[Method[_]]] = + ZIO.succeed( + answerCallbackQuery( + cb.id, + msgService.getMessage(MsgId.`tasks-must-be-confirmed`, from.language).some + ).some + ) - case Tasks(pageNumber, collaboratorId) in cb as user => - ackCb(cb) { msg => - for - userOpt <- userRepo.findById(collaboratorId) - collaborator <- ZIO.fromOption(userOpt).orElseFail(new RuntimeException(Errors.NotFound)) - pageAndMsgEntities <- botService.getTasks(user, collaborator, pageNumber) - _ <- listTasks(msg, pageAndMsgEntities._2, pageAndMsgEntities._1, collaborator, user.language) - yield () - } - - case TaskDetails(id, pageNumber) in cb as user => - returnTaskDetails(cb, user, id, pageNumber, task => ZIO.succeed(task)) - - case CheckTask(pageNumber, id) in cb as user => - def checkTask(task: TaskWithCollaborator) = - for - now <- Clock.instant - _ <- taskRepo.check(task.id, now, user.id) - yield () - - def listTasksAndNotify(task: TaskWithCollaborator, message: Message) = - task.collaborator - .map { collaborator => - for - pageAndMsgEntities <- botService.getTasks(user, collaborator, pageNumber) - _ <- listTasks(message, pageAndMsgEntities._2, pageAndMsgEntities._1, collaborator, user.language) - _ <- notify(task, user, collaborator).when(user.id != collaborator.id) - yield () - } - .getOrElse(ZIO.unit) - - taskRepo - .findByIdWithCollaborator(id, user.id) - .flatMap { taskOpt => - val answerText = - (for - _ <- taskOpt.toRight(Errors.NotFound) - _ <- cb.message.flatMap(_.toMessage).toRight(Errors.Default) - yield msgService.getMessage(MsgId.`tasks-completed`, user.language)).merge - - ZIO - .collectAllDiscard { - for - task <- taskOpt - maybeInaccessibleMessage <- cb.message - message <- maybeInaccessibleMessage.toMessage - yield checkTask(task) *> - listTasksAndNotify(task, message) - } - .as(answerCallbackQuery(cb.id, answerText.some).some) + for + id <- ZIO.fromOption(taskIdOpt).orElseFail(new RuntimeException(Errors.Default)) + taskOpt <- taskRepo.findByIdUnsafe(id) + task <- ZIO.fromOption(taskOpt).orElseFail(new RuntimeException(Errors.NotFound)) + user <- botService.updateUser(cb.from) + answerOpt <- if (task.sender == cb.from.id) mustBeConfirmedByReceiver(user) else confirm(task, user) + yield answerOpt + + override val userRoutes: CbDataUserRoutes[Task] = CallbackQueryContextRoutes + .of { + case Chats(pageNumber) in cb as user => + ackCb(cb) { msg => + for + page <- Page.request[User, Task](pageNumber, DefaultPageSize, userRepo.findUsersWithSharedTasks(user.id)) + _ <- execDiscardWithHandling( + editMessageText( + msgService.chatsWithTasks(user.language), + chatId = ChatIntId(msg.chat.id).some, + messageId = msg.messageId.some, + replyMarkup = kbService.chats(page, user).some + ) + ) + yield () } - case TaskDeadlineDate(id, date) in cb as user => - def setDeadline(task: BotTask) = - val deadline = task.deadline - .map(dt => dt.`with`(date.atTime(dt.toLocalTime()))) - .getOrElse(date.atStartOfDay()) - taskRepo.setDeadline(id, Some(deadline), user.id) - - returnTaskDetails(cb, user, id, pageNumber = 0, processTask = setDeadline) + case Tasks(pageNumber, collaboratorId) in cb as user => + ackCb(cb) { msg => + for + userOpt <- userRepo.findById(collaboratorId) + collaborator <- ZIO.fromOption(userOpt).orElseFail(new RuntimeException(Errors.NotFound)) + pageAndMsgEntities <- botService.getTasks(user, collaborator, pageNumber) + _ <- listTasks(msg, pageAndMsgEntities._2, pageAndMsgEntities._1, collaborator, user.language) + yield () + } - case RemoveTaskDeadline(id) in cb as user => - returnTaskDetails( - cb, - user, - id, - pageNumber = 0, - processTask = task => taskRepo.setDeadline(task.id, deadline = None, userId = user.id) - ) + case TaskDetails(id, pageNumber) in cb as user => + returnTaskDetails(cb, user, id, pageNumber, task => ZIO.succeed(task)) - case TimePicker(taskId, Some(hour), Some(minute), true) in cb as user => - def setDeadline(task: BotTask) = - task.deadline - .map { dt => - val deadline = dt - .withHour(hour) - .withMinute(minute) - taskRepo.setDeadline(taskId, Some(deadline), user.id) + case CheckTask(pageNumber, id) in cb as user => + def listTasksAndNotify(task: TaskWithCollaborator, message: Message) = + task.collaborator + .map { collaborator => + for + pageAndMsgEntities <- botService.getTasks(user, collaborator, pageNumber) + _ <- listTasks(message, pageAndMsgEntities._2, pageAndMsgEntities._1, collaborator, user.language) + _ <- notify(task.text, user, collaborator) + yield () + } + .getOrElse(ZIO.unit) + + taskRepo + .findByIdWithCollaborator(id, user.id) + .flatMap { taskOpt => + val answerText = + (for + _ <- taskOpt.toRight(Errors.NotFound) + _ <- cb.message.flatMap(_.toMessage).toRight(Errors.Default) + yield msgService.getMessage(MsgId.`tasks-completed`, user.language)).merge + + ZIO + .collectAllDiscard { + for + task <- taskOpt + maybeInaccessibleMessage <- cb.message + message <- maybeInaccessibleMessage.toMessage + yield checkTask(task, user.id) *> + listTasksAndNotify(task, message) + } + .as(answerCallbackQuery(cb.id, answerText.some).some) } - .getOrElse(ZIO.succeed(task)) - returnTaskDetails(cb, user, taskId, pageNumber = 0, processTask = setDeadline) + case TaskDeadlineDate(id, date) in cb as user => + def setDeadline(task: BotTask) = + val deadline = task.deadline + .map(dt => dt.`with`(date.atTime(dt.toLocalTime()))) + .getOrElse(date.atStartOfDay()) + taskRepo.setDeadline(id, Some(deadline), user.id) - case Reminders(taskId, pageNumber) in cb as user => - for - task <- getTaskSafe(taskId, user.id) - reminders <- reminderRepo.getByTaskIdAndUserId(taskId, user.id) - result <- taskDetails( - cb, - user, - task, - processTask = ZIO.succeed(_), - generateKeyboard = - (_, _) => ZIO.succeed(createRemindersKeyboard(taskId, reminders, pageNumber, language = user.language)) - ) - yield result + returnTaskDetails(cb, user, id, pageNumber = 0, processTask = setDeadline) - case StandardReminders(taskId, pageNumber) in cb as user => - for - task <- getTaskSafe(taskId, user.id) - result <- taskDetails( + case RemoveTaskDeadline(id) in cb as user => + returnTaskDetails( cb, user, - task, - processTask = ZIO.succeed(_), - generateKeyboard = (_, _) => ZIO.succeed(kbService.standardReminders(taskId, pageNumber, user.language)) + id, + pageNumber = 0, + processTask = task => taskRepo.setDeadline(task.id, deadline = None, userId = user.id) ) - yield result - case CreateReminder(taskId, offsetMinutes) in cb as user => - for - task <- getTaskSafe(taskId, user.id) - _ <- reminderRepo - .create(task.id, user.id, offsetMinutes) - .when(task.deadline.isDefined) - .catchSome { case MaxRemindersExceeded(taskId) => - ZIO.logWarning(s"Max reminders for taskId $taskId exceeded") - } - reminders <- reminderRepo.getByTaskIdAndUserId(task.id, user.id) - result <- taskDetails( - cb, - user, - task, - processTask = ZIO.succeed(_), - generateKeyboard = - (_, _) => ZIO.succeed(createRemindersKeyboard(taskId, reminders, pageNumber = 0, language = user.language)) - ) - yield result + case TimePicker(taskId, Some(hour), Some(minute), true) in cb as user => + def setDeadline(task: BotTask) = + task.deadline + .map { dt => + val deadline = dt + .withHour(hour) + .withMinute(minute) + taskRepo.setDeadline(taskId, Some(deadline), user.id) + } + .getOrElse(ZIO.succeed(task)) - case RemoveReminder(reminderId, taskId) in cb as user => - for - task <- getTaskSafe(taskId, user.id) - _ <- reminderRepo.delete(reminderId, user.id) - reminders <- reminderRepo.getByTaskIdAndUserId(task.id, user.id) - result <- taskDetails( - cb, - user, - task, - processTask = ZIO.succeed(_), - generateKeyboard = - (_, _) => ZIO.succeed(createRemindersKeyboard(taskId, reminders, pageNumber = 0, language = user.language)) - ) - yield result - } + returnTaskDetails(cb, user, taskId, pageNumber = 0, processTask = setDeadline) + + case Reminders(taskId, pageNumber) in cb as user => + for + task <- getTaskSafe(taskId, user.id) + reminders <- reminderRepo.getByTaskIdAndUserId(taskId, user.id) + result <- taskDetails( + cb, + user, + task, + processTask = ZIO.succeed(_), + generateKeyboard = + (_, _) => ZIO.succeed(createRemindersKeyboard(taskId, reminders, pageNumber, language = user.language)) + ) + yield result + + case StandardReminders(taskId, pageNumber) in cb as user => + for + task <- getTaskSafe(taskId, user.id) + result <- taskDetails( + cb, + user, + task, + processTask = ZIO.succeed(_), + generateKeyboard = (_, _) => ZIO.succeed(kbService.standardReminders(taskId, pageNumber, user.language)) + ) + yield result + + case CreateReminder(taskId, offsetMinutes) in cb as user => + for + task <- getTaskSafe(taskId, user.id) + _ <- reminderRepo + .create(task.id, user.id, offsetMinutes) + .when(task.deadline.isDefined) + .catchSome { case MaxRemindersExceeded(taskId) => + ZIO.logWarning(s"Max reminders for taskId $taskId exceeded") + } + reminders <- reminderRepo.getByTaskIdAndUserId(task.id, user.id) + result <- taskDetails( + cb, + user, + task, + processTask = ZIO.succeed(_), + generateKeyboard = (_, _) => + ZIO.succeed(createRemindersKeyboard(taskId, reminders, pageNumber = 0, language = user.language)) + ) + yield result + + case RemoveReminder(reminderId, taskId) in cb as user => + for + task <- getTaskSafe(taskId, user.id) + _ <- reminderRepo.delete(reminderId, user.id) + reminders <- reminderRepo.getByTaskIdAndUserId(task.id, user.id) + result <- taskDetails( + cb, + user, + task, + processTask = ZIO.succeed(_), + generateKeyboard = (_, _) => + ZIO.succeed(createRemindersKeyboard(taskId, reminders, pageNumber = 0, language = user.language)) + ) + yield result + } + + private def checkTask(task: TaskWithCollaborator, userId: Long) = + for + now <- Clock.instant + _ <- taskRepo.check(task.id, now, userId) + yield () - private def notify(task: TaskWithCollaborator, from: User, collaborator: User) = - collaborator.chatId - .map { chatId => - val taskText = task.text + private def notify(taskText: String, from: User, collaborator: User) = + ZIO + .foreachDiscard(collaborator.chatId): chatId => val completedBy = from.fullName sendMessage( ChatIntId(chatId), msgService.getMessage(MsgId.`tasks-completed-by`, collaborator.language, taskText, completedBy) - ).exec.unit - } - .getOrElse(ZIO.unit) + ).exec + .when(from.id != collaborator.id) + .unit private def listTasks( message: Message, @@ -259,8 +261,8 @@ final class TaskControllerLive( execDiscardWithHandling( editMessageText( messageEntities.toPlainText(), - ChatIntId(message.chat.id).some, - message.messageId.some, + chatId = ChatIntId(message.chat.id).some, + messageId = message.messageId.some, entities = messageEntities.toTelegramEntities(), replyMarkup = kbService.tasks(page, collaborator, language).some ) @@ -319,8 +321,8 @@ final class TaskControllerLive( execDiscardWithHandling( editMessageText( messageEntities.toPlainText(), - ChatIntId(message.chat.id).some, - message.messageId.some, + chatId = ChatIntId(message.chat.id).some, + messageId = message.messageId.some, entities = messageEntities.toTelegramEntities(), replyMarkup = Some(keyboard) ) diff --git a/src/test/resources/reference.conf b/src/test/resources/reference.conf index 018e84b..f7e0ae2 100644 --- a/src/test/resources/reference.conf +++ b/src/test/resources/reference.conf @@ -9,4 +9,5 @@ bot { port = 8080 token = "123" url = "" + username = "tasko_bot" } diff --git a/src/test/scala/ru/johnspade/taskobot/BotServiceSpec.scala b/src/test/scala/ru/johnspade/taskobot/BotServiceSpec.scala index 7ccb020..aa68dd7 100644 --- a/src/test/scala/ru/johnspade/taskobot/BotServiceSpec.scala +++ b/src/test/scala/ru/johnspade/taskobot/BotServiceSpec.scala @@ -5,13 +5,13 @@ import java.time.LocalDateTime import java.time.ZoneId import zio.* -import zio.test.TestAspect.sequential import zio.test.* +import zio.test.TestAspect.sequential import cats.syntax.option.* import telegramium.bots.high.messageentities.MessageEntities -import telegramium.bots.high.messageentities.MessageEntityFormat.Plain.lineBreak import telegramium.bots.high.messageentities.MessageEntityFormat.* +import telegramium.bots.high.messageentities.MessageEntityFormat.Plain.lineBreak import ru.johnspade.taskobot.messages.Language import ru.johnspade.taskobot.messages.MessageServiceLive diff --git a/src/test/scala/ru/johnspade/taskobot/CleanupRepository.scala b/src/test/scala/ru/johnspade/taskobot/CleanupRepository.scala index 9d7b917..39f68ed 100644 --- a/src/test/scala/ru/johnspade/taskobot/CleanupRepository.scala +++ b/src/test/scala/ru/johnspade/taskobot/CleanupRepository.scala @@ -9,22 +9,15 @@ import doobie.implicits.* import ru.johnspade.taskobot.DbTransactor.DbTransactor trait CleanupRepository: - def clearTasks(): Task[Unit] - def clearUsers(): Task[Unit] - def clearReminders(): Task[Unit] + def truncateTables(): Task[Unit] object CleanupRepository: - def clearTasks(): ZIO[CleanupRepository, Throwable, Unit] = - ZIO.serviceWithZIO(_.clearTasks()) - def clearUsers(): ZIO[CleanupRepository, Throwable, Unit] = - ZIO.serviceWithZIO(_.clearUsers()) - def clearReminders(): ZIO[CleanupRepository, Throwable, Unit] = - ZIO.serviceWithZIO(_.clearReminders()) + def truncateTables(): ZIO[CleanupRepository, Throwable, Unit] = + ZIO.serviceWithZIO(_.truncateTables()) final class CleanupRepositoryLive(xa: DbTransactor) extends CleanupRepository: - override def clearTasks(): Task[Unit] = sql"delete from tasks where true".update.run.transact(xa).unit - override def clearUsers(): Task[Unit] = sql"delete from users where true".update.run.transact(xa).unit - override def clearReminders(): Task[Unit] = sql"delete from reminders where true".update.run.transact(xa).unit + override def truncateTables(): Task[Unit] = + sql"truncate table tasks, users, reminders restart identity cascade".update.run.transact(xa).unit object CleanupRepositoryLive: val layer: ZLayer[DbTransactor, Nothing, CleanupRepository] = ZLayer.fromFunction(new CleanupRepositoryLive(_)) diff --git a/src/test/scala/ru/johnspade/taskobot/CommandControllerSpec.scala b/src/test/scala/ru/johnspade/taskobot/CommandControllerSpec.scala index 4c09ad7..087f7b1 100644 --- a/src/test/scala/ru/johnspade/taskobot/CommandControllerSpec.scala +++ b/src/test/scala/ru/johnspade/taskobot/CommandControllerSpec.scala @@ -103,5 +103,6 @@ object CommandControllerSpec extends ZIOSpecDefault: MessageServiceLive.layer, KeyboardServiceLive.layer, BotServiceLive.layer, - CommandControllerLive.layer + CommandControllerLive.layer, + BotConfig.live ) diff --git a/src/test/scala/ru/johnspade/taskobot/TaskobotISpec.scala b/src/test/scala/ru/johnspade/taskobot/TaskobotISpec.scala index 7612638..344d6ad 100644 --- a/src/test/scala/ru/johnspade/taskobot/TaskobotISpec.scala +++ b/src/test/scala/ru/johnspade/taskobot/TaskobotISpec.scala @@ -4,9 +4,9 @@ import java.time.Instant import java.time.LocalDate import zio.* +import zio.test.* import zio.test.Assertion.* import zio.test.TestAspect.sequential -import zio.test.* import cats.syntax.option.* import iozhik.OpenEnum @@ -21,8 +21,8 @@ import ru.johnspade.taskobot.TestBotApi.Mocks import ru.johnspade.taskobot.TestBotApi.createMock import ru.johnspade.taskobot.TestHelpers.createMessage import ru.johnspade.taskobot.TestUsers.* -import ru.johnspade.taskobot.core.TelegramOps.inlineKeyboardButton import ru.johnspade.taskobot.core.* +import ru.johnspade.taskobot.core.TelegramOps.inlineKeyboardButton import ru.johnspade.taskobot.datetime.* import ru.johnspade.taskobot.messages.MessageServiceLive import ru.johnspade.taskobot.messages.MsgConfig @@ -35,62 +35,8 @@ import ru.johnspade.taskobot.user.UserRepositoryLive object TaskobotISpec extends ZIOSpecDefault: override def spec: Spec[TestEnvironment with Scope, Any] = (suite("TaskobotISpec")( test("collaborative tasks") { - val typeTask = - for - inlineQueryReply <- withTaskobotService( - _.onInlineQueryReply(InlineQuery("0", johnTg, query = "Buy some milk", offset = "0")) - ) - assertions = assertTrue( - inlineQueryReply.contains( - Methods.answerInlineQuery( - "0", - cacheTime = 0.some, - results = List( - InlineQueryResultArticle( - "1", - "Create task", - InputTextMessageContent( - "Buy some milk", - entities = MessageEntities().bold("Buy some milk").toTelegramEntities().map(OpenEnum(_)) - ), - InlineKeyboardMarkups - .singleButton( - inlineKeyboardButton("Confirm task", ConfirmTask(id = None, senderId = john.id.some)) - ) - .some, - description = "Buy some milk".some - ) - ) - ) - ) - ) - yield assertions - - val createTask = - for - chosenInlineResultReply <- - withTaskobotService( - _.onChosenInlineResultReply( - ChosenInlineResult("0", johnTg, query = "Buy some milk", inlineMessageId = "0".some) - ) - ) - expectedEditMessageReplyMarkupReq = Methods.editMessageReplyMarkup( - inlineMessageId = "0".some, - replyMarkup = InlineKeyboardMarkups - .singleButton( - inlineKeyboardButton("Confirm task", ConfirmTask(1L.some, john.id.some)) - ) - .some - ) - yield assertTrue(chosenInlineResultReply.get.payload == expectedEditMessageReplyMarkupReq.payload) - - val confirmTask = sendCallbackQuery( - ConfirmTask(1L.some, john.id.some), - kaitrinTg, - inlineMessageId = "0".some - ) - .map(confirmTaskReply => assertTrue(confirmTaskReply.contains(Methods.answerCallbackQuery("0")))) - + val createTask = createTaskInteraction(1L) + val confirmTask = confirmTaskInteraction(1L) val listChats = for listReply <- sendMessage("/list", isCommand = true, chatId = kaitrinChatId) @@ -103,7 +49,7 @@ object TaskobotISpec extends ZIOSpecDefault: .singleColumn( List( inlineKeyboardButton("Kaitrin", Tasks(firstPage, kaitrin.id)), - InlineKeyboardButtons.url("Buy me a coffee โ˜•", "https://buymeacoff.ee/johnspade") + InlineKeyboardButtons.url("Buy me a coffee โ˜•", DonateUrl) ) ) .some @@ -134,7 +80,7 @@ object TaskobotISpec extends ZIOSpecDefault: } val addReminder = - sendCallbackQuery(CreateReminder(1L, 0)).map { detailsReply => + sendCallbackQuery(CreateReminder(1L, 60)).map { detailsReply => assertTrue(detailsReply.contains(Methods.answerCallbackQuery("0"))) } @@ -162,8 +108,8 @@ object TaskobotISpec extends ZIOSpecDefault: for _ <- TestClock.setTime(Instant.EPOCH) now <- Clock.instant - _ <- createMock(Mocks.addConfirmButton, Mocks.messageResponse) - _ <- createMock(Mocks.removeReplyMarkup, Mocks.messageResponse) + _ <- createMock(Mocks.addConfirmButtons(0L), Mocks.messageResponse) + _ <- createMock(Mocks.addTaskobotUrlButton, Mocks.messageResponse) _ <- createMock(Mocks.editMessageTextList, Mocks.messageResponse) _ <- createMock(Mocks.editMessageTextCheckTask(kaitrinChatId), Mocks.messageResponse) _ <- createMock(Mocks.taskCompletedMessage, Mocks.messageResponse) @@ -235,7 +181,7 @@ object TaskobotISpec extends ZIOSpecDefault: | |""".stripMargin + "Switch language: /settings\n" + - "Support a creator: https://buymeacoff.ee/johnspade โ˜•", + s"Support a creator: $DonateUrl โ˜•", parseMode = Html.some, replyMarkup = expectedMenu, linkPreviewOptions = LinkPreviewOptions(isDisabled = true.some).some @@ -414,7 +360,7 @@ object TaskobotISpec extends ZIOSpecDefault: List(KeyboardButtons.text("\uD83D\uDE80 New collaborative task"), KeyboardButtons.text("โ“ Help")), List( KeyboardButtons.text("โš™๏ธ Settings"), - KeyboardButton("๐ŸŒ Timezone", webApp = Some(WebAppInfo("https://timezones.johnspade.ru"))) + KeyboardButton("๐ŸŒ Timezone", webApp = Some(WebAppInfo(TimezonesAppUrl))) ) ), resizeKeyboard = true.some @@ -453,6 +399,64 @@ object TaskobotISpec extends ZIOSpecDefault: ) } + private val typeTask = + for + inlineQueryReply <- withTaskobotService( + _.onInlineQueryReply(InlineQuery("0", johnTg, query = "Buy some milk", offset = "0")) + ) + assertions = assertTrue( + inlineQueryReply.contains( + Methods.answerInlineQuery( + "0", + cacheTime = 0.some, + results = List( + InlineQueryResultArticle( + "1", + "Create task", + InputTextMessageContent( + "Buy some milk", + entities = MessageEntities().bold("Buy some milk").toTelegramEntities().map(OpenEnum(_)) + ), + InlineKeyboardMarkups + .singleColumn( + inlineKeyboardButton("Confirm task", ConfirmTask(id = None, senderId = john.id.some)), + InlineKeyboardButtons.url("\uD83D\uDE80 Taskobot", "https://t.me/tasko_bot") + ) + .some, + description = "Buy some milk".some + ) + ) + ) + ) + ) + yield assertions + + private def createTaskInteraction(taskId: Long) = + for + chosenInlineResultReply <- + withTaskobotService( + _.onChosenInlineResultReply( + ChosenInlineResult("0", johnTg, query = "Buy some milk", inlineMessageId = "0".some) + ) + ) + expectedEditMessageReplyMarkupReq = Methods.editMessageReplyMarkup( + inlineMessageId = "0".some, + replyMarkup = InlineKeyboardMarkups + .singleColumn( + inlineKeyboardButton("Confirm task", ConfirmTask(taskId.some, john.id.some)), + InlineKeyboardButtons.url("\uD83D\uDE80 Taskobot", "https://t.me/tasko_bot") + ) + .some + ) + yield assertTrue(chosenInlineResultReply.get.payload == expectedEditMessageReplyMarkupReq.payload) + + private def confirmTaskInteraction(taskId: Long) = sendCallbackQuery( + ConfirmTask(taskId.some, john.id.some), + kaitrinTg, + inlineMessageId = "0".some + ) + .map(confirmTaskReply => assertTrue(confirmTaskReply.contains(Methods.answerCallbackQuery("0")))) + private val env = ZLayer.make[MockServerClient & Taskobot]( TestDatabase.layer, TestBotApi.testApiLayer, diff --git a/src/test/scala/ru/johnspade/taskobot/TestBotApi.scala b/src/test/scala/ru/johnspade/taskobot/TestBotApi.scala index eb92094..c053e2c 100644 --- a/src/test/scala/ru/johnspade/taskobot/TestBotApi.scala +++ b/src/test/scala/ru/johnspade/taskobot/TestBotApi.scala @@ -10,6 +10,7 @@ import cats.syntax.option.* import com.dimafeng.testcontainers.MockServerContainer import org.http4s.blaze.client.BlazeClientBuilder import org.mockserver.client.MockServerClient +import org.mockserver.matchers.MatchType import org.mockserver.model.HttpRequest import org.mockserver.model.HttpRequest.request import org.mockserver.model.HttpResponse.response @@ -22,8 +23,8 @@ import telegramium.bots.high.messageentities.MessageEntities import ru.johnspade.taskobot.TelegramBotApi.TelegramBotApi import ru.johnspade.taskobot.TestUsers.* -import ru.johnspade.taskobot.core.TelegramOps.inlineKeyboardButton import ru.johnspade.taskobot.core.* +import ru.johnspade.taskobot.core.TelegramOps.inlineKeyboardButton import ru.johnspade.taskobot.messages.Language object TestBotApi: @@ -74,7 +75,7 @@ object TestBotApi: .when( request("/" + method.payload.name) .withMethod("POST") - .withBody(new JsonBody(method.payload.json.toString)) + .withBody(new JsonBody(method.payload.json.toString, MatchType.STRICT)) ) .respond(response().withBody(responseBody)) ) @@ -100,7 +101,7 @@ object TestBotApi: val listLanguages: Method[Either[Boolean, Message]] = Methods.editMessageText( "Current language: English", - ChatIntId(johnChatId).some, + chatId = ChatIntId(johnChatId).some, messageId = 0.some, replyMarkup = InlineKeyboardMarkups .singleColumn( @@ -119,7 +120,7 @@ object TestBotApi: val listLanguagesRussian: Method[Either[Boolean, Message]] = Methods.editMessageText( "ะขะตะบัƒั‰ะธะน ัะทั‹ะบ: ะ ัƒััะบะธะน", - ChatIntId(johnChatId).some, + chatId = ChatIntId(johnChatId).some, messageId = 0.some, replyMarkup = InlineKeyboardMarkups .singleColumn( @@ -145,7 +146,7 @@ object TestBotApi: List(KeyboardButtons.text("\uD83D\uDE80 ะะพะฒะฐั ัะพะฒะผะตัั‚ะฝะฐั ะทะฐะดะฐั‡ะฐ"), KeyboardButtons.text("โ“ ะกะฟั€ะฐะฒะบะฐ")), List( KeyboardButtons.text("โš™๏ธ ะะฐัั‚ั€ะพะนะบะธ"), - KeyboardButton("๐ŸŒ ะงะฐัะพะฒะพะน ะฟะพัั", webApp = Some(WebAppInfo("https://timezones.johnspade.ru"))) + KeyboardButton("๐ŸŒ ะงะฐัะพะฒะพะน ะฟะพัั", webApp = Some(WebAppInfo(TimezonesAppUrl))) ) ), resizeKeyboard = true.some @@ -162,7 +163,7 @@ object TestBotApi: List(KeyboardButtons.text("\uD83D\uDE80 New collaborative task"), KeyboardButtons.text("โ“ Help")), List( KeyboardButtons.text("โš™๏ธ Settings"), - KeyboardButton("๐ŸŒ Timezone", webApp = Some(WebAppInfo("https://timezones.johnspade.ru"))) + KeyboardButton("๐ŸŒ Timezone", webApp = Some(WebAppInfo(TimezonesAppUrl))) ) ), resizeKeyboard = true.some @@ -172,20 +173,29 @@ object TestBotApi: val removeReplyMarkup: Method[Either[Boolean, Message]] = Methods.editMessageReplyMarkup(inlineMessageId = "0".some) - val addConfirmButton: Method[Either[Boolean, Message]] = + def addConfirmButtons(taskId: Long): Method[Either[Boolean, Message]] = Methods.editMessageReplyMarkup( inlineMessageId = "0".some, replyMarkup = InlineKeyboardMarkups - .singleButton( - inlineKeyboardButton("Confirm task", ConfirmTask(1L.some, 1337L.some)) + .singleColumn( + inlineKeyboardButton("Confirm task", ConfirmTask(taskId.some, 1337L.some)), + InlineKeyboardButtons.url("\uD83D\uDE80 Taskobot", "https://t.me/tasko_bot") ) .some ) + val addTaskobotUrlButton: Method[Either[Boolean, Message]] = + Methods.editMessageReplyMarkup( + inlineMessageId = "0".some, + replyMarkup = InlineKeyboardMarkups + .singleButton(InlineKeyboardButtons.url("\uD83D\uDE80 Taskobot", "https://t.me/tasko_bot")) + .some + ) + val editMessageTextList: Method[Either[Boolean, Message]] = Methods.editMessageText( "Chat: John\n1. Buy some milk โ€“ John\n", - ChatIntId(kaitrinChatId).some, + chatId = ChatIntId(kaitrinChatId).some, messageId = 0.some, entities = MessageEntities() // format: off @@ -206,7 +216,7 @@ object TestBotApi: def editMessageTextCheckTask(chatId: Int): Method[Either[Boolean, Message]] = Methods.editMessageText( "Chat: John\n", - ChatIntId(chatId).some, + chatId = ChatIntId(chatId).some, messageId = 0.some, entities = MessageEntities() // format: off @@ -222,7 +232,7 @@ object TestBotApi: val editMessageTextPersonalTasks: Method[Either[Boolean, Message]] = Methods.editMessageText( "Chat: Personal tasks\n", - ChatIntId(johnChatId).some, + chatId = ChatIntId(johnChatId).some, messageId = 0.some, entities = MessageEntities() .plain("Chat: ") @@ -240,7 +250,7 @@ object TestBotApi: replyMarkup = InlineKeyboardMarkups .singleColumn( List.tabulate(5)(n => inlineKeyboardButton(n.toString, Tasks(0, n.toLong))) ++ - List(InlineKeyboardButtons.url("Buy me a coffee โ˜•", "https://buymeacoff.ee/johnspade")) + List(InlineKeyboardButtons.url("Buy me a coffee โ˜•", DonateUrl)) ) .some ) @@ -259,7 +269,7 @@ object TestBotApi: inlineKeyboardButton("Previous page", Chats(0)), inlineKeyboardButton("Next page", Chats(2)) ) ++ - List(InlineKeyboardButtons.url("Buy me a coffee โ˜•", "https://buymeacoff.ee/johnspade")) + List(InlineKeyboardButtons.url("Buy me a coffee โ˜•", DonateUrl)) ) .some ) @@ -305,11 +315,11 @@ object TestBotApi: replyMarkup = InlineKeyboardMarkup( List( List( - inlineKeyboardButton("1", TaskDetails(23L, 1)), - inlineKeyboardButton("2", TaskDetails(24L, 1)), - inlineKeyboardButton("3", TaskDetails(25L, 1)), - inlineKeyboardButton("4", TaskDetails(26L, 1)), - inlineKeyboardButton("5", TaskDetails(27L, 1)) + inlineKeyboardButton("1", TaskDetails(6L, 1)), + inlineKeyboardButton("2", TaskDetails(7L, 1)), + inlineKeyboardButton("3", TaskDetails(8L, 1)), + inlineKeyboardButton("4", TaskDetails(9L, 1)), + inlineKeyboardButton("5", TaskDetails(10L, 1)) ), List(inlineKeyboardButton("Previous page", Tasks(0, kaitrin.id))), List(inlineKeyboardButton("Next page", Tasks(2, kaitrin.id))), @@ -321,7 +331,7 @@ object TestBotApi: val editMessageTextTasksKaitrin: Method[Either[Boolean, Message]] = Methods.editMessageText( "Chat: Kaitrin\n", - ChatIntId(0).some, + chatId = ChatIntId(0).some, messageId = 0.some, entities = MessageEntities() // format: off @@ -333,20 +343,20 @@ object TestBotApi: val taskCompletedByJohnMessage: Method[Message] = Methods.sendMessage( - ChatIntId(kaitrinChatId), + chatId = ChatIntId(kaitrinChatId), """Task "Buy some milk" has been marked completed by John.""" ) val taskCompletedByKaitrinMessage: Method[Message] = Methods.sendMessage( - ChatIntId(johnChatId), + chatId = ChatIntId(johnChatId), """Task "Buy some milk" has been marked completed by Kaitrin.""" ) def editMessageTextTaskDetails(taskId: Long, now: Instant): Method[Either[Boolean, Message]] = val markup = InlineKeyboardMarkup( List( - List(inlineKeyboardButton("โœ…", CheckTask(0, taskId))), + List(inlineKeyboardButton("โœ…", CheckTask(0, taskId)), inlineKeyboardButton("๐Ÿ””", Reminders(taskId, 0))), List( inlineKeyboardButton( "๐Ÿ“…", @@ -437,7 +447,7 @@ object TestBotApi: def editMessageTextTaskDeadlineUpdated(taskId: Long, now: Instant): Method[Either[Boolean, Message]] = val markup = InlineKeyboardMarkup( List( - List(inlineKeyboardButton("โœ…", CheckTask(0, taskId))), + List(inlineKeyboardButton("โœ…", CheckTask(0, taskId)), inlineKeyboardButton("๐Ÿ””", Reminders(taskId, 0))), List( inlineKeyboardButton( "๐Ÿ“…", @@ -458,7 +468,7 @@ object TestBotApi: def editMessageTextTaskDeadlineRemoved(taskId: Long, now: Instant): Method[Either[Boolean, Message]] = val markup = InlineKeyboardMarkup( List( - List(inlineKeyboardButton("โœ…", CheckTask(0, taskId))), + List(inlineKeyboardButton("โœ…", CheckTask(0, taskId)), inlineKeyboardButton("๐Ÿ””", Reminders(taskId, 0))), List( inlineKeyboardButton( "๐Ÿ“…", @@ -479,7 +489,7 @@ object TestBotApi: def editMessageTextTimePicker(taskId: Long, now: Instant): Method[Either[Boolean, Message]] = val markup = InlineKeyboardMarkup( List( - List(inlineKeyboardButton("โœ…", CheckTask(0, taskId))), + List(inlineKeyboardButton("โœ…", CheckTask(0, taskId)), inlineKeyboardButton("๐Ÿ””", Reminders(taskId, 0))), List( inlineKeyboardButton( "๐Ÿ“…", @@ -519,7 +529,7 @@ object TestBotApi: .toTelegramEntities(), replyMarkup = InlineKeyboardMarkup( List( - List(inlineKeyboardButton("โœ…", CheckTask(0, taskId))), + List(inlineKeyboardButton("โœ…", CheckTask(0, taskId)), inlineKeyboardButton("๐Ÿ””", Reminders(taskId, 0))), List( inlineKeyboardButton( "๐Ÿ“…", @@ -549,7 +559,7 @@ object TestBotApi: |๐Ÿ•’ Due date: ${dueDate.getOrElse("-")} | |Created at: 1970-01-01 00:00""".stripMargin, - ChatIntId(0).some, + chatId = ChatIntId(0).some, messageId = 0.some, entities = MessageEntities() // format: off diff --git a/src/test/scala/ru/johnspade/taskobot/TestHelpers.scala b/src/test/scala/ru/johnspade/taskobot/TestHelpers.scala index d2f3317..46bb205 100644 --- a/src/test/scala/ru/johnspade/taskobot/TestHelpers.scala +++ b/src/test/scala/ru/johnspade/taskobot/TestHelpers.scala @@ -6,7 +6,7 @@ import telegramium.bots.BotCommandMessageEntity import telegramium.bots.CallbackQuery import telegramium.bots.Chat import telegramium.bots.Message -import telegramium.bots.{User => TgUser} +import telegramium.bots.User as TgUser import ru.johnspade.taskobot.TestUsers.johnChatId import ru.johnspade.taskobot.TestUsers.johnTg diff --git a/src/test/scala/ru/johnspade/taskobot/TestUsers.scala b/src/test/scala/ru/johnspade/taskobot/TestUsers.scala index 63f04fc..f5f6679 100644 --- a/src/test/scala/ru/johnspade/taskobot/TestUsers.scala +++ b/src/test/scala/ru/johnspade/taskobot/TestUsers.scala @@ -1,7 +1,7 @@ package ru.johnspade.taskobot -import cats.syntax.option._ -import telegramium.bots.{User => TgUser} +import cats.syntax.option.* +import telegramium.bots.User as TgUser import ru.johnspade.taskobot.core.TelegramOps.toUser import ru.johnspade.taskobot.user.User diff --git a/src/test/scala/ru/johnspade/taskobot/core/CbDataSpec.scala b/src/test/scala/ru/johnspade/taskobot/core/CbDataSpec.scala index b34fea8..7464050 100644 --- a/src/test/scala/ru/johnspade/taskobot/core/CbDataSpec.scala +++ b/src/test/scala/ru/johnspade/taskobot/core/CbDataSpec.scala @@ -1,9 +1,9 @@ package ru.johnspade.taskobot.core import zio.Scope +import zio.test.* import zio.test.Assertion.equalTo import zio.test.Assertion.isRight -import zio.test.* import cats.syntax.option.* diff --git a/src/test/scala/ru/johnspade/taskobot/core/PageSpec.scala b/src/test/scala/ru/johnspade/taskobot/core/PageSpec.scala index 5cdc9c5..0b77bbf 100644 --- a/src/test/scala/ru/johnspade/taskobot/core/PageSpec.scala +++ b/src/test/scala/ru/johnspade/taskobot/core/PageSpec.scala @@ -1,7 +1,7 @@ package ru.johnspade.taskobot.core -import zio.test.Assertion._ -import zio.test._ +import zio.test.* +import zio.test.Assertion.* import cats.Id diff --git a/src/test/scala/ru/johnspade/taskobot/datetime/DatePickerServiceSpec.scala b/src/test/scala/ru/johnspade/taskobot/datetime/DatePickerServiceSpec.scala index cd6c322..32cb15a 100644 --- a/src/test/scala/ru/johnspade/taskobot/datetime/DatePickerServiceSpec.scala +++ b/src/test/scala/ru/johnspade/taskobot/datetime/DatePickerServiceSpec.scala @@ -4,13 +4,13 @@ import java.time.* import zio.ZIO import zio.ZLayer -import zio.test.Assertion.* import zio.test.* +import zio.test.Assertion.* import telegramium.bots.* -import ru.johnspade.taskobot.core.TelegramOps.inlineKeyboardButton import ru.johnspade.taskobot.core.* +import ru.johnspade.taskobot.core.TelegramOps.inlineKeyboardButton import ru.johnspade.taskobot.messages.* object DatePickerServiceSpec extends ZIOSpecDefault: diff --git a/src/test/scala/ru/johnspade/taskobot/datetime/TimePickerServiceSpec.scala b/src/test/scala/ru/johnspade/taskobot/datetime/TimePickerServiceSpec.scala index 6482980..d85c4c3 100644 --- a/src/test/scala/ru/johnspade/taskobot/datetime/TimePickerServiceSpec.scala +++ b/src/test/scala/ru/johnspade/taskobot/datetime/TimePickerServiceSpec.scala @@ -1,8 +1,8 @@ package ru.johnspade.taskobot.datetime import zio.* -import zio.test.Assertion.* import zio.test.* +import zio.test.Assertion.* import telegramium.bots.InlineKeyboardMarkup diff --git a/src/test/scala/ru/johnspade/taskobot/scheduled/ReminderNotificationServiceSpec.scala b/src/test/scala/ru/johnspade/taskobot/scheduled/ReminderNotificationServiceSpec.scala index ad5a8c3..246c77d 100644 --- a/src/test/scala/ru/johnspade/taskobot/scheduled/ReminderNotificationServiceSpec.scala +++ b/src/test/scala/ru/johnspade/taskobot/scheduled/ReminderNotificationServiceSpec.scala @@ -4,19 +4,20 @@ import java.time.Instant import java.time.LocalDateTime import zio.* -import zio.test.TestAspect.* import zio.test.* +import zio.test.TestAspect.* import org.mockserver.client.MockServerClient +import ru.johnspade.taskobot.BotConfig import ru.johnspade.taskobot.BotServiceLive import ru.johnspade.taskobot.CleanupRepository import ru.johnspade.taskobot.CleanupRepositoryLive import ru.johnspade.taskobot.KeyboardServiceLive import ru.johnspade.taskobot.TestBotApi +import ru.johnspade.taskobot.TestBotApi.* import ru.johnspade.taskobot.TestBotApi.Mocks.* import ru.johnspade.taskobot.TestBotApi.Mocks.sendMessageReminder -import ru.johnspade.taskobot.TestBotApi.* import ru.johnspade.taskobot.TestDatabase import ru.johnspade.taskobot.TestUsers.* import ru.johnspade.taskobot.UTC @@ -47,7 +48,8 @@ object ReminderNotificationServiceSpec extends ZIOSpecDefault: BotServiceLive.layer, KeyboardServiceLive.layer, ReminderNotificationServiceLive.layer, - ReminderServiceLive.layer + ReminderServiceLive.layer, + BotConfig.live ) def spec = (suite("ReminderNotificationServiceSpec")( @@ -93,13 +95,8 @@ object ReminderNotificationServiceSpec extends ZIOSpecDefault: _ <- UserRepository.createOrUpdate(john).orDie _ <- UserRepository.createOrUpdate(kaitrin).orDie yield () - } @@ TestAspect.after { - for - _ <- CleanupRepository.clearReminders() - _ <- CleanupRepository.clearTasks() - _ <- CleanupRepository.clearUsers() - yield () - }).provideShared(testEnv) + } @@ TestAspect.after(CleanupRepository.truncateTables())) + .provideShared(testEnv) private val remindersTableEmpty = ReminderRepository.getEnqueued().map(_.isEmpty) diff --git a/src/test/scala/ru/johnspade/taskobot/scheduled/ReminderServiceSpec.scala b/src/test/scala/ru/johnspade/taskobot/scheduled/ReminderServiceSpec.scala index b4a21e3..16c7da1 100644 --- a/src/test/scala/ru/johnspade/taskobot/scheduled/ReminderServiceSpec.scala +++ b/src/test/scala/ru/johnspade/taskobot/scheduled/ReminderServiceSpec.scala @@ -1,21 +1,23 @@ package ru.johnspade.taskobot.scheduled +import java.time.Instant import java.time.LocalDateTime import zio.* +import zio.test.* import zio.test.Assertion.* import zio.test.TestAspect.* -import zio.test.* import org.mockserver.client.MockServerClient +import ru.johnspade.taskobot.BotConfig import ru.johnspade.taskobot.BotServiceLive import ru.johnspade.taskobot.CleanupRepository import ru.johnspade.taskobot.CleanupRepositoryLive import ru.johnspade.taskobot.KeyboardServiceLive import ru.johnspade.taskobot.TestBotApi -import ru.johnspade.taskobot.TestBotApi.Mocks.* import ru.johnspade.taskobot.TestBotApi.* +import ru.johnspade.taskobot.TestBotApi.Mocks.* import ru.johnspade.taskobot.TestDatabase import ru.johnspade.taskobot.TestUsers.* import ru.johnspade.taskobot.UTC @@ -44,7 +46,8 @@ object ReminderServiceSpec extends ZIOSpecDefault: MessageServiceLive.layer, BotServiceLive.layer, KeyboardServiceLive.layer, - ReminderServiceLive.layer + ReminderServiceLive.layer, + BotConfig.live ) private val createTaskAndReminder = @@ -116,13 +119,14 @@ object ReminderServiceSpec extends ZIOSpecDefault: "error_code": 403 } """ + val text = "Leave me alone" for now <- Clock.instant - task <- createTask("Homework assignment", Some(kaitrin.id)) + task <- createTask(text, Some(kaitrin.id)) taskWithDeadline <- TaskRepository .setDeadline(task.id, Some(LocalDateTime.from(now.atZone(UTC)).plusMinutes(1L)), john.id) reminder <- ReminderRepository.create(task.id, john.id, offsetMinutes = 0) - _ <- createMock(sendMessageReminder("Homework assignment", task.id, now), errorResponse) + _ <- createMock(sendMessageReminder(text, task.id, now), errorResponse) _ <- ReminderService.sendReminder(reminder, taskWithDeadline, john) updatedUser <- UserRepository.findById(john.id) yield assertTrue(updatedUser.flatMap(_.blockedBot).contains(true)) @@ -152,16 +156,12 @@ object ReminderServiceSpec extends ZIOSpecDefault: ) @@ sequential @@ before { for + _ <- TestClock.setTime(Instant.EPOCH) _ <- UserRepository.createOrUpdate(john).orDie _ <- UserRepository.createOrUpdate(kaitrin).orDie yield () - } @@ TestAspect.after { - for - _ <- CleanupRepository.clearReminders() - _ <- CleanupRepository.clearTasks() - _ <- CleanupRepository.clearUsers() - yield () - }).provideShared(testEnv) + } @@ after(CleanupRepository.truncateTables())) + .provideShared(testEnv) private def createTask(text: String, receiver: Option[Long]) = for diff --git a/src/test/scala/ru/johnspade/taskobot/settings/SettingsControllerSpec.scala b/src/test/scala/ru/johnspade/taskobot/settings/SettingsControllerSpec.scala index da7ff2a..cc709b8 100644 --- a/src/test/scala/ru/johnspade/taskobot/settings/SettingsControllerSpec.scala +++ b/src/test/scala/ru/johnspade/taskobot/settings/SettingsControllerSpec.scala @@ -1,9 +1,9 @@ package ru.johnspade.taskobot.settings import zio.* +import zio.test.* import zio.test.Assertion.equalTo import zio.test.Assertion.hasField -import zio.test.* import cats.syntax.option.* import org.mockserver.client.MockServerClient @@ -11,6 +11,7 @@ import ru.johnspade.tgbot.callbackqueries.CallbackQueryData import ru.johnspade.tgbot.callbackqueries.ContextCallbackQuery import telegramium.bots.high.* +import ru.johnspade.taskobot.BotConfig import ru.johnspade.taskobot.KeyboardServiceLive import ru.johnspade.taskobot.TestBotApi import ru.johnspade.taskobot.TestBotApi.Mocks @@ -85,5 +86,6 @@ object SettingsControllerSpec extends ZIOSpecDefault: MsgConfig.live, MessageServiceLive.layer, KeyboardServiceLive.layer, - SettingsControllerLive.layer + SettingsControllerLive.layer, + BotConfig.live ) diff --git a/src/test/scala/ru/johnspade/taskobot/task/TaskControllerSpec.scala b/src/test/scala/ru/johnspade/taskobot/task/TaskControllerSpec.scala index 8eebeba..4a7a7c8 100644 --- a/src/test/scala/ru/johnspade/taskobot/task/TaskControllerSpec.scala +++ b/src/test/scala/ru/johnspade/taskobot/task/TaskControllerSpec.scala @@ -5,16 +5,17 @@ import java.time.LocalDate import java.time.LocalDateTime import zio.* +import zio.test.* import zio.test.Assertion.* import zio.test.TestAspect.* -import zio.test.* import cats.syntax.option.* import org.mockserver.client.MockServerClient import ru.johnspade.tgbot.callbackqueries.* +import telegramium.bots.User as TgUser import telegramium.bots.high.Methods -import telegramium.bots.{User as TgUser} +import ru.johnspade.taskobot.BotConfig import ru.johnspade.taskobot.BotServiceLive import ru.johnspade.taskobot.CleanupRepository import ru.johnspade.taskobot.CleanupRepositoryLive @@ -26,10 +27,10 @@ import ru.johnspade.taskobot.TestDatabase import ru.johnspade.taskobot.TestHelpers.callbackQuery import ru.johnspade.taskobot.TestUsers.* import ru.johnspade.taskobot.UTC +import ru.johnspade.taskobot.core.* import ru.johnspade.taskobot.core.CreateReminder import ru.johnspade.taskobot.core.RemoveReminder import ru.johnspade.taskobot.core.TelegramOps.toUser -import ru.johnspade.taskobot.core.* import ru.johnspade.taskobot.messages.* import ru.johnspade.taskobot.user.User import ru.johnspade.taskobot.user.UserRepository @@ -115,8 +116,8 @@ object TaskControllerSpec extends ZIOSpecDefault: suite("ConfirmTask")( test("receiver should be able to confirm task") { for - _ <- createMock(Mocks.removeReplyMarkup, Mocks.messageResponse) task <- createTask("Buy some milk") + _ <- createMock(Mocks.addTaskobotUrlButton, Mocks.messageResponse) reply <- confirmTask(ConfirmTask(task.id.some, john.id.some), kaitrinTg) confirmedTask <- TaskRepository.findById(task.id) confirmedTaskAssertions = assertTrue(confirmedTask.get.receiver.contains(kaitrin.id)) @@ -126,6 +127,7 @@ object TaskControllerSpec extends ZIOSpecDefault: test("sender should not be able to confirm task") { for task <- createTask("Buy groceries") + _ <- createMock(Mocks.addTaskobotUrlButton, Mocks.messageResponse) reply <- confirmTask(ConfirmTask(task.id.some, john.id.some), johnTg) unconfirmedTask <- TaskRepository.findById(task.id) confirmTaskReplyAssertions = assertTrue( @@ -137,6 +139,7 @@ object TaskControllerSpec extends ZIOSpecDefault: test("cannot confirm task with wrong senderId") { for task <- createTask("Buy some bread") + _ <- createMock(Mocks.addTaskobotUrlButton, Mocks.messageResponse) bobId = 0L reply <- confirmTask(ConfirmTask(task.id.some, bobId.some), TgUser(bobId, isBot = false, "Bob")) unconfirmedTask <- TaskRepository.findById(task.id) @@ -269,13 +272,7 @@ object TaskControllerSpec extends ZIOSpecDefault: _ <- UserRepository.createOrUpdate(kaitrin).orDie yield () } - @@ TestAspect.after { - for - _ <- CleanupRepository.clearReminders() - _ <- CleanupRepository.clearTasks() - _ <- CleanupRepository.clearUsers() - yield () - }) + @@ TestAspect.after(CleanupRepository.truncateTables())) .provideShared(env) private val firstPage = 0 @@ -344,5 +341,6 @@ object TaskControllerSpec extends ZIOSpecDefault: MessageServiceLive.layer, KeyboardServiceLive.layer, BotServiceLive.layer, - TaskControllerLive.layer + TaskControllerLive.layer, + BotConfig.live )