hippofish/packages/backend/src/core/RoleService.ts

637 lines
21 KiB
TypeScript
Raw Normal View History

/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { Inject, Injectable } from '@nestjs/common';
2023-04-14 06:50:05 +02:00
import * as Redis from 'ioredis';
import { In } from 'typeorm';
import { ModuleRef } from '@nestjs/core';
import type {
MiRole,
MiRoleAssignment,
RoleAssignmentsRepository,
RolesRepository,
UsersRepository,
} from '@/models/_.js';
import { MemoryKVCache, MemorySingleCache } from '@/misc/cache.js';
import type { MiUser } from '@/models/User.js';
import { DI } from '@/di-symbols.js';
import { bindThis } from '@/decorators.js';
import { MetaService } from '@/core/MetaService.js';
2023-04-04 10:32:09 +02:00
import { CacheService } from '@/core/CacheService.js';
import type { RoleCondFormulaValue } from '@/models/Role.js';
2023-01-13 03:03:54 +01:00
import { UserEntityService } from '@/core/entities/UserEntityService.js';
2023-09-29 04:29:54 +02:00
import type { GlobalEvents } from '@/core/GlobalEventService.js';
import { GlobalEventService } from '@/core/GlobalEventService.js';
import { IdService } from '@/core/IdService.js';
import { ModerationLogService } from '@/core/ModerationLogService.js';
import type { Packed } from '@/misc/json-schema.js';
import { FanoutTimelineService } from '@/core/FanoutTimelineService.js';
import { NotificationService } from '@/core/NotificationService.js';
import type { OnApplicationShutdown, OnModuleInit } from '@nestjs/common';
export type RolePolicies = {
gtlAvailable: boolean;
ltlAvailable: boolean;
canPublicNote: boolean;
mentionLimit: number;
canInvite: boolean;
enhance: 招待機能の改善 (#11195) * refactor(backend): 招待機能を改修 * feat(backend): 招待コードのcreate/delete/listエンドポイントを追加 * add(misskey-js): エンドポイントと型を追加 * change(backend): metaでinvite関連の情報も返すように * add(misskey-js): エンドポイントと型を追加 * add(backend): `/endpoints/invite/limit`を追加 * fix: createdByがnullableではなかったのを修正 * fix: relationが取得できていなかった問題を修正 * fix: パラメータを間違えていたのを修正 * feat(client): 招待ページを実装 * change(client): インスタンスメニューの「招待」押した場合に招待ページに飛ぶように変更 * feat: 招待コードをコピーできるように * change(backend): metaに招待コード発行に関する情報を持たせるのをやめる * feat: ロールごとに招待コードの発行上限数などを設定できるように * change(client): 招待コードをコピーしたときにダイアログを出すように * add: 招待に関する管理者用のエンドポイントを追加 * change(backend): モデレーターであれば作成者以外でも招待コードを削除できるように * change(backend): admin/invite/listはオフセットでページネーションするように * feat(client): 招待コードの管理ページを追加 * feat(client): 招待コードのリストをソートできるように * change: `admin/invite/create`のレスポンスを修正 * fix(client): 有効期限を指定できていなかった問題を修正 * refactor: 必要のない箇所を削除 * perf(backend): use limit() instead of take() * change(client): 作成ボタンを見た目を変更 * refactor: 招待コードの生成部分を共通化し、コード内に"01OI"のいずれかの文字を含まないように * fix(client): paginationの仕様が変わっていたので修正 * change(backend): expiresAtパラメータのnullを許容 * change(client): 有効期限を設けないときは日付の入力欄を非表示に * fix: 自身のポリシーよりもインスタンス側のポリシーが優先表示される問題を修正 * fix: n時間のときに「n時間間」となってしまうのを修正 * fix(backend): ポリシーが途中で変更されたときに作成可能数がマイナス表記になってしまうのを修正 * change(client): 招待コードのユーザー名が不明な理由を表示するように * update: CHANGELOG.md * lint * refactor * refactor * tweak ui * :art: * :art: * add(backend): indexを追加 * change(backend): indexの追加に伴う変更 * change(client): インスタンスメニューの「招待」の場所を変更 * add(frontend): MkInviteCode用のstorybookを追加 * Update misskey-js.api.md * fix(misskey-js): InviteのcreatedByの型が間違っていたのを修正 --------- Co-authored-by: syuilo <Syuilotan@yahoo.co.jp> Co-authored-by: tamaina <tamaina@hotmail.co.jp>
2023-07-15 02:57:58 +02:00
inviteLimit: number;
inviteLimitCycle: number;
inviteExpirationTime: number;
canManageCustomEmojis: boolean;
canManageAvatarDecorations: boolean;
canSearchNotes: boolean;
canUseTranslator: boolean;
canHideAds: boolean;
driveCapacityMb: number;
alwaysMarkNsfw: boolean;
pinLimit: number;
antennaLimit: number;
wordMuteLimit: number;
webhookLimit: number;
clipLimit: number;
noteEachClipsLimit: number;
userListLimit: number;
userEachUserListsLimit: number;
rateLimitFactor: number;
avatarDecorationLimit: number;
};
export const DEFAULT_POLICIES: RolePolicies = {
gtlAvailable: true,
ltlAvailable: true,
canPublicNote: true,
mentionLimit: 20,
canInvite: false,
enhance: 招待機能の改善 (#11195) * refactor(backend): 招待機能を改修 * feat(backend): 招待コードのcreate/delete/listエンドポイントを追加 * add(misskey-js): エンドポイントと型を追加 * change(backend): metaでinvite関連の情報も返すように * add(misskey-js): エンドポイントと型を追加 * add(backend): `/endpoints/invite/limit`を追加 * fix: createdByがnullableではなかったのを修正 * fix: relationが取得できていなかった問題を修正 * fix: パラメータを間違えていたのを修正 * feat(client): 招待ページを実装 * change(client): インスタンスメニューの「招待」押した場合に招待ページに飛ぶように変更 * feat: 招待コードをコピーできるように * change(backend): metaに招待コード発行に関する情報を持たせるのをやめる * feat: ロールごとに招待コードの発行上限数などを設定できるように * change(client): 招待コードをコピーしたときにダイアログを出すように * add: 招待に関する管理者用のエンドポイントを追加 * change(backend): モデレーターであれば作成者以外でも招待コードを削除できるように * change(backend): admin/invite/listはオフセットでページネーションするように * feat(client): 招待コードの管理ページを追加 * feat(client): 招待コードのリストをソートできるように * change: `admin/invite/create`のレスポンスを修正 * fix(client): 有効期限を指定できていなかった問題を修正 * refactor: 必要のない箇所を削除 * perf(backend): use limit() instead of take() * change(client): 作成ボタンを見た目を変更 * refactor: 招待コードの生成部分を共通化し、コード内に"01OI"のいずれかの文字を含まないように * fix(client): paginationの仕様が変わっていたので修正 * change(backend): expiresAtパラメータのnullを許容 * change(client): 有効期限を設けないときは日付の入力欄を非表示に * fix: 自身のポリシーよりもインスタンス側のポリシーが優先表示される問題を修正 * fix: n時間のときに「n時間間」となってしまうのを修正 * fix(backend): ポリシーが途中で変更されたときに作成可能数がマイナス表記になってしまうのを修正 * change(client): 招待コードのユーザー名が不明な理由を表示するように * update: CHANGELOG.md * lint * refactor * refactor * tweak ui * :art: * :art: * add(backend): indexを追加 * change(backend): indexの追加に伴う変更 * change(client): インスタンスメニューの「招待」の場所を変更 * add(frontend): MkInviteCode用のstorybookを追加 * Update misskey-js.api.md * fix(misskey-js): InviteのcreatedByの型が間違っていたのを修正 --------- Co-authored-by: syuilo <Syuilotan@yahoo.co.jp> Co-authored-by: tamaina <tamaina@hotmail.co.jp>
2023-07-15 02:57:58 +02:00
inviteLimit: 0,
inviteLimitCycle: 60 * 24 * 7,
inviteExpirationTime: 0,
canManageCustomEmojis: false,
canManageAvatarDecorations: false,
canSearchNotes: false,
canUseTranslator: true,
canHideAds: false,
driveCapacityMb: 100,
alwaysMarkNsfw: false,
pinLimit: 5,
antennaLimit: 5,
wordMuteLimit: 200,
webhookLimit: 3,
clipLimit: 10,
noteEachClipsLimit: 200,
userListLimit: 10,
userEachUserListsLimit: 50,
rateLimitFactor: 1,
avatarDecorationLimit: 1,
};
@Injectable()
export class RoleService implements OnApplicationShutdown, OnModuleInit {
private rolesCache: MemorySingleCache<MiRole[]>;
private roleAssignmentByUserIdCache: MemoryKVCache<MiRoleAssignment[]>;
private notificationService: NotificationService;
public static AlreadyAssignedError = class extends Error {};
public static NotAssignedError = class extends Error {};
constructor(
private moduleRef: ModuleRef,
2023-04-12 04:40:08 +02:00
@Inject(DI.redis)
private redisClient: Redis.Redis,
@Inject(DI.redisForTimelines)
private redisForTimelines: Redis.Redis,
@Inject(DI.redisForSub)
private redisForSub: Redis.Redis,
@Inject(DI.usersRepository)
private usersRepository: UsersRepository,
@Inject(DI.rolesRepository)
private rolesRepository: RolesRepository,
@Inject(DI.roleAssignmentsRepository)
private roleAssignmentsRepository: RoleAssignmentsRepository,
private metaService: MetaService,
2023-04-04 10:32:09 +02:00
private cacheService: CacheService,
2023-01-13 03:03:54 +01:00
private userEntityService: UserEntityService,
private globalEventService: GlobalEventService,
private idService: IdService,
private moderationLogService: ModerationLogService,
private fanoutTimelineService: FanoutTimelineService,
) {
//this.onMessage = this.onMessage.bind(this);
this.rolesCache = new MemorySingleCache<MiRole[]>(1000 * 60 * 60 * 1);
this.roleAssignmentByUserIdCache = new MemoryKVCache<MiRoleAssignment[]>(1000 * 60 * 60 * 1);
this.redisForSub.on('message', this.onMessage);
}
async onModuleInit() {
this.notificationService = this.moduleRef.get(NotificationService.name);
}
@bindThis
private async onMessage(_: string, data: string): Promise<void> {
const obj = JSON.parse(data);
if (obj.channel === 'internal') {
2023-09-29 04:29:54 +02:00
const { type, body } = obj.message as GlobalEvents['internal']['payload'];
switch (type) {
case 'roleCreated': {
2023-04-04 10:32:09 +02:00
const cached = this.rolesCache.get();
if (cached) {
2023-01-25 03:18:16 +01:00
cached.push({
...body,
updatedAt: new Date(body.updatedAt),
lastUsedAt: new Date(body.lastUsedAt),
});
}
break;
}
case 'roleUpdated': {
2023-04-04 10:32:09 +02:00
const cached = this.rolesCache.get();
if (cached) {
const i = cached.findIndex(x => x.id === body.id);
if (i > -1) {
2023-01-25 03:18:16 +01:00
cached[i] = {
...body,
updatedAt: new Date(body.updatedAt),
lastUsedAt: new Date(body.lastUsedAt),
};
}
}
break;
}
case 'roleDeleted': {
2023-04-04 10:32:09 +02:00
const cached = this.rolesCache.get();
if (cached) {
2023-04-04 10:32:09 +02:00
this.rolesCache.set(cached.filter(x => x.id !== body.id));
}
break;
}
case 'userRoleAssigned': {
const cached = this.roleAssignmentByUserIdCache.get(body.userId);
if (cached) {
2024-01-22 09:44:03 +01:00
cached.push({ // TODO: このあたりのデシリアライズ処理は各modelファイル内に関数としてexportしたい
2023-01-25 03:18:16 +01:00
...body,
expiresAt: body.expiresAt ? new Date(body.expiresAt) : null,
2024-01-22 09:44:03 +01:00
user: null, // joinなカラムは通常取ってこないので
2024-01-22 10:00:46 +01:00
role: null, // joinなカラムは通常取ってこないので
2023-01-25 03:18:16 +01:00
});
}
break;
}
case 'userRoleUnassigned': {
const cached = this.roleAssignmentByUserIdCache.get(body.userId);
if (cached) {
this.roleAssignmentByUserIdCache.set(body.userId, cached.filter(x => x.id !== body.id));
}
break;
}
default:
break;
}
}
}
2023-01-13 03:03:54 +01:00
@bindThis
private evalCond(user: MiUser, roles: MiRole[], value: RoleCondFormulaValue): boolean {
2023-01-13 03:03:54 +01:00
try {
switch (value.type) {
// ~かつ~
2023-01-13 03:03:54 +01:00
case 'and': {
return value.values.every(v => this.evalCond(user, roles, v));
2023-01-13 03:03:54 +01:00
}
// ~または~
2023-01-13 03:03:54 +01:00
case 'or': {
return value.values.some(v => this.evalCond(user, roles, v));
2023-01-13 03:03:54 +01:00
}
// ~ではない
2023-01-13 03:03:54 +01:00
case 'not': {
return !this.evalCond(user, roles, value.value);
}
// マニュアルロールがアサインされている
case 'roleAssignedTo': {
return roles.some(r => r.id === value.roleId);
2023-01-13 03:03:54 +01:00
}
// ローカルユーザのみ
2023-01-13 03:03:54 +01:00
case 'isLocal': {
return this.userEntityService.isLocalUser(user);
}
// リモートユーザのみ
2023-01-13 03:03:54 +01:00
case 'isRemote': {
return this.userEntityService.isRemoteUser(user);
}
// サスペンド済みユーザである
case 'isSuspended': {
return user.isSuspended;
}
// 鍵アカウントユーザである
case 'isLocked': {
return user.isLocked;
}
// botユーザである
case 'isBot': {
return user.isBot;
}
// 猫である
case 'isCat': {
return user.isCat;
}
// 「ユーザを見つけやすくする」が有効なアカウント
case 'isExplorable': {
return user.isExplorable;
}
// ユーザが作成されてから指定期間経過した
2023-01-13 03:03:54 +01:00
case 'createdLessThan': {
return this.idService.parse(user.id).date.getTime() > (Date.now() - (value.sec * 1000));
2023-01-13 03:03:54 +01:00
}
// ユーザが作成されてから指定期間経っていない
2023-01-13 03:03:54 +01:00
case 'createdMoreThan': {
return this.idService.parse(user.id).date.getTime() < (Date.now() - (value.sec * 1000));
2023-01-13 03:03:54 +01:00
}
// フォロワー数が指定値以下
case 'followersLessThanOrEq': {
return user.followersCount <= value.value;
}
// フォロワー数が指定値以上
case 'followersMoreThanOrEq': {
return user.followersCount >= value.value;
}
// フォロー数が指定値以下
case 'followingLessThanOrEq': {
return user.followingCount <= value.value;
}
// フォロー数が指定値以上
case 'followingMoreThanOrEq': {
return user.followingCount >= value.value;
}
// ノート数が指定値以下
case 'notesLessThanOrEq': {
return user.notesCount <= value.value;
}
// ノート数が指定値以上
case 'notesMoreThanOrEq': {
return user.notesCount >= value.value;
}
2023-01-13 03:03:54 +01:00
default:
return false;
}
} catch (err) {
// TODO: log error
return false;
}
}
@bindThis
public async getRoles() {
const roles = await this.rolesCache.fetch(() => this.rolesRepository.findBy({}));
return roles;
}
@bindThis
public async getUserAssigns(userId: MiUser['id']) {
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));
return assigns;
}
@bindThis
public async getUserRoles(userId: MiUser['id']) {
2023-04-04 10:32:09 +02:00
const roles = await this.rolesCache.fetch(() => this.rolesRepository.findBy({}));
const assigns = await this.getUserAssigns(userId);
const assignedRoles = roles.filter(r => assigns.map(x => x.roleId).includes(r.id));
2023-04-04 10:32:09 +02:00
const user = roles.some(r => r.target === 'conditional') ? await this.cacheService.findUserById(userId) : null;
const matchedCondRoles = roles.filter(r => r.target === 'conditional' && this.evalCond(user!, assignedRoles, r.condFormula));
2023-01-13 03:03:54 +01:00
return [...assignedRoles, ...matchedCondRoles];
}
/**
*
*/
@bindThis
public async getUserBadgeRoles(userId: MiUser['id']) {
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));
2023-04-04 10:32:09 +02:00
const roles = await this.rolesCache.fetch(() => this.rolesRepository.findBy({}));
const assignedRoles = roles.filter(r => assigns.map(x => x.roleId).includes(r.id));
const assignedBadgeRoles = assignedRoles.filter(r => r.asBadge);
const badgeCondRoles = roles.filter(r => r.asBadge && (r.target === 'conditional'));
if (badgeCondRoles.length > 0) {
2023-04-04 10:32:09 +02:00
const user = roles.some(r => r.target === 'conditional') ? await this.cacheService.findUserById(userId) : null;
const matchedBadgeCondRoles = badgeCondRoles.filter(r => this.evalCond(user!, assignedRoles, r.condFormula));
return [...assignedBadgeRoles, ...matchedBadgeCondRoles];
} else {
return assignedBadgeRoles;
}
}
@bindThis
public async getUserPolicies(userId: MiUser['id'] | null): Promise<RolePolicies> {
const meta = await this.metaService.fetch();
const basePolicies = { ...DEFAULT_POLICIES, ...meta.policies };
if (userId == null) return basePolicies;
const roles = await this.getUserRoles(userId);
function calc<T extends keyof RolePolicies>(name: T, aggregate: (values: RolePolicies[T][]) => RolePolicies[T]) {
if (roles.length === 0) return basePolicies[name];
const policies = roles.map(role => role.policies[name] ?? { priority: 0, useDefault: true });
const p2 = policies.filter(policy => policy.priority === 2);
if (p2.length > 0) return aggregate(p2.map(policy => policy.useDefault ? basePolicies[name] : policy.value));
const p1 = policies.filter(policy => policy.priority === 1);
2023-01-15 19:16:44 +01:00
if (p1.length > 0) return aggregate(p1.map(policy => policy.useDefault ? basePolicies[name] : policy.value));
return aggregate(policies.map(policy => policy.useDefault ? basePolicies[name] : policy.value));
}
return {
gtlAvailable: calc('gtlAvailable', vs => vs.some(v => v === true)),
ltlAvailable: calc('ltlAvailable', vs => vs.some(v => v === true)),
canPublicNote: calc('canPublicNote', vs => vs.some(v => v === true)),
mentionLimit: calc('mentionLimit', vs => Math.max(...vs)),
canInvite: calc('canInvite', vs => vs.some(v => v === true)),
enhance: 招待機能の改善 (#11195) * refactor(backend): 招待機能を改修 * feat(backend): 招待コードのcreate/delete/listエンドポイントを追加 * add(misskey-js): エンドポイントと型を追加 * change(backend): metaでinvite関連の情報も返すように * add(misskey-js): エンドポイントと型を追加 * add(backend): `/endpoints/invite/limit`を追加 * fix: createdByがnullableではなかったのを修正 * fix: relationが取得できていなかった問題を修正 * fix: パラメータを間違えていたのを修正 * feat(client): 招待ページを実装 * change(client): インスタンスメニューの「招待」押した場合に招待ページに飛ぶように変更 * feat: 招待コードをコピーできるように * change(backend): metaに招待コード発行に関する情報を持たせるのをやめる * feat: ロールごとに招待コードの発行上限数などを設定できるように * change(client): 招待コードをコピーしたときにダイアログを出すように * add: 招待に関する管理者用のエンドポイントを追加 * change(backend): モデレーターであれば作成者以外でも招待コードを削除できるように * change(backend): admin/invite/listはオフセットでページネーションするように * feat(client): 招待コードの管理ページを追加 * feat(client): 招待コードのリストをソートできるように * change: `admin/invite/create`のレスポンスを修正 * fix(client): 有効期限を指定できていなかった問題を修正 * refactor: 必要のない箇所を削除 * perf(backend): use limit() instead of take() * change(client): 作成ボタンを見た目を変更 * refactor: 招待コードの生成部分を共通化し、コード内に"01OI"のいずれかの文字を含まないように * fix(client): paginationの仕様が変わっていたので修正 * change(backend): expiresAtパラメータのnullを許容 * change(client): 有効期限を設けないときは日付の入力欄を非表示に * fix: 自身のポリシーよりもインスタンス側のポリシーが優先表示される問題を修正 * fix: n時間のときに「n時間間」となってしまうのを修正 * fix(backend): ポリシーが途中で変更されたときに作成可能数がマイナス表記になってしまうのを修正 * change(client): 招待コードのユーザー名が不明な理由を表示するように * update: CHANGELOG.md * lint * refactor * refactor * tweak ui * :art: * :art: * add(backend): indexを追加 * change(backend): indexの追加に伴う変更 * change(client): インスタンスメニューの「招待」の場所を変更 * add(frontend): MkInviteCode用のstorybookを追加 * Update misskey-js.api.md * fix(misskey-js): InviteのcreatedByの型が間違っていたのを修正 --------- Co-authored-by: syuilo <Syuilotan@yahoo.co.jp> Co-authored-by: tamaina <tamaina@hotmail.co.jp>
2023-07-15 02:57:58 +02:00
inviteLimit: calc('inviteLimit', vs => Math.max(...vs)),
inviteLimitCycle: calc('inviteLimitCycle', vs => Math.max(...vs)),
inviteExpirationTime: calc('inviteExpirationTime', vs => Math.max(...vs)),
canManageCustomEmojis: calc('canManageCustomEmojis', vs => vs.some(v => v === true)),
canManageAvatarDecorations: calc('canManageAvatarDecorations', vs => vs.some(v => v === true)),
canSearchNotes: calc('canSearchNotes', vs => vs.some(v => v === true)),
canUseTranslator: calc('canUseTranslator', vs => vs.some(v => v === true)),
canHideAds: calc('canHideAds', vs => vs.some(v => v === true)),
driveCapacityMb: calc('driveCapacityMb', vs => Math.max(...vs)),
alwaysMarkNsfw: calc('alwaysMarkNsfw', vs => vs.some(v => v === true)),
pinLimit: calc('pinLimit', vs => Math.max(...vs)),
antennaLimit: calc('antennaLimit', vs => Math.max(...vs)),
wordMuteLimit: calc('wordMuteLimit', vs => Math.max(...vs)),
webhookLimit: calc('webhookLimit', vs => Math.max(...vs)),
clipLimit: calc('clipLimit', vs => Math.max(...vs)),
noteEachClipsLimit: calc('noteEachClipsLimit', vs => Math.max(...vs)),
userListLimit: calc('userListLimit', vs => Math.max(...vs)),
userEachUserListsLimit: calc('userEachUserListsLimit', vs => Math.max(...vs)),
rateLimitFactor: calc('rateLimitFactor', vs => Math.max(...vs)),
avatarDecorationLimit: calc('avatarDecorationLimit', vs => Math.max(...vs)),
};
}
@bindThis
public async isModerator(user: { id: MiUser['id']; isRoot: MiUser['isRoot'] } | null): Promise<boolean> {
if (user == null) return false;
return user.isRoot || (await this.getUserRoles(user.id)).some(r => r.isModerator || r.isAdministrator);
}
@bindThis
public async isAdministrator(user: { id: MiUser['id']; isRoot: MiUser['isRoot'] } | null): Promise<boolean> {
if (user == null) return false;
return user.isRoot || (await this.getUserRoles(user.id)).some(r => r.isAdministrator);
}
@bindThis
public async isExplorable(role: { id: MiRole['id']} | null): Promise<boolean> {
if (role == null) return false;
const check = await this.rolesRepository.findOneBy({ id: role.id });
if (check == null) return false;
return check.isExplorable;
}
@bindThis
public async getModeratorIds(includeAdmins = true): Promise<MiUser['id'][]> {
2023-04-04 10:32:09 +02:00
const roles = await this.rolesCache.fetch(() => this.rolesRepository.findBy({}));
const moderatorRoles = includeAdmins ? roles.filter(r => r.isModerator || r.isAdministrator) : roles.filter(r => r.isModerator);
const assigns = moderatorRoles.length > 0 ? await this.roleAssignmentsRepository.findBy({
roleId: In(moderatorRoles.map(r => r.id)),
}) : [];
// TODO: isRootなアカウントも含める
return assigns.map(a => a.userId);
}
@bindThis
public async getModerators(includeAdmins = true): Promise<MiUser[]> {
const ids = await this.getModeratorIds(includeAdmins);
const users = ids.length > 0 ? await this.usersRepository.findBy({
id: In(ids),
}) : [];
return users;
}
@bindThis
public async getAdministratorIds(): Promise<MiUser['id'][]> {
2023-04-04 10:32:09 +02:00
const roles = await this.rolesCache.fetch(() => this.rolesRepository.findBy({}));
const administratorRoles = roles.filter(r => r.isAdministrator);
const assigns = administratorRoles.length > 0 ? await this.roleAssignmentsRepository.findBy({
roleId: In(administratorRoles.map(r => r.id)),
}) : [];
// TODO: isRootなアカウントも含める
return assigns.map(a => a.userId);
}
@bindThis
public async getAdministrators(): Promise<MiUser[]> {
const ids = await this.getAdministratorIds();
const users = ids.length > 0 ? await this.usersRepository.findBy({
id: In(ids),
}) : [];
return users;
}
@bindThis
public async assign(userId: MiUser['id'], roleId: MiRole['id'], expiresAt: Date | null = null, moderator?: MiUser): Promise<void> {
const now = Date.now();
const role = await this.rolesRepository.findOneByOrFail({ id: roleId });
const existing = await this.roleAssignmentsRepository.findOneBy({
roleId: roleId,
userId: userId,
});
if (existing) {
if (existing.expiresAt && (existing.expiresAt.getTime() < now)) {
await this.roleAssignmentsRepository.delete({
roleId: roleId,
userId: userId,
});
} else {
throw new RoleService.AlreadyAssignedError();
}
}
const created = await this.roleAssignmentsRepository.insert({
id: this.idService.gen(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);
if (role.isPublic) {
this.notificationService.createNotification(userId, 'roleAssigned', {
roleId: roleId,
});
}
if (moderator) {
2023-09-25 03:29:12 +02:00
const user = await this.usersRepository.findOneByOrFail({ id: userId });
this.moderationLogService.log(moderator, 'assignRole', {
roleId: roleId,
roleName: role.name,
userId: userId,
2023-09-25 03:29:12 +02:00
userUsername: user.username,
userHost: user.host,
expiresAt: expiresAt ? expiresAt.toISOString() : null,
});
}
}
@bindThis
public async unassign(userId: MiUser['id'], roleId: MiRole['id'], moderator?: MiUser): 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);
if (moderator) {
2023-09-25 03:29:12 +02:00
const [user, role] = await Promise.all([
this.usersRepository.findOneByOrFail({ id: userId }),
this.rolesRepository.findOneByOrFail({ id: roleId }),
]);
this.moderationLogService.log(moderator, 'unassignRole', {
roleId: roleId,
roleName: role.name,
userId: userId,
2023-09-25 03:29:12 +02:00
userUsername: user.username,
userHost: user.host,
});
}
}
2023-04-12 04:40:08 +02:00
@bindThis
public async addNoteToRoleTimeline(note: Packed<'Note'>): Promise<void> {
const roles = await this.getUserRoles(note.userId);
const redisPipeline = this.redisForTimelines.pipeline();
2023-04-12 04:40:08 +02:00
for (const role of roles) {
this.fanoutTimelineService.push(`roleTimeline:${role.id}`, note.id, 1000, redisPipeline);
2023-04-12 04:40:08 +02:00
this.globalEventService.publishRoleTimelineStream(role.id, 'note', note);
}
redisPipeline.exec();
}
2023-09-25 03:29:12 +02:00
@bindThis
public async create(values: Partial<MiRole>, moderator?: MiUser): Promise<MiRole> {
const date = new Date();
const created = await this.rolesRepository.insert({
id: this.idService.gen(date.getTime()),
2023-09-25 03:29:12 +02:00
updatedAt: date,
lastUsedAt: date,
name: values.name,
description: values.description,
color: values.color,
iconUrl: values.iconUrl,
target: values.target,
condFormula: values.condFormula,
isPublic: values.isPublic,
isAdministrator: values.isAdministrator,
isModerator: values.isModerator,
isExplorable: values.isExplorable,
asBadge: values.asBadge,
canEditMembersByModerator: values.canEditMembersByModerator,
displayOrder: values.displayOrder,
policies: values.policies,
}).then(x => this.rolesRepository.findOneByOrFail(x.identifiers[0]));
this.globalEventService.publishInternalEvent('roleCreated', created);
if (moderator) {
this.moderationLogService.log(moderator, 'createRole', {
roleId: created.id,
role: created,
});
}
return created;
}
@bindThis
public async update(role: MiRole, params: Partial<MiRole>, moderator?: MiUser): Promise<void> {
const date = new Date();
await this.rolesRepository.update(role.id, {
updatedAt: date,
...params,
});
const updated = await this.rolesRepository.findOneByOrFail({ id: role.id });
this.globalEventService.publishInternalEvent('roleUpdated', updated);
if (moderator) {
this.moderationLogService.log(moderator, 'updateRole', {
roleId: role.id,
before: role,
after: updated,
});
}
}
2023-09-24 03:33:30 +02:00
@bindThis
public async delete(role: MiRole, moderator?: MiUser): Promise<void> {
await this.rolesRepository.delete({ id: role.id });
this.globalEventService.publishInternalEvent('roleDeleted', role);
if (moderator) {
this.moderationLogService.log(moderator, 'deleteRole', {
roleId: role.id,
role: role,
});
}
}
@bindThis
2023-05-29 06:21:26 +02:00
public dispose(): void {
this.redisForSub.off('message', this.onMessage);
this.roleAssignmentByUserIdCache.dispose();
}
2023-05-29 06:21:26 +02:00
@bindThis
public onApplicationShutdown(signal?: string | undefined): void {
this.dispose();
}
}