Merge branch 'fixes' into 'main'

Update schema.graphql file

Closes #1088

See merge request framasoft/mobilizon!1212
This commit is contained in:
Thomas Citharel 2022-04-20 17:55:03 +00:00
commit 04ec8fe2d3
44 changed files with 925 additions and 713 deletions

View File

@ -240,8 +240,6 @@ build-docker-tag:
package-app:
image: mobilizon/buildpack:1.13.4-erlang-24.3.3-${OS}
stage: package
before_script:
- apt-get update && apt-get install -yq build-essential git curl cmake
variables: &release-variables
MIX_ENV: "prod"
DEBIAN_FRONTEND: noninteractive

View File

@ -1,16 +1,19 @@
<template>
<div class="actor-inline">
<div class="actor-avatar">
<figure class="image is-24x24" v-if="actor.avatar">
<div class="inline-flex items-start">
<div class="flex-none mr-2">
<figure class="image is-48x48" v-if="actor.avatar">
<img class="is-rounded" :src="actor.avatar.url" alt="" />
</figure>
<b-icon v-else size="is-medium" icon="account-circle" />
<b-icon v-else size="is-large" icon="account-circle" />
</div>
<div class="actor-name">
<p>
<div class="flex-auto">
<p class="text-base line-clamp-3 md:line-clamp-2 max-w-xl">
{{ displayName(actor) }}
</p>
<p class="text-sm text-gray-500 truncate">
@{{ usernameWithDomain(actor) }}
</p>
</div>
</div>
</template>

View File

@ -190,7 +190,7 @@ export default class ShareEventModal extends Vue {
}
get mastodonShareUrl(): string {
return `https://toot.karamoff.dev/?text=${encodeURIComponent(
return `https://toot.kytta.dev/?text=${encodeURIComponent(
this.basicTextToEncode
)}`;
}

View File

@ -10,7 +10,7 @@
import { Component, Prop, Vue } from "vue-property-decorator";
import RedirectWithAccount from "@/components/Utils/RedirectWithAccount.vue";
import { FETCH_GROUP } from "@/graphql/group";
import { IGroup } from "@/types/actor";
import { displayName, IGroup } from "@/types/actor";
@Component({
components: { RedirectWithAccount },
@ -52,7 +52,7 @@ export default class JoinGroupWithAccount extends Vue {
}
get groupTitle(): undefined | string {
return this.group?.name || this.group?.preferredUsername;
return this.group && displayName(this.group);
}
sentence = this.$t(

View File

@ -117,7 +117,7 @@ import { Component, Prop, Vue, Ref } from "vue-property-decorator";
import { GroupVisibility } from "@/types/enums";
import DiasporaLogo from "../Share/DiasporaLogo.vue";
import MastodonLogo from "../Share/MastodonLogo.vue";
import TelegramLogo from "../Share/MastodonLogo.vue";
import TelegramLogo from "../Share/TelegramLogo.vue";
import { displayName, IGroup } from "@/types/actor";
@Component({
@ -177,7 +177,7 @@ export default class ShareGroupModal extends Vue {
}
get mastodonShareUrl(): string {
return `https://toot.karamoff.dev/?text=${encodeURIComponent(
return `https://toot.kytta.dev/?text=${encodeURIComponent(
this.basicTextToEncode
)}`;
}

View File

@ -9,7 +9,7 @@
:rounded="true"
style="height: 120px"
/>
<div class="title-info-wrapper has-text-grey-dark">
<div class="title-info-wrapper has-text-grey-dark px-1">
<h3 class="post-minimalist-title" :lang="post.language">
{{ post.title }}
</h3>

View File

