selfhostable, read-only reddit client
16
fork

Configure Feed

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

rework invite and admins a bit

Akshay 622c5fee 3551ff7a

+103 -136
+1 -27
flake.nix
··· 42 42 outputHashAlgo = "sha256"; 43 43 outputHashMode = "recursive"; 44 44 }; 45 - readit-gen-invite = with final; 46 - stdenv.mkDerivation { 47 - pname = "readit-gen-invite"; 48 - version = "0.0.1"; 49 - src = ./scripts; 50 - nativeBuildInputs = [makeBinaryWrapper]; 51 - buildInputs = [bun]; 52 - 53 - buildPhase = '' 54 - runHook preBuild 55 - runHook postBuild 56 - ''; 57 - 58 - dontFixup = true; 59 - 60 - installPhase = '' 61 - runHook preInstall 62 - 63 - mkdir -p $out/bin 64 - cp -R ./* $out 65 - 66 - makeBinaryWrapper ${bun}/bin/bun $out/bin/$pname \ 67 - --prefix PATH : ${lib.makeBinPath [bun]} \ 68 - --add-flags "run --prefer-offline --no-install $out/gen-invite.js" 69 - ''; 70 - }; 71 45 readit = with final; 72 46 stdenv.mkDerivation { 73 47 pname = "readit"; ··· 110 84 }); 111 85 112 86 packages = forAllSystems (system: { 113 - inherit (nixpkgsFor."${system}") readit readit-gen-invite node_modules; 87 + inherit (nixpkgsFor."${system}") readit node_modules; 114 88 }); 115 89 116 90 defaultPackage = forAllSystems (system: nixpkgsFor."${system}".readit);
-34
scripts/gen-invite.js
··· 1 - import { Database } from "bun:sqlite"; 2 - 3 - const command = process.argv[2]; 4 - 5 - const dbPath = process.argv[3] ? process.argv[3] : "readit.db"; 6 - const db = new Database(dbPath, { 7 - strict: true, 8 - }); 9 - 10 - db.run(` 11 - CREATE TABLE IF NOT EXISTS invites ( 12 - id INTEGER PRIMARY KEY AUTOINCREMENT, 13 - token TEXT NOT NULL, 14 - createdAt TIMESTAMP DEFAULT CURRENT_TIMESTAMP, 15 - usedAt TIMESTAMP 16 - ) 17 - `); 18 - 19 - if (command === "create") { 20 - createInvite(); 21 - } else { 22 - console.log("requires an arg"); 23 - } 24 - 25 - function generateInviteToken() { 26 - const hasher = new Bun.CryptoHasher("sha256", "super-secret-invite-key"); 27 - return hasher.update(Math.random().toString()).digest("hex").slice(0, 10); 28 - } 29 - 30 - function createInvite() { 31 - const token = generateInviteToken(); 32 - db.run("INSERT INTO invites (token) VALUES ($token)", { token }); 33 - console.log(`Invite token created: ${token}`); 34 - }
+13 -4
src/db.js
··· 15 15 } 16 16 17 17 // users table 18 - db.query(` 18 + db.run(` 19 19 CREATE TABLE IF NOT EXISTS users ( 20 20 id INTEGER PRIMARY KEY AUTOINCREMENT, 21 21 username TEXT UNIQUE, 22 22 password_hash TEXT 23 23 ) 24 - `).run(); 24 + `); 25 25 26 26 // subs table 27 - db.query(` 27 + db.run(` 28 28 CREATE TABLE IF NOT EXISTS subscriptions ( 29 29 id INTEGER PRIMARY KEY AUTOINCREMENT, 30 30 user_id INTEGER, ··· 32 32 FOREIGN KEY(user_id) REFERENCES users(id), 33 33 UNIQUE(user_id, subreddit) 34 34 ) 35 - `).run(); 35 + `); 36 + 37 + db.run(` 38 + CREATE TABLE IF NOT EXISTS invites ( 39 + id INTEGER PRIMARY KEY AUTOINCREMENT, 40 + token TEXT NOT NULL, 41 + createdAt TIMESTAMP DEFAULT CURRENT_TIMESTAMP, 42 + usedAt TIMESTAMP 43 + ) 44 + `); 36 45 37 46 // migrations table 38 47 db.query(`
+8
src/invite.js
··· 1 1 const { db } = require("./db"); 2 2 3 3 const validateInviteToken = async (req, res, next) => { 4 + const isFirstUser = db.query("SELECT 1 FROM users LIMIT 1").get() === null; 5 + 6 + if (isFirstUser) { 7 + req.isFirstUser = true; 8 + next(); 9 + return; 10 + } 11 + 4 12 const token = req.query.token; 5 13 6 14 if (!token) {
+21 -40
src/mixins/post.pug
··· 1 1 include ../utils 2 + include postUtils 2 3 mixin post(p) 3 4 article.post 4 5 div.post-container ··· 20 21 |  ·  21 22 a(href=`/comments/${p.id}`) #{fmtnum (p.num_comments)} ↩ 22 23 div.media-preview 23 - if p.is_gallery && p.is_gallery == true 24 - if p.gallery_data 25 - if p.gallery_data.items 26 - - var item = p.gallery_data.items[0] 27 - - var url = `https://i.redd.it/${item.media_id}.jpg` 28 - img(src=url onclick=`toggleDetails('${p.id}')`) 29 - else if p.post_hint == "image" && p.thumbnail && p.thumbnail != "self" && p.thumbnail != "default" 24 + if isPostGallery(p) 25 + - var item = postGalleryItems(p)[0] 26 + img(src=item.url onclick=`toggleDetails('${p.id}')`) 27 + else if isPostImage(p) 30 28 img(src=p.thumbnail onclick=`toggleDetails('${p.id}')`) 31 - else if p.post_hint == "hosted:video" 29 + else if isPostVideo(p) 32 30 - var url = p.secure_media.reddit_video.scrubber_media_url 33 31 video(src=url data-dashjs-player width='100px' height='100px' onclick=`toggleDetails('${p.id}')`) 34 - else if p.post_hint == "link" 32 + else if isPostLink(p) 35 33 a(href=p.url) 36 34 | ↗ 37 35 38 - if p.is_gallery && p.is_gallery == true 39 - if p.gallery_data 40 - if p.gallery_data.items 41 - details(id=`${p.id}`) 42 - summary.expand-post expand gallery 43 - div.gallery 44 - - var total = p.gallery_data.items.length 45 - - var idx = 0 46 - - var metadata = p.media_metadata 47 - - 48 - var img_ext = (id) => { 49 - if (metadata[id].status == 'valid') { 50 - return stripPrefix(metadata[id].m, "image/"); 51 - } else { 52 - // dosent matter 53 - return 'jpg'; 54 - } 55 - } 56 - each item in p.gallery_data.items 57 - - var id = item.media_id 58 - - var ext = img_ext(item.media_id) 59 - - var url = `https://i.redd.it/${id}.${ext}` 60 - div.gallery-item 61 - a(href=`/media/${url}`) 62 - img(src=url loading="lazy") 63 - div.gallery-item-idx 64 - | #{`${++idx}/${total}`} 65 - button(onclick=`toggleDetails('${p.id}')`) close 66 - if p.post_hint == "image" && p.thumbnail && p.thumbnail != "self" && p.thumbnail != "default" 36 + if isPostGallery(p) 37 + details(id=`${p.id}`) 38 + summary.expand-post expand gallery 39 + div.gallery 40 + each item in postGalleryItems(p) 41 + div.gallery-item 42 + div.gallery-item-idx 43 + | #{`${item.idx}/${item.total}`} 44 + a(href=`/media/${item.url}`) 45 + img(src=item.url loading="lazy") 46 + button(onclick=`toggleDetails('${p.id}')`) close 47 + else if isPostImage(p) 67 48 details(id=`${p.id}`) 68 49 summary.expand-post expand image 69 50 a(href=`/media/${p.url}`) 70 51 img(src=p.url loading="lazy").post-media 71 52 button(onclick=`toggleDetails('${p.id}')`) close 72 - else if p.post_hint == "hosted:video" 53 + else if isPostVideo(p) 73 54 details(id=`${p.id}`) 74 55 summary.expand-post expand video 75 56 - var url = p.secure_media.reddit_video.dash_url 76 57 video(src=url controls data-dashjs-player loading="lazy").post-media 77 58 button(onclick=`toggleDetails('${p.id}')`) close 78 - else if p.post_hint == "link" 59 + else if isPostLink(p) 79 60 details(id=`${p.id}`) 80 61 summary.expand-post expand link 81 62 a(href=`${p.url}`)
+39
src/mixins/postUtils.pug
··· 1 + - 2 + function isPostGallery(p) { 3 + return (p.is_gallery && p.is_gallery == true); 4 + } 5 + - 6 + function isPostImage(p) { 7 + return (p.post_hint == "image" && p.thumbnail && p.thumbnail != "self" && p.thumbnail != "default"); 8 + } 9 + - 10 + function isPostVideo(p) { 11 + return (p.post_hint == "hosted:video"); 12 + } 13 + - 14 + function isPostLink(p) { 15 + return (p.post_hint == "link"); 16 + } 17 + - 18 + function imgExt(p, id) { 19 + var metadata = p.media_metadata; 20 + if (metadata[id].status == 'valid') { 21 + return stripPrefix(metadata[id].m, "image/"); 22 + } else { 23 + // dosent matter 24 + return 'jpg'; 25 + } 26 + } 27 + - 28 + function postGalleryItems(p) { 29 + if (p.gallery_data && p.gallery_data.items) { 30 + return p.gallery_data.items.map((item, idx) => ({ 31 + id: item.media_id, 32 + url: `https://i.redd.it/${item.media_id}.${imgExt(p, item.media_id)}`, 33 + total: p.gallery_data.items.length, 34 + idx: idx+1, 35 + })); 36 + } else { 37 + return null; 38 + } 39 + }
+9 -15
src/routes/index.js
··· 136 136 } 137 137 138 138 try { 139 - db.run(` 140 - CREATE TABLE IF NOT EXISTS invites ( 141 - id INTEGER PRIMARY KEY AUTOINCREMENT, 142 - token TEXT NOT NULL, 143 - createdAt TIMESTAMP DEFAULT CURRENT_TIMESTAMP, 144 - usedAt TIMESTAMP 145 - ) 146 - `); 147 - 148 139 createInvite(); 149 140 return res.redirect("/dashboard"); 150 141 } catch (err) { ··· 201 192 try { 202 193 const hashedPassword = await Bun.password.hash(password); 203 194 204 - db.query( 205 - "UPDATE invites SET usedAt = CURRENT_TIMESTAMP WHERE id = $id", 206 - ).run({ 207 - id: req.invite.id, 208 - }); 195 + if (!req.isFirstUser) { 196 + db.query( 197 + "UPDATE invites SET usedAt = CURRENT_TIMESTAMP WHERE id = $id", 198 + ).run({ 199 + id: req.invite.id, 200 + }); 201 + } 209 202 210 203 const insertedRecord = db 211 204 .query( 212 - "INSERT INTO users (username, password_hash) VALUES ($username, $hashedPassword)", 205 + "INSERT INTO users (username, password_hash, isAdmin) VALUES ($username, $hashedPassword, $isAdmin)", 213 206 ) 214 207 .run({ 215 208 username, 216 209 hashedPassword, 210 + isAdmin: req.isFirstUser ? 1 : 0, 217 211 }); 218 212 const id = insertedRecord.lastInsertRowid; 219 213 const token = jwt.sign({ username, id }, JWT_KEY, { expiresIn: "5d" });
+12 -16
src/views/comments.pug
··· 1 1 include ../mixins/comment 2 2 include ../mixins/header 3 3 include ../mixins/head 4 + include ../mixins/postUtils 4 5 include ../utils 5 6 6 7 - var post = data.post ··· 33 34 h2.post-title 34 35 != post.title 35 36 36 - if post.is_gallery && post.is_gallery == true 37 - if post.gallery_data 38 - if post.gallery_data.items 39 - div.gallery 40 - - var total = post.gallery_data.items.length 41 - - var idx = 0 42 - each item in post.gallery_data.items 43 - - var url = `https://i.redd.it/${item.media_id}.jpg` 44 - div.gallery-item 45 - div.gallery-item-idx 46 - | #{`${++idx}/${total}`} 47 - a(href=`/media/${url}`) 48 - img(src=url loading="lazy") 49 - else if post.post_hint == "image" && post.thumbnail && post.thumbnail != "self" && post.thumbnail != "default" 37 + if isPostGallery(post) 38 + div.gallery 39 + each item in postGalleryItems(post) 40 + div.gallery-item 41 + div.gallery-item-idx 42 + | #{`${item.idx}/${item.total}`} 43 + a(href=`/media/${item.url}`) 44 + img(src=item.url loading="lazy") 45 + else if isPostImage(post) 50 46 img(src=post.url).post-media 51 - else if post.post_hint == 'hosted:video' 47 + else if isPostVideo(post) 52 48 - var url = post.secure_media.reddit_video.dash_url 53 49 video(controls data-dashjs-player src=`${url}`).post-media 54 - else if post.post_hint == "link" 50 + else if isPostLink(post) 55 51 a(href=post.url) 56 52 | #{post.url} 57 53