mirror of
https://github.com/TecharoHQ/anubis.git
synced 2026-04-10 10:38:45 +00:00
333 lines
8.9 KiB
TypeScript
333 lines
8.9 KiB
TypeScript
import { render } from "preact";
|
|
import { useState, useEffect, useRef } from "preact/hooks";
|
|
import algorithms from "./algorithms";
|
|
|
|
// from Xeact
|
|
const u = (url: string = "", params: Record<string, any> = {}) => {
|
|
let result = new URL(url, window.location.href);
|
|
Object.entries(params).forEach(([k, v]) => result.searchParams.set(k, v));
|
|
return result.toString();
|
|
};
|
|
|
|
const j = (id: string): any | null => {
|
|
const elem = document.getElementById(id);
|
|
if (elem === null) {
|
|
return null;
|
|
}
|
|
|
|
const text = elem.textContent;
|
|
if (text == null || text.trim() === "") {
|
|
return null;
|
|
}
|
|
|
|
return JSON.parse(text);
|
|
};
|
|
|
|
const imageURL = (
|
|
mood: string,
|
|
cacheBuster: string,
|
|
basePrefix: string,
|
|
): string =>
|
|
u(`${basePrefix}/.within.website/x/cmd/anubis/static/img/${mood}.webp`, {
|
|
cacheBuster,
|
|
});
|
|
|
|
// Use the browser language from the HTML lang attribute which is set by the server settings or request headers
|
|
const getBrowserLanguage = async () => document.documentElement.lang;
|
|
|
|
// Load translations from JSON files
|
|
const loadTranslations = async (lang: string) => {
|
|
const basePrefix = j("anubis_base_prefix");
|
|
if (basePrefix === null) {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const response = await fetch(
|
|
`${basePrefix}/.within.website/x/cmd/anubis/static/locales/${lang}.json`,
|
|
);
|
|
return await response.json();
|
|
} catch (error) {
|
|
console.warn(
|
|
`Failed to load translations for ${lang}, falling back to English`,
|
|
);
|
|
if (lang !== "en") {
|
|
return await loadTranslations("en");
|
|
}
|
|
throw error;
|
|
}
|
|
};
|
|
|
|
const getRedirectUrl = () => {
|
|
const publicUrl = j("anubis_public_url");
|
|
if (publicUrl === null) {
|
|
return;
|
|
}
|
|
if (publicUrl && window.location.href.startsWith(publicUrl)) {
|
|
const urlParams = new URLSearchParams(window.location.search);
|
|
return urlParams.get("redir");
|
|
}
|
|
return window.location.href;
|
|
};
|
|
|
|
let translations: Record<string, string> = {};
|
|
|
|
// Initialize translations
|
|
const initTranslations = async () => {
|
|
const currentLang = await getBrowserLanguage();
|
|
translations = await loadTranslations(currentLang);
|
|
};
|
|
|
|
const t = (key: string): string =>
|
|
translations[`js_${key}`] || translations[key] || key;
|
|
|
|
interface AppProps {
|
|
anubisVersion: string;
|
|
basePrefix: string;
|
|
}
|
|
|
|
function App({ anubisVersion, basePrefix }: AppProps) {
|
|
const [phase, setPhase] = useState<
|
|
"loading" | "computing" | "reading" | "error"
|
|
>("loading");
|
|
|
|
// Error info
|
|
const [errorTitle, setErrorTitle] = useState("");
|
|
const [errorMessage, setErrorMessage] = useState("");
|
|
const [errorImage, setErrorImage] = useState("");
|
|
|
|
// Computing info
|
|
const [difficulty, setDifficulty] = useState(0);
|
|
const [speed, setSpeed] = useState("0kH/s");
|
|
const [progress, setProgress] = useState(0);
|
|
const [showApology, setShowApology] = useState(false);
|
|
|
|
// Reading redirect callback
|
|
const redirectFn = useRef<(() => void) | null>(null);
|
|
const detailsRead = useRef(false);
|
|
|
|
// Main initialization
|
|
useEffect(() => {
|
|
const details = document.querySelector("details");
|
|
if (details) {
|
|
details.addEventListener("toggle", () => {
|
|
if (details.open) {
|
|
detailsRead.current = true;
|
|
}
|
|
});
|
|
}
|
|
|
|
const showError = (title: string, message: string, imageSrc: string) => {
|
|
setErrorTitle(title);
|
|
setErrorMessage(message);
|
|
setErrorImage(imageSrc);
|
|
setPhase("error");
|
|
|
|
// sync title element (it's outside of the JSX tree)
|
|
const titleEl = document.getElementById("title");
|
|
if (titleEl) {
|
|
titleEl.textContent = title;
|
|
}
|
|
};
|
|
|
|
const dependencies = [
|
|
{
|
|
name: "Web Workers",
|
|
msg: t("web_workers_error"),
|
|
value: window.Worker,
|
|
},
|
|
{
|
|
name: "Cookies",
|
|
msg: t("cookies_error"),
|
|
value: navigator.cookieEnabled,
|
|
},
|
|
];
|
|
|
|
for (const { value, name, msg } of dependencies) {
|
|
if (!value) {
|
|
showError(
|
|
`${t("missing_feature")} ${name}`,
|
|
msg,
|
|
imageURL("reject", anubisVersion, basePrefix),
|
|
);
|
|
return;
|
|
}
|
|
}
|
|
|
|
const challengeData = j("anubis_challenge");
|
|
if (!challengeData) {
|
|
showError(
|
|
t("challenge_error"),
|
|
t("challenge_data_missing"),
|
|
imageURL("reject", anubisVersion, basePrefix),
|
|
);
|
|
return;
|
|
}
|
|
const { challenge, rules } = challengeData;
|
|
|
|
const process = algorithms[rules.algorithm];
|
|
if (!process) {
|
|
showError(
|
|
t("challenge_error"),
|
|
t("challenge_error_msg"),
|
|
imageURL("reject", anubisVersion, basePrefix),
|
|
);
|
|
return;
|
|
}
|
|
|
|
setPhase("computing");
|
|
setDifficulty(rules.difficulty);
|
|
|
|
const likelihood = Math.pow(16, -rules.difficulty);
|
|
let lastSpeedUpdate = 0;
|
|
let apologyShown = false;
|
|
const t0 = Date.now();
|
|
|
|
process(
|
|
{ basePrefix, version: anubisVersion },
|
|
challenge.randomData,
|
|
rules.difficulty,
|
|
null,
|
|
(iters: number) => {
|
|
const delta = Date.now() - t0;
|
|
// only update the speed every second so it's less visually distracting
|
|
if (delta - lastSpeedUpdate > 1000) {
|
|
lastSpeedUpdate = delta;
|
|
setSpeed(`${(iters / delta).toFixed(3)}kH/s`);
|
|
}
|
|
// the probability of still being on the page is (1 - likelihood) ^ iters.
|
|
// by definition, half of the time the progress bar only gets to half, so
|
|
// apply a polynomial ease-out function to move faster in the beginning
|
|
// and then slow down as things get increasingly unlikely. quadratic felt
|
|
// the best in testing, but this may need adjustment in the future.
|
|
|
|
const probability = Math.pow(1 - likelihood, iters);
|
|
const distance = (1 - Math.pow(probability, 2)) * 100;
|
|
setProgress(distance);
|
|
|
|
if (probability < 0.1 && !apologyShown) {
|
|
apologyShown = true;
|
|
setShowApology(true);
|
|
}
|
|
},
|
|
)
|
|
.then((result: any) => {
|
|
const t1 = Date.now();
|
|
const { hash, nonce } = result;
|
|
console.log({ hash, nonce });
|
|
|
|
const doRedirect = () => {
|
|
const redir = getRedirectUrl();
|
|
window.location.replace(
|
|
u(`${basePrefix}/.within.website/x/cmd/anubis/api/pass-challenge`, {
|
|
id: challenge.id,
|
|
response: hash,
|
|
nonce,
|
|
redir,
|
|
elapsedTime: t1 - t0,
|
|
}),
|
|
);
|
|
};
|
|
|
|
if (detailsRead.current) {
|
|
redirectFn.current = doRedirect;
|
|
setPhase("reading");
|
|
setTimeout(doRedirect, 30000);
|
|
} else {
|
|
doRedirect();
|
|
}
|
|
})
|
|
.catch((err: Error) => {
|
|
showError(
|
|
t("calculation_error"),
|
|
`${t("calculation_error_msg")} ${err.message}`,
|
|
imageURL("reject", anubisVersion, basePrefix),
|
|
);
|
|
});
|
|
}, []);
|
|
|
|
const pensiveURL = imageURL("pensive", anubisVersion, basePrefix);
|
|
|
|
if (phase === "error") {
|
|
return (
|
|
<>
|
|
<img style="width:100%;max-width:256px;" src={errorImage} />
|
|
<p id="status">{errorMessage}</p>
|
|
</>
|
|
);
|
|
}
|
|
|
|
if (phase === "loading") {
|
|
return (
|
|
<>
|
|
<img style="width:100%;max-width:256px;" src={pensiveURL} />
|
|
<p id="status">{t("calculating")}</p>
|
|
</>
|
|
);
|
|
}
|
|
|
|
// computing or reading
|
|
return (
|
|
<>
|
|
<img style="width:100%;max-width:256px;" src={pensiveURL} />
|
|
<p id="status">
|
|
{`${t("calculating_difficulty")} ${difficulty}, `}
|
|
{`${t("speed")} ${speed}`}
|
|
{showApology && (
|
|
<>
|
|
<br />
|
|
{t("verification_longer")}
|
|
</>
|
|
)}
|
|
</p>
|
|
{phase === "reading" ? (
|
|
<button
|
|
id="progress"
|
|
style={{
|
|
display: "flex",
|
|
alignItems: "center",
|
|
justifyContent: "center",
|
|
height: "2rem",
|
|
borderRadius: "1rem",
|
|
cursor: "pointer",
|
|
background: "#b16286",
|
|
color: "white",
|
|
fontWeight: "bold",
|
|
outline: "4px solid #b16286",
|
|
outlineOffset: "2px",
|
|
width: "min(20rem, 90%)",
|
|
margin: "1rem auto 2rem",
|
|
border: "none",
|
|
fontSize: "inherit",
|
|
fontFamily: "inherit",
|
|
}}
|
|
onClick={() => redirectFn.current?.()}
|
|
>
|
|
{t("finished_reading")}
|
|
</button>
|
|
) : (
|
|
<div
|
|
id="progress"
|
|
role="progressbar"
|
|
aria-labelledby="status"
|
|
aria-valuenow={progress}
|
|
style={{ display: "inline-block" }}
|
|
>
|
|
<div class="bar-inner" style={{ width: `${progress}%` }}></div>
|
|
</div>
|
|
)}
|
|
</>
|
|
);
|
|
}
|
|
|
|
// Bootstrap: init translations, then mount Preact
|
|
(async () => {
|
|
await initTranslations();
|
|
const anubisVersion = j("anubis_version");
|
|
const basePrefix = j("anubis_base_prefix");
|
|
const root = document.getElementById("app");
|
|
if (root) {
|
|
render(<App anubisVersion={anubisVersion} basePrefix={basePrefix} />, root);
|
|
}
|
|
})();
|