From 79d41d5e723aed425949f0fed466b2a70cbd8ba4 Mon Sep 17 00:00:00 2001 From: Eyre_S Date: Wed, 29 Nov 2023 17:16:02 +0800 Subject: [PATCH] basic inline get social function - Now supported get social content from inline - use a supported url with prefix or suffix "get" - only support twitter photos media - support all types of pure text content. - trying get non-supported medias may cause failure. --- gradle.properties | 2 +- .../cono/morny/bot/command/GetSocial.scala | 117 +----------------- .../morny/bot/event/MornyEventListeners.scala | 1 + .../cono/morny/bot/event/OnGetSocial.scala | 93 ++++++++++++++ .../cono/morny/bot/query/MornyQueries.scala | 3 +- .../bot/query/ShareToolSocialContent.scala | 43 +++++++ .../morny/data/social/SocialContent.scala | 102 +++++++++++++++ .../data/social/SocialTwitterParser.scala | 52 ++++++++ .../morny/data/social/SocialWeiboParser.scala | 29 +++++ 9 files changed, 327 insertions(+), 115 deletions(-) create mode 100644 src/main/scala/cc/sukazyo/cono/morny/bot/event/OnGetSocial.scala create mode 100644 src/main/scala/cc/sukazyo/cono/morny/bot/query/ShareToolSocialContent.scala create mode 100644 src/main/scala/cc/sukazyo/cono/morny/data/social/SocialContent.scala create mode 100644 src/main/scala/cc/sukazyo/cono/morny/data/social/SocialTwitterParser.scala create mode 100644 src/main/scala/cc/sukazyo/cono/morny/data/social/SocialWeiboParser.scala diff --git a/gradle.properties b/gradle.properties index a0939eb..f42f3ab 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.0-dev11.1 +VERSION = 1.3.0-dev12 USE_DELTA = false VERSION_DELTA = diff --git a/src/main/scala/cc/sukazyo/cono/morny/bot/command/GetSocial.scala b/src/main/scala/cc/sukazyo/cono/morny/bot/command/GetSocial.scala index fb2a0c3..0cb9bc7 100644 --- a/src/main/scala/cc/sukazyo/cono/morny/bot/command/GetSocial.scala +++ b/src/main/scala/cc/sukazyo/cono/morny/bot/command/GetSocial.scala @@ -2,16 +2,10 @@ package cc.sukazyo.cono.morny.bot.command import cc.sukazyo.cono.morny.data.TelegramStickers import cc.sukazyo.cono.morny.util.tgapi.InputCommand import cc.sukazyo.cono.morny.MornyCoeur -import cc.sukazyo.cono.morny.extra.{twitter, weibo} -import cc.sukazyo.cono.morny.extra.twitter.{FXApi, TweetUrlInformation} +import cc.sukazyo.cono.morny.bot.event.OnGetSocial import cc.sukazyo.cono.morny.util.tgapi.TelegramExtensions.Bot.exec -import cc.sukazyo.cono.morny.Log.{exceptionLog, logger} -import cc.sukazyo.cono.morny.extra.weibo.StatusUrlInfo import com.pengrad.telegrambot.model.Update -import com.pengrad.telegrambot.model.request.{InputMedia, InputMediaPhoto, InputMediaVideo, ParseMode} -import com.pengrad.telegrambot.request.{SendMediaGroup, SendMessage, SendSticker} -import io.circe.{DecodingFailure, ParsingFailure} -import sttp.client3.{HttpError, SttpClientException} +import com.pengrad.telegrambot.request.SendSticker class GetSocial (using coeur: MornyCoeur) extends ITelegramCommand { @@ -30,111 +24,8 @@ class GetSocial (using coeur: MornyCoeur) extends ITelegramCommand { if command.args.length < 1 then { do404(); return } - var succeed = 0 - twitter.parseTweetUrl(command.args(0)) match - case None => - case Some(TweetUrlInformation(_, _, screenName, statusId, _, _)) => - succeed += 1 - try { - val api = FXApi.Fetch.status(Some(screenName), statusId) - import cc.sukazyo.cono.morny.util.tgapi.formatting.TelegramParseEscape.escapeHtml as h - api.tweet match - case None => - coeur.account exec SendMessage( - event.message.chat.id, - // language=html - s"""❌ Fix-Tweet ${api.code} - |${h(api.message)}""".stripMargin - ).replyToMessageId(event.message.messageId).parseMode(ParseMode.HTML) - case Some(tweet) => - val content: String = - // language=html - s"""⚪️ ${h(tweet.author.name)} @${h(tweet.author.screen_name)} - | - |${h(tweet.text)} - | - |💬${tweet.replies} 🔗${tweet.retweets} ❤️${tweet.likes} - |${h(tweet.created_at)}""".stripMargin - tweet.media match - case None => - coeur.account exec SendMessage( - event.message.chat.id, - content - ).replyToMessageId(event.message.messageId).parseMode(ParseMode.HTML) - case Some(media) => - val mediaGroup: List[InputMedia[?]] = - ( - media.photos match - case None => List.empty - case Some(photos) => for i <- photos yield InputMediaPhoto(i.url) - ) ::: ( - media.videos match - case None => List.empty - case Some(videos) => for i <- videos yield InputMediaVideo(i.url) - ) - mediaGroup.head.caption(content) - mediaGroup.head.parseMode(ParseMode.HTML) - coeur.account exec SendMediaGroup( - event.message.chat.id, - mediaGroup:_* - ).replyToMessageId(event.message.messageId) - } catch case e: (SttpClientException|ParsingFailure|DecodingFailure) => - coeur.account exec SendSticker( - event.message.chat.id, - TelegramStickers.ID_NETWORK_ERR - ).replyToMessageId(event.message.messageId) - logger error - "Error on requesting FixTweet API\n" + exceptionLog(e) - coeur.daemons.reporter.exception(e, "Error on requesting FixTweet API") - - weibo.parseWeiboStatusUrl(command.args(0)) match - case None => - case Some(StatusUrlInfo(_, id)) => - succeed += 1 - try { - val api = weibo.MApi.Fetch.statuses_show(id) - import cc.sukazyo.cono.morny.util.tgapi.formatting.TelegramParseEscape.{cleanupHtml as ch, escapeHtml as h} - val content = - // language=html - s"""🔸${h(api.data.user.screen_name)} - | - |${ch(api.data.text)} - | - |${h(api.data.created_at)}""".stripMargin - api.data.pics match - case None => - coeur.account exec SendMessage( - event.message.chat.id, - content - ).replyToMessageId(event.message.messageId).parseMode(ParseMode.HTML) - case Some(pics) => -// val mediaGroup = pics.map(f => -// InputMediaPhoto(weibo.PicUrl(weibo.randomPicCdn, "large", f.pid).toUrl)) - val mediaGroup = pics.map(f => InputMediaPhoto(weibo.MApi.Fetch.pic(f.large.url))) - mediaGroup.head.caption(content) - mediaGroup.head.parseMode(ParseMode.HTML) - coeur.account exec SendMediaGroup( - event.message.chat.id, - mediaGroup:_* - ).replyToMessageId(event.message.messageId) - } catch - case e: HttpError[?] => - coeur.account exec SendMessage( - event.message.chat.id, - // language=html - s"""Weibo Request Error ${e.statusCode} - |
${e.body}
""".stripMargin - ).replyToMessageId(event.message.messageId).parseMode(ParseMode.HTML) - case e: (SttpClientException|ParsingFailure|DecodingFailure) => - coeur.account exec SendSticker( - event.message.chat.id, - TelegramStickers.ID_NETWORK_ERR - ).replyToMessageId(event.message.messageId) - logger error - "Error on requesting Weibo m.API\n" + exceptionLog(e) - coeur.daemons.reporter.exception(e, "Error on requesting Weibo m.API") - - if succeed == 0 then do404() + if !OnGetSocial.tryFetchSocial(command.args(0))(using event.message.chat.id, event.message.messageId) then + do404() } diff --git a/src/main/scala/cc/sukazyo/cono/morny/bot/event/MornyEventListeners.scala b/src/main/scala/cc/sukazyo/cono/morny/bot/event/MornyEventListeners.scala index e3a6d1f..9e2d674 100644 --- a/src/main/scala/cc/sukazyo/cono/morny/bot/event/MornyEventListeners.scala +++ b/src/main/scala/cc/sukazyo/cono/morny/bot/event/MornyEventListeners.scala @@ -17,6 +17,7 @@ class MornyEventListeners (using manager: EventListenerManager) (using coeur: Mo OnUserSlashAction(), OnCallMe(), OnCallMsgSend(), + OnGetSocial(), OnMedicationNotifyApply(), OnEventHackHandle() ) diff --git a/src/main/scala/cc/sukazyo/cono/morny/bot/event/OnGetSocial.scala b/src/main/scala/cc/sukazyo/cono/morny/bot/event/OnGetSocial.scala new file mode 100644 index 0000000..eeae553 --- /dev/null +++ b/src/main/scala/cc/sukazyo/cono/morny/bot/event/OnGetSocial.scala @@ -0,0 +1,93 @@ +package cc.sukazyo.cono.morny.bot.event + +import cc.sukazyo.cono.morny.MornyCoeur +import cc.sukazyo.cono.morny.bot.api.{EventEnv, EventListener} +import cc.sukazyo.cono.morny.bot.event.OnGetSocial.tryFetchSocial +import cc.sukazyo.cono.morny.data.TelegramStickers +import cc.sukazyo.cono.morny.extra.{twitter, weibo} +import cc.sukazyo.cono.morny.util.tgapi.TelegramExtensions.Bot.exec +import cc.sukazyo.cono.morny.Log.{exceptionLog, logger} +import cc.sukazyo.cono.morny.data.social.{SocialTwitterParser, SocialWeiboParser} +import com.pengrad.telegrambot.model.Chat +import com.pengrad.telegrambot.model.request.ParseMode +import com.pengrad.telegrambot.request.{SendMessage, SendSticker} + +class OnGetSocial (using coeur: MornyCoeur) extends EventListener { + + override def onMessage (using event: EventEnv): Unit = { + import event.update.message as messageEvent + + if messageEvent.chat.`type` != Chat.Type.Private then return; + if messageEvent.text == null then return; + + if tryFetchSocial(messageEvent.text)(using messageEvent.chat.id, messageEvent.messageId) then + event.setEventOk + + } + +} + +object OnGetSocial { + + /** Try fetch from url from input and output fetched social content. + * + * @param text input text, maybe a social url. + * @param replyChat chat that should be output to. + * @param replyToMessage message that should be reply to. + * @param coeur [[MornyCoeur]] instance for executing Telegram function. + * @return [[true]] if fetched social content and sent something out. + */ + def tryFetchSocial (text: String)(using replyChat: Long, replyToMessage: Int)(using coeur: MornyCoeur): Boolean = { + val _text = text.trim + + var succeed = 0 + + import io.circe.{DecodingFailure, ParsingFailure} + import sttp.client3.{HttpError, SttpClientException} + import twitter.{FXApi, TweetUrlInformation} + import weibo.{MApi, StatusUrlInfo} + twitter.parseTweetUrl(_text) match + case None => + case Some(TweetUrlInformation(_, _, screenName, statusId, _, _)) => + succeed += 1 + try { + val api = FXApi.Fetch.status(Some(screenName), statusId) + SocialTwitterParser.parseFXTweet(api).outputToTelegram + } catch case e: (SttpClientException | ParsingFailure | DecodingFailure) => + coeur.account exec SendSticker( + replyChat, + TelegramStickers.ID_NETWORK_ERR + ).replyToMessageId(replyToMessage) + logger error + "Error on requesting FixTweet API\n" + exceptionLog(e) + coeur.daemons.reporter.exception(e, "Error on requesting FixTweet API") + + weibo.parseWeiboStatusUrl(_text) match + case None => + case Some(StatusUrlInfo(_, id)) => + succeed += 1 + try { + val api = MApi.Fetch.statuses_show(id) + SocialWeiboParser.parseMStatus(api).outputToTelegram + } catch + case e: HttpError[?] => + coeur.account exec SendMessage( + replyChat, + // language=html + s"""Weibo Request Error ${e.statusCode} + |
${e.body}
""".stripMargin + ).replyToMessageId(replyToMessage).parseMode(ParseMode.HTML) + case e: (SttpClientException | ParsingFailure | DecodingFailure) => + coeur.account exec SendSticker( + replyChat, + TelegramStickers.ID_NETWORK_ERR + ).replyToMessageId(replyToMessage) + logger error + "Error on requesting Weibo m.API\n" + exceptionLog(e) + coeur.daemons.reporter.exception(e, "Error on requesting Weibo m.API") + + succeed > 0 + + } + +} 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 11c6a12..f0436ca 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 @@ -12,7 +12,8 @@ class MornyQueries (using MornyCoeur) { RawText(), MyInformation(), ShareToolTwitter(), - ShareToolBilibili() + ShareToolBilibili(), + ShareToolSocialContent() ) def query (event: Update): List[InlineQueryUnit[_]] = { diff --git a/src/main/scala/cc/sukazyo/cono/morny/bot/query/ShareToolSocialContent.scala b/src/main/scala/cc/sukazyo/cono/morny/bot/query/ShareToolSocialContent.scala new file mode 100644 index 0000000..60912e7 --- /dev/null +++ b/src/main/scala/cc/sukazyo/cono/morny/bot/query/ShareToolSocialContent.scala @@ -0,0 +1,43 @@ +package cc.sukazyo.cono.morny.bot.query +import cc.sukazyo.cono.morny.data.social.{SocialTwitterParser, SocialWeiboParser} +import cc.sukazyo.cono.morny.extra.{twitter, weibo} +import cc.sukazyo.cono.morny.extra.twitter.{FXApi, TweetUrlInformation} +import cc.sukazyo.cono.morny.extra.weibo.{MApi, StatusUrlInfo} +import com.pengrad.telegrambot.model.Update + +class ShareToolSocialContent extends ITelegramQuery { + + override def query (event: Update): List[InlineQueryUnit[_]] | Null = { + + val _queryRaw = event.inlineQuery.query + val query = + _queryRaw.trim match + case _startsWithTag if _startsWithTag startsWith "get " => + (_startsWithTag drop 4)trim + case _endsWithTag if _endsWithTag endsWith " get" => + (_endsWithTag dropRight 4)trim + case _ => return null + + ( + twitter.parseTweetUrl(query) match + case Some(TweetUrlInformation(_, statusPath, _, statusId, _, _)) => + SocialTwitterParser.parseFXTweet(FXApi.Fetch.status(Some(statusPath), statusId)) + .genInlineQueryResults(using + "morny/share/tweet/content", statusId, + "Twitter Tweet Content" + ) + case None => Nil + ) ::: ( + weibo.parseWeiboStatusUrl(query) match + case Some(StatusUrlInfo(_, id)) => + SocialWeiboParser.parseMStatus(MApi.Fetch.statuses_show(id)) + .genInlineQueryResults(using + "morny/share/weibo/status/content", id, + "Weibo Content" + ) + case None => Nil + ) ::: Nil + + } + +} diff --git a/src/main/scala/cc/sukazyo/cono/morny/data/social/SocialContent.scala b/src/main/scala/cc/sukazyo/cono/morny/data/social/SocialContent.scala new file mode 100644 index 0000000..84bcf80 --- /dev/null +++ b/src/main/scala/cc/sukazyo/cono/morny/data/social/SocialContent.scala @@ -0,0 +1,102 @@ +package cc.sukazyo.cono.morny.data.social + +import cc.sukazyo.cono.morny.data.social.SocialContent.{SocialMedia, SocialMediaType, SocialMediaWithUrl} +import cc.sukazyo.cono.morny.data.social.SocialContent.SocialMediaType.{Photo, Video} +import cc.sukazyo.cono.morny.MornyCoeur +import cc.sukazyo.cono.morny.bot.query.InlineQueryUnit +import cc.sukazyo.cono.morny.util.tgapi.TelegramExtensions.Bot.exec +import cc.sukazyo.cono.morny.util.tgapi.formatting.NamingUtils.inlineQueryId +import com.pengrad.telegrambot.model.request.* +import com.pengrad.telegrambot.request.{SendMediaGroup, SendMessage} + +/** Model of social networks' status. for example twitter tweet or + * weibo status. + * + * Can be output to Telegram. + * + * @param text_html Formatted HTML output of the status that can be output + * directly to Telegram. Normally will contains metadata + * like status' author or like count etc. + * @param medias Status attachment medias. + * @param medias_mosaic Mosaic version of status medias. Will be used when + * the output API doesn't support multiple medias like + * Telegram inline API. This value is depends on the specific + * backend parser/formatter implementation. + * @param thumbnail Medias' thumbnail. Will be used when the output API required + * a thumbnail. This value is depends on the specific backend + * parser/formatter implementation. + */ +case class SocialContent ( + text_html: String, + medias: List[SocialMedia], + medias_mosaic: Option[SocialMedia] = None, + thumbnail: Option[SocialMedia] = None +) { + + def thumbnailOrElse[T] (orElse: T): String | T = + thumbnail match + case Some(x) if x.isInstanceOf[SocialMediaWithUrl] && x.t == Photo => + x.asInstanceOf[SocialMediaWithUrl].url + case _ => orElse + + def outputToTelegram (using replyChat: Long, replyToMessage: Int)(using coeur: MornyCoeur): Unit = { + if medias isEmpty then + coeur.account exec + SendMessage(replyChat, text_html) + .parseMode(ParseMode.HTML) + .replyToMessageId(replyToMessage) + else + val mediaGroup = medias.map(f => f.genTelegramInputMedia) + mediaGroup.head.caption(text_html) + mediaGroup.head.parseMode(ParseMode.HTML) + coeur.account exec + SendMediaGroup(replyChat, mediaGroup: _*) + .replyToMessageId(replyToMessage) + } + + def genInlineQueryResults (using id_head: String, id_param: Any, name: String): List[InlineQueryUnit[?]] = { + ( + if (this.medias.length == 1) && (this.medias.head.t == Photo) && this.medias.head.isInstanceOf[SocialMediaWithUrl] then + InlineQueryUnit(InlineQueryResultPhoto( + inlineQueryId(s"[$id_head/photo/0]$id_param"), + this.medias.head.asInstanceOf[SocialMediaWithUrl].url, + thumbnailOrElse(this.medias.head.asInstanceOf[SocialMediaWithUrl].url) + ).title(s"$name").caption(text_html).parseMode(ParseMode.HTML)) :: Nil + else if (this.medias_mosaic nonEmpty) && (medias_mosaic.get.t == Photo) && medias_mosaic.get.isInstanceOf[SocialMediaWithUrl] then + InlineQueryUnit(InlineQueryResultPhoto( + inlineQueryId(s"[$id_head/photo/mosaic]$id_param"), + medias_mosaic.get.asInstanceOf[SocialMediaWithUrl].url, + thumbnailOrElse(medias_mosaic.get.asInstanceOf[SocialMediaWithUrl].url) + ).title(s"$name").caption(text_html).parseMode(ParseMode.HTML)) :: Nil + else + InlineQueryUnit(InlineQueryResultArticle( + inlineQueryId(s"[$id_head/text]$id_param"), s"$name", + InputTextMessageContent(this.text_html).parseMode(ParseMode.HTML) + )) :: Nil + ) ::: Nil + } + +} + +object SocialContent { + + enum SocialMediaType: + case Photo + case Video + sealed trait SocialMedia(val t: SocialMediaType) { + def genTelegramInputMedia: InputMedia[?] + } + case class SocialMediaWithUrl (url: String)(t: SocialMediaType) extends SocialMedia(t) { + override def genTelegramInputMedia: InputMedia[_] = + t match + case Photo => InputMediaPhoto(url) + case Video => InputMediaVideo(url) + } + case class SocialMediaWithBytesData (data: Array[Byte])(t: SocialMediaType) extends SocialMedia(t) { + override def genTelegramInputMedia: InputMedia[_] = + t match + case Photo => InputMediaPhoto(data) + case Video => InputMediaVideo(data) + } + +} diff --git a/src/main/scala/cc/sukazyo/cono/morny/data/social/SocialTwitterParser.scala b/src/main/scala/cc/sukazyo/cono/morny/data/social/SocialTwitterParser.scala new file mode 100644 index 0000000..21e2da9 --- /dev/null +++ b/src/main/scala/cc/sukazyo/cono/morny/data/social/SocialTwitterParser.scala @@ -0,0 +1,52 @@ +package cc.sukazyo.cono.morny.data.social + +import cc.sukazyo.cono.morny.data.social.SocialContent.{SocialMedia, SocialMediaWithUrl} +import cc.sukazyo.cono.morny.data.social.SocialContent.SocialMediaType.{Photo, Video} +import cc.sukazyo.cono.morny.extra.twitter.FXApi +import cc.sukazyo.cono.morny.util.tgapi.formatting.TelegramParseEscape.escapeHtml as h + +object SocialTwitterParser { + + def parseFXTweet (api: FXApi): SocialContent = { + api.tweet match + case None => + SocialContent( + // language=html + s"""❌ Fix-Tweet ${api.code} + |${h(api.message)}""".stripMargin, + Nil + ) + case Some(tweet) => + val content: String = + // language=html + s"""⚪️ ${h(tweet.author.name)} @${h(tweet.author.screen_name)} + | + |${h(tweet.text)} + | + |💬${tweet.replies} 🔗${tweet.retweets} ❤️${tweet.likes} + |${h(tweet.created_at)}""".stripMargin + tweet.media match + case None => + SocialContent(content, Nil) + case Some(media) => + val mediaGroup: List[SocialMedia] = + ( + media.photos match + case None => List.empty + case Some(photos) => for i <- photos yield SocialMediaWithUrl(i.url)(Photo) + ) ::: ( + media.videos match + case None => List.empty + case Some(videos) => for i <- videos yield SocialMediaWithUrl(i.url)(Video) + ) + val thumbnail = + if media.videos.nonEmpty then + Some(SocialMediaWithUrl(media.videos.get.head.thumbnail_url)(Photo)) + else None + val mediaMosaic = media.mosaic match + case Some(mosaic) => Some(SocialMediaWithUrl(mosaic.formats.jpeg)(Photo)) + case None => None + SocialContent(content, mediaGroup, mediaMosaic, thumbnail) + } + +} diff --git a/src/main/scala/cc/sukazyo/cono/morny/data/social/SocialWeiboParser.scala b/src/main/scala/cc/sukazyo/cono/morny/data/social/SocialWeiboParser.scala new file mode 100644 index 0000000..44b9482 --- /dev/null +++ b/src/main/scala/cc/sukazyo/cono/morny/data/social/SocialWeiboParser.scala @@ -0,0 +1,29 @@ +package cc.sukazyo.cono.morny.data.social + +import cc.sukazyo.cono.morny.data.social.SocialContent.SocialMediaType.Photo +import cc.sukazyo.cono.morny.data.social.SocialContent.SocialMediaWithBytesData +import cc.sukazyo.cono.morny.extra.weibo.{genWeiboStatusUrl, MApi, MStatus, StatusUrlInfo} +import cc.sukazyo.cono.morny.util.tgapi.formatting.TelegramParseEscape.{cleanupHtml as ch, escapeHtml as h} +import io.circe.{DecodingFailure, ParsingFailure} +import sttp.client3.{HttpError, SttpClientException} + +object SocialWeiboParser { + + @throws[HttpError[?] | SttpClientException | ParsingFailure | DecodingFailure] + def parseMStatus (api: MApi[MStatus]): SocialContent = { + val content = + // language=html + s"""🔸${h(api.data.user.screen_name)} + | + |${ch(api.data.text)} + | + |${h(api.data.created_at)}""".stripMargin + api.data.pics match + case None => + SocialContent(content, Nil) + case Some(pics) => + val mediaGroup = pics.map(f => SocialMediaWithBytesData(MApi.Fetch.pic(f.large.url))(Photo)) + SocialContent(content, mediaGroup) + } + +}