add TelegramBotEvents.{OnGetUpdateFailed, OnListenerOccursException}
Some checks failed
Tests / check-build (push) Has been cancelled
Tests / check-unit-tests (push) Has been cancelled
Tests / check-assembly-run (push) Has been cancelled

- also add dependency da4a, change GivenContext to da4a version.
- make MornyReport uses the above events to function, instead of calling MornyReport directly in Coeur.
This commit is contained in:
A.C.Sukazyo Eyre 2025-02-20 12:40:27 +08:00
parent bd95870864
commit 96101ec434
Signed by: Eyre_S
GPG Key ID: EFB47D98FE082FAD
11 changed files with 147 additions and 171 deletions

View File

@ -8,8 +8,13 @@ ThisBuild / version := MornyProject.version
ThisBuild / scalaVersion := "3.4.1"
ThisBuild / resolvers ++= Seq(
"-ws-releases" at "https://mvn.sukazyo.cc/releases"
)
"-ws-releases" at "https://mvn.sukazyo.cc/releases",
if (MornyProject.version_is_snapshot) {
"-ws-snapshots" at "https://mvn.sukazyo.cc/snapshots"
} else {
null
}
).filter(x => x != null)
ThisBuild / crossPaths := false

View File

@ -6,16 +6,16 @@ import cc.sukazyo.cono.morny.core.bot.api.{BotExtension, EventListenerManager, M
import cc.sukazyo.cono.morny.core.bot.api.messages.ThreadingManager
import cc.sukazyo.cono.morny.core.bot.event.{MornyOnInlineQuery, MornyOnTelegramCommand, MornyOnUpdateTimestampOffsetLock}
import cc.sukazyo.cono.morny.core.bot.internal.{ErrorMessageManager, ThreadingManagerImpl}
import cc.sukazyo.cono.morny.core.event.TelegramBotEvents
import cc.sukazyo.cono.morny.core.http.api.{HttpServer, MornyHttpServerContext}
import cc.sukazyo.cono.morny.core.http.internal.MornyHttpServerContextImpl
import cc.sukazyo.cono.morny.core.module.ModuleHelper
import cc.sukazyo.cono.morny.reporter.MornyReport
import cc.sukazyo.cono.morny.system.utils.EpochDateTime.EpochMillis
import cc.sukazyo.cono.morny.system.utils.GivenContext
import cc.sukazyo.cono.morny.util.schedule.Scheduler
import cc.sukazyo.cono.morny.util.time.WatchDog
import cc.sukazyo.cono.morny.util.UseString.MString
import cc.sukazyo.cono.morny.util.UseThrowable.toLogString
import cc.sukazyo.std.contexts.GivenContext
import com.pengrad.telegrambot.TelegramBot
import com.pengrad.telegrambot.request.GetMe
@ -118,6 +118,8 @@ class MornyCoeur (modules: List[MornyModule])(using val config: MornyConfig)(tes
given MornyCoeur = this
val telegramBotEvents = new TelegramBotEvents()
val externalContext: GivenContext = GivenContext()
logger `info`
m"""The following Modules have been added to current Morny:
@ -150,7 +152,7 @@ class MornyCoeur (modules: List[MornyModule])(using val config: MornyConfig)(tes
///<<< BLOCK END instance configure & startup stage 1
/** inner value: about why morny exit, used in [[daemon.MornyReport]]. */
/** inner value: about why morny exit. */
private var whileExit_reason: Option[AnyRef] = None
/** About why morny exits. */
def exitReason: Option[AnyRef] = whileExit_reason
@ -295,7 +297,6 @@ class MornyCoeur (modules: List[MornyModule])(using val config: MornyConfig)(tes
| server responses:
|${GsonBuilder().setPrettyPrinting().create.toJson(e.response).indent(4)}
|""".stripMargin
externalContext.consume[MornyReport](_.exception(e, "Failed get updates."))
}
if (e.getCause != null) {
@ -319,7 +320,9 @@ class MornyCoeur (modules: List[MornyModule])(using val config: MornyConfig)(tes
logger `error`
s"""Failed get updates:
|${e_other.toLogString `indent` 3}""".stripMargin
externalContext.consume[MornyReport](_.exception(e_other, "Failed get updates."))
TelegramBotEvents.inCoeur.OnGetUpdateFailed.emit(e)
}
})

View File

