diff --git a/docs/social-media.md b/docs/social-media.md index d86729714f..7a5051d001 100644 --- a/docs/social-media.md +++ b/docs/social-media.md @@ -1109,6 +1109,7 @@ rule | heightOfPics | 微博配图高(生效取决于阅读器) | 不指定 / 数字 | 不指定 | | sizeOfAuthorAvatar | 作者头像大小 | 数字 | 48 | | displayVideo | 是否直接显示微博视频,只在博主 RSS 中有效 | 0/1/true/false | true | +| showEmojiInDescription | 是否展示正文中的 emoji 表情 | 0/1/true/false | true | 指定更多与默认值不同的参数选项可以改善 RSS 的可读性,如 @@ -1149,13 +1150,7 @@ rule ### 个人时间线 -::: warning 注意 - -个人时间线暂不支持改善可读性。 - -::: - - + ::: warning 注意 diff --git a/lib/router.js b/lib/router.js index a5058aab87..c4c0850469 100644 --- a/lib/router.js +++ b/lib/router.js @@ -2173,7 +2173,7 @@ router.get('/reuters/channel/:site/:channel', lazyloadRouteHandler('./routes/reu router.get('/pgyer/:app?', lazyloadRouteHandler('./routes/pgyer/app')); // 微博个人时间线 -router.get('/weibo/timeline/:uid/:feature?', lazyloadRouteHandler('./routes/weibo/timeline')); +router.get('/weibo/timeline/:uid/:feature?/:routeParams?', lazyloadRouteHandler('./routes/weibo/timeline')); // TAPTAP router.get('/taptap/topic/:id/:label?', lazyloadRouteHandler('./routes/taptap/topic')); diff --git a/lib/routes/weibo/timeline.js b/lib/routes/weibo/timeline.js index 6d96d8630b..d753d4ebbf 100644 --- a/lib/routes/weibo/timeline.js +++ b/lib/routes/weibo/timeline.js @@ -1,12 +1,37 @@ +const querystring = require('querystring'); const got = require('@/utils/got'); +const { parseDate } = require('@/utils/parse-date'); const config = require('@/config').value; +const weiboUtils = require('./utils'); +const { fallback, queryToBoolean } = require('@/utils/readable-social'); module.exports = async (ctx) => { const uid = ctx.params.uid; const feature = ctx.params.feature || 0; + const routeParams = ctx.params.routeParams || undefined; const token = await ctx.cache.get('weibotimelineuid' + uid, false); + let displayVideo = '1'; + if (routeParams) { + if (routeParams === '1' || routeParams === '0') { + displayVideo = routeParams; + } else { + const routeParams = querystring.parse(ctx.params.routeParams); + displayVideo = fallback(undefined, queryToBoolean(routeParams.displayVideo), true) ? '1' : '0'; + } + } if (token) { + const containerResponse = await got({ + method: 'get', + url: `https://m.weibo.cn/api/container/getIndex?type=uid&value=${uid}`, + headers: { + Referer: 'https://m.weibo.cn/', + }, + }); + const name = containerResponse.data.data.userInfo.screen_name; + const description = containerResponse.data.data.userInfo.description; + const profileImageUrl = containerResponse.data.data.userInfo.profile_image_url; + const response = await got.get(`https://api.weibo.com/2/statuses/home_timeline.json?access_token=${token}&count=100&feature=${feature}`); // 检查token失效 if (response.data.error !== undefined) { @@ -16,143 +41,66 @@ module.exports = async (ctx) => { ctx.set({ 'Cache-Control': 'no-cache', }); - ctx.redirect(`https://api.weibo.com/oauth2/authorize?client_id=${app_key}&redirect_uri=${redirect_url}`); + ctx.redirect(`https://api.weibo.com/oauth2/authorize?client_id=${app_key}&redirect_uri=${redirect_url}${routeParams ? `&state=${routeParams}` : ''}`); } - // 获取所有表情 - const weiboEmotion = await ctx.cache.tryGet('weiboEmotion', async () => { - const tmpEmotionList = {}; - await Promise.all( - ['face', 'ani', 'cartoon'].map(async (type) => { - try { - const rep = await got.get(`https://api.weibo.com/2/emotions.json?access_token=${token}&type=${type}`); - rep.data.forEach((item) => { - tmpEmotionList[item.phrase] = item.url; - }); - return Promise.resolve(rep); - } catch (err) { - // pass - } - }) - ); - return tmpEmotionList; - }); - - const data = await Promise.all( + const resultItem = await Promise.all( response.data.statuses.map(async (item) => { - const realContent = await ctx.cache.tryGet('weibotimelineurl' + item.user.id + item.id, async () => { - const res = []; - const videoList = []; - let picUrlList = []; - picUrlList = picUrlList.concat(item.pic_urls); - try { - const rep = await got.get(`https://m.weibo.cn/${item.user.id}/${item.id}`); - rep.body.replace(/"text": "([\s\S]*?)",/g, (s, value) => { - res.push(value.replace(/\\"/g, "'")); - }); - rep.body.replace(/"mp4_720p_mp4": "([\s\S]*?)",/, (s, value) => { - videoList.push(value); - }); - if (res.length > 1) { - const sub_rep = await got.get(`https://m.weibo.cn/${item.retweeted_status.user.id}/${item.retweeted_status.id}`); - sub_rep.body.replace(/"text": "([\s\S]*?)",/g, (s, value) => { - res[1] = value.replace(/\\"/g, "'"); - }); - sub_rep.body.replace(/"mp4_720p_mp4": "([\s\S]*?)",/, (s, value) => { - videoList.push(value); - }); - picUrlList = picUrlList.concat(item.retweeted_status.pic_urls); - } - } catch (err) { - res.push(item.text); - if (item.retweeted_status) { - res.push(item.retweeted_status.text); - picUrlList = picUrlList.concat(item.retweeted_status.pic_urls); - } - // 非全文获取的微博内容需要替换表情 - res.forEach((content, index) => { - const emotion = []; - content.replace(/(\[.*?\])/g, (s, value) => { - emotion.push(value); - }); - - if (content.length > 0) { - emotion.forEach((item) => { - res[index] = content.replace(new RegExp(`\\${item}`, 'g'), `${item}`); - }); - } - }); - } - let content = ''; - try { - if (res.length >= 2) { - content = `${res[0]}


