Merge branch 'allow-group-event-edition' into 'master'

Allow events & posts to be edited by group moderators

Closes #807 et #517

See merge request framasoft/mobilizon!1003
This commit is contained in:
Thomas Citharel 2021-08-02 10:20:59 +00:00
commit a5a0c8c18a
43 changed files with 2113 additions and 721 deletions

View File

@ -77,12 +77,12 @@
</div> </div>
<div class="media-content" v-if="actor.name"> <div class="media-content" v-if="actor.name">
<p class="is-4">{{ actor.name }}</p> <p class="is-4">{{ actor.name }}</p>
<p class="is-6 has-text-grey"> <p class="is-6 has-text-grey-dark">
{{ `@${actor.preferredUsername}` }} {{ `@${usernameWithDomain(actor)}` }}
</p> </p>
</div> </div>
<div class="media-content" v-else> <div class="media-content" v-else>
{{ `@${actor.preferredUsername}` }} {{ `@${usernameWithDomain(actor)}` }}
</div> </div>
</div> </div>
</b-checkbox> </b-checkbox>
@ -167,6 +167,8 @@ export default class OrganizerPickerWrapper extends Vue {
isComponentModalActive = false; isComponentModalActive = false;
usernameWithDomain = usernameWithDomain;
@Prop({ type: Array, required: false, default: () => [] }) @Prop({ type: Array, required: false, default: () => [] })
contacts!: IActor[]; contacts!: IActor[];
members: Paginate<IMember> = { elements: [], total: 0 }; members: Paginate<IMember> = { elements: [], total: 0 };

View File

@ -192,9 +192,6 @@ export default class ParticipationSection extends Vue {
if (this.event.draft || this.event.status === EventStatus.CANCELLED) if (this.event.draft || this.event.status === EventStatus.CANCELLED)
return false; return false;
// Organizer can't participate
if (this.actorIsOrganizer) return false;
// If capacity is OK // If capacity is OK
if (this.eventCapacityOK) return true; if (this.eventCapacityOK) return true;

View File

@ -21,19 +21,52 @@ function formatTimeString(value: string): string {
}); });
} }
function formatDateTimeString(value: string, showTime = true): string { // TODO: These can be removed in favor of dateStyle/timeStyle when those two have sufficient support
const options: DateTimeFormatOptions = { // https://caniuse.com/mdn-javascript_builtins_intl_datetimeformat_datetimeformat_datestyle
weekday: undefined, const LONG_DATE_FORMAT_OPTIONS: DateTimeFormatOptions = {
year: "numeric", weekday: undefined,
month: "long", year: "numeric",
day: "numeric", month: "long",
hour: undefined, day: "numeric",
minute: undefined, hour: undefined,
}; minute: undefined,
};
const LONG_TIME_FORMAT_OPTIONS: DateTimeFormatOptions = {
weekday: "long",
hour: "numeric",
minute: "numeric",
};
const SHORT_DATE_FORMAT_OPTIONS: DateTimeFormatOptions = {
weekday: undefined,
year: "numeric",
month: "short",
day: "numeric",
hour: undefined,
minute: undefined,
};
const SHORT_TIME_FORMAT_OPTIONS: DateTimeFormatOptions = {
weekday: "short",
hour: "numeric",
minute: "numeric",
};
function formatDateTimeString(
value: string,
showTime = true,
dateFormat = "long"
): string {
const isLongFormat = dateFormat === "long";
let options = isLongFormat
? LONG_DATE_FORMAT_OPTIONS
: SHORT_DATE_FORMAT_OPTIONS;
if (showTime) { if (showTime) {
options.weekday = "long"; options = {
options.hour = "numeric"; ...options,
options.minute = "numeric"; ...(isLongFormat ? LONG_TIME_FORMAT_OPTIONS : SHORT_TIME_FORMAT_OPTIONS),
};
} }
const format = new Intl.DateTimeFormat(locale(), options); const format = new Intl.DateTimeFormat(locale(), options);
return format.format(parseDateTime(value)); return format.format(parseDateTime(value));

View File

@ -1,6 +1,6 @@
<template> <template>
<section> <section>
<div class="container" v-if="isCurrentActorOrganizer"> <div class="container" v-if="hasCurrentActorPermissionsToEdit">
<h1 class="title" v-if="isUpdate === true"> <h1 class="title" v-if="isUpdate === true">
{{ $t("Update event {name}", { name: event.title }) }} {{ $t("Update event {name}", { name: event.title }) }}
</h1> </h1>
@ -269,6 +269,11 @@
</b-field> </b-field>
</form> </form>
</div> </div>
<div class="container section" v-else>
<b-message type="is-danger">
{{ $t("Only group moderators can create, edit and delete events.") }}
</b-message>
</div>
<b-modal v-model="dateSettingsIsOpen" has-modal-card trap-focus> <b-modal v-model="dateSettingsIsOpen" has-modal-card trap-focus>
<form action> <form action>
<div class="modal-card" style="width: auto"> <div class="modal-card" style="width: auto">
@ -305,7 +310,7 @@
aria-label="main navigation" aria-label="main navigation"
class="navbar save__navbar" class="navbar save__navbar"
:class="{ 'is-fixed-bottom': showFixedNavbar }" :class="{ 'is-fixed-bottom': showFixedNavbar }"
v-if="isCurrentActorOrganizer" v-if="hasCurrentActorPermissionsToEdit"
> >
<div class="container"> <div class="container">
<div class="navbar-menu"> <div class="navbar-menu">
@ -457,6 +462,7 @@ import {
EventJoinOptions, EventJoinOptions,
EventStatus, EventStatus,
EventVisibility, EventVisibility,
MemberRole,
ParticipantRole, ParticipantRole,
} from "@/types/enums"; } from "@/types/enums";
import OrganizerPickerWrapper from "../../components/Event/OrganizerPickerWrapper.vue"; import OrganizerPickerWrapper from "../../components/Event/OrganizerPickerWrapper.vue";
@ -472,8 +478,15 @@ import {
IDENTITIES, IDENTITIES,
LOGGED_USER_DRAFTS, LOGGED_USER_DRAFTS,
LOGGED_USER_PARTICIPATIONS, LOGGED_USER_PARTICIPATIONS,
PERSON_MEMBERSHIP_GROUP,
} from "../../graphql/actor"; } from "../../graphql/actor";
import { displayNameAndUsername, IActor, IGroup } from "../../types/actor"; import {
displayNameAndUsername,
IActor,
IGroup,
IPerson,
usernameWithDomain,
} from "../../types/actor";
import { TAGS } from "../../graphql/tags"; import { TAGS } from "../../graphql/tags";
import { ITag } from "../../types/tag.model"; import { ITag } from "../../types/tag.model";
import { import {
@ -519,6 +532,22 @@ const DEFAULT_LIMIT_NUMBER_OF_PLACES = 10;
return !this.eventId; return !this.eventId;
}, },
}, },
person: {
query: PERSON_MEMBERSHIP_GROUP,
fetchPolicy: "cache-and-network",
variables() {
return {
id: this.currentActor.id,
group: usernameWithDomain(this.event?.attributedTo),
};
},
skip() {
return (
!this.event?.attributedTo ||
!this.event?.attributedTo?.preferredUsername
);
},
},
}, },
metaInfo() { metaInfo() {
return { return {
@ -545,6 +574,8 @@ export default class EditEvent extends Vue {
identities: IActor[] = []; identities: IActor[] = [];
person!: IPerson;
config!: IConfig; config!: IConfig;
pictureFile: File | null = null; pictureFile: File | null = null;
@ -553,8 +584,6 @@ export default class EditEvent extends Vue {
EventVisibility = EventVisibility; EventVisibility = EventVisibility;
needsApproval = false;
canPromote = true; canPromote = true;
limitedPlaces = false; limitedPlaces = false;
@ -749,13 +778,23 @@ export default class EditEvent extends Vue {
} }
} }
get isCurrentActorOrganizer(): boolean { get hasCurrentActorPermissionsToEdit(): boolean {
return !( return !(
this.eventId && this.eventId &&
this.event.organizerActor?.id !== undefined && this.event.organizerActor?.id !== undefined &&
!this.identities !this.identities
.map(({ id }) => id) .map(({ id }) => id)
.includes(this.event.organizerActor?.id) .includes(this.event.organizerActor?.id) &&
!this.hasGroupPrivileges
);
}
get hasGroupPrivileges(): boolean {
return (
this.person?.memberships?.total > 0 &&
[MemberRole.MODERATOR, MemberRole.ADMINISTRATOR].includes(
this.person?.memberships?.elements[0].role
)
); );
} }
@ -908,9 +947,12 @@ export default class EditEvent extends Vue {
} }
} }
@Watch("needsApproval") get needsApproval(): boolean {
updateEventJoinOptions(needsApproval: boolean): void { return this.event?.joinOptions == EventJoinOptions.RESTRICTED;
if (needsApproval === true) { }
set needsApproval(value: boolean) {
if (value === true) {
this.event.joinOptions = EventJoinOptions.RESTRICTED; this.event.joinOptions = EventJoinOptions.RESTRICTED;
} else { } else {
this.event.joinOptions = EventJoinOptions.FREE; this.event.joinOptions = EventJoinOptions.FREE;

View File

@ -120,7 +120,7 @@
<b-icon icon="link" /> <b-icon icon="link" />
</p> </p>
</template> </template>
<template v-if="!event.local && organizer"> <template v-if="!event.local && organizer.domain">
<a :href="event.url"> <a :href="event.url">
<tag>{{ organizer.domain }}</tag> <tag>{{ organizer.domain }}</tag>
</a> </a>
@ -128,7 +128,7 @@
<p> <p>
<router-link <router-link
class="participations-link" class="participations-link"
v-if="actorIsOrganizer && event.draft === false" v-if="canManageEvent && event.draft === false"
:to="{ :to="{
name: RouteName.PARTICIPATIONS, name: RouteName.PARTICIPATIONS,
params: { eventId: event.uuid }, params: { eventId: event.uuid },
@ -214,7 +214,7 @@
<b-dropdown-item <b-dropdown-item
aria-role="listitem" aria-role="listitem"
has-link has-link
v-if="actorIsOrganizer || event.draft" v-if="canManageEvent || event.draft"
> >
<router-link <router-link
:to="{ :to="{
@ -229,7 +229,7 @@
<b-dropdown-item <b-dropdown-item
aria-role="listitem" aria-role="listitem"
has-link has-link
v-if="actorIsOrganizer || event.draft" v-if="canManageEvent || event.draft"
> >
<router-link <router-link
:to="{ :to="{
@ -243,7 +243,7 @@
</b-dropdown-item> </b-dropdown-item>
<b-dropdown-item <b-dropdown-item
aria-role="listitem" aria-role="listitem"
v-if="actorIsOrganizer || event.draft" v-if="canManageEvent || event.draft"
@click="openDeleteEventModalWrapper" @click="openDeleteEventModalWrapper"
> >
{{ $t("Delete") }} {{ $t("Delete") }}
@ -253,7 +253,7 @@
<hr <hr
class="dropdown-divider" class="dropdown-divider"
aria-role="menuitem" aria-role="menuitem"
v-if="actorIsOrganizer || event.draft" v-if="canManageEvent || event.draft"
/> />
<b-dropdown-item <b-dropdown-item
aria-role="listitem" aria-role="listitem"
@ -623,6 +623,7 @@ import {
EventJoinOptions, EventJoinOptions,
EventStatus, EventStatus,
EventVisibility, EventVisibility,
MemberRole,
ParticipantRole, ParticipantRole,
RoutingTransportationType, RoutingTransportationType,
RoutingType, RoutingType,
@ -633,7 +634,10 @@ import {
FETCH_EVENT, FETCH_EVENT,
JOIN_EVENT, JOIN_EVENT,
} from "../../graphql/event"; } from "../../graphql/event";
import { CURRENT_ACTOR_CLIENT } from "../../graphql/actor"; import {
CURRENT_ACTOR_CLIENT,
PERSON_MEMBERSHIP_GROUP,
} from "../../graphql/actor";
import { EventModel, IEvent } from "../../types/event.model"; import { EventModel, IEvent } from "../../types/event.model";
import { IActor, IPerson, Person, usernameWithDomain } from "../../types/actor"; import { IActor, IPerson, Person, usernameWithDomain } from "../../types/actor";
import { GRAPHQL_API_ENDPOINT } from "../../api/_entrypoint"; import { GRAPHQL_API_ENDPOINT } from "../../api/_entrypoint";
@ -738,6 +742,22 @@ import { ApolloCache, FetchResult } from "@apollo/client/core";
); );
}, },
}, },
person: {
query: PERSON_MEMBERSHIP_GROUP,
fetchPolicy: "cache-and-network",
variables() {
return {
id: this.currentActor.id,
group: usernameWithDomain(this.event?.attributedTo),
};
},
skip() {
return (
!this.event?.attributedTo ||
!this.event?.attributedTo?.preferredUsername
);
},
},
config: CONFIG, config: CONFIG,
}, },
metaInfo() { metaInfo() {
@ -764,6 +784,8 @@ export default class Event extends EventMixin {
config!: IConfig; config!: IConfig;
person!: IPerson;
participations: IParticipant[] = []; participations: IParticipant[] = [];
oldParticipationRole!: string; oldParticipationRole!: string;
@ -1211,6 +1233,19 @@ export default class Event extends EventMixin {
); );
} }
get hasGroupPrivileges(): boolean {
return (
this.person?.memberships?.total > 0 &&
[MemberRole.MODERATOR, MemberRole.ADMINISTRATOR].includes(
this.person?.memberships?.elements[0].role
)
);
}
get canManageEvent(): boolean {
return this.actorIsOrganizer || this.hasGroupPrivileges;
}
get endDate(): Date { get endDate(): Date {
return this.event.endsOn !== null && this.event.endsOn > this.event.beginsOn return this.event.endsOn !== null && this.event.endsOn > this.event.beginsOn
? this.event.endsOn ? this.event.endsOn

View File

@ -207,7 +207,7 @@ import { FETCH_GROUP } from "@/graphql/group";
variables() { variables() {
return { return {
id: this.currentActor.id, id: this.currentActor.id,
group: this.actualGroup.preferredUsername, group: usernameWithDomain(this.actualGroup),
}; };
}, },
skip() { skip() {

View File

@ -24,6 +24,29 @@
<b-icon icon="clock" size="is-small" /> <b-icon icon="clock" size="is-small" />
{{ post.publishAt | formatDateTimeString }} {{ post.publishAt | formatDateTimeString }}
</span> </span>
<span
class="published has-text-grey-dark"
:title="
$options.filters.formatDateTimeString(
post.updatedAt,
true,
'short'
)
"
v-else
>
<b-icon icon="clock" size="is-small" />
{{
$t("Edited {relative_time} ago", {
relative_time: formatDistanceToNowStrict(
new Date(post.updatedAt),
{
locale: $dateFnsLocale,
}
),
})
}}
</span>
<span <span
v-if="post.visibility === PostVisibility.PRIVATE" v-if="post.visibility === PostVisibility.PRIVATE"
class="has-text-grey-dark" class="has-text-grey-dark"
@ -75,7 +98,11 @@ import { mixins } from "vue-class-component";
import GroupMixin from "@/mixins/group"; import GroupMixin from "@/mixins/group";
import { PostVisibility } from "@/types/enums"; import { PostVisibility } from "@/types/enums";
import { IMember } from "@/types/actor/member.model"; import { IMember } from "@/types/actor/member.model";
import { CURRENT_ACTOR_CLIENT, PERSON_MEMBERSHIPS } from "../../graphql/actor"; import {
CURRENT_ACTOR_CLIENT,
PERSON_MEMBERSHIPS,
PERSON_MEMBERSHIP_GROUP,
} from "../../graphql/actor";
import { FETCH_POST } from "../../graphql/post"; import { FETCH_POST } from "../../graphql/post";
import { IPost } from "../../types/post.model"; import { IPost } from "../../types/post.model";
import { usernameWithDomain } from "../../types/actor"; import { usernameWithDomain } from "../../types/actor";
@ -83,6 +110,7 @@ import RouteName from "../../router/name";
import Tag from "../../components/Tag.vue"; import Tag from "../../components/Tag.vue";
import LazyImageWrapper from "../../components/Image/LazyImageWrapper.vue"; import LazyImageWrapper from "../../components/Image/LazyImageWrapper.vue";
import ActorInline from "../../components/Account/ActorInline.vue"; import ActorInline from "../../components/Account/ActorInline.vue";
import { formatDistanceToNowStrict } from "date-fns";
@Component({ @Component({
apollo: { apollo: {
@ -115,6 +143,23 @@ import ActorInline from "../../components/Account/ActorInline.vue";
this.handleErrors(graphQLErrors); this.handleErrors(graphQLErrors);
}, },
}, },
person: {
query: PERSON_MEMBERSHIP_GROUP,
fetchPolicy: "cache-and-network",
variables() {
return {
id: this.currentActor.id,
group: usernameWithDomain(this.post.attributedTo),
};
},
skip() {
return (
!this.currentActor ||
!this.currentActor.id ||
!this.post?.attributedTo
);
},
},
}, },
components: { components: {
Tag, Tag,
@ -144,6 +189,8 @@ export default class Post extends mixins(GroupMixin) {
usernameWithDomain = usernameWithDomain; usernameWithDomain = usernameWithDomain;
formatDistanceToNowStrict = formatDistanceToNowStrict;
PostVisibility = PostVisibility; PostVisibility = PostVisibility;
handleErrors(errors: any[]): void { handleErrors(errors: any[]): void {

View File

@ -426,7 +426,7 @@ defmodule Mobilizon.Federation.ActivityPub do
"id" => "#{Endpoint.url()}/leave/event/#{participant.id}" "id" => "#{Endpoint.url()}/leave/event/#{participant.id}"
}, },
audience <- audience <-
Audience.calculate_to_and_cc_from_mentions(participant), Audience.get_audience(participant),
{:ok, activity} <- create_activity(Map.merge(leave_data, audience), local), {:ok, activity} <- create_activity(Map.merge(leave_data, audience), local),
:ok <- maybe_federate(activity) do :ok <- maybe_federate(activity) do
{:ok, activity, participant} {:ok, activity, participant}
@ -803,15 +803,15 @@ defmodule Mobilizon.Federation.ActivityPub do
Scheduler.trigger_notifications_for_participant(participant), Scheduler.trigger_notifications_for_participant(participant),
participant_as_data <- Convertible.model_to_as(participant), participant_as_data <- Convertible.model_to_as(participant),
audience <- audience <-
Audience.calculate_to_and_cc_from_mentions(participant), Audience.get_audience(participant),
update_data <- accept_join_data <-
make_accept_join_data( make_accept_join_data(
participant_as_data, participant_as_data,
Map.merge(Map.merge(audience, additional), %{ Map.merge(Map.merge(audience, additional), %{
"id" => "#{Endpoint.url()}/accept/join/#{participant.id}" "id" => "#{Endpoint.url()}/accept/join/#{participant.id}"
}) })
) do ) do
{:ok, participant, update_data} {:ok, participant, accept_join_data}
else else
err -> err ->
Logger.error("Something went wrong while creating an update activity") Logger.error("Something went wrong while creating an update activity")
@ -837,15 +837,15 @@ defmodule Mobilizon.Federation.ActivityPub do
), ),
member_as_data <- Convertible.model_to_as(member), member_as_data <- Convertible.model_to_as(member),
audience <- audience <-
Audience.calculate_to_and_cc_from_mentions(member), Audience.get_audience(member),
update_data <- accept_join_data <-
make_accept_join_data( make_accept_join_data(
member_as_data, member_as_data,
Map.merge(Map.merge(audience, additional), %{ Map.merge(Map.merge(audience, additional), %{
"id" => "#{Endpoint.url()}/accept/join/#{member.id}" "id" => "#{Endpoint.url()}/accept/join/#{member.id}"
}) })
) do ) do
{:ok, member, update_data} {:ok, member, accept_join_data}
else else
err -> err ->
Logger.error("Something went wrong while creating an update activity") Logger.error("Something went wrong while creating an update activity")
@ -899,7 +899,7 @@ defmodule Mobilizon.Federation.ActivityPub do
participant_as_data <- Convertible.model_to_as(participant), participant_as_data <- Convertible.model_to_as(participant),
audience <- audience <-
participant participant
|> Audience.calculate_to_and_cc_from_mentions() |> Audience.get_audience()
|> Map.merge(additional), |> Map.merge(additional),
reject_data <- %{ reject_data <- %{
"type" => "Reject", "type" => "Reject",
@ -925,7 +925,7 @@ defmodule Mobilizon.Federation.ActivityPub do
with {:ok, %Follower{} = follower} <- Actors.delete_follower(follower), with {:ok, %Follower{} = follower} <- Actors.delete_follower(follower),
follower_as_data <- Convertible.model_to_as(follower), follower_as_data <- Convertible.model_to_as(follower),
audience <- audience <-
follower.actor |> Audience.calculate_to_and_cc_from_mentions() |> Map.merge(additional), follower.actor |> Audience.get_audience() |> Map.merge(additional),
reject_data <- %{ reject_data <- %{
"to" => [follower.actor.url], "to" => [follower.actor.url],
"type" => "Reject", "type" => "Reject",

View File

@ -55,6 +55,10 @@ defmodule Mobilizon.Federation.ActivityPub.Actor do
{:error, "Can't make a local actor from URL"} {:error, "Can't make a local actor from URL"}
else else
case Fetcher.fetch_and_prepare_actor_from_url(url) do case Fetcher.fetch_and_prepare_actor_from_url(url) do
# Just in case
{:ok, {:error, _e}} ->
raise ArgumentError, message: "Failed to make actor from url #{url}"
{:ok, data} -> {:ok, data} ->
Actors.upsert_actor(data, preload) Actors.upsert_actor(data, preload)
@ -67,7 +71,7 @@ defmodule Mobilizon.Federation.ActivityPub.Actor do
{:error, :http_error} {:error, :http_error}
{:error, e} -> {:error, e} ->
Logger.warn("Failed to make actor from url") Logger.warn("Failed to make actor from url #{url}")
{:error, e} {:error, e}
end end
end end

View File

@ -3,18 +3,99 @@ defmodule Mobilizon.Federation.ActivityPub.Audience do
Tools for calculating content audience Tools for calculating content audience
""" """
alias Mobilizon.Actors alias Mobilizon.{Actors, Events, Share}
alias Mobilizon.Actors.{Actor, Member} alias Mobilizon.Actors.{Actor, Member}
alias Mobilizon.Discussions.{Comment, Discussion} alias Mobilizon.Discussions.{Comment, Discussion}
alias Mobilizon.Events.{Event, Participant} alias Mobilizon.Events.{Event, Participant}
alias Mobilizon.Posts.Post alias Mobilizon.Posts.Post
alias Mobilizon.Share
alias Mobilizon.Storage.Repo alias Mobilizon.Storage.Repo
require Logger require Logger
@ap_public "https://www.w3.org/ns/activitystreams#Public" @ap_public "https://www.w3.org/ns/activitystreams#Public"
@type audience :: %{required(String.t()) => list(String.t())}
@doc """
Get audience for an entity
"""
@spec get_audience(Entity.t()) :: audience()
def get_audience(%Event{} = event) do
extract_actors_from_event(event)
end
def get_audience(%Post{draft: true} = post) do
get_audience(%Post{post | visibility: :private, draft: false})
end
def get_audience(%Post{attributed_to: %Actor{} = group, visibility: visibility}) do
{to, cc} = get_to_and_cc(group, [], visibility)
%{"to" => to, "cc" => cc}
end
def get_audience(%Discussion{actor: actor}) do
%{"to" => maybe_add_group_members([], actor), "cc" => []}
end
def get_audience(%Comment{discussion: %Discussion{} = discussion}) do
get_audience(discussion)
end
def get_audience(%Comment{
mentions: mentions,
actor: %Actor{} = actor,
visibility: visibility,
in_reply_to_comment: in_reply_to_comment,
event: event,
origin_comment: origin_comment,
url: url
}) do
with {to, cc} <-
extract_actors_from_mentions(mentions, actor, visibility),
{to, cc} <- {Enum.uniq(to ++ add_in_reply_to(in_reply_to_comment)), cc},
{to, cc} <- {Enum.uniq(to ++ add_event_author(event)), cc},
{to, cc} <-
{to,
Enum.uniq(
cc ++
add_comments_authors([origin_comment]) ++
add_shares_actors_followers(url)
)} do
%{"to" => to, "cc" => cc}
end
end
def get_audience(%Participant{} = participant) do
%Event{} = event = Events.get_event_with_preload!(participant.event_id)
%Actor{} = organizer = group_or_organizer_event(event)
cc =
event.id
|> Mobilizon.Events.list_actors_participants_for_event()
|> Enum.map(& &1.url)
|> Enum.filter(&(&1 != participant.actor.url))
|> maybe_add_group_members(organizer)
|> maybe_add_followers(organizer)
%{
"to" => [participant.actor.url, organizer.url],
"cc" => cc
}
end
def get_audience(%Member{} = member) do
%{"to" => [member.parent.url, member.parent.members_url], "cc" => []}
end
def get_audience(%Actor{} = actor) do
%{
"to" => [@ap_public],
"cc" =>
maybe_add_group_members([actor.followers_url], actor) ++
add_actors_that_had_our_content(actor.id)
}
end
@doc """ @doc """
Determines the full audience based on mentions for an audience Determines the full audience based on mentions for an audience
@ -39,6 +120,8 @@ defmodule Mobilizon.Federation.ActivityPub.Audience do
to = [@ap_public | mentions] to = [@ap_public | mentions]
cc = [actor.followers_url] cc = [actor.followers_url]
cc = maybe_add_group_members(cc, actor)
{to, cc} {to, cc}
end end
@ -47,13 +130,18 @@ defmodule Mobilizon.Federation.ActivityPub.Audience do
to = [actor.followers_url | mentions] to = [actor.followers_url | mentions]
cc = [@ap_public] cc = [@ap_public]
to = maybe_add_group_members(to, actor)
{to, cc} {to, cc}
end end
@spec get_to_and_cc(Actor.t(), list(), String.t()) :: {list(), list()} @spec get_to_and_cc(Actor.t(), list(), String.t()) :: {list(), list()}
def get_to_and_cc(%Actor{} = actor, mentions, :private) do def get_to_and_cc(%Actor{} = actor, mentions, :private) do
{to, cc} = get_to_and_cc(actor, mentions, :direct) {to, cc} = get_to_and_cc(actor, mentions, :direct)
{[actor.followers_url | to], cc}
to = maybe_add_group_members(to, actor)
{to, cc}
end end
@spec get_to_and_cc(Actor.t(), list(), String.t()) :: {list(), list()} @spec get_to_and_cc(Actor.t(), list(), String.t()) :: {list(), list()}
@ -65,125 +153,31 @@ defmodule Mobilizon.Federation.ActivityPub.Audience do
{mentions, []} {mentions, []}
end end
@spec maybe_add_group_members(List.t(), Actor.t()) :: List.t()
defp maybe_add_group_members(collection, %Actor{type: :Group, members_url: members_url}) do
[members_url | collection]
end
defp maybe_add_group_members(collection, %Actor{type: _}), do: collection
@spec maybe_add_followers(List.t(), Actor.t()) :: List.t()
defp maybe_add_followers(collection, %Actor{type: :Group, followers_url: followers_url}) do
[followers_url | collection]
end
defp maybe_add_followers(collection, %Actor{type: _}), do: collection
def get_addressed_actors(mentioned_users, _), do: mentioned_users def get_addressed_actors(mentioned_users, _), do: mentioned_users
def calculate_to_and_cc_from_mentions(
%Comment{discussion: %Discussion{actor_id: actor_id}} = _comment
) do
with %Actor{type: :Group, members_url: members_url} <- Actors.get_actor(actor_id) do
%{"to" => [members_url], "cc" => []}
end
end
def calculate_to_and_cc_from_mentions(%Comment{} = comment) do
with {to, cc} <-
extract_actors_from_mentions(comment.mentions, comment.actor, comment.visibility),
{to, cc} <- {Enum.uniq(to ++ add_in_reply_to(comment.in_reply_to_comment)), cc},
{to, cc} <- {Enum.uniq(to ++ add_event_author(comment.event)), cc},
{to, cc} <-
{to,
Enum.uniq(
cc ++
add_comments_authors([comment.origin_comment]) ++
add_shares_actors_followers(comment.url)
)} do
%{"to" => to, "cc" => cc}
end
end
def calculate_to_and_cc_from_mentions(%Discussion{actor_id: actor_id}) do
with %Actor{type: :Group, members_url: members_url} <- Actors.get_actor(actor_id) do
%{"to" => [members_url], "cc" => []}
end
end
def calculate_to_and_cc_from_mentions(
%Event{
attributed_to: %Actor{members_url: members_url},
visibility: visibility
} = event
) do
%{"to" => to, "cc" => cc} = extract_actors_from_event(event)
case visibility do
:public ->
%{"to" => [@ap_public, members_url] ++ to, "cc" => [] ++ cc}
:unlisted ->
%{"to" => [members_url] ++ to, "cc" => [@ap_public] ++ cc}
:private ->
# Private is restricted to only the members
%{"to" => [members_url], "cc" => []}
end
end
def calculate_to_and_cc_from_mentions(%Event{} = event) do
extract_actors_from_event(event)
end
def calculate_to_and_cc_from_mentions(%Post{
attributed_to: %Actor{members_url: members_url, followers_url: followers_url},
visibility: visibility,
draft: draft
}) do
cond do
# If the post is draft we send it only to members
draft == true ->
%{"to" => [members_url], "cc" => []}
# If public everyone
visibility == :public ->
%{"to" => [@ap_public, members_url], "cc" => [followers_url]}
# Otherwise just followers
visibility == :unlisted ->
%{"to" => [followers_url, members_url], "cc" => [@ap_public]}
visibility == :private ->
# Private is restricted to only the members
%{"to" => [members_url], "cc" => []}
true ->
%{"to" => [], "cc" => []}
end
end
def calculate_to_and_cc_from_mentions(%Participant{} = participant) do
participant = Repo.preload(participant, [:actor, :event])
actor_participants_urls =
participant.event.id
|> Mobilizon.Events.list_actors_participants_for_event()
|> Enum.map(& &1.url)
%{"to" => [participant.actor.url], "cc" => actor_participants_urls}
end
def calculate_to_and_cc_from_mentions(%Member{} = member) do
member = Repo.preload(member, [:parent])
%{"to" => [member.parent.members_url], "cc" => []}
end
def calculate_to_and_cc_from_mentions(%Actor{} = actor) do
%{
"to" => [@ap_public],
"cc" => [actor.followers_url] ++ add_actors_that_had_our_content(actor.id)
}
end
defp add_in_reply_to(%Comment{actor: %Actor{url: url}} = _comment), do: [url] defp add_in_reply_to(%Comment{actor: %Actor{url: url}} = _comment), do: [url]
defp add_in_reply_to(%Event{organizer_actor: %Actor{url: url}} = _event), do: [url] defp add_in_reply_to(%Event{organizer_actor: %Actor{url: url}} = _event), do: [url]
defp add_in_reply_to(_), do: [] defp add_in_reply_to(_), do: []
defp add_event_author(nil), do: []
defp add_event_author(%Event{} = event) do defp add_event_author(%Event{} = event) do
[Repo.preload(event, [:organizer_actor]).organizer_actor.url] [Repo.preload(event, [:organizer_actor]).organizer_actor.url]
end end
defp add_comment_author(nil), do: nil defp add_event_author(_), do: []
defp add_comment_author(%Comment{} = comment) do defp add_comment_author(%Comment{} = comment) do
case Repo.preload(comment, [:actor]) do case Repo.preload(comment, [:actor]) do
@ -195,6 +189,8 @@ defmodule Mobilizon.Federation.ActivityPub.Audience do
end end
end end
defp add_comment_author(_), do: nil
defp add_comments_authors(comments) do defp add_comments_authors(comments) do
authors = authors =
comments comments
@ -208,8 +204,6 @@ defmodule Mobilizon.Federation.ActivityPub.Audience do
defp add_shares_actors_followers(uri) do defp add_shares_actors_followers(uri) do
uri uri
|> Share.get_actors_by_share_uri() |> Share.get_actors_by_share_uri()
|> Enum.map(&Actors.list_followers_actors_for_actor/1)
|> List.flatten()
|> Enum.map(& &1.url) |> Enum.map(& &1.url)
|> Enum.uniq() |> Enum.uniq()
end end
@ -217,8 +211,6 @@ defmodule Mobilizon.Federation.ActivityPub.Audience do
defp add_actors_that_had_our_content(actor_id) do defp add_actors_that_had_our_content(actor_id) do
actor_id actor_id
|> Share.get_actors_by_owner_actor_id() |> Share.get_actors_by_owner_actor_id()
|> Enum.map(&Actors.list_followers_actors_for_actor/1)
|> List.flatten()
|> Enum.map(& &1.url) |> Enum.map(& &1.url)
|> Enum.uniq() |> Enum.uniq()
end end
@ -241,7 +233,11 @@ defmodule Mobilizon.Federation.ActivityPub.Audience do
defp extract_actors_from_event(%Event{} = event) do defp extract_actors_from_event(%Event{} = event) do
with {to, cc} <- with {to, cc} <-
extract_actors_from_mentions(event.mentions, event.organizer_actor, event.visibility), extract_actors_from_mentions(
event.mentions,
group_or_organizer_event(event),
event.visibility
),
{to, cc} <- {to, cc} <-
{to, {to,
Enum.uniq( Enum.uniq(
@ -253,4 +249,8 @@ defmodule Mobilizon.Federation.ActivityPub.Audience do
%{"to" => [], "cc" => []} %{"to" => [], "cc" => []}
end end
end end
@spec group_or_organizer_event(Event.t()) :: Actor.t()
defp group_or_organizer_event(%Event{attributed_to: %Actor{} = group}), do: group
defp group_or_organizer_event(%Event{organizer_actor: %Actor{} = actor}), do: actor
end end

View File

@ -0,0 +1,134 @@
defmodule Mobilizon.Federation.ActivityPub.Permission do
@moduledoc """
Module to check group members permissions on objects
"""
alias Mobilizon.Actors
alias Mobilizon.Actors.Actor
alias Mobilizon.Federation.ActivityPub.Types.{Entity, Ownable}
require Logger
use StructAccess
defstruct [:access, :create, :update, :delete]
@member_roles [:member, :moderator, :administrator]
@doc """
Check that actor can access the object
"""
@spec can_access_group_object?(Actor.t(), Entity.t()) :: boolean()
def can_access_group_object?(%Actor{} = actor, object) do
can_manage_group_object?(:access, actor, object)
end
@doc """
Check that actor can create such an object
"""
@spec can_create_group_object?(String.t() | integer(), String.t() | integer(), Entity.t()) ::
boolean()
def can_create_group_object?(
actor_id,
group_id,
object
) do
case object |> Ownable.permissions() |> get_in([:create]) do
:member ->
Actors.is_member?(actor_id, group_id)
:moderator ->
Actors.is_moderator?(actor_id, group_id)
:administrator ->
Actors.is_administrator?(actor_id, group_id)
_ ->
false
end
end
@doc """
Check that actor can update the object
"""
@spec can_update_group_object?(Actor.t(), Entity.t()) :: boolean()
def can_update_group_object?(%Actor{} = actor, object) do
can_manage_group_object?(:update, actor, object)
end
@doc """
Check that actor can delete the object
"""
@spec can_delete_group_object?(Actor.t(), Entity.t()) :: boolean()
def can_delete_group_object?(%Actor{} = actor, object) do
can_manage_group_object?(:delete, actor, object)
end
@type existing_object_permissions :: :access | :update | :delete
@spec can_manage_group_object?(
existing_object_permissions(),
Actor.t(),
any()
) :: boolean()
defp can_manage_group_object?(permission, %Actor{url: actor_url} = actor, object) do
if Ownable.group_actor(object) != nil do
case object |> Ownable.permissions() |> get_in([permission]) do
role when role in @member_roles ->
activity_actor_is_group_member?(actor, object, role)
_ ->
case permission do
:access ->
Logger.warn("Actor #{actor_url} can't access #{object.url}")
:update ->
Logger.warn("Actor #{actor_url} can't update #{object.url}")
:delete ->
Logger.warn("Actor #{actor_url} can't delete #{object.url}")
end
false
end
else
true
end
end
@spec activity_actor_is_group_member?(Actor.t(), Entity.t(), atom()) :: boolean()
defp activity_actor_is_group_member?(
%Actor{id: actor_id, url: actor_url},
object,
role
) do
case Ownable.group_actor(object) do
%Actor{type: :Group, id: group_id, url: group_url} ->
Logger.debug("Group object url is #{group_url}")
case role do
:moderator ->
Logger.debug(
"Checking if activity actor #{actor_url} is a moderator from group from #{object.url}"
)
Actors.is_moderator?(actor_id, group_id)
:administrator ->
Logger.debug(
"Checking if activity actor #{actor_url} is an administrator from group from #{object.url}"
)
Actors.is_administrator?(actor_id, group_id)
_ ->
Logger.debug(
"Checking if activity actor #{actor_url} is a member from group from #{object.url}"
)
Actors.is_member?(actor_id, group_id)
end
_ ->
false
end
end
end

View File

@ -14,7 +14,7 @@ defmodule Mobilizon.Federation.ActivityPub.Preloader do
@spec maybe_preload(struct()) :: {:ok, struct()} | {:error, struct()} @spec maybe_preload(struct()) :: {:ok, struct()} | {:error, struct()}
def maybe_preload(%Event{url: url}), def maybe_preload(%Event{url: url}),
do: {:ok, Events.get_public_event_by_url_with_preload!(url)} do: {:ok, Events.get_event_by_url!(url)}
def maybe_preload(%Comment{url: url}), def maybe_preload(%Comment{url: url}),
do: {:ok, Discussions.get_comment_from_url_with_preload!(url)} do: {:ok, Discussions.get_comment_from_url_with_preload!(url)}

View File

@ -17,7 +17,7 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier do
alias Mobilizon.Todos.{Todo, TodoList} alias Mobilizon.Todos.{Todo, TodoList}
alias Mobilizon.Federation.ActivityPub alias Mobilizon.Federation.ActivityPub
alias Mobilizon.Federation.ActivityPub.{Activity, Relay, Utils} alias Mobilizon.Federation.ActivityPub.{Activity, Permission, Relay, Utils}
alias Mobilizon.Federation.ActivityPub.Actor, as: ActivityPubActor alias Mobilizon.Federation.ActivityPub.Actor, as: ActivityPubActor
alias Mobilizon.Federation.ActivityPub.Types.Ownable alias Mobilizon.Federation.ActivityPub.Types.Ownable
alias Mobilizon.Federation.ActivityStream.{Converter, Convertible} alias Mobilizon.Federation.ActivityStream.{Converter, Convertible}
@ -409,7 +409,7 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier do
{:origin_check, true} <- {:origin_check, true} <-
{:origin_check, {:origin_check,
Utils.origin_check?(actor_url, update_data) || Utils.origin_check?(actor_url, update_data) ||
Utils.can_update_group_object?(actor, old_event)}, Permission.can_update_group_object?(actor, old_event)},
{:ok, %Activity{} = activity, %Event{} = new_event} <- {:ok, %Activity{} = activity, %Event{} = new_event} <-
ActivityPub.update(old_event, object_data, false) do ActivityPub.update(old_event, object_data, false) do
{:ok, activity, new_event} {:ok, activity, new_event}
@ -454,7 +454,7 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier do
{:origin_check, true} <- {:origin_check, true} <-
{:origin_check, {:origin_check,
Utils.origin_check?(actor_url, update_data["object"]) || Utils.origin_check?(actor_url, update_data["object"]) ||
Utils.can_update_group_object?(actor, old_post)}, Permission.can_update_group_object?(actor, old_post)},
{:ok, %Activity{} = activity, %Post{} = new_post} <- {:ok, %Activity{} = activity, %Post{} = new_post} <-
ActivityPub.update(old_post, object_data, false) do ActivityPub.update(old_post, object_data, false) do
{:ok, activity, new_post} {:ok, activity, new_post}
@ -482,7 +482,7 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier do
{:origin_check, true} <- {:origin_check, true} <-
{:origin_check, {:origin_check,
Utils.origin_check?(actor_url, update_data) || Utils.origin_check?(actor_url, update_data) ||
Utils.can_update_group_object?(actor, old_resource)}, Permission.can_update_group_object?(actor, old_resource)},
{:ok, %Activity{} = activity, %Resource{} = new_resource} <- {:ok, %Activity{} = activity, %Resource{} = new_resource} <-
ActivityPub.update(old_resource, object_data, false) do ActivityPub.update(old_resource, object_data, false) do
{:ok, activity, new_resource} {:ok, activity, new_resource}
@ -585,7 +585,7 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier do
{:origin_check, true} <- {:origin_check, true} <-
{:origin_check, {:origin_check,
Utils.origin_check_from_id?(actor_url, object_id) || Utils.origin_check_from_id?(actor_url, object_id) ||
Utils.can_delete_group_object?(actor, object)}, Permission.can_delete_group_object?(actor, object)},
{:ok, activity, object} <- ActivityPub.delete(object, actor, false) do {:ok, activity, object} <- ActivityPub.delete(object, actor, false) do
{:ok, activity, object} {:ok, activity, object}
else else
@ -629,7 +629,7 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier do
{:origin_check, true} <- {:origin_check, true} <-
{:origin_check, {:origin_check,
Utils.origin_check?(actor_url, data) || Utils.origin_check?(actor_url, data) ||
Utils.can_update_group_object?(actor, old_resource)}, Permission.can_update_group_object?(actor, old_resource)},
{:ok, activity, new_resource} <- ActivityPub.move(:resource, old_resource, object_data) do {:ok, activity, new_resource} <- ActivityPub.move(:resource, old_resource, object_data) do
{:ok, activity, new_resource} {:ok, activity, new_resource}
else else
@ -837,7 +837,7 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier do
# Handle incoming `Accept` activities wrapping a `Join` activity on an event # Handle incoming `Accept` activities wrapping a `Join` activity on an event
defp do_handle_incoming_accept_join(join_object, %Actor{} = actor_accepting) do defp do_handle_incoming_accept_join(join_object, %Actor{} = actor_accepting) do
case get_participant(join_object) do case get_participant(join_object, actor_accepting) do
{:ok, participant} -> {:ok, participant} ->
do_handle_incoming_accept_join_event(participant, actor_accepting) do_handle_incoming_accept_join_event(participant, actor_accepting)
@ -868,9 +868,9 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier do
%Actor{} = actor_accepting %Actor{} = actor_accepting
) )
when role in [:not_approved, :rejected] do when role in [:not_approved, :rejected] do
# TODO: The actor that accepts the Join activity may another one that the event organizer ? with %Event{} = event <- Events.get_event_with_preload!(event.id),
# Or maybe for groups it's the group that sends the Accept activity {:can_accept_event_join, true} <-
with {:same_actor, true} <- {:same_actor, actor_accepting.id == event.organizer_actor_id}, {:can_accept_event_join, can_accept_event_join?(actor_accepting, event)},
{:ok, %Activity{} = activity, %Participant{role: :participant} = participant} <- {:ok, %Activity{} = activity, %Participant{role: :participant} = participant} <-
ActivityPub.accept( ActivityPub.accept(
:join, :join,
@ -881,8 +881,8 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier do
Participation.send_emails_to_local_user(participant) do Participation.send_emails_to_local_user(participant) do
{:ok, activity, participant} {:ok, activity, participant}
else else
{:same_actor} -> {:can_accept_event_join, false} ->
{:error, "Actor who accepted the join wasn't the event organizer. Quite odd."} {:error, "Actor who accepted the join didn't have permission to do so."}
{:ok, %Participant{role: :participant} = _follow} -> {:ok, %Participant{role: :participant} = _follow} ->
{:error, "Participant"} {:error, "Participant"}
@ -902,7 +902,6 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier do
type type
) )
when role in [:not_approved, :rejected, :invited] and type in [:join, :invite] do when role in [:not_approved, :rejected, :invited] and type in [:join, :invite] do
# TODO: The actor that accepts the Join activity may another one that the event organizer ?
# Or maybe for groups it's the group that sends the Accept activity # Or maybe for groups it's the group that sends the Accept activity
with {:ok, %Activity{} = activity, %Member{role: :member} = member} <- with {:ok, %Activity{} = activity, %Member{role: :member} = member} <-
ActivityPub.accept( ActivityPub.accept(
@ -918,7 +917,7 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier do
defp do_handle_incoming_reject_join(join_object, %Actor{} = actor_accepting) do defp do_handle_incoming_reject_join(join_object, %Actor{} = actor_accepting) do
with {:join_event, {:ok, %Participant{event: event, role: role} = participant}} with {:join_event, {:ok, %Participant{event: event, role: role} = participant}}
when role != :rejected <- when role != :rejected <-
{:join_event, get_participant(join_object)}, {:join_event, get_participant(join_object, actor_accepting)},
# TODO: The actor that accepts the Join activity may another one that the event organizer ? # TODO: The actor that accepts the Join activity may another one that the event organizer ?
# Or maybe for groups it's the group that sends the Accept activity # Or maybe for groups it's the group that sends the Accept activity
{:same_actor, true} <- {:same_actor, actor_accepting.id == event.organizer_actor_id}, {:same_actor, true} <- {:same_actor, actor_accepting.id == event.organizer_actor_id},
@ -1026,14 +1025,22 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier do
end end
end end
defp get_participant(join_object) do defp get_participant(join_object, %Actor{} = actor_accepting, loop \\ 1) do
with join_object_id when not is_nil(join_object_id) <- Utils.get_url(join_object), with join_object_id when not is_nil(join_object_id) <- Utils.get_url(join_object),
{:not_found, %Participant{} = participant} <- {:not_found, %Participant{} = participant} <-
{:not_found, Events.get_participant_by_url(join_object_id)} do {:not_found, Events.get_participant_by_url(join_object_id)} do
{:ok, participant} {:ok, participant}
else else
{:not_found, _err} -> {:not_found, _err} ->
{:error, "Participant URL not found"} with true <- is_map(join_object),
true <- loop < 2,
true <- Utils.are_same_origin?(actor_accepting.url, join_object["id"]),
{:ok, _activity, %Participant{url: participant_url}} <- handle_incoming(join_object) do
get_participant(participant_url, actor_accepting, 2)
else
_ ->
{:error, "Participant URL not found"}
end
_ -> _ ->
{:error, "ActivityPub ID not found in Accept Join object"} {:error, "ActivityPub ID not found in Accept Join object"}
@ -1130,4 +1137,22 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier do
_ -> {:error, :remove_object_not_found} _ -> {:error, :remove_object_not_found}
end end
end end
defp can_accept_event_join?(
%Actor{url: actor_url} = actor,
%Event{attributed_to: %Actor{type: :Group, url: group_url} = _group} = event
) do
actor_url == group_url || Permission.can_update_group_object?(actor, event)
end
defp can_accept_event_join?(
%Actor{id: actor_id},
%Event{organizer_actor: %Actor{id: organizer_actor_id}}
) do
organizer_actor_id == actor_id
end
defp can_accept_event_join?(_actor, _event) do
false
end
end end

View File

@ -3,7 +3,7 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Actors do
alias Mobilizon.Actors alias Mobilizon.Actors
alias Mobilizon.Actors.{Actor, Follower, Member} alias Mobilizon.Actors.{Actor, Follower, Member}
alias Mobilizon.Federation.ActivityPub alias Mobilizon.Federation.ActivityPub
alias Mobilizon.Federation.ActivityPub.{Audience, Relay} alias Mobilizon.Federation.ActivityPub.{Audience, Permission, Relay}
alias Mobilizon.Federation.ActivityPub.Types.Entity alias Mobilizon.Federation.ActivityPub.Types.Entity
alias Mobilizon.Federation.ActivityStream.Convertible alias Mobilizon.Federation.ActivityStream.Convertible
alias Mobilizon.GraphQL.API.Utils, as: APIUtils alias Mobilizon.GraphQL.API.Utils, as: APIUtils
@ -47,7 +47,7 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Actors do
actor_as_data <- Convertible.model_to_as(new_actor), actor_as_data <- Convertible.model_to_as(new_actor),
{:ok, true} <- Cachex.del(:activity_pub, "actor_#{new_actor.preferred_username}"), {:ok, true} <- Cachex.del(:activity_pub, "actor_#{new_actor.preferred_username}"),
audience <- audience <-
Audience.calculate_to_and_cc_from_mentions(new_actor), Audience.get_audience(new_actor),
additional <- Map.merge(additional, %{"actor" => old_actor.url}), additional <- Map.merge(additional, %{"actor" => old_actor.url}),
update_data <- make_update_data(actor_as_data, Map.merge(audience, additional)) do update_data <- make_update_data(actor_as_data, Map.merge(audience, additional)) do
{:ok, new_actor, update_data} {:ok, new_actor, update_data}
@ -104,8 +104,14 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Actors do
def group_actor(%Actor{} = actor), do: actor def group_actor(%Actor{} = actor), do: actor
def role_needed_to_update(%Actor{} = _group), do: :administrator def permissions(%Actor{} = _group) do
def role_needed_to_delete(%Actor{} = _group), do: :administrator %Permission{
access: :member,
create: nil,
update: :administrator,
delete: :administrator
}
end
@spec join(Actor.t(), Actor.t(), boolean(), map()) :: {:ok, map(), Member.t()} @spec join(Actor.t(), Actor.t(), boolean(), map()) :: {:ok, map(), Member.t()}
def join(%Actor{type: :Group} = group, %Actor{} = actor, _local, additional) do def join(%Actor{type: :Group} = group, %Actor{} = actor, _local, additional) do
@ -136,7 +142,7 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Actors do
"object" => group.url "object" => group.url
}, },
audience <- audience <-
Audience.calculate_to_and_cc_from_mentions(member) do Audience.get_audience(member) do
approve_if_default_role_is_member( approve_if_default_role_is_member(
group, group,
actor, actor,

View File

@ -4,7 +4,7 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Comments do
alias Mobilizon.Actors.Actor alias Mobilizon.Actors.Actor
alias Mobilizon.Discussions.{Comment, Discussion} alias Mobilizon.Discussions.{Comment, Discussion}
alias Mobilizon.Events.{Event, EventOptions} alias Mobilizon.Events.{Event, EventOptions}
alias Mobilizon.Federation.ActivityPub.Audience alias Mobilizon.Federation.ActivityPub.{Audience, Permission}
alias Mobilizon.Federation.ActivityPub.Types.Entity alias Mobilizon.Federation.ActivityPub.Types.Entity
alias Mobilizon.Federation.ActivityStream.Converter.Utils, as: ConverterUtils alias Mobilizon.Federation.ActivityStream.Converter.Utils, as: ConverterUtils
alias Mobilizon.Federation.ActivityStream.Convertible alias Mobilizon.Federation.ActivityStream.Convertible
@ -32,7 +32,7 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Comments do
:ok <- maybe_publish_graphql_subscription(discussion_id), :ok <- maybe_publish_graphql_subscription(discussion_id),
comment_as_data <- Convertible.model_to_as(comment), comment_as_data <- Convertible.model_to_as(comment),
audience <- audience <-
Audience.calculate_to_and_cc_from_mentions(comment), Audience.get_audience(comment),
create_data <- create_data <-
make_create_data(comment_as_data, Map.merge(audience, additional)) do make_create_data(comment_as_data, Map.merge(audience, additional)) do
{:ok, comment, create_data} {:ok, comment, create_data}
@ -47,7 +47,7 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Comments do
{:ok, true} <- Cachex.del(:activity_pub, "comment_#{new_comment.uuid}"), {:ok, true} <- Cachex.del(:activity_pub, "comment_#{new_comment.uuid}"),
comment_as_data <- Convertible.model_to_as(new_comment), comment_as_data <- Convertible.model_to_as(new_comment),
audience <- audience <-
Audience.calculate_to_and_cc_from_mentions(new_comment), Audience.get_audience(new_comment),
update_data <- make_update_data(comment_as_data, Map.merge(audience, additional)) do update_data <- make_update_data(comment_as_data, Map.merge(audience, additional)) do
{:ok, new_comment, update_data} {:ok, new_comment, update_data}
else else
@ -79,7 +79,7 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Comments do
force_deletion = Map.get(options, :force, false) force_deletion = Map.get(options, :force, false)
with audience <- with audience <-
Audience.calculate_to_and_cc_from_mentions(comment), Audience.get_audience(comment),
{:ok, %Comment{} = updated_comment} <- {:ok, %Comment{} = updated_comment} <-
Discussions.delete_comment(comment, force: force_deletion), Discussions.delete_comment(comment, force: force_deletion),
{:ok, true} <- Cachex.del(:activity_pub, "comment_#{comment.uuid}"), {:ok, true} <- Cachex.del(:activity_pub, "comment_#{comment.uuid}"),
@ -104,8 +104,13 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Comments do
def group_actor(_), do: nil def group_actor(_), do: nil
def role_needed_to_update(%Comment{attributed_to: %Actor{} = _group}), do: :administrator def permissions(%Comment{}),
def role_needed_to_delete(%Comment{attributed_to_id: _attributed_to_id}), do: :administrator do: %Permission{
access: :member,
create: :member,
update: :administrator,
delete: :administrator
}
# Prepare and sanitize arguments for comments # Prepare and sanitize arguments for comments
defp prepare_args_for_comment(args) do defp prepare_args_for_comment(args) do

View File

@ -4,7 +4,7 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Discussions do
alias Mobilizon.{Actors, Discussions} alias Mobilizon.{Actors, Discussions}
alias Mobilizon.Actors.Actor alias Mobilizon.Actors.Actor
alias Mobilizon.Discussions.{Comment, Discussion} alias Mobilizon.Discussions.{Comment, Discussion}
alias Mobilizon.Federation.ActivityPub.Audience alias Mobilizon.Federation.ActivityPub.{Audience, Permission}
alias Mobilizon.Federation.ActivityPub.Types.Entity alias Mobilizon.Federation.ActivityPub.Types.Entity
alias Mobilizon.Federation.ActivityStream.Convertible alias Mobilizon.Federation.ActivityStream.Convertible
alias Mobilizon.GraphQL.API.Utils, as: APIUtils alias Mobilizon.GraphQL.API.Utils, as: APIUtils
@ -31,7 +31,7 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Discussions do
:ok <- maybe_publish_graphql_subscription(discussion), :ok <- maybe_publish_graphql_subscription(discussion),
comment_as_data <- Convertible.model_to_as(last_comment), comment_as_data <- Convertible.model_to_as(last_comment),
audience <- audience <-
Audience.calculate_to_and_cc_from_mentions(discussion), Audience.get_audience(discussion),
create_data <- create_data <-
make_create_data(comment_as_data, Map.merge(audience, additional)) do make_create_data(comment_as_data, Map.merge(audience, additional)) do
{:ok, discussion, create_data} {:ok, discussion, create_data}
@ -48,7 +48,7 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Discussions do
DiscussionActivity.insert_activity(discussion, subject: "discussion_created"), DiscussionActivity.insert_activity(discussion, subject: "discussion_created"),
discussion_as_data <- Convertible.model_to_as(discussion), discussion_as_data <- Convertible.model_to_as(discussion),
audience <- audience <-
Audience.calculate_to_and_cc_from_mentions(discussion), Audience.get_audience(discussion),
create_data <- create_data <-
make_create_data(discussion_as_data, Map.merge(audience, additional)) do make_create_data(discussion_as_data, Map.merge(audience, additional)) do
{:ok, discussion, create_data} {:ok, discussion, create_data}
@ -68,7 +68,7 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Discussions do
{:ok, true} <- Cachex.del(:activity_pub, "discussion_#{new_discussion.slug}"), {:ok, true} <- Cachex.del(:activity_pub, "discussion_#{new_discussion.slug}"),
discussion_as_data <- Convertible.model_to_as(new_discussion), discussion_as_data <- Convertible.model_to_as(new_discussion),
audience <- audience <-
Audience.calculate_to_and_cc_from_mentions(new_discussion), Audience.get_audience(new_discussion),
update_data <- make_update_data(discussion_as_data, Map.merge(audience, additional)) do update_data <- make_update_data(discussion_as_data, Map.merge(audience, additional)) do
{:ok, new_discussion, update_data} {:ok, new_discussion, update_data}
else else
@ -110,8 +110,9 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Discussions do
def group_actor(%Discussion{actor_id: actor_id}), do: Actors.get_actor(actor_id) def group_actor(%Discussion{actor_id: actor_id}), do: Actors.get_actor(actor_id)
def role_needed_to_update(%Discussion{}), do: :moderator def permissions(%Discussion{}) do
def role_needed_to_delete(%Discussion{}), do: :moderator %Permission{access: :member, create: :member, update: :moderator, delete: :moderator}
end
@spec maybe_publish_graphql_subscription(Discussion.t()) :: :ok @spec maybe_publish_graphql_subscription(Discussion.t()) :: :ok
defp maybe_publish_graphql_subscription(%Discussion{} = discussion) do defp maybe_publish_graphql_subscription(%Discussion{} = discussion) do

View File

@ -17,6 +17,7 @@ alias Mobilizon.Federation.ActivityPub.Types.{
alias Mobilizon.Actors.{Actor, Member} alias Mobilizon.Actors.{Actor, Member}
alias Mobilizon.Events.Event alias Mobilizon.Events.Event
alias Mobilizon.Discussions.{Comment, Discussion} alias Mobilizon.Discussions.{Comment, Discussion}
alias Mobilizon.Federation.ActivityPub.Permission
alias Mobilizon.Posts.Post alias Mobilizon.Posts.Post
alias Mobilizon.Resources.Resource alias Mobilizon.Resources.Resource
alias Mobilizon.Todos.{Todo, TodoList} alias Mobilizon.Todos.{Todo, TodoList}
@ -67,11 +68,8 @@ defprotocol Mobilizon.Federation.ActivityPub.Types.Ownable do
@doc "Returns the actor for the entity" @doc "Returns the actor for the entity"
def actor(entity) def actor(entity)
@spec role_needed_to_update(Entity.t()) :: group_role() @spec permissions(Entity.t()) :: Permission.t()
def role_needed_to_update(entity) def permissions(entity)
@spec role_needed_to_delete(Entity.t()) :: group_role()
def role_needed_to_delete(entity)
end end
defimpl Managable, for: Event do defimpl Managable, for: Event do
@ -82,8 +80,7 @@ end
defimpl Ownable, for: Event do defimpl Ownable, for: Event do
defdelegate group_actor(entity), to: Events defdelegate group_actor(entity), to: Events
defdelegate actor(entity), to: Events defdelegate actor(entity), to: Events
defdelegate role_needed_to_update(entity), to: Events defdelegate permissions(entity), to: Events
defdelegate role_needed_to_delete(entity), to: Events
end end
defimpl Managable, for: Comment do defimpl Managable, for: Comment do
@ -94,8 +91,7 @@ end
defimpl Ownable, for: Comment do defimpl Ownable, for: Comment do
defdelegate group_actor(entity), to: Comments defdelegate group_actor(entity), to: Comments
defdelegate actor(entity), to: Comments defdelegate actor(entity), to: Comments
defdelegate role_needed_to_update(entity), to: Comments defdelegate permissions(entity), to: Comments
defdelegate role_needed_to_delete(entity), to: Comments
end end
defimpl Managable, for: Post do defimpl Managable, for: Post do
@ -106,8 +102,7 @@ end
defimpl Ownable, for: Post do defimpl Ownable, for: Post do
defdelegate group_actor(entity), to: Posts defdelegate group_actor(entity), to: Posts
defdelegate actor(entity), to: Posts defdelegate actor(entity), to: Posts
defdelegate role_needed_to_update(entity), to: Posts defdelegate permissions(entity), to: Posts
defdelegate role_needed_to_delete(entity), to: Posts
end end
defimpl Managable, for: Actor do defimpl Managable, for: Actor do
@ -118,8 +113,7 @@ end
defimpl Ownable, for: Actor do defimpl Ownable, for: Actor do
defdelegate group_actor(entity), to: Actors defdelegate group_actor(entity), to: Actors
defdelegate actor(entity), to: Actors defdelegate actor(entity), to: Actors
defdelegate role_needed_to_update(entity), to: Actors defdelegate permissions(entity), to: Actors
defdelegate role_needed_to_delete(entity), to: Actors
end end
defimpl Managable, for: TodoList do defimpl Managable, for: TodoList do
@ -130,8 +124,7 @@ end
defimpl Ownable, for: TodoList do defimpl Ownable, for: TodoList do
defdelegate group_actor(entity), to: TodoLists defdelegate group_actor(entity), to: TodoLists
defdelegate actor(entity), to: TodoLists defdelegate actor(entity), to: TodoLists
defdelegate role_needed_to_update(entity), to: TodoLists defdelegate permissions(entity), to: TodoLists
defdelegate role_needed_to_delete(entity), to: TodoLists
end end
defimpl Managable, for: Todo do defimpl Managable, for: Todo do
@ -142,8 +135,7 @@ end
defimpl Ownable, for: Todo do defimpl Ownable, for: Todo do
defdelegate group_actor(entity), to: Todos defdelegate group_actor(entity), to: Todos
defdelegate actor(entity), to: Todos defdelegate actor(entity), to: Todos
defdelegate role_needed_to_update(entity), to: Todos defdelegate permissions(entity), to: Todos
defdelegate role_needed_to_delete(entity), to: Todos
end end
defimpl Managable, for: Resource do defimpl Managable, for: Resource do
@ -154,8 +146,7 @@ end
defimpl Ownable, for: Resource do defimpl Ownable, for: Resource do
defdelegate group_actor(entity), to: Resources defdelegate group_actor(entity), to: Resources
defdelegate actor(entity), to: Resources defdelegate actor(entity), to: Resources
defdelegate role_needed_to_update(entity), to: Resources defdelegate permissions(entity), to: Resources
defdelegate role_needed_to_delete(entity), to: Resources
end end
defimpl Managable, for: Discussion do defimpl Managable, for: Discussion do
@ -166,15 +157,13 @@ end
defimpl Ownable, for: Discussion do defimpl Ownable, for: Discussion do
defdelegate group_actor(entity), to: Discussions defdelegate group_actor(entity), to: Discussions
defdelegate actor(entity), to: Discussions defdelegate actor(entity), to: Discussions
defdelegate role_needed_to_update(entity), to: Discussions defdelegate permissions(entity), to: Discussions
defdelegate role_needed_to_delete(entity), to: Discussions
end end
defimpl Ownable, for: Tombstone do defimpl Ownable, for: Tombstone do
defdelegate group_actor(entity), to: Tombstones defdelegate group_actor(entity), to: Tombstones
defdelegate actor(entity), to: Tombstones defdelegate actor(entity), to: Tombstones
defdelegate role_needed_to_update(entity), to: Tombstones defdelegate permissions(entity), to: Tombstones
defdelegate role_needed_to_delete(entity), to: Tombstones
end end
defimpl Managable, for: Member do defimpl Managable, for: Member do

View File

@ -5,7 +5,7 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Events do
alias Mobilizon.Events, as: EventsManager alias Mobilizon.Events, as: EventsManager
alias Mobilizon.Events.{Event, Participant} alias Mobilizon.Events.{Event, Participant}
alias Mobilizon.Federation.ActivityPub alias Mobilizon.Federation.ActivityPub
alias Mobilizon.Federation.ActivityPub.Audience alias Mobilizon.Federation.ActivityPub.{Audience, Permission}
alias Mobilizon.Federation.ActivityPub.Types.Entity alias Mobilizon.Federation.ActivityPub.Types.Entity
alias Mobilizon.Federation.ActivityStream.Converter.Utils, as: ConverterUtils alias Mobilizon.Federation.ActivityStream.Converter.Utils, as: ConverterUtils
alias Mobilizon.Federation.ActivityStream.Convertible alias Mobilizon.Federation.ActivityStream.Convertible
@ -29,7 +29,7 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Events do
EventActivity.insert_activity(event, subject: "event_created"), EventActivity.insert_activity(event, subject: "event_created"),
event_as_data <- Convertible.model_to_as(event), event_as_data <- Convertible.model_to_as(event),
audience <- audience <-
Audience.calculate_to_and_cc_from_mentions(event), Audience.get_audience(event),
create_data <- create_data <-
make_create_data(event_as_data, Map.merge(audience, additional)) do make_create_data(event_as_data, Map.merge(audience, additional)) do
{:ok, event, create_data} {:ok, event, create_data}
@ -46,7 +46,7 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Events do
{:ok, true} <- Cachex.del(:activity_pub, "event_#{new_event.uuid}"), {:ok, true} <- Cachex.del(:activity_pub, "event_#{new_event.uuid}"),
event_as_data <- Convertible.model_to_as(new_event), event_as_data <- Convertible.model_to_as(new_event),
audience <- audience <-
Audience.calculate_to_and_cc_from_mentions(new_event), Audience.get_audience(new_event),
update_data <- make_update_data(event_as_data, Map.merge(audience, additional)) do update_data <- make_update_data(event_as_data, Map.merge(audience, additional)) do
{:ok, new_event, update_data} {:ok, new_event, update_data}
else else
@ -69,7 +69,7 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Events do
} }
with audience <- with audience <-
Audience.calculate_to_and_cc_from_mentions(event), Audience.get_audience(event),
{:ok, %Event{} = event} <- EventsManager.delete_event(event), {:ok, %Event{} = event} <- EventsManager.delete_event(event),
{:ok, _} <- {:ok, _} <-
EventActivity.insert_activity(event, subject: "event_deleted"), EventActivity.insert_activity(event, subject: "event_deleted"),
@ -95,9 +95,14 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Events do
def group_actor(_), do: nil def group_actor(_), do: nil
def role_needed_to_update(%Event{attributed_to: %Actor{} = _group}), do: :moderator def permissions(%Event{draft: draft, attributed_to_id: _attributed_to_id}) do
def role_needed_to_delete(%Event{attributed_to_id: _attributed_to_id}), do: :moderator %Permission{
def role_needed_to_delete(_), do: nil access: if(draft, do: nil, else: :member),
create: :moderator,
update: :moderator,
delete: :moderator
}
end
def join(%Event{} = event, %Actor{} = actor, _local, additional) do def join(%Event{} = event, %Actor{} = actor, _local, additional) do
with {:maximum_attendee_capacity, true} <- with {:maximum_attendee_capacity, true} <-
@ -119,7 +124,7 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Events do
}), }),
join_data <- Convertible.model_to_as(participant), join_data <- Convertible.model_to_as(participant),
audience <- audience <-
Audience.calculate_to_and_cc_from_mentions(participant) do Audience.get_audience(participant) do
approve_if_default_role_is_participant( approve_if_default_role_is_participant(
event, event,
Map.merge(join_data, audience), Map.merge(join_data, audience),
@ -142,28 +147,48 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Events do
# Set the participant to approved if the default role for new participants is :participant # Set the participant to approved if the default role for new participants is :participant
defp approve_if_default_role_is_participant(event, activity_data, participant, role) do defp approve_if_default_role_is_participant(event, activity_data, participant, role) do
if event.local do case event do
cond do %Event{attributed_to: %Actor{id: group_id, url: group_url}} ->
Mobilizon.Events.get_default_participant_role(event) === :participant && case Actors.get_single_group_moderator_actor(group_id) do
role == :participant -> %Actor{} = actor ->
{:accept, do_approve(event, activity_data, participant, role, %{
ActivityPub.accept( "actor" => actor.url,
:join, "attributedTo" => group_url
participant, })
true,
%{"actor" => event.organizer_actor.url}
)}
Mobilizon.Events.get_default_participant_role(event) === :not_approved && _ ->
role == :not_approved -> {:ok, activity_data, participant}
Scheduler.pending_participation_notification(event) end
{:ok, activity_data, participant}
true -> %Event{local: true} ->
{:ok, activity_data, participant} do_approve(event, activity_data, participant, role, %{
end "actor" => event.organizer_actor.url
else })
{:ok, activity_data, participant}
_ ->
{:ok, activity_data, participant}
end
end
defp do_approve(event, activity_data, participant, role, additionnal) do
cond do
Mobilizon.Events.get_default_participant_role(event) === :participant &&
role == :participant ->
{:accept,
ActivityPub.accept(
:join,
participant,
true,
additionnal
)}
Mobilizon.Events.get_default_participant_role(event) === :not_approved &&
role == :not_approved ->
Scheduler.pending_participation_notification(event)
{:ok, activity_data, participant}
true ->
{:ok, activity_data, participant}
end end
end end

View File

@ -2,7 +2,7 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Posts do
@moduledoc false @moduledoc false
alias Mobilizon.{Actors, Posts, Tombstone} alias Mobilizon.{Actors, Posts, Tombstone}
alias Mobilizon.Actors.Actor alias Mobilizon.Actors.Actor
alias Mobilizon.Federation.ActivityPub.Audience alias Mobilizon.Federation.ActivityPub.{Audience, Permission}
alias Mobilizon.Federation.ActivityPub.Types.Entity alias Mobilizon.Federation.ActivityPub.Types.Entity
alias Mobilizon.Federation.ActivityStream.Converter.Utils, as: ConverterUtils alias Mobilizon.Federation.ActivityStream.Converter.Utils, as: ConverterUtils
alias Mobilizon.Federation.ActivityStream.Convertible alias Mobilizon.Federation.ActivityStream.Convertible
@ -47,7 +47,7 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Posts do
post_as_data <- post_as_data <-
Convertible.model_to_as(%{post | attributed_to: group, author: creator}), Convertible.model_to_as(%{post | attributed_to: group, author: creator}),
audience <- audience <-
Audience.calculate_to_and_cc_from_mentions(post) do Audience.get_audience(post) do
update_data = make_update_data(post_as_data, Map.merge(audience, additional)) update_data = make_update_data(post_as_data, Map.merge(audience, additional))
{:ok, post, update_data} {:ok, post, update_data}
@ -91,6 +91,12 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Posts do
def group_actor(%Post{attributed_to_id: attributed_to_id}), def group_actor(%Post{attributed_to_id: attributed_to_id}),
do: Actors.get_actor(attributed_to_id) do: Actors.get_actor(attributed_to_id)
def role_needed_to_update(%Post{}), do: :moderator def permissions(%Post{}) do
def role_needed_to_delete(%Post{}), do: :moderator %Permission{
access: :member,
create: :moderator,
update: :moderator,
delete: :moderator
}
end
end end

View File

@ -2,6 +2,7 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Resources do
@moduledoc false @moduledoc false
alias Mobilizon.{Actors, Resources} alias Mobilizon.{Actors, Resources}
alias Mobilizon.Actors.Actor alias Mobilizon.Actors.Actor
alias Mobilizon.Federation.ActivityPub.Permission
alias Mobilizon.Federation.ActivityPub.Types.Entity alias Mobilizon.Federation.ActivityPub.Types.Entity
alias Mobilizon.Federation.ActivityStream.Convertible alias Mobilizon.Federation.ActivityStream.Convertible
alias Mobilizon.Resources.Resource alias Mobilizon.Resources.Resource
@ -170,6 +171,7 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Resources do
def group_actor(%Resource{actor_id: actor_id}), do: Actors.get_actor(actor_id) def group_actor(%Resource{actor_id: actor_id}), do: Actors.get_actor(actor_id)
def role_needed_to_update(%Resource{}), do: :member def permissions(%Resource{}) do
def role_needed_to_delete(%Resource{}), do: :member %Permission{access: :member, create: :member, update: :member, delete: :member}
end
end end

View File

@ -2,6 +2,7 @@ defmodule Mobilizon.Federation.ActivityPub.Types.TodoLists do
@moduledoc false @moduledoc false
alias Mobilizon.{Actors, Todos} alias Mobilizon.{Actors, Todos}
alias Mobilizon.Actors.Actor alias Mobilizon.Actors.Actor
alias Mobilizon.Federation.ActivityPub.Permission
alias Mobilizon.Federation.ActivityPub.Types.Entity alias Mobilizon.Federation.ActivityPub.Types.Entity
alias Mobilizon.Federation.ActivityStream alias Mobilizon.Federation.ActivityStream
alias Mobilizon.Federation.ActivityStream.Convertible alias Mobilizon.Federation.ActivityStream.Convertible
@ -68,6 +69,7 @@ defmodule Mobilizon.Federation.ActivityPub.Types.TodoLists do
def group_actor(%TodoList{actor_id: actor_id}), do: Actors.get_actor(actor_id) def group_actor(%TodoList{actor_id: actor_id}), do: Actors.get_actor(actor_id)
def role_needed_to_update(%TodoList{}), do: :member def permissions(%TodoList{}) do
def role_needed_to_delete(%TodoList{}), do: :member %Permission{access: :member, create: :member, update: :member, delete: :member}
end
end end

View File

@ -2,6 +2,7 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Todos do
@moduledoc false @moduledoc false
alias Mobilizon.{Actors, Todos} alias Mobilizon.{Actors, Todos}
alias Mobilizon.Actors.Actor alias Mobilizon.Actors.Actor
alias Mobilizon.Federation.ActivityPub.Permission
alias Mobilizon.Federation.ActivityPub.Types.Entity alias Mobilizon.Federation.ActivityPub.Types.Entity
alias Mobilizon.Federation.ActivityStream.Convertible alias Mobilizon.Federation.ActivityStream.Convertible
alias Mobilizon.Todos.{Todo, TodoList} alias Mobilizon.Todos.{Todo, TodoList}
@ -80,6 +81,7 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Todos do
end end
end end
def role_needed_to_update(%Todo{}), do: :member def permissions(%Todo{}) do
def role_needed_to_delete(%Todo{}), do: :member %Permission{access: :member, create: :member, update: :member, delete: :member}
end
end end

View File

@ -2,6 +2,7 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Tombstones do
@moduledoc false @moduledoc false
alias Mobilizon.{Actors, Tombstone} alias Mobilizon.{Actors, Tombstone}
alias Mobilizon.Actors.Actor alias Mobilizon.Actors.Actor
alias Mobilizon.Federation.ActivityPub.Permission
def actor(%Tombstone{actor: %Actor{id: actor_id}}), do: Actors.get_actor(actor_id) def actor(%Tombstone{actor: %Actor{id: actor_id}}), do: Actors.get_actor(actor_id)
@ -12,6 +13,7 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Tombstones do
def group_actor(_), do: nil def group_actor(_), do: nil
def role_needed_to_update(%Actor{}), do: nil def permissions(_) do
def role_needed_to_delete(%Actor{}), do: nil %Permission{access: nil, create: nil, update: nil, delete: nil}
end
end end

View File

@ -15,7 +15,6 @@ defmodule Mobilizon.Federation.ActivityPub.Utils do
alias Mobilizon.Federation.ActivityPub alias Mobilizon.Federation.ActivityPub
alias Mobilizon.Federation.ActivityPub.{Activity, Federator, Relay} alias Mobilizon.Federation.ActivityPub.{Activity, Federator, Relay}
alias Mobilizon.Federation.ActivityPub.Actor, as: ActivityPubActor alias Mobilizon.Federation.ActivityPub.Actor, as: ActivityPubActor
alias Mobilizon.Federation.ActivityPub.Types.Ownable
alias Mobilizon.Federation.ActivityStream.Converter alias Mobilizon.Federation.ActivityStream.Converter
alias Mobilizon.Federation.HTTPSignatures alias Mobilizon.Federation.HTTPSignatures
alias Mobilizon.Web.Endpoint alias Mobilizon.Web.Endpoint
@ -291,43 +290,6 @@ defmodule Mobilizon.Federation.ActivityPub.Utils do
def origin_check_from_id?(id, %{"id" => other_id} = _params) when is_binary(other_id), def origin_check_from_id?(id, %{"id" => other_id} = _params) when is_binary(other_id),
do: origin_check_from_id?(id, other_id) do: origin_check_from_id?(id, other_id)
def activity_actor_is_group_member?(
%Actor{id: actor_id, url: actor_url},
object,
role \\ :member
) do
case Ownable.group_actor(object) do
%Actor{type: :Group, id: group_id, url: group_url} ->
Logger.debug("Group object url is #{group_url}")
case role do
:moderator ->
Logger.debug(
"Checking if activity actor #{actor_url} is a moderator from group from #{object.url}"
)
Actors.is_moderator?(actor_id, group_id)
:administrator ->
Logger.debug(
"Checking if activity actor #{actor_url} is an administrator from group from #{object.url}"
)
Actors.is_administrator?(actor_id, group_id)
_ ->
Logger.debug(
"Checking if activity actor #{actor_url} is a member from group from #{object.url}"
)
Actors.is_member?(actor_id, group_id)
end
_ ->
false
end
end
@doc """ @doc """
Return AS Link data from Return AS Link data from
@ -514,6 +476,7 @@ defmodule Mobilizon.Federation.ActivityPub.Utils do
"type" => "Update", "type" => "Update",
"to" => object["to"], "to" => object["to"],
"cc" => object["cc"], "cc" => object["cc"],
"attributedTo" => object["attributedTo"] || object["actor"],
"actor" => object["actor"], "actor" => object["actor"],
"object" => object, "object" => object,
"id" => object["id"] <> "/activity" "id" => object["id"] <> "/activity"
@ -662,41 +625,6 @@ defmodule Mobilizon.Federation.ActivityPub.Utils do
:ok :ok
end end
def can_update_group_object?(%Actor{} = actor, object) do
can_manage_group_object?(:role_needed_to_update, actor, object)
end
def can_delete_group_object?(%Actor{} = actor, object) do
can_manage_group_object?(:role_needed_to_delete, actor, object)
end
@spec can_manage_group_object?(
:role_needed_to_update | :role_needed_to_delete,
Actor.t(),
any()
) :: boolean()
defp can_manage_group_object?(action_function, %Actor{url: actor_url} = actor, object) do
if Ownable.group_actor(object) != nil do
case apply(Ownable, action_function, [object]) do
role when role in [:member, :moderator, :administrator] ->
activity_actor_is_group_member?(actor, object, role)
_ ->
case action_function do
:role_needed_to_update ->
Logger.warn("Actor #{actor_url} can't update #{object.url}")
:role_needed_to_delete ->
Logger.warn("Actor #{actor_url} can't delete #{object.url}")
end
false
end
else
true
end
end
@spec label_in_collection?(any(), any()) :: boolean() @spec label_in_collection?(any(), any()) :: boolean()
defp label_in_collection?(url, coll) when is_binary(coll), do: url == coll defp label_in_collection?(url, coll) when is_binary(coll), do: url == coll
defp label_in_collection?(url, coll) when is_list(coll), do: url in coll defp label_in_collection?(url, coll) when is_list(coll), do: url in coll

View File

@ -14,6 +14,7 @@ defmodule Mobilizon.Federation.ActivityStream.Converter.Event do
alias Mobilizon.Federation.ActivityStream.{Converter, Convertible} alias Mobilizon.Federation.ActivityStream.{Converter, Convertible}
alias Mobilizon.Federation.ActivityStream.Converter.Address, as: AddressConverter alias Mobilizon.Federation.ActivityStream.Converter.Address, as: AddressConverter
alias Mobilizon.Federation.ActivityStream.Converter.Media, as: MediaConverter alias Mobilizon.Federation.ActivityStream.Converter.Media, as: MediaConverter
alias Mobilizon.Web.Endpoint
import Mobilizon.Federation.ActivityStream.Converter.Utils, import Mobilizon.Federation.ActivityStream.Converter.Utils,
only: [ only: [
@ -36,6 +37,7 @@ defmodule Mobilizon.Federation.ActivityStream.Converter.Event do
@online_address_name "Website" @online_address_name "Website"
@banner_picture_name "Banner" @banner_picture_name "Banner"
@ap_public "https://www.w3.org/ns/activitystreams#Public"
@doc """ @doc """
Converts an AP object data to our internal data structure. Converts an AP object data to our internal data structure.
@ -43,7 +45,7 @@ defmodule Mobilizon.Federation.ActivityStream.Converter.Event do
@impl Converter @impl Converter
@spec as_to_model_data(map) :: {:ok, map()} | {:error, any()} @spec as_to_model_data(map) :: {:ok, map()} | {:error, any()}
def as_to_model_data(object) do def as_to_model_data(object) do
with {%Actor{id: actor_id, domain: actor_domain}, attributed_to} <- with {%Actor{id: actor_id}, attributed_to} <-
maybe_fetch_actor_and_attributed_to_id(object), maybe_fetch_actor_and_attributed_to_id(object),
{:address, address_id} <- {:address, address_id} <-
{:address, get_address(object["location"])}, {:address, get_address(object["location"])},
@ -65,7 +67,7 @@ defmodule Mobilizon.Federation.ActivityStream.Converter.Event do
category: object["category"], category: object["category"],
visibility: visibility, visibility: visibility,
join_options: Map.get(object, "joinMode", "free"), join_options: Map.get(object, "joinMode", "free"),
local: is_nil(actor_domain), local: is_local(object["id"]),
options: options, options: options,
status: object |> Map.get("ical:status", "CONFIRMED") |> String.downcase(), status: object |> Map.get("ical:status", "CONFIRMED") |> String.downcase(),
online_address: object |> Map.get("attachment", []) |> get_online_address(), online_address: object |> Map.get("attachment", []) |> get_online_address(),
@ -91,15 +93,15 @@ defmodule Mobilizon.Federation.ActivityStream.Converter.Event do
@impl Converter @impl Converter
@spec model_to_as(EventModel.t()) :: map @spec model_to_as(EventModel.t()) :: map
def model_to_as(%EventModel{} = event) do def model_to_as(%EventModel{} = event) do
to = {to, cc} =
if event.visibility == :public, if event.visibility == :public,
do: ["https://www.w3.org/ns/activitystreams#Public"], do: {[@ap_public], []},
else: [attributed_to_or_default(event).followers_url] else: {[attributed_to_or_default(event).followers_url], [@ap_public]}
%{ %{
"type" => "Event", "type" => "Event",
"to" => to, "to" => to,
"cc" => [], "cc" => cc,
"attributedTo" => attributed_to_or_default(event).url, "attributedTo" => attributed_to_or_default(event).url,
"name" => event.title, "name" => event.title,
"actor" => "actor" =>
@ -274,4 +276,10 @@ defmodule Mobilizon.Federation.ActivityStream.Converter.Event do
&(&1 ++ medias) &(&1 ++ medias)
) )
end end
defp is_local(url) do
%URI{host: url_domain} = URI.parse(url)
%URI{host: local_domain} = URI.parse(Endpoint.url())
url_domain == local_domain
end
end end

View File

@ -41,7 +41,7 @@ defmodule Mobilizon.Federation.ActivityStream.Converter.Post do
} }
} = post } = post
) do ) do
audience = Audience.calculate_to_and_cc_from_mentions(post) audience = Audience.get_audience(post)
%{ %{
"type" => "Article", "type" => "Article",
@ -65,10 +65,11 @@ defmodule Mobilizon.Federation.ActivityStream.Converter.Post do
@impl Converter @impl Converter
@spec as_to_model_data(map) :: {:ok, map} | {:error, any()} @spec as_to_model_data(map) :: {:ok, map} | {:error, any()}
def as_to_model_data( def as_to_model_data(
%{"type" => "Article", "actor" => creator, "attributedTo" => group} = object %{"type" => "Article", "actor" => creator, "attributedTo" => group_uri} = object
) do ) do
with {:ok, %Actor{id: attributed_to_id}} <- get_actor(group), with {:ok, %Actor{id: attributed_to_id} = group} <- get_actor(group_uri),
{:ok, %Actor{id: author_id}} <- get_actor(creator), {:ok, %Actor{id: author_id}} <- get_actor(creator),
{:visibility, visibility} <- {:visibility, get_visibility(object, group)},
[description: description, picture_id: picture_id, medias: medias] <- [description: description, picture_id: picture_id, medias: medias] <-
process_pictures(object, attributed_to_id) do process_pictures(object, attributed_to_id) do
%{ %{
@ -81,6 +82,7 @@ defmodule Mobilizon.Federation.ActivityStream.Converter.Post do
publish_at: object["published"], publish_at: object["published"],
picture_id: picture_id, picture_id: picture_id,
medias: medias, medias: medias,
visibility: visibility,
draft: object["draft"] == true draft: object["draft"] == true
} }
else else
@ -128,4 +130,17 @@ defmodule Mobilizon.Federation.ActivityStream.Converter.Post do
&(&1 ++ medias) &(&1 ++ medias)
) )
end end
@ap_public "https://www.w3.org/ns/activitystreams#Public"
defp get_visibility(%{"to" => to}, %Actor{
followers_url: followers_url,
members_url: members_url
}) do
cond do
@ap_public in to -> :public
followers_url in to -> :unlisted
members_url in to -> :private
end
end
end end

View File

@ -22,7 +22,7 @@ defmodule Mobilizon.GraphQL.API.Events do
process_picture(picture, organizer_actor) process_picture(picture, organizer_actor)
end) do end) do
# For now we don't federate drafts but it will be needed if we want to edit them as groups # For now we don't federate drafts but it will be needed if we want to edit them as groups
ActivityPub.create(:event, args, args.draft == false) ActivityPub.create(:event, args, should_federate(args))
end end
end end
@ -37,7 +37,7 @@ defmodule Mobilizon.GraphQL.API.Events do
Map.update(args, :picture, nil, fn picture -> Map.update(args, :picture, nil, fn picture ->
process_picture(picture, organizer_actor) process_picture(picture, organizer_actor)
end) do end) do
ActivityPub.update(event, args, Map.get(args, :draft, false) == false) ActivityPub.update(event, args, should_federate(args))
end end
end end
@ -74,4 +74,9 @@ defmodule Mobilizon.GraphQL.API.Events do
end end
defp extract_pictures_from_event_body(args, _), do: args defp extract_pictures_from_event_body(args, _), do: args
defp should_federate(%{attributed_to_id: attributed_to_id}) when not is_nil(attributed_to_id),
do: true
defp should_federate(args), do: Map.get(args, :draft, false) == false
end end

View File

@ -12,6 +12,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Event do
alias Mobilizon.GraphQL.API alias Mobilizon.GraphQL.API
alias Mobilizon.Federation.ActivityPub.Activity alias Mobilizon.Federation.ActivityPub.Activity
alias Mobilizon.Federation.ActivityPub.Permission
import Mobilizon.Users.Guards, only: [is_moderator: 1] import Mobilizon.Users.Guards, only: [is_moderator: 1]
import Mobilizon.Web.Gettext import Mobilizon.Web.Gettext
@ -75,13 +76,28 @@ defmodule Mobilizon.GraphQL.Resolvers.Event do
defp find_private_event( defp find_private_event(
_parent, _parent,
%{uuid: uuid}, %{uuid: uuid},
%{context: %{current_user: %User{id: user_id}}} = _resolution %{context: %{current_user: %User{} = user}} = _resolution
) do ) do
case {:has_event, Events.get_own_event_by_uuid_with_preload(uuid, user_id)} do %Actor{} = profile = Users.get_actor_for_user(user)
{:has_event, %Event{} = event} ->
{:ok, event}
{:has_event, _} -> case Events.get_event_by_uuid_with_preload(uuid) do
# Event attributed to group
%Event{attributed_to: %Actor{}} = event ->
if Permission.can_access_group_object?(profile, event) do
{:ok, event}
else
{:error, :event_not_found}
end
# Own event
%Event{organizer_actor: %Actor{id: actor_id}} = event ->
if actor_id == profile.id do
{:ok, event}
else
{:error, :event_not_found}
end
_ ->
{:error, :event_not_found} {:error, :event_not_found}
end end
end end
@ -239,11 +255,19 @@ defmodule Mobilizon.GraphQL.Resolvers.Event do
# See https://github.com/absinthe-graphql/absinthe/issues/490 # See https://github.com/absinthe-graphql/absinthe/issues/490
with {:is_owned, %Actor{} = organizer_actor} <- User.owns_actor(user, organizer_actor_id), with {:is_owned, %Actor{} = organizer_actor} <- User.owns_actor(user, organizer_actor_id),
args <- Map.put(args, :options, args[:options] || %{}), args <- Map.put(args, :options, args[:options] || %{}),
{:group_check, true} <- {:group_check, is_organizer_group_member?(args)},
args_with_organizer <- Map.put(args, :organizer_actor, organizer_actor), args_with_organizer <- Map.put(args, :organizer_actor, organizer_actor),
{:ok, %Activity{data: %{"object" => %{"type" => "Event"}}}, %Event{} = event} <- {:ok, %Activity{data: %{"object" => %{"type" => "Event"}}}, %Event{} = event} <-
API.Events.create_event(args_with_organizer) do API.Events.create_event(args_with_organizer) do
{:ok, event} {:ok, event}
else else
{:group_check, false} ->
{:error,
dgettext(
"errors",
"Organizer profile doesn't have permission to create an event on behalf of this group"
)}
{:is_owned, nil} -> {:is_owned, nil} ->
{:error, dgettext("errors", "Organizer profile is not owned by the user")} {:error, dgettext("errors", "Organizer profile is not owned by the user")}
@ -270,16 +294,21 @@ defmodule Mobilizon.GraphQL.Resolvers.Event do
# See https://github.com/absinthe-graphql/absinthe/issues/490 # See https://github.com/absinthe-graphql/absinthe/issues/490
with args <- Map.put(args, :options, args[:options] || %{}), with args <- Map.put(args, :options, args[:options] || %{}),
{:ok, %Event{} = event} <- Events.get_event_with_preload(event_id), {:ok, %Event{} = event} <- Events.get_event_with_preload(event_id),
{:old_actor, {:is_owned, %Actor{}}} <- %Actor{} = actor <- Users.get_actor_for_user(user),
{:old_actor, User.owns_actor(user, event.organizer_actor_id)}, {:ok, args} <- verify_profile_change(args, event, user, actor),
new_organizer_actor_id <- args |> Map.get(:organizer_actor_id, event.organizer_actor_id), {:event_can_be_managed, true} <-
{:new_actor, {:is_owned, %Actor{} = organizer_actor}} <- {:event_can_be_managed, can_event_be_updated_by?(event, actor)},
{:new_actor, User.owns_actor(user, new_organizer_actor_id)},
args <- Map.put(args, :organizer_actor, organizer_actor),
{:ok, %Activity{data: %{"object" => %{"type" => "Event"}}}, %Event{} = event} <- {:ok, %Activity{data: %{"object" => %{"type" => "Event"}}}, %Event{} = event} <-
API.Events.update_event(args, event) do API.Events.update_event(args, event) do
{:ok, event} {:ok, event}
else else
{:event_can_be_managed, false} ->
{:error,
dgettext(
"errors",
"This profile doesn't have permission to update an event on behalf of this group"
)}
{:error, :event_not_found} -> {:error, :event_not_found} ->
{:error, dgettext("errors", "Event not found")} {:error, dgettext("errors", "Event not found")}
@ -309,7 +338,8 @@ defmodule Mobilizon.GraphQL.Resolvers.Event do
with {:ok, %Event{local: is_local} = event} <- Events.get_event_with_preload(event_id), with {:ok, %Event{local: is_local} = event} <- Events.get_event_with_preload(event_id),
%Actor{id: actor_id} = actor <- Users.get_actor_for_user(user) do %Actor{id: actor_id} = actor <- Users.get_actor_for_user(user) do
cond do cond do
{:event_can_be_managed, true} == Event.can_be_managed_by(event, actor_id) -> {:event_can_be_managed, true} ==
{:event_can_be_managed, can_event_be_deleted_by?(event, actor)} ->
do_delete_event(event, actor) do_delete_event(event, actor)
role in [:moderator, :administrator] -> role in [:moderator, :administrator] ->
@ -339,4 +369,74 @@ defmodule Mobilizon.GraphQL.Resolvers.Event do
{:ok, %{id: event.id}} {:ok, %{id: event.id}}
end end
end end
defp is_organizer_group_member?(%{
attributed_to_id: attributed_to_id,
organizer_actor_id: organizer_actor_id
})
when not is_nil(attributed_to_id) do
Actors.is_member?(organizer_actor_id, attributed_to_id) &&
Permission.can_create_group_object?(organizer_actor_id, attributed_to_id, %Event{})
end
defp is_organizer_group_member?(_), do: true
defp verify_profile_change(
args,
%Event{attributed_to: %Actor{}},
%User{} = _user,
%Actor{} = current_profile
) do
# The organizer_actor has to be the current profile, because otherwise we're left with a possible remote organizer
args =
args
|> Map.put(:organizer_actor, current_profile)
|> Map.put(:organizer_actor_id, current_profile.id)
{:ok, args}
end
defp verify_profile_change(
args,
%Event{organizer_actor: %Actor{id: organizer_actor_id}},
%User{} = user,
%Actor{} = _actor
) do
with {:old_actor, {:is_owned, %Actor{}}} <-
{:old_actor, User.owns_actor(user, organizer_actor_id)},
new_organizer_actor_id <- args |> Map.get(:organizer_actor_id, organizer_actor_id),
{:new_actor, {:is_owned, %Actor{} = organizer_actor}} <-
{:new_actor, User.owns_actor(user, new_organizer_actor_id)},
args <-
args
|> Map.put(:organizer_actor, organizer_actor)
|> Map.put(:organizer_actor_id, organizer_actor.id) do
{:ok, args}
end
end
defp can_event_be_updated_by?(
%Event{attributed_to: %Actor{type: :Group}} = event,
%Actor{} = actor_member
) do
Permission.can_update_group_object?(actor_member, event)
end
defp can_event_be_updated_by?(
%Event{} = event,
%Actor{id: actor_member_id}
) do
Event.can_be_managed_by?(event, actor_member_id)
end
defp can_event_be_deleted_by?(
%Event{attributed_to: %Actor{type: :Group}} = event,
%Actor{} = actor_member
) do
Permission.can_delete_group_object?(actor_member, event)
end
defp can_event_be_deleted_by?(%Event{} = event, %Actor{id: actor_member_id}) do
Event.can_be_managed_by?(event, actor_member_id)
end
end end

View File

@ -7,7 +7,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Post do
alias Mobilizon.{Actors, Posts, Users} alias Mobilizon.{Actors, Posts, Users}
alias Mobilizon.Actors.Actor alias Mobilizon.Actors.Actor
alias Mobilizon.Federation.ActivityPub alias Mobilizon.Federation.ActivityPub
alias Mobilizon.Federation.ActivityPub.Utils alias Mobilizon.Federation.ActivityPub.{Permission, Utils}
alias Mobilizon.Posts.Post alias Mobilizon.Posts.Post
alias Mobilizon.Storage.Page alias Mobilizon.Storage.Page
alias Mobilizon.Users.User alias Mobilizon.Users.User
@ -69,11 +69,11 @@ defmodule Mobilizon.GraphQL.Resolvers.Post do
} }
} = _resolution } = _resolution
) do ) do
with {:current_actor, %Actor{id: actor_id}} <- with {:current_actor, %Actor{} = current_profile} <-
{:current_actor, Users.get_actor_for_user(user)}, {:current_actor, Users.get_actor_for_user(user)},
{:post, %Post{attributed_to: %Actor{id: group_id}} = post} <- {:post, %Post{attributed_to: %Actor{}} = post} <-
{:post, Posts.get_post_by_slug_with_preloads(slug)}, {:post, Posts.get_post_by_slug_with_preloads(slug)},
{:member, true} <- {:member, Actors.is_member?(actor_id, group_id)} do {:member, true} <- {:member, Permission.can_access_group_object?(current_profile, post)} do
{:ok, post} {:ok, post}
else else
{:member, false} -> get_post(parent, %{slug: slug}, nil) {:member, false} -> get_post(parent, %{slug: slug}, nil)

