Merge remote-tracking branch 'misskey-original/develop' into develop
This commit is contained in:
commit
e3d8346e21
14
CHANGELOG.md
14
CHANGELOG.md
|
@ -5,13 +5,25 @@
|
||||||
-
|
-
|
||||||
|
|
||||||
### Client
|
### Client
|
||||||
-
|
- Fix: ページ一覧ページの表示がモバイル環境において崩れているのを修正
|
||||||
|
|
||||||
### Server
|
### Server
|
||||||
-
|
-
|
||||||
|
|
||||||
-->
|
-->
|
||||||
|
|
||||||
|
## 2023.x.x (unreleased)
|
||||||
|
|
||||||
|
### General
|
||||||
|
- Feat: メールアドレスの認証にverifymail.ioを使えるように (cherry-pick from https://github.com/TeamNijimiss/misskey/commit/971ba07a44550f68d2ba31c62066db2d43a0caed)
|
||||||
|
- Feat: モデレーターがユーザーのアイコンもしくはバナー画像を未設定状態にできる機能を追加 (cherry-pick from https://github.com/TeamNijimiss/misskey/commit/e0eb5a752f6e5616d6312bb7c9790302f9dbff83)
|
||||||
|
|
||||||
|
### Client
|
||||||
|
- fix: 「設定のバックアップ」で一部の項目がバックアップに含まれていなかった問題を修正
|
||||||
|
|
||||||
|
### Server
|
||||||
|
-
|
||||||
|
|
||||||
## 2023.11.1
|
## 2023.11.1
|
||||||
|
|
||||||
### General
|
### General
|
||||||
|
|
|
@ -568,6 +568,10 @@ output: "Output"
|
||||||
script: "Script"
|
script: "Script"
|
||||||
disablePagesScript: "Disable AiScript on Pages"
|
disablePagesScript: "Disable AiScript on Pages"
|
||||||
updateRemoteUser: "Update remote user information"
|
updateRemoteUser: "Update remote user information"
|
||||||
|
unsetUserAvatar: "Delete user icon"
|
||||||
|
unsetUserAvatarConfirm: "Are you sure that you want to delete this user's icon?"
|
||||||
|
unsetUserBanner: "Delete user banner"
|
||||||
|
unsetUserBannerConfirm: "Are you sure that you want to delete this user's banner?"
|
||||||
deleteAllFiles: "Delete all files"
|
deleteAllFiles: "Delete all files"
|
||||||
deleteAllFilesConfirm: "Are you sure that you want to delete all files?"
|
deleteAllFilesConfirm: "Are you sure that you want to delete all files?"
|
||||||
removeAllFollowing: "Unfollow all followed users"
|
removeAllFollowing: "Unfollow all followed users"
|
||||||
|
|
6
locales/index.d.ts
vendored
6
locales/index.d.ts
vendored
|
@ -587,6 +587,10 @@ export interface Locale {
|
||||||
"script": string;
|
"script": string;
|
||||||
"disablePagesScript": string;
|
"disablePagesScript": string;
|
||||||
"updateRemoteUser": string;
|
"updateRemoteUser": string;
|
||||||
|
"unsetUserAvatar": string;
|
||||||
|
"unsetUserAvatarConfirm": string;
|
||||||
|
"unsetUserBanner": string;
|
||||||
|
"unsetUserBannerConfirm": string;
|
||||||
"deleteAllFiles": string;
|
"deleteAllFiles": string;
|
||||||
"deleteAllFilesConfirm": string;
|
"deleteAllFilesConfirm": string;
|
||||||
"removeAllFollowing": string;
|
"removeAllFollowing": string;
|
||||||
|
@ -2469,6 +2473,8 @@ export interface Locale {
|
||||||
"createAvatarDecoration": string;
|
"createAvatarDecoration": string;
|
||||||
"updateAvatarDecoration": string;
|
"updateAvatarDecoration": string;
|
||||||
"deleteAvatarDecoration": string;
|
"deleteAvatarDecoration": string;
|
||||||
|
"unsetUserAvatar": string;
|
||||||
|
"unsetUserBanner": string;
|
||||||
};
|
};
|
||||||
"_fileViewer": {
|
"_fileViewer": {
|
||||||
"title": string;
|
"title": string;
|
||||||
|
|
|
@ -584,6 +584,10 @@ output: "出力"
|
||||||
script: "スクリプト"
|
script: "スクリプト"
|
||||||
disablePagesScript: "Pagesのスクリプトを無効にする"
|
disablePagesScript: "Pagesのスクリプトを無効にする"
|
||||||
updateRemoteUser: "リモートユーザー情報の更新"
|
updateRemoteUser: "リモートユーザー情報の更新"
|
||||||
|
unsetUserAvatar: "アイコンを解除"
|
||||||
|
unsetUserAvatarConfirm: "アイコンを解除しますか?"
|
||||||
|
unsetUserBanner: "バナーを解除"
|
||||||
|
unsetUserBannerConfirm: "バナーを解除しますか?"
|
||||||
deleteAllFiles: "すべてのファイルを削除"
|
deleteAllFiles: "すべてのファイルを削除"
|
||||||
deleteAllFilesConfirm: "すべてのファイルを削除しますか?"
|
deleteAllFilesConfirm: "すべてのファイルを削除しますか?"
|
||||||
removeAllFollowing: "フォローを全解除"
|
removeAllFollowing: "フォローを全解除"
|
||||||
|
@ -2370,6 +2374,8 @@ _moderationLogTypes:
|
||||||
createAvatarDecoration: "アイコンデコレーションを作成"
|
createAvatarDecoration: "アイコンデコレーションを作成"
|
||||||
updateAvatarDecoration: "アイコンデコレーションを更新"
|
updateAvatarDecoration: "アイコンデコレーションを更新"
|
||||||
deleteAvatarDecoration: "アイコンデコレーションを削除"
|
deleteAvatarDecoration: "アイコンデコレーションを削除"
|
||||||
|
unsetUserAvatar: "ユーザーのアイコンを解除"
|
||||||
|
unsetUserBanner: "ユーザーのバナーを解除"
|
||||||
|
|
||||||
_fileViewer:
|
_fileViewer:
|
||||||
title: "ファイルの詳細"
|
title: "ファイルの詳細"
|
||||||
|
|
|
@ -0,0 +1,18 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: syuilo and other misskey contributors
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
export class SupportVerifyMailApi1700303245007 {
|
||||||
|
name = 'SupportVerifyMailApi1700303245007'
|
||||||
|
|
||||||
|
async up(queryRunner) {
|
||||||
|
await queryRunner.query(`ALTER TABLE "meta" ADD "verifymailAuthKey" character varying(1024)`);
|
||||||
|
await queryRunner.query(`ALTER TABLE "meta" ADD "enableVerifymailApi" boolean NOT NULL DEFAULT false`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async down(queryRunner) {
|
||||||
|
await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "enableVerifymailApi"`);
|
||||||
|
await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "verifymailAuthKey"`);
|
||||||
|
}
|
||||||
|
}
|
|
@ -7,8 +7,8 @@
|
||||||
"node": ">=18.16.0"
|
"node": ">=18.16.0"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "node ./built/index.js",
|
"start": "node ./built/boot/entry.js",
|
||||||
"start:test": "NODE_ENV=test node ./built/index.js",
|
"start:test": "NODE_ENV=test node ./built/boot/entry.js",
|
||||||
"migrate": "pnpm typeorm migration:run -d ormconfig.js",
|
"migrate": "pnpm typeorm migration:run -d ormconfig.js",
|
||||||
"revert": "pnpm typeorm migration:revert -d ormconfig.js",
|
"revert": "pnpm typeorm migration:revert -d ormconfig.js",
|
||||||
"check:connect": "node ./check_connect.js",
|
"check:connect": "node ./check_connect.js",
|
||||||
|
|
|
@ -3,9 +3,11 @@
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { URLSearchParams } from 'node:url';
|
||||||
import * as nodemailer from 'nodemailer';
|
import * as nodemailer from 'nodemailer';
|
||||||
import { Inject, Injectable } from '@nestjs/common';
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
import { validate as validateEmail } from 'deep-email-validator';
|
import { validate as validateEmail } from 'deep-email-validator';
|
||||||
|
import { SubOutputFormat } from 'deep-email-validator/dist/output/output.js';
|
||||||
import { MetaService } from '@/core/MetaService.js';
|
import { MetaService } from '@/core/MetaService.js';
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
import type { Config } from '@/config.js';
|
import type { Config } from '@/config.js';
|
||||||
|
@ -13,6 +15,7 @@ import type Logger from '@/logger.js';
|
||||||
import type { UserProfilesRepository } from '@/models/_.js';
|
import type { UserProfilesRepository } from '@/models/_.js';
|
||||||
import { LoggerService } from '@/core/LoggerService.js';
|
import { LoggerService } from '@/core/LoggerService.js';
|
||||||
import { bindThis } from '@/decorators.js';
|
import { bindThis } from '@/decorators.js';
|
||||||
|
import { HttpRequestService } from '@/core/HttpRequestService.js';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class EmailService {
|
export class EmailService {
|
||||||
|
@ -27,6 +30,7 @@ export class EmailService {
|
||||||
|
|
||||||
private metaService: MetaService,
|
private metaService: MetaService,
|
||||||
private loggerService: LoggerService,
|
private loggerService: LoggerService,
|
||||||
|
private httpRequestService: HttpRequestService,
|
||||||
) {
|
) {
|
||||||
this.logger = this.loggerService.getLogger('email');
|
this.logger = this.loggerService.getLogger('email');
|
||||||
}
|
}
|
||||||
|
@ -160,14 +164,25 @@ export class EmailService {
|
||||||
email: emailAddress,
|
email: emailAddress,
|
||||||
});
|
});
|
||||||
|
|
||||||
const validated = meta.enableActiveEmailValidation ? await validateEmail({
|
const verifymailApi = meta.enableVerifymailApi && meta.verifymailAuthKey != null;
|
||||||
email: emailAddress,
|
let validated;
|
||||||
validateRegex: true,
|
|
||||||
validateMx: true,
|
if (meta.enableActiveEmailValidation && meta.verifymailAuthKey) {
|
||||||
validateTypo: false, // TLDを見ているみたいだけどclubとか弾かれるので
|
if (verifymailApi) {
|
||||||
validateDisposable: true, // 捨てアドかどうかチェック
|
validated = await this.verifyMail(emailAddress, meta.verifymailAuthKey);
|
||||||
validateSMTP: false, // 日本だと25ポートが殆どのプロバイダーで塞がれていてタイムアウトになるので
|
} else {
|
||||||
}) : { valid: true, reason: null };
|
validated = meta.enableActiveEmailValidation ? await validateEmail({
|
||||||
|
email: emailAddress,
|
||||||
|
validateRegex: true,
|
||||||
|
validateMx: true,
|
||||||
|
validateTypo: false, // TLDを見ているみたいだけどclubとか弾かれるので
|
||||||
|
validateDisposable: true, // 捨てアドかどうかチェック
|
||||||
|
validateSMTP: false, // 日本だと25ポートが殆どのプロバイダーで塞がれていてタイムアウトになるので
|
||||||
|
}) : { valid: true, reason: null };
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
validated = { valid: true, reason: null };
|
||||||
|
}
|
||||||
|
|
||||||
const available = exist === 0 && validated.valid;
|
const available = exist === 0 && validated.valid;
|
||||||
|
|
||||||
|
@ -182,4 +197,65 @@ export class EmailService {
|
||||||
null,
|
null,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async verifyMail(emailAddress: string, verifymailAuthKey: string): Promise<{
|
||||||
|
valid: boolean;
|
||||||
|
reason: 'used' | 'format' | 'disposable' | 'mx' | 'smtp' | null;
|
||||||
|
}> {
|
||||||
|
const endpoint = 'https://verifymail.io/api/' + emailAddress + '?key=' + verifymailAuthKey;
|
||||||
|
const res = await this.httpRequestService.send(endpoint, {
|
||||||
|
method: 'GET',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/x-www-form-urlencoded',
|
||||||
|
Accept: 'application/json, */*',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const json = (await res.json()) as {
|
||||||
|
block: boolean;
|
||||||
|
catch_all: boolean;
|
||||||
|
deliverable_email: boolean;
|
||||||
|
disposable: boolean;
|
||||||
|
domain: string;
|
||||||
|
email_address: string;
|
||||||
|
email_provider: string;
|
||||||
|
mx: boolean;
|
||||||
|
mx_fallback: boolean;
|
||||||
|
mx_host: string[];
|
||||||
|
mx_ip: string[];
|
||||||
|
mx_priority: { [key: string]: number };
|
||||||
|
privacy: boolean;
|
||||||
|
related_domains: string[];
|
||||||
|
};
|
||||||
|
|
||||||
|
if (json.email_address === undefined) {
|
||||||
|
return {
|
||||||
|
valid: false,
|
||||||
|
reason: 'format',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (json.deliverable_email !== undefined && !json.deliverable_email) {
|
||||||
|
return {
|
||||||
|
valid: false,
|
||||||
|
reason: 'smtp',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (json.disposable) {
|
||||||
|
return {
|
||||||
|
valid: false,
|
||||||
|
reason: 'disposable',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (json.mx !== undefined && !json.mx) {
|
||||||
|
return {
|
||||||
|
valid: false,
|
||||||
|
reason: 'mx',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
valid: true,
|
||||||
|
reason: null,
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -461,6 +461,17 @@ export class MiMeta {
|
||||||
})
|
})
|
||||||
public enableActiveEmailValidation: boolean;
|
public enableActiveEmailValidation: boolean;
|
||||||
|
|
||||||
|
@Column('boolean', {
|
||||||
|
default: false,
|
||||||
|
})
|
||||||
|
public enableVerifymailApi: boolean;
|
||||||
|
|
||||||
|
@Column('varchar', {
|
||||||
|
length: 1024,
|
||||||
|
nullable: true,
|
||||||
|
})
|
||||||
|
public verifymailAuthKey: string | null;
|
||||||
|
|
||||||
@Column('boolean', {
|
@Column('boolean', {
|
||||||
default: true,
|
default: true,
|
||||||
})
|
})
|
||||||
|
|
|
@ -24,6 +24,8 @@ import * as ep___admin_avatarDecorations_delete from './endpoints/admin/avatar-d
|
||||||
import * as ep___admin_avatarDecorations_list from './endpoints/admin/avatar-decorations/list.js';
|
import * as ep___admin_avatarDecorations_list from './endpoints/admin/avatar-decorations/list.js';
|
||||||
import * as ep___admin_avatarDecorations_update from './endpoints/admin/avatar-decorations/update.js';
|
import * as ep___admin_avatarDecorations_update from './endpoints/admin/avatar-decorations/update.js';
|
||||||
import * as ep___admin_deleteAllFilesOfAUser from './endpoints/admin/delete-all-files-of-a-user.js';
|
import * as ep___admin_deleteAllFilesOfAUser from './endpoints/admin/delete-all-files-of-a-user.js';
|
||||||
|
import * as ep___admin_unsetUserAvatar from './endpoints/admin/unset-user-avatar.js';
|
||||||
|
import * as ep___admin_unsetUserBanner from './endpoints/admin/unset-user-banner.js';
|
||||||
import * as ep___admin_drive_cleanRemoteFiles from './endpoints/admin/drive/clean-remote-files.js';
|
import * as ep___admin_drive_cleanRemoteFiles from './endpoints/admin/drive/clean-remote-files.js';
|
||||||
import * as ep___admin_drive_cleanup from './endpoints/admin/drive/cleanup.js';
|
import * as ep___admin_drive_cleanup from './endpoints/admin/drive/cleanup.js';
|
||||||
import * as ep___admin_drive_files from './endpoints/admin/drive/files.js';
|
import * as ep___admin_drive_files from './endpoints/admin/drive/files.js';
|
||||||
|
@ -388,6 +390,8 @@ const $admin_avatarDecorations_delete: Provider = { provide: 'ep:admin/avatar-de
|
||||||
const $admin_avatarDecorations_list: Provider = { provide: 'ep:admin/avatar-decorations/list', useClass: ep___admin_avatarDecorations_list.default };
|
const $admin_avatarDecorations_list: Provider = { provide: 'ep:admin/avatar-decorations/list', useClass: ep___admin_avatarDecorations_list.default };
|
||||||
const $admin_avatarDecorations_update: Provider = { provide: 'ep:admin/avatar-decorations/update', useClass: ep___admin_avatarDecorations_update.default };
|
const $admin_avatarDecorations_update: Provider = { provide: 'ep:admin/avatar-decorations/update', useClass: ep___admin_avatarDecorations_update.default };
|
||||||
const $admin_deleteAllFilesOfAUser: Provider = { provide: 'ep:admin/delete-all-files-of-a-user', useClass: ep___admin_deleteAllFilesOfAUser.default };
|
const $admin_deleteAllFilesOfAUser: Provider = { provide: 'ep:admin/delete-all-files-of-a-user', useClass: ep___admin_deleteAllFilesOfAUser.default };
|
||||||
|
const $admin_unsetUserAvatar: Provider = { provide: 'ep:admin/unset-user-avatar', useClass: ep___admin_unsetUserAvatar.default };
|
||||||
|
const $admin_unsetUserBanner: Provider = { provide: 'ep:admin/unset-user-banner', useClass: ep___admin_unsetUserBanner.default };
|
||||||
const $admin_drive_cleanRemoteFiles: Provider = { provide: 'ep:admin/drive/clean-remote-files', useClass: ep___admin_drive_cleanRemoteFiles.default };
|
const $admin_drive_cleanRemoteFiles: Provider = { provide: 'ep:admin/drive/clean-remote-files', useClass: ep___admin_drive_cleanRemoteFiles.default };
|
||||||
const $admin_drive_cleanup: Provider = { provide: 'ep:admin/drive/cleanup', useClass: ep___admin_drive_cleanup.default };
|
const $admin_drive_cleanup: Provider = { provide: 'ep:admin/drive/cleanup', useClass: ep___admin_drive_cleanup.default };
|
||||||
const $admin_drive_files: Provider = { provide: 'ep:admin/drive/files', useClass: ep___admin_drive_files.default };
|
const $admin_drive_files: Provider = { provide: 'ep:admin/drive/files', useClass: ep___admin_drive_files.default };
|
||||||
|
@ -756,6 +760,8 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention
|
||||||
$admin_avatarDecorations_list,
|
$admin_avatarDecorations_list,
|
||||||
$admin_avatarDecorations_update,
|
$admin_avatarDecorations_update,
|
||||||
$admin_deleteAllFilesOfAUser,
|
$admin_deleteAllFilesOfAUser,
|
||||||
|
$admin_unsetUserAvatar,
|
||||||
|
$admin_unsetUserBanner,
|
||||||
$admin_drive_cleanRemoteFiles,
|
$admin_drive_cleanRemoteFiles,
|
||||||
$admin_drive_cleanup,
|
$admin_drive_cleanup,
|
||||||
$admin_drive_files,
|
$admin_drive_files,
|
||||||
|
@ -1118,6 +1124,8 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention
|
||||||
$admin_avatarDecorations_list,
|
$admin_avatarDecorations_list,
|
||||||
$admin_avatarDecorations_update,
|
$admin_avatarDecorations_update,
|
||||||
$admin_deleteAllFilesOfAUser,
|
$admin_deleteAllFilesOfAUser,
|
||||||
|
$admin_unsetUserAvatar,
|
||||||
|
$admin_unsetUserBanner,
|
||||||
$admin_drive_cleanRemoteFiles,
|
$admin_drive_cleanRemoteFiles,
|
||||||
$admin_drive_cleanup,
|
$admin_drive_cleanup,
|
||||||
$admin_drive_files,
|
$admin_drive_files,
|
||||||
|
|
|
@ -25,6 +25,8 @@ import * as ep___admin_avatarDecorations_delete from './endpoints/admin/avatar-d
|
||||||
import * as ep___admin_avatarDecorations_list from './endpoints/admin/avatar-decorations/list.js';
|
import * as ep___admin_avatarDecorations_list from './endpoints/admin/avatar-decorations/list.js';
|
||||||
import * as ep___admin_avatarDecorations_update from './endpoints/admin/avatar-decorations/update.js';
|
import * as ep___admin_avatarDecorations_update from './endpoints/admin/avatar-decorations/update.js';
|
||||||
import * as ep___admin_deleteAllFilesOfAUser from './endpoints/admin/delete-all-files-of-a-user.js';
|
import * as ep___admin_deleteAllFilesOfAUser from './endpoints/admin/delete-all-files-of-a-user.js';
|
||||||
|
import * as ep___admin_unsetUserAvatar from './endpoints/admin/unset-user-avatar.js';
|
||||||
|
import * as ep___admin_unsetUserBanner from './endpoints/admin/unset-user-banner.js';
|
||||||
import * as ep___admin_drive_cleanRemoteFiles from './endpoints/admin/drive/clean-remote-files.js';
|
import * as ep___admin_drive_cleanRemoteFiles from './endpoints/admin/drive/clean-remote-files.js';
|
||||||
import * as ep___admin_drive_cleanup from './endpoints/admin/drive/cleanup.js';
|
import * as ep___admin_drive_cleanup from './endpoints/admin/drive/cleanup.js';
|
||||||
import * as ep___admin_drive_files from './endpoints/admin/drive/files.js';
|
import * as ep___admin_drive_files from './endpoints/admin/drive/files.js';
|
||||||
|
@ -385,6 +387,8 @@ const eps = [
|
||||||
['admin/avatar-decorations/list', ep___admin_avatarDecorations_list],
|
['admin/avatar-decorations/list', ep___admin_avatarDecorations_list],
|
||||||
['admin/avatar-decorations/update', ep___admin_avatarDecorations_update],
|
['admin/avatar-decorations/update', ep___admin_avatarDecorations_update],
|
||||||
['admin/delete-all-files-of-a-user', ep___admin_deleteAllFilesOfAUser],
|
['admin/delete-all-files-of-a-user', ep___admin_deleteAllFilesOfAUser],
|
||||||
|
['admin/unset-user-avatar', ep___admin_unsetUserAvatar],
|
||||||
|
['admin/unset-user-banner', ep___admin_unsetUserBanner],
|
||||||
['admin/drive/clean-remote-files', ep___admin_drive_cleanRemoteFiles],
|
['admin/drive/clean-remote-files', ep___admin_drive_cleanRemoteFiles],
|
||||||
['admin/drive/cleanup', ep___admin_drive_cleanup],
|
['admin/drive/cleanup', ep___admin_drive_cleanup],
|
||||||
['admin/drive/files', ep___admin_drive_files],
|
['admin/drive/files', ep___admin_drive_files],
|
||||||
|
|
|
@ -0,0 +1,60 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: syuilo and other misskey contributors
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
|
import type { UsersRepository } from '@/models/_.js';
|
||||||
|
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||||
|
import { DI } from '@/di-symbols.js';
|
||||||
|
import { ModerationLogService } from '@/core/ModerationLogService.js';
|
||||||
|
|
||||||
|
export const meta = {
|
||||||
|
tags: ['admin'],
|
||||||
|
|
||||||
|
requireCredential: true,
|
||||||
|
requireModerator: true,
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export const paramDef = {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
userId: { type: 'string', format: 'misskey:id' },
|
||||||
|
},
|
||||||
|
required: ['userId'],
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
// eslint-disable-next-line import/no-default-export
|
||||||
|
@Injectable()
|
||||||
|
export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||||
|
constructor(
|
||||||
|
@Inject(DI.usersRepository)
|
||||||
|
private usersRepository: UsersRepository,
|
||||||
|
|
||||||
|
private moderationLogService: ModerationLogService,
|
||||||
|
) {
|
||||||
|
super(meta, paramDef, async (ps, me) => {
|
||||||
|
const user = await this.usersRepository.findOneBy({ id: ps.userId });
|
||||||
|
|
||||||
|
if (user == null) {
|
||||||
|
throw new Error('user not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (user.avatarId == null) return;
|
||||||
|
|
||||||
|
await this.usersRepository.update(user.id, {
|
||||||
|
avatar: null,
|
||||||
|
avatarId: null,
|
||||||
|
avatarUrl: null,
|
||||||
|
avatarBlurhash: null,
|
||||||
|
});
|
||||||
|
|
||||||
|
this.moderationLogService.log(me, 'unsetUserAvatar', {
|
||||||
|
userId: user.id,
|
||||||
|
userUsername: user.username,
|
||||||
|
userHost: user.host,
|
||||||
|
fileId: user.avatarId,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,60 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: syuilo and other misskey contributors
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
|
import type { UsersRepository } from '@/models/_.js';
|
||||||
|
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||||
|
import { DI } from '@/di-symbols.js';
|
||||||
|
import { ModerationLogService } from '@/core/ModerationLogService.js';
|
||||||
|
|
||||||
|
export const meta = {
|
||||||
|
tags: ['admin'],
|
||||||
|
|
||||||
|
requireCredential: true,
|
||||||
|
requireModerator: true,
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export const paramDef = {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
userId: { type: 'string', format: 'misskey:id' },
|
||||||
|
},
|
||||||
|
required: ['userId'],
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
// eslint-disable-next-line import/no-default-export
|
||||||
|
@Injectable()
|
||||||
|
export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||||
|
constructor(
|
||||||
|
@Inject(DI.usersRepository)
|
||||||
|
private usersRepository: UsersRepository,
|
||||||
|
|
||||||
|
private moderationLogService: ModerationLogService,
|
||||||
|
) {
|
||||||
|
super(meta, paramDef, async (ps, me) => {
|
||||||
|
const user = await this.usersRepository.findOneBy({ id: ps.userId });
|
||||||
|
|
||||||
|
if (user == null) {
|
||||||
|
throw new Error('user not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (user.bannerId == null) return;
|
||||||
|
|
||||||
|
await this.usersRepository.update(user.id, {
|
||||||
|
banner: null,
|
||||||
|
bannerId: null,
|
||||||
|
bannerUrl: null,
|
||||||
|
bannerBlurhash: null,
|
||||||
|
});
|
||||||
|
|
||||||
|
this.moderationLogService.log(me, 'unsetUserBanner', {
|
||||||
|
userId: user.id,
|
||||||
|
userUsername: user.username,
|
||||||
|
userHost: user.host,
|
||||||
|
fileId: user.bannerId,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
|
@ -114,6 +114,8 @@ export const paramDef = {
|
||||||
objectStorageS3ForcePathStyle: { type: 'boolean' },
|
objectStorageS3ForcePathStyle: { type: 'boolean' },
|
||||||
enableIpLogging: { type: 'boolean' },
|
enableIpLogging: { type: 'boolean' },
|
||||||
enableActiveEmailValidation: { type: 'boolean' },
|
enableActiveEmailValidation: { type: 'boolean' },
|
||||||
|
enableVerifymailApi: { type: 'boolean' },
|
||||||
|
verifymailAuthKey: { type: 'string', nullable: true },
|
||||||
enableChartsForRemoteUser: { type: 'boolean' },
|
enableChartsForRemoteUser: { type: 'boolean' },
|
||||||
enableChartsForFederatedInstances: { type: 'boolean' },
|
enableChartsForFederatedInstances: { type: 'boolean' },
|
||||||
enableServerMachineStats: { type: 'boolean' },
|
enableServerMachineStats: { type: 'boolean' },
|
||||||
|
@ -465,6 +467,18 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
set.enableActiveEmailValidation = ps.enableActiveEmailValidation;
|
set.enableActiveEmailValidation = ps.enableActiveEmailValidation;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (ps.enableVerifymailApi !== undefined) {
|
||||||
|
set.enableVerifymailApi = ps.enableVerifymailApi;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ps.verifymailAuthKey !== undefined) {
|
||||||
|
if (ps.verifymailAuthKey === '') {
|
||||||
|
set.verifymailAuthKey = null;
|
||||||
|
} else {
|
||||||
|
set.verifymailAuthKey = ps.verifymailAuthKey;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (ps.enableChartsForRemoteUser !== undefined) {
|
if (ps.enableChartsForRemoteUser !== undefined) {
|
||||||
set.enableChartsForRemoteUser = ps.enableChartsForRemoteUser;
|
set.enableChartsForRemoteUser = ps.enableChartsForRemoteUser;
|
||||||
}
|
}
|
||||||
|
|
|
@ -63,6 +63,8 @@ export const moderationLogTypes = [
|
||||||
'createAvatarDecoration',
|
'createAvatarDecoration',
|
||||||
'updateAvatarDecoration',
|
'updateAvatarDecoration',
|
||||||
'deleteAvatarDecoration',
|
'deleteAvatarDecoration',
|
||||||
|
'unsetUserAvatar',
|
||||||
|
'unsetUserBanner',
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
export type ModerationLogPayloads = {
|
export type ModerationLogPayloads = {
|
||||||
|
@ -237,6 +239,18 @@ export type ModerationLogPayloads = {
|
||||||
avatarDecorationId: string;
|
avatarDecorationId: string;
|
||||||
avatarDecoration: any;
|
avatarDecoration: any;
|
||||||
};
|
};
|
||||||
|
unsetUserAvatar: {
|
||||||
|
userId: string;
|
||||||
|
userUsername: string;
|
||||||
|
userHost: string | null;
|
||||||
|
fileId: string;
|
||||||
|
};
|
||||||
|
unsetUserBanner: {
|
||||||
|
userId: string;
|
||||||
|
userUsername: string;
|
||||||
|
userHost: string | null;
|
||||||
|
fileId: string;
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export type Serialized<T> = {
|
export type Serialized<T> = {
|
||||||
|
|
|
@ -114,7 +114,6 @@ const props = defineProps<{
|
||||||
|
|
||||||
& + article {
|
& + article {
|
||||||
left: 0;
|
left: 0;
|
||||||
width: 100%;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -124,6 +123,7 @@ const props = defineProps<{
|
||||||
|
|
||||||
> .thumbnail {
|
> .thumbnail {
|
||||||
height: 80px;
|
height: 80px;
|
||||||
|
overflow: clip;
|
||||||
}
|
}
|
||||||
|
|
||||||
> article {
|
> article {
|
||||||
|
|
|
@ -122,6 +122,10 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
</template>
|
</template>
|
||||||
</MkFolder>
|
</MkFolder>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<MkButton v-if="iAmModerator" inline danger style="margin-right: 8px;" @click="unsetUserAvatar"><i class="ti ti-user-circle"></i> {{ i18n.ts.unsetUserAvatar }}</MkButton>
|
||||||
|
<MkButton v-if="iAmModerator" inline danger @click="unsetUserBanner"><i class="ti ti-photo"></i> {{ i18n.ts.unsetUserBanner }}</MkButton>
|
||||||
|
</div>
|
||||||
<MkButton v-if="$i.isAdmin" inline danger @click="deleteAccount">{{ i18n.ts.deleteAccount }}</MkButton>
|
<MkButton v-if="$i.isAdmin" inline danger @click="deleteAccount">{{ i18n.ts.deleteAccount }}</MkButton>
|
||||||
</div>
|
</div>
|
||||||
</FormSection>
|
</FormSection>
|
||||||
|
@ -320,6 +324,44 @@ async function toggleSuspend(v) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function unsetUserAvatar() {
|
||||||
|
const confirm = await os.confirm({
|
||||||
|
type: 'warning',
|
||||||
|
text: i18n.ts.unsetUserAvatarConfirm,
|
||||||
|
});
|
||||||
|
if (confirm.canceled) return;
|
||||||
|
const process = async () => {
|
||||||
|
await os.api('admin/unset-user-avatar', { userId: user.id });
|
||||||
|
os.success();
|
||||||
|
};
|
||||||
|
await process().catch(err => {
|
||||||
|
os.alert({
|
||||||
|
type: 'error',
|
||||||
|
text: err.toString(),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
refreshUser();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function unsetUserBanner() {
|
||||||
|
const confirm = await os.confirm({
|
||||||
|
type: 'warning',
|
||||||
|
text: i18n.ts.unsetUserBannerConfirm,
|
||||||
|
});
|
||||||
|
if (confirm.canceled) return;
|
||||||
|
const process = async () => {
|
||||||
|
await os.api('admin/unset-user-banner', { userId: user.id });
|
||||||
|
os.success();
|
||||||
|
};
|
||||||
|
await process().catch(err => {
|
||||||
|
os.alert({
|
||||||
|
type: 'error',
|
||||||
|
text: err.toString(),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
refreshUser();
|
||||||
|
}
|
||||||
|
|
||||||
async function deleteAllFiles() {
|
async function deleteAllFiles() {
|
||||||
const confirm = await os.confirm({
|
const confirm = await os.confirm({
|
||||||
type: 'warning',
|
type: 'warning',
|
||||||
|
|
|
@ -38,6 +38,7 @@ import { } from 'vue';
|
||||||
import XHeader from './_header_.vue';
|
import XHeader from './_header_.vue';
|
||||||
import MkInput from '@/components/MkInput.vue';
|
import MkInput from '@/components/MkInput.vue';
|
||||||
import MkButton from '@/components/MkButton.vue';
|
import MkButton from '@/components/MkButton.vue';
|
||||||
|
import MkSwitch from '@/components/MkSwitch.vue';
|
||||||
import FormSuspense from '@/components/form/suspense.vue';
|
import FormSuspense from '@/components/form/suspense.vue';
|
||||||
import FormSection from '@/components/form/section.vue';
|
import FormSection from '@/components/form/section.vue';
|
||||||
import * as os from '@/os.js';
|
import * as os from '@/os.js';
|
||||||
|
|
|
@ -73,6 +73,13 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<MkSwitch v-model="enableActiveEmailValidation" @update:modelValue="save">
|
<MkSwitch v-model="enableActiveEmailValidation" @update:modelValue="save">
|
||||||
<template #label>Enable</template>
|
<template #label>Enable</template>
|
||||||
</MkSwitch>
|
</MkSwitch>
|
||||||
|
<MkSwitch v-model="enableVerifymailApi" @update:modelValue="save">
|
||||||
|
<template #label>Use Verifymail API</template>
|
||||||
|
</MkSwitch>
|
||||||
|
<MkInput v-model="verifymailAuthKey" @update:modelValue="save">
|
||||||
|
<template #prefix><i class="ti ti-key"></i></template>
|
||||||
|
<template #label>Verifymail API Auth Key</template>
|
||||||
|
</MkInput>
|
||||||
</div>
|
</div>
|
||||||
</MkFolder>
|
</MkFolder>
|
||||||
|
|
||||||
|
@ -132,6 +139,8 @@ let setSensitiveFlagAutomatically: boolean = $ref(false);
|
||||||
let enableSensitiveMediaDetectionForVideos: boolean = $ref(false);
|
let enableSensitiveMediaDetectionForVideos: boolean = $ref(false);
|
||||||
let enableIpLogging: boolean = $ref(false);
|
let enableIpLogging: boolean = $ref(false);
|
||||||
let enableActiveEmailValidation: boolean = $ref(false);
|
let enableActiveEmailValidation: boolean = $ref(false);
|
||||||
|
let enableVerifymailApi: boolean = $ref(false);
|
||||||
|
let verifymailAuthKey: string | null = $ref(null);
|
||||||
|
|
||||||
async function init() {
|
async function init() {
|
||||||
const meta = await os.api('admin/meta');
|
const meta = await os.api('admin/meta');
|
||||||
|
@ -150,6 +159,8 @@ async function init() {
|
||||||
enableSensitiveMediaDetectionForVideos = meta.enableSensitiveMediaDetectionForVideos;
|
enableSensitiveMediaDetectionForVideos = meta.enableSensitiveMediaDetectionForVideos;
|
||||||
enableIpLogging = meta.enableIpLogging;
|
enableIpLogging = meta.enableIpLogging;
|
||||||
enableActiveEmailValidation = meta.enableActiveEmailValidation;
|
enableActiveEmailValidation = meta.enableActiveEmailValidation;
|
||||||
|
enableVerifymailApi = meta.enableVerifymailApi;
|
||||||
|
verifymailAuthKey = meta.verifymailAuthKey;
|
||||||
}
|
}
|
||||||
|
|
||||||
function save() {
|
function save() {
|
||||||
|
@ -167,6 +178,8 @@ function save() {
|
||||||
enableSensitiveMediaDetectionForVideos,
|
enableSensitiveMediaDetectionForVideos,
|
||||||
enableIpLogging,
|
enableIpLogging,
|
||||||
enableActiveEmailValidation,
|
enableActiveEmailValidation,
|
||||||
|
enableVerifymailApi,
|
||||||
|
verifymailAuthKey,
|
||||||
}).then(() => {
|
}).then(() => {
|
||||||
fetchInstance();
|
fetchInstance();
|
||||||
});
|
});
|
||||||
|
|
|
@ -54,22 +54,24 @@ import { miLocalStorage } from '@/local-storage.js';
|
||||||
const { t, ts } = i18n;
|
const { t, ts } = i18n;
|
||||||
|
|
||||||
const defaultStoreSaveKeys: (keyof typeof defaultStore['state'])[] = [
|
const defaultStoreSaveKeys: (keyof typeof defaultStore['state'])[] = [
|
||||||
|
'collapseRenotes',
|
||||||
'menu',
|
'menu',
|
||||||
'visibility',
|
'visibility',
|
||||||
'localOnly',
|
'localOnly',
|
||||||
'statusbars',
|
'statusbars',
|
||||||
'widgets',
|
'widgets',
|
||||||
'tl',
|
'tl',
|
||||||
|
'pinnedUserLists',
|
||||||
'overridedDeviceKind',
|
'overridedDeviceKind',
|
||||||
'serverDisconnectedBehavior',
|
'serverDisconnectedBehavior',
|
||||||
'collapseRenotes',
|
|
||||||
'showNoteActionsOnlyHover',
|
|
||||||
'nsfw',
|
'nsfw',
|
||||||
|
'highlightSensitiveMedia',
|
||||||
'animation',
|
'animation',
|
||||||
'animatedMfm',
|
'animatedMfm',
|
||||||
'advancedMfm',
|
'advancedMfm',
|
||||||
'loadRawImages',
|
'loadRawImages',
|
||||||
'imageNewTab',
|
'imageNewTab',
|
||||||
|
'enableDataSaverMode',
|
||||||
'disableShowingAnimatedImages',
|
'disableShowingAnimatedImages',
|
||||||
'emojiStyle',
|
'emojiStyle',
|
||||||
'disableDrawer',
|
'disableDrawer',
|
||||||
|
@ -89,9 +91,28 @@ const defaultStoreSaveKeys: (keyof typeof defaultStore['state'])[] = [
|
||||||
'menuDisplay',
|
'menuDisplay',
|
||||||
'reportError',
|
'reportError',
|
||||||
'squareAvatars',
|
'squareAvatars',
|
||||||
|
'showAvatarDecorations',
|
||||||
'numberOfPageCache',
|
'numberOfPageCache',
|
||||||
|
'showNoteActionsOnlyHover',
|
||||||
|
'showClipButtonInNoteFooter',
|
||||||
|
'reactionsDisplaySize',
|
||||||
|
'forceShowAds',
|
||||||
'aiChanMode',
|
'aiChanMode',
|
||||||
|
'devMode',
|
||||||
'mediaListWithOneImageAppearance',
|
'mediaListWithOneImageAppearance',
|
||||||
|
'notificationPosition',
|
||||||
|
'notificationStackAxis',
|
||||||
|
'enableCondensedLineForAcct',
|
||||||
|
'keepScreenOn',
|
||||||
|
'defaultWithReplies',
|
||||||
|
'disableStreamingTimeline',
|
||||||
|
'useGroupedNotifications',
|
||||||
|
'sound_masterVolume',
|
||||||
|
'sound_note',
|
||||||
|
'sound_noteMy',
|
||||||
|
'sound_notification',
|
||||||
|
'sound_antenna',
|
||||||
|
'sound_channel',
|
||||||
];
|
];
|
||||||
const coldDeviceStorageSaveKeys: (keyof typeof ColdDeviceStorage.default)[] = [
|
const coldDeviceStorageSaveKeys: (keyof typeof ColdDeviceStorage.default)[] = [
|
||||||
'lightTheme',
|
'lightTheme',
|
||||||
|
|
|
@ -346,6 +346,18 @@ export type Endpoints = {
|
||||||
};
|
};
|
||||||
res: null;
|
res: null;
|
||||||
};
|
};
|
||||||
|
'admin/unset-user-avatar': {
|
||||||
|
req: {
|
||||||
|
userId: User['id'];
|
||||||
|
};
|
||||||
|
res: null;
|
||||||
|
};
|
||||||
|
'admin/unset-user-banner': {
|
||||||
|
req: {
|
||||||
|
userId: User['id'];
|
||||||
|
};
|
||||||
|
res: null;
|
||||||
|
};
|
||||||
'admin/delete-logs': {
|
'admin/delete-logs': {
|
||||||
req: NoParams;
|
req: NoParams;
|
||||||
res: null;
|
res: null;
|
||||||
|
|
|
@ -15,6 +15,8 @@ export type Endpoints = {
|
||||||
// admin
|
// admin
|
||||||
'admin/abuse-user-reports': { req: TODO; res: TODO; };
|
'admin/abuse-user-reports': { req: TODO; res: TODO; };
|
||||||
'admin/delete-all-files-of-a-user': { req: { userId: User['id']; }; res: null; };
|
'admin/delete-all-files-of-a-user': { req: { userId: User['id']; }; res: null; };
|
||||||
|
'admin/unset-user-avatar': { req: { userId: User['id']; }; res: null; };
|
||||||
|
'admin/unset-user-banner': { req: { userId: User['id']; }; res: null; };
|
||||||
'admin/delete-logs': { req: NoParams; res: null; };
|
'admin/delete-logs': { req: NoParams; res: null; };
|
||||||
'admin/get-index-stats': { req: TODO; res: TODO; };
|
'admin/get-index-stats': { req: TODO; res: TODO; };
|
||||||
'admin/get-table-stats': { req: TODO; res: TODO; };
|
'admin/get-table-stats': { req: TODO; res: TODO; };
|
||||||
|
|
|
@ -81,6 +81,8 @@ export const moderationLogTypes = [
|
||||||
'createAvatarDecoration',
|
'createAvatarDecoration',
|
||||||
'updateAvatarDecoration',
|
'updateAvatarDecoration',
|
||||||
'deleteAvatarDecoration',
|
'deleteAvatarDecoration',
|
||||||
|
'unsetUserAvatar',
|
||||||
|
'unsetUserBanner',
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
export type ModerationLogPayloads = {
|
export type ModerationLogPayloads = {
|
||||||
|
@ -255,4 +257,16 @@ export type ModerationLogPayloads = {
|
||||||
avatarDecorationId: string;
|
avatarDecorationId: string;
|
||||||
avatarDecoration: any;
|
avatarDecoration: any;
|
||||||
};
|
};
|
||||||
|
unsetUserAvatar: {
|
||||||
|
userId: string;
|
||||||
|
userUsername: string;
|
||||||
|
userHost: string | null;
|
||||||
|
fileId: string;
|
||||||
|
};
|
||||||
|
unsetUserBanner: {
|
||||||
|
userId: string;
|
||||||
|
userUsername: string;
|
||||||
|
userHost: string | null;
|
||||||
|
fileId: string;
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
|
@ -731,4 +731,10 @@ export type ModerationLog = {
|
||||||
} | {
|
} | {
|
||||||
type: 'resolveAbuseReport';
|
type: 'resolveAbuseReport';
|
||||||
info: ModerationLogPayloads['resolveAbuseReport'];
|
info: ModerationLogPayloads['resolveAbuseReport'];
|
||||||
|
} | {
|
||||||
|
type: 'unsetUserAvatar';
|
||||||
|
info: ModerationLogPayloads['unsetUserAvatar'];
|
||||||
|
} | {
|
||||||
|
type: 'unsetUserBanner';
|
||||||
|
info: ModerationLogPayloads['unsetUserBanner'];
|
||||||
});
|
});
|
||||||
|
|
Loading…
Reference in a new issue