diff --git a/project/MornyConfiguration.scala b/project/MornyConfiguration.scala index 98ea059..772a4d1 100644 --- a/project/MornyConfiguration.scala +++ b/project/MornyConfiguration.scala @@ -8,7 +8,7 @@ object MornyConfiguration { val MORNY_CODE_STORE = "https://github.com/Eyre-S/Coeur-Morny-Cono" val MORNY_COMMIT_PATH = "https://github.com/Eyre-S/Coeur-Morny-Cono/commit/%s" - val VERSION = "2.0.0-alpha7" + val VERSION = "2.0.0-alpha8" val VERSION_DELTA: Option[String] = None val CODENAME = "guanggu" diff --git a/src/main/scala/cc/sukazyo/cono/morny/MornyCoeur.scala b/src/main/scala/cc/sukazyo/cono/morny/MornyCoeur.scala index bde5b4f..bbadf99 100644 --- a/src/main/scala/cc/sukazyo/cono/morny/MornyCoeur.scala +++ b/src/main/scala/cc/sukazyo/cono/morny/MornyCoeur.scala @@ -1,12 +1,12 @@ package cc.sukazyo.cono.morny import cc.sukazyo.cono.morny.bot.command.MornyCommandManager -import cc.sukazyo.cono.morny.daemon.MornyDaemons import cc.sukazyo.cono.morny.Log.{exceptionLog, logger} import cc.sukazyo.cono.morny.MornyCoeur.* import cc.sukazyo.cono.morny.bot.api.EventListenerManager import cc.sukazyo.cono.morny.bot.event.{MornyOnInlineQuery, MornyOnTelegramCommand, MornyOnUpdateTimestampOffsetLock} import cc.sukazyo.cono.morny.bot.query.MornyQueryManager +import cc.sukazyo.cono.morny.reporter.MornyReport import cc.sukazyo.cono.morny.util.schedule.Scheduler import cc.sukazyo.cono.morny.util.EpochDateTime.EpochMillis import cc.sukazyo.cono.morny.util.time.WatchDog @@ -111,6 +111,14 @@ class MornyCoeur (modules: List[MornyModule])(using val config: MornyConfig)(tes given MornyCoeur = this val externalContext: GivenContext = GivenContext() + import util.dataview.Table.format as fmtTable + logger info + s"""The following Modules have been added to current Morny: + |${fmtTable( + ("Module ID" :: "Module Name" :: "Module Version" :: Nil)::Nil ::: + modules.map(f => f.id :: f.name :: f.version :: Nil) + ).replaceAll("\n", "\n|")} + |""".stripMargin ///>>> BLOCK START instance configure & startup stage 1 @@ -156,8 +164,6 @@ class MornyCoeur (modules: List[MornyModule])(using val config: MornyConfig)(tes /** current Morny's [[MornyTrusted]] instance */ val trusted: MornyTrusted = MornyTrusted() - val daemons: MornyDaemons = MornyDaemons() - initializeContext << daemons val eventManager: EventListenerManager = EventListenerManager() val commands: MornyCommandManager = MornyCommandManager() val queries: MornyQueryManager = MornyQueryManager() @@ -192,7 +198,7 @@ class MornyCoeur (modules: List[MornyModule])(using val config: MornyConfig)(tes $MornyManagers.Exit, DirectMsgClear(), - + ) } @@ -203,7 +209,6 @@ class MornyCoeur (modules: List[MornyModule])(using val config: MornyConfig)(tes eventManager, commands, queries, initializeContext))) - eventManager register daemons.reporter.EventStatistics.EventInfoCatcher val watchDog: WatchDog = WatchDog("watch-dog", 1000, 1500, { (consumed, _) => import cc.sukazyo.cono.morny.util.CommonFormat.formatDuration as f logger warn @@ -234,7 +239,6 @@ class MornyCoeur (modules: List[MornyModule])(using val config: MornyConfig)(tes modules.foreach(it => it.onStarting(OnStartingContext( initializeContext))) - daemons.start() logger info "start telegram event listening" import com.pengrad.telegrambot.TelegramException account.setUpdatesListener(eventManager, (e: TelegramException) => { @@ -254,7 +258,7 @@ class MornyCoeur (modules: List[MornyModule])(using val config: MornyConfig)(tes | server responses: |${GsonBuilder().setPrettyPrinting().create.toJson(e.response) indent 4} |""".stripMargin - this.daemons.reporter.exception(e, "Failed get updates.") + externalContext.consume[MornyReport](_.exception(e, "Failed get updates.")) } if (e.getCause != null) { @@ -278,7 +282,7 @@ class MornyCoeur (modules: List[MornyModule])(using val config: MornyConfig)(tes logger error s"""Failed get updates: |${exceptionLog(e_other) indent 3}""".stripMargin - this.daemons.reporter.exception(e_other, "Failed get updates.") + externalContext.consume[MornyReport](_.exception(e_other, "Failed get updates.")) } }) @@ -291,7 +295,6 @@ class MornyCoeur (modules: List[MornyModule])(using val config: MornyConfig)(tes logger info "resetting telegram command list" commands.automaticTGListUpdate() - daemons.reporter.reportCoeurMornyLogin() initializeContext = null logger info "Coeur start complete." @@ -303,17 +306,26 @@ class MornyCoeur (modules: List[MornyModule])(using val config: MornyConfig)(tes } private def exitCleanup (): Unit = { - daemons.reporter.reportCoeurExit() + + // Morny Exiting modules.foreach(it => it.onExiting) - account.shutdown() - logger info "stopped bot account" - daemons.stop() + + account.removeGetUpdatesListener() + logger info "stopped bot update listener" tasks.waitForStop() logger info s"morny tasks stopped: remains ${tasks.amount} tasks not be executed" + + // Morny Exiting Post if config.commandLogoutClear then commands.automaticTGListRemove() + modules.foreach(it => it.onExitingPost) + + account.shutdown() + logger info "stopped bot account" + // Morny Exited modules.foreach(it => it.onExited) - logger info "done exit cleanup" + logger info "done exit cleanup\nMorny will EXIT now" + } private def configure_exitCleanup (): Unit = { diff --git a/src/main/scala/cc/sukazyo/cono/morny/MornyModule.scala b/src/main/scala/cc/sukazyo/cono/morny/MornyModule.scala index d50b1f2..d0d6b43 100644 --- a/src/main/scala/cc/sukazyo/cono/morny/MornyModule.scala +++ b/src/main/scala/cc/sukazyo/cono/morny/MornyModule.scala @@ -20,6 +20,7 @@ trait MornyModule { def onRoutineSavingData (using MornyCoeur): Unit = {} def onExiting (using MornyCoeur): Unit = {} + def onExitingPost (using MornyCoeur): Unit = {} def onExited (using MornyCoeur): Unit = {} } diff --git a/src/main/scala/cc/sukazyo/cono/morny/ServerMain.scala b/src/main/scala/cc/sukazyo/cono/morny/ServerMain.scala index 383277b..06f0335 100644 --- a/src/main/scala/cc/sukazyo/cono/morny/ServerMain.scala +++ b/src/main/scala/cc/sukazyo/cono/morny/ServerMain.scala @@ -9,7 +9,7 @@ import java.util.TimeZone import scala.collection.mutable.ArrayBuffer import scala.language.postfixOps -object ServerMain { +object ServerMain { val tz: TimeZone = TimeZone getDefault val tz_offset: ZoneOffset = ZoneOffset ofTotalSeconds (tz.getRawOffset/1000) diff --git a/src/main/scala/cc/sukazyo/cono/morny/ServerModulesLoader.scala b/src/main/scala/cc/sukazyo/cono/morny/ServerModulesLoader.scala index 8625440..28f168b 100644 --- a/src/main/scala/cc/sukazyo/cono/morny/ServerModulesLoader.scala +++ b/src/main/scala/cc/sukazyo/cono/morny/ServerModulesLoader.scala @@ -16,7 +16,8 @@ object ServerModulesLoader { social_share.ModuleSocialShare(), medication_timer.ModuleMedicationTimer(), morny_misc.ModuleMornyMisc(), - uni_meow.ModuleUniMeow() + uni_meow.ModuleUniMeow(), + reporter.Module() ) diff --git a/src/main/scala/cc/sukazyo/cono/morny/bot/api/EventListenerManager.scala b/src/main/scala/cc/sukazyo/cono/morny/bot/api/EventListenerManager.scala index 7a810f4..dab29d9 100644 --- a/src/main/scala/cc/sukazyo/cono/morny/bot/api/EventListenerManager.scala +++ b/src/main/scala/cc/sukazyo/cono/morny/bot/api/EventListenerManager.scala @@ -2,6 +2,7 @@ package cc.sukazyo.cono.morny.bot.api import cc.sukazyo.cono.morny.{Log, MornyCoeur} import cc.sukazyo.cono.morny.Log.{exceptionLog, logger} +import cc.sukazyo.cono.morny.reporter.MornyReport import cc.sukazyo.cono.morny.util.tgapi.event.EventRuntimeException import com.google.gson.GsonBuilder import com.pengrad.telegrambot.model.Update @@ -87,7 +88,7 @@ class EventListenerManager (using coeur: MornyCoeur) extends UpdatesListener { ) indent 4) ++= "\n" case _ => logger error errorMessage.toString - coeur.daemons.reporter.exception(e, "on event running") + coeur.externalContext.consume[MornyReport](_.exception(e, "on event running")) } } diff --git a/src/main/scala/cc/sukazyo/cono/morny/bot/command/MornyInformation.scala b/src/main/scala/cc/sukazyo/cono/morny/bot/command/MornyInformation.scala index fb683ad..36f9e2e 100644 --- a/src/main/scala/cc/sukazyo/cono/morny/bot/command/MornyInformation.scala +++ b/src/main/scala/cc/sukazyo/cono/morny/bot/command/MornyInformation.scala @@ -3,6 +3,7 @@ package cc.sukazyo.cono.morny.bot.command import cc.sukazyo.cono.morny.{MornyCoeur, MornySystem} import cc.sukazyo.cono.morny.data.MornyInformation.* import cc.sukazyo.cono.morny.data.TelegramStickers +import cc.sukazyo.cono.morny.reporter.MornyReport import cc.sukazyo.cono.morny.util.CommonFormat.{formatDate, formatDuration} import cc.sukazyo.cono.morny.util.tgapi.formatting.TelegramParseEscape.escapeHtml as h import cc.sukazyo.cono.morny.util.tgapi.InputCommand @@ -162,13 +163,17 @@ class MornyInformation (using coeur: MornyCoeur) extends ITelegramCommand { } private def echoEventStatistics (using update: Update): Unit = { - coeur.account exec SendMessage( - update.message.chat.id, - // language=html - s"""Event Statistics : - |in today - |${coeur.daemons.reporter.EventStatistics.eventStatisticsHTML}""".stripMargin - ).parseMode(ParseMode.HTML).replyToMessageId(update.message.messageId) + coeur.externalContext >> { (reporter: MornyReport) => + coeur.account exec SendMessage( + update.message.chat.id, + // language=html + s"""Event Statistics : + |in today + |${reporter.EventStatistics.eventStatisticsHTML}""".stripMargin + ).parseMode(ParseMode.HTML).replyToMessageId(update.message.messageId) + } || { + echo404 + } } private def echo404 (using event: Update): Unit = diff --git a/src/main/scala/cc/sukazyo/cono/morny/bot/command/MornyManagers.scala b/src/main/scala/cc/sukazyo/cono/morny/bot/command/MornyManagers.scala index 2dc8f60..e833843 100644 --- a/src/main/scala/cc/sukazyo/cono/morny/bot/command/MornyManagers.scala +++ b/src/main/scala/cc/sukazyo/cono/morny/bot/command/MornyManagers.scala @@ -3,7 +3,7 @@ import cc.sukazyo.cono.morny.bot.command.ICommandAlias.HiddenAlias import cc.sukazyo.cono.morny.MornyCoeur import cc.sukazyo.cono.morny.data.TelegramStickers import cc.sukazyo.cono.morny.Log.logger -import cc.sukazyo.cono.morny.daemon.MornyReport +import cc.sukazyo.cono.morny.reporter.MornyReport import cc.sukazyo.cono.morny.util.tgapi.InputCommand import cc.sukazyo.cono.morny.util.tgapi.formatting.TelegramFormatter.* import cc.sukazyo.cono.morny.util.tgapi.TelegramExtensions.Bot.exec @@ -41,7 +41,7 @@ class MornyManagers (using coeur: MornyCoeur) { TelegramStickers ID_403 ).replyToMessageId(event.message.messageId) logger attention s"403 exit caught from user ${user toLogTag}" - coeur.daemons.reporter.unauthenticatedAction("/exit", user) + coeur.externalContext.consume[MornyReport](_.unauthenticatedAction("/exit", user)) } @@ -76,7 +76,7 @@ class MornyManagers (using coeur: MornyCoeur) { TelegramStickers ID_403 ).replyToMessageId(event.message.messageId) logger attention s"403 save caught from user ${user toLogTag}" - coeur.daemons.reporter.unauthenticatedAction("/save", user) + coeur.externalContext.consume[MornyReport](_.unauthenticatedAction("/save", user)) } diff --git a/src/main/scala/cc/sukazyo/cono/morny/daemon/MornyDaemons.scala b/src/main/scala/cc/sukazyo/cono/morny/daemon/MornyDaemons.scala deleted file mode 100644 index cfef7b8..0000000 --- a/src/main/scala/cc/sukazyo/cono/morny/daemon/MornyDaemons.scala +++ /dev/null @@ -1,29 +0,0 @@ -package cc.sukazyo.cono.morny.daemon - -import cc.sukazyo.cono.morny.Log.logger -import cc.sukazyo.cono.morny.MornyCoeur - -class MornyDaemons (using val coeur: MornyCoeur) { - - val reporter: MornyReport = MornyReport() - - def start (): Unit = { - - logger notice "ALL Morny Daemons starting..." - - reporter.start() - - logger notice "Morny Daemons started." - - } - - def stop (): Unit = { - - logger notice "stopping All Morny Daemons..." - - reporter.stop() - - logger notice "stopped ALL Morny Daemons." - } - -} diff --git a/src/main/scala/cc/sukazyo/cono/morny/data/TelegramImages.scala b/src/main/scala/cc/sukazyo/cono/morny/data/TelegramImages.scala index ac45039..e6362d4 100644 --- a/src/main/scala/cc/sukazyo/cono/morny/data/TelegramImages.scala +++ b/src/main/scala/cc/sukazyo/cono/morny/data/TelegramImages.scala @@ -1,8 +1,6 @@ package cc.sukazyo.cono.morny.data -import cc.sukazyo.cono.morny.Log.{exceptionLog, logger} import cc.sukazyo.cono.morny.MornyAssets -import cc.sukazyo.cono.morny.daemon.MornyReport import cc.sukazyo.cono.morny.MornyAssets.AssetsException import java.io.IOException diff --git a/src/main/scala/cc/sukazyo/cono/morny/encrypt_tool/Encryptor.scala b/src/main/scala/cc/sukazyo/cono/morny/encrypt_tool/Encryptor.scala index 5fb9989..1292b87 100644 --- a/src/main/scala/cc/sukazyo/cono/morny/encrypt_tool/Encryptor.scala +++ b/src/main/scala/cc/sukazyo/cono/morny/encrypt_tool/Encryptor.scala @@ -5,6 +5,7 @@ import cc.sukazyo.cono.morny.MornyCoeur import cc.sukazyo.cono.morny.bot.command.{ICommandAlias, ITelegramCommand} import cc.sukazyo.cono.morny.bot.command.ICommandAlias.ListedAlias import cc.sukazyo.cono.morny.data.TelegramStickers +import cc.sukazyo.cono.morny.reporter.MornyReport import cc.sukazyo.cono.morny.util.tgapi.InputCommand import cc.sukazyo.cono.morny.util.CommonEncrypt import cc.sukazyo.cono.morny.util.CommonEncrypt.* @@ -81,7 +82,7 @@ class Encryptor (using coeur: MornyCoeur) extends ITelegramCommand { _r.document.fileName )} catch case e: IOException => logger warn s"NetworkRequest error: TelegramFileAPI:\n\t${e.getMessage}" - coeur.daemons.reporter.exception(e, "NetworkRequest error: TelegramFileAPI") + coeur.externalContext.consume[MornyReport](_.exception(e, "NetworkRequest error: TelegramFileAPI")) return } else if ((_r ne null) && (_r.photo ne null)) { try { @@ -102,11 +103,11 @@ class Encryptor (using coeur: MornyCoeur) extends ITelegramCommand { case e: IOException => //noinspection DuplicatedCode logger warn s"NetworkRequest error: TelegramFileAPI:\n\t${e.getMessage}" - coeur.daemons.reporter.exception(e, "NetworkRequest error: TelegramFileAPI") + coeur.externalContext.consume[MornyReport](_.exception(e, "NetworkRequest error: TelegramFileAPI")) return case e: IllegalArgumentException => logger warn s"FileProcess error: PhotoSize:\n\t${e.getMessage}" - coeur.daemons.reporter.exception(e, "FileProcess error: PhotoSize") + coeur.externalContext.consume[MornyReport](_.exception(e, "FileProcess error: PhotoSize")) return } else if ((_r ne null) && (_r.text ne null)) { XText(_r.text) diff --git a/src/main/scala/cc/sukazyo/cono/morny/reporter/Module.scala b/src/main/scala/cc/sukazyo/cono/morny/reporter/Module.scala new file mode 100644 index 0000000..22a6fde --- /dev/null +++ b/src/main/scala/cc/sukazyo/cono/morny/reporter/Module.scala @@ -0,0 +1,70 @@ +package cc.sukazyo.cono.morny.reporter + +import cc.sukazyo.cono.morny.internal.MornyInternalModule +import cc.sukazyo.cono.morny.Log.logger +import cc.sukazyo.cono.morny.MornyCoeur + +class Module extends MornyInternalModule { + + override val id: String = "morny.report" + override val name: String = "Morny/Coeur Reporter" + override val description: String | Null = + """Report crucial messages to a Telegram channel. + |""".stripMargin + + description.take(description.indexOf("\n")) + + override def onInitializingPre (using MornyCoeur)(cxt: MornyCoeur.OnInitializingPreContext): Unit = { + import cxt.* + + val instance = MornyReport() + externalContext << instance + givenCxt << instance + + } + + override def onInitializing (using MornyCoeur)(cxt: MornyCoeur.OnInitializingContext): Unit = { + import cxt.* + + externalContext >> { (instance: MornyReport) => + logger info "MornyReport will now collect your bot event statistics." + eventManager register instance.EventStatistics.EventInfoCatcher + } || { + logger warn "There seems no reporter instance is provided; skipped register events for it." + } + + } + + override def onStarting (using coeur: MornyCoeur)(cxt: MornyCoeur.OnStartingContext): Unit = { + import coeur.externalContext + externalContext >> { (instance: MornyReport) => + instance.start() + } || { + 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 = { + import coeur.externalContext + externalContext >> { (instance: MornyReport) => + instance.stop() + } || { + logger warn "There seems no reporter instance need to be stop." + } + } + + override def onExitingPost (using coeur: MornyCoeur): Unit = { + import coeur.externalContext + externalContext >> { (instance: MornyReport) => + instance.reportCoeurExit() + } + } + +} diff --git a/src/main/scala/cc/sukazyo/cono/morny/daemon/MornyReport.scala b/src/main/scala/cc/sukazyo/cono/morny/reporter/MornyReport.scala similarity index 99% rename from src/main/scala/cc/sukazyo/cono/morny/daemon/MornyReport.scala rename to src/main/scala/cc/sukazyo/cono/morny/reporter/MornyReport.scala index 0fd7a2e..00c888e 100644 --- a/src/main/scala/cc/sukazyo/cono/morny/daemon/MornyReport.scala +++ b/src/main/scala/cc/sukazyo/cono/morny/reporter/MornyReport.scala @@ -1,4 +1,4 @@ -package cc.sukazyo.cono.morny.daemon +package cc.sukazyo.cono.morny.reporter import cc.sukazyo.cono.morny.{MornyCoeur, MornyConfig} import cc.sukazyo.cono.morny.Log.{exceptionLog, logger} diff --git a/src/main/scala/cc/sukazyo/cono/morny/social_share/event/OnGetSocial.scala b/src/main/scala/cc/sukazyo/cono/morny/social_share/event/OnGetSocial.scala index b0775ad..726fe21 100644 --- a/src/main/scala/cc/sukazyo/cono/morny/social_share/event/OnGetSocial.scala +++ b/src/main/scala/cc/sukazyo/cono/morny/social_share/event/OnGetSocial.scala @@ -6,6 +6,7 @@ import cc.sukazyo.cono.morny.data.TelegramStickers import cc.sukazyo.cono.morny.social_share.event.OnGetSocial.tryFetchSocial import cc.sukazyo.cono.morny.util.tgapi.TelegramExtensions.Bot.exec import cc.sukazyo.cono.morny.Log.{exceptionLog, logger} +import cc.sukazyo.cono.morny.reporter.MornyReport import cc.sukazyo.cono.morny.social_share.api.{SocialTwitterParser, SocialWeiboParser} import cc.sukazyo.cono.morny.social_share.external.{twitter, weibo} import cc.sukazyo.cono.morny.util.tgapi.TelegramExtensions.Message.entitiesSafe @@ -87,7 +88,7 @@ object OnGetSocial { ).replyToMessageId(replyToMessage) logger error "Error on requesting FixTweet API\n" + exceptionLog(e) - coeur.daemons.reporter.exception(e, "Error on requesting FixTweet API") + coeur.externalContext.consume[MornyReport](_.exception(e, "Error on requesting FixTweet API")) def tryFetchSocialOfWeibo (url: weibo.StatusUrlInfo)(using replyChat: Long, replyToMessage: Int)(using coeur: MornyCoeur) = import cc.sukazyo.cono.morny.social_share.external.weibo.MApi @@ -111,6 +112,6 @@ object OnGetSocial { ).replyToMessageId(replyToMessage) logger error "Error on requesting Weibo m.API\n" + exceptionLog(e) - coeur.daemons.reporter.exception(e, "Error on requesting Weibo m.API") + coeur.externalContext.consume[MornyReport](_.exception(e, "Error on requesting Weibo m.API")) } diff --git a/src/main/scala/cc/sukazyo/cono/morny/social_share/query/ShareToolBilibili.scala b/src/main/scala/cc/sukazyo/cono/morny/social_share/query/ShareToolBilibili.scala index 6ba9ba2..c44483d 100644 --- a/src/main/scala/cc/sukazyo/cono/morny/social_share/query/ShareToolBilibili.scala +++ b/src/main/scala/cc/sukazyo/cono/morny/social_share/query/ShareToolBilibili.scala @@ -4,6 +4,7 @@ import cc.sukazyo.cono.morny.MornyCoeur import cc.sukazyo.cono.morny.util.tgapi.formatting.NamingUtils.inlineQueryId import cc.sukazyo.cono.morny.Log.{exceptionLog, logger} import cc.sukazyo.cono.morny.bot.query.{InlineQueryUnit, ITelegramQuery} +import cc.sukazyo.cono.morny.reporter.MornyReport import com.pengrad.telegrambot.model.Update import com.pengrad.telegrambot.model.request.{InlineQueryResultArticle, InputTextMessageContent, ParseMode} @@ -17,7 +18,6 @@ class ShareToolBilibili (using coeur: MornyCoeur) extends ITelegramQuery { private val ID_PREFIX_BILI_AV = "[morny/share/bili/av]" private val ID_PREFIX_BILI_BV = "[morny/share/bili/bv]" private val LINK_PREFIX = "https://bilibili.com/video/" - private val REGEX_BILI_VIDEO: Regex = "^(?:(?:https?://)?(?:www\\.)?bilibili\\.com(?:/s)?/video/((?: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 private val SHARE_FORMAT_HTML = "%s" override def query (event: Update): List[InlineQueryUnit[_]] | Null = { @@ -37,7 +37,7 @@ class ShareToolBilibili (using coeur: MornyCoeur) extends ITelegramQuery { return null; case e: IllegalStateException => logger error exceptionLog(e) - coeur.daemons.reporter.exception(e) + coeur.externalContext.consume[MornyReport](_.exception(e)) return null; val av = result.av diff --git a/src/main/scala/cc/sukazyo/cono/morny/util/dataview/Table.scala b/src/main/scala/cc/sukazyo/cono/morny/util/dataview/Table.scala new file mode 100644 index 0000000..f4a31e9 --- /dev/null +++ b/src/main/scala/cc/sukazyo/cono/morny/util/dataview/Table.scala @@ -0,0 +1,20 @@ +package cc.sukazyo.cono.morny.util.dataview + +object Table { + + def format (table: Seq[Seq[Any]]): String = { + if (table.isEmpty) "" + else { + // Get column widths based on the maximum cell width in each column (+2 for a one character padding on each side) + val colWidths = table.transpose.map(_.map(cell => if (cell == null) 0 else cell.toString.length).max + 2) + // Format each row + val rows = table.map(_.zip(colWidths).map { case (item, size) => (" %-" + (size - 1) + "s").format(item) } + .mkString("|", "|", "|")) + // Formatted separator row, used to separate the header and draw table borders + val separator = colWidths.map("-" * _).mkString("+", "+", "+") + // Put the table together and return + (separator +: rows.head +: separator +: rows.tail :+ separator).mkString("\n") + } + } + +}