;
try {
- obj = JSON.parse(data.utf8Data);
+ obj = JSON.parse(data.toString());
} catch (e) {
return;
}
@@ -246,7 +245,7 @@ export default class Connection {
const ch: Channel = channelService.create(id, this);
this.channels.push(ch);
- ch.init(params);
+ ch.init(params ?? {});
if (pong) {
this.sendMessageToWs('connected', {
diff --git a/packages/backend/src/server/web/ClientServerService.ts b/packages/backend/src/server/web/ClientServerService.ts
index 23c89c53f9..50f8ed45c8 100644
--- a/packages/backend/src/server/web/ClientServerService.ts
+++ b/packages/backend/src/server/web/ClientServerService.ts
@@ -27,7 +27,7 @@ import { PageEntityService } from '@/core/entities/PageEntityService.js';
import { GalleryPostEntityService } from '@/core/entities/GalleryPostEntityService.js';
import { ClipEntityService } from '@/core/entities/ClipEntityService.js';
import { ChannelEntityService } from '@/core/entities/ChannelEntityService.js';
-import type { ChannelsRepository, ClipsRepository, FlashsRepository, GalleryPostsRepository, NotesRepository, PagesRepository, UserProfilesRepository, UsersRepository } from '@/models/index.js';
+import type { ChannelsRepository, ClipsRepository, FlashsRepository, GalleryPostsRepository, Meta, NotesRepository, PagesRepository, UserProfilesRepository, UsersRepository } from '@/models/index.js';
import type Logger from '@/logger.js';
import { deepClone } from '@/misc/clone.js';
import { bindThis } from '@/decorators.js';
@@ -118,6 +118,18 @@ export class ClientServerService {
return (res);
}
+ @bindThis
+ private generateCommonPugData(meta: Meta) {
+ return {
+ instanceName: meta.name ?? 'Misskey',
+ icon: meta.iconUrl,
+ themeColor: meta.themeColor,
+ serverErrorImageUrl: meta.serverErrorImageUrl ?? 'https://xn--931a.moe/assets/error.jpg',
+ infoImageUrl: meta.infoImageUrl ?? 'https://xn--931a.moe/assets/info.jpg',
+ notFoundImageUrl: meta.notFoundImageUrl ?? 'https://xn--931a.moe/assets/not-found.jpg',
+ };
+ }
+
@bindThis
public createServer(fastify: FastifyInstance, options: FastifyPluginOptions, done: (err?: Error) => void) {
fastify.register(fastifyCookie, {});
@@ -355,12 +367,10 @@ export class ClientServerService {
reply.header('Cache-Control', 'public, max-age=30');
return await reply.view('base', {
img: meta.bannerUrl,
- title: meta.name ?? 'Misskey',
- instanceName: meta.name ?? 'Misskey',
url: this.config.url,
+ title: meta.name ?? 'Misskey',
desc: meta.description,
- icon: meta.iconUrl,
- themeColor: meta.themeColor,
+ ...this.generateCommonPugData(meta),
});
};
@@ -445,9 +455,7 @@ export class ClientServerService {
user, profile, me,
avatarUrl: user.avatarUrl ?? this.userEntityService.getIdenticonUrl(user),
sub: request.params.sub,
- instanceName: meta.name ?? 'Misskey',
- icon: meta.iconUrl,
- themeColor: meta.themeColor,
+ ...this.generateCommonPugData(meta),
});
} else {
// リモートユーザーなので
@@ -495,9 +503,7 @@ export class ClientServerService {
avatarUrl: _note.user.avatarUrl,
// TODO: Let locale changeable by instance setting
summary: getNoteSummary(_note),
- instanceName: meta.name ?? 'Misskey',
- icon: meta.iconUrl,
- themeColor: meta.themeColor,
+ ...this.generateCommonPugData(meta),
});
} else {
return await renderBase(reply);
@@ -536,9 +542,7 @@ export class ClientServerService {
page: _page,
profile,
avatarUrl: _page.user.avatarUrl,
- instanceName: meta.name ?? 'Misskey',
- icon: meta.iconUrl,
- themeColor: meta.themeColor,
+ ...this.generateCommonPugData(meta),
});
} else {
return await renderBase(reply);
@@ -564,9 +568,7 @@ export class ClientServerService {
flash: _flash,
profile,
avatarUrl: _flash.user.avatarUrl,
- instanceName: meta.name ?? 'Misskey',
- icon: meta.iconUrl,
- themeColor: meta.themeColor,
+ ...this.generateCommonPugData(meta),
});
} else {
return await renderBase(reply);
@@ -592,9 +594,7 @@ export class ClientServerService {
clip: _clip,
profile,
avatarUrl: _clip.user.avatarUrl,
- instanceName: meta.name ?? 'Misskey',
- icon: meta.iconUrl,
- themeColor: meta.themeColor,
+ ...this.generateCommonPugData(meta),
});
} else {
return await renderBase(reply);
@@ -618,9 +618,7 @@ export class ClientServerService {
post: _post,
profile,
avatarUrl: _post.user.avatarUrl,
- instanceName: meta.name ?? 'Misskey',
- icon: meta.iconUrl,
- themeColor: meta.themeColor,
+ ...this.generateCommonPugData(meta),
});
} else {
return await renderBase(reply);
@@ -639,9 +637,7 @@ export class ClientServerService {
reply.header('Cache-Control', 'public, max-age=15');
return await reply.view('channel', {
channel: _channel,
- instanceName: meta.name ?? 'Misskey',
- icon: meta.iconUrl,
- themeColor: meta.themeColor,
+ ...this.generateCommonPugData(meta),
});
} else {
return await renderBase(reply);
diff --git a/packages/backend/src/server/web/boot.js b/packages/backend/src/server/web/boot.js
index 4ebe310acb..61eaa794b4 100644
--- a/packages/backend/src/server/web/boot.js
+++ b/packages/backend/src/server/web/boot.js
@@ -116,9 +116,9 @@
}
}
}
- const colorSchema = localStorage.getItem('colorSchema');
- if (colorSchema) {
- document.documentElement.style.setProperty('color-schema', colorSchema);
+ const colorScheme = localStorage.getItem('colorScheme');
+ if (colorScheme) {
+ document.documentElement.style.setProperty('color-scheme', colorScheme);
}
//#endregion
@@ -160,31 +160,34 @@
- An error has occurred!
+ Failed to load
読み込みに失敗しました
- Don't worry, it's (probably) not your fault.
- If the problem persists after refreshing, please contact your instance's administrator.
You may also try the following options:
- Update your os and browser.
- Disable an adblocker.
-
-
-
-
-
-
-
-
-
-
-
+ The following actions may solve the problem. / 以下を行うと解決する可能性があります。
+ Clear the browser cache / ブラウザのキャッシュをクリアする
+ Update your os and browser / ブラウザおよびOSを最新バージョンに更新する
+ Disable an adblocker / アドブロッカーを無効にする
+
+ Other options / その他のオプション
+
+
+
+
+
+
+
+
+
+
+
+
`;
@@ -194,6 +197,7 @@
errorsElement = document.getElementById('errors');
}
const detailsElement = document.createElement('details');
+ detailsElement.id = 'errorInfo';
detailsElement.innerHTML = `
@@ -250,7 +254,7 @@
.button-label-big {
color: #222;
font-weight: bold;
- font-size: 20px;
+ font-size: 1.2em;
padding: 12px;
}
@@ -270,11 +274,6 @@
font-size: 16px;
}
- .dont-worry,
- #msg {
- font-size: 18px;
- }
-
.icon-warning {
color: #dec340;
height: 4rem;
@@ -282,14 +281,15 @@
}
h1 {
- font-size: 32px;
+ font-size: 1.5em;
+ margin: 1em;
}
code {
font-family: Fira, FiraCode, monospace;
}
- details {
+ #errorInfo {
background: #333;
margin-bottom: 2rem;
padding: 0.5rem 1rem;
@@ -299,16 +299,16 @@
margin: auto;
}
- summary {
+ #errorInfo summary {
cursor: pointer;
}
- summary > * {
+ #errorInfo summary > * {
display: inline;
}
@media screen and (max-width: 500px) {
- details {
+ #errorInfo {
width: 50%;
}
`)
diff --git a/packages/backend/src/server/web/views/base.pug b/packages/backend/src/server/web/views/base.pug
index dea26da872..bb0ac61fbe 100644
--- a/packages/backend/src/server/web/views/base.pug
+++ b/packages/backend/src/server/web/views/base.pug
@@ -25,18 +25,17 @@ html
meta(name='referrer' content='origin')
meta(name='theme-color' content= themeColor || '#86b300')
meta(name='theme-color-orig' content= themeColor || '#86b300')
- meta(property='twitter:card' content='summary')
meta(property='og:site_name' content= instanceName || 'Misskey')
meta(name='viewport' content='width=device-width, initial-scale=1')
link(rel='icon' href= icon || '/favicon.ico')
link(rel='apple-touch-icon' href= icon || '/apple-touch-icon.png')
link(rel='manifest' href='/manifest.json')
link(rel='search' type='application/opensearchdescription+xml' title=(title || "Misskey") href=`${url}/opensearch.xml`)
- link(rel='prefetch' href='https://xn--931a.moe/assets/info.jpg')
- link(rel='prefetch' href='https://xn--931a.moe/assets/not-found.jpg')
- link(rel='prefetch' href='https://xn--931a.moe/assets/error.jpg')
+ link(rel='prefetch' href=serverErrorImageUrl)
+ link(rel='prefetch' href=infoImageUrl)
+ link(rel='prefetch' href=notFoundImageUrl)
//- https://github.com/misskey-dev/misskey/issues/9842
- link(rel='stylesheet' href='/assets/tabler-icons/tabler-icons.min.css?v2.17.0')
+ link(rel='stylesheet' href='/assets/tabler-icons/tabler-icons.min.css?v2.21.0')
link(rel='modulepreload' href=`/vite/${clientEntry.file}`)
if !config.clientManifestExists
@@ -59,6 +58,7 @@ html
meta(property='og:title' content= title || 'Misskey')
meta(property='og:description' content= desc || '✨🌎✨ A interplanetary communication platform ✨🚀✨')
meta(property='og:image' content= img)
+ meta(property='twitter:card' content='summary')
style
include ../style.css
diff --git a/packages/backend/src/server/web/views/channel.pug b/packages/backend/src/server/web/views/channel.pug
index 486f0ecc47..c514025e0b 100644
--- a/packages/backend/src/server/web/views/channel.pug
+++ b/packages/backend/src/server/web/views/channel.pug
@@ -16,3 +16,4 @@ block og
meta(property='og:description' content= channel.description)
meta(property='og:url' content= url)
meta(property='og:image' content= channel.bannerUrl)
+ meta(property='twitter:card' content='summary')
diff --git a/packages/backend/src/server/web/views/clip.pug b/packages/backend/src/server/web/views/clip.pug
index 74dc62f1e7..5a0018803a 100644
--- a/packages/backend/src/server/web/views/clip.pug
+++ b/packages/backend/src/server/web/views/clip.pug
@@ -17,6 +17,7 @@ block og
meta(property='og:description' content= clip.description)
meta(property='og:url' content= url)
meta(property='og:image' content= avatarUrl)
+ meta(property='twitter:card' content='summary')
block meta
if profile.noCrawle
diff --git a/packages/backend/src/server/web/views/flash.pug b/packages/backend/src/server/web/views/flash.pug
index 5594fcdfbf..1549aa7906 100644
--- a/packages/backend/src/server/web/views/flash.pug
+++ b/packages/backend/src/server/web/views/flash.pug
@@ -17,6 +17,7 @@ block og
meta(property='og:description' content= flash.summary)
meta(property='og:url' content= url)
meta(property='og:image' content= avatarUrl)
+ meta(property='twitter:card' content='summary')
block meta
if profile.noCrawle
diff --git a/packages/backend/src/server/web/views/gallery-post.pug b/packages/backend/src/server/web/views/gallery-post.pug
index 10f2d269bc..a458d7f8c7 100644
--- a/packages/backend/src/server/web/views/gallery-post.pug
+++ b/packages/backend/src/server/web/views/gallery-post.pug
@@ -17,6 +17,7 @@ block og
meta(property='og:description' content= post.description)
meta(property='og:url' content= url)
meta(property='og:image' content= post.files[0].thumbnailUrl)
+ meta(property='twitter:card' content='summary_large_image')
block meta
if user.host || profile.noCrawle
diff --git a/packages/backend/src/server/web/views/note.pug b/packages/backend/src/server/web/views/note.pug
index badfcccd61..ea0917a80e 100644
--- a/packages/backend/src/server/web/views/note.pug
+++ b/packages/backend/src/server/web/views/note.pug
@@ -5,6 +5,8 @@ block vars
- const title = user.name ? `${user.name} (@${user.username})` : `@${user.username}`;
- const url = `${config.url}/notes/${note.id}`;
- const isRenote = note.renote && note.text == null && note.fileIds.length == 0 && note.poll == null;
+ - const image = (note.files || []).find(file => file.type.startsWith('image/') && !file.isSensitive)
+ - const video = (note.files || []).find(file => file.type.startsWith('video/') && !file.isSensitive)
block title
= `${title} | ${instanceName}`
@@ -17,7 +19,19 @@ block og
meta(property='og:title' content= title)
meta(property='og:description' content= summary)
meta(property='og:url' content= url)
- meta(property='og:image' content= avatarUrl)
+ if video
+ meta(property='og:video:url' content= video.url)
+ meta(property='og:video:secure_url' content= video.url)
+ meta(property='og:video:type' content= video.type)
+ // FIXME: add width and height
+ // FIXME: add embed player for Twitter
+ if image
+ meta(property='twitter:card' content='summary_large_image')
+ meta(property='og:image' content= image.url)
+ else
+ meta(property='twitter:card' content='summary')
+ meta(property='og:image' content= avatarUrl)
+
block meta
if user.host || isRenote || profile.noCrawle
diff --git a/packages/backend/src/server/web/views/page.pug b/packages/backend/src/server/web/views/page.pug
index ddffc361c8..08bb08ffe7 100644
--- a/packages/backend/src/server/web/views/page.pug
+++ b/packages/backend/src/server/web/views/page.pug
@@ -17,6 +17,7 @@ block og
meta(property='og:description' content= page.summary)
meta(property='og:url' content= url)
meta(property='og:image' content= page.eyeCatchingImage ? page.eyeCatchingImage.thumbnailUrl : avatarUrl)
+ meta(property='twitter:card' content= page.eyeCatchingImage ? 'summary_large_image' : 'summary')
block meta
if profile.noCrawle
diff --git a/packages/backend/src/server/web/views/user.pug b/packages/backend/src/server/web/views/user.pug
index f4c83aa89d..83d57349a6 100644
--- a/packages/backend/src/server/web/views/user.pug
+++ b/packages/backend/src/server/web/views/user.pug
@@ -16,6 +16,7 @@ block og
meta(property='og:description' content= profile.description)
meta(property='og:url' content= url)
meta(property='og:image' content= avatarUrl)
+ meta(property='twitter:card' content='summary')
block meta
if user.host || profile.noCrawle
diff --git a/packages/backend/test/e2e/2fa.ts b/packages/backend/test/e2e/2fa.ts
index 0addb430c9..5da997f28b 100644
--- a/packages/backend/test/e2e/2fa.ts
+++ b/packages/backend/test/e2e/2fa.ts
@@ -2,7 +2,7 @@ process.env.NODE_ENV = 'test';
import * as assert from 'assert';
import * as crypto from 'node:crypto';
-import * as cbor from 'cbor';
+import cbor from 'cbor';
import * as OTPAuth from 'otpauth';
import { loadConfig } from '../../src/config.js';
import { signup, api, post, react, startServer, waitFire } from '../utils.js';
diff --git a/packages/backend/test/e2e/antennas.ts b/packages/backend/test/e2e/antennas.ts
new file mode 100644
index 0000000000..dd3b09f85a
--- /dev/null
+++ b/packages/backend/test/e2e/antennas.ts
@@ -0,0 +1,653 @@
+process.env.NODE_ENV = 'test';
+
+import * as assert from 'assert';
+import { inspect } from 'node:util';
+import { DEFAULT_POLICIES } from '@/core/RoleService.js';
+import type { Packed } from '@/misc/json-schema.js';
+import {
+ signup,
+ post,
+ userList,
+ page,
+ role,
+ startServer,
+ api,
+ successfulApiCall,
+ failedApiCall,
+ uploadFile,
+ testPaginationConsistency,
+} from '../utils.js';
+import type * as misskey from 'misskey-js';
+import type { INestApplicationContext } from '@nestjs/common';
+
+const compareBy = (selector: (s: T) => string = (s: T): string => s.id) => (a: T, b: T): number => {
+ return selector(a).localeCompare(selector(b));
+};
+
+describe('アンテナ', () => {
+ // エンティティとしてのアンテナを主眼においたテストを記述する
+ // (Antennaを返すエンドポイント、Antennaエンティティを書き換えるエンドポイント、Antennaからノートを取得するエンドポイントをテストする)
+
+ // BUG misskey-jsとjson-schemaが一致していない。
+ // - srcのenumにgroupが残っている
+ // - userGroupIdが残っている, isActiveがない
+ type Antenna = misskey.entities.Antenna | Packed<'Antenna'>;
+ type User = misskey.entities.MeDetailed & { token: string };
+ type Note = misskey.entities.Note;
+
+ // アンテナを作成できる最小のパラメタ
+ const defaultParam = {
+ caseSensitive: false,
+ excludeKeywords: [['']],
+ keywords: [['keyword']],
+ name: 'test',
+ notify: false,
+ src: 'all' as const,
+ userListId: null,
+ users: [''],
+ withFile: false,
+ withReplies: false,
+ };
+
+ let app: INestApplicationContext;
+
+ let root: User;
+ let alice: User;
+ let bob: User;
+ let carol: User;
+
+ let alicePost: Note;
+ let aliceList: misskey.entities.UserList;
+ let bobFile: misskey.entities.DriveFile;
+ let bobList: misskey.entities.UserList;
+
+ let userNotExplorable: User;
+ let userLocking: User;
+ let userSilenced: User;
+ let userSuspended: User;
+ let userDeletedBySelf: User;
+ let userDeletedByAdmin: User;
+ let userFollowingAlice: User;
+ let userFollowedByAlice: User;
+ let userBlockingAlice: User;
+ let userBlockedByAlice: User;
+ let userMutingAlice: User;
+ let userMutedByAlice: User;
+
+ beforeAll(async () => {
+ app = await startServer();
+ }, 1000 * 60 * 2);
+
+ beforeAll(async () => {
+ root = await signup({ username: 'root' });
+ alice = await signup({ username: 'alice' });
+ alicePost = await post(alice, { text: 'test' });
+ aliceList = await userList(alice, {});
+ bob = await signup({ username: 'bob' });
+ aliceList = await userList(alice, {});
+ bobFile = (await uploadFile(bob)).body;
+ bobList = await userList(bob);
+ carol = await signup({ username: 'carol' });
+ await api('users/lists/push', { listId: aliceList.id, userId: bob.id }, alice);
+ await api('users/lists/push', { listId: aliceList.id, userId: carol.id }, alice);
+
+ userNotExplorable = await signup({ username: 'userNotExplorable' });
+ await post(userNotExplorable, { text: 'test' });
+ await api('i/update', { isExplorable: false }, userNotExplorable);
+ userLocking = await signup({ username: 'userLocking' });
+ await post(userLocking, { text: 'test' });
+ await api('i/update', { isLocked: true }, userLocking);
+ userSilenced = await signup({ username: 'userSilenced' });
+ await post(userSilenced, { text: 'test' });
+ const roleSilenced = await role(root, {}, { canPublicNote: { priority: 0, useDefault: false, value: false } });
+ await api('admin/roles/assign', { userId: userSilenced.id, roleId: roleSilenced.id }, root);
+ userSuspended = await signup({ username: 'userSuspended' });
+ await post(userSuspended, { text: 'test' });
+ await successfulApiCall({ endpoint: 'i/update', parameters: { description: '#user_testuserSuspended' }, user: userSuspended });
+ await api('admin/suspend-user', { userId: userSuspended.id }, root);
+ userDeletedBySelf = await signup({ username: 'userDeletedBySelf', password: 'userDeletedBySelf' });
+ await post(userDeletedBySelf, { text: 'test' });
+ await api('i/delete-account', { password: 'userDeletedBySelf' }, userDeletedBySelf);
+ userDeletedByAdmin = await signup({ username: 'userDeletedByAdmin' });
+ await post(userDeletedByAdmin, { text: 'test' });
+ await api('admin/delete-account', { userId: userDeletedByAdmin.id }, root);
+ userFollowedByAlice = await signup({ username: 'userFollowedByAlice' });
+ await post(userFollowedByAlice, { text: 'test' });
+ await api('following/create', { userId: userFollowedByAlice.id }, alice);
+ userFollowingAlice = await signup({ username: 'userFollowingAlice' });
+ await post(userFollowingAlice, { text: 'test' });
+ await api('following/create', { userId: alice.id }, userFollowingAlice);
+ userBlockingAlice = await signup({ username: 'userBlockingAlice' });
+ await post(userBlockingAlice, { text: 'test' });
+ await api('blocking/create', { userId: alice.id }, userBlockingAlice);
+ userBlockedByAlice = await signup({ username: 'userBlockedByAlice' });
+ await post(userBlockedByAlice, { text: 'test' });
+ await api('blocking/create', { userId: userBlockedByAlice.id }, alice);
+ userMutingAlice = await signup({ username: 'userMutingAlice' });
+ await post(userMutingAlice, { text: 'test' });
+ await api('mute/create', { userId: alice.id }, userMutingAlice);
+ userMutedByAlice = await signup({ username: 'userMutedByAlice' });
+ await post(userMutedByAlice, { text: 'test' });
+ await api('mute/create', { userId: userMutedByAlice.id }, alice);
+ }, 1000 * 60 * 10);
+
+ afterAll(async () => {
+ await app.close();
+ });
+
+ beforeEach(async () => {
+ // テスト間で影響し合わないように毎回全部消す。
+ for (const user of [alice, bob]) {
+ const list = await api('/antennas/list', {}, user);
+ for (const antenna of list.body) {
+ await api('/antennas/delete', { antennaId: antenna.id }, user);
+ }
+ }
+ });
+
+ //#region 作成(antennas/create)
+
+ test('が作成できること、キーが過不足なく入っていること。', async () => {
+ const response = await successfulApiCall({
+ endpoint: 'antennas/create',
+ parameters: { ...defaultParam },
+ user: alice,
+ });
+ assert.match(response.id, /[0-9a-z]{10}/);
+ const expected = {
+ id: response.id,
+ caseSensitive: false,
+ createdAt: new Date(response.createdAt).toISOString(),
+ excludeKeywords: [['']],
+ hasUnreadNote: false,
+ isActive: true,
+ keywords: [['keyword']],
+ name: 'test',
+ notify: false,
+ src: 'all',
+ userListId: null,
+ users: [''],
+ withFile: false,
+ withReplies: false,
+ } as Antenna;
+ assert.deepStrictEqual(response, expected);
+ });
+
+ test('が上限いっぱいまで作成できること', async () => {
+ // antennaLimit + 1まで作れるのがキモ
+ const response = await Promise.all([...Array(DEFAULT_POLICIES.antennaLimit + 1)].map(() => successfulApiCall({
+ endpoint: 'antennas/create',
+ parameters: { ...defaultParam },
+ user: alice,
+ })));
+
+ const expected = await successfulApiCall({ endpoint: 'antennas/list', parameters: {}, user: alice });
+ assert.deepStrictEqual(
+ response.sort(compareBy(s => s.id)),
+ expected.sort(compareBy(s => s.id)));
+
+ failedApiCall({
+ endpoint: 'antennas/create',
+ parameters: { ...defaultParam },
+ user: alice,
+ }, {
+ status: 400,
+ code: 'TOO_MANY_ANTENNAS',
+ id: 'faf47050-e8b5-438c-913c-db2b1576fde4',
+ });
+ });
+
+ test('を作成するとき他人のリストを指定したらエラーになる', async () => {
+ failedApiCall({
+ endpoint: 'antennas/create',
+ parameters: { ...defaultParam, src: 'list', userListId: bobList.id },
+ user: alice,
+ }, {
+ status: 400,
+ code: 'NO_SUCH_USER_LIST',
+ id: '95063e93-a283-4b8b-9aa5-bcdb8df69a7f',
+ });
+ });
+
+ const antennaParamPattern = [
+ { parameters: (): object => ({ name: 'x'.repeat(100) }) },
+ { parameters: (): object => ({ name: 'x' }) },
+ { parameters: (): object => ({ src: 'home' }) },
+ { parameters: (): object => ({ src: 'all' }) },
+ { parameters: (): object => ({ src: 'users' }) },
+ { parameters: (): object => ({ src: 'list' }) },
+ { parameters: (): object => ({ userListId: null }) },
+ { parameters: (): object => ({ src: 'list', userListId: aliceList.id }) },
+ { parameters: (): object => ({ keywords: [['x']] }) },
+ { parameters: (): object => ({ keywords: [['a', 'b', 'c'], ['x'], ['y'], ['z']] }) },
+ { parameters: (): object => ({ excludeKeywords: [['a', 'b', 'c'], ['x'], ['y'], ['z']] }) },
+ { parameters: (): object => ({ users: [alice.username] }) },
+ { parameters: (): object => ({ users: [alice.username, bob.username, carol.username] }) },
+ { parameters: (): object => ({ caseSensitive: false }) },
+ { parameters: (): object => ({ caseSensitive: true }) },
+ { parameters: (): object => ({ withReplies: false }) },
+ { parameters: (): object => ({ withReplies: true }) },
+ { parameters: (): object => ({ withFile: false }) },
+ { parameters: (): object => ({ withFile: true }) },
+ { parameters: (): object => ({ notify: false }) },
+ { parameters: (): object => ({ notify: true }) },
+ ];
+ test.each(antennaParamPattern)('を作成できること($#)', async ({ parameters }) => {
+ const response = await successfulApiCall({
+ endpoint: 'antennas/create',
+ parameters: { ...defaultParam, ...parameters() },
+ user: alice,
+ });
+ const expected = { ...response, ...parameters() };
+ assert.deepStrictEqual(response, expected);
+ });
+
+ //#endregion
+ //#region 更新(antennas/update)
+
+ test.each(antennaParamPattern)('を変更できること($#)', async ({ parameters }) => {
+ const antenna = await successfulApiCall({ endpoint: 'antennas/create', parameters: defaultParam, user: alice });
+ const response = await successfulApiCall({
+ endpoint: 'antennas/update',
+ parameters: { antennaId: antenna.id, ...defaultParam, ...parameters() },
+ user: alice,
+ });
+ const expected = { ...response, ...parameters() };
+ assert.deepStrictEqual(response, expected);
+ });
+ test.todo('は他人のものは変更できない');
+
+ test('を変更するとき他人のリストを指定したらエラーになる', async () => {
+ const antenna = await successfulApiCall({ endpoint: 'antennas/create', parameters: defaultParam, user: alice });
+ failedApiCall({
+ endpoint: 'antennas/update',
+ parameters: { antennaId: antenna.id, ...defaultParam, src: 'list', userListId: bobList.id },
+ user: alice,
+ }, {
+ status: 400,
+ code: 'NO_SUCH_USER_LIST',
+ id: '1c6b35c9-943e-48c2-81e4-2844989407f7',
+ });
+ });
+
+ //#endregion
+ //#region 表示(antennas/show)
+
+ test('をID指定で表示できること。', async () => {
+ const antenna = await successfulApiCall({ endpoint: 'antennas/create', parameters: defaultParam, user: alice });
+ const response = await successfulApiCall({
+ endpoint: 'antennas/show',
+ parameters: { antennaId: antenna.id },
+ user: alice,
+ });
+ const expected = { ...antenna };
+ assert.deepStrictEqual(response, expected);
+ });
+ test.todo('は他人のものをID指定で表示できない');
+
+ //#endregion
+ //#region 一覧(antennas/list)
+
+ test('をリスト形式で取得できること。', async () => {
+ const antenna = await successfulApiCall({ endpoint: 'antennas/create', parameters: defaultParam, user: alice });
+ await successfulApiCall({ endpoint: 'antennas/create', parameters: defaultParam, user: bob });
+ const response = await successfulApiCall({
+ endpoint: 'antennas/list',
+ parameters: {},
+ user: alice,
+ });
+ const expected = [{ ...antenna }];
+ assert.deepStrictEqual(response, expected);
+ });
+
+ //#endregion
+ //#region 削除(antennas/delete)
+
+ test('を削除できること。', async () => {
+ const antenna = await successfulApiCall({ endpoint: 'antennas/create', parameters: defaultParam, user: alice });
+ const response = await successfulApiCall({
+ endpoint: 'antennas/delete',
+ parameters: { antennaId: antenna.id },
+ user: alice,
+ });
+ assert.deepStrictEqual(response, null);
+ const list = await successfulApiCall({ endpoint: 'antennas/list', parameters: {}, user: alice });
+ assert.deepStrictEqual(list, []);
+ });
+ test.todo('は他人のものを削除できない');
+
+ //#endregion
+
+ describe('のノート', () => {
+ //#region アンテナのノート取得(antennas/notes)
+
+ test('を取得できること。', async () => {
+ const keyword = 'キーワード';
+ await post(bob, { text: `test ${keyword} beforehand` });
+ const antenna = await successfulApiCall({
+ endpoint: 'antennas/create',
+ parameters: { ...defaultParam, keywords: [[keyword]] },
+ user: alice,
+ });
+ const note = await post(bob, { text: `test ${keyword}` });
+ const response = await successfulApiCall({
+ endpoint: 'antennas/notes',
+ parameters: { antennaId: antenna.id },
+ user: alice,
+ });
+ const expected = [note];
+ assert.deepStrictEqual(response, expected);
+ });
+
+ const keyword = 'キーワード';
+ test.each([
+ {
+ label: '全体から',
+ parameters: (): object => ({ src: 'all' }),
+ posts: [
+ { note: (): Promise => post(alice, { text: `${keyword}` }), included: true },
+ { note: (): Promise => post(userFollowedByAlice, { text: `${keyword}` }), included: true },
+ { note: (): Promise => post(bob, { text: `test ${keyword}` }), included: true },
+ { note: (): Promise => post(carol, { text: `test ${keyword}` }), included: true },
+ ],
+ },
+ {
+ // BUG e4144a1 以降home指定は壊れている(allと同じ)
+ label: 'ホーム指定はallと同じ',
+ parameters: (): object => ({ src: 'home' }),
+ posts: [
+ { note: (): Promise => post(alice, { text: `${keyword}` }), included: true },
+ { note: (): Promise => post(userFollowedByAlice, { text: `${keyword}` }), included: true },
+ { note: (): Promise => post(bob, { text: `test ${keyword}` }), included: true },
+ { note: (): Promise => post(carol, { text: `test ${keyword}` }), included: true },
+ ],
+ },
+ {
+ // https://github.com/misskey-dev/misskey/issues/9025
+ label: 'ただし、フォロワー限定投稿とDM投稿を含まない。フォロワーであっても。',
+ parameters: (): object => ({}),
+ posts: [
+ { note: (): Promise => post(userFollowedByAlice, { text: `${keyword}`, visibility: 'public' }), included: true },
+ { note: (): Promise => post(userFollowedByAlice, { text: `${keyword}`, visibility: 'home' }), included: true },
+ { note: (): Promise => post(userFollowedByAlice, { text: `${keyword}`, visibility: 'followers' }) },
+ { note: (): Promise => post(userFollowedByAlice, { text: `${keyword}`, visibility: 'specified', visibleUserIds: [alice.id] }) },
+ ],
+ },
+ {
+ label: 'ブロックしているユーザーのノートは含む',
+ parameters: (): object => ({}),
+ posts: [
+ { note: (): Promise => post(userBlockedByAlice, { text: `${keyword}` }), included: true },
+ ],
+ },
+ {
+ label: 'ブロックされているユーザーのノートは含まない',
+ parameters: (): object => ({}),
+ posts: [
+ { note: (): Promise => post(userBlockingAlice, { text: `${keyword}` }) },
+ ],
+ },
+ {
+ label: 'ミュートしているユーザーのノートは含まない',
+ parameters: (): object => ({}),
+ posts: [
+ { note: (): Promise => post(userMutedByAlice, { text: `${keyword}` }) },
+ ],
+ },
+ {
+ label: 'ミュートされているユーザーのノートは含む',
+ parameters: (): object => ({}),
+ posts: [
+ { note: (): Promise => post(userMutingAlice, { text: `${keyword}` }), included: true },
+ ],
+ },
+ {
+ label: '「見つけやすくする」がOFFのユーザーのノートも含まれる',
+ parameters: (): object => ({}),
+ posts: [
+ { note: (): Promise => post(userNotExplorable, { text: `${keyword}` }), included: true },
+ ],
+ },
+ {
+ label: '鍵付きユーザーのノートも含まれる',
+ parameters: (): object => ({}),
+ posts: [
+ { note: (): Promise => post(userLocking, { text: `${keyword}` }), included: true },
+ ],
+ },
+ {
+ label: 'サイレンスのノートも含まれる',
+ parameters: (): object => ({}),
+ posts: [
+ { note: (): Promise => post(userSilenced, { text: `${keyword}` }), included: true },
+ ],
+ },
+ {
+ label: '削除ユーザーのノートも含まれる',
+ parameters: (): object => ({}),
+ posts: [
+ { note: (): Promise => post(userDeletedBySelf, { text: `${keyword}` }), included: true },
+ { note: (): Promise => post(userDeletedByAdmin, { text: `${keyword}` }), included: true },
+ ],
+ },
+ {
+ label: 'ユーザー指定で',
+ parameters: (): object => ({ src: 'users', users: [`@${bob.username}`, `@${carol.username}`] }),
+ posts: [
+ { note: (): Promise => post(alice, { text: `test ${keyword}` }) },
+ { note: (): Promise => post(bob, { text: `test ${keyword}` }), included: true },
+ { note: (): Promise => post(carol, { text: `test ${keyword}` }), included: true },
+ ],
+ },
+ {
+ label: 'リスト指定で',
+ parameters: (): object => ({ src: 'list', userListId: aliceList.id }),
+ posts: [
+ { note: (): Promise => post(alice, { text: `test ${keyword}` }) },
+ { note: (): Promise => post(bob, { text: `test ${keyword}` }), included: true },
+ { note: (): Promise => post(carol, { text: `test ${keyword}` }), included: true },
+ ],
+ },
+ {
+ label: 'CWにもマッチする',
+ parameters: (): object => ({ keywords: [[keyword]] }),
+ posts: [
+ { note: (): Promise => post(bob, { text: 'test', cw: `cw ${keyword}` }), included: true },
+ ],
+ },
+ {
+ label: 'キーワード1つ',
+ parameters: (): object => ({ keywords: [[keyword]] }),
+ posts: [
+ { note: (): Promise => post(alice, { text: 'test' }) },
+ { note: (): Promise => post(bob, { text: `test ${keyword}` }), included: true },
+ { note: (): Promise => post(carol, { text: 'test' }) },
+ ],
+ },
+ {
+ label: 'キーワード3つ(AND)',
+ parameters: (): object => ({ keywords: [['A', 'B', 'C']] }),
+ posts: [
+ { note: (): Promise => post(bob, { text: 'test A' }) },
+ { note: (): Promise => post(bob, { text: 'test A B' }) },
+ { note: (): Promise => post(bob, { text: 'test B C' }) },
+ { note: (): Promise => post(bob, { text: 'test A B C' }), included: true },
+ { note: (): Promise => post(bob, { text: 'test C B A A B C' }), included: true },
+ ],
+ },
+ {
+ label: 'キーワード3つ(OR)',
+ parameters: (): object => ({ keywords: [['A'], ['B'], ['C']] }),
+ posts: [
+ { note: (): Promise => post(bob, { text: 'test' }) },
+ { note: (): Promise => post(bob, { text: 'test A' }), included: true },
+ { note: (): Promise => post(bob, { text: 'test A B' }), included: true },
+ { note: (): Promise => post(bob, { text: 'test B C' }), included: true },
+ { note: (): Promise => post(bob, { text: 'test B C A' }), included: true },
+ { note: (): Promise => post(bob, { text: 'test C B' }), included: true },
+ { note: (): Promise => post(bob, { text: 'test C' }), included: true },
+ ],
+ },
+ {
+ label: '除外ワード3つ(AND)',
+ parameters: (): object => ({ excludeKeywords: [['A', 'B', 'C']] }),
+ posts: [
+ { note: (): Promise => post(bob, { text: `test ${keyword}` }), included: true },
+ { note: (): Promise => post(bob, { text: `test ${keyword} A` }), included: true },
+ { note: (): Promise => post(bob, { text: `test ${keyword} A B` }), included: true },
+ { note: (): Promise => post(bob, { text: `test ${keyword} B C` }), included: true },
+ { note: (): Promise => post(bob, { text: `test ${keyword} B C A` }) },
+ { note: (): Promise => post(bob, { text: `test ${keyword} C B` }), included: true },
+ { note: (): Promise => post(bob, { text: `test ${keyword} C` }), included: true },
+ ],
+ },
+ {
+ label: '除外ワード3つ(OR)',
+ parameters: (): object => ({ excludeKeywords: [['A'], ['B'], ['C']] }),
+ posts: [
+ { note: (): Promise => post(bob, { text: `test ${keyword}` }), included: true },
+ { note: (): Promise => post(bob, { text: `test ${keyword} A` }) },
+ { note: (): Promise => post(bob, { text: `test ${keyword} A B` }) },
+ { note: (): Promise => post(bob, { text: `test ${keyword} B C` }) },
+ { note: (): Promise => post(bob, { text: `test ${keyword} B C A` }) },
+ { note: (): Promise => post(bob, { text: `test ${keyword} C B` }) },
+ { note: (): Promise => post(bob, { text: `test ${keyword} C` }) },
+ ],
+ },
+ {
+ label: 'キーワード1つ(大文字小文字区別する)',
+ parameters: (): object => ({ keywords: [['KEYWORD']], caseSensitive: true }),
+ posts: [
+ { note: (): Promise => post(bob, { text: 'keyword' }) },
+ { note: (): Promise => post(bob, { text: 'kEyWoRd' }) },
+ { note: (): Promise => post(bob, { text: 'KEYWORD' }), included: true },
+ ],
+ },
+ {
+ label: 'キーワード1つ(大文字小文字区別しない)',
+ parameters: (): object => ({ keywords: [['KEYWORD']], caseSensitive: false }),
+ posts: [
+ { note: (): Promise => post(bob, { text: 'keyword' }), included: true },
+ { note: (): Promise => post(bob, { text: 'kEyWoRd' }), included: true },
+ { note: (): Promise => post(bob, { text: 'KEYWORD' }), included: true },
+ ],
+ },
+ {
+ label: '除外ワード1つ(大文字小文字区別する)',
+ parameters: (): object => ({ excludeKeywords: [['KEYWORD']], caseSensitive: true }),
+ posts: [
+ { note: (): Promise