Compare commits

...

37 commits
v13-2 ... main

Author SHA1 Message Date
6f00e10641 clean up matty boy 2024-09-17 22:16:45 -05:00
61911a28fc Merge branch 'main' of https://git.abtmtr.link/MeowcaTheoRange/abtmtr-v13 2024-09-06 09:16:50 -05:00
08e1cf1688 about me 2024-09-06 09:16:48 -05:00
30f99d3797 prevent trolling 2024-09-06 01:42:30 +00:00
313126b6b1 add webhook to blurbs 2024-09-03 15:04:31 -05:00
303a09d18e pubkey check 2024-09-02 13:20:42 -05:00
d834f2aa6f wspl 2024-09-02 12:48:50 -05:00
77d8f97a9e whups! 2024-09-02 10:16:46 -05:00
27e2251555 webhooks ???? lol ????????????? 2024-09-02 10:13:57 -05:00
f11d05e648 I forgot I was using NGINX 2024-09-02 09:30:03 -05:00
b66d6e696f can't sign into mininq????? 2024-09-01 16:51:54 -05:00
2d348c2118 MININQ 2024-09-01 16:28:26 -05:00
5c7e20ecd6 refine ASCII art 2024-09-01 12:26:43 -05:00
69fbe1d4ff chat, are they stupid? 2024-09-01 02:54:15 -05:00
94b25ab1eb chrs 2024-08-31 15:11:34 -05:00
064b8cca78 artist change opacity 2024-08-31 03:11:23 -05:00
839a1679cd Delete .env 2024-08-31 08:05:30 +00:00
2f0454ddb0 update gitignore 2024-08-31 03:05:01 -05:00
55dbb1616e Update .env 2024-08-31 08:03:25 +00:00
fa062a7235 LAST.FM INTEGRATION (because of course) 2024-08-31 03:01:59 -05:00
950dee968c get sha of current commit 2024-08-31 02:25:09 -05:00
9923513e80 ehhhh naw nvm. this is not a footer kinda blurb + that attribution was more for the ascii being paired with the sidebar i experimented with. 2024-08-31 02:17:29 -05:00
509db0296d matkap ascii art 2024-08-31 02:08:51 -05:00
9800e5eff4 america/ahegao 2024-08-31 01:02:00 -05:00
6e58a9d911 this site could use some CONSISTENCY !!!!! GOD !!!! 2024-08-31 00:57:23 -05:00
77ff2b43c1 rss feed ????? 2024-08-31 00:54:10 -05:00
ab2c9d8529 stuff lol 2024-08-30 12:03:57 -05:00
ca160a824b link to where my h-card is 2024-08-30 11:11:47 -05:00
88f8e713ed blurb 2024-08-30 01:31:53 -05:00
137e2ff018 really dumb focus ring 2024-08-30 01:18:32 -05:00
98d3e0a2c2 pd blacklist 2024-08-30 01:13:00 -05:00
1aa8d7b9d1 pre-redesign-redesign markers 2024-08-30 00:48:22 -05:00
2632a2860a site isn't black-and-white anymore btw 2024-08-30 00:36:04 -05:00
c3db23f9a4 footer stuff 2024-08-30 00:34:44 -05:00
61b7dc5707 let's see 2024-08-30 00:28:56 -05:00
fd5dbb92ae ew ????? ??? ? ?? ? 2024-08-30 00:18:28 -05:00
adeec12413 NEW SHIT
Reviewed-on: #1
2024-08-30 05:05:29 +00:00
29 changed files with 1076 additions and 76 deletions

1
.env
View file

@ -1 +0,0 @@
PORT=3000

3
.gitignore vendored
View file

@ -1,2 +1,3 @@
node_modules
abtmtr.db
abtmtr.db
.env

BIN
assets/accents/9s_chr.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 184 B

View file

