fix(backend): フィードのノートのMFMはHTMLにレンダーしてから返す (#14006)

* fix(backend): フィードのノートのMFMはHTMLにレンダーしてから返す (test wip)

* chore: beforeEachを使う?

* fix: プレーンテキストにフォールバックしてMFMが含まれていないか調べる方針を実装

* fix: application/jsonだとパースされるのでその作用をキャンセル

* build: fix lint error

* docs: update CHANGELOG.md

---------

Co-authored-by: syuilo <4439005+syuilo@users.noreply.github.com>
This commit is contained in:
Kisaragi 2024-06-22 12:51:02 +09:00 committed by GitHub
parent ef205fb60e
commit ac12ab8629
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 26 additions and 3 deletions

View file

@ -12,6 +12,7 @@
### Server ### Server
- チャート生成時にinstance.suspentionStateに置き換えられたinstance.isSuspendedが参照されてしまう問題を修正 - チャート生成時にinstance.suspentionStateに置き換えられたinstance.isSuspendedが参照されてしまう問題を修正
- Feat: レートリミット制限に引っかかったときに`Retry-After`ヘッダーを返すように (#13949) - Feat: レートリミット制限に引っかかったときに`Retry-After`ヘッダーを返すように (#13949)
- Fix: ユーザーのフィードページのMFMをHTMLに展開するように (#14006)
- Fix: アンテナ・クリップ・リスト・ウェブフックがロールポリシーの上限より一つ多く作れてしまうのを修正 (#14036) - Fix: アンテナ・クリップ・リスト・ウェブフックがロールポリシーの上限より一つ多く作れてしまうのを修正 (#14036)
- Fix: notRespondingSinceが実装される前に不通になったインスタンスが自動的に配信停止にならない (#14059) - Fix: notRespondingSinceが実装される前に不通になったインスタンスが自動的に配信停止にならない (#14059)
- Fix: FTT有効時、タイムライン用エンドポイントで`sinceId`にキャッシュ内最古のものより古いものを指定した場合に正しく結果が返ってこない問題を修正 - Fix: FTT有効時、タイムライン用エンドポイントで`sinceId`にキャッシュ内最古のものより古いものを指定した場合に正しく結果が返ってこない問題を修正

View file

@ -14,6 +14,8 @@ import { UserEntityService } from '@/core/entities/UserEntityService.js';
import { DriveFileEntityService } from '@/core/entities/DriveFileEntityService.js'; import { DriveFileEntityService } from '@/core/entities/DriveFileEntityService.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import { IdService } from '@/core/IdService.js'; import { IdService } from '@/core/IdService.js';
import { MfmService } from "@/core/MfmService.js";
import { parse as mfmParse } from 'mfm-js';
@Injectable() @Injectable()
export class FeedService { export class FeedService {
@ -33,6 +35,7 @@ export class FeedService {
private userEntityService: UserEntityService, private userEntityService: UserEntityService,
private driveFileEntityService: DriveFileEntityService, private driveFileEntityService: DriveFileEntityService,
private idService: IdService, private idService: IdService,
private mfmService: MfmService,
) { ) {
} }
@ -76,13 +79,14 @@ export class FeedService {
id: In(note.fileIds), id: In(note.fileIds),
}) : []; }) : [];
const file = files.find(file => file.type.startsWith('image/')); const file = files.find(file => file.type.startsWith('image/'));
const text = note.text;
feed.addItem({ feed.addItem({
title: `New note by ${author.name}`, title: `New note by ${author.name}`,
link: `${this.config.url}/notes/${note.id}`, link: `${this.config.url}/notes/${note.id}`,
date: this.idService.parse(note.id).date, date: this.idService.parse(note.id).date,
description: note.cw ?? undefined, description: note.cw ?? undefined,
content: note.text ?? undefined, content: text ? this.mfmService.toHtml(mfmParse(text), JSON.parse(note.mentionedRemoteUsers)) ?? undefined : undefined,
image: file ? this.driveFileEntityService.getPublicUrl(file) : undefined, image: file ? this.driveFileEntityService.getPublicUrl(file) : undefined,
}); });
} }

View file

@ -153,6 +153,23 @@ describe('Webリソース', () => {
path: path('nonexisting'), path: path('nonexisting'),
status: 404, status: 404,
})); }));
describe(' has entry such ', () => {
beforeEach(() => {
post(alice, { text: "**a**" })
});
test('MFMを含まない。', async () => {
const content = await simpleGet(path(alice.username), "*/*", undefined, res => res.text());
const _body: unknown = content.body;
// JSONフィードのときは改めて文字列化する
const body: string = typeof (_body) === "object" ? JSON.stringify(_body) : _body as string;
if (body.includes("**a**")) {
throw new Error("MFM shouldn't be included");
}
});
})
}); });
describe.each([{ path: '/api/foo' }])('$path', ({ path }) => { describe.each([{ path: '/api/foo' }])('$path', ({ path }) => {

View file

@ -17,6 +17,7 @@ import { validateContentTypeSetAsActivityPub } from '@/core/activitypub/misc/val
import { entities } from '../src/postgres.js'; import { entities } from '../src/postgres.js';
import { loadConfig } from '../src/config.js'; import { loadConfig } from '../src/config.js';
import type * as misskey from 'misskey-js'; import type * as misskey from 'misskey-js';
import { type Response } from 'node-fetch';
export { server as startServer, jobQueue as startJobQueue } from '@/boot/common.js'; export { server as startServer, jobQueue as startJobQueue } from '@/boot/common.js';
@ -454,7 +455,7 @@ export type SimpleGetResponse = {
type: string | null, type: string | null,
location: string | null location: string | null
}; };
export const simpleGet = async (path: string, accept = '*/*', cookie: any = undefined): Promise<SimpleGetResponse> => { export const simpleGet = async (path: string, accept = '*/*', cookie: any = undefined, bodyExtractor: (res: Response) => Promise<string | null> = _ => Promise.resolve(null)): Promise<SimpleGetResponse> => {
const res = await relativeFetch(path, { const res = await relativeFetch(path, {
headers: { headers: {
Accept: accept, Accept: accept,
@ -482,7 +483,7 @@ export const simpleGet = async (path: string, accept = '*/*', cookie: any = unde
const body = const body =
jsonTypes.includes(res.headers.get('content-type') ?? '') ? await res.json() : jsonTypes.includes(res.headers.get('content-type') ?? '') ? await res.json() :
htmlTypes.includes(res.headers.get('content-type') ?? '') ? new JSDOM(await res.text()) : htmlTypes.includes(res.headers.get('content-type') ?? '') ? new JSDOM(await res.text()) :
null; await bodyExtractor(res);
return { return {
status: res.status, status: res.status,