Compare commits

...

4 Commits

Author SHA1 Message Date
985fde9aa2
fix sources jar gen in scala 2023-10-06 22:24:14 +08:00
981098cf6e
update README 2023-10-06 21:52:31 +08:00
69086b1f36
publish 1.0.0-RC5 2023-10-06 21:42:50 +08:00
1bd795873c
code optimize
- add UseSelect
- add scaladoc for some internal trait
- code optimize, mostly use Option now
2023-10-06 20:55:26 +08:00
19 changed files with 198 additions and 71 deletions

View File

@ -4,19 +4,22 @@
[todo]: https://github.com/users/Eyre-S/projects/1 [todo]: https://github.com/users/Eyre-S/projects/1
[artifact]: https://mvn.sukazyo.cc/#/releases/cc/sukazyo/morny-coeur [artifact]: https://mvn.sukazyo.cc/#/releases/cc/sukazyo/morny-coeur
[tg4j]: https://github.com/pengrad/java-telegram-bot-api [scala]: https://www.scala-lang.org/
[spotbugs]: https://spotbugs.github.io/ [spotbugs]: https://spotbugs.github.io/
[junit5]: https://junit.org/junit5/ [tg4j]: https://github.com/pengrad/java-telegram-bot-api
[okhttp]: https://square.github.io/okhttp/
[gson]: https://github.com/google/gson
[scalatest]: https://scalatest.org/
<div align=center> <div align=center>
# ~~给所有喜欢morny的大家的~~ Morny Coeur 源代码 # ~~给所有喜欢morny的大家的~~ Morny Coeur 源代码
~~"你们又有意见又不发issue这样子我很为难的啊"~~ ~~"and nobody cares."~~
![social preview card](morny-github-social-preview-card@0.75x.png) ![social preview card](morny-github-social-preview-card@0.75x.png)
一个 telegram 上的服侍 A.C.Sukazyo Eyre 和它的花宫成员的 bot 内核 一个 telegram 上的服侍 A.C.Sukazyo Eyre 和它的花宫成员的 bot 内核
[Task Listing][todo] | [~~BBS~~][issues] | [Published][artifact] [Task Listing][todo] | [~~BBS~~][issues] | [Published][artifact]
@ -32,6 +35,9 @@
[Java Telegram Bot API][tg4j] [Java Telegram Bot API][tg4j]
[SpotBugs Annotations][spotbugs] | [JUnit 5][junit5]
[okhttp] | [Gson][gson]
[Scala][scala] | [SpotBugs Annotations][spotbugs] | [ScalaTest][scalatest]
</div> </div>

View File

