Skip to content

Commit 15ca917

Browse files
authored
Merge pull request #203 from taiyme/merge-upstream
2 parents 5b31fea + fd55f35 commit 15ca917

21 files changed

+185
-71
lines changed

CHANGELOG.md

+8
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,8 @@
3535
- Enhance: リプライにて引用がある場合テキストが空でもノートできるように
3636
- 引用したいノートのURLをコピーしリプライ投稿画面にペーストして添付することで達成できます
3737
- Enhance: フォローするかどうかの確認ダイアログを出せるように
38+
- Enhance: Playを手動でリロードできるように
39+
- Enhance: 通報のコメント内のリンクをクリックした際、ウィンドウで開くように
3840
- Chore: AiScriptを0.18.0にバージョンアップ
3941
- Fix: 一部のページ内リンクが正しく動作しない問題を修正
4042
- Fix: 周年の実績が閏年を考慮しない問題を修正
@@ -50,6 +52,10 @@
5052
- Fix: ノート詳細ページにおいてCW付き引用リノートのCWボタンのラベルに「引用」が含まれていない問題を修正
5153
- Fix: ダイアログの入力で字数制限に違反していてもEnterキーが押せてしまう問題を修正
5254
- Fix: ダイレクト投稿の宛先が保存されない問題を修正
55+
- Fix: Playのページを離れたときに、Playが正常に初期化されない問題を修正
56+
- Fix: ページのOGP URLが間違っているのを修正
57+
- Fix: リバーシの対局を正しく共有できないことがある問題を修正
58+
- Fix: 通知をグループ化している際に、人数が正常に表示されないことがある問題を修正
5359

