Skip to content

Commit

Permalink
feat: 時限ロール (misskey-dev#10145)
Browse files Browse the repository at this point in the history
* feat: 時限ロール

* クライアントから期限を確認できるように

* リファクタとか

* fix test

* fix test

* fix test

* clean up
  • Loading branch information
syuilo authored Mar 1, 2023
1 parent 7c3a390 commit 1c5291f
Show file tree
Hide file tree
Showing 16 changed files with 295 additions and 390 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ You should also include the user name that made the change.
## 13.x.x (unreleased)

### Improvements
- 時限ロール
- プッシュ通知でカスタム絵文字リアクションを表示できるように
- アンテナでCWも検索対象にするように
- ノートの操作部をホバー時のみ表示するオプションを追加
Expand Down
2 changes: 2 additions & 0 deletions locales/ja-JP.yml
Original file line number Diff line number Diff line change
Expand Up @@ -848,11 +848,13 @@ instanceDefaultLightTheme: "インスタンスデフォルトのライトテー
instanceDefaultDarkTheme: "インスタンスデフォルトのダークテーマ"
instanceDefaultThemeDescription: "オブジェクト形式のテーマコードを記入します。"
mutePeriod: "ミュートする期限"
period: "期限"
indefinitely: "無期限"
tenMinutes: "10分"
oneHour: "1時間"
oneDay: "1日"
oneWeek: "1週間"
oneMonth: "1ヶ月"
reflectMayTakeTime: "反映されるまで時間がかかる場合があります。"
failedToFetchAccountInformation: "アカウント情報の取得に失敗しました"
rateLimitExceeded: "レート制限を超えました"
Expand Down
Original file line number Diff line number Diff line change
@@ -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"`);
}
}
77 changes: 75 additions & 2 deletions packages/backend/src/core/RoleService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ import { UserCacheService } from '@/core/UserCacheService.js';
import type { RoleCondFormulaValue } from '@/models/entities/Role.js';
import { UserEntityService } from '@/core/entities/UserEntityService.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';

