diff --git a/lib/routes/misskey/user-timeline.ts b/lib/routes/misskey/user-timeline.ts new file mode 100644 index 00000000000000..851ade81841166 --- /dev/null +++ b/lib/routes/misskey/user-timeline.ts @@ -0,0 +1,42 @@ +import { Route } from '@/types'; +import utils from './utils'; +import { config } from '@/config'; +import ConfigNotFoundError from '@/errors/types/config-not-found'; +import InvalidParameterError from '@/errors/types/invalid-parameter'; + +export const route: Route = { + path: '/users/notes/:username', + categories: ['social-media'], + example: '/misskey/users/notes/support@misskey.io', + parameters: { username: 'misskey username format, like support@misskey.io' }, + features: { + requireConfig: false, + requirePuppeteer: false, + antiCrawler: false, + supportBT: false, + supportPodcast: false, + supportScihub: false, + }, + name: 'User timeline', + maintainers: ['siygle'], + handler, +}; + +async function handler(ctx) { + const username = ctx.req.param('username'); + const [, pureUsername, site] = username.match(/@?(\w+)@(\w+\.\w+)/) || []; + if (!pureUsername || !site) { + throw new InvalidParameterError('Provide a valid Misskey username'); + } + if (!config.feature.allow_user_supply_unsafe_domain && !utils.allowSiteList.includes(site)) { + throw new ConfigNotFoundError(`This RSS is disabled unless 'ALLOW_USER_SUPPLY_UNSAFE_DOMAIN' is set to 'true'.`); + } + + const { accountData } = await utils.getUserTimelineByUsername(pureUsername, site); + + return { + title: `User timeline for ${username} on ${site}`, + link: `https://${site}/@${pureUsername}`, + item: utils.parseNotes(accountData, site), + }; +} diff --git a/lib/routes/misskey/utils.ts b/lib/routes/misskey/utils.ts index 4c8bcfdf0f2ba7..5cf2969546957f 100644 --- a/lib/routes/misskey/utils.ts +++ b/lib/routes/misskey/utils.ts @@ -4,6 +4,8 @@ const __dirname = getCurrentPath(import.meta.url); import { art } from '@/utils/render'; import { parseDate } from '@/utils/parse-date'; import path from 'node:path'; +import cache from '@/utils/cache'; +import got from '@/utils/got'; const allowSiteList = ['misskey.io', 'madost.one', 'mk.nixnet.social']; @@ -28,4 +30,42 @@ const parseNotes = (data, site) => }; }); -export default { parseNotes, allowSiteList }; +async function getUserTimelineByUsername(username, site) { + const searchUrl = `https://${site}/api/users/search-by-username-and-host`; + const cacheUid = `misskey_username/${site}/${username}`; + + const accountId = await cache.tryGet(cacheUid, async () => { + const searchResponse = await got({ + method: 'post', + url: searchUrl, + json: { + username, + host: site, + detail: true, + limit: 1, + }, + }); + const userData = searchResponse.data.find((item) => item.username === username); + + if (userData.length === 0) { + throw new Error(`username ${username} not found`); + } + return userData.id; + }); + + const usernotesUrl = `https://${site}/api/users/notes`; + const usernotesResponse = await got({ + method: 'post', + url: usernotesUrl, + json: { + userId: accountId, + withChannelNotes: true, + limit: 10, + offset: 0, + }, + }); + const accountData = usernotesResponse.data; + return { site, accountId, accountData }; +} + +export default { parseNotes, getUserTimelineByUsername, allowSiteList };