Skip to content

Commit 443484c

Browse files
committed
Merge remote-tracking branch 'origin/main' into DIAL-21-Ui-settings
2 parents a8468ab + 51af836 commit 443484c

22 files changed

+1310
-5369
lines changed

server/package-lock.json

Lines changed: 834 additions & 5278 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

server/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
"@paralleldrive/cuid2": "^2.2.2",
3232
"@react-email/components": "^0.0.15",
3333
"@react-email/render": "0.0.7",
34+
"axios": "^1.6.7",
3435
"bcrypt": "^5.0.1",
3536
"bullmq": "^5.1.5",
3637
"class-transformer": "^0.5.1",
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import { BOT_ENDPOINT, PUBLIC_DOMAIN } from "@/config";
2+
import axios from "axios";
3+
4+
export class BaseChannel {
5+
id: string;
6+
contactId: string;
7+
contactName: string;
8+
channelType: string;
9+
10+
constructor(id: string, contactId: string, contactName: string, channelType: string) {
11+
this.id = id;
12+
this.contactId = contactId;
13+
this.contactName = contactName;
14+
this.channelType = channelType;
15+
}
16+
17+
public async postMessageToBot({ userId, message = '', data }) {
18+
const uid = this.initConversationId(userId);
19+
try {
20+
const postMsg = await axios({
21+
method: 'POST',
22+
url: BOT_ENDPOINT,
23+
data: {
24+
conversation: {
25+
id: uid,
26+
},
27+
from: {
28+
id: userId,
29+
},
30+
recipient: {
31+
id: this.contactId,
32+
},
33+
data: data || false,
34+
text: message,
35+
type: 'message',
36+
id: uid,
37+
channelId: this.channelType,
38+
serviceUrl: PUBLIC_DOMAIN,
39+
},
40+
})
41+
if (postMsg.data.success) {
42+
console.log(
43+
`[${this.channelType} - ${this.contactName} ${this.contactId}] - [Conversation ID: ${uid}] - [Send message to bot - Message: ${message}] - [Data: ${data}]`
44+
)
45+
}
46+
} catch (error) {
47+
console.log(
48+
`[${this.channelType} - ${this.contactName} ${this.contactId}] - [Conversation ID: ${uid}] - [Can not send message to bot - Message: ${message}] - [Error: ${error.message}]`
49+
);
50+
}
51+
}
52+
53+
initConversationId(userId: string) {
54+
return this.contactId + '-' + userId
55+
}
56+
}
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
import { Helper } from "@/utils/helper";
2+
import { Request, Response } from "express";
3+
import { BaseChannel } from "./base.channel";
4+
5+
export class MessengerChannel extends BaseChannel {
6+
pageToken: string;
7+
webhookSecret: string;
8+
messengerPostURL: string;
9+
credentials: string;
10+
11+
constructor(id: string, contactId: string, contactName: string, channelType: string, credentials: string) {
12+
super(id, contactId, contactName, channelType);
13+
14+
let parseCredentials: MessengerChannel;
15+
16+
this.credentials = credentials;
17+
18+
if (credentials && typeof credentials == 'string') parseCredentials = JSON.parse(credentials);
19+
20+
if (parseCredentials) {
21+
this.pageToken = parseCredentials.pageToken;
22+
this.webhookSecret = parseCredentials.webhookSecret;
23+
}
24+
25+
this.channelType = channelType;
26+
this.messengerPostURL = `https://graph.facebook.com/v18.0/me/messages?access_token=`;
27+
}
28+
29+
public verifyWebhook(req: Request, res: Response) {
30+
let mode = req.query['hub.mode'];
31+
let token = req.query['hub.verify_token'];
32+
let challenge = req.query['hub.challenge'];
33+
34+
if (mode === 'subscribe' && this.webhookSecret == token) {
35+
console.log(`channel ${this.channelType} - ${this.contactName} ${this.contactId} webhook verified!`);
36+
return challenge;
37+
} else {
38+
console.error(`Verification channel ${this.channelType} - ${this.contactName} ${this.contactId} failed!`);
39+
return;
40+
}
41+
}
42+
43+
public async prepareMessage(req: Request, res: Response) {
44+
const { object, entry } = req.body;
45+
46+
if (object != 'page' || !Array.isArray(entry)) return;
47+
48+
entry.forEach(pageEntry => {
49+
if (!Array.isArray(pageEntry.messaging)) return;
50+
51+
pageEntry.messaging.forEach(async (messagingEvent) => {
52+
if (messagingEvent.messaging_customer_information)
53+
return this.sendAddressToBot({
54+
userId: messagingEvent.sender.id,
55+
address: messagingEvent.messaging_customer_information.screens[0].responses,
56+
});
57+
58+
if (!messagingEvent.message && !messagingEvent.postback) return;
59+
60+
const senderId = messagingEvent.sender.id;
61+
const messageText = messagingEvent.message && messagingEvent.message.text;
62+
const payload = messagingEvent.postback && messagingEvent.postback.payload;
63+
const quick_reply = messagingEvent.message && messagingEvent.message.quick_reply;
64+
65+
if (senderId == this.contactId) return; //Agent replied to user => skip
66+
67+
return this.postMessageToBot({ userId: senderId, message: messageText || payload, data: null });
68+
});
69+
});
70+
}
71+
72+
sendAddressToBot({ userId, address }) {
73+
return this.postMessageToBot({ userId, message: 'ADDRESS', data: { USER_INFORMATION: Helper.arrayToObj(address) } });
74+
}
75+
}

