mirror of
https://github.com/Eyre-S/Coeur-Morny-Cono.git
synced 2024-11-24 12:07:39 +08:00
add better inline result preview for twitter and weibo
- cha Twitter URL reformatted added the description - cha SocialContent and ShareToolSocialContent - add support for SocialContent to set the query result title and description - cha weibo and twitter's inline result shows - add the description, with original url and preview mode. - add the title to show a brief of the content - 35 chars max currently - if there are only medias with no text content, the author name will be used instead.
This commit is contained in:
parent
0b560180f4
commit
409ad0f517
@ -28,6 +28,8 @@ class MornyOnInlineQuery (using queryManager: MornyQueries) (using coeur: MornyC
|
|||||||
if (r isPersonal) isPersonal = true
|
if (r isPersonal) isPersonal = true
|
||||||
resultAnswers += r.result
|
resultAnswers += r.result
|
||||||
}
|
}
|
||||||
|
cacheTime = 1
|
||||||
|
logger debug "Inline Query remote caches is DISABLED, you may received duplicate queries logs."
|
||||||
|
|
||||||
if (results isEmpty) return;
|
if (results isEmpty) return;
|
||||||
|
|
||||||
|
@ -66,11 +66,11 @@ object OnGetSocial {
|
|||||||
case Left(texts) =>
|
case Left(texts) =>
|
||||||
weibo.guessWeiboStatusUrl(texts.trim)
|
weibo.guessWeiboStatusUrl(texts.trim)
|
||||||
case Right(url) =>
|
case Right(url) =>
|
||||||
weibo.parseWeiboStatusUrl(url.trim).toList
|
weibo.parseWeiboStatusUrl(url.trim).map(url -> _).toList
|
||||||
}.map(f => {
|
}.map { (url, status) =>
|
||||||
succeed += 1
|
succeed += 1
|
||||||
tryFetchSocialOfWeibo(f)
|
tryFetchSocialOfWeibo(status, url)
|
||||||
})
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
val bilibiliVideos: List[BiliVideoId] = text match
|
val bilibiliVideos: List[BiliVideoId] = text match
|
||||||
@ -124,13 +124,13 @@ object OnGetSocial {
|
|||||||
"Error on requesting FixTweet API\n" + exceptionLog(e)
|
"Error on requesting FixTweet API\n" + exceptionLog(e)
|
||||||
coeur.daemons.reporter.exception(e, "Error on requesting FixTweet API")
|
coeur.daemons.reporter.exception(e, "Error on requesting FixTweet API")
|
||||||
|
|
||||||
private def tryFetchSocialOfWeibo (url: weibo.StatusUrlInfo)(using replyChat: Long, replyToMessage: Int)(using coeur: MornyCoeur) =
|
private def tryFetchSocialOfWeibo (url: weibo.StatusUrlInfo, rawUrl: String)(using replyChat: Long, replyToMessage: Int)(using coeur: MornyCoeur) =
|
||||||
import io.circe.{DecodingFailure, ParsingFailure}
|
import io.circe.{DecodingFailure, ParsingFailure}
|
||||||
import sttp.client3.{HttpError, SttpClientException}
|
import sttp.client3.{HttpError, SttpClientException}
|
||||||
import weibo.MApi
|
import weibo.MApi
|
||||||
try {
|
try {
|
||||||
val api = MApi.Fetch.statuses_show(url.id)
|
val api = MApi.Fetch.statuses_show(url.id)
|
||||||
SocialWeiboParser.parseMStatus(api).outputToTelegram
|
SocialWeiboParser.parseMStatus(api)(rawUrl).outputToTelegram
|
||||||
} catch
|
} catch
|
||||||
case e: HttpError[?] =>
|
case e: HttpError[?] =>
|
||||||
coeur.account exec SendMessage(
|
coeur.account exec SendMessage(
|
||||||
|
@ -52,17 +52,17 @@ class ShareToolSocialContent extends ITelegramQuery {
|
|||||||
SocialTwitterParser.parseFXTweet(FXApi.Fetch.status(Some(tweet.statusPath), tweet.statusId))
|
SocialTwitterParser.parseFXTweet(FXApi.Fetch.status(Some(tweet.statusPath), tweet.statusId))
|
||||||
.genInlineQueryResults(using
|
.genInlineQueryResults(using
|
||||||
"morny/share/tweet/content", tweet.statusId,
|
"morny/share/tweet/content", tweet.statusId,
|
||||||
"Twitter Tweet Content"
|
"[Twitter]"
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private def weiboStatus (query: String): List[InlineQueryUnit[_]] = {
|
private def weiboStatus (query: String): List[InlineQueryUnit[_]] = {
|
||||||
weibo.guessWeiboStatusUrl(query).flatMap { status =>
|
weibo.guessWeiboStatusUrl(query).flatMap { (url, status) =>
|
||||||
SocialWeiboParser.parseMStatus(MApi.Fetch.statuses_show(status.id))
|
SocialWeiboParser.parseMStatus(MApi.Fetch.statuses_show(status.id))(url)
|
||||||
.genInlineQueryResults(using
|
.genInlineQueryResults(using
|
||||||
"morny/share/weibo/status/content", status.id,
|
"morny/share/weibo/status/content", status.id,
|
||||||
"Weibo Content"
|
"[Weibo]"
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -29,11 +29,15 @@ class ShareToolTwitter extends ITelegramQuery {
|
|||||||
getQueryTweetId(ID_PREFIX_FX, tweet),
|
getQueryTweetId(ID_PREFIX_FX, tweet),
|
||||||
getTweetName(TITLE_FX, tweet),
|
getTweetName(TITLE_FX, tweet),
|
||||||
s"https://fxtwitter.com/${tweet.statusPath}"
|
s"https://fxtwitter.com/${tweet.statusPath}"
|
||||||
|
).description(
|
||||||
|
"URL only, and Fix-Tweet web preview available."
|
||||||
)),
|
)),
|
||||||
InlineQueryUnit(InlineQueryResultArticle(
|
InlineQueryUnit(InlineQueryResultArticle(
|
||||||
getQueryTweetId(ID_PREFIX_VX, tweet),
|
getQueryTweetId(ID_PREFIX_VX, tweet),
|
||||||
getTweetName(TITLE_VX, tweet),
|
getTweetName(TITLE_VX, tweet),
|
||||||
s"https://vxtwitter.com/${tweet.statusPath}"
|
s"https://vxtwitter.com/${tweet.statusPath}"
|
||||||
|
).description(
|
||||||
|
"URL only, and VxTwitter web preview available."
|
||||||
))
|
))
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
@ -32,7 +32,7 @@ class ShareToolXhs extends ITelegramQuery {
|
|||||||
getTitle(xhsLink),
|
getTitle(xhsLink),
|
||||||
xhsLink.link
|
xhsLink.link
|
||||||
).description(
|
).description(
|
||||||
"URL only." + (if maybeFromShare.nonEmpty then s" from $maybeFromShare" else "")
|
"URL only." + (if maybeFromShare.nonEmpty then s" from ${maybeFromShare.get}" else "")
|
||||||
))
|
))
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -30,11 +30,13 @@ import com.pengrad.telegrambot.request.{SendMediaGroup, SendMessage}
|
|||||||
* parser/formatter implementation.
|
* parser/formatter implementation.
|
||||||
*/
|
*/
|
||||||
case class SocialContent (
|
case class SocialContent (
|
||||||
|
title: String,
|
||||||
|
description: String,
|
||||||
text_html: String,
|
text_html: String,
|
||||||
text_withPicPlaceholder: String,
|
text_withPicPlaceholder: String,
|
||||||
medias: List[SocialMedia],
|
medias: List[SocialMedia],
|
||||||
medias_mosaic: Option[SocialMedia] = None,
|
medias_mosaic: Option[SocialMedia] = None,
|
||||||
thumbnail: Option[SocialMedia] = None
|
thumbnail: Option[SocialMedia] = None,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
def thumbnailOrElse[T] (orElse: T): String | T =
|
def thumbnailOrElse[T] (orElse: T): String | T =
|
||||||
@ -61,31 +63,55 @@ case class SocialContent (
|
|||||||
def genInlineQueryResults (using id_head: String, id_param: Any, name: String): List[InlineQueryUnit[?]] = {
|
def genInlineQueryResults (using id_head: String, id_param: Any, name: String): List[InlineQueryUnit[?]] = {
|
||||||
(
|
(
|
||||||
if (medias_mosaic nonEmpty) && (medias_mosaic.get.t == Photo) && medias_mosaic.get.isInstanceOf[SocialMediaWithUrl] then
|
if (medias_mosaic nonEmpty) && (medias_mosaic.get.t == Photo) && medias_mosaic.get.isInstanceOf[SocialMediaWithUrl] then
|
||||||
|
// It has multi medias, and the mosaic version is provided.
|
||||||
InlineQueryUnit(InlineQueryResultPhoto(
|
InlineQueryUnit(InlineQueryResultPhoto(
|
||||||
s"[$id_head/photo/mosaic]$id_param",
|
s"[$id_head/photo/mosaic]$id_param",
|
||||||
medias_mosaic.get.asInstanceOf[SocialMediaWithUrl].url,
|
medias_mosaic.get.asInstanceOf[SocialMediaWithUrl].url,
|
||||||
thumbnailOrElse(medias_mosaic.get.asInstanceOf[SocialMediaWithUrl].url)
|
thumbnailOrElse(medias_mosaic.get.asInstanceOf[SocialMediaWithUrl].url)
|
||||||
).title(s"$name").caption(text_html).parseMode(ParseMode.HTML)) :: Nil
|
).title(
|
||||||
|
s"$name $title"
|
||||||
|
).description(
|
||||||
|
s"Pictures are combined. $description"
|
||||||
|
).caption(
|
||||||
|
text_html
|
||||||
|
).parseMode(ParseMode.HTML)) :: Nil
|
||||||
else if (medias nonEmpty) && (medias.head.t == Photo) then
|
else if (medias nonEmpty) && (medias.head.t == Photo) then
|
||||||
val media = medias.head
|
val media = medias.head
|
||||||
media match
|
media match
|
||||||
case media_url: SocialMediaWithUrl =>
|
case media_url: SocialMediaWithUrl =>
|
||||||
|
// the medias is provided, and the first one is in URL format.
|
||||||
|
// it may still contain multiple medias.
|
||||||
|
// although in only two implementations, the Twitter implementation will always give a mosaic
|
||||||
|
// pic; and the Weibo implementation never uses URL formatted medias.
|
||||||
InlineQueryUnit(InlineQueryResultPhoto(
|
InlineQueryUnit(InlineQueryResultPhoto(
|
||||||
s"[$id_head/photo/0]$id_param",
|
s"[$id_head/photo/0]$id_param",
|
||||||
media_url.url,
|
media_url.url,
|
||||||
thumbnailOrElse(media_url.url)
|
thumbnailOrElse(media_url.url)
|
||||||
).title(s"$name").caption(text_html).parseMode(ParseMode.HTML)) :: Nil
|
).title(
|
||||||
|
s"$name $title"
|
||||||
|
).description(
|
||||||
|
s"Pic 1. $description"
|
||||||
|
).caption(
|
||||||
|
text_html
|
||||||
|
).parseMode(ParseMode.HTML)) :: Nil
|
||||||
case _ =>
|
case _ =>
|
||||||
|
// the medias are provided but are not in URL format.
|
||||||
|
// in this case, the plain text version will be used.
|
||||||
InlineQueryUnit(InlineQueryResultArticle(
|
InlineQueryUnit(InlineQueryResultArticle(
|
||||||
s"[$id_head/text_only]$id_param",
|
s"[$id_head/text_only]$id_param",
|
||||||
s"$name (text only)",
|
s"$name $title",
|
||||||
InputTextMessageContent(text_withPicPlaceholder).parseMode(ParseMode.HTML)
|
InputTextMessageContent(text_withPicPlaceholder).parseMode(ParseMode.HTML)
|
||||||
|
).description(
|
||||||
|
s"Plain text only. $description"
|
||||||
)) :: Nil
|
)) :: Nil
|
||||||
else
|
else
|
||||||
|
// There are never any medias.
|
||||||
InlineQueryUnit(InlineQueryResultArticle(
|
InlineQueryUnit(InlineQueryResultArticle(
|
||||||
s"[$id_head/text]$id_param",
|
s"[$id_head/text]$id_param",
|
||||||
s"$name",
|
s"$name $title",
|
||||||
InputTextMessageContent(text_html).parseMode(ParseMode.HTML)
|
InputTextMessageContent(text_html).parseMode(ParseMode.HTML)
|
||||||
|
).description(
|
||||||
|
description
|
||||||
)) :: Nil
|
)) :: Nil
|
||||||
) ::: Nil
|
) ::: Nil
|
||||||
}
|
}
|
||||||
|
@ -5,6 +5,8 @@ import cc.sukazyo.cono.morny.data.social.SocialContent.SocialMediaType.{Photo, V
|
|||||||
import cc.sukazyo.cono.morny.extra.twitter.{FXApi, FXTweet}
|
import cc.sukazyo.cono.morny.extra.twitter.{FXApi, FXTweet}
|
||||||
import cc.sukazyo.cono.morny.util.tgapi.formatting.TelegramParseEscape.escapeHtml as h
|
import cc.sukazyo.cono.morny.util.tgapi.formatting.TelegramParseEscape.escapeHtml as h
|
||||||
|
|
||||||
|
import cc.sukazyo.cono.morny.util.StringEnsure.ensureNotExceed
|
||||||
|
|
||||||
object SocialTwitterParser {
|
object SocialTwitterParser {
|
||||||
|
|
||||||
def parseFXTweet_forMediaPlaceholderInContent (tweet: FXTweet): String =
|
def parseFXTweet_forMediaPlaceholderInContent (tweet: FXTweet): String =
|
||||||
@ -21,7 +23,7 @@ object SocialTwitterParser {
|
|||||||
// language=html
|
// language=html
|
||||||
s"""❌ Fix-Tweet <code>${api.code}</code>
|
s"""❌ Fix-Tweet <code>${api.code}</code>
|
||||||
|<i>${h(api.message)}</i>""".stripMargin
|
|<i>${h(api.message)}</i>""".stripMargin
|
||||||
SocialContent(content, content, Nil)
|
SocialContent("ERROR", "ERROR", content, content, Nil)
|
||||||
case Some(tweet) =>
|
case Some(tweet) =>
|
||||||
val content: String =
|
val content: String =
|
||||||
// language=html
|
// language=html
|
||||||
@ -39,9 +41,11 @@ object SocialTwitterParser {
|
|||||||
|
|
|
|
||||||
|<i>💬${tweet.replies} 🔗${tweet.retweets} ❤️${tweet.likes}</i>
|
|<i>💬${tweet.replies} 🔗${tweet.retweets} ❤️${tweet.likes}</i>
|
||||||
|<i><a href="${tweet.url}">${h(tweet.created_at)}</a></i>""".stripMargin
|
|<i><a href="${tweet.url}">${h(tweet.created_at)}</a></i>""".stripMargin
|
||||||
|
val title: String = tweet.text.ensureNotExceed(35)
|
||||||
|
val description: String = tweet.url
|
||||||
tweet.media match
|
tweet.media match
|
||||||
case None =>
|
case None =>
|
||||||
SocialContent(content, content_withMediasPlaceholder, Nil)
|
SocialContent(title, description, content, content_withMediasPlaceholder, Nil)
|
||||||
case Some(media) =>
|
case Some(media) =>
|
||||||
val mediaGroup: List[SocialMedia] =
|
val mediaGroup: List[SocialMedia] =
|
||||||
(
|
(
|
||||||
@ -60,7 +64,11 @@ object SocialTwitterParser {
|
|||||||
val mediaMosaic = media.mosaic match
|
val mediaMosaic = media.mosaic match
|
||||||
case Some(mosaic) => Some(SocialMediaWithUrl(mosaic.formats.jpeg)(Photo))
|
case Some(mosaic) => Some(SocialMediaWithUrl(mosaic.formats.jpeg)(Photo))
|
||||||
case None => None
|
case None => None
|
||||||
SocialContent(content, content_withMediasPlaceholder, mediaGroup, mediaMosaic, thumbnail)
|
SocialContent(
|
||||||
|
if title.nonEmpty then title else
|
||||||
|
s"from ${tweet.author.name}",
|
||||||
|
description, content, content_withMediasPlaceholder, mediaGroup, mediaMosaic, thumbnail
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -7,6 +7,8 @@ import cc.sukazyo.cono.morny.util.tgapi.formatting.TelegramParseEscape.{cleanupH
|
|||||||
import io.circe.{DecodingFailure, ParsingFailure}
|
import io.circe.{DecodingFailure, ParsingFailure}
|
||||||
import sttp.client3.{HttpError, SttpClientException}
|
import sttp.client3.{HttpError, SttpClientException}
|
||||||
|
|
||||||
|
import cc.sukazyo.cono.morny.util.StringEnsure.ensureNotExceed
|
||||||
|
|
||||||
object SocialWeiboParser {
|
object SocialWeiboParser {
|
||||||
|
|
||||||
def parseMStatus_forPicPreview (status: MStatus): String =
|
def parseMStatus_forPicPreview (status: MStatus): String =
|
||||||
@ -24,7 +26,7 @@ object SocialWeiboParser {
|
|||||||
case None => ""
|
case None => ""
|
||||||
|
|
||||||
@throws[HttpError[?] | SttpClientException | ParsingFailure | DecodingFailure]
|
@throws[HttpError[?] | SttpClientException | ParsingFailure | DecodingFailure]
|
||||||
def parseMStatus (api: MApi[MStatus]): SocialContent = {
|
def parseMStatus (api: MApi[MStatus])(originUrl: String): SocialContent = {
|
||||||
val content =
|
val content =
|
||||||
// language=html
|
// language=html
|
||||||
s"""🔸<b><a href="${api.data.user.profile_url}">${h(api.data.user.screen_name)}</a></b>
|
s"""🔸<b><a href="${api.data.user.profile_url}">${h(api.data.user.screen_name)}</a></b>
|
||||||
@ -39,12 +41,18 @@ object SocialWeiboParser {
|
|||||||
|${ch(api.data.text)}${parseMStatus_forPicPreview(api.data)}
|
|${ch(api.data.text)}${parseMStatus_forPicPreview(api.data)}
|
||||||
|${parseMStatus_forRetweeted(api.data)}
|
|${parseMStatus_forRetweeted(api.data)}
|
||||||
|<i><a href="${genWeiboStatusUrl(StatusUrlInfo(api.data.user.id.toString, api.data.id))}">${h(api.data.created_at)}</a></i>""".stripMargin
|
|<i><a href="${genWeiboStatusUrl(StatusUrlInfo(api.data.user.id.toString, api.data.id))}">${h(api.data.created_at)}</a></i>""".stripMargin
|
||||||
|
val title = api.data.text.ensureNotExceed(35)
|
||||||
|
val description: String = originUrl
|
||||||
api.data.pics match
|
api.data.pics match
|
||||||
case None =>
|
case None =>
|
||||||
SocialContent(content, content_withPicPlaceholder, Nil)
|
SocialContent(title, description, content, content_withPicPlaceholder, Nil)
|
||||||
case Some(pics) =>
|
case Some(pics) =>
|
||||||
val mediaGroup = pics.map(f => SocialMediaWithBytesData(MApi.Fetch.pic(f.large.url))(Photo))
|
val mediaGroup = pics.map(f => SocialMediaWithBytesData(MApi.Fetch.pic(f.large.url))(Photo))
|
||||||
SocialContent(content, content_withPicPlaceholder, mediaGroup)
|
SocialContent(
|
||||||
|
if title.nonEmpty then title else
|
||||||
|
s"from ${api.data.user.screen_name}",
|
||||||
|
description, content, content_withPicPlaceholder, mediaGroup
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -30,9 +30,9 @@ package object weibo {
|
|||||||
case REGEX_WEIBO_STATUS_URL(_, uid, id, _) => Some(StatusUrlInfo(uid, id))
|
case REGEX_WEIBO_STATUS_URL(_, uid, id, _) => Some(StatusUrlInfo(uid, id))
|
||||||
case _ => None
|
case _ => None
|
||||||
|
|
||||||
def guessWeiboStatusUrl (text: String): List[StatusUrlInfo] =
|
def guessWeiboStatusUrl (text: String): List[(String, StatusUrlInfo)] =
|
||||||
REGEX_WEIBO_STATUS_URL.findAllMatchIn(text).map(matches => {
|
REGEX_WEIBO_STATUS_URL.findAllMatchIn(text).map(matches => {
|
||||||
StatusUrlInfo(matches.group(2), matches.group(3))
|
matches.group(0) -> StatusUrlInfo(matches.group(2), matches.group(3))
|
||||||
}).toList
|
}).toList
|
||||||
|
|
||||||
def genWeiboStatusUrl (url: StatusUrlInfo): String =
|
def genWeiboStatusUrl (url: StatusUrlInfo): String =
|
||||||
|
@ -12,6 +12,11 @@ object StringEnsure {
|
|||||||
} else str
|
} else str
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def ensureNotExceed (size: Int, ellipsis: String = "..."): String = {
|
||||||
|
if (str.length <= size) str
|
||||||
|
else str.take(size) + ellipsis
|
||||||
|
}
|
||||||
|
|
||||||
def deSensitive (keepStart: Int = 2, keepEnd: Int = 4, sensitive_cover: Char = '*'): String =
|
def deSensitive (keepStart: Int = 2, keepEnd: Int = 4, sensitive_cover: Char = '*'): String =
|
||||||
(str take keepStart) + (sensitive_cover.toString*(str.length-keepStart-keepEnd)) + (str takeRight keepEnd)
|
(str take keepStart) + (sensitive_cover.toString*(str.length-keepStart-keepEnd)) + (str takeRight keepEnd)
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user