A decentralized music tracking and discovery platform built on AT Protocol 馃幍
at feat/like-scrobble 377 lines 9.3 kB view raw
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;