MININQ
This commit is contained in:
parent
5c7e20ecd6
commit
2d348c2118
11 changed files with 608 additions and 48 deletions
|
@ -25,6 +25,12 @@ h1 a, h2 a, h3 a, h4 a, h5 a, h6 a {
|
||||||
color: var(--color-bg);
|
color: var(--color-bg);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
a.noblock {
|
||||||
|
display: inline;
|
||||||
|
margin-inline: 0;
|
||||||
|
padding-block: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
p {
|
p {
|
||||||
margin-inline: 2ch;
|
margin-inline: 2ch;
|
||||||
margin-block: 1.5rem;
|
margin-block: 1.5rem;
|
||||||
|
@ -44,6 +50,18 @@ ul li {
|
||||||
margin-block: 1.5rem;
|
margin-block: 1.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
details > summary {
|
||||||
|
list-style-type: '► ';
|
||||||
|
}
|
||||||
|
|
||||||
|
details[open] > summary {
|
||||||
|
list-style-type: '▼ ';
|
||||||
|
}
|
||||||
|
|
||||||
|
summary > h1, summary > h2, summary > h3, summary > h4, summary > h5, summary > h6 {
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
section {
|
section {
|
||||||
margin-block: 1.5em;
|
margin-block: 1.5em;
|
||||||
margin-inline-start: 2ch;
|
margin-inline-start: 2ch;
|
||||||
|
@ -61,11 +79,16 @@ a {
|
||||||
|
|
||||||
code {
|
code {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
|
max-width: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
font: inherit;
|
font: inherit;
|
||||||
background-color: var(--color-code);
|
background-color: var(--color-code);
|
||||||
padding-inline: 1ch;
|
padding-inline: 1ch;
|
||||||
color: var(--color-code-text);
|
color: var(--color-code-text);
|
||||||
user-select: all;
|
user-select: all;
|
||||||
|
word-break: break-word;
|
||||||
|
overflow: auto;
|
||||||
|
vertical-align: top;
|
||||||
}
|
}
|
||||||
|
|
||||||
button, input, textarea, select {
|
button, input, textarea, select {
|
||||||
|
@ -113,3 +136,24 @@ marquee {
|
||||||
display: table;
|
display: table;
|
||||||
clear: both;
|
clear: both;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
progress {
|
||||||
|
border: none;
|
||||||
|
border-radius: 0;
|
||||||
|
border-inline: 1ch solid var(--color-text);
|
||||||
|
background-color: var(--color-bg-scrim);
|
||||||
|
height: 1.5rem;
|
||||||
|
vertical-align: top;
|
||||||
|
}
|
||||||
|
progress::-webkit-progress-bar {
|
||||||
|
border: none;
|
||||||
|
border-radius: 0;
|
||||||
|
border-inline: 1ch solid var(--color-text);
|
||||||
|
background-color: var(--color-bg-scrim);
|
||||||
|
}
|
||||||
|
progress::-webkit-progress-value {
|
||||||
|
background-color: var(--color-accent);
|
||||||
|
}
|
||||||
|
progress::-moz-progress-bar {
|
||||||
|
background-color: var(--color-accent);
|
||||||
|
}
|
343
index.js
343
index.js
|
@ -6,6 +6,12 @@ import { Kysely, SqliteDialect } from 'kysely';
|
||||||
import { nanoid } from "nanoid";
|
import { nanoid } from "nanoid";
|
||||||
import { JSDOM } from "jsdom";
|
import { JSDOM } from "jsdom";
|
||||||
import Parser from "rss-parser";
|
import Parser from "rss-parser";
|
||||||
|
import nacl from "tweetnacl";
|
||||||
|
import expressBasicAuth from "express-basic-auth";
|
||||||
|
import { getRelativeTime } from "./modules/relativeTime.js";
|
||||||
|
import { fromHex } from "./modules/fromHex.js";
|
||||||
|
|
||||||
|
import sitemap from "./sitemap.json" assert { type: "json" };
|
||||||
|
|
||||||
dotenvConfig();
|
dotenvConfig();
|
||||||
|
|
||||||
|
@ -22,6 +28,7 @@ const db = new Kysely({
|
||||||
const rssParser = new Parser();
|
const rssParser = new Parser();
|
||||||
|
|
||||||
rawDB.exec(`CREATE TABLE IF NOT EXISTS blurbs( 'id' TEXT, 'site' TEXT, 'blurb' TEXT, 'verified' INTEGER, 'time' INTEGER );`);
|
rawDB.exec(`CREATE TABLE IF NOT EXISTS blurbs( 'id' TEXT, 'site' TEXT, 'blurb' TEXT, 'verified' INTEGER, 'time' INTEGER );`);
|
||||||
|
rawDB.exec(`CREATE TABLE IF NOT EXISTS mininq( 'id' TEXT, 'name' TEXT, 'ip' TEXT, 'msg' TEXT, 'url' TEXT, 'public' INTEGER, 'reply' TEXT, 'time' INTEGER );`);
|
||||||
rawDB.exec(`CREATE TABLE IF NOT EXISTS blacklist( 'domain' TEXT );`);
|
rawDB.exec(`CREATE TABLE IF NOT EXISTS blacklist( 'domain' TEXT );`);
|
||||||
|
|
||||||
const app = express();
|
const app = express();
|
||||||
|
@ -29,33 +36,11 @@ const app = express();
|
||||||
app.set('view engine', 'ejs');
|
app.set('view engine', 'ejs');
|
||||||
app.use('/assets', express.static('assets'));
|
app.use('/assets', express.static('assets'));
|
||||||
app.set('views', path.join(__dirname, "views", "pages"));
|
app.set('views', path.join(__dirname, "views", "pages"));
|
||||||
|
app.use(express.urlencoded({
|
||||||
|
extended: true
|
||||||
|
}))
|
||||||
|
|
||||||
app.locals.siteMap = [
|
app.locals.siteMap = sitemap;
|
||||||
{
|
|
||||||
link: "blurbs",
|
|
||||||
description: "what are other sites saying about abtmtr.link?"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
link: "characters",
|
|
||||||
description: "about abtmtr.link's mascots and other such characters"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
link: "updates",
|
|
||||||
description: "what's going on with abtmtr.link?"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
link: "servers",
|
|
||||||
description: "about the services on abtmtr.link and the machines that serve them."
|
|
||||||
},
|
|
||||||
{
|
|
||||||
link: "sites",
|
|
||||||
description: "a collection of other sites in the form of 88x31s."
|
|
||||||
},
|
|
||||||
{
|
|
||||||
link: "about",
|
|
||||||
description: "who runs abtmtr.link?"
|
|
||||||
}
|
|
||||||
];
|
|
||||||
app.locals.curCommit = "0000000000";
|
app.locals.curCommit = "0000000000";
|
||||||
|
|
||||||
async function getCurCommit() {
|
async function getCurCommit() {
|
||||||
|
@ -99,26 +84,6 @@ app.get('/servers', async (req, res) => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
var units = {
|
|
||||||
year: 24 * 60 * 60 * 1000 * 365,
|
|
||||||
month: 24 * 60 * 60 * 1000 * 365 / 12,
|
|
||||||
day: 24 * 60 * 60 * 1000,
|
|
||||||
hour: 60 * 60 * 1000,
|
|
||||||
minute: 60 * 1000,
|
|
||||||
second: 1000
|
|
||||||
}
|
|
||||||
|
|
||||||
var rtf = new Intl.RelativeTimeFormat('en', { numeric: 'auto' })
|
|
||||||
|
|
||||||
var getRelativeTime = (d1, d2 = new Date()) => {
|
|
||||||
var elapsed = d1 - d2
|
|
||||||
|
|
||||||
// "Math.abs" accounts for both "past" & "future" scenarios
|
|
||||||
for (var u in units)
|
|
||||||
if (Math.abs(elapsed) > units[u] || u == 'second')
|
|
||||||
return rtf.format(Math.round(elapsed / units[u]), u)
|
|
||||||
}
|
|
||||||
|
|
||||||
app.get('/about', async (req, res) => {
|
app.get('/about', async (req, res) => {
|
||||||
const linksJson = await fetch("https://cdn.abtmtr.link/site_content/v13/links.json")
|
const linksJson = await fetch("https://cdn.abtmtr.link/site_content/v13/links.json")
|
||||||
.catch(() => res.status(500).send())
|
.catch(() => res.status(500).send())
|
||||||
|
@ -142,6 +107,292 @@ app.get('/characters', async (req, res) => {
|
||||||
res.render('characters', { characters: charactersJson });
|
res.render('characters', { characters: charactersJson });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
app.get('/mininq', async (req, res) => {
|
||||||
|
const mailbox = await db
|
||||||
|
.selectFrom('mininq')
|
||||||
|
.select('id')
|
||||||
|
.where('public', '=', 0)
|
||||||
|
.where('url', '=', '')
|
||||||
|
.orderBy('time', "desc")
|
||||||
|
.limit(process.env.MININQ_MAILBOX_MAX)
|
||||||
|
.execute();
|
||||||
|
|
||||||
|
const verifiedMailbox = await db
|
||||||
|
.selectFrom('mininq')
|
||||||
|
.select('id')
|
||||||
|
.where('public', '=', 0)
|
||||||
|
.where('url', '!=', '')
|
||||||
|
.orderBy('time', "desc")
|
||||||
|
.limit(process.env.MININQ_VERIFIED_MAILBOX_MAX)
|
||||||
|
.execute();
|
||||||
|
|
||||||
|
const isIpAlreadyMininq = await db
|
||||||
|
.selectFrom('mininq')
|
||||||
|
.select('ip')
|
||||||
|
.where('ip', '=', req.ip)
|
||||||
|
.where('public', '=', 0)
|
||||||
|
.executeTakeFirst();
|
||||||
|
|
||||||
|
const { publicKey, secretKey } = nacl.box.keyPair();
|
||||||
|
|
||||||
|
const data = await db
|
||||||
|
.selectFrom('mininq')
|
||||||
|
.selectAll()
|
||||||
|
.where('public', '=', 1)
|
||||||
|
.orderBy('time', "desc")
|
||||||
|
.execute();
|
||||||
|
|
||||||
|
res.render('mininq', {
|
||||||
|
keypair: {
|
||||||
|
publicKey: Buffer.from(publicKey).toString('hex'),
|
||||||
|
secretKey: Buffer.from(secretKey).toString('hex'),
|
||||||
|
},
|
||||||
|
data,
|
||||||
|
mailboxCount: mailbox.length,
|
||||||
|
isIpAlreadyMininq,
|
||||||
|
mailboxVerifiedCount: verifiedMailbox.length,
|
||||||
|
mailboxMaximum: process.env.MININQ_MAILBOX_MAX,
|
||||||
|
mailboxVerifiedMaximum: process.env.MININQ_VERIFIED_MAILBOX_MAX
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
async function checkIsMtr(user, pass, cb) {
|
||||||
|
try {
|
||||||
|
let publicKeyWorks = await checkPublicKey({
|
||||||
|
skey: pass,
|
||||||
|
pkeyurl: `http://localhost:${process.env.PORT}/`
|
||||||
|
});
|
||||||
|
|
||||||
|
return cb(null, publicKeyWorks);
|
||||||
|
} catch (err) {
|
||||||
|
return cb(null, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
app.get('/mininq/mbox', expressBasicAuth({
|
||||||
|
authorizer: checkIsMtr,
|
||||||
|
authorizeAsync: true,
|
||||||
|
challenge: true
|
||||||
|
}), async (req, res) => {
|
||||||
|
const mailbox = await db
|
||||||
|
.selectFrom('mininq')
|
||||||
|
.selectAll()
|
||||||
|
.where('public', '=', 0)
|
||||||
|
.where('url', '=', '')
|
||||||
|
.orderBy('time', "desc")
|
||||||
|
.execute();
|
||||||
|
|
||||||
|
const verifiedMailbox = await db
|
||||||
|
.selectFrom('mininq')
|
||||||
|
.selectAll()
|
||||||
|
.where('public', '=', 0)
|
||||||
|
.where('url', '!=', '')
|
||||||
|
.orderBy('time', "desc")
|
||||||
|
.execute();
|
||||||
|
|
||||||
|
res.render('mininqbox', {
|
||||||
|
mail: verifiedMailbox.concat(mailbox),
|
||||||
|
mailboxCount: mailbox.length,
|
||||||
|
mailboxVerifiedCount: verifiedMailbox.length,
|
||||||
|
mailboxMaximum: process.env.MININQ_MAILBOX_MAX,
|
||||||
|
mailboxVerifiedMaximum: process.env.MININQ_VERIFIED_MAILBOX_MAX
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
app.post('/mininq/mbox/r', async (req, res) => {
|
||||||
|
const body = req.body;
|
||||||
|
|
||||||
|
if (typeof body.skey != "string") return res.status(400).send("skey is not a string");
|
||||||
|
if (body.skey.length != 64) return res.status(400).send("skey must be a valid secret key");
|
||||||
|
|
||||||
|
try {
|
||||||
|
let publicKeyWorks = await checkPublicKey({
|
||||||
|
skey: body.skey,
|
||||||
|
pkeyurl: `http://localhost:${process.env.PORT}/`
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!publicKeyWorks) return res.status(401).send("No?");
|
||||||
|
} catch (err) {
|
||||||
|
return res.status(400).send(err.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof body.id != "string") return res.status(400).send("id is not a string");
|
||||||
|
if (body.id.length != 32) return res.status(400).send("id must be a mininq ID");
|
||||||
|
|
||||||
|
if (typeof body.action != "string") return res.status(400).send("action is not a string");
|
||||||
|
if (body.action.length > 10) return res.status(400).send("action must be valid");
|
||||||
|
|
||||||
|
switch (body.action) {
|
||||||
|
case "public":
|
||||||
|
await db
|
||||||
|
.updateTable('mininq')
|
||||||
|
.set({
|
||||||
|
public: 1
|
||||||
|
})
|
||||||
|
.where('id', '=', body.id)
|
||||||
|
.where('public', '=', 0)
|
||||||
|
.executeTakeFirst();
|
||||||
|
break;
|
||||||
|
case "delete":
|
||||||
|
await db
|
||||||
|
.deleteFrom('mininq')
|
||||||
|
.where('id', '=', body.id)
|
||||||
|
.executeTakeFirst();
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
if (typeof body.msg != "string") return res.status(400).send("msg is not a string");
|
||||||
|
if (body.msg.length < 1) return res.status(400).send("msg is required");
|
||||||
|
if (body.msg.length > 4000) return res.status(400).send("msg must not be longer than 4000 characters");
|
||||||
|
|
||||||
|
await db
|
||||||
|
.updateTable('mininq')
|
||||||
|
.set({
|
||||||
|
public: 1,
|
||||||
|
reply: body.msg
|
||||||
|
})
|
||||||
|
.where('id', '=', body.id)
|
||||||
|
.where('public', '=', 0)
|
||||||
|
.executeTakeFirst();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
res.redirect('/mininq/mbox');
|
||||||
|
});
|
||||||
|
|
||||||
|
async function checkPublicKey(body) {
|
||||||
|
if (typeof body.skey != "string") throw ("skey is not a string");
|
||||||
|
if (body.skey.length != 64) throw ("skey must be a valid secret key");
|
||||||
|
|
||||||
|
if (body.pkeyurl.length < 1) throw ("pkeyurl is required");
|
||||||
|
if (body.pkeyurl.length > 512) throw ("pkeyurl must not be longer than 512 characters");
|
||||||
|
const site = await fetch(body.pkeyurl).then(async x => ({ s: x.status, t: await x.text() })).catch(_ => _);
|
||||||
|
if (site.s != 200) throw ("pkeyurl's site is not online");
|
||||||
|
try {
|
||||||
|
const domain = new URL(body.pkeyurl).hostname;
|
||||||
|
const isBadSite = await db
|
||||||
|
.selectFrom('blacklist')
|
||||||
|
.selectAll()
|
||||||
|
.where('domain', '=', domain)
|
||||||
|
.executeTakeFirst();
|
||||||
|
|
||||||
|
if (isBadSite != null)
|
||||||
|
throw ({
|
||||||
|
error: "Forbidden",
|
||||||
|
message: "\u{1f595} (or don't authenticate, i guess)"
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
throw ({
|
||||||
|
error: "Internal Server Error",
|
||||||
|
message: "URL CONSTRUCTOR FAILED???? i think this might be your fault...."
|
||||||
|
});
|
||||||
|
}
|
||||||
|
let relationMeta;
|
||||||
|
try {
|
||||||
|
let webDom = new JSDOM(site.t);
|
||||||
|
relationMeta = webDom.window.document.querySelector(
|
||||||
|
`meta[name="abtmtr-mininq-key"]`
|
||||||
|
);
|
||||||
|
} catch (err) {
|
||||||
|
throw ("Something went wrong parsing your site's DOM");
|
||||||
|
}
|
||||||
|
let publicKey;
|
||||||
|
if (relationMeta != null) publicKey = relationMeta.content;
|
||||||
|
else throw ("Can't find public key");
|
||||||
|
|
||||||
|
let pkMatches;
|
||||||
|
try {
|
||||||
|
const generatedPublicKey = nacl.box.keyPair.fromSecretKey(fromHex(body.skey));
|
||||||
|
pkMatches = Buffer.from(generatedPublicKey.publicKey).toString('hex') == publicKey;
|
||||||
|
} catch (err) {
|
||||||
|
throw ("Something went wrong verifying your keypair");
|
||||||
|
}
|
||||||
|
|
||||||
|
return pkMatches;
|
||||||
|
}
|
||||||
|
|
||||||
|
app.post('/mininq/send', async (req, res) => {
|
||||||
|
|
||||||
|
const isIpAlreadyMininq = await db
|
||||||
|
.selectFrom('mininq')
|
||||||
|
.select('ip')
|
||||||
|
.where('ip', '=', req.ip)
|
||||||
|
.where('public', '=', 0)
|
||||||
|
.executeTakeFirst();
|
||||||
|
|
||||||
|
if (isIpAlreadyMininq != null) return res.status(403).send("IP is already in mailbox");
|
||||||
|
|
||||||
|
const body = req.body;
|
||||||
|
|
||||||
|
let newObject = {
|
||||||
|
url: '',
|
||||||
|
id: nanoid(32),
|
||||||
|
public: 0,
|
||||||
|
reply: '',
|
||||||
|
ip: req.ip
|
||||||
|
};
|
||||||
|
|
||||||
|
if (typeof body.name != "string") return res.status(400).send("name is not a string");
|
||||||
|
if (body.name.length < 1) return res.status(400).send("name is required");
|
||||||
|
if (body.name.length > 40) return res.status(400).send("name must not be longer than 40 characters");
|
||||||
|
|
||||||
|
newObject.name = body.name;
|
||||||
|
|
||||||
|
if (typeof body.msg != "string") return res.status(400).send("msg is not a string");
|
||||||
|
if (body.msg.length < 1) return res.status(400).send("msg is required");
|
||||||
|
if (body.msg.length > 2000) return res.status(400).send("msg must not be longer than 2000 characters");
|
||||||
|
|
||||||
|
newObject.msg = body.msg;
|
||||||
|
|
||||||
|
if (typeof body.pkeyurl == "string") {
|
||||||
|
try {
|
||||||
|
let publicKeyWorks = await checkPublicKey(body);
|
||||||
|
|
||||||
|
if (publicKeyWorks) newObject.url = body.pkeyurl;
|
||||||
|
} catch (err) {
|
||||||
|
// do nothing lol
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const mailbox = await db
|
||||||
|
.selectFrom('mininq')
|
||||||
|
.select('id')
|
||||||
|
.where('public', '=', 0)
|
||||||
|
.where('url', '=', '')
|
||||||
|
.orderBy('time', "desc")
|
||||||
|
.limit(process.env.MININQ_MAILBOX_MAX)
|
||||||
|
.execute();
|
||||||
|
|
||||||
|
const verifiedMailbox = await db
|
||||||
|
.selectFrom('mininq')
|
||||||
|
.select('id')
|
||||||
|
.where('public', '=', 0)
|
||||||
|
.where('url', '!=', '')
|
||||||
|
.orderBy('time', "desc")
|
||||||
|
.limit(process.env.MININQ_VERIFIED_MAILBOX_MAX)
|
||||||
|
.execute();
|
||||||
|
|
||||||
|
if (newObject.url) {
|
||||||
|
if (verifiedMailbox.length >= process.env.MININQ_VERIFIED_MAILBOX_MAX)
|
||||||
|
return res.status(403).send("Mailbox is full");
|
||||||
|
} else {
|
||||||
|
if (mailbox.length >= process.env.MININQ_MAILBOX_MAX)
|
||||||
|
return res.status(403).send("Mailbox is full");
|
||||||
|
}
|
||||||
|
|
||||||
|
newObject.time = Date.now();
|
||||||
|
|
||||||
|
try {
|
||||||
|
await db.insertInto('mininq')
|
||||||
|
.values(newObject)
|
||||||
|
.executeTakeFirstOrThrow();
|
||||||
|
} catch (err) {
|
||||||
|
console.log(err)
|
||||||
|
return res.status(500).send('Internal Server Error');
|
||||||
|
}
|
||||||
|
|
||||||
|
res.redirect('/mininq/');
|
||||||
|
});
|
||||||
|
|
||||||
app.get('/updates', async (req, res) => {
|
app.get('/updates', async (req, res) => {
|
||||||
const rssXML = await fetch("https://cdn.abtmtr.link/site_content/rss.xml")
|
const rssXML = await fetch("https://cdn.abtmtr.link/site_content/rss.xml")
|
||||||
.catch(() => res.status(500).send())
|
.catch(() => res.status(500).send())
|
||||||
|
|
20
modules/fromHex.js
Normal file
20
modules/fromHex.js
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
const MAP_HEX = {
|
||||||
|
0: 0, 1: 1, 2: 2, 3: 3, 4: 4, 5: 5, 6: 6,
|
||||||
|
7: 7, 8: 8, 9: 9, a: 10, b: 11, c: 12, d: 13,
|
||||||
|
e: 14, f: 15, A: 10, B: 11, C: 12, D: 13,
|
||||||
|
E: 14, F: 15
|
||||||
|
};
|
||||||
|
|
||||||
|
export function fromHex(hexString) {
|
||||||
|
const bytes = new Uint8Array(Math.floor((hexString || "").length / 2));
|
||||||
|
let i;
|
||||||
|
for (i = 0; i < bytes.length; i++) {
|
||||||
|
const a = MAP_HEX[hexString[i * 2]];
|
||||||
|
const b = MAP_HEX[hexString[i * 2 + 1]];
|
||||||
|
if (a === undefined || b === undefined) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
bytes[i] = (a << 4) | b;
|
||||||
|
}
|
||||||
|
return i === bytes.length ? bytes : bytes.slice(0, i);
|
||||||
|
}
|
19
modules/relativeTime.js
Normal file
19
modules/relativeTime.js
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
var units = {
|
||||||
|
year: 24 * 60 * 60 * 1000 * 365,
|
||||||
|
month: 24 * 60 * 60 * 1000 * 365 / 12,
|
||||||
|
day: 24 * 60 * 60 * 1000,
|
||||||
|
hour: 60 * 60 * 1000,
|
||||||
|
minute: 60 * 1000,
|
||||||
|
second: 1000
|
||||||
|
}
|
||||||
|
|
||||||
|
var rtf = new Intl.RelativeTimeFormat('en', { numeric: 'auto' })
|
||||||
|
|
||||||
|
export function getRelativeTime(d1, d2 = new Date()) {
|
||||||
|
var elapsed = d1 - d2
|
||||||
|
|
||||||
|
// "Math.abs" accounts for both "past" & "future" scenarios
|
||||||
|
for (var u in units)
|
||||||
|
if (Math.abs(elapsed) > units[u] || u == 'second')
|
||||||
|
return rtf.format(Math.round(elapsed / units[u]), u)
|
||||||
|
}
|
37
package-lock.json
generated
37
package-lock.json
generated
|
@ -13,11 +13,13 @@
|
||||||
"dotenv": "^16.4.5",
|
"dotenv": "^16.4.5",
|
||||||
"ejs": "^3.1.10",
|
"ejs": "^3.1.10",
|
||||||
"express": "^4.19.2",
|
"express": "^4.19.2",
|
||||||
|
"express-basic-auth": "^1.2.1",
|
||||||
"jsdom": "^25.0.0",
|
"jsdom": "^25.0.0",
|
||||||
"kysely": "^0.27.4",
|
"kysely": "^0.27.4",
|
||||||
"nanoid": "^5.0.7",
|
"nanoid": "^5.0.7",
|
||||||
"pg": "^8.12.0",
|
"pg": "^8.12.0",
|
||||||
"rss-parser": "^3.13.0"
|
"rss-parser": "^3.13.0",
|
||||||
|
"tweetnacl": "^1.0.3"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"nodemon": "^3.1.4"
|
"nodemon": "^3.1.4"
|
||||||
|
@ -144,6 +146,24 @@
|
||||||
],
|
],
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/basic-auth": {
|
||||||
|
"version": "2.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-2.0.1.tgz",
|
||||||
|
"integrity": "sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"safe-buffer": "5.1.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/basic-auth/node_modules/safe-buffer": {
|
||||||
|
"version": "5.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
|
||||||
|
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/better-sqlite3": {
|
"node_modules/better-sqlite3": {
|
||||||
"version": "11.2.1",
|
"version": "11.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-11.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-11.2.1.tgz",
|
||||||
|
@ -680,6 +700,15 @@
|
||||||
"node": ">= 0.10.0"
|
"node": ">= 0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/express-basic-auth": {
|
||||||
|
"version": "1.2.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/express-basic-auth/-/express-basic-auth-1.2.1.tgz",
|
||||||
|
"integrity": "sha512-L6YQ1wQ/mNjVLAmK3AG1RK6VkokA1BIY6wmiH304Xtt/cLTps40EusZsU1Uop+v9lTDPxdtzbFmdXfFO3KEnwA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"basic-auth": "^2.0.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/file-uri-to-path": {
|
"node_modules/file-uri-to-path": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz",
|
||||||
|
@ -2178,6 +2207,12 @@
|
||||||
"node": "*"
|
"node": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/tweetnacl": {
|
||||||
|
"version": "1.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-1.0.3.tgz",
|
||||||
|
"integrity": "sha512-6rt+RN7aOi1nGMyC4Xa5DdYiukl2UWCbcJft7YhxReBGQD7OAM8Pbxw6YMo4r2diNEA8FEmu32YOn9rhaiE5yw==",
|
||||||
|
"license": "Unlicense"
|
||||||
|
},
|
||||||
"node_modules/type-is": {
|
"node_modules/type-is": {
|
||||||
"version": "1.6.18",
|
"version": "1.6.18",
|
||||||
"resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
|
"resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
|
||||||
|
|
|
@ -22,11 +22,13 @@
|
||||||
"dotenv": "^16.4.5",
|
"dotenv": "^16.4.5",
|
||||||
"ejs": "^3.1.10",
|
"ejs": "^3.1.10",
|
||||||
"express": "^4.19.2",
|
"express": "^4.19.2",
|
||||||
|
"express-basic-auth": "^1.2.1",
|
||||||
"jsdom": "^25.0.0",
|
"jsdom": "^25.0.0",
|
||||||
"kysely": "^0.27.4",
|
"kysely": "^0.27.4",
|
||||||
"nanoid": "^5.0.7",
|
"nanoid": "^5.0.7",
|
||||||
"pg": "^8.12.0",
|
"pg": "^8.12.0",
|
||||||
"rss-parser": "^3.13.0"
|
"rss-parser": "^3.13.0",
|
||||||
|
"tweetnacl": "^1.0.3"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"nodemon": "^3.1.4"
|
"nodemon": "^3.1.4"
|
||||||
|
|
30
sitemap.json
Normal file
30
sitemap.json
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"link": "blurbs",
|
||||||
|
"description": "what are other sites saying about abtmtr.link?"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"link": "characters",
|
||||||
|
"description": "about abtmtr.link's mascots and other such characters"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"link": "updates",
|
||||||
|
"description": "what's going on with abtmtr.link?"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"link": "mininq",
|
||||||
|
"description": "slowchat, for sysadmins who aren't quite equine."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"link": "servers",
|
||||||
|
"description": "about the services on abtmtr.link and the machines that serve them."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"link": "sites",
|
||||||
|
"description": "a collection of other sites in the form of 88x31s."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"link": "about",
|
||||||
|
"description": "who runs abtmtr.link?"
|
||||||
|
}
|
||||||
|
]
|
14
views/components/mininq.ejs
Normal file
14
views/components/mininq.ejs
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
<h3>
|
||||||
|
<%= mininq.name %>
|
||||||
|
<% if (mininq.url != '') { %>
|
||||||
|
@ <a href="<%= mininq.url %>" target="_blank" class="noblock"><%= new URL(mininq.url).host %></a>
|
||||||
|
<% } %>
|
||||||
|
sent:
|
||||||
|
</h3>
|
||||||
|
<p><%= mininq.msg %></p>
|
||||||
|
<% if (mininq.reply != '') { %>
|
||||||
|
<section>
|
||||||
|
<h3>sysadmin reply:</h3>
|
||||||
|
<p><%= mininq.reply %></p>
|
||||||
|
</section>
|
||||||
|
<% } %>
|
|
@ -3,6 +3,7 @@
|
||||||
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
||||||
<title>abtmtr.link</title>
|
<title>abtmtr.link</title>
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<meta name="abtmtr-mininq-key" content="cf28339198953cab0c635c03030fa23d63e88db92126fab5d358b7cb1aded015">
|
||||||
<link rel="stylesheet" href="/assets/style.css" />
|
<link rel="stylesheet" href="/assets/style.css" />
|
||||||
<link rel="me" href="https://abtmtr.link/about">
|
<link rel="me" href="https://abtmtr.link/about">
|
||||||
<link rel="me" href="https://abtmtr.link/blurbs/#Hx7CuB4_zIWMAsOZlyyqsUm2upXEEYEl">
|
<link rel="me" href="https://abtmtr.link/blurbs/#Hx7CuB4_zIWMAsOZlyyqsUm2upXEEYEl">
|
||||||
|
|
86
views/pages/mininq.ejs
Normal file
86
views/pages/mininq.ejs
Normal file
|
@ -0,0 +1,86 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<%- include("../components/page-head.ejs") %>
|
||||||
|
<body>
|
||||||
|
<main>
|
||||||
|
<h1>mininq</h1>
|
||||||
|
<p>slowchat, for sysadmins who aren't quite equine.</p>
|
||||||
|
<h2>submit a mininq</h2>
|
||||||
|
<p>
|
||||||
|
MAILBOX:
|
||||||
|
<progress value="<%= mailboxCount %>" max="<%= mailboxMaximum %>"
|
||||||
|
style="width: <%= mailboxMaximum * 2 %>ch;"></progress>
|
||||||
|
<%= mailboxCount %>/<%= mailboxMaximum %>
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
MAILBOX (PRIORITY):
|
||||||
|
<progress value="<%= mailboxVerifiedCount %>" max="<%= mailboxVerifiedMaximum %>"
|
||||||
|
style="width: <%= mailboxVerifiedMaximum * 2 %>ch;"></progress>
|
||||||
|
<%= mailboxVerifiedCount %>/<%= mailboxVerifiedMaximum %>
|
||||||
|
</p>
|
||||||
|
<p>mininq (<code>[min]i [inq]uiry</code>) is a ping-ponging non-realtime chat interface made in the early 2000s to satisfy the needs of the single abtmtr.link sysadmin writing it.</p>
|
||||||
|
<p>either way, mininq was conceptualized, developed, and deployed without any second consideration. now, here you are.</p>
|
||||||
|
<% if (!isIpAlreadyMininq) { %>
|
||||||
|
<% if (mailboxCount < mailboxMaximum || mailboxVerifiedCount < mailboxVerifiedMaximum) { %>
|
||||||
|
<form action="/mininq/send" method="post">
|
||||||
|
<p>
|
||||||
|
<label for="username">Name:</label>
|
||||||
|
<input type="text" name="name" id="username" required>
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<label for="message">Message:</label><br />
|
||||||
|
<textarea name="msg" id="message" rows="4" cols="30" required></textarea>
|
||||||
|
</p>
|
||||||
|
<% if (mailboxVerifiedCount < mailboxVerifiedMaximum) { %>
|
||||||
|
<div class="entity_box">
|
||||||
|
<% if (mailboxCount < mailboxMaximum) { %>
|
||||||
|
<h3>optional keyventure</h3>
|
||||||
|
<% } else { %>
|
||||||
|
<h3>mandatory keyventure</h3>
|
||||||
|
<p>uh-oh! looks like the non-priority mailbox is full. if you still want to submit a mininq, you'll have to follow the instructions below.<br />
|
||||||
|
or, wait until the non-priority mailbox has emptied.</p>
|
||||||
|
<% } %>
|
||||||
|
<p>this keypair has been generated for you to put on your site.</p>
|
||||||
|
<p>put this snippet in your document's head: <code><meta name="abtmtr-mininq-key" content="<%= keypair.publicKey %>"></code></p>
|
||||||
|
<p>then, if you want to make a mininq, you can enter your secret key and your site's URL here, and your domain will appear beside your name.<br />
|
||||||
|
you'll also get priority in the mininq inbox.</p>
|
||||||
|
<p>
|
||||||
|
<label for="secretkey">Secret Key:</label>
|
||||||
|
<input type="text" name="skey" id="secretkey" value="<%= keypair.secretKey %>"><br />
|
||||||
|
<span style="opacity: 0.5;">(keep this somewhere safe!)</span>
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<label for="pubkeylocation">Website URL:</label>
|
||||||
|
<input type="url" name="pkeyurl" id="pubkeylocation">
|
||||||
|
</p>
|
||||||
|
<p></p>
|
||||||
|
</div>
|
||||||
|
<% } else { %>
|
||||||
|
<p>uh-oh! there was a keyventure here, but it looks like the priority mailbox is full. if you still want to submit a mininq, you'll have to appear anonymously.<br />
|
||||||
|
or, wait until the priority mailbox has emptied.</p>
|
||||||
|
<% } %>
|
||||||
|
<p>
|
||||||
|
<input type="submit" value="Submit Mininq">
|
||||||
|
</p>
|
||||||
|
<p style="opacity: 0.5;">by clicking this button and successfully submitting a mininq, you agree that your input may be displayed on this site and stored on abtmtr.link servers.</p>
|
||||||
|
<p style="opacity: 0.5;">rules: no slurs, no politics, no nsfw, no spam. if you break these rules outside of plausible deniability, your domain will be blacklisted.</p>
|
||||||
|
</form>
|
||||||
|
<% } else { %>
|
||||||
|
<p>uh-oh! looks like both mailboxes are full. you'll have to wait until the mailbox has emptied - then you can submit your mininq.</p>
|
||||||
|
<% } %>
|
||||||
|
<% } else { %>
|
||||||
|
<p>you've submitted a mininq!</p>
|
||||||
|
<% } %>
|
||||||
|
<h2>current mininqs</h2>
|
||||||
|
<ul>
|
||||||
|
<% data.forEach((mininq) => { %>
|
||||||
|
<li class="mininq" id="<%= mininq.id %>">
|
||||||
|
<%- include("../components/mininq.ejs", {mininq}) %>
|
||||||
|
</li>
|
||||||
|
<% }) %>
|
||||||
|
</ul>
|
||||||
|
</main>
|
||||||
|
<%- include("../components/footer.ejs") %>
|
||||||
|
<%- include("../components/post-main.ejs") %>
|
||||||
|
</body>
|
||||||
|
</html>
|
58
views/pages/mininqbox.ejs
Normal file
58
views/pages/mininqbox.ejs
Normal file
|
@ -0,0 +1,58 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<%- include("../components/page-head.ejs") %>
|
||||||
|
<body>
|
||||||
|
<main>
|
||||||
|
<h1>mininq mailbox</h1>
|
||||||
|
<p>slowchat, for sysadmins who aren't quite equine.</p>
|
||||||
|
<h2>mailboxes</h2>
|
||||||
|
<p>
|
||||||
|
MAILBOX:
|
||||||
|
<progress value="<%= mailboxCount %>" max="<%= mailboxMaximum %>"
|
||||||
|
style="width: <%= mailboxMaximum * 2 %>ch;"></progress>
|
||||||
|
<%= mailboxCount %>/<%= mailboxMaximum %>
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
MAILBOX (PRIORITY):
|
||||||
|
<progress value="<%= mailboxVerifiedCount %>" max="<%= mailboxVerifiedMaximum %>"
|
||||||
|
style="width: <%= mailboxVerifiedMaximum * 2 %>ch;"></progress>
|
||||||
|
<%= mailboxVerifiedCount %>/<%= mailboxVerifiedMaximum %>
|
||||||
|
</p>
|
||||||
|
<h2>current mininqs</h2>
|
||||||
|
<ul>
|
||||||
|
<% mail.forEach((mininq) => { %>
|
||||||
|
<li class="mininq" id="<%= mininq.id %>">
|
||||||
|
<%- include("../components/mininq.ejs", {mininq}) %>
|
||||||
|
<details>
|
||||||
|
<summary><h3>reply...</h3></summary>
|
||||||
|
<form action="/mininq/mbox/r" method="post">
|
||||||
|
<input type="hidden" name="id" value="<%= mininq.id %>">
|
||||||
|
<p>
|
||||||
|
<label for="action">action:</label>
|
||||||
|
<select name="action" id="action" required>
|
||||||
|
<option value="reply" selected>Reply & make public</option>
|
||||||
|
<option value="public" selected>Make public</option>
|
||||||
|
<option value="delete" selected>Delete mininq</option>
|
||||||
|
</select>
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<label for="skey">Secret Key:</label>
|
||||||
|
<input type="text" name="skey" id="skey" required>
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<label for="message">Message:</label><br />
|
||||||
|
<textarea name="msg" id="message" rows="4" cols="30"></textarea>
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<input type="submit" value="Submit">
|
||||||
|
</p>
|
||||||
|
</form>
|
||||||
|
</details>
|
||||||
|
</li>
|
||||||
|
<% }) %>
|
||||||
|
</ul>
|
||||||
|
</main>
|
||||||
|
<%- include("../components/footer.ejs") %>
|
||||||
|
<%- include("../components/post-main.ejs") %>
|
||||||
|
</body>
|
||||||
|
</html>
|
Loading…
Reference in a new issue