enhance: チャットの閲覧を無効化できるように (#15765)

* enhance: チャットの閲覧を無効化できるように

* fix

* fix

* fix

* readonlyの説明を追加

* enhance: チャットが無効な場合はチャット関連の設定も隠すように

* fix

* refactor: ChatServiceからApiに関するドメイン知識を排除
This commit is contained in:
かっこかり 2025-04-07 19:09:11 +09:00 committed by GitHub
parent 6c27ab12eb
commit 9d3f3264fd
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
42 changed files with 255 additions and 136 deletions

View file

@ -94,6 +94,40 @@ export class ChatService {
) {
}
@bindThis
public async getChatAvailability(userId: MiUser['id']): Promise<{ read: boolean; write: boolean; }> {
const policies = await this.roleService.getUserPolicies(userId);
switch (policies.chatAvailability) {
case 'available':
return {
read: true,
write: true,
};
case 'readonly':
return {
read: true,
write: false,
};
case 'unavailable':
return {
read: false,
write: false,
};
default:
throw new Error('invalid chat availability (unreachable)');
}
}
/** getChatAvailabilityの糖衣。主にAPI呼び出し時に走らせて、権限的に問題ない場合はそのまま続行する */
@bindThis
public async checkChatAvailability(userId: MiUser['id'], permission: 'read' | 'write') {
const policy = await this.getChatAvailability(userId);
if (policy[permission] === false) {
throw new Error('ROLE_PERMISSION_DENIED');
}
}
@bindThis
public async createMessageToUser(fromUser: { id: MiUser['id']; host: MiUser['host']; }, toUser: MiUser, params: {
text?: string | null;
@ -140,7 +174,7 @@ export class ChatService {
}
}
if (!(await this.roleService.getUserPolicies(toUser.id)).canChat) {
if (!(await this.getChatAvailability(toUser.id)).write) {
throw new Error('recipient is cannot chat (policy)');
}

View file

@ -63,7 +63,7 @@ export type RolePolicies = {
canImportFollowing: boolean;
canImportMuting: boolean;
canImportUserLists: boolean;
canChat: boolean;
chatAvailability: 'available' | 'readonly' | 'unavailable';
};
export const DEFAULT_POLICIES: RolePolicies = {
@ -98,7 +98,7 @@ export const DEFAULT_POLICIES: RolePolicies = {
canImportFollowing: true,
canImportMuting: true,
canImportUserLists: true,
canChat: true,
chatAvailability: 'available',
};
@Injectable()
@ -370,6 +370,12 @@ export class RoleService implements OnApplicationShutdown, OnModuleInit {
return aggregate(policies.map(policy => policy.useDefault ? basePolicies[name] : policy.value));
}
function aggregateChatAvailability(vs: RolePolicies['chatAvailability'][]) {
if (vs.some(v => v === 'available')) return 'available';
if (vs.some(v => v === 'readonly')) return 'readonly';
return 'unavailable';
}
return {
gtlAvailable: calc('gtlAvailable', vs => vs.some(v => v === true)),
ltlAvailable: calc('ltlAvailable', vs => vs.some(v => v === true)),
@ -402,7 +408,7 @@ export class RoleService implements OnApplicationShutdown, OnModuleInit {
canImportFollowing: calc('canImportFollowing', vs => vs.some(v => v === true)),
canImportMuting: calc('canImportMuting', vs => vs.some(v => v === true)),
canImportUserLists: calc('canImportUserLists', vs => vs.some(v => v === true)),
canChat: calc('canChat', vs => vs.some(v => v === true)),
chatAvailability: calc('chatAvailability', aggregateChatAvailability),
};
}

View file

@ -557,7 +557,7 @@ export class UserEntityService implements OnModuleInit {
followersVisibility: profile!.followersVisibility,
followingVisibility: profile!.followingVisibility,
chatScope: user.chatScope,
canChat: this.roleService.getUserPolicies(user.id).then(r => r.canChat),
canChat: this.roleService.getUserPolicies(user.id).then(r => r.chatAvailability === 'available'),
roles: this.roleService.getUserRoles(user.id).then(roles => roles.filter(role => role.isPublic).sort((a, b) => b.displayOrder - a.displayOrder).map(role => ({
id: role.id,
name: role.name,

View file

@ -292,9 +292,10 @@ export const packedRolePoliciesSchema = {
type: 'boolean',
optional: false, nullable: false,
},
canChat: {
type: 'boolean',
chatAvailability: {
type: 'string',
optional: false, nullable: false,
enum: ['available', 'readonly', 'unavailable'],
},
},
} as const;

View file

@ -46,6 +46,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
private chatService: ChatService,
) {
super(meta, paramDef, async (ps, me) => {
await this.chatService.checkChatAvailability(me.id, 'read');
const history = ps.room ? await this.chatService.roomHistory(me.id, ps.limit) : await this.chatService.userHistory(me.id, ps.limit);
const packedMessages = await this.chatEntityService.packMessagesDetailed(history, me);

View file

@ -16,7 +16,6 @@ export const meta = {
tags: ['chat'],
requireCredential: true,
requiredRolePolicy: 'canChat',
prohibitMoved: true,
@ -74,6 +73,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
private chatService: ChatService,
) {
super(meta, paramDef, async (ps, me) => {
await this.chatService.checkChatAvailability(me.id, 'write');
const room = await this.chatService.findRoomById(ps.toRoomId);
if (room == null) {
throw new ApiError(meta.errors.noSuchRoom);

View file

@ -16,7 +16,6 @@ export const meta = {
tags: ['chat'],
requireCredential: true,
requiredRolePolicy: 'canChat',
prohibitMoved: true,
@ -86,6 +85,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
private chatService: ChatService,
) {
super(meta, paramDef, async (ps, me) => {
await this.chatService.checkChatAvailability(me.id, 'write');
let file = null;
if (ps.fileId != null) {
file = await this.driveFilesRepository.findOneBy({

View file

@ -13,7 +13,6 @@ export const meta = {
tags: ['chat'],
requireCredential: true,
requiredRolePolicy: 'canChat',
kind: 'write:chat',
@ -43,6 +42,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
private chatService: ChatService,
) {
super(meta, paramDef, async (ps, me) => {
await this.chatService.checkChatAvailability(me.id, 'write');
const message = await this.chatService.findMyMessageById(me.id, ps.messageId);
if (message == null) {
throw new ApiError(meta.errors.noSuchMessage);

View file

@ -13,7 +13,6 @@ export const meta = {
tags: ['chat'],
requireCredential: true,
requiredRolePolicy: 'canChat',
kind: 'write:chat',
@ -44,6 +43,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
private chatService: ChatService,
) {
super(meta, paramDef, async (ps, me) => {
await this.chatService.checkChatAvailability(me.id, 'write');
await this.chatService.react(ps.messageId, me.id, ps.reaction);
});
}

View file

@ -54,6 +54,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
private chatService: ChatService,
) {
super(meta, paramDef, async (ps, me) => {
await this.chatService.checkChatAvailability(me.id, 'read');
const room = await this.chatService.findRoomById(ps.roomId);
if (room == null) {
throw new ApiError(meta.errors.noSuchRoom);

View file

@ -54,6 +54,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
private chatService: ChatService,
) {
super(meta, paramDef, async (ps, me) => {
await this.chatService.checkChatAvailability(me.id, 'read');
if (ps.roomId != null) {
const room = await this.chatService.findRoomById(ps.roomId);
if (room == null) {

View file

@ -50,6 +50,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
private chatEntityService: ChatEntityService,
) {
super(meta, paramDef, async (ps, me) => {
await this.chatService.checkChatAvailability(me.id, 'read');
const message = await this.chatService.findMessageById(ps.messageId);
if (message == null) {
throw new ApiError(meta.errors.noSuchMessage);

View file

@ -13,7 +13,6 @@ export const meta = {
tags: ['chat'],
requireCredential: true,
requiredRolePolicy: 'canChat',
kind: 'write:chat',
@ -44,6 +43,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
private chatService: ChatService,
) {
super(meta, paramDef, async (ps, me) => {
await this.chatService.checkChatAvailability(me.id, 'write');
await this.chatService.unreact(ps.messageId, me.id, ps.reaction);
});
}

View file

@ -56,6 +56,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
private getterService: GetterService,
) {
super(meta, paramDef, async (ps, me) => {
await this.chatService.checkChatAvailability(me.id, 'read');
const other = await this.getterService.getUser(ps.userId).catch(err => {
if (err.id === '15348ddd-432d-49c2-8a5a-8069753becff') throw new ApiError(meta.errors.noSuchUser);
throw err;

View file

@ -15,7 +15,6 @@ export const meta = {
tags: ['chat'],
requireCredential: true,
requiredRolePolicy: 'canChat',
prohibitMoved: true,
@ -52,6 +51,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
private chatEntityService: ChatEntityService,
) {
super(meta, paramDef, async (ps, me) => {
await this.chatService.checkChatAvailability(me.id, 'write');
const room = await this.chatService.createRoom(me, {
name: ps.name,
description: ps.description ?? '',

View file

@ -42,6 +42,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
private chatService: ChatService,
) {
super(meta, paramDef, async (ps, me) => {
await this.chatService.checkChatAvailability(me.id, 'write');
const room = await this.chatService.findRoomById(ps.roomId);
if (room == null) {
throw new ApiError(meta.errors.noSuchRoom);

View file

@ -15,7 +15,6 @@ export const meta = {
tags: ['chat'],
requireCredential: true,
requiredRolePolicy: 'canChat',
prohibitMoved: true,
@ -57,6 +56,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
private chatEntityService: ChatEntityService,
) {
super(meta, paramDef, async (ps, me) => {
await this.chatService.checkChatAvailability(me.id, 'write');
const room = await this.chatService.findMyRoomById(me.id, ps.roomId);
if (room == null) {
throw new ApiError(meta.errors.noSuchRoom);

View file

@ -42,6 +42,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
private chatService: ChatService,
) {
super(meta, paramDef, async (ps, me) => {
await this.chatService.checkChatAvailability(me.id, 'write');
await this.chatService.ignoreRoomInvitation(me.id, ps.roomId);
});
}

View file

@ -47,6 +47,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
private chatService: ChatService,
) {
super(meta, paramDef, async (ps, me) => {
await this.chatService.checkChatAvailability(me.id, 'read');
const invitations = await this.chatService.getReceivedRoomInvitationsWithPagination(me.id, ps.limit, ps.sinceId, ps.untilId);
return this.chatEntityService.packRoomInvitations(invitations, me);
});

View file

@ -55,6 +55,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
private chatEntityService: ChatEntityService,
) {
super(meta, paramDef, async (ps, me) => {
await this.chatService.checkChatAvailability(me.id, 'read');
const room = await this.chatService.findMyRoomById(me.id, ps.roomId);
if (room == null) {
throw new ApiError(meta.errors.noSuchRoom);

View file

@ -42,6 +42,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
private chatService: ChatService,
) {
super(meta, paramDef, async (ps, me) => {
await this.chatService.checkChatAvailability(me.id, 'write');
await this.chatService.joinToRoom(me.id, ps.roomId);
});
}

View file

@ -47,6 +47,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
private chatEntityService: ChatEntityService,
) {
super(meta, paramDef, async (ps, me) => {
await this.chatService.checkChatAvailability(me.id, 'read');
const memberships = await this.chatService.getMyMemberships(me.id, ps.limit, ps.sinceId, ps.untilId);
return this.chatEntityService.packRoomMemberships(memberships, me, {

View file

@ -42,6 +42,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
private chatService: ChatService,
) {
super(meta, paramDef, async (ps, me) => {
await this.chatService.checkChatAvailability(me.id, 'write');
await this.chatService.leaveRoom(me.id, ps.roomId);
});
}

View file

@ -54,6 +54,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
private chatEntityService: ChatEntityService,
) {
super(meta, paramDef, async (ps, me) => {
await this.chatService.checkChatAvailability(me.id, 'read');
const room = await this.chatService.findRoomById(ps.roomId);
if (room == null) {
throw new ApiError(meta.errors.noSuchRoom);

View file

@ -43,6 +43,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
private chatService: ChatService,
) {
super(meta, paramDef, async (ps, me) => {
await this.chatService.checkChatAvailability(me.id, 'write');
await this.chatService.muteRoom(me.id, ps.roomId, ps.mute);
});
}

View file

@ -47,6 +47,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
private chatService: ChatService,
) {
super(meta, paramDef, async (ps, me) => {
await this.chatService.checkChatAvailability(me.id, 'read');
const rooms = await this.chatService.getOwnedRoomsWithPagination(me.id, ps.limit, ps.sinceId, ps.untilId);
return this.chatEntityService.packRooms(rooms, me);
});

View file

@ -47,6 +47,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
private chatEntityService: ChatEntityService,
) {
super(meta, paramDef, async (ps, me) => {
await this.chatService.checkChatAvailability(me.id, 'read');
const room = await this.chatService.findRoomById(ps.roomId);
if (room == null) {
throw new ApiError(meta.errors.noSuchRoom);

View file

@ -49,6 +49,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
private chatEntityService: ChatEntityService,
) {
super(meta, paramDef, async (ps, me) => {
await this.chatService.checkChatAvailability(me.id, 'write');
const room = await this.chatService.findMyRoomById(me.id, ps.roomId);
if (room == null) {
throw new ApiError(meta.errors.noSuchRoom);