import 'dotenv/config' import express from 'express' import { Eta } from 'eta' import render, { renderHtml } from './lib/render.js' import languageColors from './lib/languagecolors.js' import fetch from 'node-fetch' import hash from './lib/hash.js' import { DMSans, ComicSans } from './lib/fonts.js' // satori is using the native fetch api, causing a warning, so make node-fetch the default globalThis.fetch = fetch const forgejoBaseUrl = process.env.FORGEJO_BASE_URL if (!forgejoBaseUrl) throw new Error('FORGEJO_BASE_URL unspecified') const debug = process.env.DEBUG ? true : false const eta = new Eta({ views: 'views', cache: !debug }) const app = express() app.get('/', function (req, res) { res.redirect('https://git.gay/gitgay/og.git') }) function getLanguagePercentages(languages) { let sum = 0 const values = Object.values(languages) for (const value of values) { sum += value } return Object.fromEntries( Object.keys(languages).map(language => { return [language, (languages[language] / sum) * 100] }) ) } async function getLanguages(owner, repo) { const languagesResp = await fetch( `${forgejoBaseUrl}/api/v1/repos/${encodeURIComponent( owner )}/${encodeURIComponent(repo)}/languages` ) if (languagesResp.ok) { return getLanguagePercentages(await languagesResp.json()) } return {} } const comicSansNames = new Set(['trixie']) app.get('/:owner/:repo', async function (req, res) { const [repoResp, commitsResp, languages] = await Promise.all([ fetch( `${forgejoBaseUrl}/api/v1/repos/${encodeURIComponent( req.params.owner )}/${encodeURIComponent(req.params.repo)}` ), fetch( `${forgejoBaseUrl}/api/v1/repos/${encodeURIComponent( req.params.owner )}/${encodeURIComponent(req.params.repo)}/commits?limit=1` ), getLanguages(req.params.owner, req.params.repo) ]) if (!repoResp.ok) { res.status(repoResp.status) res.end() return } const repo = await repoResp.json() repo.commits_count = Number.parseInt(commitsResp.headers.get('x-total-count')) || 0 const html = await eta.renderAsync('repo', { repo, languages, languageColors, debug }) if (debug) { res.send(await renderHtml(html)) return } const options = { fonts: comicSansNames.has(req.params.owner) ? ComicSans : DMSans, format: req.query.format == 'svg' ? 'svg' : 'png' } res.type(options.format) res.set('Content-Disposition', 'inline') res.send(await render(html, options)) }) app.get('/:owner/:repo/commit/:hash', async function (req, res) { const [commitResp, languages] = await Promise.all([ fetch( `${forgejoBaseUrl}/api/v1/repos/${encodeURIComponent( req.params.owner )}/${encodeURIComponent( req.params.repo )}/git/commits/${encodeURIComponent(req.params.hash)}` ), getLanguages(req.params.owner, req.params.repo) ]) if (!commitResp.ok) { res.status(commitResp.status) res.end() return } const commit = await commitResp.json() const html = await eta.renderAsync('commit', { commit: { ...commit.commit, sha: commit.sha, repository: { full_name: `${req.params.owner}/${req.params.repo}` }, committer: commit.committer ?? { ...commit.commit.author, avatar_url: `${forgejoBaseUrl}/avatar/${hash( commit.commit.author.email.toLowerCase() )}` }, stats: commit.stats, created_at: commit.created }, languages, languageColors, debug }) if (debug) { res.send(await renderHtml(html)) return } const options = { fonts: comicSansNames.has(req.params.owner) ? ComicSans : DMSans, format: req.query.format == 'svg' ? 'svg' : 'png' } res.type(options.format) res.set('Content-Disposition', 'inline') res.send(await render(html, options)) }) app.get('/:owner/:repo/:type/:num', (req, res, next) => { if (req.params.type != 'pull' && req.params.type != 'issue') return next() res.redirect( `/${req.params.owner}/${req.params.repo}/${req.params.type}s/${req.params.num}` ) }) app.get('/:owner/:repo/issues/:num', async function (req, res) { const [issueResp, languages] = await Promise.all([ fetch( `${forgejoBaseUrl}/api/v1/repos/${encodeURIComponent( req.params.owner )}/${encodeURIComponent(req.params.repo)}/issues/${encodeURIComponent( req.params.num )}` ), getLanguages(req.params.owner, req.params.repo) ]) if (!issueResp.ok) { res.status(issueResp.status) res.end() return } const issue = await issueResp.json() if (issue.pull_request) { res.redirect( `/${req.params.owner}/${req.params.repo}/pulls/${req.params.num}` ) return } const html = await eta.renderAsync('issue', { issue, languages, languageColors, debug }) if (debug) { res.send(await renderHtml(html)) return } const options = { fonts: comicSansNames.has(req.params.owner) ? ComicSans : DMSans, format: req.query.format == 'svg' ? 'svg' : 'png' } res.type(options.format) res.set('Content-Disposition', 'inline') res.send(await render(html, options)) }) app.get('/:owner/:repo/pulls/:num', async function (req, res) { const [pullResp, pullCommitsResp, languages] = await Promise.all([ fetch( `${forgejoBaseUrl}/api/v1/repos/${encodeURIComponent( req.params.owner )}/${encodeURIComponent(req.params.repo)}/pulls/${encodeURIComponent( req.params.num )}` ), fetch( `${forgejoBaseUrl}/api/v1/repos/${encodeURIComponent( req.params.owner )}/${encodeURIComponent(req.params.repo)}/pulls/${encodeURIComponent( req.params.num )}/commits?limit=0` ), getLanguages(req.params.owner, req.params.repo) ]) if (!pullResp.ok || !pullCommitsResp.ok) { res.status(pullResp.status) res.end() return } const [pull, commits] = await Promise.all([ pullResp.json(), pullCommitsResp.json() ]) pull.commits_count = Number.parseInt(pullCommitsResp.headers.get('x-total-count')) || 0 const stats = { total: 0, additions: 0, deletions: 0 } for (const commit of commits) { stats.total += commit.stats.total stats.additions += commit.stats.additions stats.deletions += commit.stats.deletions } const html = await eta.renderAsync('issue', { issue: pull, prStats: stats, languages, languageColors, debug }) if (debug) { res.send(await renderHtml(html)) return } const options = { fonts: comicSansNames.has(req.params.owner) ? ComicSans : DMSans, format: req.query.format == 'svg' ? 'svg' : 'png' } res.type(options.format) res.set('Content-Disposition', 'inline') res.send(await render(html, options)) }) const [port, host] = [process.env.PORT ?? 9054, process.env.HOST ?? '127.0.0.1'] app.listen(port, host, () => { console.log(`Listening on http://${host}:${port}`) })