@ -179,7 +179,7 @@ export default class SharePostModal extends Vue {
}
get mastodonShareUrl(): string {
return `https://toot.karamoff.dev/?text=${encodeURIComponent(
return `https://toot.kytta.dev/?text=${encodeURIComponent(
this.basicTextToEncode
)}`;
}

View File

@ -9,7 +9,7 @@
:class="{ 'is-titleless': !title }"
>
<div class="media">
<div class="media-left">
<div class="media-left hidden md:block">
<b-icon icon="alert" type="is-warning" size="is-large" />
</div>
<div class="media-content">

View File

@ -9,7 +9,7 @@
<!-- @slot Mandatory title -->
<slot />
</h2>
<p v-show="$slots.desc">
<p v-show="$slots.desc" :class="descriptionClasses">
<!-- @slot Optional description -->
<slot name="desc" />
</p>
@ -21,6 +21,8 @@ import { Component, Prop, Vue } from "vue-property-decorator";
@Component
export default class EmptyContent extends Vue {
@Prop({ type: String, required: true }) icon!: string;
@Prop({ type: String, required: false, default: "" })
descriptionClasses!: string;
@Prop({ type: Boolean, required: false, default: false }) inline!: boolean;
@Prop({ type: Boolean, required: false, default: false }) center!: boolean;
}

View File

@ -1321,5 +1321,14 @@
"You may now close this page or {return_to_the_homepage}.": "You may now close this page or {return_to_the_homepage}.",
"This group is a remote group, it's possible the original instance has more informations.": "This group is a remote group, it's possible the original instance has more informations.",
"View the group profile on the original instance": "View the group profile on the original instance",
"View past events": "View past events"
"View past events": "View past events",
"Get informed of the upcoming public events": "Get informed of the upcoming public events",
"Join": "Join",
"Become part of the community and start organizing events": "Become part of the community and start organizing events",
"Follow requests will be approved by a group moderator": "Follow requests will be approved by a group moderator",
"Follow request pending approval": "Follow request pending approval",
"Your membership is pending approval": "Your membership is pending approval",
"Activate notifications": "Activate notifications",
"Deactivate notifications": "Deactivate notifications",
"Membership requests will be approved by a group moderator": "Membership requests will be approved by a group moderator"
}

View File

@ -1312,5 +1312,14 @@
"No instance found.": "Aucune instance trouvée.",
"This group is a remote group, it's possible the original instance has more informations.": "Ce groupe est un groupe distant, il est possible que l'instance d'origine ait plus d'informations.",
"View the group profile on the original instance": "Afficher le profil du groupe sur l'instance d'origine",
"View past events": "Voir les événements passés"
"View past events": "Voir les événements passés",
"Get informed of the upcoming public events": "Soyez informé⋅e des événements publics à venir",
"Join": "Rejoindre",
"Become part of the community and start organizing events": "Faites partie de la communauté et commencez à organiser des événements",
"Follow requests will be approved by a group moderator": "Les demandes de suivi seront approuvées par un⋅e modérateur⋅ice du groupe",
"Follow request pending approval": "Demande de suivi en attente d'approbation",
"Your membership is pending approval": "Votre adhésion est en attente d'approbation",
"Activate notifications": "Activer les notifications",
"Deactivate notifications": "Désactiver les notifications",
"Membership requests will be approved by a group moderator": "Les demandes d'adhésion seront approuvées par un⋅e modérateur⋅ice du groupe"
}

View File

@ -68,7 +68,7 @@ export function usernameWithDomain(actor: IActor, force = false): string {
}
export function displayName(actor: IActor): string {
return actor.name != null && actor.name !== ""
return actor && actor.name != null && actor.name !== ""
? actor.name
: usernameWithDomain(actor);
}

View File

@ -4,7 +4,7 @@
<p class="modal-card-title">{{ $t("Pick an identity") }}</p>
</header>
<section class="modal-card-body">
<div class="list is-hoverable">
<div class="list is-hoverable list-none">
<a
class="list-item"
v-for="identity in identities"
@ -12,7 +12,7 @@
:class="{
'is-active': currentIdentity && identity.id === currentIdentity.id,
}"
@click="changeCurrentIdentity(identity)"
@click="currentIdentity = identity"
>
<div class="media">
<img
@ -60,10 +60,11 @@ export default class IdentityPicker extends Vue {
identities: IActor[] = [];
currentIdentity: IActor = this.value;
get currentIdentity(): IActor {
return this.value;
}
changeCurrentIdentity(identity: IActor): void {
this.currentIdentity = identity;
set currentIdentity(identity: IActor) {
this.$emit("input", identity);
}
}

View File

@ -8,7 +8,9 @@
]"
/>
<h1 class="text-2xl">{{ instance.domain }}</h1>
<div class="grid md:grid-cols-4 gap-2 content-center text-center mt-2">
<div
class="grid md:grid-cols-2 xl:grid-cols-4 gap-2 content-center text-center mt-2"
>
<div class="bg-gray-50 rounded-xl p-8">
<router-link
:to="{
@ -64,7 +66,7 @@
<span class="text-sm block">{{ $t("Uploaded media size") }}</span>
</div>
</div>
<div class="mt-3 grid md:grid-cols-2 gap-4" v-if="instance.hasRelay">
<div class="mt-3 grid xl:grid-cols-2 gap-4" v-if="instance.hasRelay">
<div class="border bg-white p-6 shadow-md rounded-md">
<button
@click="removeInstanceFollow"
@ -88,7 +90,7 @@
{{ $t("Follow instance") }}
</button>
</div>
<div class="border bg-white p-6 shadow-md rounded-md">
<div class="border bg-white p-6 shadow-md rounded-md flex flex-col gap-2">
<button
@click="acceptInstance"
v-if="instance.followerStatus == InstanceFollowStatus.PENDING"
@ -98,12 +100,12 @@
</button>
<button
@click="rejectInstance"
v-else-if="instance.followerStatus != InstanceFollowStatus.NONE"
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-else>
<p v-if="instance.followerStatus == InstanceFollowStatus.NONE">
{{ $t("This instance doesn't follow yours.") }}
</p>
</div>
@ -124,7 +126,6 @@ import {
import { Component, Prop, Vue } from "vue-property-decorator";
import { formatBytes } from "@/utils/datetime";
import RouteName from "@/router/name";
import { SnackbarProgrammatic as Snackbar } from "buefy";
import { IInstance } from "@/types/instance.model";
import { ApolloCache, gql, Reference } from "@apollo/client/core";
import { InstanceFollowStatus } from "@/types/enums";
@ -154,38 +155,61 @@ export default class Instance extends Vue {
async acceptInstance(): Promise<void> {
try {
const { instance } = this;
await this.$apollo.mutate({
mutation: ACCEPT_RELAY,
variables: {
address: `relay@${this.domain}`,
},
update(cache: ApolloCache<any>) {
cache.writeFragment({
id: cache.identify(instance as unknown as Reference),
fragment: gql`
fragment InstanceFollowerStatus on Instance {
followerStatus
}
`,
data: {
followerStatus: InstanceFollowStatus.APPROVED,
},
});
},
});
} catch (e: any) {
if (e.message) {
Snackbar.open({
message: e.message,
type: "is-danger",
position: "is-bottom",
});
} catch (error: any) {
if (error.graphQLErrors && error.graphQLErrors.length > 0) {
this.$notifier.error(error.graphQLErrors[0].message);
}
}
}
/**
* Reject instance follow
*/
async rejectInstance(): Promise<void> {
try {
const { instance } = this;
await this.$apollo.mutate({
mutation: REJECT_RELAY,
variables: {
address: `relay@${this.domain}`,
},
update(cache: ApolloCache<any>) {
cache.writeFragment({
id: cache.identify(instance as unknown as Reference),
fragment: gql`
fragment InstanceFollowerStatus on Instance {
followerStatus
}
`,
data: {
followerStatus: InstanceFollowStatus.NONE,
},
});
},
});
} catch (e: any) {
if (e.message) {
Snackbar.open({
message: e.message,
type: "is-danger",
position: "is-bottom",
});
} catch (error: any) {
if (error.graphQLErrors && error.graphQLErrors.length > 0) {
this.$notifier.error(error.graphQLErrors[0].message);
}
}
}
@ -199,17 +223,16 @@ export default class Instance extends Vue {
domain: this.domain,
},
});
} catch (err: any) {
if (err.message) {
Snackbar.open({
message: err.message,
type: "is-danger",
position: "is-bottom",
});
} catch (error: any) {
if (error.graphQLErrors && error.graphQLErrors.length > 0) {
this.$notifier.error(error.graphQLErrors[0].message);
}
}
}
/**
* Stop following instance
*/
async removeInstanceFollow(): Promise<void> {
const { instance } = this;
try {
@ -232,13 +255,9 @@ export default class Instance extends Vue {
});
},
});
} catch (e: any) {
if (e.message) {
Snackbar.open({
message: e.message,
type: "is-danger",
position: "is-bottom",
});
} catch (error: any) {
if (error.graphQLErrors && error.graphQLErrors.length > 0) {
this.$notifier.error(error.graphQLErrors[0].message);
}
}
}

View File

@ -124,6 +124,17 @@
</p>
</div>
</router-link>
<b-pagination
v-show="instances.total > INSTANCES_PAGE_LIMIT"
:total="instances.total"
v-model="instancePage"
:per-page="INSTANCES_PAGE_LIMIT"
:aria-next-label="$t('Next page')"
:aria-previous-label="$t('Previous page')"
:aria-page-label="$t('Page')"
:aria-current-label="$t('Current page')"
>
</b-pagination>
</div>
<div v-else-if="instances && instances.elements.length == 0">
<empty-content icon="lan-disconnect" :inline="true">
@ -163,6 +174,8 @@ import {
import { SnackbarProgrammatic as Snackbar } from "buefy";
const { isNavigationFailure, NavigationFailureType } = VueRouter;
const INSTANCES_PAGE_LIMIT = 10;
@Component({
apollo: {
instances: {
@ -171,7 +184,7 @@ const { isNavigationFailure, NavigationFailureType } = VueRouter;
variables() {
return {
page: this.instancePage,
limit: 10,
limit: INSTANCES_PAGE_LIMIT,
filterDomain: this.filterDomain,
filterFollowStatus: this.followStatus,
};
@ -204,6 +217,8 @@ export default class Follows extends Vue {
InstanceFollowStatus = InstanceFollowStatus;
INSTANCES_PAGE_LIMIT = INSTANCES_PAGE_LIMIT;
data(): Record<string, unknown> {
return {
debouncedUpdateDomainFilter: debounce(this.updateDomainFilter, 500),

View File

@ -18,7 +18,7 @@
<h1 class="title" v-if="group">
{{
$t("{group}'s events", {
group: group.name || group.preferredUsername,
group: displayName(group),
})
}}
</h1>
@ -78,7 +78,7 @@
<b-pagination
class="mt-4"
:total="group.organizedEvents.total"
v-model="eventsPage"
v-model="page"
:per-page="EVENTS_PAGE_LIMIT"
:aria-next-label="$t('Next page')"
:aria-previous-label="$t('Previous page')"
@ -91,17 +91,15 @@
</div>
</template>
<script lang="ts">
import { Component } from "vue-property-decorator";
import { mixins } from "vue-class-component";
import { Component, Vue } from "vue-property-decorator";
import RouteName from "@/router/name";
import Subtitle from "@/components/Utils/Subtitle.vue";
import GroupedMultiEventMinimalistCard from "@/components/Event/GroupedMultiEventMinimalistCard.vue";
import { PERSON_MEMBERSHIPS } from "@/graphql/actor";
import GroupMixin from "@/mixins/group";
import { IMember } from "@/types/actor/member.model";
import { FETCH_GROUP_EVENTS } from "@/graphql/event";
import EmptyContent from "../../components/Utils/EmptyContent.vue";
import { displayName, usernameWithDomain } from "../../types/actor";
import { displayName, IGroup, usernameWithDomain } from "../../types/actor";
const EVENTS_PAGE_LIMIT = 10;
@ -127,10 +125,11 @@ const EVENTS_PAGE_LIMIT = 10;
name: this.$route.params.preferredUsername,
beforeDateTime: this.showPassedEvents ? new Date() : null,
afterDateTime: this.showPassedEvents ? null : new Date(),
organisedEventsPage: this.eventsPage,
organisedEventsPage: this.page,
organisedEventsLimit: EVENTS_PAGE_LIMIT,
};
},
update: (data) => data.group,
},
},
components: {
@ -144,15 +143,27 @@ const EVENTS_PAGE_LIMIT = 10;
const { group } = this;
return {
title: this.$t("{group} events", {
group: group?.name || usernameWithDomain(group),
group: displayName(group),
}) as string,
};
},
})
export default class GroupEvents extends mixins(GroupMixin) {
export default class GroupEvents extends Vue {
group!: IGroup;
memberships!: IMember[];
eventsPage = 1;
get page(): number {
return parseInt((this.$route.query.page as string) || "1", 10);
}
set page(page: number) {
this.$router.push({
name: RouteName.GROUP_EVENTS,
query: { ...this.$route.query, page: page.toString() },
});
this.$apollo.queries.group.refetch();
}
usernameWithDomain = usernameWithDomain;
@ -170,14 +181,11 @@ export default class GroupEvents extends mixins(GroupMixin) {
}
get showPassedEvents(): boolean {
return (
this.$route.query.future !== undefined &&
this.$route.query.future.toString() === "false"
);
return this.$route.query.future === "false";
}
set showPassedEvents(value: boolean) {
this.$router.push({ query: { future: this.showPassedEvents.toString() } });
this.$router.replace({ query: { future: (!value).toString() } });
}
}
</script>

View File

@ -27,11 +27,11 @@
<div class="title-container">
<h1 v-if="group.name">{{ group.name }}</h1>
<b-skeleton v-else :animated="true" />
<small
<span
dir="ltr"
class="has-text-grey-dark"
v-if="group.preferredUsername"
>@{{ usernameWithDomain(group) }}</small
>@{{ usernameWithDomain(group) }}</span
>
<b-skeleton v-else :animated="true" />
<br />
@ -78,7 +78,7 @@
>
</p>
</div>
<div class="buttons">
<div class="flex gap-2">
<b-button
outlined
icon-left="timeline-text"
@ -101,78 +101,123 @@
}"
>{{ $t("Group settings") }}</b-button
>
<b-tooltip
v-if="
(!isCurrentActorAGroupMember || previewPublic) &&
group.openness === Openness.INVITE_ONLY
"
:label="$t('This group is invite-only')"
position="is-bottom"
>
<b-button disabled type="is-primary">{{
$t("Join group")
}}</b-button></b-tooltip
>
<b-button
v-else-if="
((!isCurrentActorAGroupMember &&
!isCurrentActorAPendingGroupMember) ||
previewPublic) &&
currentActor.id
"
@click="joinGroup"
@keyup.enter="joinGroup"
type="is-primary"
:disabled="previewPublic"
>{{ $t("Join group") }}</b-button
<b-dropdown
aria-role="list"
trap-focus
v-show="showJoinButton && showFollowButton"
>
<template #trigger>
<b-button
:label="$t('Follow')"
type="is-primary"
icon-left="rss"
icon-right="menu-down"
/>
</template>
<b-dropdown-item
aria-role="listitem"
class="p-0"
custom
:focusable="false"
:disabled="
isCurrentActorPendingFollow && currentActor.id !== undefined
"
>
<button class="media py-4 px-2 w-full" @click="followGroup">
<b-icon class="media-left" icon="rss" />
<div class="media-content">
<h3 class="font-medium text-lg">{{ $t("Follow") }}</h3>
<p class="whitespace-normal md:whitespace-nowrap text-sm">
{{ $t("Get informed of the upcoming public events") }}
</p>
<p
v-if="
doesGroupManuallyApprovesFollowers &&
!isCurrentActorPendingFollow
"
class="whitespace-normal md:whitespace-nowrap text-sm italic"
>
{{
$t(
"Follow requests will be approved by a group moderator"
)
}}
</p>
<p
v-if="isCurrentActorPendingFollow && currentActor.id"
class="whitespace-normal md:whitespace-nowrap text-sm italic"
>
{{ $t("Follow request pending approval") }}
</p>
</div>
</button>
</b-dropdown-item>
<b-dropdown-item
aria-role="listitem"
class="p-0 border-t border-solid"
custom
:focusable="false"
:disabled="
isGroupInviteOnly || isCurrentActorAPendingGroupMember
"
>
<button class="media py-4 px-2 w-full" @click="joinGroup">
<b-icon
class="media-left"
icon="account-multiple-plus"
></b-icon>
<div class="media-content">
<h3 class="font-medium text-lg">{{ $t("Join") }}</h3>
<div v-if="showJoinButton">
<p
class="whitespace-normal md:whitespace-nowrap text-sm"
>
{{
$t(
"Become part of the community and start organizing events"
)
}}
</p>
<p
v-if="isGroupInviteOnly"
class="whitespace-normal md:whitespace-nowrap text-sm italic"
>
{{ $t("This group is invite-only") }}
</p>
<p
v-if="
areGroupMembershipsModerated &&
!isCurrentActorAPendingGroupMember
"
class="whitespace-normal md:whitespace-nowrap text-sm italic"
>
{{
$t(
"Membership requests will be approved by a group moderator"
)
}}
</p>
<p
v-if="isCurrentActorAPendingGroupMember"
class="whitespace-normal md:whitespace-nowrap text-sm italic"
>
{{ $t("Your membership is pending approval") }}
</p>
</div>
</div>
</button>
</b-dropdown-item>
</b-dropdown>
<b-button
outlined
v-else-if="isCurrentActorAPendingGroupMember"
v-if="isCurrentActorAPendingGroupMember"
@click="leaveGroup"
@keyup.enter="leaveGroup"
type="is-primary"
>{{ $t("Cancel membership request") }}</b-button
>
<b-button
tag="router-link"
:to="{
name: RouteName.GROUP_JOIN,
params: { preferredUsername: usernameWithDomain(group) },
}"
v-else-if="!isCurrentActorAGroupMember || previewPublic"
:disabled="previewPublic"
type="is-primary"
>{{ $t("Join group") }}</b-button
>
<b-button
v-if="
((!isCurrentActorFollowing && !isCurrentActorAGroupMember) ||
previewPublic) &&
!isCurrentActorPendingFollow &&
currentActor.id
"
@click="followGroup"
@keyup.enter="followGroup"
type="is-primary"
:disabled="isCurrentActorPendingFollow"
>{{ $t("Follow") }}</b-button
>
<b-button
tag="router-link"
:to="{
name: RouteName.GROUP_FOLLOW,
params: { preferredUsername: usernameWithDomain(group) },
}"
v-else-if="
!isCurrentActorPendingFollow &&
!isCurrentActorFollowing &&
previewPublic
"
:disabled="previewPublic"
type="is-primary"
>{{ $t("Follow") }}</b-button
>
<b-button
outlined
v-if="isCurrentActorPendingFollow && currentActor.id"
@ -192,12 +237,20 @@
v-if="isCurrentActorFollowing"
@click="toggleFollowNotify"
@keyup.enter="toggleFollowNotify"
class="notification-button p-1.5"
outlined
:icon-left="
isCurrentActorFollowingNotify
? 'bell-outline'
: 'bell-off-outline'
"
></b-button>
>
<span class="sr-only">{{
isCurrentActorFollowingNotify
? $t("Activate notifications")
: $t("Deactivate notifications")
}}</span>
</b-button>
<b-button
outlined
icon-left="share"
@ -308,28 +361,6 @@
)
}}
</b-message>
<b-message
v-if="
!isCurrentActorAGroupMember &&
!isCurrentActorAPendingGroupMember &&
!isCurrentActorPendingFollow &&
!isCurrentActorFollowing
"
type="is-info"
has-icon
class="m-3"
>
<i18n
path="Following the group will allow you to be informed of the {group_upcoming_public_events}, whereas joining the group means you will {access_to_group_private_content_as_well}, including group discussions, group resources and members-only posts."
>
<b slot="group_upcoming_public_events">{{
$t("group's upcoming public events")
}}</b>
<b slot="access_to_group_private_content_as_well">{{
$t("access to the group's private content as well")
}}</b>
</i18n>
</b-message>
</div>
</header>
</div>
@ -506,6 +537,12 @@
$t("View full profile")
}}</a>
</b-message>
<event-metadata-block
:title="$t('About')"
v-if="group.summary && group.summary !== '<p></p>'"
>
<div dir="auto" v-html="group.summary" />
</event-metadata-block>
<event-metadata-block :title="$t('Members')" icon="account-group">
{{
$tc("{count} members", group.members.total, {
@ -553,17 +590,6 @@
</div>
</aside>
<div class="main-content">
<section>
<subtitle>{{ $t("About") }}</subtitle>
<div
dir="auto"
v-html="group.summary"
v-if="group.summary && group.summary !== '<p></p>'"
/>
<empty-content v-else-if="group" icon="image-text" :inline="true">
{{ $t("This group doesn't have a description yet.") }}
</empty-content>
</section>
<section>
<subtitle>{{ $t("Upcoming events") }}</subtitle>
<div
@ -577,7 +603,12 @@
class="organized-event"
/>
</div>
<empty-content v-else-if="group" icon="calendar" :inline="true">
<empty-content
v-else-if="group"
icon="calendar"
:inline="true"
description-classes="flex flex-col items-stretch"
>
{{ $t("No public upcoming events") }}
<template #desc>
<template v-if="isCurrentActorFollowing">
@ -594,7 +625,7 @@
</template>
<b-button
tag="router-link"
class="my-2"
class="my-2 self-center"
type="is-text"
:to="{
name: RouteName.GROUP_EVENTS,
@ -621,8 +652,8 @@
>
</div>
</section>
<section>
<subtitle>{{ $t("Latest posts") }}</subtitle>
<section class="flex flex-col items-stretch">
<subtitle class="ml-0">{{ $t("Latest posts") }}</subtitle>
<multi-post-list-item
v-if="
@ -642,13 +673,16 @@
{{ $t("No posts yet") }}
</empty-content>
<b-skeleton animated v-else-if="$apollo.loading"></b-skeleton>
<router-link
<b-button
class="self-center my-2"
v-if="posts.total > 0"
tag="router-link"
type="is-text"
:to="{
name: RouteName.POSTS,
params: { preferredUsername: usernameWithDomain(group) },
}"
>{{ $t("View all posts") }}</router-link
>{{ $t("View all posts") }}</b-button
>
</section>
</div>
@ -806,25 +840,38 @@ export default class Group extends mixins(GroupMixin) {
}
async joinGroup(): Promise<void> {
const [group, currentActorId] = [
usernameWithDomain(this.group),
this.currentActor.id,
];
this.$apollo.mutate({
mutation: JOIN_GROUP,
variables: {
groupId: this.group.id,
},
refetchQueries: [
{
query: PERSON_STATUS_GROUP,
variables: {
id: currentActorId,
group,
},
if (!this.currentActor?.id) {
this.$router.push({
name: RouteName.GROUP_JOIN,
params: { preferredUsername: usernameWithDomain(this.group) },
});
return;
}
try {
const [group, currentActorId] = [
usernameWithDomain(this.group),
this.currentActor.id,
];
await this.$apollo.mutate({
mutation: JOIN_GROUP,
variables: {
groupId: this.group.id,
},
],
});
refetchQueries: [
{
query: PERSON_STATUS_GROUP,
variables: {
id: currentActorId,
group,
},
},
],
});
} catch (error: any) {
if (error.graphQLErrors && error.graphQLErrors.length > 0) {
this.$notifier.error(error.graphQLErrors[0].message);
}
}
}
protected async openLeaveGroupModal(): Promise<void> {
@ -870,6 +917,13 @@ export default class Group extends mixins(GroupMixin) {
}
async followGroup(): Promise<void> {
if (!this.currentActor?.id) {
this.$router.push({
name: RouteName.GROUP_FOLLOW,
params: { preferredUsername: usernameWithDomain(this.group) },
});
return;
}
try {
const [group, currentActorId] = [
usernameWithDomain(this.group),
@ -1088,6 +1142,41 @@ export default class Group extends mixins(GroupMixin) {
}),
};
}
get showFollowButton(): boolean {
return (
(!this.isCurrentActorFollowing || this.previewPublic) &&
this.currentActor?.id !== undefined
);
}
get showJoinButton(): boolean {
return (
(!this.isCurrentActorAGroupMember || this.previewPublic) &&
this.currentActor?.id !== undefined
);
}
get isGroupInviteOnly(): boolean {
return (
(!this.isCurrentActorAGroupMember || this.previewPublic) &&
this.group?.openness === Openness.INVITE_ONLY
);
}
get areGroupMembershipsModerated(): boolean {
return (
(!this.isCurrentActorAGroupMember || this.previewPublic) &&
this.group?.openness === Openness.MODERATED
);
}
get doesGroupManuallyApprovesFollowers(): boolean {
return (
(!this.isCurrentActorAGroupMember || this.previewPublic) &&
this.group?.manuallyApprovesFollowers
);
}
}
</script>
<style lang="scss" scoped>
@ -1380,4 +1469,7 @@ div.container {
height: 60vh;
width: 100%;
}
button.button.notification-button ::v-deep span.icon.is-small {
margin: 0 !important;
}
</style>

View File

@ -15,7 +15,9 @@
v-if="post.draft"
>{{ $t("Draft") }}</b-tag
>
<h1 class="title" :lang="post.language">{{ post.title }}</h1>
<h1 class="title text-3xl" :lang="post.language">
{{ post.title }}
</h1>
</div>
<p class="metadata">
<router-link
@ -441,7 +443,6 @@ article.post {
h1.title {
margin: 0;
font-weight: 500;
font-size: 38px;
font-family: "Roboto", "Helvetica", "Arial", serif;
}

View File

@ -67,7 +67,7 @@ exports[`CommentTree renders an empty comment tree 1`] = `
</article>
</form>
<transition-group-stub tag="div" name="comment-empty-list">
<empty-content-stub icon="comment" inline="true"><span>No comments yet</span></empty-content-stub>
<empty-content-stub icon="comment" descriptionclasses="" inline="true"><span>No comments yet</span></empty-content-stub>
</transition-group-stub>
</div>
`;

View File

@ -3,7 +3,7 @@
exports[`PostListItem renders post list item with basic informations 1`] = `
<a href="/p/my-blog-post-some-uuid" class="post-minimalist-card-wrapper" dir="auto">
<!---->
<div class="title-info-wrapper has-text-grey-dark">
<div class="title-info-wrapper has-text-grey-dark px-1">
<h3 lang="en" class="post-minimalist-title">
My Blog Post
</h3>
@ -17,7 +17,7 @@ exports[`PostListItem renders post list item with basic informations 1`] = `
exports[`PostListItem renders post list item with publisher name 1`] = `
<a href="/p/my-blog-post-some-uuid" class="post-minimalist-card-wrapper" dir="auto">
<!---->
<div class="title-info-wrapper has-text-grey-dark">
<div class="title-info-wrapper has-text-grey-dark px-1">
<h3 lang="en" class="post-minimalist-title">
My Blog Post
</h3>
@ -31,7 +31,7 @@ exports[`PostListItem renders post list item with publisher name 1`] = `
exports[`PostListItem renders post list item with tags 1`] = `
<a href="/p/my-blog-post-some-uuid" class="post-minimalist-card-wrapper" dir="auto">
<!---->
<div class="title-info-wrapper has-text-grey-dark">
<div class="title-info-wrapper has-text-grey-dark px-1">
<h3 lang="en" class="post-minimalist-title">
My Blog Post
</h3>

