Andrew vor 3 Wochen
Commit
a29b5859fd
69 geänderte Dateien mit 3360 neuen und 0 gelöschten Zeilen
  1. 6 0
      .gitignore
  2. 80 0
      README.md
  3. 150 0
      cli.js
  4. 99 0
      cli.md
  5. 636 0
      index.js
  6. 1 0
      ipbans.json
  7. 22 0
      package.json
  8. 1 0
      sunset.json
  9. 21 0
      user-migration.js
  10. 225 0
      utils.js
  11. 20 0
      views/video.ejs
  12. 37 0
      www/404.html
  13. BIN
      www/assets/404.png
  14. BIN
      www/assets/badong.mp3
  15. BIN
      www/assets/calldisconnect.mp3
  16. BIN
      www/assets/dislike.jpg
  17. BIN
      www/assets/johnpork-in-call.jpg
  18. BIN
      www/assets/johnpork.jpg
  19. BIN
      www/assets/like.jpg
  20. BIN
      www/assets/loading.gif
  21. BIN
      www/assets/logo.png
  22. BIN
      www/assets/piss baby.mp4
  23. BIN
      www/assets/ringtone.mp3
  24. BIN
      www/assets/troll/pomni sigma sigma GYAAAAAAAAAAAAAAAAAAAT.png
  25. BIN
      www/assets/troll/pomni sigma sigma bisexual.png
  26. BIN
      www/assets/troll/pomni sigma sigma chica gorlock.png
  27. BIN
      www/assets/troll/pomni sigma sigma chica on ozempic.png
  28. BIN
      www/assets/troll/pomni sigma sigma crocodile feet.png
  29. BIN
      www/assets/troll/pomni sigma sigma drake yellow guy.png
  30. BIN
      www/assets/troll/pomni sigma sigma feet 2.png
  31. BIN
      www/assets/troll/pomni sigma sigma feet 3.png
  32. BIN
      www/assets/troll/pomni sigma sigma feet 4.png
  33. BIN
      www/assets/troll/pomni sigma sigma feet.png
  34. BIN
      www/assets/troll/pomni sigma sigma five nights of bulking.png
  35. BIN
      www/assets/troll/pomni sigma sigma last bite was in 87.png
  36. BIN
      www/assets/troll/pomni sigma sigma molestable plushie.png
  37. BIN
      www/assets/troll/video.mp4
  38. BIN
      www/assets/user.png
  39. BIN
      www/assets/verified.png
  40. BIN
      www/assets/website.png
  41. 48 0
      www/contact.html
  42. 9 0
      www/contact.js
  43. 33 0
      www/down.html
  44. 45 0
      www/editProfile.css
  45. 25 0
      www/editProfile.html
  46. 34 0
      www/editProfile.js
  47. BIN
      www/favicon.ico
  48. BIN
      www/favicon.png
  49. 1 0
      www/google98c219521c7c5d1f.html
  50. 90 0
      www/index.html
  51. 199 0
      www/index.js
  52. 41 0
      www/login.css
  53. 33 0
      www/login.html
  54. 47 0
      www/login.js
  55. 15 0
      www/manifest.json
  56. 10 0
      www/robots.txt
  57. 44 0
      www/search.html
  58. 85 0
      www/search.js
  59. 29 0
      www/sitemap.xml
  60. 486 0
      www/style.css
  61. 77 0
      www/sunset.html
  62. 47 0
      www/upload.css
  63. 39 0
      www/upload.html
  64. 49 0
      www/upload.js
  65. 90 0
      www/user.html
  66. 123 0
      www/user.js
  67. 52 0
      www/utils.js
  68. 104 0
      www/video.html
  69. 207 0
      www/video.js

+ 6 - 0
.gitignore