@ -7,7 +7,7 @@ h1, h2, h3, h4, h5, h6, p, li, br {
h1, h2, h3, h4, h5, h6 {
margin-block: 1.5rem;
padding-inline: 0.5rem;
padding-inline: 1ch;
font-size: 1rem;
background-color: var(--color-text);
color: var(--color-bg);
@ -19,14 +19,20 @@ h3, h4, h5, h6 {
h1 a, h2 a, h3 a, h4 a, h5 a, h6 a {
display: block;
margin-inline: -0.5rem;
padding-inline: 0.5rem;
margin-inline: -1ch;
padding-inline: 1ch;
background-color: var(--color-accent);
color: var(--color-bg);
}
a.noblock {
display: inline;
margin-inline: 0;
padding-block: 0.25rem;
}
p {
margin-inline-start: 1rem;
margin-inline: 2ch;
margin-block: 1.5rem;
}
@ -36,7 +42,7 @@ p.nomargin {
ul {
margin: 0;
padding-inline-start: 2rem;
padding-inline-start: 4ch;
list-style-type: "- ";
}
@ -44,13 +50,26 @@ ul li {
margin-block: 1.5rem;
}
div {
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 {
margin-block: 1.5em;
margin-inline-start: 1em;
margin-inline-start: 2ch;
}
img {
vertical-align: middle;
background-color: #fff8;
}
a {
@ -60,29 +79,44 @@ a {
code {
display: inline-block;
max-width: 100%;
box-sizing: border-box;
font: inherit;
background-color: var(--color-code);
padding-inline: 0.5rem;
padding-inline: 1ch;
color: var(--color-code-text);
user-select: all;
word-break: break-word;
overflow: auto;
vertical-align: top;
}
button, input {
button, input, textarea, select {
border: none;
background: transparent;
color: inherit;
font: inherit;
padding: 0;
outline: none;
line-height: 1.5rem;
resize: none;
}
input[type=text], input[type=url] {
padding-inline: 0.5rem;
border-inline: 0.5em solid var(--color-accent);
select {
height: 1.5rem;
}
:focus {
outline: 1ch solid var(--color-text);
}
input[type=text], input[type=url], textarea, select {
padding-inline: 1ch;
border-inline: 1ch solid var(--color-accent);
background-color: var(--color-bg-scrim);
}
button, input[type=submit] {
padding-inline: 0.5rem;
padding-inline: 1ch;
background-color: var(--color-accent);
color: var(--color-bg);
cursor: pointer;
@ -91,4 +125,35 @@ button, input[type=submit] {
button:active, input[type=submit]:active {
background-color: transparent;
color: var(--color-accent);
}
marquee {
vertical-align: top;
}
.clearfix:after {
content: "";
display: table;
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);
}

View file

@ -5,5 +5,5 @@
}
:root {
--font-main: "dos";
--font-main: "dos", monospace;
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 137 KiB

View file

@ -22,8 +22,7 @@ html {
body {
margin: 0;
padding: 0.1px 1rem;
padding-block-end: 1.5rem;
padding: 0.1px 2ch;
min-height: 100%;
box-sizing: border-box;
width: 100%;
@ -31,4 +30,30 @@ body {
background: var(--color-bg);
color: var(--color-text);
font-family: var(--font-main);
}
.matkap_ascii {
display: inline-block;
position: fixed;
right: 0;
bottom: 0;
width: 80ch;
white-space: pre;
user-select: none;
pointer-events: none;
/* z-index: -1; */
opacity: 0.1;
line-height: 1;
}
.entity_box {
margin: 3rem 1ch;
padding-inline: 1ch;
padding-block: 0.1px;
/* border: 1ch 1rem solid var(--color-accent); */
border-image: url("/assets/accents/9s_chr.png");
border-image-slice: 12 6;
border-image-width: 1rem 1ch;
border-image-repeat: stretch;
border-image-outset: 1.25rem 1ch;
}

504
index.js
View file

@ -5,6 +5,13 @@ import SQLite from 'better-sqlite3';
import { Kysely, SqliteDialect } from 'kysely';
import { nanoid } from "nanoid";
import { JSDOM } from "jsdom";
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();
@ -18,27 +25,35 @@ const db = new Kysely({
})
});
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 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 );`);
const app = express();
app.set('trust proxy', true);
app.set('view engine', 'ejs');
app.use('/assets', express.static('assets'));
app.set('views', path.join(__dirname, "views", "pages"));
app.use(express.urlencoded({
extended: true
}))
// app.use(async (req, res, next) => {
// const buttonsJson = await fetch("https://cdn.abtmtr.link/site_content/buttons.json")
// .catch(() => res.status(500).send())
// .then((res) => res.json())
// .catch(() => res.status(500).send());
// const followingJson = await fetch("https://cdn.abtmtr.link/site_content/following.json")
// .catch(() => res.status(500).send())
// .then((res) => res.json())
// .catch(() => res.status(500).send());
// app.locals.buttons = buttonsJson;
// app.locals.following = followingJson;
// next();
// })
app.locals.siteMap = sitemap;
app.locals.curCommit = "0000000000";
async function getCurCommit() {
const curCommit = await fetch("https://git.abtmtr.link/api/v1/repos/MeowcaTheoRange/abtmtr-v13/branches/main")
.catch(() => res.status(500).send())
.then((res) => res.json());
app.locals.curCommit = curCommit.commit.id.substr(0, 10);
}
getCurCommit();
app.get('/', async (req, res) => {
const statusesJson = await fetch("https://cdn.abtmtr.link/site_content/v13/statuses.json")
@ -75,9 +90,400 @@ app.get('/about', async (req, res) => {
const linksJson = await fetch("https://cdn.abtmtr.link/site_content/v13/links.json")
.catch(() => res.status(500).send())
.then((res) => res.json());
const currentlyListeningJson = await fetch(`https://ws.audioscrobbler.com/2.0/?method=user.getrecenttracks&user=MeowcaTheoRange&api_key=${process.env.LFM_API_KEY}&format=json&limit=2&extended=1`)
.catch(() => res.status(500).send())
.then((res) => res.json());
res.render('about', {
links: linksJson
links: linksJson,
cl: currentlyListeningJson.recenttracks.track[0],
getRelativeTime
});
});
app.get('/characters', async (req, res) => {
const charactersJson = await fetch("https://cdn.abtmtr.link/site_content/v13/characters.json")
.catch(() => res.status(500).send())
.then((res) => res.json());
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: process.env.VERIF_URL
});
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: process.env.VERIF_URL
});
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":
if (process.env.MININQ_WEBHOOK != null) {
const mininq_pub = await db
.selectFrom('mininq')
.selectAll()
.where('id', '=', body.id)
.executeTakeFirst();
await fetch(process.env.MININQ_WEBHOOK, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
"content": "New mininq:",
"embeds": [
{
"description": mininq_pub.msg,
"color": 12298956,
// TODO: make this smaller
"author": mininq_pub.url != '' ? {
"name": `${mininq_pub.name} @ ${new URL(mininq_pub.url).host} sent:`,
"url": `https://abtmtr.link/mininq/#${mininq.id}`
} : {
"name": `${mininq_pub.name} sent:`,
"url": `https://abtmtr.link/mininq/#${mininq.id}`
},
"timestamp": new Date(mininq_pub.time).toISOString()
}
],
"attachments": []
})
});
}
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");
if (process.env.MININQ_WEBHOOK != null) {
const mininq = await db
.selectFrom('mininq')
.selectAll()
.where('id', '=', body.id)
.executeTakeFirst();
await fetch(process.env.MININQ_WEBHOOK, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
"content": "New mininq:",
"embeds": [
{
"description": mininq.msg,
"color": 12298956,
"author": mininq.url != '' ? {
"name": `${mininq.name} @ ${new URL(mininq.url).host} sent:`,
"url": `https://abtmtr.link/mininq/#${mininq.id}`
} : {
"name": `${mininq.name} sent:`
},
"timestamp": new Date(mininq.time).toISOString()
},
{
"description": body.msg,
"color": 13421772,
"author": {
"name": "sysadmin reply:"
}
}
],
"attachments": []
})
});
}
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;
const isUrlAlreadyMininq = await db
.selectFrom('mininq')
.select('url')
.where('url', '=', newObject.url)
.where('public', '=', 0)
.executeTakeFirst();
if (isUrlAlreadyMininq != null) return res.status(403).send("Pubkey is already in mailbox");
}
} 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) => {
const rssXML = await fetch("https://cdn.abtmtr.link/site_content/rss.xml")
.catch(() => res.status(500).send())
.then((res) => res.text());
const rssParsed = await rssParser.parseString(rssXML);
res.render('updates', {
rss: rssParsed
});
});
@ -141,7 +547,28 @@ app.get('/blurbs/send', async (req, res) => {
message: errors.join(", ")
});
try {
const domain = new URL(body.site).hostname;
const isBadSite = await db
.selectFrom('blacklist')
.selectAll()
.where('domain', '=', domain)
.executeTakeFirst();
if (isBadSite != null)
return res.status(403).json({
error: "Forbidden",
message: "\u{1f595}"
});
} catch (err) {
return res.status(500).json({
error: "Internal Server Error",
message: "URL CONSTRUCTOR FAILED???? i think this might be your fault...."
});
}
const postId = nanoid(32);
const timestamp = Date.now();
try {
await db.insertInto('blurbs')
@ -150,7 +577,7 @@ app.get('/blurbs/send', async (req, res) => {
site: body.site,
blurb: body.text,
verified: 0,
time: Date.now()
time: timestamp
})
.executeTakeFirstOrThrow();
} catch (err) {
@ -178,14 +605,37 @@ app.get('/blurbs/check', async (req, res) => {
const relationLink = dom.window.document.querySelector(
`[rel=me][href="https://abtmtr.link/blurbs/#${blurbFromId.id}"]`
);
if (relationLink != null) await db
.updateTable('blurbs')
.set({
verified: 1
})
.where('id', '=', body.id)
.executeTakeFirst();
else {
if (relationLink != null && blurbFromId.verified != 1) {
await db
.updateTable('blurbs')
.set({
verified: 1
})
.where('id', '=', body.id)
.executeTakeFirst();
if (process.env.BLURBS_WEBHOOK != null) {
await fetch(process.env.BLURBS_WEBHOOK, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
"content": "New blurb:",
"embeds": [
{
"description": blurbFromId.blurb,
"color": 12298956,
"author": {
"name": `${new URL(blurbFromId.site).host}`,
"url": `https://abtmtr.link/blurbs/#${blurbFromId.id}`
},
"timestamp": new Date(blurbFromId.time).toISOString()
}
],
"attachments": []
})
});
}
} else if (relationLink == null) {
await db
.updateTable('blurbs')
.set({
@ -204,6 +654,14 @@ app.get("/favicon.ico", (req, res) => {
res.redirect("https://cdn.abtmtr.link/site_content/favicon.ico")
})
app.get(["/rss", "/rss.xml"], (req, res) => {
res.redirect("https://cdn.abtmtr.link/site_content/rss.xml")
})
app.all('*', (req, res) => {
res.status(404).render('404');
})
app.listen(process.env.PORT, () => {
const url = new URL("http://localhost/");
url.port = process.env.PORT;

20
modules/fromHex.js Normal file
View 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
View 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)
}

85
package-lock.json generated
View file

@ -13,10 +13,13 @@
"dotenv": "^16.4.5",
"ejs": "^3.1.10",
"express": "^4.19.2",
"express-basic-auth": "^1.2.1",
"jsdom": "^25.0.0",
"kysely": "^0.27.4",
"nanoid": "^5.0.7",
"pg": "^8.12.0"
"pg": "^8.12.0",
"rss-parser": "^3.13.0",
"tweetnacl": "^1.0.3"
},
"devDependencies": {
"nodemon": "^3.1.4"
@ -143,6 +146,24 @@
],
"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": {
"version": "11.2.1",
"resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-11.2.1.tgz",
@ -679,6 +700,15 @@
"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": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz",
@ -1805,6 +1835,25 @@
"integrity": "sha512-TrEMa7JGdVm0UThDJSx7ddw5nVm3UJS9o9CCIZ72B1vSyEZoziDqBYP3XIoi/12lKrJR8rE3jeFHMok2F/Mnsg==",
"license": "MIT"
},
"node_modules/rss-parser": {
"version": "3.13.0",
"resolved": "https://registry.npmjs.org/rss-parser/-/rss-parser-3.13.0.tgz",
"integrity": "sha512-7jWUBV5yGN3rqMMj7CZufl/291QAhvrrGpDNE4k/02ZchL0npisiYYqULF71jCEKoIiHvK/Q2e6IkDwPziT7+w==",
"license": "MIT",
"dependencies": {
"entities": "^2.0.3",
"xml2js": "^0.5.0"
}
},
"node_modules/rss-parser/node_modules/entities": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/entities/-/entities-2.2.0.tgz",
"integrity": "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==",
"license": "BSD-2-Clause",
"funding": {
"url": "https://github.com/fb55/entities?sponsor=1"
}
},
"node_modules/safe-buffer": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
@ -1831,6 +1880,12 @@
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
"license": "MIT"
},
"node_modules/sax": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/sax/-/sax-1.4.1.tgz",
"integrity": "sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg==",
"license": "ISC"
},
"node_modules/saxes": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz",
@ -2152,6 +2207,12 @@
"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": {
"version": "1.6.18",
"resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
@ -2327,6 +2388,28 @@
"node": ">=18"
}
},
"node_modules/xml2js": {
"version": "0.5.0",
"resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.5.0.tgz",
"integrity": "sha512-drPFnkQJik/O+uPKpqSgr22mpuFHqKdbS835iAQrUC73L2F5WkboIRd63ai/2Yg6I1jzifPFKH2NTK+cfglkIA==",
"license": "MIT",
"dependencies": {
"sax": ">=0.6.0",
"xmlbuilder": "~11.0.0"
},
"engines": {
"node": ">=4.0.0"
}
},
"node_modules/xmlbuilder": {
"version": "11.0.1",
"resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz",
"integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==",
"license": "MIT",
"engines": {
"node": ">=4.0"
}
},
"node_modules/xmlchars": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz",

