wip
This commit is contained in:
@@ -1892,6 +1892,7 @@ _role:
|
||||
canImportFollowing: "フォローのインポートを許可"
|
||||
canImportMuting: "ミュートのインポートを許可"
|
||||
canImportUserLists: "リストのインポートを許可"
|
||||
canChat: "チャットを許可"
|
||||
_condition:
|
||||
roleAssignedTo: "マニュアルロールにアサイン済み"
|
||||
isLocal: "ローカルユーザー"
|
||||
|
@@ -16,9 +16,11 @@ import { ChatMessageEntityService } from '@/core/entities/ChatMessageEntityServi
|
||||
import { ApRendererService } from '@/core/activitypub/ApRendererService.js';
|
||||
import { PushNotificationService } from '@/core/PushNotificationService.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import type { ChatMessagesRepository, MiChatMessage, MiDriveFile, MiUser, MutingsRepository, UsersRepository } from '@/models/_.js';
|
||||
import type { ChatMessagesRepository, MiChatMessage, MiChatRoom, MiDriveFile, MiUser, MutingsRepository, UsersRepository } from '@/models/_.js';
|
||||
import { UserBlockingService } from '@/core/UserBlockingService.js';
|
||||
import { QueryService } from '@/core/QueryService.js';
|
||||
import { RoleService } from '@/core/RoleService.js';
|
||||
import { UserFollowingService } from '@/core/UserFollowingService.js';
|
||||
|
||||
@Injectable()
|
||||
export class ChatService {
|
||||
@@ -47,36 +49,46 @@ export class ChatService {
|
||||
private pushNotificationService: PushNotificationService,
|
||||
private userBlockingService: UserBlockingService,
|
||||
private queryService: QueryService,
|
||||
private roleService: RoleService,
|
||||
private userFollowingService: UserFollowingService,
|
||||
) {
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async createMessage(params: {
|
||||
fromUser: { id: MiUser['id']; host: MiUser['host']; };
|
||||
toUser?: MiUser | null;
|
||||
//toRoom?: MiUserRoom | null;
|
||||
public async createMessage(fromUser: { id: MiUser['id']; host: MiUser['host']; }, toUser: MiUser, params: {
|
||||
text?: string | null;
|
||||
file?: MiDriveFile | null;
|
||||
uri?: string | null;
|
||||
}) {
|
||||
const { fromUser, toUser /*toRoom*/ } = params;
|
||||
|
||||
if (toUser == null /*&& toRoom == null*/) {
|
||||
throw new Error('recipient is required');
|
||||
if (fromUser.id === toUser.id) {
|
||||
throw new Error('yourself');
|
||||
}
|
||||
|
||||
if (toUser) {
|
||||
const blocked = await this.userBlockingService.checkBlocked(toUser.id, fromUser.id);
|
||||
if (blocked) {
|
||||
throw new Error('blocked');
|
||||
if (toUser.chatScope === 'none') {
|
||||
throw new Error('recipient is cannot chat');
|
||||
} else if (toUser.chatScope === 'followers') {
|
||||
|
||||
} else if (toUser.chatScope === 'following') {
|
||||
} else if (toUser.chatScope === 'mutual') {
|
||||
const isMutual = await this.userFollowingService.isMutual(fromUser.id, toUser.id);
|
||||
if (!isMutual) {
|
||||
throw new Error('recipient is cannot chat');
|
||||
}
|
||||
}
|
||||
|
||||
if ((await this.roleService.getUserPolicies(toUser.id)).canChat) {
|
||||
throw new Error('recipient is cannot chat');
|
||||
}
|
||||
|
||||
const blocked = await this.userBlockingService.checkBlocked(toUser.id, fromUser.id);
|
||||
if (blocked) {
|
||||
throw new Error('blocked');
|
||||
}
|
||||
|
||||
const message = {
|
||||
id: this.idService.gen(),
|
||||
fromUserId: fromUser.id,
|
||||
toUserId: toUser ? toUser.id : null,
|
||||
//toRoomId: recipientRoom ? recipientRoom.id : null,
|
||||
toUserId: toUser.id,
|
||||
text: params.text ? params.text.trim() : null,
|
||||
fileId: params.file ? params.file.id : null,
|
||||
reads: [],
|
||||
@@ -87,36 +99,26 @@ export class ChatService {
|
||||
|
||||
const packedMessage = await this.chatMessageEntityService.packLite(inserted);
|
||||
|
||||
if (toUser) {
|
||||
if (this.userEntityService.isLocalUser(toUser)) {
|
||||
const redisPipeline = this.redisClient.pipeline();
|
||||
redisPipeline.set(`newChatMessageExists:${toUser.id}:${fromUser.id}`, message.id);
|
||||
redisPipeline.sadd(`newChatMessagesExists:${toUser.id}`, `user:${fromUser.id}`);
|
||||
redisPipeline.exec();
|
||||
}
|
||||
|
||||
if (this.userEntityService.isLocalUser(fromUser)) {
|
||||
// 自分のストリーム
|
||||
this.globalEventService.publishChatStream(fromUser.id, toUser.id, 'message', packedMessage);
|
||||
}
|
||||
if (this.userEntityService.isLocalUser(fromUser)) {
|
||||
// 自分のストリーム
|
||||
this.globalEventService.publishChatStream(fromUser.id, toUser.id, 'message', packedMessage);
|
||||
}
|
||||
|
||||
if (this.userEntityService.isLocalUser(toUser)) {
|
||||
// 相手のストリーム
|
||||
this.globalEventService.publishChatStream(toUser.id, fromUser.id, 'message', packedMessage);
|
||||
}
|
||||
}/* else if (toRoom) {
|
||||
// グループのストリーム
|
||||
this.globalEventService.publishRoomChatStream(toRoom.id, 'message', messageObj);
|
||||
|
||||
// メンバーのストリーム
|
||||
const joinings = await this.userRoomJoiningsRepository.findBy({ userRoomId: toRoom.id });
|
||||
for (const joining of joinings) {
|
||||
this.globalEventService.publishChatIndexStream(joining.userId, 'message', messageObj);
|
||||
this.globalEventService.publishMainStream(joining.userId, 'chatMessage', messageObj);
|
||||
}
|
||||
}*/
|
||||
if (this.userEntityService.isLocalUser(toUser)) {
|
||||
// 相手のストリーム
|
||||
this.globalEventService.publishChatStream(toUser.id, fromUser.id, 'message', packedMessage);
|
||||
}
|
||||
|
||||
// 3秒経っても既読にならなかったらイベント発行
|
||||
setTimeout(async () => {
|
||||
if (toUser && this.userEntityService.isLocalUser(toUser)) {
|
||||
if (this.userEntityService.isLocalUser(toUser)) {
|
||||
setTimeout(async () => {
|
||||
const marker = await this.redisClient.get(`newChatMessageExists:${toUser.id}:${fromUser.id}`);
|
||||
|
||||
if (marker == null) return; // 既読
|
||||
@@ -124,15 +126,8 @@ export class ChatService {
|
||||
const packedMessageForTo = await this.chatMessageEntityService.pack(inserted, toUser);
|
||||
this.globalEventService.publishMainStream(toUser.id, 'newChatMessage', packedMessageForTo);
|
||||
this.pushNotificationService.pushNotification(toUser.id, 'newChatMessage', packedMessageForTo);
|
||||
}/* else if (toRoom) {
|
||||
const joinings = await this.userRoomJoiningsRepository.findBy({ userRoomId: toRoom.id, userId: Not(fromUser.id) });
|
||||
for (const joining of joinings) {
|
||||
if (freshMessage.reads.includes(joining.userId)) return; // 既読
|
||||
this.globalEventService.publishMainStream(joining.userId, 'newChatMessage', messageObj);
|
||||
this.pushNotificationService.pushNotification(joining.userId, 'newChatMessage', messageObj);
|
||||
}
|
||||
}*/
|
||||
}, 3000);
|
||||
}, 3000);
|
||||
}
|
||||
|
||||
/* TODO: AP
|
||||
if (toUser && this.userEntityService.isLocalUser(fromUser) && this.userEntityService.isRemoteUser(toUser)) {
|
||||
@@ -160,6 +155,53 @@ export class ChatService {
|
||||
return packedMessage;
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async createMessageToRoom(fromUser: { id: MiUser['id']; host: MiUser['host']; }, toRoom: MiChatRoom, params: {
|
||||
text?: string | null;
|
||||
file?: MiDriveFile | null;
|
||||
uri?: string | null;
|
||||
}) {
|
||||
const message = {
|
||||
id: this.idService.gen(),
|
||||
fromUserId: fromUser.id,
|
||||
toRoomId: toRoom.id,
|
||||
text: params.text ? params.text.trim() : null,
|
||||
fileId: params.file ? params.file.id : null,
|
||||
reads: [],
|
||||
uri: params.uri ?? null,
|
||||
} satisfies Partial<MiChatMessage>;
|
||||
|
||||
const inserted = await this.chatMessagesRepository.insertOne(message);
|
||||
|
||||
const packedMessage = await this.chatMessageEntityService.packLite(inserted);
|
||||
|
||||
/*
|
||||
// グループのストリーム
|
||||
this.globalEventService.publishRoomChatStream(toRoom.id, 'message', messageObj);
|
||||
|
||||
// メンバーのストリーム
|
||||
const joinings = await this.userRoomJoiningsRepository.findBy({ userRoomId: toRoom.id });
|
||||
for (const joining of joinings) {
|
||||
this.globalEventService.publishChatIndexStream(joining.userId, 'message', messageObj);
|
||||
this.globalEventService.publishMainStream(joining.userId, 'chatMessage', messageObj);
|
||||
}
|
||||
*/
|
||||
|
||||
// 3秒経っても既読にならなかったらイベント発行
|
||||
setTimeout(async () => {
|
||||
/*
|
||||
const joinings = await this.userRoomJoiningsRepository.findBy({ userRoomId: toRoom.id, userId: Not(fromUser.id) });
|
||||
for (const joining of joinings) {
|
||||
if (freshMessage.reads.includes(joining.userId)) return; // 既読
|
||||
this.globalEventService.publishMainStream(joining.userId, 'newChatMessage', messageObj);
|
||||
this.pushNotificationService.pushNotification(joining.userId, 'newChatMessage', messageObj);
|
||||
}
|
||||
*/
|
||||
}, 3000);
|
||||
|
||||
return packedMessage;
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async readUserChatMessage(
|
||||
readerId: MiUser['id'],
|
||||
|
@@ -63,6 +63,7 @@ export type RolePolicies = {
|
||||
canImportFollowing: boolean;
|
||||
canImportMuting: boolean;
|
||||
canImportUserLists: boolean;
|
||||
canChat: boolean;
|
||||
};
|
||||
|
||||
export const DEFAULT_POLICIES: RolePolicies = {
|
||||
@@ -97,6 +98,7 @@ export const DEFAULT_POLICIES: RolePolicies = {
|
||||
canImportFollowing: true,
|
||||
canImportMuting: true,
|
||||
canImportUserLists: true,
|
||||
canChat: true,
|
||||
};
|
||||
|
||||
@Injectable()
|
||||
@@ -400,6 +402,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)),
|
||||
};
|
||||
}
|
||||
|
||||
|
@@ -5,7 +5,7 @@
|
||||
|
||||
import { Inject, Injectable, OnModuleInit } from '@nestjs/common';
|
||||
import { ModuleRef } from '@nestjs/core';
|
||||
import { IsNull } from 'typeorm';
|
||||
import { Brackets, IsNull } from 'typeorm';
|
||||
import type { MiLocalUser, MiPartialLocalUser, MiPartialRemoteUser, MiRemoteUser, MiUser } from '@/models/User.js';
|
||||
import { IdentifiableError } from '@/misc/identifiable-error.js';
|
||||
import { QueueService } from '@/core/QueueService.js';
|
||||
@@ -736,4 +736,20 @@ export class UserFollowingService implements OnModuleInit {
|
||||
.where('following.followerId = :followerId', { followerId: userId })
|
||||
.getMany();
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async isMutual(aUserId: MiUser['id'], bUserId: MiUser['id']) {
|
||||
const count = await this.followingsRepository.createQueryBuilder('following')
|
||||
.where(new Brackets(qb => {
|
||||
qb.where('following.followerId = :aUserId', { aUserId })
|
||||
.andWhere('following.followeeId = :bUserId', { bUserId });
|
||||
}))
|
||||
.orWhere(new Brackets(qb => {
|
||||
qb.where('following.followerId = :bUserId', { bUserId })
|
||||
.andWhere('following.followeeId = :aUserId', { aUserId });
|
||||
}))
|
||||
.getCount();
|
||||
|
||||
return count === 2;
|
||||
}
|
||||
}
|
||||
|
@@ -225,6 +225,17 @@ export class MiUser {
|
||||
})
|
||||
public emojis: string[];
|
||||
|
||||
// チャットを許可する相手
|
||||
// everyone: 誰からでも
|
||||
// followers: フォロワーのみ
|
||||
// following: フォローしているユーザーのみ
|
||||
// mutual: 相互フォローのみ
|
||||
// none: 誰からも受け付けない
|
||||
@Column('varchar', {
|
||||
length: 128, default: 'mutual',
|
||||
})
|
||||
public chatScope: 'everyone' | 'followers' | 'following' | 'mutual' | 'none';
|
||||
|
||||
@Index()
|
||||
@Column('varchar', {
|
||||
length: 128, nullable: true,
|
||||
|
@@ -292,6 +292,10 @@ export const packedRolePoliciesSchema = {
|
||||
type: 'boolean',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
canChat: {
|
||||
type: 'boolean',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
|
||||
|
@@ -16,6 +16,7 @@ export const meta = {
|
||||
tags: ['chat'],
|
||||
|
||||
requireCredential: true,
|
||||
requiredRolePolicy: 'canChat',
|
||||
|
||||
prohibitMoved: true,
|
||||
|
||||
|
@@ -107,6 +107,7 @@ export const ROLE_POLICIES = [
|
||||
'canImportFollowing',
|
||||
'canImportMuting',
|
||||
'canImportUserLists',
|
||||
'canChat',
|
||||
] as const;
|
||||
|
||||
// なんか動かない
|
||||
|
@@ -160,6 +160,26 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
</div>
|
||||
</MkFolder>
|
||||
|
||||
<MkFolder v-if="matchQuery([i18n.ts._role._options.canChat, 'canChat'])">
|
||||
<template #label>{{ i18n.ts._role._options.canChat }}</template>
|
||||
<template #suffix>
|
||||
<span v-if="role.policies.canChat.useDefault" :class="$style.useDefaultLabel">{{ i18n.ts._role.useBaseValue }}</span>
|
||||
<span v-else>{{ role.policies.canChat.value ? i18n.ts.yes : i18n.ts.no }}</span>
|
||||
<span :class="$style.priorityIndicator"><i :class="getPriorityIcon(role.policies.canChat)"></i></span>
|
||||
</template>
|
||||
<div class="_gaps">
|
||||
<MkSwitch v-model="role.policies.canChat.useDefault" :readonly="readonly">
|
||||
<template #label>{{ i18n.ts._role.useBaseValue }}</template>
|
||||
</MkSwitch>
|
||||
<MkSwitch v-model="role.policies.canChat.value" :disabled="role.policies.canChat.useDefault" :readonly="readonly">
|
||||
<template #label>{{ i18n.ts.enable }}</template>
|
||||
</MkSwitch>
|
||||
<MkRange v-model="role.policies.canChat.priority" :min="0" :max="2" :step="1" easing :textConverter="(v) => v === 0 ? i18n.ts._role._priority.low : v === 1 ? i18n.ts._role._priority.middle : v === 2 ? i18n.ts._role._priority.high : ''">
|
||||
<template #label>{{ i18n.ts._role.priority }}</template>
|
||||
</MkRange>
|
||||
</div>
|
||||
</MkFolder>
|
||||
|
||||
<MkFolder v-if="matchQuery([i18n.ts._role._options.mentionMax, 'mentionLimit'])">
|
||||
<template #label>{{ i18n.ts._role._options.mentionMax }}</template>
|
||||
<template #suffix>
|
||||
|
@@ -51,6 +51,14 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
</MkSwitch>
|
||||
</MkFolder>
|
||||
|
||||
<MkFolder v-if="matchQuery([i18n.ts._role._options.canChat, 'canChat'])">
|
||||
<template #label>{{ i18n.ts._role._options.canChat }}</template>
|
||||
<template #suffix>{{ policies.canChat ? i18n.ts.yes : i18n.ts.no }}</template>
|
||||
<MkSwitch v-model="policies.canChat">
|
||||
<template #label>{{ i18n.ts.enable }}</template>
|
||||
</MkSwitch>
|
||||
</MkFolder>
|
||||
|
||||
<MkFolder v-if="matchQuery([i18n.ts._role._options.mentionMax, 'mentionLimit'])">
|
||||
<template #label>{{ i18n.ts._role._options.mentionMax }}</template>
|
||||
<template #suffix>{{ policies.mentionLimit }}</template>
|
||||
|
@@ -4974,6 +4974,7 @@ export type components = {
|
||||
canImportFollowing: boolean;
|
||||
canImportMuting: boolean;
|
||||
canImportUserLists: boolean;
|
||||
canChat: boolean;
|
||||
};
|
||||
ReversiGameLite: {
|
||||
/** Format: id */
|
||||
@@ -13800,7 +13801,7 @@ export type operations = {
|
||||
/** Format: misskey:id */
|
||||
fileId?: string;
|
||||
/** Format: misskey:id */
|
||||
userId?: string | null;
|
||||
toUserId?: string | null;
|
||||
};
|
||||
};
|
||||
};
|
||||
|
Reference in New Issue
Block a user