diff --git a/gradle.properties b/gradle.properties index 8456091..f7a6fe0 100644 --- a/gradle.properties +++ b/gradle.properties @@ -5,7 +5,7 @@ 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.3.5-dev +VERSION = 1.4.0-SNAPSHOT USE_DELTA = false VERSION_DELTA = diff --git a/src/main/scala/cc/sukazyo/cono/morny/bot/query/MornyQueries.scala b/src/main/scala/cc/sukazyo/cono/morny/bot/query/MornyQueries.scala index f0436ca..d01ed62 100644 --- a/src/main/scala/cc/sukazyo/cono/morny/bot/query/MornyQueries.scala +++ b/src/main/scala/cc/sukazyo/cono/morny/bot/query/MornyQueries.scala @@ -13,6 +13,7 @@ class MornyQueries (using MornyCoeur) { MyInformation(), ShareToolTwitter(), ShareToolBilibili(), + ShareToolXhs(), ShareToolSocialContent() ) 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 new file mode 100644 index 0000000..5687274 --- /dev/null +++ b/src/main/scala/cc/sukazyo/cono/morny/bot/query/ShareToolXhs.scala @@ -0,0 +1,39 @@ +package cc.sukazyo.cono.morny.bot.query +import cc.sukazyo.cono.morny.extra.xhs.XHSLink +import com.pengrad.telegrambot.model.Update +import com.pengrad.telegrambot.model.request.InlineQueryResultArticle + +class ShareToolXhs extends ITelegramQuery { + + private val TITLE = "[Xiaohongshu] Share Link" + private val ID = "[morny/share/xhs/link]" + + override def query (event: Update): List[InlineQueryUnit[_]] | Null = { + import event.inlineQuery + + 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 + } + + List( + InlineQueryUnit(InlineQueryResultArticle( + ID+content.hashCode, + TITLE, + xhsLink.link + )) + ) + + } + +} 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 new file mode 100644 index 0000000..8ad5f5a --- /dev/null +++ b/src/main/scala/cc/sukazyo/cono/morny/extra/xhs/XHSLink.scala @@ -0,0 +1,87 @@ +package cc.sukazyo.cono.morny.extra.xhs + +import cc.sukazyo.cono.morny.util.SttpPublic.{mornyBasicRequest, Schemes} +import sttp.client3.okhttp.OkHttpSyncBackend +import sttp.client3.{HttpError, RequestT, SttpClientException} +import sttp.model.Uri + +case class XHSLink (exploreId: String) { + + def link = + s"https://www.xiaohongshu.com/explore/$exploreId" + +} + +object XHSLink { + + private lazy val http_client = OkHttpSyncBackend() + + private lazy val REGEX_EXPLORER_URL = "(?:(?:https?:)?//)?(?:www\\.)?xiaohongshu\\.com/(?:explore/|discovery/item/)([a-fA-F0-9]+)/?(?:\\?.+)?"r + private lazy val REGEX_SHARE_URL = "(?:(?:https?:)?//)?(?:www\\.)?xhslink\\.com/([a-zA-Z0-9]+)/?(?:\\?.+)?"r + private lazy val REGEX_SHARE_TEXTS = "\uD83D\uDE06 ([0-9a-zA-Z]+) \uD83D\uDE06 (?:(?:https?:)?//)?(?:www\\.)?xhslink\\.com/([a-zA-Z0-9]+)/?"r + + def matchExplorerUrl (url: String): Option[XHSLink] = { + url match + case REGEX_EXPLORER_URL(explorerId) => Some(XHSLink(explorerId)) + case _ => None + } + + def matchShareUrl (url: String): Option[ShareLink] = { + url match + case REGEX_SHARE_URL(shareId) => Some(ShareLink(shareId)) + case _ => None + } + + 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))) + } + + case class ShareLink (shareId: String) { + + def link = + s"https://xhslink.com/$shareId" + + /** Get the [[XHSLink xiaohongshu explorer link]] that this share link is linked to via sttp request. + * + * @param http_client the sttp http client backend that will be used. defaults is [[XHSLink]] owned backend. + * @param basic_request the sttp basic request that will be used. defaults is [[mornyBasicRequest]]. + * @throws IllegalArgumentException When the XHS server does not returns a valid explorer link. Mostly maybe the + * share url is invalid or expired. + * @throws IllegalStateException When cannot connect to the XHS server. + * @return the [[XHSLink xiaohongshu explorer link]] that this share link is linked to. + */ + @throws[IllegalArgumentException] + @throws[IllegalStateException] + def getXhsLink (using + http_client: sttp.client3.SttpBackend[sttp.client3.Identity, _] = http_client, + basic_request: RequestT[sttp.client3.Empty, Either[String, String], Any] = mornyBasicRequest + ): XHSLink = { + + val uri = try Uri.unsafeParse(this.link).scheme(Schemes.HTTPS) catch + case e: IllegalArgumentException => throw IllegalStateException("Cannot format this Share link to a valid request url.").initCause(e) + + try { + import sttp.client3.* + val request = basic_request + .get(uri) + .followRedirects(false) + .response(ignore) + val response = request.send(http_client) + val redir = response.header("Location").get + matchExplorerUrl(redir) match + case None => throw IllegalArgumentException(s"XHS server returns a non-XHSLink url: $redir") + case Some(link) => link + } catch + case e: SttpClientException => throw IllegalStateException("failed get response from xhs server.").initCause(e) + case e: HttpError[_] => throw IllegalStateException("failed parse response from xhs server.").initCause(e) + case e: NoSuchElementException => throw IllegalArgumentException("XHS server does not returns any url.").initCause(e) + + } + + } + +} diff --git a/src/main/scala/cc/sukazyo/cono/morny/util/SttpPublic.scala b/src/main/scala/cc/sukazyo/cono/morny/util/SttpPublic.scala index 1f338b9..276f092 100644 --- a/src/main/scala/cc/sukazyo/cono/morny/util/SttpPublic.scala +++ b/src/main/scala/cc/sukazyo/cono/morny/util/SttpPublic.scala @@ -1,7 +1,7 @@ package cc.sukazyo.cono.morny.util import cc.sukazyo.cono.morny.MornySystem -import sttp.client3.basicRequest +import sttp.client3.{basicRequest, RequestT} import sttp.model.Header object SttpPublic { @@ -23,7 +23,7 @@ object SttpPublic { } - val mornyBasicRequest = + val mornyBasicRequest: RequestT[sttp.client3.Empty, Either[String, String], Any] = basicRequest .header(Headers.UserAgent.MORNY_CURRENT, true)