Merge branch 'develop' into allow-suspended-access

This commit is contained in:
syuilo
2023-09-22 16:23:40 +09:00
1210 changed files with 16034 additions and 14774 deletions

View File

@@ -9,8 +9,16 @@ import * as assert from 'assert';
import * as crypto from 'node:crypto';
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';
import { loadConfig } from '@/config.js';
import { api, signup, startServer } from '../utils.js';
import type {
AuthenticationResponseJSON,
AuthenticatorAssertionResponseJSON,
AuthenticatorAttestationResponseJSON,
PublicKeyCredentialCreationOptionsJSON,
PublicKeyCredentialRequestOptionsJSON,
RegistrationResponseJSON,
} from '@simplewebauthn/typescript-types';
import type { INestApplicationContext } from '@nestjs/common';
import type * as misskey from 'misskey-js';
@@ -47,21 +55,20 @@ describe('2要素認証', () => {
const rpIdHash = (): Buffer => {
return crypto.createHash('sha256')
.update(Buffer.from(config.hostname, 'utf-8'))
.update(Buffer.from(config.host, 'utf-8'))
.digest();
};
const keyDoneParam = (param: {
token: string,
keyName: string,
challengeId: string,
challenge: string,
credentialId: Buffer,
creationOptions: PublicKeyCredentialCreationOptionsJSON,
}): {
attestationObject: string,
challengeId: string,
clientDataJSON: string,
token: string,
password: string,
name: string,
credential: RegistrationResponseJSON,
} => {
// A COSE encoded public key
const credentialPublicKey = cbor.encode(new Map<number, unknown>([
@@ -76,7 +83,7 @@ describe('2要素認証', () => {
// AuthenticatorAssertionResponse.authenticatorData
// https://developer.mozilla.org/en-US/docs/Web/API/AuthenticatorAssertionResponse/authenticatorData
const credentialIdLength = Buffer.allocUnsafe(2);
credentialIdLength.writeUInt16BE(param.credentialId.length);
credentialIdLength.writeUInt16BE(param.credentialId.length, 0);
const authData = Buffer.concat([
rpIdHash(), // rpIdHash(32)
Buffer.from([0x45]), // flags(1)
@@ -88,20 +95,28 @@ describe('2要素認証', () => {
]);
return {
attestationObject: cbor.encode({
fmt: 'none',
attStmt: {},
authData,
}).toString('hex'),
challengeId: param.challengeId,
clientDataJSON: JSON.stringify({
type: 'webauthn.create',
challenge: param.challenge,
origin: config.scheme + '://' + config.host,
androidPackageName: 'org.mozilla.firefox',
}),
password,
token: param.token,
name: param.keyName,
credential: <RegistrationResponseJSON>{
id: param.credentialId.toString('base64url'),
rawId: param.credentialId.toString('base64url'),
response: <AuthenticatorAttestationResponseJSON>{
clientDataJSON: Buffer.from(JSON.stringify({
type: 'webauthn.create',
challenge: param.creationOptions.challenge,
origin: config.scheme + '://' + config.host,
androidPackageName: 'org.mozilla.firefox',
}), 'utf-8').toString('base64url'),
attestationObject: cbor.encode({
fmt: 'none',
attStmt: {},
authData,
}).toString('base64url'),
},
clientExtensionResults: {},
type: 'public-key',
},
};
};
@@ -121,17 +136,12 @@ describe('2要素認証', () => {
const signinWithSecurityKeyParam = (param: {
keyName: string,
challengeId: string,
challenge: string,
credentialId: Buffer,
requestOptions: PublicKeyCredentialRequestOptionsJSON,
}): {
authenticatorData: string,
credentialId: string,
challengeId: string,
clientDataJSON: string,
username: string,
password: string,
signature: string,
credential: AuthenticationResponseJSON,
'g-recaptcha-response'?: string | null,
'hcaptcha-response'?: string | null,
} => {
@@ -144,10 +154,10 @@ describe('2要素認証', () => {
]);
const clientDataJSONBuffer = Buffer.from(JSON.stringify({
type: 'webauthn.get',
challenge: param.challenge,
challenge: param.requestOptions.challenge,
origin: config.scheme + '://' + config.host,
androidPackageName: 'org.mozilla.firefox',
}));
}), 'utf-8');
const hashedclientDataJSON = crypto.createHash('sha256')
.update(clientDataJSONBuffer)
.digest();
@@ -156,13 +166,19 @@ describe('2要素認証', () => {
.update(Buffer.concat([authenticatorData, hashedclientDataJSON]))
.sign(privateKey);
return {
authenticatorData: authenticatorData.toString('hex'),
credentialId: param.credentialId.toString('base64'),
challengeId: param.challengeId,
clientDataJSON: clientDataJSONBuffer.toString('hex'),
username,
password,
signature: signature.toString('hex'),
credential: <AuthenticationResponseJSON>{
id: param.credentialId.toString('base64url'),
rawId: param.credentialId.toString('base64url'),
response: <AuthenticatorAssertionResponseJSON>{
clientDataJSON: clientDataJSONBuffer.toString('base64url'),
authenticatorData: authenticatorData.toString('base64url'),
signature: signature.toString('base64url'),
},
clientExtensionResults: {},
type: 'public-key',
},
'g-recaptcha-response': null,
'hcaptcha-response': null,
};
@@ -191,7 +207,7 @@ describe('2要素認証', () => {
const doneResponse = await api('/i/2fa/done', {
token: otpToken(registerResponse.body.secret),
}, alice);
assert.strictEqual(doneResponse.status, 204);
assert.strictEqual(doneResponse.status, 200);
const usersShowResponse = await api('/users/show', {
username,
@@ -205,6 +221,12 @@ describe('2要素認証', () => {
});
assert.strictEqual(signinResponse.status, 200);
assert.notEqual(signinResponse.body.i, undefined);
// 後片付け
await api('/i/2fa/unregister', {
password,
token: otpToken(registerResponse.body.secret),
}, alice);
});
test('が設定でき、セキュリティキーでログインできる。', async () => {
@@ -216,25 +238,26 @@ describe('2要素認証', () => {
const doneResponse = await api('/i/2fa/done', {
token: otpToken(registerResponse.body.secret),
}, alice);
assert.strictEqual(doneResponse.status, 204);
assert.strictEqual(doneResponse.status, 200);
const registerKeyResponse = await api('/i/2fa/register-key', {
password,
token: otpToken(registerResponse.body.secret),
}, alice);
assert.strictEqual(registerKeyResponse.status, 200);
assert.notEqual(registerKeyResponse.body.challengeId, undefined);
assert.notEqual(registerKeyResponse.body.rp, undefined);
assert.notEqual(registerKeyResponse.body.challenge, undefined);
const keyName = 'example-key';
const credentialId = crypto.randomBytes(0x41);
const keyDoneResponse = await api('/i/2fa/key-done', keyDoneParam({
token: otpToken(registerResponse.body.secret),
keyName,
challengeId: registerKeyResponse.body.challengeId,
challenge: registerKeyResponse.body.challenge,
credentialId,
creationOptions: registerKeyResponse.body,
}), alice);
assert.strictEqual(keyDoneResponse.status, 200);
assert.strictEqual(keyDoneResponse.body.id, credentialId.toString('hex'));
assert.strictEqual(keyDoneResponse.body.id, credentialId.toString('base64url'));
assert.strictEqual(keyDoneResponse.body.name, keyName);
const usersShowResponse = await api('/users/show', {
@@ -248,19 +271,23 @@ describe('2要素認証', () => {
});
assert.strictEqual(signinResponse.status, 200);
assert.strictEqual(signinResponse.body.i, undefined);
assert.notEqual(signinResponse.body.challengeId, undefined);
assert.notEqual(signinResponse.body.challenge, undefined);
assert.notEqual(signinResponse.body.securityKeys, undefined);
assert.strictEqual(signinResponse.body.securityKeys[0].id, credentialId.toString('hex'));
assert.notEqual(signinResponse.body.allowCredentials, undefined);
assert.strictEqual(signinResponse.body.allowCredentials[0].id, credentialId.toString('base64url'));
const signinResponse2 = await api('/signin', signinWithSecurityKeyParam({
keyName,
challengeId: signinResponse.body.challengeId,
challenge: signinResponse.body.challenge,
credentialId,
requestOptions: signinResponse.body,
}));
assert.strictEqual(signinResponse2.status, 200);
assert.notEqual(signinResponse2.body.i, undefined);
// 後片付け
await api('/i/2fa/unregister', {
password,
token: otpToken(registerResponse.body.secret),
}, alice);
});
test('が設定でき、セキュリティキーでパスワードレスログインできる。', async () => {
@@ -272,9 +299,10 @@ describe('2要素認証', () => {
const doneResponse = await api('/i/2fa/done', {
token: otpToken(registerResponse.body.secret),
}, alice);
assert.strictEqual(doneResponse.status, 204);
assert.strictEqual(doneResponse.status, 200);
const registerKeyResponse = await api('/i/2fa/register-key', {
token: otpToken(registerResponse.body.secret),
password,
}, alice);
assert.strictEqual(registerKeyResponse.status, 200);
@@ -282,10 +310,10 @@ describe('2要素認証', () => {
const keyName = 'example-key';
const credentialId = crypto.randomBytes(0x41);
const keyDoneResponse = await api('/i/2fa/key-done', keyDoneParam({
token: otpToken(registerResponse.body.secret),
keyName,
challengeId: registerKeyResponse.body.challengeId,
challenge: registerKeyResponse.body.challenge,
credentialId,
creationOptions: registerKeyResponse.body,
}), alice);
assert.strictEqual(keyDoneResponse.status, 200);
@@ -310,14 +338,19 @@ describe('2要素認証', () => {
const signinResponse2 = await api('/signin', {
...signinWithSecurityKeyParam({
keyName,
challengeId: signinResponse.body.challengeId,
challenge: signinResponse.body.challenge,
credentialId,
requestOptions: signinResponse.body,
}),
password: '',
});
assert.strictEqual(signinResponse2.status, 200);
assert.notEqual(signinResponse2.body.i, undefined);
// 後片付け
await api('/i/2fa/unregister', {
password,
token: otpToken(registerResponse.body.secret),
}, alice);
});
test('が設定でき、設定したセキュリティキーの名前を変更できる。', async () => {
@@ -329,9 +362,10 @@ describe('2要素認証', () => {
const doneResponse = await api('/i/2fa/done', {
token: otpToken(registerResponse.body.secret),
}, alice);
assert.strictEqual(doneResponse.status, 204);
assert.strictEqual(doneResponse.status, 200);
const registerKeyResponse = await api('/i/2fa/register-key', {
token: otpToken(registerResponse.body.secret),
password,
}, alice);
assert.strictEqual(registerKeyResponse.status, 200);
@@ -339,27 +373,33 @@ describe('2要素認証', () => {
const keyName = 'example-key';
const credentialId = crypto.randomBytes(0x41);
const keyDoneResponse = await api('/i/2fa/key-done', keyDoneParam({
token: otpToken(registerResponse.body.secret),
keyName,
challengeId: registerKeyResponse.body.challengeId,
challenge: registerKeyResponse.body.challenge,
credentialId,
creationOptions: registerKeyResponse.body,
}), alice);
assert.strictEqual(keyDoneResponse.status, 200);
const renamedKey = 'other-key';
const updateKeyResponse = await api('/i/2fa/update-key', {
name: renamedKey,
credentialId: credentialId.toString('hex'),
credentialId: credentialId.toString('base64url'),
}, alice);
assert.strictEqual(updateKeyResponse.status, 200);
const iResponse = await api('/i', {
}, alice);
assert.strictEqual(iResponse.status, 200);
const securityKeys = iResponse.body.securityKeysList.filter(s => s.id === credentialId.toString('hex'));
const securityKeys = iResponse.body.securityKeysList.filter((s: { id: string; }) => s.id === credentialId.toString('base64url'));
assert.strictEqual(securityKeys.length, 1);
assert.strictEqual(securityKeys[0].name, renamedKey);
assert.notEqual(securityKeys[0].lastUsed, undefined);
// 後片付け
await api('/i/2fa/unregister', {
password,
token: otpToken(registerResponse.body.secret),
}, alice);
});
test('が設定でき、設定したセキュリティキーを削除できる。', async () => {
@@ -371,9 +411,10 @@ describe('2要素認証', () => {
const doneResponse = await api('/i/2fa/done', {
token: otpToken(registerResponse.body.secret),
}, alice);
assert.strictEqual(doneResponse.status, 204);
assert.strictEqual(doneResponse.status, 200);
const registerKeyResponse = await api('/i/2fa/register-key', {
token: otpToken(registerResponse.body.secret),
password,
}, alice);
assert.strictEqual(registerKeyResponse.status, 200);
@@ -381,10 +422,10 @@ describe('2要素認証', () => {
const keyName = 'example-key';
const credentialId = crypto.randomBytes(0x41);
const keyDoneResponse = await api('/i/2fa/key-done', keyDoneParam({
token: otpToken(registerResponse.body.secret),
keyName,
challengeId: registerKeyResponse.body.challengeId,
challenge: registerKeyResponse.body.challenge,
credentialId,
creationOptions: registerKeyResponse.body,
}), alice);
assert.strictEqual(keyDoneResponse.status, 200);
@@ -394,6 +435,7 @@ describe('2要素認証', () => {
assert.strictEqual(iResponse.status, 200);
for (const key of iResponse.body.securityKeysList) {
const removeKeyResponse = await api('/i/2fa/remove-key', {
token: otpToken(registerResponse.body.secret),
password,
credentialId: key.id,
}, alice);
@@ -412,6 +454,12 @@ describe('2要素認証', () => {
});
assert.strictEqual(signinResponse.status, 200);
assert.notEqual(signinResponse.body.i, undefined);
// 後片付け
await api('/i/2fa/unregister', {
password,
token: otpToken(registerResponse.body.secret),
}, alice);
});
test('が設定でき、設定解除できる。(パスワードのみでログインできる。)', async () => {
@@ -423,7 +471,7 @@ describe('2要素認証', () => {
const doneResponse = await api('/i/2fa/done', {
token: otpToken(registerResponse.body.secret),
}, alice);
assert.strictEqual(doneResponse.status, 204);
assert.strictEqual(doneResponse.status, 200);
const usersShowResponse = await api('/users/show', {
username,
@@ -432,6 +480,7 @@ describe('2要素認証', () => {
assert.strictEqual(usersShowResponse.body.twoFactorEnabled, true);
const unregisterResponse = await api('/i/2fa/unregister', {
token: otpToken(registerResponse.body.secret),
password,
}, alice);
assert.strictEqual(unregisterResponse.status, 204);
@@ -441,5 +490,11 @@ describe('2要素認証', () => {
});
assert.strictEqual(signinResponse.status, 200);
assert.notEqual(signinResponse.body.i, undefined);
// 後片付け
await api('/i/2fa/unregister', {
password,
token: otpToken(registerResponse.body.secret),
}, alice);
});
});

View File

@@ -721,7 +721,7 @@ describe('クリップ', () => {
await addNote({ clipId: aliceClip.id, noteId: aliceNote.id });
const res = await show({ clipId: aliceClip.id });
assert.strictEqual(res.lastClippedAt, new Date(res.lastClippedAt ?? '').toISOString());
assert.deepStrictEqual(await notes({ clipId: aliceClip.id }), [aliceNote]);
assert.deepStrictEqual((await notes({ clipId: aliceClip.id })).map(x => x.id), [aliceNote.id]);
// 他人の非公開ノートも突っ込める
await addNote({ clipId: aliceClip.id, noteId: bobHomeNote.id });
@@ -861,8 +861,8 @@ describe('クリップ', () => {
bobNote, bobHomeNote,
];
assert.deepStrictEqual(
res.sort(compareBy(s => s.id)),
expects.sort(compareBy(s => s.id)));
res.sort(compareBy(s => s.id)).map(x => x.id),
expects.sort(compareBy(s => s.id)).map(x => x.id));
});
test('を始端IDとlimitで取得できる。', async () => {
@@ -881,8 +881,8 @@ describe('クリップ', () => {
// Promise.allで返ってくる配列はID順で並んでないのでソートして厳密比較
const expects = [noteList[3], noteList[4], noteList[5]];
assert.deepStrictEqual(
res.sort(compareBy(s => s.id)),
expects.sort(compareBy(s => s.id)));
res.sort(compareBy(s => s.id)).map(x => x.id),
expects.sort(compareBy(s => s.id)).map(x => x.id));
});
test('をID範囲指定で取得できる。', async () => {
@@ -901,8 +901,8 @@ describe('クリップ', () => {
// Promise.allで返ってくる配列はID順で並んでないのでソートして厳密比較
const expects = [noteList[2], noteList[3]];
assert.deepStrictEqual(
res.sort(compareBy(s => s.id)),
expects.sort(compareBy(s => s.id)));
res.sort(compareBy(s => s.id)).map(x => x.id),
expects.sort(compareBy(s => s.id)).map(x => x.id));
});
test.todo('Remoteのートもクリップできる。どうテストしよう');
@@ -911,7 +911,7 @@ describe('クリップ', () => {
const bobClip = await create({ isPublic: true }, { user: bob } );
await addNote({ clipId: bobClip.id, noteId: aliceNote.id }, { user: bob });
const res = await notes({ clipId: bobClip.id });
assert.deepStrictEqual(res, [aliceNote]);
assert.deepStrictEqual(res.map(x => x.id), [aliceNote.id]);
});
test('はPublicなクリップなら認証なしでも取得できる。(非公開ートはhideされて返ってくる)', async () => {
@@ -928,8 +928,8 @@ describe('クリップ', () => {
hiddenNote(aliceFollowersNote), hiddenNote(aliceSpecifiedNote),
];
assert.deepStrictEqual(
res.sort(compareBy(s => s.id)),
expects.sort(compareBy(s => s.id)));
res.sort(compareBy(s => s.id)).map(x => x.id),
expects.sort(compareBy(s => s.id)).map(x => x.id));
});
test.todo('ブロック、ミュートされたユーザーからの設定取得etc.');

View File

@@ -9,7 +9,7 @@ import * as assert from 'assert';
// node-fetch only supports it's own Blob yet
// https://github.com/node-fetch/node-fetch/pull/1664
import { Blob } from 'node-fetch';
import { User } from '@/models/index.js';
import { MiUser } from '@/models/_.js';
import { startServer, signup, post, api, uploadFile, simpleGet, initTestDb } from '../utils.js';
import type { INestApplicationContext } from '@nestjs/common';
import type * as misskey from 'misskey-js';
@@ -298,7 +298,7 @@ describe('Endpoints', () => {
assert.strictEqual(res.status, 200);
const connection = await initTestDb(true);
const Users = connection.getRepository(User);
const Users = connection.getRepository(MiUser);
const newBob = await Users.findOneByOrFail({ id: bob.id });
assert.strictEqual(newBob.followersCount, 0);
assert.strictEqual(newBob.followingCount, 1);
@@ -360,7 +360,7 @@ describe('Endpoints', () => {
assert.strictEqual(res.status, 200);
const connection = await initTestDb(true);
const Users = connection.getRepository(User);
const Users = connection.getRepository(MiUser);
const newBob = await Users.findOneByOrFail({ id: bob.id });
assert.strictEqual(newBob.followersCount, 0);
assert.strictEqual(newBob.followingCount, 0);

View File

@@ -34,6 +34,8 @@ describe('Webリソース', () => {
let aliceGalleryPost: any;
let aliceChannel: any;
let bob: misskey.entities.MeSignup;
type Request = {
path: string,
accept?: string,
@@ -90,6 +92,8 @@ describe('Webリソース', () => {
fileIds: [aliceUploadedFile.body.id],
});
aliceChannel = await channel(alice, {});
bob = await signup({ username: 'alice' });
}, 1000 * 60 * 2);
afterAll(async () => {
@@ -163,9 +167,15 @@ describe('Webリソース', () => {
});
describe.each([{ path: '/queue' }])('$path', ({ path }) => {
test('はログインしないとGETできない。', async () => await notOk({
path,
status: 401,
}));
test('はadminでなければGETできない。', async () => await notOk({
path,
status: 500, // FIXME? 403ではない。
cookie: cookie(bob),
status: 403,
}));
test('はadminならGETできる。', async () => await ok({

View File

@@ -7,7 +7,7 @@ process.env.NODE_ENV = 'test';
import * as assert from 'assert';
import { loadConfig } from '@/config.js';
import { User, UsersRepository } from '@/models/index.js';
import { MiUser, UsersRepository } from '@/models/_.js';
import { jobQueue } from '@/boot/common.js';
import { secureRndstr } from '@/misc/secure-rndstr.js';
import { uploadFile, signup, startServer, initTestDb, api, sleep, successfulApiCall } from '../utils.js';
@@ -42,7 +42,7 @@ describe('Account Move', () => {
dave = await signup({ username: 'dave' });
eve = await signup({ username: 'eve' });
frank = await signup({ username: 'frank' });
Users = connection.getRepository(User);
Users = connection.getRepository(MiUser);
}, 1000 * 60 * 2);
afterAll(async () => {

View File

@@ -6,7 +6,7 @@
process.env.NODE_ENV = 'test';
import * as assert from 'assert';
import { Note } from '@/models/entities/Note.js';
import { MiNote } from '@/models/Note.js';
import { MAX_NOTE_TEXT_LENGTH } from '@/const.js';
import { signup, post, uploadUrl, startServer, initTestDb, api, uploadFile } from '../utils.js';
import type { INestApplicationContext } from '@nestjs/common';
@@ -22,7 +22,7 @@ describe('Note', () => {
beforeAll(async () => {
app = await startServer();
const connection = await initTestDb(true);
Notes = connection.getRepository(Note);
Notes = connection.getRepository(MiNote);
alice = await signup({ username: 'alice' });
bob = await signup({ username: 'bob' });
}, 1000 * 60 * 2);

View File

@@ -6,7 +6,7 @@
process.env.NODE_ENV = 'test';
import * as assert from 'assert';
import { Following } from '@/models/entities/Following.js';
import { MiFollowing } from '@/models/Following.js';
import { connectStream, signup, api, post, startServer, initTestDb, waitFire } from '../utils.js';
import type { INestApplicationContext } from '@nestjs/common';
import type * as misskey from 'misskey-js';
@@ -46,7 +46,7 @@ describe('Streaming', () => {
beforeAll(async () => {
app = await startServer();
const connection = await initTestDb(true);
Followings = connection.getRepository(Following);
Followings = connection.getRepository(MiFollowing);
ayano = await signup({ username: 'ayano' });
kyoko = await signup({ username: 'kyoko' });

View File

@@ -102,6 +102,7 @@ describe('ユーザー', () => {
birthday: user.birthday,
lang: user.lang,
fields: user.fields,
verifiedLinks: user.verifiedLinks,
followersCount: user.followersCount,
followingCount: user.followingCount,
notesCount: user.notesCount,
@@ -131,6 +132,7 @@ describe('ユーザー', () => {
isBlocked: user.isBlocked ?? false,
isMuted: user.isMuted ?? false,
isRenoteMuted: user.isRenoteMuted ?? false,
notify: user.notify ?? 'none',
});
};
@@ -152,6 +154,7 @@ describe('ユーザー', () => {
preventAiLearning: user.preventAiLearning,
isExplorable: user.isExplorable,
isDeleted: user.isDeleted,
twoFactorBackupCodesStock: user.twoFactorBackupCodesStock,
hideOnlineStatus: user.hideOnlineStatus,
hasUnreadSpecifiedNotes: user.hasUnreadSpecifiedNotes,
hasUnreadMentions: user.hasUnreadMentions,
@@ -368,6 +371,7 @@ describe('ユーザー', () => {
assert.strictEqual(response.birthday, null);
assert.strictEqual(response.lang, null);
assert.deepStrictEqual(response.fields, []);
assert.deepStrictEqual(response.verifiedLinks, []);
assert.strictEqual(response.followersCount, 0);
assert.strictEqual(response.followingCount, 0);
assert.strictEqual(response.notesCount, 0);
@@ -398,6 +402,7 @@ describe('ユーザー', () => {
assert.strictEqual(response.preventAiLearning, true);
assert.strictEqual(response.isExplorable, true);
assert.strictEqual(response.isDeleted, false);
assert.strictEqual(response.twoFactorBackupCodesStock, 'none');
assert.strictEqual(response.hideOnlineStatus, false);
assert.strictEqual(response.hasUnreadSpecifiedNotes, false);
assert.strictEqual(response.hasUnreadMentions, false);
@@ -490,7 +495,7 @@ describe('ユーザー', () => {
{ parameters: (): object => ({ mutedWords: [] }) },
{ parameters: (): object => ({ mutedInstances: ['xxxx.xxxxx'] }) },
{ parameters: (): object => ({ mutedInstances: [] }) },
{ parameters: (): object => ({ mutingNotificationTypes: ['follow', 'mention', 'reply', 'renote', 'quote', 'reaction', 'pollEnded', 'receiveFollowRequest', 'followRequestAccepted', 'achievementEarned', 'app'] }) },
{ parameters: (): object => ({ mutingNotificationTypes: ['note', 'follow', 'mention', 'reply', 'renote', 'quote', 'reaction', 'pollEnded', 'receiveFollowRequest', 'followRequestAccepted', 'achievementEarned', 'app'] }) },
{ parameters: (): object => ({ mutingNotificationTypes: [] }) },
{ parameters: (): object => ({ emailNotificationTypes: ['mention', 'reply', 'quote', 'follow', 'receiveFollowRequest'] }) },
{ parameters: (): object => ({ emailNotificationTypes: [] }) },