no-more-proxy #3

Merged
CenTdemeern1 merged 12 commits from no-more-proxy into main 2025-02-03 00:03:51 +00:00
14 changed files with 690 additions and 92 deletions

328
Cargo.lock generated
View file

@ -1,6 +1,6 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 3
version = 4
[[package]]
name = "addr2line"
@ -185,6 +185,29 @@ version = "0.8.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
[[package]]
name = "cssparser"
version = "0.34.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b7c66d1cd8ed61bf80b38432613a7a2f09401ab8d0501110655f8b341484a3e3"
dependencies = [
"cssparser-macros",
"dtoa-short",
"itoa",
"phf",
"smallvec",
]
[[package]]
name = "cssparser-macros"
version = "0.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "13b588ba4ac1a99f7f2964d24b3d896ddc6bf847ee3855dbd4366f058cfcd331"
dependencies = [
"quote",
"syn",
]
[[package]]
name = "deranged"
version = "0.3.11"
@ -194,6 +217,17 @@ dependencies = [
"powerfmt",
]
[[package]]
name = "derive_more"
version = "0.99.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5f33878137e4dafd7fa914ad4e259e18a4e8e532b9617a2d0150262bf53abfce"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "devise"
version = "0.4.2"
@ -238,6 +272,27 @@ dependencies = [
"syn",
]
[[package]]
name = "dtoa"
version = "1.0.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dcbb2bf8e87535c23f7a8a321e364ce21462d0ff10cb6407820e8e96dfff6653"
[[package]]
name = "dtoa-short"
version = "0.3.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cd1511a7b6a56299bd043a9c167a6d2bfb37bf84a6dfceaba651168adfb43c87"
dependencies = [
"dtoa",
]
[[package]]
name = "ego-tree"
version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b2972feb8dffe7bc8c5463b1dacda1b0dfbed3710e50f977d965429692d74cd8"
[[package]]
name = "either"
version = "1.13.0"
@ -275,16 +330,32 @@ version = "2.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be"
[[package]]
name = "favicon-scraper"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b9e2d1e19588bba8f650edac55fc88e1f2db227d18920d9e71322b46530f04a2"
dependencies = [
"futures",
"imagesize",
"reqwest",
"scraper",
"serde",
"url",
]
[[package]]
name = "fedirect"
version = "0.1.0"
dependencies = [
"bytes",
"favicon-scraper",
"reqwest",
"rocket",
"semver",
"serde",
"serde_json",
"tokio",
"url",
]
@ -332,6 +403,16 @@ dependencies = [
"percent-encoding",
]
[[package]]
name = "futf"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "df420e2e84819663797d1ec6544b13c5be84629e7bb00dc960d6917db2987843"
dependencies = [
"mac",
"new_debug_unreachable",
]
[[package]]
name = "futures"
version = "0.3.31"
@ -409,6 +490,15 @@ dependencies = [
"slab",
]
[[package]]
name = "fxhash"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c31b6d751ae2c7f11320402d34e41349dd1016f8d5d45e48c4312bc8625af50c"
dependencies = [
"byteorder",
]
[[package]]
name = "generator"
version = "0.7.5"
@ -422,6 +512,15 @@ dependencies = [
"windows",
]
[[package]]
name = "getopts"
version = "0.2.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "14dbbfd5c71d70241ecf9e6f13737f7b5ce823821063188d7e46c41d371eebd5"
dependencies = [
"unicode-width",
]
[[package]]
name = "getrandom"
version = "0.2.15"
@ -501,6 +600,20 @@ version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fbf6a919d6cf397374f7dfeeea91d974c7c0a7221d0d0f4f20d859d329e53fcc"
[[package]]
name = "html5ever"
version = "0.29.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2e15626aaf9c351bc696217cbe29cb9b5e86c43f8a46b5e2f5c6c5cf7cb904ce"
dependencies = [
"log",
"mac",
"markup5ever",
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "http"
version = "0.2.12"
@ -804,6 +917,12 @@ dependencies = [
"icu_properties",
]
[[package]]
name = "imagesize"
version = "0.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "edcd27d72f2f071c64249075f42e205ff93c9a4c5f6c6da53e79ed9f9832c285"
[[package]]
name = "indexmap"
version = "2.7.0"
@ -909,6 +1028,26 @@ dependencies = [
"tracing-subscriber",
]
[[package]]
name = "mac"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4"
[[package]]
name = "markup5ever"
version = "0.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "82c88c6129bd24319e62a0359cb6b958fa7e8be6e19bb1663bc396b90883aca5"
dependencies = [
"log",
"phf",
"phf_codegen",
"string_cache",
"string_cache_codegen",
"tendril",
]
[[package]]
name = "matchers"
version = "0.1.0"
@ -986,6 +1125,12 @@ dependencies = [
"tempfile",
]
[[package]]
name = "new_debug_unreachable"
version = "1.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086"
[[package]]
name = "nu-ansi-term"
version = "0.46.0"
@ -1129,6 +1274,77 @@ version = "2.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e"
[[package]]
name = "phf"
version = "0.11.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fd6780a80ae0c52cc120a26a1a42c1ae51b247a253e4e06113d23d2c2edd078"
dependencies = [
"phf_macros",
"phf_shared 0.11.3",
]
[[package]]
name = "phf_codegen"
version = "0.11.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aef8048c789fa5e851558d709946d6d79a8ff88c0440c587967f8e94bfb1216a"
dependencies = [
"phf_generator 0.11.3",
"phf_shared 0.11.3",
]
[[package]]
name = "phf_generator"
version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5d5285893bb5eb82e6aaf5d59ee909a06a16737a8970984dd7746ba9283498d6"
dependencies = [
"phf_shared 0.10.0",
"rand",
]
[[package]]
name = "phf_generator"
version = "0.11.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d"
dependencies = [
"phf_shared 0.11.3",
"rand",
]
[[package]]
name = "phf_macros"
version = "0.11.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f84ac04429c13a7ff43785d75ad27569f2951ce0ffd30a3321230db2fc727216"
dependencies = [
"phf_generator 0.11.3",
"phf_shared 0.11.3",
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "phf_shared"
version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6796ad771acdc0123d2a88dc428b5e38ef24456743ddb1744ed628f9815c096"
dependencies = [
"siphasher 0.3.11",
]
[[package]]
name = "phf_shared"
version = "0.11.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5"
dependencies = [
"siphasher 1.0.1",
]
[[package]]
name = "pin-project-lite"
version = "0.2.16"
@ -1162,6 +1378,12 @@ dependencies = [
"zerocopy",
]
[[package]]
name = "precomputed-hash"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c"
[[package]]
name = "proc-macro2"
version = "1.0.93"
@ -1530,6 +1752,21 @@ version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
[[package]]
name = "scraper"
version = "0.22.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cc3d051b884f40e309de6c149734eab57aa8cc1347992710dc80bcc1c2194c15"
dependencies = [
"cssparser",
"ego-tree",
"getopts",
"html5ever",
"precomputed-hash",
"selectors",
"tendril",
]
[[package]]
name = "security-framework"
version = "2.11.1"
@ -1553,6 +1790,25 @@ dependencies = [
"libc",
]
[[package]]
name = "selectors"
version = "0.26.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fd568a4c9bb598e291a08244a5c1f5a8a6650bee243b5b0f8dbb3d9cc1d87fe8"
dependencies = [
"bitflags",
"cssparser",
"derive_more",
"fxhash",
"log",
"new_debug_unreachable",
"phf",
"phf_codegen",
"precomputed-hash",
"servo_arc",
"smallvec",
]
[[package]]
name = "semver"
version = "1.0.24"
@ -1612,6 +1868,15 @@ dependencies = [
"serde",
]
[[package]]
name = "servo_arc"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ae65c4249478a2647db249fb43e23cec56a2c8974a427e7bd8cb5a1d0964921a"
dependencies = [
"stable_deref_trait",
]
[[package]]
name = "sharded-slab"
version = "0.1.7"
@ -1636,6 +1901,18 @@ dependencies = [
"libc",
]
[[package]]
name = "siphasher"
version = "0.3.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d"
[[package]]
name = "siphasher"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d"
[[package]]
name = "slab"
version = "0.4.9"
@ -1691,6 +1968,32 @@ dependencies = [
"loom",
]
[[package]]
name = "string_cache"
version = "0.8.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f91138e76242f575eb1d3b38b4f1362f10d3a43f47d182a5b359af488a02293b"
dependencies = [
"new_debug_unreachable",
"once_cell",
"parking_lot",
"phf_shared 0.10.0",
"precomputed-hash",
"serde",
]
[[package]]
name = "string_cache_codegen"
version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6bb30289b722be4ff74a408c3cc27edeaad656e06cb1fe8fa9231fa59c728988"
dependencies = [
"phf_generator 0.10.0",
"phf_shared 0.10.0",
"proc-macro2",
"quote",
]
[[package]]
name = "subtle"
version = "2.6.1"
@ -1763,6 +2066,17 @@ dependencies = [
"windows-sys 0.59.0",
]
[[package]]
name = "tendril"
version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d24a120c5fc464a3458240ee02c299ebcb9d67b5249c8848b09d639dca8d7bb0"
dependencies = [
"futf",
"mac",
"utf-8",
]
[[package]]
name = "thread_local"
version = "1.1.8"
@ -2039,6 +2353,12 @@ version = "1.0.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "adb9e6ca4f869e1180728b7950e35922a7fc6397f7b641499e8f3ef06e50dc83"
[[package]]
name = "unicode-width"
version = "0.1.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af"
[[package]]
name = "unicode-xid"
version = "0.2.6"
@ -2062,6 +2382,12 @@ dependencies = [
"percent-encoding",
]
[[package]]
name = "utf-8"
version = "0.7.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9"
[[package]]
name = "utf16_iter"
version = "1.0.5"

View file

@ -5,9 +5,11 @@ edition = "2021"
[dependencies]
bytes = "1.9.0"
favicon-scraper = "0.3.1"
reqwest = { version = "0.12.12", features = ["stream"] }
rocket = { version = "0.5.1", features = ["json"] }
semver = "1.0.24"
serde = { version = "1.0.217", features = ["derive"] }
serde_json = "1.0.135"
tokio = { version = "1.43.0", features = ["process"] }
url = "2.5.4"

View file

@ -2,11 +2,13 @@
Fedi links that open on your preferred instance!
## Building
## Building and Running
To compile TypeScript, the build script assumes Deno is installed.
When you have Deno and Rust installed, simply use Cargo to build the project
1. [Install Rust via rustup](https://rustup.rs/)
2. [Install Deno](https://deno.com/)
3. Use Cargo to build the project:
```sh
# For example, to build for release, you can do

2
rust-toolchain.toml Normal file
View file

@ -0,0 +1,2 @@
[toolchain]
channel = "nightly"

View file

@ -1,9 +1,14 @@
use std::net::ToSocketAddrs;
use favicon_scraper::{Icon, IconKind};
use rocket::serde::json::Json;
use serde::{Deserialize, Serialize};
use url::Url;
use crate::known_software::KNOWN_SOFTWARE_NODEINFO_NAMES;
const MINIMUM_ICON_SIZE: usize = 16;
#[derive(Serialize)]
pub struct InstanceInfo {
name: String,
@ -12,28 +17,6 @@ pub struct InstanceInfo {
icon_url: Option<String>,
}
#[derive(Deserialize)]
struct InstanceManifest {
name: Option<String>,
short_name: Option<String>,
icons: Option<Vec<InstanceIcon>>,
}
#[derive(Deserialize)]
struct InstanceIcon {
src: String,
sizes: String,
}
impl InstanceIcon {
fn get_size(&self) -> Option<usize> {
let (x, y) = self.sizes.split_once("x")?;
let x: usize = x.parse().ok()?;
let y: usize = y.parse().ok()?;
Some(x.max(y))
}
}
#[derive(Deserialize)]
struct NodeInfoDiscovery {
links: Vec<NodeInfoLink>,
@ -62,40 +45,59 @@ struct NodeInfoMetadata {
name: Option<String>,
}
async fn get_info_from_manifest(url: Url) -> Option<[Option<String>; 3]> {
// FIXME: Iceshrimp.NET doesn't have a manifest...
let response = reqwest::get(url.clone()).await.ok()?.text().await.ok()?;
let manifest: InstanceManifest = serde_json::from_str(&response).ok()?;
Some([
manifest.name,
manifest.short_name,
manifest
.icons
.as_ref()
.and_then(|icons| icons.iter().min_by_key(|icon| icon.get_size()))
.map(|icon| icon.src.to_owned()),
])
/// Scrapes icons and returns the smallest one where the smallest axis is bigger than or equal to [MINIMUM_ICON_SIZE]
async fn find_icon(host: &str) -> Option<String> {
let icons: Vec<Icon> = favicon_scraper::scrape(host)
.await
.ok()?
.into_iter()
.filter(|i| i.size.width.min(i.size.height) >= MINIMUM_ICON_SIZE)
.collect();
let priority = |kind: &IconKind| match kind {
IconKind::LinkedInHTML => 0,
IconKind::LinkedInManifest => 1,
IconKind::HardcodedURL => 2,
_ => 3,
};
let preferred_kind = icons
.iter()
.map(|i| i.kind)
.min_by(|x, y| priority(x).cmp(&priority(y)))?; // None if icons is empty
icons
.into_iter()
.filter(|i| i.kind == preferred_kind)
.min_by_key(|i| i.size)
.map(|i| i.url.into())
}
#[get("/instance_info/<secure>/<host>")]
pub async fn instance_info(secure: bool, host: &str) -> Option<Json<InstanceInfo>> {
let mut url = Url::parse(&format!(
"http{}://{host}/manifest.json",
if secure { "s" } else { "" }
))
.ok()?;
// I'm not sure if you can sneak in a path, but better safe than sorry
// I don't really care about username/password/port, those are fine
if url.path() != "/manifest.json" {
let mut url = Url::parse(if secure {
"https://temp.host/"
} else {
"http://temp.host/"
})
.unwrap();
url.set_host(Some(host)).ok()?; // Using this to catch malformed hosts
let host = url.host_str()?.to_owned(); // Shadow the original host in case things were filtered out
// Check if the host is globally routable.
// This should help filter out a bunch of invalid or potentially malicious requests
let host_with_port = format!("{host}:{}", url.port_or_known_default()?);
if !host_with_port
.to_socket_addrs()
.ok()?
.next()?
.ip()
.is_global()
{
return None;
}
let [name, short_name, icon_url] = get_info_from_manifest(url.clone())
.await
.unwrap_or_default();
let icon_url = icon_url
.and_then(|i| url.join(&i).ok())
.map(|u| u.to_string());
// FIXME: Iceshrimp.NET doesn't have a nodeinfo discovery file either.............
let icon_url = find_icon(url.as_str()).await;
url.set_path("/.well-known/nodeinfo");
let response = reqwest::get(url.clone()).await.ok()?.text().await.ok()?;
let nodeinfo_discovery: NodeInfoDiscovery = serde_json::from_str(&response).ok()?;
@ -112,11 +114,12 @@ pub async fn instance_info(secure: bool, host: &str) -> Option<Json<InstanceInfo
.and_then(|v| fork_map.get(v.build.as_str()))
.unwrap_or(software_name)
.to_owned();
Some(Json(InstanceInfo {
name: name
.or(short_name)
.or(nodeinfo.metadata.and_then(|m| m.name))
.unwrap_or(url.host_str().unwrap().to_owned()),
name: nodeinfo
.metadata
.and_then(|m| m.name)
.unwrap_or(host.to_owned()),
software,
icon_url,
}))

View file

@ -4,5 +4,9 @@ pub mod instance_info;
pub mod proxy;
pub fn get_routes() -> Vec<Route> {
routes![instance_info::instance_info, proxy::proxy]
routes![
instance_info::instance_info,
// Proxy is temporarily disabled as it's not needed
// proxy::proxy
]
}

View file

@ -1,3 +1,4 @@
#![feature(ip)]
#[macro_use]
extern crate rocket;
@ -24,6 +25,11 @@ async fn route_for_known_instance_software(
NamedFile::open("static/crossroad.html").await.ok()
}
#[get("/")]
async fn configure_page() -> Option<NamedFile> {
NamedFile::open("static/config.html").await.ok()
}
#[get("/<instance>/<route..>", rank = 2)]
fn route_for_unknown_instance_software(instance: &str, route: PathBuf) -> (ContentType, String) {
(
@ -46,7 +52,8 @@ fn rocket() -> _ {
routes![
known_software_json,
route_for_known_instance_software,
route_for_unknown_instance_software
route_for_unknown_instance_software,
configure_page
],
)
}

117
static/config.html Normal file
View file

@ -0,0 +1,117 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>FeDirect</title>
<link rel="stylesheet" href="/static/main.css">
</head>
<body>
<script type="module">
Object.assign(globalThis, await import("/static/config.mjs"));
getMainDialog().show(); // Don't show until the page is ready
</script>
<div class="flex-vcenter">
<dialog id="mainDialog" class="half-width half-height">
<header class="separator-bottom margin-large-bottom">
<div class="flex-row">
<h1>FeDirect</h1>
<p class="margin-auto-top">&ThickSpace;By Nekomata</p>
</div>
<img src="/static/nekomata_small.png" alt="Nekomata Logo" class="logo" />
</header>
<div class="flex-row align-content-center">
<div class="flex-vcenter full-height">
<center class="half-width">
<ol id="instanceList" class="align-start wfit-content"></ol>
<br>
<button onclick="showAddInstanceDialog()">Add an instance</button>
</center>
</div>
<div class="half-width align-self-start">
<div class="flex-hcenter">
<div class="flex-column buttonPanel">
<button id="save">Save</button>
<button id="reorder">Reorder</button>
<button id="reset">Destroy all data</button>
</div>
</div>
</div>
</div>
</dialog>
</div>
<dialog id="addInstance">
<h1>Add an instance</h1>
<form method="dialog" class="addInstanceForm">
<label for="instanceHost">Instance hostname or URL<br>
(for example <code>mastodon.social</code> or <code>https://kitsunes.club/</code>)<br>
</label>
<input id="instanceHost" type="text" name="instanceHost" />
<br>
<input id="autoQueryMetadata" type="checkbox" name="autoQueryMetadata" checked />
<label for="autoQueryMetadata">
<abbr title="This uses FeDirect's API to automatically try to detect instance metadata, such as the name, software, and an icon.
We do this on the backend to avoid CORS problems.
We do not track or save any requests or data.">
Automatically query metadata
</abbr>
</label>
<br><br>
<button type="submit">OK</button>
<button type="reset" class="close">Cancel</button>
</form>
</dialog>
<dialog id="instanceDetails">
<h1>Confirm instance details</h1>
<form method="dialog" class="instanceDetailsForm">
<div class="flex-row">
<div class="half-width">
<label for="instanceName">Instance name</label>
<br>
<input id="instanceName" type="text" name="instanceName" required />
<br><br>
<label for="instanceHost">Instance hostname</label>
<br>
<input id="instanceHost" type="text" name="instanceHost" required />
<br>
<input type="checkbox" name="instanceHostSecure" id="instanceHostSecure" checked />
<label for="instanceHostSecure">
<abbr title="Whether to use HTTPS (as opposed to HTTP).
Unchecking this is not recommended, and this option only exists for exceptional cases">
Secure?
</abbr>
</label>
<br><br>
<label for="instanceSoftware">Instance software</label>
<br>
<select id="instanceSoftware" type="text" name="instanceSoftware" required>
<option value="" disabled>(Please select)</option>
</select>
</div>
<div class="half-width flex-row-reverse">
<div class="full-height flex-column-reverse">
<div>
<label for="iconContainer">Instance icon</label>
<div id="iconContainer" class="square iconContainer">
<img id="instanceIcon" alt="Icon for the selected instance" class="icon" />
</div>
</div>
</div>
</div>
</div>
<br>
<label for="defaultsList">Default option for:</label><br>
<select id="defaultsList" class="full-width" multiple>
<option value="" disabled>(None, use the "Redirect always" button to set!)</option>
</select>
<button id="removeDefaults" disabled>Remove</button>
<br><br>
<button type="submit">OK</button>
<button type="reset" class="close">Cancel</button>
</form>
</dialog>
</body>
</html>

133
static/config.mts Normal file
View file

@ -0,0 +1,133 @@
import { parseHost } from "./add_an_instance.mjs";
import { initializeAddInstanceFlow } from "./add_instance_flow.mjs";
import { initializeInstanceDetailsDialog } from "./confirm_instance_details.mjs";
import { findButtonOrFail, findDialogOrFail, findOlOrFail } from "./dom.mjs";
import storageManager from "./storage_manager.mjs";
let reordering = false;
// Dragging code is a heavily modified version of https://stackoverflow.com/a/28962290
let elementBeingDragged: HTMLLIElement | undefined;
const detailsDialog = findDialogOrFail(document.body, "#instanceDetails");
const addDialog = findDialogOrFail(document.body, "#addInstance");
const instanceList = findOlOrFail(document.body, "#instanceList");
const saveButton = findButtonOrFail(document.body, "#save");
const reorderButton = findButtonOrFail(document.body, "#reorder");
saveButton.addEventListener("click", e => {
storageManager.save();
});
reorderButton.addEventListener("click", () => {
reordering = !reordering;
if (!reordering) applyReordering();
updateInstanceList();
reorderButton.innerText = reordering ? "Finish reordering" : "Reorder";
});
export const getMainDialog = () => findDialogOrFail(document.body, "#mainDialog");
const {
showInstanceDetailsDialog,
hideInstanceDetailsDialog,
populateInstanceDetailsDialog,
} = initializeInstanceDetailsDialog(detailsDialog, () => { });
export const {
showAddInstanceDialog,
hideAddInstanceDialog
} = initializeAddInstanceFlow(detailsDialog, addDialog);
updateInstanceList();
storageManager.addSaveCallback(updateInstanceList);
function updateInstanceList() {
instanceList.replaceChildren(); // Erase all child nodes
instanceList.style.listStyleType = reordering ? "\"≡ \"" : "disc";
for (let n = 0; n < storageManager.storage.instances.length; n++) {
const instance = storageManager.storage.instances[n];
const li = document.createElement("li");
li.setAttribute("x-option", n.toString());
const label = document.createElement("label");
label.htmlFor = instance.origin;
label.innerText = instance.name + " ";
label.style.cursor = "inherit";
if (instance.iconURL) {
const img = new Image();
img.src = instance.iconURL;
img.alt = `${instance.name} icon`;
img.className = "inlineIcon medium-height";
label.append(img, " ");
}
if (reordering) {
li.draggable = true;
li.addEventListener("dragstart", e => {
if (e.dataTransfer === null) return;
if (!(e.target instanceof HTMLLIElement)) return;
e.dataTransfer.effectAllowed = "move";
e.dataTransfer.setData("text/plain", "");
elementBeingDragged = e.target;
});
li.addEventListener("dragover", e => {
if (elementBeingDragged === undefined) return;
if (!(e.target instanceof HTMLElement)) return;
const listElement = e.target.closest("li");
if (listElement === null) return;
if (listElement.parentNode === null) return;
if (isBefore(elementBeingDragged, listElement))
listElement.parentNode.insertBefore(elementBeingDragged, listElement);
else
listElement.parentNode.insertBefore(elementBeingDragged, listElement.nextSibling);
e.preventDefault();
});
li.addEventListener("dragenter", e => e.preventDefault());
li.style.cursor = "grab";
} else {
const editLink = document.createElement("a");
editLink.innerText = `Edit`;
editLink.href = "#";
editLink.addEventListener("click", e => {
const host = parseHost(instance.origin)!;
populateInstanceDetailsDialog(
instance.name,
host.host,
host.secure,
instance.software,
instance.iconURL ?? null
);
showInstanceDetailsDialog();
});
const deleteLink = document.createElement("a");
deleteLink.innerText = `Delete`;
deleteLink.href = "#";
deleteLink.addEventListener("click", e => {
storageManager.storage.instances.splice(
storageManager.storage.instances.indexOf(instance)
);
updateInstanceList();
});
label.append(editLink, " ", deleteLink);
}
li.appendChild(label);
instanceList.appendChild(li);
}
}
function isBefore(el1: HTMLLIElement, el2: HTMLLIElement) {
if (el2.parentNode === el1.parentNode)
for (let cur = el1.previousSibling; cur && cur.nodeType !== 9; cur = cur.previousSibling)
if (cur === el2)
return true;
return false;
}
function applyReordering() {
const indices: number[] = [];
for (const el of instanceList.children) {
if (!(el instanceof HTMLLIElement)) continue;
const option = el.getAttribute("x-option");
if (option === null) continue;
indices.push(parseInt(option));
}
storageManager.storage.instances = indices.map(i => storageManager.storage.instances[i]);
}

View file

@ -1,7 +1,6 @@
// This file handles the "Confirm instance details" dialog
import { findButtonOrFail, findFormOrFail, findImageOrFail, findInputOrFail, findSelectOrFail } from "./dom.mjs";
import { resize } from "./image.mjs";
import knownSoftware from "./known_software.mjs";
const blankImage = "data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==";
@ -55,22 +54,16 @@ export function initializeInstanceDetailsDialog(
instanceHost.value = instanceHostValue;
instanceHostSecure.checked = instanceHostSecureValue;
instanceSoftware.value = instanceSoftwareValue;
instanceIcon.src = instanceIconValue === null ? blankImage : `/api/proxy/${encodeURIComponent(instanceIconValue)}`;
instanceIcon.src = instanceIconValue ?? blankImage;
};
form.addEventListener("submit", e => {
let image: string | null = null;
if (instanceIcon.src !== blankImage) {
try {
image = resize(instanceIcon);
} catch { }
}
callback(
instanceName.value,
instanceHost.value,
instanceHostSecure.checked,
instanceSoftware.value,
image
instanceIcon.src
);
form.reset();
});

View file

@ -6,13 +6,11 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>FeDirect</title>
<link rel="stylesheet" href="/static/main.css">
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; img-src *;">
</head>
<body>
<script type="module">
Object.assign(globalThis, await import("/static/crossroad.mjs"));
getMainDialog().show(); // Don't show until the page is ready
</script>
<script type="module" src="/static/crossroad.mjs"></script>
<div class="flex-vcenter">
<dialog id="mainDialog" class="half-width half-height">
<header class="separator-bottom margin-large-bottom">
@ -31,7 +29,7 @@
<p id="no-instance">You currently don't have any instances. You should add one!</p>
<form id="instanceSelectForm" class="align-start wfit-content"></form>
<br>
<button onclick="showAddInstanceDialog()">Add an instance</button>
<button id="showAddInstanceDialog">Add an instance</button>
</center>
</div>
<div class="half-width align-self-start">
@ -91,7 +89,7 @@ Unchecking this is not recommended, and this option only exists for exceptional
<label for="instanceSoftware">Instance software</label>
<br>
<select id="instanceSoftware" type="text" name="instanceSoftware" required>
<option value="">(Please select)</option>
<option value="" disabled>(Please select)</option>
</select>
</div>
<div class="half-width flex-row-reverse">

View file

@ -5,11 +5,16 @@ import storageManager from "./storage_manager.mjs";
const radioButtonName = "instanceSelect";
const mainDialog = findDialogOrFail(document.body, "#mainDialog");
const showAddInstanceDialogButton = findButtonOrFail(document.body, "#showAddInstanceDialog");
const detailsDialog = findDialogOrFail(document.body, "#instanceDetails");
const addDialog = findDialogOrFail(document.body, "#addInstance");
const instanceSelectForm = findFormOrFail(document.body, "#instanceSelectForm");
const redirectButton = findButtonOrFail(document.body, "#redirect");
const redirectAlwaysButton = findButtonOrFail(document.body, "#redirectAlways");
const pathText = findPreOrFail(document.body, "#path");
showAddInstanceDialogButton.addEventListener("click", e => showAddInstanceDialog());
redirectButton.addEventListener("click", e => {
// Can be assumed to not fail because the button is disabled if there are no options and the first one is selected by default
@ -23,29 +28,25 @@ redirectAlwaysButton.addEventListener("click", e => {
redirect(option);
});
export const getMainDialog = () => findDialogOrFail(document.body, "#mainDialog");
let showAddInstanceDialog = () => { };
let hideAddInstanceDialog = () => { };
export const {
showAddInstanceDialog,
hideAddInstanceDialog
} = ((): {
showAddInstanceDialog: () => void,
hideAddInstanceDialog: () => void
} => {
// Don't bother initializing if we're performing autoredirect
if (autoRedirect()) return {
showAddInstanceDialog: () => { },
hideAddInstanceDialog: () => { }
}
if (!autoRedirect()) {
createInstanceSelectOptions();
storageManager.addSaveCallback(createInstanceSelectOptions);
updateNoInstanceHint();
storageManager.addSaveCallback(updateNoInstanceHint);
findPreOrFail(document.body, "#path").innerText = getTargetPath();
pathText.innerText = getTargetPath();
return initializeAddInstanceFlow(detailsDialog, addDialog);
})();
({
showAddInstanceDialog,
hideAddInstanceDialog
} = initializeAddInstanceFlow(detailsDialog, addDialog));
mainDialog.show();
};
function updateNoInstanceHint() {
findParagraphOrFail(document.body, "#no-instance").style.display =
@ -140,5 +141,3 @@ function redirect(to: string) {
url.pathname = getTargetPath();
location.href = url.toString();
}
export { storageManager };

View file

@ -1,6 +1,13 @@
// I would've LOVED to use generics for this but unfortunately that's not possible.
// Type safety, but at what cost... >~< thanks TypeScript
export function findOlOrFail(on: Element, selector: string): HTMLOListElement {
const element = on.querySelector(selector);
if (!(element instanceof HTMLOListElement))
throw new Error(`${selector} isn't an ol`);
return element;
}
export function findPreOrFail(on: Element, selector: string): HTMLPreElement {
const element = on.querySelector(selector);
if (!(element instanceof HTMLPreElement))

View file

@ -11,6 +11,7 @@ html,
body {
background: linear-gradient(300deg, var(--red), var(--blue));
background-size: 100vw 100vh;
background-attachment: fixed;
margin: 0;
min-height: 100vh;
height: 100vh;
@ -126,6 +127,10 @@ abbr[title] {
min-height: 50%;
}
.full-width {
min-width: 100%;
}
.full-height {
min-height: 100%;
}