// 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-Startedyyyyyyyy
const { fromUnixTime, format, getUnixTime } = require("date-fns") // A date utility library
// 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") {
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('./users.js');
posts = require('./posts.js');
comments = require('./comments.js');
config = require('./config.js');
}
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.
})
// The footer div is globale because it's a site wide, so define it here
let footer_div = config.site_wide_footer
footer_div = replace_format_indicators(footer_div)
// 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));
// Initialise the program by creating users.js, comments.js, posts.js and config.js
// All require default content in them to start off with
// Then exit successfully
// returns nothing
function initialise() {
try {
const users = require("./users.js");
}
catch (error) {
console.log("Creating users file")
fs.writeFileSync(`${__dirname}/users.js`, `export const users = []`)
}
try {
const posts = require("./posts.js");
}
catch (error) {
console.log("Creating posts file")
fs.writeFileSync(`${__dirname}/posts.js`, `export const posts = []`)
}
try {
const comments = require("./comments.js");
}
catch (error) {
console.log("Creating comments file")
fs.writeFileSync(`${__dirname}/comments.js`, `export const comments = []\nexport const counter = 0`)
}
try {
const config = require("./config.js");
}
catch (error) {
console.log("Copying the example config to config.js")
console.log("!!! PLEASE MODIFY config.js TO YOUR NEEDS !!!")
fs.copyFile('example-config.js', 'config.js', (err) => {
console.log("Error copying file")
})
}
console.log("Successfully initialised")
process.exit(0)
}
// The users are stored as a list of objects [ user_object, user_object, user_object ]
// So you cannot easily find the userID (position in list) from the username
// This function returns the username for a given userID by looping over every user
// if the user is present, it returns the index of the user (integer)
// if the user is not present it returns -1
function get_userID(username) {
for (let i = 0; i < users.users.length; i++) { // Loop over every user
if (users.users[i]['username'] == username) {
return i // If the username matches then return the index of that user
}
}
return -1 // If user is not present, return -1
}
// The configuration defines a date format using the date-fns (a datetime library) syntax
// eg "yyyy-MM-dd"
// this converts unix time (an integer) into a string that is formatted according to config.js
// uses date-fns's fromUnixTime() and format() functions
// returns the formatted date (string)
function unix_time_to_date_format(unix_time) {
date = fromUnixTime(unix_time)
formatted_date = format(date, config.date_format)
return formatted_date
}
// This is similar to the above function, however, instead of formatting to the users
// configuration, it formats to RFC-822 which is the date format used by RSS feeds
// eg "Mon, 23 May 2025 18:59:59 +0100"
// accepts unix time (int)
// returns the formatted date (string)
function unix_time_to_rss_date(unix_time) {
date = fromUnixTime(unix_time)
formatted_date = format(date, "EEE, dd MMM yyyy HH:mm:ss")
return `${formatted_date} ${config.time_zone}`
}
// This function accepts a list of strings eg ["string1","string2,"string3"] (any length)
// then returns a string of them each pointing to a seperate url
// eg "string1, string2, string3"
// this is so you can have a list of tags that each point to their individual tag page
// returns: string
function hyperlink_tags(tags) {
string = "" // Initialises the string
for (let tag_index = 0; tag_index < tags.length; tag_index++) { // Loop over each tag
string += `${tags[tag_index]}` // Adds the tag to the string as a HTML href
if (tag_index < tags.length - 1) { // If there are more tags, then insert a comma
string += ", ";
}
}
return string
}
// See the readme format indicators section for a full list of format indicators
// This function replaces the format indicators in a template to the content they represent
// accepts the template (string),
// the post index (int) as an optional paramter to indicate what post is to be used (for replacing things like content and titles of posts)
// the tag (strig) as an optional parameter to indicate what tag is being used (for /tag/:tag pages)
// the user index (int) is an optional parameter to indicate what user is to be used (for replacng things like the header of the user page)
// returns the template with it's format indiactors replaced (string)
function replace_format_indicators(template, post_index=-1, tag_name="tag", user_index=-1) {
output_string = template // These should always be replaceable
.replaceAll("%%", "%")
.replaceAll("%J", "/delete_account")
.replaceAll("%P", "/post")
.replaceAll("%O", `/edit/${post_index}`)
.replaceAll("%Q", "/signup")
.replaceAll("%R", "/rss")
.replaceAll("%Y", config.site_name)
.replaceAll("%W", config.site_description)
.replaceAll("%Z", config.attribution)
.replaceAll("%S", config.seperator)
if (post_index >= 0) { // These can only be replaced if a post is specified (by default the post id is -1)
post_object = posts.posts[post_index] // Defines the post object for easy reference
output_string = output_string
.replaceAll("%A", (post_object["tags"]))
.replaceAll("%B", (hyperlink_tags(post_object["tags"])))
.replaceAll("%C", converter.makeHtml(post_object["content"]))
.replaceAll("%D", unix_time_to_date_format(post_object["pubdate"]))
.replaceAll("%E", unix_time_to_date_format(post_object["editdate"]))
.replaceAll("%F", users.users[post_object["userID"]]['prettyname'])
.replaceAll("%G", tag_name)
.replaceAll("%I", converter.makeHtml(users.users[post_object['userID']]['description']))
.replaceAll("%L", `/post/${post_index}`)
.replaceAll("%M", return_comments(post_index))
.replaceAll("%N", users.users[post_object["userID"]]['username'])
.replaceAll("%S", config.seperator)
.replaceAll("%T", post_object["title"])
.replaceAll("%U", `/user/${users.users[post_object["userID"]]['username']}`)
.replaceAll("%X", `