feat: 時限ロール (#10145)
* feat: 時限ロール * クライアントから期限を確認できるように * リファクタとか * fix test * fix test * fix test * clean up
This commit is contained in:
		
							parent
							
								
									7c3a390763
								
							
						
					
					
						commit
						1c5291f818
					
				
					 16 changed files with 296 additions and 391 deletions
				
			
		| 
						 | 
					@ -13,6 +13,7 @@ You should also include the user name that made the change.
 | 
				
			||||||
## 13.x.x (unreleased)
 | 
					## 13.x.x (unreleased)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
### Improvements
 | 
					### Improvements
 | 
				
			||||||
 | 
					- 時限ロール
 | 
				
			||||||
- プッシュ通知でカスタム絵文字リアクションを表示できるように
 | 
					- プッシュ通知でカスタム絵文字リアクションを表示できるように
 | 
				
			||||||
- アンテナでCWも検索対象にするように
 | 
					- アンテナでCWも検索対象にするように
 | 
				
			||||||
- ノートの操作部をホバー時のみ表示するオプションを追加
 | 
					- ノートの操作部をホバー時のみ表示するオプションを追加
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -848,11 +848,13 @@ instanceDefaultLightTheme: "インスタンスデフォルトのライトテー
 | 
				
			||||||
instanceDefaultDarkTheme: "インスタンスデフォルトのダークテーマ"
 | 
					instanceDefaultDarkTheme: "インスタンスデフォルトのダークテーマ"
 | 
				
			||||||
instanceDefaultThemeDescription: "オブジェクト形式のテーマコードを記入します。"
 | 
					instanceDefaultThemeDescription: "オブジェクト形式のテーマコードを記入します。"
 | 
				
			||||||
mutePeriod: "ミュートする期限"
 | 
					mutePeriod: "ミュートする期限"
 | 
				
			||||||
 | 
					period: "期限"
 | 
				
			||||||
indefinitely: "無期限"
 | 
					indefinitely: "無期限"
 | 
				
			||||||
tenMinutes: "10分"
 | 
					tenMinutes: "10分"
 | 
				
			||||||
oneHour: "1時間"
 | 
					oneHour: "1時間"
 | 
				
			||||||
oneDay: "1日"
 | 
					oneDay: "1日"
 | 
				
			||||||
oneWeek: "1週間"
 | 
					oneWeek: "1週間"
 | 
				
			||||||
 | 
					oneMonth: "1ヶ月"
 | 
				
			||||||
reflectMayTakeTime: "反映されるまで時間がかかる場合があります。"
 | 
					reflectMayTakeTime: "反映されるまで時間がかかる場合があります。"
 | 
				
			||||||
failedToFetchAccountInformation: "アカウント情報の取得に失敗しました"
 | 
					failedToFetchAccountInformation: "アカウント情報の取得に失敗しました"
 | 
				
			||||||
rateLimitExceeded: "レート制限を超えました"
 | 
					rateLimitExceeded: "レート制限を超えました"
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,13 @@
 | 
				
			||||||
 | 
					export class roleAssignmentExpiresAt1677570181236 {
 | 
				
			||||||
 | 
					    name = 'roleAssignmentExpiresAt1677570181236'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    async up(queryRunner) {
 | 
				
			||||||
 | 
					        await queryRunner.query(`ALTER TABLE "role_assignment" ADD "expiresAt" TIMESTAMP WITH TIME ZONE`);
 | 
				
			||||||
 | 
					        await queryRunner.query(`CREATE INDEX "IDX_539b6c08c05067599743bb6389" ON "role_assignment" ("expiresAt") `);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    async down(queryRunner) {
 | 
				
			||||||
 | 
					        await queryRunner.query(`DROP INDEX "public"."IDX_539b6c08c05067599743bb6389"`);
 | 
				
			||||||
 | 
					        await queryRunner.query(`ALTER TABLE "role_assignment" DROP COLUMN "expiresAt"`);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -11,6 +11,8 @@ import { UserCacheService } from '@/core/UserCacheService.js';
 | 
				
			||||||
import type { RoleCondFormulaValue } from '@/models/entities/Role.js';
 | 
					import type { RoleCondFormulaValue } from '@/models/entities/Role.js';
 | 
				
			||||||
import { UserEntityService } from '@/core/entities/UserEntityService.js';
 | 
					import { UserEntityService } from '@/core/entities/UserEntityService.js';
 | 
				
			||||||
import { StreamMessages } from '@/server/api/stream/types.js';
 | 
					import { StreamMessages } from '@/server/api/stream/types.js';
 | 
				
			||||||
 | 
					import { IdService } from '@/core/IdService.js';
 | 
				
			||||||
 | 
					import { GlobalEventService } from '@/core/GlobalEventService.js';
 | 
				
			||||||
import type { OnApplicationShutdown } from '@nestjs/common';
 | 
					import type { OnApplicationShutdown } from '@nestjs/common';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export type RolePolicies = {
 | 
					export type RolePolicies = {
 | 
				
			||||||
| 
						 | 
					@ -56,6 +58,9 @@ export class RoleService implements OnApplicationShutdown {
 | 
				
			||||||
	private rolesCache: Cache<Role[]>;
 | 
						private rolesCache: Cache<Role[]>;
 | 
				
			||||||
	private roleAssignmentByUserIdCache: Cache<RoleAssignment[]>;
 | 
						private roleAssignmentByUserIdCache: Cache<RoleAssignment[]>;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						public static AlreadyAssignedError = class extends Error {};
 | 
				
			||||||
 | 
						public static NotAssignedError = class extends Error {};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	constructor(
 | 
						constructor(
 | 
				
			||||||
		@Inject(DI.redisSubscriber)
 | 
							@Inject(DI.redisSubscriber)
 | 
				
			||||||
		private redisSubscriber: Redis.Redis,
 | 
							private redisSubscriber: Redis.Redis,
 | 
				
			||||||
| 
						 | 
					@ -72,6 +77,8 @@ export class RoleService implements OnApplicationShutdown {
 | 
				
			||||||
		private metaService: MetaService,
 | 
							private metaService: MetaService,
 | 
				
			||||||
		private userCacheService: UserCacheService,
 | 
							private userCacheService: UserCacheService,
 | 
				
			||||||
		private userEntityService: UserEntityService,
 | 
							private userEntityService: UserEntityService,
 | 
				
			||||||
 | 
							private globalEventService: GlobalEventService,
 | 
				
			||||||
 | 
							private idService: IdService,
 | 
				
			||||||
	) {
 | 
						) {
 | 
				
			||||||
		//this.onMessage = this.onMessage.bind(this);
 | 
							//this.onMessage = this.onMessage.bind(this);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -128,6 +135,7 @@ export class RoleService implements OnApplicationShutdown {
 | 
				
			||||||
						cached.push({
 | 
											cached.push({
 | 
				
			||||||
							...body,
 | 
												...body,
 | 
				
			||||||
							createdAt: new Date(body.createdAt),
 | 
												createdAt: new Date(body.createdAt),
 | 
				
			||||||
 | 
												expiresAt: body.expiresAt ? new Date(body.expiresAt) : null,
 | 
				
			||||||
						});
 | 
											});
 | 
				
			||||||
					}
 | 
										}
 | 
				
			||||||
					break;
 | 
										break;
 | 
				
			||||||
| 
						 | 
					@ -193,7 +201,10 @@ export class RoleService implements OnApplicationShutdown {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	@bindThis
 | 
						@bindThis
 | 
				
			||||||
	public async getUserRoles(userId: User['id']) {
 | 
						public async getUserRoles(userId: User['id']) {
 | 
				
			||||||
		const assigns = await this.roleAssignmentByUserIdCache.fetch(userId, () => this.roleAssignmentsRepository.findBy({ userId }));
 | 
							const now = Date.now();
 | 
				
			||||||
 | 
							let assigns = await this.roleAssignmentByUserIdCache.fetch(userId, () => this.roleAssignmentsRepository.findBy({ userId }));
 | 
				
			||||||
 | 
							// 期限切れのロールを除外
 | 
				
			||||||
 | 
							assigns = assigns.filter(a => a.expiresAt == null || (a.expiresAt.getTime() > now));
 | 
				
			||||||
		const assignedRoleIds = assigns.map(x => x.roleId);
 | 
							const assignedRoleIds = assigns.map(x => x.roleId);
 | 
				
			||||||
		const roles = await this.rolesCache.fetch(null, () => this.rolesRepository.findBy({}));
 | 
							const roles = await this.rolesCache.fetch(null, () => this.rolesRepository.findBy({}));
 | 
				
			||||||
		const assignedRoles = roles.filter(r => assignedRoleIds.includes(r.id));
 | 
							const assignedRoles = roles.filter(r => assignedRoleIds.includes(r.id));
 | 
				
			||||||
| 
						 | 
					@ -207,7 +218,10 @@ export class RoleService implements OnApplicationShutdown {
 | 
				
			||||||
	 */
 | 
						 */
 | 
				
			||||||
	@bindThis
 | 
						@bindThis
 | 
				
			||||||
	public async getUserBadgeRoles(userId: User['id']) {
 | 
						public async getUserBadgeRoles(userId: User['id']) {
 | 
				
			||||||
		const assigns = await this.roleAssignmentByUserIdCache.fetch(userId, () => this.roleAssignmentsRepository.findBy({ userId }));
 | 
							const now = Date.now();
 | 
				
			||||||
 | 
							let assigns = await this.roleAssignmentByUserIdCache.fetch(userId, () => this.roleAssignmentsRepository.findBy({ userId }));
 | 
				
			||||||
 | 
							// 期限切れのロールを除外
 | 
				
			||||||
 | 
							assigns = assigns.filter(a => a.expiresAt == null || (a.expiresAt.getTime() > now));
 | 
				
			||||||
		const assignedRoleIds = assigns.map(x => x.roleId);
 | 
							const assignedRoleIds = assigns.map(x => x.roleId);
 | 
				
			||||||
		const roles = await this.rolesCache.fetch(null, () => this.rolesRepository.findBy({}));
 | 
							const roles = await this.rolesCache.fetch(null, () => this.rolesRepository.findBy({}));
 | 
				
			||||||
		const assignedBadgeRoles = roles.filter(r => r.asBadge && assignedRoleIds.includes(r.id));
 | 
							const assignedBadgeRoles = roles.filter(r => r.asBadge && assignedRoleIds.includes(r.id));
 | 
				
			||||||
| 
						 | 
					@ -316,6 +330,65 @@ export class RoleService implements OnApplicationShutdown {
 | 
				
			||||||
		return users;
 | 
							return users;
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						@bindThis
 | 
				
			||||||
 | 
						public async assign(userId: User['id'], roleId: Role['id'], expiresAt: Date | null = null): Promise<void> {
 | 
				
			||||||
 | 
							const now = new Date();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							const existing = await this.roleAssignmentsRepository.findOneBy({
 | 
				
			||||||
 | 
								roleId: roleId,
 | 
				
			||||||
 | 
								userId: userId,
 | 
				
			||||||
 | 
							});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if (existing) {
 | 
				
			||||||
 | 
								if (existing.expiresAt && (existing.expiresAt.getTime() < now.getTime())) {
 | 
				
			||||||
 | 
									await this.roleAssignmentsRepository.delete({
 | 
				
			||||||
 | 
										roleId: roleId,
 | 
				
			||||||
 | 
										userId: userId,
 | 
				
			||||||
 | 
									});
 | 
				
			||||||
 | 
								} else {
 | 
				
			||||||
 | 
									throw new RoleService.AlreadyAssignedError();
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							const created = await this.roleAssignmentsRepository.insert({
 | 
				
			||||||
 | 
								id: this.idService.genId(),
 | 
				
			||||||
 | 
								createdAt: now,
 | 
				
			||||||
 | 
								expiresAt: expiresAt,
 | 
				
			||||||
 | 
								roleId: roleId,
 | 
				
			||||||
 | 
								userId: userId,
 | 
				
			||||||
 | 
							}).then(x => this.roleAssignmentsRepository.findOneByOrFail(x.identifiers[0]));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							this.rolesRepository.update(roleId, {
 | 
				
			||||||
 | 
								lastUsedAt: new Date(),
 | 
				
			||||||
 | 
							});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							this.globalEventService.publishInternalEvent('userRoleAssigned', created);
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						@bindThis
 | 
				
			||||||
 | 
						public async unassign(userId: User['id'], roleId: Role['id']): Promise<void> {
 | 
				
			||||||
 | 
							const now = new Date();
 | 
				
			||||||
 | 
						
 | 
				
			||||||
 | 
							const existing = await this.roleAssignmentsRepository.findOneBy({ roleId, userId });
 | 
				
			||||||
 | 
							if (existing == null) {
 | 
				
			||||||
 | 
								throw new RoleService.NotAssignedError();
 | 
				
			||||||
 | 
							} else if (existing.expiresAt && (existing.expiresAt.getTime() < now.getTime())) {
 | 
				
			||||||
 | 
								await this.roleAssignmentsRepository.delete({
 | 
				
			||||||
 | 
									roleId: roleId,
 | 
				
			||||||
 | 
									userId: userId,
 | 
				
			||||||
 | 
								});
 | 
				
			||||||
 | 
								throw new RoleService.NotAssignedError();
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							await this.roleAssignmentsRepository.delete(existing.id);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							this.rolesRepository.update(roleId, {
 | 
				
			||||||
 | 
								lastUsedAt: now,
 | 
				
			||||||
 | 
							});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							this.globalEventService.publishInternalEvent('userRoleUnassigned', existing);
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	@bindThis
 | 
						@bindThis
 | 
				
			||||||
	public onApplicationShutdown(signal?: string | undefined) {
 | 
						public onApplicationShutdown(signal?: string | undefined) {
 | 
				
			||||||
		this.redisSubscriber.off('message', this.onMessage);
 | 
							this.redisSubscriber.off('message', this.onMessage);
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,4 +1,5 @@
 | 
				
			||||||
import { Inject, Injectable } from '@nestjs/common';
 | 
					import { Inject, Injectable } from '@nestjs/common';
 | 
				
			||||||
 | 
					import { Brackets } from 'typeorm';
 | 
				
			||||||
import { DI } from '@/di-symbols.js';
 | 
					import { DI } from '@/di-symbols.js';
 | 
				
			||||||
import type { RoleAssignmentsRepository, RolesRepository } from '@/models/index.js';
 | 
					import type { RoleAssignmentsRepository, RolesRepository } from '@/models/index.js';
 | 
				
			||||||
import { awaitAll } from '@/misc/prelude/await-all.js';
 | 
					import { awaitAll } from '@/misc/prelude/await-all.js';
 | 
				
			||||||
| 
						 | 
					@ -28,9 +29,13 @@ export class RoleEntityService {
 | 
				
			||||||
	) {
 | 
						) {
 | 
				
			||||||
		const role = typeof src === 'object' ? src : await this.rolesRepository.findOneByOrFail({ id: src });
 | 
							const role = typeof src === 'object' ? src : await this.rolesRepository.findOneByOrFail({ id: src });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		const assigns = await this.roleAssignmentsRepository.findBy({
 | 
							const assignedCount = await this.roleAssignmentsRepository.createQueryBuilder('assign')
 | 
				
			||||||
			roleId: role.id,
 | 
								.where('assign.roleId = :roleId', { roleId: role.id })
 | 
				
			||||||
		});
 | 
								.andWhere(new Brackets(qb => { qb
 | 
				
			||||||
 | 
									.where('assign.expiresAt IS NOT NULL')
 | 
				
			||||||
 | 
									.orWhere('assign.expiresAt > :now', { now: new Date() });
 | 
				
			||||||
 | 
								}))
 | 
				
			||||||
 | 
								.getCount();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		const policies = { ...role.policies };
 | 
							const policies = { ...role.policies };
 | 
				
			||||||
		for (const [k, v] of Object.entries(DEFAULT_POLICIES)) {
 | 
							for (const [k, v] of Object.entries(DEFAULT_POLICIES)) {
 | 
				
			||||||
| 
						 | 
					@ -57,7 +62,7 @@ export class RoleEntityService {
 | 
				
			||||||
			asBadge: role.asBadge,
 | 
								asBadge: role.asBadge,
 | 
				
			||||||
			canEditMembersByModerator: role.canEditMembersByModerator,
 | 
								canEditMembersByModerator: role.canEditMembersByModerator,
 | 
				
			||||||
			policies: policies,
 | 
								policies: policies,
 | 
				
			||||||
			usersCount: assigns.length,
 | 
								usersCount: assignedCount,
 | 
				
			||||||
		});
 | 
							});
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -39,4 +39,10 @@ export class RoleAssignment {
 | 
				
			||||||
	})
 | 
						})
 | 
				
			||||||
	@JoinColumn()
 | 
						@JoinColumn()
 | 
				
			||||||
	public role: Role | null;
 | 
						public role: Role | null;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						@Index()
 | 
				
			||||||
 | 
						@Column('timestamp with time zone', {
 | 
				
			||||||
 | 
							nullable: true,
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
						public expiresAt: Date | null;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,7 +1,7 @@
 | 
				
			||||||
import { Inject, Injectable } from '@nestjs/common';
 | 
					import { Inject, Injectable } from '@nestjs/common';
 | 
				
			||||||
import { LessThan } from 'typeorm';
 | 
					import { In, LessThan } from 'typeorm';
 | 
				
			||||||
import { DI } from '@/di-symbols.js';
 | 
					import { DI } from '@/di-symbols.js';
 | 
				
			||||||
import type { AntennaNotesRepository, MutedNotesRepository, NotificationsRepository, UserIpsRepository } from '@/models/index.js';
 | 
					import type { AntennaNotesRepository, MutedNotesRepository, NotificationsRepository, RoleAssignmentsRepository, UserIpsRepository } from '@/models/index.js';
 | 
				
			||||||
import type { Config } from '@/config.js';
 | 
					import type { Config } from '@/config.js';
 | 
				
			||||||
import type Logger from '@/logger.js';
 | 
					import type Logger from '@/logger.js';
 | 
				
			||||||
import { bindThis } from '@/decorators.js';
 | 
					import { bindThis } from '@/decorators.js';
 | 
				
			||||||
| 
						 | 
					@ -29,6 +29,9 @@ export class CleanProcessorService {
 | 
				
			||||||
		@Inject(DI.antennaNotesRepository)
 | 
							@Inject(DI.antennaNotesRepository)
 | 
				
			||||||
		private antennaNotesRepository: AntennaNotesRepository,
 | 
							private antennaNotesRepository: AntennaNotesRepository,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							@Inject(DI.roleAssignmentsRepository)
 | 
				
			||||||
 | 
							private roleAssignmentsRepository: RoleAssignmentsRepository,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		private queueLoggerService: QueueLoggerService,
 | 
							private queueLoggerService: QueueLoggerService,
 | 
				
			||||||
		private idService: IdService,
 | 
							private idService: IdService,
 | 
				
			||||||
	) {
 | 
						) {
 | 
				
			||||||
| 
						 | 
					@ -56,6 +59,17 @@ export class CleanProcessorService {
 | 
				
			||||||
			id: LessThan(this.idService.genId(new Date(Date.now() - (1000 * 60 * 60 * 24 * 90)))),
 | 
								id: LessThan(this.idService.genId(new Date(Date.now() - (1000 * 60 * 60 * 24 * 90)))),
 | 
				
			||||||
		});
 | 
							});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							const expiredRoleAssignments = await this.roleAssignmentsRepository.createQueryBuilder('assign')
 | 
				
			||||||
 | 
								.where('assign.expiresAt IS NOT NULL')
 | 
				
			||||||
 | 
								.andWhere('assign.expiresAt < :now', { now: new Date() })
 | 
				
			||||||
 | 
								.getMany();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if (expiredRoleAssignments.length > 0) {
 | 
				
			||||||
 | 
								await this.roleAssignmentsRepository.delete({
 | 
				
			||||||
 | 
									id: In(expiredRoleAssignments.map(x => x.id)),
 | 
				
			||||||
 | 
								});
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		this.logger.succ('Cleaned.');
 | 
							this.logger.succ('Cleaned.');
 | 
				
			||||||
		done();
 | 
							done();
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,10 +1,8 @@
 | 
				
			||||||
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 type { RoleAssignmentsRepository, RolesRepository, UsersRepository } from '@/models/index.js';
 | 
					import type { RolesRepository, UsersRepository } from '@/models/index.js';
 | 
				
			||||||
import { DI } from '@/di-symbols.js';
 | 
					import { DI } from '@/di-symbols.js';
 | 
				
			||||||
import { ApiError } from '@/server/api/error.js';
 | 
					import { ApiError } from '@/server/api/error.js';
 | 
				
			||||||
import { IdService } from '@/core/IdService.js';
 | 
					 | 
				
			||||||
import { GlobalEventService } from '@/core/GlobalEventService.js';
 | 
					 | 
				
			||||||
import { RoleService } from '@/core/RoleService.js';
 | 
					import { RoleService } from '@/core/RoleService.js';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const meta = {
 | 
					export const meta = {
 | 
				
			||||||
| 
						 | 
					@ -39,6 +37,10 @@ export const paramDef = {
 | 
				
			||||||
	properties: {
 | 
						properties: {
 | 
				
			||||||
		roleId: { type: 'string', format: 'misskey:id' },
 | 
							roleId: { type: 'string', format: 'misskey:id' },
 | 
				
			||||||
		userId: { type: 'string', format: 'misskey:id' },
 | 
							userId: { type: 'string', format: 'misskey:id' },
 | 
				
			||||||
 | 
							expiresAt: {
 | 
				
			||||||
 | 
								type: 'integer',
 | 
				
			||||||
 | 
								nullable: true,
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
	},
 | 
						},
 | 
				
			||||||
	required: [
 | 
						required: [
 | 
				
			||||||
		'roleId',
 | 
							'roleId',
 | 
				
			||||||
| 
						 | 
					@ -56,12 +58,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
 | 
				
			||||||
		@Inject(DI.rolesRepository)
 | 
							@Inject(DI.rolesRepository)
 | 
				
			||||||
		private rolesRepository: RolesRepository,
 | 
							private rolesRepository: RolesRepository,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		@Inject(DI.roleAssignmentsRepository)
 | 
					 | 
				
			||||||
		private roleAssignmentsRepository: RoleAssignmentsRepository,
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		private globalEventService: GlobalEventService,
 | 
					 | 
				
			||||||
		private roleService: RoleService,
 | 
							private roleService: RoleService,
 | 
				
			||||||
		private idService: IdService,
 | 
					 | 
				
			||||||
	) {
 | 
						) {
 | 
				
			||||||
		super(meta, paramDef, async (ps, me) => {
 | 
							super(meta, paramDef, async (ps, me) => {
 | 
				
			||||||
			const role = await this.rolesRepository.findOneBy({ id: ps.roleId });
 | 
								const role = await this.rolesRepository.findOneBy({ id: ps.roleId });
 | 
				
			||||||
| 
						 | 
					@ -78,19 +75,11 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
 | 
				
			||||||
				throw new ApiError(meta.errors.noSuchUser);
 | 
									throw new ApiError(meta.errors.noSuchUser);
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			const date = new Date();
 | 
								if (ps.expiresAt && ps.expiresAt <= Date.now()) {
 | 
				
			||||||
			const created = await this.roleAssignmentsRepository.insert({
 | 
									return;
 | 
				
			||||||
				id: this.idService.genId(),
 | 
								}
 | 
				
			||||||
				createdAt: date,
 | 
					 | 
				
			||||||
				roleId: role.id,
 | 
					 | 
				
			||||||
				userId: user.id,
 | 
					 | 
				
			||||||
			}).then(x => this.roleAssignmentsRepository.findOneByOrFail(x.identifiers[0]));
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
			this.rolesRepository.update(ps.roleId, {
 | 
								await this.roleService.assign(user.id, role.id, ps.expiresAt ? new Date(ps.expiresAt) : null);
 | 
				
			||||||
				lastUsedAt: new Date(),
 | 
					 | 
				
			||||||
			});
 | 
					 | 
				
			||||||
	
 | 
					 | 
				
			||||||
			this.globalEventService.publishInternalEvent('userRoleAssigned', created);
 | 
					 | 
				
			||||||
		});
 | 
							});
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,10 +1,8 @@
 | 
				
			||||||
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 type { RoleAssignmentsRepository, RolesRepository, UsersRepository } from '@/models/index.js';
 | 
					import type { RolesRepository, UsersRepository } from '@/models/index.js';
 | 
				
			||||||
import { DI } from '@/di-symbols.js';
 | 
					import { DI } from '@/di-symbols.js';
 | 
				
			||||||
import { ApiError } from '@/server/api/error.js';
 | 
					import { ApiError } from '@/server/api/error.js';
 | 
				
			||||||
import { IdService } from '@/core/IdService.js';
 | 
					 | 
				
			||||||
import { GlobalEventService } from '@/core/GlobalEventService.js';
 | 
					 | 
				
			||||||
import { RoleService } from '@/core/RoleService.js';
 | 
					import { RoleService } from '@/core/RoleService.js';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const meta = {
 | 
					export const meta = {
 | 
				
			||||||
| 
						 | 
					@ -62,12 +60,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
 | 
				
			||||||
		@Inject(DI.rolesRepository)
 | 
							@Inject(DI.rolesRepository)
 | 
				
			||||||
		private rolesRepository: RolesRepository,
 | 
							private rolesRepository: RolesRepository,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		@Inject(DI.roleAssignmentsRepository)
 | 
					 | 
				
			||||||
		private roleAssignmentsRepository: RoleAssignmentsRepository,
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		private globalEventService: GlobalEventService,
 | 
					 | 
				
			||||||
		private roleService: RoleService,
 | 
							private roleService: RoleService,
 | 
				
			||||||
		private idService: IdService,
 | 
					 | 
				
			||||||
	) {
 | 
						) {
 | 
				
			||||||
		super(meta, paramDef, async (ps, me) => {
 | 
							super(meta, paramDef, async (ps, me) => {
 | 
				
			||||||
			const role = await this.rolesRepository.findOneBy({ id: ps.roleId });
 | 
								const role = await this.rolesRepository.findOneBy({ id: ps.roleId });
 | 
				
			||||||
| 
						 | 
					@ -84,18 +77,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
 | 
				
			||||||
				throw new ApiError(meta.errors.noSuchUser);
 | 
									throw new ApiError(meta.errors.noSuchUser);
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			const roleAssignment = await this.roleAssignmentsRepository.findOneBy({ userId: user.id, roleId: role.id });
 | 
								await this.roleService.unassign(user.id, role.id);
 | 
				
			||||||
			if (roleAssignment == null) {
 | 
					 | 
				
			||||||
				throw new ApiError(meta.errors.notAssigned);
 | 
					 | 
				
			||||||
			}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
			await this.roleAssignmentsRepository.delete(roleAssignment.id);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
			this.rolesRepository.update(ps.roleId, {
 | 
					 | 
				
			||||||
				lastUsedAt: new Date(),
 | 
					 | 
				
			||||||
			});
 | 
					 | 
				
			||||||
	
 | 
					 | 
				
			||||||
			this.globalEventService.publishInternalEvent('userRoleUnassigned', roleAssignment);
 | 
					 | 
				
			||||||
		});
 | 
							});
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,4 +1,5 @@
 | 
				
			||||||
import { Inject, Injectable } from '@nestjs/common';
 | 
					import { Inject, Injectable } from '@nestjs/common';
 | 
				
			||||||
 | 
					import { Brackets } from 'typeorm';
 | 
				
			||||||
import type { RoleAssignmentsRepository, RolesRepository } from '@/models/index.js';
 | 
					import type { RoleAssignmentsRepository, RolesRepository } from '@/models/index.js';
 | 
				
			||||||
import { Endpoint } from '@/server/api/endpoint-base.js';
 | 
					import { Endpoint } from '@/server/api/endpoint-base.js';
 | 
				
			||||||
import { QueryService } from '@/core/QueryService.js';
 | 
					import { QueryService } from '@/core/QueryService.js';
 | 
				
			||||||
| 
						 | 
					@ -56,6 +57,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			const query = this.queryService.makePaginationQuery(this.roleAssignmentsRepository.createQueryBuilder('assign'), ps.sinceId, ps.untilId)
 | 
								const query = this.queryService.makePaginationQuery(this.roleAssignmentsRepository.createQueryBuilder('assign'), ps.sinceId, ps.untilId)
 | 
				
			||||||
				.andWhere('assign.roleId = :roleId', { roleId: role.id })
 | 
									.andWhere('assign.roleId = :roleId', { roleId: role.id })
 | 
				
			||||||
 | 
									.andWhere(new Brackets(qb => { qb
 | 
				
			||||||
 | 
										.where('assign.expiresAt IS NOT NULL')
 | 
				
			||||||
 | 
										.orWhere('assign.expiresAt > :now', { now: new Date() });
 | 
				
			||||||
 | 
									}))
 | 
				
			||||||
				.innerJoinAndSelect('assign.user', 'user');
 | 
									.innerJoinAndSelect('assign.user', 'user');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			const assigns = await query
 | 
								const assigns = await query
 | 
				
			||||||
| 
						 | 
					@ -65,6 +70,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
 | 
				
			||||||
			return await Promise.all(assigns.map(async assign => ({
 | 
								return await Promise.all(assigns.map(async assign => ({
 | 
				
			||||||
				id: assign.id,
 | 
									id: assign.id,
 | 
				
			||||||
				user: await this.userEntityService.pack(assign.user!, me, { detail: true }),
 | 
									user: await this.userEntityService.pack(assign.user!, me, { detail: true }),
 | 
				
			||||||
 | 
									expiresAt: assign.expiresAt,
 | 
				
			||||||
			})));
 | 
								})));
 | 
				
			||||||
		});
 | 
							});
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,4 +1,5 @@
 | 
				
			||||||
import { Inject, Injectable } from '@nestjs/common';
 | 
					import { Inject, Injectable } from '@nestjs/common';
 | 
				
			||||||
 | 
					import { Brackets } from 'typeorm';
 | 
				
			||||||
import type { RoleAssignmentsRepository, RolesRepository } from '@/models/index.js';
 | 
					import type { RoleAssignmentsRepository, RolesRepository } from '@/models/index.js';
 | 
				
			||||||
import { Endpoint } from '@/server/api/endpoint-base.js';
 | 
					import { Endpoint } from '@/server/api/endpoint-base.js';
 | 
				
			||||||
import { QueryService } from '@/core/QueryService.js';
 | 
					import { QueryService } from '@/core/QueryService.js';
 | 
				
			||||||
| 
						 | 
					@ -56,6 +57,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			const query = this.queryService.makePaginationQuery(this.roleAssignmentsRepository.createQueryBuilder('assign'), ps.sinceId, ps.untilId)
 | 
								const query = this.queryService.makePaginationQuery(this.roleAssignmentsRepository.createQueryBuilder('assign'), ps.sinceId, ps.untilId)
 | 
				
			||||||
				.andWhere('assign.roleId = :roleId', { roleId: role.id })
 | 
									.andWhere('assign.roleId = :roleId', { roleId: role.id })
 | 
				
			||||||
 | 
									.andWhere(new Brackets(qb => { qb
 | 
				
			||||||
 | 
										.where('assign.expiresAt IS NOT NULL')
 | 
				
			||||||
 | 
										.orWhere('assign.expiresAt > :now', { now: new Date() });
 | 
				
			||||||
 | 
									}))
 | 
				
			||||||
				.innerJoinAndSelect('assign.user', 'user');
 | 
									.innerJoinAndSelect('assign.user', 'user');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			const assigns = await query
 | 
								const assigns = await query
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -3,16 +3,18 @@ process.env.NODE_ENV = 'test';
 | 
				
			||||||
import { jest } from '@jest/globals';
 | 
					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 { DataSource } from 'typeorm';
 | 
					import * as lolex from '@sinonjs/fake-timers';
 | 
				
			||||||
import rndstr from 'rndstr';
 | 
					import rndstr from 'rndstr';
 | 
				
			||||||
import { GlobalModule } from '@/GlobalModule.js';
 | 
					import { GlobalModule } from '@/GlobalModule.js';
 | 
				
			||||||
import { RoleService } from '@/core/RoleService.js';
 | 
					import { RoleService } from '@/core/RoleService.js';
 | 
				
			||||||
import type { Role, RolesRepository, RoleAssignmentsRepository, UsersRepository, User } from '@/models/index.js';
 | 
					import type { Role, RolesRepository, RoleAssignmentsRepository, UsersRepository, User } from '@/models/index.js';
 | 
				
			||||||
import { DI } from '@/di-symbols.js';
 | 
					import { DI } from '@/di-symbols.js';
 | 
				
			||||||
import { CoreModule } from '@/core/CoreModule.js';
 | 
					 | 
				
			||||||
import { MetaService } from '@/core/MetaService.js';
 | 
					import { MetaService } from '@/core/MetaService.js';
 | 
				
			||||||
import { genAid } from '@/misc/id/aid.js';
 | 
					import { genAid } from '@/misc/id/aid.js';
 | 
				
			||||||
import { UserCacheService } from '@/core/UserCacheService.js';
 | 
					import { UserCacheService } from '@/core/UserCacheService.js';
 | 
				
			||||||
 | 
					import { IdService } from '@/core/IdService.js';
 | 
				
			||||||
 | 
					import { GlobalEventService } from '@/core/GlobalEventService.js';
 | 
				
			||||||
 | 
					import { sleep } from '../utils.js';
 | 
				
			||||||
import type { TestingModule } from '@nestjs/testing';
 | 
					import type { TestingModule } from '@nestjs/testing';
 | 
				
			||||||
import type { MockFunctionMetadata } from 'jest-mock';
 | 
					import type { MockFunctionMetadata } from 'jest-mock';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -25,6 +27,7 @@ describe('RoleService', () => {
 | 
				
			||||||
	let rolesRepository: RolesRepository;
 | 
						let rolesRepository: RolesRepository;
 | 
				
			||||||
	let roleAssignmentsRepository: RoleAssignmentsRepository;
 | 
						let roleAssignmentsRepository: RoleAssignmentsRepository;
 | 
				
			||||||
	let metaService: jest.Mocked<MetaService>;
 | 
						let metaService: jest.Mocked<MetaService>;
 | 
				
			||||||
 | 
						let clock: lolex.InstalledClock;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	function createUser(data: Partial<User> = {}) {
 | 
						function createUser(data: Partial<User> = {}) {
 | 
				
			||||||
		const un = rndstr('a-z0-9', 16);
 | 
							const un = rndstr('a-z0-9', 16);
 | 
				
			||||||
| 
						 | 
					@ -50,16 +53,12 @@ describe('RoleService', () => {
 | 
				
			||||||
			.then(x => rolesRepository.findOneByOrFail(x.identifiers[0]));
 | 
								.then(x => rolesRepository.findOneByOrFail(x.identifiers[0]));
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	async function assign(roleId: Role['id'], userId: User['id']) {
 | 
					 | 
				
			||||||
		await roleAssignmentsRepository.insert({
 | 
					 | 
				
			||||||
			id: genAid(new Date()),
 | 
					 | 
				
			||||||
			createdAt: new Date(),
 | 
					 | 
				
			||||||
			roleId,
 | 
					 | 
				
			||||||
			userId,
 | 
					 | 
				
			||||||
		});
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	beforeEach(async () => {
 | 
						beforeEach(async () => {
 | 
				
			||||||
 | 
							clock = lolex.install({
 | 
				
			||||||
 | 
								now: new Date(),
 | 
				
			||||||
 | 
								shouldClearNativeTimers: true,
 | 
				
			||||||
 | 
							});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		app = await Test.createTestingModule({
 | 
							app = await Test.createTestingModule({
 | 
				
			||||||
			imports: [
 | 
								imports: [
 | 
				
			||||||
				GlobalModule,
 | 
									GlobalModule,
 | 
				
			||||||
| 
						 | 
					@ -67,6 +66,8 @@ describe('RoleService', () => {
 | 
				
			||||||
			providers: [
 | 
								providers: [
 | 
				
			||||||
				RoleService,
 | 
									RoleService,
 | 
				
			||||||
				UserCacheService,
 | 
									UserCacheService,
 | 
				
			||||||
 | 
									IdService,
 | 
				
			||||||
 | 
									GlobalEventService,
 | 
				
			||||||
			],
 | 
								],
 | 
				
			||||||
		})
 | 
							})
 | 
				
			||||||
			.useMocker((token) => {
 | 
								.useMocker((token) => {
 | 
				
			||||||
| 
						 | 
					@ -92,12 +93,15 @@ describe('RoleService', () => {
 | 
				
			||||||
	});
 | 
						});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	afterEach(async () => {
 | 
						afterEach(async () => {
 | 
				
			||||||
 | 
							clock.uninstall();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		await Promise.all([
 | 
							await Promise.all([
 | 
				
			||||||
			app.get(DI.metasRepository).delete({}),
 | 
								app.get(DI.metasRepository).delete({}),
 | 
				
			||||||
			usersRepository.delete({}),
 | 
								usersRepository.delete({}),
 | 
				
			||||||
			rolesRepository.delete({}),
 | 
								rolesRepository.delete({}),
 | 
				
			||||||
			roleAssignmentsRepository.delete({}),
 | 
								roleAssignmentsRepository.delete({}),
 | 
				
			||||||
		]);
 | 
							]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		await app.close();
 | 
							await app.close();
 | 
				
			||||||
	});
 | 
						});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -115,7 +119,7 @@ describe('RoleService', () => {
 | 
				
			||||||
			expect(result.canManageCustomEmojis).toBe(false);
 | 
								expect(result.canManageCustomEmojis).toBe(false);
 | 
				
			||||||
		});
 | 
							});
 | 
				
			||||||
	
 | 
						
 | 
				
			||||||
		test('instance default policies 2', async () => {	
 | 
							test('instance default policies 2', async () => {
 | 
				
			||||||
			const user = await createUser();
 | 
								const user = await createUser();
 | 
				
			||||||
			metaService.fetch.mockResolvedValue({
 | 
								metaService.fetch.mockResolvedValue({
 | 
				
			||||||
				policies: {
 | 
									policies: {
 | 
				
			||||||
| 
						 | 
					@ -128,7 +132,7 @@ describe('RoleService', () => {
 | 
				
			||||||
			expect(result.canManageCustomEmojis).toBe(true);
 | 
								expect(result.canManageCustomEmojis).toBe(true);
 | 
				
			||||||
		});
 | 
							});
 | 
				
			||||||
	
 | 
						
 | 
				
			||||||
		test('with role', async () => {	
 | 
							test('with role', async () => {
 | 
				
			||||||
			const user = await createUser();
 | 
								const user = await createUser();
 | 
				
			||||||
			const role = await createRole({
 | 
								const role = await createRole({
 | 
				
			||||||
				name: 'a',
 | 
									name: 'a',
 | 
				
			||||||
| 
						 | 
					@ -140,7 +144,7 @@ describe('RoleService', () => {
 | 
				
			||||||
					},
 | 
										},
 | 
				
			||||||
				},
 | 
									},
 | 
				
			||||||
			});
 | 
								});
 | 
				
			||||||
			await assign(role.id, user.id);
 | 
								await roleService.assign(user.id, role.id);
 | 
				
			||||||
			metaService.fetch.mockResolvedValue({
 | 
								metaService.fetch.mockResolvedValue({
 | 
				
			||||||
				policies: {
 | 
									policies: {
 | 
				
			||||||
					canManageCustomEmojis: false,
 | 
										canManageCustomEmojis: false,
 | 
				
			||||||
| 
						 | 
					@ -152,7 +156,7 @@ describe('RoleService', () => {
 | 
				
			||||||
			expect(result.canManageCustomEmojis).toBe(true);
 | 
								expect(result.canManageCustomEmojis).toBe(true);
 | 
				
			||||||
		});
 | 
							});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		test('priority', async () => {	
 | 
							test('priority', async () => {
 | 
				
			||||||
			const user = await createUser();
 | 
								const user = await createUser();
 | 
				
			||||||
			const role1 = await createRole({
 | 
								const role1 = await createRole({
 | 
				
			||||||
				name: 'role1',
 | 
									name: 'role1',
 | 
				
			||||||
| 
						 | 
					@ -174,8 +178,8 @@ describe('RoleService', () => {
 | 
				
			||||||
					},
 | 
										},
 | 
				
			||||||
				},
 | 
									},
 | 
				
			||||||
			});
 | 
								});
 | 
				
			||||||
			await assign(role1.id, user.id);
 | 
								await roleService.assign(user.id, role1.id);
 | 
				
			||||||
			await assign(role2.id, user.id);
 | 
								await roleService.assign(user.id, role2.id);
 | 
				
			||||||
			metaService.fetch.mockResolvedValue({
 | 
								metaService.fetch.mockResolvedValue({
 | 
				
			||||||
				policies: {
 | 
									policies: {
 | 
				
			||||||
					driveCapacityMb: 50,
 | 
										driveCapacityMb: 50,
 | 
				
			||||||
| 
						 | 
					@ -187,7 +191,7 @@ describe('RoleService', () => {
 | 
				
			||||||
			expect(result.driveCapacityMb).toBe(100);
 | 
								expect(result.driveCapacityMb).toBe(100);
 | 
				
			||||||
		});
 | 
							});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		test('conditional role', async () => {	
 | 
							test('conditional role', async () => {
 | 
				
			||||||
			const user1 = await createUser({
 | 
								const user1 = await createUser({
 | 
				
			||||||
				createdAt: new Date(Date.now() - (1000 * 60 * 60 * 24 * 365)),
 | 
									createdAt: new Date(Date.now() - (1000 * 60 * 60 * 24 * 365)),
 | 
				
			||||||
			});
 | 
								});
 | 
				
			||||||
| 
						 | 
					@ -228,5 +232,42 @@ describe('RoleService', () => {
 | 
				
			||||||
			expect(user1Policies.canManageCustomEmojis).toBe(false);
 | 
								expect(user1Policies.canManageCustomEmojis).toBe(false);
 | 
				
			||||||
			expect(user2Policies.canManageCustomEmojis).toBe(true);
 | 
								expect(user2Policies.canManageCustomEmojis).toBe(true);
 | 
				
			||||||
		});
 | 
							});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							test('expired role', async () => {
 | 
				
			||||||
 | 
								const user = await createUser();
 | 
				
			||||||
 | 
								const role = await createRole({
 | 
				
			||||||
 | 
									name: 'a',
 | 
				
			||||||
 | 
									policies: {
 | 
				
			||||||
 | 
										canManageCustomEmojis: {
 | 
				
			||||||
 | 
											useDefault: false,
 | 
				
			||||||
 | 
											priority: 0,
 | 
				
			||||||
 | 
											value: true,
 | 
				
			||||||
 | 
										},
 | 
				
			||||||
 | 
									},
 | 
				
			||||||
 | 
								});
 | 
				
			||||||
 | 
								await roleService.assign(user.id, role.id, new Date(Date.now() + (1000 * 60 * 60 * 24)));
 | 
				
			||||||
 | 
								metaService.fetch.mockResolvedValue({
 | 
				
			||||||
 | 
									policies: {
 | 
				
			||||||
 | 
										canManageCustomEmojis: false,
 | 
				
			||||||
 | 
									},
 | 
				
			||||||
 | 
								} as any);
 | 
				
			||||||
 | 
						
 | 
				
			||||||
 | 
								const result = await roleService.getUserPolicies(user.id);
 | 
				
			||||||
 | 
								expect(result.canManageCustomEmojis).toBe(true);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								clock.tick('25:00:00');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								const resultAfter25h = await roleService.getUserPolicies(user.id);
 | 
				
			||||||
 | 
								expect(resultAfter25h.canManageCustomEmojis).toBe(false);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								await roleService.assign(user.id, role.id);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								// ストリーミング経由で反映されるまでちょっと待つ
 | 
				
			||||||
 | 
								clock.uninstall();
 | 
				
			||||||
 | 
								await sleep(100);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								const resultAfter25hAgain = await roleService.getUserPolicies(user.id);
 | 
				
			||||||
 | 
								expect(resultAfter25hAgain.canManageCustomEmojis).toBe(true);
 | 
				
			||||||
 | 
							});
 | 
				
			||||||
	});
 | 
						});
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,320 +1,3 @@
 | 
				
			||||||
import * as fs from 'node:fs';
 | 
					 | 
				
			||||||
import * as path from 'node:path';
 | 
					 | 
				
			||||||
import { fileURLToPath } from 'node:url';
 | 
					 | 
				
			||||||
import { dirname } from 'node:path';
 | 
					 | 
				
			||||||
import * as childProcess from 'child_process';
 | 
					 | 
				
			||||||
import * as http from 'node:http';
 | 
					 | 
				
			||||||
import { SIGKILL } from 'constants';
 | 
					 | 
				
			||||||
import WebSocket from 'ws';
 | 
					 | 
				
			||||||
import fetch from 'node-fetch';
 | 
					 | 
				
			||||||
import FormData from 'form-data';
 | 
					 | 
				
			||||||
import { DataSource } from 'typeorm';
 | 
					 | 
				
			||||||
import got, { RequestError } from 'got';
 | 
					 | 
				
			||||||
import loadConfig from '../src/config/load.js';
 | 
					 | 
				
			||||||
import { entities } from '@/postgres.js';
 | 
					 | 
				
			||||||
import type * as misskey from 'misskey-js';
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const _filename = fileURLToPath(import.meta.url);
 | 
					 | 
				
			||||||
const _dirname = dirname(_filename);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const config = loadConfig();
 | 
					 | 
				
			||||||
export const port = config.port;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export const api = async (endpoint: string, params: any, me?: any) => {
 | 
					 | 
				
			||||||
	endpoint = endpoint.replace(/^\//, '');
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	const auth = me ? {
 | 
					 | 
				
			||||||
		i: me.token,
 | 
					 | 
				
			||||||
	} : {};
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	try {
 | 
					 | 
				
			||||||
		const res = await got<string>(`http://localhost:${port}/api/${endpoint}`, {
 | 
					 | 
				
			||||||
			method: 'POST',
 | 
					 | 
				
			||||||
			headers: {
 | 
					 | 
				
			||||||
				'Content-Type': 'application/json',
 | 
					 | 
				
			||||||
			},
 | 
					 | 
				
			||||||
			body: JSON.stringify(Object.assign(auth, params)),
 | 
					 | 
				
			||||||
			retry: {
 | 
					 | 
				
			||||||
				limit: 0,
 | 
					 | 
				
			||||||
			},
 | 
					 | 
				
			||||||
		});
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		const status = res.statusCode;
 | 
					 | 
				
			||||||
		const body = res.statusCode !== 204 ? await JSON.parse(res.body) : null;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		return {
 | 
					 | 
				
			||||||
			status,
 | 
					 | 
				
			||||||
			body,
 | 
					 | 
				
			||||||
		};
 | 
					 | 
				
			||||||
	} catch (err: unknown) {
 | 
					 | 
				
			||||||
		if (err instanceof RequestError && err.response) {
 | 
					 | 
				
			||||||
			const status = err.response.statusCode;
 | 
					 | 
				
			||||||
			const body = await JSON.parse(err.response.body as string);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
			return {
 | 
					 | 
				
			||||||
				status,
 | 
					 | 
				
			||||||
				body,
 | 
					 | 
				
			||||||
			};
 | 
					 | 
				
			||||||
		} else {
 | 
					 | 
				
			||||||
			throw err;
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
};
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export const request = async (path: string, params: any, me?: any): Promise<{ body: any, status: number }> => {
 | 
					 | 
				
			||||||
	const auth = me ? {
 | 
					 | 
				
			||||||
		i: me.token,
 | 
					 | 
				
			||||||
	} : {};
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	const res = await fetch(`http://localhost:${port}/${path}`, {
 | 
					 | 
				
			||||||
		method: 'POST',
 | 
					 | 
				
			||||||
		headers: {
 | 
					 | 
				
			||||||
			'Content-Type': 'application/json',
 | 
					 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
		body: JSON.stringify(Object.assign(auth, params)),
 | 
					 | 
				
			||||||
	});
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	const status = res.status;
 | 
					 | 
				
			||||||
	const body = res.status === 200 ? await res.json().catch() : null;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	return {
 | 
					 | 
				
			||||||
		body, status,
 | 
					 | 
				
			||||||
	};
 | 
					 | 
				
			||||||
};
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export const signup = async (params?: any): Promise<any> => {
 | 
					 | 
				
			||||||
	const q = Object.assign({
 | 
					 | 
				
			||||||
		username: 'test',
 | 
					 | 
				
			||||||
		password: 'test',
 | 
					 | 
				
			||||||
	}, params);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	const res = await api('signup', q);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	return res.body;
 | 
					 | 
				
			||||||
};
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export const post = async (user: any, params?: misskey.Endpoints['notes/create']['req']): Promise<misskey.entities.Note> => {
 | 
					 | 
				
			||||||
	const q = Object.assign({
 | 
					 | 
				
			||||||
		text: 'test',
 | 
					 | 
				
			||||||
	}, params);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	const res = await api('notes/create', q, user);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	return res.body ? res.body.createdNote : null;
 | 
					 | 
				
			||||||
};
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export const react = async (user: any, note: any, reaction: string): Promise<any> => {
 | 
					 | 
				
			||||||
	await api('notes/reactions/create', {
 | 
					 | 
				
			||||||
		noteId: note.id,
 | 
					 | 
				
			||||||
		reaction: reaction,
 | 
					 | 
				
			||||||
	}, user);
 | 
					 | 
				
			||||||
};
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
/**
 | 
					 | 
				
			||||||
 * Upload file
 | 
					 | 
				
			||||||
 * @param user User
 | 
					 | 
				
			||||||
 * @param _path Optional, absolute path or relative from ./resources/
 | 
					 | 
				
			||||||
 */
 | 
					 | 
				
			||||||
export const uploadFile = async (user: any, _path?: string): Promise<any> => {
 | 
					 | 
				
			||||||
	const absPath = _path == null ? `${_dirname}/resources/Lenna.jpg` : path.isAbsolute(_path) ? _path : `${_dirname}/resources/${_path}`;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	const formData = new FormData() as any;
 | 
					 | 
				
			||||||
	formData.append('i', user.token);
 | 
					 | 
				
			||||||
	formData.append('file', fs.createReadStream(absPath));
 | 
					 | 
				
			||||||
	formData.append('force', 'true');
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	const res = await got<string>(`http://localhost:${port}/api/drive/files/create`, {
 | 
					 | 
				
			||||||
		method: 'POST',
 | 
					 | 
				
			||||||
		body: formData,
 | 
					 | 
				
			||||||
		retry: {
 | 
					 | 
				
			||||||
			limit: 0,
 | 
					 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
	});
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	const body = res.statusCode !== 204 ? await JSON.parse(res.body) : null;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	return body;
 | 
					 | 
				
			||||||
};
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export const uploadUrl = async (user: any, url: string) => {
 | 
					 | 
				
			||||||
	let file: any;
 | 
					 | 
				
			||||||
	const marker = Math.random().toString();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	const ws = await connectStream(user, 'main', (msg) => {
 | 
					 | 
				
			||||||
		if (msg.type === 'urlUploadFinished' && msg.body.marker === marker) {
 | 
					 | 
				
			||||||
			file = msg.body.file;
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
	});
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	await api('drive/files/upload-from-url', {
 | 
					 | 
				
			||||||
		url,
 | 
					 | 
				
			||||||
		marker,
 | 
					 | 
				
			||||||
		force: true,
 | 
					 | 
				
			||||||
	}, user);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	await sleep(7000);
 | 
					 | 
				
			||||||
	ws.close();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	return file;
 | 
					 | 
				
			||||||
};
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export function connectStream(user: any, channel: string, listener: (message: Record<string, any>) => any, params?: any): Promise<WebSocket> {
 | 
					 | 
				
			||||||
	return new Promise((res, rej) => {
 | 
					 | 
				
			||||||
		const ws = new WebSocket(`ws://localhost:${port}/streaming?i=${user.token}`);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		ws.on('open', () => {
 | 
					 | 
				
			||||||
			ws.on('message', data => {
 | 
					 | 
				
			||||||
				const msg = JSON.parse(data.toString());
 | 
					 | 
				
			||||||
				if (msg.type === 'channel' && msg.body.id === 'a') {
 | 
					 | 
				
			||||||
					listener(msg.body);
 | 
					 | 
				
			||||||
				} else if (msg.type === 'connected' && msg.body.id === 'a') {
 | 
					 | 
				
			||||||
					res(ws);
 | 
					 | 
				
			||||||
				}
 | 
					 | 
				
			||||||
			});
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
			ws.send(JSON.stringify({
 | 
					 | 
				
			||||||
				type: 'connect',
 | 
					 | 
				
			||||||
				body: {
 | 
					 | 
				
			||||||
					channel: channel,
 | 
					 | 
				
			||||||
					id: 'a',
 | 
					 | 
				
			||||||
					pong: true,
 | 
					 | 
				
			||||||
					params: params,
 | 
					 | 
				
			||||||
				},
 | 
					 | 
				
			||||||
			}));
 | 
					 | 
				
			||||||
		});
 | 
					 | 
				
			||||||
	});
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export const waitFire = async (user: any, channel: string, trgr: () => any, cond: (msg: Record<string, any>) => boolean, params?: any) => {
 | 
					 | 
				
			||||||
	return new Promise<boolean>(async (res, rej) => {
 | 
					 | 
				
			||||||
		let timer: NodeJS.Timeout;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		let ws: WebSocket;
 | 
					 | 
				
			||||||
		try {
 | 
					 | 
				
			||||||
			ws = await connectStream(user, channel, msg => {
 | 
					 | 
				
			||||||
				if (cond(msg)) {
 | 
					 | 
				
			||||||
					ws.close();
 | 
					 | 
				
			||||||
					if (timer) clearTimeout(timer);
 | 
					 | 
				
			||||||
					res(true);
 | 
					 | 
				
			||||||
				}
 | 
					 | 
				
			||||||
			}, params);
 | 
					 | 
				
			||||||
		} catch (e) {
 | 
					 | 
				
			||||||
			rej(e);
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		if (!ws!) return;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		timer = setTimeout(() => {
 | 
					 | 
				
			||||||
			ws.close();
 | 
					 | 
				
			||||||
			res(false);
 | 
					 | 
				
			||||||
		}, 3000);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		try {
 | 
					 | 
				
			||||||
			await trgr();
 | 
					 | 
				
			||||||
		} catch (e) {
 | 
					 | 
				
			||||||
			ws.close();
 | 
					 | 
				
			||||||
			if (timer) clearTimeout(timer);
 | 
					 | 
				
			||||||
			rej(e);
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
	});
 | 
					 | 
				
			||||||
};
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export const simpleGet = async (path: string, accept = '*/*'): Promise<{ status?: number, type?: string, location?: string }> => {
 | 
					 | 
				
			||||||
	// node-fetchだと3xxを取れない
 | 
					 | 
				
			||||||
	return await new Promise((resolve, reject) => {
 | 
					 | 
				
			||||||
		const req = http.request(`http://localhost:${port}${path}`, {
 | 
					 | 
				
			||||||
			headers: {
 | 
					 | 
				
			||||||
				Accept: accept,
 | 
					 | 
				
			||||||
			},
 | 
					 | 
				
			||||||
		}, res => {
 | 
					 | 
				
			||||||
			if (res.statusCode! >= 400) {
 | 
					 | 
				
			||||||
				reject(res);
 | 
					 | 
				
			||||||
			} else {
 | 
					 | 
				
			||||||
				resolve({
 | 
					 | 
				
			||||||
					status: res.statusCode,
 | 
					 | 
				
			||||||
					type: res.headers['content-type'],
 | 
					 | 
				
			||||||
					location: res.headers.location,
 | 
					 | 
				
			||||||
				});
 | 
					 | 
				
			||||||
			}
 | 
					 | 
				
			||||||
		});
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		req.end();
 | 
					 | 
				
			||||||
	});
 | 
					 | 
				
			||||||
};
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export function launchServer(callbackSpawnedProcess: (p: childProcess.ChildProcess) => void, moreProcess: () => Promise<void> = async () => {}) {
 | 
					 | 
				
			||||||
	return (done: (err?: Error) => any) => {
 | 
					 | 
				
			||||||
		const p = childProcess.spawn('node', [_dirname + '/../index.js'], {
 | 
					 | 
				
			||||||
			stdio: ['inherit', 'inherit', 'inherit', 'ipc'],
 | 
					 | 
				
			||||||
			env: { NODE_ENV: 'test', PATH: process.env.PATH },
 | 
					 | 
				
			||||||
		});
 | 
					 | 
				
			||||||
		callbackSpawnedProcess(p);
 | 
					 | 
				
			||||||
		p.on('message', message => {
 | 
					 | 
				
			||||||
			if (message === 'ok') moreProcess().then(() => done()).catch(e => done(e));
 | 
					 | 
				
			||||||
		});
 | 
					 | 
				
			||||||
	};
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export async function initTestDb(justBorrow = false, initEntities?: any[]) {
 | 
					 | 
				
			||||||
	if (process.env.NODE_ENV !== 'test') throw 'NODE_ENV is not a test';
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	const db = new DataSource({
 | 
					 | 
				
			||||||
		type: 'postgres',
 | 
					 | 
				
			||||||
		host: config.db.host,
 | 
					 | 
				
			||||||
		port: config.db.port,
 | 
					 | 
				
			||||||
		username: config.db.user,
 | 
					 | 
				
			||||||
		password: config.db.pass,
 | 
					 | 
				
			||||||
		database: config.db.db,
 | 
					 | 
				
			||||||
		synchronize: true && !justBorrow,
 | 
					 | 
				
			||||||
		dropSchema: true && !justBorrow,
 | 
					 | 
				
			||||||
		entities: initEntities ?? entities,
 | 
					 | 
				
			||||||
	});
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	await db.initialize();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	return db;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export function startServer(timeout = 60 * 1000): Promise<childProcess.ChildProcess> {
 | 
					 | 
				
			||||||
	return new Promise((res, rej) => {
 | 
					 | 
				
			||||||
		const t = setTimeout(() => {
 | 
					 | 
				
			||||||
			p.kill(SIGKILL);
 | 
					 | 
				
			||||||
			rej('timeout to start');
 | 
					 | 
				
			||||||
		}, timeout);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		const p = childProcess.spawn('node', [_dirname + '/../built/index.js'], {
 | 
					 | 
				
			||||||
			stdio: ['inherit', 'inherit', 'inherit', 'ipc'],
 | 
					 | 
				
			||||||
			env: { NODE_ENV: 'test', PATH: process.env.PATH },
 | 
					 | 
				
			||||||
		});
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		p.on('error', e => rej(e));
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		p.on('message', message => {
 | 
					 | 
				
			||||||
			if (message === 'ok') {
 | 
					 | 
				
			||||||
				clearTimeout(t);
 | 
					 | 
				
			||||||
				res(p);
 | 
					 | 
				
			||||||
			}
 | 
					 | 
				
			||||||
		});
 | 
					 | 
				
			||||||
	});
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export function shutdownServer(p: childProcess.ChildProcess | undefined, timeout = 20 * 1000) {
 | 
					 | 
				
			||||||
	if (p == null) return Promise.resolve('nop');
 | 
					 | 
				
			||||||
	return new Promise((res, rej) => {
 | 
					 | 
				
			||||||
		const t = setTimeout(() => {
 | 
					 | 
				
			||||||
			p.kill(SIGKILL);
 | 
					 | 
				
			||||||
			res('force exit');
 | 
					 | 
				
			||||||
		}, timeout);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		p.once('exit', () => {
 | 
					 | 
				
			||||||
			clearTimeout(t);
 | 
					 | 
				
			||||||
			res('exited');
 | 
					 | 
				
			||||||
		});
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		p.kill();
 | 
					 | 
				
			||||||
	});
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export function sleep(msec: number) {
 | 
					export function sleep(msec: number) {
 | 
				
			||||||
	return new Promise<void>(res => {
 | 
						return new Promise<void>(res => {
 | 
				
			||||||
		setTimeout(() => {
 | 
							setTimeout(() => {
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -34,6 +34,7 @@
 | 
				
			||||||
										<MkA :class="$style.user" :to="`/user-info/${item.user.id}`">
 | 
															<MkA :class="$style.user" :to="`/user-info/${item.user.id}`">
 | 
				
			||||||
											<MkUserCardMini :user="item.user"/>
 | 
																<MkUserCardMini :user="item.user"/>
 | 
				
			||||||
										</MkA>
 | 
															</MkA>
 | 
				
			||||||
 | 
															<button v-if="item.expiresAt != null" class="_button" :class="$style.expiresAt" @click="showExpireInfo(item, $event)"><i class="ti ti-clock-hour-3"></i></button>
 | 
				
			||||||
										<button class="_button" :class="$style.unassign" @click="unassign(item.user, $event)"><i class="ti ti-x"></i></button>
 | 
															<button class="_button" :class="$style.unassign" @click="unassign(item.user, $event)"><i class="ti ti-x"></i></button>
 | 
				
			||||||
									</div>
 | 
														</div>
 | 
				
			||||||
								</div>
 | 
													</div>
 | 
				
			||||||
| 
						 | 
					@ -98,13 +99,37 @@ async function del() {
 | 
				
			||||||
	router.push('/admin/roles');
 | 
						router.push('/admin/roles');
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function assign() {
 | 
					async function assign() {
 | 
				
			||||||
	os.selectUser({
 | 
						const user = await os.selectUser({
 | 
				
			||||||
		includeSelf: true,
 | 
							includeSelf: true,
 | 
				
			||||||
	}).then(async (user) => {
 | 
					 | 
				
			||||||
		await os.apiWithDialog('admin/roles/assign', { roleId: role.id, userId: user.id });
 | 
					 | 
				
			||||||
		role.users.push(user);
 | 
					 | 
				
			||||||
	});
 | 
						});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						const { canceled: canceled2, result: period } = await os.select({
 | 
				
			||||||
 | 
							title: i18n.ts.period,
 | 
				
			||||||
 | 
							items: [{
 | 
				
			||||||
 | 
								value: 'indefinitely', text: i18n.ts.indefinitely,
 | 
				
			||||||
 | 
							}, {
 | 
				
			||||||
 | 
								value: 'oneHour', text: i18n.ts.oneHour,
 | 
				
			||||||
 | 
							}, {
 | 
				
			||||||
 | 
								value: 'oneDay', text: i18n.ts.oneDay,
 | 
				
			||||||
 | 
							}, {
 | 
				
			||||||
 | 
								value: 'oneWeek', text: i18n.ts.oneWeek,
 | 
				
			||||||
 | 
							}, {
 | 
				
			||||||
 | 
								value: 'oneMonth', text: i18n.ts.oneMonth,
 | 
				
			||||||
 | 
							}],
 | 
				
			||||||
 | 
							default: 'indefinitely',
 | 
				
			||||||
 | 
						});
 | 
				
			||||||
 | 
						if (canceled2) return;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						const expiresAt = period === 'indefinitely' ? null
 | 
				
			||||||
 | 
							: period === 'oneHour' ? Date.now() + (1000 * 60 * 60)
 | 
				
			||||||
 | 
							: period === 'oneDay' ? Date.now() + (1000 * 60 * 60 * 24)
 | 
				
			||||||
 | 
							: period === 'oneWeek' ? Date.now() + (1000 * 60 * 60 * 24 * 7)
 | 
				
			||||||
 | 
							: period === 'oneMonth' ? Date.now() + (1000 * 60 * 60 * 24 * 30)
 | 
				
			||||||
 | 
							: null;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						await os.apiWithDialog('admin/roles/assign', { roleId: role.id, userId: user.id, expiresAt });
 | 
				
			||||||
 | 
						role.users.push(user);
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
async function unassign(user, ev) {
 | 
					async function unassign(user, ev) {
 | 
				
			||||||
| 
						 | 
					@ -119,6 +144,13 @@ async function unassign(user, ev) {
 | 
				
			||||||
	}], ev.currentTarget ?? ev.target);
 | 
						}], ev.currentTarget ?? ev.target);
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					async function showExpireInfo(assignment) {
 | 
				
			||||||
 | 
						os.alert({
 | 
				
			||||||
 | 
							type: 'info',
 | 
				
			||||||
 | 
							text: assignment.expiresAt.toLocaleString(),
 | 
				
			||||||
 | 
						});
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const headerActions = $computed(() => []);
 | 
					const headerActions = $computed(() => []);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const headerTabs = $computed(() => []);
 | 
					const headerTabs = $computed(() => []);
 | 
				
			||||||
| 
						 | 
					@ -139,10 +171,15 @@ definePageMetadata(computed(() => ({
 | 
				
			||||||
	min-width: 0;
 | 
						min-width: 0;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.expiresAt,
 | 
				
			||||||
.unassign {
 | 
					.unassign {
 | 
				
			||||||
	width: 32px;
 | 
						width: 32px;
 | 
				
			||||||
	height: 32px;
 | 
						height: 32px;
 | 
				
			||||||
	margin-left: 8px;
 | 
						margin-left: 8px;
 | 
				
			||||||
	align-self: center;
 | 
						align-self: center;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.expiresAt + .unassign {
 | 
				
			||||||
 | 
						margin-left: 0;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
</style>
 | 
					</style>
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -337,7 +337,31 @@ async function assignRole() {
 | 
				
			||||||
	});
 | 
						});
 | 
				
			||||||
	if (canceled) return;
 | 
						if (canceled) return;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	await os.apiWithDialog('admin/roles/assign', { roleId, userId: user.id });
 | 
						const { canceled: canceled2, result: period } = await os.select({
 | 
				
			||||||
 | 
							title: i18n.ts.period,
 | 
				
			||||||
 | 
							items: [{
 | 
				
			||||||
 | 
								value: 'indefinitely', text: i18n.ts.indefinitely,
 | 
				
			||||||
 | 
							}, {
 | 
				
			||||||
 | 
								value: 'oneHour', text: i18n.ts.oneHour,
 | 
				
			||||||
 | 
							}, {
 | 
				
			||||||
 | 
								value: 'oneDay', text: i18n.ts.oneDay,
 | 
				
			||||||
 | 
							}, {
 | 
				
			||||||
 | 
								value: 'oneWeek', text: i18n.ts.oneWeek,
 | 
				
			||||||
 | 
							}, {
 | 
				
			||||||
 | 
								value: 'oneMonth', text: i18n.ts.oneMonth,
 | 
				
			||||||
 | 
							}],
 | 
				
			||||||
 | 
							default: 'indefinitely',
 | 
				
			||||||
 | 
						});
 | 
				
			||||||
 | 
						if (canceled2) return;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						const expiresAt = period === 'indefinitely' ? null
 | 
				
			||||||
 | 
							: period === 'oneHour' ? Date.now() + (1000 * 60 * 60)
 | 
				
			||||||
 | 
							: period === 'oneDay' ? Date.now() + (1000 * 60 * 60 * 24)
 | 
				
			||||||
 | 
							: period === 'oneWeek' ? Date.now() + (1000 * 60 * 60 * 24 * 7)
 | 
				
			||||||
 | 
							: period === 'oneMonth' ? Date.now() + (1000 * 60 * 60 * 24 * 30)
 | 
				
			||||||
 | 
							: null;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						await os.apiWithDialog('admin/roles/assign', { roleId, userId: user.id, expiresAt });
 | 
				
			||||||
	refreshUser();
 | 
						refreshUser();
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -143,8 +143,32 @@ export function getUserMenu(user: misskey.entities.UserDetailed, router: Router
 | 
				
			||||||
 | 
					
 | 
				
			||||||
					return roles.filter(r => r.target === 'manual').map(r => ({
 | 
										return roles.filter(r => r.target === 'manual').map(r => ({
 | 
				
			||||||
						text: r.name,
 | 
											text: r.name,
 | 
				
			||||||
						action: () => {
 | 
											action: async () => {
 | 
				
			||||||
							os.apiWithDialog('admin/roles/assign', { roleId: r.id, userId: user.id });
 | 
												const { canceled, result: period } = await os.select({
 | 
				
			||||||
 | 
													title: i18n.ts.period,
 | 
				
			||||||
 | 
													items: [{
 | 
				
			||||||
 | 
														value: 'indefinitely', text: i18n.ts.indefinitely,
 | 
				
			||||||
 | 
													}, {
 | 
				
			||||||
 | 
														value: 'oneHour', text: i18n.ts.oneHour,
 | 
				
			||||||
 | 
													}, {
 | 
				
			||||||
 | 
														value: 'oneDay', text: i18n.ts.oneDay,
 | 
				
			||||||
 | 
													}, {
 | 
				
			||||||
 | 
														value: 'oneWeek', text: i18n.ts.oneWeek,
 | 
				
			||||||
 | 
													}, {
 | 
				
			||||||
 | 
														value: 'oneMonth', text: i18n.ts.oneMonth,
 | 
				
			||||||
 | 
													}],
 | 
				
			||||||
 | 
													default: 'indefinitely',
 | 
				
			||||||
 | 
												});
 | 
				
			||||||
 | 
												if (canceled) return;
 | 
				
			||||||
 | 
											
 | 
				
			||||||
 | 
												const expiresAt = period === 'indefinitely' ? null
 | 
				
			||||||
 | 
													: period === 'oneHour' ? Date.now() + (1000 * 60 * 60)
 | 
				
			||||||
 | 
													: period === 'oneDay' ? Date.now() + (1000 * 60 * 60 * 24)
 | 
				
			||||||
 | 
													: period === 'oneWeek' ? Date.now() + (1000 * 60 * 60 * 24 * 7)
 | 
				
			||||||
 | 
													: period === 'oneMonth' ? Date.now() + (1000 * 60 * 60 * 24 * 30)
 | 
				
			||||||
 | 
													: null;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
												os.apiWithDialog('admin/roles/assign', { roleId: r.id, userId: user.id, expiresAt });
 | 
				
			||||||
						},
 | 
											},
 | 
				
			||||||
					}));
 | 
										}));
 | 
				
			||||||
				},
 | 
									},
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue