Styled landing, Bluesky and Fedi redirect pages.

This commit is contained in:
flash 2025-02-27 00:16:46 +00:00
parent 7e75a71bd2
commit b6f5a4e0cc
Signed by: flash
GPG key ID: 2C9C2C574D47FE3E
24 changed files with 406 additions and 132 deletions

View file

@ -1 +1 @@
20250226.1
20250226.2

View file

@ -18,10 +18,13 @@ const MszLoadingIcon = function() {
let tsLastUpdate;
let counter = 0;
let playing = false;
let delay = 50;
let playResolve;
let pauseResolve;
const update = tsCurrent => {
try {
if(tsLastUpdate !== undefined && (tsCurrent - tsLastUpdate) < 50)
if(tsLastUpdate !== undefined && (tsCurrent - tsLastUpdate) < delay)
return;
tsLastUpdate = tsCurrent;
@ -30,29 +33,94 @@ const MszLoadingIcon = function() {
++counter;
} finally {
if(playResolve)
try {
playResolve();
} finally {
playResolve = undefined;
playing = true;
}
if(pauseResolve)
try {
pauseResolve();
} finally {
pauseResolve = undefined;
playing = false;
}
if(playing)
requestAnimationFrame(update);
}
};
const play = () => {
if(playing)
return;
playing = true;
requestAnimationFrame(update);
return new Promise(resolve => {
if(playing || playResolve) {
resolve();
return;
}
playResolve = resolve;
requestAnimationFrame(update);
});
};
const pause = () => {
return new Promise(resolve => {
if(!playing || pauseResolve) {
resolve();
return;
}
pauseResolve = resolve;
});
};
const stop = async () => {
await pause();
counter = 0;
};
const restart = async () => {
await stop();
await play();
};
const reverse = () => {
blocks.reverse();
};
const setBlock = (num, state=null) => {
element.children[num].classList.toggle('msz-loading-icon-block-hidden', !state);
};
const batsu = () => {
setBlock(0, true);setBlock(1, false);setBlock(2, true);
setBlock(3, false);setBlock(4, true);setBlock(5, false);
setBlock(6, true);setBlock(7, false);setBlock(8, true);
};
const maru = () => {
setBlock(0, true);setBlock(1, true);setBlock(2, true);
setBlock(3, true);setBlock(4, false);setBlock(5, true);
setBlock(6, true);setBlock(7, true);setBlock(8, true);
};
const pause = () => { playing = false; };
const stop = () => { pause(); counter = 0; };
const restart = () => { stop(); play(); };
return {
get element() { return element; },
get playing() { return playing; },
get delay() { return delay; },
set delay(value) {
if(typeof value !== 'number')
value = parseFloat(value);
if(isNaN(value) || !isFinite(value))
return;
if(value < 0)
value = Math.abs(value);
delay = value;
},
play: play,
pause: pause,
stop: stop,
restart: restart,
play,
pause,
stop,
restart,
reverse,
batsu,
maru,
};
};

View file

@ -30,7 +30,6 @@
width: 100%;
height: 100%;
mask-image: linear-gradient(0deg, transparent 10%, var(--background-colour) 100%);
-webkit-mask-image: linear-gradient(0deg, transparent 10%, var(--background-colour) 100%);
background: var(--background-pattern);
background-color: var(--accent-colour);
background-blend-mode: multiply;

View file

@ -1,25 +0,0 @@
(async () => {
const status = document.querySelector('.js-status');
status.textContent = 'Looking up DID...';
let did = null;
try {
const response = await fetch(`${location.protocol}//${BSKY_HANDLE}/.well-known/atproto-did`);
did = await response.text();
} catch(ex) {
status.style.color = 'red';
status.textContent = `Could not find DID! ${ex}`;
return;
}
if(typeof did !== 'string' || !did.startsWith('did:')) {
status.style.color = 'red';
status.textContent = 'Look up result was not a valid DID.';
return;
}
const url = BSKY_FORMAT.replace('%s', did);
status.textContent = `Redirecting to ${url}...`;
location.replace(url);
})();

View file

@ -1,39 +0,0 @@
(async () => {
const status = document.querySelector('.js-status');
status.textContent = 'Looking up Fediverse profile...';
let url = null;
try {
const response = await fetch(`${location.protocol}//${FEDI_INSTANCE}/.well-known/webfinger?format=json&resource=acct:${FEDI_USERNAME}@${FEDI_INSTANCE}`);
const webfinger = await response.json();
if(typeof webfinger === 'object') {
if(Array.isArray(webfinger.links))
for(const link of webfinger.links)
if(typeof link === 'object' && link.rel === 'http://webfinger.net/rel/profile-page' && typeof link.href === 'string') {
url = link.href;
break;
}
if(typeof url !== 'string' && Array.isArray(webfinger.aliases))
for(const alias of webfinger.aliases)
if(typeof alias === 'string') {
url = alias;
break;
}
}
} catch(ex) {
status.style.color = 'red';
status.textContent = `Could not complete Webfinger lookup! ${ex}`;
return;
}
if(typeof url !== 'string' || (!url.startsWith('https://') && !url.startsWith('http://'))) {
status.style.color = 'red';
status.textContent = 'Could not find an acceptable profile URL.';
return;
}
status.textContent = `Redirecting to ${url}...`;
location.replace(url);
})();

View file

@ -0,0 +1,42 @@
.redir-landing {
display: flex;
flex: 1 0 auto;
padding: 10px;
width: 100%;
align-items: center;
justify-content: center;
}
.redir-landing-content {
max-width: 500px;
width: 100%;
background: #191919;
box-shadow: 0 1px 2px #0009;
display: flex;
flex-direction: column;
}
.redir-landing-body {
text-align: center;
}
.redir-landing-body p {
margin: .5em;
}
.redir-landing-footer {
font-size: .8em;
line-height: 1.4em;
text-align: center;
margin: 10px;
}
.redir-landing-logo {
display: inline-block;
vertical-align: middle;
background-color: #fff;
mask: url('/images/flashii.svg') no-repeat center;
width: 140px;
height: 140px;
margin: 10px;
font-size: 0;
}

View file

@ -0,0 +1,54 @@
:root {
--accent-colour: #8559a5;
}
body {
width: 100%;
height: 100%;
background-color: #111;
color: #fff;
font-size: 16px;
line-height: 25px;
font-family: var(--font-regular);
overflow-y: scroll;
position: static;
display: flex;
}
pre, code {
font-family: var(--font-monospace);
}
a {
color: #1e90ff;
text-decoration: none;
}
a:visited {
color: #6B4F80;
}
a:hover,
a:focus {
text-decoration: underline;
}
.redir-background {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
mask-image: linear-gradient(315deg, #000f 0, #0000 40%);
background: url('/images/clouds.png');
background-color: var(--accent-colour);
background-blend-mode: multiply;
}
.redir-foreground {
display: flex;
flex-direction: column;
flex: 1 0 auto;
margin-bottom: 10px;
}
@include landing.css;
@include social.css;

View file

@ -0,0 +1,26 @@
.redir-social {
display: flex;
flex: 1 0 auto;
padding: 10px;
width: 100%;
align-items: center;
justify-content: center;
}
.redir-social-content {
max-width: 500px;
width: 100%;
background: #191919;
box-shadow: 0 1px 2px #0009;
display: flex;
flex-direction: column;
padding: 1em;
}
.redir-social-body {
text-align: center;
}
.redir-social-body p {
margin: .5em;
line-height: 1.4em;
}

View file

@ -0,0 +1,32 @@
const MszRedirectsBsky = async () => {
const loading = new MszLoading({ element: '.js-loading', size: 2, inline: true });
const statusBig = document.querySelector('.js-status-big');
const statusSmall = document.querySelector('.js-status-small');
const setStatusSmall = body => {
$removeChildren(statusSmall);
$appendChild(statusSmall, body);
};
setStatusSmall(<>Resolving ATProto DID for @{BSKY_HANDLE}...</>);
try {
const { body } = await $xhr.get(`${location.protocol}//${BSKY_HANDLE}/.well-known/atproto-did`);
if(typeof body !== 'string' || !body.startsWith('did:'))
throw new Error('Was unable to resolve the given handle.');
const url = BSKY_FORMAT.replace('%s', body);
setStatusSmall(<>Redirecting to <a href={url} rel="noopener">{url}</a>...</>);
location.replace(url);
} catch(ex) {
await loading.icon.stop();
loading.icon.batsu();
let errorText = ex.toString();
if(errorText.startsWith('['))
errorText = 'Something went wrong.';
statusBig.textContent = 'Profile not found!';
setStatusSmall(<span style="color: red">{errorText}</span>);
}
};

View file

@ -0,0 +1,49 @@
const MszRedirectsFedi = async () => {
const loading = new MszLoading({ element: '.js-loading', size: 2, inline: true });
const statusBig = document.querySelector('.js-status-big');
const statusSmall = document.querySelector('.js-status-small');
const setStatusSmall = body => {
$removeChildren(statusSmall);
$appendChild(statusSmall, body);
};
setStatusSmall(<>Resolving Webfinger resource for @{FEDI_USERNAME}@{FEDI_INSTANCE}...</>);
try {
const { body } = await $xhr.get(`${location.protocol}//${FEDI_INSTANCE}/.well-known/webfinger?format=json&resource=acct:${FEDI_USERNAME}@${FEDI_INSTANCE}`, { type: 'json' });
let url;
if(typeof body === 'object') {
if(Array.isArray(body.links))
for(const link of body.links)
if(typeof link === 'object' && link.rel === 'http://webfinger.net/rel/profile-page' && typeof link.href === 'string') {
url = link.href;
break;
}
if(typeof url !== 'string' && Array.isArray(body.aliases))
for(const alias of body.aliases)
if(typeof alias === 'string') {
url = alias;
break;
}
}
if(typeof url !== 'string' || (!url.startsWith('https://') && !url.startsWith('http://')))
throw new Error('Unable to resolve profile URL for given handle.');
setStatusSmall(<>Redirecting to <a href={url} rel="noopener">{url}</a>...</>);
location.replace(url);
} catch(ex) {
await loading.icon.stop();
loading.icon.batsu();
let errorText = ex.toString();
if(errorText.startsWith('['))
errorText = 'Something went wrong.';
statusBig.textContent = 'Profile not found!';
setStatusSmall(<span style="color: red">{errorText}</span>);
}
};

View file

@ -0,0 +1,9 @@
#include bsky.jsx
#include fedi.jsx
(() => {
if(location.pathname === '/bsky' || location.pathname.startsWith('/bsky/'))
MszRedirectsBsky();
if(location.pathname === '/fedi' || location.pathname.startsWith('/fedi/'))
MszRedirectsFedi();
})();

View file

@ -25,14 +25,14 @@ const fs = require('fs');
{ source: 'common.js', target: '/assets', name: 'common.{hash}.js', },
{ source: 'misuzu.js', target: '/assets', name: 'misuzu.{hash}.js', },
{ source: 'oauth2.js', target: '/assets', name: 'oauth2.{hash}.js', },
{ source: 'redir-bsky.js', target: '/assets', name: 'redir-bsky.{hash}.js', },
{ source: 'redir-fedi.js', target: '/assets', name: 'redir-fedi.{hash}.js', },
{ source: 'redirects.js', target: '/assets', name: 'redirects.{hash}.js', },
],
css: [
{ source: 'errors.css', target: '/', name: 'errors.css', },
{ source: 'common.css', target: '/assets', name: 'common.{hash}.css', },
{ source: 'misuzu.css', target: '/assets', name: 'misuzu.{hash}.css', },
{ source: 'oauth2.css', target: '/assets', name: 'oauth2.{hash}.css', },
{ source: 'redirects.css', target: '/assets', name: 'redirects.{hash}.css', },
],
twig: [
{ source: 'errors/400', target: '/', name: 'error-400.html', },

View file

@ -1,21 +1,14 @@
<?php
namespace Misuzu\Redirects;
use Index\Http\{HttpRequest,HttpResponseBuilder};
use Index\Http\Routing\{HttpGet,RouteHandler,RouteHandlerCommon};
use Misuzu\Template;
class LandingRedirectsRoutes implements RouteHandler {
use RouteHandlerCommon;
#[HttpGet('/')]
public function getIndex(): string {
return <<<HTML
<!doctype html>
<meta charset=utf-8>
<title>Redirects</title>
<p>Short URL Service for <a href=/home rel=noopener>Flashii</a></p>
<p>This is a temporary landing page until backend is less tied up.</p>
<p><a href=https://flash.moe target=_blank rel=noopener>flashwave</a> 2017-2025</p>
HTML;
return Template::renderRaw('redirects.landing');
}
}

View file

@ -8,6 +8,10 @@ class RedirectsContext {
public private(set) NamedRedirectsData $named;
public private(set) IncrementalRedirectsData $incremental;
public string $bskyProfileUrlFormat {
get => $this->config->getString('bsky_profile', 'https://bsky.app/profile/%s');
}
public function __construct(
DbConnection $dbConn,
public private(set) Config $config

View file

@ -1,22 +1,16 @@
<?php
namespace Misuzu\Redirects;
use Index\Config\Config;
use Index\Http\{HttpRequest,HttpResponseBuilder};
use Index\Http\Routing\{HttpGet,RouteHandler,RouteHandlerCommon};
use Misuzu\AssetInfo;
use Misuzu\Template;
class SocialRedirectsRoutes implements RouteHandler {
use RouteHandlerCommon;
private Config $config;
public function __construct(
RedirectsContext $redirectsCtx,
private AssetInfo $assetInfo
) {
$this->config = $redirectsCtx->config->scopeTo('social');
}
private RedirectsContext $redirectsCtx
) {}
#[HttpGet('/bsky/((did:[a-z0-9]+:[A-Za-z0-9.\-_:%]+)|(([a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])))')]
public function getBlueskyRedirect(HttpResponseBuilder $response, HttpRequest $request, string $handle): int|string {
@ -41,38 +35,23 @@ class SocialRedirectsRoutes implements RouteHandler {
}
}
$format = $this->config->getString('bsky_profile', 'https://bsky.app/profile/%s');
$format = $this->redirectsCtx->bskyProfileUrlFormat;
if(is_string($did)) {
$response->redirect(sprintf($format, $did), true);
return 301;
}
$handle = rawurlencode($handle);
$script = $this->assetInfo->getAssetUrl('redir-bsky.js');
return <<<HTML
<!doctype html>
<meta charset=utf-8>
<title>Redirecting to Bluesky profile...</title>
<div class=js-status><noscript>Javascript must be enabled for Bluesky redirects to work.</noscript></div>
<script>const BSKY_FORMAT = '{$format}'; const BSKY_HANDLE = decodeURIComponent('{$handle}');</script>
<script src="{$script}"></script>
HTML;
return Template::renderRaw('redirects.bsky', [
'bsky_format' => $format,
'bsky_handle' => $handle,
]);
}
#[HttpGet('/fedi/([A-Za-z0-9._%+-]+)@([a-zA-Z0-9.-]+\.[a-zA-Z]{2,})')]
public function getFediverseRedirect(HttpResponseBuilder $response, HttpRequest $request, string $userName, string $instance): string {
$userName = rawurlencode($userName);
$instance = rawurlencode($instance);
$script = $this->assetInfo->getAssetUrl('redir-fedi.js');
return <<<HTML
<!doctype html>
<meta charset=utf-8>
<title>Redirecting to Fediverse profile...</title>
<div class=js-status><noscript>Javascript must be enabled for Fediverse redirects to work.</noscript></div>
<script>const FEDI_USERNAME = '{$userName}'; const FEDI_INSTANCE = decodeURIComponent('{$instance}');</script>
<script src="{$script}"></script>
HTML;
return Template::renderRaw('redirects.fedi', [
'fedi_username' => $userName,
'fedi_instance' => $instance,
]);
}
}

View file

@ -6,7 +6,7 @@
{% block html_head %}
<meta name="description" content="{{ error_blerb }}">
<link href="/vendor/fontawesome/css/all.min.css" type="text/css" rel="stylesheet">
<link href="/vendor/fontawesome/css/all.min.css" rel="stylesheet">
<link href="/errors.css" rel="stylesheet">
<style>
:root {

View file

@ -111,7 +111,7 @@
</form>
{% if globals.eeprom_path is not empty and globals.eeprom_app is not empty %}
<script type="text/javascript">
<script>
const peepPath = '{{ globals.eeprom_path }}', peepApp = '{{ globals.eeprom_app }}';
</script>
{% endif %}

View file

@ -52,7 +52,7 @@
<button type="button" id="-msz-manage-setting-array-enter" class="input__button input__button--save">Add</button>
</div>
</div>
<script type="text/javascript">
<script>
var mszMSF = document.getElementById('-msz-manage-setting-form'),
mszMSA = document.getElementById('-msz-manage-setting-array'),
mszMSAD = document.getElementById('-msz-manage-setting-array-delete'),

View file

@ -2,9 +2,9 @@
{% block html_head %}
{% include '_layout/meta.twig' %}
<link href="/vendor/fontawesome/css/all.min.css" type="text/css" rel="stylesheet">
<link href="{{ asset('common.css') }}" type="text/css" rel="stylesheet">
<link href="{{ asset('misuzu.css') }}" type="text/css" rel="stylesheet">
<link href="/vendor/fontawesome/css/all.min.css" rel="stylesheet">
<link href="{{ asset('common.css') }}" rel="stylesheet">
<link href="{{ asset('misuzu.css') }}" rel="stylesheet">
{% if main_css_vars is defined and main_css_vars is iterable and main_css_vars is not empty %}
<style>
:root {
@ -55,7 +55,7 @@
{% include '_layout/footer.twig' %}
{% endblock %}
<script src="/vendor/highlightjs/highlight.min.js" type="text/javascript"></script>
<script src="{{ asset('common.js') }}" type="text/javascript"></script>
<script src="{{ asset('misuzu.js') }}" type="text/javascript"></script>
<script src="/vendor/highlightjs/highlight.min.js"></script>
<script src="{{ asset('common.js') }}"></script>
<script src="{{ asset('misuzu.js') }}"></script>
{% endblock %}

View file

@ -5,7 +5,7 @@
{% endblock %}
{% if globals.eeprom_path is not empty and globals.eeprom_app_messages is not empty %}
<script type="text/javascript">
<script>
const peepPath = '{{ globals.eeprom_path }}', peepApp = '{{ globals.eeprom_app_messages }}';
</script>
{% endif %}

View file

@ -0,0 +1,19 @@
{% extends 'redirects/master.twig' %}
{% set html_title = 'Redirecting to Bluesky profile...' %}
{% block content %}
<div class="redir-social">
<div class="redir-social-content">
<div class="js-loading"></div>
<article class="redir-social-body">
<h1 class="js-status-big">Redirecting to profile...</h1>
<p class="js-status-small"><noscript>Javascript must be enabled for Bluesky redirects to work.</noscript></p>
</article>
</div>
</div>
<script>
const BSKY_FORMAT = '{{ bsky_format }}';
const BSKY_HANDLE = '{{ bsky_handle }}';
</script>
{% endblock %}

View file

@ -0,0 +1,19 @@
{% extends 'redirects/master.twig' %}
{% set html_title = 'Redirecting to Fediverse profile...' %}
{% block content %}
<div class="redir-social">
<div class="redir-social-content">
<div class="js-loading"></div>
<article class="redir-social-body">
<h1 class="js-status-big">Redirecting to profile...</h1>
<p class="js-status-small"><noscript>Javascript must be enabled for Fediverse redirects to work.</noscript></p>
</article>
</div>
</div>
<script>
const FEDI_USERNAME = '{{ fedi_username }}';
const FEDI_INSTANCE = '{{ fedi_instance }}';
</script>
{% endblock %}

View file

@ -0,0 +1,29 @@
{% extends 'redirects/master.twig' %}
{% set html_title = globals.site_info.name ~ ' Redirect Service' %}
{% block content %}
<div class="redir-landing">
<div class="redir-landing-content">
<article class="redir-landing-body">
<p><div class="redir-landing-logo"></div></p>
<h1>{{ html_title }}</h1>
<p>Short URL Service for <a href="{{ globals.site_info.url }}" rel="noopener">{{ globals.site_info.name }}</a></p>
</article>
<footer class="redir-landing-footer">
<p>
<a href="https://flash.moe" target="_blank" rel="noopener">flashwave</a>
2013-{{ ''|date('Y') }} /
{% set git_branch = git_branch() %}
{% if git_branch != 'HEAD' %}
<a href="https://patchii.net/flashii/misuzu/src/branch/{{ git_branch }}" target="_blank" rel="noopener">{{ git_branch }}</a>
{% else %}
{% set git_tag = git_tag() %}
<a href="https://patchii.net/flashii/misuzu/src/tag/{{ git_tag }}" target="_blank" rel="noopener">{{ git_tag }}</a>
{% endif %}
# <a href="https://patchii.net/flashii/misuzu/commit/{{ git_commit_hash(true) }}" target="_blank" rel="noopener">{{ git_commit_hash() }}</a>
</p>
</footer>
</div>
</div>
{% endblock %}

View file

@ -0,0 +1,16 @@
{% extends 'html.twig' %}
{% block html_head %}
<link href="/vendor/fontawesome/css/all.min.css" rel="stylesheet">
<link href="{{ asset('common.css') }}" rel="stylesheet">
<link href="{{ asset('redirects.css') }}" rel="stylesheet">
{% endblock %}
{% block html_body %}
<div class="redir-background"></div>
<div class="redir-foreground">
{% block content %}{% endblock %}
</div>
<script src="{{ asset('common.js') }}"></script>
<script src="{{ asset('redirects.js') }}"></script>
{% endblock %}