cosmic-web/server.js

230 lines
9.0 KiB
JavaScript

import { access, constants, readFile, stat } from 'node:fs';
import express from 'express';
import path from 'node:path';
import { fileURLToPath } from 'url';
import { bufferFile, wc, head, escape } from './utils.js';
const app = express()
const port = 3000
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
app.engine('cosmic', function (filePath, options, callback) {
readFile(filePath, function (err, content) {
var s
if (err) return callback(err)
var rendered = content.toString()
for (s in options) {
if (typeof options[s] === 'string') {
rendered = rendered.replace('##' + s + '##', options[s])
}
}
return callback(null, rendered)
})
})
app.set('views', './views')
app.set('view engine', 'cosmic')
app.get('/', async function (_req, res) {
const intro = bufferFile('/var/gopher/intro.gophermap')
const recent = await head('/var/gopher/listing.gophermap', 20)
var lines = await wc('/var/gopher/listing.gophermap')
var content = ''
for (let i = 0; i < recent.length; ++i) {
const split = recent[i].split('\t')
let link = split[1]
link = link.replace(/\.txt$/, '.html')
const name = split[0].substr(1)
content += '<a href="' + link + '">' + lines + ' <span class="dim">&gt;&gt;</span> ' + escape(name) + '</a>\n'
lines--
}
res.render('index', { intro: intro, recent: content })
})
app.get('/log', async function (req, res) {
const list = await head('/var/gopher/listing.gophermap')
var lines = await wc('/var/gopher/listing.gophermap')
var content = ''
for (let i = 0; i < list.length; ++i) {
const split = list[i].split('\t')
let link = split[1]
link = link.replace(/\.txt$/, '.html')
const name = split[0].substr(1)
content += '<a href="' + link + '">' + String(lines).padStart(3, '0') + ' <span class="dim">&gt;&gt;</span> ' + escape(name) + '</a>\n'
lines--
}
const back = '<a href="/"><span class="dim">&lt;&lt;</span> BACK TO COSMIC VOYAGE</a>'
const label = 'RS001 Log Entries (Newest First):'
const compiled = back + '\n\n' + label + '\n\n' + content
const fullUrl = 'https://cosmic.voyage' + req.originalUrl
res.render('basic', { content: compiled, canonical: fullUrl})
})
app.get('/ships', async function (_req, res) {
const intro = bufferFile('/var/gopher/ships/ships.gophermap')
const list = await head('/var/gopher/listing.gophermap')
const ships = {}
for (let i = 0; i < list.length; ++i) {
const split = list[i].split('\t')
let name = split[0].substr(1)
name = name.split(' - ')[0].trim()
ships[name] = ships[name] + 1 || 1
}
var keys = Object.keys(ships);
keys.sort();
var content = ''
for(let i = 0; i < keys.length; ++i){
content += '<span class="dim">&gt;&gt;</span> <a href="/ships/' + encodeURIComponent(keys[i]) + '">' + keys[i] + ' (' + ships[keys[i]] + ' logs)</a>\n'
}
res.render('ships', { intro: intro, ships: content })
})
app.get('/ships/*', async function (req, res) {
const list = await head('/var/gopher/listing.gophermap')
const ship = decodeURIComponent(req.path).replace(new RegExp('/ships/', 'i'), '').replace(new RegExp('/(?:index.html)?$', 'i'), '');
const description = escape(bufferFile('/var/gopher/' + ship + '/.description')) || ''
const license = bufferFile('/var/gopher/' + ship + '/LICENSE') || ''
var licenseLabel = ''
var licenseContent = ''
if (license) {
const l = await head('/var/gopher/' + ship + '/LICENSE', 1)
licenseLabel = l[0]
licenseContent = '<a href="/' + encodeURIComponent(ship) + '/LICENSE">' + licenseLabel + '</a>\n'
} else {
licenseContent = 'All rights reserved.'
}
var content = ship + ' - Ship Log:\n'
for (let i = list.length - 1; i >= 0; --i) {
if (list[i].startsWith('0' + ship)) {
const split = list[i].split('\t')
let link = split[1]
link = link.replace(/\.txt$/, '.html')
const name = split[0].substr(1)
content += '<a href="' + link + '">' + String(list.length - i).padStart(3, '0') + ' <span class="dim">&gt;&gt;</span> ' + escape(name) + '</a>\n'
}
}
const fullUrl = 'https://cosmic.voyage' + req.originalUrl
res.render('ship-detail', { description: description, content: content, license: licenseContent, canonical: fullUrl })
})
app.get('/rss.xml', function (_req, res) {
var content = bufferFile('/var/gopher/rss.xml')
const domainRegexp = new RegExp('gopher://cosmic.voyage(/./)?', 'g')
const extensionRegexp = new RegExp('\.txt', 'g')
content = content.replace(domainRegexp, 'https://cosmic.voyage/')
content = content.replace(extensionRegexp, '.html')
res.setHeader('content-type', 'text/xml')
res.render('raw', { content: content })
})
app.get('/atom.xml', function (_req, res) {
var content = bufferFile('/var/gopher/atom.xml')
const domainRegexp = new RegExp('gopher://cosmic.voyage/(0/)?', 'g')
const extensionRegexp = new RegExp('\.txt', 'g')
content = content.replace(domainRegexp, 'https://cosmic.voyage/')
content = content.replace(extensionRegexp, '.html')
res.setHeader('content-type', 'text/xml')
res.render('raw', { content: content })
})
app.get('/sitemap.xml', function (_req, res) {
var content = bufferFile('/var/gopher/sitemap.xml')
stat('/var/gopher/rss.xml', (err, stats) => {
if (err) {
res(err)
} else {
content = content.replace(/##date##/g, stats.mtime.toISOString())
res.setHeader('content-type', 'text/xml')
res.render('raw', { content: content })
}
})
})
app.get('/.well-known/webfinger', function(req, res) {
var resource = req.query.resource
if (resource && resource.startsWith('acct:') && resource.endsWith('@cosmic.voyage')) {
const regex = /acct:(\w+)@cosmic\.voyage/i
const resources = resource.match(regex)
if (resources.length) {
const user = resources[1].toLowerCase()
const path = '/home/' + user + '/.webfinger.json'
access(path, constants.R_OK, (err) => {
if (err) {
res.setHeader('content-type', 'application/jrd+json')
res.render('raw', { content: '' })
} else {
const content = bufferFile(path)
res.setHeader('content-type', 'application/jrd+json')
res.render('raw', { content: content })
}
})
} else {
res.setHeader('content-type', 'application/jrd+json')
res.render('raw', { content: '' })
}
} else {
res.setHeader('content-type', 'application/jrd+json')
res.render('raw', { content: '' })
}
})
// Any link to a direct static resource will show it
app.use(express.static(path.join(__dirname, '/static')))
// Override default LICENSE display and format for cosmic styles
app.get('*/LICENSE', function(req, res){
var file = path.join('/var/gopher/', decodeURIComponent(req.path));
access(file, constants.R_OK, (err) => {
if (err) {
const back = '<a href="/"><span class="dim">&lt;&lt;</span> BACK TO COSMIC VOYAGE</a>'
const error = 'Message not found. Please try again.'
const content = back + '\n\n' + error
res.status(404)
const fullUrl = 'https://cosmic.voyage' + req.originalUrl
res.render('basic', { content: content, canonical: fullUrl})
} else {
const file = escape(bufferFile('/var/gopher/' + decodeURIComponent(req.path)))
const back = '<a href="/log"><span class="dim">&lt;&lt;</span> BACK TO RS001 LOG</a>'
const content = back + '\n\n' + file
res.setHeader('content-type', 'text/html')
const fullUrl = 'https://cosmic.voyage' + req.originalUrl
res.render('basic', { content: content, canonical: fullUrl})
}
})
})
// Any other gopher content directly linked will show as-is
app.use(express.static('/var/gopher'))
// Grab anything that's a .txt and wrap it in .html
app.get('*', function(req, res){
if (req.path.indexOf('.html') !== -1) {
const file = path.join('/var/gopher/', decodeURIComponent(req.path).replace(/\.html/, '.txt'));
access(file, constants.R_OK, (err) => {
if (err) {
const back = '<a href="/"><span class="dim">&lt;&lt;</span> BACK TO COSMIC VOYAGE</a>'
const error = 'Message not found. Please try again.'
const content = back + '\n\n' + error
res.status(404)
const fullUrl = 'https://cosmic.voyage' + req.originalUrl
res.render('basic', { content: content, canonical: fullUrl})
} else {
const file = escape(bufferFile('/var/gopher/' + decodeURIComponent(req.path).replace(/\.html/, '.txt')))
const back = '<a href="/log"><span class="dim">&lt;&lt;</span> BACK TO RS001 LOG</a>'
const content = back + '\n\n' + file
const fullUrl = 'https://cosmic.voyage' + req.originalUrl
res.render('basic', { content: content, canonical: fullUrl})
}
})
} else {
const back = '<a href="/"><span class="dim">&lt;&lt;</span> BACK TO COSMIC VOYAGE</a>'
const error = 'Message not found. Please try again.'
const content = back + '\n\n' + error
res.status(404)
const fullUrl = 'https://cosmic.voyage' + req.originalUrl
res.render('basic', { content: content, canonical: fullUrl})
}
})
app.listen(port, () => console.log(`listening on port ${port}!`))