@@ -0,0 +1,6 @@
+node_modules/
+.env
+package-lock.json
+videos/*
+yarn.lock
+users.json

+ 80 - 0
README.md

@@ -0,0 +1,80 @@
+
+![Logo]()
+
+
+# SkibidiHub
+
+[![MIT License](https://img.shields.io/badge/License-MIT-green.svg)](https://choosealicense.com/licenses/mit/) 
+
+![This site is certified skibidi sigma](https://img.shields.io/badge/this_site_is_certified-skibidi_sigma-blue)
+
+
+SkibidiHub is a video streaming platform, it's mainly a shitpost website but I built it to develop my frontend and backend skills.
+
+
+## Roadmap
+
+ - Admin Panel
+ - ~~SkibidiHub Shorts (or any other name it doesn't really matter)~~
+
+## Deployment
+
+First off, you need to have a .env file with your supabase API keys and a discord webhook for updates:
+
+```
+SUPABASE_URL=""
+SUPABASE_KEY=""
+WEBHOOK_URL=""
+LOG_WEBHOOK_URL=""
+```
+
+For supabase you need to have three tables (RLS is disabled for every table, because I couldn't be bothered):
+
+``videos``:
+
+| ID          | Type        | Default Value | Primary | Nullable |
+|-------------|-------------|---------------|---------|----------|
+| id          | text        |               | ✅      | ❌      |
+| uploaded_at | timestamptz | now()         | ❌      | ❌      |
+| likes       | int8        | 0             | ❌      | ❌      |
+| dislikes    | int8        | 0             | ❌      | ❌      |
+| description | text        |               | ❌      | ✅      |
+| uploader    | text        |               | ❌      | ❌      |
+| title       | text        |               | ❌      | ❌      |
+
+``comments``:
+
+| ID          | Type        | Default Value | Primary | Nullable |
+|-------------|-------------|---------------|---------|----------|
+| id          | int8        |               | ✅      | ❌      |
+| created_at  | timestamptz | now()         | ❌      | ❌      |
+| commenter   | text        |               | ❌      | ❌      |
+| video_id    | text        |               | ❌      | ❌      |
+| text        | text        |               | ❌      | ❌      |
+
+``users``:
+
+| ID              | Type        | Default Value | Primary | Nullable |
+|-----------------|-------------|---------------|---------|----------|
+| name            | text        |               | ✅      | ❌      |
+| subscribers     | int8        | 0             | ❌      | ❌      |
+| social_score    | int8        | 0             | ❌      | ❌      |
+| description     | text        |               | ❌      | ✅      |
+| website         | text        |               | ❌      | ✅      |
+| verified        | bool        | false         | ❌      | ❌      |
+
+Once you've setup all of that, run these commands:
+
+```bash
+  git clone https://git.buttplugstudios.xyz/andrew/skibidihub
+  cd skibidi-hub
+  npm i
+
+  # I like to use pm2 to manage websites like this:
+  npm i -g pm2
+  pm2 start index.js --name skibidi-hub --log latest.log
+
+  # OR without pm2
+  node index.js
+```
+

+ 150 - 0
cli.js

@@ -0,0 +1,150 @@
+// SkibidiHub Administration CLI //
+
+require("dotenv").config();
+const supabase = require("@supabase/supabase-js");
+const fs = require("node:fs");
+const path = require("node:path");
+const utils = require("./utils.js");
+const client = supabase.createClient(
+    process.env.SUPABASE_URL,
+    process.env.SUPABASE_KEY
+);
+
+// Get process arguments
+const args = process.argv;
+args.shift();
+args.shift();
+
+async function main() {
+    let ips;
+    switch(args[0]) {
+        case "help":
+            console.log("Valid commands are:\n\ndeleteVideo [id]\nban [ip]\nunban [ip]\ncleanDatabase\ncleanDrive\nupdateSocialScore [user] [-300]\nverifyUser [user]\ndeVerifyUser [user]");
+            break;
+        case "ban":
+            if(!args[1]) return console.log("you need to provide an ip address to ban!");
+            ips = JSON.parse(fs.readFileSync("./ipbans.json"));
+            ips.push(args[1]);
+            fs.writeFileSync("./ipbans.json", JSON.stringify(ips), {encoding:'utf8', flag:'w'})
+            console.log("Successfully banned ip.");
+            break;
+        case "unban":
+            if(!args[1]) return console.log("you need to provide an ip address to unban!");
+            ips = JSON.parse(fs.readFileSync("./ipbans.json"));
+            if(!ips.includes(args[1])) return console.log("the ip address provided isnt banned.");
+            ips.splice(ips.indexOf(args[1]), 1)
+            fs.writeFileSync("./ipbans.json", JSON.stringify(ips), {encoding:'utf8', flag:'w'})
+            console.log("Successfully unbanned ip.");
+            break;
+        case "deleteVideo":
+            if(!args[1]) return console.log("you need to provide a video id to delete!");
+            if(!utils.videoExists(args[1])) return console.log("the video you are trying to delete doesnt exist!");
+            
+            await deleteVideo(args[1]);
+    
+            break;
+        case "cleanDatabase":
+            client.from("videos").select("id").then(data => {
+                data.data.forEach(async (video) => {
+                    if(!utils.videoExists(video.id)) {
+                        console.log(`Video with ID ${video.id} doesn't exist on drive. Erasing it on the database...`)
+                        await client.from("videos").delete().eq("id", video.id);
+                    }
+                });
+            })
+            break;
+        case "cleanDrive":
+            if(args[1] != "confirm") {
+                console.log("This should only be run on a BROKEN videos folder from a DEVELOPMENT environment.")
+                console.log("If you run this, you WILL destroy some videos which you can NOT undo.");
+                console.log("You must also run cleanDatabase before running this.");
+                console.log("to confirm, run node cli.js cleanDrive confirm.");
+                return;
+            }
+    
+            const videos = fs.readdirSync(path.join(__dirname, "videos"));
+            videos.forEach(async (id) => {
+                const videoPath = path.join(__dirname, path.join("videos", id));
+                if(!fs.existsSync(path.join(videoPath, "video.mp4"))) return await deleteVideo(id);
+                if(!fs.existsSync(path.join(videoPath, "thumbnail.jpg"))) return await deleteVideo(id);
+            })
+            break;
+        case "updateSocialScore":
+            if(!args[1]) return console.log("Please Provide a user to update the social credit score of.");
+            if(!args[2]) return console.log("Please provide a value to update the social credit score.");
+            if(!utils.userExists(args[1])) return console.log("User doesn't exist!");
+            
+            const socialScoreData = await client
+            .from("users")
+            .select("social_score")
+            .eq("name", args[1]);
+        
+            const socialScore = parseInt(socialScoreData["data"][0]["social_score"]) + parseInt(args[2]);
+
+            await client.from("users").update({"social_score": socialScore}).eq("name", args[1]).then(data => {
+                if(data.error) {
+                    return console.error(data);
+                } else if(data.status != 204) {
+                    return console.error(data);
+                }
+
+                return console.log("Updated successfully.");
+            });
+
+            break;
+        case "verifyUser":
+            if(!args[1]) return console.log("Please provide a user to verify.");
+            if(!utils.userExists(args[1])) return console.log("User doesn't exist!");
+
+            await client.from("users").update({"verified": "TRUE"}).eq("name", args[1]).then(data => {
+                if(data.error) {
+                    return console.error(data);
+                } else if(data.status != 204) {
+                    return console.error(data);
+                }
+
+                return console.log("Verified successfully.");
+            });
+            break;
+        case "deVerifyUser":
+            if(!args[1]) return console.log("Please provide a user to verify.");
+            if(!utils.userExists(args[1])) return console.log("User doesn't exist!");
+
+            await client.from("users").update({"verified": "FALSE"}).eq("name", args[1]).then(data => {
+                if(data.error) {
+                    return console.error(data);
+                } else if(data.status != 204) {
+                    return console.error(data);
+                }
+
+                return console.log("Deverified successfully.");
+            });
+            break;
+        case "sunset":
+            if(!args[1]) return console.log("please provide a unix timestamp for when you want skibidihub to explode");
+            const sunset = path.join(__dirname, "sunset.json")
+            const json = JSON.parse(fs.readFileSync(sunset))
+            json.sunset = true
+            json.timestamp = args[1]
+            const text = JSON.stringify(json)
+            fs.writeFileSync(sunset, text);
+            break;
+        default:
+            console.log("SkibidiHub Administration CLI\n\nPlease enter a valid command.")
+            break;
+    }    
+}
+
+async function deleteVideo(id) {
+    console.log(`Deleting video with the id ${id}...`);
+
+    // Delete video on hard drive
+    if(fs.existsSync(path.join(__dirname, path.join("videos", path.join(id, "video.mp4"))))) fs.unlinkSync(path.join(__dirname, path.join("videos", path.join(id, "video.mp4"))));
+    if(fs.existsSync(path.join(__dirname, path.join("videos", path.join(id, "thumbnail.jpg"))))) fs.unlinkSync(path.join(__dirname, path.join("videos", path.join(id, "thumbnail.jpg"))));
+    fs.rmSync(path.join(__dirname, path.join("videos", id)), { recursive: true, force: true });
+
+    // Delete video in the database
+    await client.from("videos").delete().eq("id", id);
+}
+
+main();

+ 99 - 0
cli.md

@@ -0,0 +1,99 @@
+
+# CLI Docs
+
+SkibidiHub has an administration tool called ``cli.js``.
+
+To run ``cli.js``, change into the main directory where you installed skibidihub and run: 
+
+```
+node cli.js [command]
+```
+
+Available commands:
+
+### deleteVideo [id]
+
+Deletes a video from the database and from the drive.
+Example usage:
+
+```
+node cli.js deleteVideo OaOskc
+```
+
+### ban
+
+Bans an IP address. 
+Example usage:
+
+```
+node cli.js ban XXX.XXX.XXX.XXX
+```
+
+### unban
+
+Unbans an IP address.
+Example usage:
+
+```
+node cli.js unban XXX.XXX.XXX.XXX
+```
+
+### cleanDatabase
+
+This command checks every video in the database, and if it doesn't exist on the drive it deletes the video from the database.
+Example usage:
+
+```
+node cli.js cleanDatabase
+```
+
+### cleanDrive
+
+This command checks every video on the drive to have both video.mp4 and thumbnail.jpg, otherwise it erases the video.
+This command is DESTRUCTIVE and you cannot undo it's actions. I added this command to help with managing my test instance.
+Example usage:
+
+```
+node cli.js cleanDrive
+```
+
+### updateSocialScore
+
+This command takes a users social score and adds whatever value you give it.
+Example usage:
+
+```
+# Decreases penguins1's social score by 300
+node cli.js updateSocialScore penguins1 -300
+
+# Increases penguins1's social score by 300
+node cli.js updateSocialScore penguins1 300
+```
+
+### verifyUser
+
+This command makes a user verified, which is self explanatory if you've ever touched a social media before.
+Example usage:
+
+```
+node cli.js verifyUser penguins1
+```
+
+### deVerifyUser
+
+This command undoes a users verification if you fucked up or you're mad at someone.
+Example usage:
+
+```
+node cli.js deVerifyUser penguins1
+```
+
+### help
+
+Not a very helpful command.
+Example usage:
+
+```
+node cli.js help
+```
+

+ 636 - 0
index.js

@@ -0,0 +1,636 @@
+const express = require("express");
+const fs = require("node:fs");
+const path = require("node:path");
+const supabase = require("@supabase/supabase-js");
+const cookie_parser = require("cookie-parser");
+const multer = require("multer");
+const app = express();
+require("dotenv").config();
+
+const webhookURL = process.env.WEBHOOK_URL;
+const utils = require("./utils.js");
+const url = "https://skibidihub.buttplugstudios.xyz"
+
+let ipBlacklist = JSON.parse(fs.readFileSync("./ipbans.json"));
+fs.watch("./ipbans.json", () => {
+  console.log("Reloading IP Bans...");
+  ipBlacklist = JSON.parse(fs.readFileSync("./ipbans.json"));
+});
+
+const port = 3000;
+const storage = multer.diskStorage({
+  destination: function (req, file, cb) {
+    try {
+      // Validations
+      if (!utils.checkBodyVideo(req.body)) throw new Error("invalid video body");
+      if (!utils.checkToken(req, "/api/upload multer")) throw new Error("unauthorized");
+
+      if(!req.skibidihub_id) req.skibidihub_id = utils.nanoid(7);
+      console.log("Video ID:", req.skibidihub_id);
+
+      // Check if the directory exists, and create it if it doesn't
+      const dir = path.join(__dirname, "videos", req.skibidihub_id);
+      if (!utils.videoExists(req.skibidihub_id)) {
+        console.log("Directory does not exist, creating:", dir);
+        fs.mkdirSync(dir, { recursive: true });
+      }
+
+      // Set the destination path for multer
+      cb(null, dir);
+    } catch (err) {
+      console.log("Error in destination function:", err);
+      cb(err);
+    }
+  },
+  filename: function (req, file, cb) {
+    let filename;
+    if (file.fieldname === "video") {
+      filename = "video.mp4";
+    } else if (file.fieldname === "thumbnail") {
+      filename = "thumbnail.jpg";
+    }
+    console.log("Saving file with filename:", filename);
+    cb(null, filename);
+  }
+});
+
+const upload = multer({ 
+  storage: storage,
+  limits: { fileSize: 1000000 * 250 /* 250MB in bytes */ }
+});
+
+const client = supabase.createClient(
+  process.env.SUPABASE_URL,
+  process.env.SUPABASE_KEY
+);
+
+const ipwareObject = require("@fullerstack/nax-ipware");
+const sunset = JSON.parse(fs.readFileSync(path.join(__dirname, "sunset.json")));
+const ipware = new ipwareObject.Ipware();
+app.use(function(req, res, next) {
+  req.ipInfo = ipware.getClientIP(req);
+
+  if (new Date().getTime() >= sunset.timestamp && sunset.sunset == true) {
+    if(req.path == "/assets/piss baby.mp4") {
+      if (ipBlacklist.includes(req.ipInfo.ip)) {
+        res.setHeader("Cache-Control", "no-cache");
+        return res.sendFile(path.join(__dirname, path.join("www", "down.html")));
+      }      
+    } else {
+    return res.sendFile(path.join(__dirname, path.join("www", "sunset.html")));
+    }
+  }
+
+  if(ipBlacklist.includes(req.ipInfo.ip)) {
+    res.setHeader("Cache-Control", "no-cache");
+    return res.sendFile(path.join(__dirname, path.join("www", "down.html")));
+  }
+
+  next();
+});
+
+app.use(express.static("www")); // Static folder
+app.use(express.json()); // JSON body parser
+app.use(cookie_parser()); // Cookie parser
+
+// User facing URL's
+app.get("/", (req, res) => {
+  res.sendFile(path.join(__dirname, path.join("www", "index.html")));
+});
+
+app.get("/login", (req, res) => {
+  res.sendFile(path.join(__dirname, path.join("www", "login.html")));
+});
+
+app.get("/upload", (req, res) => {
+  res.sendFile(path.join(__dirname, path.join("www", "upload.html")));
+})
+
+app.get("/contact", (req, res) => {
+  res.sendFile(path.join(__dirname, path.join("www", "contact.html")));
+})
+
+app.get("/editProfile", (req, res) => {
+  res.sendFile(path.join(__dirname, path.join("www", "editProfile.html")));
+});
+
+app.get("/search", (req, res) => {
+  res.sendFile(path.join(__dirname, path.join("www", "search.html")));
+});
+
+app.set('view engine', 'ejs');
+app.get("/video/:id", async (req, res) => {
+  if(utils.discordCheck(req) && utils.videoExists(req.params.id)) {
+    const videoInfo = await utils.videoInfo(req.params.id);
+    if(isNaN(videoInfo)) {
+      return res.render("video", {
+        video: `${url}/api/video/${req.params.id}.mp4`,
+        video_name: videoInfo.title,
+        description: videoInfo.description,
+        url: url,
+        author_url: `${url}/api/oembed/?author_name=${videoInfo.uploader}&author_url=${url}/user/${encodeURIComponent(videoInfo.uploader)}`
+      })
+    }
+  }
+
+  if(!utils.videoExists(req.params.id) && utils.checkToken(req, "/video/:id")) {
+    return res.sendFile(path.join(__dirname, path.join("www", "404.html")));
+  }
+
+  res.sendFile(path.join(__dirname, path.join("www", "video.html")));
+});
+
+app.get("/user/:user", (req, res) => {
+  if(!utils.checkToken(req, "/user/:user")) {
+    return res.sendFile(path.join(__dirname, path.join("www", "user.html")));
+  }
+
+  // Check if user exists
+  client.from("users").select().eq("name", decodeURIComponent(req.params.user)).then(data => {
+    if(data.error) {
+      return res.sendFile(path.join(__dirname, path.join("www", "404.html")));
+    } else if (data.status != 200) {
+      return res.sendFile(path.join(__dirname, path.join("www", "404.html")));
+    }
+
+    if(!data.data[0]) return res.sendFile(path.join(__dirname, path.join("www", "404.html")));
+    res.sendFile(path.join(__dirname, path.join("www", "user.html")));
+  })
+});
+
+// API //
+
+function handleVideoAPI(req, res) {
+  if(!utils.checkToken(req, "/api/video/:id") && !utils.discordCheck(req)) {
+    return res.sendFile(
+      path.join(
+        __dirname,
+        path.join("www", path.join("assets", path.join("troll", "video.mp4")))
+      )
+    );
+  }
+
+  const videoPath = path.join(__dirname, path.join("videos", path.join(req.params.id, "video.mp4")));
+  if(!fs.existsSync(videoPath)) return res.sendStatus(404);
+
+  const stat = fs.statSync(videoPath);
+  const fileSize = stat.size;
+  const range = req.headers.range;
+
+  console.log('Requested Range:', range); // Log the range for debugging
+
+  if (range) {
+    const parts = range.replace(/bytes=/, "").split("-");
+    let start = parseInt(parts[0], 10);
+    let end = parts[1] ? parseInt(parts[1], 10) : fileSize - 1;
+
+    // Handle the case where range is 0-1
+    if (start === 0 && end === 1) {
+      end = 1;
+    }
+
+    const chunksize = (end - start) + 1;
+    const file = fs.createReadStream(videoPath, {start, end});
+    const head = {
+      'Content-Range': `bytes ${start}-${end}/${fileSize}`,
+      'Accept-Ranges': 'bytes',
+      'Content-Length': chunksize,
+      'Content-Type': 'video/mp4',
+    };
+    res.writeHead(206, head);
+    file.pipe(res);
+  } else {
+    const head = {
+      'Content-Length': fileSize,
+      'Content-Type': 'video/mp4',
+      'Accept-Ranges': 'bytes',
+    };
+    res.writeHead(200, head);
+    fs.createReadStream(videoPath).pipe(res);
+  }
+}
+
+// Get an mp4 file according to its video ID.
+app.get('/api/video/:id*.mp4', handleVideoAPI);
+app.get("/api/video/:id", handleVideoAPI);
+
+// OEmbed
+const oembed = (provider_name, provider_url, author_name, author_url, url) => {
+  const baseObject = {
+      version: '1.0',
+  };
+  if (provider_name && provider_url) {
+      baseObject.provider_name = provider_name;
+      baseObject.provider_url = provider_url;
+  }
+  if (author_name) {
+      baseObject.author_name = author_name;
+  }
+  if (author_url) {
+      baseObject.author_url = author_url;
+  }
+  if (url) {
+      baseObject.url = url;
+  }
+  return baseObject;
+};
+
+app.get('/api/oembed', (req, res) => {
+  const {
+      provider_name,
+      provider_url,
+      author_name,
+      author_url,
+      url,
+  } = req.query;
+  return res.status(200).send(oembed(provider_name, provider_url, author_name, author_url, url));
+});
+
+// Get a videos thumbnail according to its video ID.
+app.get("/api/thumbnail/:id", (req, res) => {
+  if (!utils.checkToken(req, "/api/thumbnail/:id")) {
+    let images = fs.readdirSync(
+      path.join(__dirname, path.join("www", path.join("assets", "troll")))
+    );
+    res.sendFile(
+      path.join(
+        __dirname,
+        path.join(
+          "www",
+          path.join(
+            "assets",
+            path.join("troll", images[utils.getRandomInt(images.length - 1)])
+          )
+        )
+      )
+    );
+    return;
+  }
+
+  const thumbnail = utils.getThumbnail(req.params.id);
+  if(thumbnail) return res.sendFile(thumbnail);
+  if(!thumbnail) return res.sendStatus(404);
+});
+
+// Get a videos thumbnail according to its video ID.
+app.get("/api/webhookThumbnail/:id", (req, res) => {
+  const thumbnail = utils.getThumbnail(req.params.id);
+  if(thumbnail) return res.sendFile(thumbnail);
+  if(!thumbnail) return res.sendStatus(404);
+});
+
+// Get the info for a video according to its video ID.
+app.get("/api/videoInfo/:id", async (req, res) => {
+  if(!utils.checkToken(req, "/api/videoInfo/:id")) {
+    let newData = {};
+    newData.title = utils.fakeTitleList[utils.getRandomInt(utils.fakeTitleList.length)]
+    newData.description = "SIGN IN to see this EPIC content"
+    newData.likes = "69"
+    newData.dislikes = "0"
+    newData.uploader = "SIGN IN to see this EPIC content"
+    newData.uploaded_at = new Date().toISOString().toString();
+    return res.send(newData);
+  }
+
+  if(!utils.videoExists(req.params.id)) return res.sendStatus(404);
+
+  const videoInfo = await utils.videoInfo(req.params.id);
+  if(isNaN(videoInfo)) {
+    return res.send(videoInfo);
+  } else {
+    return res.sendStatus(videoInfo);
+  }
+});
+
+// Pulls the user information from the database and returns it.
+app.get("/api/userInfo/:id", (req, res) => {
+  if(!utils.checkToken(req, "/api/userInfo/:id")) {
+    let data = {
+      name: "SIGN IN to see this EPIC CONTENT",
+      subscribers: 999,
+      social_score: 999,
+      description: "SIGN IN to see this EPIC content",
+      verified: true
+    }
+
+    return res.send(data);
+  }
+
+
+  client.from("users").select().eq("name", decodeURIComponent(req.params.id)).then(data => {
+    if(data.error) {
+      return res.sendStatus(400);
+    } else if (data.status != 200) {
+      return res.sendStatus(data.status);
+    }
+
+    if(!data.data[0]) return res.sendStatus(400);
+    return res.send(data.data[0]);
+  })
+})
+
+// Subscribes to a user
+app.get("/api/subscribe/:id", async (req, res) => {
+  if(!utils.checkToken(req)) return res.sendStatus(401);
+  if(!utils.userExists(decodeURIComponent(req.params.id))) return res.sendStatus(400);
+
+  const subscribersData = await client
+    .from("users")
+    .select("subscribers")
+    .eq("name", decodeURIComponent(req.params.id));
+
+  const subscribers = subscribersData["data"][0]["subscribers"] + 1;
+
+  client.from("users").update({
+    subscribers: subscribers
+  }).eq("name", decodeURIComponent(req.params.id)).then(data => {
+    if(data.error) {
+      return res.sendStatus(400);
+    } else if (data.status != 204) {
+      return res.sendStatus(data.status);
+    }
+
+    return res.sendStatus(200);
+  })
+});
+
+// Login endpoints (Just handles user creation)
+app.post("/api/login", async (req, res) => {
+  if(!req.body.user) return res.sendStatus(400);
+  const ipInfo = ipware.getClientIP(req);
+
+  // If user doesn't exist, create the user.
+  if(!await utils.userExists(req.body.user)) {
+    console.log(`User ${req.body.user} doesn't exist. Creating the user...`);
+    await client.from("users").insert({
+      name: req.body.user
+    }).then(data => {
+      if(data.error) {
+        return res.status(500).send(data);
+      } else if(data.status != 201) {
+        return res.status(500).send(data);
+      }
+
+      console.log(`User created successfully. IP: ${ipInfo.ip}`);
+      return res.sendStatus(200);
+    });
+  } else {
+    console.log(`User ${req.body.user} already exists. Logging in... IP: ${ipInfo.ip}`);
+    return res.sendStatus(200);
+  }
+});
+
+app.post("/api/editUser", async (req, res) => {
+  // Validate request
+  const user = utils.checkUserToken(req, "/api/editUser");
+  if(!user.value) return res.status(401).json({message: "you need to login to edit your user page"});
+  if(!req.body.description && !req.body.website) return res.status(400).json({message: "invalid form data"});
+  if(req.body.website && !utils.isValidUrl(req.body.website)) return res.status(400).json({message: "invalid website"});
+  
+  // Update user
+  await client.from("users").update({
+    description: req.body.description,
+    website: req.body.website,
+  }).eq("name", user.user).then(data => {
+    if(data.error) {
+      return res.status(500).send(data);
+    } else if(data.status != 204) {
+      return res.status(500).send(data);
+    }
+
+    return res.sendStatus(200);
+  })
+});
+
+// Get the comments for a video according to its video ID.
+app.get("/api/comments/:videoID", (req, res) => {
+  if(!utils.checkToken(req, "/api/comments/:videoID")) {
+    let comments = [];
+    for(let i = 0; i < 8; i++) {
+      let comment = {};
+      comment.text = utils.fakeCommentList[utils.getRandomInt(utils.fakeCommentList.length)]
+      comment.commenter = "SIGN IN to see this EPIC content!";
+      let date = new Date();
+      date.setTime(1005286084);
+      comment.created_at = date.toISOString();
+      comments.push(comment);
+    }
+
+    return res.send(comments);
+  }
+
+  if(!utils.videoExists(req.params.videoID)) return res.sendStatus(404);
+
+  client
+    .from("comments")
+    .select()
+    .eq("video_id", req.params.videoID)
+    .then((data) => {
+      if (data.error) {
+        res.sendStatus(400);
+      } else if (data.status != 200) {
+        res.sendStatus(data.status);
+      }
+      
+      res.send(data["data"]);
+    });
+});
+
+// Get a list of all videos
+app.get("/api/getAllVideos", (req, res) => {
+  if(!utils.checkToken(req, "/api/getAllVideos/")) {
+    let newData = [];
+    for(let i = 0; i < 12; i++) {
+      let temp = {};
+      temp.description = "SIGN IN to see this EPIC content"
+      temp.title = utils.fakeTitleList[utils.getRandomInt(utils.fakeTitleList.length)]
+      temp.uploader = "SIGN IN to see this EPIC content";
+      temp.id = utils.nanoid(7);
+      newData.push(temp);
+    }
+
+    return res.send(newData);
+  }
+
+  client
+    .from("videos")
+    .select()
+    .then((data) => {
+      if (data.error) {
+        return res.sendStatus(400);
+      } else if (data.status != 200) {
+        return res.sendStatus(data.status);
+      }
+
+      return res.send(data["data"]);
+    });
+});
+
+// Search endpoint
+app.post("/api/search", (req, res) => {
+  if(!utils.checkToken(req)) return res.sendStatus(401);
+  if(!req.body.query) return res.sendStatus(400);
+  client
+  .from("videos")
+  .select()
+  .then((response) => {
+    if (response.error) {
+      return res.sendStatus(400);
+    } else if (response.status != 200) {
+      return res.sendStatus(response.status);
+    }
+
+    let results = [];
+    response.data.forEach(video => {
+      if(video.title.startsWith(req.body.query.toLowerCase()) || video.uploader.startsWith(req.body.query.toLowerCase())) results.push(video);
+    })
+
+    return res.send(results);
+  });
+})
+
+// Send a comment
+app.post("/api/comment", async (req, res) => {
+  if (!utils.checkToken(req, "/api/comment")) return res.sendStatus(401);
+  
+  // Sanity chekcs
+  if(!req.body.commenter) return res.sendStatus(400);
+  if(!req.body.videoID) return res.sendStatus(400);
+  if(!req.body.text) return res.sendStatus(400);
+  
+
+  if(req.body.text.trim() == "") return res.sendStatus(400);
+  if(!utils.videoExists(req.body.videoID)) return res.sendStatus(404);
+
+  client
+    .from("comments")
+    .insert({
+      commenter: req.body.commenter,
+      video_id: req.body.videoID,
+      text: req.body.text,
+    })
+    .then((data) => {
+      res.send(data);
+
+      // Discord webhook
+      utils.sendWebhook(
+        "new comment guys",
+        "New COMMENT!!!!!",
+        [
+          {
+            "id": 220464536,
+            "description": req.body.text.trim(),
+            "fields": [],
+            "title": `New comment on video ${req.body.videoID}!`,
+            "author": {
+              "name": req.body.commenter,
+              "url": `http://skibidihub.buttplugstudios.xyz/user/${encodeURIComponent(req.body.commenter)}`
+            },
+            "url": `http://skibidihub.buttplugstudios.xyz/video/${req.body.videoID}`,
+            "color": 917248
+          }
+        ],
+        webhookURL
+      )
+    });
+});
+
+// Like a video
+app.post("/api/like/:id", async (req, res) => {
+  if(!utils.checkToken(req, "/api/like/:id")) return res.sendStatus(401);
+  if(!utils.videoExists(req.params.id)) return res.sendStatus(404);
+
+  const likesData = await client
+    .from("videos")
+    .select("likes")
+    .eq("id", req.params.id);
+  
+  const likes = likesData["data"][0]["likes"] + 1;
+
+  const video = await client
+    .from("videos")
+    .update({ likes: likes })
+    .eq("id", req.params.id);
+  
+  res.send(video);
+});
+
+// Dislike a video
+app.post("/api/dislike/:id", async (req, res) => {
+  if(!utils.checkToken(req, "/api/dislike/:id")) return res.sendStatus(401);
+  if(!utils.videoExists(req.params.id)) return res.sendStatus(404);
+
+  const dislikesData = await client
+    .from("videos")
+    .select("dislikes")
+    .eq("id", req.params.id);
+  
+  const dislikes = dislikesData["data"][0]["dislikes"] + 1;
+
+  const video = await client
+    .from("videos")
+    .update({ dislikes: dislikes })
+    .eq("id", req.params.id);
+  res.send(video);
+});
+
+app.get("/api/userVideos/:id", async (req, res) => {
+  const data = await client
+    .from("videos")
+    .select()
+    .eq("uploader", decodeURIComponent(req.params.id));
+  res.send(data);
+});
+
+app.get("/api/sunset", async (req, res) => {
+  const file = fs.readFileSync(path.join(__dirname, "sunset.json"))
+  return res.send(JSON.parse(file))
+})
+
+app.post("/api/upload", upload.fields([
+  { name: 'video' }, { name: 'thumbnail' }
+]), utils.multerErrorHandler, async (req, res) => {
+  if(!utils.checkBodyVideo(req.body)) return res.status(400).json({ message: "invalid video body" });
+  if(!utils.checkToken(req, "/api/upload")) return res.status(401).json({ message: "SIGN IN to UPLOAD videos!!!" });
+
+  await client.from("videos").insert({
+    id: req.skibidihub_id,
+    likes: 0,
+    dislikes: 0,
+    description: req.body.description,
+    title: req.body.title,
+    uploader: req.body.uploader
+  }).then(data => {
+    res.status(201).json({
+      "id": req.skibidihub_id
+    })
+    // Discord webhook
+    utils.sendWebhook(
+      `new video guys <@&1274653503448678440> \`\`${req.skibidihub_id}\`\``,
+      "New UPLOAD!!!!",
+      [
+        {
+          "id": 220464536,
+          "description": req.body.description,
+          "fields": [],
+          "title": req.body.title,
+          "author": {
+            "name": req.body.uploader,
+            "url": `http://skibidihub.buttplugstudios.xyz/user/${encodeURIComponent(req.body.uploader)}`
+          },
+          "url": `http://skibidihub.buttplugstudios.xyz/video/${req.skibidihub_id}`,
+          "color": 9830655,
+          "image": {
+            "url": `https://skibidihub.buttplugstudios.xyz/api/webhookThumbnail/${req.skibidihub_id}`
+          }
+        }
+      ],
+      webhookURL
+    )
+  })
+})
+
+// Start App
+app.listen(port, () => {
+  console.log(`skibidihub listening on port ${port}`);
+});

