feat(nodeinfo): extract and save NodeInfo information from instances to display it on instances list

We also try to detect the application actor if it's not given by NodeInfo metadata (FEP-2677)
(guessing for Mobilizon, PeerTube & Mastodon).

Closes #1392

Signed-off-by: Thomas Citharel <tcit@tcit.fr>
This commit is contained in:
Thomas Citharel
2023-12-20 17:52:27 +01:00
parent 2fba6379f1
commit 99b2339424
25 changed files with 775 additions and 167 deletions

View File

@@ -293,7 +293,7 @@ button.menubar__button {
@apply px-3 dark:text-black;
}
.pagination-link-current {
@apply bg-primary dark:bg-primary cursor-not-allowed pointer-events-none border-primary text-white dark:text-zinc-900;
@apply bg-primary dark:bg-primary cursor-not-allowed pointer-events-none border-primary text-white;
}
.pagination-ellipsis {
@apply text-center m-1 text-gray-300;

View File

@@ -2,8 +2,8 @@
<li
class="setting-menu-item"
:class="{
'cursor-pointer bg-mbz-yellow-alt-500 dark:bg-mbz-purple-500': isActive,
'bg-mbz-yellow-alt-100 hover:bg-mbz-yellow-alt-200 dark:bg-mbz-purple-300 dark:hover:bg-mbz-purple-400 dark:text-white':
'cursor-pointer bg-mbz-yellow-alt-500 dark:bg-mbz-purple-600': isActive,
'bg-mbz-yellow-alt-100 hover:bg-mbz-yellow-alt-200 dark:bg-mbz-purple-500 dark:hover:bg-mbz-purple-600 dark:text-white':
!isActive,
}"
>

View File

@@ -1,6 +1,6 @@
<template>
<li
class="bg-mbz-yellow-alt-300 text-violet-2 dark:bg-mbz-purple-500 dark:text-zinc-100 text-xl"
class="bg-mbz-yellow-alt-300 text-violet-2 dark:bg-mbz-purple-700 dark:text-zinc-100 text-xl"
>
<router-link
class="cursor-pointer my-2 mx-0 py-2 px-3 font-medium block no-underline"

View File

