Compare commits

...

3 Commits

Author SHA1 Message Date
254ec2a5a1
fix assembly build error 2024-05-09 18:51:34 +08:00
41da55f1ef
update dependencies
- sbt : 1.10.0 <== 1.9.9
- http4s-{dsl, circe} : 0.23.27 <== 0.23.26
- circe-{core, generic, parser} : 0.14.7 <== 0.14.6
2024-05-09 18:02:00 +08:00
e2a1bc2e59
add reload and langs reload, make ongoing thread warn can be before the thread register.
- change MornyLangs re-implements the methods of Translations so that it can be used like a Translations
  - changes its inner Translations implementation can be reloaded.
- MornyCoeur.dsl changes its `translations` from type Translations to MornyLangs in order to make reload works.
- add ensureCleansState to ThreadingManager and ThreadingManagerImpl to make the warn-before-register works
- cha /test implements message threads warn-before-register
2024-05-09 17:55:31 +08:00
10 changed files with 199 additions and 105 deletions

View File

@ -74,6 +74,7 @@ lazy val root = (project in file("."))
assemblyMergeStrategy := { assemblyMergeStrategy := {
case module if module endsWith "module-info.class" => MergeStrategy.concat case module if module endsWith "module-info.class" => MergeStrategy.concat
case module_kt if module_kt endsWith ".kotlin_module" => MergeStrategy.concat case module_kt if module_kt endsWith ".kotlin_module" => MergeStrategy.concat
case version if (version startsWith "META-INF") && (version endsWith ".versions.properties") => MergeStrategy.concat
case x => case x =>
val oldStrategy = (ThisBuild / assemblyMergeStrategy).value val oldStrategy = (ThisBuild / assemblyMergeStrategy).value
oldStrategy(x) oldStrategy(x)

View File

@ -22,8 +22,8 @@ object MornyConfiguration {
"cc.sukazyo" % "resource-tools" % "0.2.2", "cc.sukazyo" % "resource-tools" % "0.2.2",
"com.github.pengrad" % "java-telegram-bot-api" % "6.2.0", "com.github.pengrad" % "java-telegram-bot-api" % "6.2.0",
"org.http4s" %% "http4s-dsl" % "0.23.26", "org.http4s" %% "http4s-dsl" % "0.23.27",
"org.http4s" %% "http4s-circe" % "0.23.26", "org.http4s" %% "http4s-circe" % "0.23.27",
"org.http4s" %% "http4s-netty-server" % "0.5.16", "org.http4s" %% "http4s-netty-server" % "0.5.16",
"com.softwaremill.sttp.client3" %% "core" % "3.9.5", "com.softwaremill.sttp.client3" %% "core" % "3.9.5",
@ -32,9 +32,9 @@ object MornyConfiguration {
"org.typelevel" %% "case-insensitive" % "1.4.0", "org.typelevel" %% "case-insensitive" % "1.4.0",
"com.google.code.gson" % "gson" % "2.10.1", "com.google.code.gson" % "gson" % "2.10.1",
"io.circe" %% "circe-core" % "0.14.6", "io.circe" %% "circe-core" % "0.14.7",
"io.circe" %% "circe-generic" % "0.14.6", "io.circe" %% "circe-generic" % "0.14.7",
"io.circe" %% "circe-parser" % "0.14.6", "io.circe" %% "circe-parser" % "0.14.7",
"org.jsoup" % "jsoup" % "1.17.2", "org.jsoup" % "jsoup" % "1.17.2",
"com.cronutils" % "cron-utils" % "9.2.1", "com.cronutils" % "cron-utils" % "9.2.1",

View File

@ -1 +1 @@
sbt.version=1.9.9 sbt.version=1.10.0

View File

@ -26,7 +26,7 @@ morny.command.info.sub_start.message
| (你可以随时通过 /info 重新获得这些信息) | (你可以随时通过 /info 重新获得这些信息)
morny.misc.command_test.message morny.misc.command_test.message
| <b>这只是一个 /test 测试命令。</b>测试 <i>id</i> 为 <code>1</code>。 | <b>这只是一个 /test 测试命令。</b>测试 <i>id</i> 为 <code>2</code>。
| 你可以回复一条消息来测试回复消息的功能。 | 你可以回复一条消息来测试回复消息的功能。
morny.misc.command_test.branch_normal.message morny.misc.command_test.branch_normal.message

View File

@ -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.GivenContext
import cc.sukazyo.cono.morny.util.UseString.MString import cc.sukazyo.cono.morny.util.UseString.MString
import cc.sukazyo.cono.morny.util.UseThrowable.toLogString 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.TelegramBot
import com.pengrad.telegrambot.request.GetMe import com.pengrad.telegrambot.request.GetMe
@ -212,10 +211,11 @@ class MornyCoeur (modules: List[MornyModule])(using val config: MornyConfig)(tes
$MornyHellos.Hello, $MornyHellos.Hello,
MornyInfoOnStart(), MornyInfoOnStart(),
$MornyManagers.SaveData,
$MornyInformation, $MornyInformation,
$MornyInformationOlds.Version, $MornyInformationOlds.Version,
$MornyInformationOlds.Runtime, $MornyInformationOlds.Runtime,
$MornyManagers.SaveData,
$MornyManagers.Reload,
$MornyManagers.Exit, $MornyManagers.Exit,
DirectMsgClear(), DirectMsgClear(),
@ -369,7 +369,7 @@ class MornyCoeur (modules: List[MornyModule])(using val config: MornyConfig)(tes
object dsl extends BotExtension { object dsl extends BotExtension {
given coeur: MornyCoeur = MornyCoeur.this given coeur: MornyCoeur = MornyCoeur.this
given account: TelegramBot = MornyCoeur.this.account given account: TelegramBot = MornyCoeur.this.account
given translations: Translations = MornyCoeur.this.lang.translations given translations: MornyLangs = MornyCoeur.this.lang
} }
def saveDataAll(): Unit = { def saveDataAll(): Unit = {
@ -377,6 +377,12 @@ class MornyCoeur (modules: List[MornyModule])(using val config: MornyConfig)(tes
logger `notice` "done all save action." 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 = { private def exitCleanup (): Unit = {
// Morny Exiting // Morny Exiting

View File

@ -1,68 +1,102 @@
package cc.sukazyo.cono.morny.core package cc.sukazyo.cono.morny.core
import cc.sukazyo.cono.morny.core.Log.logger 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.data.MornyAssets
import cc.sukazyo.cono.morny.util.hytrans.* import cc.sukazyo.cono.morny.util.hytrans.*
import cc.sukazyo.cono.morny.util.UseThrowable.toLogString import cc.sukazyo.cono.morny.util.UseThrowable.toLogString
import cc.sukazyo.cono.morny.util.var_text.{Var, VarText}
import java.io.IOException import java.io.IOException
import scala.collection.mutable import scala.collection.mutable
import scala.util.boundary import scala.util.boundary
import scala.util.boundary.break 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_index, lang_trans) = {
val (lang_dir, lang_index_content) = try {( logger `info` s"Loading Morny's translation data."
MornyAssets.pack.getResDir("langs"),
MornyAssets.pack.getResource("langs/_index.hyl").readAsString() val (lang_dir, lang_index_content) = try {
)} catch case e: IOException => (
throw Exception("Cannot read Morny's translations file.", e) MornyAssets.pack.getResDir("langs"),
val my_index = LanguageTree.parseTreeDocument(lang_index_content) MornyAssets.pack.getResource("langs/_index.hyl").readAsString()
val indexed_langs: Set[String] = { )
val lang_tags = mutable.ListBuffer.empty[String] } catch case e: IOException =>
my_index.root.traverseTree(lang_tags += _.langTag.lang) throw Exception("Cannot read Morny's translations file.", e)
logger `info` s"indexed following languages: ${lang_tags.mkString(", ")}" val my_index = LanguageTree.parseTreeDocument(lang_index_content)
lang_tags.toSet 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] Translations(lang_index, lang_trans)
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 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*)
} }

View File

@ -39,6 +39,18 @@ trait ThreadingManager {
*/ */
def doAfter[P] (thread: MessageThread[P]): Unit 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. /** Try to continue run a message thread using the given message.
* *
* @since 2.0.0 * @since 2.0.0

View File

@ -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.bot.api.ICommandAlias.HiddenAlias
import cc.sukazyo.cono.morny.core.Log.logger import cc.sukazyo.cono.morny.core.Log.logger
import cc.sukazyo.cono.morny.core.MornyCoeur 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.data.TelegramStickers
import cc.sukazyo.cono.morny.reporter.MornyReport import cc.sukazyo.cono.morny.reporter.MornyReport
import cc.sukazyo.cono.morny.util.tgapi.InputCommand import cc.sukazyo.cono.morny.util.tgapi.InputCommand
import cc.sukazyo.cono.morny.util.tgapi.formatting.TelegramFormatter.* import cc.sukazyo.cono.morny.util.tgapi.formatting.TelegramFormatter.*
import cc.sukazyo.cono.morny.util.tgapi.TelegramExtensions.Requests.unsafeExecute import cc.sukazyo.cono.morny.util.tgapi.TelegramExtensions.Requests.unsafeExecute
import com.pengrad.telegrambot.model.Update import com.pengrad.telegrambot.model.Update
import com.pengrad.telegrambot.request.SendSticker import com.pengrad.telegrambot.request.{EditMessageText, SendMessage, SendSticker}
import com.pengrad.telegrambot.TelegramBot
class MornyManagers (using coeur: MornyCoeur) { 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 { object Exit extends ITelegramCommand {
@ -24,30 +67,17 @@ class MornyManagers (using coeur: MornyCoeur) {
override val description: String = "关闭 Bot (仅可信成员)" override val description: String = "关闭 Bot (仅可信成员)"
override def execute (using command: InputCommand, event: Update): Unit = { 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(
cxt.bind_chat.id,
SendSticker( TelegramStickers ID_EXIT
event.message.chat.id, ).replyToMessageId(cxt.bind_message.messageId)
TelegramStickers ID_EXIT .unsafeExecute
).replyToMessageId(event.message.messageId) logger `attention` s"Morny exited by user ${cxt.bind_user toLogTag}"
.unsafeExecute coeur.exit(0, cxt.bind_user)
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))
}
} }
@ -61,30 +91,17 @@ class MornyManagers (using coeur: MornyCoeur) {
override val description: String = "保存缓存数据到文件(仅可信成员)" override val description: String = "保存缓存数据到文件(仅可信成员)"
override def execute (using command: InputCommand, event: Update): Unit = { 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 ${cxt.bind_user toLogTag}"
coeur.saveDataAll()
logger `attention` s"call save from command by ${user toLogTag}" SendSticker(
coeur.saveDataAll() cxt.bind_chat.id,
SendSticker( TelegramStickers ID_SAVED
event.message.chat.id, ).replyToMessageId(cxt.bind_message.messageId)
TelegramStickers ID_SAVED .unsafeExecute
).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))
}
} }

View File

@ -78,6 +78,28 @@ class ThreadingManagerImpl (using bot: TelegramBot) extends ThreadingManager {
threadMap.get(ThreadKey fromMessage message) threadMap.get(ThreadKey fromMessage message)
.exists(_.onExecuteIt(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 = override def cancelThread (threadKey: ThreadKey): Boolean =
threadMap.get(threadKey) threadMap.get(threadKey)
.exists(_.onCancelIt()) .exists(_.onCancelIt())

View File

@ -20,6 +20,8 @@ class Testing (using coeur: MornyCoeur) extends ISimpleCommand {
given context: MessagingContext.WithUserAndMessage = MessagingContext.extract(using event.message) given context: MessagingContext.WithUserAndMessage = MessagingContext.extract(using event.message)
given lang: String = context.bind_user.prefer_language given lang: String = context.bind_user.prefer_language
coeur.messageThreading.ensureCleanState
SendMessage( SendMessage(
event.message.chat.id, event.message.chat.id,
translations.trans("morny.misc.command_test.message") translations.trans("morny.misc.command_test.message")