Skip to content

Commit

Permalink
Integrate Copilot SDK (#15)
Browse files Browse the repository at this point in the history
* Refactor to fetch token from url

* Get api token while saving setting

* Change ts targe to es2017

* Make settings elements falsy check

* Add copilot variables in example

* Update target

* Test

* Try with es5

* what about es6

* Add peer dependency for next

* Just yarn lock change

* Remove peer dependencies

* Remove local dev env variable
  • Loading branch information
sazanrjb authored Dec 20, 2023
1 parent 5cd0bd1 commit 26a9863
Show file tree
Hide file tree
Showing 14 changed files with 539 additions and 261 deletions.
1 change: 1 addition & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
# Created by Vercel CLI
COPILOT_API_KEY=""
COPILOT_ENV="local"
NX_DAEMON=""
POSTGRES_DATABASE=""
POSTGRES_HOST=""
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
"@prisma/client": "^5.4.2",
"@radix-ui/react-select": "^2.0.0",
"@vercel/postgres": "^0.5.0",
"copilot-node-sdk": "^0.0.45",
"next": "latest",
"next-plugin-svgr": "^1.1.8",
"prisma": "^5.4.2",
Expand Down
21 changes: 10 additions & 11 deletions src/app/api/messages/services/message.service.ts
Original file line number Diff line number Diff line change
@@ -1,25 +1,24 @@
import { SettingService } from '@/app/api/settings/services/setting.service';
import { getCurrentUser, isWithinWorkingHours } from '@/utils/common';
import { isWithinWorkingHours } from '@/utils/common';
import { PrismaClient, SettingType } from '@prisma/client';
import { CopilotAPI } from '@/utils/copilotApiUtils';
import appConfig from '@/config/app';
import { SettingResponse } from '@/types/setting';
import { Message, SendMessageRequestSchema } from '@/types/message';
import DBClient from '@/lib/db';
import { z } from 'zod';

export class MessageService {
private copilotClient = new CopilotAPI(appConfig.copilotApiKey);
private prismaClient: PrismaClient = DBClient.getInstance();

async handleSendMessageWebhook(message: Message) {
async handleSendMessageWebhook(message: Message, { apiToken }: { apiToken: string }) {
const settingService = new SettingService();
const currentUser = await getCurrentUser();
const copilotClient = new CopilotAPI(apiToken);
const currentUser = await copilotClient.me();
const setting = await settingService.findByUserId(currentUser.id);
if (setting?.type === SettingType.DISABLED) {
return;
}

const copilotClient = new CopilotAPI(appConfig.copilotApiKey);
const client = await copilotClient.getClient(message.senderId);

if ('code' in client && client.code === 'parameter_invalid') {
Expand All @@ -41,7 +40,7 @@ export class MessageService {
}

if (setting?.type === SettingType.ENABLED) {
await this.sendMessage(setting, message);
await this.sendMessage(copilotClient, setting, message);

return;
}
Expand All @@ -51,7 +50,7 @@ export class MessageService {
}

if (!isWithinWorkingHours(setting.timezone, setting.workingHours)) {
await this.sendMessage(setting, message);
await this.sendMessage(copilotClient, setting, message);
}
}

Expand All @@ -61,18 +60,18 @@ export class MessageService {
return date;
}

async sendMessage(setting: SettingResponse, message: Message): Promise<void> {
async sendMessage(copilotClient: CopilotAPI, setting: SettingResponse, message: Message): Promise<void> {
const messageData = SendMessageRequestSchema.parse({
text: setting.message,
senderId: setting.createdById,
channelId: message.channelId,
});

await Promise.all([
this.copilotClient.sendMessage(messageData),
copilotClient.sendMessage(messageData),
this.prismaClient.message.create({
data: {
message: setting.message || '',
message: z.string().parse(setting.message),
clientId: message.senderId,
channelId: messageData.channelId,
senderId: setting.createdById,
Expand Down
4 changes: 3 additions & 1 deletion src/app/api/messages/webhook/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,9 @@ export async function POST(request: NextRequest) {
}

const messageService = new MessageService();
await messageService.handleSendMessageWebhook(payload.data);
await messageService.handleSendMessageWebhook(payload.data, {
apiToken: data.token,
});

return NextResponse.json({});
}
37 changes: 0 additions & 37 deletions src/app/api/settings/route.ts

This file was deleted.

4 changes: 2 additions & 2 deletions src/app/api/settings/services/setting.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,8 @@ export class SettingService {
return SettingResponseSchema.parse(setting);
}

async save(requestData: SettingRequest): Promise<void> {
const currentUser = await getCurrentUser();
async save(requestData: SettingRequest, { apiToken }: { apiToken: string }): Promise<void> {
const currentUser = await getCurrentUser(apiToken);

const settingByUser = await this.prismaClient.setting.findFirst({
where: {
Expand Down
27 changes: 15 additions & 12 deletions src/app/page.tsx
Original file line number Diff line number Diff line change
@@ -1,25 +1,26 @@
import { $Enums } from '@prisma/client';

import { getCurrentUser } from '@/utils/common';
import { HOUR, SettingsData } from '@/constants';
import { SettingResponse } from '@/types/setting';
import AutoResponder from '@/app/components/AutoResponder';
import { SettingService } from '@/app/api/settings/services/setting.service';
import { Client, Company, CopilotAPI, MeResponse } from '@/utils/copilotApiUtils';
import { CopilotAPI } from '@/utils/copilotApiUtils';
import { ClientResponse, CompanyResponse, MeResponse } from '@/types/common';
import { z } from 'zod';

type SearchParams = { [key: string]: string | string[] | undefined };

const settingsService = new SettingService();

async function getContent(searchParams: SearchParams) {
if (!process.env.COPILOT_API_KEY) {
throw new Error('Missing COPILOT_API_KEY');
if (!searchParams.token) {
throw new Error('Missing token');
}

const copilotAPI = new CopilotAPI(process.env.COPILOT_API_KEY);
const result: { client?: Client; company?: Company; me?: MeResponse } = {};
const copilotAPI = new CopilotAPI(z.string().parse(searchParams.token));
const result: { client?: ClientResponse; company?: CompanyResponse; me?: MeResponse } = {};

result.me = await getCurrentUser();
result.me = await copilotAPI.me();

if (searchParams.clientId && typeof searchParams.clientId === 'string') {
result.client = await copilotAPI.getClient(searchParams.clientId);
Expand All @@ -34,10 +35,10 @@ async function getContent(searchParams: SearchParams) {

const populateSettingsFormData = (settings: SettingResponse): Omit<SettingsData, 'sender'> => {
return {
autoRespond: settings.type || $Enums.SettingType.DISABLED,
response: settings.message || null,
timezone: settings.timezone || Intl.DateTimeFormat().resolvedOptions().timeZone,
selectedDays: (settings.workingHours || [])?.map((workingHour) => ({
autoRespond: settings?.type || $Enums.SettingType.DISABLED,
response: settings?.message || null,
timezone: settings?.timezone || Intl.DateTimeFormat().resolvedOptions().timeZone,
selectedDays: (settings?.workingHours || [])?.map((workingHour) => ({
day: workingHour.weekday,
startHour: workingHour.startTime as HOUR,
endHour: workingHour.endTime as HOUR,
Expand All @@ -64,7 +65,9 @@ export default async function Page({ searchParams }: { searchParams: SearchParam
}))
: data.selectedDays,
};
await settingsService.save(setting);
await settingsService.save(setting, {
apiToken: z.string().parse(searchParams.token),
});
};

return (
Expand Down
27 changes: 27 additions & 0 deletions src/types/common.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { z } from 'zod';

export const MeResponseSchema = z.object({
id: z.string(),
givenName: z.string(),
familyName: z.string(),
email: z.string(),
portalName: z.string(),
});
export type MeResponse = z.infer<typeof MeResponseSchema>;

export const ClientResponseSchema = z.object({
id: z.string(),
givenName: z.string(),
familyName: z.string(),
email: z.string(),
companyId: z.string(),
customFields: z.record(z.string(), z.union([z.string(), z.array(z.string())])),
});
export type ClientResponse = z.infer<typeof ClientResponseSchema>;

export const CompanyResponseSchema = z.object({
id: z.string(),
name: z.string(),
iconImageUrl: z.string(),
});
export type CompanyResponse = z.infer<typeof CompanyResponseSchema>;
4 changes: 2 additions & 2 deletions src/types/message.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { z } from 'zod';

export const MessageSchema = z.object({
id: z.string(),
object: z.literal('message'),
object: z.string(),
senderId: z.string().uuid(),
createdAt: z.string().datetime(),
updatedAt: z.string().datetime(),
Expand All @@ -25,4 +25,4 @@ export const SendMessageErrorResponseSchema = z.object({
message: z.string(),
error: z.object({}),
});
export type SendMessageErrorResponse = z.infer<typeof SendMessageRequestSchema>;
export type SendMessageErrorResponse = z.infer<typeof SendMessageErrorResponseSchema>;
1 change: 1 addition & 0 deletions src/types/webhook.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,5 @@ export const WebhookSchema = z.object({
created: z.string().optional(),
object: z.string().optional(),
data: z.unknown(),
token: z.string(),
});
11 changes: 4 additions & 7 deletions src/utils/common.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { NextResponse } from 'next/server';
import { CopilotAPI, MeResponse } from '@/utils/copilotApiUtils';
import { CopilotAPI } from '@/utils/copilotApiUtils';
import { WorkingHours } from '@/types/setting';
import { DayOfWeek, LocalTime, ZonedDateTime, ZoneId } from '@js-joda/core';
import '@js-joda/timezone';
import { MeResponse } from '@/types/common';

export function errorHandler(message: string, status: number = 200) {
return NextResponse.json(
Expand All @@ -13,12 +14,8 @@ export function errorHandler(message: string, status: number = 200) {
);
}

export async function getCurrentUser(): Promise<MeResponse> {
if (!process.env.COPILOT_API_KEY) {
throw new Error('Copilot API key is not set.');
}

const copilotClient = new CopilotAPI(process.env.COPILOT_API_KEY);
export async function getCurrentUser(apiToken: string): Promise<MeResponse> {
const copilotClient = new CopilotAPI(apiToken);
return await copilotClient.me();
}

Expand Down
83 changes: 25 additions & 58 deletions src/utils/copilotApiUtils.ts
Original file line number Diff line number Diff line change
@@ -1,73 +1,40 @@
import { Message, SendMessageErrorResponse, SendMessageRequest } from '@/types/message';

const BaseApiURL = 'https://api-beta.copilot.com/v1';

export type MeResponse = {
id: string;
givenName: string;
familyName: string;
email: string;
portalName: string;
};

type ClientCustomField = string | string[];

export type Client = {
id: string;
givenName: string;
familyName: string;
email: string;
companyId: string;
customFields: Record<string, ClientCustomField>;
};

export type Company = {
id: string;
name: string;
iconImageUrl: string;
};
import { copilotApi } from 'copilot-node-sdk';

enum Method {
GET = 'GET',
POST = 'POST',
PUT = 'PUT',
PATCH = 'PATCH',
DELETE = 'DELETE',
}
import { Message, SendMessageErrorResponse, SendMessageRequest } from '@/types/message';
import appConfig from '@/config/app';
import { DefaultService as Copilot } from 'copilot-node-sdk/codegen/api/services/DefaultService';
import {
ClientResponse,
ClientResponseSchema,
CompanyResponse,
CompanyResponseSchema,
MeResponse,
MeResponseSchema,
} from '@/types/common';

export class CopilotAPI {
apiKey: string;
copilot: typeof Copilot;

constructor(apiKey: string) {
this.apiKey = apiKey;
}

async sendApiData<T>(path: string, method: Method = Method.GET, payload?: unknown): Promise<T> {
const response = await fetch(`${BaseApiURL}/${path}`, {
headers: {
'x-api-key': this.apiKey,
},
method: method,
body: payload ? JSON.stringify(payload) : undefined,
constructor(apiToken: string) {
this.copilot = copilotApi({
apiKey: appConfig.copilotApiKey,
token: apiToken,
});

const data = await response.json();
return data;
}

async me() {
return this.sendApiData<MeResponse>('me');
async me(): Promise<MeResponse> {
return MeResponseSchema.parse(await this.copilot.getUserAndPortalInfo());
}

async getClient(clientId: string) {
return this.sendApiData<Client>(`clients/${clientId}`);
async getClient(clientId: string): Promise<ClientResponse> {
return ClientResponseSchema.parse(await this.copilot.retrieveAClient({ id: clientId }));
}

async getCompany(companyId: string) {
return this.sendApiData<Company>(`companies/${companyId}`);
async getCompany(companyId: string): Promise<CompanyResponse> {
return CompanyResponseSchema.parse(await this.copilot.retrieveACompany({ id: companyId }));
}

async sendMessage(payload: SendMessageRequest): Promise<Message | SendMessageErrorResponse> {
return this.sendApiData<Message | SendMessageErrorResponse>(`messages`, Method.POST, payload);
async sendMessage(payload: SendMessageRequest): Promise<Partial<Message> | SendMessageErrorResponse> {
return this.copilot.sendAMessage({ requestBody: payload });
}
}
2 changes: 1 addition & 1 deletion tsconfig.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"compilerOptions": {
"target": "es5",
"target": "es6",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
Expand Down
Loading

0 comments on commit 26a9863

Please sign in to comment.