View file

@ -22,10 +22,13 @@
"dotenv": "^16.4.5",
"ejs": "^3.1.10",
"express": "^4.19.2",
"express-basic-auth": "^1.2.1",
"jsdom": "^25.0.0",
"kysely": "^0.27.4",
"nanoid": "^5.0.7",
"pg": "^8.12.0"
"pg": "^8.12.0",
"rss-parser": "^3.13.0",
"tweetnacl": "^1.0.3"
},
"devDependencies": {
"nodemon": "^3.1.4"

30
sitemap.json Normal file
View 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?"
}
]

View file

@ -1,6 +1,9 @@
<footer>
<h1>abtmtr.link v13-2</h1>
<p class="nomargin">(c) MeowcaTheoRange 2023-24</p>
<p class="nomargin">licensed under the <a href="https://git.abtmtr.link/MeowcaTheoRange/KarkatPublicLicense/">KKPL</a>.</p>
<p class="nomargin">fork <a href="https://git.abtmtr.link/MeowcaTheoRange/abtmtr-v13/">MeowcaTheoRange/abtmtr-v13</a> with git</p>
<h1>abtmtr.link v13-2 (c. <%= curCommit %>)</h1>
<p>(c) MeowcaTheoRange 2023-24</p>
<p>
licensed under the <a href="https://git.abtmtr.link/MeowcaTheoRange/KarkatPublicLicense/">KKPL</a> v2.2.<br />
fork <a href="https://git.abtmtr.link/MeowcaTheoRange/abtmtr-v13/">MeowcaTheoRange/abtmtr-v13</a> with git
</p>
<p>font is <a href="https://int10h.org/oldschool-pc-fonts/fontlist/font?dos-v_re_jpn12">DOS/V re. JPN12</a> from <a href="https://int10h.org/oldschool-pc-fonts/">THE OLDSCHOOL PC FONT RESOURCE</a></p>
</footer>