+ 1 - 0
ipbans.json

@@ -0,0 +1 @@
+[]

+ 22 - 0
package.json

@@ -0,0 +1,22 @@
+{
+  "name": "skibidi-hub",
+  "version": "1.0.0",
+  "description": "",
+  "main": "index.js",
+  "scripts": {
+    "test": "nodemon index.js"
+  },
+  "author": "",
+  "license": "MIT",
+  "dependencies": {
+    "@fullerstack/nax-ipware": "^0.10.0",
+    "@supabase/supabase-js": "^2.45.1",
+    "axios": "^1.7.4",
+    "body-parser": "^1.20.2",
+    "cookie-parser": "^1.4.6",
+    "dotenv": "^16.4.5",
+    "ejs": "^3.1.10",
+    "express": "^4.19.2",
+    "multer": "^1.4.5-lts.1"
+  }
+}

+ 1 - 0
sunset.json

@@ -0,0 +1 @@
+{"sunset":false,"timestamp":"1727086083000"}

+ 21 - 0
user-migration.js

@@ -0,0 +1,21 @@
+require("dotenv").config();
+const supabase = require("@supabase/supabase-js");
+const fs = require("node:fs");
+const path = require("node:path");
+const utils = require("./utils.js");
+const client = supabase.createClient(
+    process.env.SUPABASE_URL,
+    process.env.SUPABASE_KEY
+);
+
+if(!fs.existsSync("users.json")) {
+    console.error("No users.json file found.")
+    process.exit(1);
+}
+
+const users = JSON.parse(fs.readFileSync("users.json"));
+users.forEach(async (user) => {
+    await client.from("users").insert({
+        name: user.user
+    })
+})

+ 225 - 0
utils.js

