Rework servers data to allow instances with inaccessible APIs

This commit is contained in:
John Wesley 2024-07-16 12:16:46 -04:00
parent b49ee68138
commit a44e087eae
3 changed files with 145 additions and 91 deletions

View file

@ -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>

View file

@ -73,62 +73,88 @@ const fetchServerList = async () => {
const fetchServerInfo = async (domain) => {
console.log('START:', domain);
const jsonNodeInfo = await (
await fetch(`https://${domain}/nodeinfo/2.1.json`)
).json();
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)`);
if (jsonNodeInfo.software.name != 'mbin') {
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();
if (jsonApiInfo.websiteDomain != domain) {
throw new Error(`${domain} api not setup correctly (skipped)`);
/** @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 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);
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',

View file

@ -41,25 +41,27 @@ 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;
pages: {
about?: string;
contact?: string;
faq?: string;
privacyPolicy?: string;
terms?: string;
api?: {
contactEmail: string;
federationEnabled: boolean;
defaultLang: string;
pages: {
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',
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>
<Chip title="Language" icon={MaterialSymbolsLanguage}>
{languageNames.of(server.language)}
</Chip>
<Show when={server.api}>
<Chip title="Language" icon={MaterialSymbolsLanguage}>
{languageNames.of(server.api!.defaultLang)}
</Chip>
</Show>
<Chip title="Total users" icon={MaterialSymbolsPerson}>
{server.totalUsers}
</Chip>
@ -254,48 +258,67 @@ export default function ServersPage() {
</Chip>
</div>
<Accordion
multiple={false}
collapsible
defaultValue={['about']}
<Show
when={server.api}
fallback={
<div>
No information available. This server's api is
inaccessible and it's not recommended to use.
</div>
}
>
<For each={Object.entries(server.pages)}>
{([page, pageContent]) => (
<Show when={pageContent || page === 'contact'}>
<AccordionItem value={page}>
<AccordionTrigger>
{pageNames[page as keyof Server['pages']]}
</AccordionTrigger>
<AccordionContent>
<Show when={page == 'contact'}>
<Button
href={`mailto:${server.contactEmail}`}
as="a"
>
Email admin: {server.contactEmail}
</Button>
</Show>
<Markdown>{pageContent}</Markdown>
</AccordionContent>
</AccordionItem>
</Show>
)}
</For>
<Show when={server.defederated.length}>
<AccordionItem value="defederated">
<AccordionTrigger>
Defederated Servers ({server.defederated.length})
</AccordionTrigger>
<AccordionContent>
<div class="flex flex-wrap gap-1">
<For each={server.defederated}>
{(server) => <Chip>{server}</Chip>}
</For>
</div>
</AccordionContent>
</AccordionItem>
</Show>
</Accordion>
<Accordion
multiple={false}
collapsible
defaultValue={['about']}
>
<For each={Object.entries(server.api!.pages)}>
{([page, pageContent]) => (
<Show when={pageContent || page === 'contact'}>
<AccordionItem value={page}>
<AccordionTrigger>
{
pageNames[
page as keyof NonNullable<
Server['api']
>['pages']
]
}
</AccordionTrigger>
<AccordionContent>
<Show when={page == 'contact'}>
<Button
href={`mailto:${
server.api!.contactEmail
}`}
as="a"
>
Email admin: {server.api!.contactEmail}
</Button>
</Show>
<Markdown>{pageContent}</Markdown>
</AccordionContent>
</AccordionItem>
</Show>
)}
</For>
<Show when={server.api!.defederated.length}>
<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}>
<DialogFooter>