View File

@ -794,11 +794,18 @@ defmodule Mobilizon.Actors do
@spec get_single_group_member_actor(integer() | String.t()) :: Actor.t() | nil @spec get_single_group_member_actor(integer() | String.t()) :: Actor.t() | nil
def get_single_group_member_actor(group_id) do def get_single_group_member_actor(group_id) do
do_get_single_group_member_actor(group_id, [:member, :moderator, :administrator, :creator])
end
@spec get_single_group_moderator_actor(integer() | String.t()) :: Actor.t() | nil
def get_single_group_moderator_actor(group_id) do
do_get_single_group_member_actor(group_id, [:moderator, :administrator, :creator])
end
@spec do_get_single_group_member_actor(integer() | String.t(), list(atom())) :: Actor.t() | nil
defp do_get_single_group_member_actor(group_id, roles) do
Member Member
|> where( |> where([m], m.parent_id == ^group_id and m.role in ^roles)
[m],
m.parent_id == ^group_id and m.role in [^:member, ^:moderator, ^:administrator, ^:creator]
)
|> join(:inner, [m], a in Actor, on: m.actor_id == a.id) |> join(:inner, [m], a in Actor, on: m.actor_id == a.id)
|> where([_m, a], is_nil(a.domain)) |> where([_m, a], is_nil(a.domain))
|> limit(1) |> limit(1)

