From 60dbcef140f9bdf3cf424556f98077c506bf17db Mon Sep 17 00:00:00 2001 From: Eyre_S Date: Tue, 17 Oct 2023 14:16:29 +0800 Subject: [PATCH] add urlencode/decode for /encrypt, add b23.tv parse for InlineBilibiliShare --- gradle.properties | 4 +- .../cono/morny/bot/command/Encryptor.scala | 38 ++++-- .../cono/morny/bot/command/Testing.scala | 1 - .../morny/bot/query/ShareToolBilibili.scala | 87 ++++++-------- .../cono/morny/data/BilibiliForms.scala | 84 +++++++++++++ .../cono/morny/data/BilibiliFormsTest.scala | 110 ++++++++++++++++++ 6 files changed, 260 insertions(+), 64 deletions(-) create mode 100644 src/main/scala/cc/sukazyo/cono/morny/data/BilibiliForms.scala create mode 100644 src/test/scala/cc/sukazyo/cono/morny/test/cc/sukazyo/cono/morny/data/BilibiliFormsTest.scala diff --git a/gradle.properties b/gradle.properties index d78eb7a..0945157 100644 --- a/gradle.properties +++ b/gradle.properties @@ -5,12 +5,12 @@ 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.xiongan-dev2 +VERSION = 1.2.0-alpha1 USE_DELTA = false VERSION_DELTA = -CODENAME = nanchang +CODENAME = xiongan # dependencies diff --git a/src/main/scala/cc/sukazyo/cono/morny/bot/command/Encryptor.scala b/src/main/scala/cc/sukazyo/cono/morny/bot/command/Encryptor.scala index 07b1c5a..b63dde4 100644 --- a/src/main/scala/cc/sukazyo/cono/morny/bot/command/Encryptor.scala +++ b/src/main/scala/cc/sukazyo/cono/morny/bot/command/Encryptor.scala @@ -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
* '''__base64decode__''', base64d, b64d
* '''__base64url-decode__''', base64ud, b64ud
+ * '''urlencode''', urlencode, urlenc, url
+ * '''__urldecoder__''', urldecode, urldec, urld
* '''__sha1__'''
* '''__sha256__'''
* '''__sha512__'''
@@ -218,6 +234,8 @@ class Encryptor (using coeur: MornyCoeur) extends ITelegramCommand { |base64url, base64u, b64u |base64decode, base64d, b64d |base64url-decode, base64ud, b64ud + |urlencoder, urlencode, urlenc, url + |urldecoder, urldecode, urldec, urld |sha1 |sha256 |sha512 diff --git a/src/main/scala/cc/sukazyo/cono/morny/bot/command/Testing.scala b/src/main/scala/cc/sukazyo/cono/morny/bot/command/Testing.scala index b7a1393..949fa83 100644 --- a/src/main/scala/cc/sukazyo/cono/morny/bot/command/Testing.scala +++ b/src/main/scala/cc/sukazyo/cono/morny/bot/command/Testing.scala @@ -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 { 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 f73fed6..85128db 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,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) + )) + ) } diff --git a/src/main/scala/cc/sukazyo/cono/morny/data/BilibiliForms.scala b/src/main/scala/cc/sukazyo/cono/morny/data/BilibiliForms.scala new file mode 100644 index 0000000..7bf78a4 --- /dev/null +++ b/src/main/scala/cc/sukazyo/cono/morny/data/BilibiliForms.scala @@ -0,0 +1,84 @@ +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_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 + + /** 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). + * @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: Int | Null = if (_url_v_part != null) _url_v_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 .") + +} diff --git a/src/test/scala/cc/sukazyo/cono/morny/test/cc/sukazyo/cono/morny/data/BilibiliFormsTest.scala b/src/test/scala/cc/sukazyo/cono/morny/test/cc/sukazyo/cono/morny/data/BilibiliFormsTest.scala new file mode 100644 index 0000000..9108053 --- /dev/null +++ b/src/test/scala/cc/sukazyo/cono/morny/test/cc/sukazyo/cono/morny/data/BilibiliFormsTest.scala @@ -0,0 +1,110 @@ +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 + +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) + // todo: implement it +// 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("b23.tv/av123456") // todo: support it + 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好文明") + + } + + "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×tamp=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×tamp=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" in: + destructB23Url(origin) shouldEqual result + } + + } + +}