Skip to content

Commit c10857e

Browse files
committed
Move Luma env schema and client into lib/luma
1 parent 6ba5b46 commit c10857e

5 files changed

Lines changed: 316 additions & 20 deletions

File tree

app/src/lib/config.ts

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { databaseConfig } from "./database/config";
33
import { electricConfig } from "./electric/config";
44
import { instanceConfig } from "./instance/config";
55
import { integrationsConfig } from "./integrations/config";
6+
import { lumaConfig } from "./luma/config";
67
import { storageConfig } from "./storage/config";
78

89
export const mainConfig = {
@@ -16,11 +17,7 @@ export const mainConfig = {
1617
resend: {
1718
apiKey: integrationsConfig.resendApiKey,
1819
},
19-
luma: {
20-
apiKey: integrationsConfig.lumaApiKey,
21-
calendarApiId: integrationsConfig.lumaCalendarApiId,
22-
calendarHandle: integrationsConfig.lumaCalendarHandle,
23-
},
20+
luma: lumaConfig,
2421
discord: {
2522
botToken: integrationsConfig.discordBotToken,
2623
reviewChannelId: integrationsConfig.discordReviewChannelId,

app/src/lib/integrations/config.ts

Lines changed: 0 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -5,18 +5,6 @@ const integrationsEnvConfig = configSchema("Integrations", {
55
env: "RESEND_API_KEY",
66
optional: true,
77
}),
8-
lumaApiKey: server({
9-
env: "LUMA_API_KEY",
10-
optional: true,
11-
}),
12-
lumaCalendarApiId: server({
13-
env: "LUMA_CALENDAR_API_ID",
14-
optional: true,
15-
}),
16-
lumaCalendarHandle: server({
17-
env: "LUMA_CALENDAR_HANDLE",
18-
optional: true,
19-
}),
208
discordBotToken: server({
219
env: "DISCORD_BOT_TOKEN",
2210
optional: true,
@@ -29,9 +17,6 @@ const integrationsEnvConfig = configSchema("Integrations", {
2917

3018
export const integrationsConfig = {
3119
resendApiKey: integrationsEnvConfig.server.resendApiKey,
32-
lumaApiKey: integrationsEnvConfig.server.lumaApiKey,
33-
lumaCalendarApiId: integrationsEnvConfig.server.lumaCalendarApiId,
34-
lumaCalendarHandle: integrationsEnvConfig.server.lumaCalendarHandle,
3520
discordBotToken: integrationsEnvConfig.server.discordBotToken,
3621
discordReviewChannelId: integrationsEnvConfig.server.discordReviewChannelId,
3722
};

app/src/lib/luma/config.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import { configSchema, server } from "better-env/config-schema";
2+
3+
const lumaEnvConfig = configSchema("Luma", {
4+
apiKey: server({
5+
env: "LUMA_API_KEY",
6+
optional: true,
7+
}),
8+
calendarApiId: server({
9+
env: "LUMA_CALENDAR_API_ID",
10+
optional: true,
11+
}),
12+
calendarHandle: server({
13+
env: "LUMA_CALENDAR_HANDLE",
14+
optional: true,
15+
}),
16+
});
17+
18+
export const lumaConfig = {
19+
apiKey: lumaEnvConfig.server.apiKey,
20+
calendarApiId: lumaEnvConfig.server.calendarApiId,
21+
calendarHandle: lumaEnvConfig.server.calendarHandle,
22+
};

app/src/lib/luma/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export * from "./luma";

app/src/lib/luma/luma.ts

Lines changed: 291 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,291 @@
1+
import { mainConfig } from "@/lib/config";
2+
3+
type LumaGeoAddress = {
4+
city: string;
5+
type: "google" | "string";
6+
country: string;
7+
latitude: number;
8+
longitude: number;
9+
place_id: string;
10+
address: string;
11+
description: string;
12+
city_state: string;
13+
full_address: string;
14+
};
15+
16+
export type LumaEvent = {
17+
api_id?: string;
18+
app_id?: string;
19+
calendar_api_id?: string;
20+
created_at: string;
21+
cover_url?: string | null;
22+
name: string;
23+
description?: string | null;
24+
description_md?: string | null;
25+
series_api_id?: string;
26+
start_at: string;
27+
duration_interval: string;
28+
end_at: string;
29+
geo_address_json?: LumaGeoAddress | null;
30+
geo_latitude?: number | null;
31+
geo_longitude?: number | null;
32+
url: string;
33+
timezone: string;
34+
event_type: "independent" | "series";
35+
user_api_id: string;
36+
visibility: "public" | "private";
37+
zoom_meeting_url?: string | null;
38+
meeting_url?: string | null;
39+
};
40+
41+
export type LumaHost = {
42+
api_id: string;
43+
name: string;
44+
email: string;
45+
};
46+
47+
export type LumaEventPayload = {
48+
event: LumaEvent;
49+
hosts: LumaHost[];
50+
};
51+
52+
export type LumaAttendee = {
53+
api_id: string;
54+
approval_status: "approved" | "declined" | "pending_approval" | "rejected";
55+
created_at: string;
56+
registered_at: string;
57+
user_api_id: string;
58+
user_name: string;
59+
user_email: string;
60+
name: string;
61+
email: string;
62+
};
63+
64+
export type LumaClient = ReturnType<typeof createLumaClient>;
65+
66+
export function getLumaUrl(lumaEventId?: string | null): string | null {
67+
if (!lumaEventId) return null;
68+
return `https://lu.ma/event/${lumaEventId}`;
69+
}
70+
71+
export const createLumaClient = () => {
72+
const apiKey = mainConfig.luma.apiKey;
73+
74+
if (!apiKey) {
75+
return {
76+
getUpcomingEvents: async () => {
77+
console.warn(
78+
"Did not fetch upcoming events because LUMA_API_KEY is not set",
79+
);
80+
return [];
81+
},
82+
getCalendarEvents: async () => {
83+
console.warn(
84+
"Did not fetch calendar events because LUMA_API_KEY is not set",
85+
);
86+
return [];
87+
},
88+
getEvent: async () => {
89+
console.warn("Did not fetch event because LUMA_API_KEY is not set");
90+
return null;
91+
},
92+
getAttendees: async () => {
93+
console.warn("Did not fetch attendees because LUMA_API_KEY is not set");
94+
return [[], { hasMoreToFetch: false, nextCursor: undefined }];
95+
},
96+
getAllAttendees: async () => {
97+
console.warn(
98+
"Did not fetch all attendees because LUMA_API_KEY is not set",
99+
);
100+
return [];
101+
},
102+
getAttendeeCount: async () => {
103+
console.warn(
104+
"Did not fetch attendee count because LUMA_API_KEY is not set",
105+
);
106+
return 0;
107+
},
108+
addAttendees: async () => {
109+
console.warn("Did not add attendees because LUMA_API_KEY is not set");
110+
return undefined;
111+
},
112+
};
113+
}
114+
115+
const headers = {
116+
accept: "application/json",
117+
"x-luma-api-key": apiKey,
118+
};
119+
120+
const parseCalendarEventsResponse = (resData: any): LumaEvent[] => {
121+
const entries = resData?.entries ?? resData?.events?.entries ?? [];
122+
if (!Array.isArray(entries)) return [];
123+
return entries
124+
.map((entry: any) => entry?.event ?? entry)
125+
.filter(Boolean) as LumaEvent[];
126+
};
127+
128+
const getCalendarEvents = async ({
129+
limit = 10,
130+
after,
131+
before,
132+
calendarApiId,
133+
calendarHandle,
134+
}: {
135+
limit?: number;
136+
after?: string;
137+
before?: string;
138+
calendarApiId?: string;
139+
calendarHandle?: string;
140+
} = {}) => {
141+
const searchParams = new URLSearchParams({
142+
pagination_limit: String(limit),
143+
});
144+
if (after) {
145+
searchParams.append("after", after);
146+
}
147+
if (before) {
148+
searchParams.append("before", before);
149+
}
150+
if (calendarApiId) {
151+
searchParams.append("calendar_api_id", calendarApiId);
152+
}
153+
if (calendarHandle) {
154+
searchParams.append("calendar_handle", calendarHandle);
155+
}
156+
157+
const url = `https://api.lu.ma/public/v1/calendar/list-events?${searchParams.toString()}`;
158+
const res = await fetch(url, {
159+
method: "GET",
160+
headers,
161+
});
162+
if (!res.ok) {
163+
throw new Error(
164+
`Failed to fetch upcoming events. Status: ${res.status} - ${res.statusText}`,
165+
);
166+
}
167+
const resData = await res.json();
168+
return parseCalendarEventsResponse(resData);
169+
};
170+
171+
const getUpcomingEvents = async () => {
172+
return getCalendarEvents({
173+
limit: 50,
174+
after: new Date().toISOString(),
175+
});
176+
};
177+
178+
const getEvent = async (eventId: string): Promise<LumaEventPayload> => {
179+
const url = `https://api.lu.ma/public/v1/event/get?api_id=${eventId}`;
180+
const res = await fetch(url, {
181+
method: "GET",
182+
headers,
183+
});
184+
if (!res.ok) {
185+
throw new Error(
186+
`Failed to fetch event. Status: ${res.status} - ${res.statusText}`,
187+
);
188+
}
189+
return await res.json();
190+
};
191+
192+
const getAttendees = async (
193+
eventId: string,
194+
options?: {
195+
cursor?: string;
196+
approvalStatus?: LumaAttendee["approval_status"];
197+
},
198+
): Promise<
199+
[
200+
LumaAttendee[],
201+
{ hasMoreToFetch: boolean; nextCursor: string | undefined },
202+
]
203+
> => {
204+
const urlSearchParams = new URLSearchParams({
205+
event_api_id: eventId,
206+
pagination_limit: "50", // 50 is the maximum limit
207+
});
208+
if (options?.cursor) {
209+
urlSearchParams.append("pagination_cursor", options.cursor);
210+
}
211+
if (options?.approvalStatus) {
212+
urlSearchParams.append("approval_status", options.approvalStatus);
213+
}
214+
const url = `https://api.lu.ma/public/v1/event/get-guests?${urlSearchParams.toString()}`;
215+
const res = await fetch(url, {
216+
method: "GET",
217+
headers,
218+
});
219+
if (!res.ok) {
220+
throw new Error(
221+
`Failed to fetch attendees. Status: ${res.status} - ${res.statusText}`,
222+
);
223+
}
224+
const resData = await res.json();
225+
const attendees = resData.entries.map((e: any) => e.guest);
226+
return [
227+
attendees,
228+
{ hasMoreToFetch: resData.has_more, nextCursor: resData.next_cursor },
229+
];
230+
};
231+
232+
const getAllAttendees = async (
233+
eventId: string,
234+
{
235+
approvalStatus,
236+
}: { approvalStatus?: LumaAttendee["approval_status"] } = {},
237+
) => {
238+
let attendees: LumaAttendee[] = [];
239+
let hasMore = true;
240+
let cursor: string | undefined;
241+
while (hasMore) {
242+
const [newAttendees, { hasMoreToFetch, nextCursor }] = await getAttendees(
243+
eventId,
244+
{ cursor, approvalStatus },
245+
);
246+
attendees = [...attendees, ...newAttendees];
247+
hasMore = hasMoreToFetch;
248+
cursor = nextCursor;
249+
}
250+
return attendees;
251+
};
252+
253+
const getAttendeeCount = async (eventId: string) => {
254+
const attendees = await getAllAttendees(eventId, {
255+
approvalStatus: "approved",
256+
});
257+
return attendees.length;
258+
};
259+
260+
const addAttendees = async (
261+
eventId: string,
262+
guests: { email: string; name: string | null }[],
263+
) => {
264+
const url = `https://api.lu.ma/public/v1/event/add-guests`;
265+
const res = await fetch(url, {
266+
method: "POST",
267+
headers,
268+
body: JSON.stringify({
269+
event_api_id: eventId,
270+
guests,
271+
}),
272+
});
273+
if (!res.ok) {
274+
const resData = await res.json();
275+
console.warn(resData);
276+
throw new Error(
277+
`Failed to add attendee. Status: ${res.status} - ${res.statusText}`,
278+
);
279+
}
280+
};
281+
282+
return {
283+
getUpcomingEvents,
284+
getCalendarEvents,
285+
getEvent,
286+
getAttendees,
287+
getAllAttendees,
288+
getAttendeeCount,
289+
addAttendees,
290+
};
291+
};

0 commit comments

Comments
 (0)