A decentralized music tracking and discovery platform built on AT Protocol 馃幍
at feat/discord-webhook 309 lines 8.0 kB view raw
1import { equals } from "@xata.io/client"; 2import axios from "axios"; 3import { ctx } from "context"; 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 { emailSchema } from "types/email"; 12 13const app = new Hono(); 14 15app.get("/login", async (c) => { 16 requestCounter.add(1, { method: "GET", route: "/googledrive/login" }); 17 const bearer = (c.req.header("authorization") || "").split(" ")[1]?.trim(); 18 19 if (!bearer || bearer === "null") { 20 c.status(401); 21 return c.text("Unauthorized"); 22 } 23 24 const { did } = jwt.verify(bearer, env.JWT_SECRET, { 25 ignoreExpiration: true, 26 }); 27 28 const user = await ctx.client.db.users.filter("did", equals(did)).getFirst(); 29 if (!user) { 30 c.status(401); 31 return c.text("Unauthorized"); 32 } 33 34 const credentials = JSON.parse( 35 fs.readFileSync("credentials.json").toString("utf-8"), 36 ); 37 const { client_id, client_secret } = credentials.installed || credentials.web; 38 const oAuth2Client = new google.auth.OAuth2( 39 client_id, 40 client_secret, 41 env.GOOGLE_REDIRECT_URI, 42 ); 43 44 // Generate Auth URL 45 const authUrl = oAuth2Client.generateAuthUrl({ 46 access_type: "offline", 47 prompt: "consent", 48 scope: ["https://www.googleapis.com/auth/drive"], 49 state: user.xata_id, 50 }); 51 return c.json({ authUrl }); 52}); 53 54app.get("/oauth/callback", async (c) => { 55 requestCounter.add(1, { 56 method: "GET", 57 route: "/googledrive/oauth/callback", 58 }); 59 const params = new URLSearchParams(c.req.url.split("?")[1]); 60 const entries = Object.fromEntries(params.entries()); 61 62 const credentials = JSON.parse( 63 fs.readFileSync("credentials.json").toString("utf-8"), 64 ); 65 const { client_id, client_secret } = credentials.installed || credentials.web; 66 67 const response = await axios.postForm("https://oauth2.googleapis.com/token", { 68 code: entries.code, 69 client_id, 70 client_secret, 71 redirect_uri: env.GOOGLE_REDIRECT_URI, 72 grant_type: "authorization_code", 73 }); 74 75 const googledrive = await ctx.client.db.google_drive 76 .select(["*", "user_id.*", "google_drive_token_id.*"]) 77 .filter("user_id.xata_id", equals(entries.state)) 78 .getFirst(); 79 80 const newGoogleDriveToken = 81 await ctx.client.db.google_drive_tokens.createOrUpdate( 82 googledrive?.google_drive_token_id?.xata_id, 83 { 84 refresh_token: encrypt( 85 response.data.refresh_token, 86 env.SPOTIFY_ENCRYPTION_KEY, 87 ), 88 }, 89 ); 90 91 await ctx.client.db.google_drive.createOrUpdate(googledrive?.xata_id, { 92 google_drive_token_id: newGoogleDriveToken.xata_id, 93 user_id: entries.state, 94 }); 95 96 return c.redirect(`${env.FRONTEND_URL}/googledrive`); 97}); 98 99app.post("/join", async (c) => { 100 requestCounter.add(1, { method: "POST", route: "/googledrive/join" }); 101 const bearer = (c.req.header("authorization") || "").split(" ")[1]?.trim(); 102 103 if (!bearer || bearer === "null") { 104 c.status(401); 105 return c.text("Unauthorized"); 106 } 107 108 const { did } = jwt.verify(bearer, env.JWT_SECRET, { 109 ignoreExpiration: true, 110 }); 111 112 const user = await ctx.client.db.users.filter("did", equals(did)).getFirst(); 113 if (!user) { 114 c.status(401); 115 return c.text("Unauthorized"); 116 } 117 118 const body = await c.req.json(); 119 const parsed = emailSchema.safeParse(body); 120 121 if (parsed.error) { 122 c.status(400); 123 return c.text("Invalid email: " + parsed.error.message); 124 } 125 126 const { email } = parsed.data; 127 128 try { 129 await ctx.client.db.google_drive_accounts.create({ 130 user_id: user.xata_id, 131 email, 132 is_beta_user: false, 133 }); 134 } catch (e) { 135 if ( 136 !e.message.includes("invalid record: column [user_id]: is not unique") 137 ) { 138 console.error(e.message); 139 } else { 140 throw e; 141 } 142 } 143 144 await fetch("https://beta.rocksky.app", { 145 method: "POST", 146 headers: { 147 "Content-Type": "application/json", 148 Authorization: `Bearer ${env.ROCKSKY_BETA_TOKEN}`, 149 }, 150 body: JSON.stringify({ email }), 151 }); 152 153 return c.json({ status: "ok" }); 154}); 155 156app.get("/files", async (c) => { 157 requestCounter.add(1, { method: "GET", route: "/googledrive/files" }); 158 const bearer = (c.req.header("authorization") || "").split(" ")[1]?.trim(); 159 160 if (!bearer || bearer === "null") { 161 c.status(401); 162 return c.text("Unauthorized"); 163 } 164 165 const { did } = jwt.verify(bearer, env.JWT_SECRET, { 166 ignoreExpiration: true, 167 }); 168 169 const user = await ctx.client.db.users.filter("did", equals(did)).getFirst(); 170 if (!user) { 171 c.status(401); 172 return c.text("Unauthorized"); 173 } 174 175 const parent_id = c.req.query("parent_id"); 176 177 try { 178 if (parent_id) { 179 const { data } = await ctx.googledrive.post( 180 "googledrive.getFilesInParents", 181 { 182 did, 183 parent_id, 184 }, 185 ); 186 return c.json(data); 187 } 188 189 let response = await ctx.googledrive.post("googledrive.getMusicDirectory", { 190 did, 191 }); 192 193 if (response.data.files.length === 0) { 194 await ctx.googledrive.post("googledrive.createMusicDirectory", { did }); 195 response = await ctx.googledrive.post("googledrive.getMusicDirectory", { 196 did, 197 }); 198 } 199 200 const { data } = await ctx.googledrive.post( 201 "googledrive.getFilesInParents", 202 { 203 did, 204 parent_id: response.data.files[0].id, 205 }, 206 ); 207 return c.json(data); 208 } catch (error) { 209 if (axios.isAxiosError(error)) { 210 console.error("Axios error:", error.response?.data || error.message); 211 212 const credentials = JSON.parse( 213 fs.readFileSync("credentials.json").toString("utf-8"), 214 ); 215 const { client_id, client_secret } = 216 credentials.installed || credentials.web; 217 const oAuth2Client = new google.auth.OAuth2( 218 client_id, 219 client_secret, 220 env.GOOGLE_REDIRECT_URI, 221 ); 222 223 // Generate Auth URL 224 const authUrl = oAuth2Client.generateAuthUrl({ 225 access_type: "offline", 226 prompt: "consent", 227 scope: ["https://www.googleapis.com/auth/drive"], 228 state: user.xata_id, 229 }); 230 231 return c.json({ 232 error: "Failed to fetch files", 233 authUrl, 234 }); 235 } 236 } 237}); 238 239app.get("/files/:id", async (c) => { 240 requestCounter.add(1, { method: "GET", route: "/googledrive/files/:id" }); 241 const bearer = (c.req.header("authorization") || "").split(" ")[1]?.trim(); 242 243 if (!bearer || bearer === "null") { 244 c.status(401); 245 return c.text("Unauthorized"); 246 } 247 248 const { did } = jwt.verify(bearer, env.JWT_SECRET, { 249 ignoreExpiration: true, 250 }); 251 252 const user = await ctx.client.db.users.filter("did", equals(did)).getFirst(); 253 if (!user) { 254 c.status(401); 255 return c.text("Unauthorized"); 256 } 257 258 const id = c.req.param("id"); 259 const response = await ctx.googledrive.post("googledrive.getFile", { 260 did, 261 file_id: id, 262 }); 263 264 return c.json(response.data); 265}); 266 267app.get("/files/:id/download", async (c) => { 268 requestCounter.add(1, { 269 method: "GET", 270 route: "/googledrive/files/:id/download", 271 }); 272 const bearer = (c.req.header("authorization") || "").split(" ")[1]?.trim(); 273 274 if (!bearer || bearer === "null") { 275 c.status(401); 276 return c.text("Unauthorized"); 277 } 278 279 const { did } = jwt.verify(bearer, env.JWT_SECRET, { 280 ignoreExpiration: true, 281 }); 282 283 const user = await ctx.client.db.users.filter("did", equals(did)).getFirst(); 284 if (!user) { 285 c.status(401); 286 return c.text("Unauthorized"); 287 } 288 289 const id = c.req.param("id"); 290 const response = await ctx.googledrive.post("googledrive.downloadFile", { 291 did, 292 file_id: id, 293 }); 294 295 c.header( 296 "Content-Type", 297 response.headers["content-type"] || "application/octet-stream", 298 ); 299 c.header( 300 "Content-Disposition", 301 response.headers["content-disposition"] || "attachment", 302 ); 303 304 return new Response(response.data, { 305 headers: c.res.headers, 306 }); 307}); 308 309export default app;