Merge remote-tracking branch 'misskey-original/develop' into develop

This commit is contained in:
mattyatea 2023-11-19 17:56:14 +09:00
commit e3d8346e21
23 changed files with 418 additions and 14 deletions

View file

@ -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

View file

@ -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
View file

@ -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;

View file

@ -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: "ファイルの詳細"

View file

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

View file

@ -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",

View file

@ -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,
};
}
} }

View file

@ -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,
}) })

View file

@ -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,

View file

@ -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],

View file

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

View file

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

View file

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

View file

@ -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> = {

View file

@ -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 {

View file

@ -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',

View file

@ -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';

View file

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

View file

@ -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',

View file

@ -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;

View file

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

View file

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

View file

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