mirror of
https://github.com/Eyre-S/Coeur-Morny-Cono.git
synced 2024-11-25 04:27:41 +08:00
change /tweet to /get and added support for weibo content
This commit is contained in:
parent
d602e1b366
commit
a9767ec1b0
@ -92,6 +92,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: 'org.jsoup', name: 'jsoup', version: '1.16.2'
|
||||
implementation group: 'com.cronutils', name: 'cron-utils', version: lib_cron_utils_v
|
||||
|
||||
// used for disable slf4j
|
||||
|
@ -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-dev10
|
||||
VERSION = 1.3.0-dev11
|
||||
|
||||
USE_DELTA = false
|
||||
VERSION_DELTA =
|
||||
|
@ -1,20 +1,23 @@
|
||||
package cc.sukazyo.cono.morny.bot.command
|
||||
import cc.sukazyo.cono.morny.data.{twitter, TelegramStickers}
|
||||
import cc.sukazyo.cono.morny.data.{twitter, weibo, TelegramStickers}
|
||||
import cc.sukazyo.cono.morny.util.tgapi.InputCommand
|
||||
import cc.sukazyo.cono.morny.MornyCoeur
|
||||
import cc.sukazyo.cono.morny.data.twitter.{FXApi, TweetUrlInformation}
|
||||
import cc.sukazyo.cono.morny.util.tgapi.TelegramExtensions.Bot.exec
|
||||
import cc.sukazyo.cono.morny.Log.{exceptionLog, logger}
|
||||
import cc.sukazyo.cono.morny.data.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}
|
||||
|
||||
class Tweet (using coeur: MornyCoeur) extends ITelegramCommand {
|
||||
class GetSocial (using coeur: MornyCoeur) extends ITelegramCommand {
|
||||
|
||||
override val name: String = "tweet"
|
||||
override val name: String = "get"
|
||||
override val aliases: Array[ICommandAlias] | Null = null
|
||||
override val paramRule: String = "<tweet-url>"
|
||||
override val description: String = "获取 Twitter(X) Tweet 内容"
|
||||
override val paramRule: String = "<tweet-url|weibo-status-url>"
|
||||
override val description: String = "从社交媒体分享链接获取其内容"
|
||||
|
||||
override def execute (using command: InputCommand, event: Update): Unit = {
|
||||
|
||||
@ -26,9 +29,11 @@ class Tweet (using coeur: MornyCoeur) extends ITelegramCommand {
|
||||
|
||||
if command.args.length < 1 then { do404(); return }
|
||||
|
||||
var succeed = 0
|
||||
twitter.parseTweetUrl(command.args(0)) match
|
||||
case None => do404()
|
||||
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
|
||||
@ -72,15 +77,64 @@ class Tweet (using coeur: MornyCoeur) extends ITelegramCommand {
|
||||
event.message.chat.id,
|
||||
mediaGroup:_*
|
||||
).replyToMessageId(event.message.messageId)
|
||||
} catch case e: Exception =>
|
||||
} catch case e: (SttpClientException|ParsingFailure|DecodingFailure) =>
|
||||
coeur.account exec SendSticker(
|
||||
event.message.chat.id,
|
||||
TelegramStickers.ID_NETWORK_ERR
|
||||
).replyToMessageId(event.message.messageId)
|
||||
logger attention
|
||||
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"""🔸<b><a href="${api.data.user.profile_url}">${h(api.data.user.screen_name)}</a></b>
|
||||
|
|
||||
|${ch(api.data.text)}
|
||||
|
|
||||
|<i><a href="${weibo.genWeiboStatusUrl(StatusUrlInfo(api.data.user.id.toString, api.data.id))}">${h(api.data.created_at)}</a></i>""".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 <code>${e.statusCode}</code>
|
||||
|<pre><code>${e.body}</code></pre>""".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()
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -44,7 +44,7 @@ class MornyCommands (using coeur: MornyCoeur) {
|
||||
$IP186Query.Whois,
|
||||
Encryptor(),
|
||||
MornyOldJrrp(),
|
||||
Tweet(),
|
||||
GetSocial(),
|
||||
|
||||
$MornyManagers.SaveData,
|
||||
$MornyInformation,
|
||||
|
@ -91,18 +91,16 @@ object FXApi {
|
||||
@throws[SttpClientException|ParsingFailure|DecodingFailure]
|
||||
def status (screen_name: Option[String], id: String, translate_to: Option[String] = None): FXApi =
|
||||
val get = mornyBasicRequest
|
||||
.header(SttpPublic.Headers.UserAgent.MORNY_CURRENT)
|
||||
.get(uri_status(screen_name, id, translate_to))
|
||||
.response(asString)
|
||||
.send(httpClient)
|
||||
val body = get.body match
|
||||
case Left(error) => error
|
||||
case Right(success) => success
|
||||
parser.parse(body) match
|
||||
case Left(error) => throw error
|
||||
case Right(value) => value.as[FXApi] match
|
||||
case Left(error) => throw error
|
||||
case Right(value) => value
|
||||
parser.parse(body)
|
||||
.toTry.get
|
||||
.as[FXApi]
|
||||
.toTry.get
|
||||
|
||||
}
|
||||
|
||||
|
66
src/main/scala/cc/sukazyo/cono/morny/data/weibo/MApi.scala
Normal file
66
src/main/scala/cc/sukazyo/cono/morny/data/weibo/MApi.scala
Normal file
@ -0,0 +1,66 @@
|
||||
package cc.sukazyo.cono.morny.data.weibo
|
||||
|
||||
case class MApi [D] (
|
||||
ok: Int,
|
||||
data: D
|
||||
)
|
||||
|
||||
object MApi {
|
||||
|
||||
object CirceADTs {
|
||||
import io.circe.Decoder
|
||||
import io.circe.generic.semiauto.deriveDecoder
|
||||
given Decoder[MUser] = deriveDecoder
|
||||
given given_Decoder_largeType_getType: Decoder[MPic.largeType.geoType] = deriveDecoder
|
||||
given Decoder[MPic.largeType] = deriveDecoder
|
||||
given Decoder[MPic.geoType] = deriveDecoder
|
||||
given Decoder[MPic] = deriveDecoder
|
||||
given Decoder[MStatus] = deriveDecoder
|
||||
given Decoder[MApi[MStatus]] = deriveDecoder
|
||||
}
|
||||
|
||||
object Fetch {
|
||||
|
||||
import cc.sukazyo.cono.morny.util.SttpPublic
|
||||
import cc.sukazyo.cono.morny.util.SttpPublic.mornyBasicRequest
|
||||
import io.circe.{parser, DecodingFailure, ParsingFailure}
|
||||
import sttp.client3.{HttpError, SttpClientException, UriContext}
|
||||
import sttp.client3.okhttp.OkHttpSyncBackend
|
||||
|
||||
val uri_base = uri"https://m.weibo.cn/"
|
||||
val uri_statuses_show =
|
||||
(id: String) => uri"$uri_base/statuses/show?id=$id"
|
||||
|
||||
private val httpClient = OkHttpSyncBackend()
|
||||
|
||||
@throws[HttpError[_]|SttpClientException|ParsingFailure|DecodingFailure]
|
||||
def statuses_show (id: String): MApi[MStatus] =
|
||||
import sttp.client3.asString
|
||||
import MApi.CirceADTs.given
|
||||
val response = mornyBasicRequest
|
||||
.get(uri_statuses_show(id))
|
||||
.response(asString.getRight)
|
||||
.send(httpClient)
|
||||
parser.parse(response.body)
|
||||
.toTry.get
|
||||
.as[MApi[MStatus]]
|
||||
.toTry.get
|
||||
|
||||
@throws[HttpError[_] | SttpClientException | ParsingFailure | DecodingFailure]
|
||||
def pic (picUrl: String): Array[Byte] =
|
||||
import sttp.client3.*
|
||||
import sttp.model.{MediaType, Uri}
|
||||
mornyBasicRequest
|
||||
.acceptEncoding(MediaType.ImageJpeg.toString)
|
||||
.get(Uri.unsafeParse(picUrl))
|
||||
.response(asByteArray.getRight)
|
||||
.send(httpClient)
|
||||
.body
|
||||
|
||||
// @throws[HttpError[_] | SttpClientException | ParsingFailure | DecodingFailure]
|
||||
// def pic (info: PicUrl): Array[Byte] =
|
||||
// pic(info.toUrl)
|
||||
|
||||
}
|
||||
|
||||
}
|
33
src/main/scala/cc/sukazyo/cono/morny/data/weibo/MPic.scala
Normal file
33
src/main/scala/cc/sukazyo/cono/morny/data/weibo/MPic.scala
Normal file
@ -0,0 +1,33 @@
|
||||
package cc.sukazyo.cono.morny.data.weibo
|
||||
|
||||
case class MPic (
|
||||
pid: String,
|
||||
url: String,
|
||||
size: String,
|
||||
geo: MPic.geoType,
|
||||
large: MPic.largeType
|
||||
)
|
||||
|
||||
object MPic {
|
||||
|
||||
case class geoType (
|
||||
// width: Int,
|
||||
// height: Int,
|
||||
croped: Boolean
|
||||
)
|
||||
|
||||
case class largeType (
|
||||
size: String,
|
||||
url: String,
|
||||
geo: largeType.geoType
|
||||
)
|
||||
|
||||
object largeType {
|
||||
case class geoType (
|
||||
// width: String,
|
||||
// height: String,
|
||||
croped: Boolean
|
||||
)
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,92 @@
|
||||
package cc.sukazyo.cono.morny.data.weibo
|
||||
|
||||
case class MStatus (
|
||||
|
||||
id: String,
|
||||
mid: String,
|
||||
bid: String,
|
||||
|
||||
created_at: String,
|
||||
text: String,
|
||||
raw_text: Option[String],
|
||||
|
||||
user: MUser,
|
||||
|
||||
retweeted_status: Option[MStatus],
|
||||
|
||||
pic_ids: List[String],
|
||||
pics: Option[List[MPic]],
|
||||
thumbnail_pic: Option[String],
|
||||
bmiddle_pic: Option[String],
|
||||
original_pic: Option[String],
|
||||
|
||||
// visible: Nothing,
|
||||
// created_at: String,
|
||||
// id: String,
|
||||
// mid: String,
|
||||
// bid: String,
|
||||
// can_edit: Boolean,
|
||||
// show_additional_indication: Int,
|
||||
// text: String,
|
||||
// textLength: Option[Int],
|
||||
// source: String,
|
||||
// favorited: Boolean,
|
||||
// pic_ids: List[String],
|
||||
// pic_focus_point: Option[List[Nothing]],
|
||||
// falls_pic_focus_point: Option[List[Nothing]],
|
||||
// pic_rectangle_object: Option[List[Nothing]],
|
||||
// pic_flag: Option[Int],
|
||||
// thumbnail_pic: Option[String],
|
||||
// bmiddle_pic: Option[String],
|
||||
// original_pic: Option[String],
|
||||
// is_paid: Boolean,
|
||||
// mblog_vip_type: Int,
|
||||
// user: Nothing,
|
||||
// picStatus: Option[String],
|
||||
// retweeted_status: Option[Nothing],
|
||||
// reposts_count: Int,
|
||||
// comments_count: Int,
|
||||
// reprint_cmt_count: Int,
|
||||
// attitudes_count: Int,
|
||||
// pending_approval_count: Int,
|
||||
// isLongText: Boolean,
|
||||
// show_mlevel: Int,
|
||||
// topic_id: Option[String],
|
||||
// sync_mblog: Option[Boolean],
|
||||
// is_imported_topic: Option[Boolean],
|
||||
// darwin_tags: List[Nothing],
|
||||
// ad_marked: Boolean,
|
||||
// mblogtype: Int,
|
||||
// item_category: String,
|
||||
// rid: String,
|
||||
// number_display_strategy: Nothing,
|
||||
// content_auth: Int,
|
||||
// safe_tags: Option[Int],
|
||||
// comment_manage_info: Nothing,
|
||||
// repost_type: Option[Int],
|
||||
// pic_num: Int,
|
||||
// jump_type: Option[Int],
|
||||
// hot_page: Nothing,
|
||||
// new_comment_style: Int,
|
||||
// ab_switcher: Int,
|
||||
// mlevel: Int,
|
||||
// region_name: String,
|
||||
// region_opt: 1,
|
||||
// page_info: Option[Nothing],
|
||||
// pics: Option[List[Nothing]],
|
||||
// raw_text: Option[String],
|
||||
// buttons: List[Nothing],
|
||||
// status_title: Option[String],
|
||||
// ok: Int,
|
||||
|
||||
|
||||
// pid: Long,
|
||||
// pidstr: String,
|
||||
// pic_types: String,
|
||||
// alchemy_params: Nothing,
|
||||
// ad_state: Int,
|
||||
// cardid: String,
|
||||
// hide_flag: Int,
|
||||
// mark: String,
|
||||
// more_info_type: Int,
|
||||
)
|
13
src/main/scala/cc/sukazyo/cono/morny/data/weibo/MUser.scala
Normal file
13
src/main/scala/cc/sukazyo/cono/morny/data/weibo/MUser.scala
Normal file
@ -0,0 +1,13 @@
|
||||
package cc.sukazyo.cono.morny.data.weibo
|
||||
|
||||
case class MUser (
|
||||
|
||||
id: Long,
|
||||
screen_name: String,
|
||||
profile_url: String,
|
||||
profile_image_url: Option[String],
|
||||
avatar_hd: Option[String],
|
||||
description: Option[String],
|
||||
cover_image_phone: Option[String],
|
||||
|
||||
)
|
@ -0,0 +1,40 @@
|
||||
package cc.sukazyo.cono.morny.data
|
||||
|
||||
package object weibo {
|
||||
|
||||
/** Information in weibo status url.
|
||||
*
|
||||
* @param uid Status owner's user id. should be a number.
|
||||
* @param id Status id. Should be unique in the whole weibo.com
|
||||
* globe. Maybe a number format mid, or a base58-like
|
||||
* bid.
|
||||
*/
|
||||
case class StatusUrlInfo (
|
||||
uid: String,
|
||||
id: String
|
||||
)
|
||||
|
||||
// case class PicUrl (
|
||||
// cdn: String,
|
||||
// mode: String,
|
||||
// pid: String
|
||||
// ) {
|
||||
// def toUrl: String =
|
||||
// s"https://$cdn.singimg.cn/$mode/$pid.jpg"
|
||||
// }
|
||||
|
||||
private val REGEX_WEIBO_STATUS_URL = "^(?:https?://)?((?:www\\.|m.)?weibo\\.(?:com|cn))/(\\d+)/([0-9a-zA-Z]+)/?(?:\\?([\\w&=-]+))?$"r
|
||||
|
||||
def parseWeiboStatusUrl (url: String): Option[StatusUrlInfo] =
|
||||
url match
|
||||
case REGEX_WEIBO_STATUS_URL(_, uid, id, _) => Some(StatusUrlInfo(uid, id))
|
||||
case _ => None
|
||||
|
||||
def genWeiboStatusUrl (url: StatusUrlInfo): String =
|
||||
s"https://weibo.com/${url.uid}/${url.id}"
|
||||
|
||||
// def randomPicCdn: String =
|
||||
// import scala.util.Random
|
||||
// s"wx${Random.nextInt(4)+1}"
|
||||
|
||||
}
|
@ -21,7 +21,9 @@ object TelegramExtensions {
|
||||
if onError_message isEmpty then response.errorCode toString else onError_message,
|
||||
response
|
||||
)
|
||||
} catch case e: RuntimeException =>
|
||||
} catch
|
||||
case e: EventRuntimeException.ActionFailed => throw e
|
||||
case e: RuntimeException =>
|
||||
throw EventRuntimeException.ClientFailed(e)
|
||||
}
|
||||
|
||||
|
@ -1,5 +1,11 @@
|
||||
package cc.sukazyo.cono.morny.util.tgapi.formatting
|
||||
|
||||
import org.jsoup.Jsoup
|
||||
import org.jsoup.nodes.Node
|
||||
|
||||
import scala.collection.mutable
|
||||
import scala.jdk.CollectionConverters.*
|
||||
|
||||
object TelegramParseEscape {
|
||||
|
||||
def escapeHtml (input: String): String =
|
||||
@ -9,4 +15,55 @@ object TelegramParseEscape {
|
||||
process = process.replaceAll(">", ">")
|
||||
process
|
||||
|
||||
def cleanupHtml (input: String): String =
|
||||
import org.jsoup.nodes.*
|
||||
val source = Jsoup.parse(input)
|
||||
val x = cleanupHtml(source.body.childNodes.asScala.toSeq)
|
||||
val doc = Document("")
|
||||
doc.outputSettings
|
||||
.prettyPrint(false)
|
||||
x.map(f => doc.appendChild(f))
|
||||
x.mkString("")
|
||||
|
||||
// def toHtmlRaw (input: Node): String =
|
||||
// import org.jsoup.nodes.*
|
||||
// input match
|
||||
// case text: TextNode => text.getWholeText
|
||||
// case _: (DataNode | XmlDeclaration | DocumentType | Comment) => ""
|
||||
// case elem: Element => elem.childNodes.asScala.map(f => toHtmlRaw(f)).mkString("")
|
||||
|
||||
def cleanupHtml (input: Seq[Node]): List[Node] =
|
||||
val result = mutable.ListBuffer.empty[Node]
|
||||
for (i <- input) {
|
||||
import org.jsoup.nodes.*
|
||||
def produceChildNodes (curr: Element): Element =
|
||||
val newOne = Element(curr.tagName)
|
||||
curr.attributes.forEach(attr => newOne.attr(attr.getKey, attr.getValue))
|
||||
for (i <- cleanupHtml(curr.childNodes.asScala.toSeq)) newOne.appendChild(i)
|
||||
newOne
|
||||
i match
|
||||
case text_cdata: CDataNode => result += CDataNode(text_cdata.text)
|
||||
case text: TextNode => result += TextNode(text.getWholeText)
|
||||
case _: (DataNode | XmlDeclaration | DocumentType | Comment) =>
|
||||
case elem: Element => elem match
|
||||
case _: Document => // should not exists here
|
||||
case _: FormElement => // ignored due to Telegram do not support form
|
||||
case elem => elem.tagName match
|
||||
case "a"|"b"|"strong"|"i"|"em"|"u"|"ins"|"s"|"strike"|"del"|"tg-spoiler"|"code"|"pre" =>
|
||||
result += produceChildNodes(elem)
|
||||
case "br" =>
|
||||
result += TextNode("\n")
|
||||
case "tg-emoji" =>
|
||||
if elem.attributes.hasKey("emoji-id") then
|
||||
result += produceChildNodes(elem)
|
||||
else
|
||||
result += TextNode(elem.text)
|
||||
case "img" =>
|
||||
if elem.attributes hasKey "alt" then
|
||||
result += TextNode(s"[${elem attr "alt"}]")
|
||||
case _ =>
|
||||
for (i <- cleanupHtml(elem.childNodes.asScala.toSeq)) result += i
|
||||
}
|
||||
result.toList
|
||||
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user