server/src/config/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,4 +28,6 @@ export const {
2828
FIREBASE_UNIVERSE_DOMAIN,
2929
FIREBASE_DATABASE_URL,
3030
SIGNATURE_SECRET,
31+
PUBLIC_DOMAIN,
32+
BOT_ENDPOINT,
3133
} = process.env;

server/src/constants/index.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,12 @@ export const ENDPOINTS = {
3939
USER: {
4040
UPDATE_INFO: '/user/update-info',
4141
CHANGE_PASSWORD: '/user/change-password',
42-
}
42+
},
43+
WEBHOOK: {
44+
INDEX: '/webhook',
45+
VERIFY: '/webhook/:contactId',
46+
INCOMING_MSG: '/webhook/:contactId',
47+
},
4348
};
4449

4550
export const LOCALE_KEY = 'lang';
Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { LOCALE_KEY } from "@/constants";
22
import { PagingDTO } from "@/dtos/paging.dto";
33
import { LocaleService } from "@/i18n/ctx";
4+
import { RequestWithUser } from "@/interfaces/auth.interface";
45
import { ChannelService } from "@/services/channels.service";
56
import { catchAsync } from "@/utils/catch-async";
67
import { plainToClass } from "class-transformer";
@@ -11,38 +12,40 @@ export class ChannelController {
1112
public channelService = Container.get(ChannelService);
1213
public localeService = Container.get<LocaleService>(LOCALE_KEY);
1314

14-
public createChannel = catchAsync(async (req, res) => {
15+
public createChannel = catchAsync(async (req: RequestWithUser, res) => {
16+
req.body.userId = req.user?.id;
1517
await this.channelService.create(req.body);
1618
res.status(StatusCodes.OK).json({
1719
message: this.localeService.i18n().CHANNEL.CREATE_SUCCESS(),
1820
});
1921
});
2022

21-
public updateChannel = catchAsync(async (req, res) => {
23+
public updateChannel = catchAsync(async (req: RequestWithUser, res) => {
24+
req.body.userId = req.user?.id;
2225
await this.channelService.updateById(req.params.id, req.body);
2326
res.status(StatusCodes.OK).json({
2427
message: this.localeService.i18n().CHANNEL.UPDATE_SUCCESS(),
2528
});
2629
})
2730

28-
public deleteChannel = catchAsync(async (req, res) => {
29-
await this.channelService.deleteById(req.params.id);
31+
public deleteChannel = catchAsync(async (req: RequestWithUser, res) => {
32+
await this.channelService.deleteById(req.params.id, req.user?.id as string);
3033
res.status(StatusCodes.OK).json({
3134
message: this.localeService.i18n().CHANNEL.DELETE_CHANNEL_SUCCESS(),
3235
});
3336
})
3437

35-
public deleteMultipleChannel = catchAsync(async (req, res) => {
36-
await this.channelService.deleteByIds(req.body.id);
38+
public deleteMultipleChannel = catchAsync(async (req: RequestWithUser, res) => {
39+
await this.channelService.deleteByIds(req.body.id, req.user?.id as string);
3740
res.status(StatusCodes.OK).json({
3841
message: this.localeService.i18n().CHANNEL.DELETE_MULTIPLE_CHANNELS_SUCCESS(),
3942
});
4043
})
4144

42-
public getChannelsPaging = catchAsync(async (req, res) => {
45+
public getAllChannels = catchAsync(async (req: RequestWithUser, res) => {
4346
const paging = plainToClass(PagingDTO, req.query);
4447

45-
const data = await this.channelService.getChannelsPaging(paging);
48+
const data = await this.channelService.getAllChannels(paging, req.user?.id as string);
4649
res.status(StatusCodes.OK).json({ data });
4750
})
4851
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import { LOCALE_KEY } from "@/constants";
2+
import { LocaleService } from "@/i18n/ctx";
3+
import { WebhookService } from "@/services/webhook.service";
4+
import { catchAsync } from "@/utils/catch-async";
5+
import { StatusCodes } from "http-status-codes";
6+
import Container from "typedi";
7+
8+
export class WebhookController {
9+
public webhookService = Container.get(WebhookService);
10+
public localeService = Container.get<LocaleService>(LOCALE_KEY);
11+
12+
13+
public verifyWebhook = catchAsync(async (req, res) => {
14+
const data = await this.webhookService.verifyWebhook(req.params.contactId as string, req, res);
15+
16+
res.status(StatusCodes.OK).send(data);
17+
});
18+
19+
public handleIncomingMessage = catchAsync(async (req, res) => {
20+
res.status(StatusCodes.OK);
21+
22+
return await this.webhookService.handleIncomingMessage(req.params.contactId, req, res);
23+
});
24+
}

server/src/database/schema.ts

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ export const channels = pgTable('channels', {
6060
active: boolean('active'),
6161
deleted: boolean('deleted').default(false),
6262
channelTypeId: text('channel_type_id').notNull(),
63+
userId: text('user_id').notNull(),
6364
createdAt: timestamp('created_at').defaultNow(),
6465
updatedAt: timestamp('updated_at'),
6566
});
@@ -73,6 +74,10 @@ export const channelsRelations = relations(channels, ({ one }) => ({
7374
fields: [channels.channelTypeId],
7475
references: [channelTypes.id],
7576
}),
77+
User: one(users, {
78+
fields: [channels.userId],
79+
references: [users.id],
80+
}),
7681
}));
7782

7883
export const settings = pgTable('settings', {
@@ -84,17 +89,21 @@ export const settings = pgTable('settings', {
8489
email: json('email')
8590
.notNull()
8691
.default({
87-
'email': '',
88-
'password': '',
89-
}).$type<{
92+
email: '',
93+
password: '',
94+
})
95+
.$type<{
9096
email: string;
9197
password: string;
9298
}>(),
9399
userId: varchar('user_id', {
94100
length: MAX_ID_LENGTH,
95-
}).notNull().references(() => users.id),
101+
})
102+
.notNull()
103+
.references(() => users.id),
96104
});
97105

98-
export const usersRelations = relations(users, ({ one }) => ({
106+
export const usersRelations = relations(users, ({ one, many }) => ({
99107
settings: one(settings),
108+
channels: many(channels),
100109
}));

server/src/database/seed.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ async function seedChannelTypes() {
1919
async function seedDefaultAccount() {
2020
try {
2121
const hashedPassword = await bcrypt.hash("Hello@123", 10);
22-
await db.insert(users).values({email: "[email protected]", password: hashedPassword, name: "admin",});
22+
await db.insert(users).values({ email: "[email protected]", password: hashedPassword, name: "admin", });
2323
} catch (error) {
2424
console.error(`Can't create default account`);
2525
}

0 commit comments

Comments
 (0)