Skip to content

Commit 4d30aa3

Browse files
committed
feat: add thread entity (Partial)
1 parent a7e243d commit 4d30aa3

File tree

11 files changed

+217
-7
lines changed

11 files changed

+217
-7
lines changed

api/src/channel/lib/EventWrapper.ts

+12-5
Original file line numberDiff line numberDiff line change
@@ -23,11 +23,8 @@ import ChannelHandler from './Handler';
2323

2424
export interface ChannelEvent {}
2525

26-
export default abstract class EventWrapper<
27-
A,
28-
E,
29-
C extends ChannelHandler = ChannelHandler,
30-
> {
26+
// eslint-disable-next-line prettier/prettier
27+
export default abstract class EventWrapper<A, E, C extends ChannelHandler = ChannelHandler> {
3128
_adapter: A = {} as A;
3229

3330
_handler: C;
@@ -203,6 +200,16 @@ export default abstract class EventWrapper<
203200
*/
204201
abstract getPayload(): Payload | string | undefined;
205202

203+
/**
204+
* Retrieves the thread id to which the message belongs to
205+
*
206+
* @returns Thread id
207+
*/
208+
getThreadId(): string {
209+
// To be implemented in child class when needed
210+
return undefined;
211+
}
212+
206213
/**
207214
* Returns the message in a standardized format
208215
*

api/src/chat/chat.module.ts

+7
Original file line numberDiff line numberDiff line change
@@ -28,13 +28,15 @@ import { ConversationRepository } from './repositories/conversation.repository';
2828
import { LabelRepository } from './repositories/label.repository';
2929
import { MessageRepository } from './repositories/message.repository';
3030
import { SubscriberRepository } from './repositories/subscriber.repository';
31+
import { ThreadRepository } from './repositories/thread.repository';
3132
import { BlockModel } from './schemas/block.schema';
3233
import { CategoryModel } from './schemas/category.schema';
3334
import { ContextVarModel } from './schemas/context-var.schema';
3435
import { ConversationModel } from './schemas/conversation.schema';
3536
import { LabelModel } from './schemas/label.schema';
3637
import { MessageModel } from './schemas/message.schema';
3738
import { SubscriberModel } from './schemas/subscriber.schema';
39+
import { ThreadModel } from './schemas/thread.schema';
3840
import { CategorySeeder } from './seeds/category.seed';
3941
import { ContextVarSeeder } from './seeds/context-var.seed';
4042
import { BlockService } from './services/block.service';
@@ -46,6 +48,7 @@ import { ConversationService } from './services/conversation.service';
4648
import { LabelService } from './services/label.service';
4749
import { MessageService } from './services/message.service';
4850
import { SubscriberService } from './services/subscriber.service';
51+
import { ThreadService } from './services/thread.service';
4952

5053
@Module({
5154
imports: [
@@ -58,6 +61,7 @@ import { SubscriberService } from './services/subscriber.service';
5861
SubscriberModel,
5962
ConversationModel,
6063
SubscriberModel,
64+
ThreadModel,
6165
]),
6266
forwardRef(() => ChannelModule),
6367
CmsModule,
@@ -81,6 +85,7 @@ import { SubscriberService } from './services/subscriber.service';
8185
MessageRepository,
8286
SubscriberRepository,
8387
ConversationRepository,
88+
ThreadRepository,
8489
CategoryService,
8590
ContextVarService,
8691
LabelService,
@@ -92,13 +97,15 @@ import { SubscriberService } from './services/subscriber.service';
9297
ConversationService,
9398
ChatService,
9499
BotService,
100+
ThreadService,
95101
],
96102
exports: [
97103
SubscriberService,
98104
MessageService,
99105
LabelService,
100106
BlockService,
101107
ConversationService,
108+
ThreadService,
102109
],
103110
})
104111
export class ChatModule {}

api/src/chat/dto/message.dto.ts

+6-1
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,8 @@ import {
1111
IsBoolean,
1212
IsNotEmpty,
1313
IsObject,
14-
IsString,
1514
IsOptional,
15+
IsString,
1616
} from 'class-validator';
1717

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

61+
@ApiProperty({ description: 'Thread', type: String })
62+
@IsString()
63+
@IsOptional()
64+
thread?: string;
65+
6166
@ApiPropertyOptional({ description: 'Message is read', type: Boolean })
6267
@IsBoolean()
6368
@IsNotEmpty()
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
/*
2+
* Copyright © 2024 Hexastack. All rights reserved.
3+
*
4+
* Licensed under the GNU Affero General Public License v3.0 (AGPLv3) with the following additional terms:
5+
* 1. The name "Hexabot" is a trademark of Hexastack. You may not use this name in derivative works without express written permission.
6+
* 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).
7+
*/
8+
9+
import { Injectable } from '@nestjs/common';
10+
import { EventEmitter2 } from '@nestjs/event-emitter';
11+
import { InjectModel } from '@nestjs/mongoose';
12+
import { Model } from 'mongoose';
13+
14+
import { BaseRepository } from '@/utils/generics/base-repository';
15+
16+
import {
17+
Thread,
18+
THREAD_POPULATE,
19+
ThreadFull,
20+
ThreadPopulate,
21+
} from '../schemas/thread.schema';
22+
23+
@Injectable()
24+
export class ThreadRepository extends BaseRepository<
25+
Thread,
26+
ThreadPopulate,
27+
ThreadFull
28+
> {
29+
constructor(
30+
readonly eventEmitter: EventEmitter2,
31+
@InjectModel(Thread.name) readonly model: Model<Thread>,
32+
) {
33+
super(eventEmitter, model, Thread, THREAD_POPULATE, ThreadFull);
34+
}
35+
}

api/src/chat/schemas/message.schema.ts

+14
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import { LifecycleHookManager } from '@/utils/generics/lifecycle-hook-manager';
1515
import { TFilterPopulateFields } from '@/utils/types/filter.types';
1616

1717
import { Subscriber } from './subscriber.schema';
18+
import { Thread } from './thread.schema';
1819
import { StdIncomingMessage, StdOutgoingMessage } from './types/message';
1920

2021
@Schema({ timestamps: true })
@@ -47,6 +48,13 @@ export class MessageStub extends BaseSchema {
4748
})
4849
sentBy?: unknown;
4950

51+
@Prop({
52+
type: MongooseSchema.Types.ObjectId,
53+
required: false,
54+
ref: 'Thread',
55+
})
56+
thread?: unknown;
57+
5058
@Prop({
5159
type: Object,
5260
required: true,
@@ -82,6 +90,9 @@ export class Message extends MessageStub {
8290

8391
@Transform(({ obj }) => obj.sentBy?.toString())
8492
sentBy?: string;
93+
94+
@Transform(({ obj }) => obj.thread?.toString())
95+
thread?: string;
8596
}
8697

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

95106
@Transform(({ obj }) => obj.sentBy?.toString())
96107
sentBy?: string; // sendBy is never populate
108+
109+
@Transform(() => Thread)
110+
thread?: Thread;
97111
}
98112

99113
export const MessageModel: ModelDefinition = LifecycleHookManager.attach({

api/src/chat/schemas/thread.schema.ts

+76
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
/*
2+
* Copyright © 2024 Hexastack. All rights reserved.
3+
*
4+
* Licensed under the GNU Affero General Public License v3.0 (AGPLv3) with the following additional terms:
5+
* 1. The name "Hexabot" is a trademark of Hexastack. You may not use this name in derivative works without express written permission.
6+
* 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).
7+
*/
8+
9+
import { ModelDefinition, Prop, Schema, SchemaFactory } from '@nestjs/mongoose';
10+
import { Exclude, Transform, Type } from 'class-transformer';
11+
import { Schema as MongooseSchema } from 'mongoose';
12+
13+
import { BaseSchema } from '@/utils/generics/base-schema';
14+
import { LifecycleHookManager } from '@/utils/generics/lifecycle-hook-manager';
15+
import {
16+
TFilterPopulateFields,
17+
THydratedDocument,
18+
} from '@/utils/types/filter.types';
19+
20+
import { Message } from './message.schema';
21+
import { Subscriber } from './subscriber.schema';
22+
23+
@Schema({ timestamps: true })
24+
export class ThreadStub extends BaseSchema {
25+
@Prop({
26+
type: String,
27+
unique: true,
28+
required: true,
29+
})
30+
title: string;
31+
32+
@Prop({
33+
type: MongooseSchema.Types.ObjectId,
34+
required: false,
35+
ref: 'Subscriber',
36+
})
37+
subscriber?: unknown;
38+
}
39+
40+
@Schema({ timestamps: true })
41+
export class Thread extends ThreadStub {
42+
@Transform(({ obj }) => obj.subscriber?.toString())
43+
subscriber: string;
44+
45+
@Exclude()
46+
messages?: never;
47+
}
48+
49+
@Schema({ timestamps: true })
50+
export class ThreadFull extends ThreadStub {
51+
@Type(() => Subscriber)
52+
subscriber: Subscriber;
53+
54+
@Type(() => Message)
55+
messages?: Message[];
56+
}
57+
58+
export type ThreadDocument = THydratedDocument<Thread>;
59+
60+
export const ThreadModel: ModelDefinition = LifecycleHookManager.attach({
61+
name: Thread.name,
62+
schema: SchemaFactory.createForClass(ThreadStub),
63+
});
64+
65+
ThreadModel.schema.virtual('messages', {
66+
ref: 'Message',
67+
localField: '_id',
68+
foreignField: 'thread',
69+
justOne: false,
70+
});
71+
72+
export default ThreadModel.schema;
73+
74+
export type ThreadPopulate = keyof TFilterPopulateFields<Thread, ThreadStub>;
75+
76+
export const THREAD_POPULATE: ThreadPopulate[] = ['messages', 'subscriber'];

api/src/chat/services/chat.service.ts

+2
Original file line numberDiff line numberDiff line change
@@ -106,8 +106,10 @@ export class ChatService {
106106
this.logger.warn('Failed to get the event id', messageId);
107107
}
108108
const subscriber = event.getSender();
109+
109110
const received: MessageCreateDto = {
110111
mid: messageId,
112+
thread: event.getThreadId(),
111113
sender: subscriber.id,
112114
message: event.getMessage(),
113115
delivery: true,

api/src/chat/services/message.service.ts

+18
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import { WebsocketGateway } from '@/websocket/websocket.gateway';
2828
import { MessageRepository } from '../repositories/message.repository';
2929
import { MessageFull, MessagePopulate } from '../schemas/message.schema';
3030
import { Subscriber } from '../schemas/subscriber.schema';
31+
import { ThreadStub } from '../schemas/thread.schema';
3132
import { AnyMessage } from '../schemas/types/message';
3233

3334
@Injectable()
@@ -135,4 +136,21 @@ export class MessageService extends BaseService<
135136

136137
return lastMessages.reverse();
137138
}
139+
140+
/**
141+
* Retrieves all the messages of a given thread
142+
*
143+
* @param subscriber The subscriber whose messages is being retrieved.
144+
* @param threadId Thread ID
145+
*
146+
* @returns All messages.
147+
*/
148+
async findByThread<T extends ThreadStub>(thread: T) {
149+
return await this.find(
150+
{
151+
thread: thread.id,
152+
},
153+
['createdAt', 'asc'],
154+
);
155+
}
138156
}
+43
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
/*
2+
* Copyright © 2024 Hexastack. All rights reserved.
3+
*
4+
* Licensed under the GNU Affero General Public License v3.0 (AGPLv3) with the following additional terms:
5+
* 1. The name "Hexabot" is a trademark of Hexastack. You may not use this name in derivative works without express written permission.
6+
* 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).
7+
*/
8+
9+
import { Injectable } from '@nestjs/common';
10+
11+
import { BaseService } from '@/utils/generics/base-service';
12+
13+
import { ThreadRepository } from '../repositories/thread.repository';
14+
import { SubscriberStub } from '../schemas/subscriber.schema';
15+
import { Thread, ThreadFull, ThreadPopulate } from '../schemas/thread.schema';
16+
17+
@Injectable()
18+
export class ThreadService extends BaseService<
19+
Thread,
20+
ThreadPopulate,
21+
ThreadFull
22+
> {
23+
constructor(private readonly threadRepository: ThreadRepository) {
24+
super(threadRepository);
25+
}
26+
27+
/**
28+
* Retrieves the latest thread for a given subscriber
29+
*
30+
* @param subscriber The subscriber whose thread is being retrieved.
31+
*
32+
* @returns Last thread
33+
*/
34+
async findLast<S extends SubscriberStub>(subscriber: S) {
35+
const [thread] = await this.findPage(
36+
{
37+
subscriber: subscriber.id,
38+
},
39+
{ skip: 0, limit: 1, sort: ['createdAt', 'desc'] },
40+
);
41+
return thread;
42+
}
43+
}

api/src/extensions/channels/web/base-web-channel.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -762,11 +762,12 @@ export default abstract class BaseWebChannelHandler<
762762
channelData,
763763
);
764764
if (event.getEventType() === 'message') {
765-
// Handler sync message sent by chabbot
765+
// Handle sync message sent by the bot
766766
if (data.sync && data.author === 'chatbot') {
767767
const sentMessage: MessageCreateDto = {
768768
mid: event.getId(),
769769
message: event.getMessage() as StdOutgoingMessage,
770+
thread: event.getThreadId(),
770771
recipient: profile.id,
771772
read: true,
772773
delivery: true,

api/src/extensions/channels/web/types.ts

+2
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,7 @@ export namespace Web {
151151
| IncomingAttachmentMessage,
152152
> = T & {
153153
mid?: string;
154+
thread?: string;
154155
author?: string;
155156
read?: boolean;
156157
delivery?: boolean;
@@ -231,6 +232,7 @@ export namespace Web {
231232

232233
export type OutgoingMessage = OutgoingMessageBase & {
233234
mid: string;
235+
thread?: string;
234236
author: string;
235237
read?: boolean;
236238
createdAt: Date;

0 commit comments

Comments
 (0)