const path = require("path"); require("dotenv").config(); const express = require("express"); const session = require("express-session"); const rateLimit = require("express-rate-limit"); const FileStore = require("session-file-store")(session); const { InstallProvider, FileInstallationStore } = require("@slack/oauth"); const { getLoginUrl, refreshToken, getCredentials, saveCredentials, spotifyRoutes, addSongToPlaylist, } = require("./spotify"); const { QuickDB } = require("quick.db"); const db = new QuickDB({ filePath: "./data/songs.sqlite", }); function arrayToCsv(data) { return data .map( (row) => row .map(String) // convert every value to String .map((v) => v.replaceAll('"', '""')) // escape double quotes .map((v) => `"${v}"`) // quote it .join(","), // comma-separated ) .join("\r\n"); // rows starting on new lines } let cacheDb = {}; const app = express(); const userScopes = ["identity.avatar", "identity.basic", "identity.team"]; // Initialize const oauth = new InstallProvider({ clientId: process.env.SLACK_CLIENT_ID, clientSecret: process.env.SLACK_CLIENT_SECRET, stateSecret: process.env.STATE_SECRET, stateVerification: false, stateStore: new FileInstallationStore( path.join(__dirname, "../data/states.json"), ), installationStore: new FileInstallationStore( path.join(__dirname, "../data/installations.json"), ), // installationStore: { //} stateStore: { generateStateParam: (installUrlOptions, date) => { // generate a random string to use as state in the URL const randomState = process.env.STATE_SECRET + Math.random().toString(36).substring(7); // save installOptions to cache/db cacheDb[randomState] = installUrlOptions; // myDB.set(randomState, installUrlOptions); // return a state string that references saved options in DB return randomState; }, // verifyStateParam's first argument is a date object and the second argument is a string representing the state // verifyStateParam is expected to return an object representing installUrlOptions verifyStateParam: (date, state) => { return cacheDb[state]; // fetch saved installOptions from DB using state reference const installUrlOptions = myDB.get(randomState); return installUrlOptions; }, }, }); app.use(express.json()); app.use(express.static(path.join(__dirname, "public"))); app.set("view engine", "ejs"); app.set("views", "src/views"); app.use(express.urlencoded({ extended: true })); app.use( session({ secret: process.env.STATE_SECRET, resave: true, store: new FileStore({ path: path.join(__dirname, "../data/sessions"), }), saveUninitialized: true, cookie: { secure: "auto", maxAge: 1000 * 60 * 60 * 24 * 7 }, }), ); try { const statusMonitor = require("express-status-monitor")({ healthChecks: [ { protocol: "http", host: "localhost", port: 3000, path: "/", timeout: 1000, interval: 1000, }, ], }); app.use(statusMonitor); app.use((req, res, next) => { // console.debug([req.headers, req.session]) next(); }); } catch (e) { // we can ignore since this is an optional dependency } app.get("/login", async (req, res) => { if (req.session.token) { res.redirect("/home"); } else { res.redirect( await oauth.generateInstallUrl({ // Add the scopes your app needs redirectUri: process.env.SLACK_REDIRECT_URI, scopes: [], userScopes: userScopes, }), ); } }); app.get("/slack/callback", (req, res) => { // console.debug(req.headers, req.url) oauth.handleCallback(req, res, { success: async (install) => { // typings // user: { token:string , scopes: string[], id: string} // console.log(install) req.session.info = install; req.session.token = install.user.token; res.redirect("/home"); }, failure: (err) => { console.log(err); res.send( "Failed to install!, please contact neon in the slack!, \n" + err.stack, ); }, }); }); app.get("/logout", (req, res) => { req.session.destroy(); res.redirect("/"); }); app.get("/", (req, res) => { res.render("index", { title: "Hack Club Spotify Bot", description: "Contribute to the hackclub spotify playlist!", }); }); const errorStrings = [ "Invalid CSRF Token!", // token = csrf token "Song is not a track! (or not even a spotify song url)", "Song already exists in the database! (its in the playlist or banned from the playlist)", ]; app.get("/home", async (req, res) => { if (!req.session.info) return res.redirect("/login"); let onetimetoken = Math.random().toString(36).substring(7); cacheDb[onetimetoken] = true; res.render("home", { title: "Hack Club Spotify Bot", description: "Contribute to the hackclub spotify playlist!", userinfo: req.session.info, onetimetoken, error: errorStrings[req.query.error], s: req.query.s, }); }); app.get("/download/db", async (req, res) => { if (!req.session.info) return res.redirect("/login"); const allSongs = await db.all(); const csvData = arrayToCsv([ ["slack_id", "url", "song_id", "added_at"], ...allSongs.map((d) => { return [d.value.added_by, d.value.song_url, d.id, d.value.added_at]; }), ]); res.setHeader("Content-Type", "text/csv"); res.setHeader("Content-Disposition", 'attachment; filename="songs.csv"'); res.send(csvData); }); const limiter = rateLimit({ windowMs: 1 * 60 * 1000, // 1 minutes limit: 5, // Limit each IP to 100 requests per `window` (here, per 15 minutes). standardHeaders: "draft-7", // draft-6: `RateLimit-*` headers; draft-7: combined `RateLimit` header legacyHeaders: false, // Disable the `X-RateLimit-*` headers. // store: ... , // Redis, Memcached, etc. See below. }); // Apply the rate limiting middleware to all requests. app.use(limiter); app.post("/spotify/submitsong", async (req, res) => { if (!req.session.token) return res.redirect("/login"); if (!cacheDb[req.query.token]) return res.redirect(`/home?error=0`); delete cacheDb[req.query.token]; const songurl = req.body.songurl; const songuriinfo = require("spotify-uri").parse(songurl); if (songuriinfo.type !== "track") return res.redirect(`/home?error=1`); const alreadyExists = await db.has(songuriinfo.id); if (alreadyExists) return res.redirect(`/home?error=2`); const formattedURI = require("spotify-uri").formatURI(songuriinfo); await db.set(songuriinfo.id, { song_url: songurl, added_by: req.session.info.user.id, added_at: Date.now(), }); addSongToPlaylist(formattedURI); fetch("https://slack.mybot.saahild.com/send-spotify", { method: "POST", body: JSON.stringify({ // message: { // channel: "C07RE4N7S4B", // text: `:new_spotify: New Song: ${songurl} - added by <@${req.session.info.user.id}>`, // }, songurl, user: req.session.info.user.id, noping: Boolean(process.env.TESTING), }), headers: { Authorization: process.env.AUTH_FOR_ZEON, "Content-Type": "application/json", }, }) .then((r) => r.json()) .then((d) => { // fetch("https://slack.mybot.saahild.com/send-private", { // method: "POST", // body: JSON.stringify({ // channel: "C07RE4N7S4B", // thread_ts: d.ts, // text: `:thread: Responses about new song here please!`, // }), // headers: { // Authorization: process.env.AUTH_FOR_ZEON, // "Content-Type": "application/json", // }, // }); }); // if (!process.env.TESTING) { // fetch("https://slack.mybot.saahild.com/send-private", { // method: "POST", // body: JSON.stringify({ // channel: "C07RE4N7S4B", // text: ``, // }), // headers: { // Authorization: process.env.AUTH_FOR_ZEON, // "Content-Type": "application/json", // }, // }); // } res.redirect("/home?s=1"); }); app.get("/spotify/link", async (req, res) => { if (!req.session.info) return res.redirect("/login"); if (req.session.info.user.id !== "U07L45W79E1") return res.status(401).end("unauthorized"); res.redirect(getLoginUrl()); }); spotifyRoutes(app); app.listen(process.env.PORT || 3000, async () => { console.log("Example app listening on port 3000!"); // if(!await db.has()) if (getCredentials() !== null) refreshToken(getCredentials().refresh_token); }); process.on("uncaughtException", function (err) { console.log(err); }); process.on("unhandledRejection", function (err) { console.log(err); });