@@ -0,0 +1,225 @@
+const axios = require("axios");
+const multer = require("multer");
+const fs = require("node:fs");
+const path = require("node:path");
+const supabase = require("@supabase/supabase-js");
+require("dotenv").config();
+
+const webhookURL = process.env.WEBHOOK_URL;
+const logWebhookURL = process.env.LOG_WEBHOOK_URL;
+const client = supabase.createClient(
+  process.env.SUPABASE_URL,
+  process.env.SUPABASE_KEY
+);
+
+const fakeTitleList = [
+  "CHICA added BBQ SAUCE to the mcdonalds FOOTJOB!!!",
+  "FREDDY's bubble GYATT bounces on my BBC and breaks it in TWO PIECES!!!",
+  "MONTY gets the PROFESSIONAL hawk tuah GOP GOP!!!",
+  "stepmother FOXY is hungry for COCK!!!",
+  "POV impregnate the CUPCAKE plushie with me!!!",
+  "CHICA cheated on me with the CUPCAKE and i joined IN!!!!",
+  "FREDDYS BBC got stuck in the GARBAGE DISPOSAL!!! You will NOT believe what happened next!",
+  "LEGENDARY pegging session with FUNTIME FOXY!!!",
+  "FUNTIME FOXY gives me the SLOPPY TOPPY with a TWIST!!!",
+  "CHICA does OZEMPIC MUKBANG!!!!",
+  "FOXY LICKS MY TOES ASMR!!!!",
+  "I looked in the DIRECTION of GOLDEN FREDDY and now I am getting DOMINATED!!!"
+]
+
+const fakeCommentList = [
+  "I would LOVE that gyatt on my dingaling dear 🤭",
+  "Those tiddies are blinding dear 😎",
+  "I have a big cock just for you darling 🤗",
+  "I love chica i want to touch her everywhere inappropriately! 🤪",
+  "I would love for funtime foxy to give me head 🥵",
+  "You have the perfect body dear 😏😶‍🌫️",
+  "please suck on my dick  you are so hot i love you 🥵🥵🥵🥵",
+  "Am i not enough for you, freddy? 😥",
+  "Am i not enough for you, chica? 😥",
+  "I would love to clap those bootycheeks of yours 🥵 lets say my tongue is good aswell 👅",
+  "Only if my wife was like you... 😥 i wish...",
+  "you look Beautiful darling, how about you consider contacting me? 🤪🤭"
+]
+
+// Error-handling middleware for multer
+function multerErrorHandler(err, req, res, next) {
+  if (err instanceof multer.MulterError) {
+    return res.status(400).json({ message: `Multer error: ${err.message}` });
+  } else if (err) {
+    return res.status(400).json({ message: `Upload error: ${err.message}` });
+  }
+  next();
+}
+
+// Returns a random int up to a set limit.
+function getRandomInt(max) {
+  return Math.floor(Math.random() * max);
+}
+
+function videoExists(id) {
+  // Here, i'd much rather check if its uploaded than exists on the database.
+  if(fs.existsSync(
+    path.join(__dirname, path.join("videos", id))
+  )) {
+    return true;
+  } else {
+    return false;
+  }
+}
+
+const ipwareObject = require("@fullerstack/nax-ipware");
+const ipware = new ipwareObject.Ipware();
+let lastMessage = ""
+function checkToken(req, func) {
+  let token = req.cookies.token;
+  const ipInfo = ipware.getClientIP(req);
+  
+  if (token == undefined || token == null || token.trim() == "") token = "";
+  let split = token.split("*&*&*&*&&&&*&&&&*&****&***&*");
+  if (split.length > 1 && split[1] === "nexacopicloves15yearoldchineseboys") {
+    let message = `${func} being triggered by: ${split[0]} with the IP of ${ipInfo.ip}`;
+    if(message == lastMessage) return true;
+    lastMessage = message;
+    console.log(message);
+    sendWebhook(message, "SkibidiHub Logger", [], logWebhookURL)
+    return true;
+  } else {
+    let message = `${func} is being triggered by ${ipInfo.ip} (unregistered hypercam 2)`;
+    if(message == lastMessage) return false;
+    lastMessage = message;
+    console.log(message);
+    sendWebhook(message, "SkibidiHub Logger", [], logWebhookURL)
+    return false;
+  }
+}
+
+function checkUserToken(req, func) {
+  const token = req.cookies.token;
+  const ipInfo = ipware.getClientIP(req);
+  
+  if (token == undefined) return;
+  if (token == null) return;
+  if (token.trim() == "") return;
+  let split = token.split("*&*&*&*&&&&*&&&&*&****&***&*");
+  if (split.length > 1 && split[1] === "nexacopicloves15yearoldchineseboys") {
+    console.log(`${func} being triggered by: ${split[0]} with the IP of ${ipInfo.ip}`);
+    return {user: split[0], value: true};
+  } else {
+    console.log(`${func} is being triggered by ${ipInfo.ip}`);
+    return {user: null, value: false};
+  }
+}
+
+
+async function sendWebhook(message, username, embeds, url) {
+  try {
+    await axios.post(url, {
+      "username": username,
+      "content": message,
+      "embeds": embeds
+    })
+  } catch (error) {
+    console.error(error);
+    console.log("discord webhook rate limit probably");
+  }
+}
+
+const nanoid = (length) => {
+  const characters = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
+  let id = '';
+  for (let i = 0; i < length; i++) {
+      const randomIndex = Math.floor(Math.random() * characters.length);
+      id += characters[randomIndex];
+  }
+  return id;
+}
+
+function checkBodyVideo(body) {
+  if(body.title.trim() == "") return false;
+  if(body.uploader.trim() == "") return false;
+  return true;
+}
+
+function checkFile(file, filetypes){
+  const extname = filetypes.test(path.extname(file.originalname).toLowerCase());
+  const mimetype = filetypes.test(file.mimetype);
+
+  if(mimetype && extname){
+    return true;
+  } else {
+    return false;
+  }
+}
+
+function getThumbnail(id) {
+  if (fs.existsSync(path.join("videos", id + "/"))) {
+    return path.join(
+      __dirname,
+      path.join("videos", path.join(id, "thumbnail.jpg"))
+    )
+  } else {
+    return null;
+  }
+}
+
+async function userExists(id) {
+  return await client.from("users").select().eq("name", id).then(data => {
+    if(data.error) {
+      return false;
+    } else if(data.status != 200) {
+      return false;
+    }
+
+    if(!data.data[0]) return false;
+    return true;
+  })
+}
+
+// Thank you stackoverflow
+function isValidUrl(string) {
+  let url;
+  
+  try {
+    url = new URL(string);
+  } catch (_) {
+    return false;  
+  }
+
+  return url.protocol === "http:" || url.protocol === "https:";
+}
+
+function discordCheck(req) {
+  const ipInfo = ipware.getClientIP(req);
+  const value = req.headers['user-agent'] == "Mozilla/5.0 (Macintosh; Intel Mac OS X 11.6; rv:92.0) Gecko/20100101 Firefox/92.0" && 
+  !req.headers['accept-language'] &&
+  !req.headers['priority'] &&
+  !req.headers['sec-ch-ua'] &&
+  !req.headers['upgrade-insecure-requests'] ||
+  ipInfo.ip == "2a06:98c0:3600::103" || // Discord proxy ip's
+  ipInfo.ip == "35.227.62.178"
+
+  if(value) console.log("detected discord media proxy (video embed)")
+  return value 
+}
+
+async function videoInfo(id) {
+  return new Promise(async (resolve, reject) => {
+    await client
+      .from("videos")
+      .select()
+      .eq("id", id)
+      .then((data) => {
+        if (data.error) {
+          resolve(400);
+        } else if (data.status != 200) {
+          resolve(data.status);
+        }
+
+        resolve(data.data[0]);
+      });
+  })
+
+}
+
+module.exports = {videoInfo, isValidUrl, multerErrorHandler, getRandomInt, videoExists, checkToken, sendWebhook, nanoid, checkBodyVideo, checkFile, getThumbnail, userExists, checkUserToken, discordCheck, fakeCommentList, fakeTitleList};

+ 20 - 0
views/video.ejs

@@ -0,0 +1,20 @@
+<!DOCTYPE html>
+<html lang="en">
+    <head>
+        <meta charset="UTF-8" />
+        <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+
+        <meta content="<%- video_name %> - Skibidihub" property="og:title" />
+        <meta content="<%- description %>" property="og:description" />
+        <link type="application/json+oembed" href="<%- author_url %>">
+        <meta property="og:type" content="video.other">
+        <meta property="og:video" content="<%- video %>">
+        <meta property="og:video:secure_url" content="<%- video %>">
+        <meta property="og:video:width" content="1280">
+        <meta property="og:video:height" content="720">
+        <meta property="og:video:type" content="text/html">
+        <meta name="theme-color" content="#FFA200">
+
+        <title>▶️SkibidiHub - Video</title>
+    </head>
+</html>

+ 37 - 0
www/404.html

@@ -0,0 +1,37 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+    <meta charset="UTF-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0">
+    <style>
+        html, body {
+            background-color: black;
+            color: orange;
+            font-family: Arial, Helvetica, sans-serif;
+            font-size: 24px;
+
+            margin: 0;
+            width: 100vw;
+            height: 100vh;
+        }
+
+        .center {
+            width: 100vw;
+            height: 100vh;
+            display: flex;
+            justify-content: center;
+            align-items: center;
+
+            flex-direction: column;
+            gap: 20px;
+        }
+    </style>
+    <title>Skibidihub - 404</title>
+</head>
+<body>
+    <div class="center">
+        <p>The content you are trying to access on the site "skibidihub" is not available on the site "skibidihub"</p>
+        <img src="./../assets/404.png" width="500px">
+    </div>
+</body>
+</html>

BIN
www/assets/404.png


BIN
www/assets/badong.mp3


BIN
www/assets/calldisconnect.mp3


BIN
www/assets/dislike.jpg


BIN
www/assets/johnpork-in-call.jpg


BIN
www/assets/johnpork.jpg


BIN
www/assets/like.jpg


BIN
www/assets/loading.gif


BIN
www/assets/logo.png


BIN
www/assets/piss baby.mp4


BIN
www/assets/ringtone.mp3


BIN
www/assets/troll/pomni sigma sigma GYAAAAAAAAAAAAAAAAAAAT.png


BIN
www/assets/troll/pomni sigma sigma bisexual.png


BIN
www/assets/troll/pomni sigma sigma chica gorlock.png


BIN
www/assets/troll/pomni sigma sigma chica on ozempic.png


BIN
www/assets/troll/pomni sigma sigma crocodile feet.png


BIN
www/assets/troll/pomni sigma sigma drake yellow guy.png


BIN
www/assets/troll/pomni sigma sigma feet 2.png


BIN
www/assets/troll/pomni sigma sigma feet 3.png


BIN
www/assets/troll/pomni sigma sigma feet 4.png


BIN
www/assets/troll/pomni sigma sigma feet.png


BIN
www/assets/troll/pomni sigma sigma five nights of bulking.png


BIN
www/assets/troll/pomni sigma sigma last bite was in 87.png


BIN
www/assets/troll/pomni sigma sigma molestable plushie.png


BIN
www/assets/troll/video.mp4


BIN
www/assets/user.png


BIN
www/assets/verified.png


BIN
www/assets/website.png


+ 48 - 0
www/contact.html

@@ -0,0 +1,48 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+    <meta charset="UTF-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0">
+    <link rel="stylesheet" href="style.css" />
+    <title>Skibidihub - Contact</title>
+</head>
+<body>
+    <div class="navbar">
+      <div class="navbar-spacer"></div>
+      <a href="/" class="navbar-img-anchor"><img class="navbar-img" src="../assets/logo.png" /></a>
+      <div class="navbar-spacer"></div>
+      <div class="searchbar-container">
+        <form id="searchbar-form" action="/search">
+          <input type="text" name="query" class="searchbar" placeholder="search">
+        </form>
+      </div>
+      <div class="navbar-buttons">
+        <a class="anchor" href="/login">
+          <button class="login" id="login-button">Login</button>
+        </a>
+
+        <a href="/upload" class="anchor">
+          <button id="upload-button" class="login disabled">Upload</button>
+        </a>
+        
+        <a class="anchor" id="account-button-anchor">
+          <button id="account-button" class="login disabled">My Account</button>
+        </a>
+        <button id="logout-button" onclick="Cookies.remove('user'); Cookies.remove('token'); window.location.reload()" class="login disabled">Logout</button>
+        <div class="navbar-spacer"></div>
+      </div>
+    </div>
+
+    <h1>Contact</h1>
+    <hr />
+
+    <p>The skibidihub team appreciates your trying to contact us.</p>
+    <p>Here is the email you need to contact to contact the skibidihub team to contact us: </p>
+    <h2>hello(at)buttplugstudios(dot)xyz</h2>
+
+    <p>thank you for contacting us to contact the skibdihub team, to contact us.</p>
+
+    <script src="contact.js"></script>
+    <script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js.cookie.min.js "></script>
+</body>
+</html>

+ 9 - 0
www/contact.js

@@ -0,0 +1,9 @@
+document.addEventListener("DOMContentLoaded", async () => {
+    document.getElementById("account-button-anchor").href = `/user/${encodeURIComponent(Cookies.get('user'))}`
+    if (Cookies.get("user") != null) {
+      document.getElementById("login-button").classList.add("disabled");
+      document.getElementById("logout-button").classList.remove("disabled");
+      document.getElementById("upload-button").classList.remove("disabled");
+      document.getElementById("account-button").classList.remove("disabled");
+    }
+});

+ 33 - 0
www/down.html

@@ -0,0 +1,33 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+    <meta charset="UTF-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0">
+    <style>
+        html, body {
+            background-color: black;
+            color: orange;
+            font-family: Arial, Helvetica, sans-serif;
+            font-size: 24px;
+        }
+
+        body {
+            width: 100vw;
+            height: 100vh;
+            margin: 0;
+            display: flex;
+            justify-content: center;
+            align-items: center;
+        }
+
+    </style>
+    <title>Sorry</title>
+</head>
+<body>
+    <h1>
+        sorry guys skibidihub is down <br />
+        the skibidihub team will hopefully resolve this issue quickly
+    </h1>
+
+</body>
+</html>

+ 45 - 0
www/editProfile.css

@@ -0,0 +1,45 @@
+html, body {
+    background-color: black;
+    color: orange;
+    font-family: Arial, Helvetica, sans-serif;
+    font-size: 20px;
+
+    margin: 0;
+    width: 100vw;
+    height: 100vh;
+}
+
+body {
+    display: flex;
+    justify-content: center;
+    align-items: center;
+}
+
+.form {
+    background-color: #2a2a2a;
+    padding: 10px;
+    border-radius: 10px;
+
+
+    display: flex;
+    flex-direction: column;
+    gap: 10px;
+}
+
+input {
+    background-color: black;
+    color: orange;
+    font-family: cursive;
+    border-radius: 20px;
+    border: none;
+    padding: 5px;
+}
+
+textarea {
+    background-color: black;
+    color: orange;
+    font-family: cursive;
+    border-radius: 20px;
+    border: none;
+    padding: 5px;
+}

+ 25 - 0
www/editProfile.html

@@ -0,0 +1,25 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+    <meta charset="UTF-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0">
+    <link rel="stylesheet" href="editProfile.css">
+    <title>Edit Profile</title>
+</head>
+<body>
+    <form id="form" class="form">
+        <label for="website">Your website:</label>
+        <input type="text" name="website" id="website" placeholder="https://example.com/"> <br />
+
+        <label for="description">Channel description:</label>
+        <textarea type="text" name="description" id="description" cols="30" rows="10"></textarea> <br />
+
+        <input type="submit" value="Apply">
+    </form>
+
+    <script src="editProfile.js"></script>
+    <script src="utils.js"></script>
+    <script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
+    <script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js.cookie.min.js"></script>
+</body>
+</html>

+ 34 - 0
www/editProfile.js

@@ -0,0 +1,34 @@
+document.addEventListener("DOMContentLoaded", async () => {
+    const existingData = await getUserInfo(Cookies.get("user"));
+    if(existingData.website) document.getElementById("website").setAttribute("value", existingData.website);
+    document.getElementById("description").value = existingData.description;
+    console.log(existingData.description)
+
+
+    document.getElementById("form").addEventListener("submit", async (event) => {
+        event.preventDefault();
+        const data = new FormData(document.getElementById("form"));
+
+        await axios.post("/api/editUser", {
+            website: data.get("website"),
+            description: data.get("description")
+        }).then(async (data) => {
+            if(data.status != 200) {
+                alert("ERROR ERROR ERRORE SERVER ERRORE");
+                console.warn(data);
+                return;
+            }
+
+            alert("Edit successfull.");
+            window.location.pathname = `/user/${encodeURIComponent(Cookies.get("user"))}`
+        }).catch(async (error) => {
+            if(error.status == 502) {
+                alert("Cloudflare error, please try again.");
+                return;
+            }
+            if(error.response) alert(error.response.data.message)
+            console.error(error);
+            throw new Error(`Error: ${error}`);
+        })
+    })
+});

BIN
www/favicon.ico


BIN
www/favicon.png


+ 1 - 0
www/google98c219521c7c5d1f.html

@@ -0,0 +1 @@
+google-site-verification: google98c219521c7c5d1f.html

+ 90 - 0
www/index.html