@@ -81,6 +81,10 @@ export const INSTANCE_FRAGMENT = gql`
fragment InstanceFragment on Instance {
domain
hasRelay
instanceName
instanceDescription
software
softwareVersion
relayAddress
followerStatus
followedStatus

View File

@@ -1638,5 +1638,8 @@
"Return to the event page": "Return to the event page",
"Cancel participation": "Cancel participation",
"Add a recipient": "Add a recipient",
"Announcements for {eventTitle}": "Announcements for {eventTitle}"
"Announcements for {eventTitle}": "Announcements for {eventTitle}",
"Visit {instance_domain}": "Visit {instance_domain}",
"Software details: {software_details}": "Software details: {software_details}",
"Only instances with an application actor can be followed": "Only instances with an application actor can be followed"
}

View File

@@ -1632,5 +1632,8 @@
"Return to the event page": "Retourner à la page de l'événement",
"Cancel participation": "Annuler la participation",
"Add a recipient": "Ajouter un·e destinataire",
"Announcements for {eventTitle}": "Annonces pour {eventTitle}"
"Announcements for {eventTitle}": "Annonces pour {eventTitle}",
"Visit {instance_domain}": "Visiter {instance_domain}",
"Software details: {software_details}": "Détails du logiciel : {software_details}",
"Only instances with an application actor can be followed": "Seules les instances avec un acteur application peuvent être suivies"
}

View File

@@ -3,6 +3,10 @@ import { InstanceFollowStatus } from "./enums";
export interface IInstance {
domain: string;
hasRelay: boolean;
instanceName: string | null;
instanceDescription: string | null;
software: string | null;
softwareVersion: string | null;
relayAddress: string | null;
followerStatus: InstanceFollowStatus;
followedStatus: InstanceFollowStatus;

View File

@@ -2,138 +2,187 @@
<div v-if="instance">
<breadcrumbs-nav
:links="[
{ name: RouteName.ADMIN, text: $t('Admin') },
{ name: RouteName.INSTANCES, text: $t('Instances') },
{ name: RouteName.ADMIN, text: t('Admin') },
{ name: RouteName.INSTANCES, text: t('Instances') },
{ text: instance.domain },
]"
/>
<h1 class="text-2xl">{{ instance.domain }}</h1>
<div
class="grid md:grid-cols-2 xl:grid-cols-4 gap-2 content-center text-center mt-2"
<section
class="flex flex-wrap md:flex-nowrap items-center justify-between gap-4"
>
<div class="bg-zinc-50 dark:bg-mbz-purple-500 rounded-xl p-8">
<router-link
:to="{
name: RouteName.PROFILES,
query: { domain: instance.domain },
}"
<div>
<h2 class="text-4xl font-bold" v-if="instance.instanceName">
{{ instance.instanceName }}
</h2>
<h2 class="text-4xl font-bold" v-else>{{ instance.domain }}</h2>
<p
v-if="instance.instanceDescription"
class="text-slate-700 dark:text-slate-400 my-4"
>
<span class="mb-4 text-xl font-semibold block">{{
instance.personCount
}}</span>
<span class="text-sm block">{{ $t("Profiles") }}</span>
</router-link>
</div>
<div class="bg-gray-50 dark:bg-mbz-purple-500 rounded-xl p-8">
<router-link
:to="{
name: RouteName.ADMIN_GROUPS,
query: { domain: instance.domain },
}"
>
<span class="mb-4 text-xl font-semibold block">{{
instance.groupCount
}}</span>
<span class="text-sm block">{{ $t("Groups") }}</span>
</router-link>
</div>
<div class="bg-zinc-50 dark:bg-mbz-purple-500 rounded-xl p-8">
<span class="mb-4 text-xl font-semibold block">{{
instance.followingsCount
}}</span>
<span class="text-sm block">{{ $t("Followings") }}</span>
</div>
<div class="bg-zinc-50 dark:bg-mbz-purple-500 rounded-xl p-8">
<span class="mb-4 text-xl font-semibold block">{{
instance.followersCount
}}</span>
<span class="text-sm block">{{ $t("Followers") }}</span>
</div>
<div class="bg-zinc-50 dark:bg-mbz-purple-500 rounded-xl p-8">
<router-link
:to="{ name: RouteName.REPORTS, query: { domain: instance.domain } }"
>
<span class="mb-4 text-xl font-semibold block">{{
instance.reportsCount
}}</span>
<span class="text-sm block">{{ $t("Reports") }}</span>
</router-link>
</div>
<div class="bg-zinc-50 dark:bg-mbz-purple-500 rounded-xl p-8">
<span class="mb-4 font-semibold block">{{
formatBytes(instance.mediaSize)
}}</span>
<span class="text-sm block">{{ $t("Uploaded media size") }}</span>
</div>
</div>
<div class="mt-3 grid xl:grid-cols-2 gap-4">
<div
class="border bg-white dark:bg-mbz-purple-500 dark:border-mbz-purple-700 p-6 shadow-md rounded-md"
v-if="instance.hasRelay"
>
<button
@click="
removeInstanceFollow({
address: instance?.relayAddress,
})
"
v-if="instance.followedStatus == InstanceFollowStatus.APPROVED"
class="bg-primary hover:bg-primary-700 focus:outline-none focus:ring-2 focus:ring-gray-400 focus:ring-offset-2 focus:ring-offset-gray-50 text-white hover:text-white font-semibold h-12 px-6 rounded-lg w-full flex items-center justify-center sm:w-auto"
>
{{ $t("Stop following instance") }}
</button>
<button
@click="
removeInstanceFollow({
address: instance?.relayAddress,
})
"
v-else-if="instance.followedStatus == InstanceFollowStatus.PENDING"
class="bg-primary hover:bg-primary-700 focus:outline-none focus:ring-2 focus:ring-gray-400 focus:ring-offset-2 focus:ring-offset-gray-50 text-white hover:text-white font-semibold h-12 px-6 rounded-lg w-full flex items-center justify-center sm:w-auto"
>
{{ $t("Cancel follow request") }}
</button>
<button
@click="followInstance"
v-else
class="bg-primary hover:bg-primary-700 focus:outline-none focus:ring-2 focus:ring-gray-400 focus:ring-offset-2 focus:ring-offset-gray-50 text-white hover:text-white font-semibold h-12 px-6 rounded-lg w-full flex items-center justify-center sm:w-auto"
>
{{ $t("Follow instance") }}
</button>
</div>
<div v-else class="md:h-48 py-16 text-center opacity-50">
{{ $t("Only Mobilizon instances can be followed") }}
</div>
<div
class="border bg-white dark:bg-mbz-purple-500 dark:border-mbz-purple-700 p-6 shadow-md rounded-md flex flex-col gap-2"
>
<button
@click="
acceptInstance({
address: instance?.relayAddress,
})
"
v-if="instance.followerStatus == InstanceFollowStatus.PENDING"
class="bg-green-700 hover:bg-primary-700 focus:outline-none focus:ring-2 focus:ring-gray-400 focus:ring-offset-2 focus:ring-offset-gray-50 text-white hover:text-white font-semibold h-12 px-6 rounded-lg w-full flex items-center justify-center sm:w-auto"
>
{{ $t("Accept follow") }}
</button>
<button
@click="
rejectInstance({
address: instance?.relayAddress,
})
"
v-if="instance.followerStatus != InstanceFollowStatus.NONE"
class="bg-red-700 hover:bg-primary-700 focus:outline-none focus:ring-2 focus:ring-gray-400 focus:ring-offset-2 focus:ring-offset-gray-50 text-white hover:text-white font-semibold h-12 px-6 rounded-lg w-full flex items-center justify-center sm:w-auto"
>
{{ $t("Reject follow") }}
</button>
<p v-if="instance.followerStatus == InstanceFollowStatus.NONE">
{{ $t("This instance doesn't follow yours.") }}
{{ instance.instanceDescription }}
</p>
<i18n-t
v-if="instance.software && instance.softwareVersion"
keypath="Software details: {software_details}"
class="my-4"
>
<template #software_details>
<span class="capitalize">
{{ instance.software }} - {{ instance.softwareVersion }}
</span>
</template>
</i18n-t>
</div>
</div>
<o-button
tag="a"
:href="`https://${instance.domain}`"
target="_blank"
rel="noopener noreferrer"
icon-right="open-in-new"
class="mx-auto md:mx-0"
>{{
t("Visit {instance_domain}", { instance_domain: instance.domain })
}}
</o-button>
</section>
<section>
<div
class="grid md:grid-cols-2 xl:grid-cols-4 gap-2 content-center text-center mt-2"
>
<div class="bg-zinc-50 dark:bg-mbz-purple-500 rounded-xl p-8">
<router-link
:to="{
name: RouteName.PROFILES,
query: { domain: instance.domain },
}"
>
<span class="mb-4 text-xl font-semibold block">{{
instance.personCount
}}</span>
<span class="text-sm block">{{ t("Profiles") }}</span>
</router-link>
</div>
<div class="bg-gray-50 dark:bg-mbz-purple-500 rounded-xl p-8">
<router-link
:to="{
name: RouteName.ADMIN_GROUPS,
query: { domain: instance.domain },
}"
>
<span class="mb-4 text-xl font-semibold block">{{
instance.groupCount
}}</span>
<span class="text-sm block">{{ t("Groups") }}</span>
</router-link>
</div>
<div class="bg-zinc-50 dark:bg-mbz-purple-500 rounded-xl p-8">
<span class="mb-4 text-xl font-semibold block">{{
instance.followingsCount
}}</span>
<span class="text-sm block">{{ t("Followings") }}</span>
</div>
<div class="bg-zinc-50 dark:bg-mbz-purple-500 rounded-xl p-8">
<span class="mb-4 text-xl font-semibold block">{{
instance.followersCount
}}</span>
<span class="text-sm block">{{ t("Followers") }}</span>
</div>
<div class="bg-zinc-50 dark:bg-mbz-purple-500 rounded-xl p-8">
<router-link
:to="{
name: RouteName.REPORTS,
query: { domain: instance.domain },
}"
>
<span class="mb-4 text-xl font-semibold block">{{
instance.reportsCount
}}</span>
<span class="text-sm block">{{ t("Reports") }}</span>
</router-link>
</div>
<div class="bg-zinc-50 dark:bg-mbz-purple-500 rounded-xl p-8">
<span class="mb-4 font-semibold block">{{
formatBytes(instance.mediaSize)
}}</span>
<span class="text-sm block">{{ t("Uploaded media size") }}</span>
</div>
</div>
</section>
<section>
<div class="mt-3 grid xl:grid-cols-2 gap-4">
<div
class="border bg-white dark:bg-mbz-purple-500 dark:border-mbz-purple-700 p-6 shadow-md rounded-md"
v-if="
instance.hasRelay &&
!['mastodon', 'peertube'].includes(
instance.software?.toLowerCase() ?? ''
)
"
>
<button
@click="
removeInstanceFollow({
address: instance?.relayAddress,
})
"
v-if="instance.followedStatus == InstanceFollowStatus.APPROVED"
class="bg-primary hover:bg-primary-700 focus:outline-none focus:ring-2 focus:ring-gray-400 focus:ring-offset-2 focus:ring-offset-gray-50 text-white hover:text-white font-semibold h-12 px-6 rounded-lg w-full flex items-center justify-center sm:w-auto"
>
{{ t("Stop following instance") }}
</button>
<button
@click="
removeInstanceFollow({
address: instance?.relayAddress,
})
"
v-else-if="instance.followedStatus == InstanceFollowStatus.PENDING"
class="bg-primary hover:bg-primary-700 focus:outline-none focus:ring-2 focus:ring-gray-400 focus:ring-offset-2 focus:ring-offset-gray-50 text-white hover:text-white font-semibold h-12 px-6 rounded-lg w-full flex items-center justify-center sm:w-auto"
>
{{ t("Cancel follow request") }}
</button>
<button
@click="followInstance"
v-else
class="bg-primary hover:bg-primary-700 focus:outline-none focus:ring-2 focus:ring-gray-400 focus:ring-offset-2 focus:ring-offset-gray-50 text-white hover:text-white font-semibold h-12 px-6 rounded-lg w-full flex items-center justify-center sm:w-auto"
>
{{ t("Follow instance") }}
</button>
</div>
<div v-else class="md:h-48 py-16 text-center opacity-50">
{{ t("Only instances with an application actor can be followed") }}
</div>
<div
class="border bg-white dark:bg-mbz-purple-500 dark:border-mbz-purple-700 p-6 shadow-md rounded-md flex flex-col gap-2"
>
<button
@click="
acceptInstance({
address: instance?.relayAddress,
})
"
v-if="instance.followerStatus == InstanceFollowStatus.PENDING"
class="bg-green-700 hover:bg-primary-700 focus:outline-none focus:ring-2 focus:ring-gray-400 focus:ring-offset-2 focus:ring-offset-gray-50 text-white hover:text-white font-semibold h-12 px-6 rounded-lg w-full flex items-center justify-center sm:w-auto"
>
{{ t("Accept follow") }}
</button>
<button
@click="
rejectInstance({
address: instance?.relayAddress,
})
"
v-if="instance.followerStatus != InstanceFollowStatus.NONE"
class="bg-red-700 hover:bg-primary-700 focus:outline-none focus:ring-2 focus:ring-gray-400 focus:ring-offset-2 focus:ring-offset-gray-50 text-white hover:text-white font-semibold h-12 px-6 rounded-lg w-full flex items-center justify-center sm:w-auto"
>
{{ t("Reject follow") }}
</button>
<p v-if="instance.followerStatus == InstanceFollowStatus.NONE">
{{ t("This instance doesn't follow yours.") }}
</p>
</div>
</div>
</section>
</div>
</template>
<script lang="ts" setup>
@@ -152,6 +201,7 @@ import { InstanceFollowStatus } from "@/types/enums";
import { useMutation, useQuery } from "@vue/apollo-composable";
import { computed, inject } from "vue";
import { Notifier } from "@/plugins/notifier";
import { useI18n } from "vue-i18n";
const props = defineProps<{ domain: string }>();
@@ -164,6 +214,8 @@ const instance = computed(() => instanceResult.value?.instance);
const notifier = inject<Notifier>("notifier");
const { t } = useI18n({ useScope: "global" });
const { mutate: acceptInstance, onError: onAcceptInstanceError } = useMutation(
ACCEPT_RELAY,
() => ({

View File

@@ -72,21 +72,65 @@
name: RouteName.INSTANCE,
params: { domain: instance.domain },
}"
class="flex items-center mb-2 rounded bg-mbz-yellow-alt-300 dark:bg-mbz-purple-400 p-4 flex-wrap justify-center gap-x-2 gap-y-3"
class="flex items-center mb-2 rounded bg-mbz-yellow-alt-300 hover:bg-mbz-yellow-alt-200 dark:bg-mbz-purple-600 dark:hover:bg-mbz-purple-700 p-4 flex-wrap justify-center gap-x-2 gap-y-3"
v-for="instance in instances.elements"
:key="instance.domain"
>
<div class="grow overflow-hidden flex items-center gap-1">
<img
class="w-12"
v-if="instance.hasRelay"
v-if="instance.software === 'Mobilizon'"
src="/img/logo.svg"
alt=""
/>
<CloudQuestion v-else :size="36" />
<mastodon-logo
class="w-8 mx-2"
alt=""
v-else-if="instance.software?.toLowerCase() === 'mastodon'"
/>
<img
class="w-8 mx-2"
v-else-if="instance.software?.toLowerCase() === 'gancio'"
src="/img/gancio.png"
alt=""
/>
<img
class="w-8 mx-2"
v-else-if="instance.software?.toLowerCase() === 'wordpress'"
src="/img/wordpress-logo.svg"
alt=""
/>
<CloudQuestion class="mx-1.5" v-else :size="36" />
<div class="">
<h3 class="text-lg truncate">{{ instance.domain }}</h3>
<h3
class="text-lg truncate font-bold text-slate-800 dark:text-slate-100"
v-if="instance.instanceName"
>
{{ instance.instanceName }}
</h3>
<h3
class="text-lg truncate font-bold text-slate-800 dark:text-slate-100"
v-else
>
{{ instance.domain }}
</h3>
<p
v-if="instance.instanceName"
class="inline-flex gap-2 text-slate-700 dark:text-slate-300"
>
<span class="capitalize" v-if="instance.software">{{
instance.software
}}</span>
-
<span>{{ instance.domain }}</span>
</p>
<p
v-else-if="instance.software"
class="capitalize text-slate-700 dark:text-slate-300"
>
{{ instance.software }}
</p>
<span
class="text-sm"
v-if="instance.followedStatus === InstanceFollowStatus.APPROVED"
@@ -186,6 +230,7 @@ import { useRouter } from "vue-router";
import { useHead } from "@unhead/vue";
import CloudQuestion from "../../../node_modules/vue-material-design-icons/CloudQuestion.vue";
import { Notifier } from "@/plugins/notifier";
import MastodonLogo from "@/components/Share/MastodonLogo.vue";
const INSTANCES_PAGE_LIMIT = 10;