5460
### Server
5561
- Enhance: エンドポイント`antennas/update`の必須項目を`antennaId`のみに
@@ -62,10 +68,12 @@
6268
- Fix: リプライのみの引用リノートと、CWのみの引用リノートが純粋なリノートとして誤って扱われてしまう問題を修正
6369
- Fix: 登録にメール認証が必須になっている場合、登録されているメールアドレスを削除できないように
6470
(Cherry-picked from https://github.com/MisskeyIO/misskey/pull/606)
71+
- Fix: Add Cache-Control to Bull Board
6572
- Fix: nginx経由で/files/にRangeリクエストされた場合に正しく応答できないのを修正
6673
- Fix: 一部のタイムラインのストリーミングでインスタンスミュートが効かない問題を修正
6774
- Fix: グローバルタイムラインで返信が表示されないことがある問題を修正
6875
- Fix: リノートをミュートしたユーザの投稿のリノートがミュートされる問題を修正
76+
- Fix: AP Link等は添付ファイル扱いしないようになど (#13754)
6977

7078
## 2024.3.1
7179

packages/backend/src/core/CustomEmojiService.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ import { query } from '@/misc/prelude/url.js';
2020
import type { Serialized } from '@/types.js';
2121
import { ModerationLogService } from '@/core/ModerationLogService.js';
2222

23-
const parseEmojiStrRegexp = /^(\w+)(?:@([\w.-]+))?$/;
23+
const parseEmojiStrRegexp = /^([-\w]+)(?:@([\w.-]+))?$/;
2424

2525
@Injectable()
2626
export class CustomEmojiService implements OnApplicationShutdown {

packages/backend/src/core/MfmService.ts

+5-3
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
import { URL } from 'node:url';
77
import { Inject, Injectable } from '@nestjs/common';
88
import * as parse5 from 'parse5';
9-
import { Window } from 'happy-dom';
9+
import { Window, XMLSerializer } from 'happy-dom';
1010
import { DI } from '@/di-symbols.js';
1111
import type { Config } from '@/config.js';
1212
import { intersperse } from '@/misc/prelude/array.js';
@@ -247,6 +247,8 @@ export class MfmService {
247247

248248
const doc = window.document;
249249

250+
const body = doc.createElement('p');
251+
250252
function appendChildren(children: mfm.MfmNode[], targetElement: any): void {
251253
if (children) {
252254
for (const child of children.map(x => (handlers as any)[x.type](x))) targetElement.appendChild(child);
@@ -457,8 +459,8 @@ export class MfmService {
457459
},
458460
};
459461

460-
appendChildren(nodes, doc.body);
462+
appendChildren(nodes, body);
461463

462-
return `<p>${doc.body.innerHTML}</p>`;
464+
return new XMLSerializer().serializeToString(body);
463465
}
464466
}

packages/backend/src/core/activitypub/models/ApImageService.ts

+10-9
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ import { bindThis } from '@/decorators.js';
1717
import { checkHttps } from '@/misc/check-https.js';
1818
import { ApResolverService } from '../ApResolverService.js';
1919
import { ApLoggerService } from '../ApLoggerService.js';
20-
import type { IObject } from '../type.js';
20+
import { isDocument, type IObject } from '../type.js';
2121

2222
@Injectable()
2323
export class ApImageService {
@@ -39,24 +39,26 @@ export class ApImageService {
3939
* Imageを作成します。
4040
*/
4141
@bindThis
42-
public async createImage(actor: MiRemoteUser, value: string | IObject): Promise<MiDriveFile> {
42+
public async createImage(actor: MiRemoteUser, value: string | IObject): Promise<MiDriveFile | null> {
4343
// 投稿者が凍結されていたらスキップ
4444
if (actor.isSuspended) {
4545
throw new Error('actor has been suspended');
4646
}
4747

4848
const image = await this.apResolverService.createResolver().resolve(value);
4949

50+
if (!isDocument(image)) return null;
51+
5052
if (image.url == null) {
51-
throw new Error('invalid image: url not provided');
53+
return null;
5254
}
5355

5456
if (typeof image.url !== 'string') {
55-
throw new Error('invalid image: unexpected type of url: ' + JSON.stringify(image.url, null, 2));
57+
return null;
5658
}
5759

5860
if (!checkHttps(image.url)) {
59-
throw new Error('invalid image: unexpected schema of url: ' + image.url);
61+
return null;
6062
}
6163

6264
this.logger.info(`Creating the Image: ${image.url}`);
@@ -86,12 +88,11 @@ export class ApImageService {
8688
/**
8789
* Imageを解決します。
8890
*
89-
* Misskeyに対象のImageが登録されていればそれを返し、そうでなければ
90-
* リモートサーバーからフェッチしてMisskeyに登録しそれを返します。
91+
* ImageをリモートサーバーからフェッチしてMisskeyに登録しそれを返します。
9192
*/
9293
@bindThis
93-
public async resolveImage(actor: MiRemoteUser, value: string | IObject): Promise<MiDriveFile> {
94-
// TODO
94+
public async resolveImage(actor: MiRemoteUser, value: string | IObject): Promise<MiDriveFile | null> {
95+
// TODO: Misskeyに対象のImageが登録されていればそれを返す
9596

9697
// リモートサーバーからフェッチしてきて登録
9798
return await this.createImage(actor, value);

packages/backend/src/core/activitypub/models/ApNoteService.ts

+7-10
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44
*/
55

66
import { forwardRef, Inject, Injectable } from '@nestjs/common';
7-
import promiseLimit from 'promise-limit';
87
import { In } from 'typeorm';
98
import { DI } from '@/di-symbols.js';
109
import type { PollsRepository, EmojisRepository } from '@/models/_.js';
@@ -209,15 +208,13 @@ export class ApNoteService {
209208
}
210209

211210
// 添付ファイル
212-
// TODO: attachmentは必ずしもImageではない
213-
// TODO: attachmentは必ずしも配列ではない
214-
const limit = promiseLimit<MiDriveFile>(2);
215-
const files = (await Promise.all(toArray(note.attachment).map(attach => (
216-
limit(() => this.apImageService.resolveImage(actor, {
217-
...attach,
218-
sensitive: note.sensitive, // Noteがsensitiveなら添付もsensitiveにする
219-
}))
220-
))));
211+
const files: MiDriveFile[] = [];
212+
213+
for (const attach of toArray(note.attachment)) {
214+
attach.sensitive ||= note.sensitive; // Noteがsensitiveなら添付もsensitiveにする
215+
const file = await this.apImageService.resolveImage(actor, attach);
216+
if (file) files.push(file);
217+
}
221218

222219
// リプライ
223220
const reply: MiNote | null = note.inReplyTo

packages/backend/src/core/activitypub/type.ts

+6-5
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ export interface IObject {
2525
endTime?: Date;
2626
icon?: any;
2727
image?: any;
28+
mediaType?: string;
2829
url?: ApObject | string;
2930
href?: string;
3031
tag?: IObject | IObject[];
@@ -240,14 +241,14 @@ export interface IKey extends IObject {
240241
}
241242

242243
export interface IApDocument extends IObject {
243-
type: 'Document';
244-
name: string | null;
245-
mediaType: string;
244+
type: 'Audio' | 'Document' | 'Image' | 'Page' | 'Video';
246245
}
247246

248-
export interface IApImage extends IObject {
247+
export const isDocument = (object: IObject): object is IApDocument =>
248+
['Audio', 'Document', 'Image', 'Page', 'Video'].includes(getApType(object));
249+
250+
export interface IApImage extends IApDocument {
249251
type: 'Image';
250-
name: string | null;
251252
}
252253

253254
export interface ICreate extends IActivity {

packages/backend/src/server/web/ClientServerService.ts

+4
Original file line numberDiff line numberDiff line change
@@ -202,6 +202,10 @@ export class ClientServerService {
202202
// %71ueueとかでリクエストされたら困るため
203203
const url = decodeURI(request.routeOptions.url);
204204
if (url === bullBoardPath || url.startsWith(bullBoardPath + '/')) {
205+
if (!url.startsWith(bullBoardPath + '/static/')) {
206+
reply.header('Cache-Control', 'private, max-age=0, must-revalidate');
207+
}
208+
205209
const token = request.cookies.token;
206210
if (token == null) {
207211
reply.code(401).send('Login required');

packages/backend/src/server/web/views/page.pug

+1-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ extends ./base
33
block vars
44
- const user = page.user;
55
- const title = page.title;
6-
- const url = `${config.url}/@${user.username}/${page.name}`;
6+
- const url = `${config.url}/@${user.username}/pages/${page.name}`;
77

88
block title
99
= `${title} | ${instanceName}`

packages/backend/test/unit/MfmService.ts

+6
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,12 @@ describe('MfmService', () => {
3939
const output = '<p>foo <i>bar</i></p>';
4040
assert.equal(mfmService.toHtml(mfm.parse(input)), output);
4141
});
42+
43+
test('escape', () => {
44+
const input = '```\n<p>Hello, world!</p>\n```';
45+
const output = '<p><pre><code>&lt;p&gt;Hello, world!&lt;/p&gt;</code></pre></p>';
46+
assert.equal(mfmService.toHtml(mfm.parse(input)), output);
47+
});
4248
});
4349

4450
describe('fromHtml', () => {

packages/backend/test/unit/activitypub.ts

+19-7
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ import { GlobalModule } from '@/GlobalModule.js';
1717
import { CoreModule } from '@/core/CoreModule.js';
1818
import { FederatedInstanceService } from '@/core/FederatedInstanceService.js';
1919
import { LoggerService } from '@/core/LoggerService.js';
20-
import type { IActor, IApDocument, ICollection, IPost } from '@/core/activitypub/type.js';
20+
import type { IActor, IApDocument, ICollection, IObject, IPost } from '@/core/activitypub/type.js';
2121
import { MiMeta, MiNote } from '@/models/_.js';
2222
import { secureRndstr } from '@/misc/secure-rndstr.js';
2323
import { DownloadService } from '@/core/DownloadService.js';
@@ -295,7 +295,7 @@ describe('ActivityPub', () => {
295295
await createRandomRemoteUser(resolver, personService),
296296
imageObject,
297297
);
298-
assert.ok(!driveFile.isLink);
298+
assert.ok(driveFile && !driveFile.isLink);
299299

300300
const sensitiveImageObject: IApDocument = {
301301
type: 'Document',
@@ -308,7 +308,7 @@ describe('ActivityPub', () => {
308308
await createRandomRemoteUser(resolver, personService),
309309
sensitiveImageObject,
310310
);
311-
assert.ok(!sensitiveDriveFile.isLink);
311+
assert.ok(sensitiveDriveFile && !sensitiveDriveFile.isLink);
312312
});
313313

314314
test('cacheRemoteFiles=false disables caching', async () => {
@@ -324,7 +324,7 @@ describe('ActivityPub', () => {
324324
await createRandomRemoteUser(resolver, personService),
325325
imageObject,
326326
);
327-
assert.ok(driveFile.isLink);
327+
assert.ok(driveFile && driveFile.isLink);
328328

329329
const sensitiveImageObject: IApDocument = {
330330
type: 'Document',
@@ -337,7 +337,7 @@ describe('ActivityPub', () => {
337337
await createRandomRemoteUser(resolver, personService),
338338
sensitiveImageObject,
339339
);
340-
assert.ok(sensitiveDriveFile.isLink);
340+
assert.ok(sensitiveDriveFile && sensitiveDriveFile.isLink);
341341
});
342342

343343
test('cacheRemoteSensitiveFiles=false only affects sensitive files', async () => {
@@ -353,7 +353,7 @@ describe('ActivityPub', () => {
353353
await createRandomRemoteUser(resolver, personService),
354354
imageObject,
355355
);
356-
assert.ok(!driveFile.isLink);
356+
assert.ok(driveFile && !driveFile.isLink);
357357

358358
const sensitiveImageObject: IApDocument = {
359359
type: 'Document',
@@ -366,7 +366,19 @@ describe('ActivityPub', () => {
366366
await createRandomRemoteUser(resolver, personService),
367367
sensitiveImageObject,
368368
);
369-
assert.ok(sensitiveDriveFile.isLink);
369+
assert.ok(sensitiveDriveFile && sensitiveDriveFile.isLink);
370+
});
371+
372+
test('Link is not an attachment files', async () => {
373+
const linkObject: IObject = {
374+
type: 'Link',
375+
href: 'https://example.com/',
376+
};
377+
const driveFile = await imageService.createImage(
378+
await createRandomRemoteUser(resolver, personService),
379+
linkObject,
380+
);
381+
assert.strictEqual(driveFile, null);
370382
});
371383
});
372384
});

packages/frontend/src/components/MkAbuseReport.vue

+1-1
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ SPDX-License-Identifier: AGPL-3.0-only
2020
</div>
2121
<div class="detail">
2222
<div>
23-
<Mfm :text="report.comment"/>
23+
<Mfm :text="report.comment" :linkBehavior="'window'"/>
2424
</div>
2525
<hr/>
2626
<div>{{ i18n.ts.reporter }}: <MkA :to="`/admin/user/${report.reporter.id}`" class="_link" :behavior="'window'">@{{ report.reporter.username }}</MkA></div>

packages/frontend/src/components/MkLink.vue

+3-1
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ SPDX-License-Identifier: AGPL-3.0-only
1313
:rel="rel ?? 'nofollow noopener'"
1414
:target="target"
1515
:title="url"
16+
:behavior="behavior"
1617
>
1718
<slot></slot>
1819
<i v-if="target === '_blank'" class="ti ti-external-link" :class="$style.icon"></i>
@@ -25,11 +26,12 @@ import { url as local } from '@/config.js';
2526
import { useTooltip } from '@/scripts/use-tooltip.js';
2627
import * as os from '@/os.js';
2728
import { isEnabledUrlPreview } from '@/instance.js';
28-
import MkA from '@/components/global/MkA.vue';
29+
import MkA, { type MkABehavior } from '@/components/global/MkA.vue';
2930

3031
const props = withDefaults(defineProps<{
3132
url: string;
3233
rel?: null | string;
34+
behavior?: MkABehavior;
3335
}>(), {
3436
});
3537

packages/frontend/src/components/MkMention.vue

+3-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ SPDX-License-Identifier: AGPL-3.0-only
44
-->
55

66
<template>
7-
<MkA v-user-preview="canonical" :class="[$style.root, { [$style.isMe]: isMe }]" :to="url" :style="{ background: bgCss }">
7+
<MkA v-user-preview="canonical" :class="[$style.root, { [$style.isMe]: isMe }]" :to="url" :style="{ background: bgCss }" :behavior="behavior">
88
<img :class="$style.icon" :src="avatarUrl" alt="">
99
<span>
1010
<span>@{{ username }}</span>
@@ -21,10 +21,12 @@ import { host as localHost } from '@/config.js';
2121
import { $i } from '@/account.js';
2222
import { defaultStore } from '@/store.js';
2323
import { getStaticImageUrl } from '@/scripts/media-proxy.js';
24+
import { type MkABehavior } from '@/components/global/MkA.vue';
2425

2526
const props = defineProps<{
2627
username: string;
2728
host: string;
29+
behavior?: MkABehavior;
2830
}>();
2931

3032
const canonical = props.host === localHost ? `@${props.username}` : `@${props.username}@${toUnicode(props.host)}`;

packages/frontend/src/components/MkNotification.vue

+8-3
Original file line numberDiff line numberDiff line change
@@ -58,8 +58,8 @@ SPDX-License-Identifier: AGPL-3.0-only
5858
<span v-else-if="notification.type === 'achievementEarned'">{{ i18n.ts._notification.achievementEarned }}</span>
5959
<span v-else-if="notification.type === 'test'">{{ i18n.ts._notification.testNotification }}</span>
6060
<MkA v-else-if="notification.type === 'follow' || notification.type === 'mention' || notification.type === 'reply' || notification.type === 'renote' || notification.type === 'quote' || notification.type === 'reaction' || notification.type === 'receiveFollowRequest' || notification.type === 'followRequestAccepted'" v-user-preview="notification.user.id" :class="$style.headerName" :to="userPage(notification.user)"><MkUserName :user="notification.user"/></MkA>
61-
<span v-else-if="notification.type === 'reaction:grouped' && notification.note.reactionAcceptance === 'likeOnly'">{{ i18n.tsx._notification.likedBySomeUsers({ n: notification.reactions.length }) }}</span>
62-
<span v-else-if="notification.type === 'reaction:grouped'">{{ i18n.tsx._notification.reactedBySomeUsers({ n: notification.reactions.length }) }}</span>
61+
<span v-else-if="notification.type === 'reaction:grouped' && notification.note.reactionAcceptance === 'likeOnly'">{{ i18n.tsx._notification.likedBySomeUsers({ n: getActualReactedUsersCount(notification) }) }}</span>
62+
<span v-else-if="notification.type === 'reaction:grouped'">{{ i18n.tsx._notification.reactedBySomeUsers({ n: getActualReactedUsersCount(notification) }) }}</span>
6363
<span v-else-if="notification.type === 'renote:grouped'">{{ i18n.tsx._notification.renotedBySomeUsers({ n: notification.users.length }) }}</span>
6464
<span v-else-if="notification.type === 'app'">{{ notification.header }}</span>
6565
<MkTime v-if="withTime" :time="notification.createdAt" :class="$style.headerTime"/>
@@ -72,7 +72,7 @@ SPDX-License-Identifier: AGPL-3.0-only
7272
</MkA>
7373
<MkA v-else-if="notification.type === 'renote' || notification.type === 'renote:grouped'" :class="$style.text" :to="notePage(notification.note)" :title="getNoteSummary(notification.note.renote)">
7474
<i class="ti ti-quote" :class="$style.quote"></i>
75-
<Mfm :text="getNoteSummary(notification.note.renote)" :plain="true" :nowrap="true" :author="notification.note.renote.user"/>
75+
<Mfm :text="getNoteSummary(notification.note.renote)" :plain="true" :nowrap="true" :author="notification.note.renote?.user"/>
7676
<i class="ti ti-quote" :class="$style.quote"></i>
7777
</MkA>
7878
<MkA v-else-if="notification.type === 'reply'" :class="$style.text" :to="notePage(notification.note)" :title="getNoteSummary(notification.note)">
@@ -174,6 +174,11 @@ const rejectFollowRequest = () => {
174174
followRequestDone.value = true;
175175
misskeyApi('following/requests/reject', { userId: props.notification.user.id });
176176
};
177+
178+
function getActualReactedUsersCount(notification: Misskey.entities.Notification) {
179+
if (notification.type !== 'reaction:grouped') return 0;
180+
return new Set(notification.reactions.map((reaction) => reaction.user.id)).size;
181+
}
177182
</script>
178183

179184
<style lang="scss" module>

packages/frontend/src/components/global/MkA.vue

+5-1
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,10 @@ SPDX-License-Identifier: AGPL-3.0-only
1515
</a>
1616
</template>
1717

18+
<script lang="ts">
19+
export type MkABehavior = 'window' | 'browser' | null;
20+
</script>
21+
1822
<script lang="ts" setup>
1923
import { computed, shallowRef } from 'vue';
2024
import * as os from '@/os.js';
@@ -26,7 +30,7 @@ import { useRouter } from '@/router/supplier.js';
2630
const props = withDefaults(defineProps<{
2731
to: string;
2832
activeClass?: null | string;
29-
behavior?: null | 'window' | 'browser';
33+
behavior?: MkABehavior;
3034
}>(), {
3135
activeClass: null,
3236
behavior: null,

0 commit comments

Comments
 (0)