View file

@ -0,0 +1,15 @@
<h3>
<%= mininq.name %>
<% if (mininq.url != '') { %>
@ <a href="<%= mininq.url %>" target="_blank" class="noblock"><%= new URL(mininq.url).host %></a>
<% } %>
sent:
</h3>
<p style="white-space: pre-line;"><%= mininq.msg %></p>
<% if (mininq.reply != '') { %>
<section>
<h3>sysadmin reply:</h3>
<p style="white-space: pre-line;"><%= mininq.reply %></p>
</section>
<% } %>
<p style="opacity: 0.5;"><%= new Date(mininq.time).toLocaleString('en-us', { timeZone: "America/Chicago" }) %> Central Time</p>

View file

@ -3,5 +3,11 @@
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<title>abtmtr.link</title>
<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="me" href="https://abtmtr.link/about">
<link rel="me" href="https://abtmtr.link/blurbs/#Hx7CuB4_zIWMAsOZlyyqsUm2upXEEYEl">
<link rel="me" href="https://abtmtr.link/blurbs/#aQArHxCaViG-Qw0aFkBRhQWbRsoxUqsB">
<link rel="alternate" type="application/rss+xml" href="https://abtmtr.link/rss.xml" title="RSS Feed">
<link rel="alternate" type="application/rss+xml" href="https://abtmtr.link/rss" title="RSS Feed">
</head>

