Skip to content

Commit

Permalink
feat: add thread entity (Partial)
Browse files Browse the repository at this point in the history
  • Loading branch information
marrouchi committed Nov 13, 2024
1 parent a7e243d commit 4d30aa3
Show file tree
Hide file tree
Showing 11 changed files with 217 additions and 7 deletions.
17 changes: 12 additions & 5 deletions api/src/channel/lib/EventWrapper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,8 @@ import ChannelHandler from './Handler';

export interface ChannelEvent {}

export default abstract class EventWrapper<
A,
E,
C extends ChannelHandler = ChannelHandler,
> {
// eslint-disable-next-line prettier/prettier
export default abstract class EventWrapper<A, E, C extends ChannelHandler = ChannelHandler> {
_adapter: A = {} as A;

_handler: C;
Expand Down Expand Up @@ -203,6 +200,16 @@ export default abstract class EventWrapper<
*/
abstract getPayload(): Payload | string | undefined;

/**
* Retrieves the thread id to which the message belongs to
*
* @returns Thread id
*/
getThreadId(): string {
// To be implemented in child class when needed
return undefined;
}

/**
* Returns the message in a standardized format
*
Expand Down
7 changes: 7 additions & 0 deletions api/src/chat/chat.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,13 +28,15 @@ import { ConversationRepository } from './repositories/conversation.repository';
import { LabelRepository } from './repositories/label.repository';
import { MessageRepository } from './repositories/message.repository';
import { SubscriberRepository } from './repositories/subscriber.repository';
import { ThreadRepository } from './repositories/thread.repository';
import { BlockModel } from './schemas/block.schema';
import { CategoryModel } from './schemas/category.schema';
import { ContextVarModel } from './schemas/context-var.schema';
import { ConversationModel } from './schemas/conversation.schema';
import { LabelModel } from './schemas/label.schema';
import { MessageModel } from './schemas/message.schema';
import { SubscriberModel } from './schemas/subscriber.schema';
import { ThreadModel } from './schemas/thread.schema';
import { CategorySeeder } from './seeds/category.seed';
import { ContextVarSeeder } from './seeds/context-var.seed';
import { BlockService } from './services/block.service';
Expand All @@ -46,6 +48,7 @@ import { ConversationService } from './services/conversation.service';
import { LabelService } from './services/label.service';
import { MessageService } from './services/message.service';
import { SubscriberService } from './services/subscriber.service';
import { ThreadService } from './services/thread.service';

@Module({
imports: [
Expand All @@ -58,6 +61,7 @@ import { SubscriberService } from './services/subscriber.service';
SubscriberModel,
ConversationModel,
SubscriberModel,
ThreadModel,
]),
forwardRef(() => ChannelModule),
CmsModule,
Expand All @@ -81,6 +85,7 @@ import { SubscriberService } from './services/subscriber.service';
MessageRepository,
SubscriberRepository,
ConversationRepository,
ThreadRepository,
CategoryService,
ContextVarService,
LabelService,
Expand All @@ -92,13 +97,15 @@ import { SubscriberService } from './services/subscriber.service';
ConversationService,
ChatService,
BotService,
ThreadService,
],
exports: [
SubscriberService,
MessageService,
LabelService,
BlockService,
ConversationService,
ThreadService,
],
})
export class ChatModule {}
7 changes: 6 additions & 1 deletion api/src/chat/dto/message.dto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@ import {
IsBoolean,
IsNotEmpty,
IsObject,
IsString,
IsOptional,
IsString,
} from 'class-validator';

import { IsObjectId } from '@/utils/validation-rules/is-object-id';
Expand Down Expand Up @@ -58,6 +58,11 @@ export class MessageCreateDto {
@IsValidMessageText({ message: 'Message should have text property' })
message: StdOutgoingMessage | StdIncomingMessage;

@ApiProperty({ description: 'Thread', type: String })
@IsString()
@IsOptional()
thread?: string;

@ApiPropertyOptional({ description: 'Message is read', type: Boolean })
@IsBoolean()
@IsNotEmpty()
Expand Down
35 changes: 35 additions & 0 deletions api/src/chat/repositories/thread.repository.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
/*
* Copyright © 2024 Hexastack. All rights reserved.
*
* Licensed under the GNU Affero General Public License v3.0 (AGPLv3) with the following additional terms:
* 1. The name "Hexabot" is a trademark of Hexastack. You may not use this name in derivative works without express written permission.
* 2. All derivative works must include clear attribution to the original creator and software, Hexastack and Hexabot, in a prominent location (e.g., in the software's "About" section, documentation, and README file).
*/

import { Injectable } from '@nestjs/common';
import { EventEmitter2 } from '@nestjs/event-emitter';
import { InjectModel } from '@nestjs/mongoose';
import { Model } from 'mongoose';

import { BaseRepository } from '@/utils/generics/base-repository';

import {
Thread,
THREAD_POPULATE,
ThreadFull,
ThreadPopulate,
} from '../schemas/thread.schema';

@Injectable()
export class ThreadRepository extends BaseRepository<
Thread,
ThreadPopulate,
ThreadFull
> {
constructor(
readonly eventEmitter: EventEmitter2,
@InjectModel(Thread.name) readonly model: Model<Thread>,
) {
super(eventEmitter, model, Thread, THREAD_POPULATE, ThreadFull);
}
}
14 changes: 14 additions & 0 deletions api/src/chat/schemas/message.schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import { LifecycleHookManager } from '@/utils/generics/lifecycle-hook-manager';
import { TFilterPopulateFields } from '@/utils/types/filter.types';

import { Subscriber } from './subscriber.schema';
import { Thread } from './thread.schema';
import { StdIncomingMessage, StdOutgoingMessage } from './types/message';

@Schema({ timestamps: true })
Expand Down Expand Up @@ -47,6 +48,13 @@ export class MessageStub extends BaseSchema {
})
sentBy?: unknown;

@Prop({
type: MongooseSchema.Types.ObjectId,
required: false,
ref: 'Thread',
})
thread?: unknown;

@Prop({
type: Object,
required: true,
Expand Down Expand Up @@ -82,6 +90,9 @@ export class Message extends MessageStub {

@Transform(({ obj }) => obj.sentBy?.toString())
sentBy?: string;

@Transform(({ obj }) => obj.thread?.toString())
thread?: string;
}

@Schema({ timestamps: true })
Expand All @@ -94,6 +105,9 @@ export class MessageFull extends MessageStub {

@Transform(({ obj }) => obj.sentBy?.toString())
sentBy?: string; // sendBy is never populate

@Transform(() => Thread)
thread?: Thread;
}

export const MessageModel: ModelDefinition = LifecycleHookManager.attach({
Expand Down
76 changes: 76 additions & 0 deletions api/src/chat/schemas/thread.schema.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
/*
* Copyright © 2024 Hexastack. All rights reserved.
*
* Licensed under the GNU Affero General Public License v3.0 (AGPLv3) with the following additional terms:
* 1. The name "Hexabot" is a trademark of Hexastack. You may not use this name in derivative works without express written permission.
* 2. All derivative works must include clear attribution to the original creator and software, Hexastack and Hexabot, in a prominent location (e.g., in the software's "About" section, documentation, and README file).
*/

import { ModelDefinition, Prop, Schema, SchemaFactory } from '@nestjs/mongoose';
import { Exclude, Transform, Type } from 'class-transformer';
import { Schema as MongooseSchema } from 'mongoose';

import { BaseSchema } from '@/utils/generics/base-schema';
import { LifecycleHookManager } from '@/utils/generics/lifecycle-hook-manager';
import {
TFilterPopulateFields,
THydratedDocument,
} from '@/utils/types/filter.types';

import { Message } from './message.schema';
import { Subscriber } from './subscriber.schema';

@Schema({ timestamps: true })
export class ThreadStub extends BaseSchema {
@Prop({
type: String,
unique: true,
required: true,
})
title: string;

@Prop({
type: MongooseSchema.Types.ObjectId,
required: false,
ref: 'Subscriber',
})
subscriber?: unknown;
}

@Schema({ timestamps: true })
export class Thread extends ThreadStub {
@Transform(({ obj }) => obj.subscriber?.toString())
subscriber: string;

@Exclude()
messages?: never;
}

@Schema({ timestamps: true })
export class ThreadFull extends ThreadStub {
@Type(() => Subscriber)
subscriber: Subscriber;

@Type(() => Message)
messages?: Message[];
}

export type ThreadDocument = THydratedDocument<Thread>;

export const ThreadModel: ModelDefinition = LifecycleHookManager.attach({
name: Thread.name,
schema: SchemaFactory.createForClass(ThreadStub),
});

ThreadModel.schema.virtual('messages', {
ref: 'Message',
localField: '_id',
foreignField: 'thread',
justOne: false,
});

export default ThreadModel.schema;

export type ThreadPopulate = keyof TFilterPopulateFields<Thread, ThreadStub>;

export const THREAD_POPULATE: ThreadPopulate[] = ['messages', 'subscriber'];
2 changes: 2 additions & 0 deletions api/src/chat/services/chat.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,8 +106,10 @@ export class ChatService {
this.logger.warn('Failed to get the event id', messageId);
}
const subscriber = event.getSender();

const received: MessageCreateDto = {
mid: messageId,
thread: event.getThreadId(),
sender: subscriber.id,
message: event.getMessage(),
delivery: true,
Expand Down
18 changes: 18 additions & 0 deletions api/src/chat/services/message.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import { WebsocketGateway } from '@/websocket/websocket.gateway';
import { MessageRepository } from '../repositories/message.repository';
import { MessageFull, MessagePopulate } from '../schemas/message.schema';
import { Subscriber } from '../schemas/subscriber.schema';
import { ThreadStub } from '../schemas/thread.schema';
import { AnyMessage } from '../schemas/types/message';

@Injectable()
Expand Down Expand Up @@ -135,4 +136,21 @@ export class MessageService extends BaseService<

return lastMessages.reverse();
}

/**
* Retrieves all the messages of a given thread
*
* @param subscriber The subscriber whose messages is being retrieved.
* @param threadId Thread ID
*
* @returns All messages.
*/
async findByThread<T extends ThreadStub>(thread: T) {
return await this.find(
{
thread: thread.id,
},
['createdAt', 'asc'],
);
}
}
43 changes: 43 additions & 0 deletions api/src/chat/services/thread.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
/*
* Copyright © 2024 Hexastack. All rights reserved.
*
* Licensed under the GNU Affero General Public License v3.0 (AGPLv3) with the following additional terms:
* 1. The name "Hexabot" is a trademark of Hexastack. You may not use this name in derivative works without express written permission.
* 2. All derivative works must include clear attribution to the original creator and software, Hexastack and Hexabot, in a prominent location (e.g., in the software's "About" section, documentation, and README file).
*/

import { Injectable } from '@nestjs/common';

import { BaseService } from '@/utils/generics/base-service';

import { ThreadRepository } from '../repositories/thread.repository';
import { SubscriberStub } from '../schemas/subscriber.schema';
import { Thread, ThreadFull, ThreadPopulate } from '../schemas/thread.schema';

@Injectable()
export class ThreadService extends BaseService<
Thread,
ThreadPopulate,
ThreadFull
> {
constructor(private readonly threadRepository: ThreadRepository) {
super(threadRepository);
}

/**
* Retrieves the latest thread for a given subscriber
*
* @param subscriber The subscriber whose thread is being retrieved.
*
* @returns Last thread
*/
async findLast<S extends SubscriberStub>(subscriber: S) {
const [thread] = await this.findPage(
{
subscriber: subscriber.id,
},
{ skip: 0, limit: 1, sort: ['createdAt', 'desc'] },
);
return thread;
}
}
3 changes: 2 additions & 1 deletion api/src/extensions/channels/web/base-web-channel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -762,11 +762,12 @@ export default abstract class BaseWebChannelHandler<
channelData,
);
if (event.getEventType() === 'message') {
// Handler sync message sent by chabbot
// Handle sync message sent by the bot
if (data.sync && data.author === 'chatbot') {
const sentMessage: MessageCreateDto = {
mid: event.getId(),
message: event.getMessage() as StdOutgoingMessage,
thread: event.getThreadId(),
recipient: profile.id,
read: true,
delivery: true,
Expand Down
2 changes: 2 additions & 0 deletions api/src/extensions/channels/web/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,7 @@ export namespace Web {
| IncomingAttachmentMessage,
> = T & {
mid?: string;
thread?: string;
author?: string;
read?: boolean;
delivery?: boolean;
Expand Down Expand Up @@ -231,6 +232,7 @@ export namespace Web {

export type OutgoingMessage = OutgoingMessageBase & {
mid: string;
thread?: string;
author: string;
read?: boolean;
createdAt: Date;
Expand Down

0 comments on commit 4d30aa3

Please sign in to comment.