A decentralized music tracking and discovery platform built on AT Protocol 馃幍 rocksky.app
spotify atproto lastfm musicbrainz scrobbling listenbrainz
96
fork

Configure Feed

Select the types of activity you want to include in your feed.

at feat/pgpull 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;