mirror of
https://github.com/MbinOrg/mbin-website.git
synced 2025-06-29 14:48:57 +00:00
Rework servers data to allow instances with inaccessible APIs
This commit is contained in:
parent
b49ee68138
commit
a44e087eae
3 changed files with 145 additions and 91 deletions
|
@ -6,23 +6,28 @@ export default function Nav() {
|
||||||
path == location.pathname
|
path == location.pathname
|
||||||
? 'border-sky-600'
|
? 'border-sky-600'
|
||||||
: 'border-transparent hover:border-sky-600';
|
: 'border-transparent hover:border-sky-600';
|
||||||
|
|
||||||
|
const navItemClass = 'border-b-2 mx-1.5 sm:mx-6 ';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<nav class="flex px-4 py-2 items-center bg-[#29144a]">
|
<nav class="flex px-4 py-2 items-center bg-[#29144a]">
|
||||||
<a href="/">
|
<a href="/">
|
||||||
<img src="/logo.svg" class="size-8" />
|
<img src="/logo.svg" class="size-8" />
|
||||||
</a>
|
</a>
|
||||||
<ul class="container flex items-center p-3 text-gray-200">
|
<ul class="container flex items-center p-3 text-gray-200">
|
||||||
<li class={`border-b-2 ${active('/')} mx-1.5 sm:mx-6`}>
|
<li class={navItemClass + active('/')}>
|
||||||
<a href="/">Home</a>
|
<a href="/">Home</a>
|
||||||
</li>
|
</li>
|
||||||
<li class={`border-b-2 ${active('/servers')} mx-1.5 sm:mx-6`}>
|
<li class={navItemClass + active('/servers')}>
|
||||||
<a href="/servers">Servers</a>
|
<a href="/servers">Servers</a>
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
<span class="grow"></span>
|
<span class="grow"></span>
|
||||||
<li class={`border-b-2 mx-1.5 sm:mx-6`}>
|
|
||||||
|
<li class={navItemClass}>
|
||||||
<a href="https://docs.joinmbin.org/">Docs</a>
|
<a href="https://docs.joinmbin.org/">Docs</a>
|
||||||
</li>
|
</li>
|
||||||
<li class={`border-b-2 mx-1.5 sm:mx-6`}>
|
<li class={navItemClass}>
|
||||||
<a href="https://github.com/mbinOrg/mbin">GitHub</a>
|
<a href="https://github.com/mbinOrg/mbin">GitHub</a>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
|
@ -73,62 +73,88 @@ const fetchServerList = async () => {
|
||||||
const fetchServerInfo = async (domain) => {
|
const fetchServerInfo = async (domain) => {
|
||||||
console.log('START:', domain);
|
console.log('START:', domain);
|
||||||
|
|
||||||
const jsonNodeInfo = await (
|
let jsonNodeInfo;
|
||||||
await fetch(`https://${domain}/nodeinfo/2.1.json`)
|
try {
|
||||||
).json();
|
jsonNodeInfo = await (
|
||||||
|
await fetch(`https://${domain}/nodeinfo/2.1.json`)
|
||||||
|
).json();
|
||||||
|
|
||||||
if (jsonNodeInfo.software.name != 'mbin') {
|
if (jsonNodeInfo.software.name != 'mbin') {
|
||||||
throw new Error(`${domain} software does not match mbin (skipped)`);
|
throw new Error(`software check failed`);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
throw new Error(`${domain}: invalid nodeinfo response (skip)`, {
|
||||||
|
cause: error,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const jsonApiInfo = await (await fetch(`https://${domain}/api/info`)).json();
|
/** @type {import('./routes/servers').Server['api']} */
|
||||||
if (jsonApiInfo.websiteDomain != domain) {
|
let apiOutput;
|
||||||
throw new Error(`${domain} api not setup correctly (skipped)`);
|
try {
|
||||||
|
const jsonApiInfo = await (
|
||||||
|
await fetch(`https://${domain}/api/info`)
|
||||||
|
).json();
|
||||||
|
if (jsonApiInfo.websiteDomain != domain) {
|
||||||
|
throw new Error(`domain check failed`);
|
||||||
|
}
|
||||||
|
const jsonApiInstance = await (
|
||||||
|
await fetch(`https://${domain}/api/instance`)
|
||||||
|
).json();
|
||||||
|
const jsonApiDefederated = await (
|
||||||
|
await fetch(`https://${domain}/api/defederated`)
|
||||||
|
).json();
|
||||||
|
|
||||||
|
apiOutput = {
|
||||||
|
defaultLang: jsonApiInfo.websiteDefaultLang ?? 'en',
|
||||||
|
federationEnabled: jsonApiInfo.websiteFederationEnabled,
|
||||||
|
contactEmail: jsonApiInfo.websiteContactEmail,
|
||||||
|
pages: jsonApiInstance,
|
||||||
|
defederated: jsonApiDefederated.instances ?? [],
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.error(
|
||||||
|
new Error(`${domain}: invalid api response (continue)`, {
|
||||||
|
cause: error,
|
||||||
|
}),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
const jsonApiInstance = await (
|
|
||||||
await fetch(`https://${domain}/api/instance`)
|
|
||||||
).json();
|
|
||||||
const jsonApiDefederated = await (
|
|
||||||
await fetch(`https://${domain}/api/defederated`)
|
|
||||||
).json();
|
|
||||||
|
|
||||||
console.log('FINISH:', domain);
|
/** @type {import('./routes/servers').Server} */
|
||||||
|
const output = {
|
||||||
return {
|
|
||||||
domain: domain,
|
domain: domain,
|
||||||
version: jsonNodeInfo.software.version,
|
version: jsonNodeInfo.software.version,
|
||||||
name: jsonNodeInfo.metadata.nodeName,
|
name: jsonNodeInfo.metadata.nodeName,
|
||||||
description: jsonNodeInfo.metadata.nodeDescription,
|
description: jsonNodeInfo.metadata.nodeDescription,
|
||||||
openRegistrations: jsonNodeInfo.openRegistrations,
|
openRegistrations: jsonNodeInfo.openRegistrations,
|
||||||
federationEnabled: jsonApiInfo.websiteFederationEnabled,
|
|
||||||
language: jsonApiInfo.websiteDefaultLang ?? 'en',
|
|
||||||
contactEmail: jsonApiInfo.websiteContactEmail,
|
|
||||||
totalUsers: jsonNodeInfo.usage.users.total,
|
totalUsers: jsonNodeInfo.usage.users.total,
|
||||||
activeHalfyearUsers: jsonNodeInfo.usage.users.activeHalfyear,
|
activeHalfyearUsers: jsonNodeInfo.usage.users.activeHalfyear,
|
||||||
activeMonthUsers: jsonNodeInfo.usage.users.activeMonth,
|
activeMonthUsers: jsonNodeInfo.usage.users.activeMonth,
|
||||||
localPosts: jsonNodeInfo.usage.localPosts,
|
localPosts: jsonNodeInfo.usage.localPosts,
|
||||||
localComments: jsonNodeInfo.usage.localComments,
|
localComments: jsonNodeInfo.usage.localComments,
|
||||||
pages: jsonApiInstance,
|
api: apiOutput,
|
||||||
defederated: jsonApiDefederated.instances ?? [],
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
console.log('FINISH:', domain);
|
||||||
|
|
||||||
|
return output;
|
||||||
};
|
};
|
||||||
|
|
||||||
const initServerData = async () => {
|
const initServerData = async () => {
|
||||||
const servers = await fetchServerList();
|
const serverDomains = await fetchServerList();
|
||||||
|
|
||||||
const serversJson = (await Promise.allSettled(servers.map(fetchServerInfo)))
|
const serversJson = (
|
||||||
|
await Promise.allSettled(serverDomains.map(fetchServerInfo))
|
||||||
|
)
|
||||||
.filter((v) => {
|
.filter((v) => {
|
||||||
const isOk = v.status == 'fulfilled';
|
const isOk = v.status == 'fulfilled';
|
||||||
|
|
||||||
if (!isOk) {
|
if (!isOk) console.error(v.reason);
|
||||||
console.error(v.reason);
|
|
||||||
}
|
|
||||||
|
|
||||||
return isOk;
|
return isOk;
|
||||||
})
|
})
|
||||||
.map((v) => v.value);
|
.map((v) => v.value);
|
||||||
|
|
||||||
console.log('Successful Mbin servers found:', serversJson.length);
|
console.log('Mbin servers found:', serversJson.length);
|
||||||
|
|
||||||
fs.writeFile(
|
fs.writeFile(
|
||||||
'./.output/data/servers.json',
|
'./.output/data/servers.json',
|
||||||
|
|
|
@ -41,25 +41,27 @@ export interface Server {
|
||||||
name: string;
|
name: string;
|
||||||
description: string;
|
description: string;
|
||||||
openRegistrations: boolean;
|
openRegistrations: boolean;
|
||||||
federationEnabled: boolean;
|
|
||||||
language: string;
|
|
||||||
contactEmail: string;
|
|
||||||
totalUsers: number;
|
totalUsers: number;
|
||||||
activeHalfyearUsers: number;
|
activeHalfyearUsers: number;
|
||||||
activeMonthUsers: number;
|
activeMonthUsers: number;
|
||||||
localPosts: number;
|
localPosts: number;
|
||||||
localComments: number;
|
localComments: number;
|
||||||
pages: {
|
api?: {
|
||||||
about?: string;
|
contactEmail: string;
|
||||||
contact?: string;
|
federationEnabled: boolean;
|
||||||
faq?: string;
|
defaultLang: string;
|
||||||
privacyPolicy?: string;
|
pages: {
|
||||||
terms?: string;
|
about?: string;
|
||||||
|
contact?: string;
|
||||||
|
faq?: string;
|
||||||
|
privacyPolicy?: string;
|
||||||
|
terms?: string;
|
||||||
|
};
|
||||||
|
defederated: string[];
|
||||||
};
|
};
|
||||||
defederated: string[];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const pageNames: Required<Server['pages']> = {
|
const pageNames: Required<NonNullable<Server['api']>['pages']> = {
|
||||||
about: 'About',
|
about: 'About',
|
||||||
contact: 'Contact',
|
contact: 'Contact',
|
||||||
faq: 'Frequently Asked Questions',
|
faq: 'Frequently Asked Questions',
|
||||||
|
@ -88,7 +90,7 @@ export default function ServersPage() {
|
||||||
.filter(
|
.filter(
|
||||||
(server) =>
|
(server) =>
|
||||||
(!filterRegistration() || server.openRegistrations) &&
|
(!filterRegistration() || server.openRegistrations) &&
|
||||||
(!langFilter() || server.language == langFilter()),
|
(!langFilter() || server.api?.defaultLang == langFilter()),
|
||||||
)
|
)
|
||||||
.sort((a, b) => {
|
.sort((a, b) => {
|
||||||
switch (sort()) {
|
switch (sort()) {
|
||||||
|
@ -123,7 +125,7 @@ export default function ServersPage() {
|
||||||
<Select
|
<Select
|
||||||
value={langFilter()}
|
value={langFilter()}
|
||||||
onChange={setLangFilter}
|
onChange={setLangFilter}
|
||||||
options={[...new Set(servers.map((v) => v.language))]}
|
options={[...new Set(servers.map((v) => v.api?.defaultLang))]}
|
||||||
placeholder="All Languages"
|
placeholder="All Languages"
|
||||||
itemComponent={(props) => (
|
itemComponent={(props) => (
|
||||||
<SelectItem item={props.item}>
|
<SelectItem item={props.item}>
|
||||||
|
@ -131,7 +133,7 @@ export default function ServersPage() {
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<SelectTrigger aria-label="Fruit" class="w-[180px]">
|
<SelectTrigger aria-label="Language" class="w-[180px]">
|
||||||
<SelectValue<string>>
|
<SelectValue<string>>
|
||||||
{(state) => languageNames.of(state.selectedOption())}
|
{(state) => languageNames.of(state.selectedOption())}
|
||||||
</SelectValue>
|
</SelectValue>
|
||||||
|
@ -151,7 +153,7 @@ export default function ServersPage() {
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<SelectTrigger aria-label="Fruit" class="w-[180px]">
|
<SelectTrigger aria-label="Fruit" class="w-[180px]">
|
||||||
<SelectValue<string>>
|
<SelectValue<keyof typeof sortNameMap>>
|
||||||
{(state) => sortNameMap[state.selectedOption()]}
|
{(state) => sortNameMap[state.selectedOption()]}
|
||||||
</SelectValue>
|
</SelectValue>
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
|
@ -178,9 +180,11 @@ export default function ServersPage() {
|
||||||
const StatChips = () => (
|
const StatChips = () => (
|
||||||
<>
|
<>
|
||||||
<Chip title="Mbin Version">Mbin {server.version}</Chip>
|
<Chip title="Mbin Version">Mbin {server.version}</Chip>
|
||||||
<Chip title="Language" icon={MaterialSymbolsLanguage}>
|
<Show when={server.api}>
|
||||||
{languageNames.of(server.language)}
|
<Chip title="Language" icon={MaterialSymbolsLanguage}>
|
||||||
</Chip>
|
{languageNames.of(server.api!.defaultLang)}
|
||||||
|
</Chip>
|
||||||
|
</Show>
|
||||||
<Chip title="Total users" icon={MaterialSymbolsPerson}>
|
<Chip title="Total users" icon={MaterialSymbolsPerson}>
|
||||||
{server.totalUsers}
|
{server.totalUsers}
|
||||||
</Chip>
|
</Chip>
|
||||||
|
@ -254,48 +258,67 @@ export default function ServersPage() {
|
||||||
</Chip>
|
</Chip>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Accordion
|
<Show
|
||||||
multiple={false}
|
when={server.api}
|
||||||
collapsible
|
fallback={
|
||||||
defaultValue={['about']}
|
<div>
|
||||||
|
No information available. This server's api is
|
||||||
|
inaccessible and it's not recommended to use.
|
||||||
|
</div>
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<For each={Object.entries(server.pages)}>
|
<Accordion
|
||||||
{([page, pageContent]) => (
|
multiple={false}
|
||||||
<Show when={pageContent || page === 'contact'}>
|
collapsible
|
||||||
<AccordionItem value={page}>
|
defaultValue={['about']}
|
||||||
<AccordionTrigger>
|
>
|
||||||
{pageNames[page as keyof Server['pages']]}
|
<For each={Object.entries(server.api!.pages)}>
|
||||||
</AccordionTrigger>
|
{([page, pageContent]) => (
|
||||||
<AccordionContent>
|
<Show when={pageContent || page === 'contact'}>
|
||||||
<Show when={page == 'contact'}>
|
<AccordionItem value={page}>
|
||||||
<Button
|
<AccordionTrigger>
|
||||||
href={`mailto:${server.contactEmail}`}
|
{
|
||||||
as="a"
|
pageNames[
|
||||||
>
|
page as keyof NonNullable<
|
||||||
Email admin: {server.contactEmail}
|
Server['api']
|
||||||
</Button>
|
>['pages']
|
||||||
</Show>
|
]
|
||||||
<Markdown>{pageContent}</Markdown>
|
}
|
||||||
</AccordionContent>
|
</AccordionTrigger>
|
||||||
</AccordionItem>
|
<AccordionContent>
|
||||||
</Show>
|
<Show when={page == 'contact'}>
|
||||||
)}
|
<Button
|
||||||
</For>
|
href={`mailto:${
|
||||||
<Show when={server.defederated.length}>
|
server.api!.contactEmail
|
||||||
<AccordionItem value="defederated">
|
}`}
|
||||||
<AccordionTrigger>
|
as="a"
|
||||||
Defederated Servers ({server.defederated.length})
|
>
|
||||||
</AccordionTrigger>
|
Email admin: {server.api!.contactEmail}
|
||||||
<AccordionContent>
|
</Button>
|
||||||
<div class="flex flex-wrap gap-1">
|
</Show>
|
||||||
<For each={server.defederated}>
|
<Markdown>{pageContent}</Markdown>
|
||||||
{(server) => <Chip>{server}</Chip>}
|
</AccordionContent>
|
||||||
</For>
|
</AccordionItem>
|
||||||
</div>
|
</Show>
|
||||||
</AccordionContent>
|
)}
|
||||||
</AccordionItem>
|
</For>
|
||||||
</Show>
|
<Show when={server.api!.defederated.length}>
|
||||||
</Accordion>
|
<AccordionItem value="defederated">
|
||||||
|
<AccordionTrigger>
|
||||||
|
Defederated Servers (
|
||||||
|
{server.api!.defederated.length})
|
||||||
|
</AccordionTrigger>
|
||||||
|
<AccordionContent>
|
||||||
|
<div class="flex flex-wrap gap-1">
|
||||||
|
<For each={server.api!.defederated}>
|
||||||
|
{(server) => <Chip>{server}</Chip>}
|
||||||
|
</For>
|
||||||
|
</div>
|
||||||
|
</AccordionContent>
|
||||||
|
</AccordionItem>
|
||||||
|
</Show>
|
||||||
|
</Accordion>
|
||||||
|
</Show>
|
||||||
|
|
||||||
<Show when={server.openRegistrations}>
|
<Show when={server.openRegistrations}>
|
||||||
<DialogFooter>
|
<DialogFooter>
|
||||||
|
|
Loading…
Add table
Reference in a new issue