diff --git a/CHANGELOG.md b/CHANGELOG.md
index eb8f2ac453..962e3b83ba 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -35,6 +35,9 @@
- Enhance: リプライにて引用がある場合テキストが空でもノートできるように
- 引用したいノートのURLをコピーしリプライ投稿画面にペーストして添付することで達成できます
- Enhance: フォローするかどうかの確認ダイアログを出せるように
+- Enhance: Playを手動でリロードできるように
+- Enhance: 通報のコメント内のリンクをクリックした際、ウィンドウで開くように
+- Chore: AiScriptを0.18.0にバージョンアップ
- Fix: 一部のページ内リンクが正しく動作しない問題を修正
- Fix: 周年の実績が閏年を考慮しない問題を修正
- Fix: ローカルURLのプレビューポップアップが左上に表示される
@@ -49,6 +52,10 @@
- Fix: ノート詳細ページにおいてCW付き引用リノートのCWボタンのラベルに「引用」が含まれていない問題を修正
- Fix: ダイアログの入力で字数制限に違反していてもEnterキーが押せてしまう問題を修正
- Fix: ダイレクト投稿の宛先が保存されない問題を修正
+- Fix: Playのページを離れたときに、Playが正常に初期化されない問題を修正
+- Fix: ページのOGP URLが間違っているのを修正
+- Fix: リバーシの対局を正しく共有できないことがある問題を修正
+- Fix: 通知をグループ化している際に、人数が正常に表示されないことがある問題を修正
### Server
- Enhance: エンドポイント`antennas/update`の必須項目を`antennaId`のみに
@@ -61,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 863830a501..971c421d61 100644
--- a/packages/backend/src/core/CustomEmojiService.ts
+++ b/packages/backend/src/core/CustomEmojiService.ts
@@ -20,7 +20,7 @@ import type { Serialized } from '@/types.js';
import { ModerationLogService } from '@/core/ModerationLogService.js';
import { MiEmojiRequest } from '@/models/EmojiRequest.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 2fb731201b..9786f8b8bb 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 89b6ef23d0..3691967270 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 0bcc353232..800bddda4e 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, NotesRepository } from '@/models/_.js';
@@ -214,15 +213,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 012fd44c19..47f216eb15 100644
--- a/packages/backend/src/core/activitypub/type.ts
+++ b/packages/backend/src/core/activitypub/type.ts
@@ -26,6 +26,7 @@ export interface IObject {
endTime?: Date;
icon?: any;
image?: any;
+ mediaType?: string;
url?: ApObject | string;
href?: string;
tag?: IObject | IObject[];
@@ -241,14 +242,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/ServerService.ts b/packages/backend/src/server/ServerService.ts
index 671dd31eb1..1324cd1361 100644
--- a/packages/backend/src/server/ServerService.ts
+++ b/packages/backend/src/server/ServerService.ts
@@ -120,12 +120,20 @@ export class ServerService implements OnApplicationShutdown {
return;
}
- const name = path.split('@')[0].replace(/\.webp$/i, '');
- const host = path.split('@')[1]?.replace(/\.webp$/i, '');
+ const emojiPath = path.replace(/\.webp$/i, '');
+ const pathChunks = emojiPath.split('@');
+
+ if (pathChunks.length > 2) {
+ reply.code(400);
+ return;
+ }
+
+ const name = pathChunks.shift();
+ const host = pathChunks.pop();
const emoji = await this.emojisRepository.findOneBy({
// `@.` is the spec of ReactionService.decodeReaction
- host: (host == null || host === '.') ? IsNull() : host,
+ host: (host === undefined || host === '.') ? IsNull() : host,
name: name,
});
diff --git a/packages/backend/src/server/api/endpoints/fetch-rss.ts b/packages/backend/src/server/api/endpoints/fetch-rss.ts
index 2085b06365..ba48b0119e 100644
--- a/packages/backend/src/server/api/endpoints/fetch-rss.ts
+++ b/packages/backend/src/server/api/endpoints/fetch-rss.ts
@@ -20,13 +20,188 @@ export const meta = {
res: {
type: 'object',
properties: {
+ image: {
+ type: 'object',
+ optional: true,
+ properties: {
+ link: {
+ type: 'string',
+ optional: true,
+ },
+ url: {
+ type: 'string',
+ optional: false,
+ },
+ title: {
+ type: 'string',
+ optional: true,
+ },
+ },
+ },
+ paginationLinks: {
+ type: 'object',
+ optional: true,
+ properties: {
+ self: {
+ type: 'string',
+ optional: true,
+ },
+ first: {
+ type: 'string',
+ optional: true,
+ },
+ next: {
+ type: 'string',
+ optional: true,
+ },
+ last: {
+ type: 'string',
+ optional: true,
+ },
+ prev: {
+ type: 'string',
+ optional: true,
+ },
+ },
+ },
+ link: {
+ type: 'string',
+ optional: true,
+ },
+ title: {
+ type: 'string',
+ optional: true,
+ },
items: {
type: 'array',
+ optional: false,
items: {
type: 'object',
+ properties: {
+ link: {
+ type: 'string',
+ optional: true,
+ },
+ guid: {
+ type: 'string',
+ optional: true,
+ },
+ title: {
+ type: 'string',
+ optional: true,
+ },
+ pubDate: {
+ type: 'string',
+ optional: true,
+ },
+ creator: {
+ type: 'string',
+ optional: true,
+ },
+ summary: {
+ type: 'string',
+ optional: true,
+ },
+ content: {
+ type: 'string',
+ optional: true,
+ },
+ isoDate: {
+ type: 'string',
+ optional: true,
+ },
+ categories: {
+ type: 'array',
+ optional: true,
+ items: {
+ type: 'string',
+ },
+ },
+ contentSnippet: {
+ type: 'string',
+ optional: true,
+ },
+ enclosure: {
+ type: 'object',
+ optional: true,
+ properties: {
+ url: {
+ type: 'string',
+ optional: false,
+ },
+ length: {
+ type: 'number',
+ optional: true,
+ },
+ type: {
+ type: 'string',
+ optional: true,
+ },
+ },
+ },
+ },
},
- }
- }
+ },
+ feedUrl: {
+ type: 'string',
+ optional: true,
+ },
+ description: {
+ type: 'string',
+ optional: true,
+ },
+ itunes: {
+ type: 'object',
+ optional: true,
+ additionalProperties: true,
+ properties: {
+ image: {
+ type: 'string',
+ optional: true,
+ },
+ owner: {
+ type: 'object',
+ optional: true,
+ properties: {
+ name: {
+ type: 'string',
+ optional: true,
+ },
+ email: {
+ type: 'string',
+ optional: true,
+ },
+ },
+ },
+ author: {
+ type: 'string',
+ optional: true,
+ },
+ summary: {
+ type: 'string',
+ optional: true,
+ },
+ explicit: {
+ type: 'string',
+ optional: true,
+ },
+ categories: {
+ type: 'array',
+ optional: true,
+ items: {
+ type: 'string',
+ },
+ },
+ keywords: {
+ type: 'array',
+ optional: true,
+ items: {
+ type: 'string',
+ },
+ },
+ },
+ },
+ },
},
} as const;
diff --git a/packages/backend/src/server/web/ClientServerService.ts b/packages/backend/src/server/web/ClientServerService.ts
index ceb0bb8c29..59f6c82008 100644
--- a/packages/backend/src/server/web/ClientServerService.ts
+++ b/packages/backend/src/server/web/ClientServerService.ts
@@ -211,6 +211,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 08bb08ffe7..03c50eca8a 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 f613fe9c7c..fd4a03413b 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 b4b06b06bd..aa3f3a4ff1 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/package.json b/packages/frontend/package.json
index cbf4e59592..95980ac0fc 100644
--- a/packages/frontend/package.json
+++ b/packages/frontend/package.json
@@ -24,7 +24,7 @@
"@rollup/plugin-json": "6.1.0",
"@rollup/plugin-replace": "5.0.5",
"@rollup/pluginutils": "5.1.0",
- "@syuilo/aiscript": "0.17.0",
+ "@syuilo/aiscript": "0.18.0",
"@tabler/icons-webfont": "2.44.0",
"@twemoji/parser": "15.0.0",
"@vitejs/plugin-vue": "5.0.4",
diff --git a/packages/frontend/src/components/MkAbuseReport.vue b/packages/frontend/src/components/MkAbuseReport.vue
index bdb4d47c28..42cf8f47ce 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.reportedNote }}
diff --git a/packages/frontend/src/components/MkLink.vue b/packages/frontend/src/components/MkLink.vue
index ca875242b4..bd1bd0e24a 100644
--- a/packages/frontend/src/components/MkLink.vue
+++ b/packages/frontend/src/components/MkLink.vue
@@ -6,6 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only
@@ -19,10 +20,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 { 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 2b0902bf92..afccf139f2 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,12 +21,14 @@ 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 { MkABehavior } from '@/components/global/MkA.vue';
const gamingType = computed(defaultStore.makeGetterSetter('gamingType'));
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 0d3a5c13ba..73cd7cd5b3 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;
+}