View File

@ -188,13 +188,11 @@ defmodule Mobilizon.Events.Event do
@doc """ @doc """
Checks whether an event can be managed. Checks whether an event can be managed.
""" """
@spec can_be_managed_by(t, integer | String.t()) :: boolean @spec can_be_managed_by?(t, integer | String.t()) :: boolean
def can_be_managed_by(%__MODULE__{organizer_actor_id: organizer_actor_id}, actor_id) def can_be_managed_by?(%__MODULE__{organizer_actor_id: organizer_actor_id}, actor_id),
when organizer_actor_id == actor_id do do: organizer_actor_id == actor_id
{:event_can_be_managed, true}
end
def can_be_managed_by(_event, _actor), do: {:event_can_be_managed, false} def can_be_managed_by?(_event, _actor), do: false
@spec put_tags(Changeset.t(), map) :: Changeset.t() @spec put_tags(Changeset.t(), map) :: Changeset.t()
defp put_tags(%Changeset{} = changeset, %{tags: tags}) do defp put_tags(%Changeset{} = changeset, %{tags: tags}) do

View File

@ -88,6 +88,8 @@ defmodule Mobilizon.Events do
:media :media
] ]
@participant_preloads [:event, :actor]
@doc """ @doc """
Gets a single event. Gets a single event.
""" """
@ -153,6 +155,7 @@ defmodule Mobilizon.Events do
def get_event_by_url!(url) do def get_event_by_url!(url) do
url url
|> event_by_url_query() |> event_by_url_query()
|> preload_for_event()
|> Repo.one!() |> Repo.one!()
end end
@ -306,8 +309,9 @@ defmodule Mobilizon.Events do
""" """
@spec update_event(Event.t(), map) :: {:ok, Event.t()} | {:error, Changeset.t()} @spec update_event(Event.t(), map) :: {:ok, Event.t()} | {:error, Changeset.t()}
def update_event(%Event{draft: old_draft} = old_event, attrs) do def update_event(%Event{draft: old_draft} = old_event, attrs) do
with %Changeset{changes: changes} = changeset <- with %Event{} = old_event <- Repo.preload(old_event, @event_preloads),
Event.update_changeset(Repo.preload(old_event, [:tags, :media]), attrs), %Changeset{changes: changes} = changeset <-
Event.update_changeset(old_event, attrs),
{:ok, %{update: %Event{} = new_event}} <- {:ok, %{update: %Event{} = new_event}} <-
Multi.new() Multi.new()
|> Multi.update(:update, changeset) |> Multi.update(:update, changeset)
@ -328,7 +332,8 @@ defmodule Mobilizon.Events do
err -> err err -> err
end end
end) end)
|> Repo.transaction() do |> Repo.transaction(),
%Event{} = new_event <- Repo.preload(new_event, @event_preloads, force: true) do
Cachex.del(:ics, "event_#{new_event.uuid}") Cachex.del(:ics, "event_#{new_event.uuid}")
Email.Event.calculate_event_diff_and_send_notifications( Email.Event.calculate_event_diff_and_send_notifications(
@ -340,7 +345,7 @@ defmodule Mobilizon.Events do
unless new_event.draft, unless new_event.draft,
do: Workers.BuildSearch.enqueue(:update_search_event, %{"event_id" => new_event.id}) do: Workers.BuildSearch.enqueue(:update_search_event, %{"event_id" => new_event.id})
{:ok, Repo.preload(new_event, @event_preloads)} {:ok, new_event}
end end
end end
@ -530,6 +535,7 @@ defmodule Mobilizon.Events do
|> events_for_ends_on(args) |> events_for_ends_on(args)
|> events_for_tags(args) |> events_for_tags(args)
|> events_for_location(args) |> events_for_location(args)
|> filter_draft()
|> filter_local_or_from_followed_instances_events() |> filter_local_or_from_followed_instances_events()
|> filter_public_visibility() |> filter_public_visibility()
|> event_order_begins_on_asc() |> event_order_begins_on_asc()
@ -726,7 +732,7 @@ defmodule Mobilizon.Events do
def get_participant(participant_id) do def get_participant(participant_id) do
Participant Participant
|> where([p], p.id == ^participant_id) |> where([p], p.id == ^participant_id)
|> preload([p], [:event, :actor]) |> preload([p], ^@participant_preloads)
|> Repo.one() |> Repo.one()
end end
@ -742,6 +748,7 @@ defmodule Mobilizon.Events do
case Participant case Participant
|> where([p], event_id: ^event_id, actor_id: ^actor_id) |> where([p], event_id: ^event_id, actor_id: ^actor_id)
|> where([p], fragment("? ->>'email' = ?", p.metadata, ^email)) |> where([p], fragment("? ->>'email' = ?", p.metadata, ^email))
|> preload([p], ^@participant_preloads)
|> Repo.one() do |> Repo.one() do
%Participant{} = participant -> %Participant{} = participant ->
{:ok, participant} {:ok, participant}
@ -756,6 +763,7 @@ defmodule Mobilizon.Events do
case Participant case Participant
|> where([p], event_id: ^event_id, actor_id: ^actor_id) |> where([p], event_id: ^event_id, actor_id: ^actor_id)
|> where([p], fragment("? ->>'cancellation_token' = ?", p.metadata, ^cancellation_token)) |> where([p], fragment("? ->>'cancellation_token' = ?", p.metadata, ^cancellation_token))
|> preload([p], ^@participant_preloads)
|> Repo.one() do |> Repo.one() do
%Participant{} = participant -> %Participant{} = participant ->
{:ok, participant} {:ok, participant}
@ -766,7 +774,9 @@ defmodule Mobilizon.Events do
end end
def get_participant(event_id, actor_id, %{}) do def get_participant(event_id, actor_id, %{}) do
case Repo.get_by(Participant, event_id: event_id, actor_id: actor_id) do case Participant
|> Repo.get_by(event_id: event_id, actor_id: actor_id)
|> Repo.preload(@participant_preloads) do
%Participant{} = participant -> %Participant{} = participant ->
{:ok, participant} {:ok, participant}
@ -779,7 +789,7 @@ defmodule Mobilizon.Events do
def get_participant_by_confirmation_token(confirmation_token) do def get_participant_by_confirmation_token(confirmation_token) do
Participant Participant
|> where([p], fragment("? ->>'confirmation_token' = ?", p.metadata, ^confirmation_token)) |> where([p], fragment("? ->>'confirmation_token' = ?", p.metadata, ^confirmation_token))
|> preload([p], [:actor, :event]) |> preload([p], ^@participant_preloads)
|> Repo.one() |> Repo.one()
end end

View File

@ -163,6 +163,7 @@ defmodule Mobilizon.Mixfile do
{:web_push_encryption, {:web_push_encryption,
git: "https://github.com/tcitworld/elixir-web-push-encryption", branch: "otp-24"}, git: "https://github.com/tcitworld/elixir-web-push-encryption", branch: "otp-24"},
{:eblurhash, "~> 1.2"}, {:eblurhash, "~> 1.2"},
{:struct_access, "~> 1.1.2"},
# Dev and test dependencies # Dev and test dependencies
{:phoenix_live_reload, "~> 1.2", only: [:dev, :e2e]}, {:phoenix_live_reload, "~> 1.2", only: [:dev, :e2e]},
{:ex_machina, "~> 2.3", only: [:dev, :test]}, {:ex_machina, "~> 2.3", only: [:dev, :test]},

View File

@ -121,6 +121,7 @@
"slugger": {:hex, :slugger, "0.3.0", "efc667ab99eee19a48913ccf3d038b1fb9f165fa4fbf093be898b8099e61b6ed", [:mix], [], "hexpm", "20d0ded0e712605d1eae6c5b4889581c3460d92623a930ddda91e0e609b5afba"}, "slugger": {:hex, :slugger, "0.3.0", "efc667ab99eee19a48913ccf3d038b1fb9f165fa4fbf093be898b8099e61b6ed", [:mix], [], "hexpm", "20d0ded0e712605d1eae6c5b4889581c3460d92623a930ddda91e0e609b5afba"},
"sobelow": {:hex, :sobelow, "0.11.1", "23438964486f8112b41e743bbfd402da3e5b296fdc9eacab29914b79c48916dd", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "9897363a7eff96f4809304a90aad819e2ad5e5d24db547af502885146746a53c"}, "sobelow": {:hex, :sobelow, "0.11.1", "23438964486f8112b41e743bbfd402da3e5b296fdc9eacab29914b79c48916dd", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "9897363a7eff96f4809304a90aad819e2ad5e5d24db547af502885146746a53c"},
"ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.6", "cf344f5692c82d2cd7554f5ec8fd961548d4fd09e7d22f5b62482e5aeaebd4b0", [:make, :mix, :rebar3], [], "hexpm", "bdb0d2471f453c88ff3908e7686f86f9be327d065cc1ec16fa4540197ea04680"}, "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.6", "cf344f5692c82d2cd7554f5ec8fd961548d4fd09e7d22f5b62482e5aeaebd4b0", [:make, :mix, :rebar3], [], "hexpm", "bdb0d2471f453c88ff3908e7686f86f9be327d065cc1ec16fa4540197ea04680"},
"struct_access": {:hex, :struct_access, "1.1.2", "a42e6ceedd9b9ea090ee94a6da089d56e16f374dbbc010c3eebdf8be17df286f", [:mix], [], "hexpm", "e4c411dcc0226081b95709909551fc92b8feb1a3476108348ea7e3f6c12e586a"},
"sweet_xml": {:hex, :sweet_xml, "0.6.6", "fc3e91ec5dd7c787b6195757fbcf0abc670cee1e4172687b45183032221b66b8", [:mix], [], "hexpm", "2e1ec458f892ffa81f9f8386e3f35a1af6db7a7a37748a64478f13163a1f3573"}, "sweet_xml": {:hex, :sweet_xml, "0.6.6", "fc3e91ec5dd7c787b6195757fbcf0abc670cee1e4172687b45183032221b66b8", [:mix], [], "hexpm", "2e1ec458f892ffa81f9f8386e3f35a1af6db7a7a37748a64478f13163a1f3573"},
"telemetry": {:hex, :telemetry, "0.4.3", "a06428a514bdbc63293cd9a6263aad00ddeb66f608163bdec7c8995784080818", [:rebar3], [], "hexpm", "eb72b8365ffda5bed68a620d1da88525e326cb82a75ee61354fc24b844768041"}, "telemetry": {:hex, :telemetry, "0.4.3", "a06428a514bdbc63293cd9a6263aad00ddeb66f608163bdec7c8995784080818", [:rebar3], [], "hexpm", "eb72b8365ffda5bed68a620d1da88525e326cb82a75ee61354fc24b844768041"},
"tesla": {:hex, :tesla, "1.4.1", "ff855f1cac121e0d16281b49e8f066c4a0d89965f98864515713878cca849ac8", [:mix], [{:castore, "~> 0.1", [hex: :castore, repo: "hexpm", optional: true]}, {:exjsx, ">= 3.0.0", [hex: :exjsx, repo: "hexpm", optional: true]}, {:finch, "~> 0.3", [hex: :finch, repo: "hexpm", optional: true]}, {:fuse, "~> 2.4", [hex: :fuse, repo: "hexpm", optional: true]}, {:gun, "~> 1.3", [hex: :gun, repo: "hexpm", optional: true]}, {:hackney, "~> 1.6", [hex: :hackney, repo: "hexpm", optional: true]}, {:ibrowse, "~> 4.4.0", [hex: :ibrowse, repo: "hexpm", optional: true]}, {:jason, ">= 1.0.0", [hex: :jason, repo: "hexpm", optional: true]}, {:mime, "~> 1.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.0", [hex: :mint, repo: "hexpm", optional: true]}, {:poison, ">= 1.0.0", [hex: :poison, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: true]}], "hexpm", "95f5de35922c8c4b3945bee7406f66eb680b0955232f78f5fb7e853aa1ce201a"}, "tesla": {:hex, :tesla, "1.4.1", "ff855f1cac121e0d16281b49e8f066c4a0d89965f98864515713878cca849ac8", [:mix], [{:castore, "~> 0.1", [hex: :castore, repo: "hexpm", optional: true]}, {:exjsx, ">= 3.0.0", [hex: :exjsx, repo: "hexpm", optional: true]}, {:finch, "~> 0.3", [hex: :finch, repo: "hexpm", optional: true]}, {:fuse, "~> 2.4", [hex: :fuse, repo: "hexpm", optional: true]}, {:gun, "~> 1.3", [hex: :gun, repo: "hexpm", optional: true]}, {:hackney, "~> 1.6", [hex: :hackney, repo: "hexpm", optional: true]}, {:ibrowse, "~> 4.4.0", [hex: :ibrowse, repo: "hexpm", optional: true]}, {:jason, ">= 1.0.0", [hex: :jason, repo: "hexpm", optional: true]}, {:mime, "~> 1.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.0", [hex: :mint, repo: "hexpm", optional: true]}, {:poison, ">= 1.0.0", [hex: :poison, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: true]}], "hexpm", "95f5de35922c8c4b3945bee7406f66eb680b0955232f78f5fb7e853aa1ce201a"},

