// Get the libraries const fs = require('fs'); // For modifying and reading files const express = require('express'); // For running a webserver in nodejs const showdown = require('showdown') // For converting markdown to html on demand, https://showdownjs.com/ const crypto = require('crypto'); // For encrypting passwords, I use sha512 // fromUnixTime(): Create a date from a Unix timestamp (in seconds). Decimal values will be discarded. // format(): Return the formatted date string in the given format. The result may vary by locale. // getUnixTime(): Get the seconds timestamp of the given date. // find out more at https://date-fns.org/ \or docs: https://date-fns.org/docs/Getting-Started const { fromUnixTime, format, getUnixTime } = require("date-fns") // A date utility library const ejs = require("ejs") const func = require("./functions.js") const init = require("./initialise.js") // There's only one possible argument, so we can just check if the user passed that one // TODO I plan on adding more such as --help and --post so I should make this more robust at some point if (process.argv[2] == "--first-time") { init.initialise() // Creates any files such users.js, posts.js, comments.js or config.js if they are not present } // Define the modules now so they are global let users // contains a list of users, each user is an object containing username,prettyname,hash and description let posts // contains a list of posts, let comments // contains a list of comments let config // contains a set of configuration for the site, see example-config.js for an example try { // We're going to try and import the modules, users = require('../data/users.json'); posts = require('../data/posts.json'); comments = require('../data/comments.json'); config = require('../config.json'); } catch (error) { // if they don't all import then // inform the user to pass --first-time and exit with an error code console.log("A file is missing!") console.log("Run with --first-time to initialise the program") console.log(error) process.exit(1) } // https://showdownjs.com/docs/available-options let converter = new showdown.Converter({ simpleLineBreaks: true, // Parse line breaks as
in paragraphs (GitHub-style behavior). tables: true, // Enable support for tables syntax. strikethrough: true, // Enable support for strikethrough: ~~text~~ tasklists: true, // Enable support for GitHub style tasklists. - [x] and - [ ] encodeEmails: true, //Enable automatic obfuscation of email addresses. emails are encoded via character entities headerLevelStart: 3, //Set starting level for the heading tags. }) // Define stuff to do with express (nodejs webserver) const app = express(); app.use(express.urlencoded({ extended: true })); app.use(express.json()); app.use(express.static(config.root_path)); // set the view engine to ejs app.set('view engine', 'ejs'); app.set('views', '../views') ////////////////////// SYNDICATION //////////////////////// // RSS protocol gets app.get(config.rss_url, (req,res) => { if (config.rss == false) { res.render("partials/message", { message: config.string.rss_disabled, config: config, }) } else { res.setHeader('content-type', 'application/rss+xml'); res.render("syndication/rss", { config, posts, converter, func, }) }; }); ///////////////////// Standard Pages ////////////////////// app.get("/", (req,res) => { // Increment the hitcount if (config.enable_hitcount) { let hitcount = parseInt(fs.readFileSync('../data/hitcount.txt')) hitcount += 1 console.log(`/ Is loaded, hitcount: ${hitcount}`) fs.writeFileSync(`../data/hitcount.txt`, `${hitcount}`, 'utf-8'); } res.render("pages/timeline", { config, posts, users, comments: comments.comments, hitcount: fs.readFileSync("../data/hitcount.txt"), fromUnixTime, format, getUnixTime, func, }) }); // / app.get("/user/:username", (req, res) => { const userID = func.get_userID(req.params.username) console.log(userID) console.log(users[userID].prettyname) res.render("pages/user", { config: config, posts: posts, user: users[userID], userID: userID, comments: comments.comments, fromUnixTime: fromUnixTime, format: format, getUnixTime: getUnixTime, func, }) }); // /user/:username app.get("/post/:post_index", (req, res) => { const postID = req.params.post_index res.render("pages/post", { config, post: posts[postID], postID: postID, user: users[posts[postID].userID], comments: comments.comments[postID], fromUnixTime, format, getUnixTime, func, }) }); // /post/:post_index app.get("/tag/:tag", (req,res) => { const tag = req.params.tag res.render("pages/tag", { config: config, tag: tag, posts: posts, users: users, comments: comments.comments, fromUnixTime: fromUnixTime, format: format, getUnixTime: getUnixTime, func, }) }); // /tag/:tag ///////////////////// Form pages //////////////////////////// app.get(config.new_post_url, (req,res) => { res.render("forms/new_post", { config }); }); // /post app.get(config.signup_url, (req,res) => { // if the server does allow signup if (config.allow_signup == true) { // Send the page for signing up to the server res.render("forms/signup", { config }); } // if the server does not allow signup else if (config.allow_signup == false) { res.render("partials/message", { message: config.string.signups_unavailable, config, }) } // If allow_signup is undefined or not a boolean, error else { res.redirect(301,"/") console.log("Error, invalid value for allow_signup (bool)") } }); // /signup app.get(config.delete_account_url, (req,res) => { res.render("forms/delete_account", { config }); }); // /delete_account app.get(`${config.edit_post_base_url}/:post_id`, (req,res) => { const post_id = req.params.post_id const post = posts[post_id] const user = users[post['userID']] res.render("forms/edit_post", { config, post, post_id, user, }); }); // /edit/:post_id ////////////////////// Form actions ///////////////////////// app.post("/submit_comment", (req,res) => { const unix_timestamp = getUnixTime(new Date()) let name = func.escape_input(req.body.name) if (name == "") { name = config.default_commenter_username } new_comment = { "name": name, "content": func.escape_input(req.body.content), "id": comments.counter, "pubdate": unix_timestamp }; let counter = comments.counter+1; comments.comments[req.body.post_index].push(new_comment); fs.writeFileSync(`../data/comments.json`, `${JSON.stringify(comments)}`, 'utf-8'); res.redirect(301,`/post/${req.body.post_index}`) }); // /submit_comment app.post("/submit_post", (req,res) => { const password = crypto.createHash('sha512').update(req.body.password).digest('hex'); const username = func.escape_input(req.body.username) const title = func.escape_input(req.body.title) const content = func.escape_input(req.body.content) const tags = func.escape_input(req.body.tags).split(','); const unix_timestamp = getUnixTime(new Date()) if (func.get_userID(username) == -1) { res.render("partials/message", { message: config.string.user_doesnt_exit, config, }) } else if (users[func.get_userID(username)]['hash'] == password) { // Password matches console.log(username, "is submitting a post titled:", title); posts.push({ "userID": func.get_userID(username), "title": title, "content": content, "pubdate": unix_timestamp, "editdate": unix_timestamp, "tags": tags, }) fs.writeFileSync(`../data/posts.json`, `${JSON.stringify(posts)}`, 'utf-8'); comments.comments.push([]) fs.writeFileSync(`../data/comments.json`, `${JSON.stringify(comments)}`) res.redirect(302, "/"); } else { res.render("partials/message", { message: config.string.incorrect_password, config, }) } }); // /submit_post app.post("/submit_signup", (req,res) => { const password = crypto.createHash('sha512').update(req.body.password).digest('hex'); const username = func.escape_input(req.body.username) const prettyname = func.escape_input(req.body.prettyname) const description = func.escape_input(req.body.description) // Check that signups are allowed if (config.allow_signup == true) { // func.get_userID will return -1 if the user does not exist // so this checks that the user does not exist if (func.get_userID(username) == -1) { users.push({ "username": username, "prettyname": prettyname, "hash": password, "description": description, }) fs.writeFileSync(`../data/users.json`, `${JSON.stringify(users)}`, 'utf-8'); res.redirect(301, `/user/${username}`) } // if the user does exist then else { res.render("partials/message", { message: config.string.user_exists, config, }) } } else if (config.allow_signup == false) { res.render("partials/message", { message: config.string.signups_unavailable, config, }) } // If allow_signup is undefined or not a boolean, error else { res.redirect(301,"/") console.log("Error, invalid value for allow_signup (bool)") } }); // /submit_signup app.post("/submit_delete_account", (req,res) => { // Get the form info const password = crypto.createHash("sha512").update(req.body.password).digest("hex"); const username = func.escape_input(req.body.username) // get the userID const userID = func.get_userID(username) if (userID >= 0) { // The user exists if (password == users[userID]['hash']) { // password matches console.log(username, "(userID:", userID, ") is trying deleting their account") // Delete the user users.splice(userID,1) // Delete all their posts for (let postid = 0; postid < posts.length; postid++) { // loop over all posts if (posts[postid]['userID'] == userID) { // if userID matches posts.splice(postid,1) // delete the post comments.comments.splice(postid,1) // the comments for this post should also be delete } }; // Write these changes fs.writeFileSync(`../data/users.json`, `${JSON.stringify(users)}`, 'utf-8'); fs.writeFileSync(`../data/posts.json`, `${JSON.stringify(posts)}`, 'utf-8'); fs.writeFileSync(`../data/comments.json`, `${JSON.stringify(comments.comments)}\nexport const counter = ${comments.counter}`, 'utf-8'); res.redirect(301,"/") } else { // password does not match res.render("partials/message", { message: config.string.incorrect_password, config } ) }; } else { res.render("partials/message", { message: config.string.user_doesnt_exist, config, }) } }); // /submit_delete_account app.post("/submit_edit", (req,res) => { const password = crypto.createHash('sha512').update(req.body.password).digest('hex'); const postID = req.body.postID const userID = req.body.userID const title = req.body.title const content = req.body.content const tags = req.body.tags.split(','); const delete_bool = req.body.delete const unix_timestamp = getUnixTime(new Date()) console.log(users[userID]['prettyname'], "is editting the post titled:", title); if (users[userID]['hash'] == password) { // password matches let post = posts[postID] post['title'] = title post['content'] = content post['tags'] = tags post['editdate'] = unix_timestamp if (typeof delete_bool != "undefined") { console.log("Deleting post!") posts.splice(postID,1) comments.comments.splice(postID,1) fs.writeFileSync(`../data/comments.json`, `${JSON.stringify(comments.comments)}\nexport const counter = ${comments.counter}`, 'utf-8'); } fs.writeFileSync(`../data/posts.json`, `${JSON.stringify(posts)}`, 'utf-8'); res.redirect(302, "/"); } else { res.render("partials/message", { message: config.string.incorrect_password, config, }) } }); // /submit_edit app.listen(config.port, () => { console.log(`Server is running at http://localhost:${config.port} webroot: ${config.root_path}`); console.log("Running in: ", __dirname) });