Rewrite It In Rust
This commit is contained in:
parent
3f13d7a0e9
commit
6c962be80c
21 changed files with 3530 additions and 1413 deletions
139
.gitignore
vendored
139
.gitignore
vendored
|
@ -1,135 +1,4 @@
|
||||||
# ---> Node
|
/target
|
||||||
# Logs
|
/files
|
||||||
logs
|
/temp
|
||||||
*.log
|
config.toml
|
||||||
npm-debug.log*
|
|
||||||
yarn-debug.log*
|
|
||||||
yarn-error.log*
|
|
||||||
lerna-debug.log*
|
|
||||||
.pnpm-debug.log*
|
|
||||||
|
|
||||||
# Diagnostic reports (https://nodejs.org/api/report.html)
|
|
||||||
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
|
|
||||||
|
|
||||||
# Runtime data
|
|
||||||
pids
|
|
||||||
*.pid
|
|
||||||
*.seed
|
|
||||||
*.pid.lock
|
|
||||||
|
|
||||||
# Directory for instrumented libs generated by jscoverage/JSCover
|
|
||||||
lib-cov
|
|
||||||
|
|
||||||
# Coverage directory used by tools like istanbul
|
|
||||||
coverage
|
|
||||||
*.lcov
|
|
||||||
|
|
||||||
# nyc test coverage
|
|
||||||
.nyc_output
|
|
||||||
|
|
||||||
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
|
|
||||||
.grunt
|
|
||||||
|
|
||||||
# Bower dependency directory (https://bower.io/)
|
|
||||||
bower_components
|
|
||||||
|
|
||||||
# node-waf configuration
|
|
||||||
.lock-wscript
|
|
||||||
|
|
||||||
# Compiled binary addons (https://nodejs.org/api/addons.html)
|
|
||||||
build/Release
|
|
||||||
|
|
||||||
# Dependency directories
|
|
||||||
node_modules/
|
|
||||||
jspm_packages/
|
|
||||||
|
|
||||||
# Snowpack dependency directory (https://snowpack.dev/)
|
|
||||||
web_modules/
|
|
||||||
|
|
||||||
# TypeScript cache
|
|
||||||
*.tsbuildinfo
|
|
||||||
|
|
||||||
# Optional npm cache directory
|
|
||||||
.npm
|
|
||||||
|
|
||||||
# Optional eslint cache
|
|
||||||
.eslintcache
|
|
||||||
|
|
||||||
# Optional stylelint cache
|
|
||||||
.stylelintcache
|
|
||||||
|
|
||||||
# Microbundle cache
|
|
||||||
.rpt2_cache/
|
|
||||||
.rts2_cache_cjs/
|
|
||||||
.rts2_cache_es/
|
|
||||||
.rts2_cache_umd/
|
|
||||||
|
|
||||||
# Optional REPL history
|
|
||||||
.node_repl_history
|
|
||||||
|
|
||||||
# Output of 'npm pack'
|
|
||||||
*.tgz
|
|
||||||
|
|
||||||
# Yarn Integrity file
|
|
||||||
.yarn-integrity
|
|
||||||
|
|
||||||
# dotenv environment variable files
|
|
||||||
.env
|
|
||||||
.env.development.local
|
|
||||||
.env.test.local
|
|
||||||
.env.production.local
|
|
||||||
.env.local
|
|
||||||
|
|
||||||
# parcel-bundler cache (https://parceljs.org/)
|
|
||||||
.cache
|
|
||||||
.parcel-cache
|
|
||||||
|
|
||||||
# Next.js build output
|
|
||||||
.next
|
|
||||||
out
|
|
||||||
|
|
||||||
# Nuxt.js build / generate output
|
|
||||||
.nuxt
|
|
||||||
dist
|
|
||||||
|
|
||||||
# Gatsby files
|
|
||||||
.cache/
|
|
||||||
# Comment in the public line in if your project uses Gatsby and not Next.js
|
|
||||||
# https://nextjs.org/blog/next-9-1#public-directory-support
|
|
||||||
# public
|
|
||||||
|
|
||||||
# vuepress build output
|
|
||||||
.vuepress/dist
|
|
||||||
|
|
||||||
# vuepress v2.x temp and cache directory
|
|
||||||
.temp
|
|
||||||
.cache
|
|
||||||
|
|
||||||
# Docusaurus cache and generated files
|
|
||||||
.docusaurus
|
|
||||||
|
|
||||||
# Serverless directories
|
|
||||||
.serverless/
|
|
||||||
|
|
||||||
# FuseBox cache
|
|
||||||
.fusebox/
|
|
||||||
|
|
||||||
# DynamoDB Local files
|
|
||||||
.dynamodb/
|
|
||||||
|
|
||||||
# TernJS port file
|
|
||||||
.tern-port
|
|
||||||
|
|
||||||
# Stores VSCode versions used for testing VSCode extensions
|
|
||||||
.vscode-test
|
|
||||||
|
|
||||||
# yarn v2
|
|
||||||
.yarn/cache
|
|
||||||
.yarn/unplugged
|
|
||||||
.yarn/build-state.yml
|
|
||||||
.yarn/install-state.gz
|
|
||||||
.pnp.*
|
|
||||||
|
|
||||||
files/
|
|
||||||
temp/
|
|
||||||
config.json
|
|
||||||
|
|
2837
Cargo.lock
generated
Normal file
2837
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load diff
35
Cargo.toml
Normal file
35
Cargo.toml
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
[package]
|
||||||
|
name = "biter"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
[profile.release]
|
||||||
|
lto = "fat"
|
||||||
|
strip = true
|
||||||
|
|
||||||
|
[features]
|
||||||
|
default = ["db"]
|
||||||
|
db = ["sqlx"]
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
askama = { version = "0.12.1", features = ["with-axum", "markdown"] }
|
||||||
|
askama_axum = "0.4.0"
|
||||||
|
axum = "0.7.4"
|
||||||
|
serde = { version = "1.0.197", features = ["derive"] }
|
||||||
|
tokio = { version = "1.36.0", features = ["rt-multi-thread", "macros"] }
|
||||||
|
toml = "0.8.10"
|
||||||
|
tower-http = { version = "0.5.2", features = ["trace", "fs"] }
|
||||||
|
tracing = "0.1.40"
|
||||||
|
tracing-subscriber = { version = "0.3.18", features = ["json", "env-filter"] }
|
||||||
|
sqlx = { version = "0.7.3", optional = true, features = [
|
||||||
|
"runtime-tokio",
|
||||||
|
"postgres",
|
||||||
|
] }
|
||||||
|
thiserror = "1.0.57"
|
||||||
|
reqwest = { version = "0.11.25", default-features = false, features = [
|
||||||
|
"rustls-tls",
|
||||||
|
"json",
|
||||||
|
] }
|
||||||
|
html5ever = "0.26.0"
|
||||||
|
url = { version = "2.5.0", features = ["serde"] }
|
||||||
|
markup5ever_rcdom = "0.2.0"
|
|
@ -1,7 +1,5 @@
|
||||||
# biter
|
# biter
|
||||||
|
|
||||||
shitty twitter proxy dont use
|
bad but also really fast twitter proxy
|
||||||
|
|
||||||
# Usage
|
still very under construction
|
||||||
|
|
||||||
read above paragraph
|
|
||||||
|
|
87
index.js
87
index.js
|
@ -1,87 +0,0 @@
|
||||||
import express from "express";
|
|
||||||
import logRequest from "./logger.js";
|
|
||||||
import { parse } from "node-html-parser";
|
|
||||||
import { format } from "util";
|
|
||||||
import escape from "escape-html";
|
|
||||||
|
|
||||||
const config = {
|
|
||||||
FUCKING_ENDPOINT:
|
|
||||||
process.env.FUCKING_ENDPOINT ??
|
|
||||||
"https://publish.twitter.com/oembed?url=",
|
|
||||||
TWITTER_BASE_URL: process.env.TWITTER_BASE_URL ?? "https://x.com/",
|
|
||||||
title: process.env.APP_TITLE ?? "biter",
|
|
||||||
description: process.env.APP_TITLE ?? "biter twitter proxy",
|
|
||||||
};
|
|
||||||
|
|
||||||
const app = express();
|
|
||||||
|
|
||||||
app.set("view engine", "ejs");
|
|
||||||
app.set("views", "views");
|
|
||||||
|
|
||||||
app.use(logRequest);
|
|
||||||
app.use(express.static("static"));
|
|
||||||
|
|
||||||
app.get("/*/status/*", async (req, res) => {
|
|
||||||
let target =
|
|
||||||
config.FUCKING_ENDPOINT +
|
|
||||||
encodeURIComponent(config.TWITTER_BASE_URL + req.url.slice(1));
|
|
||||||
let twitter_res;
|
|
||||||
try {
|
|
||||||
twitter_res = await fetch(target);
|
|
||||||
} catch (err) {
|
|
||||||
return res
|
|
||||||
.status(500)
|
|
||||||
.send(
|
|
||||||
`<p>Error from Twitter API:</p><pre>${escape(
|
|
||||||
format("%s", err)
|
|
||||||
)}</pre>`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (twitter_res.status == 200) {
|
|
||||||
let body;
|
|
||||||
try {
|
|
||||||
body = await twitter_res.json();
|
|
||||||
} catch (err) {
|
|
||||||
return res
|
|
||||||
.status(500)
|
|
||||||
.send(
|
|
||||||
`<p>Error from Twitter API:</p><pre>${escape(
|
|
||||||
format("%s", err)
|
|
||||||
)}</pre>`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
let html = parse(body.html);
|
|
||||||
let block = html.firstChild;
|
|
||||||
let content = block.childNodes[0].innerText;
|
|
||||||
let author = block.childNodes[1].toString();
|
|
||||||
let [_, authorName, authorHandle] = author.match(
|
|
||||||
/— (.+) \(@([\w\d_]+)\)/
|
|
||||||
);
|
|
||||||
let authorUrl = config.TWITTER_BASE_URL + authorHandle;
|
|
||||||
res.render("displayTweet", {
|
|
||||||
title: config.title,
|
|
||||||
description: config.description,
|
|
||||||
authorName,
|
|
||||||
authorHandle,
|
|
||||||
authorUrl,
|
|
||||||
content,
|
|
||||||
url: config.TWITTER_BASE_URL + req.url.slice(1),
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
return res.sendStatus(502);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
app.get("/", (req, res) =>
|
|
||||||
res.render("index", {
|
|
||||||
title: config.title,
|
|
||||||
description: config.description,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
let server = app.listen(process.env.PORT ?? 3000, () => {
|
|
||||||
let address = server.address();
|
|
||||||
let ip = address.address;
|
|
||||||
if (address.family === "IPv6") ip = `[${ip}]`;
|
|
||||||
console.log(`listening on http://${ip}:${address.port}/`);
|
|
||||||
});
|
|
109
logger.js
109
logger.js
|
@ -1,109 +0,0 @@
|
||||||
const out = process.stderr;
|
|
||||||
const IS_TTY = out.isTTY;
|
|
||||||
if (IS_TTY) {
|
|
||||||
out.cursorTo(0, out.getWindowSize()[1]);
|
|
||||||
}
|
|
||||||
|
|
||||||
const colorStatusCode = (n) => {
|
|
||||||
if (n >= 500) {
|
|
||||||
return `\x1b[91m${n}`;
|
|
||||||
} else if (n >= 400) {
|
|
||||||
return `\x1b[93m${n}`;
|
|
||||||
} else if (n >= 300) {
|
|
||||||
return `\x1b[33m${n}`;
|
|
||||||
} else if (n >= 200) {
|
|
||||||
return `\x1b[92m${n}`;
|
|
||||||
} else {
|
|
||||||
return n.toString();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const pretty_µs = (n) => {
|
|
||||||
let f = [];
|
|
||||||
if (n >= 3600000000) f.push(Math.floor(n / 3000600000) + "h");
|
|
||||||
if (n % 3600000000 >= 60000000)
|
|
||||||
f.push(Math.floor((n % 3600000000) / 60000000) + "m");
|
|
||||||
if (n % 60000000 >= 1000000)
|
|
||||||
f.push(Math.floor((n % 60000000) / 1000000) + "s");
|
|
||||||
if (n % 1000000 >= 1000) f.push(Math.floor((n % 1000000) / 1000) + "ms");
|
|
||||||
if (n % 1000) f.push(Math.floor(n % 1000) + "µs");
|
|
||||||
return f.join(" ");
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @typedef {Object} Logging
|
|
||||||
* @property {number} ts
|
|
||||||
* @property {string} method
|
|
||||||
* @property {string} url
|
|
||||||
* @property {function(Express.Response): void} callback
|
|
||||||
* @property {boolean} resolved
|
|
||||||
*/
|
|
||||||
/** @type {Logging[]} */
|
|
||||||
const loggingRequests = [];
|
|
||||||
let minLength = 11; // GLOBAL STATE NOOOO
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {Logging} log
|
|
||||||
*/
|
|
||||||
function processLog(log) {
|
|
||||||
loggingRequests.push(log);
|
|
||||||
let time = (log.ts / 1000).toFixed(6);
|
|
||||||
|
|
||||||
if (IS_TTY) {
|
|
||||||
out.cursorTo(0, out.getWindowSize()[1]);
|
|
||||||
if (time.length < minLength) time = time.padStart(minLength, " ");
|
|
||||||
let statusOffset = time.length + 3; // from start
|
|
||||||
let timeOffset = log.method.length + log.url.length + 3; // from end of status
|
|
||||||
out.write(
|
|
||||||
`\x1b[32m[${time}]\x1b[0m \x1b[90m...\x1b[0m \x1b[1m${log.method}\x1b[0m ${log.url}\n`
|
|
||||||
);
|
|
||||||
log.callback = (res) => {
|
|
||||||
let idx = loggingRequests.findIndex((x) => x === log);
|
|
||||||
if (idx === -1) return;
|
|
||||||
loggingRequests.splice(idx, 1);
|
|
||||||
|
|
||||||
let line =
|
|
||||||
out.getWindowSize()[1] - loggingRequests.length + idx - 2;
|
|
||||||
if (line < 0) return;
|
|
||||||
out.cursorTo(statusOffset, line);
|
|
||||||
out.write(
|
|
||||||
"\x1b[0m\x1b[1m" + colorStatusCode(res.statusCode) + "\x1b[0m"
|
|
||||||
);
|
|
||||||
out.moveCursor(timeOffset, 0);
|
|
||||||
out.write(
|
|
||||||
`\x1b[90m(${pretty_µs(
|
|
||||||
(performance.now() - log.ts) * 1000
|
|
||||||
)})\x1b[0m`
|
|
||||||
);
|
|
||||||
out.cursorTo(0, out.getWindowSize()[1]);
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
log.callback = (res) => {
|
|
||||||
out.write(
|
|
||||||
`[${time}] ${res.statusCode} ${log.method} ${
|
|
||||||
log.url
|
|
||||||
} (${pretty_µs((performance.now() - log.ts) * 1000)})\n`
|
|
||||||
);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const logRequest = (req, res, next) => {
|
|
||||||
let start = performance.now();
|
|
||||||
/** @type {Logging} */
|
|
||||||
let log = {
|
|
||||||
ts: start,
|
|
||||||
method: req.method,
|
|
||||||
url: req.url,
|
|
||||||
};
|
|
||||||
let end = res.end;
|
|
||||||
res.end = (...args) => {
|
|
||||||
end.call(res, ...args);
|
|
||||||
log.resolved = true;
|
|
||||||
if (log.callback instanceof Function) log.callback(res);
|
|
||||||
};
|
|
||||||
processLog(log);
|
|
||||||
next();
|
|
||||||
};
|
|
||||||
|
|
||||||
export default logRequest;
|
|
10
migrations/20240310230509_first.sql
Normal file
10
migrations/20240310230509_first.sql
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
CREATE TABLE posts (
|
||||||
|
id bigint PRIMARY KEY,
|
||||||
|
url text NOT NULL,
|
||||||
|
handle text NOT NULL,
|
||||||
|
name text NOT NULL,
|
||||||
|
body text NOT NULL,
|
||||||
|
media text[] NOT NULL,
|
||||||
|
image text,
|
||||||
|
alt text
|
||||||
|
);
|
975
package-lock.json
generated
975
package-lock.json
generated
|
@ -1,975 +0,0 @@
|
||||||
{
|
|
||||||
"name": "biter",
|
|
||||||
"version": "1.0.0",
|
|
||||||
"lockfileVersion": 3,
|
|
||||||
"requires": true,
|
|
||||||
"packages": {
|
|
||||||
"": {
|
|
||||||
"name": "biter",
|
|
||||||
"version": "1.0.0",
|
|
||||||
"license": "ISC",
|
|
||||||
"dependencies": {
|
|
||||||
"ejs": "^3.1.9",
|
|
||||||
"escape-html": "^1.0.3",
|
|
||||||
"express": "^4.18.3",
|
|
||||||
"node-html-parser": "^6.1.12"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/accepts": {
|
|
||||||
"version": "1.3.8",
|
|
||||||
"resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
|
|
||||||
"integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==",
|
|
||||||
"dependencies": {
|
|
||||||
"mime-types": "~2.1.34",
|
|
||||||
"negotiator": "0.6.3"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 0.6"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/ansi-styles": {
|
|
||||||
"version": "4.3.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
|
|
||||||
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
|
|
||||||
"dependencies": {
|
|
||||||
"color-convert": "^2.0.1"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=8"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/array-flatten": {
|
|
||||||
"version": "1.1.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
|
|
||||||
"integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg=="
|
|
||||||
},
|
|
||||||
"node_modules/async": {
|
|
||||||
"version": "3.2.5",
|
|
||||||
"resolved": "https://registry.npmjs.org/async/-/async-3.2.5.tgz",
|
|
||||||
"integrity": "sha512-baNZyqaaLhyLVKm/DlvdW051MSgO6b8eVfIezl9E5PqWxFgzLm/wQntEW4zOytVburDEr0JlALEpdOFwvErLsg=="
|
|
||||||
},
|
|
||||||
"node_modules/balanced-match": {
|
|
||||||
"version": "1.0.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
|
|
||||||
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="
|
|
||||||
},
|
|
||||||
"node_modules/body-parser": {
|
|
||||||
"version": "1.20.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.2.tgz",
|
|
||||||
"integrity": "sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA==",
|
|
||||||
"dependencies": {
|
|
||||||
"bytes": "3.1.2",
|
|
||||||
"content-type": "~1.0.5",
|
|
||||||
"debug": "2.6.9",
|
|
||||||
"depd": "2.0.0",
|
|
||||||
"destroy": "1.2.0",
|
|
||||||
"http-errors": "2.0.0",
|
|
||||||
"iconv-lite": "0.4.24",
|
|
||||||
"on-finished": "2.4.1",
|
|
||||||
"qs": "6.11.0",
|
|
||||||
"raw-body": "2.5.2",
|
|
||||||
"type-is": "~1.6.18",
|
|
||||||
"unpipe": "1.0.0"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 0.8",
|
|
||||||
"npm": "1.2.8000 || >= 1.4.16"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/boolbase": {
|
|
||||||
"version": "1.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz",
|
|
||||||
"integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww=="
|
|
||||||
},
|
|
||||||
"node_modules/brace-expansion": {
|
|
||||||
"version": "1.1.11",
|
|
||||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
|
|
||||||
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
|
|
||||||
"dependencies": {
|
|
||||||
"balanced-match": "^1.0.0",
|
|
||||||
"concat-map": "0.0.1"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/bytes": {
|
|
||||||
"version": "3.1.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
|
|
||||||
"integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==",
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 0.8"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/call-bind": {
|
|
||||||
"version": "1.0.7",
|
|
||||||
"resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz",
|
|
||||||
"integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==",
|
|
||||||
"dependencies": {
|
|
||||||
"es-define-property": "^1.0.0",
|
|
||||||
"es-errors": "^1.3.0",
|
|
||||||
"function-bind": "^1.1.2",
|
|
||||||
"get-intrinsic": "^1.2.4",
|
|
||||||
"set-function-length": "^1.2.1"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 0.4"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://github.com/sponsors/ljharb"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/chalk": {
|
|
||||||
"version": "4.1.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
|
|
||||||
"integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
|
|
||||||
"dependencies": {
|
|
||||||
"ansi-styles": "^4.1.0",
|
|
||||||
"supports-color": "^7.1.0"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=10"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://github.com/chalk/chalk?sponsor=1"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/color-convert": {
|
|
||||||
"version": "2.0.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
|
||||||
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
|
|
||||||
"dependencies": {
|
|
||||||
"color-name": "~1.1.4"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=7.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/color-name": {
|
|
||||||
"version": "1.1.4",
|
|
||||||
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
|
|
||||||
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="
|
|
||||||
},
|
|
||||||
"node_modules/concat-map": {
|
|
||||||
"version": "0.0.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
|
|
||||||
"integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="
|
|
||||||
},
|
|
||||||
"node_modules/content-disposition": {
|
|
||||||
"version": "0.5.4",
|
|
||||||
"resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz",
|
|
||||||
"integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==",
|
|
||||||
"dependencies": {
|
|
||||||
"safe-buffer": "5.2.1"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 0.6"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/content-type": {
|
|
||||||
"version": "1.0.5",
|
|
||||||
"resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz",
|
|
||||||
"integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==",
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 0.6"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/cookie": {
|
|
||||||
"version": "0.5.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz",
|
|
||||||
"integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==",
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 0.6"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/cookie-signature": {
|
|
||||||
"version": "1.0.6",
|
|
||||||
"resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz",
|
|
||||||
"integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ=="
|
|
||||||
},
|
|
||||||
"node_modules/css-select": {
|
|
||||||
"version": "5.1.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/css-select/-/css-select-5.1.0.tgz",
|
|
||||||
"integrity": "sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg==",
|
|
||||||
"dependencies": {
|
|
||||||
"boolbase": "^1.0.0",
|
|
||||||
"css-what": "^6.1.0",
|
|
||||||
"domhandler": "^5.0.2",
|
|
||||||
"domutils": "^3.0.1",
|
|
||||||
"nth-check": "^2.0.1"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://github.com/sponsors/fb55"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/css-what": {
|
|
||||||
"version": "6.1.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/css-what/-/css-what-6.1.0.tgz",
|
|
||||||
"integrity": "sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==",
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 6"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://github.com/sponsors/fb55"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/debug": {
|
|
||||||
"version": "2.6.9",
|
|
||||||
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
|
|
||||||
"integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
|
|
||||||
"dependencies": {
|
|
||||||
"ms": "2.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/define-data-property": {
|
|
||||||
"version": "1.1.4",
|
|
||||||
"resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz",
|
|
||||||
"integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==",
|
|
||||||
"dependencies": {
|
|
||||||
"es-define-property": "^1.0.0",
|
|
||||||
"es-errors": "^1.3.0",
|
|
||||||
"gopd": "^1.0.1"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 0.4"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://github.com/sponsors/ljharb"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/depd": {
|
|
||||||
"version": "2.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
|
|
||||||
"integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==",
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 0.8"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/destroy": {
|
|
||||||
"version": "1.2.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz",
|
|
||||||
"integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==",
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 0.8",
|
|
||||||
"npm": "1.2.8000 || >= 1.4.16"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/dom-serializer": {
|
|
||||||
"version": "2.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz",
|
|
||||||
"integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==",
|
|
||||||
"dependencies": {
|
|
||||||
"domelementtype": "^2.3.0",
|
|
||||||
"domhandler": "^5.0.2",
|
|
||||||
"entities": "^4.2.0"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://github.com/cheeriojs/dom-serializer?sponsor=1"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/domelementtype": {
|
|
||||||
"version": "2.3.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz",
|
|
||||||
"integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==",
|
|
||||||
"funding": [
|
|
||||||
{
|
|
||||||
"type": "github",
|
|
||||||
"url": "https://github.com/sponsors/fb55"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"node_modules/domhandler": {
|
|
||||||
"version": "5.0.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz",
|
|
||||||
"integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==",
|
|
||||||
"dependencies": {
|
|
||||||
"domelementtype": "^2.3.0"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 4"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://github.com/fb55/domhandler?sponsor=1"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/domutils": {
|
|
||||||
"version": "3.1.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/domutils/-/domutils-3.1.0.tgz",
|
|
||||||
"integrity": "sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA==",
|
|
||||||
"dependencies": {
|
|
||||||
"dom-serializer": "^2.0.0",
|
|
||||||
"domelementtype": "^2.3.0",
|
|
||||||
"domhandler": "^5.0.3"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://github.com/fb55/domutils?sponsor=1"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/ee-first": {
|
|
||||||
"version": "1.1.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
|
|
||||||
"integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="
|
|
||||||
},
|
|
||||||
"node_modules/ejs": {
|
|
||||||
"version": "3.1.9",
|
|
||||||
"resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.9.tgz",
|
|
||||||
"integrity": "sha512-rC+QVNMJWv+MtPgkt0y+0rVEIdbtxVADApW9JXrUVlzHetgcyczP/E7DJmWJ4fJCZF2cPcBk0laWO9ZHMG3DmQ==",
|
|
||||||
"dependencies": {
|
|
||||||
"jake": "^10.8.5"
|
|
||||||
},
|
|
||||||
"bin": {
|
|
||||||
"ejs": "bin/cli.js"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=0.10.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/encodeurl": {
|
|
||||||
"version": "1.0.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz",
|
|
||||||
"integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==",
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 0.8"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/entities": {
|
|
||||||
"version": "4.5.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz",
|
|
||||||
"integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=0.12"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://github.com/fb55/entities?sponsor=1"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/es-define-property": {
|
|
||||||
"version": "1.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz",
|
|
||||||
"integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==",
|
|
||||||
"dependencies": {
|
|
||||||
"get-intrinsic": "^1.2.4"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 0.4"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/es-errors": {
|
|
||||||
"version": "1.3.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
|
|
||||||
"integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 0.4"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/escape-html": {
|
|
||||||
"version": "1.0.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
|
|
||||||
"integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow=="
|
|
||||||
},
|
|
||||||
"node_modules/etag": {
|
|
||||||
"version": "1.8.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
|
|
||||||
"integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==",
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 0.6"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/express": {
|
|
||||||
"version": "4.18.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/express/-/express-4.18.3.tgz",
|
|
||||||
"integrity": "sha512-6VyCijWQ+9O7WuVMTRBTl+cjNNIzD5cY5mQ1WM8r/LEkI2u8EYpOotESNwzNlyCn3g+dmjKYI6BmNneSr/FSRw==",
|
|
||||||
"dependencies": {
|
|
||||||
"accepts": "~1.3.8",
|
|
||||||
"array-flatten": "1.1.1",
|
|
||||||
"body-parser": "1.20.2",
|
|
||||||
"content-disposition": "0.5.4",
|
|
||||||
"content-type": "~1.0.4",
|
|
||||||
"cookie": "0.5.0",
|
|
||||||
"cookie-signature": "1.0.6",
|
|
||||||
"debug": "2.6.9",
|
|
||||||
"depd": "2.0.0",
|
|
||||||
"encodeurl": "~1.0.2",
|
|
||||||
"escape-html": "~1.0.3",
|
|
||||||
"etag": "~1.8.1",
|
|
||||||
"finalhandler": "1.2.0",
|
|
||||||
"fresh": "0.5.2",
|
|
||||||
"http-errors": "2.0.0",
|
|
||||||
"merge-descriptors": "1.0.1",
|
|
||||||
"methods": "~1.1.2",
|
|
||||||
"on-finished": "2.4.1",
|
|
||||||
"parseurl": "~1.3.3",
|
|
||||||
"path-to-regexp": "0.1.7",
|
|
||||||
"proxy-addr": "~2.0.7",
|
|
||||||
"qs": "6.11.0",
|
|
||||||
"range-parser": "~1.2.1",
|
|
||||||
"safe-buffer": "5.2.1",
|
|
||||||
"send": "0.18.0",
|
|
||||||
"serve-static": "1.15.0",
|
|
||||||
"setprototypeof": "1.2.0",
|
|
||||||
"statuses": "2.0.1",
|
|
||||||
"type-is": "~1.6.18",
|
|
||||||
"utils-merge": "1.0.1",
|
|
||||||
"vary": "~1.1.2"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 0.10.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/filelist": {
|
|
||||||
"version": "1.0.4",
|
|
||||||
"resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.4.tgz",
|
|
||||||
"integrity": "sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==",
|
|
||||||
"dependencies": {
|
|
||||||
"minimatch": "^5.0.1"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/filelist/node_modules/brace-expansion": {
|
|
||||||
"version": "2.0.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
|
|
||||||
"integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
|
|
||||||
"dependencies": {
|
|
||||||
"balanced-match": "^1.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/filelist/node_modules/minimatch": {
|
|
||||||
"version": "5.1.6",
|
|
||||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz",
|
|
||||||
"integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==",
|
|
||||||
"dependencies": {
|
|
||||||
"brace-expansion": "^2.0.1"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=10"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/finalhandler": {
|
|
||||||
"version": "1.2.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz",
|
|
||||||
"integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==",
|
|
||||||
"dependencies": {
|
|
||||||
"debug": "2.6.9",
|
|
||||||
"encodeurl": "~1.0.2",
|
|
||||||
"escape-html": "~1.0.3",
|
|
||||||
"on-finished": "2.4.1",
|
|
||||||
"parseurl": "~1.3.3",
|
|
||||||
"statuses": "2.0.1",
|
|
||||||
"unpipe": "~1.0.0"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 0.8"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/forwarded": {
|
|
||||||
"version": "0.2.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
|
|
||||||
"integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==",
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 0.6"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/fresh": {
|
|
||||||
"version": "0.5.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz",
|
|
||||||
"integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==",
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 0.6"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/function-bind": {
|
|
||||||
"version": "1.1.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
|
|
||||||
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
|
|
||||||
"funding": {
|
|
||||||
"url": "https://github.com/sponsors/ljharb"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/get-intrinsic": {
|
|
||||||
"version": "1.2.4",
|
|
||||||
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz",
|
|
||||||
"integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==",
|
|
||||||
"dependencies": {
|
|
||||||
"es-errors": "^1.3.0",
|
|
||||||
"function-bind": "^1.1.2",
|
|
||||||
"has-proto": "^1.0.1",
|
|
||||||
"has-symbols": "^1.0.3",
|
|
||||||
"hasown": "^2.0.0"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 0.4"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://github.com/sponsors/ljharb"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/gopd": {
|
|
||||||
"version": "1.0.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz",
|
|
||||||
"integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==",
|
|
||||||
"dependencies": {
|
|
||||||
"get-intrinsic": "^1.1.3"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://github.com/sponsors/ljharb"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/has-flag": {
|
|
||||||
"version": "4.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
|
|
||||||
"integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=8"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/has-property-descriptors": {
|
|
||||||
"version": "1.0.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz",
|
|
||||||
"integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==",
|
|
||||||
"dependencies": {
|
|
||||||
"es-define-property": "^1.0.0"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://github.com/sponsors/ljharb"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/has-proto": {
|
|
||||||
"version": "1.0.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz",
|
|
||||||
"integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==",
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 0.4"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://github.com/sponsors/ljharb"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/has-symbols": {
|
|
||||||
"version": "1.0.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz",
|
|
||||||
"integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==",
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 0.4"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://github.com/sponsors/ljharb"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/hasown": {
|
|
||||||
"version": "2.0.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.1.tgz",
|
|
||||||
"integrity": "sha512-1/th4MHjnwncwXsIW6QMzlvYL9kG5e/CpVvLRZe4XPa8TOUNbCELqmvhDmnkNsAjwaG4+I8gJJL0JBvTTLO9qA==",
|
|
||||||
"dependencies": {
|
|
||||||
"function-bind": "^1.1.2"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 0.4"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/he": {
|
|
||||||
"version": "1.2.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz",
|
|
||||||
"integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==",
|
|
||||||
"bin": {
|
|
||||||
"he": "bin/he"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/http-errors": {
|
|
||||||
"version": "2.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz",
|
|
||||||
"integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==",
|
|
||||||
"dependencies": {
|
|
||||||
"depd": "2.0.0",
|
|
||||||
"inherits": "2.0.4",
|
|
||||||
"setprototypeof": "1.2.0",
|
|
||||||
"statuses": "2.0.1",
|
|
||||||
"toidentifier": "1.0.1"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 0.8"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/iconv-lite": {
|
|
||||||
"version": "0.4.24",
|
|
||||||
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
|
|
||||||
"integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
|
|
||||||
"dependencies": {
|
|
||||||
"safer-buffer": ">= 2.1.2 < 3"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=0.10.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/inherits": {
|
|
||||||
"version": "2.0.4",
|
|
||||||
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
|
|
||||||
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
|
|
||||||
},
|
|
||||||
"node_modules/ipaddr.js": {
|
|
||||||
"version": "1.9.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
|
|
||||||
"integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==",
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 0.10"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/jake": {
|
|
||||||
"version": "10.8.7",
|
|
||||||
"resolved": "https://registry.npmjs.org/jake/-/jake-10.8.7.tgz",
|
|
||||||
"integrity": "sha512-ZDi3aP+fG/LchyBzUM804VjddnwfSfsdeYkwt8NcbKRvo4rFkjhs456iLFn3k2ZUWvNe4i48WACDbza8fhq2+w==",
|
|
||||||
"dependencies": {
|
|
||||||
"async": "^3.2.3",
|
|
||||||
"chalk": "^4.0.2",
|
|
||||||
"filelist": "^1.0.4",
|
|
||||||
"minimatch": "^3.1.2"
|
|
||||||
},
|
|
||||||
"bin": {
|
|
||||||
"jake": "bin/cli.js"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=10"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/media-typer": {
|
|
||||||
"version": "0.3.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
|
|
||||||
"integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==",
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 0.6"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/merge-descriptors": {
|
|
||||||
"version": "1.0.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz",
|
|
||||||
"integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w=="
|
|
||||||
},
|
|
||||||
"node_modules/methods": {
|
|
||||||
"version": "1.1.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz",
|
|
||||||
"integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==",
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 0.6"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/mime": {
|
|
||||||
"version": "1.6.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz",
|
|
||||||
"integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==",
|
|
||||||
"bin": {
|
|
||||||
"mime": "cli.js"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=4"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/mime-db": {
|
|
||||||
"version": "1.52.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
|
|
||||||
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 0.6"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/mime-types": {
|
|
||||||
"version": "2.1.35",
|
|
||||||
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
|
|
||||||
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
|
|
||||||
"dependencies": {
|
|
||||||
"mime-db": "1.52.0"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 0.6"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/minimatch": {
|
|
||||||
"version": "3.1.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
|
|
||||||
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
|
|
||||||
"dependencies": {
|
|
||||||
"brace-expansion": "^1.1.7"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": "*"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/ms": {
|
|
||||||
"version": "2.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
|
|
||||||
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="
|
|
||||||
},
|
|
||||||
"node_modules/negotiator": {
|
|
||||||
"version": "0.6.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz",
|
|
||||||
"integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==",
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 0.6"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/node-html-parser": {
|
|
||||||
"version": "6.1.12",
|
|
||||||
"resolved": "https://registry.npmjs.org/node-html-parser/-/node-html-parser-6.1.12.tgz",
|
|
||||||
"integrity": "sha512-/bT/Ncmv+fbMGX96XG9g05vFt43m/+SYKIs9oAemQVYyVcZmDAI2Xq/SbNcpOA35eF0Zk2av3Ksf+Xk8Vt8abA==",
|
|
||||||
"dependencies": {
|
|
||||||
"css-select": "^5.1.0",
|
|
||||||
"he": "1.2.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/nth-check": {
|
|
||||||
"version": "2.1.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz",
|
|
||||||
"integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==",
|
|
||||||
"dependencies": {
|
|
||||||
"boolbase": "^1.0.0"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://github.com/fb55/nth-check?sponsor=1"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/object-inspect": {
|
|
||||||
"version": "1.13.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.1.tgz",
|
|
||||||
"integrity": "sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==",
|
|
||||||
"funding": {
|
|
||||||
"url": "https://github.com/sponsors/ljharb"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/on-finished": {
|
|
||||||
"version": "2.4.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
|
|
||||||
"integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==",
|
|
||||||
"dependencies": {
|
|
||||||
"ee-first": "1.1.1"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 0.8"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/parseurl": {
|
|
||||||
"version": "1.3.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
|
|
||||||
"integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==",
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 0.8"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/path-to-regexp": {
|
|
||||||
"version": "0.1.7",
|
|
||||||
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz",
|
|
||||||
"integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ=="
|
|
||||||
},
|
|
||||||
"node_modules/proxy-addr": {
|
|
||||||
"version": "2.0.7",
|
|
||||||
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
|
|
||||||
"integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==",
|
|
||||||
"dependencies": {
|
|
||||||
"forwarded": "0.2.0",
|
|
||||||
"ipaddr.js": "1.9.1"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 0.10"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/qs": {
|
|
||||||
"version": "6.11.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz",
|
|
||||||
"integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==",
|
|
||||||
"dependencies": {
|
|
||||||
"side-channel": "^1.0.4"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=0.6"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://github.com/sponsors/ljharb"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/range-parser": {
|
|
||||||
"version": "1.2.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
|
|
||||||
"integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==",
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 0.6"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/raw-body": {
|
|
||||||
"version": "2.5.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz",
|
|
||||||
"integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==",
|
|
||||||
"dependencies": {
|
|
||||||
"bytes": "3.1.2",
|
|
||||||
"http-errors": "2.0.0",
|
|
||||||
"iconv-lite": "0.4.24",
|
|
||||||
"unpipe": "1.0.0"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 0.8"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/safe-buffer": {
|
|
||||||
"version": "5.2.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
|
|
||||||
"integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
|
|
||||||
"funding": [
|
|
||||||
{
|
|
||||||
"type": "github",
|
|
||||||
"url": "https://github.com/sponsors/feross"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "patreon",
|
|
||||||
"url": "https://www.patreon.com/feross"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "consulting",
|
|
||||||
"url": "https://feross.org/support"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"node_modules/safer-buffer": {
|
|
||||||
"version": "2.1.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
|
|
||||||
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
|
|
||||||
},
|
|
||||||
"node_modules/send": {
|
|
||||||
"version": "0.18.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz",
|
|
||||||
"integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==",
|
|
||||||
"dependencies": {
|
|
||||||
"debug": "2.6.9",
|
|
||||||
"depd": "2.0.0",
|
|
||||||
"destroy": "1.2.0",
|
|
||||||
"encodeurl": "~1.0.2",
|
|
||||||
"escape-html": "~1.0.3",
|
|
||||||
"etag": "~1.8.1",
|
|
||||||
"fresh": "0.5.2",
|
|
||||||
"http-errors": "2.0.0",
|
|
||||||
"mime": "1.6.0",
|
|
||||||
"ms": "2.1.3",
|
|
||||||
"on-finished": "2.4.1",
|
|
||||||
"range-parser": "~1.2.1",
|
|
||||||
"statuses": "2.0.1"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 0.8.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/send/node_modules/ms": {
|
|
||||||
"version": "2.1.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
|
||||||
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
|
|
||||||
},
|
|
||||||
"node_modules/serve-static": {
|
|
||||||
"version": "1.15.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz",
|
|
||||||
"integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==",
|
|
||||||
"dependencies": {
|
|
||||||
"encodeurl": "~1.0.2",
|
|
||||||
"escape-html": "~1.0.3",
|
|
||||||
"parseurl": "~1.3.3",
|
|
||||||
"send": "0.18.0"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 0.8.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/set-function-length": {
|
|
||||||
"version": "1.2.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.1.tgz",
|
|
||||||
"integrity": "sha512-j4t6ccc+VsKwYHso+kElc5neZpjtq9EnRICFZtWyBsLojhmeF/ZBd/elqm22WJh/BziDe/SBiOeAt0m2mfLD0g==",
|
|
||||||
"dependencies": {
|
|
||||||
"define-data-property": "^1.1.2",
|
|
||||||
"es-errors": "^1.3.0",
|
|
||||||
"function-bind": "^1.1.2",
|
|
||||||
"get-intrinsic": "^1.2.3",
|
|
||||||
"gopd": "^1.0.1",
|
|
||||||
"has-property-descriptors": "^1.0.1"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 0.4"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/setprototypeof": {
|
|
||||||
"version": "1.2.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
|
|
||||||
"integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="
|
|
||||||
},
|
|
||||||
"node_modules/side-channel": {
|
|
||||||
"version": "1.0.6",
|
|
||||||
"resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz",
|
|
||||||
"integrity": "sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==",
|
|
||||||
"dependencies": {
|
|
||||||
"call-bind": "^1.0.7",
|
|
||||||
"es-errors": "^1.3.0",
|
|
||||||
"get-intrinsic": "^1.2.4",
|
|
||||||
"object-inspect": "^1.13.1"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 0.4"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://github.com/sponsors/ljharb"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/statuses": {
|
|
||||||
"version": "2.0.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz",
|
|
||||||
"integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==",
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 0.8"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/supports-color": {
|
|
||||||
"version": "7.2.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
|
|
||||||
"integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
|
|
||||||
"dependencies": {
|
|
||||||
"has-flag": "^4.0.0"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=8"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/toidentifier": {
|
|
||||||
"version": "1.0.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
|
|
||||||
"integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=0.6"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/type-is": {
|
|
||||||
"version": "1.6.18",
|
|
||||||
"resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
|
|
||||||
"integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==",
|
|
||||||
"dependencies": {
|
|
||||||
"media-typer": "0.3.0",
|
|
||||||
"mime-types": "~2.1.24"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 0.6"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/unpipe": {
|
|
||||||
"version": "1.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
|
|
||||||
"integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==",
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 0.8"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/utils-merge": {
|
|
||||||
"version": "1.0.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
|
|
||||||
"integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==",
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 0.4.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/vary": {
|
|
||||||
"version": "1.1.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
|
|
||||||
"integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==",
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 0.8"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
19
package.json
19
package.json
|
@ -1,19 +0,0 @@
|
||||||
{
|
|
||||||
"name": "biter",
|
|
||||||
"version": "1.0.0",
|
|
||||||
"description": "",
|
|
||||||
"main": "index.js",
|
|
||||||
"type": "module",
|
|
||||||
"scripts": {
|
|
||||||
"test": "echo \"Error: no test specified\" && exit 1"
|
|
||||||
},
|
|
||||||
"keywords": [],
|
|
||||||
"author": "",
|
|
||||||
"license": "ISC",
|
|
||||||
"dependencies": {
|
|
||||||
"ejs": "^3.1.9",
|
|
||||||
"escape-html": "^1.0.3",
|
|
||||||
"express": "^4.18.3",
|
|
||||||
"node-html-parser": "^6.1.12"
|
|
||||||
}
|
|
||||||
}
|
|
49
src/config.rs
Normal file
49
src/config.rs
Normal file
|
@ -0,0 +1,49 @@
|
||||||
|
use std::{
|
||||||
|
error::Error,
|
||||||
|
net::{IpAddr, Ipv4Addr},
|
||||||
|
path::Path,
|
||||||
|
};
|
||||||
|
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use tokio::io::AsyncReadExt;
|
||||||
|
use url::Url;
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Debug)]
|
||||||
|
#[serde(default)]
|
||||||
|
#[allow(non_snake_case)]
|
||||||
|
pub struct Config {
|
||||||
|
pub host: IpAddr,
|
||||||
|
pub port: u16,
|
||||||
|
pub FUCKING_ENDPOINT: Url,
|
||||||
|
pub TWITTER_BASE_URL: Url,
|
||||||
|
pub title: String,
|
||||||
|
pub description: String,
|
||||||
|
pub db_url: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for Config {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
host: IpAddr::V4(Ipv4Addr::UNSPECIFIED),
|
||||||
|
port: 3000,
|
||||||
|
FUCKING_ENDPOINT: Url::parse("https://publish.twitter.com/oembed").unwrap(),
|
||||||
|
TWITTER_BASE_URL: Url::parse("https://x.com/").unwrap(),
|
||||||
|
title: "biter".into(),
|
||||||
|
description: "biter twitter proxy".into(),
|
||||||
|
db_url: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn load(file: impl AsRef<Path>) -> Result<Config, Box<dyn Error>> {
|
||||||
|
let mut buf = String::new();
|
||||||
|
|
||||||
|
tokio::fs::OpenOptions::new()
|
||||||
|
.read(true)
|
||||||
|
.open(&file)
|
||||||
|
.await?
|
||||||
|
.read_to_string(&mut buf)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(toml::from_str(&buf)?)
|
||||||
|
}
|
519
src/main.rs
Normal file
519
src/main.rs
Normal file
|
@ -0,0 +1,519 @@
|
||||||
|
mod config;
|
||||||
|
#[cfg(feature = "db")]
|
||||||
|
mod query;
|
||||||
|
|
||||||
|
use crate::config::Config;
|
||||||
|
#[cfg(feature = "db")]
|
||||||
|
use crate::query::{INSERT_POST, SELECT_POST};
|
||||||
|
use html5ever::tendril::{SliceExt, TendrilSink};
|
||||||
|
use html5ever::{parse_document, QualName};
|
||||||
|
use reqwest::Client;
|
||||||
|
|
||||||
|
use std::sync::OnceLock;
|
||||||
|
use std::{net::SocketAddr, time::Duration};
|
||||||
|
|
||||||
|
#[macro_use]
|
||||||
|
extern crate html5ever;
|
||||||
|
|
||||||
|
use askama::filters::{escape, urlencode_strict};
|
||||||
|
use askama_axum::{IntoResponse, Template};
|
||||||
|
use axum::{
|
||||||
|
extract::{MatchedPath, Path, State},
|
||||||
|
http::{Request, StatusCode},
|
||||||
|
response::Response,
|
||||||
|
routing::get,
|
||||||
|
Router,
|
||||||
|
};
|
||||||
|
use html5ever::parse_fragment;
|
||||||
|
use markup5ever_rcdom::{Handle, NodeData, RcDom};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
#[cfg(feature = "db")]
|
||||||
|
use sqlx::{FromRow, PgPool};
|
||||||
|
use thiserror::Error;
|
||||||
|
use tokio::net::TcpListener;
|
||||||
|
use tower_http::trace::TraceLayer;
|
||||||
|
use tracing::{error, info, info_span, warn, Span};
|
||||||
|
use url::Url;
|
||||||
|
|
||||||
|
#[derive(Error, Debug)]
|
||||||
|
#[allow(unused)]
|
||||||
|
enum AppError {
|
||||||
|
#[error(transparent)]
|
||||||
|
ReqwestError(#[from] reqwest::Error),
|
||||||
|
#[cfg(feature = "db")]
|
||||||
|
#[error(transparent)]
|
||||||
|
SqlxError(#[from] sqlx::Error),
|
||||||
|
#[error("database not configured")]
|
||||||
|
NoDb,
|
||||||
|
#[error(transparent)]
|
||||||
|
AskamaError(#[from] askama::Error),
|
||||||
|
#[error(transparent)]
|
||||||
|
UrlParseError(#[from] url::ParseError),
|
||||||
|
#[error("error while parsing html: {0}")]
|
||||||
|
HtmlParseError(&'static str),
|
||||||
|
#[error("couldn't parse API response: {0}")]
|
||||||
|
APIParseError(&'static str),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl IntoResponse for AppError {
|
||||||
|
fn into_response(self) -> Response {
|
||||||
|
error!("{}", &self);
|
||||||
|
match self {
|
||||||
|
Self::ReqwestError(err) => {
|
||||||
|
if err.is_status() {
|
||||||
|
let status = StatusCode::from_u16(err.status().unwrap().as_u16()).unwrap();
|
||||||
|
(status, format!("Error response from twitter: {}", status)).into_response()
|
||||||
|
} else {
|
||||||
|
(
|
||||||
|
StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
|
"Failed to contact twitter API",
|
||||||
|
)
|
||||||
|
.into_response()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#[cfg(feature = "db")]
|
||||||
|
Self::SqlxError(err) => match err {
|
||||||
|
sqlx::Error::RowNotFound => {
|
||||||
|
(StatusCode::NOT_FOUND, "just fucking not found").into_response()
|
||||||
|
}
|
||||||
|
_ => (
|
||||||
|
StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
|
"There was an issue with the database",
|
||||||
|
)
|
||||||
|
.into_response(),
|
||||||
|
},
|
||||||
|
Self::NoDb => {
|
||||||
|
(StatusCode::INTERNAL_SERVER_ERROR, "Database not configured").into_response()
|
||||||
|
}
|
||||||
|
_ => StatusCode::INTERNAL_SERVER_ERROR.into_response(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type AppResult<T> = Result<T, AppError>;
|
||||||
|
|
||||||
|
struct AppState {
|
||||||
|
pub config: Config,
|
||||||
|
#[cfg(feature = "db")]
|
||||||
|
pub db: Option<PgPool>,
|
||||||
|
}
|
||||||
|
|
||||||
|
type RefAppState = &'static AppState;
|
||||||
|
|
||||||
|
#[derive(Template)]
|
||||||
|
#[template(path = "index.html")]
|
||||||
|
struct IndexTemplate<'a> {
|
||||||
|
title: &'a str,
|
||||||
|
description: &'a str,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Template)]
|
||||||
|
#[template(path = "display_tweet.html")]
|
||||||
|
struct TweetTemplate<'a> {
|
||||||
|
title: &'a str,
|
||||||
|
author: String,
|
||||||
|
url: String,
|
||||||
|
handle: String,
|
||||||
|
content: String,
|
||||||
|
media: Vec<String>,
|
||||||
|
image: Option<String>,
|
||||||
|
alt: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
struct OembedTweetResponse {
|
||||||
|
url: String,
|
||||||
|
author_name: String, // Display name
|
||||||
|
author_url: String,
|
||||||
|
html: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg_attr(feature = "db", derive(FromRow, Serialize, Clone, Debug))]
|
||||||
|
#[cfg_attr(not(feature = "db"), derive(Serialize, Clone, Debug))]
|
||||||
|
struct Post {
|
||||||
|
pub id: i64,
|
||||||
|
pub url: String,
|
||||||
|
pub handle: String,
|
||||||
|
pub name: String,
|
||||||
|
pub body: String,
|
||||||
|
pub media: Vec<String>,
|
||||||
|
pub image: Option<String>,
|
||||||
|
pub alt: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn reqwest_client() -> Client {
|
||||||
|
static CLIENT: OnceLock<Client> = OnceLock::new();
|
||||||
|
CLIENT
|
||||||
|
.get_or_init(|| {
|
||||||
|
Client::builder()
|
||||||
|
.user_agent("bot")
|
||||||
|
.build()
|
||||||
|
.expect("couldn't build reqwest client")
|
||||||
|
})
|
||||||
|
.clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_image_url_from_tweet_id_lol(
|
||||||
|
id: i64,
|
||||||
|
config: &Config,
|
||||||
|
) -> Result<Option<(String, Option<String>)>, AppError> {
|
||||||
|
info!(metric = "fetch_media", %id, "fetching media for post");
|
||||||
|
|
||||||
|
let mut tweet_url = config.TWITTER_BASE_URL.clone();
|
||||||
|
tweet_url.set_path(&format!("twitter/status/{}", id));
|
||||||
|
|
||||||
|
let body = reqwest_client().get(tweet_url).send().await?.text().await?;
|
||||||
|
|
||||||
|
let document = parse_document(RcDom::default(), Default::default())
|
||||||
|
.one(body)
|
||||||
|
.document;
|
||||||
|
|
||||||
|
let children = document.children.borrow();
|
||||||
|
|
||||||
|
let html = children
|
||||||
|
.iter()
|
||||||
|
.nth(1)
|
||||||
|
.ok_or(AppError::HtmlParseError("html not found (what)"))?
|
||||||
|
.children
|
||||||
|
.borrow();
|
||||||
|
|
||||||
|
let head = html
|
||||||
|
.iter()
|
||||||
|
.next()
|
||||||
|
.ok_or(AppError::HtmlParseError("head not found (what??)"))?
|
||||||
|
.children
|
||||||
|
.borrow();
|
||||||
|
|
||||||
|
let og_image = match head.iter().find(|x| match &x.data {
|
||||||
|
NodeData::Element { name, attrs, .. } => {
|
||||||
|
&name.local == "meta"
|
||||||
|
&& attrs
|
||||||
|
.borrow()
|
||||||
|
.iter()
|
||||||
|
.find(|y| &y.name.local == "property" && y.value == "og:image".to_tendril())
|
||||||
|
.is_some()
|
||||||
|
}
|
||||||
|
_ => false,
|
||||||
|
}) {
|
||||||
|
Some(val) => val,
|
||||||
|
None => return Ok(None),
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut url = match &og_image.data {
|
||||||
|
NodeData::Element { attrs, .. } => attrs
|
||||||
|
.borrow()
|
||||||
|
.iter()
|
||||||
|
.find(|attr| &attr.name.local == "content")
|
||||||
|
.ok_or(AppError::HtmlParseError("twitter is actually trolling now"))?
|
||||||
|
.value
|
||||||
|
.to_string(),
|
||||||
|
_ => unreachable!(),
|
||||||
|
};
|
||||||
|
|
||||||
|
if url.ends_with(":large") {
|
||||||
|
url = url.split_at(url.len() - 6).0.to_string();
|
||||||
|
}
|
||||||
|
|
||||||
|
url += "?name=4096x4096";
|
||||||
|
|
||||||
|
let image_alt = match head.iter().find(|x| match &x.data {
|
||||||
|
NodeData::Element { name, attrs, .. } => {
|
||||||
|
&name.local == "meta"
|
||||||
|
&& attrs
|
||||||
|
.borrow()
|
||||||
|
.iter()
|
||||||
|
.find(|y| &y.name.local == "property" && y.value == "og:image:alt".to_tendril())
|
||||||
|
.is_some()
|
||||||
|
}
|
||||||
|
_ => false,
|
||||||
|
}) {
|
||||||
|
Some(x) => match &x.data {
|
||||||
|
NodeData::Element { attrs, .. } => Some(
|
||||||
|
attrs
|
||||||
|
.borrow()
|
||||||
|
.iter()
|
||||||
|
.find(|y| &y.name.local == "content")
|
||||||
|
.ok_or(AppError::HtmlParseError("fuck"))?
|
||||||
|
.value
|
||||||
|
.to_string(),
|
||||||
|
),
|
||||||
|
_ => unreachable!(),
|
||||||
|
},
|
||||||
|
None => None,
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(Some((url, image_alt)))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn walk(handle: &Handle) -> Result<(String, Vec<String>), AppError> {
|
||||||
|
let mut html = String::new();
|
||||||
|
let mut media = Vec::new();
|
||||||
|
|
||||||
|
for child in handle.children.borrow().iter() {
|
||||||
|
match &child.data {
|
||||||
|
NodeData::Text { contents } => {
|
||||||
|
html += &escape(askama::Html, contents.borrow())?.to_string()
|
||||||
|
}
|
||||||
|
NodeData::Element { name, attrs, .. } => {
|
||||||
|
if "a" == &name.local {
|
||||||
|
let children = child.children.borrow();
|
||||||
|
if let Some(handle) = children.iter().next() {
|
||||||
|
match &handle.data {
|
||||||
|
NodeData::Text { contents } => {
|
||||||
|
let contents = contents.borrow();
|
||||||
|
if contents.starts_with("pic.twitter.com") {
|
||||||
|
media.push(
|
||||||
|
Url::parse(&format!("https://{}", contents))?.to_string(),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
html += &format!(
|
||||||
|
"<a href={:?}>{}</a>",
|
||||||
|
attrs
|
||||||
|
.borrow()
|
||||||
|
.iter()
|
||||||
|
.find(|x| &x.name.local == "href")
|
||||||
|
.ok_or(AppError::HtmlParseError("Kill yourself"))?
|
||||||
|
.value
|
||||||
|
.to_string(),
|
||||||
|
contents
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
return Err(AppError::HtmlParseError("no"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return Err(AppError::HtmlParseError("AAAA"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
return Err(AppError::HtmlParseError("the fuck"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok((html, media))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_html(html: &str) -> Result<(String, Vec<String>), AppError> {
|
||||||
|
let handle = parse_fragment(
|
||||||
|
RcDom::default(),
|
||||||
|
Default::default(),
|
||||||
|
QualName::new(None, ns!(html), local_name!("body")),
|
||||||
|
vec![],
|
||||||
|
)
|
||||||
|
.one(html)
|
||||||
|
.document;
|
||||||
|
|
||||||
|
let root = handle.children.borrow();
|
||||||
|
|
||||||
|
let elem_html = root.iter().next().unwrap().children.borrow();
|
||||||
|
|
||||||
|
let elem_blockquote = elem_html
|
||||||
|
.iter()
|
||||||
|
.next()
|
||||||
|
.ok_or(AppError::HtmlParseError("couldn't get blockquote"))?
|
||||||
|
.children
|
||||||
|
.borrow();
|
||||||
|
|
||||||
|
let elem_p = elem_blockquote
|
||||||
|
.iter()
|
||||||
|
.next()
|
||||||
|
.ok_or(AppError::HtmlParseError("couldn't get paragraph"))?;
|
||||||
|
|
||||||
|
walk(&elem_p)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn fetch_post(id: i64, config: &Config) -> Result<Post, AppError> {
|
||||||
|
info!(metric = "fetch", %id, "fetching post");
|
||||||
|
|
||||||
|
let mut url = config.FUCKING_ENDPOINT.clone();
|
||||||
|
let mut tweet_url = config.TWITTER_BASE_URL.clone();
|
||||||
|
tweet_url.set_path(&format!("twitter/status/{}", id));
|
||||||
|
|
||||||
|
url.set_query(Some(&format!(
|
||||||
|
"url={}&omit_script=1&lang=en",
|
||||||
|
urlencode_strict(tweet_url)?
|
||||||
|
)));
|
||||||
|
let res: OembedTweetResponse = reqwest_client().get(url).send().await?.json().await?;
|
||||||
|
|
||||||
|
let author_url = Url::parse(&res.author_url)?;
|
||||||
|
let handle = author_url
|
||||||
|
.path_segments()
|
||||||
|
.and_then(|x| x.last())
|
||||||
|
.ok_or(AppError::APIParseError("couldn't parse author_url"))?;
|
||||||
|
|
||||||
|
let (body, media) = parse_html(&res.html)?;
|
||||||
|
|
||||||
|
let mut image = None;
|
||||||
|
let mut alt = None;
|
||||||
|
|
||||||
|
if media.len() > 0 {
|
||||||
|
if let Some((shit, fuck)) = get_image_url_from_tweet_id_lol(id, config).await? {
|
||||||
|
image = Some(shit);
|
||||||
|
alt = fuck;
|
||||||
|
} else {
|
||||||
|
warn!("couldn't fetch media");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(Post {
|
||||||
|
id,
|
||||||
|
url: res.url,
|
||||||
|
handle: handle.to_owned(),
|
||||||
|
name: res.author_name,
|
||||||
|
body,
|
||||||
|
media,
|
||||||
|
image,
|
||||||
|
alt,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn tweet<'a>(
|
||||||
|
#[cfg(feature = "db")] State(AppState { config, db }): State<RefAppState>,
|
||||||
|
#[cfg(not(feature = "db"))] State(AppState { config }): State<RefAppState>,
|
||||||
|
Path((handle, id)): Path<(String, i64)>,
|
||||||
|
) -> AppResult<TweetTemplate<'a>> {
|
||||||
|
#[cfg(feature = "db")]
|
||||||
|
let post = match match db.as_ref() {
|
||||||
|
Some(conn) => match sqlx::query_as::<_, Post>(SELECT_POST)
|
||||||
|
.bind(id)
|
||||||
|
.fetch_one(conn)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(post) => {
|
||||||
|
info!(metric = "post_retrieve", from = "db", %id, "retrieved post from db");
|
||||||
|
Some(post)
|
||||||
|
}
|
||||||
|
Err(err) => match err {
|
||||||
|
sqlx::Error::RowNotFound => None,
|
||||||
|
_ => return Err(err.into()),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
None => None,
|
||||||
|
} {
|
||||||
|
Some(post) => post,
|
||||||
|
None => {
|
||||||
|
let post = fetch_post(id, config).await?;
|
||||||
|
info!(metric = "post_retrieve", from = "twitter", %id, "retrieved post from twitter");
|
||||||
|
|
||||||
|
if let Some(conn) = db.as_ref() {
|
||||||
|
let post = post.clone();
|
||||||
|
|
||||||
|
sqlx::query(INSERT_POST)
|
||||||
|
.bind(post.id)
|
||||||
|
.bind(post.url)
|
||||||
|
.bind(post.handle)
|
||||||
|
.bind(post.name)
|
||||||
|
.bind(post.body)
|
||||||
|
.bind(post.media)
|
||||||
|
.bind(post.image)
|
||||||
|
.bind(post.alt)
|
||||||
|
.execute(conn)
|
||||||
|
.await?;
|
||||||
|
info!(metric = "post_add", %id, "added post into db");
|
||||||
|
}
|
||||||
|
|
||||||
|
post
|
||||||
|
}
|
||||||
|
};
|
||||||
|
#[cfg(not(feature = "db"))]
|
||||||
|
let post = fetch_post(id, config).await?;
|
||||||
|
#[cfg(not(feature = "db"))]
|
||||||
|
info!(metric = "post_retrieve", from = "twitter", %id, "retrieved post from twitter");
|
||||||
|
|
||||||
|
if handle != "twitter" && handle != post.handle {
|
||||||
|
info!(metric = "dickhead", %handle, "dickhead found");
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(TweetTemplate {
|
||||||
|
title: &config.title,
|
||||||
|
author: post.name,
|
||||||
|
content: post.body,
|
||||||
|
media: post.media,
|
||||||
|
image: post.image,
|
||||||
|
alt: post.alt,
|
||||||
|
url: post.url,
|
||||||
|
handle: post.handle,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn index(State(AppState { config, .. }): State<RefAppState>) -> impl IntoResponse {
|
||||||
|
IndexTemplate {
|
||||||
|
title: &config.title,
|
||||||
|
description: &config.description,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() {
|
||||||
|
tracing_subscriber::fmt().init();
|
||||||
|
|
||||||
|
let config = config::load("config.toml").await.unwrap_or_default();
|
||||||
|
|
||||||
|
reqwest_client();
|
||||||
|
|
||||||
|
#[cfg(feature = "db")]
|
||||||
|
let db = match config.db_url.as_ref() {
|
||||||
|
Some(url) => {
|
||||||
|
let conn = PgPool::connect(url)
|
||||||
|
.await
|
||||||
|
.unwrap_or_else(|err| panic!("couldn't connect to database: {}", err));
|
||||||
|
sqlx::migrate!()
|
||||||
|
.run(&conn)
|
||||||
|
.await
|
||||||
|
.unwrap_or_else(|err| panic!("couldn't run migrations: {}", err));
|
||||||
|
Some(conn)
|
||||||
|
}
|
||||||
|
None => None,
|
||||||
|
};
|
||||||
|
|
||||||
|
let state: RefAppState = Box::leak(Box::new(AppState {
|
||||||
|
config,
|
||||||
|
#[cfg(feature = "db")]
|
||||||
|
db,
|
||||||
|
}));
|
||||||
|
|
||||||
|
let app = Router::new()
|
||||||
|
.nest_service("/static", tower_http::services::ServeDir::new("static"))
|
||||||
|
.route("/", get(index))
|
||||||
|
.route("/:user/status/:id", get(tweet))
|
||||||
|
.layer(
|
||||||
|
TraceLayer::new_for_http()
|
||||||
|
.make_span_with(|request: &Request<_>| {
|
||||||
|
let matched_path = request
|
||||||
|
.extensions()
|
||||||
|
.get::<MatchedPath>()
|
||||||
|
.map(MatchedPath::as_str);
|
||||||
|
|
||||||
|
info_span!(
|
||||||
|
"request",
|
||||||
|
method = ?request.method(),
|
||||||
|
path = ?request.uri().path(),
|
||||||
|
matched_path,
|
||||||
|
some_other_field = tracing::field::Empty,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.on_response(|response: &Response<_>, duration: Duration, span: &Span| {
|
||||||
|
let _ = span.enter();
|
||||||
|
let status = response.status();
|
||||||
|
info!(?status, ?duration);
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.with_state(state);
|
||||||
|
|
||||||
|
let listener = TcpListener::bind((state.config.host, state.config.port))
|
||||||
|
.await
|
||||||
|
.expect("couldn't listen");
|
||||||
|
let local_addr = listener.local_addr().expect("couldn't get socket address");
|
||||||
|
info!("listening on http://{}", local_addr);
|
||||||
|
|
||||||
|
axum::serve(
|
||||||
|
listener,
|
||||||
|
app.into_make_service_with_connect_info::<SocketAddr>(),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
}
|
3
src/query.rs
Normal file
3
src/query.rs
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
pub const SELECT_POST: &str = "SELECT * FROM posts WHERE id = $1";
|
||||||
|
pub const INSERT_POST: &str =
|
||||||
|
"INSERT INTO posts (id, url, handle, name, body, media, image, alt) VALUES ($1, $2, $3, $4, $5, $6, $7, $8)";
|
|
@ -1,5 +1,3 @@
|
||||||
@import url("catppuccin.css");
|
|
||||||
|
|
||||||
/* colors */
|
/* colors */
|
||||||
:root {
|
:root {
|
||||||
--base: #1e1e2e;
|
--base: #1e1e2e;
|
||||||
|
@ -31,7 +29,6 @@
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
font-family: sans-serif;
|
font-family: sans-serif;
|
||||||
scrollbar-width: none;
|
|
||||||
|
|
||||||
background-color: var(--base);
|
background-color: var(--base);
|
||||||
color: var(--text);
|
color: var(--text);
|
||||||
|
@ -52,29 +49,3 @@ a:active {
|
||||||
a:visited {
|
a:visited {
|
||||||
color: var(--mauve);
|
color: var(--mauve);
|
||||||
}
|
}
|
||||||
|
|
||||||
footer {
|
|
||||||
text-align: end;
|
|
||||||
font-size: small;
|
|
||||||
opacity: 50%;
|
|
||||||
}
|
|
||||||
|
|
||||||
summary:hover,
|
|
||||||
summary:active,
|
|
||||||
details[open] > summary {
|
|
||||||
font-weight: bolder;
|
|
||||||
}
|
|
||||||
|
|
||||||
summary::after {
|
|
||||||
content: " (click to expand)";
|
|
||||||
opacity: 50%;
|
|
||||||
font-size: smaller;
|
|
||||||
font-weight: lighter;
|
|
||||||
}
|
|
||||||
|
|
||||||
details[open] > summary::after {
|
|
||||||
content: " (click to collapse)";
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (min-width: 956px) {
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,17 +1,15 @@
|
||||||
.tweet {
|
.tweet {
|
||||||
display: block;
|
display: block;
|
||||||
width: max-content;
|
width: fit-content;
|
||||||
padding: 1em;
|
padding: 1em;
|
||||||
border: 2px solid var(--text);
|
border: 2px solid var(--text);
|
||||||
font-size: x-large;
|
font-size: x-large;
|
||||||
|
|
||||||
& > * {
|
& > .content > img {
|
||||||
margin-bottom: 0.5em;
|
display: block;
|
||||||
}
|
}
|
||||||
|
|
||||||
& > .author {
|
& > .author {
|
||||||
display: block;
|
|
||||||
|
|
||||||
& > .name {
|
& > .name {
|
||||||
font-size: larger;
|
font-size: larger;
|
||||||
}
|
}
|
||||||
|
@ -21,12 +19,7 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
& > .content {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
& > .original-link {
|
& > .original-link {
|
||||||
display: block;
|
|
||||||
font-size: smaller;
|
font-size: smaller;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
27
templates/display_tweet.html
Normal file
27
templates/display_tweet.html
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<meta
|
||||||
|
name="description"
|
||||||
|
content="Tweet by {{ author }} - {{ title }}"
|
||||||
|
/>
|
||||||
|
<meta
|
||||||
|
property="og:title"
|
||||||
|
content="Tweet by {{ author }} - {{ title }}"
|
||||||
|
/>
|
||||||
|
<meta property="og:description" content="{{ content }}" />
|
||||||
|
{% match image %} {% when Some with (link) %}
|
||||||
|
<meta property="og:image" content="{{ link }}" />
|
||||||
|
{% match alt %} {% when Some with (text) %}
|
||||||
|
<meta property="og:image:alt" content="{{ text }}" />
|
||||||
|
{% when None %} {% endmatch %} {% when None %} {% endmatch %}
|
||||||
|
<title>Tweet by {{ author }} - {{ title }}</title>
|
||||||
|
<link rel="stylesheet" href="/static/style.css" />
|
||||||
|
<link rel="stylesheet" href="/static/tweet.css" />
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
{% include "tweet.html" %}
|
||||||
|
</body>
|
||||||
|
</html>
|
18
templates/index.html
Normal file
18
templates/index.html
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<meta name="description" content="{{ title }}" />
|
||||||
|
<meta property="og:title" content="{{ title }}" />
|
||||||
|
<meta property="og:description" content="{{ description }}" />
|
||||||
|
<title>{{ title }}</title>
|
||||||
|
<link rel="stylesheet" href="/static/style.css" />
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>{{ title }}</h1>
|
||||||
|
<h3>{{ description }}</h3>
|
||||||
|
|
||||||
|
<a href="/POTUS/status/1532057523347689472">try it!</a>
|
||||||
|
</body>
|
||||||
|
</html>
|
23
templates/tweet.html
Normal file
23
templates/tweet.html
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
<div class="tweet">
|
||||||
|
<div class="author">
|
||||||
|
<span class="name">{{ author }}</span>
|
||||||
|
<a class="handle" href="https://twitter.com/{{ handle }}"
|
||||||
|
>@{{ handle }}</a
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<div class="content">
|
||||||
|
<p>{{ content|safe }}</p>
|
||||||
|
{% match image %} {% when Some with (link) %}
|
||||||
|
<img
|
||||||
|
src="{{ link }}"
|
||||||
|
alt="{% match alt %} {% when Some with (text) %} {{ text }} {% when None %}{% endmatch%}"
|
||||||
|
referrerpolicy="no-referrer"
|
||||||
|
crossorigin="anonymous"
|
||||||
|
/>
|
||||||
|
{% when None %} {% endmatch %} {% for embed in media %}
|
||||||
|
<br />
|
||||||
|
<a href="{{ embed }}">embedded media {{ loop.index }}: {{ embed }}</a>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
<a class="original-link" href="{{ url }}">View original</a>
|
||||||
|
</div>
|
|
@ -1,16 +0,0 @@
|
||||||
<!DOCTYPE html>
|
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
<%-
|
|
||||||
include("partials/head.ejs", {
|
|
||||||
title: `Tweet by ${authorName} (@${authorHandle}) - ${title}`,
|
|
||||||
description: content
|
|
||||||
})
|
|
||||||
%>
|
|
||||||
<link rel="stylesheet" href="/tweet.css" />
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<h1><%= description %></h1>
|
|
||||||
<%- include("partials/tweet.ejs", { authorName, authorHandle, authorUrl, content, url }) %>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
|
@ -1,12 +0,0 @@
|
||||||
<!DOCTYPE html>
|
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
<%- include("partials/head.ejs", { title, description }) %>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<h1><%= description %></h1>
|
|
||||||
<p>blah blah blah nitter is fucked</p>
|
|
||||||
<p><a href="/elonmusk/status/1519480761749016577">try it!</a></p>
|
|
||||||
<p><a href="https://git.slonk.ing/slonk/biter">view source code (big mistake)</a></p>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
|
@ -1,7 +0,0 @@
|
||||||
<meta charset="UTF-8" />
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
||||||
<meta name="description" content="<%= title %>" />
|
|
||||||
<meta property="og:title" content="<%= title %>" />
|
|
||||||
<meta property="og:description" content="<%= description %>" />
|
|
||||||
<title><%= title %></title>
|
|
||||||
<link rel="stylesheet" href="/style.css" />
|
|
|
@ -1,10 +0,0 @@
|
||||||
<span class="tweet">
|
|
||||||
<span class="author">
|
|
||||||
<span class="name"><%= authorName %></span>
|
|
||||||
<a href="<%= authorUrl %>">
|
|
||||||
<span class="handle">@<%= authorHandle %></span>
|
|
||||||
</a>
|
|
||||||
</span>
|
|
||||||
<span class="content"><%= content %></span>
|
|
||||||
<a class="original-link" href="<%= url %>">View original</a>
|
|
||||||
</span>
|
|
Loading…
Reference in a new issue