View File

@ -0,0 +1,11 @@
defmodule Mobilizon.Storage.Repo.Migrations.PutUniqueIndexOnParticipantsUrls do
use Ecto.Migration
def up do
create_if_not_exists(unique_index("participants", [:url]))
end
def down do
drop_if_exists(index("participants", [:url]))
end
end

View File

@ -0,0 +1,319 @@
defmodule Mobilizon.Federation.ActivityPub.AudienceTest do
use Mobilizon.DataCase
import Mobilizon.Factory
alias Mobilizon.Actors.{Actor, Member}
alias Mobilizon.Mention
alias Mobilizon.Discussions.{Comment, Discussion}
alias Mobilizon.Events.{Event, Participant}
alias Mobilizon.Federation.ActivityPub.Audience
alias Mobilizon.Posts.Post
alias Mobilizon.Storage.Repo
@ap_public "https://www.w3.org/ns/activitystreams#Public"
describe "get audience for an event created from a profile" do
test "when the event is public" do
%Event{} = event = insert(:event)
event = Repo.preload(event, [:comments])
assert %{"cc" => [event.organizer_actor.followers_url], "to" => [@ap_public]} ==
Audience.get_audience(event)
end
test "when the event is unlisted" do
%Event{} = event = insert(:event, visibility: :unlisted)
event = Repo.preload(event, [:comments])
assert %{"cc" => [@ap_public], "to" => [event.organizer_actor.followers_url]} ==
Audience.get_audience(event)
end
test "when the event is unlisted and mentions some actors" do
%Actor{id: mentionned_actor_id, url: mentionned_actor_url} =
insert(:actor, domain: "somewhere.else", url: "https://somewhere.else/@someone")
%Event{} = event = insert(:event, visibility: :unlisted)
event = Repo.preload(event, [:comments])
mentions = [%Mention{actor_id: mentionned_actor_id}]
event = %Event{event | mentions: mentions}
assert %{
"cc" => [@ap_public],
"to" => [event.organizer_actor.followers_url, mentionned_actor_url]
} ==
Audience.get_audience(event)
end
test "with interactions" do
%Actor{} = interactor = insert(:actor)
%Event{} = event = insert(:event)
insert(:share, owner_actor: event.organizer_actor, actor: interactor, uri: event.url)
event = Repo.preload(event, [:comments])
assert %{
"cc" => [event.organizer_actor.followers_url, interactor.url],
"to" => [@ap_public]
} ==
Audience.get_audience(event)
end
end
describe "get audience for an event created from a group member" do
test "when the event is public" do
%Actor{} = actor = insert(:actor)
%Actor{
followers_url: followers_url,
members_url: members_url
} = group = insert(:group, domain: "somewhere.else", url: "https://somewhere.else/@someone")
%Event{} = event = insert(:event, attributed_to: group, organizer_actor: actor)
event = Repo.preload(event, [:comments])
assert %{
"cc" => [members_url, followers_url],
"to" => [@ap_public]
} ==
Audience.get_audience(event)
end
test "when the event is unlisted" do
%Actor{} = actor = insert(:actor)
%Actor{
followers_url: followers_url,
members_url: members_url
} = group = insert(:group, domain: "somewhere.else", url: "https://somewhere.else/@someone")
%Event{} =
event =
insert(:event, visibility: :unlisted, attributed_to: group, organizer_actor: actor)
event = Repo.preload(event, [:comments])
assert %{
"cc" => [@ap_public],
"to" => [members_url, followers_url]
} ==
Audience.get_audience(event)
end
test "when the event is unlisted and mentions some actors" do
%Actor{id: mentionned_actor_id, url: mentionned_actor_url} =
insert(:actor, domain: "somewhere.else", url: "https://somewhere.else/@someone")
%Actor{} = actor = insert(:actor)
%Actor{
followers_url: followers_url,
members_url: members_url
} = group = insert(:group, domain: "somewhere.else", url: "https://somewhere.else/@a_group")
%Event{} =
event =
insert(:event, visibility: :unlisted, attributed_to: group, organizer_actor: actor)
event = Repo.preload(event, [:comments])
mentions = [%Mention{actor_id: mentionned_actor_id}]
event = %Event{event | mentions: mentions}
assert %{
"cc" => [@ap_public],
"to" => [members_url, followers_url, mentionned_actor_url]
} ==
Audience.get_audience(event)
end
end
describe "get audience for a post" do
test "when it's public" do
%Actor{
followers_url: followers_url,
members_url: members_url
} = group = insert(:group, domain: "somewhere.else", url: "https://somewhere.else/@someone")
%Post{} = post = insert(:post, attributed_to: group)
assert %{"to" => [@ap_public], "cc" => [members_url, followers_url]} ==
Audience.get_audience(post)
end
test "when it's unlisted" do
%Actor{
followers_url: followers_url,
members_url: members_url
} = group = insert(:group, domain: "somewhere.else", url: "https://somewhere.else/@someone")
%Post{} = post = insert(:post, attributed_to: group, visibility: :unlisted)
assert %{"to" => [members_url, followers_url], "cc" => [@ap_public]} ==
Audience.get_audience(post)
end
test "when it's private" do
%Actor{
members_url: members_url
} = group = insert(:group, domain: "somewhere.else", url: "https://somewhere.else/@someone")
%Post{} = post = insert(:post, attributed_to: group, visibility: :private)
assert %{"to" => [members_url], "cc" => []} ==
Audience.get_audience(post)
end
test "when it's still a draft" do
%Actor{
members_url: members_url
} = group = insert(:group, domain: "somewhere.else", url: "https://somewhere.else/@someone")
%Post{} = post = insert(:post, attributed_to: group, draft: true)
assert %{"to" => [members_url], "cc" => []} ==
Audience.get_audience(post)
end
end
describe "get audience for a discussion" do
test "basic" do
%Actor{
members_url: members_url
} = group = insert(:group, domain: "somewhere.else", url: "https://somewhere.else/@someone")
%Discussion{} = discussion = insert(:discussion, actor: group)
assert %{"to" => [members_url], "cc" => []} ==
Audience.get_audience(discussion)
end
end
describe "get audience for a comment" do
test "basic" do
%Actor{id: mentionned_actor_id, url: mentionned_actor_url} =
insert(:actor, domain: "somewhere.else", url: "https://somewhere.else/@someone")
%Comment{} = comment = insert(:comment)
mentions = [%Mention{actor_id: mentionned_actor_id}]
comment = %Comment{comment | mentions: mentions}
assert %{
"cc" => [comment.actor.followers_url],
"to" => [@ap_public, mentionned_actor_url, comment.event.organizer_actor.url]
} ==
Audience.get_audience(comment)
end
test "in reply to other comments" do
%Actor{id: mentionned_actor_id, url: mentionned_actor_url} =
insert(:actor, domain: "somewhere.else", url: "https://somewhere.else/@someone")
%Comment{} = original_comment = insert(:comment)
%Comment{} =
reply_comment =
insert(:comment, in_reply_to_comment: original_comment, origin_comment: original_comment)
%Comment{} =
comment =
insert(:comment, in_reply_to_comment: reply_comment, origin_comment: original_comment)
mentions = [%Mention{actor_id: mentionned_actor_id}]
comment = %Comment{comment | mentions: mentions}
assert %{
"cc" => [comment.actor.followers_url, original_comment.actor.url],
"to" => [
@ap_public,
mentionned_actor_url,
reply_comment.actor.url,
comment.event.organizer_actor.url
]
} ==
Audience.get_audience(comment)
end
test "part of a discussion" do
%Actor{
members_url: members_url
} = group = insert(:group, domain: "somewhere.else", url: "https://somewhere.else/@someone")
%Discussion{} = discussion = insert(:discussion, actor: group)
%Comment{} = comment = insert(:comment, discussion: discussion)
assert %{"to" => [members_url], "cc" => []} ==
Audience.get_audience(comment)
end
end
describe "participant" do
test "basic" do
%Event{} = event = insert(:event)
%Participant{} = participant2 = insert(:participant, event: event)
%Participant{} = participant = insert(:participant, event: event)
assert %{
"to" => [participant.actor.url, participant.event.organizer_actor.url],
"cc" => [participant2.actor.url]
} == Audience.get_audience(participant)
end
test "to a group event" do
%Actor{} =
group = insert(:group, domain: "somewhere.else", url: "https://somewhere.else/@someone")
%Event{} = event = insert(:event, attributed_to: group)
%Participant{} = participant2 = insert(:participant, event: event)
%Participant{} = participant = insert(:participant, event: event)
assert %{
"to" => [participant.actor.url, participant.event.attributed_to.url],
"cc" => [group.followers_url, group.members_url, participant2.actor.url]
} == Audience.get_audience(participant)
end
end
describe "member" do
test "basic" do
%Member{} = member = insert(:member)
assert %{"to" => [member.parent.url, member.parent.members_url], "cc" => []} ==
Audience.get_audience(member)
end
end
describe "actor" do
test "basic" do
%Actor{followers_url: followers_url} = actor = insert(:actor)
assert %{"to" => [@ap_public], "cc" => [followers_url]} ==
Audience.get_audience(actor)
end
test "group" do
%Actor{followers_url: followers_url, members_url: members_url, type: :Group} =
group = insert(:group)
assert %{"to" => [@ap_public], "cc" => [members_url, followers_url]} ==
Audience.get_audience(group)
end
test "with interactions" do
%Actor{followers_url: followers_url, members_url: members_url, type: :Group} =
group = insert(:group)
%Actor{} = interactor = insert(:actor)
insert(:share, owner_actor: group, actor: interactor)
assert %{
"to" => [@ap_public],
"cc" => [members_url, followers_url, interactor.url]
} ==
Audience.get_audience(group)
end
end
end