@ -92,6 +92,10 @@ dependencies {
} }
java {
withSourcesJar()
}
tasks.withType(JavaCompile).configureEach { tasks.withType(JavaCompile).configureEach {
sourceCompatibility proj_java.getMajorVersion() sourceCompatibility proj_java.getMajorVersion()
@ -144,9 +148,12 @@ buildConfig {
} }
tasks.withType(Jar).configureEach {
archiveBaseName.set proj_archive_name
}
shadowJar { shadowJar {
archiveBaseName.set proj_archive_name
archiveClassifier.set "fat" archiveClassifier.set "fat"
if (project.hasProperty("dockerBuild")) { if (project.hasProperty("dockerBuild")) {

View File

@ -7,8 +7,8 @@ MORNY_COMMIT_PATH = https://github.com/Eyre-S/Coeur-Morny-Cono/commit/%s
VERSION = 1.0.0-RC5 VERSION = 1.0.0-RC5
USE_DELTA = true USE_DELTA = false
VERSION_DELTA = scala3 VERSION_DELTA =
CODENAME = beiping CODENAME = beiping

View File

@ -31,10 +31,12 @@ class MornyCoeur (using val config: MornyConfig) {
if config.telegramBotUsername ne null then if config.telegramBotUsername ne null then
logger info s"login as:\n ${config.telegramBotUsername}" logger info s"login as:\n ${config.telegramBotUsername}"
private val __loginResult = login() private val __loginResult: LoginResult = login() match
if (__loginResult eq null) case some: Some[LoginResult] => some.get
case None =>
logger error "Login to bot failed." logger error "Login to bot failed."
System exit -1 System exit -1
throw RuntimeException()
configure_exitCleanup() configure_exitCleanup()
@ -63,8 +65,8 @@ class MornyCoeur (using val config: MornyConfig) {
val events: MornyEventListeners = MornyEventListeners(using eventManager) val events: MornyEventListeners = MornyEventListeners(using eventManager)
/** inner value: about why morny exit, used in [[daemon.MornyReport]]. */ /** inner value: about why morny exit, used in [[daemon.MornyReport]]. */
private var whileExit_reason: AnyRef|Null = _ private var whileExit_reason: Option[AnyRef] = None
def exitReason: AnyRef|Null = whileExit_reason def exitReason: Option[AnyRef] = whileExit_reason
val coeurStartTimestamp: Long = ServerMain.systemStartupTime val coeurStartTimestamp: Long = ServerMain.systemStartupTime
///>>> BLOCK START instance configure & startup stage 2 ///>>> BLOCK START instance configure & startup stage 2
@ -101,12 +103,12 @@ class MornyCoeur (using val config: MornyConfig) {
} }
def exit (status: Int, reason: AnyRef): Unit = def exit (status: Int, reason: AnyRef): Unit =
whileExit_reason = reason whileExit_reason = Some(reason)
System exit status System exit status
private case class LoginResult(account: TelegramBot, username: String, userid: Long) 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) val builder = TelegramBot.Builder(config.telegramBotKey)
var api_bot = config.telegramBotApiServer var api_bot = config.telegramBotApiServer
@ -129,7 +131,7 @@ class MornyCoeur (using val config: MornyConfig) {
val account = builder build val account = builder build
logger info "Trying to login..." logger info "Trying to login..."
boundary[LoginResult|Null] { boundary[Option[LoginResult]] {
for (i <- 0 to 3) { for (i <- 0 to 3) {
if i > 0 then logger info "retrying..." if i > 0 then logger info "retrying..."
try { try {
@ -137,16 +139,16 @@ class MornyCoeur (using val config: MornyConfig) {
if ((config.telegramBotUsername ne null) && config.telegramBotUsername != remote.username) if ((config.telegramBotUsername ne null) && config.telegramBotUsername != remote.username)
throw RuntimeException(s"Required the bot @${config.telegramBotUsername} but @${remote.username} logged in") throw RuntimeException(s"Required the bot @${config.telegramBotUsername} but @${remote.username} logged in")
logger info s"Succeed logged in to @${remote.username}" 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 } catch
case r: boundary.Break[LoginResult|Null] => throw r case r: boundary.Break[Option[LoginResult]] => throw r
case e => case e =>
logger error logger error
s"""${exceptionLog(e)} s"""${exceptionLog(e)}
|login failed""" |login failed"""
.stripMargin .stripMargin
} }
null None
} }
} }

View File

@ -10,6 +10,12 @@ import com.pengrad.telegrambot.UpdatesListener
import scala.collection.mutable import scala.collection.mutable
import scala.language.postfixOps 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 { class EventListenerManager (using coeur: MornyCoeur) extends UpdatesListener {
private val listeners = mutable.Queue.empty[EventListener] private val listeners = mutable.Queue.empty[EventListener]
@ -77,9 +83,19 @@ class EventListenerManager (using coeur: MornyCoeur) extends UpdatesListener {
} }
import java.util import java.util
import scala.jdk.CollectionConverters.* 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 = { override def process (updates: util.List[Update]): Int = {
for (update <- updates.asScala) for (update <- updates.asScala)
EventRunner(using update).start() EventRunner(using update).start()

View File

@ -1,17 +1,43 @@
package cc.sukazyo.cono.morny.bot.command package cc.sukazyo.cono.morny.bot.command
/** One alias definition, contains the necessary message of how
* to process the alias.
*/
trait ICommandAlias { trait ICommandAlias {
/** The alias name.
*
* same with the command name, it is the unique identifier of this alias.
*/
val name: String 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 val listed: Boolean
} }
/** Default implementations of [[ICommandAlias]]. */
object 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: case class ListedAlias (name: String) extends ICommandAlias:
override val listed = true 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: case class HiddenAlias (name: String) extends ICommandAlias:
override val listed = false override val listed = false

View File

@ -3,11 +3,37 @@ package cc.sukazyo.cono.morny.bot.command
import cc.sukazyo.cono.morny.util.tgapi.InputCommand import cc.sukazyo.cono.morny.util.tgapi.InputCommand
import com.pengrad.telegrambot.model.Update 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 { trait ISimpleCommand {
/** the main name of the command.
*
* must have a value as the unique identifier of this command.
*/
val name: String 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 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 def execute (using command: InputCommand, event: Update): Unit
} }

View File

@ -1,8 +1,25 @@
package cc.sukazyo.cono.morny.bot.command 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 { 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 val paramRule: String
/** The description/introduction of this command, used in human-readable
* command list.
*/
val description: String val description: String
} }

View File

@ -122,7 +122,7 @@ class MornyInformation (using coeur: MornyCoeur) extends ITelegramCommand {
event.message.chat.id, event.message.chat.id,
/* language=html */ /* language=html */
s"""system: s"""system:
|- <code>${h(if (getRuntimeHostname == null) "<unknown-host>" else getRuntimeHostname)}</code> |- <code>${h(if getRuntimeHostname nonEmpty then getRuntimeHostname.get else "<unknown-host>")}</code>
|- <code>${h(sysprop("os.name"))}</code> <code>${h(sysprop("os.arch"))}</code> <code>${h(sysprop("os.version"))}</code> |- <code>${h(sysprop("os.name"))}</code> <code>${h(sysprop("os.arch"))}</code> <code>${h(sysprop("os.version"))}</code>
|java runtime: |java runtime:
|- <code>${h(sysprop("java.vm.vendor"))}.${h(sysprop("java.vm.name"))}</code> |- <code>${h(sysprop("java.vm.vendor"))}.${h(sysprop("java.vm.name"))}</code>

View File

@ -25,14 +25,12 @@ class Nbnhhsh (using coeur: MornyCoeur) extends ITelegramCommand {
override def execute (using command: InputCommand, event: Update): Unit = { override def execute (using command: InputCommand, event: Update): Unit = {
val queryTarget: String|Null = val queryTarget: String =
if command.args nonEmpty then if command.args nonEmpty then
command.args mkString " " command.args mkString " "
else if (event.message.replyToMessage != null && event.message.replyToMessage.text != null) else if (event.message.replyToMessage != null && event.message.replyToMessage.text != null)
event.message.replyToMessage.text event.message.replyToMessage.text
else null else
if (queryTarget == null)
coeur.account exec SendSticker( coeur.account exec SendSticker(
event.message.chat.id, event.message.chat.id,
TelegramStickers ID_404 TelegramStickers ID_404

View File

@ -129,8 +129,8 @@ class OnCallMsgSend (using coeur: MornyCoeur) extends EventListener {
} }
if messageToSend.message eq null then return true if messageToSend.message eq null then return true
val testSendResponse = coeur.account execute messageToSend.toSendMessage(update.message.chat.id) val testSendResponse = coeur.account execute
.replyToMessageId(update.message.messageId) messageToSend.toSendMessage(update.message.chat.id).replyToMessageId(update.message.messageId)
if (!(testSendResponse isOk)) if (!(testSendResponse isOk))
coeur.account exec SendMessage( coeur.account exec SendMessage(
update.message.chat.id, update.message.chat.id,

View File

@ -8,6 +8,7 @@ import com.pengrad.telegrambot.model.Update
import com.pengrad.telegrambot.request.SendMessage import com.pengrad.telegrambot.request.SendMessage
import scala.language.postfixOps import scala.language.postfixOps
import scala.util.boundary
class OnQuestionMarkReply (using coeur: MornyCoeur) extends EventListener { class OnQuestionMarkReply (using coeur: MornyCoeur) extends EventListener {
@ -34,10 +35,10 @@ object OnQuestionMarkReply {
private val QUESTION_MARKS = Set('?', '', '¿', '⁈', '⁇', '‽', '❔', '❓') private val QUESTION_MARKS = Set('?', '', '¿', '⁈', '⁇', '‽', '❔', '❓')
def isAllMessageMark (using text: String): Boolean = { def isAllMessageMark (using text: String): Boolean = {
var isAll = true boundary[Boolean] {
for (c <- text) for (c <- text) if QUESTION_MARKS contains c then boundary.break(false)
if !(QUESTION_MARKS contains c) then isAll = false true
isAll }
} }
} }

View File

@ -3,6 +3,7 @@ package cc.sukazyo.cono.morny.bot.query
import cc.sukazyo.cono.morny.Log.logger import cc.sukazyo.cono.morny.Log.logger
import cc.sukazyo.cono.morny.util.tgapi.formatting.NamingUtils.inlineQueryId import cc.sukazyo.cono.morny.util.tgapi.formatting.NamingUtils.inlineQueryId
import cc.sukazyo.cono.morny.util.BiliTool 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.Update
import com.pengrad.telegrambot.model.request.{InlineQueryResultArticle, InputTextMessageContent, ParseMode} import com.pengrad.telegrambot.model.request.{InlineQueryResultArticle, InputTextMessageContent, ParseMode}
@ -24,23 +25,23 @@ class ShareToolBilibili extends ITelegramQuery {
if (event.inlineQuery.query == null) return null if (event.inlineQuery.query == null) return null
event.inlineQuery.query match 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 logger debug
s"""====== Share Tool Bilibili Catch ok s"""====== Share Tool Bilibili Catch ok
|1: ${_1} |1: ${_url_v}
|2: ${_2} |2: ${_url_av}
|3: ${_3} |3: ${_url_bv}
|4: ${_4} |4: ${_url_param}
|5: ${_5} |5: ${_url_v_part}
|6: ${_6} |6: ${_raw_av}
|7: ${_7}""" |7: ${_raw_bv}"""
.stripMargin .stripMargin
var av = if (_2 != null) _2 else if (_6 != null) _6 else null var av = select(_url_av, _raw_av)
var bv = if (_3!=null) _3 else if (_7!=null) _7 else null var bv = select(_url_bv, _raw_bv)
logger trace s"catch id av[$av] bv[$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]" logger trace s"catch video part[$part]"
if (av == null) { if (av == null) {

View File

@ -21,15 +21,15 @@ class ShareToolTwitter extends ITelegramQuery {
event.inlineQuery.query match event.inlineQuery.query match
case REGEX_TWEET_LINK(_, _2, _, _, _, _) => case REGEX_TWEET_LINK(_, _path_data, _, _, _, _) =>
List( List(
InlineQueryUnit(InlineQueryResultArticle( InlineQueryUnit(InlineQueryResultArticle(
inlineQueryId(ID_PREFIX_VX+event.inlineQuery.query), TITLE_VX, inlineQueryId(ID_PREFIX_VX+event.inlineQuery.query), TITLE_VX,
s"https://vxtwitter.com/$_2" s"https://vxtwitter.com/$_path_data"
)), )),
InlineQueryUnit(InlineQueryResultArticle( InlineQueryUnit(InlineQueryResultArticle(
inlineQueryId(ID_PREFIX_VX_COMBINED+event.inlineQuery.query), TITLE_VX_COMBINED, inlineQueryId(ID_PREFIX_VX_COMBINED+event.inlineQuery.query), TITLE_VX_COMBINED,
s"https://c.vxtwitter.com/$_2" s"https://c.vxtwitter.com/$_path_data"
)) ))
) )

View File

@ -24,7 +24,7 @@ class MedicationTimer (using coeur: MornyCoeur) extends Thread {
this.setName(DAEMON_THREAD_NAME_DEF) this.setName(DAEMON_THREAD_NAME_DEF)
private var lastNotify_messageId: Int|Null = _ private var lastNotify_messageId: Option[Int] = None
override def run (): Unit = { override def run (): Unit = {
logger info "Medication Timer started." logger info "Medication Timer started."
@ -51,8 +51,8 @@ class MedicationTimer (using coeur: MornyCoeur) extends Thread {
private def sendNotification(): Unit = { private def sendNotification(): Unit = {
val sendResponse: SendResponse = coeur.account exec SendMessage(notify_toChat, NOTIFY_MESSAGE) val sendResponse: SendResponse = coeur.account exec SendMessage(notify_toChat, NOTIFY_MESSAGE)
if sendResponse isOk then lastNotify_messageId = sendResponse.message.messageId if sendResponse isOk then lastNotify_messageId = Some(sendResponse.message.messageId)
else lastNotify_messageId = null else lastNotify_messageId = None
} }
@throws[InterruptedException | IllegalArgumentException] @throws[InterruptedException | IllegalArgumentException]
@ -61,7 +61,7 @@ class MedicationTimer (using coeur: MornyCoeur) extends Thread {
} }
def refreshNotificationWrite (edited: Message): Unit = { 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 import cc.sukazyo.cono.morny.util.CommonFormat.formatDate
val editTime = formatDate(edited.editDate*1000, use_timeZone.getTotalSeconds/60/60) val editTime = formatDate(edited.editDate*1000, use_timeZone.getTotalSeconds/60/60)
val entities = ArrayBuffer.empty[MessageEntity] val entities = ArrayBuffer.empty[MessageEntity]
@ -72,7 +72,7 @@ class MedicationTimer (using coeur: MornyCoeur) extends Thread {
edited.messageId, edited.messageId,
edited.text + s"\n-- $editTime --" edited.text + s"\n-- $editTime --"
).entities(entities toArray:_*) ).entities(entities toArray:_*)
lastNotify_messageId = null lastNotify_messageId = None
} }
} }

View File

@ -101,9 +101,9 @@ class MornyReport (using coeur: MornyCoeur) {
def reportCoeurExit (): Unit = { def reportCoeurExit (): Unit = {
val causedTag = coeur.exitReason match val causedTag = coeur.exitReason match
case u: User => u.fullnameRefHTML case None => "UNKNOWN reason"
case n if n == null => "UNKNOWN reason" case u: Some[User] => u.get.fullnameRefHTML
case a: AnyRef => /*language=html*/ s"<code>${h(a.toString)}</code>" case a: Some[_] => /*language=html*/ s"<code>${h(a.get.toString)}</code>"
executeReport(SendMessage( executeReport(SendMessage(
coeur.config.reportToChat, coeur.config.reportToChat,
// language=html // language=html

View File

@ -29,9 +29,9 @@ object MornyInformation {
} }
//noinspection ScalaWeakerAccess //noinspection ScalaWeakerAccess
def getRuntimeHostname: String | Null = { def getRuntimeHostname: Option[String] = {
try InetAddress.getLocalHost.getHostName try Some(InetAddress.getLocalHost.getHostName)
catch case _: UnknownHostException => null catch case _: UnknownHostException => None
} }
def getAboutPic: Array[Byte] = TelegramImages.IMG_ABOUT get def getAboutPic: Array[Byte] = TelegramImages.IMG_ABOUT get

View File

@ -13,17 +13,17 @@ object TelegramImages {
class AssetsFileImage (assetsPath: String) { class AssetsFileImage (assetsPath: String) {
private var cache: Array[Byte]|Null = _ private var cache: Option[Array[Byte]] = None
@throws[AssetsException] @throws[AssetsException]
def get:Array[Byte] = def get:Array[Byte] =
if cache eq null then read() if cache isEmpty then read()
cache cache.get
@throws[AssetsException] @throws[AssetsException]
private def read (): Unit = { private def read (): Unit = {
Using ((MornyAssets.pack getResource assetsPath)read) { stream => Using ((MornyAssets.pack getResource assetsPath)read) { stream =>
try { this.cache = stream.readAllBytes() } try { this.cache = Some(stream.readAllBytes()) }
catch case e: IOException => { catch case e: IOException => {
throw AssetsException(e) throw AssetsException(e)
} }

View File

@ -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
}
}
}