Skip to content

Commit

Permalink
Merge tag '2024.10.1-beta.3' into nyadev
Browse files Browse the repository at this point in the history
# Conflicts:
#	packages/backend/src/core/CaptchaService.ts
#	packages/backend/src/core/entities/MetaEntityService.ts
#	packages/backend/src/models/Meta.ts
#	packages/backend/src/models/json-schema/meta.ts
#	packages/backend/src/server/api/SignupApiService.ts
#	packages/backend/src/server/api/endpoints/admin/meta.ts
#	packages/backend/src/server/api/endpoints/admin/update-meta.ts
#	packages/frontend/src/components/MkCaptcha.vue
#	packages/frontend/src/components/MkSignupDialog.form.vue
#	packages/frontend/src/pages/admin/bot-protection.vue
#	packages/misskey-js/src/autogen/types.ts
  • Loading branch information
Candinya committed Oct 12, 2024
2 parents 889b0dc + 7778046 commit 2e187d9
Show file tree
Hide file tree
Showing 37 changed files with 796 additions and 102 deletions.
10 changes: 9 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,16 @@
## 2024.10.1
### Note
- 悪質なユーザからサーバを守る措置の一環として、モデレータ権限を持つユーザの最終アクティブ日時を確認し、
7日間活動していない場合は自動的に招待制へと移行(コントロールパネル -> モデレーション -> "誰でも新規登録できるようにする"をオフに変更)するようになりました。
詳細な経緯は https://github.com/misskey-dev/misskey/issues/13437 をご確認ください。

### Client
- Fix: メールアドレス不要でCaptchaが有効な場合にアカウント登録完了後自動でのログインに失敗する問題を修正
- Enhance: l10nの更新
- Fix: メールアドレス不要でCaptchaが有効な場合にアカウント登録完了後自動でのログインに失敗する問題を修正