View File

@ -64,6 +64,46 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier.JoinTest do
# We don't accept already accepted Accept activities # We don't accept already accepted Accept activities
:error = Transmogrifier.handle_incoming(accept_data) :error = Transmogrifier.handle_incoming(accept_data)
end end
test "it accepts Accept activities with an inline Join from same origin" do
%Actor{} = organizer = insert(:actor)
%Actor{url: participant_actor_url} =
insert(:actor, domain: "somewhere.else", url: "https://somewhere.else/@participant")
%Actor{} =
group = insert(:group, domain: "somewhere.else", url: "https://somewhere.else/@group")
insert(:member, actor: organizer, parent: group, role: :moderator)
%Actor{} =
actor_member_2 =
insert(:actor, domain: "somewhere.else", url: "https://somewhere.else/@member")
insert(:member, actor: actor_member_2, parent: group, role: :moderator)
%Event{url: event_url} =
insert(:event, organizer_actor: organizer, join_options: :restricted, attributed_to: group)
join_data =
File.read!("test/fixtures/mobilizon-join-activity.json")
|> Jason.decode!()
|> Map.put("actor", participant_actor_url)
|> Map.put("object", event_url)
|> Map.put("participationMessage", @join_message)
|> Map.put("id", "https://somewhere.else/@participant/join/event/1")
accept_data =
File.read!("test/fixtures/mastodon-accept-activity.json")
|> Jason.decode!()
|> Map.put("actor", actor_member_2.url)
|> Map.put("object", join_data)
{:ok, accept_activity, _} = Transmogrifier.handle_incoming(accept_data)
assert accept_activity.data["object"]["id"] == join_data["id"]
assert accept_activity.data["object"]["id"] =~ "/join/"
assert accept_activity.data["id"] =~ "/accept/join/"
end
end end
describe "handle incoming reject join activities" do describe "handle incoming reject join activities" do

