feat(route): add misskey home timeline (#18244)
* feat(route): add misskey home timeline * Update lib/routes/misskey/home-timeline.ts Co-authored-by: Tony <TonyRL@users.noreply.github.com> * Merge remote-tracking branch 'head/master' * feat(route/misskey): Enhance user timeline with simplifyAuthor parameter and update replyToAuthor in utils ---------
This commit is contained in:
parent
13bd5b063f
commit
72f78e2bfb
|
|
@ -238,6 +238,9 @@ export type Config = {
|
|||
instance?: string;
|
||||
token?: string;
|
||||
};
|
||||
misskey: {
|
||||
accessToken?: string;
|
||||
};
|
||||
mox: {
|
||||
cookie: string;
|
||||
};
|
||||
|
|
@ -669,6 +672,9 @@ const calculateValue = () => {
|
|||
instance: envs.MINIFLUX_INSTANCE || 'https://reader.miniflux.app',
|
||||
token: envs.MINIFLUX_TOKEN || '',
|
||||
},
|
||||
misskey: {
|
||||
accessToken: envs.MISSKEY_ACCESS_TOKEN,
|
||||
},
|
||||
mox: {
|
||||
cookie: envs.MOX_COOKIE,
|
||||
},
|
||||
|
|
|
|||
|
|
@ -0,0 +1,101 @@
|
|||
import { config } from '@/config';
|
||||
import ConfigNotFoundError from '@/errors/types/config-not-found';
|
||||
import { Route, ViewType } from '@/types';
|
||||
import got from '@/utils/got';
|
||||
import { queryToBoolean } from '@/utils/readable-social';
|
||||
import querystring from 'querystring';
|
||||
import utils from './utils';
|
||||
|
||||
export const route: Route = {
|
||||
path: '/timeline/home/:site/:routeParams?',
|
||||
categories: ['social-media'],
|
||||
view: ViewType.SocialMedia,
|
||||
example: '/misskey/timeline/home/misskey.io',
|
||||
parameters: {
|
||||
site: 'instance address, domain only, without `http://` or `https://` protocol header',
|
||||
routeParams: `
|
||||
| Key | Description | Accepted Values | Default |
|
||||
| -------------------- | --------------------------------------- | --------------- | ------- |
|
||||
| limit | Number of notes to return | integer | 10 |
|
||||
| withFiles | Only return notes containing files | 0/1/true/false | false |
|
||||
| withRenotes | Include renotes in the timeline | 0/1/true/false | true |
|
||||
| allowPartial | Allow partial results | 0/1/true/false | true |
|
||||
| simplifyAuthor | Simplify author field in feed items | 0/1/true/false | true |
|
||||
|
||||
Note: If \`withFiles\` is set to true, renotes will not be included in the timeline regardless of the value of \`withRenotes\`.
|
||||
|
||||
Examples:
|
||||
- /misskey/timeline/home/misskey.io/limit=20&withFiles=true
|
||||
- /misskey/timeline/home/misskey.io/withRenotes=false
|
||||
`,
|
||||
},
|
||||
features: {
|
||||
requireConfig: [
|
||||
{
|
||||
name: 'MISSKEY_ACCESS_TOKEN',
|
||||
optional: false,
|
||||
description: `
|
||||
Access token for Misskey API. Requires \`read:account\` access.
|
||||
|
||||
Visit the specified site's settings page to obtain an access token. E.g. https://misskey.io/settings/api
|
||||
`,
|
||||
},
|
||||
],
|
||||
requirePuppeteer: false,
|
||||
antiCrawler: false,
|
||||
supportBT: false,
|
||||
supportPodcast: false,
|
||||
supportScihub: false,
|
||||
},
|
||||
radar: [
|
||||
{
|
||||
source: ['misskey.io'],
|
||||
},
|
||||
],
|
||||
name: 'Home Timeline',
|
||||
maintainers: ['HanaokaYuzu'],
|
||||
handler,
|
||||
description: `
|
||||
::: warning
|
||||
This route is only available for self-hosted instances.
|
||||
:::
|
||||
`,
|
||||
};
|
||||
|
||||
async function handler(ctx) {
|
||||
const access_token = config.misskey.accessToken;
|
||||
if (!access_token) {
|
||||
throw new ConfigNotFoundError('Missing access token for Misskey API. Please set `MISSKEY_ACCESS_TOKEN` environment variable.');
|
||||
}
|
||||
|
||||
const site = ctx.req.param('site');
|
||||
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'.`);
|
||||
}
|
||||
|
||||
// docs on: https://misskey.io/api-doc#tag/notes/operation/notes___timeline
|
||||
const url = `https://${site}/api/notes/timeline`;
|
||||
const routeParams = querystring.parse(ctx.req.param('routeParams'));
|
||||
const response = await got({
|
||||
method: 'post',
|
||||
url,
|
||||
headers: {
|
||||
Authorization: `Bearer ${access_token}`,
|
||||
},
|
||||
json: {
|
||||
limit: Number(routeParams.limit ?? 10),
|
||||
withFiles: queryToBoolean(routeParams.withFiles ?? false),
|
||||
withRenotes: queryToBoolean(routeParams.withRenotes ?? true),
|
||||
allowPartial: queryToBoolean(routeParams.allowPartial ?? true),
|
||||
},
|
||||
});
|
||||
|
||||
const list = response.data;
|
||||
const simplifyAuthor = queryToBoolean(routeParams.simplifyAuthor ?? true);
|
||||
|
||||
return {
|
||||
title: `Home Timeline on ${site}`,
|
||||
link: `https://${site}`,
|
||||
item: utils.parseNotes(list, site, simplifyAuthor),
|
||||
};
|
||||
}
|
||||
|
|
@ -1,10 +1,10 @@
|
|||
import { Route, ViewType, Data } 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';
|
||||
import { Data, Route, ViewType } from '@/types';
|
||||
import { fallback, queryToBoolean } from '@/utils/readable-social';
|
||||
import querystring from 'querystring';
|
||||
import utils from './utils';
|
||||
|
||||
export const route: Route = {
|
||||
path: '/users/notes/:username/:routeParams?',
|
||||
|
|
@ -14,10 +14,11 @@ export const route: Route = {
|
|||
parameters: {
|
||||
username: 'Misskey username in the format of username@instance.domain',
|
||||
routeParams: `
|
||||
| Key | Description | Accepted Values | Default |
|
||||
| ----------- | ---------------------------------- | --------------- | ------- |
|
||||
| withRenotes | Include renotes in the timeline | 0/1/true/false | false |
|
||||
| mediaOnly | Only return posts containing media | 0/1/true/false | false |
|
||||
| Key | Description | Accepted Values | Default |
|
||||
| ----------------- | --------------------------------------- | --------------- | ------- |
|
||||
| withRenotes | Include renotes in the timeline | 0/1/true/false | false |
|
||||
| mediaOnly | Only return posts containing media | 0/1/true/false | false |
|
||||
| simplifyAuthor | Simplify author field in feed items | 0/1/true/false | false |
|
||||
|
||||
Note: \`withRenotes\` and \`mediaOnly\` are mutually exclusive and cannot both be set to true.
|
||||
|
||||
|
|
@ -34,7 +35,7 @@ Examples:
|
|||
supportScihub: false,
|
||||
},
|
||||
name: 'User timeline',
|
||||
maintainers: ['siygle', 'SnowAgar25'],
|
||||
maintainers: ['siygle', 'SnowAgar25', 'HanaokaYuzu'],
|
||||
handler,
|
||||
};
|
||||
|
||||
|
|
@ -51,6 +52,7 @@ async function handler(ctx): Promise<Data> {
|
|||
const routeParams = querystring.parse(ctx.req.param('routeParams'));
|
||||
const withRenotes = fallback(undefined, queryToBoolean(routeParams.withRenotes), false);
|
||||
const mediaOnly = fallback(undefined, queryToBoolean(routeParams.mediaOnly), false);
|
||||
const simplifyAuthor = fallback(undefined, queryToBoolean(routeParams.simplifyAuthor), false);
|
||||
|
||||
// Check for conflicting parameters
|
||||
if (withRenotes && mediaOnly) {
|
||||
|
|
@ -65,7 +67,7 @@ async function handler(ctx): Promise<Data> {
|
|||
return {
|
||||
title: `User timeline for ${username} on ${site}`,
|
||||
link: `https://${site}/@${pureUsername}`,
|
||||
image: avatarUrl || '',
|
||||
item: utils.parseNotes(accountData, site),
|
||||
image: avatarUrl ?? '',
|
||||
item: utils.parseNotes(accountData, site, simplifyAuthor),
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,24 +1,24 @@
|
|||
import { getCurrentPath } from '@/utils/helpers';
|
||||
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';
|
||||
import { parseDate } from '@/utils/parse-date';
|
||||
import { art } from '@/utils/render';
|
||||
import path from 'node:path';
|
||||
|
||||
import { MisskeyNote, MisskeyUser } from './types';
|
||||
|
||||
const allowSiteList = ['misskey.io', 'madost.one', 'mk.nixnet.social'];
|
||||
|
||||
const parseNotes = (data: MisskeyNote[], site: string) =>
|
||||
const parseNotes = (data: MisskeyNote[], site: string, simplifyAuthor: boolean = false) =>
|
||||
data.map((item: MisskeyNote) => {
|
||||
const isRenote = item.renote && Object.keys(item.renote).length > 0;
|
||||
const isReply = item.reply && Object.keys(item.reply).length > 0;
|
||||
const noteToUse: MisskeyNote = isRenote ? (item.renote as MisskeyNote) : item;
|
||||
|
||||
const host = noteToUse.user.host ?? site;
|
||||
const author = `${noteToUse.user.name} (${noteToUse.user.username}@${host})`;
|
||||
const author = simplifyAuthor ? String(noteToUse.user.name) : `${noteToUse.user.name} (${noteToUse.user.username}@${host})`;
|
||||
|
||||
const description = art(path.join(__dirname, 'templates/note.art'), {
|
||||
text: noteToUse.text,
|
||||
|
|
@ -30,7 +30,7 @@ const parseNotes = (data: MisskeyNote[], site: string) =>
|
|||
let title = '';
|
||||
if (isReply && item.reply) {
|
||||
const replyToHost = item.reply.user.host ?? site;
|
||||
const replyToAuthor = `${item.reply.user.name} (${item.reply.user.username}@${replyToHost})`;
|
||||
const replyToAuthor = simplifyAuthor ? item.reply.user.name : `${item.reply.user.name} (${item.reply.user.username}@${replyToHost})`;
|
||||
title = `Reply to ${replyToAuthor}: "${noteToUse.text ?? ''}"`;
|
||||
} else if (isRenote) {
|
||||
title = `Renote: ${author}: "${noteToUse.text ?? ''}"`;
|
||||
|
|
|
|||
Loading…
Reference in New Issue