1import { env as privateEnv } from '$env/dynamic/private';
2import { env as publicEnv } from '$env/dynamic/public';
3
4// Convert .cloud URL to .site URL for HTTP actions
5function getHttpUrl(): string {
6 const baseUrl = publicEnv.PUBLIC_CONVEX_URL ?? privateEnv.PUBLIC_CONVEX_URL;
7 if (!baseUrl) {
8 throw new Error('PUBLIC_CONVEX_URL is not set');
9 }
10 return baseUrl.replace('.convex.cloud', '.convex.site');
11}
12
13// Generic fetch helper for Convex HTTP endpoints (server-side only)
14async function convexFetch<T>(path: string, body: Record<string, unknown> = {}): Promise<T> {
15 const url = `${getHttpUrl()}${path}`;
16
17 const response = await fetch(url, {
18 method: 'POST',
19 headers: {
20 'Content-Type': 'application/json',
21 'X-Convex-Secret': privateEnv.CONVEX_API_SECRET || ''
22 },
23 body: JSON.stringify(body)
24 });
25
26 if (!response.ok) {
27 const error = await response.text();
28 throw new Error(`Convex HTTP error: ${response.status} - ${error}`);
29 }
30
31 return response.json();
32}
33
34// ============ USERS ============
35
36export const users = {
37 getByNickname: (nickname: string) =>
38 convexFetch<ConvexUser | null>('/api/users/getByNickname', { nickname }),
39
40 getById: (id: string) => convexFetch<ConvexUser | null>('/api/users/getById', { id }),
41
42 create: (nickname: string, password: string) =>
43 convexFetch<{ userId: string }>('/api/users/create', { nickname, password }),
44
45 updateStatus: (userId: string, status: string) =>
46 convexFetch<ConvexUser>('/api/users/updateStatus', { userId, status }),
47
48 list: () => convexFetch<ConvexUser[]>('/api/users/list', {}),
49
50 setAllOffline: () => convexFetch<{ updated: number }>('/api/users/setAllOffline', {}),
51
52 updateLastReadMentionTimestamp: (userId: string, timestamp: number) =>
53 convexFetch<{ success: boolean }>('/api/users/updateLastReadMentionTimestamp', {
54 userId,
55 timestamp
56 })
57};
58
59// ============ SESSIONS ============
60
61export const sessions = {
62 create: (tokenHash: string, userId: string, expiresAt: number) =>
63 convexFetch<{ id: string; tokenHash: string }>('/api/sessions/create', {
64 tokenHash,
65 userId,
66 expiresAt
67 }),
68
69 validate: (tokenHash: string) =>
70 convexFetch<{ session: ConvexSession | null; user: ConvexUser | null }>(
71 '/api/sessions/validate',
72 { tokenHash }
73 ),
74
75 remove: (tokenHash: string) =>
76 convexFetch<{ success: boolean }>('/api/sessions/remove', { tokenHash })
77};
78
79// ============ CHAT ROOMS ============
80
81export const chatRooms = {
82 getOrCreateDefault: () => convexFetch<ConvexChatRoom>('/api/chatRooms/getOrCreateDefault', {}),
83
84 getById: (id: string) => convexFetch<ConvexChatRoom | null>('/api/chatRooms/getById', { id })
85};
86
87// ============ MESSAGES ============
88
89export const messages = {
90 getByRoom: (roomId: string, limit?: number, beforeTimestamp?: number) =>
91 convexFetch<{ messages: ConvexMessage[]; hasMore: boolean }>('/api/messages/getByRoom', {
92 roomId,
93 limit,
94 beforeTimestamp
95 }),
96
97 send: (
98 chatRoomId: string,
99 senderId: string,
100 content: string,
101 type?: string,
102 styleData?: string,
103 hasFormatting?: boolean
104 ) =>
105 convexFetch<ConvexMessage>('/api/messages/send', {
106 chatRoomId,
107 senderId,
108 content,
109 type,
110 styleData,
111 hasFormatting
112 })
113};
114
115// ============ TYPES ============
116
117export interface ConvexUser {
118 _id: string;
119 _creationTime: number;
120 password: string;
121 nickname: string;
122 status: string;
123 avatarUrl?: string;
124 createdAt: number;
125 lastSeen?: number;
126 lastReadMentionTimestamp?: number;
127}
128
129export interface ConvexSession {
130 _id: string;
131 _creationTime: number;
132 tokenHash: string;
133 userId: string;
134 expiresAt: number;
135 createdAt: number;
136}
137
138export interface ConvexMessage {
139 _id: string;
140 _creationTime: number;
141 chatRoomId: string;
142 senderId: string;
143 content: string;
144 type: string;
145 timestamp: number;
146 styleData?: string;
147 hasFormatting?: boolean;
148}
149
150export interface ConvexChatRoom {
151 _id: string;
152 _creationTime: number;
153 name?: string;
154 type: string;
155 createdAt: number;
156 topic?: string;
157 memberIds?: string[];
158 createdBy?: string;
159}
160
161export interface ConvexTextPreferences {
162 _id: string;
163 _creationTime: number;
164 userId: string;
165 defaultStyle: {
166 fontFamily: string;
167 fontSize: number;
168 color?: string;
169 gradient?: string[];
170 bold: boolean;
171 italic: boolean;
172 underline: boolean;
173 strikethrough: boolean;
174 };
175 allowFormatting: boolean;
176 maxMessageLength: number;
177 createdAt: number;
178 updatedAt: number;
179}
180
181// ============ TEXT PREFERENCES ============
182
183export const textPreferences = {
184 get: (userId: string) =>
185 convexFetch<ConvexTextPreferences | null>('/api/textPreferences/get', { userId }),
186
187 save: (
188 userId: string,
189 defaultStyle: {
190 fontFamily: string;
191 fontSize: number;
192 color?: string;
193 gradient?: string[];
194 bold: boolean;
195 italic: boolean;
196 underline: boolean;
197 strikethrough: boolean;
198 },
199 allowFormatting: boolean,
200 maxMessageLength: number
201 ) =>
202 convexFetch<{ success: boolean; id: string }>('/api/textPreferences/save', {
203 userId,
204 defaultStyle,
205 allowFormatting,
206 maxMessageLength
207 })
208};
209
210export default { users, sessions, messages, chatRooms, textPreferences };