@ -2,7 +2,7 @@ package cc.sukazyo.cono.morny.core.bot.api
import cc.sukazyo.cono.morny.core.{Log, MornyCoeur}
import cc.sukazyo.cono.morny.core.Log.logger
import cc.sukazyo.cono.morny.reporter.MornyReport
import cc.sukazyo.cono.morny.core.event.TelegramBotEvents
import cc.sukazyo.cono.morny.system.telegram_api.event.{EventEnv, EventListener, EventRuntimeException}
import cc.sukazyo.cono.morny.util.UseThrowable.toLogString
import com.google.gson.GsonBuilder
@ -49,7 +49,7 @@ class EventListenerManager (using coeur: MornyCoeur) extends UpdatesListener {
i.atEventPost
}
private def runEventListener (i: EventListener)(using EventEnv): Unit = {
private def runEventListener (i: EventListener)(using event: EventEnv): Unit = {
try {
i.on
updateThreadName("message")
@ -80,7 +80,7 @@ class EventListenerManager (using coeur: MornyCoeur) extends UpdatesListener {
if update.chatMember ne null then i.onChatMemberUpdated
updateThreadName("chat-join-request")
if update.chatJoinRequest ne null then i.onChatJoinRequest
} catch case e => {
} catch case e: Throwable => {
val errorMessage = StringBuilder()
errorMessage ++= "Event throws unexpected exception:\n"
errorMessage ++= (e.toLogString `indent` 4)
@ -92,7 +92,7 @@ class EventListenerManager (using coeur: MornyCoeur) extends UpdatesListener {
) `indent` 4) ++= "\n"
case _ =>
logger `error` errorMessage.toString
coeur.externalContext.consume[MornyReport](_.exception(e, "on event running"))
TelegramBotEvents.inCoeur.OnListenerOccursException.emit((e, i, event))
}
}

View File

@ -0,0 +1,41 @@
package cc.sukazyo.cono.morny.core.event
import cc.sukazyo.cono.morny.core.MornyCoeur
import cc.sukazyo.cono.morny.system.telegram_api.event.{EventEnv, EventListener as TelegramEventListener}
import cc.sukazyo.std.event.{EventContext, RichEvent}
import cc.sukazyo.std.event.impl.NormalEventManager
import com.pengrad.telegrambot.TelegramException
class TelegramBotEvents (using coeur: MornyCoeur) {
private val contextInitializer: EventContext[?]=>Unit = context => {
context.givenCxt << coeur
}
/**
* Event: OnGetUpdateFailed in TelegramBotEvents
*
* This event will be emitted when an exception occurred when the Telegram Bot is trying
* to execute the GetUpdate.
*
* Provides a [[TelegramException]] that contains the exception information.
*
* Event is initialized after the [[MornyModule.onStarting]] stage, and before the
* [[MornyModule.onStartingPost]] stage.
* You should register your own listener at stage [[MornyModule.onStartingPost]].
*/
val OnGetUpdateFailed: NormalEventManager[TelegramException, Unit] =
NormalEventManager().initContextWith(contextInitializer)
val OnListenerOccursException: RichEvent[(Throwable, TelegramEventListener, EventEnv), Unit] =
NormalEventManager().initContextWith(contextInitializer)
}
object TelegramBotEvents {
def inCoeur (using coeur: MornyCoeur): TelegramBotEvents = in(coeur)
def in (coeur: MornyCoeur): TelegramBotEvents =
coeur.telegramBotEvents
}

View File

@ -3,6 +3,7 @@ package cc.sukazyo.cono.morny.reporter
import cc.sukazyo.cono.morny.core.internal.MornyInternalModule
import cc.sukazyo.cono.morny.core.Log.logger
import cc.sukazyo.cono.morny.core.MornyCoeur
import cc.sukazyo.cono.morny.core.event.TelegramBotEvents
class Module extends MornyInternalModule {
@ -38,17 +39,27 @@ class Module extends MornyInternalModule {
override def onStarting (using coeur: MornyCoeur)(cxt: MornyCoeur.OnStartingContext): Unit = {
import coeur.externalContext
externalContext >> { (instance: MornyReport) =>
instance.start()
TelegramBotEvents.inCoeur.OnGetUpdateFailed
.registerListener(instance.botErrorsReport.onGetUpdateFailed)
TelegramBotEvents.inCoeur.OnListenerOccursException
.registerListener(instance.botErrorsReport.onEventListenersThrowException)
} || {
logger `warn` "There seems no reporter instance is provided; skipped start it."
}
}
override def onStartingPost (using coeur: MornyCoeur)(cxt: MornyCoeur.OnStartingPostContext): Unit = {
import coeur.externalContext
externalContext >> { (instance: MornyReport) =>
instance.reportCoeurMornyLogin()
}
}
override def onExiting (using coeur: MornyCoeur): Unit = {

View File

@ -3,6 +3,7 @@ package cc.sukazyo.cono.morny.reporter
import cc.sukazyo.cono.morny.core.{MornyCoeur, MornyConfig}
import cc.sukazyo.cono.morny.core.Log.logger
import cc.sukazyo.cono.morny.data.MornyInformation.getVersionAllFullTagHTML
import cc.sukazyo.cono.morny.reporter.telegram_bot.BotErrorsReport
import cc.sukazyo.cono.morny.system.telegram_api.event.{EventEnv, EventListener, EventRuntimeException}
import cc.sukazyo.cono.morny.system.telegram_api.formatting.TelegramFormatter.*
import cc.sukazyo.cono.morny.system.telegram_api.formatting.TelegramParseEscape.escapeHtml as h
@ -26,7 +27,9 @@ import com.pengrad.telegrambot.TelegramException
import java.time.ZoneId
class MornyReport (using coeur: MornyCoeur) {
class MornyReport (using val coeur: MornyCoeur) {
given reporter: MornyReport = this
private val enabled = coeur.config.reportToChat != -1
if !enabled then
@ -146,6 +149,8 @@ class MornyReport (using coeur: MornyCoeur) {
).parseMode(ParseMode HTML))
}
object botErrorsReport extends BotErrorsReport()
object EventStatistics {
private var eventTotal = 0
@ -280,3 +285,10 @@ class MornyReport (using coeur: MornyCoeur) {
}
}
object MornyReport {
def inCoeur (coeur: MornyCoeur): MornyReport =
coeur.externalContext.getUnsafe[MornyReport]
}

View File

@ -0,0 +1,38 @@
package cc.sukazyo.cono.morny.reporter.telegram_bot
import cc.sukazyo.cono.morny.core.event.TelegramBotEvents
import cc.sukazyo.cono.morny.reporter.MornyReport
class BotErrorsReport (using reporter: MornyReport) {
private val _TBotEvents = TelegramBotEvents.inCoeur(using reporter.coeur)
val onGetUpdateFailed: _TBotEvents.OnGetUpdateFailed.MyCallback
= telegramException => {
if (telegramException.response != null) {
// if the response exists, means connections to the server is successful, but
// server can't process the request.
// due to connections to the server is ok, the report should be able to send to
// the telegram side.
reporter.exception(telegramException, "Failed get updates.")
}
if (telegramException.getCause != null) {
import java.net.{SocketException, SocketTimeoutException}
import javax.net.ssl.SSLHandshakeException
val caused = telegramException.getCause
caused match
case _: (SSLHandshakeException|SocketException|SocketTimeoutException) =>
case e_other =>
reporter.exception(e_other, "Failed get updates.")
}
}
val onEventListenersThrowException: _TBotEvents.OnListenerOccursException.MyCallback
= (e, _, _) => {
reporter.exception(e, "on event running")
}
}

View File

@ -2,6 +2,12 @@ package cc.sukazyo.cono.morny.social_share.external
import scala.util.matching.Regex
/** Public twitter objects and utilities.
*
* Contains:
* - Twitter's tweet url object [[TweetUrlInformation]], and its parser [[parseTweetUrl]] &
* [[guessTweetUrl]].
*/
package object twitter {
private val REGEX_TWEET_URL: Regex = "(?:https?://)?((?:(?:(?:c\\.)?vx|fx|www\\.)?twitter|(?:www\\.|fixup|fixv)?x)\\.com)/((\\w+)/status/(\\d+)(?:/photo/(\\d+))?)/?(?:\\?(\\S+))?"r
@ -69,6 +75,20 @@ package object twitter {
))
case _ => None
/** Find all the possible Twitter/X URL from the given text.
*
* It supports url like [[parseTweetUrl]] supports, the only difference is that this
* method can find all the possible urls in the text using [[Regex.findAllMatchIn]],
* instead of strictly match the text.
*
* For each Twitter URL found in the text, a corresponds [[TweetUrlInformation]] will be
* created.
*
* @param text The text that may contain tweet url.
* @return A list of [[TweetUrlInformation]].
* Each twitter url found in the text corresponds to one [[TwitterUrlInformation]].
* If no url is found, an empty list will be returned.
*/
def guessTweetUrl (text: String): List[TweetUrlInformation] =
REGEX_TWEET_URL.findAllMatchIn(text).map(f => {
TweetUrlInformation(

View File

@ -1,8 +1,8 @@
package cc.sukazyo.cono.morny.system.telegram_api.event
import cc.sukazyo.cono.morny.system.utils.EpochDateTime.EpochMillis
import cc.sukazyo.cono.morny.system.utils.GivenContext
import cc.sukazyo.messiva.utils.StackUtils
import cc.sukazyo.std.contexts.GivenContext
import com.pengrad.telegrambot.model.Update
import scala.collection.mutable

View File

@ -1,156 +0,0 @@
package cc.sukazyo.cono.morny.system.utils
import cc.sukazyo.cono.morny.system.utils.GivenContext.{ContextNotGivenException, FolderClass, RequestItemClass}
import scala.annotation.targetName
import scala.collection.mutable
import scala.reflect.{classTag, ClassTag}
object GivenContext {
case class FolderClass (clazz: Option[Class[?]])
object FolderClass:
def default: FolderClass = FolderClass(None)
case class RequestItemClass (clazz: Class[?])
class ContextNotGivenException (using
val requestItemClass: RequestItemClass,
val folderClass: FolderClass = FolderClass.default,
val requestStack: StackTraceElement = UseStacks.getStackHeadBeforeClass[GivenContext]
) extends NoSuchElementException (
s"None of the ${requestItemClass.clazz.getSimpleName} is in the context${folderClass.clazz.map(" and owned by " + _.getSimpleName).getOrElse("")}, which is required by $requestStack."
)
}
/** A mutable collection that can store(provide) any typed value and read(use/consume) that value by type.
*
* @example {{{
* val cxt = GivenContext()
* class BaseClass {}
* class MyImplementation extends BaseClass {}
*
*
* cxt.provide(true) // this provides a Boolean
* cxt.provide[BaseClass](new MyImplementation()) // although this object is of type MyImplementation, but it is stored
* // as BaseClass so you can (and actually can only) read it using BaseClass
* cxt << "string"
* cxt << classOf[Int] -> 1 // you can also manually set the stored type using this method
*
*
* cxt >> { (i: Int) => println(i) } || { println("no Int data in the context") }
* val bool =
* cxt.use[String, Boolean] { s => println(s); true } || { false } // when using .use, the return value must declared
* cxt.consume[String] { s => println(s) } // you can use .consume if you don't care the return
* // and this will return a cxt.ConsumeResult[Any]
* val cxtResultOpt = // use toOption if you do not want fallback calculation
* cxt.use[Int, String](int => s"int: $int").toOption // this returns Option[String]
* val cxtResultOpt2 =
* cxt >> { (int: Int) => s"int: $int" } |? // this returns Option[String] too
* // cxt >> { (int: Int) => cxt >> { (str: String) => { str + int } } } |? // this below is not good to use due to .flatUse
* // is not supported yet. It will return a
* // cxt.ConsumeResult[Option[String]] which is very bad
*
* try { // for now, you can use this way to use multiple data
* val int = cxt.use[Int] // this returns CxtOption[Int] which is Either[ContextNotGivenException, Int]
* .toTry.get
* val str = cxt >> classOf[String] match // this >> returns the same with the .use above
* case Right(s) => s
* case Left(err) => throw err // this is ContextNotGivenException
* val bool = cxt >!> classOf[Boolean] // the easier way to do the above
* } catch case e: ContextNotGivenException => // if any of the above val is not available, it will catch the exception
* e.printStackTrace()
* }}}
*
* TODO: Tests
*
* @since 2.0.0
*/
//noinspection NoTargetNameAnnotationForOperatorLikeDefinition
class GivenContext {
private type ImplicitsMap [T <: Any] = mutable.HashMap[Class[?], T]
private val variables: ImplicitsMap[Any] = mutable.HashMap.empty
private val variablesWithOwner: ImplicitsMap[ImplicitsMap[Any]] = mutable.HashMap.empty
infix def provide [T: ClassTag] (i: T): Unit =
variables += (classTag[T].runtimeClass -> i)
def << [T: ClassTag] (is: (Class[T], T)): Unit =
val (_, i) = is
this.provide[T](i)
def << [T: ClassTag] (i: T): Unit =
this.provide[T](i)
private type CxtOption[T] = Either[ContextNotGivenException, T]
def use [T: ClassTag]: CxtOption[T] =
given t: RequestItemClass = RequestItemClass(classTag[T].runtimeClass)
variables get t.clazz match
case Some(i) => Right(i.asInstanceOf[T])
case None => Left(ContextNotGivenException())
infix def use [T: ClassTag, U] (consumer: T => U): ConsumeResult[U] =
this.use[T] match
case Left(_) => ConsumeFailed[U]()
case Right(i) => ConsumeSucceed[U](consumer(i))
def >> [T: ClassTag] (t: Class[T]): CxtOption[T] =
this.use[T]
def >!> [T: ClassTag] (t: Class[T]): T =
this.use[T].toTry.get
def >>[T: ClassTag, U] (consumer: T => U): ConsumeResult[U] =
this.use[T,U](consumer)
infix def consume [T: ClassTag] (consume: T => Any): ConsumeResult[Any] =
this.use[T,Any](consume)
@targetName("ownedBy")
def / [O: ClassTag] (owner: O): OwnedContext[O] =
OwnedContext[O]()
def ownedBy [O: ClassTag]: OwnedContext[O] =
OwnedContext[O]()
class OwnedContext [O: ClassTag] {
infix def provide [T: ClassTag] (i: T): Unit =
(variablesWithOwner getOrElseUpdate (classTag[O].runtimeClass, mutable.HashMap.empty))
.addOne(classTag[T].runtimeClass -> i)
def << [T: ClassTag] (is: (Class[T], T)): Unit =
val (_, i) = is
this.provide[T](i)
def << [T: ClassTag] (i: T): Unit =
this.provide[T](i)
def use [T: ClassTag]: CxtOption[T] =
given t: RequestItemClass = RequestItemClass(classTag[T].runtimeClass)
given u: FolderClass = FolderClass(Some(classTag[O].runtimeClass))
variablesWithOwner get u.clazz.get match
case Some(varColl) => varColl get t.clazz match
case Some(i) => Right(i.asInstanceOf[T])
case None => Left(ContextNotGivenException())
case None => Left(ContextNotGivenException())
infix def use [T: ClassTag, U] (consumer: T => U): ConsumeResult[U] =
use[T] match
case Left(_) => ConsumeFailed[U]()
case Right(i) => ConsumeSucceed[U](consumer(i))
def >> [T: ClassTag] (t: Class[T]): CxtOption[T] =
this.use[T]
def >!> [T: ClassTag] (t: Class[T]): T =
this.use[T].toTry.get
def >> [T: ClassTag, U] (consumer: T => U): ConsumeResult[U] =
this.use[T,U](consumer)
infix def consume [T: ClassTag] (consume: T => Any): ConsumeResult[Any] =
this.use[T,Any](consume)
}
trait ConsumeResult[U]:
def toOption: Option[U]
def |? : Option[U] = toOption
@targetName("orElse")
def || (processor: =>U): U
private class ConsumeSucceed[U] (succeedValue: U) extends ConsumeResult[U]:
override def toOption: Option[U] = Some(succeedValue)
@targetName("orElse")
override def || (processor: => U): U = succeedValue
private class ConsumeFailed[U] extends ConsumeResult[U]:
override def toOption: Option[U] = None
@targetName("orElse")
override def || (processor: => U): U = processor
}

View File

@ -42,10 +42,11 @@ object MornyConfiguration {
override val dependencies = Seq(
"com.github.spotbugs" % "spotbugs-annotations" % "4.9.0" % Compile,
"com.github.spotbugs" % "spotbugs-annotations" % "4.9.1" % Compile,
"cc.sukazyo" % "messiva" % "0.2.0",
"cc.sukazyo" % "resource-tools" % "0.3.1",
"cc.sukazyo" % "da4a" % "0.2.0-SNAPSHOT" changing(),
"com.github.pengrad" % "java-telegram-bot-api" % "6.2.0",
@ -88,10 +89,11 @@ object MornyConfiguration {
override val dependencies = Seq(
"com.github.spotbugs" % "spotbugs-annotations" % "4.9.0" % Compile,
"com.github.spotbugs" % "spotbugs-annotations" % "4.9.1" % Compile,
"cc.sukazyo" % "messiva" % "0.2.0",
"cc.sukazyo" % "resource-tools" % "0.3.1",
"cc.sukazyo" % "da4a" % "0.2.0-SNAPSHOT" changing(),
"com.github.pengrad" % "java-telegram-bot-api" % "6.2.0",
"org.http4s" %% "http4s-dsl" % "0.23.30",