Compare commits

...

5 Commits

Author SHA1 Message Date
69e9459ebc
[[release 1.2.0*xiongan]]
## 📇 Function & Mechanisms

- add for /encrypt
  - add urlencode / urldecode sub-command: they process text input only
  - add alias /enc
- add for InlineBilibiliShare
  - add b23.tv share-link parse
  - add b23.tv video link parse

## 🔩 for self-hosted/developer

- cha EventListener use EventEnv instead of Update
- new LogLevel NOTICE(notice) and ATTION(attention)
  - with new formatter
2023-10-18 17:25:41 +08:00
40bdbec1ec
add tests for BilibiliForms, support b23 video link and better v-part parse 2023-10-17 18:49:57 +08:00
60dbcef140
add urlencode/decode for /encrypt, add b23.tv parse for InlineBilibiliShare 2023-10-17 14:16:29 +08:00
79206dd13b
new logger level and formatter
- add MornyLogLevels with new log level NOTICE(notice) and ATTION(attention)
  - make mechanic and morny (but not coeur) related INFO to NOTICE, warn to ATTION
- add MornyFormatterConsole to support the formatted time display, and some other formatter changed
- now startup key output will hide key except the starting and ending 4 chars
- change the function definition of consume in EventEnv
- removed src/test/scala/live/LiveMain
  - and added gitignore for src/test/scala/live
2023-10-15 21:14:54 +08:00
9c433ba0ab
use EventEnv as event encapsulate instead of Update
- make MornyOnTelegramCommand provides InputCommand
  - change OnUniMeowTrigger consume InputCommand
2023-10-12 18:10:11 +08:00
35 changed files with 603 additions and 260 deletions

View File

@ -5,19 +5,19 @@ MORNY_ARCHIVE_NAME = morny-coeur
MORNY_CODE_STORE = https://github.com/Eyre-S/Coeur-Morny-Cono
MORNY_COMMIT_PATH = https://github.com/Eyre-S/Coeur-Morny-Cono/commit/%s
VERSION = 1.1.1
VERSION = 1.2.0
USE_DELTA = false
VERSION_DELTA =
CODENAME = nanchang
CODENAME = xiongan
# dependencies
lib_spotbugs_v = 4.7.3
lib_scalamodule_xml_v = 2.2.0
lib_messiva_v = 0.1.1
lib_messiva_v = 0.2.0
lib_resourcetools_v = 0.2.2
lib_javatelegramapi_v = 6.2.0

View File

@ -1,2 +1,2 @@
rootProject.name = 'Coeur Morny Cono'
rootProject.name = "Coeur Morny Cono"

View File

