A decentralized music tracking and discovery platform built on AT Protocol 馃幍
at feat/like-scrobble 392 lines 9.2 kB view raw
1import axios from "axios"; 2import { ctx } from "context"; 3import { eq } from "drizzle-orm"; 4import { Hono } from "hono"; 5import jwt from "jsonwebtoken"; 6import { encrypt } from "lib/crypto"; 7import { env } from "lib/env"; 8import { requestCounter } from "metrics"; 9import tables from "schema"; 10import { emailSchema } from "types/email"; 11 12const app = new Hono(); 13 14app.get("/login", async (c) => { 15 requestCounter.add(1, { method: "GET", route: "/dropbox/login" }); 16 const bearer = (c.req.header("authorization") || "").split(" ")[1]?.trim(); 17 18 if (!bearer || bearer === "null") { 19 c.status(401); 20 return c.text("Unauthorized"); 21 } 22 23 const { did } = jwt.verify(bearer, env.JWT_SECRET, { 24 ignoreExpiration: true, 25 }); 26 27 const user = await ctx.db 28 .select() 29 .from(tables.users) 30 .where(eq(tables.users.did, did)) 31 .limit(1) 32 .execute() 33 .then((res) => res[0]); 34 35 if (!user) { 36 c.status(401); 37 return c.text("Unauthorized"); 38 } 39 40 const clientId = env.DROPBOX_CLIENT_ID; 41 const redirectUri = `https://www.dropbox.com/oauth2/authorize?client_id=${clientId}&redirect_uri=${env.DROPBOX_REDIRECT_URI}&response_type=code&token_access_type=offline&state=${user.id}`; 42 return c.json({ redirectUri }); 43}); 44 45app.get("/oauth/callback", async (c) => { 46 requestCounter.add(1, { method: "GET", route: "/dropbox/oauth/callback" }); 47 const params = new URLSearchParams(c.req.url.split("?")[1]); 48 const entries = Object.fromEntries(params.entries()); 49 // entries.code 50 const response = await axios.postForm( 51 "https://api.dropboxapi.com/oauth2/token", 52 { 53 code: entries.code, 54 grant_type: "authorization_code", 55 client_id: env.DROPBOX_CLIENT_ID, 56 client_secret: env.DROPBOX_CLIENT_SECRET, 57 redirect_uri: env.DROPBOX_REDIRECT_URI, 58 }, 59 ); 60 61 const { dropbox, dropbox_tokens } = await ctx.db 62 .select() 63 .from(tables.dropbox) 64 .where(eq(tables.dropbox.userId, entries.state)) 65 .leftJoin( 66 tables.dropboxTokens, 67 eq(tables.dropboxTokens.id, tables.dropbox.dropboxTokenId), 68 ) 69 .limit(1) 70 .execute() 71 .then((res) => res[0]); 72 73 const newDropboxToken = await ctx.db 74 .insert(tables.dropboxTokens) 75 .values({ 76 id: dropbox_tokens?.id, 77 refreshToken: encrypt( 78 response.data.refresh_token, 79 env.SPOTIFY_ENCRYPTION_KEY, 80 ), 81 }) 82 .onConflictDoUpdate({ 83 target: tables.dropboxTokens.id, // specify the conflict column (primary key) 84 set: { 85 refreshToken: encrypt( 86 response.data.refresh_token, 87 env.SPOTIFY_ENCRYPTION_KEY, 88 ), 89 }, 90 }) 91 .returning() 92 .execute() 93 .then((res) => res[0]); 94 95 await ctx.db 96 .insert(tables.dropbox) 97 .values({ 98 id: dropbox?.id, 99 dropboxTokenId: newDropboxToken.id, 100 userId: entries.state, 101 }) 102 .onConflictDoUpdate({ 103 target: tables.dropbox.id, 104 set: { 105 dropboxTokenId: newDropboxToken.id, 106 userId: entries.state, 107 }, 108 }) 109 .execute(); 110 111 return c.redirect(`${env.FRONTEND_URL}/dropbox`); 112}); 113 114app.post("/join", async (c) => { 115 requestCounter.add(1, { method: "POST", route: "/dropbox/join" }); 116 const bearer = (c.req.header("authorization") || "").split(" ")[1]?.trim(); 117 118 if (!bearer || bearer === "null") { 119 c.status(401); 120 return c.text("Unauthorized"); 121 } 122 123 const { did } = jwt.verify(bearer, env.JWT_SECRET, { 124 ignoreExpiration: true, 125 }); 126 127 const user = await ctx.db 128 .select() 129 .from(tables.users) 130 .where(eq(tables.users.did, did)) 131 .limit(1) 132 .execute() 133 .then((res) => res[0]); 134 if (!user) { 135 c.status(401); 136 return c.text("Unauthorized"); 137 } 138 139 const body = await c.req.json(); 140 const parsed = emailSchema.safeParse(body); 141 142 if (parsed.error) { 143 c.status(400); 144 return c.text("Invalid email: " + parsed.error.message); 145 } 146 147 const { email } = parsed.data; 148 149 try { 150 await ctx.db 151 .insert(tables.dropboxAccounts) 152 .values({ 153 userId: user.id, 154 email, 155 isBetaUser: false, 156 }) 157 .execute(); 158 } catch (e) { 159 if ( 160 !e.message.includes("invalid record: column [user_id]: is not unique") 161 ) { 162 console.error(e.message); 163 } else { 164 throw e; 165 } 166 } 167 168 await fetch("https://beta.rocksky.app", { 169 method: "POST", 170 headers: { 171 "Content-Type": "application/json", 172 Authorization: `Bearer ${env.ROCKSKY_BETA_TOKEN}`, 173 }, 174 body: JSON.stringify({ email }), 175 }); 176 177 return c.json({ status: "ok" }); 178}); 179 180app.get("/files", async (c) => { 181 requestCounter.add(1, { method: "GET", route: "/dropbox/files" }); 182 const bearer = (c.req.header("authorization") || "").split(" ")[1]?.trim(); 183 184 if (!bearer || bearer === "null") { 185 c.status(401); 186 return c.text("Unauthorized"); 187 } 188 189 const { did } = jwt.verify(bearer, env.JWT_SECRET, { 190 ignoreExpiration: true, 191 }); 192 193 const [user] = await ctx.db 194 .select() 195 .from(tables.users) 196 .where(eq(tables.users.did, did)) 197 .limit(1) 198 .execute(); 199 if (!user) { 200 c.status(401); 201 return c.text("Unauthorized"); 202 } 203 204 const path = c.req.query("path"); 205 206 if (!path) { 207 try { 208 const { data } = await ctx.dropbox.post("dropbox.getFiles", { 209 did, 210 }); 211 212 return c.json(data); 213 } catch { 214 await ctx.dropbox.post("dropbox.createMusicFolder", { 215 did, 216 }); 217 const response = await ctx.dropbox.post("dropbox.getFiles", { 218 did, 219 }); 220 return c.json(response.data); 221 } 222 } 223 224 const { data } = await ctx.dropbox.post("dropbox.getFilesAt", { 225 did, 226 path, 227 }); 228 229 return c.json(data); 230}); 231 232app.get("/temporary-link", async (c) => { 233 requestCounter.add(1, { method: "GET", route: "/dropbox/temporary-link" }); 234 const bearer = (c.req.header("authorization") || "").split(" ")[1]?.trim(); 235 236 if (!bearer || bearer === "null") { 237 c.status(401); 238 return c.text("Unauthorized"); 239 } 240 241 const { did } = jwt.verify(bearer, env.JWT_SECRET, { 242 ignoreExpiration: true, 243 }); 244 245 const [user] = await ctx.db 246 .select() 247 .from(tables.users) 248 .where(eq(tables.users.did, did)) 249 .limit(1) 250 .execute(); 251 if (!user) { 252 c.status(401); 253 return c.text("Unauthorized"); 254 } 255 256 const path = c.req.query("path"); 257 if (!path) { 258 c.status(400); 259 return c.text("Bad Request, path is required"); 260 } 261 262 const { data } = await ctx.dropbox.post("dropbox.getTemporaryLink", { 263 did, 264 path, 265 }); 266 267 return c.json(data); 268}); 269 270app.get("/files/:id", async (c) => { 271 requestCounter.add(1, { method: "GET", route: "/dropbox/files/:id" }); 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.db 284 .select() 285 .from(tables.users) 286 .where(eq(tables.users.did, did)) 287 .limit(1) 288 .execute(); 289 if (!user) { 290 c.status(401); 291 return c.text("Unauthorized"); 292 } 293 294 const path = c.req.param("id"); 295 296 const response = await ctx.dropbox.post("dropbox.getMetadata", { 297 did, 298 path, 299 }); 300 301 return c.json(response.data); 302}); 303 304app.get("/file", async (c) => { 305 requestCounter.add(1, { method: "GET", route: "/dropbox/file" }); 306 const bearer = (c.req.header("authorization") || "").split(" ")[1]?.trim(); 307 308 if (!bearer || bearer === "null") { 309 c.status(401); 310 return c.text("Unauthorized"); 311 } 312 313 const { did } = jwt.verify(bearer, env.JWT_SECRET, { 314 ignoreExpiration: true, 315 }); 316 317 const [user] = await ctx.db 318 .select() 319 .from(tables.users) 320 .where(eq(tables.users.did, did)) 321 .limit(1) 322 .execute(); 323 if (!user) { 324 c.status(401); 325 return c.text("Unauthorized"); 326 } 327 328 const path = c.req.query("path"); 329 330 if (!path) { 331 c.status(400); 332 return c.text("Bad Request, path is required"); 333 } 334 335 const response = await ctx.dropbox.post("dropbox.getMetadata", { 336 did, 337 path, 338 }); 339 340 return c.json(response.data); 341}); 342 343app.get("/download", async (c) => { 344 requestCounter.add(1, { method: "GET", route: "/dropbox/download" }); 345 const bearer = (c.req.header("authorization") || "").split(" ")[1]?.trim(); 346 347 if (!bearer || bearer === "null") { 348 c.status(401); 349 return c.text("Unauthorized"); 350 } 351 352 const { did } = jwt.verify(bearer, env.JWT_SECRET, { 353 ignoreExpiration: true, 354 }); 355 356 const [user] = await ctx.db 357 .select() 358 .from(tables.users) 359 .where(eq(tables.users.did, did)) 360 .limit(1) 361 .execute(); 362 if (!user) { 363 c.status(401); 364 return c.text("Unauthorized"); 365 } 366 367 const path = c.req.query("path"); 368 if (!path) { 369 c.status(400); 370 return c.text("Bad Request, path is required"); 371 } 372 373 const response = await ctx.dropbox.post("dropbox.downloadFile", { 374 did, 375 path, 376 }); 377 378 c.header( 379 "Content-Type", 380 response.headers["content-type"] || "application/octet-stream", 381 ); 382 c.header( 383 "Content-Disposition", 384 response.headers["content-disposition"] || "attachment", 385 ); 386 387 return new Response(response.data, { 388 headers: c.res.headers, 389 }); 390}); 391 392export default app;