View File

@ -1297,9 +1297,9 @@
integrity sha512-Fg32GrJo61m+VqYSdRSjRXMjQ06j8YIYfcTqndLYVAaHmroZHLJZCydsWBOTDqXS2v+mjxohBWEMfg97GXmYQg==
"@jridgewell/trace-mapping@^0.3.0":
version "0.3.4"
resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.4.tgz#f6a0832dffd5b8a6aaa633b7d9f8e8e94c83a0c3"
integrity sha512-vFv9ttIedivx0ux3QSjhgtCVjPZd5l46ZOMDSCwnH1yUO2e964gO8LZGyv2QkqcgR6TnBU1v+1IFqmeoG+0UJQ==
version "0.3.7"
resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.7.tgz#941982134e9b7fad031c857ccfc4a0634fc6a471"
integrity sha512-8XC0l0PwCbdg2Uc8zIIf6djNX3lYiz9GqQlC1LJ9WQvTYvcfP8IA9K2IKRnPm5tAX6X/+orF+WwKZ0doGcgJlg==
dependencies:
"@jridgewell/resolve-uri" "^3.0.3"
"@jridgewell/sourcemap-codec" "^1.4.10"
@ -1929,9 +1929,9 @@
"@types/geojson" "*"
"@types/lodash@^4.14.141":
version "4.14.181"
resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.181.tgz#d1d3740c379fda17ab175165ba04e2d03389385d"
integrity sha512-n3tyKthHJbkiWhDZs3DkhkCzt2MexYHXlX0td5iMplyfwketaOeKboEVBqzceH7juqvEg3q5oUoBFxSLu7zFag==
version "4.14.182"
resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.182.tgz#05301a4d5e62963227eaafe0ce04dd77c54ea5c2"
integrity sha512-/THyiqyQAP9AfARo4pF+aCGcyiQ94tX/Is2I7HofNRqoYLgN1PBoOWu2/zTA5zMxzP5EFutMtWtGAFRKUe961Q==
"@types/mime@^1":
version "1.3.2"
@ -1949,9 +1949,9 @@
integrity sha512-rr20mmx41OkWx4q5du2dv2sESR/6xH2tzScUQXwO8SiaQWa6PYTuan1nqBtA76FR9qkVfZY7nwQwZNC9StX/Ww==
"@types/node@*":
version "17.0.24"
resolved "https://registry.yarnpkg.com/@types/node/-/node-17.0.24.tgz#20ba1bf69c1b4ab405c7a01e950c4f446b05029f"
integrity sha512-aveCYRQbgTH9Pssp1voEP7HiuWlD2jW2BO56w+bVrJn04i61yh6mRfoKO6hEYQD9vF+W8Chkwc6j1M36uPkx4g==
version "17.0.25"
resolved "https://registry.yarnpkg.com/@types/node/-/node-17.0.25.tgz#527051f3c2f77aa52e5dc74e45a3da5fb2301448"
integrity sha512-wANk6fBrUwdpY4isjWrKTufkrXdu1D2YHCot2fD/DfWxF5sMrVSA+KN7ydckvaTCh0HiqX9IVl0L5/ZoXg5M7w==
"@types/normalize-package-data@^2.4.0":
version "2.4.1"
@ -2165,13 +2165,13 @@
"@types/yargs-parser" "*"
"@typescript-eslint/eslint-plugin@^5.0.0", "@typescript-eslint/eslint-plugin@^5.3.0":
version "5.19.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.19.0.tgz#9608a4b6d0427104bccf132f058cba629a6553c0"
integrity sha512-w59GpFqDYGnWFim9p6TGJz7a3qWeENJuAKCqjGSx+Hq/bwq3RZwXYqy98KIfN85yDqz9mq6QXiY5h0FjGQLyEg==
version "5.20.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.20.0.tgz#022531a639640ff3faafaf251d1ce00a2ef000a1"
integrity sha512-fapGzoxilCn3sBtC6NtXZX6+P/Hef7VDbyfGqTTpzYydwhlkevB+0vE0EnmHPVTVSy68GUncyJ/2PcrFBeCo5Q==
dependencies:
"@typescript-eslint/scope-manager" "5.19.0"
"@typescript-eslint/type-utils" "5.19.0"
"@typescript-eslint/utils" "5.19.0"
"@typescript-eslint/scope-manager" "5.20.0"
"@typescript-eslint/type-utils" "5.20.0"
"@typescript-eslint/utils" "5.20.0"
debug "^4.3.2"
functional-red-black-tree "^1.0.1"
ignore "^5.1.8"
@ -2202,29 +2202,29 @@
eslint-visitor-keys "^1.1.0"
"@typescript-eslint/parser@^5.0.0", "@typescript-eslint/parser@^5.3.0":
version "5.19.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-5.19.0.tgz#05e587c1492868929b931afa0cb5579b0f728e75"
integrity sha512-yhktJjMCJX8BSBczh1F/uY8wGRYrBeyn84kH6oyqdIJwTGKmzX5Qiq49LRQ0Jh0LXnWijEziSo6BRqny8nqLVQ==
version "5.20.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-5.20.0.tgz#4991c4ee0344315c2afc2a62f156565f689c8d0b"
integrity sha512-UWKibrCZQCYvobmu3/N8TWbEeo/EPQbS41Ux1F9XqPzGuV7pfg6n50ZrFo6hryynD8qOTTfLHtHjjdQtxJ0h/w==
dependencies:
"@typescript-eslint/scope-manager" "5.19.0"
"@typescript-eslint/types" "5.19.0"
"@typescript-eslint/typescript-estree" "5.19.0"
"@typescript-eslint/scope-manager" "5.20.0"
"@typescript-eslint/types" "5.20.0"
"@typescript-eslint/typescript-estree" "5.20.0"
debug "^4.3.2"
"@typescript-eslint/scope-manager@5.19.0":
version "5.19.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-5.19.0.tgz#97e59b0bcbcb54dbcdfba96fc103b9020bbe9cb4"
integrity sha512-Fz+VrjLmwq5fbQn5W7cIJZ066HxLMKvDEmf4eu1tZ8O956aoX45jAuBB76miAECMTODyUxH61AQM7q4/GOMQ5g==
"@typescript-eslint/scope-manager@5.20.0":
version "5.20.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-5.20.0.tgz#79c7fb8598d2942e45b3c881ced95319818c7980"
integrity sha512-h9KtuPZ4D/JuX7rpp1iKg3zOH0WNEa+ZIXwpW/KWmEFDxlA/HSfCMhiyF1HS/drTICjIbpA6OqkAhrP/zkCStg==
dependencies:
"@typescript-eslint/types" "5.19.0"
"@typescript-eslint/visitor-keys" "5.19.0"
"@typescript-eslint/types" "5.20.0"
"@typescript-eslint/visitor-keys" "5.20.0"
"@typescript-eslint/type-utils@5.19.0":
version "5.19.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-5.19.0.tgz#80f2125b0dfe82494bbae1ea99f1c0186d420282"
integrity sha512-O6XQ4RI4rQcBGshTQAYBUIGsKqrKeuIOz9v8bckXZnSeXjn/1+BDZndHLe10UplQeJLXDNbaZYrAytKNQO2T4Q==
"@typescript-eslint/type-utils@5.20.0":
version "5.20.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-5.20.0.tgz#151c21cbe9a378a34685735036e5ddfc00223be3"
integrity sha512-WxNrCwYB3N/m8ceyoGCgbLmuZwupvzN0rE8NBuwnl7APgjv24ZJIjkNzoFBXPRCGzLNkoU/WfanW0exvp/+3Iw==
dependencies:
"@typescript-eslint/utils" "5.19.0"
"@typescript-eslint/utils" "5.20.0"
debug "^4.3.2"
tsutils "^3.21.0"
@ -2233,10 +2233,10 @@
resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-3.10.1.tgz#1d7463fa7c32d8a23ab508a803ca2fe26e758727"
integrity sha512-+3+FCUJIahE9q0lDi1WleYzjCwJs5hIsbugIgnbB+dSCYUxl8L6PwmsyOPFZde2hc1DlTo/xnkOgiTLSyAbHiQ==
"@typescript-eslint/types@5.19.0":
version "5.19.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.19.0.tgz#12d3d600d754259da771806ee8b2c842d3be8d12"
integrity sha512-zR1ithF4Iyq1wLwkDcT+qFnhs8L5VUtjgac212ftiOP/ZZUOCuuF2DeGiZZGQXGoHA50OreZqLH5NjDcDqn34w==
"@typescript-eslint/types@5.20.0":
version "5.20.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.20.0.tgz#fa39c3c2aa786568302318f1cb51fcf64258c20c"
integrity sha512-+d8wprF9GyvPwtoB4CxBAR/s0rpP25XKgnOvMf/gMXYDvlUC3rPFHupdTQ/ow9vn7UDe5rX02ovGYQbv/IUCbg==
"@typescript-eslint/typescript-estree@3.10.1":
version "3.10.1"
@ -2252,28 +2252,28 @@
semver "^7.3.2"
tsutils "^3.17.1"
"@typescript-eslint/typescript-estree@5.19.0":
version "5.19.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-5.19.0.tgz#fc987b8f62883f9ea6a5b488bdbcd20d33c0025f"
integrity sha512-dRPuD4ocXdaE1BM/dNR21elSEUPKaWgowCA0bqJ6YbYkvtrPVEvZ+zqcX5a8ECYn3q5iBSSUcBBD42ubaOp0Hw==
"@typescript-eslint/typescript-estree@5.20.0":
version "5.20.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-5.20.0.tgz#ab73686ab18c8781bbf249c9459a55dc9417d6b0"
integrity sha512-36xLjP/+bXusLMrT9fMMYy1KJAGgHhlER2TqpUVDYUQg4w0q/NW/sg4UGAgVwAqb8V4zYg43KMUpM8vV2lve6w==
dependencies:
"@typescript-eslint/types" "5.19.0"
"@typescript-eslint/visitor-keys" "5.19.0"
"@typescript-eslint/types" "5.20.0"
"@typescript-eslint/visitor-keys" "5.20.0"
debug "^4.3.2"
globby "^11.0.4"
is-glob "^4.0.3"
semver "^7.3.5"
tsutils "^3.21.0"
"@typescript-eslint/utils@5.19.0":
version "5.19.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-5.19.0.tgz#fe87f1e3003d9973ec361ed10d36b4342f1ded1e"
integrity sha512-ZuEckdupXpXamKvFz/Ql8YnePh2ZWcwz7APICzJL985Rp5C2AYcHO62oJzIqNhAMtMK6XvrlBTZeNG8n7gS3lQ==
"@typescript-eslint/utils@5.20.0":
version "5.20.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-5.20.0.tgz#b8e959ed11eca1b2d5414e12417fd94cae3517a5"
integrity sha512-lHONGJL1LIO12Ujyx8L8xKbwWSkoUKFSO+0wDAqGXiudWB2EO7WEUT+YZLtVbmOmSllAjLb9tpoIPwpRe5Tn6w==
dependencies:
"@types/json-schema" "^7.0.9"
"@typescript-eslint/scope-manager" "5.19.0"
"@typescript-eslint/types" "5.19.0"
"@typescript-eslint/typescript-estree" "5.19.0"
"@typescript-eslint/scope-manager" "5.20.0"
"@typescript-eslint/types" "5.20.0"
"@typescript-eslint/typescript-estree" "5.20.0"
eslint-scope "^5.1.1"
eslint-utils "^3.0.0"
@ -2284,12 +2284,12 @@
dependencies:
eslint-visitor-keys "^1.1.0"
"@typescript-eslint/visitor-keys@5.19.0":
version "5.19.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-5.19.0.tgz#c84ebc7f6c744707a361ca5ec7f7f64cd85b8af6"
integrity sha512-Ym7zZoMDZcAKWsULi2s7UMLREdVQdScPQ/fKWMYefarCztWlHPFVJo8racf8R0Gc8FAEJ2eD4of8As1oFtnQlQ==
"@typescript-eslint/visitor-keys@5.20.0":
version "5.20.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-5.20.0.tgz#70236b5c6b67fbaf8b2f58bf3414b76c1e826c2a"
integrity sha512-1flRpNF+0CAQkMNlTJ6L/Z5jiODG/e5+7mk6XwtPOUS3UrTz3UOiAg9jG2VtKsWI6rZQfy4C6a232QNRZTRGlg==
dependencies:
"@typescript-eslint/types" "5.19.0"
"@typescript-eslint/types" "5.20.0"
eslint-visitor-keys "^3.0.0"
"@vue-a11y/announcer@^2.1.0":
@ -3126,11 +3126,6 @@ astral-regex@^2.0.0:
resolved "https://registry.yarnpkg.com/astral-regex/-/astral-regex-2.0.0.tgz#483143c567aeed4785759c0865786dc77d7d2e31"
integrity sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==
async@0.9.x:
version "0.9.2"
resolved "https://registry.yarnpkg.com/async/-/async-0.9.2.tgz#aea74d5e61c1f899613bf64bda66d4c78f2fd17d"
integrity sha1-rqdNXmHB+JlhO/ZL2mbUx48v0X0=
async@^2.6.2:
version "2.6.4"
resolved "https://registry.yarnpkg.com/async/-/async-2.6.4.tgz#706b7ff6084664cd7eae713f6f965433b5504221"
@ -3138,6 +3133,11 @@ async@^2.6.2:
dependencies:
lodash "^4.17.14"
async@^3.2.3:
version "3.2.3"
resolved "https://registry.yarnpkg.com/async/-/async-3.2.3.tgz#ac53dafd3f4720ee9e8a160628f18ea91df196c9"
integrity sha512-spZRyzKL5l5BZQrr/6m/SqFdBN0q3OCI0f9rjfBzCMBIP4p75P620rR3gTmaksNOhmzgdxcaxdNfMy6anrbM0g==
asynckit@^0.4.0:
version "0.4.0"
resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79"
@ -3180,9 +3180,9 @@ babel-jest@^27.1.0, babel-jest@^27.5.1:
slash "^3.0.0"
babel-loader@^8.2.2:
version "8.2.4"
resolved "https://registry.yarnpkg.com/babel-loader/-/babel-loader-8.2.4.tgz#95f5023c791b2e9e2ca6f67b0984f39c82ff384b"
integrity sha512-8dytA3gcvPPPv4Grjhnt8b5IIiTcq/zeXOPk4iTYI0SVXcsmuGg7JtBRDp8S9X+gJfhQ8ektjXZlDu1Bb33U8A==
version "8.2.5"
resolved "https://registry.yarnpkg.com/babel-loader/-/babel-loader-8.2.5.tgz#d45f585e654d5a5d90f5350a779d7647c5ed512e"
integrity sha512-OSiFfH89LrEMiWd4pLNqGz4CwJDtbs2ZVc+iGu2HrkRfPxId9F2anQj38IxWpmRfsUY0aBZYi1EFcd3mhtRMLQ==
dependencies:
find-cache-dir "^3.3.1"
loader-utils "^2.0.0"
@ -3372,6 +3372,13 @@ brace-expansion@^1.1.7:
balanced-match "^1.0.0"
concat-map "0.0.1"
brace-expansion@^2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-2.0.1.tgz#1edc459e0f0c548486ecf9fc99f2221364b9a0ae"
integrity sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==
dependencies:
balanced-match "^1.0.0"
braces@^3.0.2, braces@~3.0.2:
version "3.0.2"
resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107"
@ -3848,9 +3855,9 @@ copy-webpack-plugin@^9.0.1:
serialize-javascript "^6.0.0"
core-js-compat@^3.20.2, core-js-compat@^3.21.0, core-js-compat@^3.8.3:
version "3.22.0"
resolved "https://registry.yarnpkg.com/core-js-compat/-/core-js-compat-3.22.0.tgz#7ce17ab57c378be2c717c7c8ed8f82a50a25b3e4"
integrity sha512-WwA7xbfRGrk8BGaaHlakauVXrlYmAIkk8PNGb1FDQS+Rbrewc3pgFfwJFRw6psmJVAll7Px9UHRYE16oRQnwAQ==
version "3.22.1"
resolved "https://registry.yarnpkg.com/core-js-compat/-/core-js-compat-3.22.1.tgz#47b9c5e79efbf13935f637449fa1cdec8cd9515f"
integrity sha512-CWbNqTluLMvZg1cjsQUbGiCM91dobSHKfDIyCoxuqxthdjGuUlaMbCsSehP3CBiVvG0C7P6UIrC1v0hgFE75jw==
dependencies:
browserslist "^4.20.2"
semver "7.0.0"
@ -3866,9 +3873,9 @@ core-js@^2.4.0, core-js@^2.5.0:
integrity sha512-Kb2wC0fvsWfQrgk8HU5lW6U/Lcs8+9aaYcy4ZFc6DDlo4nZ7n70dEgE5rtR0oG6ufKDUnrwfWL1mXR5ljDatrQ==
core-js@^3.6.4, core-js@^3.8.3:
version "3.22.0"
resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.22.0.tgz#b52007870c5e091517352e833b77f0b2d2b259f3"
integrity sha512-8h9jBweRjMiY+ORO7bdWSeWfHhLPO7whobj7Z2Bl0IDo00C228EdGgH7FE4jGumbEjzcFfkfW8bXgdkEDhnwHQ==
version "3.22.1"
resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.22.1.tgz#1936e4f1da82675fe22ae10ee60ef638cd9752fd"
integrity sha512-l6CwCLq7XgITOQGhv1dIUmwCFoqFjyQ6zQHUCQlS0xKmb9d6OHIg8jDiEoswhaettT21BSF5qKr6kbvE+aKwxw==
core-util-is@~1.0.0:
version "1.0.3"
@ -4371,9 +4378,9 @@ ejs@^3.1.6:
jake "^10.6.1"
electron-to-chromium@^1.4.84:
version "1.4.111"
resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.111.tgz#897613f6504f3f17c9381c7499a635b413e4df4e"
integrity sha512-/s3+fwhKf1YK4k7btOImOzCQLpUjS6MaPf0ODTNuT4eTM1Bg4itBpLkydhOzJmpmH6Z9eXFyuuK5czsmzRzwtw==
version "1.4.114"
resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.114.tgz#d85ec0808dd50b0cf6e6b262480ffd385f71c873"
integrity sha512-gRwLpVYWHGbERPU6o8pKfR168V6enWEXzZc6zQNNXbgJ7UJna+9qzAIHY94+9KOv71D/CH+QebLA9pChD2q8zA==
emittery@^0.8.1:
version "0.8.1"
@ -4994,11 +5001,11 @@ file-entry-cache@^6.0.1:
flat-cache "^3.0.4"
filelist@^1.0.1:
version "1.0.2"
resolved "https://registry.yarnpkg.com/filelist/-/filelist-1.0.2.tgz#80202f21462d4d1c2e214119b1807c1bc0380e5b"
integrity sha512-z7O0IS8Plc39rTCq6i6iHxk43duYOn8uFJiWSewIq0Bww1RNybVHSCjahmcC87ZqAm4OTvFzlzeGu3XAzG1ctQ==
version "1.0.3"
resolved "https://registry.yarnpkg.com/filelist/-/filelist-1.0.3.tgz#448607750376484932f67ef1b9ff07386b036c83"
integrity sha512-LwjCsruLWQULGYKy7TX0OPtrL9kLpojOFKc5VCTxdFTV7w5zbsgqVKfnkKG7Qgjtq50gKfO56hJv88OfcGb70Q==
dependencies:
minimatch "^3.0.4"
minimatch "^5.0.1"
fill-range@^7.0.1:
version "7.0.1"
@ -5156,9 +5163,9 @@ functional-red-black-tree@^1.0.1:
integrity sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc=
functions-have-names@^1.2.2:
version "1.2.2"
resolved "https://registry.yarnpkg.com/functions-have-names/-/functions-have-names-1.2.2.tgz#98d93991c39da9361f8e50b337c4f6e41f120e21"
integrity sha512-bLgc3asbWdwPbx2mNk2S49kmJCuQeu0nfmaOgbs8WIyzzkw3r4htszdIi9Q9EMezDPTYuJx2wvjZ/EwgAthpnA==
version "1.2.3"
resolved "https://registry.yarnpkg.com/functions-have-names/-/functions-have-names-1.2.3.tgz#0404fe4ee2ba2f607f0e0ec3c80bae994133b834"
integrity sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==
gensync@^1.0.0-beta.2:
version "1.0.0-beta.2"
@ -5318,9 +5325,9 @@ has-ansi@^2.0.0:
ansi-regex "^2.0.0"
has-bigints@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/has-bigints/-/has-bigints-1.0.1.tgz#64fe6acb020673e3b78db035a5af69aa9d07b113"
integrity sha512-LSBS2LjbNBTf6287JEbEzvJgftkF5qFkmCo9hDRpAzKhUOlJ+hx8dd4USs00SgsUNwc4617J9ki5YtEClM2ffA==
version "1.0.2"
resolved "https://registry.yarnpkg.com/has-bigints/-/has-bigints-1.0.2.tgz#0871bd3e3d51626f6ca0966668ba35d5602d6eaa"
integrity sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==
has-flag@^3.0.0:
version "3.0.0"
@ -5502,9 +5509,9 @@ http-proxy-agent@^4.0.1:
debug "4"
http-proxy-middleware@^2.0.3:
version "2.0.4"
resolved "https://registry.yarnpkg.com/http-proxy-middleware/-/http-proxy-middleware-2.0.4.tgz#03af0f4676d172ae775cb5c33f592f40e1a4e07a"
integrity sha512-m/4FxX17SUvz4lJ5WPXOHDUuCwIqXLfLHs1s0uZ3oYjhoXlx9csYxaOa0ElDEJ+h8Q4iJ1s+lTMbiCa4EXIJqg==
version "2.0.5"
resolved "https://registry.yarnpkg.com/http-proxy-middleware/-/http-proxy-middleware-2.0.5.tgz#2d12fb41a414889372643a1f54279a2f6470aa93"
integrity sha512-ORErEaxkjyrhifofwCuQttHPUSestLtiPDwV0qQOFB0ww6695H953wIGRnkakw1K+GAP+t8/RPbfDB75RFL4Fg==
dependencies:
"@types/http-proxy" "^1.17.8"
http-proxy "^1.18.1"
@ -5694,9 +5701,9 @@ is-ci@^1.0.10:
ci-info "^1.5.0"
is-core-module@^2.8.1:
version "2.8.1"
resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.8.1.tgz#f59fdfca701d5879d0a6b100a40aa1560ce27211"
integrity sha512-SdNCUs284hr40hFTFP6l0IfZ/RSrMXF3qgoRHd3/79unUTvrFO/JoXwkGm+5J/Oe3E/b5GsnG330uUNgRpu1PA==
version "2.9.0"
resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.9.0.tgz#e1c34429cd51c6dd9e09e0799e396e27b19a9c69"
integrity sha512-+5FPy5PnwmO3lvfMb0AsoPaBG+5KHUI0wYFXOtYPnVVVspTFUuMZNfNaNVRt3FZadstu2c8x23vykRW/NBoU6A==
dependencies:
has "^1.0.3"
@ -5946,11 +5953,11 @@ iterall@^1.2.2:
integrity sha512-QZ9qOMdF+QLHxy1QIpUHUU1D5pS2CG2P69LF6L6CPjPYA/XMOmKV3PZpawHoAjHNyB0swdVTRxdYT4tbBbxqwg==
jake@^10.6.1:
version "10.8.4"
resolved "https://registry.yarnpkg.com/jake/-/jake-10.8.4.tgz#f6a8b7bf90c6306f768aa82bb7b98bf4ca15e84a"
integrity sha512-MtWeTkl1qGsWUtbl/Jsca/8xSoK3x0UmS82sNbjqxxG/de/M/3b1DntdjHgPMC50enlTNwXOCRqPXLLt5cCfZA==
version "10.8.5"
resolved "https://registry.yarnpkg.com/jake/-/jake-10.8.5.tgz#f2183d2c59382cb274226034543b9c03b8164c46"
integrity sha512-sVpxYeuAhWt0OTWITwT98oyV0GsXyMlXCF+3L1SuafBVUIr/uILGRB+NqwkzhgXKvoJpDIpQvqkUALgdmQsQxw==
dependencies:
async "0.9.x"
async "^3.2.3"
chalk "^4.0.2"
filelist "^1.0.1"
minimatch "^3.0.4"
@ -6913,6 +6920,13 @@ minimatch@^3.0.4, minimatch@^3.1.2:
dependencies:
brace-expansion "^1.1.7"
minimatch@^5.0.1:
version "5.0.1"
resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-5.0.1.tgz#fb9022f7528125187c92bd9e9b6366be1cf3415b"
integrity sha512-nLDxIFRyhDblz3qMuq+SoRZED4+miJ/G+tdDrjkkkRnjAsBexeGpgjLEQ0blJy7rHhR2b93rhQY4SvyWu9v03g==
dependencies:
brace-expansion "^2.0.1"
minimist@^1.1.1, minimist@^1.2.0, minimist@^1.2.5, minimist@^1.2.6:
version "1.2.6"
resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.6.tgz#8637a5b759ea0d6e98702cfb3a9283323c93af44"
@ -8338,9 +8352,9 @@ sass-loader@^12.0.0:
neo-async "^2.6.2"
sass@^1.34.1:
version "1.50.0"
resolved "https://registry.yarnpkg.com/sass/-/sass-1.50.0.tgz#3e407e2ebc53b12f1e35ce45efb226ea6063c7c8"
integrity sha512-cLsD6MEZ5URXHStxApajEh7gW189kkjn4Rc8DQweMyF+o5HF5nfEz8QYLMlPsTOD88DknatTmBWkOcw5/LnJLQ==
version "1.50.1"
resolved "https://registry.yarnpkg.com/sass/-/sass-1.50.1.tgz#e9b078a1748863013c4712d2466ce8ca4e4ed292"
integrity sha512-noTnY41KnlW2A9P8sdwESpDmo+KBNkukI1i8+hOK3footBUcohNHtdOJbckp46XO95nuvcHDDZ+4tmOnpK3hjw==
dependencies:
chokidar ">=3.0.0 <4.0.0"
immutable "^4.0.0"

