diff --git a/project/MornyConfiguration.scala b/project/MornyConfiguration.scala
index 9d8a1d2..6ec9177 100644
--- a/project/MornyConfiguration.scala
+++ b/project/MornyConfiguration.scala
@@ -8,7 +8,7 @@ object MornyConfiguration {
val MORNY_CODE_STORE = "https://github.com/Eyre-S/Coeur-Morny-Cono"
val MORNY_COMMIT_PATH = "https://github.com/Eyre-S/Coeur-Morny-Cono/commit/%s"
- val VERSION = "2.0.0-alpha16"
+ val VERSION = "2.0.0-alpha17"
val VERSION_DELTA: Option[String] = None
val CODENAME = "guanggu"
diff --git a/src/main/scala/cc/sukazyo/cono/morny/core/MornyCoeur.scala b/src/main/scala/cc/sukazyo/cono/morny/core/MornyCoeur.scala
index cb6b6c5..53748a5 100644
--- a/src/main/scala/cc/sukazyo/cono/morny/core/MornyCoeur.scala
+++ b/src/main/scala/cc/sukazyo/cono/morny/core/MornyCoeur.scala
@@ -5,7 +5,7 @@ import cc.sukazyo.cono.morny.core.MornyCoeur.*
import cc.sukazyo.cono.morny.core.bot.api.{EventListenerManager, MornyCommandManager, MornyQueryManager}
import cc.sukazyo.cono.morny.core.bot.api.messages.ThreadingManager
import cc.sukazyo.cono.morny.core.bot.event.{MornyOnInlineQuery, MornyOnTelegramCommand, MornyOnUpdateTimestampOffsetLock}
-import cc.sukazyo.cono.morny.core.bot.internal.ThreadingManagerImpl
+import cc.sukazyo.cono.morny.core.bot.internal.{ErrorMessageManager, ThreadingManagerImpl}
import cc.sukazyo.cono.morny.core.http.api.{HttpServer, MornyHttpServerContext}
import cc.sukazyo.cono.morny.core.http.internal.MornyHttpServerContextImpl
import cc.sukazyo.cono.morny.reporter.MornyReport
@@ -171,6 +171,7 @@ class MornyCoeur (modules: List[MornyModule])(using val config: MornyConfig)(tes
val trusted: MornyTrusted = MornyTrusted()
private val _messageThreading: ThreadingManagerImpl = ThreadingManagerImpl(using account)
val messageThreading: ThreadingManager = _messageThreading
+ val errorMessageManager: ErrorMessageManager = ErrorMessageManager()
val eventManager: EventListenerManager = EventListenerManager()
val commands: MornyCommandManager = MornyCommandManager()
@@ -211,6 +212,7 @@ class MornyCoeur (modules: List[MornyModule])(using val config: MornyConfig)(tes
DirectMsgClear(),
_messageThreading.CancelCommand,
+ errorMessageManager.ShowErrorMessageCommand,
)
}
diff --git a/src/main/scala/cc/sukazyo/cono/morny/core/bot/api/BotExtension.scala b/src/main/scala/cc/sukazyo/cono/morny/core/bot/api/BotExtension.scala
new file mode 100644
index 0000000..fdb5cc2
--- /dev/null
+++ b/src/main/scala/cc/sukazyo/cono/morny/core/bot/api/BotExtension.scala
@@ -0,0 +1,23 @@
+package cc.sukazyo.cono.morny.core.bot.api
+
+import cc.sukazyo.cono.morny.core.MornyCoeur
+import cc.sukazyo.cono.morny.core.bot.api.messages.ErrorMessage
+
+/** Bot extensions for Morny feature.
+ */
+object BotExtension {
+
+ extension (errorMessage: ErrorMessage[?, ?]) {
+
+ /** Submit this [[ErrorMessage]] to a [[MornyCoeur]].
+ *
+ * Will send this [[ErrorMessage]] with the basic send config.
+ *
+ * @see [[cc.sukazyo.cono.morny.core.bot.internal.ErrorMessageManager.sendErrorMessage]]
+ */
+ def submit (using coeur: MornyCoeur): Unit =
+ coeur.errorMessageManager.sendErrorMessage(errorMessage)
+
+ }
+
+}
diff --git a/src/main/scala/cc/sukazyo/cono/morny/core/bot/api/messages/ErrorMessage.scala b/src/main/scala/cc/sukazyo/cono/morny/core/bot/api/messages/ErrorMessage.scala
new file mode 100644
index 0000000..ee4ddb7
--- /dev/null
+++ b/src/main/scala/cc/sukazyo/cono/morny/core/bot/api/messages/ErrorMessage.scala
@@ -0,0 +1,71 @@
+package cc.sukazyo.cono.morny.core.bot.api.messages
+
+import com.pengrad.telegrambot.request.AbstractSendRequest
+
+/** A error message based on Telegram's [[AbstractSendRequest]].
+ *
+ * Contains two types ([[simple]] and [[complex]]) message that
+ * allows to choose one to show in different cases.
+ *
+ * Also there contains a [[context]] (typed with [[MessagingContext.WithMessage]])
+ * that infers the source of this error message, also can be used for
+ * the unique key of one error message.
+ *
+ * There's also a [[ErrorMessage.Types]] enum infers to the two type. You
+ * can use [[getByType]] (or [[getByTypeNormal]]) method to get the specific
+ * type's message of that type.
+ *
+ * @since 2.0.0
+ *
+ * @see [[cc.sukazyo.cono.morny.core.bot.internal.ErrorMessageManager]] This
+ * ErrorMessage's manager (consumer) implementation by the Morny Coeur.
+ * @see [[cc.sukazyo.cono.morny.core.bot.api.BotExtension.submit]] Simple
+ * way to consume this.
+ *
+ * @tparam T1 Type of the simple error message
+ * @tparam T2 Type of the complex error message
+ */
+trait ErrorMessage[T1 <: AbstractSendRequest[T1], T2 <: AbstractSendRequest[T2]] {
+
+ val simple: T1
+ val complex: T2
+
+ val context: MessagingContext.WithMessage
+
+ /** Get the simple or complex message by the given [[ErrorMessage.Types]] infer.
+ *
+ * This method returns a union type [[T1]] and [[T2]]. This may be
+ * not useful when you don't care about the specific return types (maybe for
+ * the most times). You can use [[getByTypeNormal]] instead.
+ */
+ def getByType (t: ErrorMessage.Types): AbstractSendRequest[T1] | AbstractSendRequest[T2] =
+ t match
+ case ErrorMessage.Types.Simple => simple
+ case ErrorMessage.Types.Complex => complex
+
+ /** Get the simple or complex message by the given [[ErrorMessage.Types]] infer.
+ *
+ * This works exactly the same with the [[getByType]], the only difference is
+ * this returns with a bit universal type `AbstractSendRequest[?]`.
+ *
+ * @see [[getByType]]
+ */
+ def getByTypeNormal (t: ErrorMessage.Types): AbstractSendRequest[?] =
+ getByType(t)
+
+}
+
+object ErrorMessage {
+
+ enum Types:
+ case Simple
+ case Complex
+
+ def apply [T1 <: AbstractSendRequest[T1], T2<: AbstractSendRequest[T2]]
+ (_simple: T1, _complex: T2)(using cxt: MessagingContext.WithMessage): ErrorMessage[T1, T2] =
+ new ErrorMessage[T1, T2]:
+ override val simple: T1 = _simple
+ override val complex: T2 = _complex
+ override val context: MessagingContext.WithMessage = cxt
+
+}
diff --git a/src/main/scala/cc/sukazyo/cono/morny/core/bot/api/messages/MessagingContext.scala b/src/main/scala/cc/sukazyo/cono/morny/core/bot/api/messages/MessagingContext.scala
index 601ed56..7a70892 100644
--- a/src/main/scala/cc/sukazyo/cono/morny/core/bot/api/messages/MessagingContext.scala
+++ b/src/main/scala/cc/sukazyo/cono/morny/core/bot/api/messages/MessagingContext.scala
@@ -1,5 +1,6 @@
package cc.sukazyo.cono.morny.core.bot.api.messages
+import cc.sukazyo.cono.morny.util.tgapi.Standardize.*
import com.pengrad.telegrambot.model.{Chat, Message, User}
/**
@@ -7,35 +8,65 @@ import com.pengrad.telegrambot.model.{Chat, Message, User}
*/
trait MessagingContext:
val bind_chat: Chat
+ def toChatKey: MessagingContext.Key =
+ MessagingContext.Key(this)
/**
* @since 2.0.0
*/
object MessagingContext {
- given String = "aaa"
-
+ case class Key (chatId: ChatID)
+ object Key:
+ def apply (it: MessagingContext): Key = Key(it.bind_chat.id)
def apply (_chat: Chat): MessagingContext =
new MessagingContext:
override val bind_chat: Chat = _chat
+
trait WithUser extends MessagingContext:
val bind_user: User
+ def toChatUserKey: WithUser.Key =
+ WithUser.Key(this)
+ object WithUser:
+ case class Key (chatID: ChatID, userId: UserID)
+ object Key:
+ def apply (it: WithUser): Key = Key(it.bind_chat.id, it.bind_user.id)
+ def apply (_chat: Chat, _user: User): WithUser =
+ new WithUser:
+ override val bind_chat: Chat = _chat
+ override val bind_user: User = _user
def apply (_chat: Chat, _user: User): WithUser =
- new WithUser:
- override val bind_chat: Chat = _chat
- override val bind_user: User = _user
+ WithUser(_chat, _user)
+
trait WithMessage extends MessagingContext:
val bind_message: Message
+ def toChatMessageKey: WithMessage.Key =
+ WithMessage.Key(this)
+ object WithMessage:
+ case class Key (chatId: ChatID, messageId: MessageID)
+ object Key:
+ def apply (it: WithMessage): Key = Key(it.bind_chat.id, it.bind_message.messageId)
+ def apply (_chat: Chat, _message: Message): WithMessage =
+ new WithMessage:
+ override val bind_chat: Chat = _chat
+ override val bind_message: Message = _message
def apply (_chat: Chat, _message: Message): WithMessage =
- new WithMessage:
- override val bind_chat: Chat = _chat
- override val bind_message: Message = _message
- trait WithUserAndMessage extends MessagingContext with WithMessage with WithUser
+ WithMessage(_chat, _message)
+
+ trait WithUserAndMessage extends MessagingContext with WithMessage with WithUser:
+ def toChatUserMessageKey: WithUserAndMessage.Key =
+ WithUserAndMessage.Key(this)
+ object WithUserAndMessage:
+ case class Key (chatId: ChatID, userId: UserID, messageId: MessageID)
+ object Key:
+ def apply (it: WithUserAndMessage): Key = Key(it.bind_chat.id, it.bind_user.id, it.bind_message.messageId)
+ def apply (_chat: Chat, _user: User, _message: Message): WithUserAndMessage =
+ new WithUserAndMessage:
+ override val bind_chat: Chat = _chat
+ override val bind_user: User = _user
+ override val bind_message: Message = _message
def apply (_chat: Chat, _user: User, _message: Message): WithUserAndMessage =
- new WithUserAndMessage:
- override val bind_chat: Chat = _chat
- override val bind_user: User = _user
- override val bind_message: Message = _message
+ WithUserAndMessage(_chat, _user, _message)
/** Extract a message context from a message (or message event).
*
diff --git a/src/main/scala/cc/sukazyo/cono/morny/core/bot/command/MornyInformation.scala b/src/main/scala/cc/sukazyo/cono/morny/core/bot/command/MornyInformation.scala
index fc0cc3a..977af0d 100644
--- a/src/main/scala/cc/sukazyo/cono/morny/core/bot/command/MornyInformation.scala
+++ b/src/main/scala/cc/sukazyo/cono/morny/core/bot/command/MornyInformation.scala
@@ -2,6 +2,7 @@ package cc.sukazyo.cono.morny.core.bot.command
import cc.sukazyo.cono.morny.core.{MornyCoeur, MornySystem}
import cc.sukazyo.cono.morny.core.bot.api.{ICommandAlias, ITelegramCommand}
+import cc.sukazyo.cono.morny.core.bot.api.messages.{ErrorMessage, MessagingContext}
import cc.sukazyo.cono.morny.data.MornyInformation.*
import cc.sukazyo.cono.morny.data.TelegramStickers
import cc.sukazyo.cono.morny.reporter.MornyReport
@@ -36,7 +37,7 @@ class MornyInformation (using coeur: MornyCoeur) extends ITelegramCommand {
override def execute (using command: InputCommand, event: Update): Unit = {
if (command.args isEmpty) {
- echoInfo(event.message.chat.id, event.message.messageId)
+ echoInfo(event)
return
}
@@ -53,9 +54,24 @@ class MornyInformation (using coeur: MornyCoeur) extends ITelegramCommand {
}
- private def echoInfo (chatId: Long, replyTo: Int): Unit = {
+ private def echoInfo (update: Update): Unit = {
+ val cxt = MessagingContext.extract(using update.message)
+ val cxtReplied = Option(update.message.replyToMessage).map(MessagingContext.extract(using _))
+
+ cxtReplied match
+ case None =>
+ case Some(_cxtReplied) =>
+ // check if theres associated information about error message on the replied context.
+ // if there really is, that means the replied message is a error message,
+ // so the error message's complex information will be sent
+ coeur.errorMessageManager.inspectMessage(_cxtReplied.toChatMessageKey) match
+ case None =>
+ case Some(errMessage) =>
+ coeur.errorMessageManager.sendErrorMessage(errMessage, ErrorMessage.Types.Complex, Some(cxt))
+
+ // if theres no any associated information on the context
SendPhoto(
- chatId,
+ cxt.bind_chat.id,
getAboutPic
).caption(
s"""Morny Cono
@@ -63,8 +79,9 @@ class MornyInformation (using coeur: MornyCoeur) extends ITelegramCommand {
|————————————————
|$getMornyAboutLinksHTML"""
.stripMargin
- ).parseMode(ParseMode HTML).replyToMessageId(replyTo)
+ ).parseMode(ParseMode HTML).replyToMessageId(cxt.bind_message.messageId)
.unsafeExecute
+
}
private def echoStickers (using command: InputCommand, event: Update): Unit = {
diff --git a/src/main/scala/cc/sukazyo/cono/morny/core/bot/internal/ErrorMessageManager.scala b/src/main/scala/cc/sukazyo/cono/morny/core/bot/internal/ErrorMessageManager.scala
new file mode 100644
index 0000000..0472bcf
--- /dev/null
+++ b/src/main/scala/cc/sukazyo/cono/morny/core/bot/internal/ErrorMessageManager.scala
@@ -0,0 +1,104 @@
+package cc.sukazyo.cono.morny.core.bot.internal
+
+import cc.sukazyo.cono.morny.core.MornyCoeur
+import cc.sukazyo.cono.morny.core.bot.api.{ICommandAlias, ISimpleCommand}
+import cc.sukazyo.cono.morny.core.bot.api.messages.{ErrorMessage, MessagingContext}
+import cc.sukazyo.cono.morny.util.schedule.{DelayedTask, Scheduler}
+import cc.sukazyo.cono.morny.util.tgapi.InputCommand
+import cc.sukazyo.cono.morny.util.tgapi.TelegramExtensions.Requests.unsafeExecute
+import cc.sukazyo.cono.morny.util.EpochDateTime.DurationMillis
+import com.pengrad.telegrambot.TelegramBot
+import com.pengrad.telegrambot.model.Update
+import com.pengrad.telegrambot.request.{AbstractSendRequest, SendMessage}
+
+class ErrorMessageManager (using coeur: MornyCoeur) {
+ given TelegramBot = coeur.account
+
+ private val expiresDuration: DurationMillis = 1000 * 60 * 60 * 5 // 5 hours
+
+ private val errorMessageMap = collection.mutable.Map[MessagingContext.WithMessage.Key, ErrorMessage[?, ?]]()
+ private val errorMessageMapCleaner = Scheduler(isDaemon = true)
+ private def putErrorMessage (key: MessagingContext.WithMessage.Key, message: ErrorMessage[?, ?]): Unit =
+ errorMessageMap.synchronized: // remove an error message after the given expires duration.
+ errorMessageMap += (key -> message)
+ errorMessageMapCleaner ++ DelayedTask("", expiresDuration, {
+ errorMessageMap.remove(key)
+ })
+
+ /** Send an [[ErrorMessage]] and add this error message to ErrorMessage stash.
+ *
+ * This will execute the SendRequest using the default send config.
+ */
+ def sendErrorMessage (message: ErrorMessage[?, ?]): Unit =
+ // todo: which should be sent using user config
+ val useType = ErrorMessage.Types.Simple
+ sendErrorMessage(message, useType, isNewMessage = true)
+
+ /** Send an [[ErrorMessage]] and add it to the stash if it is new.
+ *
+ * @param message The [[ErrorMessage]] to be sent.
+ * @param useType Which type of the message should be sent.
+ * @param injectedSendContext A context that determine where and how the message should
+ * be sent. Currently only the message id in this context will
+ * be used to determine the reply to message.
+ *
+ * This is part of the API limitation, but due to in normal cases,
+ * the source error message must be in the same chat so it may
+ * make sense.
+ * @param isNewMessage Is this ErrorMessage is a new error message, otherwise it should be
+ * an existed error message that just calling this method because of
+ * need to be resent some messages.
+ *
+ * If this is new, the message will be added to the stash, if not, it
+ * should be comes from the stash and will not be added to the stash again.
+ *
+ * Default is false.
+ */
+ def sendErrorMessage (
+ message: ErrorMessage[?, ?],
+ useType: ErrorMessage.Types,
+ injectedSendContext: Option[MessagingContext.WithMessage] = None,
+ isNewMessage: Boolean = false
+ ): Unit = {
+ val sendMessage = message
+ .getByTypeNormal(useType)
+ .asInstanceOf[AbstractSendRequest[Nothing]]
+ injectedSendContext match
+ case None =>
+ case Some(cxt) =>
+ sendMessage.replyToMessageId(cxt.bind_message.messageId)
+ val response = sendMessage.unsafeExecute
+ if isNewMessage then
+ val key = MessagingContext.extract(using response.message).toChatMessageKey
+ putErrorMessage(key, message)
+ }
+
+ /** Get the stashed [[ErrorMessage]] associated with the [[MessagingContext.WithMessage.Key message key]].
+ *
+ * If the message key does not associated with any error message in the stash, then
+ * [[None]] will be returned.
+ *
+ * Notice that one [[ErrorMessage]] will only be stashed for 5 hours then it will be
+ * cleaned up from the stash.
+ */
+ def inspectMessage (messageKey: MessagingContext.WithMessage.Key): Option[ErrorMessage[?, ?]] =
+ errorMessageMap.get(messageKey)
+
+ object ShowErrorMessageCommand extends ISimpleCommand {
+ override val name: String = "inspect"
+ override val aliases: List[ICommandAlias] = Nil
+ override def execute (using command: InputCommand, event: Update): Unit =
+ val cxt = MessagingContext.extract(using event.message)
+ val cxtReplied = Option(event.message.replyToMessage).map(MessagingContext.extract(using _))
+ errorMessageMap.get(cxtReplied.map(_.toChatMessageKey).orNull) match
+ case Some(msg) =>
+ sendErrorMessage(msg, ErrorMessage.Types.Complex, Some(cxt))
+ case None =>
+ SendMessage(
+ cxt.bind_chat.id,
+ "Not a error message."
+ ).replyToMessageId(cxt.bind_message.messageId)
+ .unsafeExecute
+ }
+
+}
diff --git a/src/main/scala/cc/sukazyo/cono/morny/morny_misc/Testing.scala b/src/main/scala/cc/sukazyo/cono/morny/morny_misc/Testing.scala
index 5a25fc7..92919ce 100644
--- a/src/main/scala/cc/sukazyo/cono/morny/morny_misc/Testing.scala
+++ b/src/main/scala/cc/sukazyo/cono/morny/morny_misc/Testing.scala
@@ -2,12 +2,14 @@ package cc.sukazyo.cono.morny.morny_misc
import cc.sukazyo.cono.morny.core.MornyCoeur
import cc.sukazyo.cono.morny.core.bot.api.{ICommandAlias, ISimpleCommand}
-import cc.sukazyo.cono.morny.core.bot.api.messages.MessagingContext
+import cc.sukazyo.cono.morny.core.bot.api.messages.{ErrorMessage, MessagingContext}
+import cc.sukazyo.cono.morny.core.bot.api.BotExtension.submit
+import cc.sukazyo.cono.morny.data.TelegramStickers
import cc.sukazyo.cono.morny.util.tgapi.InputCommand
import cc.sukazyo.cono.morny.util.tgapi.TelegramExtensions.Requests.unsafeExecute
import com.pengrad.telegrambot.model.{Message, Update}
import com.pengrad.telegrambot.model.request.ParseMode
-import com.pengrad.telegrambot.request.SendMessage
+import com.pengrad.telegrambot.request.{SendMessage, SendSticker}
import com.pengrad.telegrambot.TelegramBot
class Testing (using coeur: MornyCoeur) extends ISimpleCommand {
@@ -32,12 +34,33 @@ class Testing (using coeur: MornyCoeur) extends ISimpleCommand {
}
private def execute2 (message: Message, previousContext: MessagingContext.WithUserAndMessage): Unit = {
+
+ if (message.text == "oops")
+ SendMessage(
+ message.chat.id,
+ "A test error message will be generated."
+ ).replyToMessageId(message.messageId)
+ .unsafeExecute
+ ErrorMessage(
+ _simple = SendSticker(
+ message.chat.id,
+ TelegramStickers.ID_404
+ ).replyToMessageId(message.messageId),
+ _complex = SendMessage(
+ message.chat.id,
+ "Oops: There is just a test error."
+ ).replyToMessageId(message.messageId)
+ )(using MessagingContext.extract(using message))
+ .submit
+ return;
+
SendMessage(
message.chat.id,
// language=html
"Test command with following input:\n" + message.text
).replyToMessageId(message.messageId).parseMode(ParseMode HTML)
.unsafeExecute
+
}
}