### Server
- Feat: モデレータ権限を持つユーザが全員7日間活動しなかった場合は自動的に招待制へと移行するように ( #13437 )
- Fix: `admin/emoji/update`エンドポイントのidのみ指定した時不正なエラーが発生するバグを修正

## 2024.10.0

Expand Down
8 changes: 8 additions & 0 deletions locales/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5166,6 +5166,10 @@ export interface Locale extends ILocale {
* 対象
*/
"target": string;
/**
* CAPTCHAのテストを目的とした機能です。<strong>本番環境で使用しないでください。</strong>
*/
"testCaptchaWarning": string;
"_abuseUserReport": {
/**
* 転送
Expand Down Expand Up @@ -5696,6 +5700,10 @@ export interface Locale extends ILocale {
* サーバー運営者へのお問い合わせフォームのURLや、運営者の連絡先等が記載されたWebページのURLを指定します。
*/
"inquiryUrlDescription": string;
/**
* 一定期間モデレーターのアクティビティが検出されなかった場合、スパム防止のためこの設定は自動でオフになります。
*/
"thisSettingWillAutomaticallyOffWhenModeratorsInactive": string;
};
"_accountMigration": {
/**
Expand Down
2 changes: 2 additions & 0 deletions locales/ja-JP.yml
Original file line number Diff line number Diff line change
Expand Up @@ -1287,6 +1287,7 @@ passkeyVerificationFailed: "パスキーの検証に失敗しました。"
passkeyVerificationSucceededButPasswordlessLoginDisabled: "パスキーの検証に成功しましたが、パスワードレスログインが無効になっています。"
messageToFollower: "フォロワーへのメッセージ"
target: "対象"
testCaptchaWarning: "CAPTCHAのテストを目的とした機能です。<strong>本番環境で使用しないでください。</strong>"

_abuseUserReport:
forward: "転送"
Expand Down Expand Up @@ -1440,6 +1441,7 @@ _serverSettings:
reactionsBufferingDescription: "有効にすると、リアクション作成時のパフォーマンスが大幅に向上し、データベースへの負荷を軽減することが可能です。ただし、Redisのメモリ使用量は増加します。"
inquiryUrl: "問い合わせ先URL"
inquiryUrlDescription: "サーバー運営者へのお問い合わせフォームのURLや、運営者の連絡先等が記載されたWebページのURLを指定します。"
thisSettingWillAutomaticallyOffWhenModeratorsInactive: "一定期間モデレーターのアクティビティが検出されなかった場合、スパム防止のためこの設定は自動でオフになります。"

_accountMigration:
moveFrom: "別のアカウントからこのアカウントに移行"
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "misskey",
"version": "2024.10.1-beta.2",
"version": "2024.10.1-beta.3",
"codename": "nasubi",
"repository": {
"type": "git",
Expand Down
16 changes: 16 additions & 0 deletions packages/backend/migration/1728550878802-testcaptcha.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/

export class Testcaptcha1728550878802 {
name = 'Testcaptcha1728550878802'

async up(queryRunner) {
await queryRunner.query(`ALTER TABLE "meta" ADD "enableTestcaptcha" boolean NOT NULL DEFAULT false`);
}

async down(queryRunner) {
await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "enableTestcaptcha"`);
}
}
10 changes: 8 additions & 2 deletions packages/backend/src/core/AbuseReportNotificationService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,10 @@ export class AbuseReportNotificationService implements OnApplicationShutdown {
return;
}

const moderatorIds = await this.roleService.getModeratorIds(true, true);
const moderatorIds = await this.roleService.getModeratorIds({
includeAdmins: true,
excludeExpire: true,
});

for (const moderatorId of moderatorIds) {
for (const abuseReport of abuseReports) {
Expand Down Expand Up @@ -370,7 +373,10 @@ export class AbuseReportNotificationService implements OnApplicationShutdown {
}

// モデレータ権限の有無で通知先設定を振り分ける
const authorizedUserIds = await this.roleService.getModeratorIds(true, true);
const authorizedUserIds = await this.roleService.getModeratorIds({
includeAdmins: true,
excludeExpire: true,
});
const authorizedUserRecipients = Array.of<MiAbuseReportNotificationRecipient>();
const unauthorizedUserRecipients = Array.of<MiAbuseReportNotificationRecipient>();
for (const recipient of userRecipients) {
Expand Down
13 changes: 13 additions & 0 deletions packages/backend/src/core/CaptchaService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,19 @@ export class CaptchaService {
}
}

@bindThis
public async verifyTestcaptcha(response: string | null | undefined): Promise<void> {
if (response == null) {
throw new Error('testcaptcha-failed: no response provided');
}

const success = response === 'testcaptcha-passed';

if (!success) {
throw new Error('testcaptcha-failed');
}
}

@bindThis
public async verifyNyaCap(secret: string, instanceHost: string, response: string | null | undefined): Promise<void> {
if (response == null) {
Expand Down
29 changes: 22 additions & 7 deletions packages/backend/src/core/CustomEmojiService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -103,19 +103,33 @@ export class CustomEmojiService implements OnApplicationShutdown {
}

@bindThis
public async update(id: MiEmoji['id'], data: {
public async update(data: (
{ id: MiEmoji['id'], name?: string; } | { name: string; id?: MiEmoji['id'], }
) & {
driveFile?: MiDriveFile;
name?: string;
category?: string | null;
aliases?: string[];
license?: string | null;
isSensitive?: boolean;
localOnly?: boolean;
roleIdsThatCanBeUsedThisEmojiAsReaction?: MiRole['id'][];
}, moderator?: MiUser): Promise<void> {
const emoji = await this.emojisRepository.findOneByOrFail({ id: id });
const sameNameEmoji = await this.emojisRepository.findOneBy({ name: data.name, host: IsNull() });
if (sameNameEmoji != null && sameNameEmoji.id !== id) throw new Error('name already exists');
}, moderator?: MiUser): Promise<
null
| 'NO_SUCH_EMOJI'
| 'SAME_NAME_EMOJI_EXISTS'
> {
const emoji = data.id
? await this.getEmojiById(data.id)
: await this.getEmojiByName(data.name!);
if (emoji === null) return 'NO_SUCH_EMOJI';
const id = emoji.id;

// IDと絵文字名が両方指定されている場合は絵文字名の変更を行うため重複チェックが必要
const doNameUpdate = data.id && data.name && (data.name !== emoji.name);
if (doNameUpdate) {
const isDuplicate = await this.checkDuplicate(data.name!);
if (isDuplicate) return 'SAME_NAME_EMOJI_EXISTS';
}

await this.emojisRepository.update(emoji.id, {
updatedAt: new Date(),
Expand All @@ -135,7 +149,7 @@ export class CustomEmojiService implements OnApplicationShutdown {

const packed = await this.emojiEntityService.packDetailed(emoji.id);

if (emoji.name === data.name) {
if (!doNameUpdate) {
this.globalEventService.publishBroadcastStream('emojiUpdated', {
emojis: [packed],
});
Expand All @@ -157,6 +171,7 @@ export class CustomEmojiService implements OnApplicationShutdown {
after: updated,
});
}
return null;
}

@bindThis
Expand Down
7 changes: 7 additions & 0 deletions packages/backend/src/core/QueueService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,13 @@ export class QueueService {
repeat: { pattern: '0 0 * * *' },
removeOnComplete: true,
});

this.systemQueue.add('checkModeratorsActivity', {
}, {
// 毎時30分に起動
repeat: { pattern: '30 * * * *' },
removeOnComplete: true,
});
}

@bindThis
Expand Down
77 changes: 54 additions & 23 deletions packages/backend/src/core/RoleService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,7 @@ export const DEFAULT_POLICIES: RolePolicies = {

@Injectable()
export class RoleService implements OnApplicationShutdown, OnModuleInit {
private rootUserIdCache: MemorySingleCache<MiUser['id']>;
private rolesCache: MemorySingleCache<MiRole[]>;
private roleAssignmentByUserIdCache: MemoryKVCache<MiRoleAssignment[]>;
private notificationService: NotificationService;
Expand Down Expand Up @@ -138,6 +139,7 @@ export class RoleService implements OnApplicationShutdown, OnModuleInit {
private moderationLogService: ModerationLogService,
private fanoutTimelineService: FanoutTimelineService,
) {
this.rootUserIdCache = new MemorySingleCache<MiUser['id']>(1000 * 60 * 60 * 24 * 7); // 1week. rootユーザのIDは不変なので長めに
this.rolesCache = new MemorySingleCache<MiRole[]>(1000 * 60 * 60); // 1h
this.roleAssignmentByUserIdCache = new MemoryKVCache<MiRoleAssignment[]>(1000 * 60 * 5); // 5m

Expand Down Expand Up @@ -419,49 +421,78 @@ export class RoleService implements OnApplicationShutdown, OnModuleInit {
}

@bindThis
public async isExplorable(role: { id: MiRole['id']} | null): Promise<boolean> {
public async isExplorable(role: { id: MiRole['id'] } | null): Promise<boolean> {
if (role == null) return false;
const check = await this.rolesRepository.findOneBy({ id: role.id });
if (check == null) return false;
return check.isExplorable;
}

/**
* モデレーター権限のロールが割り当てられているユーザID一覧を取得する.
*
* @param opts.includeAdmins 管理者権限も含めるか(デフォルト: true)
* @param opts.includeRoot rootユーザも含めるか(デフォルト: false)
* @param opts.excludeExpire 期限切れのロールを除外するか(デフォルト: false)
*/
@bindThis
public async getModeratorIds(includeAdmins = true, excludeExpire = false): Promise<MiUser['id'][]> {
public async getModeratorIds(opts?: {
includeAdmins?: boolean,
includeRoot?: boolean,
excludeExpire?: boolean,
}): Promise<MiUser['id'][]> {
const includeAdmins = opts?.includeAdmins ?? true;
const includeRoot = opts?.includeRoot ?? false;
const excludeExpire = opts?.excludeExpire ?? false;

const roles = await this.rolesCache.fetch(() => this.rolesRepository.findBy({}));
const moderatorRoles = includeAdmins
? roles.filter(r => r.isModerator || r.isAdministrator)
: roles.filter(r => r.isModerator);

// TODO: isRootなアカウントも含める
const assigns = moderatorRoles.length > 0
? await this.roleAssignmentsRepository.findBy({ roleId: In(moderatorRoles.map(r => r.id)) })
: [];

// Setを経由して重複を除去(ユーザIDは重複する可能性があるので)
const now = Date.now();
const result = [
// Setを経由して重複を除去(ユーザIDは重複する可能性があるので)
...new Set(
assigns
.filter(it =>
(excludeExpire)
? (it.expiresAt == null || it.expiresAt.getTime() > now)
: true,
)
.map(a => a.userId),
),
];

return result.sort((x, y) => x.localeCompare(y));
const resultSet = new Set(
assigns
.filter(it =>
(excludeExpire)
? (it.expiresAt == null || it.expiresAt.getTime() > now)
: true,
)
.map(a => a.userId),
);

if (includeRoot) {
const rootUserId = await this.rootUserIdCache.fetch(async () => {
const it = await this.usersRepository.createQueryBuilder('users')
.select('id')
.where({ isRoot: true })
.getRawOne<{ id: string }>();
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
return it!.id;
});
resultSet.add(rootUserId);
}

return [...resultSet].sort((x, y) => x.localeCompare(y));
}

@bindThis
public async getModerators(includeAdmins = true): Promise<MiUser[]> {
const ids = await this.getModeratorIds(includeAdmins);
const users = ids.length > 0 ? await this.usersRepository.findBy({
id: In(ids),
}) : [];
return users;
public async getModerators(opts?: {
includeAdmins?: boolean,
includeRoot?: boolean,
excludeExpire?: boolean,
}): Promise<MiUser[]> {
const ids = await this.getModeratorIds(opts);
return ids.length > 0
? await this.usersRepository.findBy({
id: In(ids),
})
: [];
}

@bindThis
Expand Down
1 change: 1 addition & 0 deletions packages/backend/src/core/entities/MetaEntityService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@ export class MetaEntityService {
recaptchaSiteKey: instance.recaptchaSiteKey,
enableTurnstile: instance.enableTurnstile,
turnstileSiteKey: instance.turnstileSiteKey,
enableTestcaptcha: instance.enableTestcaptcha,
enableNyaCap: instance.enableNyaCap,
nyacapSiteKey: instance.nyacapSiteKey,
nyacapInstanceUrl: instance.nyacapInstanceUrl,
Expand Down
5 changes: 5 additions & 0 deletions packages/backend/src/models/Meta.ts
Original file line number Diff line number Diff line change
Expand Up @@ -258,6 +258,11 @@ export class MiMeta {
})
public turnstileSecretKey: string | null;

@Column('boolean', {
default: false,
})
public enableTestcaptcha: boolean;

@Column('boolean', {
default: false,
})
Expand Down
4 changes: 4 additions & 0 deletions packages/backend/src/models/json-schema/meta.ts
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,10 @@ export const packedMetaLiteSchema = {
type: 'string',
optional: false, nullable: true,
},
enableTestcaptcha: {
type: 'boolean',
optional: false, nullable: false,
},
enableNyaCap: {
type: 'boolean',
optional: false, nullable: false,
Expand Down
3 changes: 3 additions & 0 deletions packages/backend/src/queue/QueueProcessorModule.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import { Module } from '@nestjs/common';
import { CoreModule } from '@/core/CoreModule.js';
import { GlobalModule } from '@/GlobalModule.js';
import { CheckModeratorsActivityProcessorService } from '@/queue/processors/CheckModeratorsActivityProcessorService.js';
import { QueueLoggerService } from './QueueLoggerService.js';
import { QueueProcessorService } from './QueueProcessorService.js';
import { DeliverProcessorService } from './processors/DeliverProcessorService.js';
Expand Down Expand Up @@ -80,6 +81,8 @@ import { RelationshipProcessorService } from './processors/RelationshipProcessor
DeliverProcessorService,
InboxProcessorService,
AggregateRetentionProcessorService,
CheckExpiredMutingsProcessorService,
CheckModeratorsActivityProcessorService,
QueueProcessorService,
],
exports: [
Expand Down
3 changes: 3 additions & 0 deletions packages/backend/src/queue/QueueProcessorService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import type { Config } from '@/config.js';
import { DI } from '@/di-symbols.js';
import type Logger from '@/logger.js';
import { bindThis } from '@/decorators.js';
import { CheckModeratorsActivityProcessorService } from '@/queue/processors/CheckModeratorsActivityProcessorService.js';
import { UserWebhookDeliverProcessorService } from './processors/UserWebhookDeliverProcessorService.js';
import { SystemWebhookDeliverProcessorService } from './processors/SystemWebhookDeliverProcessorService.js';
import { EndedPollNotificationProcessorService } from './processors/EndedPollNotificationProcessorService.js';
Expand Down Expand Up @@ -120,6 +121,7 @@ export class QueueProcessorService implements OnApplicationShutdown {
private aggregateRetentionProcessorService: AggregateRetentionProcessorService,
private checkExpiredMutingsProcessorService: CheckExpiredMutingsProcessorService,
private bakeBufferedReactionsProcessorService: BakeBufferedReactionsProcessorService,
private checkModeratorsActivityProcessorService: CheckModeratorsActivityProcessorService,
private cleanProcessorService: CleanProcessorService,
) {
this.logger = this.queueLoggerService.logger;
Expand Down Expand Up @@ -150,6 +152,7 @@ export class QueueProcessorService implements OnApplicationShutdown {
case 'aggregateRetention': return this.aggregateRetentionProcessorService.process();
case 'checkExpiredMutings': return this.checkExpiredMutingsProcessorService.process();
case 'bakeBufferedReactions': return this.bakeBufferedReactionsProcessorService.process();
case 'checkModeratorsActivity': return this.checkModeratorsActivityProcessorService.process();
case 'clean': return this.cleanProcessorService.process();
default: throw new Error(`unrecognized job type ${job.name} for system`);
}
Expand Down
Loading

0 comments on commit 2e187d9

Please sign in to comment.