diff --git a/hackclub-spotify-bot/.gitignore b/hackclub-spotify-bot/.gitignore index 819e52e..1be5c96 100644 --- a/hackclub-spotify-bot/.gitignore +++ b/hackclub-spotify-bot/.gitignore @@ -1,4 +1,3 @@ node_modules .env -db.json -data/* +db.json \ No newline at end of file diff --git a/hackclub-spotify-bot/TODO.md b/hackclub-spotify-bot/TODO.md deleted file mode 100644 index 7bf6c50..0000000 --- a/hackclub-spotify-bot/TODO.md +++ /dev/null @@ -1,15 +0,0 @@ -- [ ] Slack stuff -- - ~~[ ] web api [docs](https://tools.slack.dev/node-slack-sdk/web-api/)~~ -- - [x] send messages thru zeon https://github.com/NeonGamerBot-QK/slack-zeon/blob/092d324c7c58d37e2165ecf0a6798a983c75e7d2/src/modules/slackapp.ts#L19-L53 -- - [x] oauth [docs](https://tools.slack.dev/node-slack-sdk/oauth) -- - [ ] fix the [channel](https://app.slack.com/client/T0266FRGM/C07RE4N7S4B) -- - - [x] Add ping for new-song event & send message ovs -- [ ] Spotify -- - [x] web api [docs](https://developer.spotify.com/documentation/web-api/) -- - [x] oauth [docs](https://developer.spotify.com/documentation/general/guides/authorization-guide/) -- - [x] refresh token [docs](https://developer.spotify.com/documentation/general/guides/authorization-guide/#refresh-an-access-token) -- - [x] playlist tools (creation,modifcation,deletion) -- [x] keydb (quick db or smthing) -- [x] express -- [ ] transparency of added songs -- - [ ] export db -> into csv with properties (slack_id, url, added_at) diff --git a/hackclub-spotify-bot/package.json b/hackclub-spotify-bot/package.json index 88833ad..a62b1c4 100644 --- a/hackclub-spotify-bot/package.json +++ b/hackclub-spotify-bot/package.json @@ -8,16 +8,10 @@ }, "dependencies": { "@slack/oauth": "^3.0.1", - "better-sqlite3": "^11.3.0", + "@slack/web-api": "^7.6.0", "dotenv": "^16.4.5", "ejs": "^3.1.10", "express": "^4.21.1", - "express-session": "^1.18.1", - "quick.db": "^9.1.7", - "session-file-store": "^1.5.0", - "spotify-uri": "^4.1.0" - }, - "devDependencies": { - "express-status-monitor": "^1.3.4" + "express-session": "^1.18.1" } } diff --git a/hackclub-spotify-bot/.env.example b/hackclub-spotify-bot/src/.env.example similarity index 69% rename from hackclub-spotify-bot/.env.example rename to hackclub-spotify-bot/src/.env.example index 7395f7d..0b5bff2 100644 --- a/hackclub-spotify-bot/.env.example +++ b/hackclub-spotify-bot/src/.env.example @@ -3,5 +3,4 @@ SLACK_CLIENT_SECRET= SLACK_TOKEN= SPOTIFY_CLIENT_ID= SPOTIFY_CLIENT_SECRET= -SPOTIFY_REDIRECT_URI= -SLACK_REDIRECT_URI= \ No newline at end of file +SPOTIFY_REDIRECT_URI= \ No newline at end of file diff --git a/hackclub-spotify-bot/src/TODO.md b/hackclub-spotify-bot/src/TODO.md new file mode 100644 index 0000000..0a84465 --- /dev/null +++ b/hackclub-spotify-bot/src/TODO.md @@ -0,0 +1,15 @@ +- [ ] Slack stuff +- - [ ] web api [docs](https://tools.slack.dev/node-slack-sdk/web-api/) +- - [ ] oauth [docs](https://tools.slack.dev/node-slack-sdk/oauth) +- - [ ] fix the [channel](https://app.slack.com/client/T0266FRGM/C07RE4N7S4B) +- - - [ ] Add ping for new-song event & send message ovs +- [ ] Spotify +- - [ ] web api [docs](https://developer.spotify.com/documentation/web-api/) +- - [ ] oauth [docs](https://developer.spotify.com/documentation/general/guides/authorization-guide/) +- - [ ] refresh token [docs](https://developer.spotify.com/documentation/general/guides/authorization-guide/#refresh-an-access-token) +- - [ ] playlist tools (creation,modifcation,deletion) +- [ ] keydb (quick db or smthing) +- [ ] express +- [ ] transparency of added songs +- - [ ] export db -> into csv with properties (slack_id, slack_name, spotify_id, spotify_url) + diff --git a/hackclub-spotify-bot/src/index.js b/hackclub-spotify-bot/src/index.js index 98dca8d..81b86bd 100644 --- a/hackclub-spotify-bot/src/index.js +++ b/hackclub-spotify-bot/src/index.js @@ -1,60 +1,19 @@ const path = require("path"); -require("dotenv").config(); +require("dotenv").config({ path: path.join(__dirname, ".env") }); +console.debug(process.env); const express = require("express"); const session = require("express-session"); -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 { WebClient } = require("@slack/web-api"); +const { InstallProvider } = require("@slack/oauth"); +const { getLoginUrl, refreshToken } = require("./spotify"); -const db = new QuickDB({ - filePath: "./data/songs.sqlite", -}); -let cacheDb = {}; const app = express(); -const userScopes = ["identity.avatar", "identity.basic", "identity.team"]; // Initialize +const web = new WebClient(process.env.SLACK_TOKEN); 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; - }, - }, + stateSecret: Math.random().toString(36).substring(2), }); app.use(express.json()); app.use(express.static(path.join(__dirname, "public"))); @@ -63,170 +22,24 @@ 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"), - }), + secret: Math.random().toString(36).substring(2), + resave: false, saveUninitialized: true, - cookie: { secure: "auto", maxAge: 1000 * 60 * 60 * 24 * 365 }, + cookie: { secure: true }, }), ); -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.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-private", { - method: "POST", - body: JSON.stringify({ - channel: "C07RE4N7S4B", - text: `:new_spotify: New Song: ${songurl} - added by <@${req.session.info.user.id}>`, - }), - 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", - }, - }); +app.get("/login", async (req, res) => { + if (req.session.token) { } - 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 () => { +app.listen(process.env.PORT || 3000, () => { console.log("Example app listening on port 3000!"); - // if(!await db.has()) - if (getCredentials() !== null) refreshToken(getCredentials().refresh_token); }); diff --git a/hackclub-spotify-bot/src/public/hackclub.css b/hackclub-spotify-bot/src/public/hackclub.css index feb83cb..1816be0 100644 --- a/hackclub-spotify-bot/src/public/hackclub.css +++ b/hackclub-spotify-bot/src/public/hackclub.css @@ -1,635 +1,589 @@ -/* modified version of https://css.hackclub.com/theme.css */ -:root { - /* why are the css vars swapped with incorrect names? well im to lazy to fix it. */ - --darker: #121217; - --dark: #f9fafc; - --darkless: #e0e6ed; - --black: #fff; - --steel: #273444; - --slate: #3c4858; - --muted: #8492a6; - --smoke: #252429; - --snow: #17171d; - --white: #1f2d3d; - --red: #ec3750; - --orange: #ff8c37; - --yellow: #f1c40f; - --green: #33d6a6; - --cyan: #5bc0de; - --blue: #338eda; - --purple: #a633d6; - --text: var(--black); - --background: var(--white); - --elevated: var(--white); - --sheet: var(--snow); - --sunken: var(--smoke); - --border: var(--smoke); - --primary: #ec3750; - --secondary: #8492a6; - --accent: #5bc0de; - --twitter: #1da1f2; - --facebook: #3b5998; - --instagram: #e1306c; - --breakpoint-xs: 32em; - --breakpoint-s: 48em; - --breakpoint-m: 64em; - --breakpoint-l: 96em; - --breakpoint-xl: 128em; - --spacing-0: 0px; - --spacing-1: 4px; - --spacing-2: 8px; - --spacing-3: 16px; - --spacing-4: 32px; - --spacing-5: 64px; - --spacing-6: 128px; - --spacing-7: 256px; - --spacing-8: 512px; - --font-1: 12px; - --font-2: 16px; - --font-3: 20px; - --font-4: 24px; - --font-5: 32px; - --font-6: 48px; - --font-7: 64px; - --font-8: 96px; - --font-9: 128px; - --font-10: 160px; - --font-11: 192px; - --line-height-limit: 0.875; - --line-height-title: 1; - --line-height-heading: 1.125; - --line-height-subheading: 1.25; - --line-height-caption: 1.375; - --line-height-body: 1.5; - --font-weight-body: 400; - --font-weight-bold: 700; - --font-weight-heading: var(--font-weight-bold); - --letter-spacing-title: -0.009em; - --letter-spacing-headline: 0.009em; - --size-wide-plus: 2048px; - --size-wide: 1536px; - --size-layout-plus: 1200px; - --size-layout: 1024px; - --size-copy-ultra: 980px; - --size-copy-plus: 768px; - --size-copy: 680px; - --size-narrow-plus: 600px; - --size-narrow: 512px; - --radii-small: 4px; - --radii-default: 8px; - --radii-extra: 12px; - --radii-ultra: 16px; - --radii-circle: 99999px; - --shadow-text: 0 1px 2px rgba(0, 0, 0, 0.25), 0 2px 4px rgba(0, 0, 0, 0.125); - --shadow-small: 0 1px 2px rgba(0, 0, 0, 0.0625), - 0 2px 4px rgba(0, 0, 0, 0.0625); - --shadow-card: 0 4px 8px rgba(0, 0, 0, 0.125); - --shadow-elevated: 0 1px 2px rgba(0, 0, 0, 0.0625), - 0 8px 12px rgba(0, 0, 0, 0.125); -} - -body { - font-family: - "Phantom Sans", - system-ui, - -apple-system, - BlinkMacSystemFont, - "Segoe UI", - Roboto, - sans-serif; - line-height: var(--line-height-body); - font-weight: var(--font-weight-body); - margin: 0; - min-height: 100vh; - text-rendering: optimizeLegibility; - font-smooth: antialiased; - -moz-osx-font-smoothing: grayscale; - -webkit-font-smoothing: antialiased; - color: var(--text); - background-color: var(--background); - box-sizing: border-box; -} - -* { - box-sizing: border-box; -} - -.monospace { - font-family: "SF Mono", "Roboto Mono", Menlo, Consolas, monospace; -} - -.heading { - font-weight: var(--font-weight-bold); - line-height: var(--line-height-heading); - margin-top: 0; - margin-bottom: 0; -} - -.ultratitle { - font-weight: var(--font-weight-bold); - line-height: var(--line-height-limit); - letter-spacing: var(--letter-spacing-title); -} - -.title { - font-weight: var(--font-weight-bold); - line-height: var(--line-height-title); - letter-spacing: var(--letter-spacing-title); -} - -.subtitle { - margin-top: var(--spacing-3); - font-weight: var(--font-weight-body); - line-height: var(--line-height-subheading); - letter-spacing: var(--letter-spacing-headline); -} - -.headline { - margin-top: var(--spacing-3); - margin-bottom: var(--spacing-3); - font-size: var(--font-4); - line-height: var(--line-height-heading); - letter-spacing: var(--letter-spacing-headline); -} - -.subheadline { - margin-top: var(--spacing-0); - margin-bottom: var(--spacing-3); - font-size: var(--font-2); - line-height: var(--line-height-heading); - letter-spacing: var(--letter-spacing-headline); -} - -.eyebrow { - color: var(--muted); - font-weight: var(--font-weight-heading); - letter-spacing: var(--letter-spacing-headline); - line-height: var(--line-height-subheading); - text-transform: uppercase; - margin-top: var(--spacing-0); - margin-bottom: var(--spacing-2); -} - -.lead { - font-weight: var(--font-weight-body); -} - -.caption { - color: var(--muted); - font-weight: var(--font-weight-body); - letter-spacing: var(--letter-spacing-headline); - line-height: var(--line-height-caption); -} - -.pill { - border-radius: var(--radii-circle); - padding-left: var(--spacing-3); - padding-right: var(--spacing-3); - padding-top: var(--spacing-1); - padding-bottom: var(--spacing-1); - font-size: var(--font-2); - background: var(--primary); - color: var(--background); - font-weight: var(--font-weight-bold); -} - -.outline-badge { - border-radius: var(--radii-circle); - padding-left: var(--spacing-3); - padding-right: var(--spacing-3); - padding-top: var(--spacing-1); - padding-bottom: var(--spacing-1); - font-size: var(--font-2); - background: none; - color: var(--muted); - border: 1px solid currentcolor; - font-weight: var(--font-weight-body); -} - -button { - cursor: pointer; - font-family: inherit; - font-weight: var(--font-weight-bold); - border-radius: var(--radii-circle); - display: inline-flex; - align-items: center; - justify-content: center; - box-shadow: var(--shadow-card); - letter-spacing: var(--letter-spacing-headline); - -webkit-tap-highlight-color: transparent; - transition: - transform 0.125s ease-in-out, - box-shadow 0.125s ease-in-out; - box-sizing: border-box; - margin: 0; - min-width: 0; - -webkit-appearance: none; - -moz-appearance: none; - appearance: none; - text-align: center; - line-height: inherit; - -webkit-text-decoration: none; - text-decoration: none; - padding-left: 16px; - padding-right: 16px; - padding-top: 8px; - padding-bottom: 8px; - color: var(--theme-ui-colors-white, #ffffff); - background-color: var(--theme-ui-colors-primary, #ec3750); - border: 0; - font-size: var(--font-2); -} - -button:focus, -button:hover { - box-shadow: var(--shadow-elevated); - transform: scale(1.0625); -} -.button { - cursor: pointer; - font-family: inherit; - font-weight: var(--font-weight-bold); - border-radius: var(--radii-circle); - display: inline-flex; - align-items: center; - justify-content: center; - box-shadow: var(--shadow-card); - letter-spacing: var(--letter-spacing-headline); - -webkit-tap-highlight-color: transparent; - transition: - transform 0.125s ease-in-out, - box-shadow 0.125s ease-in-out; - box-sizing: border-box; - margin: 0; - min-width: 0; - -webkit-appearance: none; - -moz-appearance: none; - appearance: none; - text-align: center; - line-height: inherit; - -webkit-text-decoration: none; - text-decoration: none; - padding-left: 16px; - padding-right: 16px; - padding-top: 8px; - padding-bottom: 8px; - color: var(--theme-ui-colors-white, #ffffff); - background-color: var(--theme-ui-colors-primary, #ec3750); - border: 0; - font-size: var(--font-2); -} - -.button:focus, -.button:hover { - box-shadow: var(--shadow-elevated); - transform: scale(1.0625); -} -button.lg { - font-size: var(--font-3) !important; - line-height: var(--line-height-title); - padding-left: var(--spacing-4); - padding-right: var(--spacing-4); - padding-top: var(--spacing-3); - padding-bottom: var(--spacing-3); -} - -button.outline { - background: none; - color: var(--primary); - border: 2px solid currentcolor; -} - -button.cta { - font-size: var(--font-2); - background-image: radial-gradient( - ellipse farthest-corner at top left, - var(--orange), - var(--red) - ); -} - -.card { - background: var(--elevated); - color: var(--text); - border-radius: var(--radii-extra); - box-shadow: var(--shadow-card); - overflow: hidden; -} - -.card.sunken { - background: var(--sunken); - box-shadow: none; -} - -.card.interactive { - text-decoration: none; - -webkit-tap-highlight-color: transparent; - transition: - transform 0.125s ease-in-out, - box-shadow 0.125s ease-in-out; -} - -.card.interactive:hover, -.card.interactive:focus { - transform: scale(1.0625); - box-shadow: var(--shadow-elevated); -} - -input, -textarea, -select { - background: var(--elevated); - color: var(--text); - font-family: inherit; - border-radius: var(--radii-small); - border: 0; - font-size: inherit; - padding: var(--spacing-2); - -webkit-appearance: none; - -moz-appearance: none; - appearance: none; -} - -input::-webkit-input-placeholder, -input::-moz-placeholder, -input:-ms-input-placeholder, -textarea::-webkit-input-placeholder, -textarea::-moz-placeholder, -textarea:-ms-input-placeholder, -select::-webkit-input-placeholder, -select::-moz-placeholder, -select:-ms-input-placeholder { - color: var(--muted); -} - -input[type="search"]::-webkit-search-decoration, -textarea[type="search"]::-webkit-search-decoration, -select[type="search"]::-webkit-search-decoration { - display: none; -} - -input[type="checkbox"] { - -webkit-appearance: checkbox; - -moz-appearance: checkbox; - appearance: checkbox; -} - -label { - color: var(--text); - display: flex; - flex-direction: column; - text-align: left; - line-height: var(--line-height-caption); - font-size: var(--font-3); -} - -label.horizontal { - display: flex; -} - -.slider { - color: var(--primary); -} - -.form-hidden { - position: absolute; - height: 1px; - width: 1px; - clip: rect(1px, 1px, 1px, 1px); - white-space: nowrap; -} - -.container { - width: 100%; - margin: auto; - padding-left: var(--spacing-3); - padding-right: var(--spacing-3); -} - -h1 { - font-size: var(--font-5); - font-weight: var(--font-weight-bold); - line-height: var(--line-height-heading); - margin-top: 0; - margin-bottom: 0; -} - -h2 { - font-size: var(--font-4); - font-weight: var(--font-weight-bold); - line-height: var(--line-height-heading); - margin-top: 0; - margin-bottom: 0; -} - -h3 { - font-size: var(--font-3); - font-weight: var(--font-weight-bold); - line-height: var(--line-height-heading); - margin-top: 0; - margin-bottom: 0; -} - -h4 { - font-size: var(--font-2); - font-weight: var(--font-weight-bold); - line-height: var(--line-height-heading); - margin-top: 0; - margin-bottom: 0; -} - -h5 { - font-size: var(--font-1); - font-weight: var(--font-weight-bold); - line-height: var(--line-height-heading); - margin-top: 0; - margin-bottom: 0; -} - -h6 { - font-weight: var(--font-weight-bold); - line-height: var(--line-height-heading); - margin-top: 0; - margin-bottom: 0; -} - -p { - color: var(--text); - font-weight: var(--font-weight-body); - line-height: var(--line-height-body); - margin-top: var(--spacing-3); - margin-bottom: var(--spacing-3); -} - -img { - max-width: 100%; -} - -hr { - border: 0; - border-bottom: 1px solid var(--border); -} - -a { - color: var(--primary); - text-decoration: underline; - text-underline-position: under; -} - -a:focus, -a:hover { - text-decoration-style: wavy; - text-decoration-skip-ink: none; -} - -pre { - font-family: "SF Mono", "Roboto Mono", Menlo, Consolas, monospace; - font-size: var(--font-1); - padding: var(--spacing-3); - color: var(--text); - background: var(--sunken); - overflow: auto; - border-radius: var(--radii-default); - white-space: inherit; -} - -pre > code { - color: inherit; - margin-left: 0; - margin-right: 0; - padding-left: 0; - padding-right: 0; -} - -code { - font-family: "SF Mono", "Roboto Mono", Menlo, Consolas, monospace; - font-size: inherit; - color: var(--purple); - background: var(--sunken); - overflow: auto; - border-radius: var(--radii-small); - margin-left: var(--spacing-1); - margin-right: var(--spacing-1); - padding-left: var(--spacing-1); - padding-right: var(--spacing-1); -} - -p > code, -li > code { - color: var(--blue); - font-size: 0.875em; -} - -p > a > code, -li > a > code { - color: var(--blue); - font-size: 0.875em; -} - -li { - margin-top: var(--spacing-2); - margin-bottom: var(--spacing-2); -} - -table { - width: 100%; - margin-top: var(--spacing-4); - margin-bottom: var(--spacing-4); - border-collapse: separate; - border-spacing: 0; -} - -table > th, -table > td { - text-align: left; - padding: 4px; - padding-left: 0px; - border-color: var(--border); - border-bottom-style: solid; -} - -th { - vertical-align: bottom; - border-bottom-width: 2px; -} - -td { - vertical-align: top; - border-bottom-width: 1px; -} - -@media screen and (min-width: 32em) { - .ultratitle { - font-size: var(--font-5); - } - .title { - font-size: var(--font-4); - } - .subtitle { - font-size: var(--font-2); - } - .eyebrow { - font-size: var(--font-3); - } - .lead { - font-size: var(--font-2); - margin-top: var(--spacing-2); - margin-bottom: var(--spacing-2); - } - .card { - padding: var(--spacing-3); - } - .container { - max-width: var(--size-layout); - } - .container.copy { - max-width: var(--size-copy); - } - .container.narrow { - max-width: var(--size-narrow); - } -} - -@media screen and (min-width: 48em) { - .ultratitle { - font-size: var(--font-6); - } - .title { - font-size: var(--font-5); - } - .subtitle { - font-size: var(--font-3); - } - .eyebrow { - font-size: var(--font-4); - } - .lead { - font-size: var(--font-3); - margin-top: var(--spacing-3); - margin-bottom: var(--spacing-3); - } - .card { - padding: var(--spacing-4); - } -} - -@media screen and (min-width: 64em) { - .ultratitle { - font-size: var(--font-7); - } - .title { - font-size: var(--font-6); - } - .container { - max-width: var(--size-layout-plus); - } - .container.wide { - max-width: var(--size-wide); - } - .container.copy { - max-width: var(--size-copy-plus); - } - .container.narrow { - max-width: var(--size-narrow-plus); - } -} +/* modified version of https://css.hackclub.com/theme.css */ +:root { + /* why are the css vars swapped with incorrect names? well im to lazy to fix it. */ + --darker: #121217; + --dark: #f9fafc; + --darkless: #e0e6ed; + --black: #fff; + --steel: #273444; + --slate: #3c4858; + --muted: #8492a6; + --smoke: #252429; + --snow: #17171d; + --white: #1f2d3d; + --red: #ec3750; + --orange: #ff8c37; + --yellow: #f1c40f; + --green: #33d6a6; + --cyan: #5bc0de; + --blue: #338eda; + --purple: #a633d6; + --text: var(--black); + --background: var(--white); + --elevated: var(--white); + --sheet: var(--snow); + --sunken: var(--smoke); + --border: var(--smoke); + --primary: #ec3750; + --secondary: #8492a6; + --accent: #5bc0de; + --twitter: #1da1f2; + --facebook: #3b5998; + --instagram: #e1306c; + --breakpoint-xs: 32em; + --breakpoint-s: 48em; + --breakpoint-m: 64em; + --breakpoint-l: 96em; + --breakpoint-xl: 128em; + --spacing-0: 0px; + --spacing-1: 4px; + --spacing-2: 8px; + --spacing-3: 16px; + --spacing-4: 32px; + --spacing-5: 64px; + --spacing-6: 128px; + --spacing-7: 256px; + --spacing-8: 512px; + --font-1: 12px; + --font-2: 16px; + --font-3: 20px; + --font-4: 24px; + --font-5: 32px; + --font-6: 48px; + --font-7: 64px; + --font-8: 96px; + --font-9: 128px; + --font-10: 160px; + --font-11: 192px; + --line-height-limit: 0.875; + --line-height-title: 1; + --line-height-heading: 1.125; + --line-height-subheading: 1.25; + --line-height-caption: 1.375; + --line-height-body: 1.5; + --font-weight-body: 400; + --font-weight-bold: 700; + --font-weight-heading: var(--font-weight-bold); + --letter-spacing-title: -0.009em; + --letter-spacing-headline: 0.009em; + --size-wide-plus: 2048px; + --size-wide: 1536px; + --size-layout-plus: 1200px; + --size-layout: 1024px; + --size-copy-ultra: 980px; + --size-copy-plus: 768px; + --size-copy: 680px; + --size-narrow-plus: 600px; + --size-narrow: 512px; + --radii-small: 4px; + --radii-default: 8px; + --radii-extra: 12px; + --radii-ultra: 16px; + --radii-circle: 99999px; + --shadow-text: 0 1px 2px rgba(0, 0, 0, 0.25), 0 2px 4px rgba(0, 0, 0, 0.125); + --shadow-small: 0 1px 2px rgba(0, 0, 0, 0.0625), + 0 2px 4px rgba(0, 0, 0, 0.0625); + --shadow-card: 0 4px 8px rgba(0, 0, 0, 0.125); + --shadow-elevated: 0 1px 2px rgba(0, 0, 0, 0.0625), + 0 8px 12px rgba(0, 0, 0, 0.125); + } + + body { + font-family: "Phantom Sans", system-ui, -apple-system, BlinkMacSystemFont, + "Segoe UI", Roboto, sans-serif; + line-height: var(--line-height-body); + font-weight: var(--font-weight-body); + margin: 0; + min-height: 100vh; + text-rendering: optimizeLegibility; + font-smooth: antialiased; + -moz-osx-font-smoothing: grayscale; + -webkit-font-smoothing: antialiased; + color: var(--text); + background-color: var(--background); + box-sizing: border-box; + } + + * { + box-sizing: border-box; + } + + .monospace { + font-family: "SF Mono", "Roboto Mono", Menlo, Consolas, monospace; + } + + .heading { + font-weight: var(--font-weight-bold); + line-height: var(--line-height-heading); + margin-top: 0; + margin-bottom: 0; + } + + .ultratitle { + font-weight: var(--font-weight-bold); + line-height: var(--line-height-limit); + letter-spacing: var(--letter-spacing-title); + } + + .title { + font-weight: var(--font-weight-bold); + line-height: var(--line-height-title); + letter-spacing: var(--letter-spacing-title); + } + + .subtitle { + margin-top: var(--spacing-3); + font-weight: var(--font-weight-body); + line-height: var(--line-height-subheading); + letter-spacing: var(--letter-spacing-headline); + } + + .headline { + margin-top: var(--spacing-3); + margin-bottom: var(--spacing-3); + font-size: var(--font-4); + line-height: var(--line-height-heading); + letter-spacing: var(--letter-spacing-headline); + } + + .subheadline { + margin-top: var(--spacing-0); + margin-bottom: var(--spacing-3); + font-size: var(--font-2); + line-height: var(--line-height-heading); + letter-spacing: var(--letter-spacing-headline); + } + + .eyebrow { + color: var(--muted); + font-weight: var(--font-weight-heading); + letter-spacing: var(--letter-spacing-headline); + line-height: var(--line-height-subheading); + text-transform: uppercase; + margin-top: var(--spacing-0); + margin-bottom: var(--spacing-2); + } + + .lead { + font-weight: var(--font-weight-body); + } + + .caption { + color: var(--muted); + font-weight: var(--font-weight-body); + letter-spacing: var(--letter-spacing-headline); + line-height: var(--line-height-caption); + } + + .pill { + border-radius: var(--radii-circle); + padding-left: var(--spacing-3); + padding-right: var(--spacing-3); + padding-top: var(--spacing-1); + padding-bottom: var(--spacing-1); + font-size: var(--font-2); + background: var(--primary); + color: var(--background); + font-weight: var(--font-weight-bold); + } + + .outline-badge { + border-radius: var(--radii-circle); + padding-left: var(--spacing-3); + padding-right: var(--spacing-3); + padding-top: var(--spacing-1); + padding-bottom: var(--spacing-1); + font-size: var(--font-2); + background: none; + color: var(--muted); + border: 1px solid currentcolor; + font-weight: var(--font-weight-body); + } + + button { + cursor: pointer; + font-family: inherit; + font-weight: var(--font-weight-bold); + border-radius: var(--radii-circle); + display: inline-flex; + align-items: center; + justify-content: center; + box-shadow: var(--shadow-card); + letter-spacing: var(--letter-spacing-headline); + -webkit-tap-highlight-color: transparent; + transition: transform 0.125s ease-in-out, box-shadow 0.125s ease-in-out; + box-sizing: border-box; + margin: 0; + min-width: 0; + -webkit-appearance: none; + -moz-appearance: none; + appearance: none; + text-align: center; + line-height: inherit; + -webkit-text-decoration: none; + text-decoration: none; + padding-left: 16px; + padding-right: 16px; + padding-top: 8px; + padding-bottom: 8px; + color: var(--theme-ui-colors-white, #ffffff); + background-color: var(--theme-ui-colors-primary, #ec3750); + border: 0; + font-size: var(--font-2); + } + + button:focus, + button:hover { + box-shadow: var(--shadow-elevated); + transform: scale(1.0625); + } + + button.lg { + font-size: var(--font-3)!important; + line-height: var(--line-height-title); + padding-left: var(--spacing-4); + padding-right: var(--spacing-4); + padding-top: var(--spacing-3); + padding-bottom: var(--spacing-3); + } + + button.outline { + background: none; + color: var(--primary); + border: 2px solid currentcolor; + } + + button.cta { + font-size: var(--font-2); + background-image: radial-gradient( + ellipse farthest-corner at top left, + var(--orange), + var(--red) + ); + } + + .card { + background: var(--elevated); + color: var(--text); + border-radius: var(--radii-extra); + box-shadow: var(--shadow-card); + overflow: hidden; + } + + .card.sunken { + background: var(--sunken); + box-shadow: none; + } + + .card.interactive { + text-decoration: none; + -webkit-tap-highlight-color: transparent; + transition: transform 0.125s ease-in-out, box-shadow 0.125s ease-in-out; + } + + .card.interactive:hover, + .card.interactive:focus { + transform: scale(1.0625); + box-shadow: var(--shadow-elevated); + } + + input, + textarea, + select { + background: var(--elevated); + color: var(--text); + font-family: inherit; + border-radius: var(--radii-small); + border: 0; + font-size: inherit; + padding: var(--spacing-2); + -webkit-appearance: none; + -moz-appearance: none; + appearance: none; + } + + input::-webkit-input-placeholder, + input::-moz-placeholder, + input:-ms-input-placeholder, + textarea::-webkit-input-placeholder, + textarea::-moz-placeholder, + textarea:-ms-input-placeholder, + select::-webkit-input-placeholder, + select::-moz-placeholder, + select:-ms-input-placeholder { + color: var(--muted); + } + + input[type="search"]::-webkit-search-decoration, + textarea[type="search"]::-webkit-search-decoration, + select[type="search"]::-webkit-search-decoration { + display: none; + } + + input[type="checkbox"] { + -webkit-appearance: checkbox; + -moz-appearance: checkbox; + appearance: checkbox; + } + + label { + color: var(--text); + display: flex; + flex-direction: column; + text-align: left; + line-height: var(--line-height-caption); + font-size: var(--font-3); + } + + label.horizontal { + display: flex; + } + + .slider { + color: var(--primary); + } + + .form-hidden { + position: absolute; + height: 1px; + width: 1px; + clip: rect(1px, 1px, 1px, 1px); + white-space: nowrap; + } + + .container { + width: 100%; + margin: auto; + padding-left: var(--spacing-3); + padding-right: var(--spacing-3); + } + + h1 { + font-size: var(--font-5); + font-weight: var(--font-weight-bold); + line-height: var(--line-height-heading); + margin-top: 0; + margin-bottom: 0; + } + + h2 { + font-size: var(--font-4); + font-weight: var(--font-weight-bold); + line-height: var(--line-height-heading); + margin-top: 0; + margin-bottom: 0; + } + + h3 { + font-size: var(--font-3); + font-weight: var(--font-weight-bold); + line-height: var(--line-height-heading); + margin-top: 0; + margin-bottom: 0; + } + + h4 { + font-size: var(--font-2); + font-weight: var(--font-weight-bold); + line-height: var(--line-height-heading); + margin-top: 0; + margin-bottom: 0; + } + + h5 { + font-size: var(--font-1); + font-weight: var(--font-weight-bold); + line-height: var(--line-height-heading); + margin-top: 0; + margin-bottom: 0; + } + + h6 { + font-weight: var(--font-weight-bold); + line-height: var(--line-height-heading); + margin-top: 0; + margin-bottom: 0; + } + + p { + color: var(--text); + font-weight: var(--font-weight-body); + line-height: var(--line-height-body); + margin-top: var(--spacing-3); + margin-bottom: var(--spacing-3); + } + + img { + max-width: 100%; + } + + hr { + border: 0; + border-bottom: 1px solid var(--border); + } + + a { + color: var(--primary); + text-decoration: underline; + text-underline-position: under; + } + + a:focus, + a:hover { + text-decoration-style: wavy; + text-decoration-skip-ink: none; + } + + pre { + font-family: "SF Mono", "Roboto Mono", Menlo, Consolas, monospace; + font-size: var(--font-1); + padding: var(--spacing-3); + color: var(--text); + background: var(--sunken); + overflow: auto; + border-radius: var(--radii-default); + white-space: inherit; + } + + pre > code { + color: inherit; + margin-left: 0; + margin-right: 0; + padding-left: 0; + padding-right: 0; + } + + code { + font-family: "SF Mono", "Roboto Mono", Menlo, Consolas, monospace; + font-size: inherit; + color: var(--purple); + background: var(--sunken); + overflow: auto; + border-radius: var(--radii-small); + margin-left: var(--spacing-1); + margin-right: var(--spacing-1); + padding-left: var(--spacing-1); + padding-right: var(--spacing-1); + } + + p > code, + li > code { + color: var(--blue); + font-size: 0.875em; + } + + p > a > code, + li > a > code { + color: var(--blue); + font-size: 0.875em; + } + + li { + margin-top: var(--spacing-2); + margin-bottom: var(--spacing-2); + } + + table { + width: 100%; + margin-top: var(--spacing-4); + margin-bottom: var(--spacing-4); + border-collapse: separate; + border-spacing: 0; + } + + table > th, + table > td { + text-align: left; + padding: 4px; + padding-left: 0px; + border-color: var(--border); + border-bottom-style: solid; + } + + th { + vertical-align: bottom; + border-bottom-width: 2px; + } + + td { + vertical-align: top; + border-bottom-width: 1px; + } + + + @media screen and (min-width: 32em) { + .ultratitle { + font-size: var(--font-5); + } + .title { + font-size: var(--font-4); + } + .subtitle { + font-size: var(--font-2); + } + .eyebrow { + font-size: var(--font-3); + } + .lead { + font-size: var(--font-2); + margin-top: var(--spacing-2); + margin-bottom: var(--spacing-2); + } + .card { + padding: var(--spacing-3); + } + .container { + max-width: var(--size-layout); + } + .container.copy { + max-width: var(--size-copy); + } + .container.narrow { + max-width: var(--size-narrow); + } + } + + @media screen and (min-width: 48em) { + .ultratitle { + font-size: var(--font-6); + } + .title { + font-size: var(--font-5); + } + .subtitle { + font-size: var(--font-3); + } + .eyebrow { + font-size: var(--font-4); + } + .lead { + font-size: var(--font-3); + margin-top: var(--spacing-3); + margin-bottom: var(--spacing-3); + } + .card { + padding: var(--spacing-4); + } + } + + @media screen and (min-width: 64em) { + .ultratitle { + font-size: var(--font-7); + } + .title { + font-size: var(--font-6); + } + .container { + max-width: var(--size-layout-plus); + } + .container.wide { + max-width: var(--size-wide); + } + .container.copy { + max-width: var(--size-copy-plus); + } + .container.narrow { + max-width: var(--size-narrow-plus); + } + } + \ No newline at end of file diff --git a/hackclub-spotify-bot/src/spotify.js b/hackclub-spotify-bot/src/spotify.js index b444435..4caa082 100644 --- a/hackclub-spotify-bot/src/spotify.js +++ b/hackclub-spotify-bot/src/spotify.js @@ -1,199 +1,113 @@ -let token = null; -let authStuff = null; -const client_id = process.env.SPOTIFY_CLIENT_ID; -const client_secret = process.env.SPOTIFY_CLIENT_SECRET; -const redirect_uri = process.env.SPOTIFY_REDIRECT_URI; -async function fetchWebApi(endpoint, method, body) { - const res = await fetch(`https://api.spotify.com/${endpoint}`, { - headers: { - Authorization: `Bearer ${token}`, - }, - method, - body: JSON.stringify(body), - }); - const text = await res.text(); - // console.debug(text) - // abs nothing is wrong - return JSON.parse(text.trim()); -} - -function getLoginUrl() { - const state = generateRandomString(16); - const scope = [ - // "ugc-image-upload", - // "user-read-playback-state", - // "user-modify-playback-state", - // "user-read-currently-playing", - // "app-remote-control", - // "streaming", - "playlist-read-private", - "playlist-read-collaborative", - "playlist-modify-private", - "playlist-modify-public", - // "user-follow-modify", - // "user-follow-read", - // "user-read-playback-position", - // "user-top-read", - // "user-read-recently-played", - "user-library-modify", - // "user-library-read", - // "user-read-email", - "user-read-private", - ].join(" "); - - return ( - "https://accounts.spotify.com/authorize?" + - `response_type=code&grant_type=client_credentials&client_id=${client_id}&scope=${scope}&redirect_uri=${redirect_uri}&state=${state}` - ); -} - -function generateRandomString(length) { - let result = ""; - const characters = - "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; - const charactersLength = characters.length; - let counter = 0; - while (counter < length) { - result += characters.charAt(Math.floor(Math.random() * charactersLength)); - counter += 1; - } - return result; -} -async function refreshToken(refresh_token) { - try { - // var refresh_token = req.query.refresh_token; - const authOptions = { - url: "https://accounts.spotify.com/api/token", - headers: { - "content-type": "application/x-www-form-urlencoded", - Authorization: - "Basic " + - new Buffer.from(client_id + ":" + client_secret).toString("base64"), - }, - form: { - grant_type: "refresh_token", - refresh_token: refresh_token, - }, - json: true, - }; - const formdm = new URLSearchParams(); - - formdm.append("grant_type", "refresh_token"); - formdm.append("refresh_token", refresh_token); - - fetch(authOptions.url, { - body: formdm, - headers: authOptions.headers, - method: "POST", - }) - .then(async (r) => { - const text = await r.text(); - // console.log(text); - return JSON.parse(text); - }) - .then((auth) => { - if (!auth.refresh_token) auth.refresh_token = refresh_token; - // console.log(auth); - authStuff = auth; - token = auth.access_token; - saveCredentials(auth); - if (auth.expires_in) { - setTimeout(() => { - refreshToken(auth.refresh_token); - }, auth.expires_in * 1000); - } - }); - } catch (e) { - console.error(`Welp it broke`); - // try again asap because we NEED THAT TOKEN - refreshToken(refresh_token); - } -} -function saveCredentials(creds) { - require("fs").writeFileSync( - "data/credentials.json", - JSON.stringify(creds, null, 2), - ); -} -function getCredentials() { - try { - return JSON.parse( - require("fs").readFileSync("data/credentials.json", "utf8"), - ); - } catch (e) { - return null; - } -} -function spotifyRoutes(app) { - app.get("/spotify/callback", async (req, res) => { - const code = req.query.code || null; - const state = req.query.state || null; - - if (state === null) { - res.redirect( - "/#" + - querystring.stringify({ - error: "state_mismatch", - }), - ); - } else { - const authOptions = { - url: "https://accounts.spotify.com/api/token", - form: { - code: code, - redirect_uri: redirect_uri, - grant_type: "authorization_code", - }, - headers: { - "content-type": "application/x-www-form-urlencoded", - Authorization: - "Basic " + - new Buffer.from(client_id + ":" + client_secret).toString("base64"), - }, - json: true, - }; - const formdm = new URLSearchParams(); - // Object.entries(authOptions.form).forEach(([key, value]) => { - // formdm.append(key, value); - // }) - formdm.append("code", code); - formdm.append("redirect_uri", redirect_uri); - formdm.append("grant_type", "authorization_code"); - - fetch(authOptions.url, { - body: formdm, - headers: authOptions.headers, - method: "POST", - }) - .then((r) => r.json()) - .then((auth) => { - // console.log(auth); - authStuff = auth; - saveCredentials(auth); - token = auth.access_token; - if (auth.expires_in) { - setTimeout(() => { - refreshToken(auth.refresh_token); - }, auth.expires_in * 1000); - } - res.status(200).end("Successfully logged in!"); - }); - } - }); -} -function addSongToPlaylist(url) { - fetchWebApi("v1/playlists/3gRv97fvllFFLVdCH6XzsE/tracks", "POST", { - uris: [url], - position: 0, - }); -} -module.exports = { - getLoginUrl, - refreshToken, - saveCredentials, - getCredentials, - spotifyRoutes, - addSongToPlaylist, - - // getToken -}; + +let token = null; +let authStuff = null; +const client_id = process.env.SPOTIFY_CLIENT_ID; +const client_secret = process.env.SPOTIFY_CLIENT_SECRET; +const redirect_uri = process.env.SPOTIFY_REDIRECT_URI; +async function fetchWebApi(endpoint, method, body) { + const res = await fetch(`https://api.spotify.com/${endpoint}`, { + headers: { + Authorization: `Bearer ${token}`, + }, + method, + body: JSON.stringify(body), + }); + const text = await res.text(); + // console.debug(text) + // abs nothing is wrong + return JSON.parse(text.trim()); + } + + function getLoginUrl() { + const state = generateRandomString(16); + const scope = [ + "ugc-image-upload", + "user-read-playback-state", + "user-modify-playback-state", + "user-read-currently-playing", + "app-remote-control", + "streaming", + "playlist-read-private", + "playlist-read-collaborative", + "playlist-modify-private", + "playlist-modify-public", + "user-follow-modify", + "user-follow-read", + "user-read-playback-position", + "user-top-read", + "user-read-recently-played", + "user-library-modify", + "user-library-read", + "user-read-email", + "user-read-private", + ].join(" "); + + return ( + "https://accounts.spotify.com/authorize?" + + `response_type=code&grant_type=client_credentials&client_id=${client_id}&scope=${scope}&redirect_uri=${redirect_uri}&state=${state}` + ); + } + + function generateRandomString(length) { + let result = ""; + const characters = + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; + const charactersLength = characters.length; + let counter = 0; + while (counter < length) { + result += characters.charAt(Math.floor(Math.random() * charactersLength)); + counter += 1; + } + return result; + } + async function refreshToken(refresh_token) { + try { + // var refresh_token = req.query.refresh_token; + const authOptions = { + url: "https://accounts.spotify.com/api/token", + headers: { + "content-type": "application/x-www-form-urlencoded", + Authorization: + "Basic " + + new Buffer.from(client_id + ":" + client_secret).toString("base64"), + }, + form: { + grant_type: "refresh_token", + refresh_token: refresh_token, + }, + json: true, + }; + console.log(authOptions); + const formdm = new URLSearchParams(); + + formdm.append("grant_type", "refresh_token"); + formdm.append("refresh_token", refresh_token); + + fetch(authOptions.url, { + body: formdm, + headers: authOptions.headers, + method: "POST", + }) + .then(async (r) => { + const text = await r.text(); + console.log(text); + return JSON.parse(text); + }) + .then((auth) => { + if (!auth.refresh_token) auth.refresh_token = refresh_token; + console.log(auth); + authStuff = auth; + token = auth.access_token; + if (auth.expires_in) { + setTimeout(() => { + refreshToken(auth.refresh_token); + }, auth.expires_in * 1000); + } + }); + } catch (e) { + console.error(`Welp it broke`); + // try again asap because we NEED THAT TOKEN + refreshToken(refresh_token); + } + } + \ No newline at end of file diff --git a/hackclub-spotify-bot/src/views/home.ejs b/hackclub-spotify-bot/src/views/home.ejs deleted file mode 100644 index e3d4601..0000000 --- a/hackclub-spotify-bot/src/views/home.ejs +++ /dev/null @@ -1,62 +0,0 @@ - - -
- - -