export type RolePolicies = {
Expand Down Expand Up @@ -56,6 +58,9 @@ export class RoleService implements OnApplicationShutdown {
private rolesCache: Cache<Role[]>;
private roleAssignmentByUserIdCache: Cache<RoleAssignment[]>;

public static AlreadyAssignedError = class extends Error {};
public static NotAssignedError = class extends Error {};

constructor(
@Inject(DI.redisSubscriber)
private redisSubscriber: Redis.Redis,
Expand All @@ -72,6 +77,8 @@ export class RoleService implements OnApplicationShutdown {
private metaService: MetaService,
private userCacheService: UserCacheService,
private userEntityService: UserEntityService,
private globalEventService: GlobalEventService,
private idService: IdService,
) {
//this.onMessage = this.onMessage.bind(this);

Expand Down Expand Up @@ -128,6 +135,7 @@ export class RoleService implements OnApplicationShutdown {
cached.push({
...body,
createdAt: new Date(body.createdAt),
expiresAt: body.expiresAt ? new Date(body.expiresAt) : null,
});
}
break;
Expand Down Expand Up @@ -193,7 +201,10 @@ export class RoleService implements OnApplicationShutdown {

@bindThis
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 roles = await this.rolesCache.fetch(null, () => this.rolesRepository.findBy({}));
const assignedRoles = roles.filter(r => assignedRoleIds.includes(r.id));
Expand All @@ -207,7 +218,10 @@ export class RoleService implements OnApplicationShutdown {
*/
@bindThis
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 roles = await this.rolesCache.fetch(null, () => this.rolesRepository.findBy({}));
const assignedBadgeRoles = roles.filter(r => r.asBadge && assignedRoleIds.includes(r.id));
Expand Down Expand Up @@ -316,6 +330,65 @@ export class RoleService implements OnApplicationShutdown {
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
public onApplicationShutdown(signal?: string | undefined) {
this.redisSubscriber.off('message', this.onMessage);
Expand Down
13 changes: 9 additions & 4 deletions packages/backend/src/core/entities/RoleEntityService.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Inject, Injectable } from '@nestjs/common';
import { Brackets } from 'typeorm';
import { DI } from '@/di-symbols.js';
import type { RoleAssignmentsRepository, RolesRepository } from '@/models/index.js';
import { awaitAll } from '@/misc/prelude/await-all.js';
Expand Down Expand Up @@ -28,9 +29,13 @@ export class RoleEntityService {
) {
const role = typeof src === 'object' ? src : await this.rolesRepository.findOneByOrFail({ id: src });

const assigns = await this.roleAssignmentsRepository.findBy({
roleId: role.id,
});
const assignedCount = await this.roleAssignmentsRepository.createQueryBuilder('assign')
.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 };
for (const [k, v] of Object.entries(DEFAULT_POLICIES)) {
Expand All @@ -57,7 +62,7 @@ export class RoleEntityService {
asBadge: role.asBadge,
canEditMembersByModerator: role.canEditMembersByModerator,
policies: policies,
usersCount: assigns.length,
usersCount: assignedCount,
});
}

Expand Down
6 changes: 6 additions & 0 deletions packages/backend/src/models/entities/RoleAssignment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,4 +39,10 @@ export class RoleAssignment {
})
@JoinColumn()
public role: Role | null;

@Index()
@Column('timestamp with time zone', {
nullable: true,
})
public expiresAt: Date | null;
}
18 changes: 16 additions & 2 deletions packages/backend/src/queue/processors/CleanProcessorService.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Inject, Injectable } from '@nestjs/common';
import { LessThan } from 'typeorm';
import { In, LessThan } from 'typeorm';
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 Logger from '@/logger.js';
import { bindThis } from '@/decorators.js';
Expand Down Expand Up @@ -29,6 +29,9 @@ export class CleanProcessorService {
@Inject(DI.antennaNotesRepository)
private antennaNotesRepository: AntennaNotesRepository,

@Inject(DI.roleAssignmentsRepository)
private roleAssignmentsRepository: RoleAssignmentsRepository,

private queueLoggerService: QueueLoggerService,
private idService: IdService,
) {
Expand Down Expand Up @@ -56,6 +59,17 @@ export class CleanProcessorService {
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.');
done();
}
Expand Down
29 changes: 9 additions & 20 deletions packages/backend/src/server/api/endpoints/admin/roles/assign.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
import { Inject, Injectable } from '@nestjs/common';
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 { ApiError } from '@/server/api/error.js';
import { IdService } from '@/core/IdService.js';
import { GlobalEventService } from '@/core/GlobalEventService.js';
import { RoleService } from '@/core/RoleService.js';

export const meta = {
Expand Down Expand Up @@ -39,6 +37,10 @@ export const paramDef = {
properties: {
roleId: { type: 'string', format: 'misskey:id' },
userId: { type: 'string', format: 'misskey:id' },
expiresAt: {
type: 'integer',
nullable: true,
},
},
required: [
'roleId',
Expand All @@ -56,12 +58,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
@Inject(DI.rolesRepository)
private rolesRepository: RolesRepository,

@Inject(DI.roleAssignmentsRepository)
private roleAssignmentsRepository: RoleAssignmentsRepository,

private globalEventService: GlobalEventService,
private roleService: RoleService,
private idService: IdService,
) {
super(meta, paramDef, async (ps, me) => {
const role = await this.rolesRepository.findOneBy({ id: ps.roleId });
Expand All @@ -78,19 +75,11 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
throw new ApiError(meta.errors.noSuchUser);
}

const date = new Date();
const created = await this.roleAssignmentsRepository.insert({
id: this.idService.genId(),
createdAt: date,
roleId: role.id,
userId: user.id,
}).then(x => this.roleAssignmentsRepository.findOneByOrFail(x.identifiers[0]));
if (ps.expiresAt && ps.expiresAt <= Date.now()) {
return;
}

this.rolesRepository.update(ps.roleId, {
lastUsedAt: new Date(),
});

this.globalEventService.publishInternalEvent('userRoleAssigned', created);
await this.roleService.assign(user.id, role.id, ps.expiresAt ? new Date(ps.expiresAt) : null);
});
}
}
22 changes: 2 additions & 20 deletions packages/backend/src/server/api/endpoints/admin/roles/unassign.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
import { Inject, Injectable } from '@nestjs/common';
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 { ApiError } from '@/server/api/error.js';
import { IdService } from '@/core/IdService.js';
import { GlobalEventService } from '@/core/GlobalEventService.js';
import { RoleService } from '@/core/RoleService.js';

export const meta = {
Expand Down Expand Up @@ -62,12 +60,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
@Inject(DI.rolesRepository)
private rolesRepository: RolesRepository,

@Inject(DI.roleAssignmentsRepository)
private roleAssignmentsRepository: RoleAssignmentsRepository,

private globalEventService: GlobalEventService,
private roleService: RoleService,
private idService: IdService,
) {
super(meta, paramDef, async (ps, me) => {
const role = await this.rolesRepository.findOneBy({ id: ps.roleId });
Expand All @@ -84,18 +77,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
throw new ApiError(meta.errors.noSuchUser);
}

const roleAssignment = await this.roleAssignmentsRepository.findOneBy({ userId: user.id, roleId: 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);
await this.roleService.unassign(user.id, role.id);
});
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Inject, Injectable } from '@nestjs/common';
import { Brackets } from 'typeorm';
import type { RoleAssignmentsRepository, RolesRepository } from '@/models/index.js';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { QueryService } from '@/core/QueryService.js';
Expand Down Expand Up @@ -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)
.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');

const assigns = await query
Expand All @@ -65,6 +70,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
return await Promise.all(assigns.map(async assign => ({
id: assign.id,
user: await this.userEntityService.pack(assign.user!, me, { detail: true }),
expiresAt: assign.expiresAt,
})));
});
}
Expand Down
5 changes: 5 additions & 0 deletions packages/backend/src/server/api/endpoints/roles/users.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Inject, Injectable } from '@nestjs/common';
import { Brackets } from 'typeorm';
import type { RoleAssignmentsRepository, RolesRepository } from '@/models/index.js';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { QueryService } from '@/core/QueryService.js';
Expand Down Expand Up @@ -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)
.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');

const assigns = await query
Expand Down
Loading

0 comments on commit 1c5291f

Please sign in to comment.