@ -1,25 +1,27 @@
package cc.sukazyo.cono.morny
import cc.sukazyo.cono.morny.internal.logging.{MornyFormatterConsole, MornyLoggerBase}
import cc.sukazyo.messiva.appender.ConsoleAppender
import cc.sukazyo.messiva.formatter.SimpleFormatter
import cc.sukazyo.messiva.log.LogLevel
import cc.sukazyo.messiva.log.LogLevels
import cc.sukazyo.messiva.logger.Logger
import java.io.{PrintWriter, StringWriter}
object Log {
val logger: Logger = Logger(
val logger: MornyLoggerBase = MornyLoggerBase(
ConsoleAppender(
SimpleFormatter()
MornyFormatterConsole()
)
).minLevel(LogLevel.INFO)
)
logger minLevel LogLevels.INFO
def debug: Boolean = logger.levelSetting.minLevel.level <= LogLevel.DEBUG.level
def debug: Boolean = logger.levelSetting.minLevel.level <= LogLevels.DEBUG.level
def debug(is: Boolean): Unit =
if is then logger.minLevel(LogLevel.ALL)
else logger.minLevel(LogLevel.INFO)
if is then logger.minLevel(LogLevels.ALL)
else logger.minLevel(LogLevels.INFO)
def exceptionLog (e: Throwable): String =
val stackTrace = StringWriter()

View File

@ -27,7 +27,8 @@ class MornyCoeur (using val config: MornyConfig) {
logger info "Coeur starting..."
logger info s"args key:\n ${config.telegramBotKey}"
import cc.sukazyo.cono.morny.util.StringEnsure.deSensitive
logger info s"args key:\n ${config.telegramBotKey deSensitive 4}"
if config.telegramBotUsername ne null then
logger info s"login as:\n ${config.telegramBotUsername}"
@ -92,7 +93,7 @@ class MornyCoeur (using val config: MornyConfig) {
def saveDataAll(): Unit = {
// nothing to do
logger info "done all save action."
logger notice "done all save action."
}
private def exitCleanup (): Unit = {

View File

@ -5,11 +5,15 @@ import cc.sukazyo.cono.morny.MornyConfig.CheckFailure
import cc.sukazyo.cono.morny.util.CommonFormat
import java.time.ZoneOffset
import java.util.TimeZone
import scala.collection.mutable.ArrayBuffer
import scala.language.postfixOps
object ServerMain {
val tz: TimeZone = TimeZone getDefault
val tz_offset: ZoneOffset = ZoneOffset ofTotalSeconds (tz.getRawOffset/1000)
private val THREAD_MORNY_INIT: String = "morny-init"
def main (args: Array[String]): Unit = {
@ -135,6 +139,9 @@ object ServerMain {
|- Morny ${MornySystem.CODENAME toUpperCase}
|- <${MornySystem.getJarMD5}> [${BuildConfig.CODE_TIMESTAMP}]""".stripMargin
// due to [[MornyFormatterConsole]] will use a localized time, it will output to the log
logger info s"logging time will use time-zone ${tz.getID} ($tz_offset)"
///
/// Check Coeur arguments
/// finally start Coeur Program

View File

@ -0,0 +1,37 @@
package cc.sukazyo.cono.morny.bot.api
import com.pengrad.telegrambot.model.Update
import scala.collection.mutable
class EventEnv (
val update: Update
) {
private var _isOk: Int = 0
private val variables: mutable.HashMap[Class[?], Any] = mutable.HashMap.empty
def isEventOk: Boolean = _isOk > 0
//noinspection UnitMethodIsParameterless
def setEventOk: Unit =
_isOk = _isOk + 1
def provide (i: Any): Unit =
variables += (i.getClass -> i)
def consume [T] (t: Class[T]) (consumer: T => Unit): ConsumeResult = {
variables get t match
case Some(i) => consumer(i.asInstanceOf[T]); ConsumeResult(true)
case None => ConsumeResult(false)
}
class ConsumeResult (success: Boolean) {
def onfail (processor: => Unit): Unit = {
if !success then processor
}
}
}

View File

@ -1,22 +1,20 @@
package cc.sukazyo.cono.morny.bot.api
import com.pengrad.telegrambot.model.Update
trait EventListener () {
def onMessage (using Update): Boolean = false
def onEditedMessage (using Update): Boolean = false
def onChannelPost (using Update): Boolean = false
def onEditedChannelPost (using Update): Boolean = false
def onInlineQuery (using Update): Boolean = false
def onChosenInlineResult (using Update): Boolean = false
def onCallbackQuery (using Update): Boolean = false
def onShippingQuery (using Update): Boolean = false
def onPreCheckoutQuery (using Update): Boolean = false
def onPoll (using Update): Boolean = false
def onPollAnswer (using Update): Boolean = false
def onMyChatMemberUpdated (using Update): Boolean = false
def onChatMemberUpdated (using Update): Boolean = false
def onChatJoinRequest (using Update): Boolean = false
def onMessage (using EventEnv): Unit = {}
def onEditedMessage (using EventEnv): Unit = {}
def onChannelPost (using EventEnv): Unit = {}
def onEditedChannelPost (using EventEnv): Unit = {}
def onInlineQuery (using EventEnv): Unit = {}
def onChosenInlineResult (using EventEnv): Unit = {}
def onCallbackQuery (using EventEnv): Unit = {}
def onShippingQuery (using EventEnv): Unit = {}
def onPreCheckoutQuery (using EventEnv): Unit = {}
def onPoll (using EventEnv): Unit = {}
def onPollAnswer (using EventEnv): Unit = {}
def onMyChatMemberUpdated (using EventEnv): Unit = {}
def onChatMemberUpdated (using EventEnv): Unit = {}
def onChatJoinRequest (using EventEnv): Unit = {}
}

View File

@ -9,6 +9,7 @@ import com.pengrad.telegrambot.UpdatesListener
import scala.collection.mutable
import scala.language.postfixOps
import scala.util.boundary
/** Contains a [[mutable.Queue]] of [[EventListener]], and delivery telegram [[Update]].
*
@ -23,46 +24,43 @@ class EventListenerManager (using coeur: MornyCoeur) extends UpdatesListener {
def register (listeners: EventListener*): Unit =
this.listeners ++= listeners
private class EventRunner (using event: Update) extends Thread {
this setName s"evt-${event.updateId()}-nn"
private class EventRunner (using update: Update) extends Thread {
this setName s"upd-${update.updateId()}-nn"
private def updateThreadName (t: String): Unit =
this setName s"evt-${event.updateId()}-$t"
this setName s"upd-${update.updateId()}-$t"
override def run (): Unit = {
for (i <- listeners) {
object status:
var _status = 0
def isOk: Boolean = _status > 0
def check (u: Boolean): Unit = if u then _status = _status + 1
given env: EventEnv = EventEnv(update)
boundary { for (i <- listeners) {
try {
updateThreadName("message")
if event.message ne null then status check i.onMessage
if update.message ne null then i.onMessage
updateThreadName("edited-message")
if event.editedMessage ne null then status check i.onEditedMessage
if update.editedMessage ne null then i.onEditedMessage
updateThreadName("channel-post")
if event.channelPost ne null then status check i.onChannelPost
if update.channelPost ne null then i.onChannelPost
updateThreadName("edited-channel-post")
if event.editedChannelPost ne null then status check i.onEditedChannelPost
if update.editedChannelPost ne null then i.onEditedChannelPost
updateThreadName("inline-query")
if event.inlineQuery ne null then status check i.onInlineQuery
if update.inlineQuery ne null then i.onInlineQuery
updateThreadName("chosen-inline-result")
if event.chosenInlineResult ne null then status check i.onChosenInlineResult
if update.chosenInlineResult ne null then i.onChosenInlineResult
updateThreadName("callback-query")
if event.callbackQuery ne null then status check i.onCallbackQuery
if update.callbackQuery ne null then i.onCallbackQuery
updateThreadName("shipping-query")
if event.shippingQuery ne null then status check i.onShippingQuery
if update.shippingQuery ne null then i.onShippingQuery
updateThreadName("pre-checkout-query")
if event.preCheckoutQuery ne null then status check i.onPreCheckoutQuery
if update.preCheckoutQuery ne null then i.onPreCheckoutQuery
updateThreadName("poll")
if event.poll ne null then status check i.onPoll
if update.poll ne null then i.onPoll
updateThreadName("poll-answer")
if event.pollAnswer ne null then status check i.onPollAnswer
if update.pollAnswer ne null then i.onPollAnswer
updateThreadName("my-chat-member")
if event.myChatMember ne null then status check i.onMyChatMemberUpdated
if update.myChatMember ne null then i.onMyChatMemberUpdated
updateThreadName("chat-member")
if event.chatMember ne null then status check i.onChatMemberUpdated
if update.chatMember ne null then i.onChatMemberUpdated
updateThreadName("chat-join-request")
if event.chatJoinRequest ne null then status check i.onChatJoinRequest
if update.chatJoinRequest ne null then i.onChatJoinRequest
} catch case e => {
val errorMessage = StringBuilder()
errorMessage ++= "Event throws unexpected exception:\n"
@ -77,8 +75,8 @@ class EventListenerManager (using coeur: MornyCoeur) extends UpdatesListener {
logger error errorMessage.toString
coeur.daemons.reporter.exception(e, "on event running")
}
if (status isOk) return
}
if env.isEventOk then boundary.break()
}}
}
}

View File

@ -2,6 +2,7 @@ package cc.sukazyo.cono.morny.bot.command
import cc.sukazyo.cono.morny.Log.logger
import cc.sukazyo.cono.morny.MornyCoeur
import cc.sukazyo.cono.morny.bot.command.ICommandAlias.ListedAlias
import cc.sukazyo.cono.morny.data.TelegramStickers
import cc.sukazyo.cono.morny.util.tgapi.InputCommand
import cc.sukazyo.cono.morny.util.CommonEncrypt
@ -13,6 +14,7 @@ import com.pengrad.telegrambot.model.request.ParseMode
import com.pengrad.telegrambot.request.{GetFile, SendDocument, SendMessage, SendSticker}
import java.io.IOException
import java.net.{URLDecoder, URLEncoder}
import java.util.Base64
import scala.language.postfixOps
@ -20,7 +22,7 @@ import scala.language.postfixOps
class Encryptor (using coeur: MornyCoeur) extends ITelegramCommand {
override val name: String = "encrypt"
override val aliases: Array[ICommandAlias] | Null = null
override val aliases: Array[ICommandAlias] | Null = Array(ListedAlias("enc"))
override val paramRule: String = "[algorithm|(l)] [(uppercase)]"
override val description: String = "通过指定算法加密回复的内容 (目前只支持文本)"
@ -135,6 +137,12 @@ class Encryptor (using coeur: MornyCoeur) extends ITelegramCommand {
def genResult_hash (source: XEncryptable, processor: Array[Byte]=>Array[Byte]): EXHash =
val hashed = processor(source asByteArray) toHex;
EXHash(if mod_uppercase then hashed toUpperCase else hashed)
//noinspection UnitMethodIsParameterless
def echo_unsupported: Unit =
coeur.account exec SendSticker(
event.message.chat.id,
TelegramStickers ID_404
).replyToMessageId(event.message.messageId)
val result: EXHash|EXFile|EXText = args(0) match
case "base64" | "b64" | "base64url" | "base64u" | "b64u" =>
val _tool_b64 =
@ -154,21 +162,27 @@ class Encryptor (using coeur: MornyCoeur) extends ITelegramCommand {
_tool_b64d.decode,
CommonEncrypt.lint_base64FileName
) } catch case _: IllegalArgumentException =>
coeur.account exec SendSticker(
event.message.chat.id,
TelegramStickers ID_404 // todo: is here better erro notify?
).replyToMessageId(event.message.messageId)
echo_unsupported
return
case "urlencoder" | "urlencode" | "urlenc" | "url" =>
input match
case x: XText =>
EXText(URLEncoder.encode(x.data, ENCRYPT_STANDARD_CHARSET))
case _: XFile => echo_unsupported; return;
case "urldecoder" | "urldecode" | "urldec" | "urld" =>
input match
case _: XFile => echo_unsupported; return;
case x: XText =>
try { EXText(URLDecoder.decode(x.data, ENCRYPT_STANDARD_CHARSET)) }
catch case _: IllegalArgumentException =>
echo_unsupported
return
case "md5" => genResult_hash(input, MD5)
case "sha1" => genResult_hash(input, SHA1)
case "sha256" => genResult_hash(input, SHA256)
case "sha512" => genResult_hash(input, SHA512)
case _ =>
coeur.account exec SendSticker(
event.message.chat.id,
TelegramStickers ID_404
).replyToMessageId(event.message.messageId)
return;
echo_unsupported; return;
// END BLOCK: encrypt
// output
@ -203,6 +217,8 @@ class Encryptor (using coeur: MornyCoeur) extends ITelegramCommand {
* '''__base64url__''', base64u, b64u<br>
* '''__base64decode__''', base64d, b64d<br>
* '''__base64url-decode__''', base64ud, b64ud<br>
* '''urlencode''', urlencode, urlenc, url<br>
* '''__urldecoder__''', urldecode, urldec, urld<br>
* '''__sha1__'''<br>
* '''__sha256__'''<br>
* '''__sha512__'''<br>
@ -218,6 +234,8 @@ class Encryptor (using coeur: MornyCoeur) extends ITelegramCommand {
|<b><u>base64url</u></b>, base64u, b64u
|<b><u>base64decode</u></b>, base64d, b64d
|<b><u>base64url-decode</u></b>, base64ud, b64ud
|<b><u>urlencoder</u></b>, urlencode, urlenc, url
|<b><u>urldecoder</u></b>, urldecode, urldec, urld
|<b><u>sha1</u></b>
|<b><u>sha256</u></b>
|<b><u>sha512</u></b>

View File

@ -91,14 +91,14 @@ class MornyCommands (using coeur: MornyCoeur) {
val listing = commands_toTelegramList
automaticTGListRemove()
coeur.account exec SetMyCommands(listing:_*)
logger info
logger notice
s"""automatic updated telegram command list :
|${commandsTelegramList_toString(listing)}""".stripMargin
}
def automaticTGListRemove (): Unit = {
coeur.account exec DeleteMyCommands()
logger info "cleaned up command list"
logger notice "cleaned up command list"
}
private def commandsTelegramList_toString (list: Array[BotCommand]): String =

View File

@ -31,7 +31,7 @@ class MornyManagers (using coeur: MornyCoeur) {
event.message.chat.id,
TelegramStickers ID_EXIT
).replyToMessageId(event.message.messageId)
logger info s"Morny exited by user ${user toLogTag}"
logger attention s"Morny exited by user ${user toLogTag}"
coeur.exit(0, user)
} else {
@ -40,7 +40,7 @@ class MornyManagers (using coeur: MornyCoeur) {
event.message.chat.id,
TelegramStickers ID_403
).replyToMessageId(event.message.messageId)
logger info s"403 exit caught from user ${user toLogTag}"
logger attention s"403 exit caught from user ${user toLogTag}"
coeur.daemons.reporter.unauthenticatedAction("/exit", user)
}
@ -62,7 +62,7 @@ class MornyManagers (using coeur: MornyCoeur) {
if (coeur.trusted isTrusted user.id) {
logger info s"call save from command by ${user toLogTag}"
logger attention s"call save from command by ${user toLogTag}"
coeur.saveDataAll()
coeur.account exec SendSticker(
event.message.chat.id,
@ -75,7 +75,7 @@ class MornyManagers (using coeur: MornyCoeur) {
event.message.chat.id,
TelegramStickers ID_403
).replyToMessageId(event.message.messageId)
logger info s"403 save caught from user ${user toLogTag}"
logger attention s"403 save caught from user ${user toLogTag}"
coeur.daemons.reporter.unauthenticatedAction("/save", user)
}

View File

@ -7,7 +7,6 @@ import com.pengrad.telegrambot.model.Update
import com.pengrad.telegrambot.model.request.ParseMode
import com.pengrad.telegrambot.request.SendMessage
import javax.annotation.{Nonnull, Nullable}
import scala.language.postfixOps
class Testing (using coeur: MornyCoeur) extends ISimpleCommand {

View File

@ -1,7 +1,7 @@
package cc.sukazyo.cono.morny.bot.event
import cc.sukazyo.cono.morny.MornyCoeur
import cc.sukazyo.cono.morny.bot.api.EventListener
import cc.sukazyo.cono.morny.bot.api.{EventEnv, EventListener}
import cc.sukazyo.cono.morny.bot.query.{InlineQueryUnit, MornyQueries}
import cc.sukazyo.cono.morny.util.tgapi.TelegramExtensions.Bot.exec
import com.pengrad.telegrambot.model.Update
@ -14,7 +14,8 @@ import scala.reflect.ClassTag
class MornyOnInlineQuery (using queryManager: MornyQueries) (using coeur: MornyCoeur) extends EventListener {
override def onInlineQuery (using update: Update): Boolean = {
override def onInlineQuery (using event: EventEnv): Unit = {
import event.update
val results: List[InlineQueryUnit[_]] = queryManager query update
@ -27,12 +28,13 @@ class MornyOnInlineQuery (using queryManager: MornyQueries) (using coeur: MornyC
resultAnswers += r.result
}
if (results isEmpty) return false
if (results isEmpty) return;
coeur.account exec AnswerInlineQuery(
update.inlineQuery.id, resultAnswers toArray:_*
).cacheTime(cacheTime).isPersonal(isPersonal)
true
event.setEventOk
}

View File

@ -1,6 +1,6 @@
package cc.sukazyo.cono.morny.bot.event
import cc.sukazyo.cono.morny.bot.api.EventListener
import cc.sukazyo.cono.morny.bot.api.{EventEnv, EventListener}
import cc.sukazyo.cono.morny.Log.logger
import cc.sukazyo.cono.morny.MornyCoeur
import cc.sukazyo.cono.morny.bot.command.MornyCommands
@ -9,7 +9,8 @@ import com.pengrad.telegrambot.model.{Message, Update}
class MornyOnTelegramCommand (using commandManager: MornyCommands) (using coeur: MornyCoeur) extends EventListener {
override def onMessage (using update: Update): Boolean = {
override def onMessage (using event: EventEnv): Unit = {
given update: Update = event.update
def _isCommandMessage(message: Message): Boolean =
if message.text eq null then false
@ -17,17 +18,19 @@ class MornyOnTelegramCommand (using commandManager: MornyCommands) (using coeur:
else if message.text startsWith "/ " then false
else true
if !_isCommandMessage(update.message) then return false
if !_isCommandMessage(update.message) then return
val inputCommand = InputCommand(update.message.text drop 1)
event provide inputCommand
logger trace ":provided InputCommand for event"
if (!(inputCommand.command matches "^\\w+$"))
logger debug "not command"
false
else if ((inputCommand.target ne null) && (inputCommand.target != coeur.username))
logger debug "not morny command"
false
else
logger debug "is command"
commandManager.execute(using inputCommand)
if commandManager.execute(using inputCommand) then
event.setEventOk
}

View File

@ -1,17 +1,18 @@
package cc.sukazyo.cono.morny.bot.event
import cc.sukazyo.cono.morny.bot.api.EventListener
import cc.sukazyo.cono.morny.bot.api.{EventEnv, EventListener}
import cc.sukazyo.cono.morny.MornyCoeur
import com.pengrad.telegrambot.model.Update
class MornyOnUpdateTimestampOffsetLock (using coeur: MornyCoeur) extends EventListener {
private def isOutdated (timestamp: Int): Boolean =
coeur.config.eventIgnoreOutdated && (timestamp < (coeur.coeurStartTimestamp/1000))
private def checkOutdated (timestamp: Int)(using event: EventEnv): Unit =
if coeur.config.eventIgnoreOutdated && (timestamp < (coeur.coeurStartTimestamp/1000)) then
event.setEventOk
override def onMessage (using update: Update): Boolean = isOutdated(update.message.date)
override def onEditedMessage (using update: Update): Boolean = isOutdated(update.editedMessage.date)
override def onChannelPost (using update: Update): Boolean = isOutdated(update.channelPost.date)
override def onEditedChannelPost (using update: Update): Boolean = isOutdated(update.editedChannelPost.date)
override def onMessage (using event: EventEnv): Unit = checkOutdated(event.update.message.date)
override def onEditedMessage (using event: EventEnv): Unit = checkOutdated(event.update.editedMessage.date)
override def onChannelPost (using event: EventEnv): Unit = checkOutdated(event.update.channelPost.date)
override def onEditedChannelPost (using event: EventEnv): Unit = checkOutdated(event.update.editedChannelPost.date)
}

View File

@ -1,7 +1,7 @@
package cc.sukazyo.cono.morny.bot.event
import cc.sukazyo.cono.morny.MornyCoeur
import cc.sukazyo.cono.morny.bot.api.EventListener
import cc.sukazyo.cono.morny.bot.api.{EventEnv, EventListener}
import cc.sukazyo.cono.morny.data.TelegramStickers
import cc.sukazyo.cono.morny.util.tgapi.formatting.TelegramFormatter.*
import cc.sukazyo.cono.morny.util.tgapi.TelegramExtensions.Bot.exec
@ -15,10 +15,11 @@ class OnCallMe (using coeur: MornyCoeur) extends EventListener {
private val me = coeur.config.trustedMaster
override def onMessage (using update: Update): Boolean = {
override def onMessage (using event: EventEnv): Unit = {
import event.update
if update.message.text == null then return false
if update.message.chat.`type` != (Chat.Type Private) then return false
if update.message.text == null then return;
if update.message.chat.`type` != (Chat.Type Private) then return
//noinspection ScalaUnnecessaryParentheses
val success = if me == -1 then false else
@ -32,7 +33,7 @@ class OnCallMe (using coeur: MornyCoeur) extends EventListener {
case cc if cc startsWith "cc::" =>
requestCustom(update.message)
case _ =>
return false
return;
if success then
coeur.account exec SendSticker(
@ -45,7 +46,7 @@ class OnCallMe (using coeur: MornyCoeur) extends EventListener {
TelegramStickers ID_501
).replyToMessageId(update.message.messageId)
true
event.setEventOk
}

View File

@ -1,6 +1,6 @@
package cc.sukazyo.cono.morny.bot.event
import cc.sukazyo.cono.morny.bot.api.EventListener
import cc.sukazyo.cono.morny.bot.api.{EventEnv, EventListener}
import cc.sukazyo.cono.morny.MornyCoeur
import cc.sukazyo.cono.morny.data.TelegramStickers
import cc.sukazyo.cono.morny.util.tgapi.TelegramExtensions.Bot.exec
@ -52,26 +52,28 @@ class OnCallMsgSend (using coeur: MornyCoeur) extends EventListener {
case _ => null
}
override def onMessage (using update: Update): Boolean = {
override def onMessage (using event: EventEnv): Unit = {
import event.update
val message = update.message
if message.chat.`type` != Chat.Type.Private then return false
if message.text eq null then return false
if !(message.text startsWith "*msg") then return false
if message.chat.`type` != Chat.Type.Private then return;
if message.text eq null then return;
if !(message.text startsWith "*msg") then return;
if (!(coeur.trusted isTrusted message.from.id))
coeur.account exec SendSticker(
message.chat.id,
TelegramStickers ID_403
).replyToMessageId(message.messageId)
return true
event.setEventOk
return;
if (message.text == "*msgsend") {
if (message.replyToMessage eq null) return answer404
if (message.replyToMessage eq null) { answer404; return }
val messageToSend = MessageToSend from message.replyToMessage
if ((messageToSend eq null) || (messageToSend.message eq null)) return answer404
if ((messageToSend eq null) || (messageToSend.message eq null)) { answer404; return }
val sendResponse = coeur.account execute messageToSend.toSendMessage()
if (sendResponse isOk) {
@ -89,20 +91,21 @@ class OnCallMsgSend (using coeur: MornyCoeur) extends EventListener {
).replyToMessageId(update.message.messageId).parseMode(ParseMode HTML)
}
return true
event.setEventOk
return
}
val messageToSend: MessageToSend =
val raw: Message =
if (message.text == "*msg")
if message.replyToMessage eq null then return answer404
if message.replyToMessage eq null then { answer404; return }
else message.replyToMessage
else if (message.text startsWith "*msg")
message
else return answer404
else { answer404; return }
val _toSend = MessageToSend from raw
if _toSend eq null then return answer404
if _toSend eq null then { answer404; return }
else _toSend
val targetChatResponse = coeur.account execute GetChat(messageToSend.targetId)
@ -128,7 +131,7 @@ class OnCallMsgSend (using coeur: MornyCoeur) extends EventListener {
).parseMode(ParseMode HTML).replyToMessageId(update.message.messageId)
}
if messageToSend.message eq null then return true
if messageToSend.message eq null then { answer404; return }
val testSendResponse = coeur.account execute
messageToSend.toSendMessage(update.message.chat.id).replyToMessageId(update.message.messageId)
if (!(testSendResponse isOk))
@ -140,15 +143,15 @@ class OnCallMsgSend (using coeur: MornyCoeur) extends EventListener {
.stripMargin
).parseMode(ParseMode HTML).replyToMessageId(update.message.messageId)
true
event.setEventOk
}
private def answer404 (using update: Update): Boolean =
private def answer404 (using event: EventEnv): Unit =
coeur.account exec SendSticker(
update.message.chat.id,
event.update.message.chat.id,
TelegramStickers ID_404
).replyToMessageId(update.message.messageId)
true
).replyToMessageId(event.update.message.messageId)
event.setEventOk
}

View File

@ -1,6 +1,6 @@
package cc.sukazyo.cono.morny.bot.event
import cc.sukazyo.cono.morny.bot.api.EventListener
import cc.sukazyo.cono.morny.bot.api.{EventEnv, EventListener}
import cc.sukazyo.cono.morny.Log.logger
import cc.sukazyo.cono.morny.MornyCoeur
import com.google.gson.GsonBuilder
@ -13,35 +13,38 @@ import scala.language.postfixOps
class OnEventHackHandle (using coeur: MornyCoeur) extends EventListener {
import coeur.daemons.eventHack.trigger
private def trigger (chat_id: Long, from_id: Long)(using event: EventEnv): Unit =
given Update = event.update
if coeur.daemons.eventHack.trigger(chat_id, from_id) then
event.setEventOk
override def onMessage (using update: Update): Boolean =
trigger(update.message.chat.id, update.message.from.id)
override def onEditedMessage (using update: Update): Boolean =
trigger(update.editedMessage.chat.id, update.editedMessage.from.id)
override def onChannelPost (using update: Update): Boolean =
trigger(update.channelPost.chat.id, 0)
override def onEditedChannelPost (using update: Update): Boolean =
trigger(update.editedChannelPost.chat.id, 0)
override def onInlineQuery (using update: Update): Boolean =
trigger(0, update.inlineQuery.from.id)
override def onChosenInlineResult (using update: Update): Boolean =
trigger(0, update.chosenInlineResult.from.id)
override def onCallbackQuery (using update: Update): Boolean =
trigger(0, update.callbackQuery.from.id)
override def onShippingQuery (using update: Update): Boolean =
trigger(0, update.shippingQuery.from.id)
override def onPreCheckoutQuery (using update: Update): Boolean =
trigger(0, update.preCheckoutQuery.from.id)
override def onPoll (using update: Update): Boolean =
override def onMessage (using event: EventEnv): Unit =
trigger(event.update.message.chat.id, event.update.message.from.id)
override def onEditedMessage (using event: EventEnv): Unit =
trigger(event.update.editedMessage.chat.id, event.update.editedMessage.from.id)
override def onChannelPost (using event: EventEnv): Unit =
trigger(event.update.channelPost.chat.id, 0)
override def onEditedChannelPost (using event: EventEnv): Unit =
trigger(event.update.editedChannelPost.chat.id, 0)
override def onInlineQuery (using event: EventEnv): Unit =
trigger(0, event.update.inlineQuery.from.id)
override def onChosenInlineResult (using event: EventEnv): Unit =
trigger(0, event.update.chosenInlineResult.from.id)
override def onCallbackQuery (using event: EventEnv): Unit =
trigger(0, event.update.callbackQuery.from.id)
override def onShippingQuery (using event: EventEnv): Unit =
trigger(0, event.update.shippingQuery.from.id)
override def onPreCheckoutQuery (using event: EventEnv): Unit =
trigger(0, event.update.preCheckoutQuery.from.id)
override def onPoll (using event: EventEnv): Unit =
trigger(0, 0)
override def onPollAnswer (using update: Update): Boolean =
trigger(0, update.pollAnswer.user.id)
override def onMyChatMemberUpdated (using update: Update): Boolean =
trigger(update.myChatMember.chat.id, update.myChatMember.from.id)
override def onChatMemberUpdated (using update: Update): Boolean =
trigger(update.chatMember.chat.id, update.chatMember.from.id)
override def onChatJoinRequest (using update: Update): Boolean =
trigger(update.chatJoinRequest.chat.id, update.chatJoinRequest.from.id)
override def onPollAnswer (using event: EventEnv): Unit =
trigger(0, event.update.pollAnswer.user.id)
override def onMyChatMemberUpdated (using event: EventEnv): Unit =
trigger(event.update.myChatMember.chat.id, event.update.myChatMember.from.id)
override def onChatMemberUpdated (using event: EventEnv): Unit =
trigger(event.update.chatMember.chat.id, event.update.chatMember.from.id)
override def onChatJoinRequest (using event: EventEnv): Unit =
trigger(event.update.chatJoinRequest.chat.id, event.update.chatJoinRequest.from.id)
}

View File

@ -1,21 +1,21 @@
package cc.sukazyo.cono.morny.bot.event
import cc.sukazyo.cono.morny.bot.api.EventListener
import cc.sukazyo.cono.morny.bot.api.{EventEnv, EventListener}
import cc.sukazyo.cono.morny.MornyCoeur
import cc.sukazyo.cono.morny.daemon.{MedicationTimer, MornyDaemons}
import com.pengrad.telegrambot.model.{Message, Update}
class OnMedicationNotifyApply (using coeur: MornyCoeur) extends EventListener {
override def onEditedMessage (using event: Update): Boolean =
editedMessageProcess(event.editedMessage)
override def onEditedChannelPost (using event: Update): Boolean =
editedMessageProcess(event.editedChannelPost)
override def onEditedMessage (using event: EventEnv): Unit =
editedMessageProcess(event.update.editedMessage)
override def onEditedChannelPost (using event: EventEnv): Unit =
editedMessageProcess(event.update.editedChannelPost)
private def editedMessageProcess (edited: Message): Boolean = {
if edited.chat.id != coeur.config.medicationNotifyToChat then return false
private def editedMessageProcess (edited: Message)(using event: EventEnv): Unit = {
if edited.chat.id != coeur.config.medicationNotifyToChat then return;
coeur.daemons.medicationTimer.refreshNotificationWrite(edited)
true
event.setEventOk
}
}

View File

@ -1,6 +1,6 @@
package cc.sukazyo.cono.morny.bot.event
import cc.sukazyo.cono.morny.bot.api.EventListener
import cc.sukazyo.cono.morny.bot.api.{EventEnv, EventListener}
import cc.sukazyo.cono.morny.MornyCoeur
import cc.sukazyo.cono.morny.bot.event.OnQuestionMarkReply.isAllMessageMark
import cc.sukazyo.cono.morny.util.tgapi.TelegramExtensions.Bot.exec
@ -12,19 +12,20 @@ import scala.util.boundary
class OnQuestionMarkReply (using coeur: MornyCoeur) extends EventListener {
override def onMessage (using event: Update): Boolean = {
override def onMessage (using event: EventEnv): Unit = {
import event.update
if event.message.text eq null then return false
if update.message.text eq null then return
import cc.sukazyo.cono.morny.util.UseMath.over
import cc.sukazyo.cono.morny.util.UseRandom.chance_is
if (1 over 8) chance_is false then return false
if !isAllMessageMark(using event.message.text) then return false
if (1 over 8) chance_is false then return;
if !isAllMessageMark(using update.message.text) then return;
coeur.account exec SendMessage(
event.message.chat.id, event.message.text
).replyToMessageId(event.message.messageId)
true
update.message.chat.id, update.message.text
).replyToMessageId(update.message.messageId)
event.setEventOk
}

View File

@ -1,23 +1,27 @@
package cc.sukazyo.cono.morny.bot.event
import cc.sukazyo.cono.morny.bot.api.EventListener
import cc.sukazyo.cono.morny.bot.api.{EventEnv, EventListener}
import cc.sukazyo.cono.morny.bot.command.MornyCommands
import cc.sukazyo.cono.morny.util.tgapi.InputCommand
import cc.sukazyo.cono.morny.Log.logger
import cc.sukazyo.cono.morny.MornyCoeur
import com.pengrad.telegrambot.model.Update
class OnUniMeowTrigger (using commands: MornyCommands) (using coeur: MornyCoeur) extends EventListener {
override def onMessage (using update: Update): Boolean = {
override def onMessage (using event: EventEnv): Unit = {
if update.message.text eq null then return false
var ok = false
for ((name, command) <- commands.commands_uni)
val _name = "/"+name
if (_name == update.message.text)
command.execute(using InputCommand(_name))
ok = true
ok
event.consume (classOf[InputCommand]) { input =>
logger trace s"got input command {$input} from event-context"
for ((name, command_instance) <- commands.commands_uni) {
logger trace s"checking uni-meow $name"
if (name == input.command)
logger trace "checked"
command_instance.execute(using input, event.update)
event.setEventOk
}
} onfail { logger trace "not command (for uni-meow)" }
}

View File

@ -1,7 +1,7 @@
package cc.sukazyo.cono.morny.bot.event
import cc.sukazyo.cono.morny.MornyCoeur
import cc.sukazyo.cono.morny.bot.api.EventListener
import cc.sukazyo.cono.morny.bot.api.{EventEnv, EventListener}
import cc.sukazyo.cono.morny.util.tgapi.TelegramExtensions.Bot.exec
import com.pengrad.telegrambot.model.Update
import com.pengrad.telegrambot.request.SendMessage
@ -17,10 +17,11 @@ class OnUserRandom (using coeur: MornyCoeur) {
private val USER_OR_QUERY = "^(.+)(?:还是|or)(.+)$" r
private val USER_IF_QUERY = "^(.+)(?:吗\\?||\\?|吗?)$" r
override def onMessage (using update: Update): Boolean = {
override def onMessage (using event: EventEnv): Unit = {
import event.update
if update.message.text == null then return false
if !(update.message.text startsWith "/") then return false
if update.message.text == null then return;
if !(update.message.text startsWith "/") then return
import cc.sukazyo.cono.morny.util.UseRandom.rand_half
val query = update.message.text substring 1
@ -29,17 +30,17 @@ class OnUserRandom (using coeur: MornyCoeur) {
if rand_half then _con1 else _con2
case USER_IF_QUERY(_con) =>
// for capability with [[OnQuestionMarkReply]]
if OnQuestionMarkReply.isAllMessageMark(using _con) then return false
if OnQuestionMarkReply.isAllMessageMark(using _con) then return;
(if rand_half then "不" else "") + _con
case _ => null
//noinspection DuplicatedCode
if result == null then return false
if result == null then return;
coeur.account exec SendMessage(
update.message.chat.id, result
update.message.chat.id,
result
).replyToMessageId(update.message.messageId)
true
event.setEventOk
}
@ -51,23 +52,23 @@ class OnUserRandom (using coeur: MornyCoeur) {
private val word_pattern = "^([\\w\\W]*)?(?:尊嘟假嘟|(?:O\\.o|o\\.O))$"r
private val keywords = Array("尊嘟假嘟", "O.o", "o.O")
override def onMessage (using event: Update): Boolean = {
override def onMessage (using event: EventEnv): Unit = {
import event.update
if event.message.text == null then return false
if update.message.text == null then return
var result: String|Null = null
import cc.sukazyo.cono.morny.util.UseRandom.rand_half
for (k <- keywords)
if event.message.text endsWith k then
if update.message.text endsWith k then
result = if rand_half then "尊嘟" else "假嘟"
//noinspection DuplicatedCode
if result == null then return false
if result == null then return;
coeur.account exec SendMessage(
event.message.chat.id,
update.message.chat.id,
result
).replyToMessageId(event.message.messageId)
true
).replyToMessageId(update.message.messageId)
event.setEventOk
}

View File

@ -1,7 +1,7 @@
package cc.sukazyo.cono.morny.bot.event
import cc.sukazyo.cono.morny.MornyCoeur
import cc.sukazyo.cono.morny.bot.api.EventListener
import cc.sukazyo.cono.morny.bot.api.{EventEnv, EventListener}
import cc.sukazyo.cono.morny.util.tgapi.formatting.TelegramFormatter.*
import cc.sukazyo.cono.morny.util.tgapi.formatting.TelegramParseEscape.escapeHtml as h
import cc.sukazyo.cono.morny.util.UniversalCommand
@ -16,10 +16,11 @@ class OnUserSlashAction (using coeur: MornyCoeur) extends EventListener {
private val TG_FORMAT = "^\\w+(@\\w+)?$"r
override def onMessage (using update: Update): Boolean = {
override def onMessage (using event: EventEnv): Unit = {
import event.update
val text = update.message.text
if text == null then return false
if text == null then return;
if (text startsWith "/") {
@ -39,14 +40,14 @@ class OnUserSlashAction (using coeur: MornyCoeur) extends EventListener {
actions(0) match
// ignore Telegram command like
case TG_FORMAT(_) =>
return false
return;
// ignore Path link
case x if x contains "/" => return false
case x if x contains "/" => return;
case _ =>
val isHardParse = actions(0) isBlank
def hp_len(i: Int) = if isHardParse then i+1 else i
if isHardParse && actions.length < 2 then return false
if isHardParse && actions.length < 2 then return
val v_verb = actions(hp_len(0))
val hasObject = actions.length != hp_len(1)
val v_object =
@ -70,9 +71,9 @@ class OnUserSlashAction (using coeur: MornyCoeur) extends EventListener {
if hasObject then h(v_object+" ") else ""
)
).parseMode(ParseMode HTML).replyToMessageId(update.message.messageId)
true
event.setEventOk
} else false
}
}

View File

@ -1,16 +1,15 @@
package cc.sukazyo.cono.morny.bot.query
import cc.sukazyo.cono.morny.Log.logger
import cc.sukazyo.cono.morny.MornyCoeur
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 cc.sukazyo.cono.morny.Log.{exceptionLog, logger}
import com.pengrad.telegrambot.model.Update
import com.pengrad.telegrambot.model.request.{InlineQueryResultArticle, InputTextMessageContent, ParseMode}
import scala.language.postfixOps
import scala.util.matching.Regex
class ShareToolBilibili extends ITelegramQuery {
class ShareToolBilibili (using coeur: MornyCoeur) extends ITelegramQuery {
private val TITLE_BILI_AV = "[bilibili] Share video / av"
private val TITLE_BILI_BV = "[bilibili] Share video / BV"
@ -23,54 +22,40 @@ class ShareToolBilibili extends ITelegramQuery {
override def query (event: Update): List[InlineQueryUnit[_]] | Null = {
if (event.inlineQuery.query == null) return null
if (event.inlineQuery.query isBlank) return null
event.inlineQuery.query match
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: ${_url_v}
|2: ${_url_av}
|3: ${_url_bv}
|4: ${_url_param}
|5: ${_url_v_part}
|6: ${_raw_av}
|7: ${_raw_bv}"""
.stripMargin
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 (_url_v_part!=null) _url_v_part toInt else null
logger trace s"catch video part[$part]"
if (av == null) {
assert (bv != null)
av = BiliTool.toAv(bv) toString;
logger trace s"converted bv[$av] to av[$av]"
} else {
bv = BiliTool.toBv(av toLong)
logger trace s"converted av[$av] to bv[$bv]"
}
val id_av = s"av$av"
val id_bv = s"BV$bv"
val linkParams = if (part!=null) s"?p=$part" else ""
val link_av = LINK_PREFIX + id_av + linkParams
val link_bv = LINK_PREFIX + id_bv + linkParams
List(
InlineQueryUnit(InlineQueryResultArticle(
inlineQueryId(ID_PREFIX_BILI_AV+av), TITLE_BILI_AV+av,
InputTextMessageContent(SHARE_FORMAT_HTML.format(link_av, id_av)).parseMode(ParseMode HTML)
)),
InlineQueryUnit(InlineQueryResultArticle(
inlineQueryId(ID_PREFIX_BILI_BV + bv), TITLE_BILI_BV + bv,
InputTextMessageContent(SHARE_FORMAT_HTML.format(link_bv, id_bv)).parseMode(ParseMode HTML)
))
)
case _ => null
import cc.sukazyo.cono.morny.data.BilibiliForms.*
val result: BiliVideoId =
try
parse_videoUrl(event.inlineQuery.query)
catch case _: IllegalArgumentException =>
try
parse_videoUrl(destructB23Url(event.inlineQuery.query))
catch
case _: IllegalArgumentException =>
return null;
case e: IllegalStateException =>
logger error exceptionLog(e)
coeur.daemons.reporter.exception(e)
return null;
val av = result.av
val bv = result.bv
val id_av = s"av$av"
val id_bv = s"BV$bv"
val linkParams = if (result.part != null) s"?p=${result.part}" else ""
val link_av = LINK_PREFIX + id_av + linkParams
val link_bv = LINK_PREFIX + id_bv + linkParams
List(
InlineQueryUnit(InlineQueryResultArticle(
inlineQueryId(ID_PREFIX_BILI_AV + av), TITLE_BILI_AV + av,
InputTextMessageContent(SHARE_FORMAT_HTML.format(link_av, id_av)).parseMode(ParseMode HTML)
)),
InlineQueryUnit(InlineQueryResultArticle(
inlineQueryId(ID_PREFIX_BILI_BV + bv), TITLE_BILI_BV + bv,
InputTextMessageContent(SHARE_FORMAT_HTML.format(link_bv, id_bv)).parseMode(ParseMode HTML)
))
)
}

View File

@ -30,11 +30,11 @@ class MedicationTimer (using coeur: MornyCoeur) extends Thread {
override def run (): Unit = {
if ((notify_toChat == -1) || (notify_atHour isEmpty)) {
logger info "Medication Timer disabled : related param is not complete set"
logger notice "Medication Timer disabled : related param is not complete set"
return
}
logger info "Medication Timer started."
logger notice "Medication Timer started."
while (!this.isInterrupted) {
try {
val next_time = calcNextRoutineTimestamp(System.currentTimeMillis, use_timeZone, notify_atHour)
@ -47,7 +47,7 @@ class MedicationTimer (using coeur: MornyCoeur) extends Thread {
} catch
case _: InterruptedException =>
interrupt()
logger info "MedicationTimer was interrupted, will be exit now"
logger notice "MedicationTimer was interrupted, will be exit now"
case ill: IllegalArgumentException =>
logger warn "MedicationTimer will not work due to: " + ill.getMessage
interrupt()
@ -58,7 +58,7 @@ class MedicationTimer (using coeur: MornyCoeur) extends Thread {
.stripMargin
coeur.daemons.reporter.exception(e)
}
logger info "Medication Timer stopped."
logger notice "Medication Timer stopped."
}

View File

@ -11,18 +11,18 @@ class MornyDaemons (using val coeur: MornyCoeur) {
def start (): Unit = {
logger info "ALL Morny Daemons starting..."
logger notice "ALL Morny Daemons starting..."
// TrackerDataManager.init();
medicationTimer.start()
logger info "Morny Daemons started."
logger notice "Morny Daemons started."
}
def stop (): Unit = {
logger.info("stopping All Morny Daemons...")
logger notice "stopping All Morny Daemons..."
// TrackerDataManager.DAEMON.interrupt();
medicationTimer.interrupt()
@ -31,7 +31,7 @@ class MornyDaemons (using val coeur: MornyCoeur) {
catch case e: InterruptedException =>
e.printStackTrace(System.out)
logger.info("stopped ALL Morny Daemons.")
logger notice "stopped ALL Morny Daemons."
}
}

View File

@ -0,0 +1,95 @@
package cc.sukazyo.cono.morny.data
import cc.sukazyo.cono.morny.util.BiliTool
import cc.sukazyo.cono.morny.util.UseSelect.select
import okhttp3.{HttpUrl, OkHttpClient, Request}
import java.io.IOException
import scala.util.matching.Regex
import scala.util.Using
object BilibiliForms {
case class BiliVideoId (av: Long, bv: String, part: Int|Null = null)
private val REGEX_BILI_ID = "^((?:av|AV)(\\d{1,12})|(?:bv|BV)([A-HJ-NP-Za-km-z1-9]{10}))$"r
private val REGEX_BILI_V_PART_IN_URL_PARAM = "(?:&|^)p=(\\d+)"r
private val REGEX_BILI_VIDEO: Regex = "^(?:(?:https?://)?(?:(?:www\\.)?bilibili\\.com(?:/s)?/video/|b23\\.tv/)((?:av|AV)(\\d{1,12})|(?:bv|BV)([A-HJ-NP-Za-km-z1-9]{10}))/?(?:\\?((?:p=(\\d+))?.*))?|(?:av|AV)(\\d{1,12})|(?:bv|BV)([A-HJ-NP-Za-km-z1-9]{10}))$" r
/** parse a Bilibili video link to a [[BiliVideoId]] format Bilibili Video Id.
*
* @param url the Bilibili video link -- should be a valid link with av/BV,
* can take some tracking params (will be ignored), can be a search
* result link (have `s/` path).
* Or, it can also be a b23 video link: starts with b23.tv hostname with
* no www. prefix, and no /video/ path.
* @throws IllegalArgumentException when the link is not the valid bilibili video link
* @return the [[BiliVideoId]] contains raw or converted av id, and raw or converted bv id,
* and video part id.
*/
@throws[IllegalArgumentException]
def parse_videoUrl (url: String): BiliVideoId =
url match
case REGEX_BILI_VIDEO(_url_v, _url_av, _url_bv, _url_param, _url_v_part, _raw_av, _raw_bv) =>
val av = select(_url_av, _raw_av)
val bv = select(_url_bv, _raw_bv)
val part_part = if (_url_param == null) null else
REGEX_BILI_V_PART_IN_URL_PARAM.findFirstMatchIn(_url_param) match
case Some(part) => part.group(1)
case None => null
val part: Int | Null = if (part_part != null) part_part toInt else null
if (av == null) {
assert(bv != null)
BiliVideoId(BiliTool.toAv(bv), bv, part)
} else {
val _av = av.toLong
BiliVideoId(_av, BiliTool.toBv(_av), part)
}
case _ => throw IllegalArgumentException(s"not a valid Bilibili video link: $url")
private val httpClient = OkHttpClient
.Builder()
.followSslRedirects(true)
.followRedirects(false)
.build()
/** get the bilibili video url from b23.tv share url.
*
* result url can be used in [[parse_videoUrl]]
*
* @param url b23.tv share url.
* @throws IllegalArgumentException the input `url` is not a b23.tv url
* @throws IllegalStateException some exception occurred when getting information from remote
* host, or failed to parse the information got
* @return bilibili video url with tracking params
*/
@throws[IllegalStateException|IllegalArgumentException]
def destructB23Url (url: String): String =
val _url: HttpUrl = HttpUrl.parse(
if url startsWith "http://" then url.replaceFirst("http://", "https://") else url
)
if _url == null then throw IllegalArgumentException("not a valid url: " + url)
if _url.host != "b23.tv" then throw IllegalArgumentException(s"not a b23 share link: $url")
if (!_url.pathSegments.isEmpty) && _url.pathSegments.get(0).matches(REGEX_BILI_ID.regex) then
throw IllegalArgumentException(s"is a b23 video link: $url ; (use parse_videoUrl directly)")
val result: Option[String] =
try {
Using(httpClient.newCall(Request.Builder().url(_url).build).execute()) { response =>
if response.isRedirect then
val _u = response header "Location"
if _u != null then
Some(_u)
else throw IllegalStateException("unable to get b23.tv redir location from: " + response)
else throw IllegalStateException("unable to get b23.tv redir location from: " + response)
}.get
} catch case e: IOException =>
throw IllegalStateException("get b23.tv failed.", e)
result match
case Some(_result) => _result
case None => throw IllegalStateException("unable to parse from b23.tv .")
}

View File

@ -0,0 +1,12 @@
package cc.sukazyo.cono.morny.internal.logging
import cc.sukazyo.messiva.log.Message
trait IMornyLogLevelImpl {
def notice (message: String): Unit
def notice (message: Message): Unit
def attention (message: String): Unit
def attention (message: Message): Unit
}

View File

@ -0,0 +1,24 @@
package cc.sukazyo.cono.morny.internal.logging
import cc.sukazyo.cono.morny.util.CommonFormat.formatDate
import cc.sukazyo.cono.morny.ServerMain
import cc.sukazyo.messiva.formatter.ILogFormatter
import cc.sukazyo.messiva.log.Log
import java.time.{ZoneId, ZoneOffset}
import java.util.TimeZone
class MornyFormatterConsole extends ILogFormatter {
override def format (log: Log): String =
val message = StringBuilder()
val dt = formatDate(log.timestamp, ServerMain.tz_offset)
val prompt_heading = s"[$dt][${log.thread.getName}]"
val prompt_newline = "'" * prompt_heading.length
val prompt_levelTag = s"${log.level.tag}::: "
message ++= prompt_heading ++= prompt_levelTag ++= log.message.message(0)
for (line <- log.message.message drop 1)
message += '\n' ++= prompt_newline ++= prompt_levelTag ++= line
message toString
}

View File

@ -0,0 +1,13 @@
package cc.sukazyo.cono.morny.internal.logging
import cc.sukazyo.messiva.log.ILogLevel
enum MornyLogLevels (
override val level: Float,
override val tag: String
) extends ILogLevel {
case NOTICE extends MornyLogLevels(0.2f, "NOTICE")
case ATTENTION extends MornyLogLevels(0.3f, "ATTION")
}

View File

@ -0,0 +1,22 @@
package cc.sukazyo.cono.morny.internal.logging
import cc.sukazyo.messiva.appender.IAppender
import cc.sukazyo.messiva.log.{Log, Message}
import cc.sukazyo.messiva.logger.Logger
class MornyLoggerBase extends Logger with IMornyLogLevelImpl {
def this (appends: IAppender*) =
this()
this.appends.addAll(java.util.List.of(appends:_*))
override def notice (message: String): Unit =
pushToAllAppender(Log(1, new Message(message), MornyLogLevels.NOTICE))
override def notice (message: Message): Unit =
pushToAllAppender(Log(1, message, MornyLogLevels.NOTICE))
override def attention (message: String): Unit =
pushToAllAppender(Log(1, new Message(message), MornyLogLevels.ATTENTION))
override def attention (message: Message): Unit =
pushToAllAppender(Log(1, message, MornyLogLevels.ATTENTION))
}

View File

@ -12,6 +12,9 @@ object StringEnsure {
} else str
}
def deSensitive (keepStart: Int = 2, keepEnd: Int = 4, sensitive_cover: Char = '*'): String =
(str take keepStart) + (sensitive_cover.toString*(str.length-keepStart-keepEnd)) + (str takeRight keepEnd)
}
}

1
src/test/scala/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
live

View File

@ -0,0 +1,117 @@
package cc.sukazyo.cono.morny.test.cc.sukazyo.cono.morny.data
import cc.sukazyo.cono.morny.data.BilibiliForms.*
import cc.sukazyo.cono.morny.test.MornyTests
import org.scalatest.prop.TableDrivenPropertyChecks
import org.scalatest.tagobjects.{Network, Slow}
class BilibiliFormsTest extends MornyTests with TableDrivenPropertyChecks {
"while parsing bilibili video link :" - {
"raw avXXX should be parsed" in:
parse_videoUrl("av455017605") shouldEqual BiliVideoId(455017605L, "1Q541167Qg")
"raw BVXXX should be parsed" in:
parse_videoUrl("BV1T24y197V2") shouldEqual BiliVideoId(688730800L, "1T24y197V2")
"raw id without av/BV prefix should not be parsed" in:
an[IllegalArgumentException] should be thrownBy parse_videoUrl("1T24y197V2")
an[IllegalArgumentException] should be thrownBy parse_videoUrl("455017605")
"av/bv prefix can be either uppercase or lowercase" in:
parse_videoUrl("bv1T24y197V2") shouldEqual BiliVideoId(688730800L, "1T24y197V2")
parse_videoUrl("AV455017605") shouldEqual BiliVideoId(455017605L, "1Q541167Qg")
"av/bv bilibili.com link should be parsed" in:
parse_videoUrl("https://www.bilibili.com/video/AV455017605") shouldEqual
BiliVideoId(455017605L, "1Q541167Qg")
parse_videoUrl("https://www.bilibili.com/video/bv1T24y197V2") shouldEqual
BiliVideoId(688730800L, "1T24y197V2")
"bilibili.com link can have protocol http:// or https://" in:
parse_videoUrl("http://www.bilibili.com/video/AV455017605") shouldEqual
BiliVideoId(455017605L, "1Q541167Qg")
"bilibili.com link can omit protocol http or https" in :
parse_videoUrl("www.bilibili.com/video/AV455017605") shouldEqual
BiliVideoId(455017605L, "1Q541167Qg")
"bilibili.com link can omit www. prefix" in :
parse_videoUrl("bilibili.com/video/AV455017605") shouldEqual
BiliVideoId(455017605L, "1Q541167Qg")
parse_videoUrl("https://bilibili.com/video/AV455017605") shouldEqual
BiliVideoId(455017605L, "1Q541167Qg")
"bilibili.com link can be search result link (with /s path prefix)" in :
parse_videoUrl("bilibili.com/s/video/AV455017605") shouldEqual
BiliVideoId(455017605L, "1Q541167Qg")
parse_videoUrl("https://www.bilibili.com/s/video/AV455017605") shouldEqual
BiliVideoId(455017605L, "1Q541167Qg")
"bilibili.com link can only be video link" in :
an[IllegalArgumentException] should be thrownBy parse_videoUrl("bilibili.com/s/media/AV455017605")
an[IllegalArgumentException] should be thrownBy parse_videoUrl("https://www.bilibili.com/media/AV455017605")
an[IllegalArgumentException] should be thrownBy parse_videoUrl("https://www.bilibili.com/AV455017605")
"bilibili.com link can take parameters" in :
parse_videoUrl("https://www.bilibili.com/video/av455017605?vd_source=123456") shouldEqual
BiliVideoId(455017605L, "1Q541167Qg")
parse_videoUrl("bilibili.com/video/AV455017605?mid=12hdowhAID82EQ&289EHD8AHDOIWU8=r2aur9%3Bi0%3AJ%7BRQJH%28QJ.%5BropWG%3AKR%24%28O%7BGR") shouldEqual
BiliVideoId(455017605L, "1Q541167Qg")
"video part within bilibili.com link params should be parsed" in :
parse_videoUrl("https://www.bilibili.com/video/BV1Q541167Qg?p=1") shouldEqual
BiliVideoId(455017605L, "1Q541167Qg", 1)
parse_videoUrl("https://www.bilibili.com/video/av455017605?p=1&vd_source=123456") shouldEqual
BiliVideoId(455017605L, "1Q541167Qg", 1)
parse_videoUrl("bilibili.com/video/AV455017605?mid=12hdowhAI&p=5&x=D82EQ&289EHD8AHDOIWU8=r2aur9%3Bi0%3AJ%7BRQJH%28QJ.%5BropWG%3AKR%24%28O%7BGR") shouldEqual
BiliVideoId(455017605L, "1Q541167Qg", 5)
"av id with more than 12 digits should not be parsed" in :
an[IllegalArgumentException] should be thrownBy parse_videoUrl("av4550176087554")
an[IllegalArgumentException] should be thrownBy parse_videoUrl("bilibili.com/video/av4550176087554")
an[IllegalArgumentException] should be thrownBy parse_videoUrl("av455017608755634345565341256")
"av id with 0 digits should not be parsed" in :
an[IllegalArgumentException] should be thrownBy parse_videoUrl("av")
an[IllegalArgumentException] should be thrownBy parse_videoUrl("bilibili.com/video/av")
"BV id with not 10 digits should not be parsed" in :
an[IllegalArgumentException] should be thrownBy parse_videoUrl("BV123456789")
an[IllegalArgumentException] should be thrownBy parse_videoUrl("BV12345678")
an[IllegalArgumentException] should be thrownBy parse_videoUrl("bilibili.com/video/BV12345678901")
"url which is not bilibili link should not be parsed" in:
an[IllegalArgumentException] should be thrownBy parse_videoUrl("https://www.pilipili.com/video/av123456")
an[IllegalArgumentException] should be thrownBy parse_videoUrl("https://pilipili.com/video/av123456")
an[IllegalArgumentException] should be thrownBy parse_videoUrl("https://blilblil.com/video/av123456")
an[IllegalArgumentException] should be thrownBy parse_videoUrl("https://bilibili.cc/video/av123456")
an[IllegalArgumentException] should be thrownBy parse_videoUrl("https://vxbilibili.com/video/av123456")
an[IllegalArgumentException] should be thrownBy parse_videoUrl("https://bilibiliexc.com/video/av123456")
an[IllegalArgumentException] should be thrownBy parse_videoUrl("C# does not have type erasure. C# has actual generic types deeply baked into the runtime.\n\n好文明")
"url which is a b23 video link should be parsed" in:
parse_videoUrl("https://b23.tv/av688730800") shouldEqual BiliVideoId(688730800L, "1T24y197V2")
parse_videoUrl("http://b23.tv/BV1T24y197V2") shouldEqual BiliVideoId(688730800L, "1T24y197V2")
parse_videoUrl("b23.tv/BV1T24y197V2") shouldEqual BiliVideoId(688730800L, "1T24y197V2")
"b23 video link should not take www. or /video prefix" in:
an[IllegalArgumentException] should be thrownBy parse_videoUrl("https://www.b23.tv/av123456")
an[IllegalArgumentException] should be thrownBy parse_videoUrl("https://b23.tv/video/av123456")
}
"while destruct b23.tv share link :" - {
val examples = Table(
("b23_link", "bilibili_video_link"),
("https://b23.tv/iiCldvZ", "https://www.bilibili.com/video/BV1Gh411P7Sh?buvid=XY6F25B69BE9CF469FF5B917D012C93E95E72&is_story_h5=false&mid=wD6DQnYivIG5pfA3sAGL6A%3D%3D&p=1&plat_id=114&share_from=ugc&share_medium=android&share_plat=android&share_session_id=8081015b-1210-4dea-a665-6746b4850fcd&share_source=COPY&share_tag=s_i&timestamp=1689605644&unique_k=iiCldvZ&up_id=19977489"),
("http://b23.tv/3ymowwx", "https://www.bilibili.com/video/BV15Y411n754?p=1&share_medium=android_i&share_plat=android&share_source=COPY&share_tag=s_i&timestamp=1650293889&unique_k=3ymowwx")
)
"not b23.tv link is not supported" in:
an[IllegalArgumentException] should be thrownBy destructB23Url("sukazyo.cc/2xhUHO2e")
an[IllegalArgumentException] should be thrownBy destructB23Url("https://sukazyo.cc/2xhUHO2e")
an[IllegalArgumentException] should be thrownBy destructB23Url("长月烬明澹台烬心理分析向解析(一)因果之锁,渡魔之路")
an[IllegalArgumentException] should be thrownBy destructB23Url("https://b23.tvb/JDo2eaD")
an[IllegalArgumentException] should be thrownBy destructB23Url("https://ab23.tv/JDo2eaD")
"b23.tv/avXXX video link is not supported" in:
an[IllegalArgumentException] should be thrownBy destructB23Url("https://b23.tv/av123456")
an[IllegalArgumentException] should be thrownBy destructB23Url("https://b23.tv/BV1Q541167Qg")
forAll (examples) { (origin, result) =>
s"b23 link $origin should be destructed to $result" taggedAs (Slow, Network) in:
destructB23Url(origin) shouldEqual result
}
}
}

View File

@ -1,9 +0,0 @@
package live
import cc.sukazyo.cono.morny.test.utils.BiliToolTest
@main def LiveMain (args: String*): Unit = {
org.scalatest.run(BiliToolTest())
}