-
-
- {{ message }}
-
-
-
-
- {{ i18n.ts.continueOnRemote }}
-
-
-
-
-
-
-
- @
- @{{ host }}
-
-
-
-
-
-
-
-
-
- {{ signing ? i18n.ts.loggingIn : i18n.ts.login }}
-
-
-
-
{{ i18n.ts.useSecurityKey }}
-
- {{ i18n.ts.retry }}
-
-
-
-
-
- {{ i18n.ts.token }} ({{ i18n.ts['2fa'] }})
-
-
-
- {{ signing ? i18n.ts.loggingIn : i18n.ts.login }}
-
-
-
-
-
-
- {{ signing ? i18n.ts.loggingIn : i18n.ts.signinWithPasskey }}
-
-
{{ i18n.ts.useSecurityKey }}
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
+
-
diff --git a/packages/frontend/src/components/MkSigninDialog.vue b/packages/frontend/src/components/MkSigninDialog.vue
index d48780e9de..8351d7d5e0 100644
--- a/packages/frontend/src/components/MkSigninDialog.vue
+++ b/packages/frontend/src/components/MkSigninDialog.vue
@@ -4,26 +4,29 @@ SPDX-License-Identifier: AGPL-3.0-only
-->
-
- {{ i18n.ts.login }}
-
-
-
-
-
+
+
+
{{ i18n.ts.login }}
+
+
+
+
+
+
+
+
+
diff --git a/packages/misskey-js/etc/misskey-js.api.md b/packages/misskey-js/etc/misskey-js.api.md
index 5f4792eb74..9ad784c296 100644
--- a/packages/misskey-js/etc/misskey-js.api.md
+++ b/packages/misskey-js/etc/misskey-js.api.md
@@ -3040,7 +3040,7 @@ type Signin = components['schemas']['Signin'];
// @public (undocumented)
type SigninRequest = {
username: string;
- password: string;
+ password?: string;
token?: string;
credential?: AuthenticationResponseJSON;
'hcaptcha-response'?: string | null;
diff --git a/packages/misskey-js/src/autogen/types.ts b/packages/misskey-js/src/autogen/types.ts
index 32646d28ed..3876a0bfe5 100644
--- a/packages/misskey-js/src/autogen/types.ts
+++ b/packages/misskey-js/src/autogen/types.ts
@@ -3782,16 +3782,13 @@ export type components = {
followingVisibility: 'public' | 'followers' | 'private';
/** @enum {string} */
followersVisibility: 'public' | 'followers' | 'private';
- /** @default false */
- twoFactorEnabled: boolean;
- /** @default false */
- usePasswordLessLogin: boolean;
- /** @default false */
- securityKeys: boolean;
roles: components['schemas']['RoleLite'][];
followedMessage?: string | null;
memo: string | null;
moderationNote?: string;
+ twoFactorEnabled?: boolean;
+ usePasswordLessLogin?: boolean;
+ securityKeys?: boolean;
isFollowing?: boolean;
isFollowed?: boolean;
hasPendingFollowRequestFromYou?: boolean;
@@ -3972,6 +3969,12 @@ export type components = {
}[];
loggedInDays: number;
policies: components['schemas']['RolePolicies'];
+ /** @default false */
+ twoFactorEnabled: boolean;
+ /** @default false */
+ usePasswordLessLogin: boolean;
+ /** @default false */
+ securityKeys: boolean;
email?: string | null;
emailVerified?: boolean | null;
securityKeysList?: {
diff --git a/packages/misskey-js/src/entities.ts b/packages/misskey-js/src/entities.ts
index 36b7f5bca3..98ac50e5a1 100644
--- a/packages/misskey-js/src/entities.ts
+++ b/packages/misskey-js/src/entities.ts
@@ -269,7 +269,7 @@ export type SignupPendingResponse = {
export type SigninRequest = {
username: string;
- password: string;
+ password?: string;
token?: string;
credential?: AuthenticationResponseJSON;
'hcaptcha-response'?: string | null;
--
cgit v1.2.3-freya
From ae3c155490d9b5a574c45309744ba2a0cbe78932 Mon Sep 17 00:00:00 2001
From: かっこかり <67428053+kakkokari-gtyih@users.noreply.github.com>
Date: Sat, 5 Oct 2024 12:03:47 +0900
Subject: fix: signin
の資格情報が足りないだけの場合はエラーにせず200を返すように (#14700)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
* fix: signin の資格情報が足りないだけの場合はエラーにせず200を返すように
* run api extractor
* fix
* fix
* fix test
* /signin -> /signin-flow
* fix
* fix lint
* rename
* fix
* fix
---
cypress/e2e/basic.cy.ts | 2 +-
cypress/support/commands.ts | 2 +-
.../backend/src/server/api/ApiServerService.ts | 2 +-
.../backend/src/server/api/SigninApiService.ts | 66 ++----
packages/backend/src/server/api/SigninService.ts | 6 +-
packages/backend/test/e2e/2fa.ts | 73 +++----
packages/backend/test/e2e/endpoints.ts | 8 +-
packages/frontend/src/components/MkSignin.vue | 236 +++++++++++----------
.../src/components/MkSignupDialog.form.vue | 11 +-
.../frontend/src/components/MkSignupDialog.vue | 4 +-
packages/misskey-js/etc/misskey-js.api.md | 24 ++-
packages/misskey-js/src/api.types.ts | 10 +-
packages/misskey-js/src/entities.ts | 22 +-
13 files changed, 231 insertions(+), 235 deletions(-)
(limited to 'packages/backend/src/server/api')
diff --git a/cypress/e2e/basic.cy.ts b/cypress/e2e/basic.cy.ts
index c9d7e0a24a..d2efbf709c 100644
--- a/cypress/e2e/basic.cy.ts
+++ b/cypress/e2e/basic.cy.ts
@@ -120,7 +120,7 @@ describe('After user signup', () => {
it('signin', () => {
cy.visitHome();
- cy.intercept('POST', '/api/signin').as('signin');
+ cy.intercept('POST', '/api/signin-flow').as('signin');
cy.get('[data-cy-signin]').click();
diff --git a/cypress/support/commands.ts b/cypress/support/commands.ts
index ed5cda31b0..197ff963ac 100644
--- a/cypress/support/commands.ts
+++ b/cypress/support/commands.ts
@@ -55,7 +55,7 @@ Cypress.Commands.add('registerUser', (username, password, isAdmin = false) => {
Cypress.Commands.add('login', (username, password) => {
cy.visitHome();
- cy.intercept('POST', '/api/signin').as('signin');
+ cy.intercept('POST', '/api/signin-flow').as('signin');
cy.get('[data-cy-signin]').click();
cy.get('[data-cy-signin-page-input]').should('be.visible', { timeout: 1000 });
diff --git a/packages/backend/src/server/api/ApiServerService.ts b/packages/backend/src/server/api/ApiServerService.ts
index 356e145681..6b760c258b 100644
--- a/packages/backend/src/server/api/ApiServerService.ts
+++ b/packages/backend/src/server/api/ApiServerService.ts
@@ -133,7 +133,7 @@ export class ApiServerService {
'turnstile-response'?: string;
'm-captcha-response'?: string;
};
- }>('/signin', (request, reply) => this.signinApiService.signin(request, reply));
+ }>('/signin-flow', (request, reply) => this.signinApiService.signin(request, reply));
fastify.post<{
Body: {
diff --git a/packages/backend/src/server/api/SigninApiService.ts b/packages/backend/src/server/api/SigninApiService.ts
index 81684beb3c..0d24ffa56a 100644
--- a/packages/backend/src/server/api/SigninApiService.ts
+++ b/packages/backend/src/server/api/SigninApiService.ts
@@ -5,8 +5,8 @@
import { Inject, Injectable } from '@nestjs/common';
import bcrypt from 'bcryptjs';
-import * as OTPAuth from 'otpauth';
import { IsNull } from 'typeorm';
+import * as Misskey from 'misskey-js';
import { DI } from '@/di-symbols.js';
import type {
MiMeta,
@@ -26,27 +26,9 @@ import { CaptchaService } from '@/core/CaptchaService.js';
import { FastifyReplyError } from '@/misc/fastify-reply-error.js';
import { RateLimiterService } from './RateLimiterService.js';
import { SigninService } from './SigninService.js';
-import type { AuthenticationResponseJSON, PublicKeyCredentialRequestOptionsJSON } from '@simplewebauthn/types';
+import type { AuthenticationResponseJSON } from '@simplewebauthn/types';
import type { FastifyReply, FastifyRequest } from 'fastify';
-/**
- * next を指定すると、次にクライアント側で行うべき処理を指定できる。
- *
- * - `captcha`: パスワードと、(有効になっている場合は)CAPTCHAを求める
- * - `password`: パスワードを求める
- * - `totp`: ワンタイムパスワードを求める
- * - `passkey`: WebAuthn認証を求める(WebAuthnに対応していないブラウザの場合はワンタイムパスワード)
- */
-
-type SigninErrorResponse = {
- id: string;
- next?: 'captcha' | 'password' | 'totp';
-} | {
- id: string;
- next: 'passkey';
- authRequest: PublicKeyCredentialRequestOptionsJSON;
-};
-
@Injectable()
export class SigninApiService {
constructor(
@@ -101,7 +83,7 @@ export class SigninApiService {
const password = body['password'];
const token = body['token'];
- function error(status: number, error: SigninErrorResponse) {
+ function error(status: number, error: { id: string }) {
reply.code(status);
return { error };
}
@@ -152,21 +134,17 @@ export class SigninApiService {
const securityKeysAvailable = await this.userSecurityKeysRepository.countBy({ userId: user.id }).then(result => result >= 1);
if (password == null) {
- reply.code(403);
+ reply.code(200);
if (profile.twoFactorEnabled) {
return {
- error: {
- id: '144ff4f8-bd6c-41bc-82c3-b672eb09efbf',
- next: 'password',
- },
- } satisfies { error: SigninErrorResponse };
+ finished: false,
+ next: 'password',
+ } satisfies Misskey.entities.SigninFlowResponse;
} else {
return {
- error: {
- id: '144ff4f8-bd6c-41bc-82c3-b672eb09efbf',
- next: 'captcha',
- },
- } satisfies { error: SigninErrorResponse };
+ finished: false,
+ next: 'captcha',
+ } satisfies Misskey.entities.SigninFlowResponse;
}
}
@@ -178,7 +156,7 @@ export class SigninApiService {
// Compare password
const same = await bcrypt.compare(password, profile.password!);
- const fail = async (status?: number, failure?: SigninErrorResponse) => {
+ const fail = async (status?: number, failure?: { id: string; }) => {
// Append signin history
await this.signinsRepository.insert({
id: this.idService.gen(),
@@ -268,27 +246,23 @@ export class SigninApiService {
const authRequest = await this.webAuthnService.initiateAuthentication(user.id);
- reply.code(403);
+ reply.code(200);
return {
- error: {
- id: '06e661b9-8146-4ae3-bde5-47138c0ae0c4',
- next: 'passkey',
- authRequest,
- },
- } satisfies { error: SigninErrorResponse };
+ finished: false,
+ next: 'passkey',
+ authRequest,
+ } satisfies Misskey.entities.SigninFlowResponse;
} else {
if (!same || !profile.twoFactorEnabled) {
return await fail(403, {
id: '932c904e-9460-45b7-9ce6-7ed33be7eb2c',
});
} else {
- reply.code(403);
+ reply.code(200);
return {
- error: {
- id: '144ff4f8-bd6c-41bc-82c3-b672eb09efbf',
- next: 'totp',
- },
- } satisfies { error: SigninErrorResponse };
+ finished: false,
+ next: 'totp',
+ } satisfies Misskey.entities.SigninFlowResponse;
}
}
// never get here
diff --git a/packages/backend/src/server/api/SigninService.ts b/packages/backend/src/server/api/SigninService.ts
index 4b041f373f..640356b50c 100644
--- a/packages/backend/src/server/api/SigninService.ts
+++ b/packages/backend/src/server/api/SigninService.ts
@@ -4,6 +4,7 @@
*/
import { Inject, Injectable } from '@nestjs/common';
+import * as Misskey from 'misskey-js';
import { DI } from '@/di-symbols.js';
import type { SigninsRepository, UserProfilesRepository } from '@/models/_.js';
import { IdService } from '@/core/IdService.js';
@@ -57,9 +58,10 @@ export class SigninService {
reply.code(200);
return {
+ finished: true,
id: user.id,
- i: user.token,
- };
+ i: user.token!,
+ } satisfies Misskey.entities.SigninFlowResponse;
}
}
diff --git a/packages/backend/test/e2e/2fa.ts b/packages/backend/test/e2e/2fa.ts
index 88c32b4346..48e1bababb 100644
--- a/packages/backend/test/e2e/2fa.ts
+++ b/packages/backend/test/e2e/2fa.ts
@@ -136,7 +136,7 @@ describe('2要素認証', () => {
keyName: string,
credentialId: Buffer,
requestOptions: PublicKeyCredentialRequestOptionsJSON,
- }): misskey.entities.SigninRequest => {
+ }): misskey.entities.SigninFlowRequest => {
// AuthenticatorAssertionResponse.authenticatorData
// https://developer.mozilla.org/en-US/docs/Web/API/AuthenticatorAssertionResponse/authenticatorData
const authenticatorData = Buffer.concat([
@@ -196,22 +196,21 @@ describe('2要素認証', () => {
}, alice);
assert.strictEqual(doneResponse.status, 200);
- const signinWithoutTokenResponse = await api('signin', {
+ const signinWithoutTokenResponse = await api('signin-flow', {
...signinParam(),
});
- assert.strictEqual(signinWithoutTokenResponse.status, 403);
+ assert.strictEqual(signinWithoutTokenResponse.status, 200);
assert.deepStrictEqual(signinWithoutTokenResponse.body, {
- error: {
- id: '144ff4f8-bd6c-41bc-82c3-b672eb09efbf',
- next: 'totp',
- },
+ finished: false,
+ next: 'totp',
});
- const signinResponse = await api('signin', {
+ const signinResponse = await api('signin-flow', {
...signinParam(),
token: otpToken(registerResponse.body.secret),
});
assert.strictEqual(signinResponse.status, 200);
+ assert.strictEqual(signinResponse.body.finished, true);
assert.notEqual(signinResponse.body.i, undefined);
// 後片付け
@@ -252,29 +251,23 @@ describe('2要素認証', () => {
assert.strictEqual(keyDoneResponse.body.id, credentialId.toString('base64url'));
assert.strictEqual(keyDoneResponse.body.name, keyName);
- const signinResponse = await api('signin', {
+ const signinResponse = await api('signin-flow', {
...signinParam(),
});
- const signinResponseBody = signinResponse.body as unknown as {
- error: {
- id: string;
- next: 'passkey';
- authRequest: PublicKeyCredentialRequestOptionsJSON;
- };
- };
- assert.strictEqual(signinResponse.status, 403);
- assert.strictEqual(signinResponseBody.error.id, '06e661b9-8146-4ae3-bde5-47138c0ae0c4');
- assert.strictEqual(signinResponseBody.error.next, 'passkey');
- assert.notEqual(signinResponseBody.error.authRequest.challenge, undefined);
- assert.notEqual(signinResponseBody.error.authRequest.allowCredentials, undefined);
- assert.strictEqual(signinResponseBody.error.authRequest.allowCredentials && signinResponseBody.error.authRequest.allowCredentials[0]?.id, credentialId.toString('base64url'));
-
- const signinResponse2 = await api('signin', signinWithSecurityKeyParam({
+ assert.strictEqual(signinResponse.status, 200);
+ assert.strictEqual(signinResponse.body.finished, false);
+ assert.strictEqual(signinResponse.body.next, 'passkey');
+ assert.notEqual(signinResponse.body.authRequest.challenge, undefined);
+ assert.notEqual(signinResponse.body.authRequest.allowCredentials, undefined);
+ assert.strictEqual(signinResponse.body.authRequest.allowCredentials && signinResponse.body.authRequest.allowCredentials[0]?.id, credentialId.toString('base64url'));
+
+ const signinResponse2 = await api('signin-flow', signinWithSecurityKeyParam({
keyName,
credentialId,
- requestOptions: signinResponseBody.error.authRequest,
+ requestOptions: signinResponse.body.authRequest,
}));
assert.strictEqual(signinResponse2.status, 200);
+ assert.strictEqual(signinResponse2.body.finished, true);
assert.notEqual(signinResponse2.body.i, undefined);
// 後片付け
@@ -320,32 +313,26 @@ describe('2要素認証', () => {
assert.strictEqual(iResponse.status, 200);
assert.strictEqual(iResponse.body.usePasswordLessLogin, true);
- const signinResponse = await api('signin', {
+ const signinResponse = await api('signin-flow', {
...signinParam(),
password: '',
});
- const signinResponseBody = signinResponse.body as unknown as {
- error: {
- id: string;
- next: 'passkey';
- authRequest: PublicKeyCredentialRequestOptionsJSON;
- };
- };
- assert.strictEqual(signinResponse.status, 403);
- assert.strictEqual(signinResponseBody.error.id, '06e661b9-8146-4ae3-bde5-47138c0ae0c4');
- assert.strictEqual(signinResponseBody.error.next, 'passkey');
- assert.notEqual(signinResponseBody.error.authRequest.challenge, undefined);
- assert.notEqual(signinResponseBody.error.authRequest.allowCredentials, undefined);
+ assert.strictEqual(signinResponse.status, 200);
+ assert.strictEqual(signinResponse.body.finished, false);
+ assert.strictEqual(signinResponse.body.next, 'passkey');
+ assert.notEqual(signinResponse.body.authRequest.challenge, undefined);
+ assert.notEqual(signinResponse.body.authRequest.allowCredentials, undefined);
- const signinResponse2 = await api('signin', {
+ const signinResponse2 = await api('signin-flow', {
...signinWithSecurityKeyParam({
keyName,
credentialId,
- requestOptions: signinResponseBody.error.authRequest,
+ requestOptions: signinResponse.body.authRequest,
} as any),
password: '',
});
assert.strictEqual(signinResponse2.status, 200);
+ assert.strictEqual(signinResponse2.body.finished, true);
assert.notEqual(signinResponse2.body.i, undefined);
// 後片付け
@@ -450,11 +437,12 @@ describe('2要素認証', () => {
assert.strictEqual(afterIResponse.status, 200);
assert.strictEqual(afterIResponse.body.securityKeys, false);
- const signinResponse = await api('signin', {
+ const signinResponse = await api('signin-flow', {
...signinParam(),
token: otpToken(registerResponse.body.secret),
});
assert.strictEqual(signinResponse.status, 200);
+ assert.strictEqual(signinResponse.body.finished, true);
assert.notEqual(signinResponse.body.i, undefined);
// 後片付け
@@ -485,10 +473,11 @@ describe('2要素認証', () => {
}, alice);
assert.strictEqual(unregisterResponse.status, 204);
- const signinResponse = await api('signin', {
+ const signinResponse = await api('signin-flow', {
...signinParam(),
});
assert.strictEqual(signinResponse.status, 200);
+ assert.strictEqual(signinResponse.body.finished, true);
assert.notEqual(signinResponse.body.i, undefined);
// 後片付け
diff --git a/packages/backend/test/e2e/endpoints.ts b/packages/backend/test/e2e/endpoints.ts
index 5aaec7f6f9..b91d77c398 100644
--- a/packages/backend/test/e2e/endpoints.ts
+++ b/packages/backend/test/e2e/endpoints.ts
@@ -66,9 +66,9 @@ describe('Endpoints', () => {
});
});
- describe('signin', () => {
+ describe('signin-flow', () => {
test('間違ったパスワードでサインインできない', async () => {
- const res = await api('signin', {
+ const res = await api('signin-flow', {
username: 'test1',
password: 'bar',
});
@@ -77,7 +77,7 @@ describe('Endpoints', () => {
});
test('クエリをインジェクションできない', async () => {
- const res = await api('signin', {
+ const res = await api('signin-flow', {
username: 'test1',
// @ts-expect-error password must be string
password: {
@@ -89,7 +89,7 @@ describe('Endpoints', () => {
});
test('正しい情報でサインインできる', async () => {
- const res = await api('signin', {
+ const res = await api('signin-flow', {
username: 'test1',
password: 'test1',
});
diff --git a/packages/frontend/src/components/MkSignin.vue b/packages/frontend/src/components/MkSignin.vue
index 03dd61f6c6..26e1ac516c 100644
--- a/packages/frontend/src/components/MkSignin.vue
+++ b/packages/frontend/src/components/MkSignin.vue
@@ -83,7 +83,7 @@ import type { AuthenticationPublicKeyCredential } from '@github/webauthn-json/br
import type { OpenOnRemoteOptions } from '@/scripts/please-login.js';
const emit = defineEmits<{
- (ev: 'login', v: Misskey.entities.SigninResponse): void;
+ (ev: 'login', v: Misskey.entities.SigninFlowResponse): void;
}>();
const props = withDefaults(defineProps<{
@@ -212,23 +212,63 @@ async function onTotpSubmitted(token: string) {
}
}
-async function tryLogin(req: Partial
): Promise {
+async function tryLogin(req: Partial): Promise {
const _req = {
username: req.username ?? userInfo.value?.username,
...req,
};
- function assertIsSigninRequest(x: Partial): x is Misskey.entities.SigninRequest {
+ function assertIsSigninFlowRequest(x: Partial): x is Misskey.entities.SigninFlowRequest {
return x.username != null;
}
- if (!assertIsSigninRequest(_req)) {
+ if (!assertIsSigninFlowRequest(_req)) {
throw new Error('Invalid request');
}
- return await misskeyApi('signin', _req).then(async (res) => {
- emit('login', res);
- await onLoginSucceeded(res);
+ return await misskeyApi('signin-flow', _req).then(async (res) => {
+ if (res.finished) {
+ emit('login', res);
+ await onLoginSucceeded(res);
+ } else {
+ switch (res.next) {
+ case 'captcha': {
+ needCaptcha.value = true;
+ page.value = 'password';
+ break;
+ }
+ case 'password': {
+ needCaptcha.value = false;
+ page.value = 'password';
+ break;
+ }
+ case 'totp': {
+ page.value = 'totp';
+ break;
+ }
+ case 'passkey': {
+ if (webAuthnSupported()) {
+ credentialRequest.value = parseRequestOptionsFromJSON({
+ publicKey: res.authRequest,
+ });
+ page.value = 'passkey';
+ } else {
+ page.value = 'totp';
+ }
+ break;
+ }
+ }
+
+ if (doingPasskeyFromInputPage.value === true) {
+ doingPasskeyFromInputPage.value = false;
+ page.value = 'input';
+ password.value = '';
+ }
+ passwordPageEl.value?.resetCaptcha();
+ nextTick(() => {
+ waiting.value = false;
+ });
+ }
return res;
}).catch((err) => {
onSigninApiError(err);
@@ -236,7 +276,7 @@ async function tryLogin(req: Partial): Promise();
@@ -269,14 +269,19 @@ async function onSubmit(): Promise {
});
emit('signupEmailPending');
} else {
- const res = await misskeyApi('signin', {
+ const res = await misskeyApi('signin-flow', {
username: username.value,
password: password.value,
});
emit('signup', res);
- if (props.autoSet) {
+ if (props.autoSet && res.finished) {
return login(res.i);
+ } else {
+ os.alert({
+ type: 'error',
+ text: i18n.ts.somethingHappened,
+ });
}
}
} catch {
diff --git a/packages/frontend/src/components/MkSignupDialog.vue b/packages/frontend/src/components/MkSignupDialog.vue
index 97310d32a6..4cccd99492 100644
--- a/packages/frontend/src/components/MkSignupDialog.vue
+++ b/packages/frontend/src/components/MkSignupDialog.vue
@@ -47,7 +47,7 @@ const props = withDefaults(defineProps<{
});
const emit = defineEmits<{
- (ev: 'done', res: Misskey.entities.SigninResponse): void;
+ (ev: 'done', res: Misskey.entities.SigninFlowResponse): void;
(ev: 'closed'): void;
}>();
@@ -55,7 +55,7 @@ const dialog = shallowRef>();
const isAcceptedServerRule = ref(false);
-function onSignup(res: Misskey.entities.SigninResponse) {
+function onSignup(res: Misskey.entities.SigninFlowResponse) {
emit('done', res);
dialog.value?.close();
}
diff --git a/packages/misskey-js/etc/misskey-js.api.md b/packages/misskey-js/etc/misskey-js.api.md
index 9ad784c296..732352abd8 100644
--- a/packages/misskey-js/etc/misskey-js.api.md
+++ b/packages/misskey-js/etc/misskey-js.api.md
@@ -1158,9 +1158,9 @@ export type Endpoints = Overwrite> = T[keyof T];
--
cgit v1.2.3-freya
From d8bf1ff7e9ab4d39b2e924bf7eae010e9b9e21f0 Mon Sep 17 00:00:00 2001
From: かっこかり <67428053+kakkokari-gtyih@users.noreply.github.com>
Date: Sat, 5 Oct 2024 13:47:50 +0900
Subject: #14675 レビューの修正 (#14705)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
packages/backend/src/server/api/ApiServerService.ts | 2 +-
packages/frontend/src/components/MkFukidashi.vue | 6 +++---
2 files changed, 4 insertions(+), 4 deletions(-)
(limited to 'packages/backend/src/server/api')
diff --git a/packages/backend/src/server/api/ApiServerService.ts b/packages/backend/src/server/api/ApiServerService.ts
index 6b760c258b..be63635efe 100644
--- a/packages/backend/src/server/api/ApiServerService.ts
+++ b/packages/backend/src/server/api/ApiServerService.ts
@@ -125,7 +125,7 @@ export class ApiServerService {
fastify.post<{
Body: {
username: string;
- password: string;
+ password?: string;
token?: string;
credential?: AuthenticationResponseJSON;
'hcaptcha-response'?: string;
diff --git a/packages/frontend/src/components/MkFukidashi.vue b/packages/frontend/src/components/MkFukidashi.vue
index ba82eb442f..09825487bf 100644
--- a/packages/frontend/src/components/MkFukidashi.vue
+++ b/packages/frontend/src/components/MkFukidashi.vue
@@ -8,7 +8,7 @@ SPDX-License-Identifier: AGPL-3.0-only
:class="[
$style.root,
tail === 'left' ? $style.left : $style.right,
- negativeMargin === true && $style.negativeMergin,
+ negativeMargin === true && $style.negativeMargin,
shadow === true && $style.shadow,
]"
>
@@ -54,7 +54,7 @@ withDefaults(defineProps<{
&.left {
padding-left: calc(var(--fukidashi-radius) * .13);
- &.negativeMergin {
+ &.negativeMargin {
margin-left: calc(calc(var(--fukidashi-radius) * .13) * -1);
}
}
@@ -62,7 +62,7 @@ withDefaults(defineProps<{
&.right {
padding-right: calc(var(--fukidashi-radius) * .13);
- &.negativeMergin {
+ &.negativeMargin {
margin-right: calc(calc(var(--fukidashi-radius) * .13) * -1);
}
}
--
cgit v1.2.3-freya
From 0d7d1091c8970d9979e8efb02f0accd6dcd39422 Mon Sep 17 00:00:00 2001
From: おさむのひと <46447427+samunohito@users.noreply.github.com>
Date: Sat, 5 Oct 2024 14:37:52 +0900
Subject: enhance: 人気のPlayを10件以上表示できるように (#14443)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Co-authored-by: osamu <46447427+sam-osamu@users.noreply.github.com>
---
CHANGELOG.md | 1 +
packages/backend/src/core/CoreModule.ts | 5 +
packages/backend/src/core/FlashService.ts | 40 ++++++
.../src/core/entities/FlashEntityService.ts | 41 ++++--
packages/backend/src/models/Flash.ts | 5 +-
.../src/server/api/endpoints/flash/featured.ts | 22 +--
packages/backend/test/unit/FlashService.ts | 152 +++++++++++++++++++++
packages/frontend/src/pages/flash/flash-index.vue | 3 +-
packages/misskey-js/etc/misskey-js.api.md | 4 +
packages/misskey-js/src/autogen/endpoint.ts | 3 +-
packages/misskey-js/src/autogen/entities.ts | 1 +
packages/misskey-js/src/autogen/types.ts | 10 ++
12 files changed, 262 insertions(+), 25 deletions(-)
create mode 100644 packages/backend/src/core/FlashService.ts
create mode 100644 packages/backend/test/unit/FlashService.ts
(limited to 'packages/backend/src/server/api')
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 04acc11ac3..6a9143ea1b 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -12,6 +12,7 @@
- Enhance: セキュリティ向上のため、サインイン時もCAPTCHAを求めるようになりました
- Enhance: 依存関係の更新
- Enhance: l10nの更新
+- Enhance: Playの「人気」タブで10件以上表示可能に #14399
- Fix: 連合のホワイトリストが正常に登録されない問題を修正
### Client
diff --git a/packages/backend/src/core/CoreModule.ts b/packages/backend/src/core/CoreModule.ts
index 3b3c35f976..734d135648 100644
--- a/packages/backend/src/core/CoreModule.ts
+++ b/packages/backend/src/core/CoreModule.ts
@@ -14,6 +14,7 @@ import { AbuseReportNotificationService } from '@/core/AbuseReportNotificationSe
import { SystemWebhookService } from '@/core/SystemWebhookService.js';
import { UserSearchService } from '@/core/UserSearchService.js';
import { WebhookTestService } from '@/core/WebhookTestService.js';
+import { FlashService } from '@/core/FlashService.js';
import { AccountMoveService } from './AccountMoveService.js';
import { AccountUpdateService } from './AccountUpdateService.js';
import { AiService } from './AiService.js';
@@ -217,6 +218,7 @@ const $SystemWebhookService: Provider = { provide: 'SystemWebhookService', useEx
const $WebhookTestService: Provider = { provide: 'WebhookTestService', useExisting: WebhookTestService };
const $UtilityService: Provider = { provide: 'UtilityService', useExisting: UtilityService };
const $FileInfoService: Provider = { provide: 'FileInfoService', useExisting: FileInfoService };
+const $FlashService: Provider = { provide: 'FlashService', useExisting: FlashService };
const $SearchService: Provider = { provide: 'SearchService', useExisting: SearchService };
const $ClipService: Provider = { provide: 'ClipService', useExisting: ClipService };
const $FeaturedService: Provider = { provide: 'FeaturedService', useExisting: FeaturedService };
@@ -367,6 +369,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
WebhookTestService,
UtilityService,
FileInfoService,
+ FlashService,
SearchService,
ClipService,
FeaturedService,
@@ -513,6 +516,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
$WebhookTestService,
$UtilityService,
$FileInfoService,
+ $FlashService,
$SearchService,
$ClipService,
$FeaturedService,
@@ -660,6 +664,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
WebhookTestService,
UtilityService,
FileInfoService,
+ FlashService,
SearchService,
ClipService,
FeaturedService,
diff --git a/packages/backend/src/core/FlashService.ts b/packages/backend/src/core/FlashService.ts
new file mode 100644
index 0000000000..2a98225382
--- /dev/null
+++ b/packages/backend/src/core/FlashService.ts
@@ -0,0 +1,40 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { Inject, Injectable } from '@nestjs/common';
+import { DI } from '@/di-symbols.js';
+import { type FlashsRepository } from '@/models/_.js';
+
+/**
+ * MisskeyPlay関係のService
+ */
+@Injectable()
+export class FlashService {
+ constructor(
+ @Inject(DI.flashsRepository)
+ private flashRepository: FlashsRepository,
+ ) {
+ }
+
+ /**
+ * 人気のあるPlay一覧を取得する.
+ */
+ public async featured(opts?: { offset?: number, limit: number }) {
+ const builder = this.flashRepository.createQueryBuilder('flash')
+ .andWhere('flash.likedCount > 0')
+ .andWhere('flash.visibility = :visibility', { visibility: 'public' })
+ .addOrderBy('flash.likedCount', 'DESC')
+ .addOrderBy('flash.updatedAt', 'DESC')
+ .addOrderBy('flash.id', 'DESC');
+
+ if (opts?.offset) {
+ builder.skip(opts.offset);
+ }
+
+ builder.take(opts?.limit ?? 10);
+
+ return await builder.getMany();
+ }
+}
diff --git a/packages/backend/src/core/entities/FlashEntityService.ts b/packages/backend/src/core/entities/FlashEntityService.ts
index 4aa7104c1e..0cdcf3310a 100644
--- a/packages/backend/src/core/entities/FlashEntityService.ts
+++ b/packages/backend/src/core/entities/FlashEntityService.ts
@@ -5,10 +5,8 @@
import { Inject, Injectable } from '@nestjs/common';
import { DI } from '@/di-symbols.js';
-import type { FlashsRepository, FlashLikesRepository } from '@/models/_.js';
-import { awaitAll } from '@/misc/prelude/await-all.js';
+import type { FlashLikesRepository, FlashsRepository } from '@/models/_.js';
import type { Packed } from '@/misc/json-schema.js';
-import type { } from '@/models/Blocking.js';
import type { MiUser } from '@/models/User.js';
import type { MiFlash } from '@/models/Flash.js';
import { bindThis } from '@/decorators.js';
@@ -20,10 +18,8 @@ export class FlashEntityService {
constructor(
@Inject(DI.flashsRepository)
private flashsRepository: FlashsRepository,
-
@Inject(DI.flashLikesRepository)
private flashLikesRepository: FlashLikesRepository,
-
private userEntityService: UserEntityService,
private idService: IdService,
) {
@@ -34,25 +30,36 @@ export class FlashEntityService {
src: MiFlash['id'] | MiFlash,
me?: { id: MiUser['id'] } | null | undefined,
hint?: {
- packedUser?: Packed<'UserLite'>
+ packedUser?: Packed<'UserLite'>,
+ likedFlashIds?: MiFlash['id'][],
},
): Promise> {
const meId = me ? me.id : null;
const flash = typeof src === 'object' ? src : await this.flashsRepository.findOneByOrFail({ id: src });
- return await awaitAll({
+ // { schema: 'UserDetailed' } すると無限ループするので注意
+ const user = hint?.packedUser ?? await this.userEntityService.pack(flash.user ?? flash.userId, me);
+
+ let isLiked = false;
+ if (meId) {
+ isLiked = hint?.likedFlashIds
+ ? hint.likedFlashIds.includes(flash.id)
+ : await this.flashLikesRepository.exists({ where: { flashId: flash.id, userId: meId } });
+ }
+
+ return {
id: flash.id,
createdAt: this.idService.parse(flash.id).date.toISOString(),
updatedAt: flash.updatedAt.toISOString(),
userId: flash.userId,
- user: hint?.packedUser ?? this.userEntityService.pack(flash.user ?? flash.userId, me), // { schema: 'UserDetailed' } すると無限ループするので注意
+ user: user,
title: flash.title,
summary: flash.summary,
script: flash.script,
visibility: flash.visibility,
likedCount: flash.likedCount,
- isLiked: meId ? await this.flashLikesRepository.exists({ where: { flashId: flash.id, userId: meId } }) : undefined,
- });
+ isLiked: isLiked,
+ };
}
@bindThis
@@ -63,7 +70,19 @@ export class FlashEntityService {
const _users = flashes.map(({ user, userId }) => user ?? userId);
const _userMap = await this.userEntityService.packMany(_users, me)
.then(users => new Map(users.map(u => [u.id, u])));
- return Promise.all(flashes.map(flash => this.pack(flash, me, { packedUser: _userMap.get(flash.userId) })));
+ const _likedFlashIds = me
+ ? await this.flashLikesRepository.createQueryBuilder('flashLike')
+ .select('flashLike.flashId')
+ .where('flashLike.userId = :userId', { userId: me.id })
+ .getRawMany<{ flashLike_flashId: string }>()
+ .then(likes => [...new Set(likes.map(like => like.flashLike_flashId))])
+ : [];
+ return Promise.all(
+ flashes.map(flash => this.pack(flash, me, {
+ packedUser: _userMap.get(flash.userId),
+ likedFlashIds: _likedFlashIds,
+ })),
+ );
}
}
diff --git a/packages/backend/src/models/Flash.ts b/packages/backend/src/models/Flash.ts
index a1469a0d94..5db7dca992 100644
--- a/packages/backend/src/models/Flash.ts
+++ b/packages/backend/src/models/Flash.ts
@@ -7,6 +7,9 @@ import { Entity, Index, JoinColumn, Column, PrimaryColumn, ManyToOne } from 'typ
import { id } from './util/id.js';
import { MiUser } from './User.js';
+export const flashVisibility = ['public', 'private'] as const;
+export type FlashVisibility = typeof flashVisibility[number];
+
@Entity('flash')
export class MiFlash {
@PrimaryColumn(id())
@@ -63,5 +66,5 @@ export class MiFlash {
@Column('varchar', {
length: 512, default: 'public',
})
- public visibility: 'public' | 'private';
+ public visibility: FlashVisibility;
}
diff --git a/packages/backend/src/server/api/endpoints/flash/featured.ts b/packages/backend/src/server/api/endpoints/flash/featured.ts
index c2d6ab5085..9a0cb461f2 100644
--- a/packages/backend/src/server/api/endpoints/flash/featured.ts
+++ b/packages/backend/src/server/api/endpoints/flash/featured.ts
@@ -8,6 +8,7 @@ import type { FlashsRepository } from '@/models/_.js';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { FlashEntityService } from '@/core/entities/FlashEntityService.js';
import { DI } from '@/di-symbols.js';
+import { FlashService } from '@/core/FlashService.js';
export const meta = {
tags: ['flash'],
@@ -27,26 +28,25 @@ export const meta = {
export const paramDef = {
type: 'object',
- properties: {},
+ properties: {
+ offset: { type: 'integer', minimum: 0, default: 0 },
+ limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 },
+ },
required: [],
} as const;
@Injectable()
export default class extends Endpoint { // eslint-disable-line import/no-default-export
constructor(
- @Inject(DI.flashsRepository)
- private flashsRepository: FlashsRepository,
-
+ private flashService: FlashService,
private flashEntityService: FlashEntityService,
) {
super(meta, paramDef, async (ps, me) => {
- const query = this.flashsRepository.createQueryBuilder('flash')
- .andWhere('flash.likedCount > 0')
- .orderBy('flash.likedCount', 'DESC');
-
- const flashs = await query.limit(10).getMany();
-
- return await this.flashEntityService.packMany(flashs, me);
+ const result = await this.flashService.featured({
+ offset: ps.offset,
+ limit: ps.limit,
+ });
+ return await this.flashEntityService.packMany(result, me);
});
}
}
diff --git a/packages/backend/test/unit/FlashService.ts b/packages/backend/test/unit/FlashService.ts
new file mode 100644
index 0000000000..12ffaf3421
--- /dev/null
+++ b/packages/backend/test/unit/FlashService.ts
@@ -0,0 +1,152 @@
+/* eslint-disable @typescript-eslint/no-unused-vars */
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { Test, TestingModule } from '@nestjs/testing';
+import { FlashService } from '@/core/FlashService.js';
+import { IdService } from '@/core/IdService.js';
+import { FlashsRepository, MiFlash, MiUser, UserProfilesRepository, UsersRepository } from '@/models/_.js';
+import { DI } from '@/di-symbols.js';
+import { GlobalModule } from '@/GlobalModule.js';
+
+describe('FlashService', () => {
+ let app: TestingModule;
+ let service: FlashService;
+
+ // --------------------------------------------------------------------------------------
+
+ let flashsRepository: FlashsRepository;
+ let usersRepository: UsersRepository;
+ let userProfilesRepository: UserProfilesRepository;
+ let idService: IdService;
+
+ // --------------------------------------------------------------------------------------
+
+ let root: MiUser;
+ let alice: MiUser;
+ let bob: MiUser;
+
+ // --------------------------------------------------------------------------------------
+
+ async function createFlash(data: Partial) {
+ return flashsRepository.insert({
+ id: idService.gen(),
+ updatedAt: new Date(),
+ userId: root.id,
+ title: 'title',
+ summary: 'summary',
+ script: 'script',
+ permissions: [],
+ likedCount: 0,
+ ...data,
+ }).then(x => flashsRepository.findOneByOrFail(x.identifiers[0]));
+ }
+
+ async function createUser(data: Partial = {}) {
+ const user = await usersRepository
+ .insert({
+ id: idService.gen(),
+ ...data,
+ })
+ .then(x => usersRepository.findOneByOrFail(x.identifiers[0]));
+
+ await userProfilesRepository.insert({
+ userId: user.id,
+ });
+
+ return user;
+ }
+
+ // --------------------------------------------------------------------------------------
+
+ beforeEach(async () => {
+ app = await Test.createTestingModule({
+ imports: [
+ GlobalModule,
+ ],
+ providers: [
+ FlashService,
+ IdService,
+ ],
+ }).compile();
+
+ service = app.get(FlashService);
+
+ flashsRepository = app.get(DI.flashsRepository);
+ usersRepository = app.get(DI.usersRepository);
+ userProfilesRepository = app.get(DI.userProfilesRepository);
+ idService = app.get(IdService);
+
+ root = await createUser({ username: 'root', usernameLower: 'root', isRoot: true });
+ alice = await createUser({ username: 'alice', usernameLower: 'alice', isRoot: false });
+ bob = await createUser({ username: 'bob', usernameLower: 'bob', isRoot: false });
+ });
+
+ afterEach(async () => {
+ await usersRepository.delete({});
+ await userProfilesRepository.delete({});
+ await flashsRepository.delete({});
+ });
+
+ afterAll(async () => {
+ await app.close();
+ });
+
+ // --------------------------------------------------------------------------------------
+
+ describe('featured', () => {
+ test('should return featured flashes', async () => {
+ const flash1 = await createFlash({ likedCount: 1 });
+ const flash2 = await createFlash({ likedCount: 2 });
+ const flash3 = await createFlash({ likedCount: 3 });
+
+ const result = await service.featured({
+ offset: 0,
+ limit: 10,
+ });
+
+ expect(result).toEqual([flash3, flash2, flash1]);
+ });
+
+ test('should return featured flashes public visibility only', async () => {
+ const flash1 = await createFlash({ likedCount: 1, visibility: 'public' });
+ const flash2 = await createFlash({ likedCount: 2, visibility: 'public' });
+ const flash3 = await createFlash({ likedCount: 3, visibility: 'private' });
+
+ const result = await service.featured({
+ offset: 0,
+ limit: 10,
+ });
+
+ expect(result).toEqual([flash2, flash1]);
+ });
+
+ test('should return featured flashes with offset', async () => {
+ const flash1 = await createFlash({ likedCount: 1 });
+ const flash2 = await createFlash({ likedCount: 2 });
+ const flash3 = await createFlash({ likedCount: 3 });
+
+ const result = await service.featured({
+ offset: 1,
+ limit: 10,
+ });
+
+ expect(result).toEqual([flash2, flash1]);
+ });
+
+ test('should return featured flashes with limit', async () => {
+ const flash1 = await createFlash({ likedCount: 1 });
+ const flash2 = await createFlash({ likedCount: 2 });
+ const flash3 = await createFlash({ likedCount: 3 });
+
+ const result = await service.featured({
+ offset: 0,
+ limit: 2,
+ });
+
+ expect(result).toEqual([flash3, flash2]);
+ });
+ });
+});
diff --git a/packages/frontend/src/pages/flash/flash-index.vue b/packages/frontend/src/pages/flash/flash-index.vue
index f63a799365..2b85489706 100644
--- a/packages/frontend/src/pages/flash/flash-index.vue
+++ b/packages/frontend/src/pages/flash/flash-index.vue
@@ -55,7 +55,8 @@ const tab = ref('featured');
const featuredFlashsPagination = {
endpoint: 'flash/featured' as const,
- noPaging: true,
+ limit: 5,
+ offsetMode: true,
};
const myFlashsPagination = {
endpoint: 'flash/my' as const,
diff --git a/packages/misskey-js/etc/misskey-js.api.md b/packages/misskey-js/etc/misskey-js.api.md
index 732352abd8..de52be3a61 100644
--- a/packages/misskey-js/etc/misskey-js.api.md
+++ b/packages/misskey-js/etc/misskey-js.api.md
@@ -1680,6 +1680,7 @@ declare namespace entities {
FlashCreateRequest,
FlashCreateResponse,
FlashDeleteRequest,
+ FlashFeaturedRequest,
FlashFeaturedResponse,
FlashLikeRequest,
FlashShowRequest,
@@ -1929,6 +1930,9 @@ type FlashCreateResponse = operations['flash___create']['responses']['200']['con
// @public (undocumented)
type FlashDeleteRequest = operations['flash___delete']['requestBody']['content']['application/json'];
+// @public (undocumented)
+type FlashFeaturedRequest = operations['flash___featured']['requestBody']['content']['application/json'];
+
// @public (undocumented)
type FlashFeaturedResponse = operations['flash___featured']['responses']['200']['content']['application/json'];
diff --git a/packages/misskey-js/src/autogen/endpoint.ts b/packages/misskey-js/src/autogen/endpoint.ts
index 42c74599a5..bf61c20628 100644
--- a/packages/misskey-js/src/autogen/endpoint.ts
+++ b/packages/misskey-js/src/autogen/endpoint.ts
@@ -465,6 +465,7 @@ import type {
FlashCreateRequest,
FlashCreateResponse,
FlashDeleteRequest,
+ FlashFeaturedRequest,
FlashFeaturedResponse,
FlashLikeRequest,
FlashShowRequest,
@@ -889,7 +890,7 @@ export type Endpoints = {
'pages/update': { req: PagesUpdateRequest; res: EmptyResponse };
'flash/create': { req: FlashCreateRequest; res: FlashCreateResponse };
'flash/delete': { req: FlashDeleteRequest; res: EmptyResponse };
- 'flash/featured': { req: EmptyRequest; res: FlashFeaturedResponse };
+ 'flash/featured': { req: FlashFeaturedRequest; res: FlashFeaturedResponse };
'flash/like': { req: FlashLikeRequest; res: EmptyResponse };
'flash/show': { req: FlashShowRequest; res: FlashShowResponse };
'flash/unlike': { req: FlashUnlikeRequest; res: EmptyResponse };
diff --git a/packages/misskey-js/src/autogen/entities.ts b/packages/misskey-js/src/autogen/entities.ts
index 87ed653d44..72c7c35ed4 100644
--- a/packages/misskey-js/src/autogen/entities.ts
+++ b/packages/misskey-js/src/autogen/entities.ts
@@ -468,6 +468,7 @@ export type PagesUpdateRequest = operations['pages___update']['requestBody']['co
export type FlashCreateRequest = operations['flash___create']['requestBody']['content']['application/json'];
export type FlashCreateResponse = operations['flash___create']['responses']['200']['content']['application/json'];
export type FlashDeleteRequest = operations['flash___delete']['requestBody']['content']['application/json'];
+export type FlashFeaturedRequest = operations['flash___featured']['requestBody']['content']['application/json'];
export type FlashFeaturedResponse = operations['flash___featured']['responses']['200']['content']['application/json'];
export type FlashLikeRequest = operations['flash___like']['requestBody']['content']['application/json'];
export type FlashShowRequest = operations['flash___show']['requestBody']['content']['application/json'];
diff --git a/packages/misskey-js/src/autogen/types.ts b/packages/misskey-js/src/autogen/types.ts
index 3876a0bfe5..0938973481 100644
--- a/packages/misskey-js/src/autogen/types.ts
+++ b/packages/misskey-js/src/autogen/types.ts
@@ -23799,6 +23799,16 @@ export type operations = {
* **Credential required**: *No*
*/
flash___featured: {
+ requestBody: {
+ content: {
+ 'application/json': {
+ /** @default 0 */
+ offset?: number;
+ /** @default 10 */
+ limit?: number;
+ };
+ };
+ };
responses: {
/** @description OK (with results) */
200: {
--
cgit v1.2.3-freya
From d8cb7305ef4d5ad6398d9eb57ece2f3ba7ca73eb Mon Sep 17 00:00:00 2001
From: syuilo <4439005+syuilo@users.noreply.github.com>
Date: Sat, 5 Oct 2024 16:20:15 +0900
Subject: feat: 通報の強化 (#14704)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
* wip
* Update CHANGELOG.md
* lint
* Update types.ts
* wip
* :v:
* Update MkAbuseReport.vue
* tweak
---
CHANGELOG.md | 3 +
locales/index.d.ts | 55 +++++++--
locales/ja-JP.yml | 15 ++-
.../1728085812127-refine-abuse-user-report.js | 18 +++
packages/backend/src/core/AbuseReportService.ts | 80 ++++++++++---
packages/backend/src/core/WebhookTestService.ts | 2 +
.../core/entities/AbuseUserReportEntityService.ts | 2 +
packages/backend/src/models/AbuseUserReport.ts | 18 +++
packages/backend/src/server/api/EndpointsModule.ts | 8 ++
packages/backend/src/server/api/endpoints.ts | 4 +
.../endpoints/admin/forward-abuse-user-report.ts | 55 +++++++++
.../endpoints/admin/resolve-abuse-user-report.ts | 4 +-
.../endpoints/admin/update-abuse-user-report.ts | 58 ++++++++++
packages/backend/src/types.ts | 15 ++-
packages/backend/test/e2e/synalio/abuse-report.ts | 6 -
packages/frontend/src/components/MkAbuseReport.vue | 74 +++++++++---
packages/frontend/src/pages/admin-user.vue | 3 +-
packages/frontend/src/pages/admin/abuses.vue | 11 +-
.../frontend/src/pages/admin/modlog.ModLog.vue | 5 +
packages/frontend/src/pages/instance-info.vue | 1 +
packages/frontend/src/pages/user/home.vue | 3 +-
packages/frontend/src/store.ts | 4 +
packages/misskey-js/etc/misskey-js.api.md | 16 ++-
packages/misskey-js/src/autogen/apiClientJSDoc.ts | 22 ++++
packages/misskey-js/src/autogen/endpoint.ts | 4 +
packages/misskey-js/src/autogen/entities.ts | 2 +
packages/misskey-js/src/autogen/types.ts | 127 ++++++++++++++++++++-
packages/misskey-js/src/consts.ts | 15 ++-
packages/misskey-js/src/entities.ts | 6 +
29 files changed, 574 insertions(+), 62 deletions(-)
create mode 100644 packages/backend/migration/1728085812127-refine-abuse-user-report.js
create mode 100644 packages/backend/src/server/api/endpoints/admin/forward-abuse-user-report.ts
create mode 100644 packages/backend/src/server/api/endpoints/admin/update-abuse-user-report.ts
(limited to 'packages/backend/src/server/api')
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 6a9143ea1b..3fd1b7f899 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -9,6 +9,9 @@
### General
- Feat: サーバー初期設定時に初期パスワードを設定できるように
+- Feat: 通報にモデレーションノートを残せるように
+- Feat: 通報の解決種別を設定できるように
+- Enhance: 通報の解決と転送を個別に行えるように
- Enhance: セキュリティ向上のため、サインイン時もCAPTCHAを求めるようになりました
- Enhance: 依存関係の更新
- Enhance: l10nの更新
diff --git a/locales/index.d.ts b/locales/index.d.ts
index 1a0547ebc6..d502c5b432 100644
--- a/locales/index.d.ts
+++ b/locales/index.d.ts
@@ -1834,6 +1834,10 @@ export interface Locale extends ILocale {
* モデレーションノート
*/
"moderationNote": string;
+ /**
+ * モデレーター間でだけ共有されるメモを記入することができます。
+ */
+ "moderationNoteDescription": string;
/**
* モデレーションノートを追加する
*/
@@ -2894,22 +2898,10 @@ export interface Locale extends ILocale {
* 通報元
*/
"reporterOrigin": string;
- /**
- * リモートサーバーに通報を転送する
- */
- "forwardReport": string;
- /**
- * リモートサーバーからはあなたの情報は見れず、匿名のシステムアカウントとして表示されます。
- */
- "forwardReportIsAnonymous": string;
/**
* 送信
*/
"send": string;
- /**
- * 対応済みにする
- */
- "abuseMarkAsResolved": string;
/**
* 新しいタブで開く
*/
@@ -5170,6 +5162,37 @@ export interface Locale extends ILocale {
* フォロワーへのメッセージ
*/
"messageToFollower": string;
+ /**
+ * 対象
+ */
+ "target": string;
+ "_abuseUserReport": {
+ /**
+ * 転送
+ */
+ "forward": string;
+ /**
+ * 匿名のシステムアカウントとして、リモートサーバーに通報を転送します。
+ */
+ "forwardDescription": string;
+ /**
+ * 解決
+ */
+ "resolve": string;
+ /**
+ * 是認
+ */
+ "accept": string;
+ /**
+ * 否認
+ */
+ "reject": string;
+ /**
+ * 内容が正当である通報に対応した場合は「是認」を選択し、肯定的にケースが解決されたことをマークします。
+ * 内容が正当でない通報の場合は「否認」を選択し、否定的にケースが解決されたことをマークします。
+ */
+ "resolveTutorial": string;
+ };
"_delivery": {
/**
* 配信状態
@@ -9785,6 +9808,14 @@ export interface Locale extends ILocale {
* 通報を解決
*/
"resolveAbuseReport": string;
+ /**
+ * 通報を転送
+ */
+ "forwardAbuseReport": string;
+ /**
+ * 通報のモデレーションノート更新
+ */
+ "updateAbuseReportNote": string;
/**
* 招待コードを作成
*/
diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml
index 92014c8abc..678bc7e66b 100644
--- a/locales/ja-JP.yml
+++ b/locales/ja-JP.yml
@@ -454,6 +454,7 @@ totpDescription: "認証アプリを使ってワンタイムパスワードを
moderator: "モデレーター"
moderation: "モデレーション"
moderationNote: "モデレーションノート"
+moderationNoteDescription: "モデレーター間でだけ共有されるメモを記入することができます。"
addModerationNote: "モデレーションノートを追加する"
moderationLogs: "モデログ"
nUsersMentioned: "{n}人が投稿"
@@ -719,10 +720,7 @@ abuseReported: "内容が送信されました。ご報告ありがとうござ
reporter: "通報者"
reporteeOrigin: "通報先"
reporterOrigin: "通報元"
-forwardReport: "リモートサーバーに通報を転送する"
-forwardReportIsAnonymous: "リモートサーバーからはあなたの情報は見れず、匿名のシステムアカウントとして表示されます。"
send: "送信"
-abuseMarkAsResolved: "対応済みにする"
openInNewTab: "新しいタブで開く"
openInSideView: "サイドビューで開く"
defaultNavigationBehaviour: "デフォルトのナビゲーション"
@@ -1288,6 +1286,15 @@ unknownWebAuthnKey: "登録されていないパスキーです。"
passkeyVerificationFailed: "パスキーの検証に失敗しました。"
passkeyVerificationSucceededButPasswordlessLoginDisabled: "パスキーの検証に成功しましたが、パスワードレスログインが無効になっています。"
messageToFollower: "フォロワーへのメッセージ"
+target: "対象"
+
+_abuseUserReport:
+ forward: "転送"
+ forwardDescription: "匿名のシステムアカウントとして、リモートサーバーに通報を転送します。"
+ resolve: "解決"
+ accept: "是認"
+ reject: "否認"
+ resolveTutorial: "内容が正当である通報に対応した場合は「是認」を選択し、肯定的にケースが解決されたことをマークします。\n内容が正当でない通報の場合は「否認」を選択し、否定的にケースが解決されたことをマークします。"
_delivery:
status: "配信状態"
@@ -2593,6 +2600,8 @@ _moderationLogTypes:
markSensitiveDriveFile: "ファイルをセンシティブ付与"
unmarkSensitiveDriveFile: "ファイルをセンシティブ解除"
resolveAbuseReport: "通報を解決"
+ forwardAbuseReport: "通報を転送"
+ updateAbuseReportNote: "通報のモデレーションノート更新"
createInvitation: "招待コードを作成"
createAd: "広告を作成"
deleteAd: "広告を削除"
diff --git a/packages/backend/migration/1728085812127-refine-abuse-user-report.js b/packages/backend/migration/1728085812127-refine-abuse-user-report.js
new file mode 100644
index 0000000000..57cbfdcf6d
--- /dev/null
+++ b/packages/backend/migration/1728085812127-refine-abuse-user-report.js
@@ -0,0 +1,18 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+export class RefineAbuseUserReport1728085812127 {
+ name = 'RefineAbuseUserReport1728085812127'
+
+ async up(queryRunner) {
+ await queryRunner.query(`ALTER TABLE "abuse_user_report" ADD "moderationNote" character varying(8192) NOT NULL DEFAULT ''`);
+ await queryRunner.query(`ALTER TABLE "abuse_user_report" ADD "resolvedAs" character varying(128)`);
+ }
+
+ async down(queryRunner) {
+ await queryRunner.query(`ALTER TABLE "abuse_user_report" DROP COLUMN "resolvedAs"`);
+ await queryRunner.query(`ALTER TABLE "abuse_user_report" DROP COLUMN "moderationNote"`);
+ }
+}
diff --git a/packages/backend/src/core/AbuseReportService.ts b/packages/backend/src/core/AbuseReportService.ts
index 69c51509ba..cddfe5eb81 100644
--- a/packages/backend/src/core/AbuseReportService.ts
+++ b/packages/backend/src/core/AbuseReportService.ts
@@ -20,8 +20,10 @@ export class AbuseReportService {
constructor(
@Inject(DI.abuseUserReportsRepository)
private abuseUserReportsRepository: AbuseUserReportsRepository,
+
@Inject(DI.usersRepository)
private usersRepository: UsersRepository,
+
private idService: IdService,
private abuseReportNotificationService: AbuseReportNotificationService,
private queueService: QueueService,
@@ -77,16 +79,16 @@ export class AbuseReportService {
* - SystemWebhook
*
* @param params 通報内容. もし複数件の通報に対応した時のために、あらかじめ複数件を処理できる前提で考える
- * @param operator 通報を処理したユーザ
+ * @param moderator 通報を処理したユーザ
* @see AbuseReportNotificationService.notify
*/
@bindThis
public async resolve(
params: {
reportId: string;
- forward: boolean;
+ resolvedAs: MiAbuseUserReport['resolvedAs'];
}[],
- operator: MiUser,
+ moderator: MiUser,
) {
const paramsMap = new Map(params.map(it => [it.reportId, it]));
const reports = await this.abuseUserReportsRepository.findBy({
@@ -99,25 +101,15 @@ export class AbuseReportService {
await this.abuseUserReportsRepository.update(report.id, {
resolved: true,
- assigneeId: operator.id,
- forwarded: ps.forward && report.targetUserHost !== null,
+ assigneeId: moderator.id,
+ resolvedAs: ps.resolvedAs,
});
- if (ps.forward && report.targetUserHost != null) {
- const actor = await this.instanceActorService.getInstanceActor();
- const targetUser = await this.usersRepository.findOneByOrFail({ id: report.targetUserId });
-
- // eslint-disable-next-line
- const flag = this.apRendererService.renderFlag(actor, targetUser.uri!, report.comment);
- const contextAssignedFlag = this.apRendererService.addContext(flag);
- this.queueService.deliver(actor, contextAssignedFlag, targetUser.inbox, false);
- }
-
this.moderationLogService
- .log(operator, 'resolveAbuseReport', {
+ .log(moderator, 'resolveAbuseReport', {
reportId: report.id,
report: report,
- forwarded: ps.forward && report.targetUserHost !== null,
+ resolvedAs: ps.resolvedAs,
})
.then();
}
@@ -125,4 +117,58 @@ export class AbuseReportService {
return this.abuseUserReportsRepository.findBy({ id: In(reports.map(it => it.id)) })
.then(reports => this.abuseReportNotificationService.notifySystemWebhook(reports, 'abuseReportResolved'));
}
+
+ @bindThis
+ public async forward(
+ reportId: MiAbuseUserReport['id'],
+ moderator: MiUser,
+ ) {
+ const report = await this.abuseUserReportsRepository.findOneByOrFail({ id: reportId });
+
+ if (report.targetUserHost == null) {
+ throw new Error('The target user host is null.');
+ }
+
+ await this.abuseUserReportsRepository.update(report.id, {
+ forwarded: true,
+ });
+
+ const actor = await this.instanceActorService.getInstanceActor();
+ const targetUser = await this.usersRepository.findOneByOrFail({ id: report.targetUserId });
+
+ const flag = this.apRendererService.renderFlag(actor, targetUser.uri!, report.comment);
+ const contextAssignedFlag = this.apRendererService.addContext(flag);
+ this.queueService.deliver(actor, contextAssignedFlag, targetUser.inbox, false);
+
+ this.moderationLogService
+ .log(moderator, 'forwardAbuseReport', {
+ reportId: report.id,
+ report: report,
+ })
+ .then();
+ }
+
+ @bindThis
+ public async update(
+ reportId: MiAbuseUserReport['id'],
+ params: {
+ moderationNote?: MiAbuseUserReport['moderationNote'];
+ },
+ moderator: MiUser,
+ ) {
+ const report = await this.abuseUserReportsRepository.findOneByOrFail({ id: reportId });
+
+ await this.abuseUserReportsRepository.update(report.id, {
+ moderationNote: params.moderationNote,
+ });
+
+ if (params.moderationNote != null && report.moderationNote !== params.moderationNote) {
+ this.moderationLogService.log(moderator, 'updateAbuseReportNote', {
+ reportId: report.id,
+ report: report,
+ before: report.moderationNote,
+ after: params.moderationNote,
+ });
+ }
+ }
}
diff --git a/packages/backend/src/core/WebhookTestService.ts b/packages/backend/src/core/WebhookTestService.ts
index 149c753d4c..4c45b95a64 100644
--- a/packages/backend/src/core/WebhookTestService.ts
+++ b/packages/backend/src/core/WebhookTestService.ts
@@ -35,6 +35,8 @@ function generateAbuseReport(override?: Partial): AbuseUserRe
comment: 'This is a dummy report for testing purposes.',
targetUserHost: null,
reporterHost: null,
+ resolvedAs: null,
+ moderationNote: 'foo',
...override,
};
diff --git a/packages/backend/src/core/entities/AbuseUserReportEntityService.ts b/packages/backend/src/core/entities/AbuseUserReportEntityService.ts
index a13c244c19..70ead890ab 100644
--- a/packages/backend/src/core/entities/AbuseUserReportEntityService.ts
+++ b/packages/backend/src/core/entities/AbuseUserReportEntityService.ts
@@ -53,6 +53,8 @@ export class AbuseUserReportEntityService {
schema: 'UserDetailedNotMe',
}) : null,
forwarded: report.forwarded,
+ resolvedAs: report.resolvedAs,
+ moderationNote: report.moderationNote,
});
}
diff --git a/packages/backend/src/models/AbuseUserReport.ts b/packages/backend/src/models/AbuseUserReport.ts
index 0615fd7eb5..cb5672e4ac 100644
--- a/packages/backend/src/models/AbuseUserReport.ts
+++ b/packages/backend/src/models/AbuseUserReport.ts
@@ -50,6 +50,9 @@ export class MiAbuseUserReport {
})
public resolved: boolean;
+ /**
+ * リモートサーバーに転送したかどうか
+ */
@Column('boolean', {
default: false,
})
@@ -60,6 +63,21 @@ export class MiAbuseUserReport {
})
public comment: string;
+ @Column('varchar', {
+ length: 8192, default: '',
+ })
+ public moderationNote: string;
+
+ /**
+ * accept 是認 ... 通報内容が正当であり、肯定的に対応された
+ * reject 否認 ... 通報内容が正当でなく、否定的に対応された
+ * null ... その他
+ */
+ @Column('varchar', {
+ length: 128, nullable: true,
+ })
+ public resolvedAs: 'accept' | 'reject' | null;
+
//#region Denormalized fields
@Index()
@Column('varchar', {
diff --git a/packages/backend/src/server/api/EndpointsModule.ts b/packages/backend/src/server/api/EndpointsModule.ts
index 08a0468ab2..3557fa40a5 100644
--- a/packages/backend/src/server/api/EndpointsModule.ts
+++ b/packages/backend/src/server/api/EndpointsModule.ts
@@ -68,6 +68,8 @@ import * as ep___admin_relays_list from './endpoints/admin/relays/list.js';
import * as ep___admin_relays_remove from './endpoints/admin/relays/remove.js';
import * as ep___admin_resetPassword from './endpoints/admin/reset-password.js';
import * as ep___admin_resolveAbuseUserReport from './endpoints/admin/resolve-abuse-user-report.js';
+import * as ep___admin_forwardAbuseUserReport from './endpoints/admin/forward-abuse-user-report.js';
+import * as ep___admin_updateAbuseUserReport from './endpoints/admin/update-abuse-user-report.js';
import * as ep___admin_sendEmail from './endpoints/admin/send-email.js';
import * as ep___admin_serverInfo from './endpoints/admin/server-info.js';
import * as ep___admin_showModerationLogs from './endpoints/admin/show-moderation-logs.js';
@@ -453,6 +455,8 @@ const $admin_relays_list: Provider = { provide: 'ep:admin/relays/list', useClass
const $admin_relays_remove: Provider = { provide: 'ep:admin/relays/remove', useClass: ep___admin_relays_remove.default };
const $admin_resetPassword: Provider = { provide: 'ep:admin/reset-password', useClass: ep___admin_resetPassword.default };
const $admin_resolveAbuseUserReport: Provider = { provide: 'ep:admin/resolve-abuse-user-report', useClass: ep___admin_resolveAbuseUserReport.default };
+const $admin_forwardAbuseUserReport: Provider = { provide: 'ep:admin/forward-abuse-user-report', useClass: ep___admin_forwardAbuseUserReport.default };
+const $admin_updateAbuseUserReport: Provider = { provide: 'ep:admin/update-abuse-user-report', useClass: ep___admin_updateAbuseUserReport.default };
const $admin_sendEmail: Provider = { provide: 'ep:admin/send-email', useClass: ep___admin_sendEmail.default };
const $admin_serverInfo: Provider = { provide: 'ep:admin/server-info', useClass: ep___admin_serverInfo.default };
const $admin_showModerationLogs: Provider = { provide: 'ep:admin/show-moderation-logs', useClass: ep___admin_showModerationLogs.default };
@@ -842,6 +846,8 @@ const $reversi_verify: Provider = { provide: 'ep:reversi/verify', useClass: ep__
$admin_relays_remove,
$admin_resetPassword,
$admin_resolveAbuseUserReport,
+ $admin_forwardAbuseUserReport,
+ $admin_updateAbuseUserReport,
$admin_sendEmail,
$admin_serverInfo,
$admin_showModerationLogs,
@@ -1225,6 +1231,8 @@ const $reversi_verify: Provider = { provide: 'ep:reversi/verify', useClass: ep__
$admin_relays_remove,
$admin_resetPassword,
$admin_resolveAbuseUserReport,
+ $admin_forwardAbuseUserReport,
+ $admin_updateAbuseUserReport,
$admin_sendEmail,
$admin_serverInfo,
$admin_showModerationLogs,
diff --git a/packages/backend/src/server/api/endpoints.ts b/packages/backend/src/server/api/endpoints.ts
index 2462781f7b..49b07d6ced 100644
--- a/packages/backend/src/server/api/endpoints.ts
+++ b/packages/backend/src/server/api/endpoints.ts
@@ -74,6 +74,8 @@ import * as ep___admin_relays_list from './endpoints/admin/relays/list.js';
import * as ep___admin_relays_remove from './endpoints/admin/relays/remove.js';
import * as ep___admin_resetPassword from './endpoints/admin/reset-password.js';
import * as ep___admin_resolveAbuseUserReport from './endpoints/admin/resolve-abuse-user-report.js';
+import * as ep___admin_forwardAbuseUserReport from './endpoints/admin/forward-abuse-user-report.js';
+import * as ep___admin_updateAbuseUserReport from './endpoints/admin/update-abuse-user-report.js';
import * as ep___admin_sendEmail from './endpoints/admin/send-email.js';
import * as ep___admin_serverInfo from './endpoints/admin/server-info.js';
import * as ep___admin_showModerationLogs from './endpoints/admin/show-moderation-logs.js';
@@ -457,6 +459,8 @@ const eps = [
['admin/relays/remove', ep___admin_relays_remove],
['admin/reset-password', ep___admin_resetPassword],
['admin/resolve-abuse-user-report', ep___admin_resolveAbuseUserReport],
+ ['admin/forward-abuse-user-report', ep___admin_forwardAbuseUserReport],
+ ['admin/update-abuse-user-report', ep___admin_updateAbuseUserReport],
['admin/send-email', ep___admin_sendEmail],
['admin/server-info', ep___admin_serverInfo],
['admin/show-moderation-logs', ep___admin_showModerationLogs],
diff --git a/packages/backend/src/server/api/endpoints/admin/forward-abuse-user-report.ts b/packages/backend/src/server/api/endpoints/admin/forward-abuse-user-report.ts
new file mode 100644
index 0000000000..3e42c91fed
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/admin/forward-abuse-user-report.ts
@@ -0,0 +1,55 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { Inject, Injectable } from '@nestjs/common';
+import { Endpoint } from '@/server/api/endpoint-base.js';
+import type { AbuseUserReportsRepository } from '@/models/_.js';
+import { DI } from '@/di-symbols.js';
+import { ApiError } from '@/server/api/error.js';
+import { AbuseReportService } from '@/core/AbuseReportService.js';
+
+export const meta = {
+ tags: ['admin'],
+
+ requireCredential: true,
+ requireModerator: true,
+ kind: 'write:admin:resolve-abuse-user-report',
+
+ errors: {
+ noSuchAbuseReport: {
+ message: 'No such abuse report.',
+ code: 'NO_SUCH_ABUSE_REPORT',
+ id: '8763e21b-d9bc-40be-acf6-54c1a6986493',
+ kind: 'server',
+ httpStatusCode: 404,
+ },
+ },
+} as const;
+
+export const paramDef = {
+ type: 'object',
+ properties: {
+ reportId: { type: 'string', format: 'misskey:id' },
+ },
+ required: ['reportId'],
+} as const;
+
+@Injectable()
+export default class extends Endpoint { // eslint-disable-line import/no-default-export
+ constructor(
+ @Inject(DI.abuseUserReportsRepository)
+ private abuseUserReportsRepository: AbuseUserReportsRepository,
+ private abuseReportService: AbuseReportService,
+ ) {
+ super(meta, paramDef, async (ps, me) => {
+ const report = await this.abuseUserReportsRepository.findOneBy({ id: ps.reportId });
+ if (!report) {
+ throw new ApiError(meta.errors.noSuchAbuseReport);
+ }
+
+ await this.abuseReportService.forward(report.id, me);
+ });
+ }
+}
diff --git a/packages/backend/src/server/api/endpoints/admin/resolve-abuse-user-report.ts b/packages/backend/src/server/api/endpoints/admin/resolve-abuse-user-report.ts
index 9b79100fcf..554d324ff2 100644
--- a/packages/backend/src/server/api/endpoints/admin/resolve-abuse-user-report.ts
+++ b/packages/backend/src/server/api/endpoints/admin/resolve-abuse-user-report.ts
@@ -32,7 +32,7 @@ export const paramDef = {
type: 'object',
properties: {
reportId: { type: 'string', format: 'misskey:id' },
- forward: { type: 'boolean', default: false },
+ resolvedAs: { type: 'string', enum: ['accept', 'reject', null], nullable: true },
},
required: ['reportId'],
} as const;
@@ -50,7 +50,7 @@ export default class extends Endpoint { // eslint-
throw new ApiError(meta.errors.noSuchAbuseReport);
}
- await this.abuseReportService.resolve([{ reportId: report.id, forward: ps.forward }], me);
+ await this.abuseReportService.resolve([{ reportId: report.id, resolvedAs: ps.resolvedAs ?? null }], me);
});
}
}
diff --git a/packages/backend/src/server/api/endpoints/admin/update-abuse-user-report.ts b/packages/backend/src/server/api/endpoints/admin/update-abuse-user-report.ts
new file mode 100644
index 0000000000..73d4b843f0
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/admin/update-abuse-user-report.ts
@@ -0,0 +1,58 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { Inject, Injectable } from '@nestjs/common';
+import { Endpoint } from '@/server/api/endpoint-base.js';
+import type { AbuseUserReportsRepository } from '@/models/_.js';
+import { DI } from '@/di-symbols.js';
+import { ApiError } from '@/server/api/error.js';
+import { AbuseReportService } from '@/core/AbuseReportService.js';
+
+export const meta = {
+ tags: ['admin'],
+
+ requireCredential: true,
+ requireModerator: true,
+ kind: 'write:admin:resolve-abuse-user-report',
+
+ errors: {
+ noSuchAbuseReport: {
+ message: 'No such abuse report.',
+ code: 'NO_SUCH_ABUSE_REPORT',
+ id: '15f51cf5-46d1-4b1d-a618-b35bcbed0662',
+ kind: 'server',
+ httpStatusCode: 404,
+ },
+ },
+} as const;
+
+export const paramDef = {
+ type: 'object',
+ properties: {
+ reportId: { type: 'string', format: 'misskey:id' },
+ moderationNote: { type: 'string' },
+ },
+ required: ['reportId'],
+} as const;
+
+@Injectable()
+export default class extends Endpoint { // eslint-disable-line import/no-default-export
+ constructor(
+ @Inject(DI.abuseUserReportsRepository)
+ private abuseUserReportsRepository: AbuseUserReportsRepository,
+ private abuseReportService: AbuseReportService,
+ ) {
+ super(meta, paramDef, async (ps, me) => {
+ const report = await this.abuseUserReportsRepository.findOneBy({ id: ps.reportId });
+ if (!report) {
+ throw new ApiError(meta.errors.noSuchAbuseReport);
+ }
+
+ await this.abuseReportService.update(report.id, {
+ moderationNote: ps.moderationNote,
+ }, me);
+ });
+ }
+}
diff --git a/packages/backend/src/types.ts b/packages/backend/src/types.ts
index 0389143daf..df3cfee171 100644
--- a/packages/backend/src/types.ts
+++ b/packages/backend/src/types.ts
@@ -99,6 +99,8 @@ export const moderationLogTypes = [
'markSensitiveDriveFile',
'unmarkSensitiveDriveFile',
'resolveAbuseReport',
+ 'forwardAbuseReport',
+ 'updateAbuseReportNote',
'createInvitation',
'createAd',
'updateAd',
@@ -267,7 +269,18 @@ export type ModerationLogPayloads = {
resolveAbuseReport: {
reportId: string;
report: any;
- forwarded: boolean;
+ forwarded?: boolean;
+ resolvedAs?: string | null;
+ };
+ forwardAbuseReport: {
+ reportId: string;
+ report: any;
+ };
+ updateAbuseReportNote: {
+ reportId: string;
+ report: any;
+ before: string;
+ after: string;
};
createInvitation: {
invitations: any[];
diff --git a/packages/backend/test/e2e/synalio/abuse-report.ts b/packages/backend/test/e2e/synalio/abuse-report.ts
index 6ce6e47781..c98d199f35 100644
--- a/packages/backend/test/e2e/synalio/abuse-report.ts
+++ b/packages/backend/test/e2e/synalio/abuse-report.ts
@@ -157,7 +157,6 @@ describe('[シナリオ] ユーザ通報', () => {
const webhookBody2 = await captureWebhook(async () => {
await resolveAbuseReport({
reportId: webhookBody1.body.id,
- forward: false,
}, admin);
});
@@ -214,7 +213,6 @@ describe('[シナリオ] ユーザ通報', () => {
const webhookBody2 = await captureWebhook(async () => {
await resolveAbuseReport({
reportId: abuseReportId,
- forward: false,
}, admin);
});
@@ -257,7 +255,6 @@ describe('[シナリオ] ユーザ通報', () => {
const webhookBody2 = await captureWebhook(async () => {
await resolveAbuseReport({
reportId: webhookBody1.body.id,
- forward: false,
}, admin);
}).catch(e => e.message);
@@ -288,7 +285,6 @@ describe('[シナリオ] ユーザ通報', () => {
const webhookBody2 = await captureWebhook(async () => {
await resolveAbuseReport({
reportId: abuseReportId,
- forward: false,
}, admin);
}).catch(e => e.message);
@@ -319,7 +315,6 @@ describe('[シナリオ] ユーザ通報', () => {
const webhookBody2 = await captureWebhook(async () => {
await resolveAbuseReport({
reportId: abuseReportId,
- forward: false,
}, admin);
}).catch(e => e.message);
@@ -350,7 +345,6 @@ describe('[シナリオ] ユーザ通報', () => {
const webhookBody2 = await captureWebhook(async () => {
await resolveAbuseReport({
reportId: abuseReportId,
- forward: false,
}, admin);
}).catch(e => e.message);
diff --git a/packages/frontend/src/components/MkAbuseReport.vue b/packages/frontend/src/components/MkAbuseReport.vue
index c9c629046e..2f0e09fc4b 100644
--- a/packages/frontend/src/components/MkAbuseReport.vue
+++ b/packages/frontend/src/components/MkAbuseReport.vue
@@ -6,26 +6,33 @@ SPDX-License-Identifier: AGPL-3.0-only
-
+
+
+
(by )
{{ report.comment }}
-
+
- Target:
+ {{ i18n.ts.target }}:
#{{ report.targetUserId.toUpperCase() }}
@@ -36,7 +43,7 @@ SPDX-License-Identifier: AGPL-3.0-only
{{ i18n.ts.details }}
-
+
@@ -51,6 +58,17 @@ SPDX-License-Identifier: AGPL-3.0-only
+
+
+ {{ i18n.ts.moderationNote }}
+ {{ moderationNote.length > 0 ? '...' : i18n.ts.none }}
+
+
+ {{ i18n.ts.moderationNoteDescription }}
+
+
+
+
{{ i18n.ts.moderator }}:
@@ -60,7 +78,7 @@ SPDX-License-Identifier: AGPL-3.0-only
diff --git a/packages/frontend/src/pages/avatar-decorations.vue b/packages/frontend/src/pages/avatar-decorations.vue
index b97e7c0eea..a5cafb1678 100644
--- a/packages/frontend/src/pages/avatar-decorations.vue
+++ b/packages/frontend/src/pages/avatar-decorations.vue
@@ -5,92 +5,38 @@ SPDX-License-Identifier: AGPL-3.0-only
-
+
-
- {{ avatarDecoration.name }}
- {{ avatarDecoration.description }}
-
-
-
-
-
-
- {{ i18n.ts.name }}
-
-
- {{ i18n.ts.description }}
-
-
- {{ i18n.ts.imageUrl }}
-
-
- {{ i18n.ts.save }}
- {{ i18n.ts.delete }}
-
-
-
+
+
+
{{ avatarDecoration.name }}
+
-
+
diff --git a/packages/frontend/src/pages/settings/avatar-decoration.decoration.vue b/packages/frontend/src/pages/settings/avatar-decoration.decoration.vue
index f72a0b9383..3c9914b4e2 100644
--- a/packages/frontend/src/pages/settings/avatar-decoration.decoration.vue
+++ b/packages/frontend/src/pages/settings/avatar-decoration.decoration.vue
@@ -10,12 +10,12 @@ SPDX-License-Identifier: AGPL-3.0-only
>
{{ decoration.name }}
-
+
diff --git a/packages/frontend/src/pages/settings/avatar-decoration.dialog.vue b/packages/frontend/src/pages/settings/avatar-decoration.dialog.vue
index 853e536ea3..aa899ac649 100644
--- a/packages/frontend/src/pages/settings/avatar-decoration.dialog.vue
+++ b/packages/frontend/src/pages/settings/avatar-decoration.dialog.vue
@@ -38,7 +38,7 @@ SPDX-License-Identifier: AGPL-3.0-only
{{ i18n.ts.update }}
{{ i18n.ts.detach }}
- {{ i18n.ts.attach }}
+ {{ i18n.ts.attach }}
@@ -61,6 +61,7 @@ const props = defineProps<{
id: string;
url: string;
name: string;
+ roleIdsThatCanBeUsedThisDecoration: string[];
};
}>();
@@ -83,6 +84,7 @@ const emit = defineEmits<{
const dialog = shallowRef
>();
const exceeded = computed(() => ($i.policies.avatarDecorationLimit - $i.avatarDecorations.length) <= 0);
+const locked = computed(() => props.decoration.roleIdsThatCanBeUsedThisDecoration.length > 0 && !$i.roles.some(r => props.decoration.roleIdsThatCanBeUsedThisDecoration.includes(r.id)));
const angle = ref((props.usingIndex != null ? $i.avatarDecorations[props.usingIndex].angle : null) ?? 0);
const flipH = ref((props.usingIndex != null ? $i.avatarDecorations[props.usingIndex].flipH : null) ?? false);
const offsetX = ref((props.usingIndex != null ? $i.avatarDecorations[props.usingIndex].offsetX : null) ?? 0);
diff --git a/packages/misskey-js/etc/misskey-js.api.md b/packages/misskey-js/etc/misskey-js.api.md
index 61de8b8c7e..061b533b72 100644
--- a/packages/misskey-js/etc/misskey-js.api.md
+++ b/packages/misskey-js/etc/misskey-js.api.md
@@ -121,6 +121,9 @@ type AdminAnnouncementsUpdateRequest = operations['admin___announcements___updat
// @public (undocumented)
type AdminAvatarDecorationsCreateRequest = operations['admin___avatar-decorations___create']['requestBody']['content']['application/json'];
+// @public (undocumented)
+type AdminAvatarDecorationsCreateResponse = operations['admin___avatar-decorations___create']['responses']['200']['content']['application/json'];
+
// @public (undocumented)
type AdminAvatarDecorationsDeleteRequest = operations['admin___avatar-decorations___delete']['requestBody']['content']['application/json'];
@@ -1253,6 +1256,7 @@ declare namespace entities {
AdminAnnouncementsListResponse,
AdminAnnouncementsUpdateRequest,
AdminAvatarDecorationsCreateRequest,
+ AdminAvatarDecorationsCreateResponse,
AdminAvatarDecorationsDeleteRequest,
AdminAvatarDecorationsListRequest,
AdminAvatarDecorationsListResponse,
diff --git a/packages/misskey-js/src/autogen/endpoint.ts b/packages/misskey-js/src/autogen/endpoint.ts
index d0367d8496..5e6bc0a99c 100644
--- a/packages/misskey-js/src/autogen/endpoint.ts
+++ b/packages/misskey-js/src/autogen/endpoint.ts
@@ -31,6 +31,7 @@ import type {
AdminAnnouncementsListResponse,
AdminAnnouncementsUpdateRequest,
AdminAvatarDecorationsCreateRequest,
+ AdminAvatarDecorationsCreateResponse,
AdminAvatarDecorationsDeleteRequest,
AdminAvatarDecorationsListRequest,
AdminAvatarDecorationsListResponse,
@@ -597,7 +598,7 @@ export type Endpoints = {
'admin/announcements/delete': { req: AdminAnnouncementsDeleteRequest; res: EmptyResponse };
'admin/announcements/list': { req: AdminAnnouncementsListRequest; res: AdminAnnouncementsListResponse };
'admin/announcements/update': { req: AdminAnnouncementsUpdateRequest; res: EmptyResponse };
- 'admin/avatar-decorations/create': { req: AdminAvatarDecorationsCreateRequest; res: EmptyResponse };
+ 'admin/avatar-decorations/create': { req: AdminAvatarDecorationsCreateRequest; res: AdminAvatarDecorationsCreateResponse };
'admin/avatar-decorations/delete': { req: AdminAvatarDecorationsDeleteRequest; res: EmptyResponse };
'admin/avatar-decorations/list': { req: AdminAvatarDecorationsListRequest; res: AdminAvatarDecorationsListResponse };
'admin/avatar-decorations/update': { req: AdminAvatarDecorationsUpdateRequest; res: EmptyResponse };
diff --git a/packages/misskey-js/src/autogen/entities.ts b/packages/misskey-js/src/autogen/entities.ts
index ced87c4c7e..f3ddf64481 100644
--- a/packages/misskey-js/src/autogen/entities.ts
+++ b/packages/misskey-js/src/autogen/entities.ts
@@ -34,6 +34,7 @@ export type AdminAnnouncementsListRequest = operations['admin___announcements___
export type AdminAnnouncementsListResponse = operations['admin___announcements___list']['responses']['200']['content']['application/json'];
export type AdminAnnouncementsUpdateRequest = operations['admin___announcements___update']['requestBody']['content']['application/json'];
export type AdminAvatarDecorationsCreateRequest = operations['admin___avatar-decorations___create']['requestBody']['content']['application/json'];
+export type AdminAvatarDecorationsCreateResponse = operations['admin___avatar-decorations___create']['responses']['200']['content']['application/json'];
export type AdminAvatarDecorationsDeleteRequest = operations['admin___avatar-decorations___delete']['requestBody']['content']['application/json'];
export type AdminAvatarDecorationsListRequest = operations['admin___avatar-decorations___list']['requestBody']['content']['application/json'];
export type AdminAvatarDecorationsListResponse = operations['admin___avatar-decorations___list']['responses']['200']['content']['application/json'];
diff --git a/packages/misskey-js/src/autogen/types.ts b/packages/misskey-js/src/autogen/types.ts
index 560960f018..a5333d4f93 100644
--- a/packages/misskey-js/src/autogen/types.ts
+++ b/packages/misskey-js/src/autogen/types.ts
@@ -6324,9 +6324,22 @@ export type operations = {
};
};
responses: {
- /** @description OK (without any results) */
- 204: {
- content: never;
+ /** @description OK (with results) */
+ 200: {
+ content: {
+ 'application/json': {
+ /** Format: id */
+ id: string;
+ /** Format: date-time */
+ createdAt: string;
+ /** Format: date-time */
+ updatedAt: string | null;
+ name: string;
+ description: string;
+ url: string;
+ roleIdsThatCanBeUsedThisDecoration: string[];
+ };
+ };
};
/** @description Client error */
400: {
--
cgit v1.2.3-freya
From b1c82213a320dd7c83f8b2e742406646ef18ff1c Mon Sep 17 00:00:00 2001
From: かっこかり <67428053+kakkokari-gtyih@users.noreply.github.com>
Date: Wed, 6 Nov 2024 22:01:21 +0900
Subject: fix(backend):
FTT無効時にユーザーリストタイムラインが使用できない問題を修正 (#14878)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
* fix: return getfromdb when FanoutTimeline is not enabled
* Update Changelog
* fix
---------
Co-authored-by: Lhc_fl
---
CHANGELOG.md | 2 ++
packages/backend/src/server/api/endpoints/notes/user-list-timeline.ts | 2 +-
2 files changed, 3 insertions(+), 1 deletion(-)
(limited to 'packages/backend/src/server/api')
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 0309f338f1..1740d0171e 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -35,6 +35,8 @@
(Cherry-picked from https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/706)
- Fix: 連合への配信時に、acctの大小文字が区別されてしまい正しくメンションが処理されないことがある問題を修正
(Cherry-picked from https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/711)
+- Fix: FTT無効時にユーザーリストタイムラインが使用できない問題を修正
+ (Cherry-picked from https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/709)
### Misskey.js
- Fix: Stream初期化時、別途WebSocketを指定する場合の型定義を修正
diff --git a/packages/backend/src/server/api/endpoints/notes/user-list-timeline.ts b/packages/backend/src/server/api/endpoints/notes/user-list-timeline.ts
index 6c7185c9eb..87f9b322a6 100644
--- a/packages/backend/src/server/api/endpoints/notes/user-list-timeline.ts
+++ b/packages/backend/src/server/api/endpoints/notes/user-list-timeline.ts
@@ -112,7 +112,7 @@ export default class extends Endpoint { // eslint-
this.activeUsersChart.read(me);
- await this.noteEntityService.packMany(timeline, me);
+ return await this.noteEntityService.packMany(timeline, me);
}
const timeline = await this.fanoutTimelineEndpointService.timeline({
--
cgit v1.2.3-freya
From bca690f256721815fb1c918c1f66a2172f4fcf40 Mon Sep 17 00:00:00 2001
From: 4ster1sk <146138447+4ster1sk@users.noreply.github.com>
Date: Thu, 7 Nov 2024 15:10:10 +0900
Subject: fix(backend): フォロワーへのメッセージの絵文字をemojisに含めるように
(#14904)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
packages/backend/src/server/api/endpoints/i/update.ts | 6 ++++++
1 file changed, 6 insertions(+)
(limited to 'packages/backend/src/server/api')
diff --git a/packages/backend/src/server/api/endpoints/i/update.ts b/packages/backend/src/server/api/endpoints/i/update.ts
index 2183beac7c..d91e2fef4b 100644
--- a/packages/backend/src/server/api/endpoints/i/update.ts
+++ b/packages/backend/src/server/api/endpoints/i/update.ts
@@ -465,6 +465,7 @@ export default class extends Endpoint { // eslint-
const newName = updates.name === undefined ? user.name : updates.name;
const newDescription = profileUpdates.description === undefined ? profile.description : profileUpdates.description;
const newFields = profileUpdates.fields === undefined ? profile.fields : profileUpdates.fields;
+ const newFollowedMessage = profileUpdates.description === undefined ? profile.followedMessage : profileUpdates.followedMessage;
if (newName != null) {
let hasProhibitedWords = false;
@@ -494,6 +495,11 @@ export default class extends Endpoint { // eslint-
]);
}
+ if (newFollowedMessage != null) {
+ const tokens = mfm.parse(newFollowedMessage);
+ emojis = emojis.concat(extractCustomEmojisFromMfm(tokens));
+ }
+
updates.emojis = emojis;
updates.tags = tags;
--
cgit v1.2.3-freya
From 794cb9ffe205b1e2ca838978f80d2d6a35f17f77 Mon Sep 17 00:00:00 2001
From: 4ster1sk <146138447+4ster1sk@users.noreply.github.com>
Date: Thu, 7 Nov 2024 17:16:51 +0900
Subject: fix(backend): followedMessageではなくdescriptionになっていたのを修正
(#14908)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
packages/backend/src/server/api/endpoints/i/update.ts | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
(limited to 'packages/backend/src/server/api')
diff --git a/packages/backend/src/server/api/endpoints/i/update.ts b/packages/backend/src/server/api/endpoints/i/update.ts
index d91e2fef4b..d3eeb75b27 100644
--- a/packages/backend/src/server/api/endpoints/i/update.ts
+++ b/packages/backend/src/server/api/endpoints/i/update.ts
@@ -465,7 +465,7 @@ export default class extends Endpoint { // eslint-
const newName = updates.name === undefined ? user.name : updates.name;
const newDescription = profileUpdates.description === undefined ? profile.description : profileUpdates.description;
const newFields = profileUpdates.fields === undefined ? profile.fields : profileUpdates.fields;
- const newFollowedMessage = profileUpdates.description === undefined ? profile.followedMessage : profileUpdates.followedMessage;
+ const newFollowedMessage = profileUpdates.followedMessage === undefined ? profile.followedMessage : profileUpdates.followedMessage;
if (newName != null) {
let hasProhibitedWords = false;
--
cgit v1.2.3-freya
From ec875d9c40d517a788a450f3b68bd589adcde5ba Mon Sep 17 00:00:00 2001
From: dakkar
Date: Fri, 8 Nov 2024 16:09:02 +0000
Subject: fix merge mistakes in `admin/accounts/create.ts`
---
packages/backend/src/server/api/endpoints/admin/accounts/create.ts | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
(limited to 'packages/backend/src/server/api')
diff --git a/packages/backend/src/server/api/endpoints/admin/accounts/create.ts b/packages/backend/src/server/api/endpoints/admin/accounts/create.ts
index d5d2e909a2..53b1c4c4ec 100644
--- a/packages/backend/src/server/api/endpoints/admin/accounts/create.ts
+++ b/packages/backend/src/server/api/endpoints/admin/accounts/create.ts
@@ -3,8 +3,9 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
-import { Injectable } from '@nestjs/common';
+import { Inject, Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js';
+import type { UsersRepository } from '@/models/_.js';
import { MiAccessToken, MiUser } from '@/models/_.js';
import { SignupService } from '@/core/SignupService.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js';
@@ -15,7 +16,6 @@ import type { Config } from '@/config.js';
import { ApiError } from '@/server/api/error.js';
import { Packed } from '@/misc/json-schema.js';
import { RoleService } from '@/core/RoleService.js';
-import { ApiError } from '@/server/api/error.js';
export const meta = {
tags: ['admin'],
--
cgit v1.2.3-freya
From 0a15ffba55054bc4446339948198049ce7a54974 Mon Sep 17 00:00:00 2001
From: dakkar
Date: Fri, 8 Nov 2024 17:53:34 +0000
Subject: remove duplicate import
---
packages/backend/src/server/api/SigninApiService.ts | 1 -
1 file changed, 1 deletion(-)
(limited to 'packages/backend/src/server/api')
diff --git a/packages/backend/src/server/api/SigninApiService.ts b/packages/backend/src/server/api/SigninApiService.ts
index 1e7a2e80ef..2945384e14 100644
--- a/packages/backend/src/server/api/SigninApiService.ts
+++ b/packages/backend/src/server/api/SigninApiService.ts
@@ -30,7 +30,6 @@ import { SigninService } from './SigninService.js';
import type { AuthenticationResponseJSON } from '@simplewebauthn/types';
import type { FastifyReply, FastifyRequest } from 'fastify';
import { isSystemAccount } from '@/misc/is-system-account.js';
-import type { MiMeta } from '@/models/_.js';
@Injectable()
export class SigninApiService {
--
cgit v1.2.3-freya
From c0d168260482caa974e3fc9e084b121fc32e8ec4 Mon Sep 17 00:00:00 2001
From: かっこかり <67428053+kakkokari-gtyih@users.noreply.github.com>
Date: Fri, 15 Nov 2024 17:30:54 +0900
Subject: feat: 送信したフォローリクエストを確認できるように (#14856)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
* FEAT: Allow users to view pending follow requests they sent
This commit implements the `following/requests/sent` interface firstly
implemented on Firefish, and provides a UI interface to view the pending
follow requests users sent.
* ux: should not show follow requests tab when have no pending sent follow req
* fix default followreq tab
* fix default followreq tab
* restore missing hasPendingReceivedFollowRequest in navbar
* refactor
* use tabler icons
* tweak design
* Revert "ux: should not show follow requests tab when have no pending sent follow req"
This reverts commit e580b92c37f27c2849c6d27e22ca4c47086081bb.
* Update Changelog
* Update Changelog
* change tab titles
---------
Co-authored-by: Lhc_fl
Co-authored-by: Hazelnoot
---
CHANGELOG.md | 2 +
locales/index.d.ts | 10 ++
locales/ja-JP.yml | 4 +
packages/backend/src/server/api/EndpointsModule.ts | 3 +
packages/backend/src/server/api/endpoints.ts | 2 +
.../api/endpoints/following/requests/sent.ts | 77 +++++++++++++++
.../frontend/src/components/MkFollowButton.vue | 10 +-
packages/frontend/src/navbar.ts | 1 -
packages/frontend/src/pages/follow-requests.vue | 105 ++++++++++++++-------
packages/misskey-js/etc/misskey-js.api.md | 8 ++
packages/misskey-js/src/autogen/apiClientJSDoc.ts | 11 +++
packages/misskey-js/src/autogen/endpoint.ts | 3 +
packages/misskey-js/src/autogen/entities.ts | 2 +
packages/misskey-js/src/autogen/types.ts | 72 ++++++++++++++
14 files changed, 272 insertions(+), 38 deletions(-)
create mode 100644 packages/backend/src/server/api/endpoints/following/requests/sent.ts
(limited to 'packages/backend/src/server/api')
diff --git a/CHANGELOG.md b/CHANGELOG.md
index fcbfed5900..232d52d7e1 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -24,6 +24,8 @@
- Enhance: カタルーニャ語 (ca-ES) に対応
- Enhance: 個別お知らせページではMetaタグを出力するように
- Enhance: ノート詳細画面にロールのバッジを表示
+- Enhance: 過去に送信したフォローリクエストを確認できるように
+ (Based on https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/663)
- Fix: 通知の範囲指定の設定項目が必要ない通知設定でも範囲指定の設定がでている問題を修正
- Fix: Turnstileが失敗・期限切れした際にも成功扱いとなってしまう問題を修正
(Cherry-picked from https://github.com/MisskeyIO/misskey/pull/768)
diff --git a/locales/index.d.ts b/locales/index.d.ts
index 440f24ac84..18fbfd15f0 100644
--- a/locales/index.d.ts
+++ b/locales/index.d.ts
@@ -10579,6 +10579,16 @@ export interface Locale extends ILocale {
*/
"description3": ParameterizedString<"link">;
};
+ "_followRequest": {
+ /**
+ * 受け取った申請
+ */
+ "recieved": string;
+ /**
+ * 送った申請
+ */
+ "sent": string;
+ };
}
declare const locales: {
[lang: string]: Locale;
diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml
index 5d8e1a5e72..439ae708c5 100644
--- a/locales/ja-JP.yml
+++ b/locales/ja-JP.yml
@@ -2819,3 +2819,7 @@ _selfXssPrevention:
description1: "ここに何かを貼り付けると、悪意のあるユーザーにアカウントを乗っ取られたり、個人情報を盗まれたりする可能性があります。"
description2: "貼り付けようとしているものが何なのかを正確に理解していない場合は、%c今すぐ作業を中止してこのウィンドウを閉じてください。"
description3: "詳しくはこちらをご確認ください。 {link}"
+
+_followRequest:
+ recieved: "受け取った申請"
+ sent: "送った申請"
diff --git a/packages/backend/src/server/api/EndpointsModule.ts b/packages/backend/src/server/api/EndpointsModule.ts
index 3557fa40a5..5bb194313d 100644
--- a/packages/backend/src/server/api/EndpointsModule.ts
+++ b/packages/backend/src/server/api/EndpointsModule.ts
@@ -187,6 +187,7 @@ import * as ep___following_invalidate from './endpoints/following/invalidate.js'
import * as ep___following_requests_accept from './endpoints/following/requests/accept.js';
import * as ep___following_requests_cancel from './endpoints/following/requests/cancel.js';
import * as ep___following_requests_list from './endpoints/following/requests/list.js';
+import * as ep___following_requests_sent from './endpoints/following/requests/sent.js';
import * as ep___following_requests_reject from './endpoints/following/requests/reject.js';
import * as ep___gallery_featured from './endpoints/gallery/featured.js';
import * as ep___gallery_popular from './endpoints/gallery/popular.js';
@@ -574,6 +575,7 @@ const $following_invalidate: Provider = { provide: 'ep:following/invalidate', us
const $following_requests_accept: Provider = { provide: 'ep:following/requests/accept', useClass: ep___following_requests_accept.default };
const $following_requests_cancel: Provider = { provide: 'ep:following/requests/cancel', useClass: ep___following_requests_cancel.default };
const $following_requests_list: Provider = { provide: 'ep:following/requests/list', useClass: ep___following_requests_list.default };
+const $following_requests_sent: Provider = { provide: 'ep:following/requests/sent', useClass: ep___following_requests_sent.default };
const $following_requests_reject: Provider = { provide: 'ep:following/requests/reject', useClass: ep___following_requests_reject.default };
const $gallery_featured: Provider = { provide: 'ep:gallery/featured', useClass: ep___gallery_featured.default };
const $gallery_popular: Provider = { provide: 'ep:gallery/popular', useClass: ep___gallery_popular.default };
@@ -965,6 +967,7 @@ const $reversi_verify: Provider = { provide: 'ep:reversi/verify', useClass: ep__
$following_requests_accept,
$following_requests_cancel,
$following_requests_list,
+ $following_requests_sent,
$following_requests_reject,
$gallery_featured,
$gallery_popular,
diff --git a/packages/backend/src/server/api/endpoints.ts b/packages/backend/src/server/api/endpoints.ts
index 49b07d6ced..15809b2678 100644
--- a/packages/backend/src/server/api/endpoints.ts
+++ b/packages/backend/src/server/api/endpoints.ts
@@ -193,6 +193,7 @@ import * as ep___following_invalidate from './endpoints/following/invalidate.js'
import * as ep___following_requests_accept from './endpoints/following/requests/accept.js';
import * as ep___following_requests_cancel from './endpoints/following/requests/cancel.js';
import * as ep___following_requests_list from './endpoints/following/requests/list.js';
+import * as ep___following_requests_sent from './endpoints/following/requests/sent.js';
import * as ep___following_requests_reject from './endpoints/following/requests/reject.js';
import * as ep___gallery_featured from './endpoints/gallery/featured.js';
import * as ep___gallery_popular from './endpoints/gallery/popular.js';
@@ -578,6 +579,7 @@ const eps = [
['following/requests/accept', ep___following_requests_accept],
['following/requests/cancel', ep___following_requests_cancel],
['following/requests/list', ep___following_requests_list],
+ ['following/requests/sent', ep___following_requests_sent],
['following/requests/reject', ep___following_requests_reject],
['gallery/featured', ep___gallery_featured],
['gallery/popular', ep___gallery_popular],
diff --git a/packages/backend/src/server/api/endpoints/following/requests/sent.ts b/packages/backend/src/server/api/endpoints/following/requests/sent.ts
new file mode 100644
index 0000000000..6325f01bb8
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/following/requests/sent.ts
@@ -0,0 +1,77 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { Inject, Injectable } from '@nestjs/common';
+import { Endpoint } from '@/server/api/endpoint-base.js';
+import { QueryService } from '@/core/QueryService.js';
+import type { FollowRequestsRepository } from '@/models/_.js';
+import { FollowRequestEntityService } from '@/core/entities/FollowRequestEntityService.js';
+import { DI } from '@/di-symbols.js';
+
+export const meta = {
+ tags: ['following', 'account'],
+
+ requireCredential: true,
+
+ kind: 'read:following',
+
+ res: {
+ type: 'array',
+ optional: false, nullable: false,
+ items: {
+ type: 'object',
+ optional: false, nullable: false,
+ properties: {
+ id: {
+ type: 'string',
+ optional: false, nullable: false,
+ format: 'id',
+ },
+ follower: {
+ type: 'object',
+ optional: false, nullable: false,
+ ref: 'UserLite',
+ },
+ followee: {
+ type: 'object',
+ optional: false, nullable: false,
+ ref: 'UserLite',
+ },
+ },
+ },
+ },
+} as const;
+
+export const paramDef = {
+ type: 'object',
+ properties: {
+ sinceId: { type: 'string', format: 'misskey:id' },
+ untilId: { type: 'string', format: 'misskey:id' },
+ limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 },
+ },
+ required: [],
+} as const;
+
+@Injectable()
+export default class extends Endpoint { // eslint-disable-line import/no-default-export
+ constructor(
+ @Inject(DI.followRequestsRepository)
+ private followRequestsRepository: FollowRequestsRepository,
+
+ private followRequestEntityService: FollowRequestEntityService,
+ private queryService: QueryService,
+ ) {
+ super(meta, paramDef, async (ps, me) => {
+ const query = this.queryService.makePaginationQuery(this.followRequestsRepository.createQueryBuilder('request'), ps.sinceId, ps.untilId)
+ .andWhere('request.followerId = :meId', { meId: me.id });
+
+ const requests = await query
+ .limit(ps.limit)
+ .getMany();
+
+ return await this.followRequestEntityService.packMany(requests, me);
+ });
+ }
+}
diff --git a/packages/frontend/src/components/MkFollowButton.vue b/packages/frontend/src/components/MkFollowButton.vue
index cc07175907..c1dc67f776 100644
--- a/packages/frontend/src/components/MkFollowButton.vue
+++ b/packages/frontend/src/components/MkFollowButton.vue
@@ -91,7 +91,10 @@ async function onClick() {
text: i18n.tsx.unfollowConfirm({ name: props.user.name || props.user.username }),
});
- if (canceled) return;
+ if (canceled) {
+ wait.value = false;
+ return;
+ }
await misskeyApi('following/delete', {
userId: props.user.id,
@@ -125,7 +128,10 @@ async function onClick() {
});
hasPendingFollowRequestFromYou.value = true;
- if ($i == null) return;
+ if ($i == null) {
+ wait.value = false;
+ return;
+ }
claimAchievement('following1');
diff --git a/packages/frontend/src/navbar.ts b/packages/frontend/src/navbar.ts
index ac730f8021..096d404a57 100644
--- a/packages/frontend/src/navbar.ts
+++ b/packages/frontend/src/navbar.ts
@@ -40,7 +40,6 @@ export const navbarItemDef = reactive({
followRequests: {
title: i18n.ts.followRequests,
icon: 'ti ti-user-plus',
- show: computed(() => $i != null && $i.isLocked),
indicated: computed(() => $i != null && $i.hasPendingReceivedFollowRequest),
to: '/my/follow-requests',
},
diff --git a/packages/frontend/src/pages/follow-requests.vue b/packages/frontend/src/pages/follow-requests.vue
index a840d0d0b3..8688863c2c 100644
--- a/packages/frontend/src/pages/follow-requests.vue
+++ b/packages/frontend/src/pages/follow-requests.vue
@@ -5,69 +5,104 @@ SPDX-License-Identifier: AGPL-3.0-only
-
+
-
-
-
-
![]()
-
{{ i18n.ts.noFollowRequests }}
-
-
-
-
-
-
-
-
-
-
@{{ acct(req.follower) }}
-
-
-
{{ i18n.ts.accept }}
-
{{ i18n.ts.reject }}
+
+
+
+
+
+
![]()
+
{{ i18n.ts.noFollowRequests }}
+
+
+
+
+
+
+
+
+
+
@{{ acct(displayUser(req)) }}
+
+
+ {{ i18n.ts.accept }}
+ {{ i18n.ts.reject }}
+
+
+ {{ i18n.ts.cancel }}
+
+
-
-
-
-
+
+
+
+