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 测试命令。测试 id1。 +| 这只是一个 /test 测试命令。测试 id2。 | 你可以回复一条消息来测试回复消息的功能。 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")