diff --git a/src/main/resources/assets_morny/langs/zh_cn.hyt b/src/main/resources/assets_morny/langs/zh_cn.hyt
index a546422..5d0ef37 100644
--- a/src/main/resources/assets_morny/langs/zh_cn.hyt
+++ b/src/main/resources/assets_morny/langs/zh_cn.hyt
@@ -26,7 +26,7 @@ morny.command.info.sub_start.message
| (你可以随时通过 /info 重新获得这些信息)
morny.misc.command_test.message
-| 这只是一个 /test 测试命令。测试 id 为 1
。
+| 这只是一个 /test 测试命令。测试 id 为 2
。
| 你可以回复一条消息来测试回复消息的功能。
morny.misc.command_test.branch_normal.message
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 f0655ed..fe973c9 100644
--- a/src/main/scala/cc/sukazyo/cono/morny/core/MornyCoeur.scala
+++ b/src/main/scala/cc/sukazyo/cono/morny/core/MornyCoeur.scala
@@ -15,7 +15,6 @@ import cc.sukazyo.cono.morny.util.time.WatchDog
import cc.sukazyo.cono.morny.util.GivenContext
import cc.sukazyo.cono.morny.util.UseString.MString
import cc.sukazyo.cono.morny.util.UseThrowable.toLogString
-import cc.sukazyo.cono.morny.util.hytrans.Translations
import com.pengrad.telegrambot.TelegramBot
import com.pengrad.telegrambot.request.GetMe
@@ -212,10 +211,11 @@ class MornyCoeur (modules: List[MornyModule])(using val config: MornyConfig)(tes
$MornyHellos.Hello,
MornyInfoOnStart(),
- $MornyManagers.SaveData,
$MornyInformation,
$MornyInformationOlds.Version,
$MornyInformationOlds.Runtime,
+ $MornyManagers.SaveData,
+ $MornyManagers.Reload,
$MornyManagers.Exit,
DirectMsgClear(),
@@ -369,7 +369,7 @@ class MornyCoeur (modules: List[MornyModule])(using val config: MornyConfig)(tes
object dsl extends BotExtension {
given coeur: MornyCoeur = MornyCoeur.this
given account: TelegramBot = MornyCoeur.this.account
- given translations: Translations = MornyCoeur.this.lang.translations
+ given translations: MornyLangs = MornyCoeur.this.lang
}
def saveDataAll(): Unit = {
@@ -377,6 +377,12 @@ class MornyCoeur (modules: List[MornyModule])(using val config: MornyConfig)(tes
logger `notice` "done all save action."
}
+ def reload (): Unit = {
+ logger `info` "Reloading coeur data / config..."
+ lang.reload()
+ logger `info` "done reload coeur data / config."
+ }
+
private def exitCleanup (): Unit = {
// Morny Exiting
diff --git a/src/main/scala/cc/sukazyo/cono/morny/core/MornyLangs.scala b/src/main/scala/cc/sukazyo/cono/morny/core/MornyLangs.scala
index 2a86535..14a980a 100644
--- a/src/main/scala/cc/sukazyo/cono/morny/core/MornyLangs.scala
+++ b/src/main/scala/cc/sukazyo/cono/morny/core/MornyLangs.scala
@@ -1,68 +1,102 @@
package cc.sukazyo.cono.morny.core
import cc.sukazyo.cono.morny.core.Log.logger
+import cc.sukazyo.cono.morny.core.MornyLangs.load
import cc.sukazyo.cono.morny.data.MornyAssets
import cc.sukazyo.cono.morny.util.hytrans.*
import cc.sukazyo.cono.morny.util.UseThrowable.toLogString
+import cc.sukazyo.cono.morny.util.var_text.{Var, VarText}
import java.io.IOException
import scala.collection.mutable
import scala.util.boundary
import scala.util.boundary.break
-class MornyLangs {
+object MornyLangs {
- private val (lang_index, lang_trans) = {
+ private def load(): Translations = {
- logger `info` s"Loading Morny's translation data."
-
- val (lang_dir, lang_index_content) = try {(
- MornyAssets.pack.getResDir("langs"),
- MornyAssets.pack.getResource("langs/_index.hyl").readAsString()
- )} catch case e: IOException =>
- throw Exception("Cannot read Morny's translations file.", e)
- val my_index = LanguageTree.parseTreeDocument(lang_index_content)
- val indexed_langs: Set[String] = {
- val lang_tags = mutable.ListBuffer.empty[String]
- my_index.root.traverseTree(lang_tags += _.langTag.lang)
- logger `info` s"indexed following languages: ${lang_tags.mkString(", ")}"
- lang_tags.toSet
+ val (lang_index, lang_trans) = {
+
+ logger `info` s"Loading Morny's translation data."
+
+ val (lang_dir, lang_index_content) = try {
+ (
+ MornyAssets.pack.getResDir("langs"),
+ MornyAssets.pack.getResource("langs/_index.hyl").readAsString()
+ )
+ } catch case e: IOException =>
+ throw Exception("Cannot read Morny's translations file.", e)
+ val my_index = LanguageTree.parseTreeDocument(lang_index_content)
+ val indexed_langs: Set[String] = {
+ val lang_tags = mutable.ListBuffer.empty[String]
+ my_index.root.traverseTree(lang_tags += _.langTag.lang)
+ logger `info` s"indexed following languages: ${lang_tags.mkString(", ")}"
+ lang_tags.toSet
+ }
+
+ val language_translations = mutable.HashMap.empty[String, Definitions]
+
+ for (file <- lang_dir.listFiles().filter(_.isFile)) yield {
+ boundary {
+ import file.getPath as raw_path
+ if !(raw_path.endsWith(".hyt") || raw_path.endsWith(".hytrans")) then break()
+ val file_name = file.getPath.reverse.takeWhile(c => (c != '/') && (c != '\\')).reverse
+ val file_basename = file_name.dropRight(
+ if file_name.endsWith(".hyt") then ".hyt".length
+ else ".hytrans".length
+ )
+ val normalized = LangTag.normalizeLangTag(file_basename)
+ if !indexed_langs.contains(normalized) then
+ logger `warn` s"translation file \"$file_name\" is not in language index, so it got ignored (normalized lang name is \"$normalized\")."
+ break()
+ val lang_def = try {
+ val content = file.readAsString()
+ Parser.parse(content)
+ } catch case e: IOException =>
+ logger `error`
+ s"""Failed read/parse translation file $file_name (normalized lang name is $normalized):
+ |${e.toLogString.indent(2)}
+ |due to failed, this ($file_name) has been ignored.""".stripMargin
+ break()
+ logger `info` s"read language file $file_name (normalized lang name is $normalized), with ${lang_def.size} entries."
+ if language_translations contains normalized then
+ // TODO: merge
+ logger `warn` s" language $normalized seems already loaded one yet, this will override the old one!"
+ else language_translations += (normalized -> lang_def)
+ }
+ }
+
+ (my_index, language_translations.toMap)
+
}
- val language_translations = mutable.HashMap.empty[String, Definitions]
-
- for (file <- lang_dir.listFiles().filter(_.isFile)) yield { boundary {
- import file.getPath as raw_path
- if !(raw_path.endsWith(".hyt") || raw_path.endsWith(".hytrans")) then break()
- val file_name = file.getPath.reverse.takeWhile(c=>(c!='/')&&(c !='\\')).reverse
- val file_basename = file_name.dropRight(
- if file_name.endsWith(".hyt") then ".hyt".length
- else ".hytrans".length
- )
- val normalized = LangTag.normalizeLangTag(file_basename)
- if !indexed_langs.contains(normalized) then
- logger `warn` s"translation file \"$file_name\" is not in language index, so it got ignored (normalized lang name is \"$normalized\")."
- break()
- val lang_def = try {
- val content = file.readAsString()
- Parser.parse(content)
- } catch case e: IOException =>
- logger `error`
- s"""Failed read/parse translation file $file_name (normalized lang name is $normalized):
- |${e.toLogString.indent(2)}
- |due to failed, this ($file_name) has been ignored.""".stripMargin
- break()
- logger `info` s"read language file $file_name (normalized lang name is $normalized), with ${lang_def.size} entries."
- if language_translations contains normalized then
- // TODO: merge
- logger `warn` s" language $normalized seems already loaded one yet, this will override the old one!"
- else language_translations += (normalized -> lang_def)
- }}
-
- (my_index, language_translations.toMap)
+ Translations(lang_index, lang_trans)
}
- val translations: Translations = Translations(lang_index, lang_trans)
+}
+
+class MornyLangs {
+
+ private var translations: Translations = load()
+
+ def reload (): Unit =
+ translations = load()
+
+ def getRaw: Translations = translations
+
+ def traverse (using lang: String)(f: (LangTag, Definitions) => Unit): Unit =
+ getRaw.traverse(f)
+ def traverseWithKey (key: String)(using lang: String)(f: (LangTag, Option[String]) => Unit): Unit =
+ getRaw.traverseWithKey(key)(f)
+ def get (key: String)(using lang: String): Option[String] =
+ getRaw.get(key)
+ def trans (key: String)(using lang: String): VarText =
+ getRaw.trans(key)
+ def trans (key: String, args: Var*)(using lang: String): String =
+ getRaw.trans(key, args*)
+ def transAsVar (key: String, args: Var*)(using lang: String): Var =
+ getRaw.transAsVar(key, args*)
}
diff --git a/src/main/scala/cc/sukazyo/cono/morny/core/bot/api/messages/ThreadingManager.scala b/src/main/scala/cc/sukazyo/cono/morny/core/bot/api/messages/ThreadingManager.scala
index b4437c4..aa48d0f 100644
--- a/src/main/scala/cc/sukazyo/cono/morny/core/bot/api/messages/ThreadingManager.scala
+++ b/src/main/scala/cc/sukazyo/cono/morny/core/bot/api/messages/ThreadingManager.scala
@@ -39,6 +39,18 @@ trait ThreadingManager {
*/
def doAfter[P] (thread: MessageThread[P]): Unit
+ /** Ensure current context have no ongoing message thread so that you can create a new one
+ * safely.
+ *
+ * If there's already an ongoing message thread, it will be canceled. Also, depends on the
+ * implementation, it may throw an exception or log a warning etc.
+ *
+ * @see [[cancelThread]] the method that will be used to cancel the ongoing message thread.
+ *
+ * @param _cxt the current context you want to check.
+ */
+ def ensureCleanState (using _cxt: MessagingContext.WithUserAndMessage): Unit
+
/** Try to continue run a message thread using the given message.
*
* @since 2.0.0
diff --git a/src/main/scala/cc/sukazyo/cono/morny/core/bot/command/MornyManagers.scala b/src/main/scala/cc/sukazyo/cono/morny/core/bot/command/MornyManagers.scala
index e3592f8..03997a6 100644
--- a/src/main/scala/cc/sukazyo/cono/morny/core/bot/command/MornyManagers.scala
+++ b/src/main/scala/cc/sukazyo/cono/morny/core/bot/command/MornyManagers.scala
@@ -3,18 +3,61 @@ package cc.sukazyo.cono.morny.core.bot.command
import cc.sukazyo.cono.morny.core.bot.api.ICommandAlias.HiddenAlias
import cc.sukazyo.cono.morny.core.Log.logger
import cc.sukazyo.cono.morny.core.MornyCoeur
-import cc.sukazyo.cono.morny.core.bot.api.{ICommandAlias, ITelegramCommand}
+import cc.sukazyo.cono.morny.core.bot.api.{messages, ICommandAlias, ISimpleCommand, ITelegramCommand}
+import cc.sukazyo.cono.morny.core.bot.api.messages.MessagingContext
import cc.sukazyo.cono.morny.data.TelegramStickers
import cc.sukazyo.cono.morny.reporter.MornyReport
import cc.sukazyo.cono.morny.util.tgapi.InputCommand
import cc.sukazyo.cono.morny.util.tgapi.formatting.TelegramFormatter.*
import cc.sukazyo.cono.morny.util.tgapi.TelegramExtensions.Requests.unsafeExecute
import com.pengrad.telegrambot.model.Update
-import com.pengrad.telegrambot.request.SendSticker
-import com.pengrad.telegrambot.TelegramBot
+import com.pengrad.telegrambot.request.{EditMessageText, SendMessage, SendSticker}
class MornyManagers (using coeur: MornyCoeur) {
- private given TelegramBot = coeur.account
+ import coeur.dsl.{*, given}
+
+ private def verifyTrusted (command: ISimpleCommand)(using cxt: MessagingContext.WithUserAndMessage): Boolean = {
+ if !(coeur.trusted isTrust cxt.bind_user) then
+ SendSticker(
+ cxt.bind_chat.id,
+ TelegramStickers ID_403
+ ).replyToMessageId(cxt.bind_message.messageId)
+ .unsafeExecute
+ logger `attention` s"403 ${command.name} caught from user ${cxt.bind_user toLogTag}"
+ coeur.externalContext.consume[MornyReport](_.unauthenticatedAction(s"/${command.name}", cxt.bind_user))
+ false
+ else true
+ }
+
+ object Reload extends ITelegramCommand {
+
+ override val name: String = "reload"
+ override val aliases: List[ICommandAlias] = Nil
+ override val paramRule: String = ""
+ override val description: String = "重新载入 Bot 资源文件 / 配置文件"
+
+ override def execute (using command: InputCommand, event: Update): Unit = {
+ given cxt: MessagingContext.WithUserAndMessage = MessagingContext.extract(using event.message)
+
+ if !verifyTrusted(this) then return
+
+ val statusMessage = SendMessage(
+ cxt.bind_chat.id,
+ "[ .... ] Coeur reload."
+ ).replyToMessageId(cxt.bind_message.messageId)
+ .unsafeExecute
+
+ coeur.reload()
+
+ EditMessageText(
+ statusMessage.message.chat.id,
+ statusMessage.message.messageId,
+ "[ OK ] Coeur reload."
+ ).unsafeExecute
+
+ }
+
+ }
object Exit extends ITelegramCommand {
@@ -24,30 +67,17 @@ class MornyManagers (using coeur: MornyCoeur) {
override val description: String = "关闭 Bot (仅可信成员)"
override def execute (using command: InputCommand, event: Update): Unit = {
+ given cxt: MessagingContext.WithUserAndMessage = MessagingContext.extract(using event.message)
- val user = event.message.from
+ if !verifyTrusted(this) then return
- if (coeur.trusted isTrust user) {
-
- SendSticker(
- event.message.chat.id,
- TelegramStickers ID_EXIT
- ).replyToMessageId(event.message.messageId)
- .unsafeExecute
- logger `attention` s"Morny exited by user ${user toLogTag}"
- coeur.exit(0, user)
-
- } else {
-
- SendSticker(
- event.message.chat.id,
- TelegramStickers ID_403
- ).replyToMessageId(event.message.messageId)
- .unsafeExecute
- logger `attention` s"403 exit caught from user ${user toLogTag}"
- coeur.externalContext.consume[MornyReport](_.unauthenticatedAction("/exit", user))
-
- }
+ SendSticker(
+ cxt.bind_chat.id,
+ TelegramStickers ID_EXIT
+ ).replyToMessageId(cxt.bind_message.messageId)
+ .unsafeExecute
+ logger `attention` s"Morny exited by user ${cxt.bind_user toLogTag}"
+ coeur.exit(0, cxt.bind_user)
}
@@ -61,30 +91,17 @@ class MornyManagers (using coeur: MornyCoeur) {
override val description: String = "保存缓存数据到文件(仅可信成员)"
override def execute (using command: InputCommand, event: Update): Unit = {
+ given cxt: MessagingContext.WithUserAndMessage = MessagingContext.extract(using event.message)
- val user = event.message.from
+ if !verifyTrusted(this) then return
- if (coeur.trusted isTrust user) {
-
- logger `attention` s"call save from command by ${user toLogTag}"
- coeur.saveDataAll()
- SendSticker(
- event.message.chat.id,
- TelegramStickers ID_SAVED
- ).replyToMessageId(event.message.messageId)
- .unsafeExecute
-
- } else {
-
- SendSticker(
- event.message.chat.id,
- TelegramStickers ID_403
- ).replyToMessageId(event.message.messageId)
- .unsafeExecute
- logger `attention` s"403 save caught from user ${user toLogTag}"
- coeur.externalContext.consume[MornyReport](_.unauthenticatedAction("/save", user))
-
- }
+ logger `attention` s"call save from command by ${cxt.bind_user toLogTag}"
+ coeur.saveDataAll()
+ SendSticker(
+ cxt.bind_chat.id,
+ TelegramStickers ID_SAVED
+ ).replyToMessageId(cxt.bind_message.messageId)
+ .unsafeExecute
}
diff --git a/src/main/scala/cc/sukazyo/cono/morny/core/bot/internal/ThreadingManagerImpl.scala b/src/main/scala/cc/sukazyo/cono/morny/core/bot/internal/ThreadingManagerImpl.scala
index b74b2e2..106fa60 100644
--- a/src/main/scala/cc/sukazyo/cono/morny/core/bot/internal/ThreadingManagerImpl.scala
+++ b/src/main/scala/cc/sukazyo/cono/morny/core/bot/internal/ThreadingManagerImpl.scala
@@ -78,6 +78,28 @@ class ThreadingManagerImpl (using bot: TelegramBot) extends ThreadingManager {
threadMap.get(ThreadKey fromMessage message)
.exists(_.onExecuteIt(message))
+ /** Ensure current context have no ongoing message thread so that you can create a new one
+ * safely.
+ *
+ * If there's already an ongoing message thread, it will be canceled, and will output a
+ * notice message to that context.
+ *
+ * @see [[cancelThread]] the method that will be used to cancel the ongoing message thread.
+ *
+ * @param _cxt the current context you want to check. This context will be converted to the
+ * [[ThreadKey]] in order to do the check, if needed, some message will also
+ * sent to this context.
+ */
+ override def ensureCleanState (using _cxt: MessagingContext.WithUserAndMessage): Unit =
+ if cancelThread(ThreadKey fromContext _cxt) then
+ SendMessage(
+ _cxt.bind_chat.id,
+ """There seems another message thread is waiting for future messages.
+ |That thread has been canceled automatically.
+ |""".stripMargin
+ ).replyToMessageId(_cxt.bind_message.messageId)
+ .unsafeExecute
+
override def cancelThread (threadKey: ThreadKey): Boolean =
threadMap.get(threadKey)
.exists(_.onCancelIt())
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 6744ca6..d5851eb 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
@@ -20,6 +20,8 @@ class Testing (using coeur: MornyCoeur) extends ISimpleCommand {
given context: MessagingContext.WithUserAndMessage = MessagingContext.extract(using event.message)
given lang: String = context.bind_user.prefer_language
+ coeur.messageThreading.ensureCleanState
+
SendMessage(
event.message.chat.id,
translations.trans("morny.misc.command_test.message")