View File

@ -28,7 +28,7 @@ defmodule Mobilizon.Federation.ActivityPub.Actions.Update do
Logger.debug("updating an activity")
Logger.debug(inspect(args))
case Managable.update(old_entity, args, additional) do
case Managable.update(old_entity, args, Map.put(additional, :local, local)) do
{:ok, entity, update_data} ->
{:ok, activity} = create_activity(update_data, local)
maybe_federate(activity)

View File

@ -8,7 +8,9 @@ defmodule Mobilizon.Federation.ActivityPub.Publisher do
alias Mobilizon.Federation.ActivityPub.{Activity, Federator, Relay, Transmogrifier, Visibility}
alias Mobilizon.Federation.HTTPSignatures.Signature
require Logger
import Mobilizon.Federation.ActivityPub.Utils, only: [remote_actors: 1]
import Mobilizon.Federation.ActivityPub.Utils,
only: [remote_actors: 1, create_full_domain_string: 1]
@doc """
Publish an activity to all appropriated audiences inboxes
@ -77,7 +79,7 @@ defmodule Mobilizon.Federation.ActivityPub.Publisher do
Tesla.Env.result()
def publish_one(%{inbox: inbox, json: json, actor: actor, id: id}) do
Logger.info("Federating #{id} to #{inbox}")
%URI{host: host, path: path} = URI.parse(inbox)
%URI{path: path} = uri = URI.new!(inbox)
digest = Signature.build_digest(json)
date = Signature.generate_date_header()
@ -87,7 +89,7 @@ defmodule Mobilizon.Federation.ActivityPub.Publisher do
signature =
Signature.sign(actor, %{
"(request-target)": "post #{path}",
host: host,
host: create_full_domain_string(uri),
"content-length": byte_size(json),
digest: digest,
date: date

View File

@ -10,6 +10,8 @@ defmodule Mobilizon.Federation.ActivityPub.Refresher do
alias Mobilizon.Federation.ActivityPub.{Fetcher, Relay, Transmogrifier, Utils}
require Logger
@collection_element_task_processing_time 60_000
@doc """
Refresh a remote profile
"""
@ -158,7 +160,7 @@ defmodule Mobilizon.Federation.ActivityPub.Refresher do
items
|> Enum.map(fn item -> Task.async(fn -> handling_element(item) end) end)
|> Task.await_many()
|> Task.await_many(@collection_element_task_processing_time)
Logger.debug("Finished processing a collection")
:ok

View File

@ -14,9 +14,9 @@ defmodule Mobilizon.Federation.ActivityPub.Relay do
alias Mobilizon.Federation.ActivityPub.{Actions, Activity, Transmogrifier}
alias Mobilizon.Federation.ActivityPub.Actor, as: ActivityPubActor
alias Mobilizon.Federation.WebFinger
alias Mobilizon.Service.Workers.Background
alias Mobilizon.GraphQL.API.Follows
alias Mobilizon.Service.Workers.Background
import Mobilizon.Federation.ActivityPub.Utils, only: [create_full_domain_string: 1]
require Logger
@ -172,14 +172,14 @@ defmodule Mobilizon.Federation.ActivityPub.Relay do
defp fetch_actor("http://" <> address), do: fetch_actor(address)
defp fetch_actor(address) do
%URI{host: host} = URI.parse("http://" <> address)
%URI{host: host} = uri = URI.parse("http://" <> address)
cond do
String.contains?(address, "@") ->
check_actor(address)
!is_nil(host) ->
check_actor("relay@#{host}")
uri |> create_full_domain_string() |> check_actor()
true ->
{:error, :bad_url}

View File

@ -41,7 +41,8 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier do
params = %{
reporter_id: params["reporter"].id,
reported_id: params["reported"].id,
comments_ids: params["comments"] |> Enum.map(& &1.id),
comments_ids:
if(params["comments"], do: params["comments"] |> Enum.map(& &1.id), else: []),
content: params["content"] || "",
additional: %{
"cc" => [params["reported"].url]
@ -406,6 +407,13 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier do
Actions.Update.update(old_actor, object_data, false, %{updater_actor: author}) do
{:ok, activity, new_actor}
else
{:error, :update_not_allowed} ->
Logger.warn("Activity tried to update an actor that's local or not a group",
activity: params
)
:error
e ->
Sentry.capture_message("Error while handling an Update activity",
extra: %{params: params}
@ -614,19 +622,25 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier do
{:error, :unknown_actor}
{:ok, %Actor{} = actor} ->
case is_group_object_gone(object_id) do
{:ok, object} ->
if Utils.origin_check_from_id?(actor_url, object_id) ||
Permission.can_delete_group_object?(actor, object) do
Actions.Delete.delete(object, actor, false)
else
Logger.warn("Object origin check failed")
:error
end
# If the actor itself is being deleted, no need to check anything other than the object being remote
if remote_actor_is_being_deleted(data) do
Actions.Delete.delete(actor, actor, false)
else
case is_group_object_gone(object_id) do
# The group object is no longer there, we can remove the element
{:ok, entity} ->
if Utils.origin_check_from_id?(actor_url, object_id) ||
Permission.can_delete_group_object?(actor, entity) do
Actions.Delete.delete(entity, actor, false)
else
Logger.warn("Object origin check failed")
:error
end
{:error, err} ->
Logger.debug(inspect(err))
{:error, err}
{:error, err} ->
Logger.debug(inspect(err))
{:error, err}
end
end
end
end
@ -1207,4 +1221,9 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier do
moderator.domain == group.domain
end
end
defp remote_actor_is_being_deleted(%{"object" => object} = data) do
object_id = Utils.get_url(object)
Utils.get_actor(data) == object_id and not Utils.are_same_origin?(object_id, Endpoint.url())
end
end

View File

@ -45,25 +45,30 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Actors do
def update(%Actor{} = old_actor, args, additional) do
updater_actor = Map.get(args, :updater_actor) || Map.get(additional, :updater_actor)
case Actors.update_actor(old_actor, args) do
{:ok, %Actor{} = new_actor} ->
GroupActivity.insert_activity(new_actor,
subject: "group_updated",
old_group: old_actor,
updater_actor: updater_actor
)
if Map.get(additional, :local, false) == true or not match?(%Actor{domain: nil}, old_actor) or
match?(%Actor{type: :Group}, old_actor) do
case Actors.update_actor(old_actor, args) do
{:ok, %Actor{} = new_actor} ->
GroupActivity.insert_activity(new_actor,
subject: "group_updated",
old_group: old_actor,
updater_actor: updater_actor
)
actor_as_data = Convertible.model_to_as(new_actor)
Cachex.del(:activity_pub, "actor_#{new_actor.preferred_username}")
audience = Audience.get_audience(new_actor)
actor_as_data = Convertible.model_to_as(new_actor)
Cachex.del(:activity_pub, "actor_#{new_actor.preferred_username}")
audience = Audience.get_audience(new_actor)
additional = Map.merge(additional, %{"actor" => (updater_actor || old_actor).url})
additional = Map.merge(additional, %{"actor" => (updater_actor || old_actor).url})
update_data = make_update_data(actor_as_data, Map.merge(audience, additional))
{:ok, new_actor, update_data}
update_data = make_update_data(actor_as_data, Map.merge(audience, additional))
{:ok, new_actor, update_data}
{:error, %Ecto.Changeset{} = err} ->
{:error, err}
{:error, %Ecto.Changeset{} = err} ->
{:error, err}
end
else
{:error, :update_not_allowed}
end
end

View File

@ -672,8 +672,15 @@ defmodule Mobilizon.Federation.ActivityPub.Utils do
@doc """
Converts PEM encoded keys to a public key representation
"""
@spec pem_to_public_key_pem(String.t()) :: String.t()
def pem_to_public_key_pem(pem) do
public_key = pem_to_public_key(pem)
public_key = :public_key.pem_entry_encode(:RSAPublicKey, public_key)
:public_key.pem_encode([public_key])
end
@spec pem_to_public_key(String.t()) :: {:RSAPublicKey, any(), any()}
def pem_to_public_key(pem) do
defp pem_to_public_key(pem) do
[key_code] = :public_key.pem_decode(pem)
key = :public_key.pem_entry_decode(key_code)
@ -686,14 +693,8 @@ defmodule Mobilizon.Federation.ActivityPub.Utils do
end
end
@spec pem_to_public_key_pem(String.t()) :: String.t()
def pem_to_public_key_pem(pem) do
public_key = pem_to_public_key(pem)
public_key = :public_key.pem_entry_encode(:RSAPublicKey, public_key)
:public_key.pem_encode([public_key])
end
def make_signature(actor, id, date) do
@spec make_signature(Actor.t(), String.t(), DateTime.t()) :: list({atom(), String.t()})
defp make_signature(actor, id, date) do
uri = URI.parse(id)
signature =
@ -780,4 +781,16 @@ defmodule Mobilizon.Federation.ActivityPub.Utils do
params
end
end
@schemes_with_no_port ["http", "https"]
def create_full_domain_string(%URI{host: host, port: nil}), do: host
def create_full_domain_string(%URI{host: host, port: port}) do
if port in Enum.map(@schemes_with_no_port, &URI.default_port/1) do
host
else
"#{host}:#{port}"
end
end
end

View File

@ -17,6 +17,7 @@ defmodule Mobilizon.Federation.ActivityStream.Converter.Actor do
alias Mobilizon.Service.RichMedia.Parser
alias Mobilizon.Web.Upload
import Mobilizon.Federation.ActivityStream.Converter.Utils, only: [get_address: 1]
import Mobilizon.Federation.ActivityPub.Utils, only: [create_full_domain_string: 1]
@behaviour Converter
@ -54,7 +55,7 @@ defmodule Mobilizon.Federation.ActivityStream.Converter.Actor do
outbox_url: data["outbox"],
following_url: data["following"],
followers_url: data["followers"],
domain: URI.parse(data["id"]).host,
domain: data["id"] |> URI.new!() |> create_full_domain_string(),
manually_approves_followers: data["manuallyApprovesFollowers"],
type: data["type"],
visibility: if(Map.get(data, "discoverable", false) == true, do: :public, else: :unlisted),

View File

@ -67,33 +67,9 @@ defmodule Mobilizon.Federation.ActivityStream.Converter.Flag do
def as_to_model(%{"object" => objects} = object) do
with {:ok, %Actor{} = reporter} <-
ActivityPubActor.get_or_fetch_actor_by_url(object["actor"]),
%Actor{} = reported <-
Enum.reduce_while(objects, nil, fn url, _ ->
case ActivityPubActor.get_or_fetch_actor_by_url(url) do
{:ok, %Actor{} = actor} ->
{:halt, actor}
_ ->
{:cont, nil}
end
end),
event <-
Enum.reduce_while(objects, nil, fn url, _ ->
case Events.get_event_by_url(url) do
%Event{} = event ->
{:halt, event}
_ ->
{:cont, nil}
end
end),
# Remove the reported actor and the event from the object list.
comments <-
Enum.filter(objects, fn url ->
!(url == reported.url || (!is_nil(event) && event.url == url))
end),
comments <- Enum.map(comments, &Discussions.get_comment_from_url/1) do
%Actor{} = reported <- find_reported(objects),
event <- find_event(objects),
comments <- find_comments(objects, reported, event) do
%{
"reporter" => reporter,
"uri" => object["id"],
@ -104,4 +80,41 @@ defmodule Mobilizon.Federation.ActivityStream.Converter.Flag do
}
end
end
@spec find_reported(list(String.t())) :: Actor.t() | nil
defp find_reported(objects) do
Enum.reduce_while(objects, nil, fn url, _ ->
case ActivityPubActor.get_or_fetch_actor_by_url(url) do
{:ok, %Actor{} = actor} ->
{:halt, actor}
_ ->
{:cont, nil}
end
end)
end
# Remove the reported actor and the event from the object list.
@spec find_comments(list(String.t()), Actor.t() | nil, Event.t() | nil) :: list(Comment.t())
defp find_comments(objects, reported, event) do
objects
|> Enum.filter(fn url ->
!((!is_nil(reported) && url == reported.url) || (!is_nil(event) && event.url == url))
end)
|> Enum.map(&Discussions.get_comment_from_url/1)
|> Enum.filter(& &1)
end
@spec find_event(list(String.t())) :: Event.t() | nil
defp find_event(objects) do
Enum.reduce_while(objects, nil, fn url, _ ->
case Events.get_event_by_url(url) do
%Event{} = event ->
{:halt, event}
_ ->
{:cont, nil}
end
end)
end
end

View File

@ -34,6 +34,7 @@ defmodule Mobilizon.Federation.ActivityStream.Converter.Utils do
@spec fetch_mentions([map()]) :: [map()]
def fetch_mentions(mentions) when is_list(mentions) do
Logger.debug("fetching mentions")
Logger.debug(inspect(mentions))
Enum.reduce(mentions, [], fn mention, acc -> create_mention(mention, acc) end)
end

View File

@ -95,6 +95,9 @@ defmodule Mobilizon.Federation.HTTPSignatures.Signature do
actor_url = key_id_to_actor_url(kid)
Logger.debug("Refetching public key for #{actor_url}")
# In this specific case we don't sign object fetches because
# this would cause infinite recursion when servers both need
# to fetch each other's keys
with {:ok, %Actor{} = actor} <-
ActivityPubActor.make_actor_from_url(actor_url, ignore_sign_object_fetches: true) do
get_actor_public_key(actor)

View File

@ -129,6 +129,7 @@ defmodule Mobilizon.Federation.WebFinger do
| :address_invalid
| :http_error
| :webfinger_information_not_json
| :webfinger_information_not_valid
| :no_url_in_webfinger_data
@doc """
@ -164,7 +165,9 @@ defmodule Mobilizon.Federation.WebFinger do
end
@spec fetch_webfinger_data(String.t()) ::
{:ok, map()} | {:error, :webfinger_information_not_json | :http_error}
{:ok, map()}
| {:error,
:webfinger_information_not_json | :webfinger_information_not_valid | :http_error}
defp fetch_webfinger_data(address) do
Logger.debug("Calling WebfingerClient with #{inspect(address)}")
@ -202,9 +205,10 @@ defmodule Mobilizon.Federation.WebFinger do
{:ok, String.t()} | {:error, :link_not_found} | {:error, any()}
defp find_webfinger_endpoint(domain) when is_binary(domain) do
Logger.debug("Calling HostMetaClient for #{domain}")
prefix = if Application.fetch_env!(:mobilizon, :env) !== :dev, do: "https", else: "http"
with {:ok, %Tesla.Env{status: 200, body: body}} <-
HostMetaClient.get("https://#{domain}/.well-known/host-meta"),
HostMetaClient.get("#{prefix}://#{domain}/.well-known/host-meta"),
link_template when is_binary(link_template) <- find_link_from_template(body) do
{:ok, link_template}
else
@ -225,7 +229,8 @@ defmodule Mobilizon.Federation.WebFinger do
_ ->
Logger.debug("Using default webfinger location")
"https://#{domain}/.well-known/webfinger?resource=acct:#{actor}"
prefix = if Application.fetch_env!(:mobilizon, :env) !== :dev, do: "https", else: "http"
"#{prefix}://#{domain}/.well-known/webfinger?resource=acct:#{actor}"
end
end
end
@ -243,28 +248,36 @@ defmodule Mobilizon.Federation.WebFinger do
end
@spec webfinger_from_json(map() | String.t()) ::
{:ok, map()} | {:error, :webfinger_information_not_json}
{:ok, map()}
| {:error, :webfinger_information_not_json | :webfinger_information_not_valid}
defp webfinger_from_json(doc) when is_map(doc) do
data =
Enum.reduce(doc["links"], %{"subject" => doc["subject"]}, fn link, data ->
case {link["type"], link["rel"]} do
{"application/activity+json", "self"} ->
Map.put(data, "url", link["href"])
links = Map.get(doc, "links")
subject = Map.get(doc, "subject")
{nil, _rel} ->
Logger.debug("No type declared for the following link #{inspect(link)}")
data
if !is_nil(links) && !is_nil(subject) do
data =
Enum.reduce(links, %{"subject" => subject}, fn link, data ->
case {link["type"], link["rel"]} do
{"application/activity+json", "self"} ->
Map.put(data, "url", link["href"])
_ ->
Logger.debug(fn ->
"Unhandled type to finger: #{inspect(link["type"])}"
end)
{nil, _rel} ->
Logger.debug("No type declared for the following link #{inspect(link)}")
data
data
end
end)
_ ->
Logger.debug(fn ->
"Unhandled type to finger: #{inspect(link)}"
end)
{:ok, data}
data
end
end)
{:ok, data}
else
{:error, :webfinger_information_not_valid}
end
end
defp webfinger_from_json(_doc), do: {:error, :webfinger_information_not_json}

