wip
This commit is contained in:
254
packages/backend/src/core/ChatService.ts
Normal file
254
packages/backend/src/core/ChatService.ts
Normal file
@@ -0,0 +1,254 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import * as Redis from 'ioredis';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import type { Config } from '@/config.js';
|
||||
import { QueueService } from '@/core/QueueService.js';
|
||||
import { IdService } from '@/core/IdService.js';
|
||||
import { GlobalEventService } from '@/core/GlobalEventService.js';
|
||||
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
||||
import { ChatMessageEntityService } from '@/core/entities/ChatMessageEntityService.js';
|
||||
import { ApRendererService } from '@/core/activitypub/ApRendererService.js';
|
||||
import { PushNotificationService } from '@/core/PushNotificationService.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import type { ChatMessagesRepository, MiChatMessage, MiDriveFile, MiUser, UsersRepository } from '@/models/_.js';
|
||||
import { UserBlockingService } from '@/core/UserBlockingService.js';
|
||||
|
||||
@Injectable()
|
||||
export class ChatService {
|
||||
constructor(
|
||||
@Inject(DI.config)
|
||||
private config: Config,
|
||||
|
||||
@Inject(DI.redis)
|
||||
private redisClient: Redis.Redis,
|
||||
|
||||
@Inject(DI.usersRepository)
|
||||
private usersRepository: UsersRepository,
|
||||
|
||||
@Inject(DI.chatMessagesRepository)
|
||||
private chatMessagesRepository: ChatMessagesRepository,
|
||||
|
||||
private userEntityService: UserEntityService,
|
||||
private chatMessageEntityService: ChatMessageEntityService,
|
||||
private idService: IdService,
|
||||
private globalEventService: GlobalEventService,
|
||||
private apRendererService: ApRendererService,
|
||||
private queueService: QueueService,
|
||||
private pushNotificationService: PushNotificationService,
|
||||
private userBlockingService: UserBlockingService,
|
||||
) {
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async createMessage(params: {
|
||||
fromUser: { id: MiUser['id']; host: MiUser['host']; };
|
||||
toUser?: MiUser | null;
|
||||
//toGroup?: MiUserGroup | null;
|
||||
text?: string | null;
|
||||
file?: MiDriveFile | null;
|
||||
uri?: string | null;
|
||||
}) {
|
||||
const { fromUser, toUser /*toGroup*/ } = params;
|
||||
|
||||
if (toUser == null /*&& toGroup == null*/) {
|
||||
throw new Error('recipient is required');
|
||||
}
|
||||
|
||||
if (toUser) {
|
||||
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,
|
||||
//toGroupId: recipientGroup ? recipientGroup.id : null,
|
||||
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);
|
||||
|
||||
if (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(toUser)) {
|
||||
// 相手のストリーム
|
||||
this.globalEventService.publishChatStream(toUser.id, fromUser.id, 'message', packedMessage);
|
||||
}
|
||||
}/* else if (toGroup) {
|
||||
// グループのストリーム
|
||||
this.globalEventService.publishGroupChatStream(toGroup.id, 'message', messageObj);
|
||||
|
||||
// メンバーのストリーム
|
||||
const joinings = await this.userGroupJoiningsRepository.findBy({ userGroupId: toGroup.id });
|
||||
for (const joining of joinings) {
|
||||
this.globalEventService.publishChatIndexStream(joining.userId, 'message', messageObj);
|
||||
this.globalEventService.publishMainStream(joining.userId, 'chatMessage', messageObj);
|
||||
}
|
||||
}*/
|
||||
|
||||
// 3秒経っても既読にならなかったらイベント発行
|
||||
setTimeout(async () => {
|
||||
if (toUser && this.userEntityService.isLocalUser(toUser)) {
|
||||
const marker = await this.redisClient.get(`newChatMessageExists:${toUser.id}:${fromUser.id}`);
|
||||
|
||||
if (marker == null) return; // 既読
|
||||
|
||||
const packedMessageForTo = await this.chatMessageEntityService.pack(inserted, toUser);
|
||||
this.globalEventService.publishMainStream(toUser.id, 'newChatMessage', packedMessageForTo);
|
||||
this.pushNotificationService.pushNotification(toUser.id, 'newChatMessage', packedMessageForTo);
|
||||
}/* else if (toGroup) {
|
||||
const joinings = await this.userGroupJoiningsRepository.findBy({ userGroupId: toGroup.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);
|
||||
|
||||
/* TODO: AP
|
||||
if (toUser && this.userEntityService.isLocalUser(fromUser) && this.userEntityService.isRemoteUser(toUser)) {
|
||||
const note = {
|
||||
id: message.id,
|
||||
createdAt: message.createdAt,
|
||||
fileIds: message.fileId ? [message.fileId] : [],
|
||||
text: message.text,
|
||||
userId: message.userId,
|
||||
visibility: 'specified',
|
||||
mentions: [toUser].map(u => u.id),
|
||||
mentionedRemoteUsers: JSON.stringify([toUser].map(u => ({
|
||||
uri: u.uri,
|
||||
username: u.username,
|
||||
host: u.host,
|
||||
}))),
|
||||
} as MiNote;
|
||||
|
||||
const activity = this.apRendererService.addContext(this.apRendererService.renderCreate(await this.apRendererService.renderNote(note, false, true), note));
|
||||
|
||||
this.queueService.deliver(fromUser, activity, toUser.inbox);
|
||||
}
|
||||
*/
|
||||
|
||||
return packedMessage;
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async readUserChatMessage(
|
||||
readerId: MiUser['id'],
|
||||
senderId: MiUser['id'],
|
||||
) {
|
||||
const redisPipeline = this.redisClient.pipeline();
|
||||
redisPipeline.del(`newChatMessageExists:${readerId}:${senderId}`);
|
||||
redisPipeline.srem(`newChatMessagesExists:${readerId}`, `user:${senderId}`);
|
||||
redisPipeline.exec();
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async deleteMessage(message: MiChatMessage) {
|
||||
await this.chatMessagesRepository.delete(message.id);
|
||||
|
||||
if (message.toUserId) {
|
||||
const fromUser = await this.usersRepository.findOneByOrFail({ id: message.fromUserId });
|
||||
const toUser = await this.usersRepository.findOneByOrFail({ id: message.toUserId });
|
||||
|
||||
if (this.userEntityService.isLocalUser(fromUser)) this.globalEventService.publishChatStream(message.fromUserId, message.toUserId, 'deleted', message.id);
|
||||
if (this.userEntityService.isLocalUser(toUser)) this.globalEventService.publishChatStream(message.toUserId, message.fromUserId, 'deleted', message.id);
|
||||
|
||||
if (this.userEntityService.isLocalUser(fromUser) && this.userEntityService.isRemoteUser(toUser)) {
|
||||
const activity = this.apRendererService.addContext(this.apRendererService.renderDelete(this.apRendererService.renderTombstone(`${this.config.url}/notes/${message.id}`), fromUser));
|
||||
this.queueService.deliver(fromUser, activity, toUser.inbox);
|
||||
}
|
||||
}/* else if (message.groupId) {
|
||||
this.globalEventService.publishGroupChatStream(message.groupId, 'deleted', message.id);
|
||||
}*/
|
||||
}
|
||||
|
||||
/*
|
||||
@bindThis
|
||||
public async readGroupChatMessage(
|
||||
userId: MiUser['id'],
|
||||
groupId: MiUserGroup['id'],
|
||||
messageIds: MiChatMessage['id'][],
|
||||
) {
|
||||
if (messageIds.length === 0) return;
|
||||
|
||||
// check joined
|
||||
const joining = await this.userGroupJoiningsRepository.findOneBy({
|
||||
userId: userId,
|
||||
userGroupId: groupId,
|
||||
});
|
||||
|
||||
if (joining == null) {
|
||||
throw new IdentifiableError('930a270c-714a-46b2-b776-ad27276dc569', 'Access denied (group).');
|
||||
}
|
||||
|
||||
const messages = await this.chatMessagesRepository.findBy({
|
||||
id: In(messageIds),
|
||||
});
|
||||
|
||||
const reads: ChatMessage['id'][] = [];
|
||||
|
||||
for (const message of messages) {
|
||||
if (message.userId === userId) continue;
|
||||
if (message.reads.includes(userId)) continue;
|
||||
|
||||
// Update document
|
||||
await this.chatMessagesRepository.createQueryBuilder().update()
|
||||
.set({
|
||||
reads: (() => `array_append("reads", '${joining.userId}')`) as any,
|
||||
})
|
||||
.where('id = :id', { id: message.id })
|
||||
.execute();
|
||||
|
||||
reads.push(message.id);
|
||||
}
|
||||
|
||||
// Publish event
|
||||
this.globalEventService.publishGroupChatStream(groupId, 'read', {
|
||||
ids: reads,
|
||||
userId: userId,
|
||||
});
|
||||
this.globalEventService.publishChatIndexStream(userId, 'read', reads);
|
||||
|
||||
if (!await this.userEntityService.getHasUnreadChatMessage(userId)) {
|
||||
// 全ての(いままで未読だった)自分宛てのメッセージを(これで)読みましたよというイベントを発行
|
||||
this.globalEventService.publishMainStream(userId, 'readAllChatMessages');
|
||||
this.pushNotificationService.pushNotification(userId, 'readAllChatMessages', undefined);
|
||||
} else {
|
||||
// そのグループにおいて未読がなければイベント発行
|
||||
const unreadExist = await this.chatMessagesRepository.createQueryBuilder('message')
|
||||
.where('message.groupId = :groupId', { groupId: groupId })
|
||||
.andWhere('message.userId != :userId', { userId: userId })
|
||||
.andWhere('NOT (:userId = ANY(message.reads))', { userId: userId })
|
||||
.andWhere('message.createdAt > :joinedAt', { joinedAt: joining.createdAt }) // 自分が加入する前の会話については、未読扱いしない
|
||||
.getOne().then(x => x != null);
|
||||
|
||||
if (!unreadExist) {
|
||||
this.pushNotificationService.pushNotification(userId, 'readAllChatMessagesOfARoom', { groupId });
|
||||
}
|
||||
}
|
||||
}
|
||||
*/
|
||||
}
|
@@ -75,6 +75,7 @@ import { ClipService } from './ClipService.js';
|
||||
import { FeaturedService } from './FeaturedService.js';
|
||||
import { FanoutTimelineService } from './FanoutTimelineService.js';
|
||||
import { ChannelFollowingService } from './ChannelFollowingService.js';
|
||||
import { ChatService } from './ChatService.js';
|
||||
import { RegistryApiService } from './RegistryApiService.js';
|
||||
import { ReversiService } from './ReversiService.js';
|
||||
|
||||
@@ -100,6 +101,7 @@ import { AppEntityService } from './entities/AppEntityService.js';
|
||||
import { AuthSessionEntityService } from './entities/AuthSessionEntityService.js';
|
||||
import { BlockingEntityService } from './entities/BlockingEntityService.js';
|
||||
import { ChannelEntityService } from './entities/ChannelEntityService.js';
|
||||
import { ChatMessageEntityService } from './entities/ChatMessageEntityService.js';
|
||||
import { ClipEntityService } from './entities/ClipEntityService.js';
|
||||
import { DriveFileEntityService } from './entities/DriveFileEntityService.js';
|
||||
import { DriveFolderEntityService } from './entities/DriveFolderEntityService.js';
|
||||
@@ -221,6 +223,7 @@ const $FeaturedService: Provider = { provide: 'FeaturedService', useExisting: Fe
|
||||
const $FanoutTimelineService: Provider = { provide: 'FanoutTimelineService', useExisting: FanoutTimelineService };
|
||||
const $FanoutTimelineEndpointService: Provider = { provide: 'FanoutTimelineEndpointService', useExisting: FanoutTimelineEndpointService };
|
||||
const $ChannelFollowingService: Provider = { provide: 'ChannelFollowingService', useExisting: ChannelFollowingService };
|
||||
const $ChatService: Provider = { provide: 'ChatService', useExisting: ChatService };
|
||||
const $RegistryApiService: Provider = { provide: 'RegistryApiService', useExisting: RegistryApiService };
|
||||
const $ReversiService: Provider = { provide: 'ReversiService', useExisting: ReversiService };
|
||||
|
||||
@@ -247,6 +250,7 @@ const $AppEntityService: Provider = { provide: 'AppEntityService', useExisting:
|
||||
const $AuthSessionEntityService: Provider = { provide: 'AuthSessionEntityService', useExisting: AuthSessionEntityService };
|
||||
const $BlockingEntityService: Provider = { provide: 'BlockingEntityService', useExisting: BlockingEntityService };
|
||||
const $ChannelEntityService: Provider = { provide: 'ChannelEntityService', useExisting: ChannelEntityService };
|
||||
const $ChatMessageEntityService: Provider = { provide: 'ChatMessageEntityService', useExisting: ChatMessageEntityService };
|
||||
const $ClipEntityService: Provider = { provide: 'ClipEntityService', useExisting: ClipEntityService };
|
||||
const $DriveFileEntityService: Provider = { provide: 'DriveFileEntityService', useExisting: DriveFileEntityService };
|
||||
const $DriveFolderEntityService: Provider = { provide: 'DriveFolderEntityService', useExisting: DriveFolderEntityService };
|
||||
@@ -370,6 +374,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
||||
FanoutTimelineService,
|
||||
FanoutTimelineEndpointService,
|
||||
ChannelFollowingService,
|
||||
ChatService,
|
||||
RegistryApiService,
|
||||
ReversiService,
|
||||
|
||||
@@ -396,6 +401,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
||||
AuthSessionEntityService,
|
||||
BlockingEntityService,
|
||||
ChannelEntityService,
|
||||
ChatMessageEntityService,
|
||||
ClipEntityService,
|
||||
DriveFileEntityService,
|
||||
DriveFolderEntityService,
|
||||
@@ -515,6 +521,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
||||
$FanoutTimelineService,
|
||||
$FanoutTimelineEndpointService,
|
||||
$ChannelFollowingService,
|
||||
$ChatService,
|
||||
$RegistryApiService,
|
||||
$ReversiService,
|
||||
|
||||
@@ -541,6 +548,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
||||
$AuthSessionEntityService,
|
||||
$BlockingEntityService,
|
||||
$ChannelEntityService,
|
||||
$ChatMessageEntityService,
|
||||
$ClipEntityService,
|
||||
$DriveFileEntityService,
|
||||
$DriveFolderEntityService,
|
||||
@@ -661,6 +669,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
||||
FanoutTimelineService,
|
||||
FanoutTimelineEndpointService,
|
||||
ChannelFollowingService,
|
||||
ChatService,
|
||||
RegistryApiService,
|
||||
ReversiService,
|
||||
|
||||
@@ -686,6 +695,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
||||
AuthSessionEntityService,
|
||||
BlockingEntityService,
|
||||
ChannelEntityService,
|
||||
ChatMessageEntityService,
|
||||
ClipEntityService,
|
||||
DriveFileEntityService,
|
||||
DriveFolderEntityService,
|
||||
@@ -804,6 +814,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
||||
$FanoutTimelineService,
|
||||
$FanoutTimelineEndpointService,
|
||||
$ChannelFollowingService,
|
||||
$ChatService,
|
||||
$RegistryApiService,
|
||||
$ReversiService,
|
||||
|
||||
@@ -829,6 +840,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
||||
$AuthSessionEntityService,
|
||||
$BlockingEntityService,
|
||||
$ChannelEntityService,
|
||||
$ChatMessageEntityService,
|
||||
$ClipEntityService,
|
||||
$DriveFileEntityService,
|
||||
$DriveFolderEntityService,
|
||||
|
@@ -163,6 +163,11 @@ export interface AdminEventTypes {
|
||||
};
|
||||
}
|
||||
|
||||
export interface ChatEventTypes {
|
||||
message: Packed<'ChatMessage'>;
|
||||
deleted: Packed<'ChatMessage'>['id'];
|
||||
}
|
||||
|
||||
export interface ReversiEventTypes {
|
||||
matched: {
|
||||
game: Packed<'ReversiGameDetailed'>;
|
||||
@@ -202,7 +207,7 @@ export interface ReversiGameEventTypes {
|
||||
type Events<T extends object> = { [K in keyof T]: { type: K; body: T[K]; } };
|
||||
type EventUnionFromDictionary<
|
||||
T extends object,
|
||||
U = Events<T>
|
||||
U = Events<T>,
|
||||
> = U[keyof U];
|
||||
|
||||
type SerializedAll<T> = {
|
||||
@@ -295,6 +300,10 @@ export type GlobalEvents = {
|
||||
name: 'notesStream';
|
||||
payload: Serialized<Packed<'Note'>>;
|
||||
};
|
||||
chat: {
|
||||
name: `chatStream:${MiUser['id']}-${MiUser['id']}`;
|
||||
payload: EventTypesToEventPayload<ChatEventTypes>;
|
||||
};
|
||||
reversi: {
|
||||
name: `reversiStream:${MiUser['id']}`;
|
||||
payload: EventTypesToEventPayload<ReversiEventTypes>;
|
||||
@@ -393,6 +402,11 @@ export class GlobalEventService {
|
||||
this.publish(`adminStream:${userId}`, type, typeof value === 'undefined' ? null : value);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public publishChatStream<K extends keyof ChatEventTypes>(fromUserId: MiUser['id'], toUserId: MiUser['id'], type: K, value?: ChatEventTypes[K]): void {
|
||||
this.publish(`chatStream:${fromUserId}-${toUserId}`, type, typeof value === 'undefined' ? null : value);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public publishReversiStream<K extends keyof ReversiEventTypes>(userId: MiUser['id'], type: K, value?: ReversiEventTypes[K]): void {
|
||||
this.publish(`reversiStream:${userId}`, type, typeof value === 'undefined' ? null : value);
|
||||
|
127
packages/backend/src/core/entities/ChatMessageEntityService.ts
Normal file
127
packages/backend/src/core/entities/ChatMessageEntityService.ts
Normal file
@@ -0,0 +1,127 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import type { MiUser, ChatMessagesRepository, MiChatMessage } from '@/models/_.js';
|
||||
import { awaitAll } from '@/misc/prelude/await-all.js';
|
||||
import type { Packed } from '@/misc/json-schema.js';
|
||||
import type { } from '@/models/Blocking.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { IdService } from '@/core/IdService.js';
|
||||
import { UserEntityService } from './UserEntityService.js';
|
||||
import { DriveFileEntityService } from './DriveFileEntityService.js';
|
||||
|
||||
@Injectable()
|
||||
export class ChatMessageEntityService {
|
||||
constructor(
|
||||
@Inject(DI.chatMessagesRepository)
|
||||
private chatMessagesRepository: ChatMessagesRepository,
|
||||
|
||||
private userEntityService: UserEntityService,
|
||||
private driveFileEntityService: DriveFileEntityService,
|
||||
private idService: IdService,
|
||||
) {
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async pack(
|
||||
src: MiChatMessage['id'] | MiChatMessage,
|
||||
me: { id: MiUser['id'] },
|
||||
options?: {
|
||||
_hint_?: {
|
||||
packedFiles: Map<MiChatMessage['fileId'], Packed<'DriveFile'> | null>;
|
||||
packedUsers: Map<MiChatMessage['id'], Packed<'UserLite'>>
|
||||
};
|
||||
},
|
||||
): Promise<Packed<'ChatMessage'>> {
|
||||
const packedUsers = options?._hint_?.packedUsers;
|
||||
const packedFiles = options?._hint_?.packedFiles;
|
||||
|
||||
const message = typeof src === 'object' ? src : await this.chatMessagesRepository.findOneByOrFail({ id: src });
|
||||
|
||||
return {
|
||||
id: message.id,
|
||||
createdAt: this.idService.parse(message.id).date.toISOString(),
|
||||
text: message.text,
|
||||
fromUserId: message.fromUserId,
|
||||
fromUser: message.fromUserId !== me.id ? (packedUsers?.get(message.fromUserId) ?? await this.userEntityService.pack(message.fromUser ?? message.fromUserId, me)) : undefined,
|
||||
toUserId: message.toUserId,
|
||||
toUser: (message.toUserId && message.toUserId !== me.id) ? (packedUsers?.get(message.toUserId) ?? await this.userEntityService.pack(message.toUser ?? message.toUserId, me)) : undefined,
|
||||
//toGroupId: message.toGroupId,
|
||||
//toGroup: message.toGroupId && opts.populateGroup ? await this.userGroupEntityService.pack(message.toGroup ?? message.toGroupId) : undefined,
|
||||
fileId: message.fileId,
|
||||
file: message.file ? (packedFiles?.get(message.fileId) ?? await this.driveFileEntityService.pack(message.file)) : null,
|
||||
};
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async packMany(
|
||||
messages: MiChatMessage[],
|
||||
me: { id: MiUser['id'] },
|
||||
) {
|
||||
if (messages.length === 0) return [];
|
||||
|
||||
const excludeMe = (x: MiUser | string) => {
|
||||
if (typeof x === 'string') {
|
||||
return x !== me.id;
|
||||
} else {
|
||||
return x.id !== me.id;
|
||||
}
|
||||
};
|
||||
|
||||
const users = [
|
||||
...messages.map((m) => m.fromUser ?? m.fromUserId).filter(excludeMe),
|
||||
...messages.map((m) => m.toUser ?? m.toUserId).filter(x => x != null).filter(excludeMe),
|
||||
];
|
||||
|
||||
const [packedUsers, packedFiles] = await Promise.all([
|
||||
this.userEntityService.packMany(users, me)
|
||||
.then(users => new Map(users.map(u => [u.id, u]))),
|
||||
this.driveFileEntityService.packMany(messages.map(m => m.file).filter(x => x != null)),
|
||||
]);
|
||||
|
||||
return Promise.all(messages.map(message => this.pack(message, me, { _hint_: { packedUsers, packedFiles } })));
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async packLite(
|
||||
src: MiChatMessage['id'] | MiChatMessage,
|
||||
options?: {
|
||||
_hint_?: {
|
||||
packedFiles: Map<MiChatMessage['fileId'], Packed<'DriveFile'> | null>;
|
||||
};
|
||||
},
|
||||
): Promise<Packed<'ChatMessageLite'>> {
|
||||
const packedFiles = options?._hint_?.packedFiles;
|
||||
|
||||
const message = typeof src === 'object' ? src : await this.chatMessagesRepository.findOneByOrFail({ id: src });
|
||||
|
||||
return {
|
||||
id: message.id,
|
||||
createdAt: this.idService.parse(message.id).date.toISOString(),
|
||||
text: message.text,
|
||||
fromUserId: message.fromUserId,
|
||||
toUserId: message.toUserId,
|
||||
//toGroupId: message.toGroupId,
|
||||
fileId: message.fileId,
|
||||
file: message.file ? (packedFiles?.get(message.fileId) ?? await this.driveFileEntityService.pack(message.file)) : null,
|
||||
};
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async packLiteMany(
|
||||
messages: MiChatMessage[],
|
||||
) {
|
||||
if (messages.length === 0) return [];
|
||||
|
||||
const [packedFiles] = await Promise.all([
|
||||
this.driveFileEntityService.packMany(messages.map(m => m.file).filter(x => x != null)),
|
||||
]);
|
||||
|
||||
return Promise.all(messages.map(message => this.packLite(message, { _hint_: { packedFiles } })));
|
||||
}
|
||||
}
|
||||
|
@@ -83,6 +83,7 @@ export const DI = {
|
||||
flashsRepository: Symbol('flashsRepository'),
|
||||
flashLikesRepository: Symbol('flashLikesRepository'),
|
||||
userMemosRepository: Symbol('userMemosRepository'),
|
||||
chatMessagesRepository: Symbol('chatMessagesRepository'),
|
||||
bubbleGameRecordsRepository: Symbol('bubbleGameRecordsRepository'),
|
||||
reversiGamesRepository: Symbol('reversiGamesRepository'),
|
||||
//#endregion
|
||||
|
@@ -63,6 +63,7 @@ import {
|
||||
} from '@/models/json-schema/meta.js';
|
||||
import { packedSystemWebhookSchema } from '@/models/json-schema/system-webhook.js';
|
||||
import { packedAbuseReportNotificationRecipientSchema } from '@/models/json-schema/abuse-report-notification-recipient.js';
|
||||
import { packedChatMessageSchema, packedChatMessageLiteSchema } from '@/models/json-schema/chat-message.js';
|
||||
|
||||
export const refs = {
|
||||
UserLite: packedUserLiteSchema,
|
||||
@@ -120,6 +121,8 @@ export const refs = {
|
||||
MetaDetailed: packedMetaDetailedSchema,
|
||||
SystemWebhook: packedSystemWebhookSchema,
|
||||
AbuseReportNotificationRecipient: packedAbuseReportNotificationRecipientSchema,
|
||||
ChatMessage: packedChatMessageSchema,
|
||||
ChatMessageLite: packedChatMessageLiteSchema,
|
||||
};
|
||||
|
||||
export type Packed<x extends keyof typeof refs> = SchemaType<typeof refs[x]>;
|
||||
@@ -170,11 +173,11 @@ export interface Schema extends OfSchema {
|
||||
|
||||
type RequiredPropertyNames<s extends Obj> = {
|
||||
[K in keyof s]:
|
||||
// K is not optional
|
||||
s[K]['optional'] extends false ? K :
|
||||
// K has default value
|
||||
s[K]['default'] extends null | string | number | boolean | Record<string, unknown> ? K :
|
||||
never
|
||||
// K is not optional
|
||||
s[K]['optional'] extends false ? K :
|
||||
// K has default value
|
||||
s[K]['default'] extends null | string | number | boolean | Record<string, unknown> ? K :
|
||||
never
|
||||
}[keyof s];
|
||||
|
||||
export type Obj = Record<string, Schema>;
|
||||
@@ -213,11 +216,11 @@ type ObjectSchemaTypeDef<p extends Schema> =
|
||||
p['anyOf'] extends ReadonlyArray<Schema> ? p['anyOf'][number]['required'] extends ReadonlyArray<keyof p['properties']> ?
|
||||
UnionObjType<p['properties'], NonNullable<p['anyOf'][number]['required']>> & ObjType<p['properties'], NonNullable<p['required']>>
|
||||
: never
|
||||
: ObjType<p['properties'], NonNullable<p['required']>>
|
||||
:
|
||||
p['anyOf'] extends ReadonlyArray<Schema> ? never : // see CONTRIBUTING.md
|
||||
p['allOf'] extends ReadonlyArray<Schema> ? UnionToIntersection<UnionSchemaType<p['allOf']>> :
|
||||
any;
|
||||
: ObjType<p['properties'], NonNullable<p['required']>>
|
||||
:
|
||||
p['anyOf'] extends ReadonlyArray<Schema> ? never : // see CONTRIBUTING.md
|
||||
p['allOf'] extends ReadonlyArray<Schema> ? UnionToIntersection<UnionSchemaType<p['allOf']>> :
|
||||
any;
|
||||
|
||||
type ObjectSchemaType<p extends Schema> = NullOrUndefined<p, ObjectSchemaTypeDef<p>>;
|
||||
|
||||
@@ -227,30 +230,30 @@ export type SchemaTypeDef<p extends Schema> =
|
||||
p['type'] extends 'number' ? number :
|
||||
p['type'] extends 'string' ? (
|
||||
p['enum'] extends readonly (string | null)[] ?
|
||||
p['enum'][number] :
|
||||
p['format'] extends 'date-time' ? string : // Dateにする??
|
||||
string
|
||||
p['enum'][number] :
|
||||
p['format'] extends 'date-time' ? string : // Dateにする??
|
||||
string
|
||||
) :
|
||||
p['type'] extends 'boolean' ? boolean :
|
||||
p['type'] extends 'object' ? ObjectSchemaTypeDef<p> :
|
||||
p['type'] extends 'array' ? (
|
||||
p['items'] extends OfSchema ? (
|
||||
p['items']['anyOf'] extends ReadonlyArray<Schema> ? UnionSchemaType<NonNullable<p['items']['anyOf']>>[] :
|
||||
p['items']['oneOf'] extends ReadonlyArray<Schema> ? ArrayUnion<UnionSchemaType<NonNullable<p['items']['oneOf']>>> :
|
||||
p['items']['allOf'] extends ReadonlyArray<Schema> ? UnionToIntersection<UnionSchemaType<NonNullable<p['items']['allOf']>>>[] :
|
||||
never
|
||||
p['type'] extends 'boolean' ? boolean :
|
||||
p['type'] extends 'object' ? ObjectSchemaTypeDef<p> :
|
||||
p['type'] extends 'array' ? (
|
||||
p['items'] extends OfSchema ? (
|
||||
p['items']['anyOf'] extends ReadonlyArray<Schema> ? UnionSchemaType<NonNullable<p['items']['anyOf']>>[] :
|
||||
p['items']['oneOf'] extends ReadonlyArray<Schema> ? ArrayUnion<UnionSchemaType<NonNullable<p['items']['oneOf']>>> :
|
||||
p['items']['allOf'] extends ReadonlyArray<Schema> ? UnionToIntersection<UnionSchemaType<NonNullable<p['items']['allOf']>>>[] :
|
||||
never
|
||||
) :
|
||||
p['prefixItems'] extends ReadonlyArray<Schema> ? (
|
||||
p['items'] extends NonNullable<Schema> ? [...ArrayToTuple<p['prefixItems']>, ...SchemaType<p['items']>[]] :
|
||||
p['items'] extends false ? ArrayToTuple<p['prefixItems']> :
|
||||
p['unevaluatedItems'] extends false ? ArrayToTuple<p['prefixItems']> :
|
||||
[...ArrayToTuple<p['prefixItems']>, ...unknown[]]
|
||||
) :
|
||||
p['items'] extends NonNullable<Schema> ? SchemaType<p['items']>[] :
|
||||
any[]
|
||||
) :
|
||||
p['prefixItems'] extends ReadonlyArray<Schema> ? (
|
||||
p['items'] extends NonNullable<Schema> ? [...ArrayToTuple<p['prefixItems']>, ...SchemaType<p['items']>[]] :
|
||||
p['items'] extends false ? ArrayToTuple<p['prefixItems']> :
|
||||
p['unevaluatedItems'] extends false ? ArrayToTuple<p['prefixItems']> :
|
||||
[...ArrayToTuple<p['prefixItems']>, ...unknown[]]
|
||||
) :
|
||||
p['items'] extends NonNullable<Schema> ? SchemaType<p['items']>[] :
|
||||
any[]
|
||||
) :
|
||||
p['anyOf'] extends ReadonlyArray<Schema> ? UnionSchemaType<p['anyOf']> & PartialIntersection<UnionSchemaType<p['anyOf']>> :
|
||||
p['oneOf'] extends ReadonlyArray<Schema> ? UnionSchemaType<p['oneOf']> :
|
||||
any;
|
||||
p['anyOf'] extends ReadonlyArray<Schema> ? UnionSchemaType<p['anyOf']> & PartialIntersection<UnionSchemaType<p['anyOf']>> :
|
||||
p['oneOf'] extends ReadonlyArray<Schema> ? UnionSchemaType<p['oneOf']> :
|
||||
any;
|
||||
|
||||
export type SchemaType<p extends Schema> = NullOrUndefined<p, SchemaTypeDef<p>>;
|
||||
|
86
packages/backend/src/models/ChatMessage.ts
Normal file
86
packages/backend/src/models/ChatMessage.ts
Normal file
@@ -0,0 +1,86 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm';
|
||||
import { id } from './util/id.js';
|
||||
import { MiUser } from './User.js';
|
||||
import { MiDriveFile } from './DriveFile.js';
|
||||
|
||||
@Entity('chat_message')
|
||||
export class MiChatMessage {
|
||||
@PrimaryColumn(id())
|
||||
public id: string;
|
||||
|
||||
@Index()
|
||||
@Column({
|
||||
...id(),
|
||||
})
|
||||
public fromUserId: MiUser['id'];
|
||||
|
||||
@ManyToOne(type => MiUser, {
|
||||
onDelete: 'CASCADE',
|
||||
})
|
||||
@JoinColumn()
|
||||
public fromUser: MiUser | null;
|
||||
|
||||
@Index()
|
||||
@Column({
|
||||
...id(), nullable: true,
|
||||
})
|
||||
public toUserId: MiUser['id'] | null;
|
||||
|
||||
@ManyToOne(type => MiUser, {
|
||||
onDelete: 'CASCADE',
|
||||
})
|
||||
@JoinColumn()
|
||||
public toUser: MiUser | null;
|
||||
|
||||
/*
|
||||
@Index()
|
||||
@Column({
|
||||
...id(), nullable: true,
|
||||
})
|
||||
public toGroupId: MiUserGroup['id'] | null;
|
||||
|
||||
@ManyToOne(type => MiUserGroup, {
|
||||
onDelete: 'CASCADE',
|
||||
})
|
||||
@JoinColumn()
|
||||
public toGroup: MiUserGroup | null;
|
||||
*/
|
||||
|
||||
@Column('varchar', {
|
||||
length: 4096, nullable: true,
|
||||
})
|
||||
public text: string | null;
|
||||
|
||||
@Column('varchar', {
|
||||
length: 512, nullable: true,
|
||||
})
|
||||
public uri: string | null;
|
||||
|
||||
@Column({
|
||||
...id(),
|
||||
array: true, default: '{}',
|
||||
})
|
||||
public reads: MiUser['id'][];
|
||||
|
||||
@Column({
|
||||
...id(),
|
||||
nullable: true,
|
||||
})
|
||||
public fileId: MiDriveFile['id'] | null;
|
||||
|
||||
@ManyToOne(type => MiDriveFile, {
|
||||
onDelete: 'SET NULL',
|
||||
})
|
||||
@JoinColumn()
|
||||
public file: MiDriveFile | null;
|
||||
|
||||
@Column('varchar', {
|
||||
length: 1024, array: true, default: '{}',
|
||||
})
|
||||
public reactions: string[];
|
||||
}
|
@@ -3,13 +3,10 @@
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { FindOneOptions, InsertQueryBuilder, ObjectLiteral, Repository, SelectQueryBuilder, TypeORMError } from 'typeorm';
|
||||
import { DriverUtils } from 'typeorm/driver/DriverUtils.js';
|
||||
import { FindOneOptions, InsertQueryBuilder, ObjectLiteral, Repository, SelectQueryBuilder } from 'typeorm';
|
||||
import { RelationCountLoader } from 'typeorm/query-builder/relation-count/RelationCountLoader.js';
|
||||
import { RelationIdLoader } from 'typeorm/query-builder/relation-id/RelationIdLoader.js';
|
||||
import { RawSqlResultsToEntityTransformer } from 'typeorm/query-builder/transformer/RawSqlResultsToEntityTransformer.js';
|
||||
import { ObjectUtils } from 'typeorm/util/ObjectUtils.js';
|
||||
import { OrmUtils } from 'typeorm/util/OrmUtils.js';
|
||||
import { MiAbuseUserReport } from '@/models/AbuseUserReport.js';
|
||||
import { MiAbuseReportNotificationRecipient } from '@/models/AbuseReportNotificationRecipient.js';
|
||||
import { MiAccessToken } from '@/models/AccessToken.js';
|
||||
@@ -78,6 +75,7 @@ import { MiRoleAssignment } from '@/models/RoleAssignment.js';
|
||||
import { MiFlash } from '@/models/Flash.js';
|
||||
import { MiFlashLike } from '@/models/FlashLike.js';
|
||||
import { MiUserListFavorite } from '@/models/UserListFavorite.js';
|
||||
import { MiChatMessage } from '@/models/ChatMessage.js';
|
||||
import { MiBubbleGameRecord } from '@/models/BubbleGameRecord.js';
|
||||
import { MiReversiGame } from '@/models/ReversiGame.js';
|
||||
import type { QueryDeepPartialEntity } from 'typeorm/query-builder/QueryPartialEntity.js';
|
||||
@@ -194,6 +192,7 @@ export {
|
||||
MiFlash,
|
||||
MiFlashLike,
|
||||
MiUserMemo,
|
||||
MiChatMessage,
|
||||
MiBubbleGameRecord,
|
||||
MiReversiGame,
|
||||
};
|
||||
@@ -266,5 +265,6 @@ export type RoleAssignmentsRepository = Repository<MiRoleAssignment> & MiReposit
|
||||
export type FlashsRepository = Repository<MiFlash> & MiRepository<MiFlash>;
|
||||
export type FlashLikesRepository = Repository<MiFlashLike> & MiRepository<MiFlashLike>;
|
||||
export type UserMemoRepository = Repository<MiUserMemo> & MiRepository<MiUserMemo>;
|
||||
export type ChatMessagesRepository = Repository<MiChatMessage> & MiRepository<MiChatMessage>;
|
||||
export type BubbleGameRecordsRepository = Repository<MiBubbleGameRecord> & MiRepository<MiBubbleGameRecord>;
|
||||
export type ReversiGamesRepository = Repository<MiReversiGame> & MiRepository<MiReversiGame>;
|
||||
|
86
packages/backend/src/models/json-schema/chat-message.ts
Normal file
86
packages/backend/src/models/json-schema/chat-message.ts
Normal file
@@ -0,0 +1,86 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
export const packedChatMessageSchema = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: {
|
||||
type: 'string',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
createdAt: {
|
||||
type: 'string',
|
||||
format: 'date-time',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
fromUserId: {
|
||||
type: 'string',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
fromUser: {
|
||||
type: 'object',
|
||||
optional: true, nullable: false,
|
||||
ref: 'UserLite',
|
||||
},
|
||||
toUserId: {
|
||||
type: 'string',
|
||||
optional: true, nullable: true,
|
||||
},
|
||||
toUser: {
|
||||
type: 'object',
|
||||
optional: true, nullable: true,
|
||||
ref: 'UserLite',
|
||||
},
|
||||
text: {
|
||||
type: 'string',
|
||||
optional: true, nullable: true,
|
||||
},
|
||||
fileId: {
|
||||
type: 'string',
|
||||
optional: true, nullable: true,
|
||||
},
|
||||
file: {
|
||||
type: 'object',
|
||||
optional: true, nullable: true,
|
||||
ref: 'DriveFile',
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
|
||||
export const packedChatMessageLiteSchema = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: {
|
||||
type: 'string',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
createdAt: {
|
||||
type: 'string',
|
||||
format: 'date-time',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
fromUserId: {
|
||||
type: 'string',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
toUserId: {
|
||||
type: 'string',
|
||||
optional: true, nullable: true,
|
||||
},
|
||||
text: {
|
||||
type: 'string',
|
||||
optional: true, nullable: true,
|
||||
},
|
||||
fileId: {
|
||||
type: 'string',
|
||||
optional: true, nullable: true,
|
||||
},
|
||||
file: {
|
||||
type: 'object',
|
||||
optional: true, nullable: true,
|
||||
ref: 'DriveFile',
|
||||
},
|
||||
},
|
||||
} as const;
|
@@ -76,6 +76,7 @@ import { MiRoleAssignment } from '@/models/RoleAssignment.js';
|
||||
import { MiFlash } from '@/models/Flash.js';
|
||||
import { MiFlashLike } from '@/models/FlashLike.js';
|
||||
import { MiUserMemo } from '@/models/UserMemo.js';
|
||||
import { MiChatMessage } from '@/models/ChatMessage.js';
|
||||
import { MiBubbleGameRecord } from '@/models/BubbleGameRecord.js';
|
||||
import { MiReversiGame } from '@/models/ReversiGame.js';
|
||||
|
||||
@@ -236,6 +237,7 @@ export const entities = [
|
||||
MiFlash,
|
||||
MiFlashLike,
|
||||
MiUserMemo,
|
||||
MiChatMessage,
|
||||
MiBubbleGameRecord,
|
||||
MiReversiGame,
|
||||
...charts,
|
||||
|
@@ -397,4 +397,5 @@ export * as 'users/search' from './endpoints/users/search.js';
|
||||
export * as 'users/search-by-username-and-host' from './endpoints/users/search-by-username-and-host.js';
|
||||
export * as 'users/show' from './endpoints/users/show.js';
|
||||
export * as 'users/update-memo' from './endpoints/users/update-memo.js';
|
||||
export * as 'chat/messages/create' from './endpoints/chat/messages/create.js';
|
||||
export * as 'v2/admin/emoji/list' from './endpoints/v2/admin/emoji/list.js';
|
||||
|
@@ -0,0 +1,169 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import ms from 'ms';
|
||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||
import { GetterService } from '@/server/api/GetterService.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { ApiError } from '@/server/api/error.js';
|
||||
import { ChatService } from '@/core/ChatService.js';
|
||||
import { DriveFilesRepository, MiUser } from '@/models/_.js';
|
||||
|
||||
export const meta = {
|
||||
tags: ['chat'],
|
||||
|
||||
requireCredential: true,
|
||||
|
||||
kind: 'write:chat',
|
||||
|
||||
limit: {
|
||||
duration: ms('1hour'),
|
||||
max: 500,
|
||||
},
|
||||
|
||||
res: {
|
||||
type: 'object',
|
||||
optional: false, nullable: false,
|
||||
ref: 'ChatMessageLite',
|
||||
},
|
||||
|
||||
errors: {
|
||||
recipientIsYourself: {
|
||||
message: 'You can not send a message to yourself.',
|
||||
code: 'RECIPIENT_IS_YOURSELF',
|
||||
id: '17e2ba79-e22a-4cbc-bf91-d327643f4a7e',
|
||||
},
|
||||
|
||||
noSuchUser: {
|
||||
message: 'No such user.',
|
||||
code: 'NO_SUCH_USER',
|
||||
id: '11795c64-40ea-4198-b06e-3c873ed9039d',
|
||||
},
|
||||
|
||||
noSuchGroup: {
|
||||
message: 'No such group.',
|
||||
code: 'NO_SUCH_GROUP',
|
||||
id: 'c94e2a5d-06aa-4914-8fa6-6a42e73d6537',
|
||||
},
|
||||
|
||||
groupAccessDenied: {
|
||||
message: 'You can not send messages to groups that you have not joined.',
|
||||
code: 'GROUP_ACCESS_DENIED',
|
||||
id: 'd96b3cca-5ad1-438b-ad8b-02f931308fbd',
|
||||
},
|
||||
|
||||
noSuchFile: {
|
||||
message: 'No such file.',
|
||||
code: 'NO_SUCH_FILE',
|
||||
id: '4372b8e2-185d-4146-8749-2f68864a3e5f',
|
||||
},
|
||||
|
||||
contentRequired: {
|
||||
message: 'Content required. You need to set text or fileId.',
|
||||
code: 'CONTENT_REQUIRED',
|
||||
id: '25587321-b0e6-449c-9239-f8925092942c',
|
||||
},
|
||||
|
||||
youHaveBeenBlocked: {
|
||||
message: 'You cannot send a message because you have been blocked by this user.',
|
||||
code: 'YOU_HAVE_BEEN_BLOCKED',
|
||||
id: 'c15a5199-7422-4968-941a-2a462c478f7d',
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
|
||||
export const paramDef = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
text: { type: 'string', nullable: true, maxLength: 2000 },
|
||||
fileId: { type: 'string', format: 'misskey:id' },
|
||||
},
|
||||
anyOf: [
|
||||
{
|
||||
properties: {
|
||||
userId: { type: 'string', format: 'misskey:id' },
|
||||
},
|
||||
required: ['userId'],
|
||||
},
|
||||
{
|
||||
properties: {
|
||||
groupId: { type: 'string', format: 'misskey:id' },
|
||||
},
|
||||
required: ['groupId'],
|
||||
},
|
||||
],
|
||||
} as const;
|
||||
|
||||
@Injectable()
|
||||
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
|
||||
constructor(
|
||||
@Inject(DI.driveFilesRepository)
|
||||
private driveFilesRepository: DriveFilesRepository,
|
||||
|
||||
private getterService: GetterService,
|
||||
private chatService: ChatService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
let toUser: MiUser | null;
|
||||
//let toGroup: UserGroup | null;
|
||||
|
||||
if (ps.userId != null) {
|
||||
// Myself
|
||||
if (ps.userId === me.id) {
|
||||
throw new ApiError(meta.errors.recipientIsYourself);
|
||||
}
|
||||
|
||||
// Fetch recipient (user)
|
||||
toUser = await this.getterService.getUser(ps.userId).catch(err => {
|
||||
if (err.id === '15348ddd-432d-49c2-8a5a-8069753becff') throw new ApiError(meta.errors.noSuchUser);
|
||||
throw err;
|
||||
});
|
||||
}/* else if (ps.groupId != null) {
|
||||
// Fetch recipient (group)
|
||||
recipientGroup = await this.userGroupsRepository.findOneBy({ id: ps.groupId! });
|
||||
|
||||
if (recipientGroup == null) {
|
||||
throw new ApiError(meta.errors.noSuchGroup);
|
||||
}
|
||||
|
||||
// check joined
|
||||
const joining = await this.userGroupJoiningsRepository.findOneBy({
|
||||
userId: me.id,
|
||||
userGroupId: recipientGroup.id,
|
||||
});
|
||||
|
||||
if (joining == null) {
|
||||
throw new ApiError(meta.errors.groupAccessDenied);
|
||||
}
|
||||
}*/
|
||||
|
||||
let file = null;
|
||||
if (ps.fileId != null) {
|
||||
file = await this.driveFilesRepository.findOneBy({
|
||||
id: ps.fileId,
|
||||
userId: me.id,
|
||||
});
|
||||
|
||||
if (file == null) {
|
||||
throw new ApiError(meta.errors.noSuchFile);
|
||||
}
|
||||
}
|
||||
|
||||
// テキストが無いかつ添付ファイルも無かったらエラー
|
||||
if (ps.text == null && file == null) {
|
||||
throw new ApiError(meta.errors.contentRequired);
|
||||
}
|
||||
|
||||
return await this.chatService.createMessage({
|
||||
fromUser: me,
|
||||
toUser,
|
||||
toGroup,
|
||||
text: ps.text,
|
||||
file: file,
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user