Compare commits
12 Commits
2024.10.1-
...
2024.10.1-
Author | SHA1 | Date | |
---|---|---|---|
![]() |
777804605e | ||
![]() |
af1cbc131f | ||
![]() |
c397b42242 | ||
![]() |
a2cd6a7709 | ||
![]() |
12bc671511 | ||
![]() |
d376aab45e | ||
![]() |
1ad3148533 | ||
![]() |
132c4ba6ce | ||
![]() |
67a5fccb3b | ||
![]() |
4c84842f3d | ||
![]() |
54849bde6c | ||
![]() |
b668d161a9 |
14
CHANGELOG.md
14
CHANGELOG.md
@@ -1,14 +1,16 @@
|
|||||||
## 2024.10.1
|
## 2024.10.1
|
||||||
|
### Note
|
||||||
### General
|
- 悪質なユーザからサーバを守る措置の一環として、モデレータ権限を持つユーザの最終アクティブ日時を確認し、
|
||||||
-
|
7日間活動していない場合は自動的に招待制へと移行(コントロールパネル -> モデレーション -> "誰でも新規登録できるようにする"をオフに変更)するようになりました。
|
||||||
|
詳細な経緯は https://github.com/misskey-dev/misskey/issues/13437 をご確認ください。
|
||||||
|
|
||||||
### Client
|
### Client
|
||||||
- メールアドレス不要でCaptchaが有効な場合にアカウント登録完了後自動でのログインに失敗する問題を修正
|
- Enhance: l10nの更新
|
||||||
|
- Fix: メールアドレス不要でCaptchaが有効な場合にアカウント登録完了後自動でのログインに失敗する問題を修正
|
||||||
|
|
||||||
### Server
|
### Server
|
||||||
-
|
- Feat: モデレータ権限を持つユーザが全員7日間活動しなかった場合は自動的に招待制へと移行するように ( #13437 )
|
||||||
|
- Fix: `admin/emoji/update`エンドポイントのidのみ指定した時不正なエラーが発生するバグを修正
|
||||||
|
|
||||||
## 2024.10.0
|
## 2024.10.0
|
||||||
|
|
||||||
|
4
locales/index.d.ts
vendored
4
locales/index.d.ts
vendored
@@ -5166,6 +5166,10 @@ export interface Locale extends ILocale {
|
|||||||
* 対象
|
* 対象
|
||||||
*/
|
*/
|
||||||
"target": string;
|
"target": string;
|
||||||
|
/**
|
||||||
|
* CAPTCHAのテストを目的とした機能です。<strong>本番環境で使用しないでください。</strong>
|
||||||
|
*/
|
||||||
|
"testCaptchaWarning": string;
|
||||||
"_abuseUserReport": {
|
"_abuseUserReport": {
|
||||||
/**
|
/**
|
||||||
* 転送
|
* 転送
|
||||||
|
@@ -1287,6 +1287,7 @@ passkeyVerificationFailed: "パスキーの検証に失敗しました。"
|
|||||||
passkeyVerificationSucceededButPasswordlessLoginDisabled: "パスキーの検証に成功しましたが、パスワードレスログインが無効になっています。"
|
passkeyVerificationSucceededButPasswordlessLoginDisabled: "パスキーの検証に成功しましたが、パスワードレスログインが無効になっています。"
|
||||||
messageToFollower: "フォロワーへのメッセージ"
|
messageToFollower: "フォロワーへのメッセージ"
|
||||||
target: "対象"
|
target: "対象"
|
||||||
|
testCaptchaWarning: "CAPTCHAのテストを目的とした機能です。<strong>本番環境で使用しないでください。</strong>"
|
||||||
|
|
||||||
_abuseUserReport:
|
_abuseUserReport:
|
||||||
forward: "転送"
|
forward: "転送"
|
||||||
@@ -1440,6 +1441,7 @@ _serverSettings:
|
|||||||
reactionsBufferingDescription: "有効にすると、リアクション作成時のパフォーマンスが大幅に向上し、データベースへの負荷を軽減することが可能です。ただし、Redisのメモリ使用量は増加します。"
|
reactionsBufferingDescription: "有効にすると、リアクション作成時のパフォーマンスが大幅に向上し、データベースへの負荷を軽減することが可能です。ただし、Redisのメモリ使用量は増加します。"
|
||||||
inquiryUrl: "問い合わせ先URL"
|
inquiryUrl: "問い合わせ先URL"
|
||||||
inquiryUrlDescription: "サーバー運営者へのお問い合わせフォームのURLや、運営者の連絡先等が記載されたWebページのURLを指定します。"
|
inquiryUrlDescription: "サーバー運営者へのお問い合わせフォームのURLや、運営者の連絡先等が記載されたWebページのURLを指定します。"
|
||||||
|
thisSettingWillAutomaticallyOffWhenModeratorsInactive: "一定期間モデレーターのアクティビティが検出されなかった場合、スパム防止のためこの設定は自動でオフになります。"
|
||||||
|
|
||||||
_accountMigration:
|
_accountMigration:
|
||||||
moveFrom: "別のアカウントからこのアカウントに移行"
|
moveFrom: "別のアカウントからこのアカウントに移行"
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "misskey",
|
"name": "misskey",
|
||||||
"version": "2024.10.1-beta.1",
|
"version": "2024.10.1-beta.3",
|
||||||
"codename": "nasubi",
|
"codename": "nasubi",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
|
16
packages/backend/migration/1728550878802-testcaptcha.js
Normal file
16
packages/backend/migration/1728550878802-testcaptcha.js
Normal file
@@ -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"`);
|
||||||
|
}
|
||||||
|
}
|
@@ -61,7 +61,10 @@ export class AbuseReportNotificationService implements OnApplicationShutdown {
|
|||||||
return;
|
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 moderatorId of moderatorIds) {
|
||||||
for (const abuseReport of abuseReports) {
|
for (const abuseReport of abuseReports) {
|
||||||
@@ -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 authorizedUserRecipients = Array.of<MiAbuseReportNotificationRecipient>();
|
||||||
const unauthorizedUserRecipients = Array.of<MiAbuseReportNotificationRecipient>();
|
const unauthorizedUserRecipients = Array.of<MiAbuseReportNotificationRecipient>();
|
||||||
for (const recipient of userRecipients) {
|
for (const recipient of userRecipients) {
|
||||||
|
@@ -119,5 +119,18 @@ export class CaptchaService {
|
|||||||
throw new Error(`turnstile-failed: ${errorCodes}`);
|
throw new Error(`turnstile-failed: ${errorCodes}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@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');
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -103,19 +103,33 @@ export class CustomEmojiService implements OnApplicationShutdown {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public async update(id: MiEmoji['id'], data: {
|
public async update(data: (
|
||||||
|
{ id: MiEmoji['id'], name?: string; } | { name: string; id?: MiEmoji['id'], }
|
||||||
|
) & {
|
||||||
driveFile?: MiDriveFile;
|
driveFile?: MiDriveFile;
|
||||||
name?: string;
|
|
||||||
category?: string | null;
|
category?: string | null;
|
||||||
aliases?: string[];
|
aliases?: string[];
|
||||||
license?: string | null;
|
license?: string | null;
|
||||||
isSensitive?: boolean;
|
isSensitive?: boolean;
|
||||||
localOnly?: boolean;
|
localOnly?: boolean;
|
||||||
roleIdsThatCanBeUsedThisEmojiAsReaction?: MiRole['id'][];
|
roleIdsThatCanBeUsedThisEmojiAsReaction?: MiRole['id'][];
|
||||||
}, moderator?: MiUser): Promise<void> {
|
}, moderator?: MiUser): Promise<
|
||||||
const emoji = await this.emojisRepository.findOneByOrFail({ id: id });
|
null
|
||||||
const sameNameEmoji = await this.emojisRepository.findOneBy({ name: data.name, host: IsNull() });
|
| 'NO_SUCH_EMOJI'
|
||||||
if (sameNameEmoji != null && sameNameEmoji.id !== id) throw new Error('name already exists');
|
| '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, {
|
await this.emojisRepository.update(emoji.id, {
|
||||||
updatedAt: new Date(),
|
updatedAt: new Date(),
|
||||||
@@ -135,7 +149,7 @@ export class CustomEmojiService implements OnApplicationShutdown {
|
|||||||
|
|
||||||
const packed = await this.emojiEntityService.packDetailed(emoji.id);
|
const packed = await this.emojiEntityService.packDetailed(emoji.id);
|
||||||
|
|
||||||
if (emoji.name === data.name) {
|
if (!doNameUpdate) {
|
||||||
this.globalEventService.publishBroadcastStream('emojiUpdated', {
|
this.globalEventService.publishBroadcastStream('emojiUpdated', {
|
||||||
emojis: [packed],
|
emojis: [packed],
|
||||||
});
|
});
|
||||||
@@ -157,6 +171,7 @@ export class CustomEmojiService implements OnApplicationShutdown {
|
|||||||
after: updated,
|
after: updated,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
|
@@ -93,6 +93,13 @@ export class QueueService {
|
|||||||
repeat: { pattern: '0 0 * * *' },
|
repeat: { pattern: '0 0 * * *' },
|
||||||
removeOnComplete: true,
|
removeOnComplete: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.systemQueue.add('checkModeratorsActivity', {
|
||||||
|
}, {
|
||||||
|
// 毎時30分に起動
|
||||||
|
repeat: { pattern: '30 * * * *' },
|
||||||
|
removeOnComplete: true,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
|
@@ -101,6 +101,7 @@ export const DEFAULT_POLICIES: RolePolicies = {
|
|||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class RoleService implements OnApplicationShutdown, OnModuleInit {
|
export class RoleService implements OnApplicationShutdown, OnModuleInit {
|
||||||
|
private rootUserIdCache: MemorySingleCache<MiUser['id']>;
|
||||||
private rolesCache: MemorySingleCache<MiRole[]>;
|
private rolesCache: MemorySingleCache<MiRole[]>;
|
||||||
private roleAssignmentByUserIdCache: MemoryKVCache<MiRoleAssignment[]>;
|
private roleAssignmentByUserIdCache: MemoryKVCache<MiRoleAssignment[]>;
|
||||||
private notificationService: NotificationService;
|
private notificationService: NotificationService;
|
||||||
@@ -136,6 +137,7 @@ export class RoleService implements OnApplicationShutdown, OnModuleInit {
|
|||||||
private moderationLogService: ModerationLogService,
|
private moderationLogService: ModerationLogService,
|
||||||
private fanoutTimelineService: FanoutTimelineService,
|
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.rolesCache = new MemorySingleCache<MiRole[]>(1000 * 60 * 60); // 1h
|
||||||
this.roleAssignmentByUserIdCache = new MemoryKVCache<MiRoleAssignment[]>(1000 * 60 * 5); // 5m
|
this.roleAssignmentByUserIdCache = new MemoryKVCache<MiRoleAssignment[]>(1000 * 60 * 5); // 5m
|
||||||
|
|
||||||
@@ -416,49 +418,78 @@ export class RoleService implements OnApplicationShutdown, OnModuleInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@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;
|
if (role == null) return false;
|
||||||
const check = await this.rolesRepository.findOneBy({ id: role.id });
|
const check = await this.rolesRepository.findOneBy({ id: role.id });
|
||||||
if (check == null) return false;
|
if (check == null) return false;
|
||||||
return check.isExplorable;
|
return check.isExplorable;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* モデレーター権限のロールが割り当てられているユーザID一覧を取得する.
|
||||||
|
*
|
||||||
|
* @param opts.includeAdmins 管理者権限も含めるか(デフォルト: true)
|
||||||
|
* @param opts.includeRoot rootユーザも含めるか(デフォルト: false)
|
||||||
|
* @param opts.excludeExpire 期限切れのロールを除外するか(デフォルト: false)
|
||||||
|
*/
|
||||||
@bindThis
|
@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 roles = await this.rolesCache.fetch(() => this.rolesRepository.findBy({}));
|
||||||
const moderatorRoles = includeAdmins
|
const moderatorRoles = includeAdmins
|
||||||
? roles.filter(r => r.isModerator || r.isAdministrator)
|
? roles.filter(r => r.isModerator || r.isAdministrator)
|
||||||
: roles.filter(r => r.isModerator);
|
: roles.filter(r => r.isModerator);
|
||||||
|
|
||||||
// TODO: isRootなアカウントも含める
|
|
||||||
const assigns = moderatorRoles.length > 0
|
const assigns = moderatorRoles.length > 0
|
||||||
? await this.roleAssignmentsRepository.findBy({ roleId: In(moderatorRoles.map(r => r.id)) })
|
? await this.roleAssignmentsRepository.findBy({ roleId: In(moderatorRoles.map(r => r.id)) })
|
||||||
: [];
|
: [];
|
||||||
|
|
||||||
|
// Setを経由して重複を除去(ユーザIDは重複する可能性があるので)
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
const result = [
|
const resultSet = new Set(
|
||||||
// Setを経由して重複を除去(ユーザIDは重複する可能性があるので)
|
assigns
|
||||||
...new Set(
|
.filter(it =>
|
||||||
assigns
|
(excludeExpire)
|
||||||
.filter(it =>
|
? (it.expiresAt == null || it.expiresAt.getTime() > now)
|
||||||
(excludeExpire)
|
: true,
|
||||||
? (it.expiresAt == null || it.expiresAt.getTime() > now)
|
)
|
||||||
: true,
|
.map(a => a.userId),
|
||||||
)
|
);
|
||||||
.map(a => a.userId),
|
|
||||||
),
|
|
||||||
];
|
|
||||||
|
|
||||||
return result.sort((x, y) => x.localeCompare(y));
|
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
|
@bindThis
|
||||||
public async getModerators(includeAdmins = true): Promise<MiUser[]> {
|
public async getModerators(opts?: {
|
||||||
const ids = await this.getModeratorIds(includeAdmins);
|
includeAdmins?: boolean,
|
||||||
const users = ids.length > 0 ? await this.usersRepository.findBy({
|
includeRoot?: boolean,
|
||||||
id: In(ids),
|
excludeExpire?: boolean,
|
||||||
}) : [];
|
}): Promise<MiUser[]> {
|
||||||
return users;
|
const ids = await this.getModeratorIds(opts);
|
||||||
|
return ids.length > 0
|
||||||
|
? await this.usersRepository.findBy({
|
||||||
|
id: In(ids),
|
||||||
|
})
|
||||||
|
: [];
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
|
@@ -96,6 +96,7 @@ export class MetaEntityService {
|
|||||||
recaptchaSiteKey: instance.recaptchaSiteKey,
|
recaptchaSiteKey: instance.recaptchaSiteKey,
|
||||||
enableTurnstile: instance.enableTurnstile,
|
enableTurnstile: instance.enableTurnstile,
|
||||||
turnstileSiteKey: instance.turnstileSiteKey,
|
turnstileSiteKey: instance.turnstileSiteKey,
|
||||||
|
enableTestcaptcha: instance.enableTestcaptcha,
|
||||||
swPublickey: instance.swPublicKey,
|
swPublickey: instance.swPublicKey,
|
||||||
themeColor: instance.themeColor,
|
themeColor: instance.themeColor,
|
||||||
mascotImageUrl: instance.mascotImageUrl ?? '/assets/ai.png',
|
mascotImageUrl: instance.mascotImageUrl ?? '/assets/ai.png',
|
||||||
|
@@ -258,6 +258,11 @@ export class MiMeta {
|
|||||||
})
|
})
|
||||||
public turnstileSecretKey: string | null;
|
public turnstileSecretKey: string | null;
|
||||||
|
|
||||||
|
@Column('boolean', {
|
||||||
|
default: false,
|
||||||
|
})
|
||||||
|
public enableTestcaptcha: boolean;
|
||||||
|
|
||||||
// chaptcha系を追加した際にはnodeinfoのレスポンスに追加するのを忘れないようにすること
|
// chaptcha系を追加した際にはnodeinfoのレスポンスに追加するのを忘れないようにすること
|
||||||
|
|
||||||
@Column('enum', {
|
@Column('enum', {
|
||||||
|
@@ -115,6 +115,10 @@ export const packedMetaLiteSchema = {
|
|||||||
type: 'string',
|
type: 'string',
|
||||||
optional: false, nullable: true,
|
optional: false, nullable: true,
|
||||||
},
|
},
|
||||||
|
enableTestcaptcha: {
|
||||||
|
type: 'boolean',
|
||||||
|
optional: false, nullable: false,
|
||||||
|
},
|
||||||
swPublickey: {
|
swPublickey: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
optional: false, nullable: true,
|
optional: false, nullable: true,
|
||||||
|
@@ -6,6 +6,7 @@
|
|||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
import { CoreModule } from '@/core/CoreModule.js';
|
import { CoreModule } from '@/core/CoreModule.js';
|
||||||
import { GlobalModule } from '@/GlobalModule.js';
|
import { GlobalModule } from '@/GlobalModule.js';
|
||||||
|
import { CheckModeratorsActivityProcessorService } from '@/queue/processors/CheckModeratorsActivityProcessorService.js';
|
||||||
import { QueueLoggerService } from './QueueLoggerService.js';
|
import { QueueLoggerService } from './QueueLoggerService.js';
|
||||||
import { QueueProcessorService } from './QueueProcessorService.js';
|
import { QueueProcessorService } from './QueueProcessorService.js';
|
||||||
import { DeliverProcessorService } from './processors/DeliverProcessorService.js';
|
import { DeliverProcessorService } from './processors/DeliverProcessorService.js';
|
||||||
@@ -80,6 +81,8 @@ import { RelationshipProcessorService } from './processors/RelationshipProcessor
|
|||||||
DeliverProcessorService,
|
DeliverProcessorService,
|
||||||
InboxProcessorService,
|
InboxProcessorService,
|
||||||
AggregateRetentionProcessorService,
|
AggregateRetentionProcessorService,
|
||||||
|
CheckExpiredMutingsProcessorService,
|
||||||
|
CheckModeratorsActivityProcessorService,
|
||||||
QueueProcessorService,
|
QueueProcessorService,
|
||||||
],
|
],
|
||||||
exports: [
|
exports: [
|
||||||
|
@@ -10,6 +10,7 @@ import type { Config } from '@/config.js';
|
|||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
import type Logger from '@/logger.js';
|
import type Logger from '@/logger.js';
|
||||||
import { bindThis } from '@/decorators.js';
|
import { bindThis } from '@/decorators.js';
|
||||||
|
import { CheckModeratorsActivityProcessorService } from '@/queue/processors/CheckModeratorsActivityProcessorService.js';
|
||||||
import { UserWebhookDeliverProcessorService } from './processors/UserWebhookDeliverProcessorService.js';
|
import { UserWebhookDeliverProcessorService } from './processors/UserWebhookDeliverProcessorService.js';
|
||||||
import { SystemWebhookDeliverProcessorService } from './processors/SystemWebhookDeliverProcessorService.js';
|
import { SystemWebhookDeliverProcessorService } from './processors/SystemWebhookDeliverProcessorService.js';
|
||||||
import { EndedPollNotificationProcessorService } from './processors/EndedPollNotificationProcessorService.js';
|
import { EndedPollNotificationProcessorService } from './processors/EndedPollNotificationProcessorService.js';
|
||||||
@@ -120,6 +121,7 @@ export class QueueProcessorService implements OnApplicationShutdown {
|
|||||||
private aggregateRetentionProcessorService: AggregateRetentionProcessorService,
|
private aggregateRetentionProcessorService: AggregateRetentionProcessorService,
|
||||||
private checkExpiredMutingsProcessorService: CheckExpiredMutingsProcessorService,
|
private checkExpiredMutingsProcessorService: CheckExpiredMutingsProcessorService,
|
||||||
private bakeBufferedReactionsProcessorService: BakeBufferedReactionsProcessorService,
|
private bakeBufferedReactionsProcessorService: BakeBufferedReactionsProcessorService,
|
||||||
|
private checkModeratorsActivityProcessorService: CheckModeratorsActivityProcessorService,
|
||||||
private cleanProcessorService: CleanProcessorService,
|
private cleanProcessorService: CleanProcessorService,
|
||||||
) {
|
) {
|
||||||
this.logger = this.queueLoggerService.logger;
|
this.logger = this.queueLoggerService.logger;
|
||||||
@@ -150,6 +152,7 @@ export class QueueProcessorService implements OnApplicationShutdown {
|
|||||||
case 'aggregateRetention': return this.aggregateRetentionProcessorService.process();
|
case 'aggregateRetention': return this.aggregateRetentionProcessorService.process();
|
||||||
case 'checkExpiredMutings': return this.checkExpiredMutingsProcessorService.process();
|
case 'checkExpiredMutings': return this.checkExpiredMutingsProcessorService.process();
|
||||||
case 'bakeBufferedReactions': return this.bakeBufferedReactionsProcessorService.process();
|
case 'bakeBufferedReactions': return this.bakeBufferedReactionsProcessorService.process();
|
||||||
|
case 'checkModeratorsActivity': return this.checkModeratorsActivityProcessorService.process();
|
||||||
case 'clean': return this.cleanProcessorService.process();
|
case 'clean': return this.cleanProcessorService.process();
|
||||||
default: throw new Error(`unrecognized job type ${job.name} for system`);
|
default: throw new Error(`unrecognized job type ${job.name} for system`);
|
||||||
}
|
}
|
||||||
|
@@ -0,0 +1,127 @@
|
|||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import type Logger from '@/logger.js';
|
||||||
|
import { bindThis } from '@/decorators.js';
|
||||||
|
import { MetaService } from '@/core/MetaService.js';
|
||||||
|
import { RoleService } from '@/core/RoleService.js';
|
||||||
|
import { QueueLoggerService } from '../QueueLoggerService.js';
|
||||||
|
|
||||||
|
// モデレーターが不在と判断する日付の閾値
|
||||||
|
const MODERATOR_INACTIVITY_LIMIT_DAYS = 7;
|
||||||
|
const ONE_DAY_MILLI_SEC = 1000 * 60 * 60 * 24;
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class CheckModeratorsActivityProcessorService {
|
||||||
|
private logger: Logger;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private metaService: MetaService,
|
||||||
|
private roleService: RoleService,
|
||||||
|
private queueLoggerService: QueueLoggerService,
|
||||||
|
) {
|
||||||
|
this.logger = this.queueLoggerService.logger.createSubLogger('check-moderators-activity');
|
||||||
|
}
|
||||||
|
|
||||||
|
@bindThis
|
||||||
|
public async process(): Promise<void> {
|
||||||
|
this.logger.info('start.');
|
||||||
|
|
||||||
|
const meta = await this.metaService.fetch(false);
|
||||||
|
if (!meta.disableRegistration) {
|
||||||
|
await this.processImpl();
|
||||||
|
} else {
|
||||||
|
this.logger.info('is already invitation only.');
|
||||||
|
}
|
||||||
|
|
||||||
|
this.logger.succ('finish.');
|
||||||
|
}
|
||||||
|
|
||||||
|
@bindThis
|
||||||
|
private async processImpl() {
|
||||||
|
const { isModeratorsInactive, inactivityLimitCountdown } = await this.evaluateModeratorsInactiveDays();
|
||||||
|
if (isModeratorsInactive) {
|
||||||
|
this.logger.warn(`The moderator has been inactive for ${MODERATOR_INACTIVITY_LIMIT_DAYS} days. We will move to invitation only.`);
|
||||||
|
await this.changeToInvitationOnly();
|
||||||
|
|
||||||
|
// TODO: モデレータに通知メール+Misskey通知
|
||||||
|
// TODO: SystemWebhook通知
|
||||||
|
} else {
|
||||||
|
if (inactivityLimitCountdown <= 2) {
|
||||||
|
this.logger.warn(`A moderator has been inactive for a period of time. If you are inactive for an additional ${inactivityLimitCountdown} days, it will switch to invitation only.`);
|
||||||
|
|
||||||
|
// TODO: 警告メール
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* モデレーターが不在であるかどうかを確認する。trueの場合はモデレーターが不在である。
|
||||||
|
* isModerator, isAdministrator, isRootのいずれかがtrueのユーザを対象に、
|
||||||
|
* {@link MiUser.lastActiveDate}の値が実行日時の{@link MODERATOR_INACTIVITY_LIMIT_DAYS}日前よりも古いユーザがいるかどうかを確認する。
|
||||||
|
* {@link MiUser.lastActiveDate}がnullの場合は、そのユーザは確認の対象外とする。
|
||||||
|
*
|
||||||
|
* -----
|
||||||
|
*
|
||||||
|
* ### サンプルパターン
|
||||||
|
* - 実行日時: 2022-01-30 12:00:00
|
||||||
|
* - 判定基準: 2022-01-23 12:00:00(実行日時の{@link MODERATOR_INACTIVITY_LIMIT_DAYS}日前)
|
||||||
|
*
|
||||||
|
* #### パターン①
|
||||||
|
* - モデレータA: lastActiveDate = 2022-01-20 00:00:00 ※アウト
|
||||||
|
* - モデレータB: lastActiveDate = 2022-01-23 12:00:00 ※セーフ(判定基準と同値なのでギリギリ残り0日)
|
||||||
|
* - モデレータC: lastActiveDate = 2022-01-23 11:59:59 ※アウト(残り-1日)
|
||||||
|
* - モデレータD: lastActiveDate = null
|
||||||
|
*
|
||||||
|
* この場合、モデレータBのアクティビティのみ判定基準日よりも古くないため、モデレーターが在席と判断される。
|
||||||
|
*
|
||||||
|
* #### パターン②
|
||||||
|
* - モデレータA: lastActiveDate = 2022-01-20 00:00:00 ※アウト
|
||||||
|
* - モデレータB: lastActiveDate = 2022-01-22 12:00:00 ※アウト(残り-1日)
|
||||||
|
* - モデレータC: lastActiveDate = 2022-01-23 11:59:59 ※アウト(残り-1日)
|
||||||
|
* - モデレータD: lastActiveDate = null
|
||||||
|
*
|
||||||
|
* この場合、モデレータA, B, Cのアクティビティは判定基準日よりも古いため、モデレーターが不在と判断される。
|
||||||
|
*/
|
||||||
|
@bindThis
|
||||||
|
public async evaluateModeratorsInactiveDays() {
|
||||||
|
const today = new Date();
|
||||||
|
const inactivePeriod = new Date(today);
|
||||||
|
inactivePeriod.setDate(today.getDate() - MODERATOR_INACTIVITY_LIMIT_DAYS);
|
||||||
|
|
||||||
|
const moderators = await this.fetchModerators()
|
||||||
|
.then(it => it.filter(it => it.lastActiveDate != null));
|
||||||
|
const inactiveModerators = moderators
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||||
|
.filter(it => it.lastActiveDate!.getTime() < inactivePeriod.getTime());
|
||||||
|
|
||||||
|
// 残りの猶予を示したいので、最終アクティブ日時が一番若いモデレータの日数を基準に猶予を計算する
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||||
|
const newestLastActiveDate = new Date(Math.max(...moderators.map(it => it.lastActiveDate!.getTime())));
|
||||||
|
const inactivityLimitCountdown = Math.floor((newestLastActiveDate.getTime() - inactivePeriod.getTime()) / ONE_DAY_MILLI_SEC);
|
||||||
|
|
||||||
|
return {
|
||||||
|
isModeratorsInactive: inactiveModerators.length === moderators.length,
|
||||||
|
inactiveModerators,
|
||||||
|
inactivityLimitCountdown,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@bindThis
|
||||||
|
private async changeToInvitationOnly() {
|
||||||
|
await this.metaService.update({ disableRegistration: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
@bindThis
|
||||||
|
private async fetchModerators() {
|
||||||
|
// TODO: モデレーター以外にも特別な権限を持つユーザーがいる場合は考慮する
|
||||||
|
return this.roleService.getModerators({
|
||||||
|
includeAdmins: true,
|
||||||
|
includeRoot: true,
|
||||||
|
excludeExpire: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
@@ -119,6 +119,7 @@ export class ApiServerService {
|
|||||||
'g-recaptcha-response'?: string;
|
'g-recaptcha-response'?: string;
|
||||||
'turnstile-response'?: string;
|
'turnstile-response'?: string;
|
||||||
'm-captcha-response'?: string;
|
'm-captcha-response'?: string;
|
||||||
|
'testcaptcha-response'?: string;
|
||||||
}
|
}
|
||||||
}>('/signup', (request, reply) => this.signupApiService.signup(request, reply));
|
}>('/signup', (request, reply) => this.signupApiService.signup(request, reply));
|
||||||
|
|
||||||
@@ -132,6 +133,7 @@ export class ApiServerService {
|
|||||||
'g-recaptcha-response'?: string;
|
'g-recaptcha-response'?: string;
|
||||||
'turnstile-response'?: string;
|
'turnstile-response'?: string;
|
||||||
'm-captcha-response'?: string;
|
'm-captcha-response'?: string;
|
||||||
|
'testcaptcha-response'?: string;
|
||||||
};
|
};
|
||||||
}>('/signin-flow', (request, reply) => this.signinApiService.signin(request, reply));
|
}>('/signin-flow', (request, reply) => this.signinApiService.signin(request, reply));
|
||||||
|
|
||||||
|
@@ -71,6 +71,7 @@ export class SigninApiService {
|
|||||||
'g-recaptcha-response'?: string;
|
'g-recaptcha-response'?: string;
|
||||||
'turnstile-response'?: string;
|
'turnstile-response'?: string;
|
||||||
'm-captcha-response'?: string;
|
'm-captcha-response'?: string;
|
||||||
|
'testcaptcha-response'?: string;
|
||||||
};
|
};
|
||||||
}>,
|
}>,
|
||||||
reply: FastifyReply,
|
reply: FastifyReply,
|
||||||
@@ -194,6 +195,12 @@ export class SigninApiService {
|
|||||||
throw new FastifyReplyError(400, err);
|
throw new FastifyReplyError(400, err);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.meta.enableTestcaptcha) {
|
||||||
|
await this.captchaService.verifyTestcaptcha(body['testcaptcha-response']).catch(err => {
|
||||||
|
throw new FastifyReplyError(400, err);
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (same) {
|
if (same) {
|
||||||
|
@@ -67,6 +67,7 @@ export class SignupApiService {
|
|||||||
'g-recaptcha-response'?: string;
|
'g-recaptcha-response'?: string;
|
||||||
'turnstile-response'?: string;
|
'turnstile-response'?: string;
|
||||||
'm-captcha-response'?: string;
|
'm-captcha-response'?: string;
|
||||||
|
'testcaptcha-response'?: string;
|
||||||
}
|
}
|
||||||
}>,
|
}>,
|
||||||
reply: FastifyReply,
|
reply: FastifyReply,
|
||||||
@@ -99,6 +100,12 @@ export class SignupApiService {
|
|||||||
throw new FastifyReplyError(400, err);
|
throw new FastifyReplyError(400, err);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.meta.enableTestcaptcha) {
|
||||||
|
await this.captchaService.verifyTestcaptcha(body['testcaptcha-response']).catch(err => {
|
||||||
|
throw new FastifyReplyError(400, err);
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const username = body['username'];
|
const username = body['username'];
|
||||||
|
@@ -6,7 +6,7 @@
|
|||||||
import { Inject, Injectable } from '@nestjs/common';
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||||
import { CustomEmojiService } from '@/core/CustomEmojiService.js';
|
import { CustomEmojiService } from '@/core/CustomEmojiService.js';
|
||||||
import type { DriveFilesRepository } from '@/models/_.js';
|
import type { DriveFilesRepository, MiEmoji } from '@/models/_.js';
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
import { ApiError } from '../../../error.js';
|
import { ApiError } from '../../../error.js';
|
||||||
|
|
||||||
@@ -78,25 +78,14 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||||||
if (driveFile == null) throw new ApiError(meta.errors.noSuchFile);
|
if (driveFile == null) throw new ApiError(meta.errors.noSuchFile);
|
||||||
}
|
}
|
||||||
|
|
||||||
let emojiId;
|
// JSON schemeのanyOfの型変換がうまくいっていないらしい
|
||||||
if (ps.id) {
|
const required = { id: ps.id, name: ps.name } as
|
||||||
emojiId = ps.id;
|
| { id: MiEmoji['id']; name?: string }
|
||||||
const emoji = await this.customEmojiService.getEmojiById(ps.id);
|
| { id?: MiEmoji['id']; name: string };
|
||||||
if (!emoji) throw new ApiError(meta.errors.noSuchEmoji);
|
|
||||||
if (ps.name && (ps.name !== emoji.name)) {
|
|
||||||
const isDuplicate = await this.customEmojiService.checkDuplicate(ps.name);
|
|
||||||
if (isDuplicate) throw new ApiError(meta.errors.sameNameEmojiExists);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (!ps.name) throw new Error('Invalid Params unexpectedly passed. This is a BUG. Please report it to the development team.');
|
|
||||||
const emoji = await this.customEmojiService.getEmojiByName(ps.name);
|
|
||||||
if (!emoji) throw new ApiError(meta.errors.noSuchEmoji);
|
|
||||||
emojiId = emoji.id;
|
|
||||||
}
|
|
||||||
|
|
||||||
await this.customEmojiService.update(emojiId, {
|
const error = await this.customEmojiService.update({
|
||||||
|
...required,
|
||||||
driveFile,
|
driveFile,
|
||||||
name: ps.name,
|
|
||||||
category: ps.category,
|
category: ps.category,
|
||||||
aliases: ps.aliases,
|
aliases: ps.aliases,
|
||||||
license: ps.license,
|
license: ps.license,
|
||||||
@@ -104,6 +93,14 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||||||
localOnly: ps.localOnly,
|
localOnly: ps.localOnly,
|
||||||
roleIdsThatCanBeUsedThisEmojiAsReaction: ps.roleIdsThatCanBeUsedThisEmojiAsReaction,
|
roleIdsThatCanBeUsedThisEmojiAsReaction: ps.roleIdsThatCanBeUsedThisEmojiAsReaction,
|
||||||
}, me);
|
}, me);
|
||||||
|
|
||||||
|
switch (error) {
|
||||||
|
case null: return;
|
||||||
|
case 'NO_SUCH_EMOJI': throw new ApiError(meta.errors.noSuchEmoji);
|
||||||
|
case 'SAME_NAME_EMOJI_EXISTS': throw new ApiError(meta.errors.sameNameEmojiExists);
|
||||||
|
}
|
||||||
|
// 網羅性チェック
|
||||||
|
const mustBeNever: never = error;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -69,6 +69,10 @@ export const meta = {
|
|||||||
type: 'string',
|
type: 'string',
|
||||||
optional: false, nullable: true,
|
optional: false, nullable: true,
|
||||||
},
|
},
|
||||||
|
enableTestcaptcha: {
|
||||||
|
type: 'boolean',
|
||||||
|
optional: false, nullable: false,
|
||||||
|
},
|
||||||
swPublickey: {
|
swPublickey: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
optional: false, nullable: true,
|
optional: false, nullable: true,
|
||||||
@@ -555,6 +559,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||||||
recaptchaSiteKey: instance.recaptchaSiteKey,
|
recaptchaSiteKey: instance.recaptchaSiteKey,
|
||||||
enableTurnstile: instance.enableTurnstile,
|
enableTurnstile: instance.enableTurnstile,
|
||||||
turnstileSiteKey: instance.turnstileSiteKey,
|
turnstileSiteKey: instance.turnstileSiteKey,
|
||||||
|
enableTestcaptcha: instance.enableTestcaptcha,
|
||||||
swPublickey: instance.swPublicKey,
|
swPublickey: instance.swPublicKey,
|
||||||
themeColor: instance.themeColor,
|
themeColor: instance.themeColor,
|
||||||
mascotImageUrl: instance.mascotImageUrl,
|
mascotImageUrl: instance.mascotImageUrl,
|
||||||
|
@@ -71,13 +71,13 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case 'moderator': {
|
case 'moderator': {
|
||||||
const moderatorIds = await this.roleService.getModeratorIds(false);
|
const moderatorIds = await this.roleService.getModeratorIds({ includeAdmins: false });
|
||||||
if (moderatorIds.length === 0) return [];
|
if (moderatorIds.length === 0) return [];
|
||||||
query.where('user.id IN (:...moderatorIds)', { moderatorIds: moderatorIds });
|
query.where('user.id IN (:...moderatorIds)', { moderatorIds: moderatorIds });
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case 'adminOrModerator': {
|
case 'adminOrModerator': {
|
||||||
const adminOrModeratorIds = await this.roleService.getModeratorIds();
|
const adminOrModeratorIds = await this.roleService.getModeratorIds({ includeAdmins: true });
|
||||||
if (adminOrModeratorIds.length === 0) return [];
|
if (adminOrModeratorIds.length === 0) return [];
|
||||||
query.where('user.id IN (:...adminOrModeratorIds)', { adminOrModeratorIds: adminOrModeratorIds });
|
query.where('user.id IN (:...adminOrModeratorIds)', { adminOrModeratorIds: adminOrModeratorIds });
|
||||||
break;
|
break;
|
||||||
|
@@ -78,6 +78,7 @@ export const paramDef = {
|
|||||||
enableTurnstile: { type: 'boolean' },
|
enableTurnstile: { type: 'boolean' },
|
||||||
turnstileSiteKey: { type: 'string', nullable: true },
|
turnstileSiteKey: { type: 'string', nullable: true },
|
||||||
turnstileSecretKey: { type: 'string', nullable: true },
|
turnstileSecretKey: { type: 'string', nullable: true },
|
||||||
|
enableTestcaptcha: { type: 'boolean' },
|
||||||
sensitiveMediaDetection: { type: 'string', enum: ['none', 'all', 'local', 'remote'] },
|
sensitiveMediaDetection: { type: 'string', enum: ['none', 'all', 'local', 'remote'] },
|
||||||
sensitiveMediaDetectionSensitivity: { type: 'string', enum: ['medium', 'low', 'high', 'veryLow', 'veryHigh'] },
|
sensitiveMediaDetectionSensitivity: { type: 'string', enum: ['medium', 'low', 'high', 'veryLow', 'veryHigh'] },
|
||||||
setSensitiveFlagAutomatically: { type: 'boolean' },
|
setSensitiveFlagAutomatically: { type: 'boolean' },
|
||||||
@@ -357,6 +358,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||||||
set.turnstileSecretKey = ps.turnstileSecretKey;
|
set.turnstileSecretKey = ps.turnstileSecretKey;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (ps.enableTestcaptcha !== undefined) {
|
||||||
|
set.enableTestcaptcha = ps.enableTestcaptcha;
|
||||||
|
}
|
||||||
|
|
||||||
if (ps.sensitiveMediaDetection !== undefined) {
|
if (ps.sensitiveMediaDetection !== undefined) {
|
||||||
set.sensitiveMediaDetection = ps.sensitiveMediaDetection;
|
set.sensitiveMediaDetection = ps.sensitiveMediaDetection;
|
||||||
}
|
}
|
||||||
|
@@ -10,6 +10,8 @@ import { jest } from '@jest/globals';
|
|||||||
import { ModuleMocker } from 'jest-mock';
|
import { ModuleMocker } from 'jest-mock';
|
||||||
import { Test } from '@nestjs/testing';
|
import { Test } from '@nestjs/testing';
|
||||||
import * as lolex from '@sinonjs/fake-timers';
|
import * as lolex from '@sinonjs/fake-timers';
|
||||||
|
import type { TestingModule } from '@nestjs/testing';
|
||||||
|
import type { MockFunctionMetadata } from 'jest-mock';
|
||||||
import { GlobalModule } from '@/GlobalModule.js';
|
import { GlobalModule } from '@/GlobalModule.js';
|
||||||
import { RoleService } from '@/core/RoleService.js';
|
import { RoleService } from '@/core/RoleService.js';
|
||||||
import {
|
import {
|
||||||
@@ -31,8 +33,6 @@ import { secureRndstr } from '@/misc/secure-rndstr.js';
|
|||||||
import { NotificationService } from '@/core/NotificationService.js';
|
import { NotificationService } from '@/core/NotificationService.js';
|
||||||
import { RoleCondFormulaValue } from '@/models/Role.js';
|
import { RoleCondFormulaValue } from '@/models/Role.js';
|
||||||
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
||||||
import type { TestingModule } from '@nestjs/testing';
|
|
||||||
import type { MockFunctionMetadata } from 'jest-mock';
|
|
||||||
|
|
||||||
const moduleMocker = new ModuleMocker(global);
|
const moduleMocker = new ModuleMocker(global);
|
||||||
|
|
||||||
@@ -277,9 +277,9 @@ describe('RoleService', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('getModeratorIds', () => {
|
describe('getModeratorIds', () => {
|
||||||
test('includeAdmins = false, excludeExpire = false', async () => {
|
test('includeAdmins = false, includeRoot = false, excludeExpire = false', async () => {
|
||||||
const [adminUser1, adminUser2, modeUser1, modeUser2, normalUser1, normalUser2] = await Promise.all([
|
const [adminUser1, adminUser2, modeUser1, modeUser2, normalUser1, normalUser2, rootUser] = await Promise.all([
|
||||||
createUser(), createUser(), createUser(), createUser(), createUser(), createUser(),
|
createUser(), createUser(), createUser(), createUser(), createUser(), createUser(), createUser({ isRoot: true }),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const role1 = await createRole({ name: 'admin', isAdministrator: true });
|
const role1 = await createRole({ name: 'admin', isAdministrator: true });
|
||||||
@@ -295,13 +295,17 @@ describe('RoleService', () => {
|
|||||||
assignRole({ userId: normalUser2.id, roleId: role3.id, expiresAt: new Date(Date.now() - 1000) }),
|
assignRole({ userId: normalUser2.id, roleId: role3.id, expiresAt: new Date(Date.now() - 1000) }),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const result = await roleService.getModeratorIds(false, false);
|
const result = await roleService.getModeratorIds({
|
||||||
|
includeAdmins: false,
|
||||||
|
includeRoot: false,
|
||||||
|
excludeExpire: false,
|
||||||
|
});
|
||||||
expect(result).toEqual([modeUser1.id, modeUser2.id]);
|
expect(result).toEqual([modeUser1.id, modeUser2.id]);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('includeAdmins = false, excludeExpire = true', async () => {
|
test('includeAdmins = false, includeRoot = false, excludeExpire = true', async () => {
|
||||||
const [adminUser1, adminUser2, modeUser1, modeUser2, normalUser1, normalUser2] = await Promise.all([
|
const [adminUser1, adminUser2, modeUser1, modeUser2, normalUser1, normalUser2, rootUser] = await Promise.all([
|
||||||
createUser(), createUser(), createUser(), createUser(), createUser(), createUser(),
|
createUser(), createUser(), createUser(), createUser(), createUser(), createUser(), createUser({ isRoot: true }),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const role1 = await createRole({ name: 'admin', isAdministrator: true });
|
const role1 = await createRole({ name: 'admin', isAdministrator: true });
|
||||||
@@ -317,13 +321,17 @@ describe('RoleService', () => {
|
|||||||
assignRole({ userId: normalUser2.id, roleId: role3.id, expiresAt: new Date(Date.now() - 1000) }),
|
assignRole({ userId: normalUser2.id, roleId: role3.id, expiresAt: new Date(Date.now() - 1000) }),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const result = await roleService.getModeratorIds(false, true);
|
const result = await roleService.getModeratorIds({
|
||||||
|
includeAdmins: false,
|
||||||
|
includeRoot: false,
|
||||||
|
excludeExpire: true,
|
||||||
|
});
|
||||||
expect(result).toEqual([modeUser1.id]);
|
expect(result).toEqual([modeUser1.id]);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('includeAdmins = true, excludeExpire = false', async () => {
|
test('includeAdmins = true, includeRoot = false, excludeExpire = false', async () => {
|
||||||
const [adminUser1, adminUser2, modeUser1, modeUser2, normalUser1, normalUser2] = await Promise.all([
|
const [adminUser1, adminUser2, modeUser1, modeUser2, normalUser1, normalUser2, rootUser] = await Promise.all([
|
||||||
createUser(), createUser(), createUser(), createUser(), createUser(), createUser(),
|
createUser(), createUser(), createUser(), createUser(), createUser(), createUser(), createUser({ isRoot: true }),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const role1 = await createRole({ name: 'admin', isAdministrator: true });
|
const role1 = await createRole({ name: 'admin', isAdministrator: true });
|
||||||
@@ -339,13 +347,17 @@ describe('RoleService', () => {
|
|||||||
assignRole({ userId: normalUser2.id, roleId: role3.id, expiresAt: new Date(Date.now() - 1000) }),
|
assignRole({ userId: normalUser2.id, roleId: role3.id, expiresAt: new Date(Date.now() - 1000) }),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const result = await roleService.getModeratorIds(true, false);
|
const result = await roleService.getModeratorIds({
|
||||||
|
includeAdmins: true,
|
||||||
|
includeRoot: false,
|
||||||
|
excludeExpire: false,
|
||||||
|
});
|
||||||
expect(result).toEqual([adminUser1.id, adminUser2.id, modeUser1.id, modeUser2.id]);
|
expect(result).toEqual([adminUser1.id, adminUser2.id, modeUser1.id, modeUser2.id]);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('includeAdmins = true, excludeExpire = true', async () => {
|
test('includeAdmins = true, includeRoot = false, excludeExpire = true', async () => {
|
||||||
const [adminUser1, adminUser2, modeUser1, modeUser2, normalUser1, normalUser2] = await Promise.all([
|
const [adminUser1, adminUser2, modeUser1, modeUser2, normalUser1, normalUser2, rootUser] = await Promise.all([
|
||||||
createUser(), createUser(), createUser(), createUser(), createUser(), createUser(),
|
createUser(), createUser(), createUser(), createUser(), createUser(), createUser(), createUser({ isRoot: true }),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const role1 = await createRole({ name: 'admin', isAdministrator: true });
|
const role1 = await createRole({ name: 'admin', isAdministrator: true });
|
||||||
@@ -361,9 +373,111 @@ describe('RoleService', () => {
|
|||||||
assignRole({ userId: normalUser2.id, roleId: role3.id, expiresAt: new Date(Date.now() - 1000) }),
|
assignRole({ userId: normalUser2.id, roleId: role3.id, expiresAt: new Date(Date.now() - 1000) }),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const result = await roleService.getModeratorIds(true, true);
|
const result = await roleService.getModeratorIds({
|
||||||
|
includeAdmins: true,
|
||||||
|
includeRoot: false,
|
||||||
|
excludeExpire: true,
|
||||||
|
});
|
||||||
expect(result).toEqual([adminUser1.id, modeUser1.id]);
|
expect(result).toEqual([adminUser1.id, modeUser1.id]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('includeAdmins = false, includeRoot = true, excludeExpire = false', async () => {
|
||||||
|
const [adminUser1, adminUser2, modeUser1, modeUser2, normalUser1, normalUser2, rootUser] = await Promise.all([
|
||||||
|
createUser(), createUser(), createUser(), createUser(), createUser(), createUser(), createUser({ isRoot: true }),
|
||||||
|
]);
|
||||||
|
|
||||||
|
const role1 = await createRole({ name: 'admin', isAdministrator: true });
|
||||||
|
const role2 = await createRole({ name: 'moderator', isModerator: true });
|
||||||
|
const role3 = await createRole({ name: 'normal' });
|
||||||
|
|
||||||
|
await Promise.all([
|
||||||
|
assignRole({ userId: adminUser1.id, roleId: role1.id }),
|
||||||
|
assignRole({ userId: adminUser2.id, roleId: role1.id, expiresAt: new Date(Date.now() - 1000) }),
|
||||||
|
assignRole({ userId: modeUser1.id, roleId: role2.id }),
|
||||||
|
assignRole({ userId: modeUser2.id, roleId: role2.id, expiresAt: new Date(Date.now() - 1000) }),
|
||||||
|
assignRole({ userId: normalUser1.id, roleId: role3.id }),
|
||||||
|
assignRole({ userId: normalUser2.id, roleId: role3.id, expiresAt: new Date(Date.now() - 1000) }),
|
||||||
|
]);
|
||||||
|
|
||||||
|
const result = await roleService.getModeratorIds({
|
||||||
|
includeAdmins: false,
|
||||||
|
includeRoot: true,
|
||||||
|
excludeExpire: false,
|
||||||
|
});
|
||||||
|
expect(result).toEqual([modeUser1.id, modeUser2.id, rootUser.id]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('root has moderator role', async () => {
|
||||||
|
const [adminUser1, modeUser1, normalUser1, rootUser] = await Promise.all([
|
||||||
|
createUser(), createUser(), createUser(), createUser({ isRoot: true }),
|
||||||
|
]);
|
||||||
|
|
||||||
|
const role1 = await createRole({ name: 'admin', isAdministrator: true });
|
||||||
|
const role2 = await createRole({ name: 'moderator', isModerator: true });
|
||||||
|
const role3 = await createRole({ name: 'normal' });
|
||||||
|
|
||||||
|
await Promise.all([
|
||||||
|
assignRole({ userId: adminUser1.id, roleId: role1.id }),
|
||||||
|
assignRole({ userId: modeUser1.id, roleId: role2.id }),
|
||||||
|
assignRole({ userId: rootUser.id, roleId: role2.id }),
|
||||||
|
assignRole({ userId: normalUser1.id, roleId: role3.id }),
|
||||||
|
]);
|
||||||
|
|
||||||
|
const result = await roleService.getModeratorIds({
|
||||||
|
includeAdmins: false,
|
||||||
|
includeRoot: true,
|
||||||
|
excludeExpire: false,
|
||||||
|
});
|
||||||
|
expect(result).toEqual([modeUser1.id, rootUser.id]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('root has administrator role', async () => {
|
||||||
|
const [adminUser1, modeUser1, normalUser1, rootUser] = await Promise.all([
|
||||||
|
createUser(), createUser(), createUser(), createUser({ isRoot: true }),
|
||||||
|
]);
|
||||||
|
|
||||||
|
const role1 = await createRole({ name: 'admin', isAdministrator: true });
|
||||||
|
const role2 = await createRole({ name: 'moderator', isModerator: true });
|
||||||
|
const role3 = await createRole({ name: 'normal' });
|
||||||
|
|
||||||
|
await Promise.all([
|
||||||
|
assignRole({ userId: adminUser1.id, roleId: role1.id }),
|
||||||
|
assignRole({ userId: rootUser.id, roleId: role1.id }),
|
||||||
|
assignRole({ userId: modeUser1.id, roleId: role2.id }),
|
||||||
|
assignRole({ userId: normalUser1.id, roleId: role3.id }),
|
||||||
|
]);
|
||||||
|
|
||||||
|
const result = await roleService.getModeratorIds({
|
||||||
|
includeAdmins: true,
|
||||||
|
includeRoot: true,
|
||||||
|
excludeExpire: false,
|
||||||
|
});
|
||||||
|
expect(result).toEqual([adminUser1.id, modeUser1.id, rootUser.id]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('root has moderator role(expire)', async () => {
|
||||||
|
const [adminUser1, modeUser1, normalUser1, rootUser] = await Promise.all([
|
||||||
|
createUser(), createUser(), createUser(), createUser({ isRoot: true }),
|
||||||
|
]);
|
||||||
|
|
||||||
|
const role1 = await createRole({ name: 'admin', isAdministrator: true });
|
||||||
|
const role2 = await createRole({ name: 'moderator', isModerator: true });
|
||||||
|
const role3 = await createRole({ name: 'normal' });
|
||||||
|
|
||||||
|
await Promise.all([
|
||||||
|
assignRole({ userId: adminUser1.id, roleId: role1.id }),
|
||||||
|
assignRole({ userId: modeUser1.id, roleId: role2.id, expiresAt: new Date(Date.now() - 1000) }),
|
||||||
|
assignRole({ userId: rootUser.id, roleId: role2.id, expiresAt: new Date(Date.now() - 1000) }),
|
||||||
|
assignRole({ userId: normalUser1.id, roleId: role3.id }),
|
||||||
|
]);
|
||||||
|
|
||||||
|
const result = await roleService.getModeratorIds({
|
||||||
|
includeAdmins: false,
|
||||||
|
includeRoot: true,
|
||||||
|
excludeExpire: true,
|
||||||
|
});
|
||||||
|
expect(result).toEqual([rootUser.id]);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('conditional role', () => {
|
describe('conditional role', () => {
|
||||||
|
@@ -0,0 +1,235 @@
|
|||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { jest } from '@jest/globals';
|
||||||
|
import { Test, TestingModule } from '@nestjs/testing';
|
||||||
|
import * as lolex from '@sinonjs/fake-timers';
|
||||||
|
import { addHours, addSeconds, subDays, subHours, subSeconds } from 'date-fns';
|
||||||
|
import { CheckModeratorsActivityProcessorService } from '@/queue/processors/CheckModeratorsActivityProcessorService.js';
|
||||||
|
import { MiUser, UserProfilesRepository, UsersRepository } from '@/models/_.js';
|
||||||
|
import { IdService } from '@/core/IdService.js';
|
||||||
|
import { RoleService } from '@/core/RoleService.js';
|
||||||
|
import { GlobalModule } from '@/GlobalModule.js';
|
||||||
|
import { MetaService } from '@/core/MetaService.js';
|
||||||
|
import { DI } from '@/di-symbols.js';
|
||||||
|
import { QueueLoggerService } from '@/queue/QueueLoggerService.js';
|
||||||
|
|
||||||
|
const baseDate = new Date(Date.UTC(2000, 11, 15, 12, 0, 0));
|
||||||
|
|
||||||
|
describe('CheckModeratorsActivityProcessorService', () => {
|
||||||
|
let app: TestingModule;
|
||||||
|
let clock: lolex.InstalledClock;
|
||||||
|
let service: CheckModeratorsActivityProcessorService;
|
||||||
|
|
||||||
|
// --------------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
let usersRepository: UsersRepository;
|
||||||
|
let userProfilesRepository: UserProfilesRepository;
|
||||||
|
let idService: IdService;
|
||||||
|
let roleService: jest.Mocked<RoleService>;
|
||||||
|
|
||||||
|
// --------------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
async function createUser(data: Partial<MiUser> = {}) {
|
||||||
|
const id = idService.gen();
|
||||||
|
const user = await usersRepository
|
||||||
|
.insert({
|
||||||
|
id: id,
|
||||||
|
username: `user_${id}`,
|
||||||
|
usernameLower: `user_${id}`.toLowerCase(),
|
||||||
|
...data,
|
||||||
|
})
|
||||||
|
.then(x => usersRepository.findOneByOrFail(x.identifiers[0]));
|
||||||
|
|
||||||
|
await userProfilesRepository.insert({
|
||||||
|
userId: user.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
return user;
|
||||||
|
}
|
||||||
|
|
||||||
|
function mockModeratorRole(users: MiUser[]) {
|
||||||
|
roleService.getModerators.mockReset();
|
||||||
|
roleService.getModerators.mockResolvedValue(users);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --------------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
app = await Test
|
||||||
|
.createTestingModule({
|
||||||
|
imports: [
|
||||||
|
GlobalModule,
|
||||||
|
],
|
||||||
|
providers: [
|
||||||
|
CheckModeratorsActivityProcessorService,
|
||||||
|
IdService,
|
||||||
|
{
|
||||||
|
provide: RoleService, useFactory: () => ({ getModerators: jest.fn() }),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
provide: MetaService, useFactory: () => ({ fetch: jest.fn() }),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
provide: QueueLoggerService, useFactory: () => ({
|
||||||
|
logger: ({
|
||||||
|
createSubLogger: () => ({
|
||||||
|
info: jest.fn(),
|
||||||
|
warn: jest.fn(),
|
||||||
|
succ: jest.fn(),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
.compile();
|
||||||
|
|
||||||
|
usersRepository = app.get(DI.usersRepository);
|
||||||
|
userProfilesRepository = app.get(DI.userProfilesRepository);
|
||||||
|
|
||||||
|
service = app.get(CheckModeratorsActivityProcessorService);
|
||||||
|
idService = app.get(IdService);
|
||||||
|
roleService = app.get(RoleService) as jest.Mocked<RoleService>;
|
||||||
|
|
||||||
|
app.enableShutdownHooks();
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
clock = lolex.install({
|
||||||
|
now: new Date(baseDate),
|
||||||
|
shouldClearNativeTimers: true,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
clock.uninstall();
|
||||||
|
await usersRepository.delete({});
|
||||||
|
await userProfilesRepository.delete({});
|
||||||
|
roleService.getModerators.mockReset();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(async () => {
|
||||||
|
await app.close();
|
||||||
|
});
|
||||||
|
|
||||||
|
// --------------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
describe('evaluateModeratorsInactiveDays', () => {
|
||||||
|
test('[isModeratorsInactive] inactiveなモデレーターがいても他のモデレーターがアクティブなら"運営が非アクティブ"としてみなされない', async () => {
|
||||||
|
const [user1, user2, user3, user4] = await Promise.all([
|
||||||
|
// 期限よりも1秒新しいタイミングでアクティブ化(セーフ)
|
||||||
|
createUser({ lastActiveDate: subDays(addSeconds(baseDate, 1), 7) }),
|
||||||
|
// 期限ちょうどにアクティブ化(セーフ)
|
||||||
|
createUser({ lastActiveDate: subDays(baseDate, 7) }),
|
||||||
|
// 期限よりも1秒古いタイミングでアクティブ化(アウト)
|
||||||
|
createUser({ lastActiveDate: subDays(subSeconds(baseDate, 1), 7) }),
|
||||||
|
// 対象外
|
||||||
|
createUser({ lastActiveDate: null }),
|
||||||
|
]);
|
||||||
|
|
||||||
|
mockModeratorRole([user1, user2, user3, user4]);
|
||||||
|
|
||||||
|
const result = await service.evaluateModeratorsInactiveDays();
|
||||||
|
expect(result.isModeratorsInactive).toBe(false);
|
||||||
|
expect(result.inactiveModerators).toEqual([user3]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('[isModeratorsInactive] 全員非アクティブなら"運営が非アクティブ"としてみなされる', async () => {
|
||||||
|
const [user1, user2] = await Promise.all([
|
||||||
|
// 期限よりも1秒古いタイミングでアクティブ化(アウト)
|
||||||
|
createUser({ lastActiveDate: subDays(subSeconds(baseDate, 1), 7) }),
|
||||||
|
// 対象外
|
||||||
|
createUser({ lastActiveDate: null }),
|
||||||
|
]);
|
||||||
|
|
||||||
|
mockModeratorRole([user1, user2]);
|
||||||
|
|
||||||
|
const result = await service.evaluateModeratorsInactiveDays();
|
||||||
|
expect(result.isModeratorsInactive).toBe(true);
|
||||||
|
expect(result.inactiveModerators).toEqual([user1]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('[countdown] 猶予まで24時間ある場合、猶予1日として計算される', async () => {
|
||||||
|
const [user1, user2] = await Promise.all([
|
||||||
|
createUser({ lastActiveDate: subDays(baseDate, 8) }),
|
||||||
|
// 猶予はこのユーザ基準で計算される想定。
|
||||||
|
// 期限まで残り24時間->猶予1日として計算されるはずである
|
||||||
|
createUser({ lastActiveDate: subDays(baseDate, 6) }),
|
||||||
|
]);
|
||||||
|
|
||||||
|
mockModeratorRole([user1, user2]);
|
||||||
|
|
||||||
|
const result = await service.evaluateModeratorsInactiveDays();
|
||||||
|
expect(result.isModeratorsInactive).toBe(false);
|
||||||
|
expect(result.inactiveModerators).toEqual([user1]);
|
||||||
|
expect(result.inactivityLimitCountdown).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('[countdown] 猶予まで25時間ある場合、猶予1日として計算される', async () => {
|
||||||
|
const [user1, user2] = await Promise.all([
|
||||||
|
createUser({ lastActiveDate: subDays(baseDate, 8) }),
|
||||||
|
// 猶予はこのユーザ基準で計算される想定。
|
||||||
|
// 期限まで残り25時間->猶予1日として計算されるはずである
|
||||||
|
createUser({ lastActiveDate: subDays(addHours(baseDate, 1), 6) }),
|
||||||
|
]);
|
||||||
|
|
||||||
|
mockModeratorRole([user1, user2]);
|
||||||
|
|
||||||
|
const result = await service.evaluateModeratorsInactiveDays();
|
||||||
|
expect(result.isModeratorsInactive).toBe(false);
|
||||||
|
expect(result.inactiveModerators).toEqual([user1]);
|
||||||
|
expect(result.inactivityLimitCountdown).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('[countdown] 猶予まで23時間ある場合、猶予0日として計算される', async () => {
|
||||||
|
const [user1, user2] = await Promise.all([
|
||||||
|
createUser({ lastActiveDate: subDays(baseDate, 8) }),
|
||||||
|
// 猶予はこのユーザ基準で計算される想定。
|
||||||
|
// 期限まで残り23時間->猶予0日として計算されるはずである
|
||||||
|
createUser({ lastActiveDate: subDays(subHours(baseDate, 1), 6) }),
|
||||||
|
]);
|
||||||
|
|
||||||
|
mockModeratorRole([user1, user2]);
|
||||||
|
|
||||||
|
const result = await service.evaluateModeratorsInactiveDays();
|
||||||
|
expect(result.isModeratorsInactive).toBe(false);
|
||||||
|
expect(result.inactiveModerators).toEqual([user1]);
|
||||||
|
expect(result.inactivityLimitCountdown).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('[countdown] 期限ちょうどの場合、猶予0日として計算される', async () => {
|
||||||
|
const [user1, user2] = await Promise.all([
|
||||||
|
createUser({ lastActiveDate: subDays(baseDate, 8) }),
|
||||||
|
// 猶予はこのユーザ基準で計算される想定。
|
||||||
|
// 期限ちょうど->猶予0日として計算されるはずである
|
||||||
|
createUser({ lastActiveDate: subDays(baseDate, 7) }),
|
||||||
|
]);
|
||||||
|
|
||||||
|
mockModeratorRole([user1, user2]);
|
||||||
|
|
||||||
|
const result = await service.evaluateModeratorsInactiveDays();
|
||||||
|
expect(result.isModeratorsInactive).toBe(false);
|
||||||
|
expect(result.inactiveModerators).toEqual([user1]);
|
||||||
|
expect(result.inactivityLimitCountdown).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('[countdown] 期限より1時間超過している場合、猶予-1日として計算される', async () => {
|
||||||
|
const [user1, user2] = await Promise.all([
|
||||||
|
createUser({ lastActiveDate: subDays(baseDate, 8) }),
|
||||||
|
// 猶予はこのユーザ基準で計算される想定。
|
||||||
|
// 期限より1時間超過->猶予-1日として計算されるはずである
|
||||||
|
createUser({ lastActiveDate: subDays(subHours(baseDate, 1), 7) }),
|
||||||
|
]);
|
||||||
|
|
||||||
|
mockModeratorRole([user1, user2]);
|
||||||
|
|
||||||
|
const result = await service.evaluateModeratorsInactiveDays();
|
||||||
|
expect(result.isModeratorsInactive).toBe(true);
|
||||||
|
expect(result.inactiveModerators).toEqual([user1, user2]);
|
||||||
|
expect(result.inactivityLimitCountdown).toBe(-1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
@@ -31,10 +31,10 @@ defineProps<{
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding: var(--margin);
|
padding: var(--MI-margin);
|
||||||
margin-top: 4px;
|
margin-top: 4px;
|
||||||
border: 1px solid var(--MI_THEME-inputBorder);
|
border: 1px solid var(--MI_THEME-inputBorder);
|
||||||
border-radius: var(--radius);
|
border-radius: var(--MI-radius);
|
||||||
background-color: var(--MI_THEME-panel);
|
background-color: var(--MI_THEME-panel);
|
||||||
transition: background-color .1s, border-color .1s;
|
transition: background-color .1s, border-color .1s;
|
||||||
|
|
||||||
|
@@ -29,9 +29,9 @@ defineProps<{
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
height: auto;
|
height: auto;
|
||||||
aspect-ratio: 16 / 9;
|
aspect-ratio: 16 / 9;
|
||||||
padding: var(--margin);
|
padding: var(--MI-margin);
|
||||||
border: 1px solid var(--MI_THEME-divider);
|
border: 1px solid var(--MI_THEME-divider);
|
||||||
border-radius: var(--radius);
|
border-radius: var(--MI-radius);
|
||||||
background-color: #000;
|
background-color: #000;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
|
@@ -190,7 +190,7 @@ const isDeleted = ref(false);
|
|||||||
width: calc(100% - 8px);
|
width: calc(100% - 8px);
|
||||||
height: calc(100% - 8px);
|
height: calc(100% - 8px);
|
||||||
border: dashed 2px var(--MI_THEME-focus);
|
border: dashed 2px var(--MI_THEME-focus);
|
||||||
border-radius: var(--radius);
|
border-radius: var(--MI-radius);
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -356,7 +356,7 @@ const isDeleted = ref(false);
|
|||||||
width: 58px;
|
width: 58px;
|
||||||
height: 58px;
|
height: 58px;
|
||||||
position: sticky !important;
|
position: sticky !important;
|
||||||
top: calc(22px + var(--stickyTop, 0px));
|
top: calc(22px + var(--MI-stickyTop, 0px));
|
||||||
left: 0;
|
left: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -377,7 +377,7 @@ const isDeleted = ref(false);
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
margin-top: 14px;
|
margin-top: 14px;
|
||||||
position: sticky;
|
position: sticky;
|
||||||
bottom: calc(var(--stickyBottom, 0px) + 14px);
|
bottom: calc(var(--MI-stickyBottom, 0px) + 14px);
|
||||||
}
|
}
|
||||||
|
|
||||||
.showLessLabel {
|
.showLessLabel {
|
||||||
@@ -430,7 +430,7 @@ const isDeleted = ref(false);
|
|||||||
|
|
||||||
.translation {
|
.translation {
|
||||||
border: solid 0.5px var(--MI_THEME-divider);
|
border: solid 0.5px var(--MI_THEME-divider);
|
||||||
border-radius: var(--radius);
|
border-radius: var(--MI-radius);
|
||||||
padding: 12px;
|
padding: 12px;
|
||||||
margin-top: 8px;
|
margin-top: 8px;
|
||||||
}
|
}
|
||||||
@@ -550,7 +550,7 @@ const isDeleted = ref(false);
|
|||||||
margin: 0 10px 0 0;
|
margin: 0 10px 0 0;
|
||||||
width: 46px;
|
width: 46px;
|
||||||
height: 46px;
|
height: 46px;
|
||||||
top: calc(14px + var(--stickyTop, 0px));
|
top: calc(14px + var(--MI-stickyTop, 0px));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -364,7 +364,7 @@ const collapsed = ref(appearNote.value.cw == null && isLong);
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
margin-top: 14px;
|
margin-top: 14px;
|
||||||
position: sticky;
|
position: sticky;
|
||||||
bottom: calc(var(--stickyBottom, 0px) + 14px);
|
bottom: calc(var(--MI-stickyBottom, 0px) + 14px);
|
||||||
}
|
}
|
||||||
|
|
||||||
.showLessLabel {
|
.showLessLabel {
|
||||||
|
@@ -53,7 +53,7 @@ const showContent = ref(false);
|
|||||||
height: 34px;
|
height: 34px;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
position: sticky !important;
|
position: sticky !important;
|
||||||
top: calc(16px + var(--stickyTop, 0px));
|
top: calc(16px + var(--MI-stickyTop, 0px));
|
||||||
left: 0;
|
left: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -100,7 +100,7 @@ const collapsed = ref(isLong);
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
margin-top: 14px;
|
margin-top: 14px;
|
||||||
position: sticky;
|
position: sticky;
|
||||||
bottom: calc(var(--stickyBottom, 0px) + 14px);
|
bottom: calc(var(--MI-stickyBottom, 0px) + 14px);
|
||||||
}
|
}
|
||||||
|
|
||||||
.showLessLabel {
|
.showLessLabel {
|
||||||
|
@@ -100,7 +100,7 @@ function top(ev: MouseEvent) {
|
|||||||
display: flex;
|
display: flex;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: var(--margin);
|
gap: var(--MI-margin);
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
|
||||||
.headerClipIconRoot {
|
.headerClipIconRoot {
|
||||||
|
@@ -83,7 +83,7 @@ function top(ev: MouseEvent) {
|
|||||||
display: flex;
|
display: flex;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: var(--margin);
|
gap: var(--MI-margin);
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
|
||||||
.headerClipIconRoot {
|
.headerClipIconRoot {
|
||||||
|
@@ -117,7 +117,7 @@ function top(ev: MouseEvent) {
|
|||||||
display: flex;
|
display: flex;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: var(--margin);
|
gap: var(--MI-margin);
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
|
||||||
.avatarLink {
|
.avatarLink {
|
||||||
|
@@ -7,11 +7,11 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
--radius: 12px;
|
--MI-radius: 12px;
|
||||||
--marginFull: 14px;
|
--MI-marginFull: 14px;
|
||||||
--marginHalf: 10px;
|
--MI-marginHalf: 10px;
|
||||||
|
|
||||||
--margin: var(--marginFull);
|
--MI-margin: var(--MI-marginFull);
|
||||||
}
|
}
|
||||||
|
|
||||||
html {
|
html {
|
||||||
@@ -218,12 +218,12 @@ rt {
|
|||||||
|
|
||||||
._panel {
|
._panel {
|
||||||
background: var(--MI_THEME-panel);
|
background: var(--MI_THEME-panel);
|
||||||
border-radius: var(--radius);
|
border-radius: var(--MI-radius);
|
||||||
overflow: clip;
|
overflow: clip;
|
||||||
}
|
}
|
||||||
|
|
||||||
._margin {
|
._margin {
|
||||||
margin: var(--margin) 0;
|
margin: var(--MI-margin) 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
._gaps_m {
|
._gaps_m {
|
||||||
@@ -241,7 +241,7 @@ rt {
|
|||||||
._gaps {
|
._gaps {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: var(--margin);
|
gap: var(--MI-margin);
|
||||||
}
|
}
|
||||||
|
|
||||||
._buttons {
|
._buttons {
|
||||||
@@ -264,7 +264,7 @@ rt {
|
|||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
border: solid 0.5px var(--MI_THEME-divider);
|
border: solid 0.5px var(--MI_THEME-divider);
|
||||||
border-radius: var(--radius);
|
border-radius: var(--MI-radius);
|
||||||
|
|
||||||
&:active {
|
&:active {
|
||||||
border-color: var(--MI_THEME-accent);
|
border-color: var(--MI_THEME-accent);
|
||||||
@@ -273,14 +273,14 @@ rt {
|
|||||||
|
|
||||||
._popup {
|
._popup {
|
||||||
background: var(--MI_THEME-popup);
|
background: var(--MI_THEME-popup);
|
||||||
border-radius: var(--radius);
|
border-radius: var(--MI-radius);
|
||||||
contain: content;
|
contain: content;
|
||||||
}
|
}
|
||||||
|
|
||||||
._acrylic {
|
._acrylic {
|
||||||
background: var(--MI_THEME-acrylicPanel);
|
background: var(--MI_THEME-acrylicPanel);
|
||||||
-webkit-backdrop-filter: var(--blur, blur(15px));
|
-webkit-backdrop-filter: var(--MI-blur, blur(15px));
|
||||||
backdrop-filter: var(--blur, blur(15px));
|
backdrop-filter: var(--MI-blur, blur(15px));
|
||||||
}
|
}
|
||||||
|
|
||||||
._fullinfo {
|
._fullinfo {
|
||||||
|
@@ -95,7 +95,7 @@ onUnmounted(() => {
|
|||||||
height: auto;
|
height: auto;
|
||||||
|
|
||||||
&.rounded {
|
&.rounded {
|
||||||
border-radius: var(--radius);
|
border-radius: var(--MI-radius);
|
||||||
}
|
}
|
||||||
|
|
||||||
&.noBorder {
|
&.noBorder {
|
||||||
|
BIN
packages/frontend/assets/testcaptcha.png
Normal file
BIN
packages/frontend/assets/testcaptcha.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 2.6 KiB |
@@ -186,14 +186,14 @@ export async function common(createVue: () => App<Element>) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
watch(defaultStore.reactiveState.useBlurEffectForModal, v => {
|
watch(defaultStore.reactiveState.useBlurEffectForModal, v => {
|
||||||
document.documentElement.style.setProperty('--modalBgFilter', v ? 'blur(4px)' : 'none');
|
document.documentElement.style.setProperty('--MI-modalBgFilter', v ? 'blur(4px)' : 'none');
|
||||||
}, { immediate: true });
|
}, { immediate: true });
|
||||||
|
|
||||||
watch(defaultStore.reactiveState.useBlurEffect, v => {
|
watch(defaultStore.reactiveState.useBlurEffect, v => {
|
||||||
if (v) {
|
if (v) {
|
||||||
document.documentElement.style.removeProperty('--blur');
|
document.documentElement.style.removeProperty('--MI-blur');
|
||||||
} else {
|
} else {
|
||||||
document.documentElement.style.setProperty('--blur', 'none');
|
document.documentElement.style.setProperty('--MI-blur', 'none');
|
||||||
}
|
}
|
||||||
}, { immediate: true });
|
}, { immediate: true });
|
||||||
|
|
||||||
|
@@ -34,7 +34,7 @@ misskeyApi('users/show', { userId: props.movedTo }).then(u => user.value = u);
|
|||||||
font-size: 90%;
|
font-size: 90%;
|
||||||
background: var(--MI_THEME-infoWarnBg);
|
background: var(--MI_THEME-infoWarnBg);
|
||||||
color: var(--MI_THEME-error);
|
color: var(--MI_THEME-error);
|
||||||
border-radius: var(--radius);
|
border-radius: var(--MI-radius);
|
||||||
}
|
}
|
||||||
|
|
||||||
.link {
|
.link {
|
||||||
|
@@ -84,7 +84,7 @@ onMounted(() => {
|
|||||||
max-width: 480px;
|
max-width: 480px;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
background: var(--MI_THEME-panel);
|
background: var(--MI_THEME-panel);
|
||||||
border-radius: var(--radius);
|
border-radius: var(--MI-radius);
|
||||||
}
|
}
|
||||||
|
|
||||||
.header {
|
.header {
|
||||||
|
@@ -10,6 +10,17 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||||||
<div id="mcaptcha__widget-container" class="m-captcha-style"></div>
|
<div id="mcaptcha__widget-container" class="m-captcha-style"></div>
|
||||||
<div ref="captchaEl"></div>
|
<div ref="captchaEl"></div>
|
||||||
</div>
|
</div>
|
||||||
|
<div v-if="props.provider == 'testcaptcha'" style="background: #eee; border: solid 1px #888; padding: 8px; color: #000; max-width: 320px; display: flex; gap: 10px; align-items: center; box-shadow: 2px 2px 6px #0004; border-radius: 4px;">
|
||||||
|
<img src="/client-assets/testcaptcha.png" style="width: 60px; height: 60px; "/>
|
||||||
|
<div v-if="testcaptchaPassed">
|
||||||
|
<div style="color: green;">Test captcha passed!</div>
|
||||||
|
</div>
|
||||||
|
<div v-else>
|
||||||
|
<div style="font-size: 13px; margin-bottom: 4px;">Type "ai-chan-kawaii" to pass captcha</div>
|
||||||
|
<input v-model="testcaptchaInput" data-cy-testcaptcha-input/>
|
||||||
|
<button type="button" data-cy-testcaptcha-submit @click="testcaptchaSubmit">Submit</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div v-else ref="captchaEl"></div>
|
<div v-else ref="captchaEl"></div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -29,7 +40,7 @@ export type Captcha = {
|
|||||||
getResponse(id: string): string;
|
getResponse(id: string): string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type CaptchaProvider = 'hcaptcha' | 'recaptcha' | 'turnstile' | 'mcaptcha';
|
export type CaptchaProvider = 'hcaptcha' | 'recaptcha' | 'turnstile' | 'mcaptcha' | 'testcaptcha';
|
||||||
|
|
||||||
type CaptchaContainer = {
|
type CaptchaContainer = {
|
||||||
readonly [_ in CaptchaProvider]?: Captcha;
|
readonly [_ in CaptchaProvider]?: Captcha;
|
||||||
@@ -54,12 +65,16 @@ const available = ref(false);
|
|||||||
|
|
||||||
const captchaEl = shallowRef<HTMLDivElement | undefined>();
|
const captchaEl = shallowRef<HTMLDivElement | undefined>();
|
||||||
|
|
||||||
|
const testcaptchaInput = ref('');
|
||||||
|
const testcaptchaPassed = ref(false);
|
||||||
|
|
||||||
const variable = computed(() => {
|
const variable = computed(() => {
|
||||||
switch (props.provider) {
|
switch (props.provider) {
|
||||||
case 'hcaptcha': return 'hcaptcha';
|
case 'hcaptcha': return 'hcaptcha';
|
||||||
case 'recaptcha': return 'grecaptcha';
|
case 'recaptcha': return 'grecaptcha';
|
||||||
case 'turnstile': return 'turnstile';
|
case 'turnstile': return 'turnstile';
|
||||||
case 'mcaptcha': return 'mcaptcha';
|
case 'mcaptcha': return 'mcaptcha';
|
||||||
|
case 'testcaptcha': return 'testcaptcha';
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -71,6 +86,7 @@ const src = computed(() => {
|
|||||||
case 'recaptcha': return 'https://www.recaptcha.net/recaptcha/api.js?render=explicit';
|
case 'recaptcha': return 'https://www.recaptcha.net/recaptcha/api.js?render=explicit';
|
||||||
case 'turnstile': return 'https://challenges.cloudflare.com/turnstile/v0/api.js?render=explicit';
|
case 'turnstile': return 'https://challenges.cloudflare.com/turnstile/v0/api.js?render=explicit';
|
||||||
case 'mcaptcha': return null;
|
case 'mcaptcha': return null;
|
||||||
|
case 'testcaptcha': return null;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -78,7 +94,7 @@ const scriptId = computed(() => `script-${props.provider}`);
|
|||||||
|
|
||||||
const captcha = computed<Captcha>(() => window[variable.value] || {} as unknown as Captcha);
|
const captcha = computed<Captcha>(() => window[variable.value] || {} as unknown as Captcha);
|
||||||
|
|
||||||
if (loaded || props.provider === 'mcaptcha') {
|
if (loaded || props.provider === 'mcaptcha' || props.provider === 'testcaptcha') {
|
||||||
available.value = true;
|
available.value = true;
|
||||||
} else if (src.value !== null) {
|
} else if (src.value !== null) {
|
||||||
(document.getElementById(scriptId.value) ?? document.head.appendChild(Object.assign(document.createElement('script'), {
|
(document.getElementById(scriptId.value) ?? document.head.appendChild(Object.assign(document.createElement('script'), {
|
||||||
@@ -91,6 +107,8 @@ if (loaded || props.provider === 'mcaptcha') {
|
|||||||
|
|
||||||
function reset() {
|
function reset() {
|
||||||
if (captcha.value.reset) captcha.value.reset();
|
if (captcha.value.reset) captcha.value.reset();
|
||||||
|
testcaptchaPassed.value = false;
|
||||||
|
testcaptchaInput.value = '';
|
||||||
}
|
}
|
||||||
|
|
||||||
async function requestRender() {
|
async function requestRender() {
|
||||||
@@ -127,6 +145,12 @@ function onReceivedMessage(message: MessageEvent) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function testcaptchaSubmit() {
|
||||||
|
testcaptchaPassed.value = testcaptchaInput.value === 'ai-chan-kawaii';
|
||||||
|
callback(testcaptchaPassed.value ? 'testcaptcha-passed' : undefined);
|
||||||
|
if (!testcaptchaPassed.value) testcaptchaInput.value = '';
|
||||||
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
if (available.value) {
|
if (available.value) {
|
||||||
window.addEventListener('message', onReceivedMessage);
|
window.addEventListener('message', onReceivedMessage);
|
||||||
|
@@ -863,8 +863,8 @@ onMounted(() => {
|
|||||||
left: 0;
|
left: 0;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
-webkit-backdrop-filter: var(--blur, blur(12px));
|
-webkit-backdrop-filter: var(--MI-blur, blur(12px));
|
||||||
backdrop-filter: var(--blur, blur(12px));
|
backdrop-filter: var(--MI-blur, blur(12px));
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
@@ -165,7 +165,7 @@ onUnmounted(() => {
|
|||||||
|
|
||||||
.header {
|
.header {
|
||||||
position: sticky;
|
position: sticky;
|
||||||
top: var(--stickyTop, 0px);
|
top: var(--MI-stickyTop, 0px);
|
||||||
left: 0;
|
left: 0;
|
||||||
color: var(--MI_THEME-panelHeaderFg);
|
color: var(--MI_THEME-panelHeaderFg);
|
||||||
background: var(--MI_THEME-panelHeaderBg);
|
background: var(--MI_THEME-panelHeaderBg);
|
||||||
@@ -201,7 +201,7 @@ onUnmounted(() => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.content {
|
.content {
|
||||||
--stickyTop: 0px;
|
--MI-stickyTop: 0px;
|
||||||
|
|
||||||
&.omitted {
|
&.omitted {
|
||||||
position: relative;
|
position: relative;
|
||||||
|
@@ -170,8 +170,8 @@ onMounted(() => {
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
-webkit-backdrop-filter: var(--blur, blur(10px));
|
-webkit-backdrop-filter: var(--MI-blur, blur(10px));
|
||||||
backdrop-filter: var(--blur, blur(10px));
|
backdrop-filter: var(--MI-blur, blur(10px));
|
||||||
background: rgba(0, 0, 0, 0.5);
|
background: rgba(0, 0, 0, 0.5);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -86,7 +86,7 @@ function cancel() {
|
|||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
height: 40cqh;
|
height: 40cqh;
|
||||||
background-image: repeating-linear-gradient(45deg, transparent, transparent 8px, var(--MI_THEME-X5) 8px, var(--MI_THEME-X5) 14px);
|
background-image: repeating-linear-gradient(45deg, transparent, transparent 8px, var(--MI_THEME-X5) 8px, var(--MI_THEME-X5) 14px);
|
||||||
border-radius: var(--radius);
|
border-radius: var(--MI-radius);
|
||||||
margin: auto;
|
margin: auto;
|
||||||
overflow-y: hidden;
|
overflow-y: hidden;
|
||||||
}
|
}
|
||||||
@@ -103,6 +103,6 @@ function cancel() {
|
|||||||
padding: 3px 10px;
|
padding: 3px 10px;
|
||||||
background-color: var(--MI_THEME-X5);
|
background-color: var(--MI_THEME-X5);
|
||||||
border: solid 1px var(--MI_THEME-divider);
|
border: solid 1px var(--MI_THEME-divider);
|
||||||
border-radius: var(--radius);
|
border-radius: var(--MI-radius);
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
@@ -182,7 +182,7 @@ export default defineComponent({
|
|||||||
}
|
}
|
||||||
|
|
||||||
&:not(.date-separated-list-nogap) > *:not(:last-child) {
|
&:not(.date-separated-list-nogap) > *:not(:last-child) {
|
||||||
margin-bottom: var(--margin);
|
margin-bottom: var(--MI-margin);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -65,12 +65,12 @@ function neverShow() {
|
|||||||
.root {
|
.root {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
z-index: v-bind(zIndex);
|
z-index: v-bind(zIndex);
|
||||||
bottom: var(--margin);
|
bottom: var(--MI-margin);
|
||||||
left: 0;
|
left: 0;
|
||||||
right: 0;
|
right: 0;
|
||||||
margin: auto;
|
margin: auto;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
width: calc(100% - (var(--margin) * 2));
|
width: calc(100% - (var(--MI-margin) * 2));
|
||||||
max-width: 500px;
|
max-width: 500px;
|
||||||
display: flex;
|
display: flex;
|
||||||
}
|
}
|
||||||
|
@@ -36,13 +36,13 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { computed, defineAsyncComponent, ref } from 'vue';
|
import { computed, defineAsyncComponent, ref } from 'vue';
|
||||||
import * as Misskey from 'misskey-js';
|
import * as Misskey from 'misskey-js';
|
||||||
|
import type { MenuItem } from '@/types/menu.js';
|
||||||
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 { i18n } from '@/i18n.js';
|
import { i18n } from '@/i18n.js';
|
||||||
import { defaultStore } from '@/store.js';
|
import { defaultStore } from '@/store.js';
|
||||||
import { claimAchievement } from '@/scripts/achievements.js';
|
import { claimAchievement } from '@/scripts/achievements.js';
|
||||||
import { copyToClipboard } from '@/scripts/copy-to-clipboard.js';
|
import { copyToClipboard } from '@/scripts/copy-to-clipboard.js';
|
||||||
import type { MenuItem } from '@/types/menu.js';
|
|
||||||
|
|
||||||
const props = withDefaults(defineProps<{
|
const props = withDefaults(defineProps<{
|
||||||
folder: Misskey.entities.DriveFolder;
|
folder: Misskey.entities.DriveFolder;
|
||||||
@@ -375,7 +375,6 @@ function onContextmenu(ev: MouseEvent) {
|
|||||||
.name {
|
.name {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-size: 0.9em;
|
font-size: 0.9em;
|
||||||
color: var(--desktopDriveFolderFg);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon {
|
.icon {
|
||||||
@@ -388,6 +387,5 @@ function onContextmenu(ev: MouseEvent) {
|
|||||||
margin: 4px 4px;
|
margin: 4px 4px;
|
||||||
font-size: 0.8em;
|
font-size: 0.8em;
|
||||||
text-align: right;
|
text-align: right;
|
||||||
color: var(--desktopDriveFolderFg);
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
@@ -768,7 +768,7 @@ onBeforeUnmount(() => {
|
|||||||
.main {
|
.main {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
padding: var(--margin);
|
padding: var(--MI-margin);
|
||||||
user-select: none;
|
user-select: none;
|
||||||
|
|
||||||
&.fetching {
|
&.fetching {
|
||||||
|
@@ -110,7 +110,7 @@ const emits = defineEmits<{
|
|||||||
|
|
||||||
<style lang="scss" module>
|
<style lang="scss" module>
|
||||||
.extInstallerRoot {
|
.extInstallerRoot {
|
||||||
border-radius: var(--radius);
|
border-radius: var(--MI-radius);
|
||||||
background: var(--MI_THEME-panel);
|
background: var(--MI_THEME-panel);
|
||||||
padding: 1.5rem;
|
padding: 1.5rem;
|
||||||
}
|
}
|
||||||
|
@@ -83,7 +83,6 @@ const props = defineProps<{
|
|||||||
> p {
|
> p {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
color: var(--urlPreviewInfo);
|
|
||||||
font-size: 0.8em;
|
font-size: 0.8em;
|
||||||
line-height: 16px;
|
line-height: 16px;
|
||||||
vertical-align: top;
|
vertical-align: top;
|
||||||
|
@@ -118,9 +118,9 @@ onMounted(() => {
|
|||||||
position: relative;
|
position: relative;
|
||||||
z-index: 10;
|
z-index: 10;
|
||||||
position: sticky;
|
position: sticky;
|
||||||
top: var(--stickyTop, 0px);
|
top: var(--MI-stickyTop, 0px);
|
||||||
-webkit-backdrop-filter: var(--blur, blur(8px));
|
-webkit-backdrop-filter: var(--MI-blur, blur(8px));
|
||||||
backdrop-filter: var(--blur, blur(20px));
|
backdrop-filter: var(--MI-blur, blur(20px));
|
||||||
}
|
}
|
||||||
|
|
||||||
.title {
|
.title {
|
||||||
|
@@ -145,8 +145,8 @@ onMounted(() => {
|
|||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
padding: 9px 12px 9px 12px;
|
padding: 9px 12px 9px 12px;
|
||||||
background: var(--MI_THEME-folderHeaderBg);
|
background: var(--MI_THEME-folderHeaderBg);
|
||||||
-webkit-backdrop-filter: var(--blur, blur(15px));
|
-webkit-backdrop-filter: var(--MI-blur, blur(15px));
|
||||||
backdrop-filter: var(--blur, blur(15px));
|
backdrop-filter: var(--MI-blur, blur(15px));
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
transition: border-radius 0.3s;
|
transition: border-radius 0.3s;
|
||||||
|
|
||||||
@@ -236,12 +236,12 @@ onMounted(() => {
|
|||||||
.footer {
|
.footer {
|
||||||
position: sticky !important;
|
position: sticky !important;
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
bottom: var(--stickyBottom, 0px);
|
bottom: var(--MI-stickyBottom, 0px);
|
||||||
left: 0;
|
left: 0;
|
||||||
padding: 12px;
|
padding: 12px;
|
||||||
background: var(--MI_THEME-acrylicBg);
|
background: var(--MI_THEME-acrylicBg);
|
||||||
-webkit-backdrop-filter: var(--blur, blur(15px));
|
-webkit-backdrop-filter: var(--MI-blur, blur(15px));
|
||||||
backdrop-filter: var(--blur, blur(15px));
|
backdrop-filter: var(--MI-blur, blur(15px));
|
||||||
background-size: auto auto;
|
background-size: auto auto;
|
||||||
background-image: repeating-linear-gradient(135deg, transparent, transparent 5px, var(--MI_THEME-panel) 5px, var(--MI_THEME-panel) 10px);
|
background-image: repeating-linear-gradient(135deg, transparent, transparent 5px, var(--MI_THEME-panel) 5px, var(--MI_THEME-panel) 10px);
|
||||||
border-radius: 0 0 6px 6px;
|
border-radius: 0 0 6px 6px;
|
||||||
|
@@ -39,7 +39,7 @@ withDefaults(defineProps<{
|
|||||||
|
|
||||||
<style module lang="scss">
|
<style module lang="scss">
|
||||||
.root {
|
.root {
|
||||||
--fukidashi-radius: var(--radius);
|
--fukidashi-radius: var(--MI-radius);
|
||||||
--fukidashi-bg: var(--MI_THEME-panel);
|
--fukidashi-bg: var(--MI_THEME-panel);
|
||||||
|
|
||||||
position: relative;
|
position: relative;
|
||||||
|
@@ -38,7 +38,7 @@ function close() {
|
|||||||
font-size: 90%;
|
font-size: 90%;
|
||||||
background: var(--MI_THEME-infoBg);
|
background: var(--MI_THEME-infoBg);
|
||||||
color: var(--MI_THEME-infoFg);
|
color: var(--MI_THEME-infoFg);
|
||||||
border-radius: var(--radius);
|
border-radius: var(--MI-radius);
|
||||||
white-space: pre-wrap;
|
white-space: pre-wrap;
|
||||||
|
|
||||||
&.warn {
|
&.warn {
|
||||||
|
@@ -257,7 +257,7 @@ onMounted(() => {
|
|||||||
min-width: 0;
|
min-width: 0;
|
||||||
position: relative;
|
position: relative;
|
||||||
background: var(--MI_THEME-panel);
|
background: var(--MI_THEME-panel);
|
||||||
border-radius: var(--radius);
|
border-radius: var(--MI-radius);
|
||||||
padding: 24px;
|
padding: 24px;
|
||||||
max-height: 300px;
|
max-height: 300px;
|
||||||
|
|
||||||
|
@@ -392,7 +392,7 @@ onDeactivated(() => {
|
|||||||
container-type: inline-size;
|
container-type: inline-size;
|
||||||
position: relative;
|
position: relative;
|
||||||
border: .5px solid var(--MI_THEME-divider);
|
border: .5px solid var(--MI_THEME-divider);
|
||||||
border-radius: var(--radius);
|
border-radius: var(--MI-radius);
|
||||||
overflow: clip;
|
overflow: clip;
|
||||||
|
|
||||||
&:focus-visible {
|
&:focus-visible {
|
||||||
@@ -454,7 +454,7 @@ onDeactivated(() => {
|
|||||||
|
|
||||||
.controlButton {
|
.controlButton {
|
||||||
padding: 6px;
|
padding: 6px;
|
||||||
border-radius: calc(var(--radius) / 2);
|
border-radius: calc(var(--MI-radius) / 2);
|
||||||
font-size: 1.05rem;
|
font-size: 1.05rem;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
|
@@ -68,7 +68,6 @@ async function show() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.download {
|
.download {
|
||||||
background: var(--noteAttachedFile);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.sensitive {
|
.sensitive {
|
||||||
|
@@ -226,8 +226,8 @@ html[data-color-scheme=light] .visible {
|
|||||||
position: absolute;
|
position: absolute;
|
||||||
border-radius: 999px;
|
border-radius: 999px;
|
||||||
background-color: rgba(0, 0, 0, 0.3);
|
background-color: rgba(0, 0, 0, 0.3);
|
||||||
-webkit-backdrop-filter: var(--blur, blur(15px));
|
-webkit-backdrop-filter: var(--MI-blur, blur(15px));
|
||||||
backdrop-filter: var(--blur, blur(15px));
|
backdrop-filter: var(--MI-blur, blur(15px));
|
||||||
color: #fff;
|
color: #fff;
|
||||||
font-size: 0.8em;
|
font-size: 0.8em;
|
||||||
width: 28px;
|
width: 28px;
|
||||||
|
@@ -317,7 +317,7 @@ defineExpose({
|
|||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
.pswp__bg {
|
.pswp__bg {
|
||||||
background: var(--MI_THEME-modalBg);
|
background: var(--MI_THEME-modalBg);
|
||||||
backdrop-filter: var(--modalBgFilter);
|
backdrop-filter: var(--MI-modalBgFilter);
|
||||||
}
|
}
|
||||||
|
|
||||||
.pswp__alt-text-container {
|
.pswp__alt-text-container {
|
||||||
@@ -338,8 +338,8 @@ defineExpose({
|
|||||||
color: var(--MI_THEME-fg);
|
color: var(--MI_THEME-fg);
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
padding: var(--margin);
|
padding: var(--MI-margin);
|
||||||
border-radius: var(--radius);
|
border-radius: var(--MI-radius);
|
||||||
max-height: 8em;
|
max-height: 8em;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
text-shadow: var(--MI_THEME-bg) 0 0 10px, var(--MI_THEME-bg) 0 0 3px, var(--MI_THEME-bg) 0 0 3px;
|
text-shadow: var(--MI_THEME-bg) 0 0 10px, var(--MI_THEME-bg) 0 0 3px, var(--MI_THEME-bg) 0 0 3px;
|
||||||
|
@@ -658,7 +658,7 @@ onDeactivated(() => {
|
|||||||
|
|
||||||
.controlButton {
|
.controlButton {
|
||||||
padding: 6px;
|
padding: 6px;
|
||||||
border-radius: calc(var(--radius) / 2);
|
border-radius: calc(var(--MI-radius) / 2);
|
||||||
transition: background-color .2s ease-in-out;
|
transition: background-color .2s ease-in-out;
|
||||||
font-size: 1.05rem;
|
font-size: 1.05rem;
|
||||||
|
|
||||||
|
@@ -90,7 +90,7 @@ defineExpose({
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
contain: content;
|
contain: content;
|
||||||
border-radius: var(--radius);
|
border-radius: var(--MI-radius);
|
||||||
|
|
||||||
--root-margin: 24px;
|
--root-margin: 24px;
|
||||||
|
|
||||||
@@ -106,8 +106,8 @@ defineExpose({
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
background: var(--MI_THEME-windowHeader);
|
background: var(--MI_THEME-windowHeader);
|
||||||
-webkit-backdrop-filter: var(--blur, blur(15px));
|
-webkit-backdrop-filter: var(--MI-blur, blur(15px));
|
||||||
backdrop-filter: var(--blur, blur(15px));
|
backdrop-filter: var(--MI-blur, blur(15px));
|
||||||
}
|
}
|
||||||
|
|
||||||
.headerButton {
|
.headerButton {
|
||||||
|
@@ -644,7 +644,7 @@ function emitUpdReaction(emoji: string, delta: number) {
|
|||||||
width: calc(100% - 8px);
|
width: calc(100% - 8px);
|
||||||
height: calc(100% - 8px);
|
height: calc(100% - 8px);
|
||||||
border: dashed 2px var(--MI_THEME-focus);
|
border: dashed 2px var(--MI_THEME-focus);
|
||||||
border-radius: var(--radius);
|
border-radius: var(--MI-radius);
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -810,7 +810,7 @@ function emitUpdReaction(emoji: string, delta: number) {
|
|||||||
width: 58px;
|
width: 58px;
|
||||||
height: 58px;
|
height: 58px;
|
||||||
position: sticky !important;
|
position: sticky !important;
|
||||||
top: calc(22px + var(--stickyTop, 0px));
|
top: calc(22px + var(--MI-stickyTop, 0px));
|
||||||
left: 0;
|
left: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -831,7 +831,7 @@ function emitUpdReaction(emoji: string, delta: number) {
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
margin-top: 14px;
|
margin-top: 14px;
|
||||||
position: sticky;
|
position: sticky;
|
||||||
bottom: calc(var(--stickyBottom, 0px) + 14px);
|
bottom: calc(var(--MI-stickyBottom, 0px) + 14px);
|
||||||
}
|
}
|
||||||
|
|
||||||
.showLessLabel {
|
.showLessLabel {
|
||||||
@@ -884,7 +884,7 @@ function emitUpdReaction(emoji: string, delta: number) {
|
|||||||
|
|
||||||
.translation {
|
.translation {
|
||||||
border: solid 0.5px var(--MI_THEME-divider);
|
border: solid 0.5px var(--MI_THEME-divider);
|
||||||
border-radius: var(--radius);
|
border-radius: var(--MI-radius);
|
||||||
padding: 12px;
|
padding: 12px;
|
||||||
margin-top: 8px;
|
margin-top: 8px;
|
||||||
}
|
}
|
||||||
@@ -998,7 +998,7 @@ function emitUpdReaction(emoji: string, delta: number) {
|
|||||||
margin: 0 10px 0 0;
|
margin: 0 10px 0 0;
|
||||||
width: 46px;
|
width: 46px;
|
||||||
height: 46px;
|
height: 46px;
|
||||||
top: calc(14px + var(--stickyTop, 0px));
|
top: calc(14px + var(--MI-stickyTop, 0px));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -570,7 +570,7 @@ function loadConversation() {
|
|||||||
width: calc(100% - 8px);
|
width: calc(100% - 8px);
|
||||||
height: calc(100% - 8px);
|
height: calc(100% - 8px);
|
||||||
border: dashed 2px var(--MI_THEME-focus);
|
border: dashed 2px var(--MI_THEME-focus);
|
||||||
border-radius: var(--radius);
|
border-radius: var(--MI-radius);
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -711,7 +711,7 @@ function loadConversation() {
|
|||||||
|
|
||||||
.translation {
|
.translation {
|
||||||
border: solid 0.5px var(--MI_THEME-divider);
|
border: solid 0.5px var(--MI_THEME-divider);
|
||||||
border-radius: var(--radius);
|
border-radius: var(--MI-radius);
|
||||||
padding: 12px;
|
padding: 12px;
|
||||||
margin-top: 8px;
|
margin-top: 8px;
|
||||||
}
|
}
|
||||||
|
@@ -51,7 +51,7 @@ const showContent = ref(false);
|
|||||||
height: 34px;
|
height: 34px;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
position: sticky !important;
|
position: sticky !important;
|
||||||
top: calc(16px + var(--stickyTop, 0px));
|
top: calc(16px + var(--MI-stickyTop, 0px));
|
||||||
left: 0;
|
left: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -66,7 +66,7 @@ defineExpose({
|
|||||||
|
|
||||||
.note {
|
.note {
|
||||||
background: var(--MI_THEME-panel);
|
background: var(--MI_THEME-panel);
|
||||||
border-radius: var(--radius);
|
border-radius: var(--MI-radius);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -47,7 +47,7 @@ onUnmounted(() => {
|
|||||||
|
|
||||||
<style lang="scss" module>
|
<style lang="scss" module>
|
||||||
.content {
|
.content {
|
||||||
--stickyTop: 0px;
|
--MI-stickyTop: 0px;
|
||||||
|
|
||||||
&.omitted {
|
&.omitted {
|
||||||
position: relative;
|
position: relative;
|
||||||
|
@@ -42,7 +42,7 @@ const props = defineProps<{
|
|||||||
.eyeCatchingImageRoot {
|
.eyeCatchingImageRoot {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 200px;
|
height: 200px;
|
||||||
border-radius: var(--radius) var(--radius) 0 0;
|
border-radius: var(--MI-radius) var(--MI-radius) 0 0;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
@@ -67,7 +67,7 @@ const props = defineProps<{
|
|||||||
left: 0;
|
left: 0;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
border-radius: var(--radius);
|
border-radius: var(--MI-radius);
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
box-shadow: inset 0 0 0 2px var(--MI_THEME-focus);
|
box-shadow: inset 0 0 0 2px var(--MI_THEME-focus);
|
||||||
}
|
}
|
||||||
@@ -75,14 +75,14 @@ const props = defineProps<{
|
|||||||
|
|
||||||
> .thumbnail {
|
> .thumbnail {
|
||||||
& + article {
|
& + article {
|
||||||
border-radius: 0 0 var(--radius) var(--radius);
|
border-radius: 0 0 var(--MI-radius) var(--MI-radius);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
> article {
|
> article {
|
||||||
background-color: var(--MI_THEME-panel);
|
background-color: var(--MI_THEME-panel);
|
||||||
padding: 16px;
|
padding: 16px;
|
||||||
border-radius: var(--radius);
|
border-radius: var(--MI-radius);
|
||||||
|
|
||||||
> header {
|
> header {
|
||||||
margin-bottom: 8px;
|
margin-bottom: 8px;
|
||||||
@@ -115,7 +115,6 @@ const props = defineProps<{
|
|||||||
> p {
|
> p {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
color: var(--urlPreviewInfo);
|
|
||||||
font-size: 0.8em;
|
font-size: 0.8em;
|
||||||
line-height: 16px;
|
line-height: 16px;
|
||||||
vertical-align: top;
|
vertical-align: top;
|
||||||
|
@@ -181,6 +181,6 @@ defineExpose({
|
|||||||
min-height: 100%;
|
min-height: 100%;
|
||||||
background: var(--MI_THEME-bg);
|
background: var(--MI_THEME-bg);
|
||||||
|
|
||||||
--margin: var(--marginHalf);
|
--MI-margin: var(--MI-marginHalf);
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
@@ -21,7 +21,7 @@ defineProps<{
|
|||||||
padding: 16px;
|
padding: 16px;
|
||||||
background: var(--MI_THEME-infoWarnBg);
|
background: var(--MI_THEME-infoWarnBg);
|
||||||
color: var(--MI_THEME-infoWarnFg);
|
color: var(--MI_THEME-infoWarnFg);
|
||||||
border-radius: var(--radius);
|
border-radius: var(--MI-radius);
|
||||||
overflow: clip;
|
overflow: clip;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -28,6 +28,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||||||
<MkCaptcha v-if="instance.enableMcaptcha" ref="mcaptcha" v-model="mCaptchaResponse" :class="$style.captcha" provider="mcaptcha" :sitekey="instance.mcaptchaSiteKey" :instanceUrl="instance.mcaptchaInstanceUrl"/>
|
<MkCaptcha v-if="instance.enableMcaptcha" ref="mcaptcha" v-model="mCaptchaResponse" :class="$style.captcha" provider="mcaptcha" :sitekey="instance.mcaptchaSiteKey" :instanceUrl="instance.mcaptchaInstanceUrl"/>
|
||||||
<MkCaptcha v-if="instance.enableRecaptcha" ref="recaptcha" v-model="reCaptchaResponse" :class="$style.captcha" provider="recaptcha" :sitekey="instance.recaptchaSiteKey"/>
|
<MkCaptcha v-if="instance.enableRecaptcha" ref="recaptcha" v-model="reCaptchaResponse" :class="$style.captcha" provider="recaptcha" :sitekey="instance.recaptchaSiteKey"/>
|
||||||
<MkCaptcha v-if="instance.enableTurnstile" ref="turnstile" v-model="turnstileResponse" :class="$style.captcha" provider="turnstile" :sitekey="instance.turnstileSiteKey"/>
|
<MkCaptcha v-if="instance.enableTurnstile" ref="turnstile" v-model="turnstileResponse" :class="$style.captcha" provider="turnstile" :sitekey="instance.turnstileSiteKey"/>
|
||||||
|
<MkCaptcha v-if="instance.enableTestcaptcha" ref="testcaptcha" v-model="testcaptchaResponse" :class="$style.captcha" provider="testcaptcha"/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<MkButton type="submit" :disabled="needCaptcha && captchaFailed" large primary rounded style="margin: 0 auto;" data-cy-signin-page-password-continue>{{ i18n.ts.continue }} <i class="ti ti-arrow-right"></i></MkButton>
|
<MkButton type="submit" :disabled="needCaptcha && captchaFailed" large primary rounded style="margin: 0 auto;" data-cy-signin-page-password-continue>{{ i18n.ts.continue }} <i class="ti ti-arrow-right"></i></MkButton>
|
||||||
@@ -44,6 +45,7 @@ export type PwResponse = {
|
|||||||
mCaptchaResponse: string | null;
|
mCaptchaResponse: string | null;
|
||||||
reCaptchaResponse: string | null;
|
reCaptchaResponse: string | null;
|
||||||
turnstileResponse: string | null;
|
turnstileResponse: string | null;
|
||||||
|
testcaptchaResponse: string | null;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
@@ -75,18 +77,21 @@ const hCaptcha = useTemplateRef('hcaptcha');
|
|||||||
const mCaptcha = useTemplateRef('mcaptcha');
|
const mCaptcha = useTemplateRef('mcaptcha');
|
||||||
const reCaptcha = useTemplateRef('recaptcha');
|
const reCaptcha = useTemplateRef('recaptcha');
|
||||||
const turnstile = useTemplateRef('turnstile');
|
const turnstile = useTemplateRef('turnstile');
|
||||||
|
const testcaptcha = useTemplateRef('testcaptcha');
|
||||||
|
|
||||||
const hCaptchaResponse = ref<string | null>(null);
|
const hCaptchaResponse = ref<string | null>(null);
|
||||||
const mCaptchaResponse = ref<string | null>(null);
|
const mCaptchaResponse = ref<string | null>(null);
|
||||||
const reCaptchaResponse = ref<string | null>(null);
|
const reCaptchaResponse = ref<string | null>(null);
|
||||||
const turnstileResponse = ref<string | null>(null);
|
const turnstileResponse = ref<string | null>(null);
|
||||||
|
const testcaptchaResponse = ref<string | null>(null);
|
||||||
|
|
||||||
const captchaFailed = computed((): boolean => {
|
const captchaFailed = computed((): boolean => {
|
||||||
return (
|
return (
|
||||||
(instance.enableHcaptcha && !hCaptchaResponse.value) ||
|
(instance.enableHcaptcha && !hCaptchaResponse.value) ||
|
||||||
(instance.enableMcaptcha && !mCaptchaResponse.value) ||
|
(instance.enableMcaptcha && !mCaptchaResponse.value) ||
|
||||||
(instance.enableRecaptcha && !reCaptchaResponse.value) ||
|
(instance.enableRecaptcha && !reCaptchaResponse.value) ||
|
||||||
(instance.enableTurnstile && !turnstileResponse.value)
|
(instance.enableTurnstile && !turnstileResponse.value) ||
|
||||||
|
(instance.enableTestcaptcha && !testcaptchaResponse.value)
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -104,6 +109,7 @@ function onSubmit() {
|
|||||||
mCaptchaResponse: mCaptchaResponse.value,
|
mCaptchaResponse: mCaptchaResponse.value,
|
||||||
reCaptchaResponse: reCaptchaResponse.value,
|
reCaptchaResponse: reCaptchaResponse.value,
|
||||||
turnstileResponse: turnstileResponse.value,
|
turnstileResponse: turnstileResponse.value,
|
||||||
|
testcaptchaResponse: testcaptchaResponse.value,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -113,6 +119,7 @@ function resetCaptcha() {
|
|||||||
mCaptcha.value?.reset();
|
mCaptcha.value?.reset();
|
||||||
reCaptcha.value?.reset();
|
reCaptcha.value?.reset();
|
||||||
turnstile.value?.reset();
|
turnstile.value?.reset();
|
||||||
|
testcaptcha.value?.reset();
|
||||||
}
|
}
|
||||||
|
|
||||||
defineExpose({
|
defineExpose({
|
||||||
|
@@ -68,6 +68,8 @@ import { nextTick, onBeforeUnmount, ref, shallowRef, useTemplateRef } from 'vue'
|
|||||||
import * as Misskey from 'misskey-js';
|
import * as Misskey from 'misskey-js';
|
||||||
import { supported as webAuthnSupported, parseRequestOptionsFromJSON } from '@github/webauthn-json/browser-ponyfill';
|
import { supported as webAuthnSupported, parseRequestOptionsFromJSON } from '@github/webauthn-json/browser-ponyfill';
|
||||||
|
|
||||||
|
import type { AuthenticationPublicKeyCredential } from '@github/webauthn-json/browser-ponyfill';
|
||||||
|
import type { OpenOnRemoteOptions } from '@/scripts/please-login.js';
|
||||||
import { misskeyApi } from '@/scripts/misskey-api.js';
|
import { misskeyApi } from '@/scripts/misskey-api.js';
|
||||||
import { showSuspendedDialog } from '@/scripts/show-suspended-dialog.js';
|
import { showSuspendedDialog } from '@/scripts/show-suspended-dialog.js';
|
||||||
import { login } from '@/account.js';
|
import { login } from '@/account.js';
|
||||||
@@ -79,9 +81,6 @@ import XPassword, { type PwResponse } from '@/components/MkSignin.password.vue';
|
|||||||
import XTotp from '@/components/MkSignin.totp.vue';
|
import XTotp from '@/components/MkSignin.totp.vue';
|
||||||
import XPasskey from '@/components/MkSignin.passkey.vue';
|
import XPasskey from '@/components/MkSignin.passkey.vue';
|
||||||
|
|
||||||
import type { AuthenticationPublicKeyCredential } from '@github/webauthn-json/browser-ponyfill';
|
|
||||||
import type { OpenOnRemoteOptions } from '@/scripts/please-login.js';
|
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
(ev: 'login', v: Misskey.entities.SigninFlowResponse & { finished: true }): void;
|
(ev: 'login', v: Misskey.entities.SigninFlowResponse & { finished: true }): void;
|
||||||
}>();
|
}>();
|
||||||
@@ -188,6 +187,7 @@ async function onPasswordSubmitted(pw: PwResponse) {
|
|||||||
'm-captcha-response': pw.captcha.mCaptchaResponse,
|
'm-captcha-response': pw.captcha.mCaptchaResponse,
|
||||||
'g-recaptcha-response': pw.captcha.reCaptchaResponse,
|
'g-recaptcha-response': pw.captcha.reCaptchaResponse,
|
||||||
'turnstile-response': pw.captcha.turnstileResponse,
|
'turnstile-response': pw.captcha.turnstileResponse,
|
||||||
|
'testcaptcha-response': pw.captcha.testcaptchaResponse,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -70,7 +70,7 @@ function onLogin(res: Misskey.entities.SigninFlowResponse & { finished: true })
|
|||||||
max-height: 450px;
|
max-height: 450px;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
background: var(--MI_THEME-panel);
|
background: var(--MI_THEME-panel);
|
||||||
border-radius: var(--radius);
|
border-radius: var(--MI-radius);
|
||||||
}
|
}
|
||||||
|
|
||||||
.header {
|
.header {
|
||||||
@@ -83,7 +83,7 @@ function onLogin(res: Misskey.entities.SigninFlowResponse & { finished: true })
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
backdrop-filter: var(--blur, blur(15px));
|
backdrop-filter: var(--MI-blur, blur(15px));
|
||||||
background: var(--MI_THEME-acrylicBg);
|
background: var(--MI_THEME-acrylicBg);
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
}
|
}
|
||||||
|
@@ -66,6 +66,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||||||
<MkCaptcha v-if="instance.enableMcaptcha" ref="mcaptcha" v-model="mCaptchaResponse" :class="$style.captcha" provider="mcaptcha" :sitekey="instance.mcaptchaSiteKey" :instanceUrl="instance.mcaptchaInstanceUrl"/>
|
<MkCaptcha v-if="instance.enableMcaptcha" ref="mcaptcha" v-model="mCaptchaResponse" :class="$style.captcha" provider="mcaptcha" :sitekey="instance.mcaptchaSiteKey" :instanceUrl="instance.mcaptchaInstanceUrl"/>
|
||||||
<MkCaptcha v-if="instance.enableRecaptcha" ref="recaptcha" v-model="reCaptchaResponse" :class="$style.captcha" provider="recaptcha" :sitekey="instance.recaptchaSiteKey"/>
|
<MkCaptcha v-if="instance.enableRecaptcha" ref="recaptcha" v-model="reCaptchaResponse" :class="$style.captcha" provider="recaptcha" :sitekey="instance.recaptchaSiteKey"/>
|
||||||
<MkCaptcha v-if="instance.enableTurnstile" ref="turnstile" v-model="turnstileResponse" :class="$style.captcha" provider="turnstile" :sitekey="instance.turnstileSiteKey"/>
|
<MkCaptcha v-if="instance.enableTurnstile" ref="turnstile" v-model="turnstileResponse" :class="$style.captcha" provider="turnstile" :sitekey="instance.turnstileSiteKey"/>
|
||||||
|
<MkCaptcha v-if="instance.enableTestcaptcha" ref="testcaptcha" v-model="testcaptchaResponse" :class="$style.captcha" provider="testcaptcha"/>
|
||||||
<MkButton type="submit" :disabled="shouldDisableSubmitting" large gradate rounded data-cy-signup-submit style="margin: 0 auto;">
|
<MkButton type="submit" :disabled="shouldDisableSubmitting" large gradate rounded data-cy-signup-submit style="margin: 0 auto;">
|
||||||
<template v-if="submitting">
|
<template v-if="submitting">
|
||||||
<MkLoading :em="true" :colored="false"/>
|
<MkLoading :em="true" :colored="false"/>
|
||||||
@@ -108,6 +109,7 @@ const hcaptcha = ref<Captcha | undefined>();
|
|||||||
const mcaptcha = ref<Captcha | undefined>();
|
const mcaptcha = ref<Captcha | undefined>();
|
||||||
const recaptcha = ref<Captcha | undefined>();
|
const recaptcha = ref<Captcha | undefined>();
|
||||||
const turnstile = ref<Captcha | undefined>();
|
const turnstile = ref<Captcha | undefined>();
|
||||||
|
const testcaptcha = ref<Captcha | undefined>();
|
||||||
|
|
||||||
const username = ref<string>('');
|
const username = ref<string>('');
|
||||||
const password = ref<string>('');
|
const password = ref<string>('');
|
||||||
@@ -123,6 +125,7 @@ const hCaptchaResponse = ref<string | null>(null);
|
|||||||
const mCaptchaResponse = ref<string | null>(null);
|
const mCaptchaResponse = ref<string | null>(null);
|
||||||
const reCaptchaResponse = ref<string | null>(null);
|
const reCaptchaResponse = ref<string | null>(null);
|
||||||
const turnstileResponse = ref<string | null>(null);
|
const turnstileResponse = ref<string | null>(null);
|
||||||
|
const testcaptchaResponse = ref<string | null>(null);
|
||||||
const usernameAbortController = ref<null | AbortController>(null);
|
const usernameAbortController = ref<null | AbortController>(null);
|
||||||
const emailAbortController = ref<null | AbortController>(null);
|
const emailAbortController = ref<null | AbortController>(null);
|
||||||
|
|
||||||
@@ -132,6 +135,7 @@ const shouldDisableSubmitting = computed((): boolean => {
|
|||||||
instance.enableMcaptcha && !mCaptchaResponse.value ||
|
instance.enableMcaptcha && !mCaptchaResponse.value ||
|
||||||
instance.enableRecaptcha && !reCaptchaResponse.value ||
|
instance.enableRecaptcha && !reCaptchaResponse.value ||
|
||||||
instance.enableTurnstile && !turnstileResponse.value ||
|
instance.enableTurnstile && !turnstileResponse.value ||
|
||||||
|
instance.enableTestcaptcha && !testcaptchaResponse.value ||
|
||||||
instance.emailRequiredForSignup && emailState.value !== 'ok' ||
|
instance.emailRequiredForSignup && emailState.value !== 'ok' ||
|
||||||
usernameState.value !== 'ok' ||
|
usernameState.value !== 'ok' ||
|
||||||
passwordRetypeState.value !== 'match';
|
passwordRetypeState.value !== 'match';
|
||||||
@@ -259,6 +263,7 @@ async function onSubmit(): Promise<void> {
|
|||||||
'm-captcha-response': mCaptchaResponse.value,
|
'm-captcha-response': mCaptchaResponse.value,
|
||||||
'g-recaptcha-response': reCaptchaResponse.value,
|
'g-recaptcha-response': reCaptchaResponse.value,
|
||||||
'turnstile-response': turnstileResponse.value,
|
'turnstile-response': turnstileResponse.value,
|
||||||
|
'testcaptcha-response': testcaptchaResponse.value,
|
||||||
};
|
};
|
||||||
|
|
||||||
const res = await fetch(`${config.apiUrl}/signup`, {
|
const res = await fetch(`${config.apiUrl}/signup`, {
|
||||||
@@ -301,6 +306,7 @@ function onSignupApiError() {
|
|||||||
mcaptcha.value?.reset?.();
|
mcaptcha.value?.reset?.();
|
||||||
recaptcha.value?.reset?.();
|
recaptcha.value?.reset?.();
|
||||||
turnstile.value?.reset?.();
|
turnstile.value?.reset?.();
|
||||||
|
testcaptcha.value?.reset?.();
|
||||||
|
|
||||||
os.alert({
|
os.alert({
|
||||||
type: 'error',
|
type: 'error',
|
||||||
|
@@ -170,7 +170,7 @@ async function updateAgreeNote(v: boolean) {
|
|||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
display: flex;
|
display: flex;
|
||||||
position: sticky;
|
position: sticky;
|
||||||
top: calc(var(--stickyTop, 0px) + 8px);
|
top: calc(var(--MI-stickyTop, 0px) + 8px);
|
||||||
counter-increment: item;
|
counter-increment: item;
|
||||||
content: counter(item);
|
content: counter(item);
|
||||||
width: 32px;
|
width: 32px;
|
||||||
|
@@ -63,12 +63,12 @@ function close() {
|
|||||||
.root {
|
.root {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
z-index: v-bind(zIndex);
|
z-index: v-bind(zIndex);
|
||||||
bottom: var(--margin);
|
bottom: var(--MI-margin);
|
||||||
left: 0;
|
left: 0;
|
||||||
right: 0;
|
right: 0;
|
||||||
margin: auto;
|
margin: auto;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
width: calc(100% - (var(--margin) * 2));
|
width: calc(100% - (var(--MI-margin) * 2));
|
||||||
max-width: 500px;
|
max-width: 500px;
|
||||||
display: flex;
|
display: flex;
|
||||||
}
|
}
|
||||||
|
@@ -97,7 +97,7 @@ const collapsed = ref(isLong);
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
margin-top: 14px;
|
margin-top: 14px;
|
||||||
position: sticky;
|
position: sticky;
|
||||||
bottom: calc(var(--stickyBottom, 0px) + 14px);
|
bottom: calc(var(--MI-stickyBottom, 0px) + 14px);
|
||||||
}
|
}
|
||||||
|
|
||||||
.showLessLabel {
|
.showLessLabel {
|
||||||
|
@@ -263,8 +263,8 @@ onMounted(async () => {
|
|||||||
padding: 12px;
|
padding: 12px;
|
||||||
border-top: solid 0.5px var(--MI_THEME-divider);
|
border-top: solid 0.5px var(--MI_THEME-divider);
|
||||||
background: var(--MI_THEME-acrylicBg);
|
background: var(--MI_THEME-acrylicBg);
|
||||||
-webkit-backdrop-filter: var(--blur, blur(15px));
|
-webkit-backdrop-filter: var(--MI-blur, blur(15px));
|
||||||
backdrop-filter: var(--blur, blur(15px));
|
backdrop-filter: var(--MI-blur, blur(15px));
|
||||||
}
|
}
|
||||||
|
|
||||||
.switchBox {
|
.switchBox {
|
||||||
|
@@ -226,7 +226,7 @@ onUnmounted(() => {
|
|||||||
|
|
||||||
.mfmPreview {
|
.mfmPreview {
|
||||||
padding: 12px;
|
padding: 12px;
|
||||||
border-radius: var(--radius);
|
border-radius: var(--MI-radius);
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
min-height: 130px;
|
min-height: 130px;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
|
@@ -137,7 +137,7 @@ function enableAll(): void {
|
|||||||
margin: 8px -6px 0;
|
margin: 8px -6px 0;
|
||||||
padding: 24px 6px 6px;
|
padding: 24px 6px 6px;
|
||||||
border: 2px solid var(--MI_THEME-error);
|
border: 2px solid var(--MI_THEME-error);
|
||||||
border-radius: calc(var(--radius) / 2);
|
border-radius: calc(var(--MI-radius) / 2);
|
||||||
}
|
}
|
||||||
|
|
||||||
.adminPermissionsHeader {
|
.adminPermissionsHeader {
|
||||||
|
@@ -105,7 +105,7 @@ function removeReaction(emoji) {
|
|||||||
|
|
||||||
<style lang="scss" module>
|
<style lang="scss" module>
|
||||||
.exampleNoteRoot {
|
.exampleNoteRoot {
|
||||||
border-radius: var(--radius);
|
border-radius: var(--MI-radius);
|
||||||
border: var(--MI_THEME-panelBorder);
|
border: var(--MI_THEME-panelBorder);
|
||||||
background: var(--MI_THEME-panel);
|
background: var(--MI_THEME-panel);
|
||||||
}
|
}
|
||||||
|
@@ -81,7 +81,7 @@ const exampleCWNote = reactive<Misskey.entities.Note>({
|
|||||||
<style lang="scss" module>
|
<style lang="scss" module>
|
||||||
.exampleRoot {
|
.exampleRoot {
|
||||||
max-width: none!important;
|
max-width: none!important;
|
||||||
border-radius: var(--radius);
|
border-radius: var(--MI-radius);
|
||||||
border: var(--MI_THEME-panelBorder);
|
border: var(--MI_THEME-panelBorder);
|
||||||
background: var(--MI_THEME-panel);
|
background: var(--MI_THEME-panel);
|
||||||
}
|
}
|
||||||
|
@@ -91,7 +91,7 @@ const exampleNote = reactive<Misskey.entities.Note>({
|
|||||||
|
|
||||||
<style lang="scss" module>
|
<style lang="scss" module>
|
||||||
.exampleRoot {
|
.exampleRoot {
|
||||||
border-radius: var(--radius);
|
border-radius: var(--MI-radius);
|
||||||
border: var(--MI_THEME-panelBorder);
|
border: var(--MI_THEME-panelBorder);
|
||||||
background: var(--MI_THEME-panel);
|
background: var(--MI_THEME-panel);
|
||||||
}
|
}
|
||||||
|
@@ -31,7 +31,7 @@ import { basicTimelineIconClass, basicTimelineTypes } from '@/timelines.js';
|
|||||||
|
|
||||||
<style lang="scss" module>
|
<style lang="scss" module>
|
||||||
.exampleNoteRoot {
|
.exampleNoteRoot {
|
||||||
border-radius: var(--radius);
|
border-radius: var(--MI-radius);
|
||||||
border: var(--MI_THEME-panelBorder);
|
border: var(--MI_THEME-panelBorder);
|
||||||
background: var(--MI_THEME-panel);
|
background: var(--MI_THEME-panel);
|
||||||
}
|
}
|
||||||
|
@@ -47,7 +47,7 @@ onMounted(() => {
|
|||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
background: var(--MI_THEME-panel);
|
background: var(--MI_THEME-panel);
|
||||||
border-radius: var(--radius);
|
border-radius: var(--MI-radius);
|
||||||
}
|
}
|
||||||
|
|
||||||
.title {
|
.title {
|
||||||
|
@@ -84,13 +84,13 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { defineAsyncComponent, onDeactivated, onUnmounted, ref } from 'vue';
|
import { defineAsyncComponent, onDeactivated, onUnmounted, ref } from 'vue';
|
||||||
import type { summaly } from '@misskey-dev/summaly';
|
|
||||||
import { url as local } from '@@/js/config.js';
|
import { url as local } from '@@/js/config.js';
|
||||||
|
import { versatileLang } from '@@/js/intl-const.js';
|
||||||
|
import type { summaly } from '@misskey-dev/summaly';
|
||||||
import { i18n } from '@/i18n.js';
|
import { i18n } from '@/i18n.js';
|
||||||
import * as os from '@/os.js';
|
import * as os from '@/os.js';
|
||||||
import { deviceKind } from '@/scripts/device-kind.js';
|
import { deviceKind } from '@/scripts/device-kind.js';
|
||||||
import MkButton from '@/components/MkButton.vue';
|
import MkButton from '@/components/MkButton.vue';
|
||||||
import { versatileLang } from '@@/js/intl-const.js';
|
|
||||||
import { transformPlayerUrl } from '@/scripts/player-url-transform.js';
|
import { transformPlayerUrl } from '@/scripts/player-url-transform.js';
|
||||||
import { defaultStore } from '@/store.js';
|
import { defaultStore } from '@/store.js';
|
||||||
|
|
||||||
@@ -317,7 +317,6 @@ onUnmounted(() => {
|
|||||||
.siteName {
|
.siteName {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
color: var(--urlPreviewInfo);
|
|
||||||
font-size: 0.8em;
|
font-size: 0.8em;
|
||||||
line-height: 16px;
|
line-height: 16px;
|
||||||
vertical-align: top;
|
vertical-align: top;
|
||||||
|
@@ -142,7 +142,7 @@ async function del() {
|
|||||||
left: 0;
|
left: 0;
|
||||||
padding: 12px;
|
padding: 12px;
|
||||||
border-top: solid 0.5px var(--MI_THEME-divider);
|
border-top: solid 0.5px var(--MI_THEME-divider);
|
||||||
-webkit-backdrop-filter: var(--blur, blur(15px));
|
-webkit-backdrop-filter: var(--MI-blur, blur(15px));
|
||||||
backdrop-filter: var(--blur, blur(15px));
|
backdrop-filter: var(--MI-blur, blur(15px));
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
@@ -39,6 +39,6 @@ const props = withDefaults(defineProps<{
|
|||||||
.root {
|
.root {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
|
grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
|
||||||
grid-gap: var(--margin);
|
grid-gap: var(--MI-margin);
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
@@ -62,7 +62,7 @@ const popularUsers: Paging = {
|
|||||||
.users {
|
.users {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(auto-fill, minmax(230px, 1fr));
|
grid-template-columns: repeat(auto-fill, minmax(230px, 1fr));
|
||||||
grid-gap: var(--margin);
|
grid-gap: var(--MI-margin);
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
@@ -107,7 +107,7 @@ function showMenu(ev: MouseEvent) {
|
|||||||
.panel {
|
.panel {
|
||||||
position: relative;
|
position: relative;
|
||||||
background: var(--MI_THEME-panel);
|
background: var(--MI_THEME-panel);
|
||||||
border-radius: var(--radius);
|
border-radius: var(--MI-radius);
|
||||||
box-shadow: 0 12px 32px rgb(0 0 0 / 25%);
|
box-shadow: 0 12px 32px rgb(0 0 0 / 25%);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -48,7 +48,7 @@ watch(() => props.showing, () => {
|
|||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
background: var(--MI_THEME-panel);
|
background: var(--MI_THEME-panel);
|
||||||
border-radius: var(--radius);
|
border-radius: var(--MI-radius);
|
||||||
width: 250px;
|
width: 250px;
|
||||||
|
|
||||||
&.iconOnly {
|
&.iconOnly {
|
||||||
|
@@ -7,7 +7,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||||||
<div :class="$style.root">
|
<div :class="$style.root">
|
||||||
<template v-if="edit">
|
<template v-if="edit">
|
||||||
<header :class="$style.editHeader">
|
<header :class="$style.editHeader">
|
||||||
<MkSelect v-model="widgetAdderSelected" style="margin-bottom: var(--margin)" data-cy-widget-select>
|
<MkSelect v-model="widgetAdderSelected" style="margin-bottom: var(--MI-margin)" data-cy-widget-select>
|
||||||
<template #label>{{ i18n.ts.selectWidget }}</template>
|
<template #label>{{ i18n.ts.selectWidget }}</template>
|
||||||
<option v-for="widget in widgetDefs" :key="widget" :value="widget">{{ i18n.ts._widgets[widget] }}</option>
|
<option v-for="widget in widgetDefs" :key="widget" :value="widget">{{ i18n.ts._widgets[widget] }}</option>
|
||||||
</MkSelect>
|
</MkSelect>
|
||||||
@@ -123,7 +123,7 @@ function onContextmenu(widget: Widget, ev: MouseEvent) {
|
|||||||
|
|
||||||
.widget {
|
.widget {
|
||||||
contain: content;
|
contain: content;
|
||||||
margin: var(--margin) 0;
|
margin: var(--MI-margin) 0;
|
||||||
|
|
||||||
&:first-of-type {
|
&:first-of-type {
|
||||||
margin-top: 0;
|
margin-top: 0;
|
||||||
|
@@ -502,7 +502,7 @@ defineExpose({
|
|||||||
contain: content;
|
contain: content;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
border-radius: var(--radius);
|
border-radius: var(--MI-radius);
|
||||||
}
|
}
|
||||||
|
|
||||||
.header {
|
.header {
|
||||||
@@ -515,8 +515,8 @@ defineExpose({
|
|||||||
user-select: none;
|
user-select: none;
|
||||||
height: var(--height);
|
height: var(--height);
|
||||||
background: var(--MI_THEME-windowHeader);
|
background: var(--MI_THEME-windowHeader);
|
||||||
-webkit-backdrop-filter: var(--blur, blur(15px));
|
-webkit-backdrop-filter: var(--MI-blur, blur(15px));
|
||||||
backdrop-filter: var(--blur, blur(15px));
|
backdrop-filter: var(--MI-blur, blur(15px));
|
||||||
//border-bottom: solid 1px var(--MI_THEME-divider);
|
//border-bottom: solid 1px var(--MI_THEME-divider);
|
||||||
font-size: 90%;
|
font-size: 90%;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
|
@@ -43,9 +43,9 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { ref, computed } from 'vue';
|
import { ref, computed } from 'vue';
|
||||||
|
import { url as local, host } from '@@/js/config.js';
|
||||||
import { i18n } from '@/i18n.js';
|
import { i18n } from '@/i18n.js';
|
||||||
import { instance } from '@/instance.js';
|
import { instance } from '@/instance.js';
|
||||||
import { url as local, host } from '@@/js/config.js';
|
|
||||||
import MkButton from '@/components/MkButton.vue';
|
import MkButton from '@/components/MkButton.vue';
|
||||||
import { defaultStore } from '@/store.js';
|
import { defaultStore } from '@/store.js';
|
||||||
import * as os from '@/os.js';
|
import * as os from '@/os.js';
|
||||||
@@ -124,7 +124,7 @@ function reduceFrequency(): void {
|
|||||||
<style lang="scss" module>
|
<style lang="scss" module>
|
||||||
.root {
|
.root {
|
||||||
background-size: auto auto;
|
background-size: auto auto;
|
||||||
background-image: repeating-linear-gradient(45deg, transparent, transparent 8px, var(--ad) 8px, var(--ad) 14px );
|
background-image: repeating-linear-gradient(45deg, transparent, transparent 8px, var(--bg) 8px, var(--bg) 14px );
|
||||||
}
|
}
|
||||||
|
|
||||||
.main {
|
.main {
|
||||||
|
@@ -130,8 +130,8 @@ onUnmounted(() => {
|
|||||||
|
|
||||||
<style lang="scss" module>
|
<style lang="scss" module>
|
||||||
.root {
|
.root {
|
||||||
-webkit-backdrop-filter: var(--blur, blur(15px));
|
-webkit-backdrop-filter: var(--MI-blur, blur(15px));
|
||||||
backdrop-filter: var(--blur, blur(15px));
|
backdrop-filter: var(--MI-blur, blur(15px));
|
||||||
border-bottom: solid 0.5px var(--MI_THEME-divider);
|
border-bottom: solid 0.5px var(--MI_THEME-divider);
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
@@ -145,7 +145,7 @@ onUnmounted(() => {
|
|||||||
.upper {
|
.upper {
|
||||||
--height: 50px;
|
--height: 50px;
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: var(--margin);
|
gap: var(--MI-margin);
|
||||||
height: var(--height);
|
height: var(--height);
|
||||||
|
|
||||||
.tabs:first-child {
|
.tabs:first-child {
|
||||||
|
@@ -69,28 +69,28 @@ onMounted(() => {
|
|||||||
|
|
||||||
watch(childStickyTop, () => {
|
watch(childStickyTop, () => {
|
||||||
if (bodyEl.value == null) return;
|
if (bodyEl.value == null) return;
|
||||||
bodyEl.value.style.setProperty('--stickyTop', `${childStickyTop.value}px`);
|
bodyEl.value.style.setProperty('--MI-stickyTop', `${childStickyTop.value}px`);
|
||||||
}, {
|
}, {
|
||||||
immediate: true,
|
immediate: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
watch(childStickyBottom, () => {
|
watch(childStickyBottom, () => {
|
||||||
if (bodyEl.value == null) return;
|
if (bodyEl.value == null) return;
|
||||||
bodyEl.value.style.setProperty('--stickyBottom', `${childStickyBottom.value}px`);
|
bodyEl.value.style.setProperty('--MI-stickyBottom', `${childStickyBottom.value}px`);
|
||||||
}, {
|
}, {
|
||||||
immediate: true,
|
immediate: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (headerEl.value != null) {
|
if (headerEl.value != null) {
|
||||||
headerEl.value.style.position = 'sticky';
|
headerEl.value.style.position = 'sticky';
|
||||||
headerEl.value.style.top = 'var(--stickyTop, 0)';
|
headerEl.value.style.top = 'var(--MI-stickyTop, 0)';
|
||||||
headerEl.value.style.zIndex = '1';
|
headerEl.value.style.zIndex = '1';
|
||||||
observer.observe(headerEl.value);
|
observer.observe(headerEl.value);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (footerEl.value != null) {
|
if (footerEl.value != null) {
|
||||||
footerEl.value.style.position = 'sticky';
|
footerEl.value.style.position = 'sticky';
|
||||||
footerEl.value.style.bottom = 'var(--stickyBottom, 0)';
|
footerEl.value.style.bottom = 'var(--MI-stickyBottom, 0)';
|
||||||
footerEl.value.style.zIndex = '1';
|
footerEl.value.style.zIndex = '1';
|
||||||
observer.observe(footerEl.value);
|
observer.observe(footerEl.value);
|
||||||
}
|
}
|
||||||
|
@@ -28,8 +28,8 @@ const props = defineProps<{
|
|||||||
<style lang="scss" module>
|
<style lang="scss" module>
|
||||||
.root {
|
.root {
|
||||||
border: 1px solid var(--MI_THEME-divider);
|
border: 1px solid var(--MI_THEME-divider);
|
||||||
border-radius: var(--radius);
|
border-radius: var(--MI-radius);
|
||||||
padding: var(--margin);
|
padding: var(--MI-margin);
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -29,7 +29,7 @@ onMounted(() => {
|
|||||||
<style lang="scss" module>
|
<style lang="scss" module>
|
||||||
.root {
|
.root {
|
||||||
border: 1px solid var(--MI_THEME-divider);
|
border: 1px solid var(--MI_THEME-divider);
|
||||||
border-radius: var(--radius);
|
border-radius: var(--MI-radius);
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
.mediaList {
|
.mediaList {
|
||||||
|
@@ -36,6 +36,6 @@ onMounted(() => {
|
|||||||
<style lang="scss" module>
|
<style lang="scss" module>
|
||||||
.root {
|
.root {
|
||||||
border: 1px solid var(--MI_THEME-divider);
|
border: 1px solid var(--MI_THEME-divider);
|
||||||
border-radius: var(--radius);
|
border-radius: var(--MI-radius);
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
@@ -441,7 +441,7 @@ definePageMetadata(() => ({
|
|||||||
.znqjceqz {
|
.znqjceqz {
|
||||||
> .about {
|
> .about {
|
||||||
position: relative;
|
position: relative;
|
||||||
border-radius: var(--radius);
|
border-radius: var(--MI-radius);
|
||||||
|
|
||||||
> .treasure {
|
> .treasure {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user