/** * URL 路由分发 */ import Router from '@koa/router'; import NodeRSS from 'rss'; import { RSSKoaContext, RSSKoaState } from '../types'; import config from '../config'; import { DomainNotFoundError, statusToHTML, UserNotFoundError, WeiboCookieInvalidError } from './weibo/weibo'; import { ThrottledError } from './throttler'; import { logger } from './logger'; import { PageInvalidError, parsePageSpec } from './page'; export class UidInvalidError extends Error { constructor(uid: string) { super(`uid: ${uid}`); } } export class DomainInvalidError extends Error { constructor(domain: string) { super(`domain: ${domain}`); } } export const registerRoutes = ( router: Router ) => { router.get('/rss/user/:id', async (ctx) => { const uid = ctx.params['id']; try { // check uid format if (!/^[0-9]{10}$/.test(uid)) { throw new UidInvalidError(uid); } const pageSpec = parsePageSpec(ctx.query.page); // get data let cacheMiss = false; const xmlData = await ctx.cache.memo(async () => { const weiboData = await ctx.weibo.fetchUserLatestWeibo(uid, pageSpec.pages); if (weiboData) { // basic info const feed = new NodeRSS({ site_url: "https://weibo.com/" + uid, feed_url: '', title: weiboData.screenName + '的微博', description: weiboData.description, generator: 'https://github.com/zgq354/weibo-rss', ttl: config.rssTTL, }); // item weiboData.statusList?.forEach((status) => { if (!status) return; feed.item({ title: status.status_title || (status.text ? status.text.replace(/<[^>]+>/g, '').replace(/[\n]/g, '').substr(0, 25) : null), description: statusToHTML(status), url: 'https://weibo.com/' + uid + '/' + status.bid, date: new Date(status.created_at), }); }); cacheMiss = true; return feed.xml(); } }, `xml-${uid}-page-${pageSpec.key}`, config.cacheTTL.rssXml); // send data ctx.set('Content-Type', 'text/xml'); ctx.body = xmlData; // mark hit cache ctx.state.hit = cacheMiss ? 0 : 1; } catch (error) { if (error instanceof UidInvalidError) { ctx.status = 404; ctx.body = `找不到用户,传入 UID 格式有误。uid: ${uid}`; return; } if (error instanceof UserNotFoundError) { ctx.status = 404; ctx.body = `找不到用户,可能用户仅登录可见,不支持订阅。可以通过打开 https://m.weibo.cn/u/:uid 验证(uid: ${uid})`; return; } if (error instanceof PageInvalidError) { ctx.status = 400; ctx.body = `page 参数格式有误。支持单页如 7,范围如 1-10,或列表如 1,3,5。page: ${error.message.replace(/^page:\s*/, '')}`; return; } if (error instanceof WeiboCookieInvalidError) { ctx.status = 401; ctx.body = `微博 Cookie 可能已过期或失效,请更新 WEIBO_COOKIE 后重试。uid: ${uid}`; return; } if (error instanceof ThrottledError) { ctx.status = 503; ctx.body = `暂时无法拉取到数据,请稍后再试。uid: ${uid}`; return; } ctx.status = 500; ctx.body = `未知错误,需管理员检查日志。uid: ${uid}`; logger.error(error); } }); router.get('/api/domain2uid', async (ctx) => { const domain = ctx.request.query['domain'] as string; try { // verify if (!domain || !/^[A-Za-z0-9]{3,20}$/.test(domain)) { throw new DomainInvalidError(domain); } // start fetching let cacheMiss = false; const uid = await ctx.cache.memo( () => { cacheMiss = true; return ctx.weibo.fetchUIDByDomain(domain); }, `dm-${domain}`, config.cacheTTL.apiDomain, ); logger.debug(`domain: ${domain}, uid: ${uid}`); ctx.body = { success: true, uid }; // mark hit cache ctx.state.hit = cacheMiss ? 0 : 1; } catch (error) { if (error instanceof DomainInvalidError || error instanceof DomainNotFoundError) { ctx.status = 404; ctx.body = { success: false, msg: '找不到用户,可能是地址格式不正确', }; return; } if (error instanceof WeiboCookieInvalidError) { ctx.status = 401; ctx.body = { success: false, msg: '微博 Cookie 可能已过期或失效,请更新 WEIBO_COOKIE 后重试', }; return; } logger.error(error); ctx.status = 500; ctx.body = { success: false, msg: '获取数据时发生了错误' }; } }) };