View file

@ -0,0 +1 @@
<span class="matkap_ascii" aria-hidden="true" hidden><%- include("../misc/MATKAP_ASCII.txt") %></span>

View file

@ -0,0 +1,36 @@
▄▄▄
▄▀. ▀██▄
▄█▀ ▀█▄
▄██▀ `██▄
_███└ ▀██▄
_▄__ ▄██▀ ▄_ ▀██▄
▀█_ ²T══█▄▄J_ ▐█▀▄≡ º██
▀▄ _²²T▀ `█▄» └█_ ▄
_▄▄æ═ ▀█░_ █▌═└█
,█▀▀ ▀██▄▄ ██░_ ▐▌░ █
█ ▄A▀└ ▀█▄v _ ▐▌░╤▄_
,█▀ ▀▀██░ █▌░ █`
▄▄██▄▄▄ _▄▄___ ▀▀█▄▄▄▄▄▄▄▀
▄██▀▀░░░░▀█▄ ▄████▀▀▀██▄ .,. ▀▀▀▀▀▀▀▀▀▀▀█
██▀░░░░░░░░░▀█_ ▄▄█▀▀░░░░░░░▀██▄ █_
╒██▄▄░▄▄▄▄▄▄▄▄██▄██▄▄▄▄▄▄░░░░░░██ █_
▄▄▄▄▄▄██ █▌ ███▌ '└└▀█▀└█r _ ▐█
███████ █▌ █▌█▌ ▀ ▐█∩ █▌ █▀
███████ ▀█▄═ _█▀º░█▄ ┌█▀ ██▄▄▄═T└ █▌
`█████▌ º░▀█▄▄J;¿▄█▀░º ░▀█▄ ▄▄▀░` _▄═██░ _██
╘███▀ º²╜▀░░²º `░░▀▀▀▀▀▀▀░ ███░ j█▀
╘██▄ `ººº ╒███░ ▀═▄▄██▀
▀██¿ ═ ▄▄███▄▄▄▄ ¡▄███─
▀██▄ _▄▀ ╓███░░ *▄▄▄████¡ ▐▌
└▀█▄_ ▄▄█▌ ⁿ▄▄████░∩..⌐─▀▀:░∩ █▌▌█
▀███▄_ _▄█▀▀└ _▄███▀░░░█░░░█░▀█æ╧▀ ▐█
└▀█████▄▄▄██▀▀└ _▄▄▄██▀█.▀█º░░▀▄_▐▌ ▀█∩
┌█▀ ,²▀▀██▀█████████▀▀▀▀▀▀█▄▌ ª█▄░ '. ▀█▄▄▄═▄
▀▀ ▄▄░░▀░░██░░░░░░░░░░░░░▄█░ █: └ █
j█▀ `ººº └▀█Ñ▄_░░░░░░░█▀ ▄"█▄▀▄Y▄ █▀
██ ▐▄_ ²▀▀▀▀╤███▀ ,█: ▀██▀▀▄▄ ══▀:
███ ²▀╤▄_ '' ¬▀_ ▀██.º█∩ █▌
,██¡ ▀█░░█▄ ╘█
█▀ ▀█▀ ▀▄_└█▄
█▀ ▀█ └└