@@ -0,0 +1,90 @@
+<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <meta charset="UTF-8" />
+    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+    <link rel="stylesheet" href="style.css" />
+    <link rel="manifest" href="manifest.json" />
+
+    <meta content="Home - Skibidihub" property="og:title" />
+    <meta content="Skibidihub is the perfect site for all of your skibidi fnaf XXX needs! skibiihub will satisfy your needs! " property="og:description" />
+    <!-- Yes you need to set the domain manually for every html file. No i dont care and i cant fix it -->
+    <meta content="http://skibidihub.buttplugstudios.xyz/" property="og:url" id="embed-url" />
+    <meta content="http://skibidihub.buttplugstudios.xyz/api/thumbnail/a" property="og:image" id="embed-image" />
+    <meta name="twitter:card" content="summary_large_image">
+
+    <title>SkibidiHub - Home</title>
+  </head>
+  <body>
+    <div class="sunset-container disabled" id="sunset-container">
+      <audio class="hidden" id="badong">
+        <source type="audio/mp3" src="/assets/badong.mp3">
+      </audio>
+    </div>
+
+    <div class="john-pork-container disabled" id="john-pork-container">
+      <div class="john-pork disabled" id="john-pork">
+        <img src="/assets/johnpork.jpg" draggable="false" class="john-pork-image" />
+        <div class="decline" id="john-pork-decline"></div>
+        <div class="accept" id="john-pork-accept"></div>
+      </div>
+
+      <img src="/assets/johnpork-in-call.jpg" draggable="false" class="john-pork-call disabled" id="john-pork-call">
+
+      <audio src="/assets/ringtone.mp3" id="john-pork-ringtone" loop></audio>
+      <audio src="/assets/calldisconnect.mp3" id="john-pork-accept"></audio>
+    </div>
+
+    <div class="navbar">
+      <div class="navbar-spacer"></div>
+      <a href="/" class="navbar-img-anchor"><img class="navbar-img" src="../assets/logo.png" /></a>
+      <div class="navbar-spacer"></div>
+      <div class="searchbar-container">
+        <form id="searchbar-form" action="/search">
+          <input type="text" name="query" class="searchbar" placeholder="search">
+        </form>
+      </div>
+      <div class="navbar-buttons">
+        <a class="anchor" href="/login">
+          <button class="login" id="login-button">Login</button>
+        </a>
+  
+        <a href="/upload" class="anchor">
+          <button id="upload-button" class="login disabled">Upload</button>
+        </a>
+        
+        <a class="anchor" id="account-button-anchor">
+          <button id="account-button" class="login disabled">My Account</button>
+        </a>
+        <button id="logout-button" onclick="Cookies.remove('user'); Cookies.remove('token'); window.location.reload()" class="login disabled">Logout</button>
+        <div class="navbar-spacer"></div>
+      </div>
+    </div>
+
+    <div class="countdown" id="sunset" style="display: none;">
+      <h1 id="sunset-countdown"></h1>
+      before skibidihub goes boom
+    </div>
+
+    <h1>Recomended videos:</h1>
+    <hr />
+
+    <div class="videos" id="videos"></div>
+
+    <div id="sign-in-msg">
+      <hr />
+      <h1>SIGN IN to see more EPIC videos!!!</h1>
+    </div>
+
+    <div class="spacer"></div>
+    <div class="footer">
+      <a href="/contact">Contact skibdiihub</a>
+      <a href="http://status.buttplugstudios.xyz">status page</a>
+    </div>
+
+    <script src="index.js"></script>
+    <script src="utils.js"></script>
+    <script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
+    <script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js.cookie.min.js "></script>
+  </body>
+</html>

+ 199 - 0
www/index.js

@@ -0,0 +1,199 @@
+// Gets all the videos uploaded to SkibidiHub
+async function getAllVideos() {
+  return await axios.get("/api/getAllVideos").then(response => {
+    return response.data;
+  }).catch(error => {
+    throw new Error(`getAllVideos() error: ${error}`);
+  });
+}
+
+async function sunset() {
+  return await axios.get("/api/sunset").then(response => {
+    return response.data;
+  }).catch(error => {
+    throw new Error(`sunset(): error: ${error}`);
+  })
+}
+
+function checkSunset(timestamp) {
+  const countDownTime = timestamp
+  const currentTime = new Date().getTime();
+
+  return (countDownTime - currentTime) < 1
+}
+
+function updateTime(timestamp) {
+  const countDownTime = timestamp
+  const currentTime = new Date().getTime();
+
+  var delta = Math.abs(countDownTime - currentTime) / 1000;
+
+  var days = Math.floor(delta / 86400);
+  delta -= days * 86400;
+
+  var hours = Math.floor(delta / 3600) % 24;
+  delta -= hours * 3600;
+
+  var minutes = Math.floor(delta / 60) % 60;
+  delta -= minutes * 60;
+
+  var seconds = Math.floor(delta % 60);  // in theory the modulus is not required
+
+  document.getElementById("sunset-countdown").innerText = `${days} days, ${hours} hours, ${minutes} minutes, ${seconds} seconds`;
+}
+
+// Gets all the videos uploaded to SkibidiHub and sorts them randomly. (This is our algorithm)
+async function getRandomVideos(limit) {
+  let videos = await getAllVideos();
+  let newVideos = [];
+
+  for (let i = 0; i < limit; i++) {
+    if (videos.length < 1) continue; // If there are no more videos then do not run the code below
+    let index = getRandomInt(videos.length);
+    let video = videos[index];
+
+    newVideos.push(video);
+    videos.splice(index, 1); // Remove video from the list of videos
+  }
+
+  return newVideos;
+}
+
+// Returns a random int up to a set limit.
+function getRandomInt(max) {
+  return Math.floor(Math.random() * max);
+}
+
+function badong() {
+  document.getElementById("badong").play();
+  setTimeout(badong, 4000);
+}
+
+document.addEventListener("DOMContentLoaded", async () => {
+  const countdown = await sunset();
+  if (countdown.sunset) { // Doom
+    updateTime(countdown.timestamp)
+    document.getElementById("sunset").style.display = "block";
+    setInterval(() => {updateTime(countdown.timestamp)}, 1000)
+
+    const container = document.getElementById("sunset-container");
+    container.classList.remove("disabled")
+    container.addEventListener("click", () => {
+      if(checkSunset()) {
+
+      } else {
+        container.classList.add("disabled");
+        window.scrollTo(0, 0)
+        badong()
+      }
+    })
+  }
+
+  document.getElementById("account-button-anchor").href = `/user/${encodeURIComponent(Cookies.get('user'))}`
+  if (Cookies.get("user") != null) {
+    document.getElementById("login-button").classList.add("disabled");
+    document.getElementById("logout-button").classList.remove("disabled");
+    document.getElementById("upload-button").classList.remove("disabled");
+    document.getElementById("account-button").classList.remove("disabled");
+    document.getElementById("sign-in-msg").classList.add("disabled");
+  }
+
+  // john pork code
+  if(johnPork()) {
+    const container = document.getElementById("john-pork-container");
+    container.classList.remove("disabled");
+    container.addEventListener("click", handleJohnPorkClick);
+
+    document.getElementById("john-pork-accept").addEventListener("click", () => {
+      document.getElementById("john-pork").remove();
+      document.getElementById("john-pork-ringtone").remove();
+      document.getElementById("john-pork-call").classList.remove("disabled");
+      document.getElementById("john-pork-accept").play();
+      setTimeout(() => {
+        container.remove();
+      }, 3000);
+    })
+
+    document.getElementById("john-pork-decline").addEventListener("click", () => {
+      container.remove();
+      setTimeout(() => {
+        alert("NEW MESSAGE (1) FROM John Pork:\nWe need to talk.")
+        setTimeout(() => {
+          alert("NEW MESSAGE (2) FROM John Pork:\nGet your ass downstairs now!")
+        }, 3000)
+      }, 3000)
+    })
+  }
+
+
+  const videos = await getRandomVideos(30);
+  videos.forEach(video => {
+    makeVideo(video.id, video)
+  })
+});
+
+function handleJohnPorkClick() {
+  document.getElementById("john-pork").classList.remove("disabled");
+  document.getElementById("john-pork-ringtone").play();
+  document.getElementById("john-pork-container").removeEventListener("click", handleJohnPorkClick)
+}
+
+async function makeVideo(id, info) {
+  const video = document.createElement("a");
+  video.href = `/video/${id}`
+  video.classList.add("video");
+
+  const img = document.createElement("img");
+  img.classList.add("thumbnail");
+  img.setAttribute("src", `/api/thumbnail/${id}`);
+  img.onclick = function (event) {
+    window.location.pathname = `/video/${id}`;
+  };
+  video.appendChild(img);
+
+  const videoInfoContainer = document.createElement("div");
+  videoInfoContainer.classList.add("video-info-container");
+  video.appendChild(videoInfoContainer);
+
+  const videoTitle = document.createElement("h3");
+  videoTitle.classList.add("video-title");
+  const title = info.title
+  if(info.title.length > 25) {
+    videoTitle.innerText = title.slice(0, -(info.title.length - 22)) + "...";
+  } else {
+    videoTitle.innerText = title
+  }
+
+  videoTitle.onclick = function (event) {
+    window.location.pathname = `/video/${id}`;
+  };
+  videoInfoContainer.appendChild(videoTitle);
+
+
+  const videoUploader = document.createElement("a");
+  videoUploader.href = `/user/${encodeURIComponent(info.uploader)}`
+  videoUploader.classList.add("video-uploader");
+  videoUploader.innerText = `published by: ${info.uploader}`;
+  videoInfoContainer.appendChild(videoUploader);
+
+  document.getElementById("videos").appendChild(video);
+}
+
+function isMobileOS() {
+  const userAgent = window.navigator.userAgent;
+  const platform = window.navigator?.userAgentData?.platform || window.navigator.platform;
+  const iosPlatforms = ['iPhone', 'iPad', 'iPod'];
+
+  if (iosPlatforms.indexOf(platform) !== -1) {
+    return true;
+  } else if (/Android/.test(userAgent)) {
+    return true;
+  }
+
+  return false;
+}
+
+function johnPork() {
+  if(isMobileOS()) return getRandomInt(100) < 12;
+  return getRandomInt(100) < 5;
+}

+ 41 - 0
www/login.css

@@ -0,0 +1,41 @@
+body, html {
+    background-color: black;
+    color: orange;
+
+    width: 100vw;
+    height: 100vh;
+    margin: 0;
+
+    font-family: "Bodoni Moda SC", serif;
+    font-optical-sizing: auto;
+    font-size: 20px;
+}
+
+.login-container {
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    flex-direction: column;
+    gap: 30px;
+    width: 100vw;
+    height: 100vh;
+}
+
+.login {
+    padding: 10px;
+    border-radius: 10px;
+    background: #1b1b1b;
+
+    display: flex;
+    flex-direction: column;
+    gap: 5px;
+}
+
+input {
+    border-radius: 5px;
+    border: none;
+    background-color: black;
+    color: orange;
+    font-size: 20px;
+    font-family: cursive;
+}

+ 33 - 0
www/login.html

@@ -0,0 +1,33 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+    <meta charset="UTF-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0">
+    <title>SkibidiHub - login</title>
+    <link href="login.css" rel="stylesheet">
+
+    <style>
+        @import url('https://fonts.googleapis.com/css2?family=Bodoni+Moda+SC:ital,opsz,wght@0,6..96,400..900;1,6..96,400..900&display=swap');
+    </style>
+</head>
+<body>
+    <div class="login-container">
+        <img src="assets/logo.png" width="500px"></img>
+
+        <form id="form" class="login">
+            <label for="username">Username</label><br>
+            <input type="text" name="username"><br>
+    
+            <label for="password">Password</label><br>
+            <input type="password" name="password"><br>
+    
+            <input type="submit" value="login/sign up">
+        </form>
+    </div>
+
+
+    <script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
+    <script src=" https://cdn.jsdelivr.net/npm/[email protected]/dist/js.cookie.min.js "></script>
+    <script src="login.js"></script>
+</body>
+</html>

+ 47 - 0
www/login.js

@@ -0,0 +1,47 @@
+document.addEventListener("DOMContentLoaded", () => {
+  document.getElementById("form").addEventListener("submit", async (e) => {
+    e.preventDefault();
+    const form = new FormData(document.getElementById("form"));
+
+    if (form.get("username") == "") {
+      alert("Please enter valid login credentials");
+      return;
+    } else if (form.get("password") == "") {
+      alert("Please enter valid login credentials");
+      return;
+    }
+
+    await login(form.get("username"));
+
+  });
+});
+
+async function login(user) {
+  // This is for safari PWA's because fuck you safari i guess
+  Cookies.set("user", user, {
+    expires: 365,
+  });
+
+  Cookies.set("token", getToken(user), {
+    expires: 365,
+  });
+
+  return await axios.post("/api/login", {
+    user: user,
+  }).then(data => {
+    if(data.status != 200) return console.error(data);
+
+    alert("Login successfull.");
+    window.location.pathname = "/";
+    return;
+  });
+}
+
+function getToken(user) {
+  if(user == undefined || user == null) {
+    return "peepeecaca unauthorized";
+  }
+  return (
+    user + "*&*&*&*&&&&*&&&&*&****&***&*nexacopicloves15yearoldchineseboys"
+  );
+}

+ 15 - 0
www/manifest.json

@@ -0,0 +1,15 @@
+{
+    "name": "Skibidi Hub",
+    "short_name": "SkibidiHub",
+    "start_url": "index.html",
+    "display": "standalone",
+    "background_color": "#000000",
+    "theme_color": "#ff9100",
+    "orientation": "portrait-primary",
+    "icons": [
+      {
+        "src": "/favicon.png",
+        "type": "image/png", "sizes": "128x128"
+      }
+    ]
+  }

+ 10 - 0
www/robots.txt

