diff --git a/README.md b/README.md index 35a8bfe..f6d5b04 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,10 @@ [//]: # ([Task Listing][todo] | [~~BBS~~][issues] | [Published][artifact]) [badge_release_img]: https://img.shields.io/github/v/release/Eyre-S/Coeur-Morny-Cono?display_name=release&label=latest&color=#00fa9a [badge_release_target]: https://mvn.sukazyo.cc/#/releases/cc/sukazyo/morny-coeur -[badge_tests_img]: https://img.shields.io/github/actions/workflow/status/Eyre-S/Coeur-Morny-Cono/test?label=Tests&color=dark-green +[//]: # (on branch master) +[badge_tests_img]: https://img.shields.io/github/actions/workflow/status/Eyre-S/Coeur-Morny-Cono/test.yml?branch=master&label=Tests +[//]: # (on branch 2.0.0) +[//]: # ([badge_tests_img]: https://img.shields.io/github/actions/workflow/status/Eyre-S/Coeur-Morny-Cono/test.yml?branch=2.0.0&label=Tests%20on%202.0.0) [badge_snapshot_img]: https://img.shields.io/maven-metadata/v?metadataUrl=https%3A%2F%2Fmvn.sukazyo.cc%2Fsnapshots%2Fcc%2Fsukazyo%2Fmorny-coeur%2Fmaven-metadata.xml&label=snapshots&color=%231e90ff [badge_snapshot_target]: https://mvn.sukazyo.cc/#/snapshots/cc/sukazyo/morny-coeur [![GitHub release][badge_release_img]][badge_release_target] @@ -46,12 +49,16 @@ [okhttp]: https://square.github.io/okhttp/ [gson]: https://github.com/google/gson [scalatest]: https://scalatest.org/ +[messiva]: https://github.com/suk-ws/messiva +[resource-tools]: https://github.com/Eyre-S/ResourceTools +[sttp]: https://sttp.softwaremill.com/ +[circe]: https://circe.github.io/circe/ +[jsoup]: https://jsoup.org/ +[cron-utils]: https://github.com/jmrozanec/cron-utils -[Java Telegram Bot API][tg4j] - - -[okhttp] | [Gson][gson] - -[Scala][scala] | [SpotBugs Annotations][spotbugs] | [ScalaTest][scalatest] +[Scala 3][scala] | [SpotBugs Annotations][spotbugs] | [ScalaTest] \ +[messiva] | [resource-tools] \ +[Java Telegram Bot API][tg4j] \ +[okhttp] | [sttp] | [Gson] | [circe] | [jsoup] | [cron-utils] diff --git a/build.gradle b/build.gradle index efb9383..6d72e7f 100644 --- a/build.gradle +++ b/build.gradle @@ -7,6 +7,7 @@ plugins { id 'com.github.johnrengelman.shadow' version '8.1.1' id 'com.github.gmazzo.buildconfig' version '4.1.2' id 'org.ajoberstar.grgit' version '5.2.0' + id "me.champeau.jmh" version "0.7.2" } import org.ajoberstar.grgit.Status @@ -93,6 +94,7 @@ dependencies { implementation group: 'io.circe', name: scala('circe-core'), version: lib_circe_v implementation group: 'io.circe', name: scala('circe-generic'), version: lib_circe_v implementation group: 'io.circe', name: scala('circe-parser'), version: lib_circe_v + implementation group: 'io.circe', name: scala('circe-yaml-v12'), version: '0.16.0' implementation group: 'org.jsoup', name: 'jsoup', version: lib_jsoup_v implementation group: 'com.cronutils', name: 'cron-utils', version: lib_cron_utils_v diff --git a/src/jmh/java/cc/sukazyo/cono/morny/social/xhs/Benchmark_XHS_MatchOrSearch.java b/src/jmh/java/cc/sukazyo/cono/morny/social/xhs/Benchmark_XHS_MatchOrSearch.java new file mode 100644 index 0000000..1827f99 --- /dev/null +++ b/src/jmh/java/cc/sukazyo/cono/morny/social/xhs/Benchmark_XHS_MatchOrSearch.java @@ -0,0 +1,42 @@ +package cc.sukazyo.cono.morny.social.xhs; + +import cc.sukazyo.cono.morny.extra.xhs.XHSLink; +import org.openjdk.jmh.annotations.*; + +@State(Scope.Benchmark) +@Fork(1) +@Warmup(iterations = 5, time = 5) +@Measurement(iterations = 5, time = 5) +public class Benchmark_XHS_MatchOrSearch { + + public String SHARE_TEXT_PC = "\"24 【这个长得很像晓美焰的角色到底是谁..? - 正义少女天使酱 | 小红书 - 你的生活指南】 \\uD83D\\uDE06 ghMU496vOiKgPGk \\uD83D\\uDE06 http://xhslink.com/59LkLS\""; + public String NORMAL_TEXTS_SHORT = ".map{ case x => ... }\n" + "相当于\n" + ".map{ f => f match { case x => ... } }"; + public String NORMAL_TEXTS_LONG = "关于巴黎奥运开幕式,以下引用一些群友们的话:\n" + "\n" + "这次,跨性别者被禁止参赛的同时,变装皇后被拽上了舞台。马克龙政权真的有权挪用这些解放符号与否我不知道,但国内博主的时评却着实让我从另一个方角疯狂下头。\n" + "\n" + "这些时评每一条都在强调着「正确的秩序应当如何」,但这一套关于「什么是正确」的概念反而是非常带有殖民色彩的,几乎就是19世纪白人的主流世界观。比如,在性的论述里对种族肤色的特别关切(「杂交」!)、对非异性配偶的恐惧之类。这或许也解释了为什么同一群人基本上会认为「欧洲历史部分其实还可以」,但是当代社会却让他们这么抓狂。这些社会焦虑反而很白人特色,没什么中国本土感。底下那位大哥甚至能直接替基督教天主教被亵渎感到丢脸。\n" + "\n" + "从这个角度来分析的话,类似的思想本质上或许不是排外反西方,而是有点类似「崖山之后无中华」的精神在,是还向往着当初被强制开国时候的19世纪列强社会,却发现那个世界已经被颠覆了,于是只好退而自诩真正现代文明之精神传人。这反而正是一种殖民病,一种被殖民者对成为旧时代殖民者的强烈欲望,觉得只有那样才算文明。 在巴黎奥运没有跨性别能够参与[1]的情况下,已有两位顺性别女性运动员被造谣为指派男性[2],鉴跨的最后是伤害所有人。\n" + "transphobia kills everyone\n" + "\n" + "[1] https://www.thenation.com/article/society/trans-athletes-paris-olympics/\n" + "[2] https://x.com/dw_chinese/status/1819189497613242683"; + + @Benchmark + public scala.collection.immutable.List searchShareTexts_onMatch () { + return XHSLink.searchShareText(SHARE_TEXT_PC); + } + @Benchmark + public scala.collection.immutable.List searchShareTexts_onMismatch_shortTexts () { + return XHSLink.searchShareText(NORMAL_TEXTS_SHORT); + } + @Benchmark + public scala.collection.immutable.List searchShareTexts_onMismatch_longTexts () { + return XHSLink.searchShareText(NORMAL_TEXTS_LONG); + } + + @Benchmark + public scala.collection.immutable.List searchShareLinks_onMatch () { + return XHSLink.searchShareUrl(SHARE_TEXT_PC); + } + @Benchmark + public scala.collection.immutable.List searchShareLinks_onMismatch_shortTexts () { + return XHSLink.searchShareUrl(NORMAL_TEXTS_SHORT); + } + @Benchmark + public scala.collection.immutable.List searchShareLinks_onMismatch_longTexts () { + return XHSLink.searchShareUrl(NORMAL_TEXTS_LONG); + } + +} diff --git a/src/main/scala/cc/sukazyo/cono/morny/bot/query/ShareToolBilibili.scala b/src/main/scala/cc/sukazyo/cono/morny/bot/query/ShareToolBilibili.scala index 6c9e497..d6446eb 100644 --- a/src/main/scala/cc/sukazyo/cono/morny/bot/query/ShareToolBilibili.scala +++ b/src/main/scala/cc/sukazyo/cono/morny/bot/query/ShareToolBilibili.scala @@ -1,14 +1,12 @@ package cc.sukazyo.cono.morny.bot.query -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 com.pengrad.telegrambot.model.Update import com.pengrad.telegrambot.model.request.{InlineQueryResultArticle, InputTextMessageContent, ParseMode} import scala.language.postfixOps -class ShareToolBilibili (using coeur: MornyCoeur) extends ITelegramQuery { +class ShareToolBilibili extends ITelegramQuery { private val TITLE_BILI_AV = "[bilibili] Share video / av" private val TITLE_BILI_BV = "[bilibili] Share video / BV" @@ -21,31 +19,26 @@ class ShareToolBilibili (using coeur: MornyCoeur) extends ITelegramQuery { if (event.inlineQuery.query == null) return null if (event.inlineQuery.query isBlank) return null + val content = event.inlineQuery.query import cc.sukazyo.cono.morny.extra.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 results: List[(String, BiliVideoId)] = + BiliVideoId.searchIn(content).map(x => (x.toString, x)) ++ + BiliB23.searchIn(content).map(x => (x.toString, x.toVideoId)) - List( - InlineQueryUnit(InlineQueryResultArticle( - inlineQueryId(ID_PREFIX_BILI_AV + result.av), TITLE_BILI_AV + result.av, - InputTextMessageContent(formatShareHTML(result.avLink, result.toAvString)).parseMode(ParseMode HTML) - )), - InlineQueryUnit(InlineQueryResultArticle( - inlineQueryId(ID_PREFIX_BILI_BV + result.bv), TITLE_BILI_BV + result.bv, - InputTextMessageContent(formatShareHTML(result.bvLink, result.toBvString)).parseMode(ParseMode HTML) - )) + results.flatMap( (_, it) => + List( + InlineQueryUnit(InlineQueryResultArticle( + inlineQueryId(ID_PREFIX_BILI_AV + it.av), + TITLE_BILI_AV + it.av, + InputTextMessageContent(formatShareHTML(it.avLink, it.toAvString)).parseMode(ParseMode HTML) + )), + InlineQueryUnit(InlineQueryResultArticle( + inlineQueryId(ID_PREFIX_BILI_BV + it.bv), + TITLE_BILI_BV + it.bv, + InputTextMessageContent(formatShareHTML(it.bvLink, it.toBvString)).parseMode(ParseMode HTML) + )) + ) ) } diff --git a/src/main/scala/cc/sukazyo/cono/morny/bot/query/ShareToolTwitter.scala b/src/main/scala/cc/sukazyo/cono/morny/bot/query/ShareToolTwitter.scala index 3105b05..be1fb13 100644 --- a/src/main/scala/cc/sukazyo/cono/morny/bot/query/ShareToolTwitter.scala +++ b/src/main/scala/cc/sukazyo/cono/morny/bot/query/ShareToolTwitter.scala @@ -20,22 +20,25 @@ class ShareToolTwitter extends ITelegramQuery { if (event.inlineQuery.query == null) return null - twitter.parseTweetUrl(event.inlineQuery.query) match - - case Some(TweetUrlInformation(_, _path_data, _, _, _, _)) => - List( - InlineQueryUnit(InlineQueryResultArticle( - inlineQueryId(ID_PREFIX_FX + event.inlineQuery.query), TITLE_FX, - s"https://fxtwitter.com/$_path_data" - )), - InlineQueryUnit(InlineQueryResultArticle( - inlineQueryId(ID_PREFIX_VX+event.inlineQuery.query), TITLE_VX, - s"https://vxtwitter.com/$_path_data" - )) - ) - - case _ => null + def getQueryTweetId (prefix: String, tweet: TweetUrlInformation): String = + prefix + tweet.hashCode + def getTweetName (title_prefix: String, tweet: TweetUrlInformation): String = + s"$title_prefix ${tweet.screenName}.${tweet.statusId}" + twitter.guessTweetUrl(event.inlineQuery.query).flatMap(tweet => + List( + InlineQueryUnit(InlineQueryResultArticle( + getQueryTweetId(ID_PREFIX_FX, tweet), + getTweetName(TITLE_FX, tweet), + s"https://fxtwitter.com/${tweet.statusPath}" + )), + InlineQueryUnit(InlineQueryResultArticle( + getQueryTweetId(ID_PREFIX_VX, tweet), + getTweetName(TITLE_VX, tweet), + s"https://vxtwitter.com/${tweet.statusPath}" + )) + ) + ) } } diff --git a/src/main/scala/cc/sukazyo/cono/morny/bot/query/ShareToolXhs.scala b/src/main/scala/cc/sukazyo/cono/morny/bot/query/ShareToolXhs.scala index 5687274..3c3dab4 100644 --- a/src/main/scala/cc/sukazyo/cono/morny/bot/query/ShareToolXhs.scala +++ b/src/main/scala/cc/sukazyo/cono/morny/bot/query/ShareToolXhs.scala @@ -14,22 +14,22 @@ class ShareToolXhs extends ITelegramQuery { if inlineQuery.query == null then return null val content = inlineQuery.query - val xhsLink: XHSLink = { - XHSLink.matchUrl(content) match - case Some(matched) => matched match - case xhsLink: XHSLink => xhsLink - case shareLink: XHSLink.ShareLink => - shareLink.getXhsLink - case None => - XHSLink.searchShareText(content).map(_.getXhsLink) match - case Some(found) => found - case None => return null + def getTitle (xhsLink: XHSLink): String = { + s"$TITLE [${xhsLink.exploreId}]" } - List( + val xhsLinks: List[(String, XHSLink)] = { + XHSLink.searchUrls(content).map { + case xhsLink: XHSLink => (xhsLink.toString, xhsLink) + case shareLink: XHSLink.ShareLink => + (shareLink.toString, shareLink.getXhsLink) + } + } + + xhsLinks.map((uniqueId, xhsLink) => InlineQueryUnit(InlineQueryResultArticle( - ID+content.hashCode, - TITLE, + ID+uniqueId, + getTitle(xhsLink), xhsLink.link )) ) diff --git a/src/main/scala/cc/sukazyo/cono/morny/extra/BilibiliForms.scala b/src/main/scala/cc/sukazyo/cono/morny/extra/BilibiliForms.scala index e17e734..fbd4d95 100644 --- a/src/main/scala/cc/sukazyo/cono/morny/extra/BilibiliForms.scala +++ b/src/main/scala/cc/sukazyo/cono/morny/extra/BilibiliForms.scala @@ -7,6 +7,7 @@ import sttp.client3.{HttpError, SttpClientException} import sttp.client3.okhttp.OkHttpSyncBackend import sttp.model.Uri +import scala.runtime.stdLibPatches.Predef.assert as fromBv import scala.util.matching.Regex object BilibiliForms { @@ -15,23 +16,80 @@ object BilibiliForms { def withPart (part: Int|Null): BiliVideoId = BiliVideoId(av, bv, part) def link (useFormat: BiliVideoId.Formats = BiliVideoId.Formats.AV): String = val useId: String = useFormat match - case BiliVideoId.Formats.AV => avLink - case BiliVideoId.Formats.BV => bvLink + case BiliVideoId.Formats.AV => toAvString + case BiliVideoId.Formats.BV => toBvString s"https://www.bilibili.com/video/$useId" + (if part == null then "" else s"?p=$part") def avLink: String = link(BiliVideoId.Formats.AV) def bvLink: String = link(BiliVideoId.Formats.BV) def toAvString: String = s"av$av" def toBvString: String = s"BV$bv" - object BiliVideoId: + object BiliVideoId { + enum Formats: case AV, BV + def fromAv (av: Long): BiliVideoId = BiliVideoId(av, BiliTool.toBv(av)) def fromBv (bv: String): BiliVideoId = BiliVideoId(BiliTool.toAv(bv), bv) + + def searchIn (texts: String): List[BiliVideoId] = + REGEX_BILI_VIDEO findAllMatchIn texts map { + case Regex.Groups(_url_v, _url_av, _url_bv, _url_param, _url_v_part, _raw_av, _raw_bv) => + parseBiliUrlParamsToBiliVideoId(_url_v, _url_av, _url_bv, _url_param, _url_v_part, _raw_av, _raw_bv) + case _ => throw IllegalArgumentException("Unexpected tokenize result in BiliVideoId.searchIn") + } toList + + def matchUrl (url: String): BiliVideoId = + url match + case REGEX_BILI_VIDEO(_url_v, _url_av, _url_bv, _url_param, _url_v_part, _raw_av, _raw_bv) => + BiliVideoId.parseBiliUrlParamsToBiliVideoId(_url_v, _url_av, _url_bv, _url_param, _url_v_part, _raw_av, _raw_bv) + case _ => throw IllegalArgumentException(s"not a valid Bilibili video link: $url") + + private[BilibiliForms] def parseBiliUrlParamsToBiliVideoId ( + _url_v: String, _url_av: String, _url_bv: String, + _url_param: String, _url_v_part: String, + _raw_av: String, _raw_bv: String + ): BiliVideoId = { + + val av = select(_url_av, _raw_av) + val bv = "1" + select(_url_bv, _raw_bv) + + val part_part = if _url_param == null then 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 then part_part toInt else null + + if (av == null) { + assert(bv != null) + BiliVideoId fromBv bv withPart part + } else { + val _av = av.toLong + BiliVideoId fromAv _av withPart part + } + + } + + } - private val REGEX_BILI_ID = "^((?:av|AV)(\\d{1,16})|(?:bv1|BV1)([A-HJ-NP-Za-km-z1-9]{9}))$"r + case class BiliB23 (shareId: String): + def link: String = s"https://b23.tv/$shareId" + def toVideoId: BiliVideoId = + BilibiliForms.parse_videoUrl(BilibiliForms.destructB23Url(this.link)) + object BiliB23 { + + val REGEX_URL: Regex = "(?:https?://)?(?:www\\.)?b23\\.tv/(\\w{7})(?!\\w)" r + + def searchIn (texts: String): List[BiliB23] = + (REGEX_URL findAllMatchIn texts).toList map { + case Regex.Groups(shareId) => BiliB23(shareId) + case _ => throw IllegalArgumentException("Unexpected tokenize result in BiliB23.searchIn") + } + + } + private val REGEX_BILI_ID = "((?:av|AV)(\\d{1,16})|(?:bv1|BV1)([A-HJ-NP-Za-km-z1-9]{9}))"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,16})|(?:bv1|BV1)([A-HJ-NP-Za-km-z1-9]{9}))/?(?:\\?((?:p=(\\d+))?.*))?|(?:av|AV)(\\d{1,16})|(?:bv1|BV1)([A-HJ-NP-Za-km-z1-9]{9}))$" r + private val REGEX_BILI_VIDEO: Regex = "(?:https?://)?(?:(?:www\\.)?bilibili\\.com(?:/s)?/video/|b23\\.tv/)((?:av|AV)(\\d{1,16})|(?:bv1|BV1)([A-HJ-NP-Za-km-z1-9]{9}))/?(?:\\?((?:p=(\\d+))?.*))?|(?:av|AV)(\\d{1,16})|(?:bv1|BV1)([A-HJ-NP-Za-km-z1-9]{9})"r /** parse a Bilibili video link to a [[BiliVideoId]] format Bilibili Video Id. * @@ -46,27 +104,7 @@ object BilibiliForms { */ @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 = "1" + select(_url_bv, _raw_bv) - - val part_part = if _url_param == null then 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 then part_part toInt else null - - if (av == null) { - assert(bv != null) - BiliVideoId fromBv bv withPart part - } else { - val _av = av.toLong - BiliVideoId fromAv _av withPart part - } - - case _ => throw IllegalArgumentException(s"not a valid Bilibili video link: $url") + BiliVideoId.matchUrl(url) private lazy val httpClient = OkHttpSyncBackend() diff --git a/src/main/scala/cc/sukazyo/cono/morny/extra/xhs/XHSLink.scala b/src/main/scala/cc/sukazyo/cono/morny/extra/xhs/XHSLink.scala index 8ad5f5a..45fe74f 100644 --- a/src/main/scala/cc/sukazyo/cono/morny/extra/xhs/XHSLink.scala +++ b/src/main/scala/cc/sukazyo/cono/morny/extra/xhs/XHSLink.scala @@ -5,6 +5,8 @@ import sttp.client3.okhttp.OkHttpSyncBackend import sttp.client3.{HttpError, RequestT, SttpClientException} import sttp.model.Uri +import scala.util.matching.Regex.Groups + case class XHSLink (exploreId: String) { def link = @@ -26,18 +28,39 @@ object XHSLink { case _ => None } + def searchExplorerUrl (texts: String): List[XHSLink] = { + REGEX_EXPLORER_URL.findAllMatchIn(texts).map { + case Groups(explorerId) => XHSLink(explorerId) + case _ => throw IllegalArgumentException("Unexpected tokenize result in XHSLink.searchExplorerUrl") + }.toList + } + def matchShareUrl (url: String): Option[ShareLink] = { url match case REGEX_SHARE_URL(shareId) => Some(ShareLink(shareId)) case _ => None } + def searchShareUrl (texts: String): List[ShareLink] = { + REGEX_SHARE_URL.findAllMatchIn(texts).map { + case Groups(shareId) => ShareLink(shareId) + case _ => throw IllegalArgumentException("Unexpected tokenize result in XHSLink.searchShareUrl") + }.toList + } + def matchUrl (url: String): Option[XHSLink|ShareLink] = { matchExplorerUrl(url) orElse matchShareUrl(url) } - def searchShareText (texts: String): Option[ShareLink] = { - REGEX_SHARE_TEXTS.findFirstMatchIn(texts).map(x => ShareLink(x.group(2))) + def searchUrls (texts: String): List[XHSLink|ShareLink] = { + searchExplorerUrl(texts) ++ searchShareUrl(texts) + } + + def searchShareText (texts: String): List[ShareLink] = { + REGEX_SHARE_TEXTS.findAllMatchIn(texts).map { + case Groups(shareId) => ShareLink(shareId) + case _ => throw IllegalArgumentException("Unexpected tokenize result in XHSLink.searchShareText") + }.toList } case class ShareLink (shareId: String) { diff --git a/src/test/resources/assets_morny_tests/bilibili/messages_with_urls.yml b/src/test/resources/assets_morny_tests/bilibili/messages_with_urls.yml new file mode 100644 index 0000000..371e697 --- /dev/null +++ b/src/test/resources/assets_morny_tests/bilibili/messages_with_urls.yml @@ -0,0 +1,66 @@ +without_b23_url: + - content: | + 【兔子把化妆品涂在了脸上】 https://www.bilibili.com/video/BV1mB4y1o7dP/?share_source=copy_web&vd_source=653acafde45a589accad0a1287a9c7d8d + with_links: + - raw: "https://www.bilibili.com/video/BV1mB4y1o7dP/?share_source=copy_web&vd_source=653acafde45a589accad0a1287a9c7d8d" + bv: "1mB4y1o7dP" + - content: | + b23.tv/BV1QT421D7Md + 绘画型高松灯(确信 + with_links: + - raw: "b23.tv/BV1QT421D7Md" + bv: "1QT421D7Md" + - content: | + “那些哈基米史上难以超越的经典镜头” (https://www.bilibili.com/video/av1056285333?p=1) + @松鼠撅鱼 (https://space.bilibili.com/94735961): + 鬼畜调教 - 高燃混剪 + 〰〰〰〰〰〰〰〰〰〰 + 🔝> @松鼠撅鱼 (https://space.bilibili.com/94735961): + 经典镜头离不开@魔法师学徒的玩具 ! 有喜欢神鹰黑手哥便携式飞行器,偏铝酸钠,抛瓦椅毛绒玩具的朋友看看,能不能做哈基米玩具就看他了!https://b23.tv/mall-6PV9N-14Vr1mkj6B6 + with_links: + - raw: "https://www.bilibili.com/video/av1056285333?p=1" + bv: "1jH4y1c7R9" + - content: "av112936634811010" + with_links: + - raw: "av112936634811010" + bv: "1DHYjeBEMV" + - content: "BV1Pw4m1k7ot" + with_links: + - raw: "BV1Pw4m1k7ot" + bv: "1Pw4m1k7ot" +with_b23_url: + - content: | + 【四十小时三转机全程廉航 纽约回国终极折磨之旅-哔哩哔哩】 https://b23.tv/H6TxLwN + with_links: + - raw: "https://b23.tv/H6TxLwN" + shareId: "H6TxLwN" + bv: "1e4421Q7Hy" + - content: "【2只鸡蛋就能做舒芙蕾欧姆蛋,外加奶油培根白蘑菇-哔哩哔哩】 https://b23.tv/0kPCod3" + with_links: + - raw: "https://b23.tv/0kPCod3" + shareId: "0kPCod3" + bv: "1sJ4m1j7Vw" + - content: "https://b23.tv/EbD4M1n" + with_links: + - raw: "https://b23.tv/EbD4M1n" + shareId: "EbD4M1n" + bv: "11KbRebEgp" + - content: "⚡班长又出新歌,唱到封号,老c都顶唔顺⚡ https://b23.tv/igpw6X4" + with_links: + - raw: "https://b23.tv/igpw6X4" + shareId: "igpw6X4" + bv: "1RT421k7T1" + - content: | + b23.tv/BV1QT421D7Md + 绘画型高松灯(确信 + “那些哈基米史上难以超越的经典镜头” (https://www.bilibili.com/video/av1056285333?p=1) + @松鼠撅鱼 (https://space.bilibili.com/94735961): + 鬼畜调教 - 高燃混剪 + 〰〰〰〰〰〰〰〰〰〰 + 🔝> @松鼠撅鱼 (https://space.bilibili.com/94735961): + 经典镜头离不开@魔法师学徒的玩具 ! 有喜欢神鹰黑手哥便携式飞行器,偏铝酸钠,抛瓦椅毛绒玩具的朋友看看,能不能做哈基米玩具就看他了!https://b23.tv/mall-6PV9N-14Vr1mkj6B6 + with_links: + - raw: "b23.tv/BV1QT421D7Md" + bv: "1QT421D7Md" + - raw: "https://www.bilibili.com/video/av1056285333?p=1" + bv: "1jH4y1c7R9" diff --git a/src/test/resources/assets_morny_tests/random_message/text_message.yml b/src/test/resources/assets_morny_tests/random_message/text_message.yml new file mode 100644 index 0000000..c179af9 --- /dev/null +++ b/src/test/resources/assets_morny_tests/random_message/text_message.yml @@ -0,0 +1,2 @@ +normal_message: + - "你好,世界!" diff --git a/src/test/scala/cc/sukazyo/cono/morny/test/MornyTests.scala b/src/test/scala/cc/sukazyo/cono/morny/test/MornyTests.scala index 623b616..cc46e37 100644 --- a/src/test/scala/cc/sukazyo/cono/morny/test/MornyTests.scala +++ b/src/test/scala/cc/sukazyo/cono/morny/test/MornyTests.scala @@ -4,11 +4,21 @@ import cc.sukazyo.restools.{ResourceDirectory, ResourcePackage} import org.scalatest.freespec.AnyFreeSpec import org.scalatest.matchers.should -abstract class MornyTests extends AnyFreeSpec with should.Matchers { +object MornyTests extends MornyTests.Assets with MornyTests.Keywords { - val assets: ResourceDirectory = - ResourcePackage.get("assets_morny_tests").getDirectory("assets_morny_tests") + trait Assets { + val assetsPackage: ResourcePackage = ResourcePackage.get("assets_morny_tests") + val assets: ResourceDirectory = assetsPackage.getDirectory("assets_morny_tests") + } - val pending_val = "[not-implemented]" + trait Keywords { + val pending_val = "[not-implemented]" + } } + +abstract class MornyTests + extends AnyFreeSpec + with should.Matchers + with MornyTests.Assets + with MornyTests.Keywords diff --git a/src/test/scala/cc/sukazyo/cono/morny/test/assets/BilibiliAssets.scala b/src/test/scala/cc/sukazyo/cono/morny/test/assets/BilibiliAssets.scala new file mode 100644 index 0000000..9ee360b --- /dev/null +++ b/src/test/scala/cc/sukazyo/cono/morny/test/assets/BilibiliAssets.scala @@ -0,0 +1,31 @@ +package cc.sukazyo.cono.morny.test.assets + +import cc.sukazyo.cono.morny.test.MornyTests +import io.circe.Codec +import io.circe.generic.semiauto.deriveCodec + +object BilibiliAssets { + + case class MessagesWithUrls ( + without_b23_url: List[MessageData], + with_b23_url: List[MessageData] + ) + case class MessageData ( + content: String, + with_links: List[InMessageLink] + ) + case class InMessageLink ( + raw: String, + bv: String, + shareId: Option[String] + ) + + implicit val codec_MessagesWithUrls: Codec[MessagesWithUrls] = deriveCodec + implicit val codec_MessageData: Codec[MessageData] = deriveCodec + implicit val codec_InMessageLink: Codec[InMessageLink] = deriveCodec + lazy val message_with_urls: MessagesWithUrls = + import io.circe.yaml.v12.parser + parser.parse(MornyTests.assets.getFile("bilibili", "messages_with_urls.yml").readString).toTry.get + .as[MessagesWithUrls].toTry.get + +} diff --git a/src/test/scala/cc/sukazyo/cono/morny/test/assets/RandomMessages.scala b/src/test/scala/cc/sukazyo/cono/morny/test/assets/RandomMessages.scala new file mode 100644 index 0000000..0a225fa --- /dev/null +++ b/src/test/scala/cc/sukazyo/cono/morny/test/assets/RandomMessages.scala @@ -0,0 +1,17 @@ +package cc.sukazyo.cono.morny.test.assets + +import cc.sukazyo.cono.morny.test.MornyTests +import io.circe.Codec +import io.circe.generic.semiauto.deriveCodec + +object RandomMessages { + + case class TextMessage (normal_message: List[String]) + implicit val codec_TextMessage: Codec[TextMessage] = deriveCodec + + lazy val text_message: TextMessage = + import io.circe.yaml.v12.parser + parser.parse(MornyTests.assets.getFile("random_message", "text_message.yml").readString).toTry.get + .as[TextMessage].toTry.get + +} diff --git a/src/test/scala/cc/sukazyo/cono/morny/test/bot/query/ShareToolBilibiliTest.scala b/src/test/scala/cc/sukazyo/cono/morny/test/bot/query/ShareToolBilibiliTest.scala new file mode 100644 index 0000000..265345b --- /dev/null +++ b/src/test/scala/cc/sukazyo/cono/morny/test/bot/query/ShareToolBilibiliTest.scala @@ -0,0 +1,81 @@ +package cc.sukazyo.cono.morny.test.bot.query + +import cc.sukazyo.cono.morny.bot.query.{InlineQueryUnit, ShareToolBilibili} +import cc.sukazyo.cono.morny.test.MornyTests +import cc.sukazyo.cono.morny.test.assets.{BilibiliAssets, RandomMessages} +import cc.sukazyo.cono.morny.test.assets.BilibiliAssets.InMessageLink +import com.pengrad.telegrambot.model.{InlineQuery, Update} +import com.pengrad.telegrambot.model.request.InlineQueryResultArticle +import org.scalatest.exceptions.TestFailedException +import org.scalatest.tagobjects.{Network, Slow} + +class ShareToolBilibiliTest extends MornyTests { + + def formatToQueryUpdate (message: String): Update = + val inlineQuery = InlineQuery() + val InlineQuery_query = classOf[InlineQuery].getDeclaredField("query") + InlineQuery_query.setAccessible(true) + InlineQuery_query.set(inlineQuery, message) + val update = Update() + val Update_inlineQuery = classOf[Update].getDeclaredField("inline_query") + Update_inlineQuery.setAccessible(true) + Update_inlineQuery.set(update, inlineQuery) + update + + extension (ex: TestFailedException) { + def addedExtraInfo (message: String): TestFailedException = + ex.modifyMessage { + case Some(errLog) => Some(errLog + "\n" + message) + case None => Some(message) + } + } + + def withInfos (info: =>String)(tests: =>Any): Any = + try tests + catch case ex: TestFailedException => throw ex.addedExtraInfo(info) + + + "when parsing texts" - { + + def queryResultShouldContains (queryResult: List[InlineQueryUnit[?]], shouldContains: List[InMessageLink]): Any = { + val titles = queryResult.map(x => { + val rx = x.result.asInstanceOf[InlineQueryResultArticle] + val rx_title = classOf[InlineQueryResultArticle].getDeclaredField("title") + rx_title.setAccessible(true) + rx_title.get(rx).asInstanceOf[String] + }) + for (shou <- shouldContains) + titles should contain(s"[bilibili] Share video / BV${shou.bv}") + } + + "that contains a video id/url should contains it in result" in { + for (messageObject <- BilibiliAssets.message_with_urls.without_b23_url) { + withInfos(s"In Message:\n${messageObject.content.indent(2)}") { + queryResultShouldContains( + ShareToolBilibili().query(formatToQueryUpdate(messageObject.content)), + messageObject.with_links + ) + } + } + } + + "that contains a b23 video share url should contains it in result" taggedAs (Slow, Network) in { + for (messageObject <- BilibiliAssets.message_with_urls.with_b23_url) { + withInfos(s"In Message:\n${messageObject.content.indent(2)}") { + queryResultShouldContains( + ShareToolBilibili().query(formatToQueryUpdate(messageObject.content)), + messageObject.with_links + ) + } + } + } + + "that have no bilibili video url should returns null" in { + for (message <- RandomMessages.text_message.normal_message) { withInfos ("In Message:\n" + message.indent(2)) { + ShareToolBilibili().query(formatToQueryUpdate(message)) shouldEqual List.empty + }} + } + + } + +} diff --git a/src/test/scala/cc/sukazyo/cono/morny/test/extra/BilibiliFormsTest.scala b/src/test/scala/cc/sukazyo/cono/morny/test/extra/BilibiliFormsTest.scala index 84c225d..eb10b8c 100644 --- a/src/test/scala/cc/sukazyo/cono/morny/test/extra/BilibiliFormsTest.scala +++ b/src/test/scala/cc/sukazyo/cono/morny/test/extra/BilibiliFormsTest.scala @@ -2,6 +2,7 @@ package cc.sukazyo.cono.morny.test.extra import cc.sukazyo.cono.morny.extra.BilibiliForms.* import cc.sukazyo.cono.morny.test.MornyTests +import cc.sukazyo.cono.morny.test.assets.BilibiliAssets import org.scalatest.prop.TableDrivenPropertyChecks class BilibiliFormsTest extends MornyTests with TableDrivenPropertyChecks { @@ -93,6 +94,22 @@ class BilibiliFormsTest extends MornyTests with TableDrivenPropertyChecks { } + "b23.tv share url" - { + + "should be get" - { + + "from searching in texts" in { + for (messageIt <- BilibiliAssets.message_with_urls.with_b23_url) { + BiliB23.searchIn(messageIt.content) shouldEqual messageIt.with_links + .map(_.shareId.map(BiliB23(_)).orNull) + .filterNot(_ == null) + } + } + + } + + } + // Due to this url is expirable, I have no energy to update links in time. // So I decide to deprecate the tests. // "while destruct b23.tv share link :" - {