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

Resolve PR comments #16

Open
wants to merge 13 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all 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"
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
2 changes: 1 addition & 1 deletion src/app/components/AutoResponder.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -215,7 +215,7 @@ const AutoResponder = ({ onSave, activeSettings }: Props) => {
);
}
}
}, [autoRespond]);
}, [autoRespond, isDirty, setValue]);

const toggleSelectedDay = (day: DAY_VALUE) => {
const selectedDayIndex = selectedDays.fields.findIndex((selectedDay) => selectedDay.day === day);
Expand Down
7 changes: 4 additions & 3 deletions src/app/components/Days.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import Typography from './Typography';
import { DAYS, SelectedDay, DAY_KEY, DAY_VALUE } from '@/constants';
import { keys } from '@/utils/common';

interface Props {
selectedDays: SelectedDay[];
Expand All @@ -9,12 +10,12 @@ interface Props {
const Days = ({ selectedDays, onDayClick }: Props) => {
return (
<ul className="flex flex-wrap items-center gap-4">
{Object.keys(DAYS).map((day: DAY_KEY | string) => {
const isSelected = !!selectedDays.find((selectedDay) => DAYS[day as DAY_KEY] === selectedDay.day);
{keys(DAYS).map((day) => {
const isSelected = !!selectedDays.find((selectedDay) => DAYS[day] === selectedDay.day);
return (
<li
key={day}
onClick={() => onDayClick(DAYS[day as DAY_KEY])}
onClick={() => onDayClick(DAYS[day])}
className={`w-8 h-8 flex items-center justify-center rounded-full
uppercase text-center leading-8 ${isSelected ? 'bg-text' : 'bg-border'} ${
isSelected ? 'text-white' : 'text-text'
Expand Down
7 changes: 4 additions & 3 deletions src/app/components/WorkingHours.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import Typography from './Typography';
import SelectField from './Select';
import { DAYS, DAY_KEY, HOUR, HOURS_SELECT_OPTIONS, SelectedDay } from '@/constants';
import React, { useMemo } from 'react';
import { keys } from '@/utils/common';

interface Props {
selectedDays: SelectedDay[];
Expand All @@ -12,16 +13,16 @@ interface Props {

const WorkingHours = ({ selectedDays, errors }: Props) => {
const daysToRender = useMemo(() => {
return Object.keys(DAYS).map((day: DAY_KEY | string) => {
return keys(DAYS).map((day) => {
const selectedDayIndex = selectedDays.findIndex((selectedDay) => {
return selectedDay.day === DAYS[day as DAY_KEY];
return selectedDay.day === DAYS[day];
});

if (selectedDayIndex < 0) {
return null;
}

const error = errors[DAYS[day as DAY_KEY]];
const error = errors[DAYS[day]];

return (
<li key={day}>
Expand Down
2 changes: 1 addition & 1 deletion src/app/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

* {
box-sizing: border-box;
font-family: 'Inter', 'Helvetica', 'Arial', san-serif;
font-family: 'Inter', 'Helvetica', 'Arial', sans-serif, serif;
}

:root {
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
4 changes: 3 additions & 1 deletion src/config/app.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
export default {
const appConfig = {
copilotApiKey: process.env.COPILOT_API_KEY || '',
webhookSigningSecret: process.env.WEBHOOK_SIGNING_SECRET || '',
};

export default appConfig;
2 changes: 1 addition & 1 deletion src/constants/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ export const DAYS = {
THURSDAY: 4,
FRIDAY: 5,
SATURDAY: 6,
};
} as const;

export enum HOUR {
'0:00-AM' = '00:00',
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(),
});
19 changes: 12 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 All @@ -37,3 +34,11 @@ export function isWithinWorkingHours(timezone: string, workingHours: WorkingHour
currentTime.isAfter(LocalTime.parse(workingDay.startTime)) && currentTime.isBefore(LocalTime.parse(workingDay.endTime))
);
}

export function keys<T extends Record<string, unknown>>(obj: T): (keyof T)[] {
const result: (keyof T)[] = [];
for (const key in obj) {
result.push(key);
}
return result;
}
Loading