13
views/pages/404.ejs Normal file
View file

@ -0,0 +1,13 @@
<!DOCTYPE html>
<html>
<%- include("../components/page-head.ejs") %>
<body>
<main>
<h1>404 Not Found</h1>
<p>it seems the resource you're looking for doesn't exist.<br />try again later?</p>
<p>&lt;&lt; <a href="/">go back home</a></p>
</main>
<%- include("../components/footer.ejs") %>
<%- include("../components/post-main.ejs") %>
</body>
</html>

View file

@ -4,9 +4,10 @@
<body>
<main>
<h1>about</h1>
<p>hey, i'm MeowcaTheoRange.<br />i run abtmtr.link, for the most part.</p>
<p>who runs abtmtr.link?</p>
<h2>MeowcaTheoRange's h-card</h2>
<div class="h-card">
<p>hey, i'm MeowcaTheoRange.<br />i run abtmtr.link, for the most part.</p>
<section class="h-card">
<h3><span class="p-name">Theo Range</span>
(<span class="p-nickname">MeowcaTheoRange</span>)</h3>
<p class="nomargin">(<span class="p-honorific-prefix">Mx.</span>
@ -16,7 +17,7 @@
<span class="p-gender-identity">Non-binary</span>,
<span class="p-pronouns">they/them</span>
</p>
<p><img class="u-photo" src="https://abtmtr.link/favicon.ico" height="72"></p>
<p><img class="u-photo" src="https://abtmtr.link/favicon.ico" height="72" width="72"></p>
<p class="nomargin">
<a class="u-url" href="https://abtmtr.link/">website</a>,
<a class="u-email" href="mailto:me@abtmtr.link">email</a>
@ -28,7 +29,34 @@
<span class="p-region">Minnesota</span>,
<span class="p-country-name">USA</span>
</p>
</div>
</section>
<h2>about MeowcaTheoRange</h2>
<section class="entity_box">
<h3>cheesy nicknames</h3>
<p class="nomargin">none <a href="/mininq">(yet)</a></p>
<h3>favourite hobby</h3>
<p class="nomargin">server management</p>
<h3>favourite sport</h3>
<p class="nomargin">server management</p>
<h3>favourite website</h3>
<p class="nomargin">https://abtmtr.link</p>
<h3>favourite artists</h3>
<p class="nomargin">mostly HALLEY LABS, some dariacore</p>
<h3>flavour</h3>
<p class="nomargin">would NOT taste good</p>
</section>
<% if (cl["@attr"] != null && cl["@attr"].nowplaying) { %>
<h2>MeowcaTheoRange is currently listening to</h2>
<% } else { %>
<h2>MeowcaTheoRange last listened to (<%= getRelativeTime(cl.date.uts * 1000) %>)</h2>
<% } %>
<p style="float: inline-start; margin-block: 0; margin-inline-end: 2ch;"><img class="u-photo" src="<%= cl.image[2]["#text"] %>" height="72" width="72"></p>
<p class="clearfix">
<%= cl.name %><br />
<span style="opacity: 0.5;"><%= cl.album["#text"] %></span><br />
<%= cl.artist.name %><br />
<a href="<%= cl.url %>" target="_blank">See on Last.fm</a>
</p>
<h2>MeowcaTheoRange's links</h2>
<p>where else is MeowcaTheoRange?</p>
<ul>
@ -40,5 +68,6 @@
</ul>
</main>
<%- include("../components/footer.ejs") %>
<%- include("../components/post-main.ejs") %>
</body>
</html>

View file

@ -20,15 +20,21 @@
</p>
<p style="opacity: 0.5;">by clicking this button and successfully submitting a blurb, you agree that your input may be displayed on this site and stored on abtmtr.link servers.</p>
<p style="opacity: 0.5;">you'll also need to verify your blurb by leaving a X/HTML snippet on your site. further instructions will be given to do this after you submit your blurb. thanks!</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>
<h2>current blurbs</h2>
<h2>current blurbs (<%= data.length %>)</h2>
<ul>
<% data.forEach((blurb) => { %>
<li class="blurb" id="<%= blurb.id %>"><a href="<%= blurb.site %>" target="_blank"><%= new URL(blurb.site).host %></a>: <%= blurb.blurb %> <a class="removePopup" href="/blurbs/check?id=<%= blurb.id %>">remove</a></li>
<li class="blurb" id="<%= blurb.id %>">
<a href="<%= blurb.site %>" target="_blank"><%= new URL(blurb.site).host %></a><% if (blurb.time < 1724994580263) { %> <span style="opacity: 0.5;">(pre-v13-2)</span><% } %>:
<%= blurb.blurb %>
<a class="removePopup" href="/blurbs/check?id=<%= blurb.id %>">remove</a>
</li>
<% }) %>
</ul>
</main>
<%- include("../components/footer.ejs") %>
<%- include("../components/post-main.ejs") %>
<style>
.blurb .removePopup {
visibility: hidden;

View file

@ -21,6 +21,7 @@
<p style="opacity: 0.5;">can't/don't want to add the snippet? <a href="/about" target="_blank">contact me</a>.</p>
</main>
<%- include("../components/footer.ejs") %>
<%- include("../components/post-main.ejs") %>
<style>
.blurb .removePopup {
visibility: hidden;

View file

@ -0,0 +1,29 @@
<!DOCTYPE html>
<html>
<%- include("../components/page-head.ejs") %>
<body>
<main>
<h1>characters</h1>
<p>about abtmtr.link's mascots and other such characters</p>
<h2>entity index</h2>
<% characters.forEach((entity) => { %>
<section class="entity_box">
<p style="float: left; margin-inline: 0 2ch;" class="nomargin">
<img src="<%= entity.img %>" height="72" width="72">
</p>
<h3><%= entity.name.toLowerCase() %> / <%= entity.pronouns.toLowerCase() %></h3>
<p class="nomargin"><%= entity.age %> / <%= entity.species %></p>
<p class="nomargin"><%= entity.sexuality %> / <%= entity.gender %></p>
<p class="clearfix nomargin" style="opacity: 0.5;"><%= entity.blurb %></p>
<br />
<% entity.rows.forEach(([k, v]) => { %>
<h4><%= k %></h4>
<p class="nomargin"><%= v %></p>
<% }) %>
</section>
<% }) %>
</main>
<%- include("../components/footer.ejs") %>
<%- include("../components/post-main.ejs") %>
</body>
</html>

View file

@ -7,24 +7,15 @@
<p>abtmtr.link is a domain for all kinds of services,<br />from web search to live-streaming.</p>
<h2>site links</h2>
<ul>
<li>
<h3><a href="/blurbs/">blurbs</a></h3>
<p class="nomargin">what are other sites saying about abtmtr.link?</p>
</li>
<li>
<h3><a href="/servers/">servers</a></h3>
<p class="nomargin">about the services on abtmtr.link and the machines that serve them.</p>
</li>
<li>
<h3><a href="/sites/">sites</a></h3>
<p class="nomargin">a collection of other sites in the form of 88x31s.</p>
</li>
<li>
<h3><a href="/about/">about</a></h3>
<p class="nomargin">who runs abtmtr.link?</p>
</li>
<% siteMap.forEach((crumb) => { %>
<li>
<h3><a href="/<%= crumb.link %>/"><%= crumb.link %></a></h3>
<p class="nomargin"><%= crumb.description %></p>
</li>
<% }) %>
</ul>
</main>
<%- include("../components/footer.ejs") %>
<%- include("../components/post-main.ejs") %>
</body>
</html>

86
views/pages/mininq.ejs Normal file
View 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>&lt;meta name="abtmtr-mininq-key" content="<%= keypair.publicKey %>"&gt;</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
View 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>

View file

@ -28,5 +28,6 @@
</ul>
</main>
<%- include("../components/footer.ejs") %>
<%- include("../components/post-main.ejs") %>
</body>
</html>

View file

@ -4,8 +4,8 @@
<body>
<main>
<h1>sites</h1>
<p>a large collection of 88x31s.</p>
<h2>friends (<%= b.length %>)</h2>
<p>a collection of other sites in the form of 88x31s.</p>
<h2>friends of abtmtr.link (<%= b.length %>)</h2>
<div>
<% b.forEach((button) => { %>
<% if (button.img != null) { %>
@ -15,7 +15,7 @@
<% } %>
<% }) %>
</div>
<h2>following (<%= f.length %>)</h2>
<h2>sites of interest (<%= f.length %>)</h2>
<div>
<% f.forEach((button) => { %>
<% if (button.img != null) { %>
@ -27,6 +27,7 @@
</div>
</main>
<%- include("../components/footer.ejs") %>
<%- include("../components/post-main.ejs") %>
<style>
@property --hue {
syntax: '<number>';
@ -54,10 +55,9 @@
.web-button {
position: relative;
filter: grayscale(1);
left: 0;
top: 0;
transition: all 0.5s ease-out;
transition: box-shadow 0.5s ease-out, left 0.5s ease-out, top 0.5s ease-out;
opacity: 1 !important;
display: inline-block;
width: 88px;
@ -87,7 +87,6 @@
left: -4px;
top: -4px;
z-index: 999;
filter: grayscale(0);
animation: hue-shift 1s linear infinite;
transition: all 0.0625s ease-out;
--shadow: hsl(var(--hue), 50%, 75%);

23
views/pages/updates.ejs Normal file
View file

@ -0,0 +1,23 @@
<!DOCTYPE html>
<html>
<%- include("../components/page-head.ejs") %>
<body>
<main>
<h1>updates</h1>
<p>what's going on with abtmtr.link?</p>
<h2>items</h2>
<p>subscribe to the <a href="/rss">rss feed</a>!</p>
<ul>
<% rss.items.forEach((item) => { %>
<li>
<h3><a href="<%= item.link %>"><%= item.title %></a></h3>
<p class="nomargin" style="opacity: 0.5">by <%= item.author %> / <%= new Date(item.isoDate).toLocaleString(rss.language, { timeZone: "America/Chicago" }) %> Central Time</p>
<p style="white-space: pre-line;"><%= item.content.trim() %></p>
</li>
<% }) %>
</ul>
</main>
<%- include("../components/footer.ejs") %>
<%- include("../components/post-main.ejs") %>
</body>
</html>