diff --git a/CHANGELOG.md b/CHANGELOG.md
index 87db026183b0..68015596bdcc 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -35,6 +35,8 @@
- Enhance: リプライにて引用がある場合テキストが空でもノートできるように
- 引用したいノートのURLをコピーしリプライ投稿画面にペーストして添付することで達成できます
- Enhance: フォローするかどうかの確認ダイアログを出せるように
+- Enhance: Playを手動でリロードできるように
+- Enhance: 通報のコメント内のリンクをクリックした際、ウィンドウで開くように
- Chore: AiScriptを0.18.0にバージョンアップ
- Fix: 一部のページ内リンクが正しく動作しない問題を修正
- Fix: 周年の実績が閏年を考慮しない問題を修正
@@ -50,6 +52,10 @@
- Fix: ノート詳細ページにおいてCW付き引用リノートのCWボタンのラベルに「引用」が含まれていない問題を修正
- Fix: ダイアログの入力で字数制限に違反していてもEnterキーが押せてしまう問題を修正
- Fix: ダイレクト投稿の宛先が保存されない問題を修正
+- Fix: Playのページを離れたときに、Playが正常に初期化されない問題を修正
+- Fix: ページのOGP URLが間違っているのを修正
+- Fix: リバーシの対局を正しく共有できないことがある問題を修正
+- Fix: 通知をグループ化している際に、人数が正常に表示されないことがある問題を修正
### Server
- Enhance: エンドポイント`antennas/update`の必須項目を`antennaId`のみに
@@ -62,10 +68,12 @@
- Fix: リプライのみの引用リノートと、CWのみの引用リノートが純粋なリノートとして誤って扱われてしまう問題を修正
- Fix: 登録にメール認証が必須になっている場合、登録されているメールアドレスを削除できないように
(Cherry-picked from https://github.com/MisskeyIO/misskey/pull/606)
+- Fix: Add Cache-Control to Bull Board
- Fix: nginx経由で/files/にRangeリクエストされた場合に正しく応答できないのを修正
- Fix: 一部のタイムラインのストリーミングでインスタンスミュートが効かない問題を修正
- Fix: グローバルタイムラインで返信が表示されないことがある問題を修正
- Fix: リノートをミュートしたユーザの投稿のリノートがミュートされる問題を修正
+- Fix: AP Link等は添付ファイル扱いしないようになど (#13754)
## 2024.3.1
diff --git a/packages/backend/src/core/CustomEmojiService.ts b/packages/backend/src/core/CustomEmojiService.ts
index edb9335b6ea7..1c75566755ad 100644
--- a/packages/backend/src/core/CustomEmojiService.ts
+++ b/packages/backend/src/core/CustomEmojiService.ts
@@ -20,7 +20,7 @@ import { query } from '@/misc/prelude/url.js';
import type { Serialized } from '@/types.js';
import { ModerationLogService } from '@/core/ModerationLogService.js';
-const parseEmojiStrRegexp = /^(\w+)(?:@([\w.-]+))?$/;
+const parseEmojiStrRegexp = /^([-\w]+)(?:@([\w.-]+))?$/;
@Injectable()
export class CustomEmojiService implements OnApplicationShutdown {
diff --git a/packages/backend/src/core/MfmService.ts b/packages/backend/src/core/MfmService.ts
index 2fb731201bd2..9786f8b8bb7a 100644
--- a/packages/backend/src/core/MfmService.ts
+++ b/packages/backend/src/core/MfmService.ts
@@ -6,7 +6,7 @@
import { URL } from 'node:url';
import { Inject, Injectable } from '@nestjs/common';
import * as parse5 from 'parse5';
-import { Window } from 'happy-dom';
+import { Window, XMLSerializer } from 'happy-dom';
import { DI } from '@/di-symbols.js';
import type { Config } from '@/config.js';
import { intersperse } from '@/misc/prelude/array.js';
@@ -247,6 +247,8 @@ export class MfmService {
const doc = window.document;
+ const body = doc.createElement('p');
+
function appendChildren(children: mfm.MfmNode[], targetElement: any): void {
if (children) {
for (const child of children.map(x => (handlers as any)[x.type](x))) targetElement.appendChild(child);
@@ -457,8 +459,8 @@ export class MfmService {
},
};
- appendChildren(nodes, doc.body);
+ appendChildren(nodes, body);
- return `
${doc.body.innerHTML}
`;
+ return new XMLSerializer().serializeToString(body);
}
}
diff --git a/packages/backend/src/core/activitypub/models/ApImageService.ts b/packages/backend/src/core/activitypub/models/ApImageService.ts
index 89b6ef23d016..369196727014 100644
--- a/packages/backend/src/core/activitypub/models/ApImageService.ts
+++ b/packages/backend/src/core/activitypub/models/ApImageService.ts
@@ -17,7 +17,7 @@ import { bindThis } from '@/decorators.js';
import { checkHttps } from '@/misc/check-https.js';
import { ApResolverService } from '../ApResolverService.js';
import { ApLoggerService } from '../ApLoggerService.js';
-import type { IObject } from '../type.js';
+import { isDocument, type IObject } from '../type.js';
@Injectable()
export class ApImageService {
@@ -39,7 +39,7 @@ export class ApImageService {
* Imageを作成します。
*/
@bindThis
- public async createImage(actor: MiRemoteUser, value: string | IObject): Promise {
+ public async createImage(actor: MiRemoteUser, value: string | IObject): Promise {
// 投稿者が凍結されていたらスキップ
if (actor.isSuspended) {
throw new Error('actor has been suspended');
@@ -47,16 +47,18 @@ export class ApImageService {
const image = await this.apResolverService.createResolver().resolve(value);
+ if (!isDocument(image)) return null;
+
if (image.url == null) {
- throw new Error('invalid image: url not provided');
+ return null;
}
if (typeof image.url !== 'string') {
- throw new Error('invalid image: unexpected type of url: ' + JSON.stringify(image.url, null, 2));
+ return null;
}
if (!checkHttps(image.url)) {
- throw new Error('invalid image: unexpected schema of url: ' + image.url);
+ return null;
}
this.logger.info(`Creating the Image: ${image.url}`);
@@ -86,12 +88,11 @@ export class ApImageService {
/**
* Imageを解決します。
*
- * Misskeyに対象のImageが登録されていればそれを返し、そうでなければ
- * リモートサーバーからフェッチしてMisskeyに登録しそれを返します。
+ * ImageをリモートサーバーからフェッチしてMisskeyに登録しそれを返します。
*/
@bindThis
- public async resolveImage(actor: MiRemoteUser, value: string | IObject): Promise {
- // TODO
+ public async resolveImage(actor: MiRemoteUser, value: string | IObject): Promise {
+ // TODO: Misskeyに対象のImageが登録されていればそれを返す
// リモートサーバーからフェッチしてきて登録
return await this.createImage(actor, value);
diff --git a/packages/backend/src/core/activitypub/models/ApNoteService.ts b/packages/backend/src/core/activitypub/models/ApNoteService.ts
index 4d64b08e15c4..05f7879983d7 100644
--- a/packages/backend/src/core/activitypub/models/ApNoteService.ts
+++ b/packages/backend/src/core/activitypub/models/ApNoteService.ts
@@ -4,7 +4,6 @@
*/
import { forwardRef, Inject, Injectable } from '@nestjs/common';
-import promiseLimit from 'promise-limit';
import { In } from 'typeorm';
import { DI } from '@/di-symbols.js';
import type { PollsRepository, EmojisRepository } from '@/models/_.js';
@@ -209,15 +208,13 @@ export class ApNoteService {
}
// 添付ファイル
- // TODO: attachmentは必ずしもImageではない
- // TODO: attachmentは必ずしも配列ではない
- const limit = promiseLimit(2);
- const files = (await Promise.all(toArray(note.attachment).map(attach => (
- limit(() => this.apImageService.resolveImage(actor, {
- ...attach,
- sensitive: note.sensitive, // Noteがsensitiveなら添付もsensitiveにする
- }))
- ))));
+ const files: MiDriveFile[] = [];
+
+ for (const attach of toArray(note.attachment)) {
+ attach.sensitive ||= note.sensitive; // Noteがsensitiveなら添付もsensitiveにする
+ const file = await this.apImageService.resolveImage(actor, attach);
+ if (file) files.push(file);
+ }
// リプライ
const reply: MiNote | null = note.inReplyTo
diff --git a/packages/backend/src/core/activitypub/type.ts b/packages/backend/src/core/activitypub/type.ts
index b43dddad61d2..09322888d518 100644
--- a/packages/backend/src/core/activitypub/type.ts
+++ b/packages/backend/src/core/activitypub/type.ts
@@ -25,6 +25,7 @@ export interface IObject {
endTime?: Date;
icon?: any;
image?: any;
+ mediaType?: string;
url?: ApObject | string;
href?: string;
tag?: IObject | IObject[];
@@ -240,14 +241,14 @@ export interface IKey extends IObject {
}
export interface IApDocument extends IObject {
- type: 'Document';
- name: string | null;
- mediaType: string;
+ type: 'Audio' | 'Document' | 'Image' | 'Page' | 'Video';
}
-export interface IApImage extends IObject {
+export const isDocument = (object: IObject): object is IApDocument =>
+ ['Audio', 'Document', 'Image', 'Page', 'Video'].includes(getApType(object));
+
+export interface IApImage extends IApDocument {
type: 'Image';
- name: string | null;
}
export interface ICreate extends IActivity {
diff --git a/packages/backend/src/server/web/ClientServerService.ts b/packages/backend/src/server/web/ClientServerService.ts
index b1af0c3df652..ba2f8b432409 100644
--- a/packages/backend/src/server/web/ClientServerService.ts
+++ b/packages/backend/src/server/web/ClientServerService.ts
@@ -202,6 +202,10 @@ export class ClientServerService {
// %71ueueとかでリクエストされたら困るため
const url = decodeURI(request.routeOptions.url);
if (url === bullBoardPath || url.startsWith(bullBoardPath + '/')) {
+ if (!url.startsWith(bullBoardPath + '/static/')) {
+ reply.header('Cache-Control', 'private, max-age=0, must-revalidate');
+ }
+
const token = request.cookies.token;
if (token == null) {
reply.code(401).send('Login required');
diff --git a/packages/backend/src/server/web/views/page.pug b/packages/backend/src/server/web/views/page.pug
index 08bb08ffe7e1..03c50eca8a51 100644
--- a/packages/backend/src/server/web/views/page.pug
+++ b/packages/backend/src/server/web/views/page.pug
@@ -3,7 +3,7 @@ extends ./base
block vars
- const user = page.user;
- const title = page.title;
- - const url = `${config.url}/@${user.username}/${page.name}`;
+ - const url = `${config.url}/@${user.username}/pages/${page.name}`;
block title
= `${title} | ${instanceName}`
diff --git a/packages/backend/test/unit/MfmService.ts b/packages/backend/test/unit/MfmService.ts
index f613fe9c7c45..fd4a03413bb1 100644
--- a/packages/backend/test/unit/MfmService.ts
+++ b/packages/backend/test/unit/MfmService.ts
@@ -39,6 +39,12 @@ describe('MfmService', () => {
const output = 'foo bar
';
assert.equal(mfmService.toHtml(mfm.parse(input)), output);
});
+
+ test('escape', () => {
+ const input = '```\nHello, world!
\n```';
+ const output = '<p>Hello, world!</p>
';
+ assert.equal(mfmService.toHtml(mfm.parse(input)), output);
+ });
});
describe('fromHtml', () => {
diff --git a/packages/backend/test/unit/activitypub.ts b/packages/backend/test/unit/activitypub.ts
index b4b06b06bd05..aa3f3a4ff1d9 100644
--- a/packages/backend/test/unit/activitypub.ts
+++ b/packages/backend/test/unit/activitypub.ts
@@ -17,7 +17,7 @@ import { GlobalModule } from '@/GlobalModule.js';
import { CoreModule } from '@/core/CoreModule.js';
import { FederatedInstanceService } from '@/core/FederatedInstanceService.js';
import { LoggerService } from '@/core/LoggerService.js';
-import type { IActor, IApDocument, ICollection, IPost } from '@/core/activitypub/type.js';
+import type { IActor, IApDocument, ICollection, IObject, IPost } from '@/core/activitypub/type.js';
import { MiMeta, MiNote } from '@/models/_.js';
import { secureRndstr } from '@/misc/secure-rndstr.js';
import { DownloadService } from '@/core/DownloadService.js';
@@ -295,7 +295,7 @@ describe('ActivityPub', () => {
await createRandomRemoteUser(resolver, personService),
imageObject,
);
- assert.ok(!driveFile.isLink);
+ assert.ok(driveFile && !driveFile.isLink);
const sensitiveImageObject: IApDocument = {
type: 'Document',
@@ -308,7 +308,7 @@ describe('ActivityPub', () => {
await createRandomRemoteUser(resolver, personService),
sensitiveImageObject,
);
- assert.ok(!sensitiveDriveFile.isLink);
+ assert.ok(sensitiveDriveFile && !sensitiveDriveFile.isLink);
});
test('cacheRemoteFiles=false disables caching', async () => {
@@ -324,7 +324,7 @@ describe('ActivityPub', () => {
await createRandomRemoteUser(resolver, personService),
imageObject,
);
- assert.ok(driveFile.isLink);
+ assert.ok(driveFile && driveFile.isLink);
const sensitiveImageObject: IApDocument = {
type: 'Document',
@@ -337,7 +337,7 @@ describe('ActivityPub', () => {
await createRandomRemoteUser(resolver, personService),
sensitiveImageObject,
);
- assert.ok(sensitiveDriveFile.isLink);
+ assert.ok(sensitiveDriveFile && sensitiveDriveFile.isLink);
});
test('cacheRemoteSensitiveFiles=false only affects sensitive files', async () => {
@@ -353,7 +353,7 @@ describe('ActivityPub', () => {
await createRandomRemoteUser(resolver, personService),
imageObject,
);
- assert.ok(!driveFile.isLink);
+ assert.ok(driveFile && !driveFile.isLink);
const sensitiveImageObject: IApDocument = {
type: 'Document',
@@ -366,7 +366,19 @@ describe('ActivityPub', () => {
await createRandomRemoteUser(resolver, personService),
sensitiveImageObject,
);
- assert.ok(sensitiveDriveFile.isLink);
+ assert.ok(sensitiveDriveFile && sensitiveDriveFile.isLink);
+ });
+
+ test('Link is not an attachment files', async () => {
+ const linkObject: IObject = {
+ type: 'Link',
+ href: 'https://example.com/',
+ };
+ const driveFile = await imageService.createImage(
+ await createRandomRemoteUser(resolver, personService),
+ linkObject,
+ );
+ assert.strictEqual(driveFile, null);
});
});
});
diff --git a/packages/frontend/src/components/MkAbuseReport.vue b/packages/frontend/src/components/MkAbuseReport.vue
index 271b94feaada..ab65ea7ec70e 100644
--- a/packages/frontend/src/components/MkAbuseReport.vue
+++ b/packages/frontend/src/components/MkAbuseReport.vue
@@ -20,7 +20,7 @@ SPDX-License-Identifier: AGPL-3.0-only
-
+
{{ i18n.ts.reporter }}: @{{ report.reporter.username }}
diff --git a/packages/frontend/src/components/MkLink.vue b/packages/frontend/src/components/MkLink.vue
index 1d883d2b5a28..7871d1b50d97 100644
--- a/packages/frontend/src/components/MkLink.vue
+++ b/packages/frontend/src/components/MkLink.vue
@@ -13,6 +13,7 @@ SPDX-License-Identifier: AGPL-3.0-only
:rel="rel ?? 'nofollow noopener'"
:target="target"
:title="url"
+ :behavior="behavior"
>
@@ -25,11 +26,12 @@ import { url as local } from '@/config.js';
import { useTooltip } from '@/scripts/use-tooltip.js';
import * as os from '@/os.js';
import { isEnabledUrlPreview } from '@/instance.js';
-import MkA from '@/components/global/MkA.vue';
+import MkA, { type MkABehavior } from '@/components/global/MkA.vue';
const props = withDefaults(defineProps<{
url: string;
rel?: null | string;
+ behavior?: MkABehavior;
}>(), {
});
diff --git a/packages/frontend/src/components/MkMention.vue b/packages/frontend/src/components/MkMention.vue
index e6e8711f679c..6d364b688e7e 100644
--- a/packages/frontend/src/components/MkMention.vue
+++ b/packages/frontend/src/components/MkMention.vue
@@ -4,7 +4,7 @@ SPDX-License-Identifier: AGPL-3.0-only
-->
-
+
@{{ username }}
@@ -21,10 +21,12 @@ import { host as localHost } from '@/config.js';
import { $i } from '@/account.js';
import { defaultStore } from '@/store.js';
import { getStaticImageUrl } from '@/scripts/media-proxy.js';
+import { type MkABehavior } from '@/components/global/MkA.vue';
const props = defineProps<{
username: string;
host: string;
+ behavior?: MkABehavior;
}>();
const canonical = props.host === localHost ? `@${props.username}` : `@${props.username}@${toUnicode(props.host)}`;
diff --git a/packages/frontend/src/components/MkNotification.vue b/packages/frontend/src/components/MkNotification.vue
index 0fe80e2ed9fa..364e237217b0 100644
--- a/packages/frontend/src/components/MkNotification.vue
+++ b/packages/frontend/src/components/MkNotification.vue
@@ -58,8 +58,8 @@ SPDX-License-Identifier: AGPL-3.0-only
{{ i18n.ts._notification.achievementEarned }}
{{ i18n.ts._notification.testNotification }}
- {{ i18n.tsx._notification.likedBySomeUsers({ n: notification.reactions.length }) }}
- {{ i18n.tsx._notification.reactedBySomeUsers({ n: notification.reactions.length }) }}
+ {{ i18n.tsx._notification.likedBySomeUsers({ n: getActualReactedUsersCount(notification) }) }}
+ {{ i18n.tsx._notification.reactedBySomeUsers({ n: getActualReactedUsersCount(notification) }) }}
{{ i18n.tsx._notification.renotedBySomeUsers({ n: notification.users.length }) }}
{{ notification.header }}
@@ -72,7 +72,7 @@ SPDX-License-Identifier: AGPL-3.0-only
-
+
@@ -174,6 +174,11 @@ const rejectFollowRequest = () => {
followRequestDone.value = true;
misskeyApi('following/requests/reject', { userId: props.notification.user.id });
};
+
+function getActualReactedUsersCount(notification: Misskey.entities.Notification) {
+ if (notification.type !== 'reaction:grouped') return 0;
+ return new Set(notification.reactions.map((reaction) => reaction.user.id)).size;
+}