View File

@ -69,10 +69,6 @@ defmodule Mobilizon.GraphQL.API.Follows do
)
case Actors.check_follow(follower, followed) do
%Follower{approved: false} = follow ->
Actors.delete_follower(follow)
{:error, "Follow already rejected"}
%Follower{} = follow ->
Actions.Reject.reject(
:follow,

View File

@ -6,6 +6,8 @@ defmodule Mobilizon.Service.DateTime do
@typep to_string_format :: :short | :medium | :long | :full
@utc_timezone "Etc/UTC"
@spec datetime_to_string(DateTime.t(), String.t(), to_string_format()) :: String.t()
def datetime_to_string(%DateTime{} = datetime, locale \\ "en", format \\ :medium) do
Mobilizon.Cldr.DateTime.to_string!(datetime,
@ -75,7 +77,7 @@ defmodule Mobilizon.Service.DateTime do
def calculate_next_day_notification(%Date{} = day, options \\ []) do
compare_to = Keyword.get(options, :compare_to, DateTime.utc_now())
notification_time = Keyword.get(options, :notification_time, ~T[18:00:00])
timezone = Keyword.get(options, :timezone, "Etc/UTC") || "Etc/UTC"
timezone = options |> Keyword.get(:timezone, @utc_timezone) |> fallback_tz()
send_at = DateTime.new!(day, notification_time, timezone)
@ -145,7 +147,7 @@ defmodule Mobilizon.Service.DateTime do
@spec appropriate_first_day_of_week(DateTime.t(), keyword) :: DateTime.t() | nil
defp appropriate_first_day_of_week(%DateTime{} = datetime, options) do
locale = Keyword.get(options, :locale, "en")
timezone = Keyword.get(options, :timezone, "Etc/UTC")
timezone = options |> Keyword.get(:timezone, @utc_timezone) |> fallback_tz()
local_datetime = datetime_tz_convert(datetime, timezone)
@ -170,7 +172,7 @@ defmodule Mobilizon.Service.DateTime do
options
) do
notification_time = Keyword.get(options, :notification_time, ~T[08:00:00])
timezone = Keyword.get(options, :timezone, "Etc/UTC")
timezone = options |> Keyword.get(:timezone, @utc_timezone) |> fallback_tz()
DateTime.new!(date, notification_time, timezone)
end
@ -182,7 +184,7 @@ defmodule Mobilizon.Service.DateTime do
compare_to_day = Keyword.get(options, :compare_to_day, Date.utc_today())
compare_to = Keyword.get(options, :compare_to_datetime, DateTime.utc_now())
start_time = Keyword.get(options, :start_time, @start_time)
timezone = Keyword.get(options, :timezone, "Etc/UTC") || "Etc/UTC"
timezone = options |> Keyword.get(:timezone, @utc_timezone) |> fallback_tz()
end_time = Keyword.get(options, :end_time, @end_time)
DateTime.compare(compare_to, DateTime.new!(compare_to_day, start_time, timezone)) in [
@ -213,4 +215,13 @@ defmodule Mobilizon.Service.DateTime do
def is_same_day?(%DateTime{} = one, %DateTime{} = two) do
DateTime.to_date(one) == DateTime.to_date(two)
end
@spec fallback_tz(String.t()) :: String.t()
defp fallback_tz(timezone) do
if Tzdata.zone_exists?(timezone) do
timezone
else
@utc_timezone
end
end
end

View File

@ -8,8 +8,8 @@ defmodule Mobilizon.Service.Formatter do
Formats input text to structured data, extracts mentions and hashtags.
"""
alias Mobilizon.Actors
alias Mobilizon.Actors.Actor
alias Mobilizon.Federation.ActivityPub.Actor, as: ActivityPubActor
alias Mobilizon.Service.Formatter.HTML
alias Phoenix.HTML.Tag
@ -22,26 +22,26 @@ defmodule Mobilizon.Service.Formatter do
@spec escape_mention_handler(String.t(), String.t(), any(), any()) :: String.t()
defp escape_mention_handler("@" <> nickname = mention, buffer, _, _) do
case Actors.get_actor_by_name(nickname) do
%Actor{} ->
case ActivityPubActor.find_or_make_actor_from_nickname(nickname) do
{:ok, %Actor{}} ->
# escape markdown characters with `\\`
# (we don't want something like @user__name to be parsed by markdown)
String.replace(mention, @markdown_characters_regex, "\\\\\\1")
nil ->
{:error, _err} ->
buffer
end
end
@spec mention_handler(String.t(), String.t(), any(), map()) :: {String.t(), map()}
def mention_handler("@" <> nickname, buffer, _opts, acc) do
case Actors.get_actor_by_name(nickname) do
case ActivityPubActor.find_or_make_actor_from_nickname(nickname) do
# %Actor{preferred_username: preferred_username} = actor ->
# link = "<span class='h-card mention'>@<span>#{preferred_username}</span></span>"
#
# {link, %{acc | mentions: MapSet.put(acc.mentions, {"@" <> nickname, actor})}}
%Actor{type: :Person, id: id, preferred_username: preferred_username} = actor ->
{:ok, %Actor{type: :Person, id: id, preferred_username: preferred_username} = actor} ->
# link =
# "<span class='h-card mention' data-user='#{id}'>@<span>#{preferred_username}</span></span>"
@ -62,7 +62,7 @@ defmodule Mobilizon.Service.Formatter do
{link, %{acc | mentions: MapSet.put(acc.mentions, {"@" <> nickname, actor})}}
nil ->
{:error, _} ->
{buffer, acc}
end
end

View File

@ -1,6 +1,6 @@
defmodule Mobilizon.Service.Workers.CleanSuspendedActors do
@moduledoc """
Worker to clean unattached media
Worker to clean suspended actors
"""
use Oban.Worker, queue: "background"

View File

@ -24,6 +24,9 @@ defmodule Mobilizon.Service.Workers.RefreshInstances do
| {:error,
Mobilizon.Federation.ActivityPub.Actor.make_actor_errors()
| Mobilizon.Federation.WebFinger.finger_errors()}
defp refresh_instance_actor(%Instance{domain: nil}) do
{:error, :not_remote_instance}
end
defp refresh_instance_actor(%Instance{domain: domain}) do
ActivityPubActor.find_or_make_actor_from_nickname("relay@#{domain}")

View File

@ -167,7 +167,7 @@ defmodule Mobilizon.Mixfile do
{:mogrify, "~> 0.9"},
{:linkify, "~> 0.3"},
{:http_signatures, "~> 0.1.0"},
{:ex_cldr, "2.27.1"},
{:ex_cldr, "~> 2.28.0"},
{:ex_cldr_dates_times, "~> 2.2"},
{:ex_optimizer, "~> 0.1"},
{:progress_bar, "~> 2.0"},

View File

@ -34,12 +34,12 @@
"erlex": {:hex, :erlex, "0.2.6", "c7987d15e899c7a2f34f5420d2a2ea0d659682c06ac607572df55a43753aa12e", [:mix], [], "hexpm", "2ed2e25711feb44d52b17d2780eabf998452f6efda104877a3881c2f8c0c0c75"},
"erlport": {:hex, :erlport, "0.10.1", "c96ffa51bbcab0298232fcdfe8c3e110f1598011de71ae6b9082b80c9e2e476a", [:rebar3], [], "hexpm", "34931e8cb62a131d1bc8a2bd04d4007c73c03e4f10e22ee4a218e7172227a918"},
"eternal": {:hex, :eternal, "1.2.2", "d1641c86368de99375b98d183042dd6c2b234262b8d08dfd72b9eeaafc2a1abd", [:mix], [], "hexpm", "2c9fe32b9c3726703ba5e1d43a1d255a4f3f2d8f8f9bc19f094c7cb1a7a9e782"},
"ex_cldr": {:hex, :ex_cldr, "2.27.1", "b4fdedc29d6566b5201aea9bb7b554db16ac7b30366188aa0c50790b4393c268", [:mix], [{:castore, "~> 0.1", [hex: :castore, repo: "hexpm", optional: true]}, {:certifi, "~> 2.5", [hex: :certifi, repo: "hexpm", optional: true]}, {:cldr_utils, "~> 2.17", [hex: :cldr_utils, repo: "hexpm", optional: false]}, {:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:gettext, "~> 0.19", [hex: :gettext, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:nimble_parsec, "~> 0.5 or ~> 1.0", [hex: :nimble_parsec, repo: "hexpm", optional: true]}, {:plug, "~> 1.9", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "b5c5c31fb7cf43d47e1267a7ede8e46c2cf12de3308ea1ba614241bd29bdcebf"},
"ex_cldr": {:hex, :ex_cldr, "2.28.0", "8f7a8c70a49dc31f656eb02d4c6280550ab52abd340406e8341dd4ba2390798d", [:mix], [{:castore, "~> 0.1", [hex: :castore, repo: "hexpm", optional: true]}, {:certifi, "~> 2.5", [hex: :certifi, repo: "hexpm", optional: true]}, {:cldr_utils, "~> 2.17", [hex: :cldr_utils, repo: "hexpm", optional: false]}, {:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:gettext, "~> 0.19", [hex: :gettext, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:nimble_parsec, "~> 0.5 or ~> 1.0", [hex: :nimble_parsec, repo: "hexpm", optional: true]}, {:plug, "~> 1.9", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "564608c4a344c9cca54874e95fb77cd7149f593ccf522db5cbe8943c0b183630"},
"ex_cldr_calendars": {:hex, :ex_cldr_calendars, "1.18.0", "aa86e673f02b3c65d9cc29c483a4dfec1878b2e2460619c2b06c121ef801334e", [:mix], [{:calendar_interval, "~> 0.2", [hex: :calendar_interval, repo: "hexpm", optional: true]}, {:ex_cldr_lists, "~> 2.10", [hex: :ex_cldr_lists, repo: "hexpm", optional: true]}, {:ex_cldr_numbers, "~> 2.25", [hex: :ex_cldr_numbers, repo: "hexpm", optional: false]}, {:ex_cldr_units, "~> 3.12", [hex: :ex_cldr_units, repo: "hexpm", optional: true]}, {:ex_doc, "~> 0.21", [hex: :ex_doc, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "5b47bf4e90bdd6746ac9ca9cb3f9d36c3e656c18768ace8366061e3e02899cdc"},
"ex_cldr_currencies": {:hex, :ex_cldr_currencies, "2.13.0", "13188b99e527d724ed3dc2af37e54f0dac42dec42b620a34c4ed4d4902fad6dd", [:mix], [{:ex_cldr, "~> 2.24", [hex: :ex_cldr, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm", "64731e49ac3530aa88872b52c319eb5231bfb1a3ebb0956044c34abc0ed4f520"},
"ex_cldr_dates_times": {:hex, :ex_cldr_dates_times, "2.11.0", "eb00d2def8c16feb250ea2436c2e07b31b6e0ad22f9ff569c7714e807c8327df", [:mix], [{:calendar_interval, "~> 0.2", [hex: :calendar_interval, repo: "hexpm", optional: true]}, {:ex_cldr_calendars, "~> 1.18", [hex: :ex_cldr_calendars, repo: "hexpm", optional: false]}, {:ex_cldr_numbers, "~> 2.25", [hex: :ex_cldr_numbers, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm", "36b2dd6bea88f295b9761d6ca26cccce573708cffe6e196f9930a24ca57baecf"},
"ex_cldr_languages": {:hex, :ex_cldr_languages, "0.3.3", "9787002803552b15a7ade19496c9e46fc921baca992ea80d0394e11fe3acea45", [:mix], [{:ex_cldr, "~> 2.25", [hex: :ex_cldr, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm", "22fb1fef72b7b4b4872d243b34e7b83734247a78ad87377986bf719089cc447a"},
"ex_cldr_numbers": {:hex, :ex_cldr_numbers, "2.25.2", "9bd1f21bb7a300e1e0fd080b99f0c912d2adf90292e7d7c0335a6da84758c8a8", [:mix], [{:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:ex_cldr, "~> 2.26", [hex: :ex_cldr, repo: "hexpm", optional: false]}, {:ex_cldr_currencies, "~> 2.13", [hex: :ex_cldr_currencies, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm", "5de180d21018797bc412aae727402b3004df4c5666ed2602863448993b035c9b"},
"ex_cldr_numbers": {:hex, :ex_cldr_numbers, "2.26.0", "f1104498a96666fc0b0c0995f17266c8700593e984418aa05e7b731bfd8c5670", [:mix], [{:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:ex_cldr, "~> 2.28", [hex: :ex_cldr, repo: "hexpm", optional: false]}, {:ex_cldr_currencies, "~> 2.13", [hex: :ex_cldr_currencies, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm", "35b1c7bd6c87149e7f31ef24705d254419b35f9f26fe439af88d02950710e11b"},
"ex_doc": {:hex, :ex_doc, "0.28.3", "6eea2f69995f5fba94cd6dd398df369fe4e777a47cd887714a0976930615c9e6", [:mix], [{:earmark_parser, "~> 1.4.19", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1", [hex: :makeup_erlang, repo: "hexpm", optional: false]}], "hexpm", "05387a6a2655b5f9820f3f627450ed20b4325c25977b2ee69bed90af6688e718"},
"ex_ical": {:hex, :ex_ical, "0.2.0", "4b928b554614704016cc0c9ee226eb854da9327a1cc460457621ceacb1ac29a6", [:mix], [{:timex, "~> 3.1", [hex: :timex, repo: "hexpm", optional: false]}], "hexpm", "db76473b2ae0259e6633c6c479a5a4d8603f09497f55c88f9ef4d53d2b75befb"},
"ex_machina": {:hex, :ex_machina, "2.7.0", "b792cc3127fd0680fecdb6299235b4727a4944a09ff0fa904cc639272cd92dc7", [:mix], [{:ecto, "~> 2.2 or ~> 3.0", [hex: :ecto, repo: "hexpm", optional: true]}, {:ecto_sql, "~> 3.0", [hex: :ecto_sql, repo: "hexpm", optional: true]}], "hexpm", "419aa7a39bde11894c87a615c4ecaa52d8f107bbdd81d810465186f783245bf8"},

File diff suppressed because it is too large Load Diff

View File

@ -175,6 +175,9 @@ type Config {
"The instance's admins languages"
languages: [String]
"The instance list of event categories possibilities"
eventCategories: [EventCategoryOption]
"Whether the registrations are opened"
registrationsOpen: Boolean
@ -246,6 +249,9 @@ type Config {
"The instance list of export formats"
exportFormats: ExportFormats
"Configuration for diverse analytics services"
analytics: [Analytics]
}
"A tag"
@ -368,6 +374,15 @@ human-readable ID strings.
"""
scalar UUID
"A paginated list of instances"
type PaginatedInstanceList {
"A list of instances"
elements: [Instance]
"The total number of instances in the list"
total: Int
}
"A paginated list of discussions"
type PaginatedDiscussionList {
"A list of discussion"
@ -667,6 +682,26 @@ type RootSubscriptionType {
): Discussion
}
"Event categories list configuration"
type EventCategoryOption {
"The ID of the event category"
id: String
"The translated name of the event category"
label: String
}
type AnalyticsConfiguration {
"The key for the analytics configuration element"
key: String
"The value for the analytics configuration element"
value: String
"The analytics configuration type"
type: AnalyticsConfigurationType
}
"Represents a deleted feed_token"
type DeletedFeedToken {
"The user that owned the deleted feed token"
@ -930,6 +965,27 @@ type PaginatedFollowerList {
total: Int
}
type Analytics {
"ID of the analytics service"
id: String
"Whether the service is activated or not"
enabled: Boolean
"A list of key-values configuration"
configuration: [AnalyticsConfiguration]
}
enum InstancesSortFields {
EVENT_COUNT
PERSON_COUNT
GROUP_COUNT
FOLLOWERS_COUNT
FOLLOWINGS_COUNT
REPORTS_COUNT
MEDIA_SIZE
}
"The list of possible options for the event's status"
enum EventStatus {
"The event is tentative"
@ -1154,11 +1210,14 @@ type Person implements ActionLogObject & Actor {
limit: Int
): PaginatedParticipantList
"The list of group this person is member of"
"The list of groups this person is member of"
memberships(
"Filter by group federated username"
group: String
"Filter by group ID"
groupId: ID
"The page in the paginated memberships list"
page: Int
@ -1514,7 +1573,7 @@ type RootMutationType {
attributedToId: ID
"The event's category"
category: String
category: EventCategory
"The event's physical address"
physicalAddress: AddressInput
@ -1580,7 +1639,7 @@ type RootMutationType {
attributedToId: ID
"The event's category"
category: String
category: EventCategory
"The event's physical address"
physicalAddress: AddressInput
@ -1841,6 +1900,12 @@ type RootMutationType {
noteId: ID!
): DeletedObject
"Add an instance subscription"
addInstance(
"The instance domain to add"
domain: String!
): Instance
"Add a relay subscription"
addRelay(
"The relay hostname to add"
@ -1910,6 +1975,24 @@ type RootMutationType {
instanceLanguages: [String]
): AdminSettings
"For an admin to update an user"
adminUpdateUser(
"The user's ID"
id: ID!
"The user's new email"
email: String
"Manually confirm the user's account"
confirmed: Boolean
"Set user's new role"
role: UserRole
"Whether or not to notify the user of the change"
notify: Boolean
): User
"Create a todo list"
createTodoList(
"The todo list title"
@ -2225,6 +2308,9 @@ type RootQueryType {
"Whether the event is online or in person"
type: EventType
"The category for the event"
category: String
"Radius around the location to search in"
radius: Float
@ -2258,6 +2344,9 @@ type RootQueryType {
"Filter users by email"
email: String
"Filter users by current signed-in IP address"
currentSignInIp: String
"The page in the paginated users list"
page: Int
@ -2349,6 +2438,12 @@ type RootQueryType {
preferredUsername: String!
): Group
"Get a group by its preferred username"
groupById(
"The group local ID"
id: ID!
): Group
"Get all events"
events(
"The page in the paginated event list"
@ -2439,6 +2534,9 @@ type RootQueryType {
"Filter reports by status"
status: ReportStatus
"Filter reports by domain name"
domain: String
): PaginatedReportList
"Get a report by id"
@ -2486,6 +2584,30 @@ type RootQueryType {
direction: String
): PaginatedFollowerList
"List instances"
instances(
"The page in the paginated relay followings list"
page: Int
"The limit of relay followings per page"
limit: Int
"The field to order by the list"
orderBy: InstancesSortFields
"Filter by domain"
filterDomain: String
"Whether or not to filter instances by the follow status"
filterFollowStatus: InstanceFilterFollowStatus
"Whether or not to filter instances by the suspended status"
filterSuspendStatus: InstanceFilterSuspendStatus
"The sorting direction"
direction: String
): PaginatedInstanceList
"Get an instance's details"
instance(
"The instance domain"
@ -2569,15 +2691,15 @@ type Instance {
"The domain name of the instance"
domain: ID
"Whether this instance has a Mobilizon relay actor"
hasRelay: Boolean
"Do we follow this instance"
followerStatus: InstanceFollowStatus
"Does this instance follow us?"
followedStatus: InstanceFollowStatus
"The number of events on this instance we know of"
eventCount: Int
"The number of profiles on this instance we know of"
personCount: Int
@ -2595,6 +2717,9 @@ type Instance {
"The size of all the media files sent by actors from this instance"
mediaSize: Int
"Whether this instance has a relay, meaning that it's a Mobilizon instance that we can follow"
hasRelay: Boolean
}
"""
@ -2865,7 +2990,7 @@ type Event implements ActivityObject & Interactable & ActionLogObject {
tags: [Tag]
"The event's category"
category: String
category: EventCategory
"Whether or not the event is a draft"
draft: Boolean
@ -3296,6 +3421,39 @@ type Activity {
group: Group
}
enum EventCategory {
ARTS
BOOK_CLUBS
BUSINESS
CAUSES
COMEDY
CRAFTS
FOOD_DRINK
HEALTH
MUSIC
AUTO_BOAT_AIR
COMMUNITY
FAMILY_EDUCATION
FASHION_BEAUTY
FILM_MEDIA
GAMES
LANGUAGE_CULTURE
LEARNING
LGBTQ
MOVEMENTS_POLITICS
NETWORKING
PARTY
PERFORMING_VISUAL_ARTS
PETS
PHOTOGRAPHY
OUTDOORS_ADVENTURE
SPIRITUALITY_RELIGION_BELIEFS
SCIENCE_TECH
SPORTS
THEATRE
MEETING
}
"The list of visibility options for a post"
enum PostVisibility {
"Publicly listed and federated. Can be shared."
@ -3355,6 +3513,20 @@ type ReportNote implements ActionLogObject {
insertedAt: DateTime
}
enum AnalyticsConfigurationType {
"A string"
STRING
"An integer"
INTEGER
"A boolean"
BOOLEAN
"A float"
FLOAT
}
"The types of Group that exist"
enum GroupType {
"A private group of persons"
@ -3494,6 +3666,12 @@ type PaginatedFollowedGroupEvents {
total: Int
}
enum InstanceFilterFollowStatus {
ALL
FOLLOWING
FOLLOWED
}
"Instance map tiles configuration"
type Tiles {
"The instance's tiles endpoint"
@ -3661,6 +3839,11 @@ type AdminSettings {
instanceLanguages: [String]
}
enum InstanceFilterSuspendStatus {
ALL
SUSPENDED
}
"The instance's features"
type Features {
"Whether groups are activated on this instance"
@ -3668,9 +3851,6 @@ type Features {
"Whether event creation is allowed on this instance"
eventCreation: Boolean
"Activate link to Koena Connect"
koenaConnect: Boolean
}
"A set of user settings"

View File

@ -195,7 +195,7 @@ defmodule Mobilizon.Federation.ActivityPubTest do
actor = insert(:actor)
actor_data = %{summary: @updated_actor_summary}
{:ok, update, _} = Actions.Update.update(actor, actor_data, false)
{:ok, update, _} = Actions.Update.update(actor, actor_data, true)
assert update.data["actor"] == actor.url
assert update.data["to"] == [@activity_pub_public_audience]

View File

@ -3,12 +3,13 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier.UpdateTest do
use Oban.Testing, repo: Mobilizon.Storage.Repo
import Mobilizon.Factory
import Mox
import ExUnit.CaptureLog
alias Mobilizon.{Actors, Events, Posts}
alias Mobilizon.Actors.{Actor, Member}
alias Mobilizon.Events.Event
alias Mobilizon.Posts.Post
alias Mobilizon.Federation.ActivityPub.{Activity, Transmogrifier}
alias Mobilizon.Federation.ActivityPub.{Activity, Relay, Transmogrifier}
alias Mobilizon.Federation.ActivityStream.Convertible
alias Mobilizon.Service.HTTP.ActivityPub.Mock
@ -50,6 +51,29 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier.UpdateTest do
assert actor.summary == "<p>Some bio</p>"
end
test "it fails for incoming update activies on local actors" do
%Actor{url: relay_actor_url} = Relay.get_actor()
update_data = File.read!("test/fixtures/mastodon-update.json") |> Jason.decode!()
object =
update_data["object"]
|> Map.put("actor", relay_actor_url)
|> Map.put("id", relay_actor_url)
update_data =
update_data
|> Map.put("actor", relay_actor_url)
|> Map.put("object", object)
assert capture_log([level: :warn], fn ->
:error = Transmogrifier.handle_incoming(update_data)
end) =~ "[warning] Activity tried to update an actor that's local or not a group"
{:ok, %Actor{keys: keys}} = Actors.get_actor_by_url(relay_actor_url)
assert Regex.match?(~r/BEGIN RSA PRIVATE KEY/, keys)
end
test "it works for incoming update activities on events" do
data = File.read!("test/fixtures/mobilizon-post-activity.json") |> Jason.decode!()