${item.retweeted_status.user.screen_name}:

${res[1]}
`; - } else if (res.length === 1) { - content = res[0]; - } - - // 手机端reeder查看内容时,表情会导致换行-此处做转换 - content.replace(/([\s\S]*?)<\/span>/g, (s, value) => { - content = content.replace(s, value); - }); - } catch (error) { - // pass - } - - let title = ''; - if (res.length >= 2) { - title = `${item.user.name}--转发微博`; - } else if (item.pic_urls.length > 0) { - title = `${item.user.name}--原创图文微博`; - } else { - title = `${item.user.name}--原创微博`; - } - return { - title, - content, - author: item.user.name, - date: item.created_at, - link: `https://m.weibo.cn/detail/${item.id}`, - pic_urls: picUrlList, - videoList: Array.from(new Set(videoList)), - }; + const key = `weibotimelineurl${item.user.id}${item.id}`; + const value = await ctx.cache.tryGet(key, async () => { + const _data = await weiboUtils.getShowData(uid, item.id); + return JSON.stringify(_data); }); - return Promise.resolve(realContent); + const data = JSON.parse(value); + + // 是否通过api拿到了data + const isDataOK = data?.text; + if (isDataOK) { + item = data; + } + + // 转发的长微博处理 + const retweet = item.retweeted_status; + if (retweet?.isLongText) { + const retweetData = await weiboUtils.getShowData(retweet.user.id, retweet.id); + if (retweetData?.text) { + item.retweeted_status.text = retweetData.text; + } + } + + const link = `https://weibo.com/${uid}/${item.id}`; + + const formatExtended = weiboUtils.formatExtended(ctx, item); + let description = formatExtended.description; + const title = formatExtended.title; + const pubDate = isDataOK ? parseDate(data.created_at) : parseDate(item.created_at); + + // 视频的处理 + if (displayVideo === '1') { + description = weiboUtils.formatVideo(description, item); + } + + return { + title, + description, + link, + pubDate, + author: item.user.screen_name, + }; }) ); ctx.state.data = { - title: '个人微博时间线', - link: 'https://weibo.com', - item: data.map((item) => { - let description = ''; - if (item.content !== null) { - description = item.content; - } - - item.pic_urls.forEach((pic) => { - description += `配图
`; - }); - if (item.videoList.length !== 0) { - item.videoList.forEach((url) => { - description += ``; - }); - } - return { - description: description - .replace(/\b(url|href|src)\b=(['|"])http:/g, '$1=$2https:') - .replace('http://t.cn/', 'https://t.cn/') - .replace('http://m.weibo.', 'https://m.weibo.'), - link: item.link, - pubDate: item.date, - title: item.title, - author: item.author, - }; - }), + title: `个人微博时间线--${name}`, + link: `http://weibo.com/${uid}/`, + description, + image: profileImageUrl, + item: resultItem, }; } else if (uid === '0' || ctx.querystring) { const { app_key = '', redirect_url = ctx.request.origin + '/weibo/timeline/0', app_secret = '' } = config.weibo; - const code = Object.values(ctx.query)[0]; + const code = ctx.query.code; + const routeParams = ctx.query.state; if (code) { const rep = await got.post(`https://api.weibo.com/oauth2/access_token?client_id=${app_key}&client_secret=${app_secret}&code=${code}&redirect_uri=${redirect_url}&grant_type=authorization_code`); const token = rep.data.access_token; @@ -164,7 +112,7 @@ module.exports = async (ctx) => { 'Content-Type': 'text/html; charset=UTF-8', 'Cache-Control': 'no-cache', }); - ctx.body = ``; + ctx.body = ``; } } else { const { app_key = '', redirect_url = ctx.request.origin + '/weibo/timeline/0' } = config.weibo; @@ -173,6 +121,6 @@ module.exports = async (ctx) => { ctx.set({ 'Cache-Control': 'no-cache', }); - ctx.redirect(`https://api.weibo.com/oauth2/authorize?client_id=${app_key}&redirect_uri=${redirect_url}`); + ctx.redirect(`https://api.weibo.com/oauth2/authorize?client_id=${app_key}&redirect_uri=${redirect_url}${routeParams ? `&state=${feature}/${routeParams.replace(/&/g, '%26')}` : ''}`); } }; diff --git a/lib/routes/weibo/utils.js b/lib/routes/weibo/utils.js index 4e9d822f49..b032f0839c 100644 --- a/lib/routes/weibo/utils.js +++ b/lib/routes/weibo/utils.js @@ -22,6 +22,7 @@ const weiboUtils = { 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), true), }; params = mergedParams; @@ -41,17 +42,20 @@ const weiboUtils = { widthOfPics, heightOfPics, sizeOfAuthorAvatar, + showEmojiInDescription, } = params; let retweeted = ''; // 长文章的处理 let htmlNewLineUnreplaced = (status.longText && status.longText.longTextContent) || status.text || ''; // 表情图标转换为文字 - htmlNewLineUnreplaced = htmlNewLineUnreplaced.replace(/<\/span>/g, '$1'); + if (!showEmojiInDescription) { + htmlNewLineUnreplaced = htmlNewLineUnreplaced.replace(/<\/span>/g, '$1'); + } // 去掉外部链接的图标 htmlNewLineUnreplaced = htmlNewLineUnreplaced.replace(/.*?<\/span>(.*?)<\/span>/g, '$1'); // 去掉乱七八糟的图标 - htmlNewLineUnreplaced = htmlNewLineUnreplaced.replace(/(.*?)<\/span>/g, ''); + // htmlNewLineUnreplaced = htmlNewLineUnreplaced.replace(/(.*?)<\/span>/g, ''); // 去掉全文 htmlNewLineUnreplaced = htmlNewLineUnreplaced.replace(/全文
/g, '
'); htmlNewLineUnreplaced = htmlNewLineUnreplaced.replace(/全文<\/a>/g, '');