Merge branch 'misskey-dev:develop' into dev

This commit is contained in:
MomentQYC 2024-10-05 21:56:59 +08:00 committed by GitHub
commit f3ef935cd3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
56 changed files with 1151 additions and 344 deletions

View File

@ -9,9 +9,13 @@
### General ### General
- Feat: サーバー初期設定時に初期パスワードを設定できるように - Feat: サーバー初期設定時に初期パスワードを設定できるように
- Feat: 通報にモデレーションノートを残せるように
- Feat: 通報の解決種別を設定できるように
- Enhance: 通報の解決と転送を個別に行えるように
- Enhance: セキュリティ向上のため、サインイン時もCAPTCHAを求めるようになりました - Enhance: セキュリティ向上のため、サインイン時もCAPTCHAを求めるようになりました
- Enhance: 依存関係の更新 - Enhance: 依存関係の更新
- Enhance: l10nの更新 - Enhance: l10nの更新
- Enhance: Playの「人気」タブで10件以上表示可能に #14399
- Fix: 連合のホワイトリストが正常に登録されない問題を修正 - Fix: 連合のホワイトリストが正常に登録されない問題を修正
### Client ### Client
@ -21,7 +25,8 @@
### Server ### Server
- Enhance: セキュリティ向上のため、ログイン時にメール通知を行うように - Enhance: セキュリティ向上のため、ログイン時にメール通知を行うように
- Enhance: 自分とモデレーター以外のユーザーから二要素認証関連のデータが取得できないように - Enhance: 自分とモデレーター以外のユーザーから二要素認証関連のデータが取得できないように
- Enhance: 通報および通報解決時に送出されるSystemWebhookにユーザ情報を含めるように ( #14697 )
- Fix: `admin/abuse-user-reports`エンドポイントのスキーマが間違っていた問題を修正
## 2024.9.0 ## 2024.9.0

View File

@ -120,7 +120,7 @@ describe('After user signup', () => {
it('signin', () => { it('signin', () => {
cy.visitHome(); 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]').click();

View File

@ -55,7 +55,7 @@ Cypress.Commands.add('registerUser', (username, password, isAdmin = false) => {
Cypress.Commands.add('login', (username, password) => { Cypress.Commands.add('login', (username, password) => {
cy.visitHome(); 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]').click();
cy.get('[data-cy-signin-page-input]').should('be.visible', { timeout: 1000 }); cy.get('[data-cy-signin-page-input]').should('be.visible', { timeout: 1000 });

55
locales/index.d.ts vendored
View File

@ -1834,6 +1834,10 @@ export interface Locale extends ILocale {
* *
*/ */
"moderationNote": string; "moderationNote": string;
/**
*
*/
"moderationNoteDescription": string;
/** /**
* *
*/ */
@ -2894,22 +2898,10 @@ export interface Locale extends ILocale {
* *
*/ */
"reporterOrigin": string; "reporterOrigin": string;
/**
*
*/
"forwardReport": string;
/**
*
*/
"forwardReportIsAnonymous": string;
/** /**
* *
*/ */
"send": string; "send": string;
/**
*
*/
"abuseMarkAsResolved": string;
/** /**
* *
*/ */
@ -5178,6 +5170,37 @@ export interface Locale extends ILocale {
* *
*/ */
"messageToFollower": string; "messageToFollower": string;
/**
*
*/
"target": string;
"_abuseUserReport": {
/**
*
*/
"forward": string;
/**
*
*/
"forwardDescription": string;
/**
*
*/
"resolve": string;
/**
*
*/
"accept": string;
/**
*
*/
"reject": string;
/**
*
*
*/
"resolveTutorial": string;
};
"_delivery": { "_delivery": {
/** /**
* *
@ -9801,6 +9824,14 @@ export interface Locale extends ILocale {
* *
*/ */
"resolveAbuseReport": string; "resolveAbuseReport": string;
/**
*
*/
"forwardAbuseReport": string;
/**
*
*/
"updateAbuseReportNote": string;
/** /**
* *
*/ */

View File

@ -454,6 +454,7 @@ totpDescription: "認証アプリを使ってワンタイムパスワードを
moderator: "モデレーター" moderator: "モデレーター"
moderation: "モデレーション" moderation: "モデレーション"
moderationNote: "モデレーションノート" moderationNote: "モデレーションノート"
moderationNoteDescription: "モデレーター間でだけ共有されるメモを記入することができます。"
addModerationNote: "モデレーションノートを追加する" addModerationNote: "モデレーションノートを追加する"
moderationLogs: "モデログ" moderationLogs: "モデログ"
nUsersMentioned: "{n}人が投稿" nUsersMentioned: "{n}人が投稿"
@ -719,10 +720,7 @@ abuseReported: "内容が送信されました。ご報告ありがとうござ
reporter: "通報者" reporter: "通報者"
reporteeOrigin: "通報先" reporteeOrigin: "通報先"
reporterOrigin: "通報元" reporterOrigin: "通報元"
forwardReport: "リモートサーバーに通報を転送する"
forwardReportIsAnonymous: "リモートサーバーからはあなたの情報は見れず、匿名のシステムアカウントとして表示されます。"
send: "送信" send: "送信"
abuseMarkAsResolved: "対応済みにする"
openInNewTab: "新しいタブで開く" openInNewTab: "新しいタブで開く"
openInSideView: "サイドビューで開く" openInSideView: "サイドビューで開く"
defaultNavigationBehaviour: "デフォルトのナビゲーション" defaultNavigationBehaviour: "デフォルトのナビゲーション"
@ -1290,6 +1288,15 @@ unknownWebAuthnKey: "登録されていないパスキーです。"
passkeyVerificationFailed: "パスキーの検証に失敗しました。" passkeyVerificationFailed: "パスキーの検証に失敗しました。"
passkeyVerificationSucceededButPasswordlessLoginDisabled: "パスキーの検証に成功しましたが、パスワードレスログインが無効になっています。" passkeyVerificationSucceededButPasswordlessLoginDisabled: "パスキーの検証に成功しましたが、パスワードレスログインが無効になっています。"
messageToFollower: "フォロワーへのメッセージ" messageToFollower: "フォロワーへのメッセージ"
target: "対象"
_abuseUserReport:
forward: "転送"
forwardDescription: "匿名のシステムアカウントとして、リモートサーバーに通報を転送します。"
resolve: "解決"
accept: "是認"
reject: "否認"
resolveTutorial: "内容が正当である通報に対応した場合は「是認」を選択し、肯定的にケースが解決されたことをマークします。\n内容が正当でない通報の場合は「否認」を選択し、否定的にケースが解決されたことをマークします。"
_delivery: _delivery:
status: "配信状態" status: "配信状態"
@ -2597,6 +2604,8 @@ _moderationLogTypes:
markSensitiveDriveFile: "ファイルをセンシティブ付与" markSensitiveDriveFile: "ファイルをセンシティブ付与"
unmarkSensitiveDriveFile: "ファイルをセンシティブ解除" unmarkSensitiveDriveFile: "ファイルをセンシティブ解除"
resolveAbuseReport: "通報を解決" resolveAbuseReport: "通報を解決"
forwardAbuseReport: "通報を転送"
updateAbuseReportNote: "通報のモデレーションノート更新"
createInvitation: "招待コードを作成" createInvitation: "招待コードを作成"
createAd: "広告を作成" createAd: "広告を作成"
deleteAd: "広告を削除" deleteAd: "広告を削除"

View File

@ -1,6 +1,6 @@
{ {
"name": "misskey", "name": "misskey",
"version": "2024.10.0-beta.4", "version": "2024.10.0-beta.5",
"codename": "nasubi", "codename": "nasubi",
"repository": { "repository": {
"type": "git", "type": "git",

View File

@ -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"`);
}
}

View File

@ -22,6 +22,7 @@ import { RoleService } from '@/core/RoleService.js';
import { RecipientMethod } from '@/models/AbuseReportNotificationRecipient.js'; import { RecipientMethod } from '@/models/AbuseReportNotificationRecipient.js';
import { ModerationLogService } from '@/core/ModerationLogService.js'; import { ModerationLogService } from '@/core/ModerationLogService.js';
import { SystemWebhookService } from '@/core/SystemWebhookService.js'; import { SystemWebhookService } from '@/core/SystemWebhookService.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js';
import { IdService } from './IdService.js'; import { IdService } from './IdService.js';
@Injectable() @Injectable()
@ -42,6 +43,7 @@ export class AbuseReportNotificationService implements OnApplicationShutdown {
private emailService: EmailService, private emailService: EmailService,
private moderationLogService: ModerationLogService, private moderationLogService: ModerationLogService,
private globalEventService: GlobalEventService, private globalEventService: GlobalEventService,
private userEntityService: UserEntityService,
) { ) {
this.redisForSub.on('message', this.onMessage); this.redisForSub.on('message', this.onMessage);
} }
@ -135,6 +137,26 @@ export class AbuseReportNotificationService implements OnApplicationShutdown {
return; return;
} }
const usersMap = await this.userEntityService.packMany(
[
...new Set([
...abuseReports.map(it => it.reporter ?? it.reporterId),
...abuseReports.map(it => it.targetUser ?? it.targetUserId),
...abuseReports.map(it => it.assignee ?? it.assigneeId),
].filter(x => x != null)),
],
null,
{ schema: 'UserLite' },
).then(it => new Map(it.map(it => [it.id, it])));
const convertedReports = abuseReports.map(it => {
return {
...it,
reporter: usersMap.get(it.reporterId),
targetUser: usersMap.get(it.targetUserId),
assignee: it.assigneeId ? usersMap.get(it.assigneeId) : null,
};
});
const recipientWebhookIds = await this.fetchWebhookRecipients() const recipientWebhookIds = await this.fetchWebhookRecipients()
.then(it => it .then(it => it
.filter(it => it.isActive && it.systemWebhookId && it.method === 'webhook') .filter(it => it.isActive && it.systemWebhookId && it.method === 'webhook')
@ -142,7 +164,7 @@ export class AbuseReportNotificationService implements OnApplicationShutdown {
.filter(x => x != null)); .filter(x => x != null));
for (const webhookId of recipientWebhookIds) { for (const webhookId of recipientWebhookIds) {
await Promise.all( await Promise.all(
abuseReports.map(it => { convertedReports.map(it => {
return this.systemWebhookService.enqueueSystemWebhook( return this.systemWebhookService.enqueueSystemWebhook(
webhookId, webhookId,
type, type,

View File

@ -20,8 +20,10 @@ export class AbuseReportService {
constructor( constructor(
@Inject(DI.abuseUserReportsRepository) @Inject(DI.abuseUserReportsRepository)
private abuseUserReportsRepository: AbuseUserReportsRepository, private abuseUserReportsRepository: AbuseUserReportsRepository,
@Inject(DI.usersRepository) @Inject(DI.usersRepository)
private usersRepository: UsersRepository, private usersRepository: UsersRepository,
private idService: IdService, private idService: IdService,
private abuseReportNotificationService: AbuseReportNotificationService, private abuseReportNotificationService: AbuseReportNotificationService,
private queueService: QueueService, private queueService: QueueService,
@ -77,16 +79,16 @@ export class AbuseReportService {
* - SystemWebhook * - SystemWebhook
* *
* @param params . * @param params .
* @param operator * @param moderator
* @see AbuseReportNotificationService.notify * @see AbuseReportNotificationService.notify
*/ */
@bindThis @bindThis
public async resolve( public async resolve(
params: { params: {
reportId: string; reportId: string;
forward: boolean; resolvedAs: MiAbuseUserReport['resolvedAs'];
}[], }[],
operator: MiUser, moderator: MiUser,
) { ) {
const paramsMap = new Map(params.map(it => [it.reportId, it])); const paramsMap = new Map(params.map(it => [it.reportId, it]));
const reports = await this.abuseUserReportsRepository.findBy({ const reports = await this.abuseUserReportsRepository.findBy({
@ -99,25 +101,15 @@ export class AbuseReportService {
await this.abuseUserReportsRepository.update(report.id, { await this.abuseUserReportsRepository.update(report.id, {
resolved: true, resolved: true,
assigneeId: operator.id, assigneeId: moderator.id,
forwarded: ps.forward && report.targetUserHost !== null, 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 this.moderationLogService
.log(operator, 'resolveAbuseReport', { .log(moderator, 'resolveAbuseReport', {
reportId: report.id, reportId: report.id,
report: report, report: report,
forwarded: ps.forward && report.targetUserHost !== null, resolvedAs: ps.resolvedAs,
}) })
.then(); .then();
} }
@ -125,4 +117,62 @@ export class AbuseReportService {
return this.abuseUserReportsRepository.findBy({ id: In(reports.map(it => it.id)) }) return this.abuseUserReportsRepository.findBy({ id: In(reports.map(it => it.id)) })
.then(reports => this.abuseReportNotificationService.notifySystemWebhook(reports, 'abuseReportResolved')); .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.');
}
if (report.forwarded) {
throw new Error('The report has already been forwarded.');
}
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,
});
}
}
} }

View File

@ -14,6 +14,7 @@ import { AbuseReportNotificationService } from '@/core/AbuseReportNotificationSe
import { SystemWebhookService } from '@/core/SystemWebhookService.js'; import { SystemWebhookService } from '@/core/SystemWebhookService.js';
import { UserSearchService } from '@/core/UserSearchService.js'; import { UserSearchService } from '@/core/UserSearchService.js';
import { WebhookTestService } from '@/core/WebhookTestService.js'; import { WebhookTestService } from '@/core/WebhookTestService.js';
import { FlashService } from '@/core/FlashService.js';
import { AccountMoveService } from './AccountMoveService.js'; import { AccountMoveService } from './AccountMoveService.js';
import { AccountUpdateService } from './AccountUpdateService.js'; import { AccountUpdateService } from './AccountUpdateService.js';
import { AiService } from './AiService.js'; import { AiService } from './AiService.js';
@ -217,6 +218,7 @@ const $SystemWebhookService: Provider = { provide: 'SystemWebhookService', useEx
const $WebhookTestService: Provider = { provide: 'WebhookTestService', useExisting: WebhookTestService }; const $WebhookTestService: Provider = { provide: 'WebhookTestService', useExisting: WebhookTestService };
const $UtilityService: Provider = { provide: 'UtilityService', useExisting: UtilityService }; const $UtilityService: Provider = { provide: 'UtilityService', useExisting: UtilityService };
const $FileInfoService: Provider = { provide: 'FileInfoService', useExisting: FileInfoService }; const $FileInfoService: Provider = { provide: 'FileInfoService', useExisting: FileInfoService };
const $FlashService: Provider = { provide: 'FlashService', useExisting: FlashService };
const $SearchService: Provider = { provide: 'SearchService', useExisting: SearchService }; const $SearchService: Provider = { provide: 'SearchService', useExisting: SearchService };
const $ClipService: Provider = { provide: 'ClipService', useExisting: ClipService }; const $ClipService: Provider = { provide: 'ClipService', useExisting: ClipService };
const $FeaturedService: Provider = { provide: 'FeaturedService', useExisting: FeaturedService }; const $FeaturedService: Provider = { provide: 'FeaturedService', useExisting: FeaturedService };
@ -367,6 +369,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
WebhookTestService, WebhookTestService,
UtilityService, UtilityService,
FileInfoService, FileInfoService,
FlashService,
SearchService, SearchService,
ClipService, ClipService,
FeaturedService, FeaturedService,
@ -513,6 +516,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
$WebhookTestService, $WebhookTestService,
$UtilityService, $UtilityService,
$FileInfoService, $FileInfoService,
$FlashService,
$SearchService, $SearchService,
$ClipService, $ClipService,
$FeaturedService, $FeaturedService,
@ -660,6 +664,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
WebhookTestService, WebhookTestService,
UtilityService, UtilityService,
FileInfoService, FileInfoService,
FlashService,
SearchService, SearchService,
ClipService, ClipService,
FeaturedService, FeaturedService,

View File

@ -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();
}
}

View File

@ -218,7 +218,7 @@ export class NoteCreateService implements OnApplicationShutdown {
private utilityService: UtilityService, private utilityService: UtilityService,
private userBlockingService: UserBlockingService, private userBlockingService: UserBlockingService,
) { ) {
this.updateNotesCountQueue = new CollapsedQueue(60 * 1000 * 5, this.collapseNotesCount, this.performUpdateNotesCount); this.updateNotesCountQueue = new CollapsedQueue(process.env.NODE_ENV !== 'test' ? 60 * 1000 * 5 : 0, this.collapseNotesCount, this.performUpdateNotesCount);
} }
@bindThis @bindThis

View File

@ -15,8 +15,14 @@ import { QueueService } from '@/core/QueueService.js';
const oneDayMillis = 24 * 60 * 60 * 1000; const oneDayMillis = 24 * 60 * 60 * 1000;
function generateAbuseReport(override?: Partial<MiAbuseUserReport>): MiAbuseUserReport { type AbuseUserReportDto = Omit<MiAbuseUserReport, 'targetUser' | 'reporter' | 'assignee'> & {
return { targetUser: Packed<'UserLite'> | null,
reporter: Packed<'UserLite'> | null,
assignee: Packed<'UserLite'> | null,
};
function generateAbuseReport(override?: Partial<MiAbuseUserReport>): AbuseUserReportDto {
const result: MiAbuseUserReport = {
id: 'dummy-abuse-report1', id: 'dummy-abuse-report1',
targetUserId: 'dummy-target-user', targetUserId: 'dummy-target-user',
targetUser: null, targetUser: null,
@ -29,8 +35,17 @@ function generateAbuseReport(override?: Partial<MiAbuseUserReport>): MiAbuseUser
comment: 'This is a dummy report for testing purposes.', comment: 'This is a dummy report for testing purposes.',
targetUserHost: null, targetUserHost: null,
reporterHost: null, reporterHost: null,
resolvedAs: null,
moderationNote: 'foo',
...override, ...override,
}; };
return {
...result,
targetUser: result.targetUser ? toPackedUserLite(result.targetUser) : null,
reporter: result.reporter ? toPackedUserLite(result.reporter) : null,
assignee: result.assignee ? toPackedUserLite(result.assignee) : null,
};
} }
function generateDummyUser(override?: Partial<MiUser>): MiUser { function generateDummyUser(override?: Partial<MiUser>): MiUser {
@ -268,7 +283,8 @@ const dummyUser3 = generateDummyUser({
@Injectable() @Injectable()
export class WebhookTestService { export class WebhookTestService {
public static NoSuchWebhookError = class extends Error {}; public static NoSuchWebhookError = class extends Error {
};
constructor( constructor(
private userWebhookService: UserWebhookService, private userWebhookService: UserWebhookService,

View File

@ -53,6 +53,8 @@ export class AbuseUserReportEntityService {
schema: 'UserDetailedNotMe', schema: 'UserDetailedNotMe',
}) : null, }) : null,
forwarded: report.forwarded, forwarded: report.forwarded,
resolvedAs: report.resolvedAs,
moderationNote: report.moderationNote,
}); });
} }

View File

@ -5,10 +5,8 @@
import { Inject, Injectable } from '@nestjs/common'; import { Inject, Injectable } from '@nestjs/common';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
import type { FlashsRepository, FlashLikesRepository } from '@/models/_.js'; import type { FlashLikesRepository, FlashsRepository } from '@/models/_.js';
import { awaitAll } from '@/misc/prelude/await-all.js';
import type { Packed } from '@/misc/json-schema.js'; import type { Packed } from '@/misc/json-schema.js';
import type { } from '@/models/Blocking.js';
import type { MiUser } from '@/models/User.js'; import type { MiUser } from '@/models/User.js';
import type { MiFlash } from '@/models/Flash.js'; import type { MiFlash } from '@/models/Flash.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
@ -20,10 +18,8 @@ export class FlashEntityService {
constructor( constructor(
@Inject(DI.flashsRepository) @Inject(DI.flashsRepository)
private flashsRepository: FlashsRepository, private flashsRepository: FlashsRepository,
@Inject(DI.flashLikesRepository) @Inject(DI.flashLikesRepository)
private flashLikesRepository: FlashLikesRepository, private flashLikesRepository: FlashLikesRepository,
private userEntityService: UserEntityService, private userEntityService: UserEntityService,
private idService: IdService, private idService: IdService,
) { ) {
@ -34,25 +30,36 @@ export class FlashEntityService {
src: MiFlash['id'] | MiFlash, src: MiFlash['id'] | MiFlash,
me?: { id: MiUser['id'] } | null | undefined, me?: { id: MiUser['id'] } | null | undefined,
hint?: { hint?: {
packedUser?: Packed<'UserLite'> packedUser?: Packed<'UserLite'>,
likedFlashIds?: MiFlash['id'][],
}, },
): Promise<Packed<'Flash'>> { ): Promise<Packed<'Flash'>> {
const meId = me ? me.id : null; const meId = me ? me.id : null;
const flash = typeof src === 'object' ? src : await this.flashsRepository.findOneByOrFail({ id: src }); 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, id: flash.id,
createdAt: this.idService.parse(flash.id).date.toISOString(), createdAt: this.idService.parse(flash.id).date.toISOString(),
updatedAt: flash.updatedAt.toISOString(), updatedAt: flash.updatedAt.toISOString(),
userId: flash.userId, userId: flash.userId,
user: hint?.packedUser ?? this.userEntityService.pack(flash.user ?? flash.userId, me), // { schema: 'UserDetailed' } すると無限ループするので注意 user: user,
title: flash.title, title: flash.title,
summary: flash.summary, summary: flash.summary,
script: flash.script, script: flash.script,
visibility: flash.visibility, visibility: flash.visibility,
likedCount: flash.likedCount, likedCount: flash.likedCount,
isLiked: meId ? await this.flashLikesRepository.exists({ where: { flashId: flash.id, userId: meId } }) : undefined, isLiked: isLiked,
}); };
} }
@bindThis @bindThis
@ -63,7 +70,19 @@ export class FlashEntityService {
const _users = flashes.map(({ user, userId }) => user ?? userId); const _users = flashes.map(({ user, userId }) => user ?? userId);
const _userMap = await this.userEntityService.packMany(_users, me) const _userMap = await this.userEntityService.packMany(_users, me)
.then(users => new Map(users.map(u => [u.id, u]))); .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,
})),
);
} }
} }

View File

@ -50,6 +50,9 @@ export class MiAbuseUserReport {
}) })
public resolved: boolean; public resolved: boolean;
/**
*
*/
@Column('boolean', { @Column('boolean', {
default: false, default: false,
}) })
@ -60,6 +63,21 @@ export class MiAbuseUserReport {
}) })
public comment: string; 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 //#region Denormalized fields
@Index() @Index()
@Column('varchar', { @Column('varchar', {

View File

@ -7,6 +7,9 @@ import { Entity, Index, JoinColumn, Column, PrimaryColumn, ManyToOne } from 'typ
import { id } from './util/id.js'; import { id } from './util/id.js';
import { MiUser } from './User.js'; import { MiUser } from './User.js';
export const flashVisibility = ['public', 'private'] as const;
export type FlashVisibility = typeof flashVisibility[number];
@Entity('flash') @Entity('flash')
export class MiFlash { export class MiFlash {
@PrimaryColumn(id()) @PrimaryColumn(id())
@ -63,5 +66,5 @@ export class MiFlash {
@Column('varchar', { @Column('varchar', {
length: 512, default: 'public', length: 512, default: 'public',
}) })
public visibility: 'public' | 'private'; public visibility: FlashVisibility;
} }

View File

@ -59,7 +59,7 @@ export class InboxProcessorService implements OnApplicationShutdown {
private queueLoggerService: QueueLoggerService, private queueLoggerService: QueueLoggerService,
) { ) {
this.logger = this.queueLoggerService.logger.createSubLogger('inbox'); this.logger = this.queueLoggerService.logger.createSubLogger('inbox');
this.updateInstanceQueue = new CollapsedQueue(60 * 1000 * 5, this.collapseUpdateInstanceJobs, this.performUpdateInstance); this.updateInstanceQueue = new CollapsedQueue(process.env.NODE_ENV !== 'test' ? 60 * 1000 * 5 : 0, this.collapseUpdateInstanceJobs, this.performUpdateInstance);
} }
@bindThis @bindThis

View File

@ -125,7 +125,7 @@ export class ApiServerService {
fastify.post<{ fastify.post<{
Body: { Body: {
username: string; username: string;
password: string; password?: string;
token?: string; token?: string;
credential?: AuthenticationResponseJSON; credential?: AuthenticationResponseJSON;
'hcaptcha-response'?: string; 'hcaptcha-response'?: string;
@ -133,7 +133,7 @@ export class ApiServerService {
'turnstile-response'?: string; 'turnstile-response'?: string;
'm-captcha-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<{ fastify.post<{
Body: { Body: {

View File

@ -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_relays_remove from './endpoints/admin/relays/remove.js';
import * as ep___admin_resetPassword from './endpoints/admin/reset-password.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_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_sendEmail from './endpoints/admin/send-email.js';
import * as ep___admin_serverInfo from './endpoints/admin/server-info.js'; import * as ep___admin_serverInfo from './endpoints/admin/server-info.js';
import * as ep___admin_showModerationLogs from './endpoints/admin/show-moderation-logs.js'; import * as ep___admin_showModerationLogs from './endpoints/admin/show-moderation-logs.js';
@ -454,6 +456,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_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_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_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_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_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 }; const $admin_showModerationLogs: Provider = { provide: 'ep:admin/show-moderation-logs', useClass: ep___admin_showModerationLogs.default };
@ -844,6 +848,8 @@ const $reversi_verify: Provider = { provide: 'ep:reversi/verify', useClass: ep__
$admin_relays_remove, $admin_relays_remove,
$admin_resetPassword, $admin_resetPassword,
$admin_resolveAbuseUserReport, $admin_resolveAbuseUserReport,
$admin_forwardAbuseUserReport,
$admin_updateAbuseUserReport,
$admin_sendEmail, $admin_sendEmail,
$admin_serverInfo, $admin_serverInfo,
$admin_showModerationLogs, $admin_showModerationLogs,
@ -1228,6 +1234,8 @@ const $reversi_verify: Provider = { provide: 'ep:reversi/verify', useClass: ep__
$admin_relays_remove, $admin_relays_remove,
$admin_resetPassword, $admin_resetPassword,
$admin_resolveAbuseUserReport, $admin_resolveAbuseUserReport,
$admin_forwardAbuseUserReport,
$admin_updateAbuseUserReport,
$admin_sendEmail, $admin_sendEmail,
$admin_serverInfo, $admin_serverInfo,
$admin_showModerationLogs, $admin_showModerationLogs,

View File

@ -5,8 +5,8 @@
import { Inject, Injectable } from '@nestjs/common'; import { Inject, Injectable } from '@nestjs/common';
import bcrypt from 'bcryptjs'; import bcrypt from 'bcryptjs';
import * as OTPAuth from 'otpauth';
import { IsNull } from 'typeorm'; import { IsNull } from 'typeorm';
import * as Misskey from 'misskey-js';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
import type { import type {
MiMeta, MiMeta,
@ -26,27 +26,9 @@ import { CaptchaService } from '@/core/CaptchaService.js';
import { FastifyReplyError } from '@/misc/fastify-reply-error.js'; import { FastifyReplyError } from '@/misc/fastify-reply-error.js';
import { RateLimiterService } from './RateLimiterService.js'; import { RateLimiterService } from './RateLimiterService.js';
import { SigninService } from './SigninService.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'; 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() @Injectable()
export class SigninApiService { export class SigninApiService {
constructor( constructor(
@ -101,7 +83,7 @@ export class SigninApiService {
const password = body['password']; const password = body['password'];
const token = body['token']; const token = body['token'];
function error(status: number, error: SigninErrorResponse) { function error(status: number, error: { id: string }) {
reply.code(status); reply.code(status);
return { error }; return { error };
} }
@ -152,21 +134,17 @@ export class SigninApiService {
const securityKeysAvailable = await this.userSecurityKeysRepository.countBy({ userId: user.id }).then(result => result >= 1); const securityKeysAvailable = await this.userSecurityKeysRepository.countBy({ userId: user.id }).then(result => result >= 1);
if (password == null) { if (password == null) {
reply.code(403); reply.code(200);
if (profile.twoFactorEnabled) { if (profile.twoFactorEnabled) {
return { return {
error: { finished: false,
id: '144ff4f8-bd6c-41bc-82c3-b672eb09efbf',
next: 'password', next: 'password',
}, } satisfies Misskey.entities.SigninFlowResponse;
} satisfies { error: SigninErrorResponse };
} else { } else {
return { return {
error: { finished: false,
id: '144ff4f8-bd6c-41bc-82c3-b672eb09efbf',
next: 'captcha', next: 'captcha',
}, } satisfies Misskey.entities.SigninFlowResponse;
} satisfies { error: SigninErrorResponse };
} }
} }
@ -178,7 +156,7 @@ export class SigninApiService {
// Compare password // Compare password
const same = await bcrypt.compare(password, profile.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 // Append signin history
await this.signinsRepository.insert({ await this.signinsRepository.insert({
id: this.idService.gen(), id: this.idService.gen(),
@ -268,27 +246,23 @@ export class SigninApiService {
const authRequest = await this.webAuthnService.initiateAuthentication(user.id); const authRequest = await this.webAuthnService.initiateAuthentication(user.id);
reply.code(403); reply.code(200);
return { return {
error: { finished: false,
id: '06e661b9-8146-4ae3-bde5-47138c0ae0c4',
next: 'passkey', next: 'passkey',
authRequest, authRequest,
}, } satisfies Misskey.entities.SigninFlowResponse;
} satisfies { error: SigninErrorResponse };
} else { } else {
if (!same || !profile.twoFactorEnabled) { if (!same || !profile.twoFactorEnabled) {
return await fail(403, { return await fail(403, {
id: '932c904e-9460-45b7-9ce6-7ed33be7eb2c', id: '932c904e-9460-45b7-9ce6-7ed33be7eb2c',
}); });
} else { } else {
reply.code(403); reply.code(200);
return { return {
error: { finished: false,
id: '144ff4f8-bd6c-41bc-82c3-b672eb09efbf',
next: 'totp', next: 'totp',
}, } satisfies Misskey.entities.SigninFlowResponse;
} satisfies { error: SigninErrorResponse };
} }
} }
// never get here // never get here

View File

@ -4,6 +4,7 @@
*/ */
import { Inject, Injectable } from '@nestjs/common'; import { Inject, Injectable } from '@nestjs/common';
import * as Misskey from 'misskey-js';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
import type { SigninsRepository, UserProfilesRepository } from '@/models/_.js'; import type { SigninsRepository, UserProfilesRepository } from '@/models/_.js';
import { IdService } from '@/core/IdService.js'; import { IdService } from '@/core/IdService.js';
@ -57,9 +58,10 @@ export class SigninService {
reply.code(200); reply.code(200);
return { return {
finished: true,
id: user.id, id: user.id,
i: user.token, i: user.token!,
}; } satisfies Misskey.entities.SigninFlowResponse;
} }
} }

View File

@ -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_relays_remove from './endpoints/admin/relays/remove.js';
import * as ep___admin_resetPassword from './endpoints/admin/reset-password.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_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_sendEmail from './endpoints/admin/send-email.js';
import * as ep___admin_serverInfo from './endpoints/admin/server-info.js'; import * as ep___admin_serverInfo from './endpoints/admin/server-info.js';
import * as ep___admin_showModerationLogs from './endpoints/admin/show-moderation-logs.js'; import * as ep___admin_showModerationLogs from './endpoints/admin/show-moderation-logs.js';
@ -458,6 +460,8 @@ const eps = [
['admin/relays/remove', ep___admin_relays_remove], ['admin/relays/remove', ep___admin_relays_remove],
['admin/reset-password', ep___admin_resetPassword], ['admin/reset-password', ep___admin_resetPassword],
['admin/resolve-abuse-user-report', ep___admin_resolveAbuseUserReport], ['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/send-email', ep___admin_sendEmail],
['admin/server-info', ep___admin_serverInfo], ['admin/server-info', ep___admin_serverInfo],
['admin/show-moderation-logs', ep___admin_showModerationLogs], ['admin/show-moderation-logs', ep___admin_showModerationLogs],

View File

@ -71,9 +71,22 @@ export const meta = {
}, },
assignee: { assignee: {
type: 'object', type: 'object',
nullable: true, optional: true, nullable: true, optional: false,
ref: 'UserDetailedNotMe', ref: 'UserDetailedNotMe',
}, },
forwarded: {
type: 'boolean',
nullable: false, optional: false,
},
resolvedAs: {
type: 'string',
nullable: true, optional: false,
enum: ['accept', 'reject', null],
},
moderationNote: {
type: 'string',
nullable: false, optional: false,
},
}, },
}, },
}, },
@ -88,7 +101,6 @@ export const paramDef = {
state: { type: 'string', nullable: true, default: null }, state: { type: 'string', nullable: true, default: null },
reporterOrigin: { type: 'string', enum: ['combined', 'local', 'remote'], default: 'combined' }, reporterOrigin: { type: 'string', enum: ['combined', 'local', 'remote'], default: 'combined' },
targetUserOrigin: { type: 'string', enum: ['combined', 'local', 'remote'], default: 'combined' }, targetUserOrigin: { type: 'string', enum: ['combined', 'local', 'remote'], default: 'combined' },
forwarded: { type: 'boolean', default: false },
}, },
required: [], required: [],
} as const; } as const;

View File

@ -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<typeof meta, typeof paramDef> { // 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);
});
}
}

View File

@ -32,7 +32,7 @@ export const paramDef = {
type: 'object', type: 'object',
properties: { properties: {
reportId: { type: 'string', format: 'misskey:id' }, reportId: { type: 'string', format: 'misskey:id' },
forward: { type: 'boolean', default: false }, resolvedAs: { type: 'string', enum: ['accept', 'reject', null], nullable: true },
}, },
required: ['reportId'], required: ['reportId'],
} as const; } as const;
@ -50,7 +50,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
throw new ApiError(meta.errors.noSuchAbuseReport); 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);
}); });
} }
} }

View File

@ -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<typeof meta, typeof paramDef> { // 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);
});
}
}

View File

@ -8,6 +8,7 @@ import type { FlashsRepository } from '@/models/_.js';
import { Endpoint } from '@/server/api/endpoint-base.js'; import { Endpoint } from '@/server/api/endpoint-base.js';
import { FlashEntityService } from '@/core/entities/FlashEntityService.js'; import { FlashEntityService } from '@/core/entities/FlashEntityService.js';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
import { FlashService } from '@/core/FlashService.js';
export const meta = { export const meta = {
tags: ['flash'], tags: ['flash'],
@ -27,26 +28,25 @@ export const meta = {
export const paramDef = { export const paramDef = {
type: 'object', type: 'object',
properties: {}, properties: {
offset: { type: 'integer', minimum: 0, default: 0 },
limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 },
},
required: [], required: [],
} as const; } as const;
@Injectable() @Injectable()
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
constructor( constructor(
@Inject(DI.flashsRepository) private flashService: FlashService,
private flashsRepository: FlashsRepository,
private flashEntityService: FlashEntityService, private flashEntityService: FlashEntityService,
) { ) {
super(meta, paramDef, async (ps, me) => { super(meta, paramDef, async (ps, me) => {
const query = this.flashsRepository.createQueryBuilder('flash') const result = await this.flashService.featured({
.andWhere('flash.likedCount > 0') offset: ps.offset,
.orderBy('flash.likedCount', 'DESC'); limit: ps.limit,
});
const flashs = await query.limit(10).getMany(); return await this.flashEntityService.packMany(result, me);
return await this.flashEntityService.packMany(flashs, me);
}); });
} }
} }

View File

@ -99,6 +99,8 @@ export const moderationLogTypes = [
'markSensitiveDriveFile', 'markSensitiveDriveFile',
'unmarkSensitiveDriveFile', 'unmarkSensitiveDriveFile',
'resolveAbuseReport', 'resolveAbuseReport',
'forwardAbuseReport',
'updateAbuseReportNote',
'createInvitation', 'createInvitation',
'createAd', 'createAd',
'updateAd', 'updateAd',
@ -267,7 +269,18 @@ export type ModerationLogPayloads = {
resolveAbuseReport: { resolveAbuseReport: {
reportId: string; reportId: string;
report: any; report: any;
forwarded: boolean; forwarded?: boolean;
resolvedAs?: string | null;
};
forwardAbuseReport: {
reportId: string;
report: any;
};
updateAbuseReportNote: {
reportId: string;
report: any;
before: string;
after: string;
}; };
createInvitation: { createInvitation: {
invitations: any[]; invitations: any[];

View File

@ -136,7 +136,7 @@ describe('2要素認証', () => {
keyName: string, keyName: string,
credentialId: Buffer, credentialId: Buffer,
requestOptions: PublicKeyCredentialRequestOptionsJSON, requestOptions: PublicKeyCredentialRequestOptionsJSON,
}): misskey.entities.SigninRequest => { }): misskey.entities.SigninFlowRequest => {
// AuthenticatorAssertionResponse.authenticatorData // AuthenticatorAssertionResponse.authenticatorData
// https://developer.mozilla.org/en-US/docs/Web/API/AuthenticatorAssertionResponse/authenticatorData // https://developer.mozilla.org/en-US/docs/Web/API/AuthenticatorAssertionResponse/authenticatorData
const authenticatorData = Buffer.concat([ const authenticatorData = Buffer.concat([
@ -196,22 +196,21 @@ describe('2要素認証', () => {
}, alice); }, alice);
assert.strictEqual(doneResponse.status, 200); assert.strictEqual(doneResponse.status, 200);
const signinWithoutTokenResponse = await api('signin', { const signinWithoutTokenResponse = await api('signin-flow', {
...signinParam(), ...signinParam(),
}); });
assert.strictEqual(signinWithoutTokenResponse.status, 403); assert.strictEqual(signinWithoutTokenResponse.status, 200);
assert.deepStrictEqual(signinWithoutTokenResponse.body, { assert.deepStrictEqual(signinWithoutTokenResponse.body, {
error: { finished: false,
id: '144ff4f8-bd6c-41bc-82c3-b672eb09efbf',
next: 'totp', next: 'totp',
},
}); });
const signinResponse = await api('signin', { const signinResponse = await api('signin-flow', {
...signinParam(), ...signinParam(),
token: otpToken(registerResponse.body.secret), token: otpToken(registerResponse.body.secret),
}); });
assert.strictEqual(signinResponse.status, 200); assert.strictEqual(signinResponse.status, 200);
assert.strictEqual(signinResponse.body.finished, true);
assert.notEqual(signinResponse.body.i, undefined); assert.notEqual(signinResponse.body.i, undefined);
// 後片付け // 後片付け
@ -252,29 +251,23 @@ describe('2要素認証', () => {
assert.strictEqual(keyDoneResponse.body.id, credentialId.toString('base64url')); assert.strictEqual(keyDoneResponse.body.id, credentialId.toString('base64url'));
assert.strictEqual(keyDoneResponse.body.name, keyName); assert.strictEqual(keyDoneResponse.body.name, keyName);
const signinResponse = await api('signin', { const signinResponse = await api('signin-flow', {
...signinParam(), ...signinParam(),
}); });
const signinResponseBody = signinResponse.body as unknown as { assert.strictEqual(signinResponse.status, 200);
error: { assert.strictEqual(signinResponse.body.finished, false);
id: string; assert.strictEqual(signinResponse.body.next, 'passkey');
next: 'passkey'; assert.notEqual(signinResponse.body.authRequest.challenge, undefined);
authRequest: PublicKeyCredentialRequestOptionsJSON; assert.notEqual(signinResponse.body.authRequest.allowCredentials, undefined);
}; assert.strictEqual(signinResponse.body.authRequest.allowCredentials && signinResponse.body.authRequest.allowCredentials[0]?.id, credentialId.toString('base64url'));
};
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({ const signinResponse2 = await api('signin-flow', signinWithSecurityKeyParam({
keyName, keyName,
credentialId, credentialId,
requestOptions: signinResponseBody.error.authRequest, requestOptions: signinResponse.body.authRequest,
})); }));
assert.strictEqual(signinResponse2.status, 200); assert.strictEqual(signinResponse2.status, 200);
assert.strictEqual(signinResponse2.body.finished, true);
assert.notEqual(signinResponse2.body.i, undefined); assert.notEqual(signinResponse2.body.i, undefined);
// 後片付け // 後片付け
@ -320,32 +313,26 @@ describe('2要素認証', () => {
assert.strictEqual(iResponse.status, 200); assert.strictEqual(iResponse.status, 200);
assert.strictEqual(iResponse.body.usePasswordLessLogin, true); assert.strictEqual(iResponse.body.usePasswordLessLogin, true);
const signinResponse = await api('signin', { const signinResponse = await api('signin-flow', {
...signinParam(), ...signinParam(),
password: '', password: '',
}); });
const signinResponseBody = signinResponse.body as unknown as { assert.strictEqual(signinResponse.status, 200);
error: { assert.strictEqual(signinResponse.body.finished, false);
id: string; assert.strictEqual(signinResponse.body.next, 'passkey');
next: 'passkey'; assert.notEqual(signinResponse.body.authRequest.challenge, undefined);
authRequest: PublicKeyCredentialRequestOptionsJSON; assert.notEqual(signinResponse.body.authRequest.allowCredentials, undefined);
};
};
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);
const signinResponse2 = await api('signin', { const signinResponse2 = await api('signin-flow', {
...signinWithSecurityKeyParam({ ...signinWithSecurityKeyParam({
keyName, keyName,
credentialId, credentialId,
requestOptions: signinResponseBody.error.authRequest, requestOptions: signinResponse.body.authRequest,
} as any), } as any),
password: '', password: '',
}); });
assert.strictEqual(signinResponse2.status, 200); assert.strictEqual(signinResponse2.status, 200);
assert.strictEqual(signinResponse2.body.finished, true);
assert.notEqual(signinResponse2.body.i, undefined); assert.notEqual(signinResponse2.body.i, undefined);
// 後片付け // 後片付け
@ -450,11 +437,12 @@ describe('2要素認証', () => {
assert.strictEqual(afterIResponse.status, 200); assert.strictEqual(afterIResponse.status, 200);
assert.strictEqual(afterIResponse.body.securityKeys, false); assert.strictEqual(afterIResponse.body.securityKeys, false);
const signinResponse = await api('signin', { const signinResponse = await api('signin-flow', {
...signinParam(), ...signinParam(),
token: otpToken(registerResponse.body.secret), token: otpToken(registerResponse.body.secret),
}); });
assert.strictEqual(signinResponse.status, 200); assert.strictEqual(signinResponse.status, 200);
assert.strictEqual(signinResponse.body.finished, true);
assert.notEqual(signinResponse.body.i, undefined); assert.notEqual(signinResponse.body.i, undefined);
// 後片付け // 後片付け
@ -485,10 +473,11 @@ describe('2要素認証', () => {
}, alice); }, alice);
assert.strictEqual(unregisterResponse.status, 204); assert.strictEqual(unregisterResponse.status, 204);
const signinResponse = await api('signin', { const signinResponse = await api('signin-flow', {
...signinParam(), ...signinParam(),
}); });
assert.strictEqual(signinResponse.status, 200); assert.strictEqual(signinResponse.status, 200);
assert.strictEqual(signinResponse.body.finished, true);
assert.notEqual(signinResponse.body.i, undefined); assert.notEqual(signinResponse.body.i, undefined);
// 後片付け // 後片付け

View File

@ -66,9 +66,9 @@ describe('Endpoints', () => {
}); });
}); });
describe('signin', () => { describe('signin-flow', () => {
test('間違ったパスワードでサインインできない', async () => { test('間違ったパスワードでサインインできない', async () => {
const res = await api('signin', { const res = await api('signin-flow', {
username: 'test1', username: 'test1',
password: 'bar', password: 'bar',
}); });
@ -77,7 +77,7 @@ describe('Endpoints', () => {
}); });
test('クエリをインジェクションできない', async () => { test('クエリをインジェクションできない', async () => {
const res = await api('signin', { const res = await api('signin-flow', {
username: 'test1', username: 'test1',
// @ts-expect-error password must be string // @ts-expect-error password must be string
password: { password: {
@ -89,7 +89,7 @@ describe('Endpoints', () => {
}); });
test('正しい情報でサインインできる', async () => { test('正しい情報でサインインできる', async () => {
const res = await api('signin', { const res = await api('signin-flow', {
username: 'test1', username: 'test1',
password: 'test1', password: 'test1',
}); });

View File

@ -157,7 +157,6 @@ describe('[シナリオ] ユーザ通報', () => {
const webhookBody2 = await captureWebhook(async () => { const webhookBody2 = await captureWebhook(async () => {
await resolveAbuseReport({ await resolveAbuseReport({
reportId: webhookBody1.body.id, reportId: webhookBody1.body.id,
forward: false,
}, admin); }, admin);
}); });
@ -214,7 +213,6 @@ describe('[シナリオ] ユーザ通報', () => {
const webhookBody2 = await captureWebhook(async () => { const webhookBody2 = await captureWebhook(async () => {
await resolveAbuseReport({ await resolveAbuseReport({
reportId: abuseReportId, reportId: abuseReportId,
forward: false,
}, admin); }, admin);
}); });
@ -257,7 +255,6 @@ describe('[シナリオ] ユーザ通報', () => {
const webhookBody2 = await captureWebhook(async () => { const webhookBody2 = await captureWebhook(async () => {
await resolveAbuseReport({ await resolveAbuseReport({
reportId: webhookBody1.body.id, reportId: webhookBody1.body.id,
forward: false,
}, admin); }, admin);
}).catch(e => e.message); }).catch(e => e.message);
@ -288,7 +285,6 @@ describe('[シナリオ] ユーザ通報', () => {
const webhookBody2 = await captureWebhook(async () => { const webhookBody2 = await captureWebhook(async () => {
await resolveAbuseReport({ await resolveAbuseReport({
reportId: abuseReportId, reportId: abuseReportId,
forward: false,
}, admin); }, admin);
}).catch(e => e.message); }).catch(e => e.message);
@ -319,7 +315,6 @@ describe('[シナリオ] ユーザ通報', () => {
const webhookBody2 = await captureWebhook(async () => { const webhookBody2 = await captureWebhook(async () => {
await resolveAbuseReport({ await resolveAbuseReport({
reportId: abuseReportId, reportId: abuseReportId,
forward: false,
}, admin); }, admin);
}).catch(e => e.message); }).catch(e => e.message);
@ -350,7 +345,6 @@ describe('[シナリオ] ユーザ通報', () => {
const webhookBody2 = await captureWebhook(async () => { const webhookBody2 = await captureWebhook(async () => {
await resolveAbuseReport({ await resolveAbuseReport({
reportId: abuseReportId, reportId: abuseReportId,
forward: false,
}, admin); }, admin);
}).catch(e => e.message); }).catch(e => e.message);

View File

@ -5,6 +5,7 @@
import { jest } from '@jest/globals'; import { jest } from '@jest/globals';
import { Test, TestingModule } from '@nestjs/testing'; import { Test, TestingModule } from '@nestjs/testing';
import { randomString } from '../utils.js';
import { AbuseReportNotificationService } from '@/core/AbuseReportNotificationService.js'; import { AbuseReportNotificationService } from '@/core/AbuseReportNotificationService.js';
import { import {
AbuseReportNotificationRecipientRepository, AbuseReportNotificationRecipientRepository,
@ -25,7 +26,7 @@ import { ModerationLogService } from '@/core/ModerationLogService.js';
import { GlobalEventService } from '@/core/GlobalEventService.js'; import { GlobalEventService } from '@/core/GlobalEventService.js';
import { RecipientMethod } from '@/models/AbuseReportNotificationRecipient.js'; import { RecipientMethod } from '@/models/AbuseReportNotificationRecipient.js';
import { SystemWebhookService } from '@/core/SystemWebhookService.js'; import { SystemWebhookService } from '@/core/SystemWebhookService.js';
import { randomString } from '../utils.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js';
process.env.NODE_ENV = 'test'; process.env.NODE_ENV = 'test';
@ -110,6 +111,9 @@ describe('AbuseReportNotificationService', () => {
{ {
provide: SystemWebhookService, useFactory: () => ({ enqueueSystemWebhook: jest.fn() }), provide: SystemWebhookService, useFactory: () => ({ enqueueSystemWebhook: jest.fn() }),
}, },
{
provide: UserEntityService, useFactory: () => ({ pack: (v: any) => v }),
},
{ {
provide: EmailService, useFactory: () => ({ sendEmail: jest.fn() }), provide: EmailService, useFactory: () => ({ sendEmail: jest.fn() }),
}, },

View File

@ -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<MiFlash>) {
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<MiUser> = {}) {
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]);
});
});
});

View File

@ -6,26 +6,33 @@ SPDX-License-Identifier: AGPL-3.0-only
<template> <template>
<MkFolder> <MkFolder>
<template #icon> <template #icon>
<i v-if="report.resolved" class="ti ti-check" style="color: var(--success)"></i> <i v-if="report.resolved && report.resolvedAs === 'accept'" class="ti ti-check" style="color: var(--success)"></i>
<i v-else-if="report.resolved && report.resolvedAs === 'reject'" class="ti ti-x" style="color: var(--error)"></i>
<i v-else-if="report.resolved" class="ti ti-slash"></i>
<i v-else class="ti ti-exclamation-circle" style="color: var(--warn)"></i> <i v-else class="ti ti-exclamation-circle" style="color: var(--warn)"></i>
</template> </template>
<template #label><MkAcct :user="report.targetUser"/> (by <MkAcct :user="report.reporter"/>)</template> <template #label><MkAcct :user="report.targetUser"/> (by <MkAcct :user="report.reporter"/>)</template>
<template #caption>{{ report.comment }}</template> <template #caption>{{ report.comment }}</template>
<template #suffix><MkTime :time="report.createdAt"/></template> <template #suffix><MkTime :time="report.createdAt"/></template>
<template v-if="!report.resolved" #footer> <template #footer>
<div class="_buttons"> <div class="_buttons">
<MkButton primary @click="resolve">{{ i18n.ts.abuseMarkAsResolved }}</MkButton> <template v-if="!report.resolved">
<template v-if="report.targetUser.host == null || report.resolved"> <MkButton @click="resolve('accept')"><i class="ti ti-check" style="color: var(--success)"></i> {{ i18n.ts._abuseUserReport.resolve }} ({{ i18n.ts._abuseUserReport.accept }})</MkButton>
<MkButton primary @click="resolveAndForward">{{ i18n.ts.forwardReport }}</MkButton> <MkButton @click="resolve('reject')"><i class="ti ti-x" style="color: var(--error)"></i> {{ i18n.ts._abuseUserReport.resolve }} ({{ i18n.ts._abuseUserReport.reject }})</MkButton>
<div v-tooltip:dialog="i18n.ts.forwardReportIsAnonymous" class="_button _help"><i class="ti ti-help-circle"></i></div> <MkButton @click="resolve(null)"><i class="ti ti-slash"></i> {{ i18n.ts._abuseUserReport.resolve }} ({{ i18n.ts.other }})</MkButton>
</template> </template>
<template v-if="report.targetUser.host == null">
<MkButton :disabled="report.forwarded" primary @click="forward"><i class="ti ti-corner-up-right"></i> {{ i18n.ts._abuseUserReport.forward }}</MkButton>
<div v-tooltip:dialog="i18n.ts._abuseUserReport.forwardDescription" class="_button _help"><i class="ti ti-help-circle"></i></div>
</template>
<button class="_button" style="margin-left: auto; width: 34px;" @click="showMenu"><i class="ti ti-dots"></i></button>
</div> </div>
</template> </template>
<div :class="$style.root" class="_gaps_s"> <div :class="$style.root" class="_gaps_s">
<MkFolder :withSpacer="false"> <MkFolder :withSpacer="false">
<template #icon><MkAvatar :user="report.targetUser" style="width: 18px; height: 18px;"/></template> <template #icon><MkAvatar :user="report.targetUser" style="width: 18px; height: 18px;"/></template>
<template #label>Target: <MkAcct :user="report.targetUser"/></template> <template #label>{{ i18n.ts.target }}: <MkAcct :user="report.targetUser"/></template>
<template #suffix>#{{ report.targetUserId.toUpperCase() }}</template> <template #suffix>#{{ report.targetUserId.toUpperCase() }}</template>
<div style="container-type: inline-size;"> <div style="container-type: inline-size;">
@ -36,7 +43,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkFolder :defaultOpen="true"> <MkFolder :defaultOpen="true">
<template #icon><i class="ti ti-message-2"></i></template> <template #icon><i class="ti ti-message-2"></i></template>
<template #label>{{ i18n.ts.details }}</template> <template #label>{{ i18n.ts.details }}</template>
<div> <div class="_gaps_s">
<Mfm :text="report.comment" :linkNavigationBehavior="'window'"/> <Mfm :text="report.comment" :linkNavigationBehavior="'window'"/>
</div> </div>
</MkFolder> </MkFolder>
@ -51,6 +58,17 @@ SPDX-License-Identifier: AGPL-3.0-only
</div> </div>
</MkFolder> </MkFolder>
<MkFolder :defaultOpen="false">
<template #icon><i class="ti ti-message-2"></i></template>
<template #label>{{ i18n.ts.moderationNote }}</template>
<template #suffix>{{ moderationNote.length > 0 ? '...' : i18n.ts.none }}</template>
<div class="_gaps_s">
<MkTextarea v-model="moderationNote" manualSave>
<template #caption>{{ i18n.ts.moderationNoteDescription }}</template>
</MkTextarea>
</div>
</MkFolder>
<div v-if="report.assignee"> <div v-if="report.assignee">
{{ i18n.ts.moderator }}: {{ i18n.ts.moderator }}:
<MkAcct :user="report.assignee"/> <MkAcct :user="report.assignee"/>
@ -60,7 +78,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { provide, ref } from 'vue'; import { provide, ref, watch } from 'vue';
import * as Misskey from 'misskey-js'; import * as Misskey from 'misskey-js';
import MkButton from '@/components/MkButton.vue'; import MkButton from '@/components/MkButton.vue';
import MkSwitch from '@/components/MkSwitch.vue'; import MkSwitch from '@/components/MkSwitch.vue';
@ -71,6 +89,8 @@ import { dateString } from '@/filters/date.js';
import MkFolder from '@/components/MkFolder.vue'; import MkFolder from '@/components/MkFolder.vue';
import RouterView from '@/components/global/RouterView.vue'; import RouterView from '@/components/global/RouterView.vue';
import { useRouterFactory } from '@/router/supplier'; import { useRouterFactory } from '@/router/supplier';
import MkTextarea from '@/components/MkTextarea.vue';
import { copyToClipboard } from '@/scripts/copy-to-clipboard.js';
const props = defineProps<{ const props = defineProps<{
report: Misskey.entities.AdminAbuseUserReportsResponse[number]; report: Misskey.entities.AdminAbuseUserReportsResponse[number];
@ -86,22 +106,48 @@ targetRouter.init();
const reporterRouter = routerFactory(`/admin/user/${props.report.reporterId}`); const reporterRouter = routerFactory(`/admin/user/${props.report.reporterId}`);
reporterRouter.init(); reporterRouter.init();
function resolve() { const moderationNote = ref(props.report.moderationNote ?? '');
watch(moderationNote, async () => {
os.apiWithDialog('admin/update-abuse-user-report', {
reportId: props.report.id,
moderationNote: moderationNote.value,
}).then(() => {
});
});
function resolve(resolvedAs) {
os.apiWithDialog('admin/resolve-abuse-user-report', { os.apiWithDialog('admin/resolve-abuse-user-report', {
reportId: props.report.id, reportId: props.report.id,
resolvedAs,
}).then(() => { }).then(() => {
emit('resolved', props.report.id); emit('resolved', props.report.id);
}); });
} }
function resolveAndForward() { function forward() {
os.apiWithDialog('admin/resolve-abuse-user-report', { os.apiWithDialog('admin/forward-abuse-user-report', {
forward: true,
reportId: props.report.id, reportId: props.report.id,
}).then(() => { }).then(() => {
emit('resolved', props.report.id);
}); });
} }
function showMenu(ev: MouseEvent) {
os.popupMenu([{
icon: 'ti ti-id',
text: 'Copy ID',
action: () => {
copyToClipboard(props.report.id);
},
}, {
icon: 'ti ti-json',
text: 'Copy JSON',
action: () => {
copyToClipboard(JSON.stringify(props.report, null, '\t'));
},
}], ev.currentTarget ?? ev.target);
}
</script> </script>
<style lang="scss" module> <style lang="scss" module>

View File

@ -8,7 +8,7 @@ SPDX-License-Identifier: AGPL-3.0-only
:class="[ :class="[
$style.root, $style.root,
tail === 'left' ? $style.left : $style.right, tail === 'left' ? $style.left : $style.right,
negativeMargin === true && $style.negativeMergin, negativeMargin === true && $style.negativeMargin,
shadow === true && $style.shadow, shadow === true && $style.shadow,
]" ]"
> >
@ -54,7 +54,7 @@ withDefaults(defineProps<{
&.left { &.left {
padding-left: calc(var(--fukidashi-radius) * .13); padding-left: calc(var(--fukidashi-radius) * .13);
&.negativeMergin { &.negativeMargin {
margin-left: calc(calc(var(--fukidashi-radius) * .13) * -1); margin-left: calc(calc(var(--fukidashi-radius) * .13) * -1);
} }
} }
@ -62,7 +62,7 @@ withDefaults(defineProps<{
&.right { &.right {
padding-right: calc(var(--fukidashi-radius) * .13); padding-right: calc(var(--fukidashi-radius) * .13);
&.negativeMergin { &.negativeMargin {
margin-right: calc(calc(var(--fukidashi-radius) * .13) * -1); margin-right: calc(calc(var(--fukidashi-radius) * .13) * -1);
} }
} }

View File

@ -437,9 +437,11 @@ onBeforeUnmount(() => {
&.big:not(.asDrawer) { &.big:not(.asDrawer) {
> .menu { > .menu {
min-width: 230px;
> .item { > .item {
padding: 6px 20px; padding: 6px 20px;
font-size: 1em; font-size: 0.95em;
line-height: 24px; line-height: 24px;
} }
} }

View File

@ -83,7 +83,7 @@ import type { AuthenticationPublicKeyCredential } from '@github/webauthn-json/br
import type { OpenOnRemoteOptions } from '@/scripts/please-login.js'; import type { OpenOnRemoteOptions } from '@/scripts/please-login.js';
const emit = defineEmits<{ const emit = defineEmits<{
(ev: 'login', v: Misskey.entities.SigninResponse): void; (ev: 'login', v: Misskey.entities.SigninFlowResponse): void;
}>(); }>();
const props = withDefaults(defineProps<{ const props = withDefaults(defineProps<{
@ -212,41 +212,26 @@ async function onTotpSubmitted(token: string) {
} }
} }
async function tryLogin(req: Partial<Misskey.entities.SigninRequest>): Promise<Misskey.entities.SigninResponse> { async function tryLogin(req: Partial<Misskey.entities.SigninFlowRequest>): Promise<Misskey.entities.SigninFlowResponse> {
const _req = { const _req = {
username: req.username ?? userInfo.value?.username, username: req.username ?? userInfo.value?.username,
...req, ...req,
}; };
function assertIsSigninRequest(x: Partial<Misskey.entities.SigninRequest>): x is Misskey.entities.SigninRequest { function assertIsSigninFlowRequest(x: Partial<Misskey.entities.SigninFlowRequest>): x is Misskey.entities.SigninFlowRequest {
return x.username != null; return x.username != null;
} }
if (!assertIsSigninRequest(_req)) { if (!assertIsSigninFlowRequest(_req)) {
throw new Error('Invalid request'); throw new Error('Invalid request');
} }
return await misskeyApi('signin', _req).then(async (res) => { return await misskeyApi('signin-flow', _req).then(async (res) => {
if (res.finished) {
emit('login', res); emit('login', res);
await onLoginSucceeded(res); await onLoginSucceeded(res);
return res; } else {
}).catch((err) => { switch (res.next) {
onSigninApiError(err);
return Promise.reject(err);
});
}
async function onLoginSucceeded(res: Misskey.entities.SigninResponse) {
if (props.autoSet) {
await login(res.i);
}
}
function onSigninApiError(err?: any): void {
const id = err?.id ?? null;
if (typeof err === 'object' && 'next' in err) {
switch (err.next) {
case 'captcha': { case 'captcha': {
needCaptcha.value = true; needCaptcha.value = true;
page.value = 'password'; page.value = 'password';
@ -262,9 +247,9 @@ function onSigninApiError(err?: any): void {
break; break;
} }
case 'passkey': { case 'passkey': {
if (webAuthnSupported() && 'authRequest' in err) { if (webAuthnSupported()) {
credentialRequest.value = parseRequestOptionsFromJSON({ credentialRequest.value = parseRequestOptionsFromJSON({
publicKey: err.authRequest, publicKey: res.authRequest,
}); });
page.value = 'passkey'; page.value = 'passkey';
} else { } else {
@ -273,7 +258,33 @@ function onSigninApiError(err?: any): void {
break; break;
} }
} }
} else {
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);
return Promise.reject(err);
});
}
async function onLoginSucceeded(res: Misskey.entities.SigninFlowResponse & { finished: true; }) {
if (props.autoSet) {
await login(res.i);
}
}
function onSigninApiError(err?: any): void {
const id = err?.id ?? null;
switch (id) { switch (id) {
case '6cc579cc-885d-43d8-95c2-b8c7fc963280': { case '6cc579cc-885d-43d8-95c2-b8c7fc963280': {
os.alert({ os.alert({
@ -352,7 +363,6 @@ function onSigninApiError(err?: any): void {
}); });
} }
} }
}
if (doingPasskeyFromInputPage.value === true) { if (doingPasskeyFromInputPage.value === true) {
doingPasskeyFromInputPage.value = false; doingPasskeyFromInputPage.value = false;

View File

@ -98,7 +98,7 @@ const props = withDefaults(defineProps<{
}); });
const emit = defineEmits<{ const emit = defineEmits<{
(ev: 'signup', user: Misskey.entities.SigninResponse): void; (ev: 'signup', user: Misskey.entities.SigninFlowResponse): void;
(ev: 'signupEmailPending'): void; (ev: 'signupEmailPending'): void;
}>(); }>();
@ -269,14 +269,19 @@ async function onSubmit(): Promise<void> {
}); });
emit('signupEmailPending'); emit('signupEmailPending');
} else { } else {
const res = await misskeyApi('signin', { const res = await misskeyApi('signin-flow', {
username: username.value, username: username.value,
password: password.value, password: password.value,
}); });
emit('signup', res); emit('signup', res);
if (props.autoSet) { if (props.autoSet && res.finished) {
return login(res.i); return login(res.i);
} else {
os.alert({
type: 'error',
text: i18n.ts.somethingHappened,
});
} }
} }
} catch { } catch {

View File

@ -47,7 +47,7 @@ const props = withDefaults(defineProps<{
}); });
const emit = defineEmits<{ const emit = defineEmits<{
(ev: 'done', res: Misskey.entities.SigninResponse): void; (ev: 'done', res: Misskey.entities.SigninFlowResponse): void;
(ev: 'closed'): void; (ev: 'closed'): void;
}>(); }>();
@ -55,7 +55,7 @@ const dialog = shallowRef<InstanceType<typeof MkModalWindow>>();
const isAcceptedServerRule = ref(false); const isAcceptedServerRule = ref(false);
function onSignup(res: Misskey.entities.SigninResponse) { function onSignup(res: Misskey.entities.SigninFlowResponse) {
emit('done', res); emit('done', res);
dialog.value?.close(); dialog.value?.close();
} }

View File

@ -53,6 +53,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkTextarea v-model="moderationNote" manualSave> <MkTextarea v-model="moderationNote" manualSave>
<template #label>{{ i18n.ts.moderationNote }}</template> <template #label>{{ i18n.ts.moderationNote }}</template>
<template #caption>{{ i18n.ts.moderationNoteDescription }}</template>
</MkTextarea> </MkTextarea>
<!-- <!--
@ -205,6 +206,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts" setup> <script lang="ts" setup>
import { computed, defineAsyncComponent, watch, ref } from 'vue'; import { computed, defineAsyncComponent, watch, ref } from 'vue';
import * as Misskey from 'misskey-js'; import * as Misskey from 'misskey-js';
import { url } from '@@/js/config.js';
import MkChart from '@/components/MkChart.vue'; import MkChart from '@/components/MkChart.vue';
import MkObjectView from '@/components/MkObjectView.vue'; import MkObjectView from '@/components/MkObjectView.vue';
import MkTextarea from '@/components/MkTextarea.vue'; import MkTextarea from '@/components/MkTextarea.vue';
@ -220,7 +222,6 @@ import MkFileListForAdmin from '@/components/MkFileListForAdmin.vue';
import MkInfo from '@/components/MkInfo.vue'; import MkInfo from '@/components/MkInfo.vue';
import * as os from '@/os.js'; import * as os from '@/os.js';
import { misskeyApi } from '@/scripts/misskey-api.js'; import { misskeyApi } from '@/scripts/misskey-api.js';
import { url } from '@@/js/config.js';
import { acct } from '@/filters/user.js'; import { acct } from '@/filters/user.js';
import { definePageMetadata } from '@/scripts/page-metadata.js'; import { definePageMetadata } from '@/scripts/page-metadata.js';
import { i18n } from '@/i18n.js'; import { i18n } from '@/i18n.js';

View File

@ -12,6 +12,10 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkButton link to="/admin/abuse-report-notification-recipient" primary>{{ i18n.ts.notificationSetting }}</MkButton> <MkButton link to="/admin/abuse-report-notification-recipient" primary>{{ i18n.ts.notificationSetting }}</MkButton>
</div> </div>
<MkInfo v-if="!defaultStore.reactiveState.abusesTutorial.value" closable @close="closeTutorial()">
{{ i18n.ts._abuseUserReport.resolveTutorial }}
</MkInfo>
<div :class="$style.inputs" class="_gaps"> <div :class="$style.inputs" class="_gaps">
<MkSelect v-model="state" style="margin: 0; flex: 1;"> <MkSelect v-model="state" style="margin: 0; flex: 1;">
<template #label>{{ i18n.ts.state }}</template> <template #label>{{ i18n.ts.state }}</template>
@ -56,7 +60,6 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts" setup> <script lang="ts" setup>
import { computed, shallowRef, ref } from 'vue'; import { computed, shallowRef, ref } from 'vue';
import XHeader from './_header_.vue'; import XHeader from './_header_.vue';
import MkSelect from '@/components/MkSelect.vue'; import MkSelect from '@/components/MkSelect.vue';
import MkPagination from '@/components/MkPagination.vue'; import MkPagination from '@/components/MkPagination.vue';
@ -64,6 +67,8 @@ import XAbuseReport from '@/components/MkAbuseReport.vue';
import { i18n } from '@/i18n.js'; import { i18n } from '@/i18n.js';
import { definePageMetadata } from '@/scripts/page-metadata.js'; import { definePageMetadata } from '@/scripts/page-metadata.js';
import MkButton from '@/components/MkButton.vue'; import MkButton from '@/components/MkButton.vue';
import MkInfo from '@/components/MkInfo.vue';
import { defaultStore } from '@/store.js';
const reports = shallowRef<InstanceType<typeof MkPagination>>(); const reports = shallowRef<InstanceType<typeof MkPagination>>();
@ -87,6 +92,10 @@ function resolved(reportId) {
reports.value?.removeItem(reportId); reports.value?.removeItem(reportId);
} }
function closeTutorial() {
defaultStore.set('abusesTutorial', false);
}
const headerActions = computed(() => []); const headerActions = computed(() => []);
const headerTabs = computed(() => []); const headerTabs = computed(() => []);

View File

@ -165,6 +165,11 @@ SPDX-License-Identifier: AGPL-3.0-only
<CodeDiff :context="5" :hideHeader="true" :oldString="JSON5.stringify(log.info.before, null, '\t')" :newString="JSON5.stringify(log.info.after, null, '\t')" language="javascript" maxHeight="300px"/> <CodeDiff :context="5" :hideHeader="true" :oldString="JSON5.stringify(log.info.before, null, '\t')" :newString="JSON5.stringify(log.info.after, null, '\t')" language="javascript" maxHeight="300px"/>
</div> </div>
</template> </template>
<template v-else-if="log.type === 'updateAbuseReportNote'">
<div :class="$style.diff">
<CodeDiff :context="5" :hideHeader="true" :oldString="log.info.before ?? ''" :newString="log.info.after ?? ''" maxHeight="300px"/>
</div>
</template>
<details> <details>
<summary>raw</summary> <summary>raw</summary>

View File

@ -55,7 +55,8 @@ const tab = ref('featured');
const featuredFlashsPagination = { const featuredFlashsPagination = {
endpoint: 'flash/featured' as const, endpoint: 'flash/featured' as const,
noPaging: true, limit: 5,
offsetMode: true,
}; };
const myFlashsPagination = { const myFlashsPagination = {
endpoint: 'flash/my' as const, endpoint: 'flash/my' as const,

View File

@ -51,6 +51,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkButton @click="refreshMetadata"><i class="ti ti-refresh"></i> Refresh metadata</MkButton> <MkButton @click="refreshMetadata"><i class="ti ti-refresh"></i> Refresh metadata</MkButton>
<MkTextarea v-model="moderationNote" manualSave> <MkTextarea v-model="moderationNote" manualSave>
<template #label>{{ i18n.ts.moderationNote }}</template> <template #label>{{ i18n.ts.moderationNote }}</template>
<template #caption>{{ i18n.ts.moderationNoteDescription }}</template>
</MkTextarea> </MkTextarea>
</div> </div>
</FormSection> </FormSection>

View File

@ -50,7 +50,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<div v-if="user.followedMessage != null" class="followedMessage"> <div v-if="user.followedMessage != null" class="followedMessage">
<MkFukidashi class="fukidashi" :tail="narrow ? 'none' : 'left'" negativeMargin shadow> <MkFukidashi class="fukidashi" :tail="narrow ? 'none' : 'left'" negativeMargin shadow>
<div class="messageHeader">{{ i18n.ts.messageToFollower }}</div> <div class="messageHeader">{{ i18n.ts.messageToFollower }}</div>
<div><Mfm :text="user.followedMessage" :author="user"/></div> <div><MkSparkle><Mfm :plain="true" :text="user.followedMessage" :author="user"/></MkSparkle></div>
</MkFukidashi> </MkFukidashi>
</div> </div>
<div v-if="user.roles.length > 0" class="roles"> <div v-if="user.roles.length > 0" class="roles">
@ -64,6 +64,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<div v-if="iAmModerator" class="moderationNote"> <div v-if="iAmModerator" class="moderationNote">
<MkTextarea v-if="editModerationNote || (moderationNote != null && moderationNote !== '')" v-model="moderationNote" manualSave> <MkTextarea v-if="editModerationNote || (moderationNote != null && moderationNote !== '')" v-model="moderationNote" manualSave>
<template #label>{{ i18n.ts.moderationNote }}</template> <template #label>{{ i18n.ts.moderationNote }}</template>
<template #caption>{{ i18n.ts.moderationNoteDescription }}</template>
</MkTextarea> </MkTextarea>
<div v-else> <div v-else>
<MkButton small @click="editModerationNote = true">{{ i18n.ts.addModerationNote }}</MkButton> <MkButton small @click="editModerationNote = true">{{ i18n.ts.addModerationNote }}</MkButton>
@ -159,6 +160,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts" setup> <script lang="ts" setup>
import { defineAsyncComponent, computed, onMounted, onUnmounted, nextTick, watch, ref } from 'vue'; import { defineAsyncComponent, computed, onMounted, onUnmounted, nextTick, watch, ref } from 'vue';
import * as Misskey from 'misskey-js'; import * as Misskey from 'misskey-js';
import { getScrollPosition } from '@@/js/scroll.js';
import MkNote from '@/components/MkNote.vue'; import MkNote from '@/components/MkNote.vue';
import MkFollowButton from '@/components/MkFollowButton.vue'; import MkFollowButton from '@/components/MkFollowButton.vue';
import MkAccountMoved from '@/components/MkAccountMoved.vue'; import MkAccountMoved from '@/components/MkAccountMoved.vue';
@ -168,7 +170,6 @@ import MkTextarea from '@/components/MkTextarea.vue';
import MkOmit from '@/components/MkOmit.vue'; import MkOmit from '@/components/MkOmit.vue';
import MkInfo from '@/components/MkInfo.vue'; import MkInfo from '@/components/MkInfo.vue';
import MkButton from '@/components/MkButton.vue'; import MkButton from '@/components/MkButton.vue';
import { getScrollPosition } from '@@/js/scroll.js';
import { getUserMenu } from '@/scripts/get-user-menu.js'; import { getUserMenu } from '@/scripts/get-user-menu.js';
import number from '@/filters/number.js'; import number from '@/filters/number.js';
import { userPage } from '@/filters/user.js'; import { userPage } from '@/filters/user.js';
@ -182,6 +183,7 @@ import { misskeyApi } from '@/scripts/misskey-api.js';
import { isFollowingVisibleForMe, isFollowersVisibleForMe } from '@/scripts/isFfVisibleForMe.js'; import { isFollowingVisibleForMe, isFollowersVisibleForMe } from '@/scripts/isFfVisibleForMe.js';
import { useRouter } from '@/router/supplier.js'; import { useRouter } from '@/router/supplier.js';
import { getStaticImageUrl } from '@/scripts/media-proxy.js'; import { getStaticImageUrl } from '@/scripts/media-proxy.js';
import MkSparkle from '@/components/MkSparkle.vue';
function calcAge(birthdate: string): number { function calcAge(birthdate: string): number {
const date = new Date(birthdate); const date = new Date(birthdate);
@ -472,7 +474,7 @@ onUnmounted(() => {
> .fukidashi { > .fukidashi {
display: block; display: block;
--fukidashi-bg: color-mix(in srgb, var(--love), var(--panel) 85%); --fukidashi-bg: color-mix(in srgb, var(--accent), var(--panel) 85%);
--fukidashi-radius: 16px; --fukidashi-radius: 16px;
font-size: 0.9em; font-size: 0.9em;

View File

@ -78,6 +78,10 @@ export const defaultStore = markRaw(new Storage('base', {
global: false, global: false,
}, },
}, },
abusesTutorial: {
where: 'account',
default: false,
},
keepCw: { keepCw: {
where: 'account', where: 'account',
default: true, default: true,
@ -222,7 +226,7 @@ export const defaultStore = markRaw(new Storage('base', {
}, },
animatedMfm: { animatedMfm: {
where: 'device', where: 'device',
default: false, default: true,
}, },
advancedMfm: { advancedMfm: {
where: 'device', where: 'device',

View File

@ -213,6 +213,9 @@ type AdminFederationRemoveAllFollowingRequest = operations['admin___federation__
// @public (undocumented) // @public (undocumented)
type AdminFederationUpdateInstanceRequest = operations['admin___federation___update-instance']['requestBody']['content']['application/json']; type AdminFederationUpdateInstanceRequest = operations['admin___federation___update-instance']['requestBody']['content']['application/json'];
// @public (undocumented)
type AdminForwardAbuseUserReportRequest = operations['admin___forward-abuse-user-report']['requestBody']['content']['application/json'];
// @public (undocumented) // @public (undocumented)
type AdminGetIndexStatsResponse = operations['admin___get-index-stats']['responses']['200']['content']['application/json']; type AdminGetIndexStatsResponse = operations['admin___get-index-stats']['responses']['200']['content']['application/json'];
@ -378,6 +381,9 @@ type AdminUnsetUserBannerRequest = operations['admin___unset-user-banner']['requ
// @public (undocumented) // @public (undocumented)
type AdminUnsuspendUserRequest = operations['admin___unsuspend-user']['requestBody']['content']['application/json']; type AdminUnsuspendUserRequest = operations['admin___unsuspend-user']['requestBody']['content']['application/json'];
// @public (undocumented)
type AdminUpdateAbuseUserReportRequest = operations['admin___update-abuse-user-report']['requestBody']['content']['application/json'];
// @public (undocumented) // @public (undocumented)
type AdminUpdateMetaRequest = operations['admin___update-meta']['requestBody']['content']['application/json']; type AdminUpdateMetaRequest = operations['admin___update-meta']['requestBody']['content']['application/json'];
@ -1158,9 +1164,9 @@ export type Endpoints = Overwrite<Endpoints_2, {
req: SignupPendingRequest; req: SignupPendingRequest;
res: SignupPendingResponse; res: SignupPendingResponse;
}; };
'signin': { 'signin-flow': {
req: SigninRequest; req: SigninFlowRequest;
res: SigninResponse; res: SigninFlowResponse;
}; };
'signin-with-passkey': { 'signin-with-passkey': {
req: SigninWithPasskeyRequest; req: SigninWithPasskeyRequest;
@ -1208,11 +1214,11 @@ declare namespace entities {
SignupResponse, SignupResponse,
SignupPendingRequest, SignupPendingRequest,
SignupPendingResponse, SignupPendingResponse,
SigninRequest, SigninFlowRequest,
SigninFlowResponse,
SigninWithPasskeyRequest, SigninWithPasskeyRequest,
SigninWithPasskeyInitResponse, SigninWithPasskeyInitResponse,
SigninWithPasskeyResponse, SigninWithPasskeyResponse,
SigninResponse,
PartialRolePolicyOverride, PartialRolePolicyOverride,
EmptyRequest, EmptyRequest,
EmptyResponse, EmptyResponse,
@ -1298,6 +1304,8 @@ declare namespace entities {
AdminResetPasswordRequest, AdminResetPasswordRequest,
AdminResetPasswordResponse, AdminResetPasswordResponse,
AdminResolveAbuseUserReportRequest, AdminResolveAbuseUserReportRequest,
AdminForwardAbuseUserReportRequest,
AdminUpdateAbuseUserReportRequest,
AdminSendEmailRequest, AdminSendEmailRequest,
AdminServerInfoResponse, AdminServerInfoResponse,
AdminShowModerationLogsRequest, AdminShowModerationLogsRequest,
@ -1682,6 +1690,7 @@ declare namespace entities {
FlashCreateRequest, FlashCreateRequest,
FlashCreateResponse, FlashCreateResponse,
FlashDeleteRequest, FlashDeleteRequest,
FlashFeaturedRequest,
FlashFeaturedResponse, FlashFeaturedResponse,
FlashLikeRequest, FlashLikeRequest,
FlashShowRequest, FlashShowRequest,
@ -1931,6 +1940,9 @@ type FlashCreateResponse = operations['flash___create']['responses']['200']['con
// @public (undocumented) // @public (undocumented)
type FlashDeleteRequest = operations['flash___delete']['requestBody']['content']['application/json']; type FlashDeleteRequest = operations['flash___delete']['requestBody']['content']['application/json'];
// @public (undocumented)
type FlashFeaturedRequest = operations['flash___featured']['requestBody']['content']['application/json'];
// @public (undocumented) // @public (undocumented)
type FlashFeaturedResponse = operations['flash___featured']['responses']['200']['content']['application/json']; type FlashFeaturedResponse = operations['flash___featured']['responses']['200']['content']['application/json'];
@ -2544,6 +2556,12 @@ type ModerationLog = {
} | { } | {
type: 'resolveAbuseReport'; type: 'resolveAbuseReport';
info: ModerationLogPayloads['resolveAbuseReport']; info: ModerationLogPayloads['resolveAbuseReport'];
} | {
type: 'forwardAbuseReport';
info: ModerationLogPayloads['forwardAbuseReport'];
} | {
type: 'updateAbuseReportNote';
info: ModerationLogPayloads['updateAbuseReportNote'];
} | { } | {
type: 'unsetUserAvatar'; type: 'unsetUserAvatar';
info: ModerationLogPayloads['unsetUserAvatar']; info: ModerationLogPayloads['unsetUserAvatar'];
@ -2583,7 +2601,7 @@ type ModerationLog = {
}); });
// @public (undocumented) // @public (undocumented)
export const moderationLogTypes: readonly ["updateServerSettings", "suspend", "unsuspend", "updateUserNote", "addCustomEmoji", "updateCustomEmoji", "deleteCustomEmoji", "assignRole", "unassignRole", "createRole", "updateRole", "deleteRole", "clearQueue", "promoteQueue", "deleteDriveFile", "deleteNote", "createGlobalAnnouncement", "createUserAnnouncement", "updateGlobalAnnouncement", "updateUserAnnouncement", "deleteGlobalAnnouncement", "deleteUserAnnouncement", "resetPassword", "suspendRemoteInstance", "unsuspendRemoteInstance", "updateRemoteInstanceNote", "markSensitiveDriveFile", "unmarkSensitiveDriveFile", "resolveAbuseReport", "createInvitation", "createAd", "updateAd", "deleteAd", "createAvatarDecoration", "updateAvatarDecoration", "deleteAvatarDecoration", "unsetUserAvatar", "unsetUserBanner", "createSystemWebhook", "updateSystemWebhook", "deleteSystemWebhook", "createAbuseReportNotificationRecipient", "updateAbuseReportNotificationRecipient", "deleteAbuseReportNotificationRecipient", "deleteAccount", "deletePage", "deleteFlash", "deleteGalleryPost"]; export const moderationLogTypes: readonly ["updateServerSettings", "suspend", "unsuspend", "updateUserNote", "addCustomEmoji", "updateCustomEmoji", "deleteCustomEmoji", "assignRole", "unassignRole", "createRole", "updateRole", "deleteRole", "clearQueue", "promoteQueue", "deleteDriveFile", "deleteNote", "createGlobalAnnouncement", "createUserAnnouncement", "updateGlobalAnnouncement", "updateUserAnnouncement", "deleteGlobalAnnouncement", "deleteUserAnnouncement", "resetPassword", "suspendRemoteInstance", "unsuspendRemoteInstance", "updateRemoteInstanceNote", "markSensitiveDriveFile", "unmarkSensitiveDriveFile", "resolveAbuseReport", "forwardAbuseReport", "updateAbuseReportNote", "createInvitation", "createAd", "updateAd", "deleteAd", "createAvatarDecoration", "updateAvatarDecoration", "deleteAvatarDecoration", "unsetUserAvatar", "unsetUserBanner", "createSystemWebhook", "updateSystemWebhook", "deleteSystemWebhook", "createAbuseReportNotificationRecipient", "updateAbuseReportNotificationRecipient", "deleteAbuseReportNotificationRecipient", "deleteAccount", "deletePage", "deleteFlash", "deleteGalleryPost"];
// @public (undocumented) // @public (undocumented)
type MuteCreateRequest = operations['mute___create']['requestBody']['content']['application/json']; type MuteCreateRequest = operations['mute___create']['requestBody']['content']['application/json'];
@ -3046,7 +3064,7 @@ type ServerStatsLog = ServerStats[];
type Signin = components['schemas']['Signin']; type Signin = components['schemas']['Signin'];
// @public (undocumented) // @public (undocumented)
type SigninRequest = { type SigninFlowRequest = {
username: string; username: string;
password?: string; password?: string;
token?: string; token?: string;
@ -3058,9 +3076,17 @@ type SigninRequest = {
}; };
// @public (undocumented) // @public (undocumented)
type SigninResponse = { type SigninFlowResponse = {
finished: true;
id: User['id']; id: User['id'];
i: string; i: string;
} | {
finished: false;
next: 'captcha' | 'password' | 'totp';
} | {
finished: false;
next: 'passkey';
authRequest: PublicKeyCredentialRequestOptionsJSON;
}; };
// @public (undocumented) // @public (undocumented)
@ -3077,7 +3103,7 @@ type SigninWithPasskeyRequest = {
// @public (undocumented) // @public (undocumented)
type SigninWithPasskeyResponse = { type SigninWithPasskeyResponse = {
signinResponse: SigninResponse; signinResponse: SigninFlowResponse;
}; };
// @public (undocumented) // @public (undocumented)

View File

@ -1,7 +1,7 @@
{ {
"type": "module", "type": "module",
"name": "misskey-js", "name": "misskey-js",
"version": "2024.10.0-beta.4", "version": "2024.10.0-beta.5",
"description": "Misskey SDK for JavaScript", "description": "Misskey SDK for JavaScript",
"license": "MIT", "license": "MIT",
"main": "./built/index.js", "main": "./built/index.js",

View File

@ -3,8 +3,8 @@ import { UserDetailed } from './autogen/models.js';
import { AdminRolesCreateRequest, AdminRolesCreateResponse, UsersShowRequest } from './autogen/entities.js'; import { AdminRolesCreateRequest, AdminRolesCreateResponse, UsersShowRequest } from './autogen/entities.js';
import { import {
PartialRolePolicyOverride, PartialRolePolicyOverride,
SigninRequest, SigninFlowRequest,
SigninResponse, SigninFlowResponse,
SigninWithPasskeyInitResponse, SigninWithPasskeyInitResponse,
SigninWithPasskeyRequest, SigninWithPasskeyRequest,
SigninWithPasskeyResponse, SigninWithPasskeyResponse,
@ -81,9 +81,9 @@ export type Endpoints = Overwrite<
res: SignupPendingResponse; res: SignupPendingResponse;
}, },
// api.jsonには載せないものなのでここで定義 // api.jsonには載せないものなのでここで定義
'signin': { 'signin-flow': {
req: SigninRequest; req: SigninFlowRequest;
res: SigninResponse; res: SigninFlowResponse;
}, },
'signin-with-passkey': { 'signin-with-passkey': {
req: SigninWithPasskeyRequest; req: SigninWithPasskeyRequest;

View File

@ -691,6 +691,28 @@ declare module '../api.js' {
credential?: string | null, credential?: string | null,
): Promise<SwitchCaseResponseType<E, P>>; ): Promise<SwitchCaseResponseType<E, P>>;
/**
* No description provided.
*
* **Credential required**: *Yes* / **Permission**: *write:admin:resolve-abuse-user-report*
*/
request<E extends 'admin/forward-abuse-user-report', P extends Endpoints[E]['req']>(
endpoint: E,
params: P,
credential?: string | null,
): Promise<SwitchCaseResponseType<E, P>>;
/**
* No description provided.
*
* **Credential required**: *Yes* / **Permission**: *write:admin:resolve-abuse-user-report*
*/
request<E extends 'admin/update-abuse-user-report', P extends Endpoints[E]['req']>(
endpoint: E,
params: P,
credential?: string | null,
): Promise<SwitchCaseResponseType<E, P>>;
/** /**
* No description provided. * No description provided.
* *

View File

@ -83,6 +83,8 @@ import type {
AdminResetPasswordRequest, AdminResetPasswordRequest,
AdminResetPasswordResponse, AdminResetPasswordResponse,
AdminResolveAbuseUserReportRequest, AdminResolveAbuseUserReportRequest,
AdminForwardAbuseUserReportRequest,
AdminUpdateAbuseUserReportRequest,
AdminSendEmailRequest, AdminSendEmailRequest,
AdminServerInfoResponse, AdminServerInfoResponse,
AdminShowModerationLogsRequest, AdminShowModerationLogsRequest,
@ -467,6 +469,7 @@ import type {
FlashCreateRequest, FlashCreateRequest,
FlashCreateResponse, FlashCreateResponse,
FlashDeleteRequest, FlashDeleteRequest,
FlashFeaturedRequest,
FlashFeaturedResponse, FlashFeaturedResponse,
FlashLikeRequest, FlashLikeRequest,
FlashShowRequest, FlashShowRequest,
@ -640,6 +643,8 @@ export type Endpoints = {
'admin/relays/remove': { req: AdminRelaysRemoveRequest; res: EmptyResponse }; 'admin/relays/remove': { req: AdminRelaysRemoveRequest; res: EmptyResponse };
'admin/reset-password': { req: AdminResetPasswordRequest; res: AdminResetPasswordResponse }; 'admin/reset-password': { req: AdminResetPasswordRequest; res: AdminResetPasswordResponse };
'admin/resolve-abuse-user-report': { req: AdminResolveAbuseUserReportRequest; res: EmptyResponse }; 'admin/resolve-abuse-user-report': { req: AdminResolveAbuseUserReportRequest; res: EmptyResponse };
'admin/forward-abuse-user-report': { req: AdminForwardAbuseUserReportRequest; res: EmptyResponse };
'admin/update-abuse-user-report': { req: AdminUpdateAbuseUserReportRequest; res: EmptyResponse };
'admin/send-email': { req: AdminSendEmailRequest; res: EmptyResponse }; 'admin/send-email': { req: AdminSendEmailRequest; res: EmptyResponse };
'admin/server-info': { req: EmptyRequest; res: AdminServerInfoResponse }; 'admin/server-info': { req: EmptyRequest; res: AdminServerInfoResponse };
'admin/show-moderation-logs': { req: AdminShowModerationLogsRequest; res: AdminShowModerationLogsResponse }; 'admin/show-moderation-logs': { req: AdminShowModerationLogsRequest; res: AdminShowModerationLogsResponse };
@ -892,7 +897,7 @@ export type Endpoints = {
'pages/update': { req: PagesUpdateRequest; res: EmptyResponse }; 'pages/update': { req: PagesUpdateRequest; res: EmptyResponse };
'flash/create': { req: FlashCreateRequest; res: FlashCreateResponse }; 'flash/create': { req: FlashCreateRequest; res: FlashCreateResponse };
'flash/delete': { req: FlashDeleteRequest; res: EmptyResponse }; '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/like': { req: FlashLikeRequest; res: EmptyResponse };
'flash/show': { req: FlashShowRequest; res: FlashShowResponse }; 'flash/show': { req: FlashShowRequest; res: FlashShowResponse };
'flash/unlike': { req: FlashUnlikeRequest; res: EmptyResponse }; 'flash/unlike': { req: FlashUnlikeRequest; res: EmptyResponse };

View File

@ -86,6 +86,8 @@ export type AdminRelaysRemoveRequest = operations['admin___relays___remove']['re
export type AdminResetPasswordRequest = operations['admin___reset-password']['requestBody']['content']['application/json']; export type AdminResetPasswordRequest = operations['admin___reset-password']['requestBody']['content']['application/json'];
export type AdminResetPasswordResponse = operations['admin___reset-password']['responses']['200']['content']['application/json']; export type AdminResetPasswordResponse = operations['admin___reset-password']['responses']['200']['content']['application/json'];
export type AdminResolveAbuseUserReportRequest = operations['admin___resolve-abuse-user-report']['requestBody']['content']['application/json']; export type AdminResolveAbuseUserReportRequest = operations['admin___resolve-abuse-user-report']['requestBody']['content']['application/json'];
export type AdminForwardAbuseUserReportRequest = operations['admin___forward-abuse-user-report']['requestBody']['content']['application/json'];
export type AdminUpdateAbuseUserReportRequest = operations['admin___update-abuse-user-report']['requestBody']['content']['application/json'];
export type AdminSendEmailRequest = operations['admin___send-email']['requestBody']['content']['application/json']; export type AdminSendEmailRequest = operations['admin___send-email']['requestBody']['content']['application/json'];
export type AdminServerInfoResponse = operations['admin___server-info']['responses']['200']['content']['application/json']; export type AdminServerInfoResponse = operations['admin___server-info']['responses']['200']['content']['application/json'];
export type AdminShowModerationLogsRequest = operations['admin___show-moderation-logs']['requestBody']['content']['application/json']; export type AdminShowModerationLogsRequest = operations['admin___show-moderation-logs']['requestBody']['content']['application/json'];
@ -470,6 +472,7 @@ export type PagesUpdateRequest = operations['pages___update']['requestBody']['co
export type FlashCreateRequest = operations['flash___create']['requestBody']['content']['application/json']; export type FlashCreateRequest = operations['flash___create']['requestBody']['content']['application/json'];
export type FlashCreateResponse = operations['flash___create']['responses']['200']['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 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 FlashFeaturedResponse = operations['flash___featured']['responses']['200']['content']['application/json'];
export type FlashLikeRequest = operations['flash___like']['requestBody']['content']['application/json']; export type FlashLikeRequest = operations['flash___like']['requestBody']['content']['application/json'];
export type FlashShowRequest = operations['flash___show']['requestBody']['content']['application/json']; export type FlashShowRequest = operations['flash___show']['requestBody']['content']['application/json'];

View File

@ -576,6 +576,24 @@ export type paths = {
*/ */
post: operations['admin___resolve-abuse-user-report']; post: operations['admin___resolve-abuse-user-report'];
}; };
'/admin/forward-abuse-user-report': {
/**
* admin/forward-abuse-user-report
* @description No description provided.
*
* **Credential required**: *Yes* / **Permission**: *write:admin:resolve-abuse-user-report*
*/
post: operations['admin___forward-abuse-user-report'];
};
'/admin/update-abuse-user-report': {
/**
* admin/update-abuse-user-report
* @description No description provided.
*
* **Credential required**: *Yes* / **Permission**: *write:admin:resolve-abuse-user-report*
*/
post: operations['admin___update-abuse-user-report'];
};
'/admin/send-email': { '/admin/send-email': {
/** /**
* admin/send-email * admin/send-email
@ -5264,8 +5282,6 @@ export type operations = {
* @enum {string} * @enum {string}
*/ */
targetUserOrigin?: 'combined' | 'local' | 'remote'; targetUserOrigin?: 'combined' | 'local' | 'remote';
/** @default false */
forwarded?: boolean;
}; };
}; };
}; };
@ -5292,7 +5308,11 @@ export type operations = {
assigneeId: string | null; assigneeId: string | null;
reporter: components['schemas']['UserDetailedNotMe']; reporter: components['schemas']['UserDetailedNotMe'];
targetUser: components['schemas']['UserDetailedNotMe']; targetUser: components['schemas']['UserDetailedNotMe'];
assignee?: components['schemas']['UserDetailedNotMe'] | null; assignee: components['schemas']['UserDetailedNotMe'] | null;
forwarded: boolean;
/** @enum {string|null} */
resolvedAs: 'accept' | 'reject' | null;
moderationNote: string;
})[]; })[];
}; };
}; };
@ -8705,8 +8725,113 @@ export type operations = {
'application/json': { 'application/json': {
/** Format: misskey:id */ /** Format: misskey:id */
reportId: string; reportId: string;
/** @default false */ /** @enum {string|null} */
forward?: boolean; resolvedAs?: 'accept' | 'reject' | null;
};
};
};
responses: {
/** @description OK (without any results) */
204: {
content: never;
};
/** @description Client error */
400: {
content: {
'application/json': components['schemas']['Error'];
};
};
/** @description Authentication error */
401: {
content: {
'application/json': components['schemas']['Error'];
};
};
/** @description Forbidden error */
403: {
content: {
'application/json': components['schemas']['Error'];
};
};
/** @description I'm Ai */
418: {
content: {
'application/json': components['schemas']['Error'];
};
};
/** @description Internal server error */
500: {
content: {
'application/json': components['schemas']['Error'];
};
};
};
};
/**
* admin/forward-abuse-user-report
* @description No description provided.
*
* **Credential required**: *Yes* / **Permission**: *write:admin:resolve-abuse-user-report*
*/
'admin___forward-abuse-user-report': {
requestBody: {
content: {
'application/json': {
/** Format: misskey:id */
reportId: string;
};
};
};
responses: {
/** @description OK (without any results) */
204: {
content: never;
};
/** @description Client error */
400: {
content: {
'application/json': components['schemas']['Error'];
};
};
/** @description Authentication error */
401: {
content: {
'application/json': components['schemas']['Error'];
};
};
/** @description Forbidden error */
403: {
content: {
'application/json': components['schemas']['Error'];
};
};
/** @description I'm Ai */
418: {
content: {
'application/json': components['schemas']['Error'];
};
};
/** @description Internal server error */
500: {
content: {
'application/json': components['schemas']['Error'];
};
};
};
};
/**
* admin/update-abuse-user-report
* @description No description provided.
*
* **Credential required**: *Yes* / **Permission**: *write:admin:resolve-abuse-user-report*
*/
'admin___update-abuse-user-report': {
requestBody: {
content: {
'application/json': {
/** Format: misskey:id */
reportId: string;
moderationNote?: string;
}; };
}; };
}; };
@ -23871,6 +23996,16 @@ export type operations = {
* **Credential required**: *No* * **Credential required**: *No*
*/ */
flash___featured: { flash___featured: {
requestBody: {
content: {
'application/json': {
/** @default 0 */
offset?: number;
/** @default 10 */
limit?: number;
};
};
};
responses: { responses: {
/** @description OK (with results) */ /** @description OK (with results) */
200: { 200: {

View File

@ -142,6 +142,8 @@ export const moderationLogTypes = [
'markSensitiveDriveFile', 'markSensitiveDriveFile',
'unmarkSensitiveDriveFile', 'unmarkSensitiveDriveFile',
'resolveAbuseReport', 'resolveAbuseReport',
'forwardAbuseReport',
'updateAbuseReportNote',
'createInvitation', 'createInvitation',
'createAd', 'createAd',
'updateAd', 'updateAd',
@ -330,7 +332,18 @@ export type ModerationLogPayloads = {
resolveAbuseReport: { resolveAbuseReport: {
reportId: string; reportId: string;
report: ReceivedAbuseReport; report: ReceivedAbuseReport;
forwarded: boolean; forwarded?: boolean;
resolvedAs?: string | null;
};
forwardAbuseReport: {
reportId: string;
report: ReceivedAbuseReport;
};
updateAbuseReportNote: {
reportId: string;
report: ReceivedAbuseReport;
before: string;
after: string;
}; };
createInvitation: { createInvitation: {
invitations: InviteCode[]; invitations: InviteCode[];

View File

@ -153,6 +153,12 @@ export type ModerationLog = {
} | { } | {
type: 'resolveAbuseReport'; type: 'resolveAbuseReport';
info: ModerationLogPayloads['resolveAbuseReport']; info: ModerationLogPayloads['resolveAbuseReport'];
} | {
type: 'forwardAbuseReport';
info: ModerationLogPayloads['forwardAbuseReport'];
} | {
type: 'updateAbuseReportNote';
info: ModerationLogPayloads['updateAbuseReportNote'];
} | { } | {
type: 'unsetUserAvatar'; type: 'unsetUserAvatar';
info: ModerationLogPayloads['unsetUserAvatar']; info: ModerationLogPayloads['unsetUserAvatar'];
@ -267,7 +273,7 @@ export type SignupPendingResponse = {
i: string, i: string,
}; };
export type SigninRequest = { export type SigninFlowRequest = {
username: string; username: string;
password?: string; password?: string;
token?: string; token?: string;
@ -278,6 +284,19 @@ export type SigninRequest = {
'm-captcha-response'?: string | null; 'm-captcha-response'?: string | null;
}; };
export type SigninFlowResponse = {
finished: true;
id: User['id'];
i: string;
} | {
finished: false;
next: 'captcha' | 'password' | 'totp';
} | {
finished: false;
next: 'passkey';
authRequest: PublicKeyCredentialRequestOptionsJSON;
};
export type SigninWithPasskeyRequest = { export type SigninWithPasskeyRequest = {
credential?: AuthenticationResponseJSON; credential?: AuthenticationResponseJSON;
context?: string; context?: string;
@ -289,12 +308,7 @@ export type SigninWithPasskeyInitResponse = {
}; };
export type SigninWithPasskeyResponse = { export type SigninWithPasskeyResponse = {
signinResponse: SigninResponse; signinResponse: SigninFlowResponse;
};
export type SigninResponse = {
id: User['id'],
i: string,
}; };
type Values<T extends Record<PropertyKey, unknown>> = T[keyof T]; type Values<T extends Record<PropertyKey, unknown>> = T[keyof T];