UNPKG

rsshub

Version:
378 lines (376 loc) 20 kB
import { t as config } from "./config-C37vj7VH.mjs"; import { t as logger_default } from "./logger-Czu8UMNd.mjs"; import { t as cache_default } from "./cache-Bo__VnGm.mjs"; import { t as got_default } from "./got-KxxWdaxq.mjs"; import { t as getPuppeteerPage } from "./puppeteer-DGmvuGvT.mjs"; import { i as queryToInteger, n as queryToBoolean, t as fallback } from "./readable-social-DoIL4WB3.mjs"; import { t as getCookies } from "./puppeteer-utils-BK3JC9qW.mjs"; import { load } from "cheerio"; import querystring from "node:querystring"; //#region lib/routes/weibo/utils.ts var RenewWeiboCookiesError = class extends Error { constructor(message) { super(message); this.name = "RenewWeiboCookiesError"; } }; const weiboUtils = { apiHeaders: { "MWeibo-Pwa": 1, "X-Requested-With": "XMLHttpRequest", "User-Agent": "Mozilla/5.0 (iPhone; CPU iPhone OS 11_0 like Mac OS X) AppleWebKit/604.1.38 (KHTML, like Gecko) Version/11.0 Mobile/15A372 Safari/604.1" }, RenewWeiboCookiesError, getCookies: async (renew = false) => { if (config.weibo.cookies) { if (renew) throw new Error("Cookies expired. Please update WEIBO_COOKIES"); return config.weibo.cookies; } const cacheKey = "weibo:guest-cookies"; if (renew) cache_default.set(cacheKey, "", 1); return await cache_default.tryGet(cacheKey, async () => { const url = "https://m.weibo.cn/"; if (renew) logger_default.warn(`Renewing visitor Cookies from ${url}`); else logger_default.info(`Fetching visitor Cookies from ${url}`); let times = 0; const { page, destory } = await getPuppeteerPage(url, { onBeforeLoad: async (page$1) => { const expectResourceTypes = new Set([ "document", "script", "xhr", "fetch" ]); await page$1.setUserAgent(weiboUtils.apiHeaders["User-Agent"]); await page$1.setRequestInterception(true); page$1.on("request", (request) => { if (!expectResourceTypes.has(request.resourceType()) || times >= 2) { request.abort(); return; } if (request.url().startsWith(url)) times++; request.continue(); }); }, gotoConfig: { waitUntil: "networkidle0" } }); const cookies = await getCookies(page, "weibo.cn"); await destory(); if (times < 2) throw new Error(`Unexpected redirection. Last URL: ${page.url()}`); if (!cookies) throw new Error(`Unable to fetch visitor cookies. Please set WEIBO_COOKIES. Last URL: ${page.url()}`); return cookies; }); }, tryWithCookies: (() => { let errors = 0; return async (callback) => { try { return await callback(await weiboUtils.getCookies(false)); } catch (error) { if (error.message?.includes("WEIBO_COOKIES")) throw error; if (errors > 10) { logger_default.warn(`Too many errors while fetching data from weibo API, renewing Cookies: ${error.message}`); logger_default.info("Please open an issue on GitHub if renewing Cookies fixes the error"); } else if ((error.name === "HTTPError" || error.name === "FetchError") && error.status === 432) {} else if (error.name === "RenewWeiboCookiesError") {} else { errors++; throw error; } errors = 0; return await callback(await weiboUtils.getCookies(true)); } }; })(), formatTitle: (html) => html.replaceAll(/<span class=["']url-icon["']><img\s[^>]*?alt=["']?([^>]+?)["']?\s[^>]*?\/?><\/span>/g, "$1").replaceAll(/<span class=["']url-icon["']>(<img\s[^>]*>)<\/span>/g, "").replaceAll(/<img\s[^<]*>/g, "[图片]").replaceAll(/<[^<]*>/g, "").replaceAll("\n", " ").trim(), formatExtended: (ctx, status, uid, params = {}, picsPrefixes = []) => { const routeParams = querystring.parse(ctx.req.param("routeParams")); params = { readable: fallback(params.readable, queryToBoolean(routeParams.readable), false), authorNameBold: fallback(params.authorNameBold, queryToBoolean(routeParams.authorNameBold), false), showAuthorInTitle: fallback(params.showAuthorInTitle, queryToBoolean(routeParams.showAuthorInTitle), false), showAuthorInDesc: fallback(params.showAuthorInDesc, queryToBoolean(routeParams.showAuthorInDesc), false), showAuthorAvatarInDesc: fallback(params.showAuthorAvatarInDesc, queryToBoolean(routeParams.showAuthorAvatarInDesc), false), showAtBeforeAuthor: fallback(params.showAtBeforeAuthor, null, false), showEmojiForRetweet: fallback(params.showEmojiForRetweet, queryToBoolean(routeParams.showEmojiForRetweet), false), showRetweetTextInTitle: fallback(params.showRetweetTextInTitle, queryToBoolean(routeParams.showRetweetTextInTitle), true), addLinkForPics: fallback(params.addLinkForPics, queryToBoolean(routeParams.addLinkForPics), false), showTimestampInDescription: fallback(params.showTimestampInDescription, queryToBoolean(routeParams.showTimestampInDescription), false), widthOfPics: fallback(params.widthOfPics, queryToInteger(routeParams.widthOfPics), -1), heightOfPics: fallback(params.heightOfPics, queryToInteger(routeParams.heightOfPics), -1), sizeOfAuthorAvatar: fallback(params.sizeOfAuthorAvatar, queryToInteger(routeParams.sizeOfAuthorAvatar), 48), showEmojiInDescription: fallback(params.showEmojiInDescription, queryToInteger(routeParams.showEmojiInDescription), false), showLinkIconInDescription: fallback(params.showLinkIconInDescription, queryToInteger(routeParams.showLinkIconInDescription), true), preferMobileLink: fallback(params.preferMobileLink, queryToBoolean(routeParams.preferMobileLink), false) }; const { readable, authorNameBold, showAuthorInTitle, showAuthorInDesc, showAuthorAvatarInDesc, showAtBeforeAuthor, showEmojiForRetweet, showRetweetTextInTitle, addLinkForPics, showTimestampInDescription, widthOfPics, heightOfPics, sizeOfAuthorAvatar, showEmojiInDescription, showLinkIconInDescription, preferMobileLink } = params; let retweeted = ""; let htmlNewLineUnreplaced = status.longText && status.longText.longTextContent || status.text || ""; if (!showEmojiInDescription) htmlNewLineUnreplaced = htmlNewLineUnreplaced.replaceAll(/<span class=["']?url-icon["']?><img\s[^>]*?alt=["']?([^>]+?)["']?\s[^>]*?\/><\/span>/g, "$1"); if (!showLinkIconInDescription) htmlNewLineUnreplaced = htmlNewLineUnreplaced.replaceAll(/(<a\s[^>]*>)<span class=["']?url-icon["']?><img\s[^>]*><\/span>[^<>]*?<span class=["']?surl-text["']?>([^<>]*?)<\/span><\/a>/g, "$1$2</a>"); const category = htmlNewLineUnreplaced.match(/<span class=["']?surl-text["']?>#([^<>]*?)#<\/span>/g)?.map((e) => e?.match(/#([^#]+)#/)?.[1]); htmlNewLineUnreplaced = htmlNewLineUnreplaced.replaceAll("全文<br>", "<br>"); htmlNewLineUnreplaced = htmlNewLineUnreplaced.replaceAll(/<a href="(.*?)">全文<\/a>/g, ""); htmlNewLineUnreplaced = htmlNewLineUnreplaced.replaceAll(/"https:\/\/weibo\.cn\/sinaurl.*?[&?]u=(http.*?)"/g, (match, p1) => `"${decodeURIComponent(p1)}"`); htmlNewLineUnreplaced = htmlNewLineUnreplaced.replaceAll(/<a\s+href="https?:\/\/[^"]+\.(jpg|png|gif)"/g, (match) => `${match} data-rsshub-image="href"`); let html = htmlNewLineUnreplaced.replaceAll("\n", "<br>"); if (showAuthorInDesc) { let usernameAndAvatar = `<a href="https://weibo.com/${status.user.id}" target="_blank">`; if (showAuthorAvatarInDesc) usernameAndAvatar += `<img width="${sizeOfAuthorAvatar}" height="${sizeOfAuthorAvatar}" src="${status.user.profile_image_url}" ${readable ? "hspace=\"8\" vspace=\"8\" align=\"left\"" : ""} />`; let name = status.user.screen_name; if (showAtBeforeAuthor) name = "@" + name; usernameAndAvatar += authorNameBold ? `<strong>${name}</strong></a>:&ensp;` : `${name}</a>:&ensp;`; html = usernameAndAvatar + html; } if (status.pics && !Array.isArray(status.pics) && typeof status.pics === "object") status.pics = Object.values(status.pics); if (status.page_info && status.page_info.type === "article" && status.page_info.page_pic && status.page_info.page_pic.url) { const pagePic = { large: { url: status.page_info.page_pic.url } }; if (status.pics) status.pics.push(pagePic); else status.pics = [pagePic]; } const livePhotoCount = status.pics ? status.pics.filter((pic) => pic.type === "livephoto").length : 0; const pics = status.pics && status.pics.filter((pic) => pic.type !== "livephoto"); if (pics) { if (readable) html += "<br clear=\"both\" /><div style=\"clear: both\"></div>"; let picsPrefix = ""; for (const item of pics) picsPrefix += `<img width="0" height="0" hidden="true" src="${item.large.url}">`; picsPrefixes.push(picsPrefix); for (const item of pics) { if (addLinkForPics) html += "<a href=\"" + item.large.url + "\">"; let style = ""; html += "<img "; html += readable ? "vspace=\"8\" hspace=\"4\"" : ""; if (item.large) { const { geo, url } = item.large; if (geo?.width || widthOfPics >= 0) { const width = geo?.width || widthOfPics; html += ` width="${width}"`; style += `width: ${width}px;`; } if (geo?.height || heightOfPics >= 0) { const height = geo?.height || heightOfPics; html += ` height="${height}"`; style += `height: ${height}px;`; } html += ` style="${style}" src="${url}">`; } if (addLinkForPics) html += "</a>"; htmlNewLineUnreplaced += "<img src=\"\" />"; } } if (status.retweeted_status) { html += readable ? `<br clear="both" /><div style="clear: both"></div><blockquote style="background: #80808010;border-top:1px solid #80808030;border-bottom:1px solid #80808030;margin:0;padding:5px 20px;">` : `<br><blockquote> - 转发 `; if (!status.retweeted_status.user) status.retweeted_status.user = { profile_image_url: "", screen_name: "[原微博不可访问]", id: "sorry" }; const retweetedParams = Object.assign({}, params); retweetedParams.showAuthorInDesc = true; retweetedParams.showAuthorAvatarInDesc = false; retweetedParams.showAtBeforeAuthor = true; retweeted += weiboUtils.formatExtended(ctx, status.retweeted_status, void 0, retweetedParams, picsPrefixes).description; html += retweeted; if (readable) html += `<br><small>原博:<a href="https://weibo.com/${status.retweeted_status.user.id}/${status.retweeted_status.bid}" target="_blank" rel="noopener noreferrer">https://weibo.com/${status.retweeted_status.user.id}/${status.retweeted_status.bid}</a></small>`; if (showTimestampInDescription) html += `<br><small>` + new Date(status.retweeted_status.created_at).toLocaleString() + `</small>`; if (readable) html += `<br clear="both" /><div style="clear: both"></div>`; html += "</blockquote>"; } if (showAuthorInDesc && showAuthorAvatarInDesc) html = picsPrefixes.join("") + html; let title = ""; if (showAuthorInTitle) title += status.user.screen_name + ": "; if (!status.retweeted_status || showRetweetTextInTitle) title += weiboUtils.formatTitle(htmlNewLineUnreplaced); if (status.retweeted_status) { title += showEmojiForRetweet ? "🔁 " : " - 转发 "; title += weiboUtils.formatTitle(retweeted); } if (livePhotoCount > 0) { title += " "; title += Array.from({ length: livePhotoCount + 1 }).join("[Live Photo]"); } if (status.page_info && status.page_info === "video") title += " [视频]"; uid = uid || status.user?.id; const bid = status.bid || status.id; const guid = uid ? `https://weibo.com/${uid}/${bid}` : `https://m.weibo.cn/status/${bid}`; const link = preferMobileLink ? `https://m.weibo.cn/status/${bid}` : guid; const author = [{ name: status.user?.screen_name, url: `https://weibo.com/${uid}`, avatar: status.user?.avatar_hd }]; const pubDate = status.created_at; return { description: html, title, link, guid, author, pubDate, category }; }, getShowData: async (uid, bid) => { const link = `https://m.weibo.cn/statuses/show?id=${bid}`; return (await got_default.get(link, { headers: { Referer: `https://m.weibo.cn/u/${uid}`, ...weiboUtils.apiHeaders } })).data.data; }, formatVideo: (itemDesc, status) => { const pageInfo = status.page_info; const livePhotos = status.pics && status.pics.filter((pic) => pic.type === "livephoto" && pic.videoSrc); let video = "<br clear=\"both\" /><div style=\"clear: both\"></div>"; let anyVideo = false; if (livePhotos) for (const livePhoto of livePhotos) { video += `<video controls="controls" poster="${livePhoto.large && livePhoto.large.url || livePhoto.url}" src="${livePhoto.videoSrc}" style="width: 100%"></video>`; anyVideo = true; } if (pageInfo && pageInfo.type === "video") { const pagePic = pageInfo.page_pic; const posterUrl = pagePic ? pagePic.url : ""; const pageUrl = pageInfo.page_url; const mediaInfo = pageInfo.media_info || {}; const urls = pageInfo.urls || {}; const video720p = urls.mp4_720p_mp4 || mediaInfo.mp4_720p_mp4 || ""; const videoHd = urls.mp4_hd_mp4 || mediaInfo.mp4_hd_url || mediaInfo.stream_url_hd || ""; const videoHdHevc = urls.hevc_mp4_hd || ""; const videoLd = urls.mp4_ld_mp4 || mediaInfo.mp4_sd_url || mediaInfo.stream_url || ""; if (video720p || videoHd || videoHdHevc || videoLd) { video += `<video controls="controls" poster="${posterUrl}" style="width: 100%">`; if (video720p) video += `<source src="${video720p}">`; if (videoHd) video += `<source src="${videoHd}">`; if (videoHdHevc) video += `<source src="${videoHdHevc}">`; if (videoLd) video += `<source src="${videoLd}">`; if (pageUrl) video += `<p>视频无法显示,请前往<a href="${pageUrl}" target="_blank" rel="noopener noreferrer">微博视频</a>观看。</p>`; video += "</video>"; anyVideo = true; } } if (anyVideo) itemDesc += video; return itemDesc; }, formatArticle: async (ctx, itemDesc, status) => { const pageInfo = status.page_info; if (pageInfo && pageInfo.type === "article" && pageInfo.page_url) { const articleIdMatch = pageInfo.page_url.match(/id=(\d+)/); if (!articleIdMatch) return itemDesc; const articleId = articleIdMatch[1]; const link = `https://card.weibo.com/article/m/aj/detail?id=${articleId}`; const article = (await cache_default.tryGet(link, async () => { return (await got_default.get(link, { headers: { Referer: `https://card.weibo.com/article/m/show/id/${articleId}`, ...weiboUtils.apiHeaders } })).data; })).data; if (article && article.title && article.content) { const title = article.title; const content = article.content; const summary = article.summary; const createAt = article.create_at; const readCount = article.read_count; const isOriginal = article.is_original; const isArticleNonFree = article.is_article_free; let html = "<br clear=\"both\" /><br clear=\"both\" />"; html += "<div style=\"clear: both\"></div><div style=\"background: #fff;border:5px solid #80808030;margin:0;padding:3% 5%;overflow-wrap: break-word\">"; html += `<h1 style="font-size: 1.5rem;line-height: 1.25;color: #333;">${title}</h1>`; const iconStyle = "display: inline-block;margin-inline: 0.25rem;width: 2.25rem; height: 1.125rem; background: #eee; border-radius: 2px; box-sizing: border-box; text-align: center; line-height: 1.0625rem; font-size: 0.75rem; color: #aaa;"; let articleMeta = "<p style=\"line-height: 1.66; color: #999;margin: 0 0 0.75rem;font-size: 0.75rem;padding: 0\">"; if (isArticleNonFree) articleMeta += `<span style="${iconStyle}">试读</span> `; if (isOriginal) articleMeta += `<span style="${iconStyle}">原创</span> `; articleMeta += `<span style="margin-inline: 0.25rem;">发布时间: ${createAt}</span> `; articleMeta += `<span style="margin-inline: 0.25rem;">阅读量: ${readCount}</span> `; articleMeta += "</p>"; html += articleMeta; if (summary) html += `<p style="color: #999;line-height: 1.5rem;padding: 0.0625rem 0 0.875rem;margin: 0">${summary}</p>`; html += "<div style=\"height: 0;border-bottom: 1px dashed #999;margin-bottom: 0.75rem;\"></div>"; const $ = load(content); $("p").each((_, elem) => { elem = $(elem); let style = elem.attr("style") || ""; style = "margin: 0;padding: 0;border: 0;" + style; elem.attr("style", style); }); $(".image").each((_, elem) => { elem = $(elem); let style = elem.attr("style") || ""; style = "display: table;text-align: center;margin-left: auto;margin-right: auto;clear: both;min-width: 50px;" + style; elem.attr("style", style); }); $("img").each((_, elem) => { elem = $(elem); let style = elem.attr("style") || ""; style = "display: block;max-width: 100%;margin-left: auto;margin-right: auto;min-width: 50px;" + style; elem.attr("style", style); }); const contentHtml = $.html(); html += `<div style="line-height: 1.59;text-align: justify;font-size: 1.0625rem;color: #333;">${contentHtml}</div>`; html += "</div>"; itemDesc += html; } } return itemDesc; }, formatComments: async (ctx, itemDesc, status, showBloggerIcons) => { if (status && status.comments_count && status.id && status.mid) { const id = status.id; const link = `https://m.weibo.cn/comments/hotflow?id=${id}&mid=${status.mid}&max_id_type=0`; const response = await cache_default.tryGet(link, async () => { return (await got_default.get(link, { headers: { Referer: `https://m.weibo.cn/detail/${id}`, ...weiboUtils.apiHeaders } })).data; }); if (response.data && response.data.data) { const comments = response.data.data; itemDesc += `<br clear="both" /><div style="clear: both"></div><div style="background: #80808010;border-top:1px solid #80808030;border-bottom:1px solid #80808030;margin:0;padding:5px 20px;">`; itemDesc += "<h3>热门评论</h3>"; for (const comment of comments) { itemDesc += "<p style=\"margin-bottom: 0.5em;margin-top: 0.5em\">"; let name = comment.user.screen_name; if (showBloggerIcons === "1" && comment.blogger_icons) name += comment.blogger_icons[0].name; itemDesc += `<a href="https://weibo.com/${comment.user.id}" target="_blank">${name}</a>: ${comment.text}`; if ("pic" in comment) itemDesc += `<br><img src="${comment.pic.url}">`; if (comment.comments) { itemDesc += "<blockquote style=\"border-left:0.2em solid #80808080; margin-left: 0.3em; padding-left: 0.5em; margin-bottom: 0.5em; margin-top: 0.25em\">"; for (const com of comment.comments) { const matches = com.text.match(/<a\s+href="https:\/\/weibo\.cn\/sinaurl\?u=([^"]+)"[^>]*><span class='url-icon'><img[^>]*><\/span><span class="surl-text">(查看图片|评论配图|查看动图)<\/span><\/a>/g); if (matches) for (const match of matches) { const hrefMatch = match.match(/href="https:\/\/weibo\.cn\/sinaurl\?u=([^"]+)"/); if (hrefMatch) { const imgTag = `<img src="${decodeURIComponent(hrefMatch[1])}" style="width: 1rem; height: 1rem;">`; com.text = com.text.replaceAll(match, imgTag); } } itemDesc += "<div style=\"font-size: 0.9em\">"; let name$1 = com.user.screen_name; if (showBloggerIcons === "1" && com.blogger_icons) name$1 += com.blogger_icons[0].name; itemDesc += `<a href="https://weibo.com/${com.user.id}" target="_blank">${name$1}</a>: ${com.text}`; itemDesc += "</div>"; } itemDesc += "</blockquote>"; } itemDesc += "</p>"; } itemDesc += "</div>"; } } return itemDesc; }, sinaimgTvax: (() => { const regex = /(?<=\/\/)wx(?=[1-4]\.sinaimg\.cn\/)/gi; const replace = (html) => html.replaceAll(regex, "tvax"); const replaceKV = (obj, keys) => { for (const key of keys) if (obj[key]) obj[key] = replace(obj[key]); }; const dataKeys = ["description", "image"]; const itemKeys = ["description"]; return (data) => { if (data) { replaceKV(data, dataKeys); if (data.item) for (const item of data.item) replaceKV(item, itemKeys); } return data; }; })() }; var utils_default = weiboUtils; //#endregion export { utils_default as t };