Skip to content

Commit

Permalink
feat(route): add misskey home timeline (#18244)
Browse files Browse the repository at this point in the history
* feat(route): add misskey home timeline

* Update lib/routes/misskey/home-timeline.ts

Co-authored-by: Tony <[email protected]>

* Merge remote-tracking branch 'head/master'

* feat(route/misskey): Enhance user timeline with simplifyAuthor parameter and update replyToAuthor in utils

---------
  • Loading branch information
HanaokaYuzu authored Feb 3, 2025
1 parent 13bd5b0 commit 72f78e2
Show file tree
Hide file tree
Showing 4 changed files with 124 additions and 15 deletions.
6 changes: 6 additions & 0 deletions lib/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -238,6 +238,9 @@ export type Config = {
instance?: string;
token?: string;
};
misskey: {
accessToken?: string;
};
mox: {
cookie: string;
};
Expand Down Expand Up @@ -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,
},
Expand Down
101 changes: 101 additions & 0 deletions lib/routes/misskey/home-timeline.ts
Original file line number Diff line number Diff line change
@@ -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),
};
}
20 changes: 11 additions & 9 deletions lib/routes/misskey/user-timeline.ts
Original file line number Diff line number Diff line change
@@ -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?',
Expand All @@ -14,10 +14,11 @@ export const route: Route = {
parameters: {
username: 'Misskey username in the format of [email protected]',
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.
Expand All @@ -34,7 +35,7 @@ Examples:
supportScihub: false,
},
name: 'User timeline',
maintainers: ['siygle', 'SnowAgar25'],
maintainers: ['siygle', 'SnowAgar25', 'HanaokaYuzu'],
handler,
};

Expand All @@ -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) {
Expand All @@ -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),
};
}
12 changes: 6 additions & 6 deletions lib/routes/misskey/utils.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -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 ?? ''}"`;
Expand Down

0 comments on commit 72f78e2

Please sign in to comment.