diff --git a/apps/query.js b/apps/query.js index f012c3f..59c6338 100644 --- a/apps/query.js +++ b/apps/query.js @@ -8,12 +8,17 @@ import axios from "axios"; // 常量 import { CAT_LIMIT } from "../constants/constant.js"; // 书库 -import { getZHelper, getYiBook, getZBook } from "../utils/books.js"; +import { getYiBook, getZBook, getZHelper } from "../utils/books.js"; // 工具类 -import _ from "lodash"; import TokenBucket from '../utils/token-bucket.js' export class query extends plugin { + /** + * 令牌桶 拿来限流 + * @type {TokenBucket} + */ + static #tokenBucket = new TokenBucket(1, 1, 60); + constructor() { super({ name: "R插件查询类", @@ -59,19 +64,19 @@ export class query extends plugin { async doctor(e) { const keyword = e.msg.replace("#医药查询", "").trim(); - const url = `https://api2.dayi.org.cn/api/search2?keyword=${keyword}&pageNo=1&pageSize=10`; + const url = `https://api2.dayi.org.cn/api/search2?keyword=${ keyword }&pageNo=1&pageSize=10`; try { const res = await fetch(url) .then(resp => resp.json()) .then(resp => resp.list); const promises = res.map(async element => { const title = this.removeTag(element.title); - const template = `${title}\n标签:${element.secondTitle}\n介绍:${element.introduction}`; + const template = `${ title }\n标签:${ element.secondTitle }\n介绍:${ element.introduction }`; if (title === keyword) { const browser = await puppeteer.browserInit(); const page = await browser.newPage(); - await page.goto(`https://www.dayi.org.cn/drug/${element.id}`); + await page.goto(`https://www.dayi.org.cn/drug/${ element.id }`); const buff = await page.screenshot({ fullPage: true, type: "jpeg", @@ -101,8 +106,8 @@ export class query extends plugin { async cat(e) { const [shibes, cats] = await Promise.allSettled([ - fetch(`https://shibe.online/api/cats?count=${CAT_LIMIT}`).then(data => data.json()), - fetch(`https://api.thecatapi.com/v1/images/search?limit=${CAT_LIMIT}`).then(data => + fetch(`https://shibe.online/api/cats?count=${ CAT_LIMIT }`).then(data => data.json()), + fetch(`https://api.thecatapi.com/v1/images/search?limit=${ CAT_LIMIT }`).then(data => data.json(), ), ]); @@ -139,7 +144,7 @@ export class query extends plugin { .filter(result => result.status === "fulfilled") // 只保留已解决的 Promise .flatMap(result => result.value.data.list.map(element => { - const template = `推荐软件:${element.title}\n地址:${element.url}\n`; + const template = `推荐软件:${ element.title }\n地址:${ element.url }\n`; return { message: { type: "text", text: template }, nickname: e.sender.card || e.user_id, @@ -228,14 +233,14 @@ export class query extends plugin { // 封装答案 let ans = ""; for (let i = 0; i < result.length; i++) { - ans += `${i + 1}. ${result[i]}\n`; + ans += `${ i + 1 }. ${ result[i] }\n`; } e.reply(ans); const imgMatch = uri.match(/[^\/]+/g); const imgId = imgMatch[imgMatch.length - 2]; axios - .get(`https://h5.cyol.com/special/daxuexi/${imgId}/images/end.jpg`, { + .get(`https://h5.cyol.com/special/daxuexi/${ imgId }/images/end.jpg`, { headers: { "User-Agent": "Mozilla/5.0 (Linux; Android 5.0; SM-G900P Build/LRX21T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.25 Mobile Safari/537.36", @@ -353,15 +358,15 @@ export class query extends plugin { .sort((a, b) => b.luSort - a.luSort) .map(item => { const { pn, pa, zn, lu, pu, pq, aa, hl } = item; - const template = `标题:${pn}\n${pa}\n期刊:${zn}\n发布日期距今:${lu}\n链接1:${pu}\n链接2:${pq}\n\n 大致描述:${hl + const template = `标题:${ pn }\n${ pa }\n期刊:${ zn }\n发布日期距今:${ lu }\n链接1:${ pu }\n链接2:${ pq }\n\n 大致描述:${ hl .join("\n") - .replace(/<\/?font[^>]*>/g, "")}`; + .replace(/<\/?font[^>]*>/g, "") }`; return { message: [segment.image(aa), template], nickname: this.e.sender.card || this.e.user_id, user_id: this.e.user_id, }; - }); + }); e.reply(await Bot.makeForwardMsg(content)); }); return true; @@ -377,7 +382,7 @@ export class query extends plugin { if (query.#tokenBucket.consume(e.user_id, 1)) { await func(); } else { - e.reply(`🙅‍${e.nickname}你已经被限流,请稍后再试!`, true); + e.reply(`🙅‍${ e.nickname }你已经被限流,请稍后再试!`, true); } } @@ -386,10 +391,4 @@ export class query extends plugin { const titleRex = /<[^>]+>/g; return title.replace(titleRex, ""); } - - /** - * 令牌桶 拿来限流 - * @type {TokenBucket} - */ - static #tokenBucket = new TokenBucket(1, 1, 60); } diff --git a/apps/tools.js b/apps/tools.js index 8453bdb..573a1d2 100644 --- a/apps/tools.js +++ b/apps/tools.js @@ -26,7 +26,7 @@ import { TWITTER_BEARER_TOKEN, XHS_NO_WATERMARK_HEADER, } from "../constants/constant.js"; -import { containsChinese, formatBiliInfo, getIdVideo, secondsToTime } from "../utils/common.js"; +import { containsChinese, downloadImg, formatBiliInfo, getIdVideo, secondsToTime } from "../utils/common.js"; import config from "../model/index.js"; import Translate from "../utils/trans-strategy.js"; import * as xBogus from "../utils/x-bogus.cjs"; @@ -38,6 +38,7 @@ import { getWbi } from "../utils/biliWbi.js"; import { BILI_SUMMARY, DY_INFO, + MIYOUSHE_ARTICLE, TIKTOK_INFO, TWITTER_TWEET_INFO, XHS_REQ_LINK, @@ -47,6 +48,7 @@ import { import child_process from 'node:child_process' import { getAudio, getVideo } from "../utils/y2b.js"; import { processTikTokUrl } from "../utils/tiktok.js"; +import { getDS } from "../utils/mihoyo.js"; export class tools extends plugin { /** @@ -134,6 +136,10 @@ export class tools extends plugin { { reg: "(ixigua.com)", fnc: "xigua" + }, + { + reg: "(miyoushe.com)", + fnc: "miyoushe" } ], }); @@ -566,7 +572,7 @@ export class tools extends plugin { for (let item of resp.includes.media) { if (item.type === "photo") { // 图片 - task.push(this.downloadImg(item.url, downloadPath, "", true)); + task.push(downloadImg(item.url, downloadPath, "", true)); } else if (item.type === "video") { // 视频 await this.downloadVideo(resp.includes.media[0].variants[0].url, true).then( @@ -686,7 +692,7 @@ export class tools extends plugin { } else if (type === "normal") { e.reply(`识别:小红书, ${ title }\n${ desc }`); noteData.imageList.map(async (item, index) => { - imgPromise.push(this.downloadImg(item.urlDefault, downloadPath, index.toString())); + imgPromise.push(downloadImg(item.urlDefault, downloadPath, index.toString())); }); } const paths = await Promise.all(imgPromise); @@ -773,7 +779,7 @@ export class tools extends plugin { // 判断是否是海外服务器 const isOversea = await this.isOverseasServer(); // 简单封装图片下载 - const downloadImg = (url, destination) => { + const downloadInsImg = (url, destination) => { return new Promise((resolve, reject) => { fetch(url, { timeout: 10000, @@ -805,7 +811,7 @@ export class tools extends plugin { .exec(item)[0] .replace(/#38/g, "") .replace(/;/g, ""); - imgPromise.push(downloadImg(imgUrl, `${ downloadPath }/${ index }.jpg`)); + imgPromise.push(downloadInsImg(imgUrl, `${ downloadPath }/${ index }.jpg`)); }); } // TODO 视频,会出bug暂时不做 @@ -924,10 +930,25 @@ export class tools extends plugin { }, timeout: 10000 // 设置超时时间 }).then(resp => { - const url = resp.data.data.url; - this.downloadVideo(url).then(path => { - e.reply(segment.video(path + "/temp.mp4")); - }); + // 图片:https://kph8gvfz.m.chenzhongtech.com/fw/photo/3x45s52s9wchwwm + + if (resp.data.data?.imageUrl) { + const imageUrl = resp.data.data.imageUrl; + const images = imageUrl.map(item => { + return { + message: segment.image(item), + nickname: this.e.sender.card || this.e.user_id, + user_id: this.e.user_id, + } + }) + e.reply(Bot.makeForwardMsg(images)); + } else { + // 视频:https://www.kuaishou.com/short-video/3xhjgcmir24m4nm + const url = resp.data.data.url; + this.downloadVideo(url).then(path => { + e.reply(segment.video(path + "/temp.mp4")); + }); + } }); } @@ -1075,6 +1096,60 @@ export class tools extends plugin { return true } + async miyoushe(e) { + let msg = /(?:https?:\/\/)?(m|www)\.miyoushe\.com\/[A-Za-z\d._?%&+\-=\/#]*/.exec(e.msg)[0]; + const id = /\/(\d+)$/.exec(msg)?.[0].replace("\/", ""); + + fetch(MIYOUSHE_ARTICLE.replace("{}", id), { + headers: { + "Accept-Encoding": "gzip, deflate, br", + "Accept-Language": "zh-cn", + "Connection": "keep-alive", + "Host": "api-takumi.mihoyo.com", + "x-rpc-app_version": "2.11.0", + "x-rpc-client_type": "4", + "Referer": "https://bbs.mihoyo.com/", + "DS": getDS(), + } + }).then(async resp => { + const respJson = await resp.json(); + const data = respJson.data.post.post; + // 分别获取:封面、主题、内容、图片 + const { cover, subject, content, images, structured_content } = data; + let realContent = ""; + // safe JSON.parse + try { + realContent = JSON.parse(content); + } catch (e) { + realContent = content; + } + const normalMsg = `识别:米游社,${ subject }\n${ realContent }`; + const replyMsg = cover ? [segment.image(cover), normalMsg] : normalMsg; + e.reply(replyMsg); + // 视频 + if (structured_content) { + const sc = JSON.parse(structured_content); + const resolutions = sc?.[1].insert.vod.resolutions; + // 暂时选取分辨率较低的video进行解析 + const videoUrl = resolutions[0].url; + this.downloadVideo(videoUrl).then(path => { + e.reply(segment.video(path + "/temp.mp4")); + }); + } + // 这个判断防止发送重复图片 + if (images && images.length > 1) { + const replyImages = images.map(item => { + return { + message: segment.image(item), + nickname: this.e.sender.card || this.e.user_id, + user_id: this.e.user_id, + } + }); + e.reply(Bot.makeForwardMsg(replyImages)); + } + }) + } + /** * 哔哩哔哩下载 * @param title @@ -1111,58 +1186,6 @@ export class tools extends plugin { }); } - /** - * 下载一张网络图片(自动以url的最后一个为名字) - * @param img - * @param dir - * @param fileName - * @param isProxy - * @returns {Promise} - */ - async downloadImg(img, dir, fileName = "", isProxy = false) { - if (fileName === "") { - fileName = img.split("/").pop(); - } - const filepath = `${ dir }/${ fileName }`; - await mkdirIfNotExists(dir) - const writer = fs.createWriteStream(filepath); - const axiosConfig = { - headers: { - "User-Agent": - "Mozilla/5.0 (Linux; Android 5.0; SM-G900P Build/LRX21T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.25 Mobile Safari/537.36", - }, - responseType: "stream", - }; - - if (isProxy) { - axiosConfig.httpAgent = tunnel.httpOverHttp({ - proxy: { host: this.proxyAddr, port: this.proxyPort }, - }); - axiosConfig.httpsAgent = tunnel.httpOverHttp({ - proxy: { host: this.proxyAddr, port: this.proxyPort }, - }); - } - try { - const res = await axios.get(img, axiosConfig); - res.data.pipe(writer); - - return new Promise((resolve, reject) => { - writer.on("finish", () => { - writer.close(() => { - resolve(filepath); - }); - }); - writer.on("error", err => { - fs.unlink(filepath, () => { - reject(err); - }); - }); - }); - } catch (err) { - logger.error("图片下载失败"); - } - } - /** * douyin 请求参数 * @param url diff --git a/config/help.yaml b/config/help.yaml index a9774b7..44c14ba 100644 --- a/config/help.yaml +++ b/config/help.yaml @@ -56,6 +56,12 @@ - icon: kuaishou title: "快手(测试阶段)" desc: 快手分享实时下载 + - icon: xigua + title: "西瓜(测试阶段)" + desc: 西瓜分享实时下载 + - icon: miyoushe + title: "米游社" + desc: 米游社文章分享实时下载 - icon: literature title: "论文" desc: SCI论文实时解析 diff --git a/config/version.yaml b/config/version.yaml index ca7c2ed..5fdca3e 100644 --- a/config/version.yaml +++ b/config/version.yaml @@ -1,7 +1,8 @@ - { - version: 1.3.5, + version: 1.4.0, data: [ + 新增米游社解析功能, 新增🍉解析功能, 新增油管解析功能, 新增小红书无水印下载功能, diff --git a/constants/tools.js b/constants/tools.js index a8858ef..d6ef3cd 100644 --- a/constants/tools.js +++ b/constants/tools.js @@ -40,6 +40,13 @@ export const BILI_VIDEO_INFO = "http://api.bilibili.com/x/web-interface/view" */ export const BILI_NAV = "https://api.bilibili.com/x/web-interface/nav" +/** + * 米游社网页端获取文章 + * https://github.com/UIGF-org/mihoyo-api-collect/blob/main/hoyolab/article/article.md#%E8%8E%B7%E5%8F%96%E5%AE%8C%E6%95%B4%E6%96%87%E7%AB%A0%E4%BF%A1%E6%81%AF + * @type {string} + */ +export const MIYOUSHE_ARTICLE = "https://bbs-api.miyoushe.com/post/wapi/getPostFull?post_id={}" + /** * 视频请求链接CDN * @type {string} diff --git a/resources/img/icon/miyoushe.png b/resources/img/icon/miyoushe.png new file mode 100644 index 0000000..ef94bbf Binary files /dev/null and b/resources/img/icon/miyoushe.png differ diff --git a/resources/img/icon/xigua.png b/resources/img/icon/xigua.png new file mode 100644 index 0000000..7ec24e9 Binary files /dev/null and b/resources/img/icon/xigua.png differ diff --git a/utils/common.js b/utils/common.js index 54c2a6c..a44817b 100644 --- a/utils/common.js +++ b/utils/common.js @@ -14,6 +14,7 @@ export class jFetch { const r = await fetch(url); return await r.json(); } + async post(url, params) { const r = await fetch(url, { ...params, method: "POST" }); return await r.json(); @@ -64,7 +65,7 @@ export function retry(func, maxRetries = 3, delay = 1000) { if (remainingTries === 1) { reject(error); } else { - console.log(`错误: ${error}. 重试将在 ${delay/1000} 秒...`); + console.log(`错误: ${ error }. 重试将在 ${ delay / 1000 } 秒...`); setTimeout(() => attempt(remainingTries - 1), delay); } }); @@ -79,7 +80,7 @@ export function retry(func, maxRetries = 3, delay = 1000) { * @param filename * @returns {Promise} */ -export function downloadPDF (url, filename) { +export function downloadPDF(url, filename) { return axios({ url: url, responseType: "stream", @@ -111,7 +112,7 @@ export async function getIdVideo(url) { return idVideo.length > 19 ? idVideo.substring(0, idVideo.indexOf("?")) : idVideo; } -export function generateRandomStr(randomlength = 16){ +export function generateRandomStr(randomlength = 16) { const base_str = 'ABCDEFGHIGKLMNOPQRSTUVWXYZabcdefghigklmnopqrstuvwxyz0123456789=' let random_str = '' for (let i = 0; i < randomlength; i++) { @@ -163,6 +164,58 @@ export async function downloadMp3(mp3Url, path, redirect = "manual") { }); } +/** + * 下载一张网络图片(自动以url的最后一个为名字) + * @param img + * @param dir + * @param fileName + * @param isProxy + * @returns {Promise} + */ +export async function downloadImg(img, dir, fileName = "", isProxy = false) { + if (fileName === "") { + fileName = img.split("/").pop(); + } + const filepath = `${ dir }/${ fileName }`; + await mkdirIfNotExists(dir) + const writer = fs.createWriteStream(filepath); + const axiosConfig = { + headers: { + "User-Agent": + "Mozilla/5.0 (Linux; Android 5.0; SM-G900P Build/LRX21T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.25 Mobile Safari/537.36", + }, + responseType: "stream", + }; + + if (isProxy) { + axiosConfig.httpAgent = tunnel.httpOverHttp({ + proxy: { host: this.proxyAddr, port: this.proxyPort }, + }); + axiosConfig.httpsAgent = tunnel.httpOverHttp({ + proxy: { host: this.proxyAddr, port: this.proxyPort }, + }); + } + try { + const res = await axios.get(img, axiosConfig); + res.data.pipe(writer); + + return new Promise((resolve, reject) => { + writer.on("finish", () => { + writer.close(() => { + resolve(filepath); + }); + }); + writer.on("error", err => { + fs.unlink(filepath, () => { + reject(err); + }); + }); + }); + } catch (err) { + logger.error("图片下载失败"); + } +} + /** * 千位数的数据处理 * @param data @@ -178,7 +231,7 @@ const dataProcessing = data => { * @return {string} */ export function formatBiliInfo(data) { - return Object.keys(data).map(key => `${key}:${dataProcessing(data[key])}`).join(' | '); + return Object.keys(data).map(key => `${ key }:${ dataProcessing(data[key]) }`).join(' | '); } /** @@ -197,7 +250,7 @@ export function secondsToTime(seconds) { // return `${pad(minutes, 2)}:${pad(secs, 2)}`; // 完整的 HH:MM:SS 格式 - return `${pad(hours, 2)}:${pad(minutes, 2)}:${pad(secs, 2)}`; + return `${ pad(hours, 2) }:${ pad(minutes, 2) }:${ pad(secs, 2) }`; } /** diff --git a/utils/mihoyo.js b/utils/mihoyo.js new file mode 100644 index 0000000..1195107 --- /dev/null +++ b/utils/mihoyo.js @@ -0,0 +1,14 @@ +import md5 from 'md5'; + +export const getDS = () => { + const salt = "ZSHlXeQUBis52qD1kEgKt5lUYed4b7Bb"; + const lettersAndNumbers = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789' + + const i = Math.floor(Date.now() / 1000); + let r = "" + for (let i; i < 6; i++) { + r += lettersAndNumbers[Math.floor(Math.random() * lettersAndNumbers.length)] + } + const c = md5(`salt=${ salt }&t=${ i }&r=${ r }`); + return `${ i },${ r },${ c }`; +} \ No newline at end of file