Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Integrate Copilot SDK #15

Merged
merged 13 commits into from
Dec 20, 2023
Merged
Show file tree
Hide file tree
Changes from 12 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# Created by Vercel CLI
COPILOT_API_KEY=""
COPILOT_LOCAL_DEV="test"
sazanrjb marked this conversation as resolved.
Show resolved Hide resolved
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
Loading