forked from
rocksky.app/rocksky
A decentralized music tracking and discovery platform built on AT Protocol 馃幍
1import axios from "axios";
2import { ctx } from "context";
3import { eq } from "drizzle-orm";
4import fs from "fs";
5import { google } from "googleapis";
6import { Hono } from "hono";
7import jwt from "jsonwebtoken";
8import { encrypt } from "lib/crypto";
9import { env } from "lib/env";
10import { requestCounter } from "metrics";
11import googleDriveAccounts from "schema/google-drive-accounts";
12import googleDriveTokens from "schema/google-drive-tokens";
13import googleDrive from "schema/googledrive";
14import users from "schema/users";
15import { emailSchema } from "types/email";
16
17const app = new Hono();
18
19app.get("/login", async (c) => {
20 requestCounter.add(1, { method: "GET", route: "/googledrive/login" });
21 const bearer = (c.req.header("authorization") || "").split(" ")[1]?.trim();
22
23 if (!bearer || bearer === "null") {
24 c.status(401);
25 return c.text("Unauthorized");
26 }
27
28 const { did } = jwt.verify(bearer, env.JWT_SECRET, {
29 ignoreExpiration: true,
30 });
31
32 const user = await ctx.db
33 .select()
34 .from(users)
35 .where(eq(users.did, did))
36 .limit(1)
37 .then((rows) => rows[0]);
38
39 if (!user) {
40 c.status(401);
41 return c.text("Unauthorized");
42 }
43
44 const credentials = JSON.parse(
45 fs.readFileSync("credentials.json").toString("utf-8"),
46 );
47 const { client_id, client_secret } = credentials.installed || credentials.web;
48 const oAuth2Client = new google.auth.OAuth2(
49 client_id,
50 client_secret,
51 env.GOOGLE_REDIRECT_URI,
52 );
53
54 // Generate Auth URL
55 const authUrl = oAuth2Client.generateAuthUrl({
56 access_type: "offline",
57 prompt: "consent",
58 scope: ["https://www.googleapis.com/auth/drive"],
59 state: user.id,
60 });
61 return c.json({ authUrl });
62});
63
64app.get("/oauth/callback", async (c) => {
65 requestCounter.add(1, {
66 method: "GET",
67 route: "/googledrive/oauth/callback",
68 });
69 const params = new URLSearchParams(c.req.url.split("?")[1]);
70 const entries = Object.fromEntries(params.entries());
71
72 const credentials = JSON.parse(
73 fs.readFileSync("credentials.json").toString("utf-8"),
74 );
75 const { client_id, client_secret } = credentials.installed || credentials.web;
76
77 const response = await axios.postForm("https://oauth2.googleapis.com/token", {
78 code: entries.code,
79 client_id,
80 client_secret,
81 redirect_uri: env.GOOGLE_REDIRECT_URI,
82 grant_type: "authorization_code",
83 });
84
85 const existingGoogleDrive = await ctx.db
86 .select({
87 googleDrive: googleDrive,
88 user: users,
89 token: googleDriveTokens,
90 })
91 .from(googleDrive)
92 .innerJoin(users, eq(googleDrive.userId, users.id))
93 .leftJoin(
94 googleDriveTokens,
95 eq(googleDrive.googleDriveTokenId, googleDriveTokens.id),
96 )
97 .where(eq(users.id, entries.state))
98 .limit(1)
99 .then((rows) => rows[0]);
100
101 let tokenId: string;
102 if (existingGoogleDrive?.token) {
103 const [updatedToken] = await ctx.db
104 .update(googleDriveTokens)
105 .set({
106 refreshToken: encrypt(
107 response.data.refresh_token,
108 env.SPOTIFY_ENCRYPTION_KEY,
109 ),
110 })
111 .where(eq(googleDriveTokens.id, existingGoogleDrive.token.id))
112 .returning();
113 tokenId = updatedToken.id;
114 } else {
115 const [newToken] = await ctx.db
116 .insert(googleDriveTokens)
117 .values({
118 refreshToken: encrypt(
119 response.data.refresh_token,
120 env.SPOTIFY_ENCRYPTION_KEY,
121 ),
122 })
123 .returning();
124 tokenId = newToken.id;
125 }
126
127 if (existingGoogleDrive?.googleDrive) {
128 await ctx.db
129 .update(googleDrive)
130 .set({
131 googleDriveTokenId: tokenId,
132 userId: entries.state,
133 })
134 .where(eq(googleDrive.id, existingGoogleDrive.googleDrive.id));
135 } else {
136 await ctx.db.insert(googleDrive).values({
137 googleDriveTokenId: tokenId,
138 userId: entries.state,
139 });
140 }
141
142 return c.redirect(`${env.FRONTEND_URL}/googledrive`);
143});
144
145app.post("/join", async (c) => {
146 requestCounter.add(1, { method: "POST", route: "/googledrive/join" });
147 const bearer = (c.req.header("authorization") || "").split(" ")[1]?.trim();
148
149 if (!bearer || bearer === "null") {
150 c.status(401);
151 return c.text("Unauthorized");
152 }
153
154 const { did } = jwt.verify(bearer, env.JWT_SECRET, {
155 ignoreExpiration: true,
156 });
157
158 const user = await ctx.db
159 .select()
160 .from(users)
161 .where(eq(users.did, did))
162 .limit(1)
163 .then((rows) => rows[0]);
164
165 if (!user) {
166 c.status(401);
167 return c.text("Unauthorized");
168 }
169
170 const body = await c.req.json();
171 const parsed = emailSchema.safeParse(body);
172
173 if (parsed.error) {
174 c.status(400);
175 return c.text("Invalid email: " + parsed.error.message);
176 }
177
178 const { email } = parsed.data;
179
180 try {
181 await ctx.db.insert(googleDriveAccounts).values({
182 userId: user.id,
183 email,
184 isBetaUser: false,
185 });
186 } catch (e) {
187 if (!e.message.includes("duplicate key value violates unique constraint")) {
188 console.error(e.message);
189 } else {
190 throw e;
191 }
192 }
193
194 await fetch("https://beta.rocksky.app", {
195 method: "POST",
196 headers: {
197 "Content-Type": "application/json",
198 Authorization: `Bearer ${env.ROCKSKY_BETA_TOKEN}`,
199 },
200 body: JSON.stringify({ email }),
201 });
202
203 return c.json({ status: "ok" });
204});
205
206app.get("/files", async (c) => {
207 requestCounter.add(1, { method: "GET", route: "/googledrive/files" });
208 const bearer = (c.req.header("authorization") || "").split(" ")[1]?.trim();
209
210 if (!bearer || bearer === "null") {
211 c.status(401);
212 return c.text("Unauthorized");
213 }
214
215 const { did } = jwt.verify(bearer, env.JWT_SECRET, {
216 ignoreExpiration: true,
217 });
218
219 const user = await ctx.db
220 .select()
221 .from(users)
222 .where(eq(users.did, did))
223 .limit(1)
224 .then((rows) => rows[0]);
225
226 if (!user) {
227 c.status(401);
228 return c.text("Unauthorized");
229 }
230
231 const parent_id = c.req.query("parent_id");
232
233 try {
234 if (parent_id) {
235 const { data } = await ctx.googledrive.post(
236 "googledrive.getFilesInParents",
237 {
238 did,
239 parent_id,
240 },
241 );
242 return c.json(data);
243 }
244
245 let response = await ctx.googledrive.post("googledrive.getMusicDirectory", {
246 did,
247 });
248
249 if (response.data.files.length === 0) {
250 await ctx.googledrive.post("googledrive.createMusicDirectory", { did });
251 response = await ctx.googledrive.post("googledrive.getMusicDirectory", {
252 did,
253 });
254 }
255
256 const { data } = await ctx.googledrive.post(
257 "googledrive.getFilesInParents",
258 {
259 did,
260 parent_id: response.data.files[0].id,
261 },
262 );
263 return c.json(data);
264 } catch (error) {
265 if (axios.isAxiosError(error)) {
266 console.error("Axios error:", error.response?.data || error.message);
267
268 const credentials = JSON.parse(
269 fs.readFileSync("credentials.json").toString("utf-8"),
270 );
271 const { client_id, client_secret } =
272 credentials.installed || credentials.web;
273 const oAuth2Client = new google.auth.OAuth2(
274 client_id,
275 client_secret,
276 env.GOOGLE_REDIRECT_URI,
277 );
278
279 // Generate Auth URL
280 const authUrl = oAuth2Client.generateAuthUrl({
281 access_type: "offline",
282 prompt: "consent",
283 scope: ["https://www.googleapis.com/auth/drive"],
284 state: user.id,
285 });
286
287 return c.json({
288 error: "Failed to fetch files",
289 authUrl,
290 });
291 }
292 }
293});
294
295app.get("/files/:id", async (c) => {
296 requestCounter.add(1, { method: "GET", route: "/googledrive/files/:id" });
297 const bearer = (c.req.header("authorization") || "").split(" ")[1]?.trim();
298
299 if (!bearer || bearer === "null") {
300 c.status(401);
301 return c.text("Unauthorized");
302 }
303
304 const { did } = jwt.verify(bearer, env.JWT_SECRET, {
305 ignoreExpiration: true,
306 });
307
308 const user = await ctx.db
309 .select()
310 .from(users)
311 .where(eq(users.did, did))
312 .limit(1)
313 .then((rows) => rows[0]);
314
315 if (!user) {
316 c.status(401);
317 return c.text("Unauthorized");
318 }
319
320 const id = c.req.param("id");
321 const response = await ctx.googledrive.post("googledrive.getFile", {
322 did,
323 file_id: id,
324 });
325
326 return c.json(response.data);
327});
328
329app.get("/files/:id/download", async (c) => {
330 requestCounter.add(1, {
331 method: "GET",
332 route: "/googledrive/files/:id/download",
333 });
334 const bearer = (c.req.header("authorization") || "").split(" ")[1]?.trim();
335
336 if (!bearer || bearer === "null") {
337 c.status(401);
338 return c.text("Unauthorized");
339 }
340
341 const { did } = jwt.verify(bearer, env.JWT_SECRET, {
342 ignoreExpiration: true,
343 });
344
345 const user = await ctx.db
346 .select()
347 .from(users)
348 .where(eq(users.did, did))
349 .limit(1)
350 .then((rows) => rows[0]);
351
352 if (!user) {
353 c.status(401);
354 return c.text("Unauthorized");
355 }
356
357 const id = c.req.param("id");
358 const response = await ctx.googledrive.post("googledrive.downloadFile", {
359 did,
360 file_id: id,
361 });
362
363 c.header(
364 "Content-Type",
365 response.headers["content-type"] || "application/octet-stream",
366 );
367 c.header(
368 "Content-Disposition",
369 response.headers["content-disposition"] || "attachment",
370 );
371
372 return new Response(response.data, {
373 headers: c.res.headers,
374 });
375});
376
377export default app;