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
|
||||
? 'border-sky-600'
|
||||
: 'border-transparent hover:border-sky-600';
|
||||
|
||||
const navItemClass = 'border-b-2 mx-1.5 sm:mx-6 ';
|
||||
|
||||
return (
|
||||
<nav class="flex px-4 py-2 items-center bg-[#29144a]">
|
||||
<a href="/">
|
||||
<img src="/logo.svg" class="size-8" />
|
||||
</a>
|
||||
<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>
|
||||
</li>
|
||||
<li class={`border-b-2 ${active('/servers')} mx-1.5 sm:mx-6`}>
|
||||
<li class={navItemClass + active('/servers')}>
|
||||
<a href="/servers">Servers</a>
|
||||
</li>
|
||||
|
||||
<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>
|
||||
</li>
|
||||
<li class={`border-b-2 mx-1.5 sm:mx-6`}>
|
||||
<li class={navItemClass}>
|
||||
<a href="https://github.com/mbinOrg/mbin">GitHub</a>
|
||||
</li>
|
||||
</ul>
|
||||
|
|
|
@ -73,17 +73,29 @@ const fetchServerList = async () => {
|
|||
const fetchServerInfo = async (domain) => {
|
||||
console.log('START:', domain);
|
||||
|
||||
const jsonNodeInfo = await (
|
||||
let jsonNodeInfo;
|
||||
try {
|
||||
jsonNodeInfo = await (
|
||||
await fetch(`https://${domain}/nodeinfo/2.1.json`)
|
||||
).json();
|
||||
|
||||
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']} */
|
||||
let apiOutput;
|
||||
try {
|
||||
const jsonApiInfo = await (
|
||||
await fetch(`https://${domain}/api/info`)
|
||||
).json();
|
||||
if (jsonApiInfo.websiteDomain != domain) {
|
||||
throw new Error(`${domain} api not setup correctly (skipped)`);
|
||||
throw new Error(`domain check failed`);
|
||||
}
|
||||
const jsonApiInstance = await (
|
||||
await fetch(`https://${domain}/api/instance`)
|
||||
|
@ -92,43 +104,57 @@ const fetchServerInfo = async (domain) => {
|
|||
await fetch(`https://${domain}/api/defederated`)
|
||||
).json();
|
||||
|
||||
console.log('FINISH:', domain);
|
||||
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,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
/** @type {import('./routes/servers').Server} */
|
||||
const output = {
|
||||
domain: domain,
|
||||
version: jsonNodeInfo.software.version,
|
||||
name: jsonNodeInfo.metadata.nodeName,
|
||||
description: jsonNodeInfo.metadata.nodeDescription,
|
||||
openRegistrations: jsonNodeInfo.openRegistrations,
|
||||
federationEnabled: jsonApiInfo.websiteFederationEnabled,
|
||||
language: jsonApiInfo.websiteDefaultLang ?? 'en',
|
||||
contactEmail: jsonApiInfo.websiteContactEmail,
|
||||
totalUsers: jsonNodeInfo.usage.users.total,
|
||||
activeHalfyearUsers: jsonNodeInfo.usage.users.activeHalfyear,
|
||||
activeMonthUsers: jsonNodeInfo.usage.users.activeMonth,
|
||||
localPosts: jsonNodeInfo.usage.localPosts,
|
||||
localComments: jsonNodeInfo.usage.localComments,
|
||||
pages: jsonApiInstance,
|
||||
defederated: jsonApiDefederated.instances ?? [],
|
||||
api: apiOutput,
|
||||
};
|
||||
|
||||
console.log('FINISH:', domain);
|
||||
|
||||
return output;
|
||||
};
|
||||
|
||||
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) => {
|
||||
const isOk = v.status == 'fulfilled';
|
||||
|
||||
if (!isOk) {
|
||||
console.error(v.reason);
|
||||
}
|
||||
if (!isOk) console.error(v.reason);
|
||||
|
||||
return isOk;
|
||||
})
|
||||
.map((v) => v.value);
|
||||
|
||||
console.log('Successful Mbin servers found:', serversJson.length);
|
||||
console.log('Mbin servers found:', serversJson.length);
|
||||
|
||||
fs.writeFile(
|
||||
'./.output/data/servers.json',
|
||||
|
|
|
@ -41,14 +41,15 @@ export interface Server {
|
|||
name: string;
|
||||
description: string;
|
||||
openRegistrations: boolean;
|
||||
federationEnabled: boolean;
|
||||
language: string;
|
||||
contactEmail: string;
|
||||
totalUsers: number;
|
||||
activeHalfyearUsers: number;
|
||||
activeMonthUsers: number;
|
||||
localPosts: number;
|
||||
localComments: number;
|
||||
api?: {
|
||||
contactEmail: string;
|
||||
federationEnabled: boolean;
|
||||
defaultLang: string;
|
||||
pages: {
|
||||
about?: string;
|
||||
contact?: string;
|
||||
|
@ -57,9 +58,10 @@ export interface Server {
|
|||
terms?: string;
|
||||
};
|
||||
defederated: string[];
|
||||
};
|
||||
}
|
||||
|
||||
const pageNames: Required<Server['pages']> = {
|
||||
const pageNames: Required<NonNullable<Server['api']>['pages']> = {
|
||||
about: 'About',
|
||||
contact: 'Contact',
|
||||
faq: 'Frequently Asked Questions',
|
||||
|
@ -88,7 +90,7 @@ export default function ServersPage() {
|
|||
.filter(
|
||||
(server) =>
|
||||
(!filterRegistration() || server.openRegistrations) &&
|
||||
(!langFilter() || server.language == langFilter()),
|
||||
(!langFilter() || server.api?.defaultLang == langFilter()),
|
||||
)
|
||||
.sort((a, b) => {
|
||||
switch (sort()) {
|
||||
|
@ -123,7 +125,7 @@ export default function ServersPage() {
|
|||
<Select
|
||||
value={langFilter()}
|
||||
onChange={setLangFilter}
|
||||
options={[...new Set(servers.map((v) => v.language))]}
|
||||
options={[...new Set(servers.map((v) => v.api?.defaultLang))]}
|
||||
placeholder="All Languages"
|
||||
itemComponent={(props) => (
|
||||
<SelectItem item={props.item}>
|
||||
|
@ -131,7 +133,7 @@ export default function ServersPage() {
|
|||
</SelectItem>
|
||||
)}
|
||||
>
|
||||
<SelectTrigger aria-label="Fruit" class="w-[180px]">
|
||||
<SelectTrigger aria-label="Language" class="w-[180px]">
|
||||
<SelectValue<string>>
|
||||
{(state) => languageNames.of(state.selectedOption())}
|
||||
</SelectValue>
|
||||
|
@ -151,7 +153,7 @@ export default function ServersPage() {
|
|||
)}
|
||||
>
|
||||
<SelectTrigger aria-label="Fruit" class="w-[180px]">
|
||||
<SelectValue<string>>
|
||||
<SelectValue<keyof typeof sortNameMap>>
|
||||
{(state) => sortNameMap[state.selectedOption()]}
|
||||
</SelectValue>
|
||||
</SelectTrigger>
|
||||
|
@ -178,9 +180,11 @@ export default function ServersPage() {
|
|||
const StatChips = () => (
|
||||
<>
|
||||
<Chip title="Mbin Version">Mbin {server.version}</Chip>
|
||||
<Show when={server.api}>
|
||||
<Chip title="Language" icon={MaterialSymbolsLanguage}>
|
||||
{languageNames.of(server.language)}
|
||||
{languageNames.of(server.api!.defaultLang)}
|
||||
</Chip>
|
||||
</Show>
|
||||
<Chip title="Total users" icon={MaterialSymbolsPerson}>
|
||||
{server.totalUsers}
|
||||
</Chip>
|
||||
|
@ -254,25 +258,42 @@ export default function ServersPage() {
|
|||
</Chip>
|
||||
</div>
|
||||
|
||||
<Show
|
||||
when={server.api}
|
||||
fallback={
|
||||
<div>
|
||||
No information available. This server's api is
|
||||
inaccessible and it's not recommended to use.
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<Accordion
|
||||
multiple={false}
|
||||
collapsible
|
||||
defaultValue={['about']}
|
||||
>
|
||||
<For each={Object.entries(server.pages)}>
|
||||
<For each={Object.entries(server.api!.pages)}>
|
||||
{([page, pageContent]) => (
|
||||
<Show when={pageContent || page === 'contact'}>
|
||||
<AccordionItem value={page}>
|
||||
<AccordionTrigger>
|
||||
{pageNames[page as keyof Server['pages']]}
|
||||
{
|
||||
pageNames[
|
||||
page as keyof NonNullable<
|
||||
Server['api']
|
||||
>['pages']
|
||||
]
|
||||
}
|
||||
</AccordionTrigger>
|
||||
<AccordionContent>
|
||||
<Show when={page == 'contact'}>
|
||||
<Button
|
||||
href={`mailto:${server.contactEmail}`}
|
||||
href={`mailto:${
|
||||
server.api!.contactEmail
|
||||
}`}
|
||||
as="a"
|
||||
>
|
||||
Email admin: {server.contactEmail}
|
||||
Email admin: {server.api!.contactEmail}
|
||||
</Button>
|
||||
</Show>
|
||||
<Markdown>{pageContent}</Markdown>
|
||||
|
@ -281,14 +302,15 @@ export default function ServersPage() {
|
|||
</Show>
|
||||
)}
|
||||
</For>
|
||||
<Show when={server.defederated.length}>
|
||||
<Show when={server.api!.defederated.length}>
|
||||
<AccordionItem value="defederated">
|
||||
<AccordionTrigger>
|
||||
Defederated Servers ({server.defederated.length})
|
||||
Defederated Servers (
|
||||
{server.api!.defederated.length})
|
||||
</AccordionTrigger>
|
||||
<AccordionContent>
|
||||
<div class="flex flex-wrap gap-1">
|
||||
<For each={server.defederated}>
|
||||
<For each={server.api!.defederated}>
|
||||
{(server) => <Chip>{server}</Chip>}
|
||||
</For>
|
||||
</div>
|
||||
|
@ -296,6 +318,7 @@ export default function ServersPage() {
|
|||
</AccordionItem>
|
||||
</Show>
|
||||
</Accordion>
|
||||
</Show>
|
||||
|
||||
<Show when={server.openRegistrations}>
|
||||
<DialogFooter>
|
||||
|
|
Loading…
Add table
Reference in a new issue