View File

@ -0,0 +1,440 @@
defmodule Mobilizon.Federation.ActivityPub.Types.EventsTest do
use Mobilizon.DataCase
import Mobilizon.Factory
alias Mobilizon.Actors.Actor
alias Mobilizon.Events
alias Mobilizon.Events.{Event, Participant}
alias Mobilizon.Federation.ActivityPub.Activity
alias Mobilizon.Federation.ActivityPub.Types.Events
@ap_public "https://www.w3.org/ns/activitystreams#Public"
describe "test event creation" do
@event_begins_on "2021-07-28T15:04:22Z"
@event_title "hey"
@event_data %{title: @event_title, begins_on: @event_begins_on}
test "from a simple profile" do
%Actor{id: organizer_actor_id, url: actor_url, followers_url: followers_url} =
insert(:actor)
assert {:ok, %Event{}, data} =
Events.create(
Map.merge(@event_data, %{organizer_actor_id: organizer_actor_id}),
%{}
)
assert match?(
%{
"actor" => ^actor_url,
"attributedTo" => ^actor_url,
"cc" => [^followers_url],
"object" => %{
"actor" => ^actor_url,
"anonymousParticipationEnabled" => false,
"attachment" => [],
"attributedTo" => ^actor_url,
"category" => nil,
"cc" => [],
"commentsEnabled" => false,
"content" => nil,
"draft" => false,
"endTime" => nil,
"ical:status" => "CONFIRMED",
"joinMode" => "free",
"maximumAttendeeCapacity" => nil,
"mediaType" => "text/html",
"name" => @event_title,
"repliesModerationOption" => nil,
"startTime" => @event_begins_on,
"tag" => [],
"to" => [@ap_public],
"type" => "Event"
},
"to" => [@ap_public],
"type" => "Create"
},
data
)
end
test "an unlisted event" do
%Actor{id: organizer_actor_id, url: actor_url, followers_url: followers_url} =
insert(:actor)
assert {:ok, %Event{}, data} =
Events.create(
Map.merge(@event_data, %{
organizer_actor_id: organizer_actor_id,
visibility: :unlisted
}),
%{}
)
assert match?(
%{
"actor" => ^actor_url,
"attributedTo" => ^actor_url,
"cc" => [@ap_public],
"object" => %{
"actor" => ^actor_url,
"anonymousParticipationEnabled" => false,
"attachment" => [],
"attributedTo" => ^actor_url,
"category" => nil,
"cc" => [@ap_public],
"commentsEnabled" => false,
"content" => nil,
"draft" => false,
"endTime" => nil,
"ical:status" => "CONFIRMED",
"joinMode" => "free",
"maximumAttendeeCapacity" => nil,
"mediaType" => "text/html",
"name" => @event_title,
"repliesModerationOption" => nil,
"startTime" => @event_begins_on,
"tag" => [],
"to" => [^followers_url],
"type" => "Event"
},
"to" => [^followers_url],
"type" => "Create"
},
data
)
end
test "from a group member" do
%Actor{id: organizer_actor_id, url: actor_url} = actor = insert(:actor)
%Actor{
id: attributed_to_id,
url: group_url,
followers_url: followers_url,
members_url: members_url
} = group = insert(:group, domain: "somewhere.else", url: "https://somewhere.else/@someone")
insert(:member, parent: group, actor: actor, role: :moderator)
assert {:ok, %Event{}, data} =
Events.create(
Map.merge(@event_data, %{
organizer_actor_id: organizer_actor_id,
attributed_to_id: attributed_to_id
}),
%{}
)
assert match?(
%{
"actor" => ^actor_url,
"attributedTo" => ^group_url,
"cc" => [^members_url, ^followers_url],
"object" => %{
"actor" => ^actor_url,
"anonymousParticipationEnabled" => false,
"attachment" => [],
"attributedTo" => ^group_url,
"category" => nil,
"cc" => [],
"commentsEnabled" => false,
"content" => nil,
"draft" => false,
"endTime" => nil,
"ical:status" => "CONFIRMED",
"joinMode" => "free",
"maximumAttendeeCapacity" => nil,
"mediaType" => "text/html",
"name" => @event_title,
"repliesModerationOption" => nil,
"startTime" => @event_begins_on,
"tag" => [],
"to" => [@ap_public],
"type" => "Event"
},
"to" => [@ap_public],
"type" => "Create"
},
data
)
end
end
@event_updated_title "my event updated"
@event_update_data %{title: @event_updated_title}
describe "test event update" do
test "from a simple profile" do
%Actor{url: actor_url, followers_url: followers_url} = actor = insert(:actor)
{:ok, begins_on, _} = DateTime.from_iso8601(@event_begins_on)
%Event{} = event = insert(:event, organizer_actor: actor, begins_on: begins_on)
assert {:ok, %Event{}, data} =
Events.update(
event,
@event_update_data,
%{}
)
assert match?(
%{
"actor" => ^actor_url,
"attributedTo" => ^actor_url,
"cc" => [^followers_url],
"object" => %{
"actor" => ^actor_url,
"anonymousParticipationEnabled" => false,
"attributedTo" => ^actor_url,
"cc" => [],
"commentsEnabled" => false,
"draft" => false,
"ical:status" => "CONFIRMED",
"joinMode" => "free",
"maximumAttendeeCapacity" => nil,
"mediaType" => "text/html",
"name" => @event_updated_title,
"repliesModerationOption" => nil,
"startTime" => @event_begins_on,
"tag" => [],
"to" => [@ap_public],
"type" => "Event"
},
"to" => [@ap_public],
"type" => "Update"
},
data
)
end
test "from a group member" do
%Actor{} = actor_1 = insert(:actor)
%Actor{id: organizer_actor_2_id, url: actor_2_url} = actor_2 = insert(:actor)
%Actor{
url: group_url,
followers_url: followers_url,
members_url: members_url
} = group = insert(:group, domain: "somewhere.else", url: "https://somewhere.else/@someone")
insert(:member, parent: group, actor: actor_1, role: :moderator)
insert(:member, parent: group, actor: actor_2, role: :moderator)
{:ok, begins_on, _} = DateTime.from_iso8601(@event_begins_on)
%Event{} =
event =
insert(:event, organizer_actor: actor_1, begins_on: begins_on, attributed_to: group)
assert {:ok, %Event{}, data} =
Events.update(
event,
Map.merge(@event_update_data, %{
organizer_actor_id: organizer_actor_2_id
}),
%{}
)
assert match?(
%{
"actor" => ^actor_2_url,
"attributedTo" => ^group_url,
"cc" => [^members_url, ^followers_url],
"object" => %{
"actor" => ^actor_2_url,
"anonymousParticipationEnabled" => false,
"attributedTo" => ^group_url,
"cc" => [],
"commentsEnabled" => false,
"draft" => false,
"ical:status" => "CONFIRMED",
"joinMode" => "free",
"maximumAttendeeCapacity" => nil,
"mediaType" => "text/html",
"name" => @event_updated_title,
"repliesModerationOption" => nil,
"startTime" => @event_begins_on,
"tag" => [],
"to" => [@ap_public],
"type" => "Event"
},
"to" => [@ap_public],
"type" => "Update"
},
data
)
end
test "from a remote group member" do
%Actor{id: organizer_actor_1_id, url: actor_1_url} = actor_1 = insert(:actor)
%Actor{} = actor_2 = insert(:actor)
%Actor{
url: group_url,
followers_url: followers_url,
members_url: members_url
} = group = insert(:group, domain: "somewhere.else", url: "https://somewhere.else/@someone")
insert(:member, parent: group, actor: actor_1, role: :moderator)
insert(:member, parent: group, actor: actor_2, role: :moderator)
{:ok, begins_on, _} = DateTime.from_iso8601(@event_begins_on)
%Event{} =
event =
insert(:event, organizer_actor: actor_2, begins_on: begins_on, attributed_to: group)
assert {:ok, %Event{}, data} =
Events.update(
event,
Map.merge(@event_update_data, %{
organizer_actor_id: organizer_actor_1_id
}),
%{}
)
assert match?(
%{
"actor" => ^actor_1_url,
"attributedTo" => ^group_url,
"cc" => [^members_url, ^followers_url],
"object" => %{
"actor" => ^actor_1_url,
"anonymousParticipationEnabled" => false,
"attributedTo" => ^group_url,
"cc" => [],
"commentsEnabled" => false,
"draft" => false,
"ical:status" => "CONFIRMED",
"joinMode" => "free",
"maximumAttendeeCapacity" => nil,
"mediaType" => "text/html",
"name" => @event_updated_title,
"repliesModerationOption" => nil,
"startTime" => @event_begins_on,
"tag" => [],
"to" => [@ap_public],
"type" => "Event"
},
"to" => [@ap_public],
"type" => "Update"
},
data
)
end
end
describe "join an event" do
test "simple and remote" do
%Actor{url: organizer_actor_url} =
organizer_actor = insert(:actor, domain: "somewhere.else")
%Actor{url: participant_actor_url} = actor = insert(:actor, domain: nil)
%Event{url: event_url} =
event = insert(:event, organizer_actor: organizer_actor, local: false)
assert {:ok, data, %Participant{}} = Events.join(event, actor, true, %{})
assert match?(
%{
"actor" => ^participant_actor_url,
"cc" => [],
"object" => ^event_url,
"participationMessage" => nil,
"to" => [^participant_actor_url, ^organizer_actor_url],
"type" => "Join"
},
data
)
end
test "simple and local" do
%Actor{url: organizer_actor_url} = organizer_actor = insert(:actor, domain: nil)
%Actor{url: participant_actor_url} = actor = insert(:actor, domain: nil)
%Event{url: event_url} = event = insert(:event, organizer_actor: organizer_actor)
assert {:accept, {:ok, %Activity{data: data, local: true}, %Participant{}}} =
Events.join(event, actor, true, %{})
assert match?(
%{
"actor" => ^organizer_actor_url,
"cc" => [],
"object" => %{
"actor" => ^participant_actor_url,
"object" => ^event_url,
"participationMessage" => nil,
"type" => "Join"
},
"to" => [^participant_actor_url, ^organizer_actor_url],
"type" => "Accept"
},
data
)
end
test "group event local" do
%Actor{url: organizer_group_url, members_url: members_url, followers_url: followers_url} =
organizer_group = insert(:group, domain: nil)
%Actor{url: participant_actor_url} = actor = insert(:actor, domain: nil)
%Event{url: event_url} = event = insert(:event, attributed_to: organizer_group)
assert {:ok, data, %Participant{}} = Events.join(event, actor, true, %{})
assert match?(
%{
"actor" => ^participant_actor_url,
"cc" => [^followers_url, ^members_url],
"object" => ^event_url,
"participationMessage" => nil,
"to" => [^participant_actor_url, ^organizer_group_url],
"type" => "Join"
},
data
)
end
test "group event with organizer remote" do
%Actor{url: organizer_group_url, members_url: members_url, followers_url: followers_url} =
organizer_group = insert(:group, domain: nil)
%Actor{url: organizer_actor_url} =
organizer_actor =
insert(:actor, domain: "somewhere.else", url: "https://somewhere.else/@someone")
insert(:member, parent: organizer_group, actor: organizer_actor, role: :moderator)
%Actor{url: participant_actor_url} = actor = insert(:actor, domain: nil)
%Event{url: event_url} =
event =
insert(:event,
attributed_to: organizer_group,
organizer_actor: organizer_actor,
local: true
)
assert {:ok, data, %Participant{}} = Events.join(event, actor, true, %{})
assert match?(
%{
"actor" => ^participant_actor_url,
"cc" => [^followers_url, ^members_url],
"object" => ^event_url,
"participationMessage" => nil,
"to" => [^participant_actor_url, ^organizer_group_url],
"type" => "Join"
},
data
)
end
end
end

