diff --git a/gradle.properties b/gradle.properties index 3d87307..d37e7e7 100644 --- a/gradle.properties +++ b/gradle.properties @@ -8,7 +8,7 @@ MORNY_COMMIT_PATH = https://github.com/Eyre-S/Coeur-Morny-Cono/commit/%s VERSION = 1.0.0-RC5 USE_DELTA = true -VERSION_DELTA = scala3 +VERSION_DELTA = scala4 CODENAME = beiping diff --git a/src/main/scala/cc/sukazyo/cono/morny/MornyCoeur.scala b/src/main/scala/cc/sukazyo/cono/morny/MornyCoeur.scala index 7df72aa..9d0f2ba 100644 --- a/src/main/scala/cc/sukazyo/cono/morny/MornyCoeur.scala +++ b/src/main/scala/cc/sukazyo/cono/morny/MornyCoeur.scala @@ -31,10 +31,12 @@ class MornyCoeur (using val config: MornyConfig) { if config.telegramBotUsername ne null then logger info s"login as:\n ${config.telegramBotUsername}" - private val __loginResult = login() - if (__loginResult eq null) - logger error "Login to bot failed." - System exit -1 + private val __loginResult: LoginResult = login() match + case some: Some[LoginResult] => some.get + case None => + logger error "Login to bot failed." + System exit -1 + throw RuntimeException() configure_exitCleanup() @@ -63,8 +65,8 @@ class MornyCoeur (using val config: MornyConfig) { val events: MornyEventListeners = MornyEventListeners(using eventManager) /** inner value: about why morny exit, used in [[daemon.MornyReport]]. */ - private var whileExit_reason: AnyRef|Null = _ - def exitReason: AnyRef|Null = whileExit_reason + private var whileExit_reason: Option[AnyRef] = None + def exitReason: Option[AnyRef] = whileExit_reason val coeurStartTimestamp: Long = ServerMain.systemStartupTime ///>>> BLOCK START instance configure & startup stage 2 @@ -101,12 +103,12 @@ class MornyCoeur (using val config: MornyConfig) { } def exit (status: Int, reason: AnyRef): Unit = - whileExit_reason = reason + whileExit_reason = Some(reason) System exit status private case class LoginResult(account: TelegramBot, username: String, userid: Long) - private def login (): LoginResult|Null = { + private def login (): Option[LoginResult] = { val builder = TelegramBot.Builder(config.telegramBotKey) var api_bot = config.telegramBotApiServer @@ -129,7 +131,7 @@ class MornyCoeur (using val config: MornyConfig) { val account = builder build logger info "Trying to login..." - boundary[LoginResult|Null] { + boundary[Option[LoginResult]] { for (i <- 0 to 3) { if i > 0 then logger info "retrying..." try { @@ -137,16 +139,16 @@ class MornyCoeur (using val config: MornyConfig) { if ((config.telegramBotUsername ne null) && config.telegramBotUsername != remote.username) throw RuntimeException(s"Required the bot @${config.telegramBotUsername} but @${remote.username} logged in") logger info s"Succeed logged in to @${remote.username}" - break(LoginResult(account, remote.username, remote.id)) + break(Some(LoginResult(account, remote.username, remote.id))) } catch - case r: boundary.Break[LoginResult|Null] => throw r + case r: boundary.Break[Option[LoginResult]] => throw r case e => logger error s"""${exceptionLog(e)} |login failed""" .stripMargin } - null + None } } diff --git a/src/main/scala/cc/sukazyo/cono/morny/bot/api/EventListenerManager.scala b/src/main/scala/cc/sukazyo/cono/morny/bot/api/EventListenerManager.scala index 935f561..5448204 100644 --- a/src/main/scala/cc/sukazyo/cono/morny/bot/api/EventListenerManager.scala +++ b/src/main/scala/cc/sukazyo/cono/morny/bot/api/EventListenerManager.scala @@ -10,6 +10,12 @@ import com.pengrad.telegrambot.UpdatesListener import scala.collection.mutable import scala.language.postfixOps +/** Contains a [[mutable.Queue]] of [[EventListener]], and delivery telegram [[Update]]. + * + * Implemented [[process]] in [[UpdatesListener]] so it can directly used in [[com.pengrad.telegrambot.TelegramBot.setupListener]]. + * + * @param coeur the [[MornyCoeur]] context. + */ class EventListenerManager (using coeur: MornyCoeur) extends UpdatesListener { private val listeners = mutable.Queue.empty[EventListener] @@ -77,9 +83,19 @@ class EventListenerManager (using coeur: MornyCoeur) extends UpdatesListener { } + import java.util import scala.jdk.CollectionConverters.* - + /** Delivery the telegram [[Update]]s. + * + * The implementation of [[UpdatesListener]]. + * + * For each [[Update]], create an [[EventRunner]] for it, and + * start the it. + * + * @return [[UpdatesListener.CONFIRMED_UPDATES_ALL]], for all Updates + * should be processed in [[EventRunner]] created for it. + */ override def process (updates: util.List[Update]): Int = { for (update <- updates.asScala) EventRunner(using update).start() diff --git a/src/main/scala/cc/sukazyo/cono/morny/bot/command/ICommandAlias.scala b/src/main/scala/cc/sukazyo/cono/morny/bot/command/ICommandAlias.scala index 749f550..de9dbe7 100644 --- a/src/main/scala/cc/sukazyo/cono/morny/bot/command/ICommandAlias.scala +++ b/src/main/scala/cc/sukazyo/cono/morny/bot/command/ICommandAlias.scala @@ -1,17 +1,43 @@ package cc.sukazyo.cono.morny.bot.command +/** One alias definition, contains the necessary message of how + * to process the alias. + */ trait ICommandAlias { + /** The alias name. + * + * same with the command name, it is the unique identifier of this alias. + */ val name: String + /** If the alias should be listed while list commands to end-user. + * + * The alias can only be listed when the parent command can be listed + * (meanwhile the parent command implemented [[ITelegramCommand]]). If the + * parent command cannot be listed, it will always cannot be listed. + */ val listed: Boolean } +/** Default implementations of [[ICommandAlias]]. */ object ICommandAlias { + /** Alias which can be listed to end-user. + * + * the [[ICommandAlias.listed]] value is always true. + * + * @param name The alias name, see more in [[ICommandAlias.name]] + */ case class ListedAlias (name: String) extends ICommandAlias: override val listed = true + /** Alias which cannot be listed to end-user. + * + * the [[ICommandAlias.listed]] value is always false. + * + * @param name The alias name, see more in [[ICommandAlias.name]] + */ case class HiddenAlias (name: String) extends ICommandAlias: override val listed = false diff --git a/src/main/scala/cc/sukazyo/cono/morny/bot/command/ISimpleCommand.scala b/src/main/scala/cc/sukazyo/cono/morny/bot/command/ISimpleCommand.scala index 25181bd..24c624f 100644 --- a/src/main/scala/cc/sukazyo/cono/morny/bot/command/ISimpleCommand.scala +++ b/src/main/scala/cc/sukazyo/cono/morny/bot/command/ISimpleCommand.scala @@ -3,11 +3,37 @@ package cc.sukazyo.cono.morny.bot.command import cc.sukazyo.cono.morny.util.tgapi.InputCommand import com.pengrad.telegrambot.model.Update +/** A simple command. + * + * Contains only [[name]] and [[aliases]]. + * + * Won't be listed to end-user. if you want the command listed, + * see [[ITelegramCommand]]. + * + */ trait ISimpleCommand { + /** the main name of the command. + * + * must have a value as the unique identifier of this command. + */ val name: String + /** aliases of the command. + * + * Alias means it is the same to call [[name main name]] when call this. + * There can be multiple aliases. But notice that, although alias is not + * the unique identifier, it uses the same namespace with [[name]], means + * it also cannot be duplicate with other [[name]] or [[aliases]]. + * + * It can be [[Null]], means no aliases. + */ val aliases: Array[ICommandAlias]|Null + /** The work code of this command. + * + * @param command The parsed input command which called this command. + * @param event The raw event which called this command. + */ def execute (using command: InputCommand, event: Update): Unit } diff --git a/src/main/scala/cc/sukazyo/cono/morny/bot/command/ITelegramCommand.scala b/src/main/scala/cc/sukazyo/cono/morny/bot/command/ITelegramCommand.scala index 2c16263..1a720b8 100644 --- a/src/main/scala/cc/sukazyo/cono/morny/bot/command/ITelegramCommand.scala +++ b/src/main/scala/cc/sukazyo/cono/morny/bot/command/ITelegramCommand.scala @@ -1,8 +1,25 @@ package cc.sukazyo.cono.morny.bot.command +/** A complex telegram command. + * + * the extension of [[ISimpleCommand]], with external defines of the necessary + * introduction message ([[paramRule]] and [[description]]). + * + * It can be listed to end-user. + */ trait ITelegramCommand extends ISimpleCommand { + /** The param rule of this command, used in human-readable command list. + * + * The param rule uses a symbol language to describe how this command + * receives paras. + * + * Set it empty to make this scope not available. + */ val paramRule: String + /** The description/introduction of this command, used in human-readable + * command list. + */ val description: String } diff --git a/src/main/scala/cc/sukazyo/cono/morny/bot/command/MornyInformation.scala b/src/main/scala/cc/sukazyo/cono/morny/bot/command/MornyInformation.scala index be4aafc..36222eb 100644 --- a/src/main/scala/cc/sukazyo/cono/morny/bot/command/MornyInformation.scala +++ b/src/main/scala/cc/sukazyo/cono/morny/bot/command/MornyInformation.scala @@ -122,7 +122,7 @@ class MornyInformation (using coeur: MornyCoeur) extends ITelegramCommand { event.message.chat.id, /* language=html */ s"""system: - |- ${h(if (getRuntimeHostname == null) "" else getRuntimeHostname)} + |- ${h(if getRuntimeHostname nonEmpty then getRuntimeHostname.get else "")} |- ${h(sysprop("os.name"))} ${h(sysprop("os.arch"))} ${h(sysprop("os.version"))} |java runtime: |- ${h(sysprop("java.vm.vendor"))}.${h(sysprop("java.vm.name"))} diff --git a/src/main/scala/cc/sukazyo/cono/morny/bot/command/Nbnhhsh.scala b/src/main/scala/cc/sukazyo/cono/morny/bot/command/Nbnhhsh.scala index 351eb58..543a783 100644 --- a/src/main/scala/cc/sukazyo/cono/morny/bot/command/Nbnhhsh.scala +++ b/src/main/scala/cc/sukazyo/cono/morny/bot/command/Nbnhhsh.scala @@ -25,19 +25,17 @@ class Nbnhhsh (using coeur: MornyCoeur) extends ITelegramCommand { override def execute (using command: InputCommand, event: Update): Unit = { - val queryTarget: String|Null = + val queryTarget: String = if command.args nonEmpty then command.args mkString " " else if (event.message.replyToMessage != null && event.message.replyToMessage.text != null) event.message.replyToMessage.text - else null - - if (queryTarget == null) - coeur.account exec SendSticker( - event.message.chat.id, - TelegramStickers ID_404 - ).replyToMessageId(event.message.messageId) - return; + else + coeur.account exec SendSticker( + event.message.chat.id, + TelegramStickers ID_404 + ).replyToMessageId(event.message.messageId) + return; try { diff --git a/src/main/scala/cc/sukazyo/cono/morny/bot/event/OnCallMsgSend.scala b/src/main/scala/cc/sukazyo/cono/morny/bot/event/OnCallMsgSend.scala index 9c2c80c..db920c0 100644 --- a/src/main/scala/cc/sukazyo/cono/morny/bot/event/OnCallMsgSend.scala +++ b/src/main/scala/cc/sukazyo/cono/morny/bot/event/OnCallMsgSend.scala @@ -17,11 +17,11 @@ class OnCallMsgSend (using coeur: MornyCoeur) extends EventListener { private val REGEX_MSG_SENDREQ_DATA_HEAD: Regex = "^\\*msg(-?\\d+)(\\*\\S+)?(?:\\n([\\s\\S]+))?$"r case class MessageToSend ( - message: String|Null, - entities: Array[MessageEntity]|Null, - parseMode: ParseMode|Null, - targetId: Long - ) { + message: String|Null, + entities: Array[MessageEntity]|Null, + parseMode: ParseMode|Null, + targetId: Long + ) { def toSendMessage (target_override: Long|Null = null): SendMessage = val useTarget = if target_override == null then targetId else target_override val sendMessage = SendMessage(useTarget, message) @@ -129,8 +129,8 @@ class OnCallMsgSend (using coeur: MornyCoeur) extends EventListener { } if messageToSend.message eq null then return true - val testSendResponse = coeur.account execute messageToSend.toSendMessage(update.message.chat.id) - .replyToMessageId(update.message.messageId) + val testSendResponse = coeur.account execute + messageToSend.toSendMessage(update.message.chat.id).replyToMessageId(update.message.messageId) if (!(testSendResponse isOk)) coeur.account exec SendMessage( update.message.chat.id, diff --git a/src/main/scala/cc/sukazyo/cono/morny/bot/event/OnQuestionMarkReply.scala b/src/main/scala/cc/sukazyo/cono/morny/bot/event/OnQuestionMarkReply.scala index 9f397a7..1aff5f2 100644 --- a/src/main/scala/cc/sukazyo/cono/morny/bot/event/OnQuestionMarkReply.scala +++ b/src/main/scala/cc/sukazyo/cono/morny/bot/event/OnQuestionMarkReply.scala @@ -8,6 +8,7 @@ import com.pengrad.telegrambot.model.Update import com.pengrad.telegrambot.request.SendMessage import scala.language.postfixOps +import scala.util.boundary class OnQuestionMarkReply (using coeur: MornyCoeur) extends EventListener { @@ -34,10 +35,10 @@ object OnQuestionMarkReply { private val QUESTION_MARKS = Set('?', '?', '¿', '⁈', '⁇', '‽', '❔', '❓') def isAllMessageMark (using text: String): Boolean = { - var isAll = true - for (c <- text) - if !(QUESTION_MARKS contains c) then isAll = false - isAll + boundary[Boolean] { + for (c <- text) if QUESTION_MARKS contains c then boundary.break(false) + true + } } } diff --git a/src/main/scala/cc/sukazyo/cono/morny/bot/query/ShareToolBilibili.scala b/src/main/scala/cc/sukazyo/cono/morny/bot/query/ShareToolBilibili.scala index a74aad2..f73fed6 100644 --- a/src/main/scala/cc/sukazyo/cono/morny/bot/query/ShareToolBilibili.scala +++ b/src/main/scala/cc/sukazyo/cono/morny/bot/query/ShareToolBilibili.scala @@ -3,6 +3,7 @@ package cc.sukazyo.cono.morny.bot.query import cc.sukazyo.cono.morny.Log.logger import cc.sukazyo.cono.morny.util.tgapi.formatting.NamingUtils.inlineQueryId import cc.sukazyo.cono.morny.util.BiliTool +import cc.sukazyo.cono.morny.util.UseSelect.select import com.pengrad.telegrambot.model.Update import com.pengrad.telegrambot.model.request.{InlineQueryResultArticle, InputTextMessageContent, ParseMode} @@ -24,23 +25,23 @@ class ShareToolBilibili extends ITelegramQuery { if (event.inlineQuery.query == null) return null event.inlineQuery.query match - case REGEX_BILI_VIDEO(_1, _2, _3, _4, _5, _6, _7) => + case REGEX_BILI_VIDEO(_url_v, _url_av, _url_bv, _url_param, _url_v_part, _raw_av, _raw_bv) => logger debug s"""====== Share Tool Bilibili Catch ok - |1: ${_1} - |2: ${_2} - |3: ${_3} - |4: ${_4} - |5: ${_5} - |6: ${_6} - |7: ${_7}""" + |1: ${_url_v} + |2: ${_url_av} + |3: ${_url_bv} + |4: ${_url_param} + |5: ${_url_v_part} + |6: ${_raw_av} + |7: ${_raw_bv}""" .stripMargin - var av = if (_2 != null) _2 else if (_6 != null) _6 else null - var bv = if (_3!=null) _3 else if (_7!=null) _7 else null + var av = select(_url_av, _raw_av) + var bv = select(_url_bv, _raw_bv) logger trace s"catch id av[$av] bv[$bv]" - val part: Int|Null = if (_5!=null) _5 toInt else null + val part: Int|Null = if (_url_v_part!=null) _url_v_part toInt else null logger trace s"catch video part[$part]" if (av == null) { diff --git a/src/main/scala/cc/sukazyo/cono/morny/bot/query/ShareToolTwitter.scala b/src/main/scala/cc/sukazyo/cono/morny/bot/query/ShareToolTwitter.scala index 03ba0de..a550501 100644 --- a/src/main/scala/cc/sukazyo/cono/morny/bot/query/ShareToolTwitter.scala +++ b/src/main/scala/cc/sukazyo/cono/morny/bot/query/ShareToolTwitter.scala @@ -21,15 +21,15 @@ class ShareToolTwitter extends ITelegramQuery { event.inlineQuery.query match - case REGEX_TWEET_LINK(_, _2, _, _, _, _) => + case REGEX_TWEET_LINK(_, _path_data, _, _, _, _) => List( InlineQueryUnit(InlineQueryResultArticle( inlineQueryId(ID_PREFIX_VX+event.inlineQuery.query), TITLE_VX, - s"https://vxtwitter.com/$_2" + s"https://vxtwitter.com/$_path_data" )), InlineQueryUnit(InlineQueryResultArticle( inlineQueryId(ID_PREFIX_VX_COMBINED+event.inlineQuery.query), TITLE_VX_COMBINED, - s"https://c.vxtwitter.com/$_2" + s"https://c.vxtwitter.com/$_path_data" )) ) diff --git a/src/main/scala/cc/sukazyo/cono/morny/daemon/MedicationTimer.scala b/src/main/scala/cc/sukazyo/cono/morny/daemon/MedicationTimer.scala index df11781..f01671a 100644 --- a/src/main/scala/cc/sukazyo/cono/morny/daemon/MedicationTimer.scala +++ b/src/main/scala/cc/sukazyo/cono/morny/daemon/MedicationTimer.scala @@ -24,7 +24,7 @@ class MedicationTimer (using coeur: MornyCoeur) extends Thread { this.setName(DAEMON_THREAD_NAME_DEF) - private var lastNotify_messageId: Int|Null = _ + private var lastNotify_messageId: Option[Int] = None override def run (): Unit = { logger info "Medication Timer started." @@ -51,8 +51,8 @@ class MedicationTimer (using coeur: MornyCoeur) extends Thread { private def sendNotification(): Unit = { val sendResponse: SendResponse = coeur.account exec SendMessage(notify_toChat, NOTIFY_MESSAGE) - if sendResponse isOk then lastNotify_messageId = sendResponse.message.messageId - else lastNotify_messageId = null + if sendResponse isOk then lastNotify_messageId = Some(sendResponse.message.messageId) + else lastNotify_messageId = None } @throws[InterruptedException | IllegalArgumentException] @@ -61,7 +61,7 @@ class MedicationTimer (using coeur: MornyCoeur) extends Thread { } def refreshNotificationWrite (edited: Message): Unit = { - if lastNotify_messageId != (edited.messageId toInt) then return + if (lastNotify_messageId isEmpty) || (lastNotify_messageId.get != (edited.messageId toInt)) then return import cc.sukazyo.cono.morny.util.CommonFormat.formatDate val editTime = formatDate(edited.editDate*1000, use_timeZone.getTotalSeconds/60/60) val entities = ArrayBuffer.empty[MessageEntity] @@ -72,7 +72,7 @@ class MedicationTimer (using coeur: MornyCoeur) extends Thread { edited.messageId, edited.text + s"\n-- $editTime --" ).entities(entities toArray:_*) - lastNotify_messageId = null + lastNotify_messageId = None } } diff --git a/src/main/scala/cc/sukazyo/cono/morny/daemon/MornyReport.scala b/src/main/scala/cc/sukazyo/cono/morny/daemon/MornyReport.scala index 9461880..3528ff1 100644 --- a/src/main/scala/cc/sukazyo/cono/morny/daemon/MornyReport.scala +++ b/src/main/scala/cc/sukazyo/cono/morny/daemon/MornyReport.scala @@ -101,9 +101,9 @@ class MornyReport (using coeur: MornyCoeur) { def reportCoeurExit (): Unit = { val causedTag = coeur.exitReason match - case u: User => u.fullnameRefHTML - case n if n == null => "UNKNOWN reason" - case a: AnyRef => /*language=html*/ s"${h(a.toString)}" + case None => "UNKNOWN reason" + case u: Some[User] => u.get.fullnameRefHTML + case a: Some[_] => /*language=html*/ s"${h(a.get.toString)}" executeReport(SendMessage( coeur.config.reportToChat, // language=html diff --git a/src/main/scala/cc/sukazyo/cono/morny/data/MornyInformation.scala b/src/main/scala/cc/sukazyo/cono/morny/data/MornyInformation.scala index 12e7671..19e4642 100644 --- a/src/main/scala/cc/sukazyo/cono/morny/data/MornyInformation.scala +++ b/src/main/scala/cc/sukazyo/cono/morny/data/MornyInformation.scala @@ -29,9 +29,9 @@ object MornyInformation { } //noinspection ScalaWeakerAccess - def getRuntimeHostname: String | Null = { - try InetAddress.getLocalHost.getHostName - catch case _: UnknownHostException => null + def getRuntimeHostname: Option[String] = { + try Some(InetAddress.getLocalHost.getHostName) + catch case _: UnknownHostException => None } def getAboutPic: Array[Byte] = TelegramImages.IMG_ABOUT get diff --git a/src/main/scala/cc/sukazyo/cono/morny/data/TelegramImages.scala b/src/main/scala/cc/sukazyo/cono/morny/data/TelegramImages.scala index 6fb7757..ac45039 100644 --- a/src/main/scala/cc/sukazyo/cono/morny/data/TelegramImages.scala +++ b/src/main/scala/cc/sukazyo/cono/morny/data/TelegramImages.scala @@ -13,17 +13,17 @@ object TelegramImages { class AssetsFileImage (assetsPath: String) { - private var cache: Array[Byte]|Null = _ + private var cache: Option[Array[Byte]] = None @throws[AssetsException] def get:Array[Byte] = - if cache eq null then read() - cache + if cache isEmpty then read() + cache.get @throws[AssetsException] private def read (): Unit = { Using ((MornyAssets.pack getResource assetsPath)read) { stream => - try { this.cache = stream.readAllBytes() } + try { this.cache = Some(stream.readAllBytes()) } catch case e: IOException => { throw AssetsException(e) } diff --git a/src/main/scala/cc/sukazyo/cono/morny/util/UseSelect.scala b/src/main/scala/cc/sukazyo/cono/morny/util/UseSelect.scala new file mode 100644 index 0000000..feb3e99 --- /dev/null +++ b/src/main/scala/cc/sukazyo/cono/morny/util/UseSelect.scala @@ -0,0 +1,27 @@ +package cc.sukazyo.cono.morny.util + +import scala.util.boundary + +/** Useful utils of select one specific value in the given values. + * + * contains: + * - [[select()]] can select one value which is not [[Null]]. + * + */ +object UseSelect { + + /** Select the non-null value in the given values. + * + * @tparam T The value's type. + * @param values Given values, may be a T value or [[Null]]. + * @return The first non-null value in the given values, or [[Null]] if + * there's no non-null value. + */ + def select [T] (values: T|Null*): T|Null = { + boundary[T|Null] { + for (i <- values) if i != null then boundary.break(i) + null + } + } + +}