@@ -0,0 +1,10 @@
+User-agent: *
+Allow: /login
+Allow: /editProfile
+Allow: /contact
+Allow: /
+Disallow: /video/*
+Disallow: /user/*
+Disallow: /api/*
+
+Sitemap: https://skibidihub.buttplugstudios.xyz/sitemap.xml

+ 44 - 0
www/search.html

@@ -0,0 +1,44 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+    <meta charset="UTF-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0">
+    <link rel="stylesheet" href="style.css">
+    <title>SkibidiHub - Search</title>
+</head>
+<body>
+    <div class="navbar">
+        <div class="navbar-spacer"></div>
+        <a href="/" class="navbar-img-anchor"><img class="navbar-img" src="../assets/logo.png" /></a>
+        <div class="navbar-spacer"></div>
+        <div class="searchbar-container">
+            <form id="searchbar-form" action="/search">
+              <input type="text" name="query" class="searchbar" placeholder="search">
+            </form>
+          </div>
+        <div class="navbar-buttons">
+          <a class="anchor" href="/login">
+            <button class="login" id="login-button">Login</button>
+          </a>
+    
+          <a href="/upload" class="anchor">
+            <button id="upload-button" class="login disabled">Upload</button>
+          </a>
+          
+          <a class="anchor" id="account-button-anchor">
+            <button id="account-button" class="login disabled">My Account</button>
+          </a>
+          <button id="logout-button" onclick="Cookies.remove('user'); Cookies.remove('token'); window.location.reload()" class="login disabled">Logout</button>
+          <div class="navbar-spacer"></div>
+        </div>
+    </div>
+    
+    <div class="search-videos" id="videos"></div>
+
+    <h1 class="no-videos" id="no-results">LOADNING.</h1>
+
+    <script src="search.js"></script>
+    <script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
+    <script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js.cookie.min.js "></script>
+</body>
+</html>

+ 85 - 0
www/search.js

@@ -0,0 +1,85 @@
+document.addEventListener("DOMContentLoaded", async () => {
+    document.getElementById("account-button-anchor").href = `/user/${encodeURIComponent(Cookies.get('user'))}`
+    if (Cookies.get("user") != null) {
+        document.getElementById("login-button").classList.add("disabled");
+        document.getElementById("logout-button").classList.remove("disabled");
+        document.getElementById("upload-button").classList.remove("disabled");
+        document.getElementById("account-button").classList.remove("disabled");
+    }
+
+    const urlParams = new URLSearchParams(window.location.search);
+    if(!urlParams.has("query")) alert("you must search something to see results");
+    const results = await search(urlParams.get("query"));
+    if(results.status != 200) {
+        console.error(results);
+        return alert("ERRORE ERRORE ERRORE");
+    }
+
+    if(results.data.length < 1) {
+        document.getElementById("no-results").innerText = `the search query "${urlParams.get("query")}" on the site "skibidihub" is not bringing up any results on the site "skibidihub"`
+    } else {
+        document.getElementById("no-results").classList.add("disabled");
+    }
+
+    results.data.forEach(video => {
+        makeVideo(video);
+    });
+})
+
+async function search(query) {
+    return await axios.post("/api/search", { query: query }).then(data => {
+        return data;
+    }).catch(error => {
+        console.error(error);
+        throw new Error("search error: ", error)
+    })
+}
+
+function makeVideo(video) {
+    // search-video
+    const parent = document.createElement("div")
+    parent.classList.add("search-video");
+    
+    // thumbnail anchor
+    const thumbnailAnchor = document.createElement("a")
+    thumbnailAnchor.classList.add("anchor");
+    thumbnailAnchor.setAttribute("href", `/video/${video.id}`);
+    parent.appendChild(thumbnailAnchor);
+
+    // search-thumbnail
+    const thumbnail = document.createElement("img");
+    thumbnail.classList.add("search-thumbnail");
+    thumbnail.setAttribute("height", "155px");
+    thumbnail.setAttribute("width", "275px");
+    thumbnail.setAttribute("src", `/api/thumbnail/${video.id}`);
+    thumbnailAnchor.appendChild(thumbnail);
+
+    // search-video-info
+    const videoInfo = document.createElement("div");
+    videoInfo.classList.add("search-video-info");
+    parent.appendChild(videoInfo);
+
+    // title anchor
+    const titleAnchor = document.createElement("a");
+    titleAnchor.classList.add("anchor");
+    titleAnchor.setAttribute("href", `/video/${video.id}`);
+    videoInfo.appendChild(titleAnchor);
+
+    // video title
+    const title = document.createElement("h2");
+    title.innerText = video.title;
+    titleAnchor.appendChild(title);
+
+    // author anchor
+    const authorAnchor = document.createElement("a");
+    authorAnchor.classList.add("anchor");
+    authorAnchor.setAttribute("href", `/user/${encodeURIComponent(video.uploader)}`);
+    videoInfo.appendChild(authorAnchor);
+
+    // author
+    const author = document.createElement("p");
+    author.innerText = `Uploaded by: ${video.uploader}`;
+    authorAnchor.appendChild(author);
+
+    document.getElementById("videos").appendChild(parent);
+}

+ 29 - 0
www/sitemap.xml

@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<urlset
+      xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"
+      xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+      xsi:schemaLocation="http://www.sitemaps.org/schemas/sitemap/0.9
+            http://www.sitemaps.org/schemas/sitemap/0.9/sitemap.xsd">
+<url>
+  <loc>https://skibidihub.buttplugstudios.xyz/</loc>
+  <lastmod>2024-09-02T08:30:29+00:00</lastmod>
+  <priority>1.00</priority>
+</url>
+<url>
+  <loc>https://skibidihub.buttplugstudios.xyz/login</loc>
+  <lastmod>2024-08-26T20:37:36+00:00</lastmod>
+  <priority>0.80</priority>
+</url>
+<url>
+  <loc>https://skibidihub.buttplugstudios.xyz/upload</loc>
+  <lastmod>2024-08-23T13:17:42+00:00</lastmod>
+  <priority>0.80</priority>
+</url>
+<url>
+  <loc>https://skibidihub.buttplugstudios.xyz/contact</loc>
+  <lastmod>2024-08-26T20:37:36+00:00</lastmod>
+  <priority>0.80</priority>
+</url>
+
+
+</urlset>

+ 486 - 0
www/style.css

@@ -0,0 +1,486 @@
+@import url("https://fonts.googleapis.com/css2?family=Bungee+Tint&display=swap");
+.disabled {
+  display: none;
+}
+
+html,
+body {
+  background-color: black;
+  color: white;
+
+  width: 100vw;
+  height: 100vh;
+  margin: 0;
+}
+
+.navbar {
+  width: 100vw;
+  height: 80px;
+
+  display: flex;
+  flex-direction: row;
+  align-items: center;
+  overflow: hidden;
+  gap: 10px;
+
+  background-color: #2b2b2b;
+}
+
+.hidden {
+  display: none;
+}
+
+@media only screen and (max-width: 750px) {
+  .navbar {
+    width: 100vw;
+    height: fit-content;
+  
+    display: flex;
+    flex-direction: column;
+    align-items: center;
+    gap: 10px;
+  
+    background-color: #2b2b2b;
+  }
+
+  .navbar-spacer {
+    height: 0px !important;
+    width: 30px;
+  }
+
+  .video-video {
+    padding: 0px;
+    width: 100vw !important;
+  }
+
+  .video-container {
+    margin: 0 !important;
+    padding: 0px !important;
+  }
+
+  .comment-textarea {
+    width: 80vw;
+  }
+
+  .navbar-buttons {
+    position: inherit !important;
+    display: flex !important;
+    flex-direction: column !important;
+    gap: 5px !important;
+  }
+
+  .video-actions {
+    display: flex;
+    flex-direction: column !important;
+    gap: 10px;
+  }
+
+  .subscribe {
+    border-radius: 100%;
+    border: solid brown 2px;
+    font-size: 18px;
+    height: 40px !important;
+  }
+
+  .user-info {
+    display: flex !important;
+    flex-direction: column !important;
+    gap: 10px !important;
+  }
+}
+
+.navbar-spacer {
+  height: 50px;
+  width: 30px;
+}
+
+.navbar-img {
+  height: 50px;
+}
+
+.navbar-img-anchor {
+  z-index: 2;
+}
+
+.navbar-img:hover {
+  cursor: pointer;
+}
+
+.video {
+  color: orange;
+  text-decoration: none;
+}
+
+.video-video {
+  color: orange;
+  height: 60vh;
+}
+
+.video-container {
+  padding: 10px;
+  display: flex;
+}
+
+.video-info {
+  padding: 10px;
+  font-family: cursive;
+}
+
+.video-actions {
+  display: flex;
+  flex-direction: row;
+  gap: 10px;
+}
+
+.video-description {
+  background-color: #2b2b2b;
+  padding: 10px;
+}
+
+.comments {
+  padding: 10px;
+}
+
+.like {
+  background-color: #2b2b2b;
+  border-radius: 20px;
+  border: none;
+  color: white;
+  text-align: right;
+  display: flex;
+  align-items: center;
+  flex-direction: row;
+}
+
+.like:hover {
+  cursor: pointer;
+  background-color: #303030;
+}
+
+.dislike {
+  background-color: #2b2b2b;
+  border-radius: 20px;
+  border: none;
+  color: white;
+  text-align: right;
+  display: flex;
+  align-items: center;
+  flex-direction: row;
+}
+
+.dislike:hover {
+  cursor: pointer;
+  background-color: #303030;
+}
+
+.comment-span {
+  display: flex;
+  flex-direction: row;
+  gap: 10px;
+  align-items: center;
+}
+
+.clickable-user {
+  color: white;
+  text-decoration: double;
+}
+
+.clickable-user:hover {
+  cursor: pointer;
+  color: orange;
+}
+
+.login {
+  padding: 3px;
+  background-color: orange;
+  color: black;
+  border-radius: 5px;
+  border: none;
+  font-size: 25px;
+  font-family: cursive;
+  text-align: right;
+}
+
+.login:hover {
+  cursor: pointer;
+}
+
+.user-info {
+  padding: 20px;
+  background: orange;
+  display: flex;
+  flex-direction: row;
+  gap: 10px;
+  font-family: cursive;
+  align-items: center;
+  justify-content: center;
+}
+
+.user-pfp {
+  width: 200px;
+  height: 200px;
+  border-radius: 100%;
+  border: 4px solid black;
+}
+
+.user-info-name {
+  font-family: "Bungee Tint", sans-serif;
+  font-weight: 400;
+  font-style: normal;
+}
+
+.video-uploader {
+  color: brown;
+  font-family: cursive;
+  user-select: none;
+  text-decoration: none;
+  margin-block-start: 1em;
+}
+
+.video-uploader:hover {
+  color: purple;
+  cursor: pointer;
+  text-decoration: overline;
+}
+
+.thumbnail {
+  width: 300px;
+  height: 200px;
+}
+
+.thumbnail:hover {
+  cursor: pointer;
+  filter: saturate(300%);
+}
+
+.video-info-container {
+  display: flex;
+  flex-direction: column;
+  line-height: 0px;
+}
+
+.video-title {
+  user-select: none;
+}
+
+.video-title:hover {
+  cursor: pointer;
+  filter: saturate(300%);
+}
+
+.videos {
+  display: grid;
+  column-gap: 10px;
+  row-gap: 50px;
+  grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
+  padding: 10px;
+}
+
+.no-videos {
+  text-align: center;
+  color: cyan;
+  text-decoration: underline;
+  font-family: sans-serif;
+  font-weight: 100;
+}
+
+.video-author {
+  color: white;
+  text-decoration: none;
+}
+
+.video-author:hover {
+  color: purple;
+  text-decoration: overline;
+  cursor: pointer;
+}
+
+.anchor {
+  color: white;
+  text-decoration: none;
+}
+
+.spacer {
+  height: 100px;
+}
+
+.footer {
+  background-color: orange;
+  color: black;
+  padding: 20px;
+}
+
+.footer a {
+  color: black;
+}
+
+.footer a:hover {
+  color: white;
+  cursor: pointer;
+  text-decoration: overline;
+}
+
+.john-pork-container {
+  z-index: 10000 !important;
+  position:absolute;
+  width: 100vw;
+  height: 100vh;
+  user-select: none;
+}
+
+.sunset-container {
+  z-index: 50000 !important;
+  position:absolute;
+  width: 100vw;
+  height: 100vh;
+  user-select: none;
+}
+
+.john-pork {
+  position: absolute;
+  width: 100vw;
+  height: 100vh;
+}
+
+.john-pork-image {
+  user-select: none;
+  width: 100vw;
+  height: 100vh;
+}
+
+.john-pork-map {
+  width: 100vw;
+  height: 100vh;
+}
+
+.decline {
+  cursor: pointer;
+  border-radius: 100%;
+  position: absolute;
+  top: 72%;
+  left: 14.1%;
+  width: 30.2%;
+  height: 17%;
+}
+
+.accept {
+  cursor: pointer;
+  border-radius: 100%;
+  position: absolute;
+  top: 72.5%;
+  left: 63%;
+  width: 29.4%;
+  height: 16.5%;
+}
+
+.john-pork-call {
+  width: 100vw;
+  height: 100vh;
+  user-select: none;
+}
+
+.span {
+  display: flex;
+  flex-direction: row;
+  gap: 5px;
+  align-items: center;
+}
+
+.subscribe {
+  border-radius: 100%;
+  border: solid brown 2px;
+  font-size: 18px;
+}
+
+.description-dialog {
+  border: 2px solid orange;
+  border-radius: 10px;
+  padding: 10px;
+  background-color: black;
+  color: orange;
+}
+
+.close {
+  border-radius: 5px;
+  padding: 5px;
+  background-color: orange;
+  color: black;
+  font-family: cursive;
+}
+
+.close:hover {
+  cursor: pointer;
+  background-color: purple;
+}
+
+.description-click {
+  text-decoration: underline;
+}
+
+.description-click:hover {
+  color: grey;
+  cursor: pointer;
+}
+
+.edit-profile {
+  border: 2px black solid;
+  border-radius: 50px;
+  color: orange;
+  font-family: cursive;
+  font-size: 15px;
+  background-color: purple;
+}
+
+.edit-profile:hover {
+  cursor: pointer;
+}
+
+.navbar-buttons {
+  position: absolute;
+  width: 100vw;
+  
+  display: flex;
+  align-items: center;
+  justify-content: end;
+  flex-direction: row;
+  gap: 5px;
+}
+
+.searchbar-container {
+  z-index: 2;
+}
+
+.searchbar {
+  background-color: black;
+  color: orange;
+  font-family: 'Times New Roman', Times, serif;
+  font-size: 24px;
+
+  border: none;
+  border-radius: 30px;
+  padding: 10px;
+}
+
+.search-videos {
+  padding: 10px;
+  display: flex;
+  flex-direction: column;
+  gap: 15px;
+}
+
+.search-video {
+  display: flex;
+  flex-direction: row;
+  gap: 15px;
+}
+
+.countdown {
+  color: black;
+  background-color: white;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  padding: 20px;
+  font-size: 30px;
+  text-align: center;
+}

+ 77 - 0
www/sunset.html

@@ -0,0 +1,77 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+    <meta charset="UTF-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0">
+</head>
+<body>
+    <div class="start" id="start">click to start</div>
+    
+    <video src="assets/piss baby.mp4" id="video"></video>
+
+    <div class="skib">
+        Skibidihub is over. Thank you for the memories. - SKibididihub team.
+    </div>
+
+    <script>
+        document.addEventListener("DOMContentLoaded", () => {
+            document.getElementById("start").addEventListener("click", () => {
+                document.getElementById("start").style.display = "none";
+                document.getElementById("video").play();
+            })
+
+            document.getElementById("video").addEventListener("ended", () => {
+                document.getElementById("video").style.display = "none";
+            })
+        })
+    </script>
+
+    <style>
+        .skib {
+            position: absolute;
+            background-color: black;
+            color: orange;
+            width: 100vw;
+            height: 100vh;
+            font-size: 32px;
+            display: flex;
+            align-items: center;
+            justify-content: center;
+            font-family: cursive;
+            
+        }
+
+        html, body {
+            background-color: black;
+            width: 100vw;
+            height: 100vh;
+            margin: 0;
+            overflow: hidden;
+        }
+
+        .start {
+            z-index: 6000;
+            position: absolute;
+            background-color: black;
+            color: white;
+            width: 100vw;
+            height: 100vh;
+            font-size: 32px;
+            display: flex;
+            align-items: center;
+            justify-content: center;
+        }
+
+        #video {
+            z-index: 3000;
+            position: absolute;
+            width: 100vw;
+            height: 100vh;
+        }
+
+        .start:hover {
+            cursor: pointer;
+        }
+    </style>
+</body>
+</html>

+ 47 - 0
www/upload.css

@@ -0,0 +1,47 @@
+html, body {
+    width: 100vw;
+    height: 100vh;
+    margin: 0;
+
+    background-color: black;
+    font-size: 20px;
+    color: orange;
+    display: flex;
+    justify-content: center;
+    align-items: center;
+    flex-direction: column;
+}
+
+.form {
+    padding: 10px;
+    background: #232323;
+    border-radius: 10px;
+    color: orange;
+    border: 2px solid black;
+    display: flex;
+    flex-direction: column;
+    gap: 10px;
+    width: 600px;
+}
+
+input {
+    background-color: black;
+    color: orange;
+    font-family:'Gill Sans', 'Gill Sans MT', Calibri, 'Trebuchet MS', sans-serif;
+    border: none;
+    border-radius: 20px;
+    font-size: 20px;
+}
+
+textarea {
+    background-color: purple;
+    color: white;
+    border: none;
+    text-decoration: dotted;
+    font-size: 17px;
+    font-family: cursive;
+}
+
+.disabled {
+    display: none;
+}

+ 39 - 0
www/upload.html

@@ -0,0 +1,39 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+    <meta charset="UTF-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0">
+    <link rel="stylesheet" href="upload.css">
+    <title>SkibidiHub - Upload</title>
+</head>
+<body>
+    <div class="container">
+        <form id="form" class="form">
+            <input type="text" name="uploader" class="disabled">
+
+            <label for="title">video title</label> <br />
+            <input type="text" name="title"> <br />
+
+            <label for="description">video description</label> <br />
+            <textarea name="description" rows="20"></textarea> <br />
+
+            <label for="video">video (must be .mp4)</label> <br />
+            <input type="file" name="video" accept="video/mp4"> <br />
+            
+            <label for="thumbnail">thumbnail (must be .jpg)</label> <br />
+            <input type="file" name="thumbnail" accept="image/jpeg"> <br />
+
+            <input type="submit" value="upload">
+        </form>
+
+        <br>
+        <progress id="progress-bar" value="0" max="100"></progress>
+        <label for="progress-bar" id="progress">0%</label>
+    </div>
+
+    <script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js.cookie.min.js "></script>
+    <script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
+    <script src="utils.js"></script>
+    <script src="upload.js"></script>
+</body>
+</html>

+ 49 - 0
www/upload.js

@@ -0,0 +1,49 @@
+const nanoid = (length) => {
+    const characters = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
+    let id = '';
+    for (let i = 0; i < length; i++) {
+        const randomIndex = Math.floor(Math.random() * characters.length);
+        id += characters[randomIndex];
+    }
+    return id;
+}
+
+document.addEventListener("DOMContentLoaded", () => {
+    if(Cookies.get("user") == null || Cookies.get("user") == undefined) {
+        alert("You need to be logged in to upload videos!");
+        window.location.pathname = "/";
+    }
+
+    document.getElementById("form").addEventListener("submit", (event) => {
+        const data = new FormData(document.getElementById("form"));
+        event.preventDefault();
+
+        data.set("uploader", Cookies.get("user"));
+        if(data.get("thumbnail").size < 1) return alert("Please enter valid form data");
+        if(data.get("video").size < 1) return alert("Please enter valid form data");
+        if(data.get("title").trim() == "") return alert("Please enter valid form data");
+
+        axios.post("/api/upload", data, {
+            headers: {
+                'Content-Type': 'multipart/form-data'
+            },
+
+            onUploadProgress: function(event) {
+                const percent = (event.loaded / event.total) * 100
+
+                document.getElementById("progress-bar").setAttribute('value', percent);
+                document.getElementById("progress").innerText = `${percent}%`
+            },
+        }).then(data => {
+            if(data.status == 201) {
+                alert("Upload successfull!");
+                window.location.pathname = `/video/${data.data.id}`
+            }
+        }).catch(error => {
+            if(error.response) {
+                alert(error.response.data.message)
+            }
+            throw new Error(`upload error: ${error}`);
+        })
+    })
+});

+ 90 - 0
www/user.html

@@ -0,0 +1,90 @@
+<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <meta charset="UTF-8" />
+    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+    <script src=" https://cdn.jsdelivr.net/npm/[email protected]/dist/js.cookie.min.js "></script>
+
+    <meta content="Skibidihub" property="og:title" />
+    <meta content="come and Look at all of the videos made by this user on skibidihub !" property="og:description" />
+    <!-- Yes you need to set the domain manually for every html file. No i dont care and i cant fix it -->
+    <meta content="http://skibidihub.buttplugstudios.xyz/" property="og:url" id="embed-url" />
+    <meta content="http://skibidihub.buttplugstudios.xyz/api/thumbnail/a" property="og:image" id="embed-image" />
+    <meta name="twitter:card" content="summary_large_image">
+
+    <title>SkibidiHub - User</title>
+    <link href="../style.css" rel="stylesheet" />
+  </head>
+  <body>
+    <div class="navbar">
+      <div class="navbar-spacer"></div>
+      <a href="/" class="navbar-img-anchor"><img class="navbar-img" src="../assets/logo.png" /></a>
+      <div class="navbar-spacer"></div>
+      <div class="searchbar-container">
+        <form id="searchbar-form" action="/search">
+          <input type="text" name="query" class="searchbar" placeholder="search">
+        </form>
+      </div>
+      <div class="navbar-buttons">
+        <a class="anchor" href="/login">
+          <button class="login" id="login-button">Login</button>
+        </a>
+  
+        <a href="/upload" class="anchor">
+          <button id="upload-button" class="login disabled">Upload</button>
+        </a>
+        
+        <a class="anchor" id="account-button-anchor">
+          <button id="account-button" class="login disabled">My Account</button>
+        </a>
+        <button id="logout-button" onclick="Cookies.remove('user'); Cookies.remove('token'); window.location.reload()" class="login disabled">Logout</button>
+        <div class="navbar-spacer"></div>
+      </div>
+    </div>
+
+    <div class="user-info">
+      <div class="user-info-extra">
+        <h2 id="subscribers">LOADING</h2>
+        <h3 id="social-score">LOADING</h3>
+        <p id="description" class="description">LOADING</p>
+      </div>
+
+      <img src="./../assets/user.png" class="user-pfp" />
+      <h1>
+        Videos published by: <br />
+        <div class="span">
+          <p id="user-info-name" class="user-info-name"></p>
+          <img src="/assets/verified.png" width="30" id="verified" class="disabled">        
+          <a href="caca" id="website-anchor">
+            <img src="/assets/website.png" width="50">
+          </a>
+          <button class="subscribe" id="subscribe">
+            Subscribe
+          </button>
+          <a href="/editProfile">
+            <button class="edit-profile disabled" id="edit-profile">edit profile</button>
+          </a>
+        </div>
+      </h1>
+    </div>
+
+    <dialog id="description-dialog" class="description-dialog">
+      <button onclick="document.getElementById('description-dialog').close()" class="close">X</button>
+      <br />
+
+      <p id="description-dialog-text"></p>
+    </dialog>
+
+    <h1 id="loading" class="no-videos">
+      LOADING,,,,, <br />
+
+      <img src="./../assets/loading.gif" height="300px" />
+    </h1>
+    <h1 id="no-videos" class="no-videos disabled"></h1>
+    <div class="videos" id="videos"></div>
+
+    <script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
+    <script src="./../utils.js"></script>
+    <script src="./../user.js"></script>
+  </body>
+</html>

+ 123 - 0
www/user.js

@@ -0,0 +1,123 @@
+document.addEventListener("DOMContentLoaded", async () => {
+  const id = decodeURIComponent(window.location.pathname.split("/")[2]);
+  document.getElementById("account-button-anchor").href = `/user/${encodeURIComponent(Cookies.get('user'))}`
+
+  document.getElementById(
+    "no-videos"
+  ).innerText = `There seems to be no videos published by user of the site "skibidihub" by the name of "${id}".`;
+
+  const user = await getUserInfo(id)
+  if(user.verified) document.getElementById("verified").classList.remove('disabled');
+  if(Cookies.get("user") === id) document.getElementById("edit-profile").classList.remove("disabled")
+
+  if(user.website) {
+    document.getElementById("website-anchor").href = user.website;
+  } else {
+    document.getElementById("website-anchor").remove();
+  }
+  document.getElementById("subscribers").innerText = `${user.subscribers} subscribers`;
+  document.getElementById("social-score").innerText = `${user.social_score} social credit score`
+  if(user.description) {
+    if(user.description.length > 18) {
+      document.getElementById("description").innerText = (user.description.slice(0, -(user.description.length - 15)) + "...").replace(/(\r\n|\n|\r)/gm, " ");
+      document.getElementById("description").classList.add("description-click");
+      document.getElementById("description").addEventListener("click", () => {
+        document.getElementById("description-dialog-text").innerText = user.description;
+        document.getElementById("description-dialog").showModal()
+      })
+    } else {
+      document.getElementById("description").innerText = user.description.replace(/(\r\n|\n|\r)/gm, " ")
+    } 
+  } else {
+    document.getElementById("description").innerText = "no description set"
+  }
+  
+  // Check if the user is already subscribed
+  if(!localStorage.getItem("subscribed")) localStorage.setItem("subscribed", JSON.stringify([]))
+  // If so, disable the subscribe button
+  if(JSON.parse(localStorage.getItem("subscribed")).includes(user.name)) document.getElementById("subscribe").setAttribute("disabled", "true");
+
+  document.getElementById("subscribe").onclick = function(event) { subscribe(user) }
+
+  if (Cookies.get("user") != null) {
+    document.getElementById("user-info-name").innerText = id;
+    document.getElementById("login-button").classList.add("disabled");
+    document.getElementById("logout-button").classList.remove("disabled");
+    document.getElementById("account-button").classList.remove("disabled");
+    document.getElementById("upload-button").classList.remove("disabled");
+  } else {
+    document.getElementById("user-info-name").innerText =
+      "⚠️⚠️⚠️⚠️SIGN IN to see this EPIC content❌❌💋🩻";
+  }
+
+  const videos = await getAllUserVideos(encodeURIComponent(id))
+  document.getElementById("loading").classList.add("disabled");
+  if (videos.data.length > 0) {
+    videos.data.forEach((video) => {
+      makeVideo(video.id, video);
+    });
+  } else {
+    document.getElementById("no-videos").classList.remove("disabled");
+  }
+});
+
+async function makeVideo(id, info) {
+  const video = document.createElement("a");
+  video.href = `/video/${id}`
+  video.classList.add("video");
+
+  const img = document.createElement("img");
+  img.classList.add("thumbnail");
+  img.setAttribute("src", `/api/thumbnail/${id}`);
+  video.appendChild(img);
+
+  const videoInfoContainer = document.createElement("div");
+  videoInfoContainer.classList.add("video-info-container");
+  video.appendChild(videoInfoContainer);
+
+  const videoTitle = document.createElement("h3");
+  videoTitle.classList.add("video-title");
+  const title = info.title
+  if(info.title.length > 25) {
+    videoTitle.innerText = title.slice(0, -(info.title.length - 22)) + "...";
+  } else {
+    videoTitle.innerText = title
+  }
+  videoTitle.onclick = function (event) {
+    window.location.pathname = `/video/${id}`;
+  };
+  videoInfoContainer.appendChild(videoTitle);
+
+  const videoUploader = document.createElement("a");
+  videoUploader.href = `/user/${encodeURIComponent(info.uploader)}`
+  videoUploader.classList.add("video-uploader");
+  videoUploader.innerText = `published by: ${info.uploader}`;
+  videoInfoContainer.appendChild(videoUploader);
+
+  document.getElementById("videos").appendChild(video);
+}
+
+// TODO: for now subscribe function is fine but maybe polish it up a little
+function subscribe(authorInfo) {
+  return axios.get(`/api/subscribe/${encodeURIComponent(authorInfo.name)}`).then(data => {
+    if(data.status != 200) {
+      return document.getElementById("subscribers").innerText = "ERROR ERROR ERROE SEREVER ERROR!!!!"
+    }
+
+    // Write to localStorage
+    if(!localStorage.getItem("subscribed")) localStorage.setItem("subscribed", JSON.stringify([]))
+    const subscribed = JSON.parse(localStorage.getItem("subscribed"));
+    subscribed.push(authorInfo.name);
+
+    localStorage.setItem("subscribed", JSON.stringify(subscribed))
+
+    // Disable subscribed button
+    document.getElementById("subscribe").setAttribute("disabled", "true");
+
+    // Update subscribed counter
+    document.getElementById("subscribers").innerText = `${authorInfo.subscribers + 1} subscribers`;
+  }).catch(error => {
+    console.error(error);
+    return document.getElementById("subscribers").innerText = "ERROR ERROR ERROE SEREVER ERROR!!!!"
+  })
+}

+ 52 - 0
www/utils.js

@@ -0,0 +1,52 @@
+async function getAllUserVideos(userID) {
+  return axios.get(`/api/userVideos/${userID}`).then(response => {
+    return response.data;
+  }).catch(error => {
+    throw new Error(`getAllUserVideos() error: ${error}`);
+  })
+}
+
+async function getUserInfo(name) {
+  return axios.get(`/api/userInfo/${encodeURIComponent(name)}`).then(response => {
+    return response.data;
+  }).catch(error => {
+    throw new Error(`getUserInfo() error: ${error}`);
+  })
+}
+
+async function getInfo(id) {
+  return await axios.get(`/api/videoInfo/${id}`, {
+    responseType: "json",
+  }).then(response => {
+    return response.data;
+  }).catch(error => {
+    throw new Error(`getInfo() error: ${error}`);
+  })
+}
+
+async function getComments(id) {
+  return await axios.get(`/api/comments/${id}`).then(response => {
+    return response.data;
+  }).catch(error => {
+    throw new Error(`getComments() error: ${error}`)
+  })
+}
+
+function isLoggedIn() {
+  return true ? Cookies.get("user") != null : false;
+}
+
+function getToken(user) {
+  if(user == undefined || user == null) {
+    return "peepeecaca unauthorized";
+  }
+  return (
+    user + "*&*&*&*&&&&*&&&&*&****&***&*nexacopicloves15yearoldchineseboys"
+  );
+}
+
+function parseTimestamp(str) {
+  let date = str.split("T")[0];
+  let timestamp = str.split("T")[1].split("+")[0].split(".")[0];
+  return { date: date, timestamp: timestamp };
+}

+ 104 - 0
www/video.html

@@ -0,0 +1,104 @@
+<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <meta charset="UTF-8" />
+    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+    <link href="../style.css" rel="stylesheet" />
+    <title>▶️SkibidiHub - Video</title>
+  </head>
+  <body>
+    <div class="navbar">
+      <div class="navbar-spacer"></div>
+      <a href="/" class="navbar-img-anchor"><img class="navbar-img" src="../assets/logo.png" /></a>
+      <div class="navbar-spacer"></div>
+      <div class="searchbar-container">
+        <form id="searchbar-form" action="/search">
+          <input type="text" name="query" class="searchbar" placeholder="search">
+        </form>
+      </div>
+      <div class="navbar-buttons">
+        <a class="anchor" href="/login">
+          <button class="login" id="login-button">Login</button>
+        </a>
+  
+        <a href="/upload" class="anchor">
+          <button id="upload-button" class="login disabled">Upload</button>
+        </a>
+        
+        <a class="anchor" id="account-button-anchor">
+          <button id="account-button" class="login disabled">My Account</button>
+        </a>
+        <button id="logout-button" onclick="Cookies.remove('user'); Cookies.remove('token'); window.location.reload()" class="login disabled">Logout</button>
+        <div class="navbar-spacer"></div>
+      </div>
+    </div>
+
+    <div class="video-container">
+
+      <video id="video" class="video-video" autoplay controls></video>
+
+      <img src="./../assets/loading.gif" width="300" id="loading">
+    </div>
+
+    <div class="video-info">
+      <h1 id="video-title">LOADING</h1>
+      <div class="video-actions">
+        <div class="span">
+          <a id="video-author-anchor" class="video-author"><h2 id="video-author" class="video-author">LOADING</h2></a>
+          <img src="/assets/verified.png" width="30" id="verified" class="disabled">       
+        </div>
+        <div class="navbar-spacer"></div>
+        <button
+          class="like"
+          onclick="like(window.location.pathname.split('/')[2])"
+        >
+          <img src="../assets/like.jpg" width="50" />
+          <p id="video-likes">LOADING</p>
+        </button>
+        <button
+          class="dislike"
+          onclick="dislike(window.location.pathname.split('/')[2])"
+        >
+          <img src="../assets/dislike.jpg" width="50" />
+          <p id="video-dislikes">LOADING</p>
+        </button>
+        <button class="subscribe" id="subscribe">
+          Subscribe
+        </button>
+      </div>
+    </div>
+
+
+    <div class="video-description">
+      <h3 id="author-info"></h3>
+      <h2 id="video-date">LOADING</h2>
+
+      <p id="video-description">LOADING</p>
+    </div>
+
+    <div id="comments" class="comments">
+      <h1>Comments</h1>
+      <hr />
+      <form id="comment-form">
+        <label for="text">Comment...</label> <br />
+        <textarea
+          name="text"
+          rows="5"
+          cols="50"
+          placeholder="Comment here what you think about the video!"
+          id="comment-textarea"
+          class="comment-textarea"
+        >
+        </textarea>
+        <br />
+        <input type="submit" value="comment!" />
+      </form>
+      <hr />
+    </div>
+
+    <script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
+    <script src=" https://cdn.jsdelivr.net/npm/[email protected]/dist/js.cookie.min.js "></script>
+    <script src="./../utils.js"></script>
+    <script src="./../video.js"></script>
+  </body>
+</html>

+ 207 - 0
www/video.js

@@ -0,0 +1,207 @@
+
+
+let liked = false;
+let disliked = false;
+
+async function like(id) {
+  if(!Cookies.get("user")) {
+    return alert("you must be loggefd in to like!");
+  }
+
+  if (liked)
+    return alert(
+      "you already like video you can not like it anymore beacuse you aleady like video!!!"
+    );
+  
+  axios.post(`/api/like/${id}`, {}).then(response => {
+    if (response.data.status == 204) {
+      liked = true;
+      document.getElementById("video-likes").innerText =
+        parseInt(document.getElementById("video-likes").innerText) + 1;
+    } else {
+      document.getElementById("video-likes").innerText =
+        "ERROR ERROR ERROR ERROR ERRROR SERVCER ERROR";
+    }
+  }).catch(error => {
+    throw new Error(`like() error: ${error}`);
+  })
+}
+
+async function dislike(id) {
+  if(!Cookies.get("user")) {
+    return alert("you must be loggefd in to dislike!");
+  }
+
+  if (disliked)
+    return alert(
+      "you already dislike video you can not dislike it anymore beacuse you aleady dislike video!!!"
+    );
+  
+  axios.post(`/api/dislike/${id}`, {}).then(response => {
+    if (response.data.status == 204) {
+      disliked = true;
+      document.getElementById("video-dislikes").innerText =
+        parseInt(document.getElementById("video-dislikes").innerText) + 1;
+    } else {
+      document.getElementById("video-dislikes").innerText =
+        "ERROR ERROR ERROR ERROR ERRROR SERVCER ERROR";
+    }
+  }).catch(error => {
+    throw new Error(`dislike() error: ${error}`);
+  })
+}
+
+function gotoUser(user) {
+  window.location.pathname = `/user/${user}`;
+}
+
+document.addEventListener("DOMContentLoaded", async () => {
+  let video = document.getElementById("video");
+  let id = window.location.pathname.split("/")[2];
+
+  document.getElementById("account-button-anchor").href = `/user/${encodeURIComponent(Cookies.get('user'))}`
+  if (Cookies.get("user") != null) {
+    document.getElementById("login-button").classList.add("disabled");
+    document.getElementById("logout-button").classList.remove("disabled");
+    document.getElementById("account-button").classList.remove("disabled");
+    document.getElementById("upload-button").classList.remove("disabled");
+  }
+
+  document
+    .getElementById("comment-form")
+    .addEventListener("submit", async (e) => {
+      e.preventDefault();
+      var formData = new FormData(document.getElementById("comment-form"));
+      var json = Object.fromEntries(formData);
+      const user = Cookies.get("user");
+
+      if (!user) {
+        alert("You must log in to comment.");
+        return;
+      }
+
+      await axios.post(`/api/comment`, {
+        commenter: user,
+        videoID: id,
+        text: json["text"]
+      }, {
+        headers: {
+          'Authorization': getToken(Cookies.get("user"))
+        }
+      }).then(response => {
+        if (response.data.status == 201) {
+          document.getElementById("comment-textarea").innerText = ""
+          makeComment(Cookies.get("user"), "right now", json["text"]);
+        } else {
+          alert("ERROR ERROR ERROR ERORROR SERVCER ERROR!!!");
+        }
+      }).catch(error => {
+        throw new Error(`comment error: ${error}`)
+      })
+    });
+  
+  // Load video
+  let source = document.createElement("source");
+  source.setAttribute("src", `/api/video/${id}`)
+  source.setAttribute("type", "video/mp4");
+  video.appendChild(source);
+  video.setAttribute("poster", `/api/thumbnail/${id}`);
+
+  // Load video info
+  const info = await getInfo(id);
+  document.getElementById("video-title").innerText = info.title;
+  document.getElementById(
+    "video-author"
+  ).innerText = `Uploaded by: ${info.uploader}`;
+  document.getElementById("video-author-anchor").href = `/user/${encodeURIComponent(info.uploader)}`;
+  document.getElementById("video-date").innerText =
+    parseTimestamp(info.uploaded_at).date +
+    " " +
+    parseTimestamp(info.uploaded_at).timestamp;
+  document.getElementById("video-description").innerText = info.description;
+  document.getElementById("video-likes").innerText = info.likes;
+  document.getElementById("video-dislikes").innerText = info.dislikes;
+
+  // Load author info
+  const authorInfo = await getUserInfo(info.uploader);
+  if(authorInfo.verified) document.getElementById("verified").classList.remove('disabled')
+  document.getElementById("author-info").innerText = `${authorInfo.subscribers} subscribers ඞ ${authorInfo.social_score} social credit score`;
+
+  // Check if the user is already subscribed
+  if(!localStorage.getItem("subscribed")) localStorage.setItem("subscribed", JSON.stringify([]))
+  // If so, disable the subscribe button
+  if(JSON.parse(localStorage.getItem("subscribed")).includes(authorInfo.name)) document.getElementById("subscribe").setAttribute("disabled", "true");
+
+  document.getElementById("subscribe").onclick = function(event) { subscribe(authorInfo) }
+
+  // Load comments
+  const comments = await getComments(id);
+  comments.forEach((comment) => {
+    makeComment(
+      comment.commenter,
+      parseTimestamp(comment.created_at).date +
+        " " +
+        parseTimestamp(comment.created_at).timestamp,
+      comment.text
+    );
+  });
+
+  document.getElementById("loading").classList.add("disabled")
+});
+
+function makeComment(username, timestamp, content) {
+  let comment = document.createElement("div");
+  comment.setAttribute("class", "comment");
+
+  let commentSpan = document.createElement("div");
+  commentSpan.setAttribute("class", "comment-span");
+  comment.appendChild(commentSpan);
+
+  let userAnchor = document.createElement("a");
+  userAnchor.href = `/user/${encodeURIComponent(username)}`;
+  userAnchor.classList.add("clickable-user")
+  commentSpan.appendChild(userAnchor);
+
+  let user = document.createElement("h2");
+  user.innerText = username;
+  user.setAttribute("class", "clickable-user");
+  userAnchor.appendChild(user);
+
+  let date = document.createElement("h6");
+  date.innerText = timestamp;
+  commentSpan.appendChild(date);
+
+  let text = document.createElement("p");
+  text.innerText = content;
+  comment.appendChild(text);
+  document.getElementById("comments").appendChild(comment);
+}
+
+function home() {
+  window.location.pathname = "/";
+}
+
+// TODO: for now subscribe function is fine but maybe polish it up a little
+function subscribe(authorInfo) {
+  return axios.get(`/api/subscribe/${encodeURIComponent(authorInfo.name)}`).then(data => {
+    if(data.status != 200) {
+      return document.getElementById("author-info").innerText = "ERROR ERROR ERROE SEREVER ERROR!!!!"
+    }
+
+    // Write to localStorage
+    if(!localStorage.getItem("subscribed")) localStorage.setItem("subscribed", JSON.stringify([]))
+    const subscribed = JSON.parse(localStorage.getItem("subscribed"));
+    subscribed.push(authorInfo.name);
+
+    localStorage.setItem("subscribed", JSON.stringify(subscribed))
+
+    // Disable subscribed button
+    document.getElementById("subscribe").setAttribute("disabled", "true");
+
+    // Update subscribed counter
+    document.getElementById("author-info").innerText = `${authorInfo.subscribers + 1} subscribers ඞ ${authorInfo.social_score} social credit score`;
+  }).catch(error => {
+    console.error(error);
+    return document.getElementById("author-info").innerText = "ERROR ERROR ERROE SEREVER ERROR!!!!"
+  })
+}