View File

@ -5,8 +5,11 @@ defmodule Mobilizon.Web.Resolvers.EventTest do
import Mobilizon.Factory import Mobilizon.Factory
alias Mobilizon.Events alias Mobilizon.Actors.Actor
alias Mobilizon.{Events, Users}
alias Mobilizon.Events.Event
alias Mobilizon.Service.Workers alias Mobilizon.Service.Workers
alias Mobilizon.Users.User
alias Mobilizon.GraphQL.AbsintheHelpers alias Mobilizon.GraphQL.AbsintheHelpers
@ -21,6 +24,15 @@ defmodule Mobilizon.Web.Resolvers.EventTest do
category: "meeting" category: "meeting"
} }
@find_event_query """
query Event($uuid: UUID!) {
event(uuid: $uuid) {
uuid,
draft
}
}
"""
setup %{conn: conn} do setup %{conn: conn} do
user = insert(:user) user = insert(:user)
actor = insert(:actor, user: user, preferred_username: "test") actor = insert(:actor, user: user, preferred_username: "test")
@ -28,16 +40,7 @@ defmodule Mobilizon.Web.Resolvers.EventTest do
{:ok, conn: conn, actor: actor, user: user} {:ok, conn: conn, actor: actor, user: user}
end end
describe "Event Resolver" do describe "Find an event" do
@find_event_query """
query Event($uuid: UUID!) {
event(uuid: $uuid) {
uuid,
draft
}
}
"""
test "find_event/3 returns an event", context do test "find_event/3 returns an event", context do
event = event =
@event @event
@ -66,7 +69,9 @@ defmodule Mobilizon.Web.Resolvers.EventTest do
assert [%{"message" => "Event not found"}] = res["errors"] assert [%{"message" => "Event not found"}] = res["errors"]
end end
end
describe "create_event/3 for a regular profile" do
@create_event_mutation """ @create_event_mutation """
mutation CreateEvent( mutation CreateEvent(
$title: String!, $title: String!,
@ -76,6 +81,7 @@ defmodule Mobilizon.Web.Resolvers.EventTest do
$status: EventStatus, $status: EventStatus,
$visibility: EventVisibility, $visibility: EventVisibility,
$organizer_actor_id: ID!, $organizer_actor_id: ID!,
$attributed_to_id: ID,
$online_address: String, $online_address: String,
$options: EventOptionsInput, $options: EventOptionsInput,
$draft: Boolean $draft: Boolean
@ -88,6 +94,7 @@ defmodule Mobilizon.Web.Resolvers.EventTest do
status: $status, status: $status,
visibility: $visibility, visibility: $visibility,
organizer_actor_id: $organizer_actor_id, organizer_actor_id: $organizer_actor_id,
attributed_to_id: $attributed_to_id,
online_address: $online_address, online_address: $online_address,
options: $options, options: $options,
draft: $draft draft: $draft
@ -629,30 +636,171 @@ defmodule Mobilizon.Web.Resolvers.EventTest do
assert json_response(res, 200)["data"]["createEvent"]["picture"]["url"] assert json_response(res, 200)["data"]["createEvent"]["picture"]["url"]
end end
end
test "update_event/3 should check the event exists", %{conn: conn, actor: _actor, user: user} do describe "create_event/3 on behalf of a group" do
mutation = """ @variables %{
mutation { title: "come to my event",
updateEvent( description: "it will be fine",
event_id: 45, begins_on: "2021-07-26T09:00:00Z"
title: "my event updated" }
) {
title, test "create_event/3 should check the member has permission to create a group event", %{
uuid, conn: conn
tags { } do
title, %User{} = user = insert(:user)
slug %Actor{id: group_id} = group = insert(:group)
}
} %Actor{id: member_not_approved_actor_id} =
} member_not_approved_actor = insert(:actor, user: user)
"""
insert(:member, parent: group, actor: member_not_approved_actor)
%Actor{id: member_actor_id} = member_actor = insert(:actor, user: user)
insert(:member, parent: group, actor: member_actor, role: :member)
%Actor{id: moderator_actor_id} = moderator_actor = insert(:actor, user: user)
insert(:member, parent: group, actor: moderator_actor, role: :moderator)
%Actor{id: not_member_actor_id} = insert(:actor, user: user)
variables = Map.put(@variables, :attributed_to_id, "#{group_id}")
res = res =
conn conn
|> auth_conn(user) |> auth_conn(user)
|> post("/api", AbsintheHelpers.mutation_skeleton(mutation)) |> AbsintheHelpers.graphql_query(
query: @create_event_mutation,
variables: Map.put(variables, :organizer_actor_id, "#{member_not_approved_actor_id}")
)
assert hd(json_response(res, 200)["errors"])["message"] == "Event not found" assert res["data"]["createEvent"] == nil
assert hd(res["errors"])["message"] ==
"Organizer profile doesn't have permission to create an event on behalf of this group"
res =
conn
|> auth_conn(user)
|> AbsintheHelpers.graphql_query(
query: @create_event_mutation,
variables: Map.put(variables, :organizer_actor_id, "#{not_member_actor_id}")
)
assert res["data"]["createEvent"] == nil
assert hd(res["errors"])["message"] ==
"Organizer profile doesn't have permission to create an event on behalf of this group"
res =
conn
|> auth_conn(user)
|> AbsintheHelpers.graphql_query(
query: @create_event_mutation,
variables: Map.put(variables, :organizer_actor_id, "#{member_actor_id}")
)
assert res["data"]["createEvent"] == nil
assert hd(res["errors"])["message"] ==
"Organizer profile doesn't have permission to create an event on behalf of this group"
res =
conn
|> auth_conn(user)
|> AbsintheHelpers.graphql_query(
query: @create_event_mutation,
variables: Map.put(variables, :organizer_actor_id, "#{moderator_actor_id}")
)
assert res["errors"] == nil
assert res["data"]["createEvent"] != nil
end
end
@update_event_mutation """
mutation updateEvent(
$eventId: ID!
$title: String
$description: String
$beginsOn: DateTime
$endsOn: DateTime
$status: EventStatus
$visibility: EventVisibility
$joinOptions: EventJoinOptions
$draft: Boolean
$tags: [String]
$picture: MediaInput
$onlineAddress: String
$phoneAddress: String
$organizerActorId: ID
$attributedToId: ID
$category: String
$physicalAddress: AddressInput
$options: EventOptionsInput
$contacts: [Contact]
) {
updateEvent(
eventId: $eventId
title: $title
description: $description
beginsOn: $beginsOn
endsOn: $endsOn
status: $status
visibility: $visibility
joinOptions: $joinOptions
draft: $draft
tags: $tags
picture: $picture
onlineAddress: $onlineAddress
phoneAddress: $phoneAddress
organizerActorId: $organizerActorId
attributedToId: $attributedToId
category: $category
physicalAddress: $physicalAddress
options: $options
contacts: $contacts
) {
id,
uuid,
url,
title
draft
description
beginsOn
endsOn
status
tags {
title,
slug
},
online_address,
phone_address,
category,
options {
maximumAttendeeCapacity,
showRemainingAttendeeCapacity
},
physicalAddress {
url,
geom,
street
}
picture {
name
}
}
}
"""
describe "update_event/3" do
test "update_event/3 should check the event exists", %{conn: conn, actor: _actor, user: user} do
res =
conn
|> auth_conn(user)
|> AbsintheHelpers.graphql_query(
query: @update_event_mutation,
variables: %{eventId: 45, title: "my event updated"}
)
assert hd(res["errors"])["message"] == "Event not found"
end end
test "update_event/3 should check the user can change the organizer", %{ test "update_event/3 should check the user can change the organizer", %{
@ -663,29 +811,15 @@ defmodule Mobilizon.Web.Resolvers.EventTest do
event = insert(:event, organizer_actor: actor) event = insert(:event, organizer_actor: actor)
actor2 = insert(:actor) actor2 = insert(:actor)
mutation = """
mutation {
updateEvent(
title: "my event updated",
event_id: #{event.id}
organizer_actor_id: #{actor2.id}
) {
title,
uuid,
tags {
title,
slug
}
}
}
"""
res = res =
conn conn
|> auth_conn(user) |> auth_conn(user)
|> post("/api", AbsintheHelpers.mutation_skeleton(mutation)) |> AbsintheHelpers.graphql_query(
query: @update_event_mutation,
variables: %{eventId: event.id, title: "my event updated", organizerActorId: actor2.id}
)
assert hd(json_response(res, 200)["errors"])["message"] == assert hd(res["errors"])["message"] ==
"You can't attribute this event to this profile." "You can't attribute this event to this profile."
end end
@ -696,28 +830,15 @@ defmodule Mobilizon.Web.Resolvers.EventTest do
} do } do
event = insert(:event) event = insert(:event)
mutation = """
mutation {
updateEvent(
title: "my event updated",
event_id: #{event.id}
) {
title,
uuid,
tags {
title,
slug
}
}
}
"""
res = res =
conn conn
|> auth_conn(user) |> auth_conn(user)
|> post("/api", AbsintheHelpers.mutation_skeleton(mutation)) |> AbsintheHelpers.graphql_query(
query: @update_event_mutation,
variables: %{eventId: event.id, title: "my event updated"}
)
assert hd(json_response(res, 200)["errors"])["message"] == "You can't edit this event." assert hd(res["errors"])["message"] == "You can't edit this event."
end end
test "update_event/3 should check the user is the organizer also when it's changed", %{ test "update_event/3 should check the user is the organizer also when it's changed", %{
@ -727,29 +848,15 @@ defmodule Mobilizon.Web.Resolvers.EventTest do
} do } do
event = insert(:event) event = insert(:event)
mutation = """
mutation {
updateEvent(
title: "my event updated",
event_id: #{event.id},
organizer_actor_id: #{actor.id}
) {
title,
uuid,
tags {
title,
slug
}
}
}
"""
res = res =
conn conn
|> auth_conn(user) |> auth_conn(user)
|> post("/api", AbsintheHelpers.mutation_skeleton(mutation)) |> AbsintheHelpers.graphql_query(
query: @update_event_mutation,
variables: %{eventId: event.id, title: "my event updated", organizerActorId: actor.id}
)
assert hd(json_response(res, 200)["errors"])["message"] == "You can't edit this event." assert hd(res["errors"])["message"] == "You can't edit this event."
end end
test "update_event/3 should check end time is after the beginning time", %{ test "update_event/3 should check end time is after the beginning time", %{
@ -759,29 +866,19 @@ defmodule Mobilizon.Web.Resolvers.EventTest do
} do } do
event = insert(:event, organizer_actor: actor) event = insert(:event, organizer_actor: actor)
mutation = """
mutation {
updateEvent(
title: "my event updated",
ends_on: "#{Timex.shift(event.begins_on, hours: -2)}",
event_id: #{event.id}
) {
title,
uuid,
tags {
title,
slug
}
}
}
"""
res = res =
conn conn
|> auth_conn(user) |> auth_conn(user)
|> post("/api", AbsintheHelpers.mutation_skeleton(mutation)) |> AbsintheHelpers.graphql_query(
query: @update_event_mutation,
variables: %{
eventId: event.id,
title: "my event updated",
endsOn: event.begins_on |> DateTime.add(3600 * -2) |> DateTime.to_iso8601()
}
)
assert hd(json_response(res, 200)["errors"])["message"] == assert hd(res["errors"])["message"] ==
["ends_on cannot be set before begins_on"] ["ends_on cannot be set before begins_on"]
end end
@ -799,69 +896,41 @@ defmodule Mobilizon.Web.Resolvers.EventTest do
begins_on = DateTime.utc_now() |> DateTime.truncate(:second) |> DateTime.to_iso8601() begins_on = DateTime.utc_now() |> DateTime.truncate(:second) |> DateTime.to_iso8601()
ends_on = DateTime.utc_now() |> DateTime.truncate(:second) |> DateTime.to_iso8601() ends_on = DateTime.utc_now() |> DateTime.truncate(:second) |> DateTime.to_iso8601()
mutation = """
mutation {
updateEvent(
event_id: #{event.id},
title: "my event updated",
description: "description updated",
begins_on: "#{begins_on}",
ends_on: "#{ends_on}",
status: TENTATIVE,
tags: ["tag1_updated", "tag2_updated"],
online_address: "toto@example.com",
phone_address: "0000000000",
category: "birthday",
options: {
maximumAttendeeCapacity: 30,
showRemainingAttendeeCapacity: true
},
physical_address: {
street: "#{address.street}",
locality: "#{address.locality}"
}
) {
id,
uuid,
url,
title,
description,
begins_on,
ends_on,
status,
tags {
title,
slug
},
online_address,
phone_address,
category,
options {
maximumAttendeeCapacity,
showRemainingAttendeeCapacity
},
physicalAddress {
url,
geom,
street
}
}
}
"""
res = res =
conn conn
|> auth_conn(user) |> auth_conn(user)
|> post("/api", AbsintheHelpers.mutation_skeleton(mutation)) |> AbsintheHelpers.graphql_query(
query: @update_event_mutation,
variables: %{
eventId: event.id,
title: "my event updated",
description: "description updated",
beginsOn: "#{begins_on}",
endsOn: "#{ends_on}",
status: "TENTATIVE",
tags: ["tag1_updated", "tag2_updated"],
onlineAddress: "toto@example.com",
phoneAddress: "0000000000",
category: "birthday",
options: %{
maximumAttendeeCapacity: 30,
showRemainingAttendeeCapacity: true
},
physicalAddress: %{
street: "#{address.street}",
locality: "#{address.locality}"
}
}
)
assert json_response(res, 200)["errors"] == nil assert res["errors"] == nil
event_res = json_response(res, 200)["data"]["updateEvent"] event_res = res["data"]["updateEvent"]
assert event_res["title"] == "my event updated" assert event_res["title"] == "my event updated"
assert event_res["description"] == "description updated" assert event_res["description"] == "description updated"
assert event_res["begins_on"] == "#{begins_on}" assert event_res["beginsOn"] == "#{begins_on}"
assert event_res["ends_on"] == "#{ends_on}" assert event_res["endsOn"] == "#{ends_on}"
assert event_res["status"] == "TENTATIVE" assert event_res["status"] == "TENTATIVE"
assert event_res["online_address"] == "toto@example.com" assert event_res["online_address"] == "toto@example.com"
assert event_res["phone_address"] == "0000000000" assert event_res["phone_address"] == "0000000000"
@ -920,212 +989,166 @@ defmodule Mobilizon.Web.Resolvers.EventTest do
begins_on = begins_on =
event.begins_on event.begins_on
|> Timex.shift(hours: 3) |> DateTime.add(3 * 3600)
|> DateTime.truncate(:second) |> DateTime.truncate(:second)
|> DateTime.to_iso8601() |> DateTime.to_iso8601()
mutation = """
mutation {
updateEvent(
title: "my event updated",
description: "description updated",
begins_on: "#{begins_on}",
event_id: #{event.id},
category: "birthday",
picture: {
media: {
name: "picture for my event",
alt: "A very sunny landscape",
file: "event.jpg",
actor_id: "#{actor.id}"
}
}
) {
title,
uuid,
url,
beginsOn,
picture {
name,
url
}
}
}
"""
map = %{
"query" => mutation,
"event.jpg" => %Plug.Upload{
path: "test/fixtures/picture.png",
filename: "event.jpg"
}
}
res = res =
conn conn
|> auth_conn(user) |> auth_conn(user)
|> put_req_header("content-type", "multipart/form-data") |> put_req_header("content-type", "multipart/form-data")
|> post("/api", map) |> AbsintheHelpers.graphql_query(
query: @update_event_mutation,
variables: %{
eventId: event.id,
title: "my event updated",
description: "description updated",
beginsOn: "#{begins_on}",
category: "birthday",
picture: %{
media: %{
name: "picture for my event",
alt: "A very sunny landscape",
file: "event.jpg",
actorId: "#{actor.id}"
}
}
},
uploads: %{
"event.jpg" => %Plug.Upload{
path: "test/fixtures/picture.png",
filename: "event.jpg"
}
}
)
assert json_response(res, 200)["errors"] == nil assert res["errors"] == nil
assert json_response(res, 200)["data"]["updateEvent"]["title"] == "my event updated" assert res["data"]["updateEvent"]["title"] == "my event updated"
assert json_response(res, 200)["data"]["updateEvent"]["uuid"] == event.uuid assert res["data"]["updateEvent"]["uuid"] == event.uuid
assert json_response(res, 200)["data"]["updateEvent"]["url"] == event.url assert res["data"]["updateEvent"]["url"] == event.url
assert json_response(res, 200)["data"]["updateEvent"]["beginsOn"] == assert res["data"]["updateEvent"]["beginsOn"] ==
DateTime.to_iso8601(event.begins_on |> Timex.shift(hours: 3)) event.begins_on |> DateTime.add(3 * 3600) |> DateTime.to_iso8601()
assert json_response(res, 200)["data"]["updateEvent"]["picture"]["name"] == assert res["data"]["updateEvent"]["picture"]["name"] ==
"picture for my event" "picture for my event"
end end
test "update_event/3 respects the draft status", %{conn: conn, actor: actor, user: user} do @person_participations_query """
query EventPersonParticipation($actorId: ID!, $eventId: ID!) {
person(id: $actorId) {
id
participations(eventId: $eventId) {
total
elements {
role
actor {
id
}
event {
id
}
}
}
}
}
"""
test "respects the draft status", %{conn: conn, actor: actor, user: user} do
event = insert(:event, organizer_actor: actor, draft: true) event = insert(:event, organizer_actor: actor, draft: true)
mutation = """
mutation {
updateEvent(
event_id: #{event.id},
title: "my event updated but still draft"
) {
draft,
title,
uuid
}
}
"""
res = res =
conn conn
|> auth_conn(user) |> auth_conn(user)
|> post("/api", AbsintheHelpers.mutation_skeleton(mutation)) |> AbsintheHelpers.graphql_query(
query: @update_event_mutation,
assert json_response(res, 200)["data"]["updateEvent"]["draft"] == true variables: %{
eventId: event.id,
query = """ title: "my event updated but still draft",
{ draft: true
event(uuid: "#{event.uuid}") {
uuid,
draft
}
}
"""
res =
conn
|> get("/api", AbsintheHelpers.query_skeleton(query, "event"))
assert hd(json_response(res, 200)["errors"])["message"] =~ "not found"
query = """
{
event(uuid: "#{event.uuid}") {
uuid,
draft
}
}
"""
res =
conn
|> auth_conn(user)
|> get("/api", AbsintheHelpers.query_skeleton(query, "event"))
assert json_response(res, 200)["errors"] == nil
assert json_response(res, 200)["data"]["event"]["draft"] == true
query = """
{
person(id: "#{actor.id}") {
id,
participations(eventId: #{event.id}) {
elements {
id,
role,
actor {
id
},
event {
id
}
}
} }
} )
}
""" assert res["data"]["updateEvent"]["draft"] == true
res = res =
conn conn
|> auth_conn(user) |> AbsintheHelpers.graphql_query(
|> get("/api", AbsintheHelpers.query_skeleton(query, "person")) query: @find_event_query,
variables: %{
assert json_response(res, 200)["errors"] == nil uuid: event.uuid
assert json_response(res, 200)["data"]["person"]["participations"]["elements"] == []
mutation = """
mutation {
updateEvent(
event_id: #{event.id},
title: "my event updated and no longer draft",
draft: false
) {
draft,
title,
uuid
}
}
"""
res =
conn
|> auth_conn(user)
|> post("/api", AbsintheHelpers.mutation_skeleton(mutation))
assert json_response(res, 200)["data"]["updateEvent"]["draft"] == false
query = """
{
event(uuid: "#{event.uuid}") {
uuid,
draft
}
}
"""
res =
conn
|> get("/api", AbsintheHelpers.query_skeleton(query, "event"))
assert json_response(res, 200)["errors"] == nil
assert json_response(res, 200)["data"]["event"]["draft"] == false
query = """
{
person(id: "#{actor.id}") {
id,
participations(eventId: #{event.id}) {
elements {
role,
actor {
id
},
event {
id
}
}
} }
} )
}
""" assert hd(res["errors"])["message"] =~ "not found"
res = res =
conn conn
|> auth_conn(user) |> auth_conn(user)
|> get("/api", AbsintheHelpers.query_skeleton(query, "person")) |> AbsintheHelpers.graphql_query(
query: @find_event_query,
variables: %{
uuid: event.uuid
}
)
assert json_response(res, 200)["errors"] == nil assert res["errors"] == nil
assert res["data"]["event"]["draft"] == true
assert json_response(res, 200)["data"]["person"]["participations"]["elements"] == [ res =
conn
|> auth_conn(user)
|> AbsintheHelpers.graphql_query(
query: @person_participations_query,
variables: %{
eventId: event.id,
actorId: actor.id
}
)
assert res["errors"] == nil
assert res["data"]["person"]["participations"]["elements"] == []
res =
conn
|> auth_conn(user)
|> AbsintheHelpers.graphql_query(
query: @update_event_mutation,
variables: %{
eventId: event.id,
title: "my event updated and no longer draft",
draft: false
}
)
assert res["data"]["updateEvent"]["draft"] == false
res =
conn
|> AbsintheHelpers.graphql_query(
query: @find_event_query,
variables: %{
uuid: event.uuid
}
)
assert res["errors"] == nil
assert res["data"]["event"]["draft"] == false
res =
conn
|> auth_conn(user)
|> AbsintheHelpers.graphql_query(
query: @person_participations_query,
variables: %{
eventId: event.id,
actorId: actor.id
}
)
assert res["errors"] == nil
assert res["data"]["person"]["participations"]["elements"] == [
%{ %{
"actor" => %{"id" => to_string(actor.id)}, "actor" => %{"id" => to_string(actor.id)},
"event" => %{"id" => to_string(event.id)}, "event" => %{"id" => to_string(event.id)},
@ -1133,7 +1156,98 @@ defmodule Mobilizon.Web.Resolvers.EventTest do
} }
] ]
end end
end
describe "update_event/3 on behalf of a group" do
test "should check the member has permission to update a group event", %{
conn: conn
} do
%User{} = user = insert(:user)
%Actor{id: group_id} = group = insert(:group)
%Actor{id: member_not_approved_actor_id} =
member_not_approved_actor = insert(:actor, user: user)
insert(:member, parent: group, actor: member_not_approved_actor)
%Actor{id: member_actor_id} = member_actor = insert(:actor, user: user)
insert(:member, parent: group, actor: member_actor, role: :member)
%Actor{id: moderator_actor_id} = moderator_actor = insert(:actor, user: user)
insert(:member, parent: group, actor: moderator_actor, role: :moderator)
%Actor{} = administrator_actor = insert(:actor, user: user)
insert(:member, parent: group, actor: administrator_actor, role: :administrator)
%Actor{id: not_member_actor_id} = insert(:actor, user: user)
%Event{} =
event = insert(:event, attributed_to: group, organizer_actor: administrator_actor)
variables =
@variables
|> Map.put(:attributed_to_id, "#{group_id}")
|> Map.put(:eventId, to_string(event.id))
Users.update_user_default_actor(user.id, member_not_approved_actor_id)
res =
conn
|> auth_conn(user)
|> AbsintheHelpers.graphql_query(
query: @update_event_mutation,
variables: Map.put(variables, :organizer_actor_id, "#{member_not_approved_actor_id}")
)
assert res["data"]["updateEvent"] == nil
assert hd(res["errors"])["message"] ==
"This profile doesn't have permission to update an event on behalf of this group"
Users.update_user_default_actor(user.id, not_member_actor_id)
res =
conn
|> auth_conn(user)
|> AbsintheHelpers.graphql_query(
query: @update_event_mutation,
variables: Map.put(variables, :organizer_actor_id, "#{not_member_actor_id}")
)
assert res["data"]["updateEvent"] == nil
assert hd(res["errors"])["message"] ==
"This profile doesn't have permission to update an event on behalf of this group"
Users.update_user_default_actor(user.id, member_actor_id)
res =
conn
|> auth_conn(user)
|> AbsintheHelpers.graphql_query(
query: @update_event_mutation,
variables: Map.put(variables, :organizer_actor_id, "#{member_actor_id}")
)
assert res["data"]["updateEvent"] == nil
assert hd(res["errors"])["message"] ==
"This profile doesn't have permission to update an event on behalf of this group"
Users.update_user_default_actor(user.id, moderator_actor_id)
res =
conn
|> auth_conn(user)
|> AbsintheHelpers.graphql_query(
query: @update_event_mutation,
variables: Map.put(variables, :organizer_actor_id, "#{moderator_actor_id}")
)
assert res["errors"] == nil
assert res["data"]["updateEvent"] != nil
end
end
describe "list_events/3" do
@fetch_events_query """ @fetch_events_query """
query Events($page: Int, $limit: Int) { query Events($page: Int, $limit: Int) {
events(page: $page, limit: $limit) { events(page: $page, limit: $limit) {
@ -1250,7 +1364,9 @@ defmodule Mobilizon.Web.Resolvers.EventTest do
# assert json_response(res, 200)["errors"] |> hd |> Map.get("message") == # assert json_response(res, 200)["errors"] |> hd |> Map.get("message") ==
# "Event with UUID #{event.uuid} not found" # "Event with UUID #{event.uuid} not found"
# end # end
end
describe "delete_event/3" do
test "delete_event/3 deletes an event", %{conn: conn, user: user, actor: actor} do test "delete_event/3 deletes an event", %{conn: conn, user: user, actor: actor} do
event = insert(:event, organizer_actor: actor) event = insert(:event, organizer_actor: actor)
@ -1393,7 +1509,9 @@ defmodule Mobilizon.Web.Resolvers.EventTest do
"object" => %{"title" => event.title, "id" => to_string(event.id)} "object" => %{"title" => event.title, "id" => to_string(event.id)}
} }
end end
end
describe "list_related_events/3" do
test "list_related_events/3 should give related events", %{ test "list_related_events/3 should give related events", %{
conn: conn, conn: conn,
actor: actor actor: actor

View File

@ -297,7 +297,7 @@ defmodule Mobilizon.GraphQL.Resolvers.PostTest do
} }
) )
assert is_nil(res["errors"]) assert res["errors"] == nil
assert res["data"]["post"]["title"] == post_draft.title assert res["data"]["post"]["title"] == post_draft.title
assert res["data"]["post"]["draft"] == true assert res["data"]["post"]["draft"] == true

View File

@ -205,6 +205,19 @@ defmodule Mobilizon.GraphQL.Resolvers.SearchTest do
assert hd(res["data"]["searchEvents"]["elements"])["uuid"] == assert hd(res["data"]["searchEvents"]["elements"])["uuid"] ==
event.uuid event.uuid
end end
test "doesn't find drafts", %{conn: conn} do
insert(:event, title: "A draft event", draft: true)
res =
AbsintheHelpers.graphql_query(conn,
query: @search_events_query,
variables: %{term: "draft"}
)
assert res["errors"] == nil
assert res["data"]["searchEvents"]["total"] == 0
end
end end
describe "search_persons/3" do describe "search_persons/3" do

View File

@ -28,14 +28,24 @@ defmodule Mobilizon.GraphQL.AbsintheHelpers do
@spec graphql_query(Conn.t(), Keyword.t()) :: map | no_return @spec graphql_query(Conn.t(), Keyword.t()) :: map | no_return
def graphql_query(conn, options) do def graphql_query(conn, options) do
conn conn
|> post("/api", build_query(options[:query], Keyword.get(options, :variables, %{}))) |> post(
"/api",
build_query(
options[:query],
Keyword.get(options, :variables, %{}),
Keyword.get(options, :uploads, %{})
)
)
|> json_response(200) |> json_response(200)
end end
defp build_query(query, variables) do defp build_query(query, variables, uploads) do
%{ Map.merge(
"query" => query, %{
"variables" => variables "query" => query,
} "variables" => variables
},
uploads
)
end end
end end

View File

@ -158,6 +158,7 @@ defmodule Mobilizon.Factory do
deleted_at: nil, deleted_at: nil,
tags: build_list(3, :tag), tags: build_list(3, :tag),
in_reply_to_comment: nil, in_reply_to_comment: nil,
origin_comment: nil,
is_announcement: false, is_announcement: false,
published_at: DateTime.utc_now(), published_at: DateTime.utc_now(),
url: Routes.page_url(Endpoint, :comment, uuid) url: Routes.page_url(Endpoint, :comment, uuid)
@ -450,4 +451,12 @@ defmodule Mobilizon.Factory do
user: build(:user) user: build(:user)
} }
end end
def share_factory do
%Mobilizon.Share{
actor: build(:actor),
owner_actor: build(:actor),
uri: sequence("https://someshare.uri/p/12")
}
end
end end