forked from potsda.mn/mobilizon
Merge branch 'dializer' into 'master'
Various typespec and compilation improvements See merge request framasoft/mobilizon!1062
This commit is contained in:
commit
d8b64e9a19
15
.doctor.exs
Normal file
15
.doctor.exs
Normal file
|
@ -0,0 +1,15 @@
|
|||
%Doctor.Config{
|
||||
exception_moduledoc_required: true,
|
||||
failed: false,
|
||||
ignore_modules: [Mobilizon.Web, Mobilizon.GraphQL.Schema, Mobilizon.Service.Activity.Renderer, Mobilizon.Service.Workers.Helper],
|
||||
ignore_paths: [],
|
||||
min_module_doc_coverage: 100,
|
||||
min_module_spec_coverage: 50,
|
||||
min_overall_doc_coverage: 100,
|
||||
min_overall_spec_coverage: 90,
|
||||
moduledoc_required: true,
|
||||
raise: false,
|
||||
reporter: Doctor.Reporters.Full,
|
||||
struct_type_spec_required: true,
|
||||
umbrella: false
|
||||
}
|
|
@ -179,6 +179,8 @@ config :phoenix, :filter_parameters, ["password", "token"]
|
|||
config :absinthe, schema: Mobilizon.GraphQL.Schema
|
||||
config :absinthe, Absinthe.Logger, filter_variables: ["token", "password", "secret"]
|
||||
|
||||
config :mobilizon, Mobilizon.Web.Gettext, one_module_per_locale: true
|
||||
|
||||
config :ex_cldr,
|
||||
default_locale: "en",
|
||||
default_backend: Mobilizon.Cldr
|
||||
|
@ -189,7 +191,8 @@ config :http_signatures,
|
|||
config :mobilizon, :cldr,
|
||||
locales: [
|
||||
"fr",
|
||||
"en"
|
||||
"en",
|
||||
"ru"
|
||||
]
|
||||
|
||||
config :mobilizon, :activitypub,
|
||||
|
|
|
@ -58,6 +58,8 @@ config :logger, :console, format: "[$level] $message\n", level: :debug
|
|||
|
||||
config :mobilizon, Mobilizon.Service.Geospatial, service: Mobilizon.Service.Geospatial.Nominatim
|
||||
|
||||
config :mobilizon, Mobilizon.Web.Gettext, allowed_locales: ["fr", "en"]
|
||||
|
||||
# Set a higher stacktrace during development. Avoid configuring such
|
||||
# in production as building large stacktraces may be expensive.
|
||||
config :phoenix, :stacktrace_depth, 20
|
||||
|
|
|
@ -77,6 +77,8 @@ config :mobilizon, Mobilizon.Web.Auth.Guardian, secret_key: "some secret"
|
|||
|
||||
config :mobilizon, :activitypub, sign_object_fetches: false
|
||||
|
||||
config :mobilizon, Mobilizon.Web.Gettext, allowed_locales: ["fr", "en", "es", "ru"]
|
||||
|
||||
config :junit_formatter, report_dir: "."
|
||||
|
||||
if System.get_env("DOCKER", "false") == "false" && File.exists?("./config/test.secret.exs") do
|
||||
|
|
|
@ -29,19 +29,28 @@
|
|||
</template>
|
||||
<script lang="ts">
|
||||
import { Component, Prop, Vue } from "vue-property-decorator";
|
||||
import get from "lodash/get";
|
||||
import differenceBy from "lodash/differenceBy";
|
||||
import { ITag } from "../../types/tag.model";
|
||||
import { FILTER_TAGS } from "@/graphql/tags";
|
||||
|
||||
@Component
|
||||
@Component({
|
||||
apollo: {
|
||||
tags: {
|
||||
query: FILTER_TAGS,
|
||||
variables() {
|
||||
return {
|
||||
filter: this.text,
|
||||
};
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
export default class TagInput extends Vue {
|
||||
@Prop({ required: false, default: () => [] }) data!: ITag[];
|
||||
|
||||
@Prop({ required: true, default: "value" }) path!: string;
|
||||
|
||||
@Prop({ required: true }) value!: ITag[];
|
||||
|
||||
filteredTags: ITag[] = [];
|
||||
tags!: ITag[];
|
||||
|
||||
text = "";
|
||||
|
||||
private static componentId = 0;
|
||||
|
||||
|
@ -53,13 +62,20 @@ export default class TagInput extends Vue {
|
|||
return `tag-input-${TagInput.componentId}`;
|
||||
}
|
||||
|
||||
getFilteredTags(text: string): void {
|
||||
this.filteredTags = differenceBy(this.data, this.value, "id").filter(
|
||||
async getFilteredTags(text: string): Promise<void> {
|
||||
this.text = text;
|
||||
await this.$apollo.queries.tags.refetch();
|
||||
}
|
||||
|
||||
get filteredTags(): ITag[] {
|
||||
return differenceBy(this.tags, this.value, "id").filter(
|
||||
(option) =>
|
||||
get(option, this.path)
|
||||
option.title
|
||||
.toString()
|
||||
.toLowerCase()
|
||||
.indexOf(text.toLowerCase()) >= 0
|
||||
.indexOf(this.text.toLowerCase()) >= 0 ||
|
||||
option.slug.toString().toLowerCase().indexOf(this.text.toLowerCase()) >=
|
||||
0
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -9,16 +9,22 @@ export const TAG_FRAGMENT = gql`
|
|||
`;
|
||||
|
||||
export const TAGS = gql`
|
||||
query {
|
||||
query Tags {
|
||||
tags {
|
||||
id
|
||||
related {
|
||||
id
|
||||
slug
|
||||
title
|
||||
...TagFragment
|
||||
}
|
||||
slug
|
||||
title
|
||||
...TagFragment
|
||||
}
|
||||
}
|
||||
${TAG_FRAGMENT}
|
||||
`;
|
||||
|
||||
export const FILTER_TAGS = gql`
|
||||
query FilterTags($filter: String) {
|
||||
tags(filter: $filter) {
|
||||
...TagFragment
|
||||
}
|
||||
}
|
||||
${TAG_FRAGMENT}
|
||||
`;
|
||||
|
|
|
@ -21,7 +21,6 @@
|
|||
"A discussion has been created or updated": "Se ha creado o actualizado una discusión",
|
||||
"A federated software": "Un software federado",
|
||||
"A fediverse account URL to follow for event updates": "Una URL de cuenta de fediverse a seguir para actualizaciones de eventos",
|
||||
"A group with this name already exists": "Ya existe un grupo con este nombre",
|
||||
"A link to a page presenting the event schedule": "Un enlace a una página que presenta el calendario del evento",
|
||||
"A link to a page presenting the price options": "Un enlace a una página que presenta las opciones de precio",
|
||||
"A member has been updated": "Un miembro ha sido actualizado",
|
||||
|
|
|
@ -19,7 +19,6 @@
|
|||
"A discussion has been created or updated": "Une discussion a été créée ou mise à jour",
|
||||
"A federated software": "Un logiciel fédéré",
|
||||
"A fediverse account URL to follow for event updates": "Un compte sur le fediverse à suivre pour les mises à jour de l'événement",
|
||||
"A group with this name already exists": "Un groupe avec ce nom existe déjà",
|
||||
"A link to a page presenting the event schedule": "Un lien vers une page présentant le programme de l'événement",
|
||||
"A link to a page presenting the price options": "Un lien vers une page présentant la tarification",
|
||||
"A member has been updated": "Un membre a été mis à jour",
|
||||
|
|
|
@ -31,7 +31,7 @@
|
|||
/>
|
||||
</b-field>
|
||||
|
||||
<tag-input v-model="event.tags" :data="tags" path="title" />
|
||||
<tag-input v-model="event.tags" />
|
||||
|
||||
<b-field
|
||||
horizontal
|
||||
|
@ -556,8 +556,6 @@ import {
|
|||
IPerson,
|
||||
usernameWithDomain,
|
||||
} from "../../types/actor";
|
||||
import { TAGS } from "../../graphql/tags";
|
||||
import { ITag } from "../../types/tag.model";
|
||||
import {
|
||||
buildFileFromIMedia,
|
||||
buildFileVariable,
|
||||
|
@ -590,7 +588,6 @@ const DEFAULT_LIMIT_NUMBER_OF_PLACES = 10;
|
|||
},
|
||||
apollo: {
|
||||
currentActor: CURRENT_ACTOR_CLIENT,
|
||||
tags: TAGS,
|
||||
config: CONFIG,
|
||||
identities: IDENTITIES,
|
||||
event: {
|
||||
|
@ -643,8 +640,6 @@ export default class EditEvent extends Vue {
|
|||
|
||||
currentActor!: IActor;
|
||||
|
||||
tags: ITag[] = [];
|
||||
|
||||
event: IEvent = new EventModel();
|
||||
|
||||
unmodifiedEvent: IEvent = new EventModel();
|
||||
|
|
|
@ -67,7 +67,7 @@
|
|||
/>
|
||||
</b-field>
|
||||
|
||||
<tag-input v-model="editablePost.tags" :data="tags" path="title" />
|
||||
<tag-input v-model="editablePost.tags" />
|
||||
|
||||
<div class="field">
|
||||
<label class="label">{{ $t("Post") }}</label>
|
||||
|
@ -166,7 +166,6 @@ import {
|
|||
} from "@/utils/image";
|
||||
import GroupMixin from "@/mixins/group";
|
||||
import { PostVisibility } from "@/types/enums";
|
||||
import { TAGS } from "../../graphql/tags";
|
||||
import { CONFIG } from "../../graphql/config";
|
||||
import {
|
||||
FETCH_POST,
|
||||
|
@ -187,7 +186,6 @@ import { FETCH_GROUP } from "@/graphql/group";
|
|||
|
||||
@Component({
|
||||
apollo: {
|
||||
tags: TAGS,
|
||||
config: CONFIG,
|
||||
group: {
|
||||
query: FETCH_GROUP,
|
||||
|
|
|
@ -320,6 +320,8 @@ export default class AccountSettings extends Vue {
|
|||
},
|
||||
});
|
||||
|
||||
this.oldPassword = "";
|
||||
this.newPassword = "";
|
||||
this.$notifier.success(
|
||||
this.$t("The password was successfully changed") as string
|
||||
);
|
||||
|
|
|
@ -4,8 +4,10 @@ defmodule Mobilizon.ConfigProvider do
|
|||
"""
|
||||
@behaviour Config.Provider
|
||||
|
||||
@spec init(String.t()) :: String.t()
|
||||
def init(path) when is_binary(path), do: path
|
||||
|
||||
@spec load(Keyword.t(), String.t()) :: Keyword.t()
|
||||
def load(config, path) do
|
||||
config_path = System.get_env("MOBILIZON_CONFIG_PATH") || path
|
||||
|
||||
|
|
169
lib/federation/activity_pub/actions/accept.ex
Normal file
169
lib/federation/activity_pub/actions/accept.ex
Normal file
|
@ -0,0 +1,169 @@
|
|||
defmodule Mobilizon.Federation.ActivityPub.Actions.Accept do
|
||||
@moduledoc """
|
||||
Accept things
|
||||
"""
|
||||
|
||||
alias Mobilizon.{Actors, Events}
|
||||
alias Mobilizon.Actors.{Actor, Follower, Member}
|
||||
alias Mobilizon.Events.Participant
|
||||
alias Mobilizon.Federation.ActivityPub.{Audience, Refresher}
|
||||
alias Mobilizon.Federation.ActivityStream
|
||||
alias Mobilizon.Federation.ActivityStream.Convertible
|
||||
alias Mobilizon.Service.Notifications.Scheduler
|
||||
alias Mobilizon.Web.Endpoint
|
||||
require Logger
|
||||
|
||||
import Mobilizon.Federation.ActivityPub.Utils,
|
||||
only: [
|
||||
make_accept_join_data: 2,
|
||||
create_activity: 2,
|
||||
maybe_federate: 1,
|
||||
maybe_relay_if_group_activity: 1
|
||||
]
|
||||
|
||||
@type acceptable_types :: :join | :follow | :invite
|
||||
@type acceptable_entities ::
|
||||
accept_join_entities | accept_follow_entities | accept_invite_entities
|
||||
|
||||
@spec accept(acceptable_types, acceptable_entities, boolean, map) ::
|
||||
{:ok, ActivityStream.t(), acceptable_entities}
|
||||
def accept(type, entity, local \\ true, additional \\ %{}) do
|
||||
Logger.debug("We're accepting something")
|
||||
|
||||
accept_res =
|
||||
case type do
|
||||
:join -> accept_join(entity, additional)
|
||||
:follow -> accept_follow(entity, additional)
|
||||
:invite -> accept_invite(entity, additional)
|
||||
end
|
||||
|
||||
with {:ok, entity, update_data} <- accept_res do
|
||||
{:ok, activity} = create_activity(update_data, local)
|
||||
maybe_federate(activity)
|
||||
maybe_relay_if_group_activity(activity)
|
||||
{:ok, activity, entity}
|
||||
end
|
||||
end
|
||||
|
||||
@type accept_follow_entities :: Follower.t()
|
||||
|
||||
@spec accept_follow(Follower.t(), map) ::
|
||||
{:ok, Follower.t(), Activity.t()} | {:error, Ecto.Changeset.t()}
|
||||
defp accept_follow(%Follower{} = follower, additional) do
|
||||
with {:ok, %Follower{} = follower} <- Actors.update_follower(follower, %{approved: true}) do
|
||||
follower_as_data = Convertible.model_to_as(follower)
|
||||
|
||||
update_data =
|
||||
make_accept_join_data(
|
||||
follower_as_data,
|
||||
Map.merge(additional, %{
|
||||
"id" => "#{Endpoint.url()}/accept/follow/#{follower.id}",
|
||||
"to" => [follower.actor.url],
|
||||
"cc" => [],
|
||||
"actor" => follower.target_actor.url
|
||||
})
|
||||
)
|
||||
|
||||
{:ok, follower, update_data}
|
||||
end
|
||||
end
|
||||
|
||||
@type accept_join_entities :: Participant.t() | Member.t()
|
||||
|
||||
@spec accept_join(Participant.t() | Member.t(), map) ::
|
||||
{:ok, Participant.t() | Member.t(), Activity.t()} | {:error, Ecto.Changeset.t()}
|
||||
defp accept_join(%Participant{} = participant, additional) do
|
||||
with {:ok, %Participant{} = participant} <-
|
||||
Events.update_participant(participant, %{role: :participant}) do
|
||||
Absinthe.Subscription.publish(Endpoint, participant.actor,
|
||||
event_person_participation_changed: participant.actor.id
|
||||
)
|
||||
|
||||
Scheduler.trigger_notifications_for_participant(participant)
|
||||
participant_as_data = Convertible.model_to_as(participant)
|
||||
audience = Audience.get_audience(participant)
|
||||
|
||||
accept_join_data =
|
||||
make_accept_join_data(
|
||||
participant_as_data,
|
||||
Map.merge(Map.merge(audience, additional), %{
|
||||
"id" => "#{Endpoint.url()}/accept/join/#{participant.id}"
|
||||
})
|
||||
)
|
||||
|
||||
{:ok, participant, accept_join_data}
|
||||
end
|
||||
end
|
||||
|
||||
defp accept_join(%Member{} = member, additional) do
|
||||
with {:ok, %Member{} = member} <-
|
||||
Actors.update_member(member, %{role: :member}) do
|
||||
Mobilizon.Service.Activity.Member.insert_activity(member,
|
||||
subject: "member_approved"
|
||||
)
|
||||
|
||||
maybe_refresh_group(member)
|
||||
|
||||
Absinthe.Subscription.publish(Endpoint, member.actor,
|
||||
group_membership_changed: [
|
||||
Actor.preferred_username_and_domain(member.parent),
|
||||
member.actor.id
|
||||
]
|
||||
)
|
||||
|
||||
member_as_data = Convertible.model_to_as(member)
|
||||
audience = Audience.get_audience(member)
|
||||
|
||||
accept_join_data =
|
||||
make_accept_join_data(
|
||||
member_as_data,
|
||||
Map.merge(Map.merge(audience, additional), %{
|
||||
"id" => "#{Endpoint.url()}/accept/join/#{member.id}"
|
||||
})
|
||||
)
|
||||
|
||||
{:ok, member, accept_join_data}
|
||||
end
|
||||
end
|
||||
|
||||
@type accept_invite_entities :: Member.t()
|
||||
|
||||
@spec accept_invite(Member.t(), map()) ::
|
||||
{:ok, Member.t(), Activity.t()} | {:error, Ecto.Changeset.t()}
|
||||
defp accept_invite(
|
||||
%Member{invited_by_id: invited_by_id, actor_id: actor_id} = member,
|
||||
_additional
|
||||
) do
|
||||
with %Actor{} = inviter <- Actors.get_actor!(invited_by_id),
|
||||
%Actor{url: actor_url} <- Actors.get_actor!(actor_id),
|
||||
{:ok, %Member{id: member_id} = member} <-
|
||||
Actors.update_member(member, %{role: :member}) do
|
||||
Mobilizon.Service.Activity.Member.insert_activity(member,
|
||||
subject: "member_accepted_invitation"
|
||||
)
|
||||
|
||||
maybe_refresh_group(member)
|
||||
|
||||
accept_data = %{
|
||||
"type" => "Accept",
|
||||
"attributedTo" => member.parent.url,
|
||||
"to" => [inviter.url, member.parent.members_url],
|
||||
"cc" => [member.parent.url],
|
||||
"actor" => actor_url,
|
||||
"object" => Convertible.model_to_as(member),
|
||||
"id" => "#{Endpoint.url()}/accept/invite/member/#{member_id}"
|
||||
}
|
||||
|
||||
{:ok, member, accept_data}
|
||||
end
|
||||
end
|
||||
|
||||
@spec maybe_refresh_group(Member.t()) :: :ok | nil
|
||||
defp maybe_refresh_group(%Member{
|
||||
parent: %Actor{domain: parent_domain, url: parent_url},
|
||||
actor: %Actor{} = actor
|
||||
}) do
|
||||
unless is_nil(parent_domain),
|
||||
do: Refresher.fetch_group(parent_url, actor)
|
||||
end
|
||||
end
|
60
lib/federation/activity_pub/actions/announce.ex
Normal file
60
lib/federation/activity_pub/actions/announce.ex
Normal file
|
@ -0,0 +1,60 @@
|
|||
defmodule Mobilizon.Federation.ActivityPub.Actions.Announce do
|
||||
@moduledoc """
|
||||
Announce things
|
||||
"""
|
||||
alias Mobilizon.Actors.Actor
|
||||
alias Mobilizon.Federation.ActivityPub.Activity
|
||||
alias Mobilizon.Federation.ActivityPub.Actor, as: ActivityPubActor
|
||||
alias Mobilizon.Federation.ActivityStream
|
||||
alias Mobilizon.Share
|
||||
|
||||
import Mobilizon.Federation.ActivityPub.Utils,
|
||||
only: [
|
||||
create_activity: 2,
|
||||
maybe_federate: 1,
|
||||
make_announce_data: 3,
|
||||
make_announce_data: 4,
|
||||
make_unannounce_data: 3
|
||||
]
|
||||
|
||||
@doc """
|
||||
Announce (reshare) an activity to the world, using an activity of type `Announce`.
|
||||
"""
|
||||
@spec announce(Actor.t(), ActivityStream.t(), String.t() | nil, boolean, boolean) ::
|
||||
{:ok, Activity.t(), ActivityStream.t()} | {:error, any()}
|
||||
def announce(
|
||||
%Actor{} = actor,
|
||||
object,
|
||||
activity_id \\ nil,
|
||||
local \\ true,
|
||||
public \\ true
|
||||
) do
|
||||
with {:ok, %Actor{id: object_owner_actor_id}} <-
|
||||
ActivityPubActor.get_or_fetch_actor_by_url(object["actor"]),
|
||||
{:ok, %Share{} = _share} <- Share.create(object["id"], actor.id, object_owner_actor_id) do
|
||||
announce_data = make_announce_data(actor, object, activity_id, public)
|
||||
{:ok, activity} = create_activity(announce_data, local)
|
||||
:ok = maybe_federate(activity)
|
||||
{:ok, activity, object}
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Cancel the announcement of an activity to the world, using an activity of type `Undo` an `Announce`.
|
||||
"""
|
||||
@spec unannounce(Actor.t(), ActivityStream.t(), String.t() | nil, String.t() | nil, boolean) ::
|
||||
{:ok, Activity.t(), ActivityStream.t()}
|
||||
def unannounce(
|
||||
%Actor{} = actor,
|
||||
object,
|
||||
activity_id \\ nil,
|
||||
cancelled_activity_id \\ nil,
|
||||
local \\ true
|
||||
) do
|
||||
announce_activity = make_announce_data(actor, object, cancelled_activity_id)
|
||||
unannounce_data = make_unannounce_data(actor, announce_activity, activity_id)
|
||||
{:ok, unannounce_activity} = create_activity(unannounce_data, local)
|
||||
maybe_federate(unannounce_activity)
|
||||
{:ok, unannounce_activity, object}
|
||||
end
|
||||
end
|
71
lib/federation/activity_pub/actions/create.ex
Normal file
71
lib/federation/activity_pub/actions/create.ex
Normal file
|
@ -0,0 +1,71 @@
|
|||
defmodule Mobilizon.Federation.ActivityPub.Actions.Create do
|
||||
@moduledoc """
|
||||
Create things
|
||||
"""
|
||||
alias Mobilizon.Tombstone
|
||||
alias Mobilizon.Federation.ActivityPub.{Activity, Types}
|
||||
require Logger
|
||||
|
||||
import Mobilizon.Federation.ActivityPub.Utils,
|
||||
only: [
|
||||
create_activity: 2,
|
||||
maybe_federate: 1,
|
||||
maybe_relay_if_group_activity: 1
|
||||
]
|
||||
|
||||
@type create_entities ::
|
||||
:event | :comment | :discussion | :actor | :todo_list | :todo | :resource | :post
|
||||
|
||||
@doc """
|
||||
Create an activity of type `Create`
|
||||
|
||||
* Creates the object, which returns AS data
|
||||
* Wraps ActivityStreams data into a `Create` activity
|
||||
* Creates an `Mobilizon.Federation.ActivityPub.Activity` from this
|
||||
* Federates (asynchronously) the activity
|
||||
* Returns the activity
|
||||
"""
|
||||
@spec create(create_entities(), map(), boolean, map()) ::
|
||||
{:ok, Activity.t(), Entity.t()}
|
||||
| {:error, :entity_tombstoned | atom() | Ecto.Changeset.t()}
|
||||
def create(type, args, local \\ false, additional \\ %{}) do
|
||||
Logger.debug("creating an activity")
|
||||
Logger.debug(inspect(args))
|
||||
|
||||
case check_for_tombstones(args) do
|
||||
nil ->
|
||||
case do_create(type, args, additional) do
|
||||
{:ok, entity, create_data} ->
|
||||
{:ok, activity} = create_activity(create_data, local)
|
||||
maybe_federate(activity)
|
||||
maybe_relay_if_group_activity(activity)
|
||||
{:ok, activity, entity}
|
||||
|
||||
{:error, err} ->
|
||||
{:error, err}
|
||||
end
|
||||
|
||||
%Tombstone{} ->
|
||||
{:error, :entity_tombstoned}
|
||||
end
|
||||
end
|
||||
|
||||
@spec do_create(create_entities(), map(), map()) ::
|
||||
{:ok, Entity.t(), Activity.t()} | {:error, Ecto.Changeset.t() | atom()}
|
||||
defp do_create(type, args, additional) do
|
||||
case type do
|
||||
:event -> Types.Events.create(args, additional)
|
||||
:comment -> Types.Comments.create(args, additional)
|
||||
:discussion -> Types.Discussions.create(args, additional)
|
||||
:actor -> Types.Actors.create(args, additional)
|
||||
:todo_list -> Types.TodoLists.create(args, additional)
|
||||
:todo -> Types.Todos.create(args, additional)
|
||||
:resource -> Types.Resources.create(args, additional)
|
||||
:post -> Types.Posts.create(args, additional)
|
||||
end
|
||||
end
|
||||
|
||||
@spec check_for_tombstones(map()) :: Tombstone.t() | nil
|
||||
defp check_for_tombstones(%{url: url}), do: Tombstone.find_tombstone(url)
|
||||
defp check_for_tombstones(_), do: nil
|
||||
end
|
33
lib/federation/activity_pub/actions/delete.ex
Normal file
33
lib/federation/activity_pub/actions/delete.ex
Normal file
|
@ -0,0 +1,33 @@
|
|||
defmodule Mobilizon.Federation.ActivityPub.Actions.Delete do
|
||||
@moduledoc """
|
||||
Delete things
|
||||
"""
|
||||
alias Mobilizon.Actors.Actor
|
||||
alias Mobilizon.Federation.ActivityPub.Activity
|
||||
alias Mobilizon.Federation.ActivityPub.Types.{Entity, Managable, Ownable}
|
||||
require Logger
|
||||
|
||||
import Mobilizon.Federation.ActivityPub.Utils,
|
||||
only: [
|
||||
create_activity: 2,
|
||||
maybe_federate: 1,
|
||||
maybe_relay_if_group_activity: 2,
|
||||
check_for_actor_key_rotation: 1
|
||||
]
|
||||
|
||||
@doc """
|
||||
Delete an entity, using an activity of type `Delete`
|
||||
"""
|
||||
@spec delete(Entity.t(), Actor.t(), boolean, map) :: {:ok, Activity.t(), Entity.t()}
|
||||
def delete(object, actor, local \\ true, additional \\ %{}) do
|
||||
with {:ok, activity_data, actor, object} <-
|
||||
Managable.delete(object, actor, local, additional),
|
||||
group <- Ownable.group_actor(object),
|
||||
:ok <- check_for_actor_key_rotation(actor),
|
||||
{:ok, activity} <- create_activity(activity_data, local),
|
||||
:ok <- maybe_federate(activity),
|
||||
:ok <- maybe_relay_if_group_activity(activity, group) do
|
||||
{:ok, activity, object}
|
||||
end
|
||||
end
|
||||
end
|
31
lib/federation/activity_pub/actions/flag.ex
Normal file
31
lib/federation/activity_pub/actions/flag.ex
Normal file
|
@ -0,0 +1,31 @@
|
|||
defmodule Mobilizon.Federation.ActivityPub.Actions.Flag do
|
||||
@moduledoc """
|
||||
Delete things
|
||||
"""
|
||||
alias Mobilizon.Users
|
||||
alias Mobilizon.Federation.ActivityPub.{Activity, Types}
|
||||
alias Mobilizon.Web.Email.{Admin, Mailer}
|
||||
require Logger
|
||||
|
||||
import Mobilizon.Federation.ActivityPub.Utils,
|
||||
only: [
|
||||
create_activity: 2,
|
||||
maybe_federate: 1
|
||||
]
|
||||
|
||||
@spec flag(map, boolean, map) :: {:ok, Activity.t(), Report.t()} | {:error, Ecto.Changeset.t()}
|
||||
def flag(args, local \\ false, additional \\ %{}) do
|
||||
with {:ok, report, report_as_data} <- Types.Reports.flag(args, local, additional) do
|
||||
{:ok, activity} = create_activity(report_as_data, local)
|
||||
maybe_federate(activity)
|
||||
|
||||
Enum.each(Users.list_moderators(), fn moderator ->
|
||||
moderator
|
||||
|> Admin.report(report)
|
||||
|> Mailer.send_email_later()
|
||||
end)
|
||||
|
||||
{:ok, activity, report}
|
||||
end
|
||||
end
|
||||
end
|
74
lib/federation/activity_pub/actions/follow.ex
Normal file
74
lib/federation/activity_pub/actions/follow.ex
Normal file
|
@ -0,0 +1,74 @@
|
|||
defmodule Mobilizon.Federation.ActivityPub.Actions.Follow do
|
||||
@moduledoc """
|
||||
Follow people
|
||||
"""
|
||||
|
||||
alias Mobilizon.Actors
|
||||
alias Mobilizon.Actors.{Actor, Follower}
|
||||
alias Mobilizon.Federation.ActivityPub.Types
|
||||
alias Mobilizon.Federation.ActivityStream.Convertible
|
||||
alias Mobilizon.Web.Endpoint
|
||||
require Logger
|
||||
|
||||
import Mobilizon.Federation.ActivityPub.Utils,
|
||||
only: [
|
||||
create_activity: 2,
|
||||
maybe_federate: 1,
|
||||
make_unfollow_data: 4
|
||||
]
|
||||
|
||||
@doc """
|
||||
Make an actor follow another, using an activity of type `Follow`
|
||||
"""
|
||||
@spec follow(Actor.t(), Actor.t(), String.t() | nil, boolean, map) ::
|
||||
{:ok, Activity.t(), Follower.t()} | {:error, atom | Ecto.Changeset.t() | String.t()}
|
||||
def follow(
|
||||
%Actor{} = follower,
|
||||
%Actor{} = followed,
|
||||
activity_id \\ nil,
|
||||
local \\ true,
|
||||
additional \\ %{}
|
||||
) do
|
||||
if followed.id != follower.id do
|
||||
case Types.Actors.follow(
|
||||
follower,
|
||||
followed,
|
||||
local,
|
||||
Map.merge(additional, %{"activity_id" => activity_id})
|
||||
) do
|
||||
{:ok, activity_data, %Follower{} = follower} ->
|
||||
{:ok, activity} = create_activity(activity_data, local)
|
||||
maybe_federate(activity)
|
||||
{:ok, activity, follower}
|
||||
|
||||
{:error, err} ->
|
||||
{:error, err}
|
||||
end
|
||||
else
|
||||
{:error, "Can't follow yourself"}
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Make an actor unfollow another, using an activity of type `Undo` a `Follow`.
|
||||
"""
|
||||
@spec unfollow(Actor.t(), Actor.t(), String.t() | nil, boolean()) ::
|
||||
{:ok, Activity.t(), Follower.t()} | {:error, String.t()}
|
||||
def unfollow(%Actor{} = follower, %Actor{} = followed, activity_id \\ nil, local \\ true) do
|
||||
with {:ok, %Follower{id: follow_id} = follow} <- Actors.unfollow(followed, follower) do
|
||||
# We recreate the follow activity
|
||||
follow_as_data =
|
||||
Convertible.model_to_as(%{follow | actor: follower, target_actor: followed})
|
||||
|
||||
{:ok, follow_activity} = create_activity(follow_as_data, local)
|
||||
activity_unfollow_id = activity_id || "#{Endpoint.url()}/unfollow/#{follow_id}/activity"
|
||||
|
||||
unfollow_data =
|
||||
make_unfollow_data(follower, followed, follow_activity, activity_unfollow_id)
|
||||
|
||||
{:ok, activity} = create_activity(unfollow_data, local)
|
||||
maybe_federate(activity)
|
||||
{:ok, activity, follow}
|
||||
end
|
||||
end
|
||||
end
|
86
lib/federation/activity_pub/actions/invite.ex
Normal file
86
lib/federation/activity_pub/actions/invite.ex
Normal file
|
@ -0,0 +1,86 @@
|
|||
defmodule Mobilizon.Federation.ActivityPub.Actions.Invite do
|
||||
@moduledoc """
|
||||
Invite people to things
|
||||
"""
|
||||
alias Mobilizon.Actors
|
||||
alias Mobilizon.Actors.{Actor, Member}
|
||||
alias Mobilizon.Web.Email.Group
|
||||
require Logger
|
||||
|
||||
import Mobilizon.Federation.ActivityPub.Utils,
|
||||
only: [
|
||||
create_activity: 2,
|
||||
maybe_federate: 1,
|
||||
maybe_relay_if_group_activity: 1
|
||||
]
|
||||
|
||||
@spec invite(Actor.t(), Actor.t(), Actor.t(), boolean, map()) ::
|
||||
{:ok, map(), Member.t()} | {:error, :not_able_to_invite | Ecto.Changeset.t()}
|
||||
def invite(
|
||||
%Actor{url: group_url, id: group_id, members_url: members_url} = group,
|
||||
%Actor{url: actor_url, id: actor_id} = actor,
|
||||
%Actor{url: target_actor_url, id: target_actor_id} = _target_actor,
|
||||
local \\ true,
|
||||
additional \\ %{}
|
||||
) do
|
||||
Logger.debug("Handling #{actor_url} invite to #{group_url} sent to #{target_actor_url}")
|
||||
|
||||
if is_able_to_invite?(actor, group) do
|
||||
with {:ok, %Member{url: member_url} = member} <-
|
||||
Actors.create_member(%{
|
||||
parent_id: group_id,
|
||||
actor_id: target_actor_id,
|
||||
role: :invited,
|
||||
invited_by_id: actor_id,
|
||||
url: Map.get(additional, :url)
|
||||
}) do
|
||||
Mobilizon.Service.Activity.Member.insert_activity(member,
|
||||
moderator: actor,
|
||||
subject: "member_invited"
|
||||
)
|
||||
|
||||
{:ok, activity} =
|
||||
create_activity(
|
||||
%{
|
||||
"type" => "Invite",
|
||||
"attributedTo" => group_url,
|
||||
"actor" => actor_url,
|
||||
"object" => group_url,
|
||||
"target" => target_actor_url,
|
||||
"id" => member_url
|
||||
}
|
||||
|> Map.merge(%{"to" => [target_actor_url, members_url], "cc" => [group_url]})
|
||||
|> Map.merge(additional),
|
||||
local
|
||||
)
|
||||
|
||||
maybe_federate(activity)
|
||||
maybe_relay_if_group_activity(activity)
|
||||
Group.send_invite_to_user(member)
|
||||
{:ok, activity, member}
|
||||
end
|
||||
else
|
||||
{:error, :not_able_to_invite}
|
||||
end
|
||||
end
|
||||
|
||||
@spec is_able_to_invite?(Actor.t(), Actor.t()) :: boolean
|
||||
defp is_able_to_invite?(%Actor{domain: actor_domain, id: actor_id}, %Actor{
|
||||
domain: group_domain,
|
||||
id: group_id
|
||||
}) do
|
||||
# If the actor comes from the same domain we trust it
|
||||
if actor_domain == group_domain do
|
||||
true
|
||||
else
|
||||
# If local group, we'll send the invite
|
||||
case Actors.get_member(actor_id, group_id) do
|
||||
{:ok, %Member{} = admin_member} ->
|
||||
Member.is_administrator(admin_member)
|
||||
|
||||
_ ->
|
||||
false
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
51
lib/federation/activity_pub/actions/join.ex
Normal file
51
lib/federation/activity_pub/actions/join.ex
Normal file
|
@ -0,0 +1,51 @@
|
|||
defmodule Mobilizon.Federation.ActivityPub.Actions.Join do
|
||||
@moduledoc """
|
||||
Join things
|
||||
"""
|
||||
|
||||
alias Mobilizon.Actors.{Actor, Member}
|
||||
alias Mobilizon.Events.{Event, Participant}
|
||||
alias Mobilizon.Federation.ActivityPub.Types
|
||||
require Logger
|
||||
|
||||
import Mobilizon.Federation.ActivityPub.Utils,
|
||||
only: [
|
||||
create_activity: 2,
|
||||
maybe_federate: 1
|
||||
]
|
||||
|
||||
@doc """
|
||||
Join an entity (an event or a group), using an activity of type `Join`
|
||||
"""
|
||||
@spec join(Event.t(), Actor.t(), boolean, map) ::
|
||||
{:ok, Activity.t(), Participant.t()} | {:error, :maximum_attendee_capacity}
|
||||
@spec join(Actor.t(), Actor.t(), boolean, map) :: {:ok, Activity.t(), Member.t()}
|
||||
def join(entity_to_join, actor_joining, local \\ true, additional \\ %{})
|
||||
|
||||
def join(%Event{} = event, %Actor{} = actor, local, additional) do
|
||||
case Types.Events.join(event, actor, local, additional) do
|
||||
{:ok, activity_data, participant} ->
|
||||
{:ok, activity} = create_activity(activity_data, local)
|
||||
maybe_federate(activity)
|
||||
{:ok, activity, participant}
|
||||
|
||||
{:error, :maximum_attendee_capacity_reached} ->
|
||||
{:error, :maximum_attendee_capacity_reached}
|
||||
|
||||
{:accept, accept} ->
|
||||
accept
|
||||
end
|
||||
end
|
||||
|
||||
def join(%Actor{type: :Group} = group, %Actor{} = actor, local, additional) do
|
||||
with {:ok, activity_data, %Member{} = member} <-
|
||||
Types.Actors.join(group, actor, local, additional),
|
||||
{:ok, activity} <- create_activity(activity_data, local),
|
||||
:ok <- maybe_federate(activity) do
|
||||
{:ok, activity, member}
|
||||
else
|
||||
{:accept, accept} ->
|
||||
accept
|
||||
end
|
||||
end
|
||||
end
|
104
lib/federation/activity_pub/actions/leave.ex
Normal file
104
lib/federation/activity_pub/actions/leave.ex
Normal file
|
@ -0,0 +1,104 @@
|
|||
defmodule Mobilizon.Federation.ActivityPub.Actions.Leave do
|
||||
@moduledoc """
|
||||
Leave things
|
||||
"""
|
||||
|
||||
alias Mobilizon.{Actors, Events}
|
||||
alias Mobilizon.Actors.{Actor, Member}
|
||||
alias Mobilizon.Events.{Event, Participant}
|
||||
alias Mobilizon.Federation.ActivityPub.Audience
|
||||
alias Mobilizon.Web.Endpoint
|
||||
require Logger
|
||||
|
||||
import Mobilizon.Federation.ActivityPub.Utils,
|
||||
only: [
|
||||
create_activity: 2,
|
||||
maybe_federate: 1,
|
||||
maybe_relay_if_group_activity: 1
|
||||
]
|
||||
|
||||
@spec leave(Event.t(), Actor.t(), boolean, map) ::
|
||||
{:ok, Activity.t(), Participant.t()}
|
||||
| {:error, :is_only_organizer | :participant_not_found | Ecto.Changeset.t()}
|
||||
@spec leave(Actor.t(), Actor.t(), boolean, map) ::
|
||||
{:ok, Activity.t(), Member.t()} | {:error, atom() | Ecto.Changeset.t()}
|
||||
def leave(object, actor, local \\ true, additional \\ %{})
|
||||
|
||||
@doc """
|
||||
Leave an event or a group
|
||||
"""
|
||||
def leave(
|
||||
%Event{id: event_id, url: event_url} = _event,
|
||||
%Actor{id: actor_id, url: actor_url} = _actor,
|
||||
local,
|
||||
additional
|
||||
) do
|
||||
if Participant.is_not_only_organizer(event_id, actor_id) do
|
||||
{:error, :is_only_organizer}
|
||||
else
|
||||
case Mobilizon.Events.get_participant(
|
||||
event_id,
|
||||
actor_id,
|
||||
Map.get(additional, :metadata, %{})
|
||||
) do
|
||||
{:ok, %Participant{} = participant} ->
|
||||
case Events.delete_participant(participant) do
|
||||
{:ok, %{participant: %Participant{} = participant}} ->
|
||||
leave_data = %{
|
||||
"type" => "Leave",
|
||||
# If it's an exclusion it should be something else
|
||||
"actor" => actor_url,
|
||||
"object" => event_url,
|
||||
"id" => "#{Endpoint.url()}/leave/event/#{participant.id}"
|
||||
}
|
||||
|
||||
audience = Audience.get_audience(participant)
|
||||
{:ok, activity} = create_activity(Map.merge(leave_data, audience), local)
|
||||
maybe_federate(activity)
|
||||
{:ok, activity, participant}
|
||||
|
||||
{:error, _type, %Ecto.Changeset{} = err, _} ->
|
||||
{:error, err}
|
||||
end
|
||||
|
||||
{:error, :participant_not_found} ->
|
||||
{:error, :participant_not_found}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def leave(
|
||||
%Actor{type: :Group, id: group_id, url: group_url, members_url: group_members_url},
|
||||
%Actor{id: actor_id, url: actor_url},
|
||||
local,
|
||||
additional
|
||||
) do
|
||||
with {:member, {:ok, %Member{id: member_id} = member}} <-
|
||||
{:member, Actors.get_member(actor_id, group_id)},
|
||||
{:is_not_only_admin, true} <-
|
||||
{:is_not_only_admin,
|
||||
Map.get(additional, :force_member_removal, false) ||
|
||||
!Actors.is_only_administrator?(member_id, group_id)},
|
||||
{:delete, {:ok, %Member{} = member}} <- {:delete, Actors.delete_member(member)} do
|
||||
Mobilizon.Service.Activity.Member.insert_activity(member, subject: "member_quit")
|
||||
|
||||
leave_data = %{
|
||||
"to" => [group_members_url],
|
||||
"cc" => [group_url],
|
||||
"attributedTo" => group_url,
|
||||
"type" => "Leave",
|
||||
"actor" => actor_url,
|
||||
"object" => group_url
|
||||
}
|
||||
|
||||
{:ok, activity} = create_activity(leave_data, local)
|
||||
maybe_federate(activity)
|
||||
maybe_relay_if_group_activity(activity)
|
||||
{:ok, activity, member}
|
||||
else
|
||||
{:member, nil} -> {:error, :member_not_found}
|
||||
{:is_not_only_admin, false} -> {:error, :is_not_only_admin}
|
||||
{:error, %Ecto.Changeset{} = err} -> {:error, err}
|
||||
end
|
||||
end
|
||||
end
|
30
lib/federation/activity_pub/actions/move.ex
Normal file
30
lib/federation/activity_pub/actions/move.ex
Normal file
|
@ -0,0 +1,30 @@
|
|||
defmodule Mobilizon.Federation.ActivityPub.Actions.Move do
|
||||
@moduledoc """
|
||||
Move things
|
||||
"""
|
||||
alias Mobilizon.Resources.Resource
|
||||
alias Mobilizon.Federation.ActivityPub.{Activity, Types}
|
||||
require Logger
|
||||
|
||||
import Mobilizon.Federation.ActivityPub.Utils,
|
||||
only: [
|
||||
create_activity: 2,
|
||||
maybe_federate: 1
|
||||
]
|
||||
|
||||
@spec move(:resource, Resource.t(), map, boolean, map) ::
|
||||
{:ok, Activity.t(), Resource.t()} | {:error, Ecto.Changeset.t() | atom()}
|
||||
def move(type, old_entity, args, local \\ false, additional \\ %{}) do
|
||||
Logger.debug("We're moving something")
|
||||
Logger.debug(inspect(args))
|
||||
|
||||
with {:ok, entity, update_data} <-
|
||||
(case type do
|
||||
:resource -> Types.Resources.move(old_entity, args, additional)
|
||||
end) do
|
||||
{:ok, activity} = create_activity(update_data, local)
|
||||
maybe_federate(activity)
|
||||
{:ok, activity, entity}
|
||||
end
|
||||
end
|
||||
end
|
127
lib/federation/activity_pub/actions/reject.ex
Normal file
127
lib/federation/activity_pub/actions/reject.ex
Normal file
|
@ -0,0 +1,127 @@
|
|||
defmodule Mobilizon.Federation.ActivityPub.Actions.Reject do
|
||||
@moduledoc """
|
||||
Reject things
|
||||
"""
|
||||
|
||||
alias Mobilizon.{Actors, Events}
|
||||
alias Mobilizon.Actors.{Actor, Follower, Member}
|
||||
alias Mobilizon.Events.Participant
|
||||
alias Mobilizon.Federation.ActivityPub.Actions.Accept
|
||||
alias Mobilizon.Federation.ActivityPub.Audience
|
||||
alias Mobilizon.Federation.ActivityStream
|
||||
alias Mobilizon.Federation.ActivityStream.Convertible
|
||||
alias Mobilizon.Web.Endpoint
|
||||
require Logger
|
||||
|
||||
import Mobilizon.Federation.ActivityPub.Utils,
|
||||
only: [
|
||||
create_activity: 2,
|
||||
maybe_federate: 1,
|
||||
maybe_relay_if_group_activity: 1
|
||||
]
|
||||
|
||||
@spec reject(Accept.acceptable_types(), Accept.acceptable_entities(), boolean, map) ::
|
||||
{:ok, ActivityStream.t(), Accept.acceptable_entities()}
|
||||
def reject(type, entity, local \\ true, additional \\ %{}) do
|
||||
{:ok, entity, update_data} =
|
||||
case type do
|
||||
:join -> reject_join(entity, additional)
|
||||
:follow -> reject_follow(entity, additional)
|
||||
:invite -> reject_invite(entity, additional)
|
||||
end
|
||||
|
||||
with {:ok, activity} <- create_activity(update_data, local),
|
||||
:ok <- maybe_federate(activity),
|
||||
:ok <- maybe_relay_if_group_activity(activity) do
|
||||
{:ok, activity, entity}
|
||||
else
|
||||
err ->
|
||||
Logger.error("Something went wrong while creating an activity")
|
||||
Logger.debug(inspect(err))
|
||||
err
|
||||
end
|
||||
end
|
||||
|
||||
@spec reject_join(Participant.t(), map()) :: {:ok, Participant.t(), Activity.t()} | any()
|
||||
defp reject_join(%Participant{} = participant, additional) do
|
||||
with {:ok, %Participant{} = participant} <-
|
||||
Events.update_participant(participant, %{role: :rejected}),
|
||||
Absinthe.Subscription.publish(Endpoint, participant.actor,
|
||||
event_person_participation_changed: participant.actor.id
|
||||
),
|
||||
participant_as_data <- Convertible.model_to_as(participant),
|
||||
audience <-
|
||||
participant
|
||||
|> Audience.get_audience()
|
||||
|> Map.merge(additional),
|
||||
reject_data <- %{
|
||||
"type" => "Reject",
|
||||
"object" => participant_as_data
|
||||
},
|
||||
update_data <-
|
||||
reject_data
|
||||
|> Map.merge(audience)
|
||||
|> Map.merge(%{
|
||||
"id" => "#{Endpoint.url()}/reject/join/#{participant.id}"
|
||||
}) do
|
||||
{:ok, participant, update_data}
|
||||
else
|
||||
err ->
|
||||
Logger.error("Something went wrong while creating an update activity")
|
||||
Logger.debug(inspect(err))
|
||||
err
|
||||
end
|
||||
end
|
||||
|
||||
@spec reject_follow(Follower.t(), map()) :: {:ok, Follower.t(), Activity.t()} | any()
|
||||
defp reject_follow(%Follower{} = follower, additional) do
|
||||
with {:ok, %Follower{} = follower} <- Actors.delete_follower(follower),
|
||||
follower_as_data <- Convertible.model_to_as(follower),
|
||||
audience <-
|
||||
follower.actor |> Audience.get_audience() |> Map.merge(additional),
|
||||
reject_data <- %{
|
||||
"to" => [follower.actor.url],
|
||||
"type" => "Reject",
|
||||
"actor" => follower.target_actor.url,
|
||||
"object" => follower_as_data
|
||||
},
|
||||
update_data <-
|
||||
audience
|
||||
|> Map.merge(reject_data)
|
||||
|> Map.merge(%{
|
||||
"id" => "#{Endpoint.url()}/reject/follow/#{follower.id}"
|
||||
}) do
|
||||
{:ok, follower, update_data}
|
||||
else
|
||||
err ->
|
||||
Logger.error("Something went wrong while creating an update activity")
|
||||
Logger.debug(inspect(err))
|
||||
err
|
||||
end
|
||||
end
|
||||
|
||||
@spec reject_invite(Member.t(), map()) :: {:ok, Member.t(), Activity.t()} | any
|
||||
defp reject_invite(
|
||||
%Member{invited_by_id: invited_by_id, actor_id: actor_id} = member,
|
||||
_additional
|
||||
) do
|
||||
with %Actor{} = inviter <- Actors.get_actor(invited_by_id),
|
||||
%Actor{url: actor_url} <- Actors.get_actor(actor_id),
|
||||
{:ok, %Member{url: member_url, id: member_id} = member} <-
|
||||
Actors.delete_member(member),
|
||||
Mobilizon.Service.Activity.Member.insert_activity(member,
|
||||
subject: "member_rejected_invitation"
|
||||
),
|
||||
accept_data <- %{
|
||||
"type" => "Reject",
|
||||
"actor" => actor_url,
|
||||
"attributedTo" => member.parent.url,
|
||||
"to" => [inviter.url, member.parent.members_url],
|
||||
"cc" => [member.parent.url],
|
||||
"object" => member_url,
|
||||
"id" => "#{Endpoint.url()}/reject/invite/member/#{member_id}"
|
||||
} do
|
||||
{:ok, member, accept_data}
|
||||
end
|
||||
end
|
||||
end
|
56
lib/federation/activity_pub/actions/remove.ex
Normal file
56
lib/federation/activity_pub/actions/remove.ex
Normal file
|
@ -0,0 +1,56 @@
|
|||
defmodule Mobilizon.Federation.ActivityPub.Actions.Remove do
|
||||
@moduledoc """
|
||||
Remove things
|
||||
"""
|
||||
alias Mobilizon.Actors
|
||||
alias Mobilizon.Actors.{Actor, Member}
|
||||
alias Mobilizon.Federation.ActivityPub.Activity
|
||||
alias Mobilizon.Web.Email.Group
|
||||
require Logger
|
||||
|
||||
import Mobilizon.Federation.ActivityPub.Utils,
|
||||
only: [
|
||||
create_activity: 2,
|
||||
maybe_federate: 1,
|
||||
maybe_relay_if_group_activity: 1
|
||||
]
|
||||
|
||||
@doc """
|
||||
Remove an activity, using an activity of type `Remove`
|
||||
"""
|
||||
@spec remove(Member.t(), Actor.t(), Actor.t(), boolean, map) ::
|
||||
{:ok, Activity.t(), Member.t()} | {:error, :member_not_found | Ecto.Changeset.t()}
|
||||
def remove(
|
||||
%Member{} = member,
|
||||
%Actor{type: :Group, url: group_url, members_url: group_members_url},
|
||||
%Actor{url: moderator_url} = moderator,
|
||||
local,
|
||||
_additional \\ %{}
|
||||
) do
|
||||
with {:ok, %Member{id: member_id}} <- Actors.update_member(member, %{role: :rejected}),
|
||||
%Member{} = member <- Actors.get_member(member_id) do
|
||||
Mobilizon.Service.Activity.Member.insert_activity(member,
|
||||
moderator: moderator,
|
||||
subject: "member_removed"
|
||||
)
|
||||
|
||||
Group.send_notification_to_removed_member(member)
|
||||
|
||||
remove_data = %{
|
||||
"to" => [group_members_url],
|
||||
"type" => "Remove",
|
||||
"actor" => moderator_url,
|
||||
"object" => member.url,
|
||||
"origin" => group_url
|
||||
}
|
||||
|
||||
{:ok, activity} = create_activity(remove_data, local)
|
||||
maybe_federate(activity)
|
||||
maybe_relay_if_group_activity(activity)
|
||||
{:ok, activity, member}
|
||||
else
|
||||
nil -> {:error, :member_not_found}
|
||||
{:error, %Ecto.Changeset{} = err} -> {:error, err}
|
||||
end
|
||||
end
|
||||
end
|
44
lib/federation/activity_pub/actions/update.ex
Normal file
44
lib/federation/activity_pub/actions/update.ex
Normal file
|
@ -0,0 +1,44 @@
|
|||
defmodule Mobilizon.Federation.ActivityPub.Actions.Update do
|
||||
@moduledoc """
|
||||
Update things
|
||||
"""
|
||||
alias Mobilizon.Federation.ActivityPub.Activity
|
||||
alias Mobilizon.Federation.ActivityPub.Types.Managable
|
||||
require Logger
|
||||
|
||||
import Mobilizon.Federation.ActivityPub.Utils,
|
||||
only: [
|
||||
create_activity: 2,
|
||||
maybe_federate: 1,
|
||||
maybe_relay_if_group_activity: 1
|
||||
]
|
||||
|
||||
@doc """
|
||||
Create an activity of type `Update`
|
||||
|
||||
* Updates the object, which returns AS data
|
||||
* Wraps ActivityStreams data into a `Update` activity
|
||||
* Creates an `Mobilizon.Federation.ActivityPub.Activity` from this
|
||||
* Federates (asynchronously) the activity
|
||||
* Returns the activity
|
||||
"""
|
||||
@spec update(Entity.t(), map(), boolean, map()) ::
|
||||
{:ok, Activity.t(), Entity.t()} | {:error, atom() | Ecto.Changeset.t()}
|
||||
def update(old_entity, args, local \\ false, additional \\ %{}) do
|
||||
Logger.debug("updating an activity")
|
||||
Logger.debug(inspect(args))
|
||||
|
||||
case Managable.update(old_entity, args, additional) do
|
||||
{:ok, entity, update_data} ->
|
||||
{:ok, activity} = create_activity(update_data, local)
|
||||
maybe_federate(activity)
|
||||
maybe_relay_if_group_activity(activity)
|
||||
{:ok, activity, entity}
|
||||
|
||||
{:error, err} ->
|
||||
Logger.error("Something went wrong while creating an activity")
|
||||
Logger.debug(inspect(err))
|
||||
{:error, err}
|
||||
end
|
||||
end
|
||||
end
|
|
@ -4,7 +4,7 @@ defmodule Mobilizon.Federation.ActivityPub.Activity do
|
|||
"""
|
||||
|
||||
@type t :: %__MODULE__{
|
||||
data: String.t(),
|
||||
data: map(),
|
||||
local: boolean,
|
||||
actor: Actor.t(),
|
||||
recipients: [String.t()]
|
||||
|
|
|
@ -12,77 +12,45 @@ defmodule Mobilizon.Federation.ActivityPub do
|
|||
|
||||
alias Mobilizon.{
|
||||
Actors,
|
||||
Config,
|
||||
Discussions,
|
||||
Events,
|
||||
Posts,
|
||||
Resources,
|
||||
Share,
|
||||
Users
|
||||
Resources
|
||||
}
|
||||
|
||||
alias Mobilizon.Actors.{Actor, Follower, Member}
|
||||
alias Mobilizon.Actors.Actor
|
||||
alias Mobilizon.Discussions.Comment
|
||||
alias Mobilizon.Events.{Event, Participant}
|
||||
alias Mobilizon.Events.Event
|
||||
alias Mobilizon.Tombstone
|
||||
|
||||
alias Mobilizon.Federation.ActivityPub.{
|
||||
Activity,
|
||||
Audience,
|
||||
Federator,
|
||||
Fetcher,
|
||||
Preloader,
|
||||
Refresher,
|
||||
Relay,
|
||||
Transmogrifier,
|
||||
Types,
|
||||
Visibility
|
||||
Relay
|
||||
}
|
||||
|
||||
alias Mobilizon.Federation.ActivityPub.Actor, as: ActivityPubActor
|
||||
|
||||
alias Mobilizon.Federation.ActivityPub.Types.{Managable, Ownable}
|
||||
|
||||
alias Mobilizon.Federation.ActivityStream.Convertible
|
||||
alias Mobilizon.Federation.HTTPSignatures.Signature
|
||||
|
||||
alias Mobilizon.Service.Notifications.Scheduler
|
||||
alias Mobilizon.Storage.Page
|
||||
|
||||
alias Mobilizon.Web.Endpoint
|
||||
alias Mobilizon.Web.Email.{Admin, Group, Mailer}
|
||||
|
||||
require Logger
|
||||
|
||||
@public_ap_adress "https://www.w3.org/ns/activitystreams#Public"
|
||||
|
||||
@doc """
|
||||
Wraps an object into an activity
|
||||
"""
|
||||
@spec create_activity(map(), boolean()) :: {:ok, Activity.t()}
|
||||
def create_activity(map, local \\ true) when is_map(map) do
|
||||
with map <- lazy_put_activity_defaults(map) do
|
||||
{:ok,
|
||||
%Activity{
|
||||
data: map,
|
||||
local: local,
|
||||
actor: map["actor"],
|
||||
recipients: get_recipients(map)
|
||||
}}
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Fetch an object from an URL, from our local database of events and comments, then eventually remote
|
||||
"""
|
||||
# TODO: Make database calls parallel
|
||||
@spec fetch_object_from_url(String.t(), Keyword.t()) ::
|
||||
{:ok, struct()} | {:error, any()}
|
||||
{:ok, struct()} | {:ok, atom(), struct()} | {:error, any()}
|
||||
def fetch_object_from_url(url, options \\ []) do
|
||||
Logger.info("Fetching object from url #{url}")
|
||||
|
||||
with {:not_http, true} <- {:not_http, String.starts_with?(url, "http")},
|
||||
{:existing, nil} <-
|
||||
if String.starts_with?(url, "http") do
|
||||
with {:existing, nil} <-
|
||||
{:existing, Tombstone.find_tombstone(url)},
|
||||
{:existing, nil} <- {:existing, Events.get_event_by_url(url)},
|
||||
{:existing, nil} <-
|
||||
|
@ -104,16 +72,15 @@ defmodule Mobilizon.Federation.ActivityPub do
|
|||
{:error, e} ->
|
||||
Logger.warn("Something failed while fetching url #{url} #{inspect(e)}")
|
||||
{:error, e}
|
||||
|
||||
e ->
|
||||
Logger.warn("Something failed while fetching url #{url} #{inspect(e)}")
|
||||
{:error, e}
|
||||
end
|
||||
else
|
||||
{:error, :url_not_http}
|
||||
end
|
||||
end
|
||||
|
||||
@spec handle_existing_entity(String.t(), struct(), Keyword.t()) ::
|
||||
{:ok, struct()}
|
||||
| {:ok, struct()}
|
||||
| {:ok, atom(), struct()}
|
||||
| {:error, String.t(), struct()}
|
||||
| {:error, String.t()}
|
||||
defp handle_existing_entity(url, entity, options) do
|
||||
|
@ -128,13 +95,13 @@ defmodule Mobilizon.Federation.ActivityPub do
|
|||
{:ok, entity} = Preloader.maybe_preload(entity)
|
||||
{:error, status, entity}
|
||||
|
||||
err ->
|
||||
err
|
||||
{:error, err} ->
|
||||
{:error, err}
|
||||
end
|
||||
end
|
||||
|
||||
@spec refresh_entity(String.t(), struct(), Keyword.t()) ::
|
||||
{:ok, struct()} | {:error, String.t(), struct()} | {:error, String.t()}
|
||||
{:ok, struct()} | {:error, atom(), struct()} | {:error, atom()}
|
||||
defp refresh_entity(url, entity, options) do
|
||||
force_fetch = Keyword.get(options, :force, false)
|
||||
|
||||
|
@ -145,573 +112,24 @@ defmodule Mobilizon.Federation.ActivityPub do
|
|||
{:ok, _activity, entity} ->
|
||||
{:ok, entity}
|
||||
|
||||
{:error, "Gone"} ->
|
||||
{:error, "Gone", entity}
|
||||
{:error, :http_gone} ->
|
||||
{:error, :http_gone, entity}
|
||||
|
||||
{:error, "Not found"} ->
|
||||
{:error, "Not found", entity}
|
||||
{:error, :http_not_found} ->
|
||||
{:error, :http_not_found, entity}
|
||||
|
||||
{:error, "Object origin check failed"} ->
|
||||
{:error, "Object origin check failed"}
|
||||
{:error, err} ->
|
||||
{:error, err}
|
||||
end
|
||||
else
|
||||
{:ok, entity}
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Create an activity of type `Create`
|
||||
|
||||
* Creates the object, which returns AS data
|
||||
* Wraps ActivityStreams data into a `Create` activity
|
||||
* Creates an `Mobilizon.Federation.ActivityPub.Activity` from this
|
||||
* Federates (asynchronously) the activity
|
||||
* Returns the activity
|
||||
"""
|
||||
@spec create(atom(), map(), boolean, map()) :: {:ok, Activity.t(), struct()} | any()
|
||||
def create(type, args, local \\ false, additional \\ %{}) do
|
||||
Logger.debug("creating an activity")
|
||||
Logger.debug(inspect(args))
|
||||
|
||||
with {:tombstone, nil} <- {:tombstone, check_for_tombstones(args)},
|
||||
{:ok, entity, create_data} <-
|
||||
(case type do
|
||||
:event -> Types.Events.create(args, additional)
|
||||
:comment -> Types.Comments.create(args, additional)
|
||||
:discussion -> Types.Discussions.create(args, additional)
|
||||
:actor -> Types.Actors.create(args, additional)
|
||||
:todo_list -> Types.TodoLists.create(args, additional)
|
||||
:todo -> Types.Todos.create(args, additional)
|
||||
:resource -> Types.Resources.create(args, additional)
|
||||
:post -> Types.Posts.create(args, additional)
|
||||
end),
|
||||
{:ok, activity} <- create_activity(create_data, local),
|
||||
:ok <- maybe_federate(activity),
|
||||
:ok <- maybe_relay_if_group_activity(activity) do
|
||||
{:ok, activity, entity}
|
||||
else
|
||||
err ->
|
||||
Logger.error("Something went wrong while creating an activity")
|
||||
Logger.debug(inspect(err))
|
||||
err
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Create an activity of type `Update`
|
||||
|
||||
* Updates the object, which returns AS data
|
||||
* Wraps ActivityStreams data into a `Update` activity
|
||||
* Creates an `Mobilizon.Federation.ActivityPub.Activity` from this
|
||||
* Federates (asynchronously) the activity
|
||||
* Returns the activity
|
||||
"""
|
||||
@spec update(struct(), map(), boolean, map()) :: {:ok, Activity.t(), struct()} | any()
|
||||
def update(old_entity, args, local \\ false, additional \\ %{}) do
|
||||
Logger.debug("updating an activity")
|
||||
Logger.debug(inspect(args))
|
||||
|
||||
with {:ok, entity, update_data} <- Managable.update(old_entity, args, additional),
|
||||
{:ok, activity} <- create_activity(update_data, local),
|
||||
:ok <- maybe_federate(activity),
|
||||
:ok <- maybe_relay_if_group_activity(activity) do
|
||||
{:ok, activity, entity}
|
||||
else
|
||||
err ->
|
||||
Logger.error("Something went wrong while creating an activity")
|
||||
Logger.debug(inspect(err))
|
||||
err
|
||||
end
|
||||
end
|
||||
|
||||
def accept(type, entity, local \\ true, additional \\ %{}) do
|
||||
Logger.debug("We're accepting something")
|
||||
|
||||
{:ok, entity, update_data} =
|
||||
case type do
|
||||
:join -> accept_join(entity, additional)
|
||||
:follow -> accept_follow(entity, additional)
|
||||
:invite -> accept_invite(entity, additional)
|
||||
end
|
||||
|
||||
with {:ok, activity} <- create_activity(update_data, local),
|
||||
:ok <- maybe_federate(activity),
|
||||
:ok <- maybe_relay_if_group_activity(activity) do
|
||||
{:ok, activity, entity}
|
||||
else
|
||||
err ->
|
||||
Logger.error("Something went wrong while creating an activity")
|
||||
Logger.debug(inspect(err))
|
||||
err
|
||||
end
|
||||
end
|
||||
|
||||
def reject(type, entity, local \\ true, additional \\ %{}) do
|
||||
{:ok, entity, update_data} =
|
||||
case type do
|
||||
:join -> reject_join(entity, additional)
|
||||
:follow -> reject_follow(entity, additional)
|
||||
:invite -> reject_invite(entity, additional)
|
||||
end
|
||||
|
||||
with {:ok, activity} <- create_activity(update_data, local),
|
||||
:ok <- maybe_federate(activity),
|
||||
:ok <- maybe_relay_if_group_activity(activity) do
|
||||
{:ok, activity, entity}
|
||||
else
|
||||
err ->
|
||||
Logger.error("Something went wrong while creating an activity")
|
||||
Logger.debug(inspect(err))
|
||||
err
|
||||
end
|
||||
end
|
||||
|
||||
def announce(
|
||||
%Actor{} = actor,
|
||||
object,
|
||||
activity_id \\ nil,
|
||||
local \\ true,
|
||||
public \\ true
|
||||
) do
|
||||
with {:ok, %Actor{id: object_owner_actor_id}} <-
|
||||
ActivityPubActor.get_or_fetch_actor_by_url(object["actor"]),
|
||||
{:ok, %Share{} = _share} <- Share.create(object["id"], actor.id, object_owner_actor_id),
|
||||
announce_data <- make_announce_data(actor, object, activity_id, public),
|
||||
{:ok, activity} <- create_activity(announce_data, local),
|
||||
:ok <- maybe_federate(activity) do
|
||||
{:ok, activity, object}
|
||||
else
|
||||
error ->
|
||||
{:error, error}
|
||||
end
|
||||
end
|
||||
|
||||
def unannounce(
|
||||
%Actor{} = actor,
|
||||
object,
|
||||
activity_id \\ nil,
|
||||
cancelled_activity_id \\ nil,
|
||||
local \\ true
|
||||
) do
|
||||
with announce_activity <- make_announce_data(actor, object, cancelled_activity_id),
|
||||
unannounce_data <- make_unannounce_data(actor, announce_activity, activity_id),
|
||||
{:ok, unannounce_activity} <- create_activity(unannounce_data, local),
|
||||
:ok <- maybe_federate(unannounce_activity) do
|
||||
{:ok, unannounce_activity, object}
|
||||
else
|
||||
_e -> {:ok, object}
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Make an actor follow another
|
||||
"""
|
||||
def follow(
|
||||
%Actor{} = follower,
|
||||
%Actor{} = followed,
|
||||
activity_id \\ nil,
|
||||
local \\ true,
|
||||
additional \\ %{}
|
||||
) do
|
||||
with {:different_actors, true} <- {:different_actors, followed.id != follower.id},
|
||||
{:ok, activity_data, %Follower{} = follower} <-
|
||||
Types.Actors.follow(
|
||||
follower,
|
||||
followed,
|
||||
local,
|
||||
Map.merge(additional, %{"activity_id" => activity_id})
|
||||
),
|
||||
{:ok, activity} <- create_activity(activity_data, local),
|
||||
:ok <- maybe_federate(activity) do
|
||||
{:ok, activity, follower}
|
||||
else
|
||||
{:error, err, msg} when err in [:already_following, :suspended, :no_person] ->
|
||||
{:error, msg}
|
||||
|
||||
{:different_actors, _} ->
|
||||
{:error, "Can't follow yourself"}
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Make an actor unfollow another
|
||||
"""
|
||||
@spec unfollow(Actor.t(), Actor.t(), String.t(), boolean()) :: {:ok, map()} | any()
|
||||
def unfollow(%Actor{} = follower, %Actor{} = followed, activity_id \\ nil, local \\ true) do
|
||||
with {:ok, %Follower{id: follow_id} = follow} <- Actors.unfollow(followed, follower),
|
||||
# We recreate the follow activity
|
||||
follow_as_data <-
|
||||
Convertible.model_to_as(%{follow | actor: follower, target_actor: followed}),
|
||||
{:ok, follow_activity} <- create_activity(follow_as_data, local),
|
||||
activity_unfollow_id <-
|
||||
activity_id || "#{Endpoint.url()}/unfollow/#{follow_id}/activity",
|
||||
unfollow_data <-
|
||||
make_unfollow_data(follower, followed, follow_activity, activity_unfollow_id),
|
||||
{:ok, activity} <- create_activity(unfollow_data, local),
|
||||
:ok <- maybe_federate(activity) do
|
||||
{:ok, activity, follow}
|
||||
else
|
||||
err ->
|
||||
Logger.debug("Error while unfollowing an actor #{inspect(err)}")
|
||||
err
|
||||
end
|
||||
end
|
||||
|
||||
def delete(object, actor, local \\ true, additional \\ %{}) do
|
||||
with {:ok, activity_data, actor, object} <-
|
||||
Managable.delete(object, actor, local, additional),
|
||||
group <- Ownable.group_actor(object),
|
||||
:ok <- check_for_actor_key_rotation(actor),
|
||||
{:ok, activity} <- create_activity(activity_data, local),
|
||||
:ok <- maybe_federate(activity),
|
||||
:ok <- maybe_relay_if_group_activity(activity, group) do
|
||||
{:ok, activity, object}
|
||||
end
|
||||
end
|
||||
|
||||
def join(entity_to_join, actor_joining, local \\ true, additional \\ %{})
|
||||
|
||||
def join(%Event{} = event, %Actor{} = actor, local, additional) do
|
||||
with {:ok, activity_data, participant} <- Types.Events.join(event, actor, local, additional),
|
||||
{:ok, activity} <- create_activity(activity_data, local),
|
||||
:ok <- maybe_federate(activity) do
|
||||
{:ok, activity, participant}
|
||||
else
|
||||
{:maximum_attendee_capacity, err} ->
|
||||
{:maximum_attendee_capacity, err}
|
||||
|
||||
{:accept, accept} ->
|
||||
accept
|
||||
end
|
||||
end
|
||||
|
||||
def join(%Actor{type: :Group} = group, %Actor{} = actor, local, additional) do
|
||||
with {:ok, activity_data, %Member{} = member} <-
|
||||
Types.Actors.join(group, actor, local, additional),
|
||||
{:ok, activity} <- create_activity(activity_data, local),
|
||||
:ok <- maybe_federate(activity) do
|
||||
{:ok, activity, member}
|
||||
else
|
||||
{:accept, accept} ->
|
||||
accept
|
||||
end
|
||||
end
|
||||
|
||||
def leave(object, actor, local \\ true, additional \\ %{})
|
||||
|
||||
@doc """
|
||||
Leave an event or a group
|
||||
"""
|
||||
def leave(
|
||||
%Event{id: event_id, url: event_url} = _event,
|
||||
%Actor{id: actor_id, url: actor_url} = _actor,
|
||||
local,
|
||||
additional
|
||||
) do
|
||||
with {:only_organizer, false} <-
|
||||
{:only_organizer, Participant.is_not_only_organizer(event_id, actor_id)},
|
||||
{:ok, %Participant{} = participant} <-
|
||||
Mobilizon.Events.get_participant(
|
||||
event_id,
|
||||
actor_id,
|
||||
Map.get(additional, :metadata, %{})
|
||||
),
|
||||
{:ok, %Participant{} = participant} <-
|
||||
Events.delete_participant(participant),
|
||||
leave_data <- %{
|
||||
"type" => "Leave",
|
||||
# If it's an exclusion it should be something else
|
||||
"actor" => actor_url,
|
||||
"object" => event_url,
|
||||
"id" => "#{Endpoint.url()}/leave/event/#{participant.id}"
|
||||
},
|
||||
audience <-
|
||||
Audience.get_audience(participant),
|
||||
{:ok, activity} <- create_activity(Map.merge(leave_data, audience), local),
|
||||
:ok <- maybe_federate(activity) do
|
||||
{:ok, activity, participant}
|
||||
end
|
||||
end
|
||||
|
||||
def leave(
|
||||
%Actor{type: :Group, id: group_id, url: group_url, members_url: group_members_url},
|
||||
%Actor{id: actor_id, url: actor_url},
|
||||
local,
|
||||
additional
|
||||
) do
|
||||
with {:member, {:ok, %Member{id: member_id} = member}} <-
|
||||
{:member, Actors.get_member(actor_id, group_id)},
|
||||
{:is_not_only_admin, true} <-
|
||||
{:is_not_only_admin,
|
||||
Map.get(additional, :force_member_removal, false) ||
|
||||
!Actors.is_only_administrator?(member_id, group_id)},
|
||||
{:delete, {:ok, %Member{} = member}} <- {:delete, Actors.delete_member(member)},
|
||||
Mobilizon.Service.Activity.Member.insert_activity(member, subject: "member_quit"),
|
||||
leave_data <- %{
|
||||
"to" => [group_members_url],
|
||||
"cc" => [group_url],
|
||||
"attributedTo" => group_url,
|
||||
"type" => "Leave",
|
||||
"actor" => actor_url,
|
||||
"object" => group_url
|
||||
},
|
||||
{:ok, activity} <- create_activity(leave_data, local),
|
||||
:ok <- maybe_federate(activity),
|
||||
:ok <- maybe_relay_if_group_activity(activity) do
|
||||
{:ok, activity, member}
|
||||
end
|
||||
end
|
||||
|
||||
def remove(
|
||||
%Member{} = member,
|
||||
%Actor{type: :Group, url: group_url, members_url: group_members_url},
|
||||
%Actor{url: moderator_url} = moderator,
|
||||
local,
|
||||
_additional \\ %{}
|
||||
) do
|
||||
with {:ok, %Member{id: member_id}} <- Actors.update_member(member, %{role: :rejected}),
|
||||
%Member{} = member <- Actors.get_member(member_id),
|
||||
{:ok, _} <-
|
||||
Mobilizon.Service.Activity.Member.insert_activity(member,
|
||||
moderator: moderator,
|
||||
subject: "member_removed"
|
||||
),
|
||||
:ok <- Group.send_notification_to_removed_member(member),
|
||||
remove_data <- %{
|
||||
"to" => [group_members_url],
|
||||
"type" => "Remove",
|
||||
"actor" => moderator_url,
|
||||
"object" => member.url,
|
||||
"origin" => group_url
|
||||
},
|
||||
{:ok, activity} <- create_activity(remove_data, local),
|
||||
:ok <- maybe_federate(activity),
|
||||
:ok <- maybe_relay_if_group_activity(activity) do
|
||||
{:ok, activity, member}
|
||||
end
|
||||
end
|
||||
|
||||
@spec invite(Actor.t(), Actor.t(), Actor.t(), boolean, map()) ::
|
||||
{:ok, map(), Member.t()} | {:error, :member_not_found}
|
||||
def invite(
|
||||
%Actor{url: group_url, id: group_id, members_url: members_url} = group,
|
||||
%Actor{url: actor_url, id: actor_id} = actor,
|
||||
%Actor{url: target_actor_url, id: target_actor_id} = _target_actor,
|
||||
local \\ true,
|
||||
additional \\ %{}
|
||||
) do
|
||||
Logger.debug("Handling #{actor_url} invite to #{group_url} sent to #{target_actor_url}")
|
||||
|
||||
with {:is_able_to_invite, true} <- {:is_able_to_invite, is_able_to_invite(actor, group)},
|
||||
{:ok, %Member{url: member_url} = member} <-
|
||||
Actors.create_member(%{
|
||||
parent_id: group_id,
|
||||
actor_id: target_actor_id,
|
||||
role: :invited,
|
||||
invited_by_id: actor_id,
|
||||
url: Map.get(additional, :url)
|
||||
}),
|
||||
{:ok, _} <-
|
||||
Mobilizon.Service.Activity.Member.insert_activity(member,
|
||||
moderator: actor,
|
||||
subject: "member_invited"
|
||||
),
|
||||
invite_data <- %{
|
||||
"type" => "Invite",
|
||||
"attributedTo" => group_url,
|
||||
"actor" => actor_url,
|
||||
"object" => group_url,
|
||||
"target" => target_actor_url,
|
||||
"id" => member_url
|
||||
},
|
||||
{:ok, activity} <-
|
||||
create_activity(
|
||||
invite_data
|
||||
|> Map.merge(%{"to" => [target_actor_url, members_url], "cc" => [group_url]})
|
||||
|> Map.merge(additional),
|
||||
local
|
||||
),
|
||||
:ok <- maybe_federate(activity),
|
||||
:ok <- maybe_relay_if_group_activity(activity),
|
||||
:ok <- Group.send_invite_to_user(member) do
|
||||
{:ok, activity, member}
|
||||
end
|
||||
end
|
||||
|
||||
defp is_able_to_invite(%Actor{domain: actor_domain, id: actor_id}, %Actor{
|
||||
domain: group_domain,
|
||||
id: group_id
|
||||
}) do
|
||||
# If the actor comes from the same domain we trust it
|
||||
if actor_domain == group_domain do
|
||||
true
|
||||
else
|
||||
# If local group, we'll send the invite
|
||||
with {:ok, %Member{} = admin_member} <- Actors.get_member(actor_id, group_id) do
|
||||
Member.is_administrator(admin_member)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def move(type, old_entity, args, local \\ false, additional \\ %{}) do
|
||||
Logger.debug("We're moving something")
|
||||
Logger.debug(inspect(args))
|
||||
|
||||
with {:ok, entity, update_data} <-
|
||||
(case type do
|
||||
:resource -> Types.Resources.move(old_entity, args, additional)
|
||||
end),
|
||||
{:ok, activity} <- create_activity(update_data, local),
|
||||
:ok <- maybe_federate(activity) do
|
||||
{:ok, activity, entity}
|
||||
else
|
||||
err ->
|
||||
Logger.error("Something went wrong while creating a Move activity")
|
||||
Logger.debug(inspect(err))
|
||||
err
|
||||
end
|
||||
end
|
||||
|
||||
def flag(args, local \\ false, additional \\ %{}) do
|
||||
with {report, report_as_data} <- Types.Reports.flag(args, local, additional),
|
||||
{:ok, activity} <- create_activity(report_as_data, local),
|
||||
:ok <- maybe_federate(activity) do
|
||||
Enum.each(Users.list_moderators(), fn moderator ->
|
||||
moderator
|
||||
|> Admin.report(report)
|
||||
|> Mailer.send_email_later()
|
||||
end)
|
||||
|
||||
{:ok, activity, report}
|
||||
else
|
||||
err ->
|
||||
Logger.error("Something went wrong while creating an activity")
|
||||
Logger.debug(inspect(err))
|
||||
err
|
||||
end
|
||||
end
|
||||
|
||||
@spec is_create_activity?(Activity.t()) :: boolean
|
||||
defp is_create_activity?(%Activity{data: %{"type" => "Create"}}), do: true
|
||||
defp is_create_activity?(_), do: false
|
||||
|
||||
@spec convert_members_in_recipients(list(String.t())) :: {list(String.t()), list(Actor.t())}
|
||||
defp convert_members_in_recipients(recipients) do
|
||||
Enum.reduce(recipients, {recipients, []}, fn recipient, {recipients, member_actors} = acc ->
|
||||
case Actors.get_group_by_members_url(recipient) do
|
||||
# If the group is local just add external members
|
||||
%Actor{domain: domain} = group when is_nil(domain) ->
|
||||
{Enum.filter(recipients, fn recipient -> recipient != group.members_url end),
|
||||
member_actors ++ Actors.list_external_actors_members_for_group(group)}
|
||||
|
||||
# If it's remote add the remote group actor as well
|
||||
%Actor{} = group ->
|
||||
{Enum.filter(recipients, fn recipient -> recipient != group.members_url end),
|
||||
member_actors ++ Actors.list_external_actors_members_for_group(group) ++ [group]}
|
||||
|
||||
_ ->
|
||||
acc
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
defp convert_followers_in_recipients(recipients) do
|
||||
Enum.reduce(recipients, {recipients, []}, fn recipient, {recipients, follower_actors} = acc ->
|
||||
case Actors.get_actor_by_followers_url(recipient) do
|
||||
%Actor{} = group ->
|
||||
{Enum.filter(recipients, fn recipient -> recipient != group.followers_url end),
|
||||
follower_actors ++ Actors.list_external_followers_for_actor(group)}
|
||||
|
||||
_ ->
|
||||
acc
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
# @spec is_announce_activity?(Activity.t()) :: boolean
|
||||
# defp is_announce_activity?(%Activity{data: %{"type" => "Announce"}}), do: true
|
||||
# defp is_announce_activity?(_), do: false
|
||||
|
||||
@doc """
|
||||
Publish an activity to all appropriated audiences inboxes
|
||||
"""
|
||||
# credo:disable-for-lines:47
|
||||
@spec publish(Actor.t(), Activity.t()) :: :ok
|
||||
def publish(actor, %Activity{recipients: recipients} = activity) do
|
||||
Logger.debug("Publishing an activity")
|
||||
Logger.debug(inspect(activity, pretty: true))
|
||||
|
||||
public = Visibility.is_public?(activity)
|
||||
Logger.debug("is public ? #{public}")
|
||||
|
||||
if public && is_create_activity?(activity) && Config.get([:instance, :allow_relay]) do
|
||||
Logger.info(fn -> "Relaying #{activity.data["id"]} out" end)
|
||||
|
||||
Relay.publish(activity)
|
||||
end
|
||||
|
||||
recipients = Enum.uniq(recipients)
|
||||
|
||||
{recipients, followers} = convert_followers_in_recipients(recipients)
|
||||
|
||||
{recipients, members} = convert_members_in_recipients(recipients)
|
||||
|
||||
remote_inboxes =
|
||||
(remote_actors(recipients) ++ followers ++ members)
|
||||
|> Enum.map(fn actor -> actor.shared_inbox_url || actor.inbox_url end)
|
||||
|> Enum.uniq()
|
||||
|
||||
{:ok, data} = Transmogrifier.prepare_outgoing(activity.data)
|
||||
json = Jason.encode!(data)
|
||||
Logger.debug(fn -> "Remote inboxes are : #{inspect(remote_inboxes)}" end)
|
||||
|
||||
Enum.each(remote_inboxes, fn inbox ->
|
||||
Federator.enqueue(:publish_single_ap, %{
|
||||
inbox: inbox,
|
||||
json: json,
|
||||
actor: actor,
|
||||
id: activity.data["id"]
|
||||
})
|
||||
end)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Publish an activity to a specific inbox
|
||||
"""
|
||||
def publish_one(%{inbox: inbox, json: json, actor: actor, id: id}) do
|
||||
Logger.info("Federating #{id} to #{inbox}")
|
||||
%URI{host: host, path: path} = URI.parse(inbox)
|
||||
|
||||
digest = Signature.build_digest(json)
|
||||
date = Signature.generate_date_header()
|
||||
|
||||
# request_target = Signature.generate_request_target("POST", path)
|
||||
|
||||
signature =
|
||||
Signature.sign(actor, %{
|
||||
"(request-target)": "post #{path}",
|
||||
host: host,
|
||||
"content-length": byte_size(json),
|
||||
digest: digest,
|
||||
date: date
|
||||
})
|
||||
|
||||
Tesla.post(
|
||||
inbox,
|
||||
json,
|
||||
headers: [
|
||||
{"Content-Type", "application/activity+json"},
|
||||
{"signature", signature},
|
||||
{"digest", digest},
|
||||
{"date", date}
|
||||
]
|
||||
)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Return all public activities (events & comments) for an actor
|
||||
"""
|
||||
@spec fetch_public_activities_for_actor(Actor.t(), integer(), integer()) :: map()
|
||||
@spec fetch_public_activities_for_actor(Actor.t(), pos_integer(), pos_integer()) :: map()
|
||||
def fetch_public_activities_for_actor(%Actor{id: actor_id} = actor, page \\ 1, limit \\ 10) do
|
||||
%Actor{id: relay_actor_id} = Relay.get_actor()
|
||||
|
||||
|
@ -758,217 +176,4 @@ defmodule Mobilizon.Federation.ActivityPub do
|
|||
local: local
|
||||
}
|
||||
end
|
||||
|
||||
# Get recipients for an activity or object
|
||||
@spec get_recipients(map()) :: list()
|
||||
defp get_recipients(data) do
|
||||
Map.get(data, "to", []) ++ Map.get(data, "cc", [])
|
||||
end
|
||||
|
||||
@spec check_for_tombstones(map()) :: Tombstone.t() | nil
|
||||
defp check_for_tombstones(%{url: url}), do: Tombstone.find_tombstone(url)
|
||||
defp check_for_tombstones(_), do: nil
|
||||
|
||||
@spec accept_follow(Follower.t(), map) :: {:ok, Follower.t(), Activity.t()} | any
|
||||
defp accept_follow(%Follower{} = follower, additional) do
|
||||
with {:ok, %Follower{} = follower} <- Actors.update_follower(follower, %{approved: true}),
|
||||
follower_as_data <- Convertible.model_to_as(follower),
|
||||
update_data <-
|
||||
make_accept_join_data(
|
||||
follower_as_data,
|
||||
Map.merge(additional, %{
|
||||
"id" => "#{Endpoint.url()}/accept/follow/#{follower.id}",
|
||||
"to" => [follower.actor.url],
|
||||
"cc" => [],
|
||||
"actor" => follower.target_actor.url
|
||||
})
|
||||
) do
|
||||
{:ok, follower, update_data}
|
||||
else
|
||||
err ->
|
||||
Logger.error("Something went wrong while creating an update activity")
|
||||
Logger.debug(inspect(err))
|
||||
err
|
||||
end
|
||||
end
|
||||
|
||||
@spec accept_join(Participant.t(), map) :: {:ok, Participant.t(), Activity.t()} | any
|
||||
defp accept_join(%Participant{} = participant, additional) do
|
||||
with {:ok, %Participant{} = participant} <-
|
||||
Events.update_participant(participant, %{role: :participant}),
|
||||
Absinthe.Subscription.publish(Endpoint, participant.actor,
|
||||
event_person_participation_changed: participant.actor.id
|
||||
),
|
||||
{:ok, _} <-
|
||||
Scheduler.trigger_notifications_for_participant(participant),
|
||||
participant_as_data <- Convertible.model_to_as(participant),
|
||||
audience <-
|
||||
Audience.get_audience(participant),
|
||||
accept_join_data <-
|
||||
make_accept_join_data(
|
||||
participant_as_data,
|
||||
Map.merge(Map.merge(audience, additional), %{
|
||||
"id" => "#{Endpoint.url()}/accept/join/#{participant.id}"
|
||||
})
|
||||
) do
|
||||
{:ok, participant, accept_join_data}
|
||||
else
|
||||
err ->
|
||||
Logger.error("Something went wrong while creating an update activity")
|
||||
Logger.debug(inspect(err))
|
||||
err
|
||||
end
|
||||
end
|
||||
|
||||
@spec accept_join(Member.t(), map) :: {:ok, Member.t(), Activity.t()} | any
|
||||
defp accept_join(%Member{} = member, additional) do
|
||||
with {:ok, %Member{} = member} <-
|
||||
Actors.update_member(member, %{role: :member}),
|
||||
{:ok, _} <-
|
||||
Mobilizon.Service.Activity.Member.insert_activity(member,
|
||||
subject: "member_approved"
|
||||
),
|
||||
_ <- maybe_refresh_group(member),
|
||||
Absinthe.Subscription.publish(Endpoint, member.actor,
|
||||
group_membership_changed: [
|
||||
Actor.preferred_username_and_domain(member.parent),
|
||||
member.actor.id
|
||||
]
|
||||
),
|
||||
member_as_data <- Convertible.model_to_as(member),
|
||||
audience <-
|
||||
Audience.get_audience(member),
|
||||
accept_join_data <-
|
||||
make_accept_join_data(
|
||||
member_as_data,
|
||||
Map.merge(Map.merge(audience, additional), %{
|
||||
"id" => "#{Endpoint.url()}/accept/join/#{member.id}"
|
||||
})
|
||||
) do
|
||||
{:ok, member, accept_join_data}
|
||||
else
|
||||
err ->
|
||||
Logger.error("Something went wrong while creating an update activity")
|
||||
Logger.debug(inspect(err))
|
||||
err
|
||||
end
|
||||
end
|
||||
|
||||
@spec accept_invite(Member.t(), map()) :: {:ok, Member.t(), Activity.t()} | any
|
||||
defp accept_invite(
|
||||
%Member{invited_by_id: invited_by_id, actor_id: actor_id} = member,
|
||||
_additional
|
||||
) do
|
||||
with %Actor{} = inviter <- Actors.get_actor(invited_by_id),
|
||||
%Actor{url: actor_url} <- Actors.get_actor(actor_id),
|
||||
{:ok, %Member{id: member_id} = member} <-
|
||||
Actors.update_member(member, %{role: :member}),
|
||||
{:ok, _} <-
|
||||
Mobilizon.Service.Activity.Member.insert_activity(member,
|
||||
subject: "member_accepted_invitation"
|
||||
),
|
||||
_ <- maybe_refresh_group(member),
|
||||
accept_data <- %{
|
||||
"type" => "Accept",
|
||||
"attributedTo" => member.parent.url,
|
||||
"to" => [inviter.url, member.parent.members_url],
|
||||
"cc" => [member.parent.url],
|
||||
"actor" => actor_url,
|
||||
"object" => Convertible.model_to_as(member),
|
||||
"id" => "#{Endpoint.url()}/accept/invite/member/#{member_id}"
|
||||
} do
|
||||
{:ok, member, accept_data}
|
||||
end
|
||||
end
|
||||
|
||||
defp maybe_refresh_group(%Member{
|
||||
parent: %Actor{domain: parent_domain, url: parent_url},
|
||||
actor: %Actor{} = actor
|
||||
}) do
|
||||
unless is_nil(parent_domain),
|
||||
do: Refresher.fetch_group(parent_url, actor)
|
||||
end
|
||||
|
||||
@spec reject_join(Participant.t(), map()) :: {:ok, Participant.t(), Activity.t()} | any()
|
||||
defp reject_join(%Participant{} = participant, additional) do
|
||||
with {:ok, %Participant{} = participant} <-
|
||||
Events.update_participant(participant, %{role: :rejected}),
|
||||
Absinthe.Subscription.publish(Endpoint, participant.actor,
|
||||
event_person_participation_changed: participant.actor.id
|
||||
),
|
||||
participant_as_data <- Convertible.model_to_as(participant),
|
||||
audience <-
|
||||
participant
|
||||
|> Audience.get_audience()
|
||||
|> Map.merge(additional),
|
||||
reject_data <- %{
|
||||
"type" => "Reject",
|
||||
"object" => participant_as_data
|
||||
},
|
||||
update_data <-
|
||||
reject_data
|
||||
|> Map.merge(audience)
|
||||
|> Map.merge(%{
|
||||
"id" => "#{Endpoint.url()}/reject/join/#{participant.id}"
|
||||
}) do
|
||||
{:ok, participant, update_data}
|
||||
else
|
||||
err ->
|
||||
Logger.error("Something went wrong while creating an update activity")
|
||||
Logger.debug(inspect(err))
|
||||
err
|
||||
end
|
||||
end
|
||||
|
||||
@spec reject_follow(Follower.t(), map()) :: {:ok, Follower.t(), Activity.t()} | any()
|
||||
defp reject_follow(%Follower{} = follower, additional) do
|
||||
with {:ok, %Follower{} = follower} <- Actors.delete_follower(follower),
|
||||
follower_as_data <- Convertible.model_to_as(follower),
|
||||
audience <-
|
||||
follower.actor |> Audience.get_audience() |> Map.merge(additional),
|
||||
reject_data <- %{
|
||||
"to" => [follower.actor.url],
|
||||
"type" => "Reject",
|
||||
"actor" => follower.target_actor.url,
|
||||
"object" => follower_as_data
|
||||
},
|
||||
update_data <-
|
||||
audience
|
||||
|> Map.merge(reject_data)
|
||||
|> Map.merge(%{
|
||||
"id" => "#{Endpoint.url()}/reject/follow/#{follower.id}"
|
||||
}) do
|
||||
{:ok, follower, update_data}
|
||||
else
|
||||
err ->
|
||||
Logger.error("Something went wrong while creating an update activity")
|
||||
Logger.debug(inspect(err))
|
||||
err
|
||||
end
|
||||
end
|
||||
|
||||
@spec reject_invite(Member.t(), map()) :: {:ok, Member.t(), Activity.t()} | any
|
||||
defp reject_invite(
|
||||
%Member{invited_by_id: invited_by_id, actor_id: actor_id} = member,
|
||||
_additional
|
||||
) do
|
||||
with %Actor{} = inviter <- Actors.get_actor(invited_by_id),
|
||||
%Actor{url: actor_url} <- Actors.get_actor(actor_id),
|
||||
{:ok, %Member{url: member_url, id: member_id} = member} <-
|
||||
Actors.delete_member(member),
|
||||
Mobilizon.Service.Activity.Member.insert_activity(member,
|
||||
subject: "member_rejected_invitation"
|
||||
),
|
||||
accept_data <- %{
|
||||
"type" => "Reject",
|
||||
"actor" => actor_url,
|
||||
"attributedTo" => member.parent.url,
|
||||
"to" => [inviter.url, member.parent.members_url],
|
||||
"cc" => [member.parent.url],
|
||||
"object" => member_url,
|
||||
"id" => "#{Endpoint.url()}/reject/invite/member/#{member_id}"
|
||||
} do
|
||||
{:ok, member, accept_data}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -14,65 +14,57 @@ defmodule Mobilizon.Federation.ActivityPub.Actor do
|
|||
@doc """
|
||||
Getting an actor from url, eventually creating it if we don't have it locally or if it needs an update
|
||||
"""
|
||||
@spec get_or_fetch_actor_by_url(String.t(), boolean) :: {:ok, Actor.t()} | {:error, String.t()}
|
||||
@spec get_or_fetch_actor_by_url(url :: String.t(), preload :: boolean()) ::
|
||||
{:ok, Actor.t()}
|
||||
| {:error, make_actor_errors}
|
||||
| {:error, :no_internal_relay_actor}
|
||||
| {:error, :url_nil}
|
||||
def get_or_fetch_actor_by_url(url, preload \\ false)
|
||||
|
||||
def get_or_fetch_actor_by_url(nil, _preload), do: {:error, "Can't fetch a nil url"}
|
||||
def get_or_fetch_actor_by_url(nil, _preload), do: {:error, :url_nil}
|
||||
|
||||
def get_or_fetch_actor_by_url("https://www.w3.org/ns/activitystreams#Public", _preload) do
|
||||
with %Actor{url: url} <- Relay.get_actor() do
|
||||
%Actor{url: url} = Relay.get_actor()
|
||||
get_or_fetch_actor_by_url(url)
|
||||
end
|
||||
end
|
||||
|
||||
@spec get_or_fetch_actor_by_url(String.t(), boolean()) :: {:ok, Actor.t()} | {:error, any()}
|
||||
def get_or_fetch_actor_by_url(url, preload) do
|
||||
with {:ok, %Actor{} = cached_actor} <- Actors.get_actor_by_url(url, preload),
|
||||
false <- Actors.needs_update?(cached_actor) do
|
||||
{:ok, cached_actor}
|
||||
case Actors.get_actor_by_url(url, preload) do
|
||||
{:ok, %Actor{} = cached_actor} ->
|
||||
if Actors.needs_update?(cached_actor) do
|
||||
__MODULE__.make_actor_from_url(url, preload)
|
||||
else
|
||||
_ ->
|
||||
# For tests, see https://github.com/jjh42/mock#not-supported---mocking-internal-function-calls and Mobilizon.Federation.ActivityPubTest
|
||||
case __MODULE__.make_actor_from_url(url, preload) do
|
||||
{:ok, %Actor{} = actor} ->
|
||||
{:ok, actor}
|
||||
{:ok, cached_actor}
|
||||
end
|
||||
|
||||
{:error, err} ->
|
||||
Logger.debug("Could not fetch by AP id")
|
||||
Logger.debug(inspect(err))
|
||||
{:error, "Could not fetch by AP id"}
|
||||
end
|
||||
{:error, :actor_not_found} ->
|
||||
# For tests, see https://github.com/jjh42/mock#not-supported---mocking-internal-function-calls and Mobilizon.Federation.ActivityPubTest
|
||||
__MODULE__.make_actor_from_url(url, preload)
|
||||
end
|
||||
end
|
||||
|
||||
@type make_actor_errors :: Fetcher.fetch_actor_errors() | :actor_is_local
|
||||
|
||||
@doc """
|
||||
Create an actor locally by its URL (AP ID)
|
||||
"""
|
||||
@spec make_actor_from_url(String.t(), boolean()) ::
|
||||
{:ok, %Actor{}} | {:error, :actor_deleted} | {:error, :http_error} | {:error, any()}
|
||||
@spec make_actor_from_url(url :: String.t(), preload :: boolean()) ::
|
||||
{:ok, Actor.t()} | {:error, make_actor_errors}
|
||||
def make_actor_from_url(url, preload \\ false) do
|
||||
if are_same_origin?(url, Endpoint.url()) do
|
||||
{:error, "Can't make a local actor from URL"}
|
||||
{:error, :actor_is_local}
|
||||
else
|
||||
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} when is_map(data) ->
|
||||
Actors.upsert_actor(data, preload)
|
||||
|
||||
# Request returned 410
|
||||
{:error, :actor_deleted} ->
|
||||
Logger.info("Actor was deleted")
|
||||
Logger.info("Actor #{url} was deleted")
|
||||
{:error, :actor_deleted}
|
||||
|
||||
{:error, :http_error} ->
|
||||
{:error, :http_error}
|
||||
|
||||
{:error, e} ->
|
||||
Logger.warn("Failed to make actor from url #{url}")
|
||||
{:error, e}
|
||||
{:error, err} when err in [:http_error, :json_decode_error] ->
|
||||
{:error, err}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -80,8 +72,8 @@ defmodule Mobilizon.Federation.ActivityPub.Actor do
|
|||
@doc """
|
||||
Find an actor in our local database or call WebFinger to find what's its AP ID is and then fetch it
|
||||
"""
|
||||
@spec find_or_make_actor_from_nickname(String.t(), atom() | nil) ::
|
||||
{:ok, Actor.t()} | {:error, any()}
|
||||
@spec find_or_make_actor_from_nickname(nickname :: String.t(), type :: atom() | nil) ::
|
||||
{:ok, Actor.t()} | {:error, make_actor_errors | WebFinger.finger_errors()}
|
||||
def find_or_make_actor_from_nickname(nickname, type \\ nil) do
|
||||
case Actors.get_actor_by_name_with_preload(nickname, type) do
|
||||
%Actor{url: actor_url} = actor ->
|
||||
|
@ -96,20 +88,22 @@ defmodule Mobilizon.Federation.ActivityPub.Actor do
|
|||
end
|
||||
end
|
||||
|
||||
@spec find_or_make_group_from_nickname(String.t()) :: tuple()
|
||||
@spec find_or_make_group_from_nickname(nick :: String.t()) ::
|
||||
{:error, make_actor_errors | WebFinger.finger_errors()}
|
||||
def find_or_make_group_from_nickname(nick), do: find_or_make_actor_from_nickname(nick, :Group)
|
||||
|
||||
@doc """
|
||||
Create an actor inside our database from username, using WebFinger to find out its AP ID and then fetch it
|
||||
"""
|
||||
@spec make_actor_from_nickname(String.t()) :: {:ok, %Actor{}} | {:error, any()}
|
||||
@spec make_actor_from_nickname(nickname :: String.t(), preload :: boolean) ::
|
||||
{:ok, Actor.t()} | {:error, make_actor_errors | WebFinger.finger_errors()}
|
||||
def make_actor_from_nickname(nickname, preload \\ false) do
|
||||
case WebFinger.finger(nickname) do
|
||||
{:ok, url} when is_binary(url) ->
|
||||
make_actor_from_url(url, preload)
|
||||
|
||||
_e ->
|
||||
{:error, "No ActivityPub URL found in WebFinger"}
|
||||
{:error, e} ->
|
||||
{:error, e}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -7,6 +7,7 @@ defmodule Mobilizon.Federation.ActivityPub.Audience do
|
|||
alias Mobilizon.Actors.{Actor, Member}
|
||||
alias Mobilizon.Discussions.{Comment, Discussion}
|
||||
alias Mobilizon.Events.{Event, Participant}
|
||||
alias Mobilizon.Federation.ActivityPub.Types.Entity
|
||||
alias Mobilizon.Posts.Post
|
||||
alias Mobilizon.Storage.Repo
|
||||
|
||||
|
@ -99,6 +100,8 @@ defmodule Mobilizon.Federation.ActivityPub.Audience do
|
|||
}
|
||||
end
|
||||
|
||||
@spec get_to_and_cc(Actor.t(), list(), :direct | :private | :public | :unlisted | {:list, any}) ::
|
||||
{list(), list()}
|
||||
@doc """
|
||||
Determines the full audience based on mentions for an audience
|
||||
|
||||
|
@ -118,7 +121,6 @@ defmodule Mobilizon.Federation.ActivityPub.Audience do
|
|||
* `to` : the mentioned actors and the eventual actor we're replying to
|
||||
* `cc` : none
|
||||
"""
|
||||
@spec get_to_and_cc(Actor.t(), list(), String.t()) :: {list(), list()}
|
||||
def get_to_and_cc(%Actor{} = actor, mentions, :public) do
|
||||
to = [@ap_public | mentions]
|
||||
cc = [actor.followers_url]
|
||||
|
@ -128,7 +130,6 @@ defmodule Mobilizon.Federation.ActivityPub.Audience do
|
|||
{to, cc}
|
||||
end
|
||||
|
||||
@spec get_to_and_cc(Actor.t(), list(), String.t()) :: {list(), list()}
|
||||
def get_to_and_cc(%Actor{} = actor, mentions, :unlisted) do
|
||||
to = [actor.followers_url | mentions]
|
||||
cc = [@ap_public]
|
||||
|
@ -138,7 +139,6 @@ defmodule Mobilizon.Federation.ActivityPub.Audience do
|
|||
{to, cc}
|
||||
end
|
||||
|
||||
@spec get_to_and_cc(Actor.t(), list(), String.t()) :: {list(), list()}
|
||||
def get_to_and_cc(%Actor{} = actor, mentions, :private) do
|
||||
{to, cc} = get_to_and_cc(actor, mentions, :direct)
|
||||
|
||||
|
@ -147,7 +147,6 @@ defmodule Mobilizon.Federation.ActivityPub.Audience do
|
|||
{to, cc}
|
||||
end
|
||||
|
||||
@spec get_to_and_cc(Actor.t(), list(), String.t()) :: {list(), list()}
|
||||
def get_to_and_cc(_actor, mentions, :direct) do
|
||||
{mentions, []}
|
||||
end
|
||||
|
@ -156,22 +155,20 @@ defmodule Mobilizon.Federation.ActivityPub.Audience do
|
|||
{mentions, []}
|
||||
end
|
||||
|
||||
@spec maybe_add_group_members(List.t(), Actor.t()) :: List.t()
|
||||
@spec maybe_add_group_members(list(String.t()), Actor.t()) :: list(String.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()
|
||||
@spec maybe_add_followers(list(String.t()), Actor.t()) :: list(String.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
|
||||
|
||||
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(_), do: []
|
||||
|
@ -239,29 +236,27 @@ defmodule Mobilizon.Federation.ActivityPub.Audience do
|
|||
|
||||
@spec extract_actors_from_mentions(list(), Actor.t(), atom()) :: {list(), list()}
|
||||
defp extract_actors_from_mentions(mentions, actor, visibility) do
|
||||
with mentioned_actors <- Enum.map(mentions, &process_mention/1),
|
||||
addressed_actors <- get_addressed_actors(mentioned_actors, nil) do
|
||||
get_to_and_cc(actor, addressed_actors, visibility)
|
||||
end
|
||||
get_to_and_cc(actor, Enum.map(mentions, &process_mention/1), visibility)
|
||||
end
|
||||
|
||||
@spec extract_actors_from_event(Event.t()) :: %{
|
||||
String.t() => list(String.t())
|
||||
}
|
||||
defp extract_actors_from_event(%Event{} = event) do
|
||||
with {to, cc} <-
|
||||
{to, cc} =
|
||||
extract_actors_from_mentions(
|
||||
event.mentions,
|
||||
group_or_organizer_event(event),
|
||||
event.visibility
|
||||
),
|
||||
{to, cc} <-
|
||||
)
|
||||
|
||||
{to, cc} =
|
||||
{to,
|
||||
Enum.uniq(
|
||||
cc ++ add_comments_authors(event.comments) ++ add_shares_actors_followers(event.url)
|
||||
)} do
|
||||
)}
|
||||
|
||||
%{"to" => to, "cc" => cc}
|
||||
else
|
||||
_ ->
|
||||
%{"to" => [], "cc" => []}
|
||||
end
|
||||
end
|
||||
|
||||
@spec group_or_organizer_event(Event.t()) :: Actor.t()
|
||||
|
|
|
@ -12,17 +12,19 @@ defmodule Mobilizon.Federation.ActivityPub.Federator do
|
|||
|
||||
alias Mobilizon.Actors.Actor
|
||||
alias Mobilizon.Federation.ActivityPub
|
||||
alias Mobilizon.Federation.ActivityPub.{Activity, Transmogrifier}
|
||||
alias Mobilizon.Federation.ActivityPub.Actor, as: ActivityPubActor
|
||||
alias Mobilizon.Federation.ActivityPub.Transmogrifier
|
||||
|
||||
require Logger
|
||||
|
||||
@max_jobs 20
|
||||
|
||||
@spec init(any()) :: {:ok, any()}
|
||||
def init(args) do
|
||||
{:ok, args}
|
||||
end
|
||||
|
||||
@spec start_link(any) :: GenServer.on_start()
|
||||
def start_link(_) do
|
||||
spawn(fn ->
|
||||
# 1 minute
|
||||
|
@ -39,6 +41,8 @@ defmodule Mobilizon.Federation.ActivityPub.Federator do
|
|||
)
|
||||
end
|
||||
|
||||
@spec handle(:publish | :publish_single_ap | atom(), Activity.t() | map()) ::
|
||||
:ok | {:ok, Activity.t()} | Tesla.Env.result() | {:error, String.t()}
|
||||
def handle(:publish, activity) do
|
||||
Logger.debug(inspect(activity))
|
||||
Logger.debug(fn -> "Running publish for #{activity.data["id"]}" end)
|
||||
|
@ -46,7 +50,7 @@ defmodule Mobilizon.Federation.ActivityPub.Federator do
|
|||
with {:ok, %Actor{} = actor} <-
|
||||
ActivityPubActor.get_or_fetch_actor_by_url(activity.data["actor"]) do
|
||||
Logger.info(fn -> "Sending #{activity.data["id"]} out via AP" end)
|
||||
ActivityPub.publish(actor, activity)
|
||||
ActivityPub.Publisher.publish(actor, activity)
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -58,9 +62,6 @@ defmodule Mobilizon.Federation.ActivityPub.Federator do
|
|||
{:ok, activity, _data} ->
|
||||
{:ok, activity}
|
||||
|
||||
%Activity{} ->
|
||||
Logger.info("Already had #{params["id"]}")
|
||||
|
||||
e ->
|
||||
# Just drop those for now
|
||||
Logger.debug("Unhandled activity")
|
||||
|
@ -70,7 +71,7 @@ defmodule Mobilizon.Federation.ActivityPub.Federator do
|
|||
end
|
||||
|
||||
def handle(:publish_single_ap, params) do
|
||||
ActivityPub.publish_one(params)
|
||||
ActivityPub.Publisher.publish_one(params)
|
||||
end
|
||||
|
||||
def handle(type, _) do
|
||||
|
@ -78,6 +79,7 @@ defmodule Mobilizon.Federation.ActivityPub.Federator do
|
|||
{:error, "Don't know what to do with this"}
|
||||
end
|
||||
|
||||
@spec enqueue(atom(), map(), pos_integer()) :: :ok | {:ok, any()} | {:error, any()}
|
||||
def enqueue(type, payload, priority \\ 1) do
|
||||
Logger.debug("enqueue something with type #{inspect(type)}")
|
||||
|
||||
|
@ -88,6 +90,7 @@ defmodule Mobilizon.Federation.ActivityPub.Federator do
|
|||
end
|
||||
end
|
||||
|
||||
@spec maybe_start_job(any(), any()) :: {any(), any()}
|
||||
def maybe_start_job(running_jobs, queue) do
|
||||
if :sets.size(running_jobs) < @max_jobs && queue != [] do
|
||||
{{type, payload}, queue} = queue_pop(queue)
|
||||
|
@ -99,6 +102,7 @@ defmodule Mobilizon.Federation.ActivityPub.Federator do
|
|||
end
|
||||
end
|
||||
|
||||
@spec handle_cast(any(), any()) :: {:noreply, any()}
|
||||
def handle_cast({:enqueue, type, payload, _priority}, state)
|
||||
when type in [:incoming_doc, :incoming_ap_doc] do
|
||||
%{in: {i_running_jobs, i_queue}, out: {o_running_jobs, o_queue}} = state
|
||||
|
@ -122,6 +126,7 @@ defmodule Mobilizon.Federation.ActivityPub.Federator do
|
|||
{:noreply, state}
|
||||
end
|
||||
|
||||
@spec handle_info({:DOWN, any(), :process, any, any()}, any) :: {:noreply, map()}
|
||||
def handle_info({:DOWN, ref, :process, _pid, _reason}, state) do
|
||||
%{in: {i_running_jobs, i_queue}, out: {o_running_jobs, o_queue}} = state
|
||||
i_running_jobs = :sets.del_element(ref, i_running_jobs)
|
||||
|
@ -132,11 +137,13 @@ defmodule Mobilizon.Federation.ActivityPub.Federator do
|
|||
{:noreply, %{in: {i_running_jobs, i_queue}, out: {o_running_jobs, o_queue}}}
|
||||
end
|
||||
|
||||
@spec enqueue_sorted(any(), any(), pos_integer()) :: any()
|
||||
def enqueue_sorted(queue, element, priority) do
|
||||
[%{item: element, priority: priority} | queue]
|
||||
|> Enum.sort_by(fn %{priority: priority} -> priority end)
|
||||
end
|
||||
|
||||
@spec queue_pop(list(any())) :: {any(), list(any())}
|
||||
def queue_pop([%{item: element} | queue]) do
|
||||
{element, queue}
|
||||
end
|
||||
|
|
|
@ -15,107 +15,141 @@ defmodule Mobilizon.Federation.ActivityPub.Fetcher do
|
|||
import Mobilizon.Federation.ActivityPub.Utils,
|
||||
only: [maybe_date_fetch: 2, sign_fetch: 4, origin_check?: 2]
|
||||
|
||||
@spec fetch(String.t(), Keyword.t()) :: {:ok, map()}
|
||||
import Mobilizon.Service.Guards, only: [is_valid_string: 1]
|
||||
|
||||
@spec fetch(String.t(), Keyword.t()) ::
|
||||
{:ok, map()}
|
||||
| {:error,
|
||||
:invalid_url | :http_gone | :http_error | :http_not_found | :content_not_json}
|
||||
def fetch(url, options \\ []) do
|
||||
on_behalf_of = Keyword.get(options, :on_behalf_of, Relay.get_actor())
|
||||
date = Signature.generate_date_header()
|
||||
|
||||
with false <- address_invalid(url),
|
||||
date <- Signature.generate_date_header(),
|
||||
headers <-
|
||||
headers =
|
||||
[{:Accept, "application/activity+json"}]
|
||||
|> maybe_date_fetch(date)
|
||||
|> sign_fetch(on_behalf_of, url, date),
|
||||
client <-
|
||||
ActivityPubClient.client(headers: headers),
|
||||
{:ok, %Tesla.Env{body: data, status: code}} when code in 200..299 <-
|
||||
ActivityPubClient.get(client, url) do
|
||||
|> sign_fetch(on_behalf_of, url, date)
|
||||
|
||||
client = ActivityPubClient.client(headers: headers)
|
||||
|
||||
if address_valid?(url) do
|
||||
case ActivityPubClient.get(client, url) do
|
||||
{:ok, %Tesla.Env{body: data, status: code}} when code in 200..299 and is_map(data) ->
|
||||
{:ok, data}
|
||||
else
|
||||
|
||||
{:ok, %Tesla.Env{status: 410}} ->
|
||||
Logger.debug("Resource at #{url} is 410 Gone")
|
||||
{:error, "Gone"}
|
||||
{:error, :http_gone}
|
||||
|
||||
{:ok, %Tesla.Env{status: 404}} ->
|
||||
Logger.debug("Resource at #{url} is 404 Gone")
|
||||
{:error, "Not found"}
|
||||
{:error, :http_not_found}
|
||||
|
||||
{:ok, %Tesla.Env{body: data}} when is_binary(data) ->
|
||||
{:error, :content_not_json}
|
||||
|
||||
{:ok, %Tesla.Env{} = res} ->
|
||||
{:error, res}
|
||||
|
||||
{:error, err} ->
|
||||
{:error, err}
|
||||
Logger.debug("Resource returned bad HTTP code inspect #{res}")
|
||||
{:error, :http_error}
|
||||
end
|
||||
else
|
||||
{:error, :invalid_url}
|
||||
end
|
||||
end
|
||||
|
||||
@spec fetch_and_create(String.t(), Keyword.t()) :: {:ok, map(), struct()}
|
||||
@spec fetch_and_create(String.t(), Keyword.t()) ::
|
||||
{:ok, map(), struct()} | {:error, atom()} | :error
|
||||
def fetch_and_create(url, options \\ []) do
|
||||
with {:ok, data} when is_map(data) <- fetch(url, options),
|
||||
{:origin_check, true} <- {:origin_check, origin_check?(url, data)},
|
||||
params <- %{
|
||||
case fetch(url, options) do
|
||||
{:ok, data} when is_map(data) ->
|
||||
if origin_check?(url, data) do
|
||||
case Transmogrifier.handle_incoming(%{
|
||||
"type" => "Create",
|
||||
"to" => data["to"],
|
||||
"cc" => data["cc"],
|
||||
"actor" => data["actor"] || data["attributedTo"],
|
||||
"attributedTo" => data["attributedTo"] || data["actor"],
|
||||
"object" => data
|
||||
} do
|
||||
Transmogrifier.handle_incoming(params)
|
||||
else
|
||||
{:origin_check, false} ->
|
||||
Logger.warn("Object origin check failed")
|
||||
{:error, "Object origin check failed"}
|
||||
}) do
|
||||
{:ok, entity, structure} ->
|
||||
{:ok, entity, structure}
|
||||
|
||||
# Returned content is not JSON
|
||||
{:ok, data} when is_binary(data) ->
|
||||
{:error, "Failed to parse content as JSON"}
|
||||
{:error, error} when is_atom(error) ->
|
||||
{:error, error}
|
||||
|
||||
:error ->
|
||||
{:error, :transmogrifier_error}
|
||||
end
|
||||
else
|
||||
Logger.warn("Object origin check failed")
|
||||
{:error, :object_origin_check_failed}
|
||||
end
|
||||
|
||||
{:error, err} ->
|
||||
{:error, err}
|
||||
end
|
||||
end
|
||||
|
||||
@spec fetch_and_update(String.t(), Keyword.t()) :: {:ok, map(), struct()}
|
||||
@spec fetch_and_update(String.t(), Keyword.t()) ::
|
||||
{:ok, map(), struct()} | {:error, atom()}
|
||||
def fetch_and_update(url, options \\ []) do
|
||||
with {:ok, data} when is_map(data) <- fetch(url, options),
|
||||
{:origin_check, true} <- {:origin_check, origin_check(url, data)},
|
||||
params <- %{
|
||||
case fetch(url, options) do
|
||||
{:ok, data} when is_map(data) ->
|
||||
if origin_check(url, data) do
|
||||
Transmogrifier.handle_incoming(%{
|
||||
"type" => "Update",
|
||||
"to" => data["to"],
|
||||
"cc" => data["cc"],
|
||||
"actor" => data["actor"] || data["attributedTo"],
|
||||
"attributedTo" => data["attributedTo"] || data["actor"],
|
||||
"object" => data
|
||||
} do
|
||||
Transmogrifier.handle_incoming(params)
|
||||
})
|
||||
else
|
||||
{:origin_check, false} ->
|
||||
{:error, "Object origin check failed"}
|
||||
Logger.warn("Object origin check failed")
|
||||
{:error, :object_origin_check_failed}
|
||||
end
|
||||
|
||||
{:error, err} ->
|
||||
{:error, err}
|
||||
end
|
||||
end
|
||||
|
||||
@type fetch_actor_errors ::
|
||||
:json_decode_error | :actor_deleted | :http_error | :actor_not_allowed_type
|
||||
|
||||
@doc """
|
||||
Fetching a remote actor's information through its AP ID
|
||||
"""
|
||||
@spec fetch_and_prepare_actor_from_url(String.t()) :: {:ok, map()} | {:error, atom()} | any()
|
||||
@spec fetch_and_prepare_actor_from_url(String.t()) ::
|
||||
{:ok, map()} | {:error, fetch_actor_errors}
|
||||
def fetch_and_prepare_actor_from_url(url) do
|
||||
Logger.debug("Fetching and preparing actor from url")
|
||||
Logger.debug(inspect(url))
|
||||
|
||||
res =
|
||||
with {:ok, %{status: 200, body: body}} <-
|
||||
Tesla.get(url,
|
||||
case Tesla.get(url,
|
||||
headers: [{"Accept", "application/activity+json"}],
|
||||
follow_redirect: true
|
||||
),
|
||||
:ok <- Logger.debug("response okay, now decoding json"),
|
||||
{:ok, data} <- Jason.decode(body) do
|
||||
) do
|
||||
{:ok, %{status: 200, body: body}} ->
|
||||
Logger.debug("response okay, now decoding json")
|
||||
|
||||
case Jason.decode(body) do
|
||||
{:ok, data} when is_map(data) ->
|
||||
Logger.debug("Got activity+json response at actor's endpoint, now converting data")
|
||||
{:ok, ActorConverter.as_to_model_data(data)}
|
||||
else
|
||||
# Actor is gone, probably deleted
|
||||
|
||||
case ActorConverter.as_to_model_data(data) do
|
||||
{:error, :actor_not_allowed_type} ->
|
||||
{:error, :actor_not_allowed_type}
|
||||
|
||||
map when is_map(map) ->
|
||||
{:ok, map}
|
||||
end
|
||||
|
||||
{:error, %Jason.DecodeError{} = e} ->
|
||||
Logger.warn("Could not decode actor at fetch #{url}, #{inspect(e)}")
|
||||
{:error, :json_decode_error}
|
||||
end
|
||||
|
||||
{:ok, %{status: 410}} ->
|
||||
Logger.info("Response HTTP 410")
|
||||
{:error, :actor_deleted}
|
||||
|
@ -124,16 +158,10 @@ defmodule Mobilizon.Federation.ActivityPub.Fetcher do
|
|||
Logger.info("Non 200 HTTP Code")
|
||||
{:error, :http_error}
|
||||
|
||||
{:error, e} ->
|
||||
Logger.warn("Could not decode actor at fetch #{url}, #{inspect(e)}")
|
||||
{:error, e}
|
||||
|
||||
e ->
|
||||
Logger.warn("Could not decode actor at fetch #{url}, #{inspect(e)}")
|
||||
{:error, e}
|
||||
{:error, error} ->
|
||||
Logger.warn("Could not fetch actor at fetch #{url}, #{inspect(error)}")
|
||||
{:error, :http_error}
|
||||
end
|
||||
|
||||
res
|
||||
end
|
||||
|
||||
@spec origin_check(String.t(), map()) :: boolean()
|
||||
|
@ -147,11 +175,9 @@ defmodule Mobilizon.Federation.ActivityPub.Fetcher do
|
|||
end
|
||||
end
|
||||
|
||||
@spec address_invalid(String.t()) :: false | {:error, :invalid_url}
|
||||
defp address_invalid(address) do
|
||||
with %URI{host: host, scheme: scheme} <- URI.parse(address),
|
||||
true <- is_nil(host) or is_nil(scheme) do
|
||||
{:error, :invalid_url}
|
||||
end
|
||||
@spec address_valid?(String.t()) :: boolean
|
||||
defp address_valid?(address) do
|
||||
%URI{host: host, scheme: scheme} = URI.parse(address)
|
||||
is_valid_string(host) and is_valid_string(scheme)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -13,6 +13,17 @@ defmodule Mobilizon.Federation.ActivityPub.Permission do
|
|||
|
||||
@member_roles [:member, :moderator, :administrator]
|
||||
|
||||
@type object :: %{id: String.t(), url: String.t()}
|
||||
|
||||
@type permissions_member_role :: nil | :member | :moderator | :administrator
|
||||
|
||||
@type t :: %__MODULE__{
|
||||
access: permissions_member_role,
|
||||
create: permissions_member_role,
|
||||
update: permissions_member_role,
|
||||
delete: permissions_member_role
|
||||
}
|
||||
|
||||
@doc """
|
||||
Check that actor can access the object
|
||||
"""
|
||||
|
@ -66,8 +77,8 @@ defmodule Mobilizon.Federation.ActivityPub.Permission do
|
|||
|
||||
@spec can_manage_group_object?(
|
||||
existing_object_permissions(),
|
||||
Actor.t(),
|
||||
any()
|
||||
%Actor{url: String.t()},
|
||||
object()
|
||||
) :: boolean()
|
||||
defp can_manage_group_object?(permission, %Actor{url: actor_url} = actor, object) do
|
||||
if Ownable.group_actor(object) != nil do
|
||||
|
@ -94,7 +105,7 @@ defmodule Mobilizon.Federation.ActivityPub.Permission do
|
|||
end
|
||||
end
|
||||
|
||||
@spec activity_actor_is_group_member?(Actor.t(), Entity.t(), atom()) :: boolean()
|
||||
@spec activity_actor_is_group_member?(Actor.t(), object(), atom()) :: boolean()
|
||||
defp activity_actor_is_group_member?(
|
||||
%Actor{id: actor_id, url: actor_url},
|
||||
object,
|
||||
|
|
128
lib/federation/activity_pub/publisher.ex
Normal file
128
lib/federation/activity_pub/publisher.ex
Normal file
|
@ -0,0 +1,128 @@
|
|||
defmodule Mobilizon.Federation.ActivityPub.Publisher do
|
||||
@moduledoc """
|
||||
Handle publishing activities
|
||||
"""
|
||||
alias Mobilizon.Actors
|
||||
alias Mobilizon.Actors.Actor
|
||||
alias Mobilizon.Config
|
||||
alias Mobilizon.Federation.ActivityPub.{Activity, Federator, Relay, Transmogrifier, Visibility}
|
||||
alias Mobilizon.Federation.HTTPSignatures.Signature
|
||||
require Logger
|
||||
import Mobilizon.Federation.ActivityPub.Utils, only: [remote_actors: 1]
|
||||
|
||||
@doc """
|
||||
Publish an activity to all appropriated audiences inboxes
|
||||
"""
|
||||
# credo:disable-for-lines:47
|
||||
@spec publish(Actor.t(), Activity.t()) :: :ok
|
||||
def publish(actor, %Activity{recipients: recipients} = activity) do
|
||||
Logger.debug("Publishing an activity")
|
||||
Logger.debug(inspect(activity, pretty: true))
|
||||
|
||||
public = Visibility.is_public?(activity)
|
||||
Logger.debug("is public ? #{public}")
|
||||
|
||||
if public && is_create_activity?(activity) && Config.get([:instance, :allow_relay]) do
|
||||
Logger.info(fn -> "Relaying #{activity.data["id"]} out" end)
|
||||
|
||||
Relay.publish(activity)
|
||||
end
|
||||
|
||||
recipients = Enum.uniq(recipients)
|
||||
|
||||
{recipients, followers} = convert_followers_in_recipients(recipients)
|
||||
|
||||
{recipients, members} = convert_members_in_recipients(recipients)
|
||||
|
||||
remote_inboxes =
|
||||
(remote_actors(recipients) ++ followers ++ members)
|
||||
|> Enum.map(fn actor -> actor.shared_inbox_url || actor.inbox_url end)
|
||||
|> Enum.uniq()
|
||||
|
||||
{:ok, data} = Transmogrifier.prepare_outgoing(activity.data)
|
||||
json = Jason.encode!(data)
|
||||
Logger.debug(fn -> "Remote inboxes are : #{inspect(remote_inboxes)}" end)
|
||||
|
||||
Enum.each(remote_inboxes, fn inbox ->
|
||||
Federator.enqueue(:publish_single_ap, %{
|
||||
inbox: inbox,
|
||||
json: json,
|
||||
actor: actor,
|
||||
id: activity.data["id"]
|
||||
})
|
||||
end)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Publish an activity to a specific inbox
|
||||
"""
|
||||
@spec publish_one(%{inbox: String.t(), json: String.t(), actor: Actor.t(), id: String.t()}) ::
|
||||
Tesla.Env.result()
|
||||
def publish_one(%{inbox: inbox, json: json, actor: actor, id: id}) do
|
||||
Logger.info("Federating #{id} to #{inbox}")
|
||||
%URI{host: host, path: path} = URI.parse(inbox)
|
||||
|
||||
digest = Signature.build_digest(json)
|
||||
date = Signature.generate_date_header()
|
||||
|
||||
# request_target = Signature.generate_request_target("POST", path)
|
||||
|
||||
signature =
|
||||
Signature.sign(actor, %{
|
||||
"(request-target)": "post #{path}",
|
||||
host: host,
|
||||
"content-length": byte_size(json),
|
||||
digest: digest,
|
||||
date: date
|
||||
})
|
||||
|
||||
Tesla.post(
|
||||
inbox,
|
||||
json,
|
||||
headers: [
|
||||
{"Content-Type", "application/activity+json"},
|
||||
{"signature", signature},
|
||||
{"digest", digest},
|
||||
{"date", date}
|
||||
]
|
||||
)
|
||||
end
|
||||
|
||||
@spec convert_followers_in_recipients(list(String.t())) :: {list(String.t()), list(String.t())}
|
||||
defp convert_followers_in_recipients(recipients) do
|
||||
Enum.reduce(recipients, {recipients, []}, fn recipient, {recipients, follower_actors} = acc ->
|
||||
case Actors.get_actor_by_followers_url(recipient) do
|
||||
%Actor{} = group ->
|
||||
{Enum.filter(recipients, fn recipient -> recipient != group.followers_url end),
|
||||
follower_actors ++ Actors.list_external_followers_for_actor(group)}
|
||||
|
||||
nil ->
|
||||
acc
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
@spec is_create_activity?(Activity.t()) :: boolean
|
||||
defp is_create_activity?(%Activity{data: %{"type" => "Create"}}), do: true
|
||||
defp is_create_activity?(_), do: false
|
||||
|
||||
@spec convert_members_in_recipients(list(String.t())) :: {list(String.t()), list(Actor.t())}
|
||||
defp convert_members_in_recipients(recipients) do
|
||||
Enum.reduce(recipients, {recipients, []}, fn recipient, {recipients, member_actors} = acc ->
|
||||
case Actors.get_group_by_members_url(recipient) do
|
||||
# If the group is local just add external members
|
||||
%Actor{domain: domain} = group when is_nil(domain) ->
|
||||
{Enum.filter(recipients, fn recipient -> recipient != group.members_url end),
|
||||
member_actors ++ Actors.list_external_actors_members_for_group(group)}
|
||||
|
||||
# If it's remote add the remote group actor as well
|
||||
%Actor{} = group ->
|
||||
{Enum.filter(recipients, fn recipient -> recipient != group.members_url end),
|
||||
member_actors ++ Actors.list_external_actors_members_for_group(group) ++ [group]}
|
||||
|
||||
_ ->
|
||||
acc
|
||||
end
|
||||
end)
|
||||
end
|
||||
end
|
|
@ -8,13 +8,12 @@ defmodule Mobilizon.Federation.ActivityPub.Refresher do
|
|||
alias Mobilizon.Federation.ActivityPub
|
||||
alias Mobilizon.Federation.ActivityPub.Actor, as: ActivityPubActor
|
||||
alias Mobilizon.Federation.ActivityPub.{Fetcher, Relay, Transmogrifier, Utils}
|
||||
alias Mobilizon.Service.ErrorReporting.Sentry
|
||||
require Logger
|
||||
|
||||
@doc """
|
||||
Refresh a remote profile
|
||||
"""
|
||||
@spec refresh_profile(Actor.t()) :: {:ok, Actor.t()}
|
||||
@spec refresh_profile(Actor.t()) :: {:ok, Actor.t()} | {:error, fetch_actor_errors()} | {:error}
|
||||
def refresh_profile(%Actor{domain: nil}), do: {:error, "Can only refresh remote actors"}
|
||||
|
||||
def refresh_profile(%Actor{type: :Group, url: url, id: group_id} = group) do
|
||||
|
@ -27,22 +26,39 @@ defmodule Mobilizon.Federation.ActivityPub.Refresher do
|
|||
Relay.get_actor()
|
||||
end
|
||||
|
||||
with :ok <- fetch_group(url, on_behalf_of) do
|
||||
case fetch_group(url, on_behalf_of) do
|
||||
{:error, error} ->
|
||||
{:error, error}
|
||||
|
||||
:ok ->
|
||||
{:ok, group}
|
||||
end
|
||||
end
|
||||
|
||||
def refresh_profile(%Actor{type: type, url: url}) when type in [:Person, :Application] do
|
||||
with {:ok, %Actor{outbox_url: outbox_url} = actor} <-
|
||||
ActivityPubActor.make_actor_from_url(url),
|
||||
:ok <- fetch_collection(outbox_url, Relay.get_actor()) do
|
||||
{:ok, actor}
|
||||
case ActivityPubActor.make_actor_from_url(url) do
|
||||
{:error, error} ->
|
||||
{:error, error}
|
||||
|
||||
{:ok, %Actor{outbox_url: outbox_url} = actor} ->
|
||||
case fetch_collection(outbox_url, Relay.get_actor()) do
|
||||
:ok -> {:ok, actor}
|
||||
{:error, error} -> {:error, error}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@spec fetch_group(String.t(), Actor.t()) :: :ok
|
||||
@type fetch_actor_errors :: ActivityPubActor.make_actor_errors() | fetch_collection_errors()
|
||||
|
||||
@spec fetch_group(String.t(), Actor.t()) :: :ok | {:error, fetch_actor_errors}
|
||||
def fetch_group(group_url, %Actor{} = on_behalf_of) do
|
||||
with {:ok,
|
||||
case ActivityPubActor.make_actor_from_url(group_url) do
|
||||
{:error, err}
|
||||
when err in [:actor_deleted, :http_error, :json_decode_error, :actor_is_local] ->
|
||||
Logger.debug("Error while making actor")
|
||||
{:error, err}
|
||||
|
||||
{:ok,
|
||||
%Actor{
|
||||
outbox_url: outbox_url,
|
||||
resources_url: resources_url,
|
||||
|
@ -51,9 +67,10 @@ defmodule Mobilizon.Federation.ActivityPub.Refresher do
|
|||
todos_url: todos_url,
|
||||
discussions_url: discussions_url,
|
||||
events_url: events_url
|
||||
}} <-
|
||||
ActivityPubActor.make_actor_from_url(group_url),
|
||||
:ok <- fetch_collection(outbox_url, on_behalf_of),
|
||||
}} ->
|
||||
Logger.debug("Fetched group OK, now doing collections")
|
||||
|
||||
with :ok <- fetch_collection(outbox_url, on_behalf_of),
|
||||
:ok <- fetch_collection(members_url, on_behalf_of),
|
||||
:ok <- fetch_collection(resources_url, on_behalf_of),
|
||||
:ok <- fetch_collection(posts_url, on_behalf_of),
|
||||
|
@ -62,49 +79,45 @@ defmodule Mobilizon.Federation.ActivityPub.Refresher do
|
|||
:ok <- fetch_collection(events_url, on_behalf_of) do
|
||||
:ok
|
||||
else
|
||||
{:error, :actor_deleted} ->
|
||||
{:error, :actor_deleted}
|
||||
|
||||
{:error, :http_error} ->
|
||||
{:error, :http_error}
|
||||
|
||||
{:error, err} ->
|
||||
Logger.error("Error while refreshing a group")
|
||||
|
||||
Sentry.capture_message("Error while refreshing a group",
|
||||
extra: %{group_url: group_url}
|
||||
)
|
||||
|
||||
Logger.debug(inspect(err))
|
||||
{:error, err}
|
||||
|
||||
err ->
|
||||
Logger.error("Error while refreshing a group")
|
||||
|
||||
Sentry.capture_message("Error while refreshing a group",
|
||||
extra: %{group_url: group_url}
|
||||
)
|
||||
|
||||
Logger.debug(inspect(err))
|
||||
err
|
||||
when err in [:error, :process_error, :fetch_error, :collection_url_nil] ->
|
||||
Logger.debug("Error while fetching actor collection")
|
||||
{:error, err}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def fetch_collection(nil, _on_behalf_of), do: :error
|
||||
@typep fetch_collection_errors :: :process_error | :fetch_error | :collection_url_nil
|
||||
|
||||
@spec fetch_collection(String.t() | nil, any) ::
|
||||
:ok | {:error, fetch_collection_errors}
|
||||
def fetch_collection(nil, _on_behalf_of), do: {:error, :collection_url_nil}
|
||||
|
||||
def fetch_collection(collection_url, on_behalf_of) do
|
||||
Logger.debug("Fetching and preparing collection from url")
|
||||
Logger.debug(inspect(collection_url))
|
||||
|
||||
with {:ok, data} <- Fetcher.fetch(collection_url, on_behalf_of: on_behalf_of),
|
||||
:ok <- Logger.debug("Fetch ok, passing to process_collection"),
|
||||
:ok <- process_collection(data, on_behalf_of) do
|
||||
case Fetcher.fetch(collection_url, on_behalf_of: on_behalf_of) do
|
||||
{:ok, data} when is_map(data) ->
|
||||
Logger.debug("Fetch ok, passing to process_collection")
|
||||
|
||||
case process_collection(data, on_behalf_of) do
|
||||
:ok ->
|
||||
Logger.debug("Finished processing a collection")
|
||||
:ok
|
||||
|
||||
:error ->
|
||||
Logger.debug("Failed to process collection #{collection_url}")
|
||||
{:error, :process_error}
|
||||
end
|
||||
|
||||
{:error, _err} ->
|
||||
Logger.debug("Failed to fetch collection #{collection_url}")
|
||||
{:error, :fetch_error}
|
||||
end
|
||||
end
|
||||
|
||||
@spec fetch_element(String.t(), Actor.t()) :: any()
|
||||
@spec fetch_element(String.t(), Actor.t()) :: {:ok, struct()} | {:error, any()}
|
||||
def fetch_element(url, %Actor{} = on_behalf_of) do
|
||||
with {:ok, data} <- Fetcher.fetch(url, on_behalf_of: on_behalf_of) do
|
||||
case handling_element(data) do
|
||||
|
@ -114,6 +127,9 @@ defmodule Mobilizon.Federation.ActivityPub.Refresher do
|
|||
{:ok, entity} ->
|
||||
{:ok, entity}
|
||||
|
||||
:error ->
|
||||
{:error, :err_fetching_element}
|
||||
|
||||
err ->
|
||||
{:error, err}
|
||||
end
|
||||
|
@ -127,6 +143,7 @@ defmodule Mobilizon.Federation.ActivityPub.Refresher do
|
|||
|> Enum.each(&refresh_profile/1)
|
||||
end
|
||||
|
||||
@spec process_collection(map(), any()) :: :ok | :error
|
||||
defp process_collection(%{"type" => type, "orderedItems" => items}, _on_behalf_of)
|
||||
when type in ["OrderedCollection", "OrderedCollectionPage"] do
|
||||
Logger.debug(
|
||||
|
@ -168,6 +185,8 @@ defmodule Mobilizon.Federation.ActivityPub.Refresher do
|
|||
defp process_collection(_, _), do: :error
|
||||
|
||||
# If we're handling an activity
|
||||
@spec handling_element(map()) :: {:ok, any, struct} | :error
|
||||
@spec handling_element(String.t()) :: {:ok, struct} | {:error, any()}
|
||||
defp handling_element(%{"type" => activity_type} = data)
|
||||
when activity_type in ["Create", "Update", "Delete"] do
|
||||
object = get_in(data, ["object"])
|
||||
|
|
|
@ -11,8 +11,7 @@ defmodule Mobilizon.Federation.ActivityPub.Relay do
|
|||
alias Mobilizon.Actors
|
||||
alias Mobilizon.Actors.{Actor, Follower}
|
||||
|
||||
alias Mobilizon.Federation.ActivityPub
|
||||
alias Mobilizon.Federation.ActivityPub.{Activity, Transmogrifier}
|
||||
alias Mobilizon.Federation.ActivityPub.{Actions, Activity, Transmogrifier}
|
||||
alias Mobilizon.Federation.ActivityPub.Actor, as: ActivityPubActor
|
||||
alias Mobilizon.Federation.WebFinger
|
||||
alias Mobilizon.Service.Workers.Background
|
||||
|
@ -27,76 +26,100 @@ defmodule Mobilizon.Federation.ActivityPub.Relay do
|
|||
get_actor()
|
||||
end
|
||||
|
||||
@spec get_actor() :: Actor.t() | {:error, Ecto.Changeset.t()}
|
||||
@spec get_actor() :: Actor.t() | no_return
|
||||
def get_actor do
|
||||
with {:ok, %Actor{} = actor} <-
|
||||
Actors.get_or_create_internal_actor("relay") do
|
||||
case Actors.get_or_create_internal_actor("relay") do
|
||||
{:ok, %Actor{} = actor} ->
|
||||
actor
|
||||
|
||||
{:error, %Ecto.Changeset{} = _err} ->
|
||||
raise("Relay actor not found")
|
||||
end
|
||||
end
|
||||
|
||||
@spec follow(String.t()) :: {:ok, Activity.t(), Follower.t()}
|
||||
@spec follow(String.t()) ::
|
||||
{:ok, Activity.t(), Follower.t()} | {:error, atom()} | {:error, String.t()}
|
||||
def follow(address) do
|
||||
%Actor{} = local_actor = get_actor()
|
||||
|
||||
with {:ok, target_instance} <- fetch_actor(address),
|
||||
%Actor{} = local_actor <- get_actor(),
|
||||
{:ok, %Actor{} = target_actor} <-
|
||||
ActivityPubActor.get_or_fetch_actor_by_url(target_instance),
|
||||
{:ok, activity, follow} <- Follows.follow(local_actor, target_actor) do
|
||||
Logger.info("Relay: followed instance #{target_instance}; id=#{activity.data["id"]}")
|
||||
{:ok, activity, follow}
|
||||
else
|
||||
{:error, :person_no_follow} ->
|
||||
Logger.warn("Only group and instances can be followed")
|
||||
{:error, :person_no_follow}
|
||||
|
||||
{:error, e} ->
|
||||
Logger.warn("Error while following remote instance: #{inspect(e)}")
|
||||
{:error, e}
|
||||
|
||||
e ->
|
||||
Logger.warn("Error while following remote instance: #{inspect(e)}")
|
||||
{:error, e}
|
||||
end
|
||||
end
|
||||
|
||||
@spec unfollow(String.t()) :: {:ok, Activity.t(), Follower.t()}
|
||||
@spec unfollow(String.t()) ::
|
||||
{:ok, Activity.t(), Follower.t()} | {:error, atom()} | {:error, String.t()}
|
||||
def unfollow(address) do
|
||||
%Actor{} = local_actor = get_actor()
|
||||
|
||||
with {:ok, target_instance} <- fetch_actor(address),
|
||||
%Actor{} = local_actor <- get_actor(),
|
||||
{:ok, %Actor{} = target_actor} <-
|
||||
ActivityPubActor.get_or_fetch_actor_by_url(target_instance),
|
||||
{:ok, activity, follow} <- Follows.unfollow(local_actor, target_actor) do
|
||||
Logger.info("Relay: unfollowed instance #{target_instance}: id=#{activity.data["id"]}")
|
||||
{:ok, activity, follow}
|
||||
else
|
||||
e ->
|
||||
{:error, e} ->
|
||||
Logger.warn("Error while unfollowing remote instance: #{inspect(e)}")
|
||||
{:error, e}
|
||||
end
|
||||
end
|
||||
|
||||
@spec accept(String.t()) :: {:ok, Activity.t(), Follower.t()}
|
||||
@spec accept(String.t()) ::
|
||||
{:ok, Activity.t(), Follower.t()} | {:error, atom()} | {:error, String.t()}
|
||||
def accept(address) do
|
||||
Logger.debug("We're trying to accept a relay subscription")
|
||||
%Actor{} = local_actor = get_actor()
|
||||
|
||||
with {:ok, target_instance} <- fetch_actor(address),
|
||||
%Actor{} = local_actor <- get_actor(),
|
||||
{:ok, %Actor{} = target_actor} <-
|
||||
ActivityPubActor.get_or_fetch_actor_by_url(target_instance),
|
||||
{:ok, activity, follow} <- Follows.accept(target_actor, local_actor) do
|
||||
{:ok, activity, follow}
|
||||
else
|
||||
{:error, e} ->
|
||||
Logger.warn("Error while accepting remote instance follow: #{inspect(e)}")
|
||||
{:error, e}
|
||||
end
|
||||
end
|
||||
|
||||
@spec reject(String.t()) ::
|
||||
{:ok, Activity.t(), Follower.t()} | {:error, atom()} | {:error, String.t()}
|
||||
def reject(address) do
|
||||
Logger.debug("We're trying to reject a relay subscription")
|
||||
%Actor{} = local_actor = get_actor()
|
||||
|
||||
with {:ok, target_instance} <- fetch_actor(address),
|
||||
%Actor{} = local_actor <- get_actor(),
|
||||
{:ok, %Actor{} = target_actor} <-
|
||||
ActivityPubActor.get_or_fetch_actor_by_url(target_instance),
|
||||
{:ok, activity, follow} <- Follows.reject(target_actor, local_actor) do
|
||||
{:ok, activity, follow}
|
||||
else
|
||||
{:error, e} ->
|
||||
Logger.warn("Error while rejecting remote instance follow: #{inspect(e)}")
|
||||
{:error, e}
|
||||
end
|
||||
end
|
||||
|
||||
@spec refresh(String.t()) :: {:ok, any()}
|
||||
@spec refresh(String.t()) ::
|
||||
{:ok, Oban.Job.t()}
|
||||
| {:error, Ecto.Changeset.t()}
|
||||
| {:error, :bad_url}
|
||||
| {:error, ActivityPubActor.make_actor_errors()}
|
||||
| {:error, :no_internal_relay_actor}
|
||||
| {:error, :url_nil}
|
||||
def refresh(address) do
|
||||
Logger.debug("We're trying to refresh a remote instance")
|
||||
|
||||
|
@ -106,6 +129,10 @@ defmodule Mobilizon.Federation.ActivityPub.Relay do
|
|||
Background.enqueue("refresh_profile", %{
|
||||
"actor_id" => target_actor_id
|
||||
})
|
||||
else
|
||||
{:error, e} ->
|
||||
Logger.warn("Error while refreshing remote instance: #{inspect(e)}")
|
||||
{:error, e}
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -117,7 +144,7 @@ defmodule Mobilizon.Federation.ActivityPub.Relay do
|
|||
{object, object_id} <- fetch_object(object),
|
||||
id <- "#{object_id}/announces/#{actor_id}" do
|
||||
Logger.info("Publishing activity #{id} to all relays")
|
||||
ActivityPub.announce(actor, object, id, true, false)
|
||||
Actions.Announce.announce(actor, object, id, true, false)
|
||||
else
|
||||
e ->
|
||||
Logger.error("Error while getting local instance actor: #{inspect(e)}")
|
||||
|
@ -138,7 +165,8 @@ defmodule Mobilizon.Federation.ActivityPub.Relay do
|
|||
|
||||
defp fetch_object(object) when is_binary(object), do: {object, object}
|
||||
|
||||
@spec fetch_actor(String.t()) :: {:ok, String.t()} | {:error, String.t()}
|
||||
@spec fetch_actor(String.t()) ::
|
||||
{:ok, String.t()} | {:error, WebFinger.finger_errors() | :bad_url}
|
||||
# Dirty hack
|
||||
defp fetch_actor("https://" <> address), do: fetch_actor(address)
|
||||
defp fetch_actor("http://" <> address), do: fetch_actor(address)
|
||||
|
@ -154,26 +182,15 @@ defmodule Mobilizon.Federation.ActivityPub.Relay do
|
|||
check_actor("relay@#{host}")
|
||||
|
||||
true ->
|
||||
{:error, "Bad URL"}
|
||||
{:error, :bad_url}
|
||||
end
|
||||
end
|
||||
|
||||
@spec check_actor(String.t()) :: {:ok, String.t()} | {:error, String.t()}
|
||||
@spec check_actor(String.t()) :: {:ok, String.t()} | {:error, WebFinger.finger_errors()}
|
||||
defp check_actor(username_and_domain) do
|
||||
case Actors.get_actor_by_name(username_and_domain) do
|
||||
%Actor{url: url} -> {:ok, url}
|
||||
nil -> finger_actor(username_and_domain)
|
||||
end
|
||||
end
|
||||
|
||||
@spec finger_actor(String.t()) :: {:ok, String.t()} | {:error, String.t()}
|
||||
defp finger_actor(nickname) do
|
||||
case WebFinger.finger(nickname) do
|
||||
{:ok, url} when is_binary(url) ->
|
||||
{:ok, url}
|
||||
|
||||
_e ->
|
||||
{:error, "No ActivityPub URL found in WebFinger"}
|
||||
nil -> WebFinger.finger(username_and_domain)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -17,7 +17,7 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier do
|
|||
alias Mobilizon.Todos.{Todo, TodoList}
|
||||
|
||||
alias Mobilizon.Federation.ActivityPub
|
||||
alias Mobilizon.Federation.ActivityPub.{Activity, Permission, Relay, Utils}
|
||||
alias Mobilizon.Federation.ActivityPub.{Actions, Activity, Permission, Relay, Utils}
|
||||
alias Mobilizon.Federation.ActivityPub.Actor, as: ActivityPubActor
|
||||
alias Mobilizon.Federation.ActivityPub.Types.Ownable
|
||||
alias Mobilizon.Federation.ActivityStream.{Converter, Convertible}
|
||||
|
@ -32,6 +32,7 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier do
|
|||
@doc """
|
||||
Handle incoming activities
|
||||
"""
|
||||
@spec handle_incoming(map()) :: :error | {:ok, any(), struct()}
|
||||
def handle_incoming(%{"id" => nil}), do: :error
|
||||
def handle_incoming(%{"id" => ""}), do: :error
|
||||
|
||||
|
@ -49,7 +50,7 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier do
|
|||
local: false
|
||||
}
|
||||
|
||||
ActivityPub.flag(params, false)
|
||||
Actions.Flag.flag(params, false)
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -76,17 +77,17 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier do
|
|||
{:ok, %Activity{} = activity, entity} <-
|
||||
(if is_data_for_comment_or_discussion?(object_data) do
|
||||
Logger.debug("Chosing to create a regular comment")
|
||||
ActivityPub.create(:comment, object_data, false)
|
||||
Actions.Create.create(:comment, object_data, false)
|
||||
else
|
||||
Logger.debug("Chosing to initialize or add a comment to a conversation")
|
||||
ActivityPub.create(:discussion, object_data, false)
|
||||
Actions.Create.create(:discussion, object_data, false)
|
||||
end) do
|
||||
{:ok, activity, entity}
|
||||
else
|
||||
{:existing_comment, {:ok, %Comment{} = comment}} ->
|
||||
{:ok, nil, comment}
|
||||
|
||||
{:error, :event_comments_are_closed} ->
|
||||
{:error, :event_not_allow_commenting} ->
|
||||
Logger.debug("Tried to reply to an event for which comments are closed")
|
||||
:error
|
||||
end
|
||||
|
@ -109,7 +110,7 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier do
|
|||
object |> Converter.Event.as_to_model_data(),
|
||||
{:existing_event, nil} <- {:existing_event, Events.get_event_by_url(object_data.url)},
|
||||
{:ok, %Activity{} = activity, %Event{} = event} <-
|
||||
ActivityPub.create(:event, object_data, false) do
|
||||
Actions.Create.create(:event, object_data, false) do
|
||||
{:ok, activity, event}
|
||||
else
|
||||
{:existing_event, %Event{} = event} -> {:ok, nil, event}
|
||||
|
@ -145,7 +146,7 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier do
|
|||
%Actor{type: :Group} = group <- Actors.get_actor(object_data.parent_id),
|
||||
%Actor{} = actor <- Actors.get_actor(object_data.actor_id),
|
||||
{:ok, %Activity{} = activity, %Member{} = member} <-
|
||||
ActivityPub.join(group, actor, false, %{
|
||||
Actions.Join.join(group, actor, false, %{
|
||||
url: object_data.url,
|
||||
metadata: %{role: object_data.role}
|
||||
}) do
|
||||
|
@ -172,7 +173,7 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier do
|
|||
{:existing_post, nil} <-
|
||||
{:existing_post, Posts.get_post_by_url(object_data.url)},
|
||||
{:ok, %Activity{} = activity, %Post{} = post} <-
|
||||
ActivityPub.create(:post, object_data, false) do
|
||||
Actions.Create.create(:post, object_data, false) do
|
||||
{:ok, activity, post}
|
||||
else
|
||||
{:existing_post, %Post{} = post} ->
|
||||
|
@ -197,7 +198,7 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier do
|
|||
{:ok, nil, comment}
|
||||
|
||||
{:ok, entity} ->
|
||||
ActivityPub.delete(entity, Relay.get_actor(), false)
|
||||
Actions.Delete.delete(entity, Relay.get_actor(), false)
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -206,10 +207,15 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier do
|
|||
) do
|
||||
with {:ok, %Actor{} = followed} <- ActivityPubActor.get_or_fetch_actor_by_url(followed, true),
|
||||
{:ok, %Actor{} = follower} <- ActivityPubActor.get_or_fetch_actor_by_url(follower),
|
||||
{:ok, activity, object} <- ActivityPub.follow(follower, followed, id, false) do
|
||||
{:ok, activity, object} <-
|
||||
Actions.Follow.follow(follower, followed, id, false) do
|
||||
{:ok, activity, object}
|
||||
else
|
||||
e ->
|
||||
{:error, :person_no_follow} ->
|
||||
Logger.warn("Only group and instances can be followed")
|
||||
:error
|
||||
|
||||
{:error, e} ->
|
||||
Logger.warn("Unable to handle Follow activity #{inspect(e)}")
|
||||
:error
|
||||
end
|
||||
|
@ -228,7 +234,9 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier do
|
|||
object_data when is_map(object_data) <-
|
||||
object |> Converter.TodoList.as_to_model_data(),
|
||||
{:ok, %Activity{} = activity, %TodoList{} = todo_list} <-
|
||||
ActivityPub.create(:todo_list, object_data, false, %{"actor" => actor_url}) do
|
||||
Actions.Create.create(:todo_list, object_data, false, %{
|
||||
"actor" => actor_url
|
||||
}) do
|
||||
{:ok, activity, todo_list}
|
||||
else
|
||||
{:error, :group_not_found} -> :error
|
||||
|
@ -247,7 +255,7 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier do
|
|||
object_data <-
|
||||
object |> Converter.Todo.as_to_model_data(),
|
||||
{:ok, %Activity{} = activity, %Todo{} = todo} <-
|
||||
ActivityPub.create(:todo, object_data, false) do
|
||||
Actions.Create.create(:todo, object_data, false) do
|
||||
{:ok, activity, todo}
|
||||
else
|
||||
{:existing_todo, %Todo{} = todo} -> {:ok, nil, todo}
|
||||
|
@ -272,7 +280,7 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier do
|
|||
{:member, true} <-
|
||||
{:member, Actors.is_member?(object_data.creator_id, object_data.actor_id)},
|
||||
{:ok, %Activity{} = activity, %Resource{} = resource} <-
|
||||
ActivityPub.create(:resource, object_data, false) do
|
||||
Actions.Create.create(:resource, object_data, false) do
|
||||
{:ok, activity, resource}
|
||||
else
|
||||
{:existing_resource, %Resource{} = resource} ->
|
||||
|
@ -383,7 +391,7 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier do
|
|||
object_data <-
|
||||
object |> Converter.Actor.as_to_model_data(),
|
||||
{:ok, %Activity{} = activity, %Actor{} = new_actor} <-
|
||||
ActivityPub.update(old_actor, object_data, false) do
|
||||
Actions.Update.update(old_actor, object_data, false) do
|
||||
{:ok, activity, new_actor}
|
||||
else
|
||||
e ->
|
||||
|
@ -411,7 +419,7 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier do
|
|||
Utils.origin_check?(actor_url, update_data) ||
|
||||
Permission.can_update_group_object?(actor, old_event)},
|
||||
{:ok, %Activity{} = activity, %Event{} = new_event} <-
|
||||
ActivityPub.update(old_event, object_data, false) do
|
||||
Actions.Update.update(old_event, object_data, false) do
|
||||
{:ok, activity, new_event}
|
||||
else
|
||||
_e ->
|
||||
|
@ -433,7 +441,7 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier do
|
|||
{:ok, old_entity} <- object |> Utils.get_url() |> ActivityPub.fetch_object_from_url(),
|
||||
object_data <- transform_object_data_for_discussion(object_data),
|
||||
{:ok, %Activity{} = activity, new_entity} <-
|
||||
ActivityPub.update(old_entity, object_data, false) do
|
||||
Actions.Update.update(old_entity, object_data, false) do
|
||||
{:ok, activity, new_entity}
|
||||
else
|
||||
_e ->
|
||||
|
@ -456,7 +464,7 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier do
|
|||
Utils.origin_check?(actor_url, update_data["object"]) ||
|
||||
Permission.can_update_group_object?(actor, old_post)},
|
||||
{:ok, %Activity{} = activity, %Post{} = new_post} <-
|
||||
ActivityPub.update(old_post, object_data, false) do
|
||||
Actions.Update.update(old_post, object_data, false) do
|
||||
{:ok, activity, new_post}
|
||||
else
|
||||
{:origin_check, _} ->
|
||||
|
@ -484,7 +492,7 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier do
|
|||
Utils.origin_check?(actor_url, update_data) ||
|
||||
Permission.can_update_group_object?(actor, old_resource)},
|
||||
{:ok, %Activity{} = activity, %Resource{} = new_resource} <-
|
||||
ActivityPub.update(old_resource, object_data, false) do
|
||||
Actions.Update.update(old_resource, object_data, false) do
|
||||
{:ok, activity, new_resource}
|
||||
else
|
||||
_e ->
|
||||
|
@ -505,7 +513,7 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier do
|
|||
object_data <- Converter.Member.as_to_model_data(object),
|
||||
{:ok, old_entity} <- object |> Utils.get_url() |> ActivityPub.fetch_object_from_url(),
|
||||
{:ok, %Activity{} = activity, new_entity} <-
|
||||
ActivityPub.update(old_entity, object_data, false, %{moderator: actor}) do
|
||||
Actions.Update.update(old_entity, object_data, false, %{moderator: actor}) do
|
||||
{:ok, activity, new_entity}
|
||||
else
|
||||
_e ->
|
||||
|
@ -522,7 +530,7 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier do
|
|||
|
||||
with object_url <- Utils.get_url(object),
|
||||
{:ok, entity} <- ActivityPub.fetch_object_from_url(object_url) do
|
||||
ActivityPub.delete(entity, Relay.get_actor(), false)
|
||||
Actions.Delete.delete(entity, Relay.get_actor(), false)
|
||||
else
|
||||
{:ok, %Tombstone{} = tombstone} ->
|
||||
{:ok, nil, tombstone}
|
||||
|
@ -545,7 +553,13 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier do
|
|||
{:ok, %Actor{} = actor} <- ActivityPubActor.get_or_fetch_actor_by_url(actor),
|
||||
{:ok, object} <- fetch_obj_helper_as_activity_streams(object_id),
|
||||
{:ok, activity, object} <-
|
||||
ActivityPub.unannounce(actor, object, id, cancelled_activity_id, false) do
|
||||
Actions.Announce.unannounce(
|
||||
actor,
|
||||
object,
|
||||
id,
|
||||
cancelled_activity_id,
|
||||
false
|
||||
) do
|
||||
{:ok, activity, object}
|
||||
else
|
||||
_e -> :error
|
||||
|
@ -563,7 +577,8 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier do
|
|||
with {:ok, %Actor{domain: nil} = followed} <-
|
||||
ActivityPubActor.get_or_fetch_actor_by_url(followed),
|
||||
{:ok, %Actor{} = follower} <- ActivityPubActor.get_or_fetch_actor_by_url(follower),
|
||||
{:ok, activity, object} <- ActivityPub.unfollow(follower, followed, id, false) do
|
||||
{:ok, activity, object} <-
|
||||
Actions.Follow.unfollow(follower, followed, id, false) do
|
||||
{:ok, activity, object}
|
||||
else
|
||||
e ->
|
||||
|
@ -577,6 +592,8 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier do
|
|||
def handle_incoming(
|
||||
%{"type" => "Delete", "object" => object, "actor" => _actor, "id" => _id} = data
|
||||
) do
|
||||
Logger.info("Handle incoming to delete an object")
|
||||
|
||||
with actor_url <- Utils.get_actor(data),
|
||||
{:actor, {:ok, %Actor{} = actor}} <-
|
||||
{:actor, ActivityPubActor.get_or_fetch_actor_by_url(actor_url)},
|
||||
|
@ -586,14 +603,14 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier do
|
|||
{:origin_check,
|
||||
Utils.origin_check_from_id?(actor_url, object_id) ||
|
||||
Permission.can_delete_group_object?(actor, object)},
|
||||
{:ok, activity, object} <- ActivityPub.delete(object, actor, false) do
|
||||
{:ok, activity, object} <- Actions.Delete.delete(object, actor, false) do
|
||||
{:ok, activity, object}
|
||||
else
|
||||
{:origin_check, false} ->
|
||||
Logger.warn("Object origin check failed")
|
||||
:error
|
||||
|
||||
{:actor, {:error, "Could not fetch by AP id"}} ->
|
||||
{:actor, {:error, _err}} ->
|
||||
{:error, :unknown_actor}
|
||||
|
||||
{:error, e} ->
|
||||
|
@ -630,7 +647,8 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier do
|
|||
{:origin_check,
|
||||
Utils.origin_check?(actor_url, data) ||
|
||||
Permission.can_update_group_object?(actor, old_resource)},
|
||||
{:ok, activity, new_resource} <- ActivityPub.move(:resource, old_resource, object_data) do
|
||||
{:ok, activity, new_resource} <-
|
||||
Actions.Move.move(:resource, old_resource, object_data) do
|
||||
{:ok, activity, new_resource}
|
||||
else
|
||||
e ->
|
||||
|
@ -658,7 +676,7 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier do
|
|||
object <- Utils.get_url(object),
|
||||
{:ok, object} <- ActivityPub.fetch_object_from_url(object),
|
||||
{:ok, activity, object} <-
|
||||
ActivityPub.join(object, actor, false, %{
|
||||
Actions.Join.join(object, actor, false, %{
|
||||
url: id,
|
||||
metadata: %{message: Map.get(data, "participationMessage")}
|
||||
}) do
|
||||
|
@ -675,7 +693,7 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier do
|
|||
{:ok, %Actor{} = actor} <- ActivityPubActor.get_or_fetch_actor_by_url(actor),
|
||||
object <- Utils.get_url(object),
|
||||
{:ok, object} <- ActivityPub.fetch_object_from_url(object),
|
||||
{:ok, activity, object} <- ActivityPub.leave(object, actor, false) do
|
||||
{:ok, activity, object} <- Actions.Leave.leave(object, actor, false) do
|
||||
{:ok, activity, object}
|
||||
else
|
||||
{:only_organizer, true} ->
|
||||
|
@ -707,7 +725,7 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier do
|
|||
{:ok, %Actor{} = target} <-
|
||||
target |> Utils.get_url() |> ActivityPubActor.get_or_fetch_actor_by_url(),
|
||||
{:ok, activity, %Member{} = member} <-
|
||||
ActivityPub.invite(object, actor, target, false, %{url: id}) do
|
||||
Actions.Invite.invite(object, actor, target, false, %{url: id}) do
|
||||
{:ok, activity, member}
|
||||
end
|
||||
end
|
||||
|
@ -727,7 +745,7 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier do
|
|||
{:is_admin, Actors.get_member(moderator_id, group_id)},
|
||||
{:is_member, {:ok, %Member{role: role} = member}} when role != :rejected <-
|
||||
{:is_member, Actors.get_member(person_id, group_id)} do
|
||||
ActivityPub.remove(member, group, moderator, false)
|
||||
Actions.Remove.remove(member, group, moderator, false)
|
||||
else
|
||||
{:is_admin, {:ok, %Member{}}} ->
|
||||
Logger.warn(
|
||||
|
@ -779,7 +797,7 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier do
|
|||
{:follow, get_follow(follow_object)},
|
||||
{:same_actor, true} <- {:same_actor, actor.id == followed.id},
|
||||
{:ok, %Activity{} = activity, %Follower{approved: true} = follow} <-
|
||||
ActivityPub.accept(
|
||||
Actions.Accept.accept(
|
||||
:follow,
|
||||
follow,
|
||||
false
|
||||
|
@ -817,7 +835,7 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier do
|
|||
{:follow, get_follow(follow_object)},
|
||||
{:same_actor, true} <- {:same_actor, actor.id == followed.id},
|
||||
{:ok, activity, _} <-
|
||||
ActivityPub.reject(:follow, follow) do
|
||||
Actions.Reject.reject(:follow, follow) do
|
||||
{:ok, activity, follow}
|
||||
else
|
||||
{:follow, _err} ->
|
||||
|
@ -872,7 +890,7 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier do
|
|||
{:can_accept_event_join, true} <-
|
||||
{:can_accept_event_join, can_manage_event?(actor_accepting, event)},
|
||||
{:ok, %Activity{} = activity, %Participant{role: :participant} = participant} <-
|
||||
ActivityPub.accept(
|
||||
Actions.Accept.accept(
|
||||
:join,
|
||||
participant,
|
||||
false
|
||||
|
@ -904,7 +922,7 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier do
|
|||
when role in [:not_approved, :rejected, :invited] and type in [:join, :invite] do
|
||||
# Or maybe for groups it's the group that sends the Accept activity
|
||||
with {:ok, %Activity{} = activity, %Member{role: :member} = member} <-
|
||||
ActivityPub.accept(
|
||||
Actions.Accept.accept(
|
||||
type,
|
||||
member,
|
||||
false
|
||||
|
@ -922,7 +940,7 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier do
|
|||
{:can_accept_event_reject, true} <-
|
||||
{:can_accept_event_reject, can_manage_event?(actor_accepting, event)},
|
||||
{:ok, activity, participant} <-
|
||||
ActivityPub.reject(:join, participant, false),
|
||||
Actions.Reject.reject(:join, participant, false),
|
||||
:ok <- Participation.send_emails_to_local_user(participant) do
|
||||
{:ok, activity, participant}
|
||||
else
|
||||
|
@ -951,9 +969,9 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier do
|
|||
defp do_handle_incoming_reject_invite(invite_object, %Actor{} = actor_rejecting) do
|
||||
with {:invite, {:ok, %Member{role: :invited, actor_id: actor_id} = member}} <-
|
||||
{:invite, get_member(invite_object)},
|
||||
{:same_actor, true} <- {:same_actor, actor_rejecting.id === actor_id},
|
||||
{:same_actor, true} <- {:same_actor, actor_rejecting.id == actor_id},
|
||||
{:ok, activity, member} <-
|
||||
ActivityPub.reject(:invite, member, false) do
|
||||
Actions.Reject.reject(:invite, member, false) do
|
||||
{:ok, activity, member}
|
||||
end
|
||||
end
|
||||
|
@ -992,7 +1010,6 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier do
|
|||
end
|
||||
|
||||
# Comment initiates a whole discussion only if it has full title
|
||||
@spec is_data_for_comment_or_discussion?(map()) :: boolean()
|
||||
defp is_data_a_discussion_initialization?(object_data) do
|
||||
not Map.has_key?(object_data, :title) or
|
||||
is_nil(object_data.title) or object_data.title == ""
|
||||
|
@ -1106,22 +1123,22 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier do
|
|||
end
|
||||
|
||||
defp is_group_object_gone(object_id) do
|
||||
case ActivityPub.fetch_object_from_url(object_id, force: true) do
|
||||
{:error, error_message, object} when error_message in ["Gone", "Not found"] ->
|
||||
{:ok, object}
|
||||
Logger.debug("is_group_object_gone #{object_id}")
|
||||
|
||||
case ActivityPub.fetch_object_from_url(object_id, force: true) do
|
||||
# comments are just emptied
|
||||
{:ok, %Comment{deleted_at: deleted_at} = object} when not is_nil(deleted_at) ->
|
||||
{:ok, object}
|
||||
|
||||
{:error, :http_gone, object} ->
|
||||
Logger.debug("object is really gone")
|
||||
{:ok, object}
|
||||
|
||||
{:ok, %{url: url} = object} ->
|
||||
if Utils.are_same_origin?(url, Endpoint.url()),
|
||||
do: {:ok, object},
|
||||
else: {:error, "Group object URL remote"}
|
||||
|
||||
{:error, {:error, err}} ->
|
||||
{:error, err}
|
||||
|
||||
{:error, err} ->
|
||||
{:error, err}
|
||||
|
||||
|
@ -1133,7 +1150,7 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier do
|
|||
# Before 1.0.4 the object of a "Remove" activity was an actor's URL
|
||||
# instead of the member's URL.
|
||||
# TODO: Remove in 1.2
|
||||
@spec get_remove_object(map() | String.t()) :: {:ok, String.t() | integer()}
|
||||
@spec get_remove_object(map() | String.t()) :: {:ok, integer()}
|
||||
defp get_remove_object(object) do
|
||||
case object |> Utils.get_url() |> ActivityPub.fetch_object_from_url() do
|
||||
{:ok, %Member{actor: %Actor{id: person_id}}} -> {:ok, person_id}
|
||||
|
@ -1156,7 +1173,7 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier do
|
|||
organizer_actor_id == actor_id
|
||||
end
|
||||
|
||||
defp can_manage_event?(_actor, _event) do
|
||||
defp can_manage_event?(%Actor{} = _actor, %Event{} = _event) do
|
||||
false
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
defmodule Mobilizon.Federation.ActivityPub.Types.Actors do
|
||||
@moduledoc false
|
||||
alias Mobilizon.Actors
|
||||
alias Mobilizon.Actors.{Actor, Follower, Member}
|
||||
alias Mobilizon.Federation.ActivityPub
|
||||
alias Mobilizon.Federation.ActivityPub.{Audience, Permission, Relay}
|
||||
alias Mobilizon.Actors.{Actor, Follower, Member, MemberRole}
|
||||
alias Mobilizon.Federation.ActivityPub.{Actions, Audience, Permission, Relay}
|
||||
alias Mobilizon.Federation.ActivityPub.Types.Entity
|
||||
alias Mobilizon.Federation.ActivityStream
|
||||
alias Mobilizon.Federation.ActivityStream.Convertible
|
||||
alias Mobilizon.GraphQL.API.Utils, as: APIUtils
|
||||
alias Mobilizon.Service.Activity.Group, as: GroupActivity
|
||||
|
@ -17,46 +17,57 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Actors do
|
|||
@behaviour Entity
|
||||
|
||||
@impl Entity
|
||||
@spec create(map(), map()) :: {:ok, map()}
|
||||
@spec create(map(), map()) ::
|
||||
{:ok, Actor.t(), ActivityStream.t()} | {:error, Ecto.Changeset.t()}
|
||||
def create(args, additional) do
|
||||
with args <- prepare_args_for_actor(args),
|
||||
{:ok, %Actor{} = actor} <- Actors.create_actor(args),
|
||||
{:ok, _} <-
|
||||
args = prepare_args_for_actor(args)
|
||||
|
||||
case Actors.create_actor(args) do
|
||||
{:ok, %Actor{} = actor} ->
|
||||
GroupActivity.insert_activity(actor,
|
||||
subject: "group_created",
|
||||
actor_id: args.creator_actor_id
|
||||
),
|
||||
actor_as_data <- Convertible.model_to_as(actor),
|
||||
audience <- %{"to" => ["https://www.w3.org/ns/activitystreams#Public"], "cc" => []},
|
||||
create_data <-
|
||||
make_create_data(actor_as_data, Map.merge(audience, additional)) do
|
||||
)
|
||||
|
||||
actor_as_data = Convertible.model_to_as(actor)
|
||||
audience = %{"to" => ["https://www.w3.org/ns/activitystreams#Public"], "cc" => []}
|
||||
create_data = make_create_data(actor_as_data, Map.merge(audience, additional))
|
||||
{:ok, actor, create_data}
|
||||
|
||||
{:error, %Ecto.Changeset{} = err} ->
|
||||
{:error, err}
|
||||
end
|
||||
end
|
||||
|
||||
@impl Entity
|
||||
@spec update(Actor.t(), map, map) :: {:ok, Actor.t(), Activity.t()} | any
|
||||
@spec update(Actor.t(), map, map) ::
|
||||
{:ok, Actor.t(), ActivityStream.t()} | {:error, Ecto.Changeset.t()}
|
||||
def update(%Actor{} = old_actor, args, additional) do
|
||||
with {:ok, %Actor{} = new_actor} <- Actors.update_actor(old_actor, args),
|
||||
{:ok, _} <-
|
||||
case Actors.update_actor(old_actor, args) do
|
||||
{:ok, %Actor{} = new_actor} ->
|
||||
GroupActivity.insert_activity(new_actor,
|
||||
subject: "group_updated",
|
||||
old_group: old_actor,
|
||||
updater_actor: Map.get(args, :updater_actor)
|
||||
),
|
||||
actor_as_data <- Convertible.model_to_as(new_actor),
|
||||
{:ok, true} <- Cachex.del(:activity_pub, "actor_#{new_actor.preferred_username}"),
|
||||
audience <-
|
||||
Audience.get_audience(new_actor),
|
||||
additional <- Map.merge(additional, %{"actor" => old_actor.url}),
|
||||
update_data <- make_update_data(actor_as_data, Map.merge(audience, additional)) do
|
||||
)
|
||||
|
||||
actor_as_data = Convertible.model_to_as(new_actor)
|
||||
Cachex.del(:activity_pub, "actor_#{new_actor.preferred_username}")
|
||||
audience = Audience.get_audience(new_actor)
|
||||
additional = Map.merge(additional, %{"actor" => old_actor.url})
|
||||
update_data = make_update_data(actor_as_data, Map.merge(audience, additional))
|
||||
{:ok, new_actor, update_data}
|
||||
|
||||
{:error, %Ecto.Changeset{} = err} ->
|
||||
{:error, err}
|
||||
end
|
||||
end
|
||||
|
||||
@public_ap "https://www.w3.org/ns/activitystreams#Public"
|
||||
|
||||
@impl Entity
|
||||
@spec delete(Actor.t(), Actor.t(), boolean, map) ::
|
||||
{:ok, ActivityStream.t(), Actor.t(), Actor.t()} | {:error, Ecto.Changeset.t()}
|
||||
def delete(
|
||||
%Actor{
|
||||
followers_url: followers_url,
|
||||
|
@ -89,21 +100,27 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Actors do
|
|||
|
||||
suspension = Map.get(additionnal, :suspension, false)
|
||||
|
||||
with {:ok, %Oban.Job{}} <-
|
||||
Actors.delete_actor(target_actor,
|
||||
case Actors.delete_actor(target_actor,
|
||||
# We completely delete the actor if the actor is remote
|
||||
reserve_username: is_nil(domain),
|
||||
suspension: suspension,
|
||||
author_id: author_id
|
||||
) do
|
||||
{:ok, %Oban.Job{}} ->
|
||||
{:ok, activity_data, actor, target_actor}
|
||||
|
||||
{:error, %Ecto.Changeset{} = err} ->
|
||||
{:error, err}
|
||||
end
|
||||
end
|
||||
|
||||
@spec actor(Actor.t()) :: Actor.t() | nil
|
||||
def actor(%Actor{} = actor), do: actor
|
||||
|
||||
@spec group_actor(Actor.t()) :: Actor.t() | nil
|
||||
def group_actor(%Actor{} = actor), do: actor
|
||||
|
||||
@spec permissions(Actor.t()) :: Permission.t()
|
||||
def permissions(%Actor{} = _group) do
|
||||
%Permission{
|
||||
access: :member,
|
||||
|
@ -113,14 +130,14 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Actors do
|
|||
}
|
||||
end
|
||||
|
||||
@spec join(Actor.t(), Actor.t(), boolean(), map()) :: {:ok, map(), Member.t()}
|
||||
@spec join(Actor.t(), Actor.t(), boolean(), map()) :: {:ok, ActivityStreams.t(), Member.t()}
|
||||
def join(%Actor{type: :Group} = group, %Actor{} = actor, _local, additional) do
|
||||
with role <-
|
||||
role =
|
||||
additional
|
||||
|> Map.get(:metadata, %{})
|
||||
|> Map.get(:role, Mobilizon.Actors.get_default_member_role(group)),
|
||||
{:ok, %Member{} = member} <-
|
||||
Mobilizon.Actors.create_member(%{
|
||||
|> Map.get(:role, Mobilizon.Actors.get_default_member_role(group))
|
||||
|
||||
case Mobilizon.Actors.create_member(%{
|
||||
role: role,
|
||||
parent_id: group.id,
|
||||
actor_id: actor.id,
|
||||
|
@ -129,20 +146,23 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Actors do
|
|||
additional
|
||||
|> Map.get(:metadata, %{})
|
||||
|> Map.update(:message, nil, &String.trim(HTML.strip_tags(&1)))
|
||||
}),
|
||||
{:ok, _} <-
|
||||
Mobilizon.Service.Activity.Member.insert_activity(member, subject: "member_joined"),
|
||||
}) do
|
||||
{:ok, %Member{} = member} ->
|
||||
Mobilizon.Service.Activity.Member.insert_activity(member, subject: "member_joined")
|
||||
|
||||
Absinthe.Subscription.publish(Endpoint, actor,
|
||||
group_membership_changed: [Actor.preferred_username_and_domain(group), actor.id]
|
||||
),
|
||||
join_data <- %{
|
||||
)
|
||||
|
||||
join_data = %{
|
||||
"type" => "Join",
|
||||
"id" => member.url,
|
||||
"actor" => actor.url,
|
||||
"object" => group.url
|
||||
},
|
||||
audience <-
|
||||
Audience.get_audience(member) do
|
||||
}
|
||||
|
||||
audience = Audience.get_audience(member)
|
||||
|
||||
approve_if_default_role_is_member(
|
||||
group,
|
||||
actor,
|
||||
|
@ -150,21 +170,34 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Actors do
|
|||
member,
|
||||
role
|
||||
)
|
||||
|
||||
{:error, %Ecto.Changeset{} = err} ->
|
||||
{:error, err}
|
||||
end
|
||||
end
|
||||
|
||||
@spec follow(Actor.t(), Actor.t(), boolean, map) ::
|
||||
{:accept, any}
|
||||
| {:ok, ActivityStreams.t(), Follower.t()}
|
||||
| {:error,
|
||||
:person_no_follow | :already_following | :followed_suspended | Ecto.Changeset.t()}
|
||||
def follow(%Actor{} = follower_actor, %Actor{type: type} = followed, _local, additional)
|
||||
when type != :Person do
|
||||
with {:ok, %Follower{} = follower} <-
|
||||
Mobilizon.Actors.follow(followed, follower_actor, additional["activity_id"], false),
|
||||
:ok <- FollowMailer.send_notification_to_admins(follower),
|
||||
follower_as_data <- Convertible.model_to_as(follower) do
|
||||
case Mobilizon.Actors.follow(followed, follower_actor, additional["activity_id"], false) do
|
||||
{:ok, %Follower{} = follower} ->
|
||||
FollowMailer.send_notification_to_admins(follower)
|
||||
follower_as_data = Convertible.model_to_as(follower)
|
||||
approve_if_manually_approves_followers(follower, follower_as_data)
|
||||
|
||||
{:error, error} ->
|
||||
{:error, error}
|
||||
end
|
||||
end
|
||||
|
||||
def follow(_, _, _, _), do: {:error, :no_person, "Only group and instances can be followed"}
|
||||
# "Only group and instances can be followed"
|
||||
def follow(_, _, _, _), do: {:error, :person_no_follow}
|
||||
|
||||
@spec prepare_args_for_actor(map) :: map
|
||||
defp prepare_args_for_actor(args) do
|
||||
args
|
||||
|> maybe_sanitize_username()
|
||||
|
@ -191,8 +224,14 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Actors do
|
|||
defp maybe_sanitize_summary(args), do: args
|
||||
|
||||
# Set the participant to approved if the default role for new participants is :participant
|
||||
@spec approve_if_default_role_is_member(Actor.t(), Actor.t(), map(), Member.t(), atom()) ::
|
||||
{:ok, map(), Member.t()}
|
||||
@spec approve_if_default_role_is_member(
|
||||
Actor.t(),
|
||||
Actor.t(),
|
||||
ActivityStreams.t(),
|
||||
Member.t(),
|
||||
MemberRole.t()
|
||||
) ::
|
||||
{:ok, ActivityStreams.t(), Member.t()}
|
||||
defp approve_if_default_role_is_member(
|
||||
%Actor{type: :Group} = group,
|
||||
%Actor{} = actor,
|
||||
|
@ -202,17 +241,17 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Actors do
|
|||
) do
|
||||
if is_nil(group.domain) && !is_nil(actor.domain) do
|
||||
cond do
|
||||
Mobilizon.Actors.get_default_member_role(group) === :member &&
|
||||
Mobilizon.Actors.get_default_member_role(group) == :member &&
|
||||
role == :member ->
|
||||
{:accept,
|
||||
ActivityPub.accept(
|
||||
Actions.Accept.accept(
|
||||
:join,
|
||||
member,
|
||||
true,
|
||||
%{"actor" => group.url}
|
||||
)}
|
||||
|
||||
Mobilizon.Actors.get_default_member_role(group) === :not_approved &&
|
||||
Mobilizon.Actors.get_default_member_role(group) == :not_approved &&
|
||||
role == :not_approved ->
|
||||
Scheduler.pending_membership_notification(group)
|
||||
{:ok, activity_data, member}
|
||||
|
@ -225,6 +264,11 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Actors do
|
|||
end
|
||||
end
|
||||
|
||||
@spec approve_if_manually_approves_followers(
|
||||
follower :: Follower.t(),
|
||||
follow_as_data :: ActivityStreams.t()
|
||||
) ::
|
||||
{:accept, any} | {:ok, ActivityStreams.t(), Follower.t()}
|
||||
defp approve_if_manually_approves_followers(
|
||||
%Follower{} = follower,
|
||||
follow_as_data
|
||||
|
@ -237,7 +281,7 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Actors do
|
|||
Logger.debug("Target doesn't manually approves followers, we can accept right away")
|
||||
|
||||
{:accept,
|
||||
ActivityPub.accept(
|
||||
Actions.Accept.accept(
|
||||
:follow,
|
||||
follower,
|
||||
true,
|
||||
|
|
|
@ -6,6 +6,7 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Comments do
|
|||
alias Mobilizon.Events.{Event, EventOptions}
|
||||
alias Mobilizon.Federation.ActivityPub.{Audience, Permission}
|
||||
alias Mobilizon.Federation.ActivityPub.Types.Entity
|
||||
alias Mobilizon.Federation.ActivityStream
|
||||
alias Mobilizon.Federation.ActivityStream.Converter.Utils, as: ConverterUtils
|
||||
alias Mobilizon.Federation.ActivityStream.Convertible
|
||||
alias Mobilizon.GraphQL.API.Utils, as: APIUtils
|
||||
|
@ -20,47 +21,56 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Comments do
|
|||
@behaviour Entity
|
||||
|
||||
@impl Entity
|
||||
@spec create(map(), map()) :: {:ok, map()}
|
||||
@spec create(map(), map()) ::
|
||||
{:ok, Comment.t(), ActivityStream.t()}
|
||||
| {:error, Ecto.Changeset.t()}
|
||||
| {:error, :event_not_allow_commenting}
|
||||
def create(args, additional) do
|
||||
with args <- prepare_args_for_comment(args),
|
||||
:ok <- make_sure_event_allows_commenting(args),
|
||||
{:ok, %Comment{discussion_id: discussion_id} = comment} <-
|
||||
Discussions.create_comment(args),
|
||||
{:ok, _} <-
|
||||
args = prepare_args_for_comment(args)
|
||||
|
||||
if event_allows_commenting?(args) do
|
||||
case Discussions.create_comment(args) do
|
||||
{:ok, %Comment{discussion_id: discussion_id} = comment} ->
|
||||
CommentActivity.insert_activity(comment,
|
||||
subject: "comment_posted"
|
||||
),
|
||||
:ok <- maybe_publish_graphql_subscription(discussion_id),
|
||||
comment_as_data <- Convertible.model_to_as(comment),
|
||||
audience <-
|
||||
Audience.get_audience(comment),
|
||||
create_data <-
|
||||
make_create_data(comment_as_data, Map.merge(audience, additional)) do
|
||||
)
|
||||
|
||||
maybe_publish_graphql_subscription(discussion_id)
|
||||
comment_as_data = Convertible.model_to_as(comment)
|
||||
audience = Audience.get_audience(comment)
|
||||
create_data = make_create_data(comment_as_data, Map.merge(audience, additional))
|
||||
{:ok, comment, create_data}
|
||||
end
|
||||
end
|
||||
|
||||
@impl Entity
|
||||
@spec update(Comment.t(), map(), map()) :: {:ok, Comment.t(), Activity.t()} | any()
|
||||
def update(%Comment{} = old_comment, args, additional) do
|
||||
with args <- prepare_args_for_comment_update(args),
|
||||
{:ok, %Comment{} = new_comment} <- Discussions.update_comment(old_comment, args),
|
||||
{:ok, true} <- Cachex.del(:activity_pub, "comment_#{new_comment.uuid}"),
|
||||
comment_as_data <- Convertible.model_to_as(new_comment),
|
||||
audience <-
|
||||
Audience.get_audience(new_comment),
|
||||
update_data <- make_update_data(comment_as_data, Map.merge(audience, additional)) do
|
||||
{:ok, new_comment, update_data}
|
||||
{:error, %Ecto.Changeset{} = err} ->
|
||||
{:error, err}
|
||||
end
|
||||
else
|
||||
err ->
|
||||
Logger.error("Something went wrong while creating an update activity")
|
||||
Logger.debug(inspect(err))
|
||||
err
|
||||
{:error, :event_not_allow_commenting}
|
||||
end
|
||||
end
|
||||
|
||||
@impl Entity
|
||||
@spec delete(Comment.t(), Actor.t(), boolean, map()) :: {:ok, Comment.t()}
|
||||
@spec update(Comment.t(), map(), map()) ::
|
||||
{:ok, Comment.t(), ActivityStream.t()} | {:error, Ecto.Changeset.t()}
|
||||
def update(%Comment{} = old_comment, args, additional) do
|
||||
args = prepare_args_for_comment_update(args)
|
||||
|
||||
case Discussions.update_comment(old_comment, args) do
|
||||
{:ok, %Comment{} = new_comment} ->
|
||||
{:ok, true} = Cachex.del(:activity_pub, "comment_#{new_comment.uuid}")
|
||||
comment_as_data = Convertible.model_to_as(new_comment)
|
||||
audience = Audience.get_audience(new_comment)
|
||||
update_data = make_update_data(comment_as_data, Map.merge(audience, additional))
|
||||
{:ok, new_comment, update_data}
|
||||
|
||||
{:error, %Ecto.Changeset{} = err} ->
|
||||
{:error, err}
|
||||
end
|
||||
end
|
||||
|
||||
@impl Entity
|
||||
@spec delete(Comment.t(), Actor.t(), boolean, map()) ::
|
||||
{:error, Ecto.Changeset.t()} | {:ok, ActivityStream.t(), Actor.t(), Comment.t()}
|
||||
def delete(
|
||||
%Comment{url: url, id: comment_id},
|
||||
%Actor{} = actor,
|
||||
|
@ -79,18 +89,21 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Comments do
|
|||
|
||||
force_deletion = Map.get(options, :force, false)
|
||||
|
||||
with audience <-
|
||||
Audience.get_audience(comment),
|
||||
{:ok, %Comment{} = updated_comment} <-
|
||||
Discussions.delete_comment(comment, force: force_deletion),
|
||||
{:ok, true} <- Cachex.del(:activity_pub, "comment_#{comment.uuid}"),
|
||||
{:ok, %Tombstone{} = _tombstone} <-
|
||||
Tombstone.create_tombstone(%{uri: comment.url, actor_id: actor.id}) do
|
||||
audience = Audience.get_audience(comment)
|
||||
|
||||
case Discussions.delete_comment(comment, force: force_deletion) do
|
||||
{:ok, %Comment{} = updated_comment} ->
|
||||
Cachex.del(:activity_pub, "comment_#{comment.uuid}")
|
||||
Tombstone.create_tombstone(%{uri: comment.url, actor_id: actor.id})
|
||||
Share.delete_all_by_uri(comment.url)
|
||||
{:ok, Map.merge(activity_data, audience), actor, updated_comment}
|
||||
|
||||
{:error, %Ecto.Changeset{} = err} ->
|
||||
{:error, err}
|
||||
end
|
||||
end
|
||||
|
||||
@spec actor(Comment.t()) :: Actor.t() | nil
|
||||
def actor(%Comment{actor: %Actor{} = actor}), do: actor
|
||||
|
||||
def actor(%Comment{actor_id: actor_id}) when not is_nil(actor_id),
|
||||
|
@ -98,6 +111,7 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Comments do
|
|||
|
||||
def actor(_), do: nil
|
||||
|
||||
@spec group_actor(Comment.t()) :: Actor.t() | nil
|
||||
def group_actor(%Comment{attributed_to: %Actor{} = group}), do: group
|
||||
|
||||
def group_actor(%Comment{attributed_to_id: attributed_to_id}) when not is_nil(attributed_to_id),
|
||||
|
@ -105,6 +119,7 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Comments do
|
|||
|
||||
def group_actor(_), do: nil
|
||||
|
||||
@spec permissions(Comment.t()) :: Permission.t()
|
||||
def permissions(%Comment{}),
|
||||
do: %Permission{
|
||||
access: :member,
|
||||
|
@ -114,6 +129,7 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Comments do
|
|||
}
|
||||
|
||||
# Prepare and sanitize arguments for comments
|
||||
@spec prepare_args_for_comment(map) :: map
|
||||
defp prepare_args_for_comment(args) do
|
||||
with in_reply_to_comment <-
|
||||
args |> Map.get(:in_reply_to_comment_id) |> Discussions.get_comment_with_preload(),
|
||||
|
@ -150,6 +166,7 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Comments do
|
|||
end
|
||||
end
|
||||
|
||||
@spec prepare_args_for_comment_update(map) :: map
|
||||
defp prepare_args_for_comment_update(args) do
|
||||
with {text, mentions, tags} <-
|
||||
APIUtils.make_content_html(
|
||||
|
@ -174,32 +191,39 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Comments do
|
|||
|
||||
defp handle_event_for_comment(nil), do: nil
|
||||
|
||||
@spec maybe_publish_graphql_subscription(String.t() | integer() | nil) :: :ok
|
||||
defp maybe_publish_graphql_subscription(nil), do: :ok
|
||||
|
||||
defp maybe_publish_graphql_subscription(discussion_id) do
|
||||
with %Discussion{} = discussion <- Discussions.get_discussion(discussion_id) do
|
||||
case Discussions.get_discussion(discussion_id) do
|
||||
%Discussion{} = discussion ->
|
||||
Absinthe.Subscription.publish(Endpoint, discussion,
|
||||
discussion_comment_changed: discussion.slug
|
||||
)
|
||||
|
||||
:ok
|
||||
|
||||
nil ->
|
||||
:ok
|
||||
end
|
||||
end
|
||||
|
||||
defp make_sure_event_allows_commenting(%{
|
||||
@spec event_allows_commenting?(%{
|
||||
required(:actor_id) => String.t() | integer,
|
||||
required(:event) => Event.t() | nil,
|
||||
optional(atom) => any()
|
||||
}) :: boolean
|
||||
defp event_allows_commenting?(%{
|
||||
actor_id: actor_id,
|
||||
event: %Event{
|
||||
options: %EventOptions{comment_moderation: comment_moderation},
|
||||
organizer_actor_id: organizer_actor_id
|
||||
}
|
||||
}) do
|
||||
if comment_moderation != :closed ||
|
||||
to_string(actor_id) == to_string(organizer_actor_id) do
|
||||
:ok
|
||||
else
|
||||
{:error, :event_comments_are_closed}
|
||||
end
|
||||
comment_moderation != :closed ||
|
||||
to_string(actor_id) == to_string(organizer_actor_id)
|
||||
end
|
||||
|
||||
defp make_sure_event_allows_commenting(_), do: :ok
|
||||
# Comments not attached to events
|
||||
defp event_allows_commenting?(_), do: true
|
||||
end
|
||||
|
|
|
@ -6,6 +6,7 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Discussions do
|
|||
alias Mobilizon.Discussions.{Comment, Discussion}
|
||||
alias Mobilizon.Federation.ActivityPub.{Audience, Permission}
|
||||
alias Mobilizon.Federation.ActivityPub.Types.Entity
|
||||
alias Mobilizon.Federation.ActivityStream
|
||||
alias Mobilizon.Federation.ActivityStream.Convertible
|
||||
alias Mobilizon.GraphQL.API.Utils, as: APIUtils
|
||||
alias Mobilizon.Service.Activity.Discussion, as: DiscussionActivity
|
||||
|
@ -16,83 +17,100 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Discussions do
|
|||
@behaviour Entity
|
||||
|
||||
@impl Entity
|
||||
@spec create(map(), map()) :: {:ok, map()}
|
||||
@spec create(map(), map()) ::
|
||||
{:ok, Discussion.t(), ActivityStream.t()}
|
||||
| {:error, :discussion_not_found | :last_comment_not_found | Ecto.Changeset.t()}
|
||||
def create(%{discussion_id: discussion_id} = args, additional) when not is_nil(discussion_id) do
|
||||
with args <- prepare_args(args),
|
||||
%Discussion{} = discussion <- Discussions.get_discussion(discussion_id),
|
||||
{:ok, %Discussion{last_comment_id: last_comment_id} = discussion} <-
|
||||
Discussions.reply_to_discussion(discussion, args),
|
||||
{:ok, _} <-
|
||||
args = prepare_args(args)
|
||||
|
||||
case Discussions.get_discussion(discussion_id) do
|
||||
%Discussion{} = discussion ->
|
||||
case Discussions.reply_to_discussion(discussion, args) do
|
||||
{:ok, %Discussion{last_comment_id: last_comment_id} = discussion} ->
|
||||
DiscussionActivity.insert_activity(discussion,
|
||||
subject: "discussion_replied",
|
||||
actor_id: Map.get(args, :creator_id, args.actor_id)
|
||||
),
|
||||
%Comment{} = last_comment <- Discussions.get_comment_with_preload(last_comment_id),
|
||||
:ok <- maybe_publish_graphql_subscription(discussion),
|
||||
comment_as_data <- Convertible.model_to_as(last_comment),
|
||||
audience <-
|
||||
Audience.get_audience(discussion),
|
||||
create_data <-
|
||||
make_create_data(comment_as_data, Map.merge(audience, additional)) do
|
||||
)
|
||||
|
||||
case Discussions.get_comment_with_preload(last_comment_id) do
|
||||
%Comment{} = last_comment ->
|
||||
maybe_publish_graphql_subscription(discussion)
|
||||
comment_as_data = Convertible.model_to_as(last_comment)
|
||||
audience = Audience.get_audience(discussion)
|
||||
create_data = make_create_data(comment_as_data, Map.merge(audience, additional))
|
||||
{:ok, discussion, create_data}
|
||||
|
||||
nil ->
|
||||
{:error, :last_comment_not_found}
|
||||
end
|
||||
|
||||
{:error, _, %Ecto.Changeset{} = err, _} ->
|
||||
{:error, err}
|
||||
end
|
||||
|
||||
nil ->
|
||||
{:error, :discussion_not_found}
|
||||
end
|
||||
end
|
||||
|
||||
@impl Entity
|
||||
@spec create(map(), map()) :: {:ok, map()}
|
||||
def create(args, additional) do
|
||||
with args <- prepare_args(args),
|
||||
{:ok, %Discussion{} = discussion} <-
|
||||
Discussions.create_discussion(args),
|
||||
{:ok, _} <-
|
||||
DiscussionActivity.insert_activity(discussion, subject: "discussion_created"),
|
||||
discussion_as_data <- Convertible.model_to_as(discussion),
|
||||
audience <-
|
||||
Audience.get_audience(discussion),
|
||||
create_data <-
|
||||
make_create_data(discussion_as_data, Map.merge(audience, additional)) do
|
||||
args = prepare_args(args)
|
||||
|
||||
case Discussions.create_discussion(args) do
|
||||
{:ok, %Discussion{} = discussion} ->
|
||||
DiscussionActivity.insert_activity(discussion, subject: "discussion_created")
|
||||
discussion_as_data = Convertible.model_to_as(discussion)
|
||||
audience = Audience.get_audience(discussion)
|
||||
create_data = make_create_data(discussion_as_data, Map.merge(audience, additional))
|
||||
{:ok, discussion, create_data}
|
||||
|
||||
{:error, _, %Ecto.Changeset{} = err, _} ->
|
||||
{:error, err}
|
||||
end
|
||||
end
|
||||
|
||||
@impl Entity
|
||||
@spec update(Discussion.t(), map(), map()) :: {:ok, Discussion.t(), Activity.t()} | any()
|
||||
@spec update(Discussion.t(), map(), map()) ::
|
||||
{:ok, Discussion.t(), ActivityStream.t()} | {:error, Ecto.Changeset.t()}
|
||||
def update(%Discussion{} = old_discussion, args, additional) do
|
||||
with {:ok, %Discussion{} = new_discussion} <-
|
||||
Discussions.update_discussion(old_discussion, args),
|
||||
{:ok, _} <-
|
||||
case Discussions.update_discussion(old_discussion, args) do
|
||||
{:ok, %Discussion{} = new_discussion} ->
|
||||
DiscussionActivity.insert_activity(new_discussion,
|
||||
subject: "discussion_renamed",
|
||||
old_discussion: old_discussion
|
||||
),
|
||||
{:ok, true} <- Cachex.del(:activity_pub, "discussion_#{new_discussion.slug}"),
|
||||
discussion_as_data <- Convertible.model_to_as(new_discussion),
|
||||
audience <-
|
||||
Audience.get_audience(new_discussion),
|
||||
update_data <- make_update_data(discussion_as_data, Map.merge(audience, additional)) do
|
||||
)
|
||||
|
||||
Cachex.del(:activity_pub, "discussion_#{new_discussion.slug}")
|
||||
discussion_as_data = Convertible.model_to_as(new_discussion)
|
||||
audience = Audience.get_audience(new_discussion)
|
||||
update_data = make_update_data(discussion_as_data, Map.merge(audience, additional))
|
||||
{:ok, new_discussion, update_data}
|
||||
else
|
||||
err ->
|
||||
Logger.error("Something went wrong while creating an update activity")
|
||||
Logger.debug(inspect(err))
|
||||
err
|
||||
|
||||
{:error, %Ecto.Changeset{} = err} ->
|
||||
{:error, err}
|
||||
end
|
||||
end
|
||||
|
||||
@impl Entity
|
||||
@spec delete(Discussion.t(), Actor.t(), boolean, map()) :: {:ok, Discussion.t()}
|
||||
@spec delete(Discussion.t(), Actor.t(), boolean, map()) ::
|
||||
{:error, Ecto.Changeset.t()} | {:ok, ActivityStream.t(), Actor.t(), Discussion.t()}
|
||||
def delete(
|
||||
%Discussion{actor: group, url: url} = discussion,
|
||||
%Actor{} = actor,
|
||||
_local,
|
||||
_additionnal
|
||||
) do
|
||||
with {:ok, _} <- Discussions.delete_discussion(discussion),
|
||||
{:ok, _} <-
|
||||
case Discussions.delete_discussion(discussion) do
|
||||
{:error, _, %Ecto.Changeset{} = err, _} ->
|
||||
{:error, err}
|
||||
|
||||
{:ok, %{comments: {_, _}}} ->
|
||||
DiscussionActivity.insert_activity(discussion,
|
||||
subject: "discussion_deleted",
|
||||
moderator: actor
|
||||
) do
|
||||
)
|
||||
|
||||
# This is just fake
|
||||
activity_data = %{
|
||||
"type" => "Delete",
|
||||
|
@ -106,10 +124,13 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Discussions do
|
|||
end
|
||||
end
|
||||
|
||||
@spec actor(Discussion.t()) :: Actor.t() | nil
|
||||
def actor(%Discussion{creator_id: creator_id}), do: Actors.get_actor(creator_id)
|
||||
|
||||
@spec group_actor(Discussion.t()) :: Actor.t() | nil
|
||||
def group_actor(%Discussion{actor_id: actor_id}), do: Actors.get_actor(actor_id)
|
||||
|
||||
@spec permissions(Discussion.t()) :: Permission.t()
|
||||
def permissions(%Discussion{}) do
|
||||
%Permission{access: :member, create: :member, update: :moderator, delete: :moderator}
|
||||
end
|
||||
|
@ -123,6 +144,7 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Discussions do
|
|||
:ok
|
||||
end
|
||||
|
||||
@spec prepare_args(map) :: map
|
||||
defp prepare_args(args) do
|
||||
{text, _mentions, _tags} =
|
||||
APIUtils.make_content_html(
|
||||
|
|
|
@ -28,16 +28,16 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Entity do
|
|||
@moduledoc """
|
||||
ActivityPub entity behaviour
|
||||
"""
|
||||
@type t :: %{id: String.t()}
|
||||
@type t :: %{required(:id) => any(), optional(:url) => String.t(), optional(atom()) => any()}
|
||||
|
||||
@callback create(data :: any(), additionnal :: map()) ::
|
||||
{:ok, t(), ActivityStream.t()}
|
||||
{:ok, t(), ActivityStream.t()} | {:error, any()}
|
||||
|
||||
@callback update(struct :: t(), attrs :: map(), additionnal :: map()) ::
|
||||
{:ok, t(), ActivityStream.t()}
|
||||
@callback update(structure :: t(), attrs :: map(), additionnal :: map()) ::
|
||||
{:ok, t(), ActivityStream.t()} | {:error, any()}
|
||||
|
||||
@callback delete(struct :: t(), Actor.t(), local :: boolean(), map()) ::
|
||||
{:ok, ActivityStream.t(), Actor.t(), t()}
|
||||
@callback delete(structure :: t(), actor :: Actor.t(), local :: boolean(), additionnal :: map()) ::
|
||||
{:ok, ActivityStream.t(), Actor.t(), t()} | {:error, any()}
|
||||
end
|
||||
|
||||
defprotocol Mobilizon.Federation.ActivityPub.Types.Managable do
|
||||
|
@ -45,46 +45,61 @@ defprotocol Mobilizon.Federation.ActivityPub.Types.Managable do
|
|||
ActivityPub entity Managable protocol.
|
||||
"""
|
||||
|
||||
@spec update(Entity.t(), map(), map()) :: {:ok, Entity.t(), ActivityStream.t()}
|
||||
@doc """
|
||||
Updates a `Managable` entity with the appropriate attributes and returns the updated entity and an activitystream representation for it
|
||||
"""
|
||||
@spec update(Entity.t(), map(), map()) ::
|
||||
{:ok, Entity.t(), ActivityStream.t()} | {:error, any()}
|
||||
def update(entity, attrs, additionnal)
|
||||
|
||||
@spec delete(Entity.t(), Actor.t(), boolean(), map()) ::
|
||||
{:ok, ActivityStream.t(), Actor.t(), Entity.t()}
|
||||
@doc "Deletes an entity and returns the activitystream representation for it"
|
||||
@spec delete(Entity.t(), Actor.t(), boolean(), map()) ::
|
||||
{:ok, ActivityStream.t(), Actor.t(), Entity.t()} | {:error, any()}
|
||||
def delete(entity, actor, local, additionnal)
|
||||
end
|
||||
|
||||
defprotocol Mobilizon.Federation.ActivityPub.Types.Ownable do
|
||||
@type group_role :: :member | :moderator | :administrator | nil
|
||||
|
||||
@spec group_actor(Entity.t()) :: Actor.t() | nil
|
||||
@doc "Returns an eventual group for the entity"
|
||||
@spec group_actor(Entity.t()) :: Actor.t() | nil
|
||||
def group_actor(entity)
|
||||
|
||||
@spec actor(Entity.t()) :: Actor.t() | nil
|
||||
@doc "Returns the actor for the entity"
|
||||
@spec actor(Entity.t()) :: Actor.t() | nil
|
||||
def actor(entity)
|
||||
|
||||
@doc """
|
||||
Returns the list of permissions for an entity
|
||||
"""
|
||||
@spec permissions(Entity.t()) :: Permission.t()
|
||||
def permissions(entity)
|
||||
end
|
||||
|
||||
defimpl Managable, for: Event do
|
||||
@spec update(Event.t(), map, map) ::
|
||||
{:error, atom() | Ecto.Changeset.t()} | {:ok, Event.t(), ActivityStream.t()}
|
||||
defdelegate update(entity, attrs, additionnal), to: Events
|
||||
|
||||
@spec delete(entity :: Event.t(), actor :: Actor.t(), local :: boolean(), additionnal :: map()) ::
|
||||
{:ok, ActivityStream.t(), Actor.t(), Event.t()} | {:error, atom() | Ecto.Changeset.t()}
|
||||
defdelegate delete(entity, actor, local, additionnal), to: Events
|
||||
end
|
||||
|
||||
defimpl Ownable, for: Event do
|
||||
@spec group_actor(Event.t()) :: Actor.t() | nil
|
||||
defdelegate group_actor(entity), to: Events
|
||||
@spec actor(Event.t()) :: Actor.t() | nil
|
||||
defdelegate actor(entity), to: Events
|
||||
@spec permissions(Event.t()) :: Permission.t()
|
||||
defdelegate permissions(entity), to: Events
|
||||
end
|
||||
|
||||
defimpl Managable, for: Comment do
|
||||
@spec update(Comment.t(), map, map) ::
|
||||
{:error, Ecto.Changeset.t()} | {:ok, Comment.t(), ActivityStream.t()}
|
||||
defdelegate update(entity, attrs, additionnal), to: Comments
|
||||
|
||||
@spec delete(Comment.t(), Actor.t(), boolean, map) ::
|
||||
{:error, Ecto.Changeset.t()} | {:ok, ActivityStream.t(), Actor.t(), Comment.t()}
|
||||
defdelegate delete(entity, actor, local, additionnal), to: Comments
|
||||
end
|
||||
|
||||
|
|
|
@ -3,10 +3,10 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Events do
|
|||
alias Mobilizon.Actors
|
||||
alias Mobilizon.Actors.Actor
|
||||
alias Mobilizon.Events, as: EventsManager
|
||||
alias Mobilizon.Events.{Event, Participant}
|
||||
alias Mobilizon.Federation.ActivityPub
|
||||
alias Mobilizon.Federation.ActivityPub.{Audience, Permission}
|
||||
alias Mobilizon.Events.{Event, Participant, ParticipantRole}
|
||||
alias Mobilizon.Federation.ActivityPub.{Actions, Audience, Permission}
|
||||
alias Mobilizon.Federation.ActivityPub.Types.Entity
|
||||
alias Mobilizon.Federation.ActivityStream
|
||||
alias Mobilizon.Federation.ActivityStream.Converter.Utils, as: ConverterUtils
|
||||
alias Mobilizon.Federation.ActivityStream.Convertible
|
||||
alias Mobilizon.GraphQL.API.Utils, as: APIUtils
|
||||
|
@ -22,44 +22,53 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Events do
|
|||
@behaviour Entity
|
||||
|
||||
@impl Entity
|
||||
@spec create(map(), map()) :: {:ok, map()}
|
||||
@spec create(map(), map()) ::
|
||||
{:ok, Event.t(), ActivityStream.t()} | {:error, Ecto.Changeset.t()}
|
||||
def create(args, additional) do
|
||||
with args <- prepare_args_for_event(args),
|
||||
{:ok, %Event{} = event} <- EventsManager.create_event(args),
|
||||
{:ok, _} <-
|
||||
EventActivity.insert_activity(event, subject: "event_created"),
|
||||
event_as_data <- Convertible.model_to_as(event),
|
||||
audience <-
|
||||
Audience.get_audience(event),
|
||||
create_data <-
|
||||
make_create_data(event_as_data, Map.merge(audience, additional)) do
|
||||
args = prepare_args_for_event(args)
|
||||
|
||||
case EventsManager.create_event(args) do
|
||||
{:ok, %Event{} = event} ->
|
||||
EventActivity.insert_activity(event, subject: "event_created")
|
||||
event_as_data = Convertible.model_to_as(event)
|
||||
audience = Audience.get_audience(event)
|
||||
create_data = make_create_data(event_as_data, Map.merge(audience, additional))
|
||||
{:ok, event, create_data}
|
||||
|
||||
{:error, _step, %Ecto.Changeset{} = err, _} ->
|
||||
{:error, err}
|
||||
|
||||
{:error, %Ecto.Changeset{} = err} ->
|
||||
{:error, err}
|
||||
end
|
||||
end
|
||||
|
||||
@impl Entity
|
||||
@spec update(Event.t(), map(), map()) :: {:ok, Event.t(), Activity.t()} | any()
|
||||
@spec update(Event.t(), map(), map()) ::
|
||||
{:ok, Event.t(), ActivityStream.t()} | {:error, Ecto.Changeset.t()}
|
||||
def update(%Event{} = old_event, args, additional) do
|
||||
with args <- prepare_args_for_event(args),
|
||||
{:ok, %Event{} = new_event} <- EventsManager.update_event(old_event, args),
|
||||
{:ok, _} <-
|
||||
EventActivity.insert_activity(new_event, subject: "event_updated"),
|
||||
{:ok, true} <- Cachex.del(:activity_pub, "event_#{new_event.uuid}"),
|
||||
event_as_data <- Convertible.model_to_as(new_event),
|
||||
audience <-
|
||||
Audience.get_audience(new_event),
|
||||
update_data <- make_update_data(event_as_data, Map.merge(audience, additional)) do
|
||||
args = prepare_args_for_event(args)
|
||||
|
||||
case EventsManager.update_event(old_event, args) do
|
||||
{:ok, %Event{} = new_event} ->
|
||||
EventActivity.insert_activity(new_event, subject: "event_updated")
|
||||
Cachex.del(:activity_pub, "event_#{new_event.uuid}")
|
||||
event_as_data = Convertible.model_to_as(new_event)
|
||||
audience = Audience.get_audience(new_event)
|
||||
update_data = make_update_data(event_as_data, Map.merge(audience, additional))
|
||||
{:ok, new_event, update_data}
|
||||
else
|
||||
err ->
|
||||
Logger.error("Something went wrong while creating an update activity")
|
||||
Logger.debug(inspect(err))
|
||||
err
|
||||
|
||||
{:error, _step, %Ecto.Changeset{} = err, _} ->
|
||||
{:error, err}
|
||||
|
||||
{:error, err} ->
|
||||
{:error, err}
|
||||
end
|
||||
end
|
||||
|
||||
@impl Entity
|
||||
@spec delete(Event.t(), Actor.t(), boolean, map()) :: {:ok, Event.t()}
|
||||
@spec delete(Event.t(), Actor.t(), boolean, map()) ::
|
||||
{:ok, ActivityStream.t(), Actor.t(), Event.t()} | {:error, Ecto.Changeset.t()}
|
||||
def delete(%Event{url: url} = event, %Actor{} = actor, _local, _additionnal) do
|
||||
activity_data = %{
|
||||
"type" => "Delete",
|
||||
|
@ -69,19 +78,27 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Events do
|
|||
"id" => url <> "/delete"
|
||||
}
|
||||
|
||||
with audience <-
|
||||
Audience.get_audience(event),
|
||||
{:ok, %Event{} = event} <- EventsManager.delete_event(event),
|
||||
{:ok, _} <-
|
||||
EventActivity.insert_activity(event, subject: "event_deleted"),
|
||||
{:ok, true} <- Cachex.del(:activity_pub, "event_#{event.uuid}"),
|
||||
{:ok, %Tombstone{} = _tombstone} <-
|
||||
Tombstone.create_tombstone(%{uri: event.url, actor_id: actor.id}) do
|
||||
audience = Audience.get_audience(event)
|
||||
|
||||
case EventsManager.delete_event(event) do
|
||||
{:ok, %Event{} = event} ->
|
||||
case Tombstone.create_tombstone(%{uri: event.url, actor_id: actor.id}) do
|
||||
{:ok, %Tombstone{} = _tombstone} ->
|
||||
EventActivity.insert_activity(event, subject: "event_deleted")
|
||||
Cachex.del(:activity_pub, "event_#{event.uuid}")
|
||||
Share.delete_all_by_uri(event.url)
|
||||
{:ok, Map.merge(activity_data, audience), actor, event}
|
||||
|
||||
{:error, %Ecto.Changeset{} = err} ->
|
||||
{:error, err}
|
||||
end
|
||||
|
||||
{:error, %Ecto.Changeset{} = err} ->
|
||||
{:error, err}
|
||||
end
|
||||
end
|
||||
|
||||
@spec actor(Event.t()) :: Actor.t() | nil
|
||||
def actor(%Event{organizer_actor: %Actor{} = actor}), do: actor
|
||||
|
||||
def actor(%Event{organizer_actor_id: organizer_actor_id}),
|
||||
|
@ -89,6 +106,7 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Events do
|
|||
|
||||
def actor(_), do: nil
|
||||
|
||||
@spec group_actor(Event.t()) :: Actor.t() | nil
|
||||
def group_actor(%Event{attributed_to: %Actor{} = group}), do: group
|
||||
|
||||
def group_actor(%Event{attributed_to_id: attributed_to_id}) when not is_nil(attributed_to_id),
|
||||
|
@ -96,6 +114,7 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Events do
|
|||
|
||||
def group_actor(_), do: nil
|
||||
|
||||
@spec permissions(Event.t()) :: Permission.t()
|
||||
def permissions(%Event{draft: draft, attributed_to_id: _attributed_to_id}) do
|
||||
%Permission{
|
||||
access: if(draft, do: nil, else: :member),
|
||||
|
@ -105,15 +124,18 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Events do
|
|||
}
|
||||
end
|
||||
|
||||
@spec join(Event.t(), Actor.t(), boolean, map) ::
|
||||
{:ok, ActivityStreams.t(), Participant.t()}
|
||||
| {:accept, any()}
|
||||
| {:error, :maximum_attendee_capacity_reached}
|
||||
def join(%Event{} = event, %Actor{} = actor, _local, additional) do
|
||||
with {:maximum_attendee_capacity, true} <-
|
||||
{:maximum_attendee_capacity, check_attendee_capacity(event)},
|
||||
role <-
|
||||
if check_attendee_capacity?(event) do
|
||||
role =
|
||||
additional
|
||||
|> Map.get(:metadata, %{})
|
||||
|> Map.get(:role, Mobilizon.Events.get_default_participant_role(event)),
|
||||
{:ok, %Participant{} = participant} <-
|
||||
Mobilizon.Events.create_participant(%{
|
||||
|> Map.get(:role, Mobilizon.Events.get_default_participant_role(event))
|
||||
|
||||
case Mobilizon.Events.create_participant(%{
|
||||
role: role,
|
||||
event_id: event.id,
|
||||
actor_id: actor.id,
|
||||
|
@ -122,31 +144,41 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Events do
|
|||
additional
|
||||
|> Map.get(:metadata, %{})
|
||||
|> Map.update(:message, nil, &String.trim(HTML.strip_tags(&1)))
|
||||
}),
|
||||
join_data <- Convertible.model_to_as(participant),
|
||||
audience <-
|
||||
Audience.get_audience(participant) do
|
||||
}) do
|
||||
{:ok, %Participant{} = participant} ->
|
||||
join_data = Convertible.model_to_as(participant)
|
||||
audience = Audience.get_audience(participant)
|
||||
|
||||
approve_if_default_role_is_participant(
|
||||
event,
|
||||
Map.merge(join_data, audience),
|
||||
participant,
|
||||
role
|
||||
)
|
||||
|
||||
{:error, %Ecto.Changeset{} = err} ->
|
||||
{:error, err}
|
||||
end
|
||||
else
|
||||
{:maximum_attendee_capacity, err} ->
|
||||
{:maximum_attendee_capacity, err}
|
||||
{:error, :maximum_attendee_capacity_reached}
|
||||
end
|
||||
end
|
||||
|
||||
defp check_attendee_capacity(%Event{options: options} = event) do
|
||||
with maximum_attendee_capacity <-
|
||||
Map.get(options, :maximum_attendee_capacity) || 0 do
|
||||
@spec check_attendee_capacity?(Event.t()) :: boolean
|
||||
defp check_attendee_capacity?(%Event{options: options} = event) do
|
||||
maximum_attendee_capacity = Map.get(options, :maximum_attendee_capacity) || 0
|
||||
|
||||
maximum_attendee_capacity == 0 ||
|
||||
Mobilizon.Events.count_participant_participants(event.id) < maximum_attendee_capacity
|
||||
end
|
||||
end
|
||||
|
||||
# Set the participant to approved if the default role for new participants is :participant
|
||||
@spec approve_if_default_role_is_participant(
|
||||
Event.t(),
|
||||
ActivityStreams.t(),
|
||||
Participant.t(),
|
||||
ParticipantRole.t()
|
||||
) :: {:ok, ActivityStreams.t(), Participant.t()} | {:accept, any()}
|
||||
defp approve_if_default_role_is_participant(event, activity_data, participant, role) do
|
||||
case event do
|
||||
%Event{attributed_to: %Actor{id: group_id, url: group_url}} ->
|
||||
|
@ -161,29 +193,31 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Events do
|
|||
{:ok, activity_data, participant}
|
||||
end
|
||||
|
||||
%Event{local: true} ->
|
||||
%Event{attributed_to: nil, local: true} ->
|
||||
do_approve(event, activity_data, participant, role, %{
|
||||
"actor" => event.organizer_actor.url
|
||||
})
|
||||
|
||||
_ ->
|
||||
%Event{} ->
|
||||
{:ok, activity_data, participant}
|
||||
end
|
||||
end
|
||||
|
||||
@spec do_approve(Event.t(), ActivityStreams.t(), Particpant.t(), ParticipantRole.t(), map()) ::
|
||||
{:accept, any} | {:ok, ActivityStreams.t(), Participant.t()}
|
||||
defp do_approve(event, activity_data, participant, role, additionnal) do
|
||||
cond do
|
||||
Mobilizon.Events.get_default_participant_role(event) === :participant &&
|
||||
Mobilizon.Events.get_default_participant_role(event) == :participant &&
|
||||
role == :participant ->
|
||||
{:accept,
|
||||
ActivityPub.accept(
|
||||
Actions.Accept.accept(
|
||||
:join,
|
||||
participant,
|
||||
true,
|
||||
additionnal
|
||||
)}
|
||||
|
||||
Mobilizon.Events.get_default_participant_role(event) === :not_approved &&
|
||||
Mobilizon.Events.get_default_participant_role(event) == :not_approved &&
|
||||
role == :not_approved ->
|
||||
Scheduler.pending_participation_notification(event)
|
||||
{:ok, activity_data, participant}
|
||||
|
@ -194,6 +228,7 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Events do
|
|||
end
|
||||
|
||||
# Prepare and sanitize arguments for events
|
||||
@spec prepare_args_for_event(map) :: map
|
||||
defp prepare_args_for_event(args) do
|
||||
# If title is not set: we are not updating it
|
||||
args =
|
||||
|
|
|
@ -1,14 +1,18 @@
|
|||
defmodule Mobilizon.Federation.ActivityPub.Types.Members do
|
||||
@moduledoc false
|
||||
alias Mobilizon.Actors
|
||||
alias Mobilizon.Actors.{Actor, Member}
|
||||
alias Mobilizon.Federation.ActivityPub
|
||||
alias Mobilizon.Actors.{Actor, Member, MemberRole}
|
||||
alias Mobilizon.Federation.ActivityPub.Actions
|
||||
alias Mobilizon.Federation.ActivityStream
|
||||
alias Mobilizon.Federation.ActivityStream.Convertible
|
||||
alias Mobilizon.Service.Activity.Member, as: MemberActivity
|
||||
alias Mobilizon.Web.Endpoint
|
||||
require Logger
|
||||
import Mobilizon.Federation.ActivityPub.Utils, only: [make_update_data: 2]
|
||||
|
||||
@spec update(Member.t(), map, map) ::
|
||||
{:ok, Member.t(), ActivityStream.t()}
|
||||
| {:error, :member_not_found | :only_admin_left | Ecto.Changeset.t()}
|
||||
def update(
|
||||
%Member{
|
||||
parent: %Actor{id: group_id} = group,
|
||||
|
@ -19,43 +23,51 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Members do
|
|||
%{role: updated_role} = args,
|
||||
%{moderator: %Actor{url: moderator_url, id: moderator_id} = moderator} = additional
|
||||
) do
|
||||
with additional <- Map.delete(additional, :moderator),
|
||||
{:has_rights_to_update_role, {:ok, %Member{role: moderator_role}}}
|
||||
when moderator_role in [:moderator, :administrator, :creator] <-
|
||||
{:has_rights_to_update_role, Actors.get_member(moderator_id, group_id)},
|
||||
{:is_only_admin, false} <-
|
||||
{:is_only_admin, check_admins_left(member_id, group_id, current_role, updated_role)},
|
||||
{:ok, %Member{} = member} <-
|
||||
Actors.update_member(old_member, args),
|
||||
{:ok, _} <-
|
||||
additional = Map.delete(additional, :moderator)
|
||||
|
||||
case Actors.get_member(moderator_id, group_id) do
|
||||
{:error, :member_not_found} ->
|
||||
{:error, :member_not_found}
|
||||
|
||||
{:ok, %Member{role: moderator_role}}
|
||||
when moderator_role in [:moderator, :administrator, :creator] ->
|
||||
if check_admins_left?(member_id, group_id, current_role, updated_role) do
|
||||
{:error, :only_admin_left}
|
||||
else
|
||||
case Actors.update_member(old_member, args) do
|
||||
{:error, %Ecto.Changeset{} = err} ->
|
||||
{:error, err}
|
||||
|
||||
{:ok, %Member{} = member} ->
|
||||
MemberActivity.insert_activity(member,
|
||||
old_member: old_member,
|
||||
moderator: moderator,
|
||||
subject: "member_updated"
|
||||
),
|
||||
)
|
||||
|
||||
Absinthe.Subscription.publish(Endpoint, actor,
|
||||
group_membership_changed: [Actor.preferred_username_and_domain(group), actor_id]
|
||||
),
|
||||
{:ok, true} <- Cachex.del(:activity_pub, "member_#{member_id}"),
|
||||
member_as_data <-
|
||||
Convertible.model_to_as(member),
|
||||
audience <- %{
|
||||
)
|
||||
|
||||
Cachex.del(:activity_pub, "member_#{member_id}")
|
||||
member_as_data = Convertible.model_to_as(member)
|
||||
|
||||
audience = %{
|
||||
"to" => [member.parent.members_url, member.actor.url],
|
||||
"cc" => [member.parent.url],
|
||||
"actor" => moderator_url,
|
||||
"attributedTo" => [member.parent.url]
|
||||
} do
|
||||
update_data = make_update_data(member_as_data, Map.merge(audience, additional))
|
||||
}
|
||||
|
||||
update_data = make_update_data(member_as_data, Map.merge(audience, additional))
|
||||
{:ok, member, update_data}
|
||||
else
|
||||
err ->
|
||||
Logger.debug(inspect(err))
|
||||
err
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Used only when a group is suspended
|
||||
@spec delete(Member.t(), Actor.t(), boolean(), map()) :: {:ok, Activity.t(), Member.t()}
|
||||
def delete(
|
||||
%Member{parent: %Actor{} = group, actor: %Actor{} = actor} = _member,
|
||||
%Actor{},
|
||||
|
@ -63,16 +75,24 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Members do
|
|||
_additionnal
|
||||
) do
|
||||
Logger.debug("Deleting a member")
|
||||
ActivityPub.leave(group, actor, local, %{force_member_removal: true})
|
||||
Actions.Leave.leave(group, actor, local, %{force_member_removal: true})
|
||||
end
|
||||
|
||||
@spec actor(Member.t()) :: Actor.t() | nil
|
||||
def actor(%Member{actor_id: actor_id}),
|
||||
do: Actors.get_actor(actor_id)
|
||||
|
||||
@spec group_actor(Member.t()) :: Actor.t() | nil
|
||||
def group_actor(%Member{parent_id: parent_id}),
|
||||
do: Actors.get_actor(parent_id)
|
||||
|
||||
defp check_admins_left(member_id, group_id, current_role, updated_role) do
|
||||
@spec check_admins_left?(
|
||||
String.t() | integer,
|
||||
String.t() | integer,
|
||||
MemberRole.t(),
|
||||
MemberRole.t()
|
||||
) :: boolean
|
||||
defp check_admins_left?(member_id, group_id, current_role, updated_role) do
|
||||
Actors.is_only_administrator?(member_id, group_id) && current_role == :administrator &&
|
||||
updated_role != :administrator
|
||||
end
|
||||
|
|
|
@ -4,6 +4,7 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Posts do
|
|||
alias Mobilizon.Actors.Actor
|
||||
alias Mobilizon.Federation.ActivityPub.{Audience, Permission}
|
||||
alias Mobilizon.Federation.ActivityPub.Types.Entity
|
||||
alias Mobilizon.Federation.ActivityStream
|
||||
alias Mobilizon.Federation.ActivityStream.Converter.Utils, as: ConverterUtils
|
||||
alias Mobilizon.Federation.ActivityStream.Convertible
|
||||
alias Mobilizon.Posts.Post
|
||||
|
@ -17,6 +18,7 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Posts do
|
|||
@public_ap "https://www.w3.org/ns/activitystreams#Public"
|
||||
|
||||
@impl Entity
|
||||
@spec create(map(), map()) :: {:ok, Post.t(), ActivityStream.t()}
|
||||
def create(args, additional) do
|
||||
with args <- prepare_args(args),
|
||||
{:ok, %Post{attributed_to_id: group_id, author_id: creator_id} = post} <-
|
||||
|
@ -37,6 +39,7 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Posts do
|
|||
end
|
||||
|
||||
@impl Entity
|
||||
@spec update(Post.t(), map(), map()) :: {:ok, Post.t(), ActivityStream.t()}
|
||||
def update(%Post{} = post, args, additional) do
|
||||
with args <- prepare_args(args),
|
||||
{:ok, %Post{attributed_to_id: group_id, author_id: creator_id} = post} <-
|
||||
|
@ -60,6 +63,8 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Posts do
|
|||
end
|
||||
|
||||
@impl Entity
|
||||
@spec delete(Post.t(), Actor.t(), boolean, map) ::
|
||||
{:ok, ActivityStream.t(), Actor.t(), Post.t()}
|
||||
def delete(
|
||||
%Post{
|
||||
url: url,
|
||||
|
@ -86,12 +91,15 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Posts do
|
|||
end
|
||||
end
|
||||
|
||||
@spec actor(Post.t()) :: Actor.t() | nil
|
||||
def actor(%Post{author_id: author_id}),
|
||||
do: Actors.get_actor(author_id)
|
||||
|
||||
@spec group_actor(Post.t()) :: Actor.t() | nil
|
||||
def group_actor(%Post{attributed_to_id: attributed_to_id}),
|
||||
do: Actors.get_actor(attributed_to_id)
|
||||
|
||||
@spec permissions(Post.t()) :: Permission.t()
|
||||
def permissions(%Post{}) do
|
||||
%Permission{
|
||||
access: :member,
|
||||
|
@ -101,6 +109,7 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Posts do
|
|||
}
|
||||
end
|
||||
|
||||
@spec prepare_args(map()) :: map
|
||||
defp prepare_args(args) do
|
||||
args
|
||||
|> Map.update(:tags, [], &ConverterUtils.fetch_tags/1)
|
||||
|
|
|
@ -1,36 +1,47 @@
|
|||
defmodule Mobilizon.Federation.ActivityPub.Types.Reports do
|
||||
@moduledoc false
|
||||
alias Mobilizon.{Actors, Discussions, Reports}
|
||||
alias Mobilizon.{Actors, Discussions, Events, Reports}
|
||||
alias Mobilizon.Actors.Actor
|
||||
alias Mobilizon.Events.Event
|
||||
alias Mobilizon.Federation.ActivityStream
|
||||
alias Mobilizon.Federation.ActivityStream.Convertible
|
||||
alias Mobilizon.Reports.Report
|
||||
alias Mobilizon.Service.Formatter.HTML
|
||||
require Logger
|
||||
|
||||
@spec flag(map(), boolean(), map()) ::
|
||||
{:ok, Report.t(), ActivityStream.t()} | {:error, Ecto.Changeset.t()}
|
||||
def flag(args, local \\ false, _additional \\ %{}) do
|
||||
with {:build_args, args} <- {:build_args, prepare_args_for_report(args)},
|
||||
{:create_report, {:ok, %Report{} = report}} <-
|
||||
{:create_report, Reports.create_report(args)},
|
||||
report_as_data <- Convertible.model_to_as(report),
|
||||
cc <- if(local, do: [report.reported.url], else: []),
|
||||
report_as_data <- Map.merge(report_as_data, %{"to" => [], "cc" => cc}) do
|
||||
{report, report_as_data}
|
||||
with {:ok, %Report{} = report} <- args |> prepare_args_for_report() |> Reports.create_report() do
|
||||
report_as_data = Convertible.model_to_as(report)
|
||||
cc = if(local, do: [report.reported.url], else: [])
|
||||
report_as_data = Map.merge(report_as_data, %{"to" => [], "cc" => cc})
|
||||
{:ok, report, report_as_data}
|
||||
end
|
||||
end
|
||||
|
||||
@spec prepare_args_for_report(map()) :: map()
|
||||
defp prepare_args_for_report(args) do
|
||||
with {:reporter, %Actor{} = reporter_actor} <-
|
||||
{:reporter, Actors.get_actor!(args.reporter_id)},
|
||||
{:reported, %Actor{} = reported_actor} <-
|
||||
{:reported, Actors.get_actor!(args.reported_id)},
|
||||
content <- HTML.strip_tags(args.content),
|
||||
event <- Discussions.get_comment(Map.get(args, :event_id)),
|
||||
{:get_report_comments, comments} <-
|
||||
{:get_report_comments,
|
||||
%Actor{} = reporter_actor = Actors.get_actor!(args.reporter_id)
|
||||
%Actor{} = reported_actor = Actors.get_actor!(args.reported_id)
|
||||
content = HTML.strip_tags(args.content)
|
||||
|
||||
event_id = Map.get(args, :event_id)
|
||||
|
||||
event =
|
||||
if is_nil(event_id) do
|
||||
nil
|
||||
else
|
||||
{:ok, %Event{} = event} = Events.get_event(event_id)
|
||||
event
|
||||
end
|
||||
|
||||
comments =
|
||||
Discussions.list_comments_by_actor_and_ids(
|
||||
reported_actor.id,
|
||||
Map.get(args, :comments_ids, [])
|
||||
)} do
|
||||
)
|
||||
|
||||
Map.merge(args, %{
|
||||
reporter: reporter_actor,
|
||||
reported: reported_actor,
|
||||
|
@ -39,5 +50,4 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Reports do
|
|||
comments: comments
|
||||
})
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -4,6 +4,7 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Resources do
|
|||
alias Mobilizon.Actors.Actor
|
||||
alias Mobilizon.Federation.ActivityPub.Permission
|
||||
alias Mobilizon.Federation.ActivityPub.Types.Entity
|
||||
alias alias Mobilizon.Federation.ActivityStream
|
||||
alias Mobilizon.Federation.ActivityStream.Convertible
|
||||
alias Mobilizon.Resources.Resource
|
||||
alias Mobilizon.Service.Activity.Resource, as: ResourceActivity
|
||||
|
@ -16,6 +17,9 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Resources do
|
|||
@behaviour Entity
|
||||
|
||||
@impl Entity
|
||||
@spec create(map(), map()) ::
|
||||
{:ok, Resource.t(), ActivityStream.t()}
|
||||
| {:error, Ecto.Changeset.t() | :creator_not_found | :group_not_found}
|
||||
def create(%{type: type} = args, additional) do
|
||||
args =
|
||||
case type do
|
||||
|
@ -35,17 +39,18 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Resources do
|
|||
with {:ok,
|
||||
%Resource{actor_id: group_id, creator_id: creator_id, parent_id: parent_id} = resource} <-
|
||||
Resources.create_resource(args),
|
||||
{:ok, _} <- ResourceActivity.insert_activity(resource, subject: "resource_created"),
|
||||
{:ok, %Actor{} = group} <- Actors.get_group_by_actor_id(group_id),
|
||||
%Actor{url: creator_url} = creator <- Actors.get_actor(creator_id),
|
||||
resource_as_data <-
|
||||
Convertible.model_to_as(%{resource | actor: group, creator: creator}),
|
||||
audience <- %{
|
||||
{:ok, %Actor{} = group, %Actor{url: creator_url} = creator} <-
|
||||
group_and_creator(group_id, creator_id) do
|
||||
ResourceActivity.insert_activity(resource, subject: "resource_created")
|
||||
resource_as_data = Convertible.model_to_as(%{resource | actor: group, creator: creator})
|
||||
|
||||
audience = %{
|
||||
"to" => [group.members_url],
|
||||
"cc" => [],
|
||||
"actor" => creator_url,
|
||||
"attributedTo" => [creator_url]
|
||||
} do
|
||||
}
|
||||
|
||||
create_data =
|
||||
case parent_id do
|
||||
nil ->
|
||||
|
@ -58,14 +63,13 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Resources do
|
|||
end
|
||||
|
||||
{:ok, resource, create_data}
|
||||
else
|
||||
err ->
|
||||
Logger.debug(inspect(err))
|
||||
err
|
||||
end
|
||||
end
|
||||
|
||||
@impl Entity
|
||||
@spec update(Resource.t(), map(), map()) ::
|
||||
{:ok, Resource.t(), ActivityStream.t()}
|
||||
| {:error, Ecto.Changeset.t() | :creator_not_found | :group_not_found}
|
||||
def update(
|
||||
%Resource{parent_id: old_parent_id} = old_resource,
|
||||
%{parent_id: parent_id} = args,
|
||||
|
@ -79,31 +83,35 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Resources do
|
|||
def update(%Resource{} = old_resource, %{title: title} = _args, additional) do
|
||||
with {:ok, %Resource{actor_id: group_id, creator_id: creator_id} = resource} <-
|
||||
Resources.update_resource(old_resource, %{title: title}),
|
||||
{:ok, _} <-
|
||||
{:ok, %Actor{} = group, %Actor{url: creator_url}} <-
|
||||
group_and_creator(group_id, creator_id) do
|
||||
ResourceActivity.insert_activity(resource,
|
||||
subject: "resource_renamed",
|
||||
old_resource: old_resource
|
||||
),
|
||||
{:ok, %Actor{} = group} <- Actors.get_group_by_actor_id(group_id),
|
||||
%Actor{url: creator_url} <- Actors.get_actor(creator_id),
|
||||
resource_as_data <-
|
||||
Convertible.model_to_as(%{resource | actor: group}),
|
||||
audience <- %{
|
||||
)
|
||||
|
||||
resource_as_data = Convertible.model_to_as(%{resource | actor: group})
|
||||
|
||||
audience = %{
|
||||
"to" => [group.members_url],
|
||||
"cc" => [],
|
||||
"actor" => creator_url,
|
||||
"attributedTo" => [creator_url]
|
||||
},
|
||||
update_data <-
|
||||
make_update_data(resource_as_data, Map.merge(audience, additional)) do
|
||||
}
|
||||
|
||||
update_data = make_update_data(resource_as_data, Map.merge(audience, additional))
|
||||
{:ok, resource, update_data}
|
||||
else
|
||||
err ->
|
||||
Logger.debug(inspect(err))
|
||||
err
|
||||
end
|
||||
end
|
||||
|
||||
@spec move(Resource.t(), map(), map()) ::
|
||||
{:ok, Resource.t(), ActivityStream.t()}
|
||||
| {:error,
|
||||
Ecto.Changeset.t()
|
||||
| :creator_not_found
|
||||
| :group_not_found
|
||||
| :new_parent_not_found
|
||||
| :old_parent_not_found}
|
||||
def move(
|
||||
%Resource{parent_id: old_parent_id} = old_resource,
|
||||
%{parent_id: _new_parent_id} = args,
|
||||
|
@ -113,35 +121,34 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Resources do
|
|||
%Resource{actor_id: group_id, creator_id: creator_id, parent_id: new_parent_id} =
|
||||
resource} <-
|
||||
Resources.update_resource(old_resource, args),
|
||||
{:ok, _} <- ResourceActivity.insert_activity(resource, subject: "resource_moved"),
|
||||
old_parent <- Resources.get_resource(old_parent_id),
|
||||
new_parent <- Resources.get_resource(new_parent_id),
|
||||
{:ok, %Actor{} = group} <- Actors.get_group_by_actor_id(group_id),
|
||||
%Actor{url: creator_url} <- Actors.get_actor(creator_id),
|
||||
resource_as_data <-
|
||||
Convertible.model_to_as(%{resource | actor: group}),
|
||||
audience <- %{
|
||||
{:ok, old_parent, new_parent} <- parents(old_parent_id, new_parent_id),
|
||||
{:ok, %Actor{} = group, %Actor{url: creator_url}} <-
|
||||
group_and_creator(group_id, creator_id) do
|
||||
ResourceActivity.insert_activity(resource, subject: "resource_moved")
|
||||
resource_as_data = Convertible.model_to_as(%{resource | actor: group})
|
||||
|
||||
audience = %{
|
||||
"to" => [group.members_url],
|
||||
"cc" => [],
|
||||
"actor" => creator_url,
|
||||
"attributedTo" => [creator_url]
|
||||
},
|
||||
move_data <-
|
||||
}
|
||||
|
||||
move_data =
|
||||
make_move_data(
|
||||
resource_as_data,
|
||||
old_parent,
|
||||
new_parent,
|
||||
Map.merge(audience, additional)
|
||||
) do
|
||||
)
|
||||
|
||||
{:ok, resource, move_data}
|
||||
else
|
||||
err ->
|
||||
Logger.debug(inspect(err))
|
||||
err
|
||||
end
|
||||
end
|
||||
|
||||
@impl Entity
|
||||
@spec delete(Resource.t(), Actor.t(), boolean, map()) ::
|
||||
{:ok, ActivityStream.t(), Actor.t(), Resource.t()} | {:error, Ecto.Changeset.t()}
|
||||
def delete(
|
||||
%Resource{url: url, actor: %Actor{url: group_url, members_url: members_url}} = resource,
|
||||
%Actor{url: actor_url} = actor,
|
||||
|
@ -159,19 +166,50 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Resources do
|
|||
"to" => [members_url]
|
||||
}
|
||||
|
||||
with {:ok, _resource} <- Resources.delete_resource(resource),
|
||||
{:ok, _} <- ResourceActivity.insert_activity(resource, subject: "resource_deleted"),
|
||||
{:ok, true} <- Cachex.del(:activity_pub, "resource_#{resource.id}") do
|
||||
case Resources.delete_resource(resource) do
|
||||
{:ok, _resource} ->
|
||||
ResourceActivity.insert_activity(resource, subject: "resource_deleted")
|
||||
Cachex.del(:activity_pub, "resource_#{resource.id}")
|
||||
{:ok, activity_data, actor, resource}
|
||||
|
||||
{:error, %Ecto.Changeset{} = err} ->
|
||||
{:error, err}
|
||||
end
|
||||
end
|
||||
|
||||
@spec actor(Todo.t()) :: Actor.t() | nil
|
||||
def actor(%Resource{creator_id: creator_id}),
|
||||
do: Actors.get_actor(creator_id)
|
||||
|
||||
@spec group_actor(Todo.t()) :: Actor.t() | nil
|
||||
def group_actor(%Resource{actor_id: actor_id}), do: Actors.get_actor(actor_id)
|
||||
|
||||
@spec permissions(TodoList.t()) :: Permission.t()
|
||||
def permissions(%Resource{}) do
|
||||
%Permission{access: :member, create: :member, update: :member, delete: :member}
|
||||
end
|
||||
|
||||
@spec group_and_creator(integer(), integer()) ::
|
||||
{:ok, Actor.t(), Actor.t()} | {:error, :creator_not_found | :group_not_found}
|
||||
defp group_and_creator(group_id, creator_id) do
|
||||
case Actors.get_group_by_actor_id(group_id) do
|
||||
{:ok, %Actor{} = group} ->
|
||||
case Actors.get_actor(creator_id) do
|
||||
%Actor{} = creator ->
|
||||
{:ok, group, creator}
|
||||
|
||||
nil ->
|
||||
{:error, :creator_not_found}
|
||||
end
|
||||
|
||||
{:error, :group_not_found} ->
|
||||
{:error, :group_not_found}
|
||||
end
|
||||
end
|
||||
|
||||
@spec parents(String.t(), String.t()) ::
|
||||
{:ok, Resource.t(), Resource.t()}
|
||||
defp parents(old_parent_id, new_parent_id) do
|
||||
{:ok, Resources.get_resource(old_parent_id), Resources.get_resource(new_parent_id)}
|
||||
end
|
||||
end
|
||||
|
|
|
@ -13,36 +13,37 @@ defmodule Mobilizon.Federation.ActivityPub.Types.TodoLists do
|
|||
@behaviour Entity
|
||||
|
||||
@impl Entity
|
||||
@spec create(map(), map()) :: {:ok, map()}
|
||||
@spec create(map(), map()) ::
|
||||
{:ok, TodoList.t(), ActivityStream.t()}
|
||||
| {:error, :group_not_found | Ecto.Changeset.t()}
|
||||
def create(args, additional) do
|
||||
with {:ok, %TodoList{actor_id: group_id} = todo_list} <- Todos.create_todo_list(args),
|
||||
{:ok, %Actor{} = group} <- Actors.get_group_by_actor_id(group_id),
|
||||
todo_list_as_data <- Convertible.model_to_as(%{todo_list | actor: group}),
|
||||
audience <- %{"to" => [group.members_url], "cc" => []},
|
||||
create_data <-
|
||||
make_create_data(todo_list_as_data, Map.merge(audience, additional)) do
|
||||
{:ok, %Actor{} = group} <- Actors.get_group_by_actor_id(group_id) do
|
||||
todo_list_as_data = Convertible.model_to_as(%{todo_list | actor: group})
|
||||
audience = %{"to" => [group.members_url], "cc" => []}
|
||||
create_data = make_create_data(todo_list_as_data, Map.merge(audience, additional))
|
||||
{:ok, todo_list, create_data}
|
||||
end
|
||||
end
|
||||
|
||||
@impl Entity
|
||||
@spec update(TodoList.t(), map, map) :: {:ok, TodoList.t(), Activity.t()} | any
|
||||
@spec update(TodoList.t(), map, map) ::
|
||||
{:ok, TodoList.t(), ActivityStream.t()}
|
||||
| {:error, Ecto.Changeset.t() | :group_not_found}
|
||||
def update(%TodoList{} = old_todo_list, args, additional) do
|
||||
with {:ok, %TodoList{actor_id: group_id} = todo_list} <-
|
||||
Todos.update_todo_list(old_todo_list, args),
|
||||
{:ok, %Actor{} = group} <- Actors.get_group_by_actor_id(group_id),
|
||||
todo_list_as_data <-
|
||||
Convertible.model_to_as(%{todo_list | actor: group}),
|
||||
audience <- %{"to" => [group.members_url], "cc" => []},
|
||||
update_data <-
|
||||
make_update_data(todo_list_as_data, Map.merge(audience, additional)) do
|
||||
{:ok, %Actor{} = group} <- Actors.get_group_by_actor_id(group_id) do
|
||||
todo_list_as_data = Convertible.model_to_as(%{todo_list | actor: group})
|
||||
audience = %{"to" => [group.members_url], "cc" => []}
|
||||
update_data = make_update_data(todo_list_as_data, Map.merge(audience, additional))
|
||||
{:ok, todo_list, update_data}
|
||||
end
|
||||
end
|
||||
|
||||
@impl Entity
|
||||
@spec delete(TodoList.t(), Actor.t(), boolean(), map()) ::
|
||||
{:ok, ActivityStream.t(), Actor.t(), TodoList.t()}
|
||||
{:ok, ActivityStream.t(), Actor.t(), TodoList.t()} | {:error, Ecto.Changeset.t()}
|
||||
def delete(
|
||||
%TodoList{url: url, actor: %Actor{url: group_url}} = todo_list,
|
||||
%Actor{url: actor_url} = actor,
|
||||
|
@ -59,16 +60,23 @@ defmodule Mobilizon.Federation.ActivityPub.Types.TodoLists do
|
|||
"to" => [group_url]
|
||||
}
|
||||
|
||||
with {:ok, _todo_list} <- Todos.delete_todo_list(todo_list),
|
||||
{:ok, true} <- Cachex.del(:activity_pub, "todo_list_#{todo_list.id}") do
|
||||
case Todos.delete_todo_list(todo_list) do
|
||||
{:ok, _todo_list} ->
|
||||
Cachex.del(:activity_pub, "todo_list_#{todo_list.id}")
|
||||
{:ok, activity_data, actor, todo_list}
|
||||
|
||||
{:error, %Ecto.Changeset{} = err} ->
|
||||
{:error, err}
|
||||
end
|
||||
end
|
||||
|
||||
@spec actor(TodoList.t()) :: nil
|
||||
def actor(%TodoList{}), do: nil
|
||||
|
||||
@spec group_actor(TodoList.t()) :: Actor.t() | nil
|
||||
def group_actor(%TodoList{actor_id: actor_id}), do: Actors.get_actor(actor_id)
|
||||
|
||||
@spec permissions(TodoList.t()) :: Permission.t()
|
||||
def permissions(%TodoList{}) do
|
||||
%Permission{access: :member, create: :member, update: :member, delete: :member}
|
||||
end
|
||||
|
|
|
@ -1,9 +1,12 @@
|
|||
defmodule Mobilizon.Federation.ActivityPub.Types.Todos do
|
||||
@moduledoc false
|
||||
@moduledoc """
|
||||
ActivityPub type handler for Todos
|
||||
"""
|
||||
alias Mobilizon.{Actors, Todos}
|
||||
alias Mobilizon.Actors.Actor
|
||||
alias Mobilizon.Federation.ActivityPub.Permission
|
||||
alias Mobilizon.Federation.ActivityPub.Types.Entity
|
||||
alias Mobilizon.Federation.ActivityStream
|
||||
alias Mobilizon.Federation.ActivityStream.Convertible
|
||||
alias Mobilizon.Todos.{Todo, TodoList}
|
||||
import Mobilizon.Federation.ActivityPub.Utils, only: [make_create_data: 2, make_update_data: 2]
|
||||
|
@ -12,41 +15,75 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Todos do
|
|||
@behaviour Entity
|
||||
|
||||
@impl Entity
|
||||
@spec create(map(), map()) :: {:ok, map()}
|
||||
@spec create(map(), map()) ::
|
||||
{:ok, Todo.t(), ActivityStream.t()} | {:error, Ecto.Changeset.t() | atom()}
|
||||
def create(args, additional) do
|
||||
with {:ok, %Todo{todo_list_id: todo_list_id, creator_id: creator_id} = todo} <-
|
||||
Todos.create_todo(args),
|
||||
%TodoList{actor_id: group_id} = todo_list <- Todos.get_todo_list(todo_list_id),
|
||||
%Actor{} = creator <- Actors.get_actor(creator_id),
|
||||
{:ok, %Actor{} = group} <- Actors.get_group_by_actor_id(group_id),
|
||||
todo <- %{todo | todo_list: %{todo_list | actor: group}, creator: creator},
|
||||
todo_as_data <-
|
||||
Convertible.model_to_as(todo),
|
||||
audience <- %{"to" => [group.members_url], "cc" => []},
|
||||
create_data <-
|
||||
make_create_data(todo_as_data, Map.merge(audience, additional)) do
|
||||
{:ok, %Actor{} = creator, %TodoList{} = todo_list, %Actor{} = group} <-
|
||||
creator_todo_list_and_group(creator_id, todo_list_id) do
|
||||
todo = %{todo | todo_list: %{todo_list | actor: group}, creator: creator}
|
||||
todo_as_data = Convertible.model_to_as(todo)
|
||||
audience = %{"to" => [group.members_url], "cc" => []}
|
||||
create_data = make_create_data(todo_as_data, Map.merge(audience, additional))
|
||||
{:ok, todo, create_data}
|
||||
end
|
||||
end
|
||||
|
||||
@impl Entity
|
||||
@spec update(Todo.t(), map, map) :: {:ok, Todo.t(), Activity.t()} | any
|
||||
@spec update(Todo.t(), map, map) ::
|
||||
{:ok, Todo.t(), ActivityStream.t()}
|
||||
| {:error, atom() | Ecto.Changeset.t()}
|
||||
def update(%Todo{} = old_todo, args, additional) do
|
||||
with {:ok, %Todo{todo_list_id: todo_list_id} = todo} <- Todos.update_todo(old_todo, args),
|
||||
%TodoList{actor_id: group_id} = todo_list <- Todos.get_todo_list(todo_list_id),
|
||||
{:ok, %Actor{} = group} <- Actors.get_group_by_actor_id(group_id),
|
||||
todo_as_data <-
|
||||
Convertible.model_to_as(%{todo | todo_list: %{todo_list | actor: group}}),
|
||||
audience <- %{"to" => [group.members_url], "cc" => []},
|
||||
update_data <-
|
||||
make_update_data(todo_as_data, Map.merge(audience, additional)) do
|
||||
{:ok, %TodoList{} = todo_list, %Actor{} = group} <- todo_list_and_group(todo_list_id) do
|
||||
todo_as_data = Convertible.model_to_as(%{todo | todo_list: %{todo_list | actor: group}})
|
||||
audience = %{"to" => [group.members_url], "cc" => []}
|
||||
update_data = make_update_data(todo_as_data, Map.merge(audience, additional))
|
||||
{:ok, todo, update_data}
|
||||
end
|
||||
end
|
||||
|
||||
@spec creator_todo_list_and_group(integer(), String.t()) ::
|
||||
{:ok, Actor.t(), TodoList.t(), Actor.t()}
|
||||
| {:error, :creator_not_found | :group_not_found | :todo_list_not_found}
|
||||
defp creator_todo_list_and_group(creator_id, todo_list_id) do
|
||||
case Actors.get_actor(creator_id) do
|
||||
%Actor{} = creator ->
|
||||
case todo_list_and_group(todo_list_id) do
|
||||
{:ok, %TodoList{} = todo_list, %Actor{} = group} ->
|
||||
{:ok, creator, todo_list, group}
|
||||
|
||||
{:error, err} ->
|
||||
{:error, err}
|
||||
end
|
||||
|
||||
nil ->
|
||||
{:error, :creator_not_found}
|
||||
end
|
||||
end
|
||||
|
||||
@spec todo_list_and_group(String.t()) ::
|
||||
{:ok, TodoList.t(), Actor.t()} | {:error, :group_not_found | :todo_list_not_found}
|
||||
defp todo_list_and_group(todo_list_id) do
|
||||
case Todos.get_todo_list(todo_list_id) do
|
||||
%TodoList{actor_id: group_id} = todo_list ->
|
||||
case Actors.get_group_by_actor_id(group_id) do
|
||||
{:ok, %Actor{} = group} ->
|
||||
{:ok, todo_list, group}
|
||||
|
||||
{:error, :group_not_found} ->
|
||||
{:error, :group_not_found}
|
||||
end
|
||||
|
||||
nil ->
|
||||
{:error, :todo_list_not_found}
|
||||
end
|
||||
end
|
||||
|
||||
@impl Entity
|
||||
@spec delete(Todo.t(), Actor.t(), boolean(), map()) ::
|
||||
{:ok, ActivityStream.t(), Actor.t(), Todo.t()}
|
||||
@spec delete(Todo.t(), Actor.t(), any(), any()) ::
|
||||
{:ok, ActivityStream.t(), Actor.t(), Todo.t()} | {:error, Ecto.Changeset.t()}
|
||||
def delete(
|
||||
%Todo{url: url, creator: %Actor{url: group_url}} = todo,
|
||||
%Actor{url: actor_url} = actor,
|
||||
|
@ -59,18 +96,24 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Todos do
|
|||
"actor" => actor_url,
|
||||
"type" => "Delete",
|
||||
"object" => Convertible.model_to_as(url),
|
||||
"id" => url <> "/delete",
|
||||
"id" => "#{url}/delete",
|
||||
"to" => [group_url]
|
||||
}
|
||||
|
||||
with {:ok, _todo} <- Todos.delete_todo(todo),
|
||||
{:ok, true} <- Cachex.del(:activity_pub, "todo_#{todo.id}") do
|
||||
case Todos.delete_todo(todo) do
|
||||
{:ok, _todo} ->
|
||||
Cachex.del(:activity_pub, "todo_#{todo.id}")
|
||||
{:ok, activity_data, actor, todo}
|
||||
|
||||
{:error, %Ecto.Changeset{} = err} ->
|
||||
{:error, err}
|
||||
end
|
||||
end
|
||||
|
||||
@spec actor(Todo.t()) :: Actor.t() | nil
|
||||
def actor(%Todo{creator_id: creator_id}), do: Actors.get_actor(creator_id)
|
||||
|
||||
@spec group_actor(Todo.t()) :: Actor.t() | nil
|
||||
def group_actor(%Todo{todo_list_id: todo_list_id}) do
|
||||
case Todos.get_todo_list(todo_list_id) do
|
||||
%TodoList{actor_id: group_id} ->
|
||||
|
@ -81,6 +124,7 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Todos do
|
|||
end
|
||||
end
|
||||
|
||||
@spec permissions(Todo.t()) :: Permission.t()
|
||||
def permissions(%Todo{}) do
|
||||
%Permission{access: :member, create: :member, update: :member, delete: :member}
|
||||
end
|
||||
|
|
|
@ -4,6 +4,7 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Tombstones do
|
|||
alias Mobilizon.Actors.Actor
|
||||
alias Mobilizon.Federation.ActivityPub.Permission
|
||||
|
||||
@spec actor(Tombstone.t()) :: Actor.t() | nil
|
||||
def actor(%Tombstone{actor: %Actor{id: actor_id}}), do: Actors.get_actor(actor_id)
|
||||
|
||||
def actor(%Tombstone{actor_id: actor_id}) when not is_nil(actor_id),
|
||||
|
@ -11,8 +12,10 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Tombstones do
|
|||
|
||||
def actor(_), do: nil
|
||||
|
||||
@spec group_actor(any()) :: nil
|
||||
def group_actor(_), do: nil
|
||||
|
||||
@spec permissions(any()) :: Permission.t()
|
||||
def permissions(_) do
|
||||
%Permission{access: nil, create: nil, update: nil, delete: nil}
|
||||
end
|
||||
|
|
|
@ -12,8 +12,7 @@ defmodule Mobilizon.Federation.ActivityPub.Utils do
|
|||
alias Mobilizon.Actors.Actor
|
||||
alias Mobilizon.Medias.Media
|
||||
|
||||
alias Mobilizon.Federation.ActivityPub
|
||||
alias Mobilizon.Federation.ActivityPub.{Activity, Federator, Relay}
|
||||
alias Mobilizon.Federation.ActivityPub.{Actions, Activity, Federator, Relay}
|
||||
alias Mobilizon.Federation.ActivityPub.Actor, as: ActivityPubActor
|
||||
alias Mobilizon.Federation.ActivityStream.Converter
|
||||
alias Mobilizon.Federation.HTTPSignatures
|
||||
|
@ -23,13 +22,35 @@ defmodule Mobilizon.Federation.ActivityPub.Utils do
|
|||
|
||||
@actor_types ["Group", "Person", "Application"]
|
||||
|
||||
# Wraps an object into an activity
|
||||
@spec create_activity(map(), boolean()) :: {:ok, Activity.t()}
|
||||
def create_activity(map, local) when is_map(map) do
|
||||
with map <- lazy_put_activity_defaults(map) do
|
||||
{:ok,
|
||||
%Activity{
|
||||
data: map,
|
||||
local: local,
|
||||
actor: map["actor"],
|
||||
recipients: get_recipients(map)
|
||||
}}
|
||||
end
|
||||
end
|
||||
|
||||
# Get recipients for an activity or object
|
||||
@spec get_recipients(map()) :: list()
|
||||
defp get_recipients(data) do
|
||||
Map.get(data, "to", []) ++ Map.get(data, "cc", [])
|
||||
end
|
||||
|
||||
# Some implementations send the actor URI as the actor field, others send the entire actor object,
|
||||
# so figure out what the actor's URI is based on what we have.
|
||||
@spec get_url(map() | String.t() | list(String.t()) | any()) :: String.t() | nil
|
||||
def get_url(%{"id" => id}), do: id
|
||||
def get_url(id) when is_binary(id), do: id
|
||||
def get_url(ids) when is_list(ids), do: get_url(hd(ids))
|
||||
def get_url(_), do: nil
|
||||
|
||||
@spec make_json_ld_header :: map()
|
||||
def make_json_ld_header do
|
||||
%{
|
||||
"@context" => [
|
||||
|
@ -99,6 +120,7 @@ defmodule Mobilizon.Federation.ActivityPub.Utils do
|
|||
}
|
||||
end
|
||||
|
||||
@spec make_date :: String.t()
|
||||
def make_date do
|
||||
DateTime.utc_now() |> DateTime.truncate(:second) |> DateTime.to_iso8601()
|
||||
end
|
||||
|
@ -106,6 +128,7 @@ defmodule Mobilizon.Federation.ActivityPub.Utils do
|
|||
@doc """
|
||||
Enqueues an activity for federation if it's local
|
||||
"""
|
||||
@spec maybe_federate(activity :: Activity.t()) :: :ok
|
||||
def maybe_federate(%Activity{local: true} = activity) do
|
||||
Logger.debug("Maybe federate an activity")
|
||||
|
||||
|
@ -129,6 +152,7 @@ defmodule Mobilizon.Federation.ActivityPub.Utils do
|
|||
Applies to activities sent by group members from outside this instance to a group of this instance,
|
||||
we then need to relay (`Announce`) the object to other members on other instances.
|
||||
"""
|
||||
@spec maybe_relay_if_group_activity(Activity.t(), Actor.t() | nil | list(Actor.t())) :: :ok
|
||||
def maybe_relay_if_group_activity(activity, attributed_to \\ nil)
|
||||
|
||||
def maybe_relay_if_group_activity(
|
||||
|
@ -144,7 +168,7 @@ defmodule Mobilizon.Federation.ActivityPub.Utils do
|
|||
%Activity{data: %{"object" => object}},
|
||||
%Actor{url: attributed_to_url}
|
||||
)
|
||||
when is_binary(object) do
|
||||
when is_binary(object) and is_binary(attributed_to_url) do
|
||||
do_maybe_relay_if_group_activity(object, attributed_to_url)
|
||||
end
|
||||
|
||||
|
@ -152,6 +176,7 @@ defmodule Mobilizon.Federation.ActivityPub.Utils do
|
|||
:ok
|
||||
end
|
||||
|
||||
@spec do_maybe_relay_if_group_activity(map(), list(String.t()) | String.t()) :: :ok
|
||||
defp do_maybe_relay_if_group_activity(object, attributed_to) when is_list(attributed_to),
|
||||
do: do_maybe_relay_if_group_activity(object, hd(attributed_to))
|
||||
|
||||
|
@ -160,17 +185,17 @@ defmodule Mobilizon.Federation.ActivityPub.Utils do
|
|||
|
||||
case Actors.get_local_group_by_url(attributed_to) do
|
||||
%Actor{} = group ->
|
||||
case ActivityPub.announce(group, object, id, true, false) do
|
||||
case Actions.Announce.announce(group, object, id, true, false) do
|
||||
{:ok, _activity, _object} ->
|
||||
Logger.info("Forwarded activity to external members of the group")
|
||||
:ok
|
||||
|
||||
_ ->
|
||||
{:error, _err} ->
|
||||
Logger.info("Failed to forward activity to external members of the group")
|
||||
:error
|
||||
end
|
||||
|
||||
_ ->
|
||||
nil ->
|
||||
:ok
|
||||
end
|
||||
end
|
||||
|
@ -198,6 +223,7 @@ defmodule Mobilizon.Federation.ActivityPub.Utils do
|
|||
Adds an id and a published data if they aren't there,
|
||||
also adds it to an included object
|
||||
"""
|
||||
@spec lazy_put_activity_defaults(map()) :: map()
|
||||
def lazy_put_activity_defaults(%{"object" => _object} = map) do
|
||||
if is_map(map["object"]) do
|
||||
object = lazy_put_object_defaults(map["object"])
|
||||
|
@ -214,6 +240,7 @@ defmodule Mobilizon.Federation.ActivityPub.Utils do
|
|||
Map.put_new_lazy(map, "published", &make_date/0)
|
||||
end
|
||||
|
||||
@spec get_actor(map()) :: String.t() | nil
|
||||
def get_actor(%{"actor" => actor}) when is_binary(actor) do
|
||||
actor
|
||||
end
|
||||
|
@ -241,6 +268,7 @@ defmodule Mobilizon.Federation.ActivityPub.Utils do
|
|||
|
||||
Takes the actor or attributedTo attributes (considers only the first elem if they're an array)
|
||||
"""
|
||||
@spec origin_check?(String.t(), map()) :: boolean()
|
||||
def origin_check?(id, %{"type" => "Tombstone", "id" => tombstone_id}), do: id == tombstone_id
|
||||
|
||||
def origin_check?(id, %{"actor" => actor, "attributedTo" => _attributed_to} = params)
|
||||
|
@ -282,6 +310,7 @@ defmodule Mobilizon.Federation.ActivityPub.Utils do
|
|||
compare_uris?(uri_1, uri_2)
|
||||
end
|
||||
|
||||
@spec compare_uris?(URI.t(), URI.t()) :: boolean()
|
||||
defp compare_uris?(%URI{} = id_uri, %URI{} = other_uri),
|
||||
do: id_uri.host == other_uri.host && id_uri.port == other_uri.port
|
||||
|
||||
|
@ -311,7 +340,7 @@ defmodule Mobilizon.Federation.ActivityPub.Utils do
|
|||
{:ok, media} ->
|
||||
media
|
||||
|
||||
_ ->
|
||||
{:error, _err} ->
|
||||
nil
|
||||
end
|
||||
end
|
||||
|
@ -509,7 +538,7 @@ defmodule Mobilizon.Federation.ActivityPub.Utils do
|
|||
@doc """
|
||||
Make add activity data
|
||||
"""
|
||||
@spec make_add_data(map(), map()) :: map()
|
||||
@spec make_add_data(map(), map(), map()) :: map()
|
||||
def make_add_data(object, target, additional \\ %{}) do
|
||||
Logger.debug("Making add data")
|
||||
Logger.debug(inspect(object))
|
||||
|
@ -530,7 +559,7 @@ defmodule Mobilizon.Federation.ActivityPub.Utils do
|
|||
@doc """
|
||||
Make move activity data
|
||||
"""
|
||||
@spec make_add_data(map(), map()) :: map()
|
||||
@spec make_move_data(map(), map(), map(), map()) :: map()
|
||||
def make_move_data(object, origin, target, additional \\ %{}) do
|
||||
Logger.debug("Making move data")
|
||||
Logger.debug(inspect(object))
|
||||
|
@ -554,6 +583,7 @@ defmodule Mobilizon.Federation.ActivityPub.Utils do
|
|||
@doc """
|
||||
Converts PEM encoded keys to a public key representation
|
||||
"""
|
||||
@spec pem_to_public_key(String.t()) :: {:RSAPublicKey, any(), any()}
|
||||
def pem_to_public_key(pem) do
|
||||
[key_code] = :public_key.pem_decode(pem)
|
||||
key = :public_key.pem_entry_decode(key_code)
|
||||
|
@ -567,6 +597,7 @@ defmodule Mobilizon.Federation.ActivityPub.Utils do
|
|||
end
|
||||
end
|
||||
|
||||
@spec pem_to_public_key_pem(String.t()) :: String.t()
|
||||
def pem_to_public_key_pem(pem) do
|
||||
public_key = pem_to_public_key(pem)
|
||||
public_key = :public_key.pem_entry_encode(:RSAPublicKey, public_key)
|
||||
|
|
|
@ -3,5 +3,5 @@ defmodule Mobilizon.Federation.ActivityStream do
|
|||
The ActivityStream Type
|
||||
"""
|
||||
|
||||
@type t :: map()
|
||||
@type t :: %{String.t() => String.t() | list(String.t()) | map() | nil}
|
||||
end
|
||||
|
|
|
@ -29,7 +29,7 @@ defmodule Mobilizon.Federation.ActivityStream.Converter.Actor do
|
|||
Converts an AP object data to our internal data structure.
|
||||
"""
|
||||
@impl Converter
|
||||
@spec as_to_model_data(map()) :: {:ok, map()}
|
||||
@spec as_to_model_data(map()) :: map() | {:error, :actor_not_allowed_type}
|
||||
def as_to_model_data(%{"type" => type} = data) when type in @allowed_types do
|
||||
avatar =
|
||||
download_picture(get_in(data, ["icon", "url"]), get_in(data, ["icon", "name"]), "avatar")
|
||||
|
@ -64,7 +64,7 @@ defmodule Mobilizon.Federation.ActivityStream.Converter.Actor do
|
|||
}
|
||||
end
|
||||
|
||||
def as_to_model_data(_), do: :error
|
||||
def as_to_model_data(_), do: {:error, :actor_not_allowed_type}
|
||||
|
||||
@doc """
|
||||
Convert an actor struct to an ActivityStream representation.
|
||||
|
@ -135,7 +135,7 @@ defmodule Mobilizon.Federation.ActivityStream.Converter.Actor do
|
|||
end
|
||||
end
|
||||
|
||||
@spec download_picture(String.t() | nil, String.t(), String.t()) :: map()
|
||||
@spec download_picture(String.t() | nil, String.t(), String.t()) :: map() | nil
|
||||
defp download_picture(nil, _name, _default_name), do: nil
|
||||
|
||||
defp download_picture(url, name, default_name) do
|
||||
|
|
|
@ -38,18 +38,15 @@ defmodule Mobilizon.Federation.ActivityStream.Converter.Comment do
|
|||
Converts an AP object data to our internal data structure.
|
||||
"""
|
||||
@impl Converter
|
||||
@spec as_to_model_data(map) :: {:ok, map} | {:error, any()}
|
||||
@spec as_to_model_data(map) :: map | {:error, atom}
|
||||
def as_to_model_data(object) do
|
||||
Logger.debug("We're converting raw ActivityStream data to a comment entity")
|
||||
Logger.debug(inspect(object))
|
||||
|
||||
with {%Actor{id: actor_id, domain: actor_domain}, attributed_to} <-
|
||||
maybe_fetch_actor_and_attributed_to_id(object),
|
||||
{:tags, tags} <- {:tags, fetch_tags(Map.get(object, "tag", []))},
|
||||
{:mentions, mentions} <-
|
||||
{:mentions, fetch_mentions(Map.get(object, "tag", []))},
|
||||
discussion <-
|
||||
Discussions.get_discussion_by_url(Map.get(object, "context")) do
|
||||
tag_object = Map.get(object, "tag", [])
|
||||
|
||||
case maybe_fetch_actor_and_attributed_to_id(object) do
|
||||
{:ok, %Actor{id: actor_id, domain: actor_domain}, attributed_to} ->
|
||||
Logger.debug("Inserting full comment")
|
||||
Logger.debug(inspect(object))
|
||||
|
||||
|
@ -64,9 +61,9 @@ defmodule Mobilizon.Federation.ActivityStream.Converter.Comment do
|
|||
in_reply_to_comment_id: nil,
|
||||
event_id: nil,
|
||||
uuid: object["uuid"],
|
||||
discussion_id: if(is_nil(discussion), do: nil, else: discussion.id),
|
||||
tags: tags,
|
||||
mentions: mentions,
|
||||
discussion_id: get_discussion_id(object),
|
||||
tags: fetch_tags(tag_object),
|
||||
mentions: fetch_mentions(tag_object),
|
||||
local: is_nil(actor_domain),
|
||||
visibility: if(Visibility.is_public?(object), do: :public, else: :private),
|
||||
published_at: object["published"],
|
||||
|
@ -81,9 +78,9 @@ defmodule Mobilizon.Federation.ActivityStream.Converter.Comment do
|
|||
Logger.debug("Converted object after fetching parents")
|
||||
Logger.debug(inspect(data))
|
||||
data
|
||||
else
|
||||
{:ok, %Actor{suspended: true}} ->
|
||||
:error
|
||||
|
||||
{:error, err} ->
|
||||
{:error, err}
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -94,9 +91,20 @@ defmodule Mobilizon.Federation.ActivityStream.Converter.Comment do
|
|||
"""
|
||||
@impl Converter
|
||||
@spec model_to_as(CommentModel.t()) :: map
|
||||
def model_to_as(%CommentModel{deleted_at: nil} = comment) do
|
||||
def model_to_as(
|
||||
%CommentModel{
|
||||
deleted_at: nil,
|
||||
attributed_to: attributed_to,
|
||||
actor: %Actor{url: comment_actor_url}
|
||||
} = comment
|
||||
) do
|
||||
to = determine_to(comment)
|
||||
|
||||
attributed_to =
|
||||
if is_nil(attributed_to),
|
||||
do: comment_actor_url,
|
||||
else: Map.get(attributed_to, :url, comment_actor_url)
|
||||
|
||||
object = %{
|
||||
"type" => "Note",
|
||||
"to" => to,
|
||||
|
@ -104,9 +112,7 @@ defmodule Mobilizon.Federation.ActivityStream.Converter.Comment do
|
|||
"content" => comment.text,
|
||||
"mediaType" => "text/html",
|
||||
"actor" => comment.actor.url,
|
||||
"attributedTo" =>
|
||||
if(is_nil(comment.attributed_to), do: nil, else: comment.attributed_to.url) ||
|
||||
comment.actor.url,
|
||||
"attributedTo" => attributed_to,
|
||||
"uuid" => comment.uuid,
|
||||
"id" => comment.url,
|
||||
"tag" => build_mentions(comment.mentions) ++ build_tags(comment.tags),
|
||||
|
@ -132,7 +138,6 @@ defmodule Mobilizon.Federation.ActivityStream.Converter.Comment do
|
|||
end
|
||||
|
||||
@impl Converter
|
||||
@spec model_to_as(CommentModel.t()) :: map
|
||||
def model_to_as(%CommentModel{} = comment) do
|
||||
Convertible.model_to_as(%TombstoneModel{
|
||||
uri: comment.url,
|
||||
|
@ -203,4 +208,13 @@ defmodule Mobilizon.Federation.ActivityStream.Converter.Comment do
|
|||
data
|
||||
end
|
||||
end
|
||||
|
||||
defp get_discussion_id(%{"context" => context}) do
|
||||
case Discussions.get_discussion_by_url(context) do
|
||||
%Discussion{id: discussion_id} -> discussion_id
|
||||
nil -> nil
|
||||
end
|
||||
end
|
||||
|
||||
defp get_discussion_id(_object), do: nil
|
||||
end
|
||||
|
|
|
@ -8,6 +8,6 @@ defmodule Mobilizon.Federation.ActivityStream.Converter do
|
|||
|
||||
@type model_data :: map()
|
||||
|
||||
@callback as_to_model_data(as_data :: ActivityStream.t()) :: model_data()
|
||||
@callback as_to_model_data(as_data :: ActivityStream.t()) :: model_data() | {:error, any()}
|
||||
@callback model_to_as(model :: struct()) :: ActivityStream.t()
|
||||
end
|
||||
|
|
|
@ -12,6 +12,7 @@ defmodule Mobilizon.Federation.ActivityStream.Converter.Discussion do
|
|||
alias Mobilizon.Federation.ActivityStream.{Converter, Convertible}
|
||||
alias Mobilizon.Federation.ActivityStream.Converter.Discussion, as: DiscussionConverter
|
||||
alias Mobilizon.Storage.Repo
|
||||
import Mobilizon.Service.Guards, only: [is_valid_string: 1]
|
||||
|
||||
require Logger
|
||||
|
||||
|
@ -45,20 +46,28 @@ defmodule Mobilizon.Federation.ActivityStream.Converter.Discussion do
|
|||
end
|
||||
|
||||
@impl Converter
|
||||
@spec as_to_model_data(map) :: {:ok, map} | {:error, any()}
|
||||
def as_to_model_data(%{"type" => "Note", "name" => name} = object) when not is_nil(name) do
|
||||
with creator_url <- Map.get(object, "actor"),
|
||||
{:ok, %Actor{id: creator_id, suspended: false}} <-
|
||||
@spec as_to_model_data(map) :: map() | {:error, atom()}
|
||||
def as_to_model_data(%{"type" => "Note", "name" => name} = object) when is_valid_string(name) do
|
||||
case extract_actors(object) do
|
||||
%{actor_id: actor_id, creator_id: creator_id} ->
|
||||
%{actor_id: actor_id, creator_id: creator_id, title: name, url: object["id"]}
|
||||
|
||||
{:error, error} ->
|
||||
{:error, error}
|
||||
end
|
||||
end
|
||||
|
||||
@spec extract_actors(map()) ::
|
||||
%{actor_id: String.t(), creator_id: String.t()} | {:error, atom()}
|
||||
defp extract_actors(%{"actor" => creator_url, "attributedTo" => actor_url} = _object)
|
||||
when is_valid_string(creator_url) and is_valid_string(actor_url) do
|
||||
with {:ok, %Actor{id: creator_id, suspended: false}} <-
|
||||
ActivityPubActor.get_or_fetch_actor_by_url(creator_url),
|
||||
actor_url <- Map.get(object, "attributedTo"),
|
||||
{:ok, %Actor{id: actor_id, suspended: false}} <-
|
||||
ActivityPubActor.get_or_fetch_actor_by_url(actor_url) do
|
||||
%{
|
||||
title: name,
|
||||
actor_id: actor_id,
|
||||
creator_id: creator_id,
|
||||
url: object["id"]
|
||||
}
|
||||
%{actor_id: actor_id, creator_id: creator_id}
|
||||
else
|
||||
{:error, error} -> {:error, error}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -45,19 +45,20 @@ defmodule Mobilizon.Federation.ActivityStream.Converter.Event do
|
|||
Converts an AP object data to our internal data structure.
|
||||
"""
|
||||
@impl Converter
|
||||
@spec as_to_model_data(map) :: {:ok, map()} | {:error, any()}
|
||||
@spec as_to_model_data(map) :: map() | {:error, atom()}
|
||||
def as_to_model_data(object) do
|
||||
with {%Actor{id: actor_id}, attributed_to} <-
|
||||
maybe_fetch_actor_and_attributed_to_id(object),
|
||||
{:address, address_id} <-
|
||||
{:address, get_address(object["location"])},
|
||||
{:tags, tags} <- {:tags, fetch_tags(object["tag"])},
|
||||
{:mentions, mentions} <- {:mentions, fetch_mentions(object["tag"])},
|
||||
{:visibility, visibility} <- {:visibility, get_visibility(object)},
|
||||
{:options, options} <- {:options, get_options(object)},
|
||||
{:metadata, metadata} <- {:metadata, get_metdata(object)},
|
||||
[description: description, picture_id: picture_id, medias: medias] <-
|
||||
process_pictures(object, actor_id) do
|
||||
case maybe_fetch_actor_and_attributed_to_id(object) do
|
||||
{:ok, %Actor{id: actor_id}, attributed_to} ->
|
||||
address_id = get_address(object["location"])
|
||||
tags = fetch_tags(object["tag"])
|
||||
mentions = fetch_mentions(object["tag"])
|
||||
visibility = get_visibility(object)
|
||||
options = get_options(object)
|
||||
metadata = get_metdata(object)
|
||||
|
||||
[description: description, picture_id: picture_id, medias: medias] =
|
||||
process_pictures(object, actor_id)
|
||||
|
||||
%{
|
||||
title: object["name"],
|
||||
description: description,
|
||||
|
@ -86,9 +87,9 @@ defmodule Mobilizon.Federation.ActivityStream.Converter.Event do
|
|||
publish_at: object["published"],
|
||||
language: object["inLanguage"]
|
||||
}
|
||||
else
|
||||
{:ok, %Actor{suspended: true}} ->
|
||||
:error
|
||||
|
||||
{:error, err} ->
|
||||
{:error, err}
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -4,9 +4,11 @@ defmodule Mobilizon.Federation.ActivityStream.Converter.EventMetadata do
|
|||
"""
|
||||
|
||||
alias Mobilizon.Events.EventMetadata
|
||||
alias Mobilizon.Federation.ActivityStream
|
||||
|
||||
@property_value "PropertyValue"
|
||||
|
||||
@spec metadata_to_as(EventMetadata.t()) :: map()
|
||||
def metadata_to_as(%EventMetadata{type: :boolean, value: value, key: key})
|
||||
when value in ["true", "false"] do
|
||||
%{
|
||||
|
@ -47,6 +49,7 @@ defmodule Mobilizon.Federation.ActivityStream.Converter.EventMetadata do
|
|||
)
|
||||
end
|
||||
|
||||
@spec as_to_metadata(ActivityStream.t()) :: map()
|
||||
def as_to_metadata(%{"type" => @property_value, "propertyID" => key, "value" => value})
|
||||
when is_boolean(value) do
|
||||
%{type: :boolean, key: key, value: to_string(value)}
|
||||
|
|
|
@ -6,6 +6,7 @@ defmodule Mobilizon.Federation.ActivityStream.Converter.Media do
|
|||
internal one, and back.
|
||||
"""
|
||||
|
||||
alias Mobilizon.Federation.ActivityStream
|
||||
alias Mobilizon.Medias
|
||||
alias Mobilizon.Medias.Media, as: MediaModel
|
||||
|
||||
|
@ -18,7 +19,7 @@ defmodule Mobilizon.Federation.ActivityStream.Converter.Media do
|
|||
@doc """
|
||||
Convert a media struct to an ActivityStream representation.
|
||||
"""
|
||||
@spec model_to_as(MediaModel.t()) :: map
|
||||
@spec model_to_as(MediaModel.t()) :: ActivityStream.t()
|
||||
def model_to_as(%MediaModel{file: file}) do
|
||||
%{
|
||||
"type" => "Document",
|
||||
|
@ -31,29 +32,53 @@ defmodule Mobilizon.Federation.ActivityStream.Converter.Media do
|
|||
@doc """
|
||||
Save media data from raw data and return AS Link data.
|
||||
"""
|
||||
@spec find_or_create_media(map(), String.t() | integer()) ::
|
||||
{:ok, MediaModel.t()} | {:error, atom() | String.t() | Ecto.Changeset.t()}
|
||||
def find_or_create_media(%{"type" => "Link", "href" => url}, actor_id),
|
||||
do: find_or_create_media(url, actor_id)
|
||||
do:
|
||||
find_or_create_media(
|
||||
%{"type" => "Document", "url" => url, "name" => "External media"},
|
||||
actor_id
|
||||
)
|
||||
|
||||
def find_or_create_media(
|
||||
%{"type" => "Document", "url" => media_url, "name" => name},
|
||||
actor_id
|
||||
)
|
||||
when is_binary(media_url) do
|
||||
with {:ok, %{body: body}} <- Tesla.get(media_url, opts: @http_options),
|
||||
{:ok, %{url: url} = uploaded} <-
|
||||
Upload.store(%{body: body, name: name}),
|
||||
{:media_exists, nil} <- {:media_exists, Medias.get_media_by_url(url)} do
|
||||
case upload_media(media_url, name) do
|
||||
{:error, err} ->
|
||||
{:error, err}
|
||||
|
||||
{:ok, %{url: url} = uploaded} ->
|
||||
case Medias.get_media_by_url(url) do
|
||||
%MediaModel{file: _file} = media ->
|
||||
{:ok, media}
|
||||
|
||||
nil ->
|
||||
Medias.create_media(%{
|
||||
file: Map.take(uploaded, [:url, :name, :content_type, :size]),
|
||||
metadata: Map.take(uploaded, [:width, :height, :blurhash]),
|
||||
actor_id: actor_id
|
||||
})
|
||||
else
|
||||
{:media_exists, %MediaModel{file: _file} = media} ->
|
||||
{:ok, media}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
err ->
|
||||
err
|
||||
@spec upload_media(String.t(), String.t()) :: {:ok, map()} | {:error, atom() | String.t()}
|
||||
defp upload_media(media_url, name) do
|
||||
case Tesla.get(media_url, opts: @http_options) do
|
||||
{:ok, %{body: body}} ->
|
||||
case Upload.store(%{body: body, name: name}) do
|
||||
{:ok, %{url: _url} = uploaded} ->
|
||||
{:ok, uploaded}
|
||||
|
||||
{:error, err} ->
|
||||
{:error, err}
|
||||
end
|
||||
|
||||
{:error, err} ->
|
||||
{:error, err}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -33,6 +33,7 @@ defmodule Mobilizon.Federation.ActivityStream.Converter.Member do
|
|||
}
|
||||
end
|
||||
|
||||
@spec as_to_model_data(map()) :: map()
|
||||
def as_to_model_data(%{
|
||||
"type" => "Member",
|
||||
"actor" => actor,
|
||||
|
|
|
@ -18,6 +18,8 @@ defmodule Mobilizon.Federation.ActivityStream.Converter.Post do
|
|||
process_pictures: 2
|
||||
]
|
||||
|
||||
import Mobilizon.Service.Guards, only: [is_valid_string: 1]
|
||||
|
||||
@behaviour Converter
|
||||
|
||||
defimpl Convertible, for: Post do
|
||||
|
@ -63,15 +65,15 @@ defmodule Mobilizon.Federation.ActivityStream.Converter.Post do
|
|||
Converts an AP object data to our internal data structure.
|
||||
"""
|
||||
@impl Converter
|
||||
@spec as_to_model_data(map) :: {:ok, map} | {:error, any()}
|
||||
@spec as_to_model_data(map) :: map() | {:error, any()}
|
||||
def as_to_model_data(
|
||||
%{"type" => "Article", "actor" => creator, "attributedTo" => group_uri} = object
|
||||
) do
|
||||
with {:ok, %Actor{id: attributed_to_id} = group} <- get_actor(group_uri),
|
||||
{:ok, %Actor{id: author_id}} <- get_actor(creator),
|
||||
{:visibility, visibility} <- {:visibility, get_visibility(object, group)},
|
||||
[description: description, picture_id: picture_id, medias: medias] <-
|
||||
process_pictures(object, attributed_to_id) do
|
||||
{:ok, %Actor{id: author_id}} <- get_actor(creator) do
|
||||
[description: description, picture_id: picture_id, medias: medias] =
|
||||
process_pictures(object, attributed_to_id)
|
||||
|
||||
%{
|
||||
title: object["name"],
|
||||
body: description,
|
||||
|
@ -82,7 +84,7 @@ defmodule Mobilizon.Federation.ActivityStream.Converter.Post do
|
|||
publish_at: object["published"],
|
||||
picture_id: picture_id,
|
||||
medias: medias,
|
||||
visibility: visibility,
|
||||
visibility: get_visibility(object, group),
|
||||
draft: object["draft"] == true
|
||||
}
|
||||
else
|
||||
|
@ -92,11 +94,12 @@ defmodule Mobilizon.Federation.ActivityStream.Converter.Post do
|
|||
end
|
||||
|
||||
@spec get_actor(String.t() | map() | nil) :: {:ok, Actor.t()} | {:error, String.t()}
|
||||
defp get_actor(nil), do: {:error, "nil property found for actor data"}
|
||||
|
||||
defp get_actor(actor),
|
||||
defp get_actor(actor) when is_valid_string(actor),
|
||||
do: actor |> Utils.get_url() |> ActivityPubActor.get_or_fetch_actor_by_url()
|
||||
|
||||
defp get_actor(_), do: {:error, "nil property found for actor data"}
|
||||
|
||||
@spec to_date(DateTime.t() | NaiveDateTime.t() | nil) :: String.t() | nil
|
||||
defp to_date(nil), do: nil
|
||||
defp to_date(%DateTime{} = date), do: DateTime.to_iso8601(date)
|
||||
defp to_date(%NaiveDateTime{} = date), do: NaiveDateTime.to_iso8601(date)
|
||||
|
|
|
@ -56,18 +56,17 @@ defmodule Mobilizon.Federation.ActivityStream.Converter.Resource do
|
|||
Converts an AP object data to our internal data structure.
|
||||
"""
|
||||
@impl Converter
|
||||
@spec as_to_model_data(map) :: {:ok, map} | {:error, any()}
|
||||
@spec as_to_model_data(map) :: map() | {:error, any()}
|
||||
def as_to_model_data(%{"type" => type, "actor" => creator, "attributedTo" => group} = object) do
|
||||
with {:ok, %Actor{id: actor_id, resources_url: resources_url}} <- get_actor(group),
|
||||
{:ok, %Actor{id: creator_id}} <- get_actor(creator),
|
||||
parent_id <- get_parent_id(object["context"], resources_url) do
|
||||
{:ok, %Actor{id: creator_id}} <- get_actor(creator) do
|
||||
data = %{
|
||||
title: object["name"],
|
||||
summary: object["summary"],
|
||||
url: object["id"],
|
||||
actor_id: actor_id,
|
||||
creator_id: creator_id,
|
||||
parent_id: parent_id,
|
||||
parent_id: get_parent_id(object["context"], resources_url),
|
||||
published_at: object["published"]
|
||||
}
|
||||
|
||||
|
|
|
@ -47,14 +47,14 @@ defmodule Mobilizon.Federation.ActivityStream.Converter.Todo do
|
|||
Converts an AP object data to our internal data structure.
|
||||
"""
|
||||
@impl Converter
|
||||
@spec as_to_model_data(map) :: {:ok, map} | {:error, any()}
|
||||
@spec as_to_model_data(map) :: map() | {:error, any()}
|
||||
def as_to_model_data(
|
||||
%{"type" => "Todo", "actor" => actor_url, "todoList" => todo_list_url} = object
|
||||
) do
|
||||
with {:ok, %Actor{id: creator_id} = _creator} <-
|
||||
ActivityPubActor.get_or_fetch_actor_by_url(actor_url),
|
||||
{:todo_list, %TodoList{id: todo_list_id}} <-
|
||||
{:todo_list, Todos.get_todo_list_by_url(todo_list_url)} do
|
||||
case ActivityPubActor.get_or_fetch_actor_by_url(actor_url) do
|
||||
{:ok, %Actor{id: creator_id} = _creator} ->
|
||||
case Todos.get_todo_list_by_url(todo_list_url) do
|
||||
%TodoList{id: todo_list_id} ->
|
||||
%{
|
||||
title: object["name"],
|
||||
status: object["status"],
|
||||
|
@ -63,11 +63,22 @@ defmodule Mobilizon.Federation.ActivityStream.Converter.Todo do
|
|||
creator_id: creator_id,
|
||||
published_at: object["published"]
|
||||
}
|
||||
else
|
||||
{:todo_list, nil} ->
|
||||
with {:ok, %TodoList{}} <- ActivityPub.fetch_object_from_url(todo_list_url) do
|
||||
|
||||
nil ->
|
||||
case ActivityPub.fetch_object_from_url(todo_list_url) do
|
||||
{:ok, _, %TodoList{}} ->
|
||||
as_to_model_data(object)
|
||||
|
||||
{:ok, %TodoList{}} ->
|
||||
as_to_model_data(object)
|
||||
|
||||
{:error, err} ->
|
||||
{:error, err}
|
||||
end
|
||||
end
|
||||
|
||||
{:error, err} ->
|
||||
{:error, err}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -37,7 +37,7 @@ defmodule Mobilizon.Federation.ActivityStream.Converter.TodoList do
|
|||
Converts an AP object data to our internal data structure.
|
||||
"""
|
||||
@impl Converter
|
||||
@spec as_to_model_data(map) :: {:ok, map} | {:error, any()}
|
||||
@spec as_to_model_data(map) :: map() | {:error, :group_not_found}
|
||||
def as_to_model_data(%{"type" => "TodoList", "actor" => actor_url} = object) do
|
||||
case ActivityPubActor.get_or_fetch_actor_by_url(actor_url) do
|
||||
{:ok, %Actor{type: :Group, id: group_id} = _group} ->
|
||||
|
|
|
@ -111,7 +111,6 @@ defmodule Mobilizon.Federation.ActivityStream.Converter.Utils do
|
|||
acc ++ [%{actor_id: actor_id}]
|
||||
end
|
||||
|
||||
@spec create_mention(map(), list()) :: list()
|
||||
defp create_mention(mention, acc) when is_map(mention) do
|
||||
with true <- mention["type"] == "Mention",
|
||||
{:ok, %Actor{id: actor_id}} <-
|
||||
|
@ -128,22 +127,34 @@ defmodule Mobilizon.Federation.ActivityStream.Converter.Utils do
|
|||
create_mention(mention, acc)
|
||||
end
|
||||
|
||||
@spec maybe_fetch_actor_and_attributed_to_id(map()) :: {Actor.t() | nil, Actor.t() | nil}
|
||||
@spec maybe_fetch_actor_and_attributed_to_id(map()) ::
|
||||
{:ok, Actor.t(), Actor.t() | nil} | {:error, atom()}
|
||||
def maybe_fetch_actor_and_attributed_to_id(%{
|
||||
"actor" => actor_url,
|
||||
"attributedTo" => attributed_to_url
|
||||
})
|
||||
when is_nil(attributed_to_url) do
|
||||
{fetch_actor(actor_url), nil}
|
||||
case fetch_actor(actor_url) do
|
||||
{:ok, %Actor{} = actor} ->
|
||||
{:ok, actor, nil}
|
||||
|
||||
{:error, err} ->
|
||||
{:error, err}
|
||||
end
|
||||
end
|
||||
|
||||
@spec maybe_fetch_actor_and_attributed_to_id(map()) :: {Actor.t() | nil, Actor.t() | nil}
|
||||
def maybe_fetch_actor_and_attributed_to_id(%{
|
||||
"actor" => actor_url,
|
||||
"attributedTo" => attributed_to_url
|
||||
})
|
||||
when is_nil(actor_url) do
|
||||
{fetch_actor(attributed_to_url), nil}
|
||||
case fetch_actor(attributed_to_url) do
|
||||
{:ok, %Actor{} = actor} ->
|
||||
{:ok, actor, nil}
|
||||
|
||||
{:error, err} ->
|
||||
{:error, err}
|
||||
end
|
||||
end
|
||||
|
||||
# Only when both actor and attributedTo fields are both filled is when we can return both
|
||||
|
@ -152,9 +163,12 @@ defmodule Mobilizon.Federation.ActivityStream.Converter.Utils do
|
|||
"attributedTo" => attributed_to_url
|
||||
})
|
||||
when actor_url != attributed_to_url do
|
||||
with actor <- fetch_actor(actor_url),
|
||||
attributed_to <- fetch_actor(attributed_to_url) do
|
||||
{actor, attributed_to}
|
||||
with {:ok, %Actor{} = actor} <- fetch_actor(actor_url),
|
||||
{:ok, %Actor{} = attributed_to} <- fetch_actor(attributed_to_url) do
|
||||
{:ok, actor, attributed_to}
|
||||
else
|
||||
{:error, err} ->
|
||||
{:error, err}
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -162,16 +176,25 @@ defmodule Mobilizon.Federation.ActivityStream.Converter.Utils do
|
|||
def maybe_fetch_actor_and_attributed_to_id(%{
|
||||
"attributedTo" => attributed_to_url
|
||||
}) do
|
||||
{fetch_actor(attributed_to_url), nil}
|
||||
case fetch_actor(attributed_to_url) do
|
||||
{:ok, %Actor{} = attributed_to} -> {:ok, attributed_to, nil}
|
||||
{:error, err} -> {:error, err}
|
||||
end
|
||||
end
|
||||
|
||||
def maybe_fetch_actor_and_attributed_to_id(_), do: {nil, nil}
|
||||
def maybe_fetch_actor_and_attributed_to_id(_), do: {:error, :no_actor_found}
|
||||
|
||||
@spec fetch_actor(String.t()) :: Actor.t()
|
||||
@spec fetch_actor(String.t()) :: {:ok, Actor.t()} | {:error, atom()}
|
||||
defp fetch_actor(actor_url) do
|
||||
with {:ok, %Actor{suspended: false} = actor} <-
|
||||
ActivityPubActor.get_or_fetch_actor_by_url(actor_url) do
|
||||
actor
|
||||
case ActivityPubActor.get_or_fetch_actor_by_url(actor_url) do
|
||||
{:ok, %Actor{suspended: false} = actor} ->
|
||||
{:ok, actor}
|
||||
|
||||
{:ok, %Actor{suspended: true} = _actor} ->
|
||||
{:error, :actor_suspended}
|
||||
|
||||
{:error, err} ->
|
||||
{:error, err}
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -203,12 +226,17 @@ defmodule Mobilizon.Federation.ActivityStream.Converter.Utils do
|
|||
|> Map.new()
|
||||
|
||||
picture_id =
|
||||
with banner when is_map(banner) <- get_banner_picture(attachements),
|
||||
{:ok, %Media{id: picture_id}} <-
|
||||
MediaConverter.find_or_create_media(banner, actor_id) do
|
||||
case get_banner_picture(attachements) do
|
||||
banner when is_map(banner) ->
|
||||
case MediaConverter.find_or_create_media(banner, actor_id) do
|
||||
{:error, _err} ->
|
||||
nil
|
||||
|
||||
{:ok, %Media{id: picture_id}} ->
|
||||
picture_id
|
||||
else
|
||||
_err ->
|
||||
end
|
||||
|
||||
_ ->
|
||||
nil
|
||||
end
|
||||
|
||||
|
|
|
@ -19,7 +19,7 @@ defmodule Mobilizon.Federation.HTTPSignatures.Signature do
|
|||
|
||||
@spec key_id_to_actor_url(String.t()) :: String.t()
|
||||
def key_id_to_actor_url(key_id) do
|
||||
%{path: path} =
|
||||
%URI{path: path} =
|
||||
uri =
|
||||
key_id
|
||||
|> URI.parse()
|
||||
|
@ -29,7 +29,7 @@ defmodule Mobilizon.Federation.HTTPSignatures.Signature do
|
|||
if is_nil(path) do
|
||||
uri
|
||||
else
|
||||
Map.put(uri, :path, String.trim_trailing(path, "/publickey"))
|
||||
%URI{uri | path: String.trim_trailing(path, "/publickey")}
|
||||
end
|
||||
|
||||
URI.to_string(uri)
|
||||
|
@ -78,15 +78,25 @@ defmodule Mobilizon.Federation.HTTPSignatures.Signature do
|
|||
end
|
||||
end
|
||||
|
||||
@spec fetch_public_key(Plug.Conn.t()) ::
|
||||
{:ok, String.t()}
|
||||
| {:error, :actor_fetch_error | :actor_not_fetchable | :pem_decode_error}
|
||||
def fetch_public_key(conn) do
|
||||
with %{"keyId" => kid} <- HTTPSignatures.signature_for_conn(conn),
|
||||
actor_id <- key_id_to_actor_url(kid),
|
||||
:ok <- Logger.debug("Fetching public key for #{actor_id}"),
|
||||
{:ok, public_key} <- get_public_key_for_url(actor_id) do
|
||||
{:ok, public_key}
|
||||
else
|
||||
{:error, err} ->
|
||||
{:error, err}
|
||||
end
|
||||
end
|
||||
|
||||
@spec refetch_public_key(Plug.Conn.t()) ::
|
||||
{:ok, String.t()}
|
||||
| {:error, :actor_fetch_error | :actor_not_fetchable | :pem_decode_error,
|
||||
:actor_is_local}
|
||||
def refetch_public_key(conn) do
|
||||
with %{"keyId" => kid} <- HTTPSignatures.signature_for_conn(conn),
|
||||
actor_id <- key_id_to_actor_url(kid),
|
||||
|
@ -94,9 +104,13 @@ defmodule Mobilizon.Federation.HTTPSignatures.Signature do
|
|||
{:ok, _actor} <- ActivityPubActor.make_actor_from_url(actor_id),
|
||||
{:ok, public_key} <- get_public_key_for_url(actor_id) do
|
||||
{:ok, public_key}
|
||||
else
|
||||
{:error, err} ->
|
||||
{:error, err}
|
||||
end
|
||||
end
|
||||
|
||||
@spec sign(Actor.t(), map()) :: String.t() | {:error, :pem_decode_error} | no_return
|
||||
def sign(%Actor{domain: domain, keys: keys} = actor, headers) when is_nil(domain) do
|
||||
Logger.debug("Signing a payload on behalf of #{actor.url}")
|
||||
Logger.debug("headers")
|
||||
|
@ -112,14 +126,17 @@ defmodule Mobilizon.Federation.HTTPSignatures.Signature do
|
|||
raise ArgumentError, message: "Can't do a signature on remote actor #{url}"
|
||||
end
|
||||
|
||||
@spec generate_date_header :: String.t()
|
||||
def generate_date_header, do: generate_date_header(NaiveDateTime.utc_now())
|
||||
|
||||
def generate_date_header(%NaiveDateTime{} = date) do
|
||||
Timex.format!(date, "{WDshort}, {0D} {Mshort} {YYYY} {h24}:{m}:{s} GMT")
|
||||
end
|
||||
|
||||
@spec generate_request_target(String.t(), String.t()) :: String.t()
|
||||
def generate_request_target(method, path), do: "#{method} #{path}"
|
||||
|
||||
@spec build_digest(String.t()) :: String.t()
|
||||
def build_digest(body) do
|
||||
"SHA-256=#{:sha256 |> :crypto.hash(body) |> Base.encode64()}"
|
||||
end
|
||||
|
|
|
@ -19,11 +19,15 @@ defmodule Mobilizon.Federation.WebFinger do
|
|||
require Logger
|
||||
import SweetXml
|
||||
|
||||
@doc """
|
||||
Returns the Web Host Metadata (for `/.well-known/host-meta`) representation for the instance, following RFC6414.
|
||||
"""
|
||||
@spec host_meta :: String.t()
|
||||
def host_meta do
|
||||
base_url = Endpoint.url()
|
||||
%URI{host: host} = URI.parse(base_url)
|
||||
|
||||
{
|
||||
XmlBuilder.to_doc({
|
||||
:XRD,
|
||||
%{
|
||||
xmlns: "http://docs.oasis-open.org/ns/xri/xrd-1.0",
|
||||
|
@ -43,10 +47,13 @@ defmodule Mobilizon.Federation.WebFinger do
|
|||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|> XmlBuilder.to_doc()
|
||||
})
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns the Webfinger representation for the instance, following RFC7033.
|
||||
"""
|
||||
@spec webfinger(String.t(), String.t()) :: {:ok, map} | {:error, :actor_not_found}
|
||||
def webfinger(resource, "JSON") do
|
||||
host = Endpoint.host()
|
||||
regex = ~r/(acct:)?(?<name>\w+)@#{host}/
|
||||
|
@ -61,15 +68,18 @@ defmodule Mobilizon.Federation.WebFinger do
|
|||
{:ok, represent_actor(actor, "JSON")}
|
||||
|
||||
_e ->
|
||||
{:error, "Couldn't find actor"}
|
||||
{:error, :actor_not_found}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@spec represent_actor(Actor.t()) :: struct()
|
||||
@doc """
|
||||
Return an `Mobilizon.Actors.Actor` Webfinger representation (as JSON)
|
||||
"""
|
||||
@spec represent_actor(Actor.t()) :: map()
|
||||
@spec represent_actor(Actor.t(), String.t()) :: map()
|
||||
def represent_actor(%Actor{} = actor), do: represent_actor(actor, "JSON")
|
||||
|
||||
@spec represent_actor(Actor.t(), String.t()) :: struct()
|
||||
def represent_actor(%Actor{} = actor, "JSON") do
|
||||
links =
|
||||
[
|
||||
|
@ -89,6 +99,7 @@ defmodule Mobilizon.Federation.WebFinger do
|
|||
}
|
||||
end
|
||||
|
||||
@spec maybe_add_avatar(list(map()), Actor.t()) :: list(map())
|
||||
defp maybe_add_avatar(data, %Actor{avatar: avatar}) when not is_nil(avatar) do
|
||||
data ++
|
||||
[
|
||||
|
@ -102,6 +113,7 @@ defmodule Mobilizon.Federation.WebFinger do
|
|||
|
||||
defp maybe_add_avatar(data, _actor), do: data
|
||||
|
||||
@spec maybe_add_profile_page(list(map()), Actor.t()) :: list(map())
|
||||
defp maybe_add_profile_page(data, %Actor{type: :Group, url: url}) do
|
||||
data ++
|
||||
[
|
||||
|
@ -115,37 +127,76 @@ defmodule Mobilizon.Federation.WebFinger do
|
|||
|
||||
defp maybe_add_profile_page(data, _actor), do: data
|
||||
|
||||
@type finger_errors ::
|
||||
:host_not_found | :address_invalid | :http_error | :webfinger_information_not_json
|
||||
|
||||
@doc """
|
||||
Finger an actor to retreive it's ActivityPub ID/URL
|
||||
|
||||
Fetches the Extensible Resource Descriptor endpoint `/.well-known/host-meta` to find the Webfinger endpoint (usually `/.well-known/webfinger?resource=`) with `find_webfinger_endpoint/1` and then performs a Webfinger query to get the ActivityPub ID associated to an actor.
|
||||
Fetches the Extensible Resource Descriptor endpoint `/.well-known/host-meta` to find the Webfinger endpoint (usually `/.well-known/webfinger?resource=`) and then performs a Webfinger query to get the ActivityPub ID associated to an actor.
|
||||
"""
|
||||
@spec finger(String.t()) :: {:ok, String.t()} | {:error, atom()}
|
||||
@spec finger(String.t()) ::
|
||||
{:ok, String.t()}
|
||||
| {:error, finger_errors}
|
||||
def finger(actor) do
|
||||
actor = String.trim_leading(actor, "@")
|
||||
|
||||
with address when is_binary(address) <- apply_webfinger_endpoint(actor),
|
||||
false <- address_invalid(address),
|
||||
{:ok, %{body: body, status: code}} when code in 200..299 <-
|
||||
WebfingerClient.get(address),
|
||||
{:ok, %{"url" => url}} <- webfinger_from_json(body) do
|
||||
case validate_endpoint(actor) do
|
||||
{:ok, address} ->
|
||||
case fetch_webfinger_data(address) do
|
||||
{:ok, %{"url" => url}} ->
|
||||
{:ok, url}
|
||||
else
|
||||
e ->
|
||||
Logger.debug("Couldn't finger #{actor}")
|
||||
Logger.debug(inspect(e))
|
||||
{:error, e}
|
||||
|
||||
{:error, err} ->
|
||||
Logger.debug("Couldn't process webfinger data for #{actor}")
|
||||
{:error, err}
|
||||
end
|
||||
|
||||
{:error, err} ->
|
||||
Logger.debug("Couldn't find webfinger endpoint for #{actor}")
|
||||
{:error, err}
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Fetches the Extensible Resource Descriptor endpoint `/.well-known/host-meta` to find the Webfinger endpoint (usually `/.well-known/webfinger?resource=`)
|
||||
"""
|
||||
@spec find_webfinger_endpoint(String.t()) :: String.t()
|
||||
def find_webfinger_endpoint(domain) when is_binary(domain) do
|
||||
@spec fetch_webfinger_data(String.t()) ::
|
||||
{:ok, map()} | {:error, :webfinger_information_not_json | :http_error}
|
||||
defp fetch_webfinger_data(address) do
|
||||
case WebfingerClient.get(address) do
|
||||
{:ok, %{body: body, status: code}} when code in 200..299 ->
|
||||
webfinger_from_json(body)
|
||||
|
||||
_ ->
|
||||
{:error, :http_error}
|
||||
end
|
||||
end
|
||||
|
||||
@spec validate_endpoint(String.t()) ::
|
||||
{:ok, String.t()} | {:error, :address_invalid | :host_not_found}
|
||||
defp validate_endpoint(actor) do
|
||||
case apply_webfinger_endpoint(actor) do
|
||||
address when is_binary(address) ->
|
||||
if address_invalid(address) do
|
||||
{:error, :address_invalid}
|
||||
else
|
||||
{:ok, address}
|
||||
end
|
||||
|
||||
_ ->
|
||||
{:error, :host_not_found}
|
||||
end
|
||||
end
|
||||
|
||||
# Fetches the Extensible Resource Descriptor endpoint `/.well-known/host-meta`
|
||||
# to find the Webfinger endpoint (usually `/.well-known/webfinger?resource=`)
|
||||
@spec find_webfinger_endpoint(String.t()) ::
|
||||
{:ok, String.t()} | {:error, :link_not_found} | {:error, any()}
|
||||
defp find_webfinger_endpoint(domain) when is_binary(domain) do
|
||||
with {:ok, %{body: body}} <- fetch_document("http://#{domain}/.well-known/host-meta"),
|
||||
link_template when is_binary(link_template) <- find_link_from_template(body) do
|
||||
{:ok, link_template}
|
||||
else
|
||||
{:error, :link_not_found} -> {:error, :link_not_found}
|
||||
{:error, error} -> {:error, error}
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -5,42 +5,54 @@
|
|||
|
||||
defmodule Mobilizon.Federation.WebFinger.XmlBuilder do
|
||||
@moduledoc """
|
||||
Builds XRD for WebFinger host_meta.
|
||||
Extremely basic XML encoder. Builds XRD for WebFinger host_meta.
|
||||
"""
|
||||
|
||||
def to_xml({tag, attributes, content}) do
|
||||
@typep content :: list({tag :: atom(), attributes :: map()}) | String.t()
|
||||
@typep document :: {tag :: atom(), attributes :: map(), content :: content}
|
||||
|
||||
@doc """
|
||||
Return the XML representation for a document.
|
||||
"""
|
||||
@spec to_doc(document :: document) :: String.t()
|
||||
def to_doc(document), do: ~s(<?xml version="1.0" encoding="UTF-8"?>) <> to_xml(document)
|
||||
|
||||
@spec to_xml(document) :: String.t()
|
||||
@spec to_xml({tag :: atom(), attributes :: map()}) :: String.t()
|
||||
@spec to_xml({tag :: atom(), content :: content}) :: String.t()
|
||||
@spec to_xml(content :: content) :: String.t()
|
||||
defp to_xml({tag, attributes, content}) do
|
||||
open_tag = make_open_tag(tag, attributes)
|
||||
content_xml = to_xml(content)
|
||||
|
||||
"<#{open_tag}>#{content_xml}</#{tag}>"
|
||||
end
|
||||
|
||||
def to_xml({tag, %{} = attributes}) do
|
||||
defp to_xml({tag, %{} = attributes}) do
|
||||
open_tag = make_open_tag(tag, attributes)
|
||||
|
||||
"<#{open_tag} />"
|
||||
end
|
||||
|
||||
def to_xml({tag, content}), do: to_xml({tag, %{}, content})
|
||||
defp to_xml({tag, content}), do: to_xml({tag, %{}, content})
|
||||
|
||||
def to_xml(content) when is_binary(content), do: to_string(content)
|
||||
defp to_xml(content) when is_binary(content), do: to_string(content)
|
||||
|
||||
def to_xml(content) when is_list(content) do
|
||||
defp to_xml(content) when is_list(content) do
|
||||
content
|
||||
|> Enum.map(&to_xml/1)
|
||||
|> Enum.join()
|
||||
end
|
||||
|
||||
def to_xml(%NaiveDateTime{} = time), do: NaiveDateTime.to_iso8601(time)
|
||||
|
||||
def to_doc(content), do: ~s(<?xml version="1.0" encoding="UTF-8"?>) <> to_xml(content)
|
||||
defp to_xml(%NaiveDateTime{} = time), do: NaiveDateTime.to_iso8601(time)
|
||||
|
||||
@spec make_open_tag(tag :: atom, attributes :: map()) :: String.t()
|
||||
defp make_open_tag(tag, attributes) do
|
||||
attributes_string =
|
||||
attributes
|
||||
|> Enum.map(fn {attribute, value} -> "#{attribute}=\"#{value}\"" end)
|
||||
|> Enum.join(" ")
|
||||
|
||||
[tag, attributes_string] |> Enum.join(" ") |> String.trim()
|
||||
[to_string(tag), attributes_string] |> Enum.join(" ") |> String.trim()
|
||||
end
|
||||
end
|
||||
|
|
|
@ -5,8 +5,7 @@ defmodule Mobilizon.GraphQL.API.Comments do
|
|||
|
||||
alias Mobilizon.Actors.Actor
|
||||
alias Mobilizon.Discussions.Comment
|
||||
alias Mobilizon.Federation.ActivityPub
|
||||
alias Mobilizon.Federation.ActivityPub.Activity
|
||||
alias Mobilizon.Federation.ActivityPub.{Actions, Activity}
|
||||
alias Mobilizon.GraphQL.API.Utils
|
||||
|
||||
@doc """
|
||||
|
@ -15,7 +14,7 @@ defmodule Mobilizon.GraphQL.API.Comments do
|
|||
@spec create_comment(map) :: {:ok, Activity.t(), Comment.t()} | any
|
||||
def create_comment(args) do
|
||||
args = extract_pictures_from_comment_body(args)
|
||||
ActivityPub.create(:comment, args, true)
|
||||
Actions.Create.create(:comment, args, true)
|
||||
end
|
||||
|
||||
@doc """
|
||||
|
@ -24,7 +23,7 @@ defmodule Mobilizon.GraphQL.API.Comments do
|
|||
@spec update_comment(Comment.t(), map()) :: {:ok, Activity.t(), Comment.t()} | any
|
||||
def update_comment(%Comment{} = comment, args) do
|
||||
args = extract_pictures_from_comment_body(args)
|
||||
ActivityPub.update(comment, args, true)
|
||||
Actions.Update.update(comment, args, true)
|
||||
end
|
||||
|
||||
@doc """
|
||||
|
@ -32,7 +31,7 @@ defmodule Mobilizon.GraphQL.API.Comments do
|
|||
"""
|
||||
@spec delete_comment(Comment.t(), Actor.t()) :: {:ok, Activity.t(), Comment.t()} | any
|
||||
def delete_comment(%Comment{} = comment, %Actor{} = actor) do
|
||||
ActivityPub.delete(comment, actor, true)
|
||||
Actions.Delete.delete(comment, actor, true)
|
||||
end
|
||||
|
||||
@doc """
|
||||
|
@ -42,7 +41,7 @@ defmodule Mobilizon.GraphQL.API.Comments do
|
|||
def create_discussion(args) do
|
||||
args = extract_pictures_from_comment_body(args)
|
||||
|
||||
ActivityPub.create(
|
||||
Actions.Create.create(
|
||||
:discussion,
|
||||
args,
|
||||
true
|
||||
|
|
|
@ -6,8 +6,7 @@ defmodule Mobilizon.GraphQL.API.Events do
|
|||
alias Mobilizon.Actors.Actor
|
||||
alias Mobilizon.Events.Event
|
||||
|
||||
alias Mobilizon.Federation.ActivityPub
|
||||
alias Mobilizon.Federation.ActivityPub.{Activity, Utils}
|
||||
alias Mobilizon.Federation.ActivityPub.{Actions, Activity, Utils}
|
||||
alias Mobilizon.GraphQL.API.Utils, as: APIUtils
|
||||
|
||||
@doc """
|
||||
|
@ -15,15 +14,8 @@ defmodule Mobilizon.GraphQL.API.Events do
|
|||
"""
|
||||
@spec create_event(map) :: {:ok, Activity.t(), Event.t()} | any
|
||||
def create_event(args) do
|
||||
with organizer_actor <- Map.get(args, :organizer_actor),
|
||||
args <- extract_pictures_from_event_body(args, organizer_actor),
|
||||
args <-
|
||||
Map.update(args, :picture, nil, fn picture ->
|
||||
process_picture(picture, organizer_actor)
|
||||
end) do
|
||||
# For now we don't federate drafts but it will be needed if we want to edit them as groups
|
||||
ActivityPub.create(:event, args, should_federate(args))
|
||||
end
|
||||
Actions.Create.create(:event, prepare_args(args), should_federate(args))
|
||||
end
|
||||
|
||||
@doc """
|
||||
|
@ -31,21 +23,26 @@ defmodule Mobilizon.GraphQL.API.Events do
|
|||
"""
|
||||
@spec update_event(map, Event.t()) :: {:ok, Activity.t(), Event.t()} | any
|
||||
def update_event(args, %Event{} = event) do
|
||||
with organizer_actor <- Map.get(args, :organizer_actor),
|
||||
args <- extract_pictures_from_event_body(args, organizer_actor),
|
||||
args <-
|
||||
Map.update(args, :picture, nil, fn picture ->
|
||||
process_picture(picture, organizer_actor)
|
||||
end) do
|
||||
ActivityPub.update(event, args, should_federate(args))
|
||||
end
|
||||
Actions.Update.update(event, prepare_args(args), should_federate(args))
|
||||
end
|
||||
|
||||
@doc """
|
||||
Trigger the deletion of an event
|
||||
"""
|
||||
@spec delete_event(Event.t(), Actor.t(), boolean()) :: {:ok, Activity.t(), Entity.t()} | any()
|
||||
def delete_event(%Event{} = event, %Actor{} = actor, federate \\ true) do
|
||||
ActivityPub.delete(event, actor, federate)
|
||||
Actions.Delete.delete(event, actor, federate)
|
||||
end
|
||||
|
||||
@spec prepare_args(map) :: map
|
||||
defp prepare_args(args) do
|
||||
organizer_actor = Map.get(args, :organizer_actor)
|
||||
|
||||
args
|
||||
|> extract_pictures_from_event_body(organizer_actor)
|
||||
|> Map.update(:picture, nil, fn picture ->
|
||||
process_picture(picture, organizer_actor)
|
||||
end)
|
||||
end
|
||||
|
||||
defp process_picture(nil, _), do: nil
|
||||
|
@ -75,6 +72,7 @@ defmodule Mobilizon.GraphQL.API.Events do
|
|||
|
||||
defp extract_pictures_from_event_body(args, _), do: args
|
||||
|
||||
@spec should_federate(map()) :: boolean
|
||||
defp should_federate(%{attributed_to_id: attributed_to_id}) when not is_nil(attributed_to_id),
|
||||
do: true
|
||||
|
||||
|
|
|
@ -6,73 +6,81 @@ defmodule Mobilizon.GraphQL.API.Follows do
|
|||
alias Mobilizon.Actors
|
||||
alias Mobilizon.Actors.{Actor, Follower}
|
||||
|
||||
alias Mobilizon.Federation.ActivityPub
|
||||
alias Mobilizon.Federation.ActivityPub.Activity
|
||||
alias Mobilizon.Federation.ActivityPub.{Actions, Activity}
|
||||
|
||||
require Logger
|
||||
|
||||
@doc """
|
||||
Make an actor (`follower`) follow another (`followed`).
|
||||
"""
|
||||
@spec follow(follower :: Actor.t(), followed :: Actor.t()) ::
|
||||
{:ok, Activity.t(), Mobilizon.Actors.Follower.t()}
|
||||
| {:error, String.t()}
|
||||
def follow(%Actor{} = follower, %Actor{} = followed) do
|
||||
case ActivityPub.follow(follower, followed) do
|
||||
{:ok, activity, follow} ->
|
||||
{:ok, activity, follow}
|
||||
|
||||
{:error, e} ->
|
||||
Logger.warn("Error while following actor: #{inspect(e)}")
|
||||
{:error, e}
|
||||
|
||||
e ->
|
||||
Logger.warn("Error while following actor: #{inspect(e)}")
|
||||
{:error, e}
|
||||
end
|
||||
Actions.Follow.follow(follower, followed)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Make an actor (`follower`) unfollow another (`followed`).
|
||||
"""
|
||||
@spec unfollow(follower :: Actor.t(), followed :: Actor.t()) ::
|
||||
{:ok, Activity.t(), Mobilizon.Actors.Follower.t()}
|
||||
| {:error, String.t()}
|
||||
def unfollow(%Actor{} = follower, %Actor{} = followed) do
|
||||
case ActivityPub.unfollow(follower, followed) do
|
||||
{:ok, activity, follow} ->
|
||||
{:ok, activity, follow}
|
||||
|
||||
e ->
|
||||
Logger.warn("Error while unfollowing actor: #{inspect(e)}")
|
||||
{:error, e}
|
||||
end
|
||||
Actions.Follow.unfollow(follower, followed)
|
||||
end
|
||||
|
||||
def accept(%Actor{} = follower, %Actor{} = followed) do
|
||||
Logger.debug("We're trying to accept a follow")
|
||||
@doc """
|
||||
Make an actor (`followed`) accept the follow from another (`follower`).
|
||||
"""
|
||||
@spec accept(follower :: Actor.t(), followed :: Actor.t()) ::
|
||||
{:ok, Activity.t(), Mobilizon.Actors.Follower.t()}
|
||||
| {:error, String.t()}
|
||||
def accept(%Actor{url: follower_url} = follower, %Actor{url: followed_url} = followed) do
|
||||
Logger.debug(
|
||||
"We're trying to accept a follow: #{followed_url} is accepting #{follower_url} follow request."
|
||||
)
|
||||
|
||||
with %Follower{approved: false} = follow <-
|
||||
Actors.is_following(follower, followed),
|
||||
{:ok, %Activity{} = activity, %Follower{approved: true} = follow} <-
|
||||
ActivityPub.accept(
|
||||
case Actors.is_following(follower, followed) do
|
||||
%Follower{approved: false} = follow ->
|
||||
Actions.Accept.accept(
|
||||
:follow,
|
||||
follow,
|
||||
true
|
||||
) do
|
||||
{:ok, activity, follow}
|
||||
else
|
||||
)
|
||||
|
||||
%Follower{approved: true} ->
|
||||
{:error, "Follow already accepted"}
|
||||
|
||||
nil ->
|
||||
{:error, "Can't accept follow: #{follower_url} is not following #{followed_url}."}
|
||||
end
|
||||
end
|
||||
|
||||
def reject(%Actor{} = follower, %Actor{} = followed) do
|
||||
Logger.debug("We're trying to reject a follow")
|
||||
@doc """
|
||||
Make an actor (`followed`) reject the follow from another (`follower`).
|
||||
"""
|
||||
@spec reject(follower :: Actor.t(), followed :: Actor.t()) ::
|
||||
{:ok, Activity.t(), Mobilizon.Actors.Follower.t()}
|
||||
| {:error, String.t()}
|
||||
def reject(%Actor{url: follower_url} = follower, %Actor{url: followed_url} = followed) do
|
||||
Logger.debug(
|
||||
"We're trying to reject a follow: #{followed_url} is rejecting #{follower_url} follow request."
|
||||
)
|
||||
|
||||
with {:follower, %Follower{} = follow} <-
|
||||
{:follower, Actors.is_following(follower, followed)},
|
||||
{:ok, %Activity{} = activity, %Follower{} = follow} <-
|
||||
ActivityPub.reject(
|
||||
case Actors.is_following(follower, followed) do
|
||||
%Follower{approved: true} ->
|
||||
{:error, "Follow already accepted"}
|
||||
|
||||
%Follower{} = follow ->
|
||||
Actions.Reject.reject(
|
||||
:follow,
|
||||
follow,
|
||||
true
|
||||
) do
|
||||
{:ok, activity, follow}
|
||||
else
|
||||
{:follower, nil} ->
|
||||
{:error, "Follow not found"}
|
||||
)
|
||||
|
||||
{:follower, %Follower{approved: true}} ->
|
||||
{:error, "Follow already accepted"}
|
||||
nil ->
|
||||
{:error, "Follow not found"}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -6,39 +6,35 @@ defmodule Mobilizon.GraphQL.API.Groups do
|
|||
alias Mobilizon.Actors
|
||||
alias Mobilizon.Actors.Actor
|
||||
|
||||
alias Mobilizon.Federation.ActivityPub
|
||||
alias Mobilizon.Federation.ActivityPub.Activity
|
||||
alias Mobilizon.Federation.ActivityPub.{Actions, Activity}
|
||||
alias Mobilizon.Service.Formatter.HTML
|
||||
|
||||
@doc """
|
||||
Create a group
|
||||
"""
|
||||
@spec create_group(map) :: {:ok, Activity.t(), Actor.t()} | any
|
||||
@spec create_group(map) ::
|
||||
{:ok, Activity.t(), Actor.t()}
|
||||
| {:error, String.t() | Ecto.Changeset.t()}
|
||||
def create_group(args) do
|
||||
with preferred_username <-
|
||||
args |> Map.get(:preferred_username) |> HTML.strip_tags() |> String.trim(),
|
||||
{:existing_group, nil} <-
|
||||
{:existing_group, Actors.get_local_actor_by_name(preferred_username)},
|
||||
args <- args |> Map.put(:type, :Group),
|
||||
{:ok, %Activity{} = activity, %Actor{} = group} <-
|
||||
ActivityPub.create(:actor, args, true, %{"actor" => args.creator_actor.url}) do
|
||||
{:ok, activity, group}
|
||||
else
|
||||
{:existing_group, _} ->
|
||||
{:error, "A group with this name already exists"}
|
||||
preferred_username =
|
||||
args |> Map.get(:preferred_username) |> HTML.strip_tags() |> String.trim()
|
||||
|
||||
args = args |> Map.put(:type, :Group)
|
||||
|
||||
case Actors.get_local_actor_by_name(preferred_username) do
|
||||
nil ->
|
||||
Actions.Create.create(:actor, args, true, %{"actor" => args.creator_actor.url})
|
||||
|
||||
%Actor{} ->
|
||||
{:error, "A profile or group with that name already exists"}
|
||||
end
|
||||
end
|
||||
|
||||
@spec create_group(map) :: {:ok, Activity.t(), Actor.t()} | any
|
||||
@spec update_group(map) ::
|
||||
{:ok, Activity.t(), Actor.t()} | {:error, :group_not_found | Ecto.Changeset.t()}
|
||||
def update_group(%{id: id} = args) do
|
||||
with {:existing_group, {:ok, %Actor{type: :Group} = group}} <-
|
||||
{:existing_group, Actors.get_group_by_actor_id(id)},
|
||||
{:ok, %Activity{} = activity, %Actor{} = group} <-
|
||||
ActivityPub.update(group, args, true, %{"actor" => args.updater_actor.url}) do
|
||||
{:ok, activity, group}
|
||||
else
|
||||
{:existing_group, _} ->
|
||||
{:error, "A group with this name already exists"}
|
||||
with {:ok, %Actor{type: :Group} = group} <- Actors.get_group_by_actor_id(id) do
|
||||
Actions.Update.update(group, args, true, %{"actor" => args.updater_actor.url})
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -6,28 +6,27 @@ defmodule Mobilizon.GraphQL.API.Participations do
|
|||
alias Mobilizon.Actors.Actor
|
||||
alias Mobilizon.Events
|
||||
alias Mobilizon.Events.{Event, Participant}
|
||||
alias Mobilizon.Federation.ActivityPub
|
||||
alias Mobilizon.Federation.ActivityPub.Activity
|
||||
alias Mobilizon.Federation.ActivityPub.{Actions, Activity}
|
||||
alias Mobilizon.Service.Notifications.Scheduler
|
||||
alias Mobilizon.Web.Email.Participation
|
||||
|
||||
@spec join(Event.t(), Actor.t(), map()) :: {:ok, Activity.t(), Participant.t()}
|
||||
@spec join(Event.t(), Actor.t(), map()) ::
|
||||
{:ok, Activity.t(), Participant.t()} | {:error, :already_participant}
|
||||
def join(%Event{id: event_id} = event, %Actor{id: actor_id} = actor, args \\ %{}) do
|
||||
with {:error, :participant_not_found} <-
|
||||
Mobilizon.Events.get_participant(event_id, actor_id, args),
|
||||
{:ok, activity, participant} <-
|
||||
ActivityPub.join(event, actor, Map.get(args, :local, true), %{metadata: args}) do
|
||||
{:ok, activity, participant}
|
||||
case Mobilizon.Events.get_participant(event_id, actor_id, args) do
|
||||
{:ok, %Participant{}} ->
|
||||
{:error, :already_participant}
|
||||
|
||||
{:error, :participant_not_found} ->
|
||||
Actions.Join.join(event, actor, Map.get(args, :local, true), %{metadata: args})
|
||||
end
|
||||
end
|
||||
|
||||
@spec leave(Event.t(), Actor.t()) :: {:ok, Activity.t(), Participant.t()}
|
||||
def leave(%Event{} = event, %Actor{} = actor, args \\ %{}) do
|
||||
with {:ok, activity, participant} <-
|
||||
ActivityPub.leave(event, actor, Map.get(args, :local, true), %{metadata: args}) do
|
||||
{:ok, activity, participant}
|
||||
end
|
||||
end
|
||||
@spec leave(Event.t(), Actor.t(), map()) ::
|
||||
{:ok, Activity.t(), Participant.t()}
|
||||
| {:error, :is_only_organizer | :participant_not_found | Ecto.Changeset.t()}
|
||||
def leave(%Event{} = event, %Actor{} = actor, args \\ %{}),
|
||||
do: Actions.Leave.leave(event, actor, Map.get(args, :local, true), %{metadata: args})
|
||||
|
||||
@doc """
|
||||
Update participation status
|
||||
|
@ -36,7 +35,6 @@ defmodule Mobilizon.GraphQL.API.Participations do
|
|||
def update(%Participant{} = participation, %Actor{} = moderator, :participant),
|
||||
do: accept(participation, moderator)
|
||||
|
||||
@spec update(Participant.t(), Actor.t(), atom()) :: {:ok, Activity.t(), Participant.t()}
|
||||
def update(%Participant{} = participation, %Actor{} = _moderator, :not_approved) do
|
||||
with {:ok, %Participant{} = participant} <-
|
||||
Events.update_participant(participation, %{role: :not_approved}) do
|
||||
|
@ -45,7 +43,6 @@ defmodule Mobilizon.GraphQL.API.Participations do
|
|||
end
|
||||
end
|
||||
|
||||
@spec update(Participant.t(), Actor.t(), atom()) :: {:ok, Activity.t(), Participant.t()}
|
||||
def update(%Participant{} = participation, %Actor{} = moderator, :rejected),
|
||||
do: reject(participation, moderator)
|
||||
|
||||
|
@ -54,15 +51,18 @@ defmodule Mobilizon.GraphQL.API.Participations do
|
|||
%Participant{} = participation,
|
||||
%Actor{} = moderator
|
||||
) do
|
||||
with {:ok, activity, %Participant{role: :participant} = participation} <-
|
||||
ActivityPub.accept(
|
||||
case Actions.Accept.accept(
|
||||
:join,
|
||||
participation,
|
||||
true,
|
||||
%{"actor" => moderator.url}
|
||||
),
|
||||
:ok <- Participation.send_emails_to_local_user(participation) do
|
||||
) do
|
||||
{:ok, activity, %Participant{role: :participant} = participation} ->
|
||||
Participation.send_emails_to_local_user(participation)
|
||||
{:ok, activity, participation}
|
||||
|
||||
{:error, err} ->
|
||||
{:error, err}
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -72,7 +72,7 @@ defmodule Mobilizon.GraphQL.API.Participations do
|
|||
%Actor{} = moderator
|
||||
) do
|
||||
with {:ok, activity, %Participant{role: :rejected} = participation} <-
|
||||
ActivityPub.reject(
|
||||
Actions.Reject.reject(
|
||||
:join,
|
||||
participation,
|
||||
true,
|
||||
|
|
|
@ -9,48 +9,46 @@ defmodule Mobilizon.GraphQL.API.Reports do
|
|||
alias Mobilizon.Reports.{Note, Report, ReportStatus}
|
||||
alias Mobilizon.Users.User
|
||||
|
||||
alias Mobilizon.Federation.ActivityPub
|
||||
alias Mobilizon.Federation.ActivityPub.Activity
|
||||
alias Mobilizon.Federation.ActivityPub.{Actions, Activity}
|
||||
|
||||
@doc """
|
||||
Create a report/flag on an actor, and optionally on an event or on comments.
|
||||
"""
|
||||
@spec report(map()) :: {:ok, Activity.t(), Report.t()} | {:error, Ecto.Changeset.t()}
|
||||
def report(args) do
|
||||
case {:make_activity, ActivityPub.flag(args, Map.get(args, :forward, false) == true)} do
|
||||
{:make_activity, {:ok, %Activity{} = activity, %Report{} = report}} ->
|
||||
{:ok, activity, report}
|
||||
|
||||
{:make_activity, err} ->
|
||||
{:error, err}
|
||||
end
|
||||
Actions.Flag.flag(args, Map.get(args, :forward, false) == true)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Update the state of a report
|
||||
"""
|
||||
@spec update_report_status(Actor.t(), Report.t(), ReportStatus.t()) ::
|
||||
{:ok, Report.t()} | {:error, Ecto.Changeset.t() | String.t()}
|
||||
def update_report_status(%Actor{} = actor, %Report{} = report, state) do
|
||||
with {:valid_state, true} <-
|
||||
{:valid_state, ReportStatus.valid_value?(state)},
|
||||
{:ok, report} <- ReportsAction.update_report(report, %{"status" => state}),
|
||||
{:ok, _} <- Admin.log_action(actor, "update", report) do
|
||||
if ReportStatus.valid_value?(state) do
|
||||
with {:ok, %Report{} = report} <- ReportsAction.update_report(report, %{"status" => state}) do
|
||||
Admin.log_action(actor, "update", report)
|
||||
{:ok, report}
|
||||
end
|
||||
else
|
||||
{:valid_state, false} -> {:error, "Unsupported state"}
|
||||
{:error, "Unsupported state"}
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Create a note on a report
|
||||
"""
|
||||
@spec create_report_note(Report.t(), Actor.t(), String.t()) :: {:ok, Note.t()}
|
||||
@spec create_report_note(Report.t(), Actor.t(), String.t()) ::
|
||||
{:ok, Note.t()} | {:error, String.t() | Ecto.Changeset.t()}
|
||||
def create_report_note(
|
||||
%Report{id: report_id},
|
||||
%Actor{id: moderator_id, user_id: user_id} = moderator,
|
||||
content
|
||||
) do
|
||||
with %User{role: role} <- Users.get_user!(user_id),
|
||||
{:role, true} <- {:role, role in [:administrator, :moderator]},
|
||||
{:ok, %Note{} = note} <-
|
||||
%User{role: role} = Users.get_user!(user_id)
|
||||
|
||||
if role in [:administrator, :moderator] do
|
||||
with {:ok, %Note{} = note} <-
|
||||
Mobilizon.Reports.create_note(%{
|
||||
"report_id" => report_id,
|
||||
"moderator_id" => moderator_id,
|
||||
|
@ -58,8 +56,8 @@ defmodule Mobilizon.GraphQL.API.Reports do
|
|||
}),
|
||||
{:ok, _} <- Admin.log_action(moderator, "create", note) do
|
||||
{:ok, note}
|
||||
end
|
||||
else
|
||||
{:role, false} ->
|
||||
{:error, "You need to be a moderator or an administrator to create a note on a report"}
|
||||
end
|
||||
end
|
||||
|
@ -67,23 +65,25 @@ defmodule Mobilizon.GraphQL.API.Reports do
|
|||
@doc """
|
||||
Delete a report note
|
||||
"""
|
||||
@spec delete_report_note(Note.t(), Actor.t()) :: {:ok, Note.t()}
|
||||
@spec delete_report_note(Note.t(), Actor.t()) ::
|
||||
{:ok, Note.t()} | {:error, Ecto.Changeset.t() | String.t()}
|
||||
def delete_report_note(
|
||||
%Note{moderator_id: note_moderator_id} = note,
|
||||
%Actor{id: moderator_id, user_id: user_id} = moderator
|
||||
) do
|
||||
with {:same_actor, true} <- {:same_actor, note_moderator_id == moderator_id},
|
||||
%User{role: role} <- Users.get_user!(user_id),
|
||||
{:role, true} <- {:role, role in [:administrator, :moderator]},
|
||||
{:ok, %Note{} = note} <-
|
||||
if note_moderator_id == moderator_id do
|
||||
%User{role: role} = Users.get_user!(user_id)
|
||||
|
||||
if role in [:administrator, :moderator] do
|
||||
with {:ok, %Note{} = note} <-
|
||||
Mobilizon.Reports.delete_note(note),
|
||||
{:ok, _} <- Admin.log_action(moderator, "delete", note) do
|
||||
{:ok, note}
|
||||
end
|
||||
else
|
||||
{:role, false} ->
|
||||
{:error, "You need to be a moderator or an administrator to create a note on a report"}
|
||||
|
||||
{:same_actor, false} ->
|
||||
end
|
||||
else
|
||||
{:error, "You can only remove your own notes"}
|
||||
end
|
||||
end
|
||||
|
|
|
@ -59,8 +59,8 @@ defmodule Mobilizon.GraphQL.API.Search do
|
|||
@doc """
|
||||
Search events
|
||||
"""
|
||||
@spec search_events(String.t(), integer | nil, integer | nil) ::
|
||||
{:ok, Page.t()} | {:error, String.t()}
|
||||
@spec search_events(map(), integer | nil, integer | nil) ::
|
||||
{:ok, Page.t()}
|
||||
def search_events(%{term: term} = args, page \\ 1, limit \\ 10) do
|
||||
term = String.trim(term)
|
||||
|
||||
|
@ -78,6 +78,7 @@ defmodule Mobilizon.GraphQL.API.Search do
|
|||
end
|
||||
end
|
||||
|
||||
@spec interact(String.t()) :: {:ok, struct()} | {:error, :not_found}
|
||||
def interact(uri) do
|
||||
case ActivityPub.fetch_object_from_url(uri) do
|
||||
{:ok, object} ->
|
||||
|
|
|
@ -10,7 +10,7 @@ defmodule Mobilizon.GraphQL.API.Utils do
|
|||
@doc """
|
||||
Creates HTML content from text and mentions
|
||||
"""
|
||||
@spec make_content_html(String.t(), list(), String.t()) :: String.t()
|
||||
@spec make_content_html(String.t(), list(), String.t()) :: {String.t(), list(), list()}
|
||||
def make_content_html(text, additional_tags, content_type) do
|
||||
with {text, mentions, tags} <- format_input(text, content_type, []) do
|
||||
{text, mentions, additional_tags ++ Enum.map(tags, fn {_, tag} -> tag end)}
|
||||
|
|
|
@ -8,11 +8,19 @@ defmodule Mobilizon.GraphQL.Error do
|
|||
alias Mobilizon.Web.Gettext, as: GettextBackend
|
||||
import Mobilizon.Web.Gettext, only: [dgettext: 2]
|
||||
|
||||
@type t :: %{code: atom(), message: String.t(), status_code: pos_integer(), field: atom()}
|
||||
|
||||
defstruct [:code, :message, :status_code, :field]
|
||||
|
||||
@type error :: {:error, any()} | {:error, any(), any(), any()} | atom()
|
||||
|
||||
@doc """
|
||||
Normalize an error to return `t`.
|
||||
"""
|
||||
# Error Tuples
|
||||
# ------------
|
||||
# Regular errors
|
||||
@spec normalize(error | list(error) | String.t() | any()) :: t()
|
||||
def normalize({:error, reason}) do
|
||||
handle(reason)
|
||||
end
|
||||
|
|
40
lib/graphql/middleware/current_actor_provider.ex
Normal file
40
lib/graphql/middleware/current_actor_provider.ex
Normal file
|
@ -0,0 +1,40 @@
|
|||
defmodule Mobilizon.GraphQL.Middleware.CurrentActorProvider do
|
||||
@moduledoc """
|
||||
Absinthe Error Handler
|
||||
"""
|
||||
alias Mobilizon.Actors.Actor
|
||||
alias Mobilizon.Users
|
||||
alias Mobilizon.Users.User
|
||||
|
||||
@behaviour Absinthe.Middleware
|
||||
|
||||
@impl Absinthe.Middleware
|
||||
@spec call(Absinthe.Resolution.t(), any) :: Absinthe.Resolution.t()
|
||||
def call(
|
||||
%Absinthe.Resolution{context: %{current_user: %User{id: user_id} = user} = context} =
|
||||
resolution,
|
||||
_config
|
||||
) do
|
||||
case Cachex.fetch(:default_actors, to_string(user_id), fn -> default(user) end) do
|
||||
{status, %Actor{} = current_actor} when status in [:ok, :commit] ->
|
||||
context = Map.put(context, :current_actor, current_actor)
|
||||
%Absinthe.Resolution{resolution | context: context}
|
||||
|
||||
{_, nil} ->
|
||||
resolution
|
||||
end
|
||||
end
|
||||
|
||||
def call(%Absinthe.Resolution{} = resolution, _config), do: resolution
|
||||
|
||||
@spec default(User.t()) :: {:commit, Actor.t()} | {:ignore, nil}
|
||||
defp default(%User{} = user) do
|
||||
case Users.get_actor_for_user(user) do
|
||||
%Actor{} = actor ->
|
||||
{:commit, actor}
|
||||
|
||||
nil ->
|
||||
{:ignore, nil}
|
||||
end
|
||||
end
|
||||
end
|
|
@ -4,7 +4,8 @@ defmodule Mobilizon.GraphQL.Resolvers.Activity do
|
|||
"""
|
||||
|
||||
import Mobilizon.Users.Guards
|
||||
alias Mobilizon.{Activities, Actors, Users}
|
||||
alias Mobilizon.{Activities, Actors}
|
||||
alias Mobilizon.Activities.Activity
|
||||
alias Mobilizon.Actors.Actor
|
||||
alias Mobilizon.Service.Activity.Utils
|
||||
alias Mobilizon.Storage.Page
|
||||
|
@ -12,11 +13,12 @@ defmodule Mobilizon.GraphQL.Resolvers.Activity do
|
|||
|
||||
require Logger
|
||||
|
||||
@spec group_activity(Actor.t(), map(), Absinthe.Resolution.t()) ::
|
||||
{:ok, Page.t(Activity.t())} | {:error, :unauthorized | :unauthenticated}
|
||||
def group_activity(%Actor{type: :Group, id: group_id}, %{page: page, limit: limit} = args, %{
|
||||
context: %{current_user: %User{role: role} = user}
|
||||
context: %{current_user: %User{role: role}, current_actor: %Actor{id: actor_id}}
|
||||
}) do
|
||||
with {:actor, %Actor{id: actor_id} = _actor} <- {:actor, Users.get_actor_for_user(user)},
|
||||
{:member, true} <- {:member, Actors.is_member?(actor_id, group_id) or is_moderator(role)} do
|
||||
if Actors.is_member?(actor_id, group_id) or is_moderator(role) do
|
||||
%Page{total: total, elements: elements} =
|
||||
Activities.list_group_activities_for_member(
|
||||
group_id,
|
||||
|
@ -30,7 +32,6 @@ defmodule Mobilizon.GraphQL.Resolvers.Activity do
|
|||
|
||||
{:ok, %Page{total: total, elements: elements}}
|
||||
else
|
||||
{:member, false} ->
|
||||
{:error, :unauthorized}
|
||||
end
|
||||
end
|
||||
|
|
|
@ -4,15 +4,17 @@ defmodule Mobilizon.GraphQL.Resolvers.Actor do
|
|||
"""
|
||||
|
||||
import Mobilizon.Users.Guards
|
||||
alias Mobilizon.{Actors, Admin, Users}
|
||||
alias Mobilizon.{Actors, Admin}
|
||||
alias Mobilizon.Actors.Actor
|
||||
alias Mobilizon.Federation.ActivityPub
|
||||
alias Mobilizon.Federation.ActivityPub.Actions
|
||||
alias Mobilizon.Service.Workers.Background
|
||||
alias Mobilizon.Users.User
|
||||
import Mobilizon.Web.Gettext, only: [dgettext: 2]
|
||||
|
||||
require Logger
|
||||
|
||||
@spec refresh_profile(any(), map(), Absinthe.Resolution.t()) ::
|
||||
{:ok, Actor.t()} | {:error, String.t()}
|
||||
def refresh_profile(_parent, %{id: id}, %{context: %{current_user: %User{role: role}}})
|
||||
when is_admin(role) do
|
||||
case Actors.get_actor(id) do
|
||||
|
@ -31,40 +33,38 @@ defmodule Mobilizon.GraphQL.Resolvers.Actor do
|
|||
end
|
||||
end
|
||||
|
||||
@spec suspend_profile(any(), map(), Absinthe.Resolution.t()) ::
|
||||
{:ok, Actor.t()} | {:error, String.t()}
|
||||
def suspend_profile(_parent, %{id: id}, %{
|
||||
context: %{current_user: %User{role: role} = user}
|
||||
context: %{
|
||||
current_user: %User{role: role},
|
||||
current_actor: %Actor{} = moderator_actor
|
||||
}
|
||||
})
|
||||
when is_moderator(role) do
|
||||
with {:moderator_actor, %Actor{} = moderator_actor} <-
|
||||
{:moderator_actor, Users.get_actor_for_user(user)},
|
||||
%Actor{suspended: false} = actor <- Actors.get_actor_with_preload(id) do
|
||||
case actor do
|
||||
case Actors.get_actor_with_preload(id) do
|
||||
# Suspend a group on this instance
|
||||
%Actor{type: :Group, domain: nil} ->
|
||||
%Actor{suspended: false, type: :Group, domain: nil} = actor ->
|
||||
Logger.debug("We're suspending a group on this very instance")
|
||||
ActivityPub.delete(actor, moderator_actor, true, %{suspension: true})
|
||||
Actions.Delete.delete(actor, moderator_actor, true, %{suspension: true})
|
||||
Admin.log_action(moderator_actor, "suspend", actor)
|
||||
{:ok, actor}
|
||||
|
||||
# Delete a remote actor
|
||||
%Actor{domain: domain} when not is_nil(domain) ->
|
||||
%Actor{suspended: false, domain: domain} = actor when not is_nil(domain) ->
|
||||
Logger.debug("We're just deleting a remote instance")
|
||||
Actors.delete_actor(actor, suspension: true)
|
||||
Admin.log_action(moderator_actor, "suspend", actor)
|
||||
{:ok, actor}
|
||||
|
||||
%Actor{domain: nil} ->
|
||||
%Actor{suspended: false, domain: nil} ->
|
||||
{:error, dgettext("errors", "No remote profile found with this ID")}
|
||||
end
|
||||
else
|
||||
{:moderator_actor, nil} ->
|
||||
{:error, dgettext("errors", "No profile found for the moderator user")}
|
||||
|
||||
%Actor{suspended: true} ->
|
||||
{:error, dgettext("errors", "Profile already suspended")}
|
||||
|
||||
{:error, _} ->
|
||||
{:error, dgettext("errors", "Error while performing background task")}
|
||||
nil ->
|
||||
{:error, dgettext("errors", "Profile not found")}
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -72,13 +72,16 @@ defmodule Mobilizon.GraphQL.Resolvers.Actor do
|
|||
{:error, dgettext("errors", "Only moderators and administrators can suspend a profile")}
|
||||
end
|
||||
|
||||
@spec unsuspend_profile(any(), map(), Absinthe.Resolution.t()) ::
|
||||
{:ok, Actor.t()} | {:error, String.t()}
|
||||
def unsuspend_profile(_parent, %{id: id}, %{
|
||||
context: %{current_user: %User{role: role} = user}
|
||||
context: %{
|
||||
current_user: %User{role: role},
|
||||
current_actor: %Actor{} = moderator_actor
|
||||
}
|
||||
})
|
||||
when is_moderator(role) do
|
||||
with {:moderator_actor, %Actor{} = moderator_actor} <-
|
||||
{:moderator_actor, Users.get_actor_for_user(user)},
|
||||
%Actor{suspended: true} = actor <-
|
||||
with %Actor{suspended: true} = actor <-
|
||||
Actors.get_actor_with_preload(id, true),
|
||||
{:delete_tombstones, {_, nil}} <-
|
||||
{:delete_tombstones, Mobilizon.Tombstone.delete_actor_tombstones(id)},
|
||||
|
|
|
@ -6,14 +6,13 @@ defmodule Mobilizon.GraphQL.Resolvers.Admin do
|
|||
import Mobilizon.Users.Guards
|
||||
|
||||
alias Mobilizon.{Actors, Admin, Config, Events}
|
||||
alias Mobilizon.Actors.Actor
|
||||
alias Mobilizon.Actors.{Actor, Follower}
|
||||
alias Mobilizon.Admin.{ActionLog, Setting}
|
||||
alias Mobilizon.Cldr.Language
|
||||
alias Mobilizon.Config
|
||||
alias Mobilizon.Discussions.Comment
|
||||
alias Mobilizon.Events.Event
|
||||
alias Mobilizon.Federation.ActivityPub
|
||||
alias Mobilizon.Federation.ActivityPub.Relay
|
||||
alias Mobilizon.Federation.ActivityPub.{Actions, Relay}
|
||||
alias Mobilizon.Reports.{Note, Report}
|
||||
alias Mobilizon.Service.Statistics
|
||||
alias Mobilizon.Storage.Page
|
||||
|
@ -21,6 +20,8 @@ defmodule Mobilizon.GraphQL.Resolvers.Admin do
|
|||
import Mobilizon.Web.Gettext
|
||||
require Logger
|
||||
|
||||
@spec list_action_logs(any(), map(), Absinthe.Resolution.t()) ::
|
||||
{:ok, Page.t(ActionLog.t())} | {:error, String.t()}
|
||||
def list_action_logs(
|
||||
_parent,
|
||||
%{page: page, limit: limit},
|
||||
|
@ -38,10 +39,10 @@ defmodule Mobilizon.GraphQL.Resolvers.Admin do
|
|||
id: id,
|
||||
inserted_at: inserted_at
|
||||
} = action_log ->
|
||||
with data when is_map(data) <-
|
||||
transform_action_log(String.to_existing_atom(target_type), action, action_log) do
|
||||
Map.merge(data, %{actor: actor, id: id, inserted_at: inserted_at})
|
||||
end
|
||||
target_type
|
||||
|> String.to_existing_atom()
|
||||
|> transform_action_log(action, action_log)
|
||||
|> Map.merge(%{actor: actor, id: id, inserted_at: inserted_at})
|
||||
end)
|
||||
|> Enum.filter(& &1)
|
||||
|
||||
|
@ -53,6 +54,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Admin do
|
|||
{:error, dgettext("errors", "You need to be logged-in and a moderator to list action logs")}
|
||||
end
|
||||
|
||||
@spec transform_action_log(module(), atom(), ActionLog.t()) :: map()
|
||||
defp transform_action_log(
|
||||
Report,
|
||||
:update,
|
||||
|
@ -123,6 +125,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Admin do
|
|||
end
|
||||
|
||||
# Changes are stored as %{"key" => "value"} so we need to convert them back as struct
|
||||
@spec convert_changes_to_struct(module(), map()) :: struct()
|
||||
defp convert_changes_to_struct(struct, %{"report_id" => _report_id} = changes) do
|
||||
with data <- for({key, val} <- changes, into: %{}, do: {String.to_existing_atom(key), val}),
|
||||
data <- Map.put(data, :report, Mobilizon.Reports.get_report(data.report_id)) do
|
||||
|
@ -143,6 +146,8 @@ defmodule Mobilizon.GraphQL.Resolvers.Admin do
|
|||
end
|
||||
|
||||
# datetimes are not unserialized as DateTime/NaiveDateTime so we do it manually with changeset data
|
||||
@spec process_eventual_type(Ecto.Changeset.t(), String.t(), String.t() | nil) ::
|
||||
DateTime.t() | NaiveDateTime.t() | any()
|
||||
defp process_eventual_type(changeset, key, val) do
|
||||
cond do
|
||||
changeset[String.to_existing_atom(key)] == :utc_datetime and not is_nil(val) ->
|
||||
|
@ -158,6 +163,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Admin do
|
|||
end
|
||||
end
|
||||
|
||||
@spec get_list_of_languages(any(), any(), any()) :: {:ok, String.t()} | {:error, any()}
|
||||
def get_list_of_languages(_parent, %{codes: codes}, _resolution) when is_list(codes) do
|
||||
locale = Gettext.get_locale()
|
||||
locale = if Cldr.known_locale_name?(locale), do: locale, else: "en"
|
||||
|
@ -187,6 +193,8 @@ defmodule Mobilizon.GraphQL.Resolvers.Admin do
|
|||
end
|
||||
end
|
||||
|
||||
@spec get_dashboard(any(), any(), Absinthe.Resolution.t()) ::
|
||||
{:ok, map()} | {:error, String.t()}
|
||||
def get_dashboard(_parent, _args, %{context: %{current_user: %User{role: role}}})
|
||||
when is_admin(role) do
|
||||
last_public_event_published =
|
||||
|
@ -225,6 +233,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Admin do
|
|||
)}
|
||||
end
|
||||
|
||||
@spec get_settings(any(), any(), Absinthe.Resolution.t()) :: {:ok, map()} | {:error, String.t()}
|
||||
def get_settings(_parent, _args, %{
|
||||
context: %{current_user: %User{role: role}}
|
||||
})
|
||||
|
@ -237,6 +246,8 @@ defmodule Mobilizon.GraphQL.Resolvers.Admin do
|
|||
dgettext("errors", "You need to be logged-in and an administrator to access admin settings")}
|
||||
end
|
||||
|
||||
@spec save_settings(any(), map(), Absinthe.Resolution.t()) ::
|
||||
{:ok, map()} | {:error, String.t()}
|
||||
def save_settings(_parent, args, %{
|
||||
context: %{current_user: %User{role: role}}
|
||||
})
|
||||
|
@ -261,6 +272,8 @@ defmodule Mobilizon.GraphQL.Resolvers.Admin do
|
|||
dgettext("errors", "You need to be logged-in and an administrator to save admin settings")}
|
||||
end
|
||||
|
||||
@spec list_relay_followers(any(), map(), Absinthe.Resolution.t()) ::
|
||||
{:ok, Page.t(Follower.t())} | {:error, :unauthorized | :unauthenticated}
|
||||
def list_relay_followers(
|
||||
_parent,
|
||||
%{page: page, limit: limit},
|
||||
|
@ -283,6 +296,8 @@ defmodule Mobilizon.GraphQL.Resolvers.Admin do
|
|||
{:error, :unauthenticated}
|
||||
end
|
||||
|
||||
@spec list_relay_followings(any(), map(), Absinthe.Resolution.t()) ::
|
||||
{:ok, Page.t(Follower.t())} | {:error, :unauthorized | :unauthenticated}
|
||||
def list_relay_followings(
|
||||
_parent,
|
||||
%{page: page, limit: limit},
|
||||
|
@ -305,28 +320,34 @@ defmodule Mobilizon.GraphQL.Resolvers.Admin do
|
|||
{:error, :unauthenticated}
|
||||
end
|
||||
|
||||
@spec create_relay(any(), map(), Absinthe.Resolution.t()) ::
|
||||
{:ok, Follower.t()} | {:error, any()}
|
||||
def create_relay(_parent, %{address: address}, %{context: %{current_user: %User{role: role}}})
|
||||
when is_admin(role) do
|
||||
case Relay.follow(address) do
|
||||
{:ok, _activity, follow} ->
|
||||
{:ok, follow}
|
||||
|
||||
{:error, err} when is_binary(err) ->
|
||||
{:error, err} ->
|
||||
{:error, err}
|
||||
end
|
||||
end
|
||||
|
||||
@spec remove_relay(any(), map(), Absinthe.Resolution.t()) ::
|
||||
{:ok, Follower.t()} | {:error, any()}
|
||||
def remove_relay(_parent, %{address: address}, %{context: %{current_user: %User{role: role}}})
|
||||
when is_admin(role) do
|
||||
case Relay.unfollow(address) do
|
||||
{:ok, _activity, follow} ->
|
||||
{:ok, follow}
|
||||
|
||||
{:error, {:error, err}} when is_binary(err) ->
|
||||
{:error, err} ->
|
||||
{:error, err}
|
||||
end
|
||||
end
|
||||
|
||||
@spec accept_subscription(any(), map(), Absinthe.Resolution.t()) ::
|
||||
{:ok, Follower.t()} | {:error, any()}
|
||||
def accept_subscription(
|
||||
_parent,
|
||||
%{address: address},
|
||||
|
@ -337,14 +358,13 @@ defmodule Mobilizon.GraphQL.Resolvers.Admin do
|
|||
{:ok, _activity, follow} ->
|
||||
{:ok, follow}
|
||||
|
||||
{:error, {:error, err}} when is_binary(err) ->
|
||||
{:error, err}
|
||||
|
||||
{:error, err} when is_binary(err) ->
|
||||
{:error, err} ->
|
||||
{:error, err}
|
||||
end
|
||||
end
|
||||
|
||||
@spec reject_subscription(any(), map(), Absinthe.Resolution.t()) ::
|
||||
{:ok, Follower.t()} | {:error, any()}
|
||||
def reject_subscription(
|
||||
_parent,
|
||||
%{address: address},
|
||||
|
@ -355,15 +375,12 @@ defmodule Mobilizon.GraphQL.Resolvers.Admin do
|
|||
{:ok, _activity, follow} ->
|
||||
{:ok, follow}
|
||||
|
||||
{:error, {:error, err}} when is_binary(err) ->
|
||||
{:error, err}
|
||||
|
||||
{:error, err} when is_binary(err) ->
|
||||
{:error, err} ->
|
||||
{:error, err}
|
||||
end
|
||||
end
|
||||
|
||||
@spec eventually_update_instance_actor(map()) :: :ok
|
||||
@spec eventually_update_instance_actor(map()) :: :ok | {:error, :instance_actor_update_failure}
|
||||
defp eventually_update_instance_actor(admin_setting_args) do
|
||||
args = %{}
|
||||
new_instance_description = Map.get(admin_setting_args, :instance_description)
|
||||
|
@ -385,16 +402,18 @@ defmodule Mobilizon.GraphQL.Resolvers.Admin do
|
|||
do: Map.put(args, :name, new_instance_name),
|
||||
else: args
|
||||
|
||||
with {:changes, true} <- {:changes, args != %{}},
|
||||
%Actor{} = instance_actor <- Relay.get_actor(),
|
||||
{:ok, _activity, _actor} <- ActivityPub.update(instance_actor, args, true) do
|
||||
:ok
|
||||
else
|
||||
{:changes, false} ->
|
||||
if args != %{} do
|
||||
%Actor{} = instance_actor = Relay.get_actor()
|
||||
|
||||
case Actions.Update.update(instance_actor, args, true) do
|
||||
{:ok, _activity, _actor} ->
|
||||
:ok
|
||||
|
||||
err ->
|
||||
err
|
||||
{:error, _err} ->
|
||||
{:error, :instance_actor_update_failure}
|
||||
end
|
||||
else
|
||||
:ok
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -3,7 +3,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Comment do
|
|||
Handles the comment-related GraphQL calls.
|
||||
"""
|
||||
|
||||
alias Mobilizon.{Actors, Admin, Discussions, Events, Users}
|
||||
alias Mobilizon.{Actors, Admin, Discussions, Events}
|
||||
alias Mobilizon.Actors.Actor
|
||||
alias Mobilizon.Discussions.Comment, as: CommentModel
|
||||
alias Mobilizon.Events.{Event, EventOptions}
|
||||
|
@ -14,61 +14,78 @@ defmodule Mobilizon.GraphQL.Resolvers.Comment do
|
|||
|
||||
require Logger
|
||||
|
||||
@spec get_thread(any(), map(), Absinthe.Resolution.t()) :: {:ok, [CommentModel.t()]}
|
||||
def get_thread(_parent, %{id: thread_id}, _context) do
|
||||
{:ok, Discussions.get_thread_replies(thread_id)}
|
||||
end
|
||||
|
||||
@spec create_comment(any(), map(), Absinthe.Resolution.t()) ::
|
||||
{:ok, CommentModel.t()} | {:error, :unauthorized | :not_found | any() | String.t()}
|
||||
def create_comment(
|
||||
_parent,
|
||||
%{event_id: event_id} = args,
|
||||
%{
|
||||
context: %{
|
||||
current_user: %User{} = user
|
||||
current_actor: %Actor{id: actor_id}
|
||||
}
|
||||
}
|
||||
) do
|
||||
with %Actor{id: actor_id} <- Users.get_actor_for_user(user),
|
||||
{:find_event,
|
||||
case Events.get_event(event_id) do
|
||||
{:ok,
|
||||
%Event{
|
||||
options: %EventOptions{comment_moderation: comment_moderation},
|
||||
organizer_actor_id: organizer_actor_id
|
||||
}}} <-
|
||||
{:find_event, Events.get_event(event_id)},
|
||||
{:allowed, true} <-
|
||||
{:allowed, comment_moderation != :closed || actor_id == organizer_actor_id},
|
||||
args <- Map.put(args, :actor_id, actor_id),
|
||||
{:ok, _, %CommentModel{} = comment} <-
|
||||
Comments.create_comment(args) do
|
||||
}} ->
|
||||
if comment_moderation != :closed || actor_id == organizer_actor_id do
|
||||
args = Map.put(args, :actor_id, actor_id)
|
||||
|
||||
case Comments.create_comment(args) do
|
||||
{:ok, _, %CommentModel{} = comment} ->
|
||||
{:ok, comment}
|
||||
else
|
||||
|
||||
{:error, err} ->
|
||||
{:error, err}
|
||||
|
||||
{:allowed, false} ->
|
||||
end
|
||||
else
|
||||
{:error, :unauthorized}
|
||||
end
|
||||
|
||||
{:error, :event_not_found} ->
|
||||
{:error, :not_found}
|
||||
end
|
||||
end
|
||||
|
||||
def create_comment(_parent, _args, _context) do
|
||||
{:error, dgettext("errors", "You are not allowed to create a comment if not connected")}
|
||||
end
|
||||
|
||||
@spec update_comment(any(), map(), Absinthe.Resolution.t()) ::
|
||||
{:ok, CommentModel.t()} | {:error, :unauthorized | :not_found | any() | String.t()}
|
||||
def update_comment(
|
||||
_parent,
|
||||
%{text: text, comment_id: comment_id},
|
||||
%{
|
||||
context: %{
|
||||
current_user: %User{} = user
|
||||
current_actor: %Actor{id: actor_id}
|
||||
}
|
||||
}
|
||||
) do
|
||||
with {:actor, %Actor{id: actor_id} = _actor} <- {:actor, Users.get_actor_for_user(user)},
|
||||
%CommentModel{actor_id: comment_actor_id} = comment <-
|
||||
Mobilizon.Discussions.get_comment_with_preload(comment_id),
|
||||
true <- actor_id === comment_actor_id,
|
||||
{:ok, _, %CommentModel{} = comment} <- Comments.update_comment(comment, %{text: text}) do
|
||||
case Mobilizon.Discussions.get_comment_with_preload(comment_id) do
|
||||
%CommentModel{actor_id: comment_actor_id} = comment ->
|
||||
if actor_id == comment_actor_id do
|
||||
case Comments.update_comment(comment, %{text: text}) do
|
||||
{:ok, _, %CommentModel{} = comment} ->
|
||||
{:ok, comment}
|
||||
|
||||
{:error, err} ->
|
||||
{:error, err}
|
||||
end
|
||||
else
|
||||
{:error, dgettext("errors", "You are not the comment creator")}
|
||||
end
|
||||
|
||||
nil ->
|
||||
{:error, :not_found}
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -81,13 +98,13 @@ defmodule Mobilizon.GraphQL.Resolvers.Comment do
|
|||
%{comment_id: comment_id},
|
||||
%{
|
||||
context: %{
|
||||
current_user: %User{role: role} = user
|
||||
current_user: %User{role: role},
|
||||
current_actor: %Actor{id: actor_id} = actor
|
||||
}
|
||||
}
|
||||
) do
|
||||
with {:actor, %Actor{id: actor_id} = actor} <- {:actor, Users.get_actor_for_user(user)},
|
||||
%CommentModel{deleted_at: nil} = comment <-
|
||||
Discussions.get_comment_with_preload(comment_id) do
|
||||
case Discussions.get_comment_with_preload(comment_id) do
|
||||
%CommentModel{deleted_at: nil} = comment ->
|
||||
cond do
|
||||
{:comment_can_be_managed, true} == CommentModel.can_be_managed_by(comment, actor_id) ->
|
||||
do_delete_comment(comment, actor)
|
||||
|
@ -103,9 +120,12 @@ defmodule Mobilizon.GraphQL.Resolvers.Comment do
|
|||
true ->
|
||||
{:error, dgettext("errors", "You cannot delete this comment")}
|
||||
end
|
||||
else
|
||||
|
||||
%CommentModel{deleted_at: deleted_at} when not is_nil(deleted_at) ->
|
||||
{:error, dgettext("errors", "Comment is already deleted")}
|
||||
|
||||
nil ->
|
||||
{:error, dgettext("errors", "Comment not found")}
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -113,10 +133,15 @@ defmodule Mobilizon.GraphQL.Resolvers.Comment do
|
|||
{:error, dgettext("errors", "You are not allowed to delete a comment if not connected")}
|
||||
end
|
||||
|
||||
@spec do_delete_comment(CommentModel.t(), Actor.t()) ::
|
||||
{:ok, CommentModel.t()} | {:error, any()}
|
||||
defp do_delete_comment(%CommentModel{} = comment, %Actor{} = actor) do
|
||||
with {:ok, _, %CommentModel{} = comment} <-
|
||||
Comments.delete_comment(comment, actor) do
|
||||
case Comments.delete_comment(comment, actor) do
|
||||
{:ok, _, %CommentModel{} = comment} ->
|
||||
{:ok, comment}
|
||||
|
||||
{:error, err} ->
|
||||
{:error, err}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -8,6 +8,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Config do
|
|||
@doc """
|
||||
Gets config.
|
||||
"""
|
||||
@spec get_config(any(), map(), Absinthe.Resolution.t()) :: {:ok, map()}
|
||||
def get_config(_parent, _params, %{context: %{ip: ip}}) do
|
||||
geolix = Geolix.lookup(ip)
|
||||
|
||||
|
@ -28,6 +29,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Config do
|
|||
{:ok, data}
|
||||
end
|
||||
|
||||
@spec terms(any(), map(), Absinthe.Resolution.t()) :: {:ok, map()}
|
||||
def terms(_parent, %{locale: locale}, _resolution) do
|
||||
type = Config.instance_terms_type()
|
||||
|
||||
|
@ -41,6 +43,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Config do
|
|||
{:ok, %{body_html: body_html, type: type, url: url}}
|
||||
end
|
||||
|
||||
@spec privacy(any(), map(), Absinthe.Resolution.t()) :: {:ok, map()}
|
||||
def privacy(_parent, %{locale: locale}, _resolution) do
|
||||
type = Config.instance_privacy_type()
|
||||
|
||||
|
@ -54,6 +57,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Config do
|
|||
{:ok, %{body_html: body_html, type: type, url: url}}
|
||||
end
|
||||
|
||||
@spec config_cache :: map()
|
||||
defp config_cache do
|
||||
case Cachex.fetch(:config, "full_config", fn _key ->
|
||||
case build_config_cache() do
|
||||
|
@ -62,10 +66,11 @@ defmodule Mobilizon.GraphQL.Resolvers.Config do
|
|||
end
|
||||
end) do
|
||||
{status, value} when status in [:ok, :commit] -> value
|
||||
_err -> nil
|
||||
_err -> %{}
|
||||
end
|
||||
end
|
||||
|
||||
@spec build_config_cache :: map()
|
||||
defp build_config_cache do
|
||||
%{
|
||||
name: Config.instance_name(),
|
||||
|
|
|
@ -3,26 +3,27 @@ defmodule Mobilizon.GraphQL.Resolvers.Discussion do
|
|||
Handles the group-related GraphQL calls.
|
||||
"""
|
||||
|
||||
alias Mobilizon.{Actors, Discussions, Users}
|
||||
alias Mobilizon.{Actors, Discussions}
|
||||
alias Mobilizon.Actors.Actor
|
||||
alias Mobilizon.Discussions.{Comment, Discussion}
|
||||
alias Mobilizon.Federation.ActivityPub
|
||||
alias Mobilizon.Federation.ActivityPub.Actions
|
||||
alias Mobilizon.GraphQL.API.Comments
|
||||
alias Mobilizon.Storage.Page
|
||||
alias Mobilizon.Users.User
|
||||
import Mobilizon.Web.Gettext
|
||||
|
||||
@spec find_discussions_for_actor(Actor.t(), map(), Absinthe.Resolution.t()) ::
|
||||
{:ok, Page.t(Discussion.t())} | {:error, :unauthenticated}
|
||||
def find_discussions_for_actor(
|
||||
%Actor{id: group_id},
|
||||
%{page: page, limit: limit},
|
||||
%{
|
||||
context: %{
|
||||
current_user: %User{} = user
|
||||
current_actor: %Actor{id: actor_id}
|
||||
}
|
||||
}
|
||||
) do
|
||||
with {:actor, %Actor{id: actor_id} = _actor} <- {:actor, Users.get_actor_for_user(user)},
|
||||
{:member, true} <- {:member, Actors.is_member?(actor_id, group_id)},
|
||||
with {:member, true} <- {:member, Actors.is_member?(actor_id, group_id)},
|
||||
{:ok, %Actor{type: :Group} = group} <- Actors.get_group_by_actor_id(group_id) do
|
||||
{:ok, Discussions.find_discussions_for_actor(group, page, limit)}
|
||||
else
|
||||
|
@ -31,30 +32,42 @@ defmodule Mobilizon.GraphQL.Resolvers.Discussion do
|
|||
end
|
||||
end
|
||||
|
||||
def find_discussions_for_actor(%Actor{}, _args, _resolution) do
|
||||
def find_discussions_for_actor(%Actor{}, _args, %{
|
||||
context: %{
|
||||
current_user: %User{}
|
||||
}
|
||||
}) do
|
||||
{:ok, %Page{total: 0, elements: []}}
|
||||
end
|
||||
|
||||
def find_discussions_for_actor(%Actor{}, _args, _resolution), do: {:error, :unauthenticated}
|
||||
|
||||
@spec get_discussion(any(), map(), Absinthe.Resolution.t()) ::
|
||||
{:ok, Discussion.t()} | {:error, :unauthorized | :discussion_not_found | String.t()}
|
||||
def get_discussion(_parent, %{id: id}, %{
|
||||
context: %{
|
||||
current_user: %User{} = user
|
||||
current_actor: %Actor{id: creator_id}
|
||||
}
|
||||
}) do
|
||||
with {:actor, %Actor{id: creator_id} = _actor} <- {:actor, Users.get_actor_for_user(user)},
|
||||
%Discussion{actor_id: actor_id} = discussion <-
|
||||
Discussions.get_discussion(id),
|
||||
{:member, true} <- {:member, Actors.is_member?(creator_id, actor_id)} do
|
||||
case Discussions.get_discussion(id) do
|
||||
%Discussion{actor_id: actor_id} = discussion ->
|
||||
if Actors.is_member?(creator_id, actor_id) do
|
||||
{:ok, discussion}
|
||||
else
|
||||
{:error, :unauthorized}
|
||||
end
|
||||
|
||||
nil ->
|
||||
{:error, :discussion_not_found}
|
||||
end
|
||||
end
|
||||
|
||||
def get_discussion(_parent, %{slug: slug}, %{
|
||||
context: %{
|
||||
current_user: %User{} = user
|
||||
current_actor: %Actor{id: creator_id}
|
||||
}
|
||||
}) do
|
||||
with {:actor, %Actor{id: creator_id} = _actor} <- {:actor, Users.get_actor_for_user(user)},
|
||||
%Discussion{actor_id: actor_id} = discussion <-
|
||||
with %Discussion{actor_id: actor_id} = discussion <-
|
||||
Discussions.get_discussion_by_slug(slug),
|
||||
{:member, true} <- {:member, Actors.is_member?(creator_id, actor_id)} do
|
||||
{:ok, discussion}
|
||||
|
@ -76,6 +89,8 @@ defmodule Mobilizon.GraphQL.Resolvers.Discussion do
|
|||
def get_discussion(_parent, _args, _resolution),
|
||||
do: {:error, dgettext("errors", "You need to be logged-in to access discussions")}
|
||||
|
||||
@spec get_comments_for_discussion(Discussion.t(), map(), Absinthe.Resolution.t()) ::
|
||||
{:ok, Page.t(Discussion.t())}
|
||||
def get_comments_for_discussion(
|
||||
%Discussion{id: discussion_id},
|
||||
%{page: page, limit: limit},
|
||||
|
@ -84,48 +99,54 @@ defmodule Mobilizon.GraphQL.Resolvers.Discussion do
|
|||
{:ok, Discussions.get_comments_for_discussion(discussion_id, page, limit)}
|
||||
end
|
||||
|
||||
@spec create_discussion(any(), map(), Absinthe.Resolution.t()) ::
|
||||
{:ok, Discussion.t()}
|
||||
| {:error, Ecto.Changeset.t() | String.t() | :unauthorized | :unauthenticated}
|
||||
def create_discussion(
|
||||
_parent,
|
||||
%{title: title, text: text, actor_id: group_id},
|
||||
%{
|
||||
context: %{
|
||||
current_user: %User{} = user
|
||||
current_actor: %Actor{id: creator_id}
|
||||
}
|
||||
}
|
||||
) do
|
||||
with {:actor, %Actor{id: creator_id} = _actor} <- {:actor, Users.get_actor_for_user(user)},
|
||||
{:member, true} <- {:member, Actors.is_member?(creator_id, group_id)},
|
||||
{:ok, _activity, %Discussion{} = discussion} <-
|
||||
Comments.create_discussion(%{
|
||||
if Actors.is_member?(creator_id, group_id) do
|
||||
case Comments.create_discussion(%{
|
||||
title: title,
|
||||
text: text,
|
||||
actor_id: group_id,
|
||||
creator_id: creator_id,
|
||||
attributed_to_id: group_id
|
||||
}) do
|
||||
{:ok, _activity, %Discussion{} = discussion} ->
|
||||
{:ok, discussion}
|
||||
else
|
||||
{:error, type, err, _} when type in [:discussion, :comment] ->
|
||||
|
||||
{:error, %Ecto.Changeset{} = err} ->
|
||||
{:error, err}
|
||||
|
||||
{:member, false} ->
|
||||
{:error, _err} ->
|
||||
{:error, dgettext("errors", "Error while creating a discussion")}
|
||||
end
|
||||
else
|
||||
{:error, :unauthorized}
|
||||
end
|
||||
end
|
||||
|
||||
def create_discussion(_, _, _), do: {:error, :unauthenticated}
|
||||
|
||||
@spec reply_to_discussion(any(), map(), Absinthe.Resolution.t()) ::
|
||||
{:ok, Discussion.t()} | {:error, :discussion_not_found | :unauthenticated}
|
||||
def reply_to_discussion(
|
||||
_parent,
|
||||
%{text: text, discussion_id: discussion_id},
|
||||
%{
|
||||
context: %{
|
||||
current_user: %User{} = user
|
||||
current_actor: %Actor{id: creator_id}
|
||||
}
|
||||
}
|
||||
) do
|
||||
with {:actor, %Actor{id: creator_id} = _actor} <- {:actor, Users.get_actor_for_user(user)},
|
||||
{:no_discussion,
|
||||
with {:no_discussion,
|
||||
%Discussion{
|
||||
actor_id: actor_id,
|
||||
last_comment: %Comment{
|
||||
|
@ -155,22 +176,22 @@ defmodule Mobilizon.GraphQL.Resolvers.Discussion do
|
|||
|
||||
def reply_to_discussion(_, _, _), do: {:error, :unauthenticated}
|
||||
|
||||
@spec update_discussion(map(), map(), map()) :: {:ok, Discussion.t()}
|
||||
@spec update_discussion(map(), map(), map()) ::
|
||||
{:ok, Discussion.t()} | {:error, :unauthorized | :unauthenticated}
|
||||
def update_discussion(
|
||||
_parent,
|
||||
%{title: title, discussion_id: discussion_id},
|
||||
%{
|
||||
context: %{
|
||||
current_user: %User{} = user
|
||||
current_actor: %Actor{id: creator_id}
|
||||
}
|
||||
}
|
||||
) do
|
||||
with {:actor, %Actor{id: creator_id} = _actor} <- {:actor, Users.get_actor_for_user(user)},
|
||||
{:no_discussion, %Discussion{actor_id: actor_id} = discussion} <-
|
||||
with {:no_discussion, %Discussion{actor_id: actor_id} = discussion} <-
|
||||
{:no_discussion, Discussions.get_discussion(discussion_id)},
|
||||
{:member, true} <- {:member, Actors.is_member?(creator_id, actor_id)},
|
||||
{:ok, _activity, %Discussion{} = discussion} <-
|
||||
ActivityPub.update(
|
||||
Actions.Update.update(
|
||||
discussion,
|
||||
%{
|
||||
title: title
|
||||
|
@ -185,17 +206,19 @@ defmodule Mobilizon.GraphQL.Resolvers.Discussion do
|
|||
|
||||
def update_discussion(_, _, _), do: {:error, :unauthenticated}
|
||||
|
||||
@spec delete_discussion(any(), map(), Absinthe.Resolution.t()) ::
|
||||
{:ok, Discussion.t()} | {:error, String.t() | :unauthorized | :unauthenticated}
|
||||
def delete_discussion(_parent, %{discussion_id: discussion_id}, %{
|
||||
context: %{
|
||||
current_user: %User{} = user
|
||||
current_user: %User{},
|
||||
current_actor: %Actor{id: creator_id} = actor
|
||||
}
|
||||
}) do
|
||||
with {:actor, %Actor{id: creator_id} = actor} <- {:actor, Users.get_actor_for_user(user)},
|
||||
{:no_discussion, %Discussion{actor_id: actor_id} = discussion} <-
|
||||
with {:no_discussion, %Discussion{actor_id: actor_id} = discussion} <-
|
||||
{:no_discussion, Discussions.get_discussion(discussion_id)},
|
||||
{:member, true} <- {:member, Actors.is_member?(creator_id, actor_id)},
|
||||
{:ok, _activity, %Discussion{} = discussion} <-
|
||||
ActivityPub.delete(discussion, actor) do
|
||||
Actions.Delete.delete(discussion, actor) do
|
||||
{:ok, discussion}
|
||||
else
|
||||
{:no_discussion, _} ->
|
||||
|
|
|
@ -3,7 +3,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Event do
|
|||
Handles the event-related GraphQL calls.
|
||||
"""
|
||||
|
||||
alias Mobilizon.{Actors, Admin, Events, Users}
|
||||
alias Mobilizon.{Actors, Admin, Events}
|
||||
alias Mobilizon.Actors.Actor
|
||||
alias Mobilizon.Config
|
||||
alias Mobilizon.Events.{Event, EventParticipantStats}
|
||||
|
@ -21,14 +21,17 @@ defmodule Mobilizon.GraphQL.Resolvers.Event do
|
|||
@event_max_limit 100
|
||||
@number_of_related_events 3
|
||||
|
||||
@spec organizer_for_event(Event.t(), map(), Absinthe.Resolution.t()) ::
|
||||
{:ok, Actor.t() | nil} | {:error, String.t()}
|
||||
def organizer_for_event(
|
||||
%Event{attributed_to_id: attributed_to_id, organizer_actor_id: organizer_actor_id},
|
||||
_args,
|
||||
%{context: %{current_user: %User{role: user_role} = user}} = _resolution
|
||||
%{
|
||||
context: %{current_user: %User{role: user_role}, current_actor: %Actor{id: actor_id}}
|
||||
} = _resolution
|
||||
)
|
||||
when not is_nil(attributed_to_id) do
|
||||
with %Actor{id: group_id} <- Actors.get_actor(attributed_to_id),
|
||||
%Actor{id: actor_id} <- Users.get_actor_for_user(user),
|
||||
{:member, true} <-
|
||||
{:member, Actors.is_member?(actor_id, group_id) or is_moderator(user_role)},
|
||||
%Actor{} = actor <- Actors.get_actor(organizer_actor_id) do
|
||||
|
@ -61,6 +64,8 @@ defmodule Mobilizon.GraphQL.Resolvers.Event do
|
|||
end
|
||||
end
|
||||
|
||||
@spec list_events(any(), map(), Absinthe.Resolution.t()) ::
|
||||
{:ok, Page.t(Event.t())} | {:error, :events_max_limit_reached}
|
||||
def list_events(
|
||||
_parent,
|
||||
%{page: page, limit: limit, order_by: order_by, direction: direction},
|
||||
|
@ -74,13 +79,13 @@ defmodule Mobilizon.GraphQL.Resolvers.Event do
|
|||
{:error, :events_max_limit_reached}
|
||||
end
|
||||
|
||||
@spec find_private_event(any(), map(), Absinthe.Resolution.t()) ::
|
||||
{:ok, Event.t()} | {:error, :event_not_found}
|
||||
defp find_private_event(
|
||||
_parent,
|
||||
%{uuid: uuid},
|
||||
%{context: %{current_user: %User{} = user}} = _resolution
|
||||
%{context: %{current_actor: %Actor{} = profile}} = _resolution
|
||||
) do
|
||||
%Actor{} = profile = Users.get_actor_for_user(user)
|
||||
|
||||
case Events.get_event_by_uuid_with_preload(uuid) do
|
||||
# Event attributed to group
|
||||
%Event{attributed_to: %Actor{}} = event ->
|
||||
|
@ -107,6 +112,8 @@ defmodule Mobilizon.GraphQL.Resolvers.Event do
|
|||
{:error, :event_not_found}
|
||||
end
|
||||
|
||||
@spec find_event(any(), map(), Absinthe.Resolution.t()) ::
|
||||
{:ok, Event.t()} | {:error, :event_not_found}
|
||||
def find_event(parent, %{uuid: uuid} = args, %{context: context} = resolution) do
|
||||
with {:has_event, %Event{} = event} <-
|
||||
{:has_event, Events.get_public_event_by_uuid_with_preload(uuid)},
|
||||
|
@ -133,15 +140,15 @@ defmodule Mobilizon.GraphQL.Resolvers.Event do
|
|||
@doc """
|
||||
List participants for event (through an event request)
|
||||
"""
|
||||
@spec list_participants_for_event(Event.t(), map(), Absinthe.Resolution.t()) ::
|
||||
{:ok, Page.t(Participant.t())} | {:error, String.t()}
|
||||
def list_participants_for_event(
|
||||
%Event{id: event_id} = event,
|
||||
%{page: page, limit: limit, roles: roles},
|
||||
%{context: %{current_user: %User{} = user}} = _resolution
|
||||
%{context: %{current_actor: %Actor{} = actor}} = _resolution
|
||||
) do
|
||||
with %Actor{} = actor <- Users.get_actor_for_user(user),
|
||||
# Check that moderator has right
|
||||
{:event_can_be_managed, true} <-
|
||||
{:event_can_be_managed, can_event_be_updated_by?(event, actor)} do
|
||||
if can_event_be_updated_by?(event, actor) do
|
||||
roles =
|
||||
case roles do
|
||||
nil ->
|
||||
|
@ -160,7 +167,6 @@ defmodule Mobilizon.GraphQL.Resolvers.Event do
|
|||
participants = Events.list_participants_for_event(event_id, roles, page, limit)
|
||||
{:ok, participants}
|
||||
else
|
||||
{:event_can_be_managed, _} ->
|
||||
{:error,
|
||||
dgettext("errors", "Provided profile doesn't have moderator permissions on this event")}
|
||||
end
|
||||
|
@ -170,6 +176,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Event do
|
|||
{:ok, %{total: 0, elements: []}}
|
||||
end
|
||||
|
||||
@spec stats_participants(Event.t(), map(), Absinthe.Resolution.t()) :: {:ok, map()}
|
||||
def stats_participants(
|
||||
%Event{participant_stats: %EventParticipantStats{} = stats, id: event_id} = _event,
|
||||
_args,
|
||||
|
@ -202,6 +209,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Event do
|
|||
@doc """
|
||||
List related events
|
||||
"""
|
||||
@spec list_related_events(Event.t(), map(), Absinthe.Resolution.t()) :: {:ok, list(Event.t())}
|
||||
def list_related_events(
|
||||
%Event{tags: tags, organizer_actor: organizer_actor, uuid: uuid},
|
||||
_args,
|
||||
|
@ -243,11 +251,14 @@ defmodule Mobilizon.GraphQL.Resolvers.Event do
|
|||
{:ok, events}
|
||||
end
|
||||
|
||||
@spec uniq_events(list(Event.t())) :: list(Event.t())
|
||||
defp uniq_events(events), do: Enum.uniq_by(events, fn event -> event.uuid end)
|
||||
|
||||
@doc """
|
||||
Create an event
|
||||
"""
|
||||
@spec create_event(any(), map(), Absinthe.Resolution.t()) ::
|
||||
{:ok, Event.t()} | {:error, String.t() | Ecto.Changeset.t()}
|
||||
def create_event(
|
||||
_parent,
|
||||
%{organizer_actor_id: organizer_actor_id} = args,
|
||||
|
@ -287,15 +298,17 @@ defmodule Mobilizon.GraphQL.Resolvers.Event do
|
|||
@doc """
|
||||
Update an event
|
||||
"""
|
||||
@spec update_event(any(), map(), Absinthe.Resolution.t()) ::
|
||||
{:ok, Event.t()} | {:error, String.t() | Ecto.Changeset.t()}
|
||||
def update_event(
|
||||
_parent,
|
||||
%{event_id: event_id} = args,
|
||||
%{context: %{current_user: %User{} = user}} = _resolution
|
||||
%{context: %{current_user: %User{} = user, current_actor: %Actor{} = actor}} = _resolution
|
||||
) do
|
||||
# See https://github.com/absinthe-graphql/absinthe/issues/490
|
||||
with args <- Map.put(args, :options, args[:options] || %{}),
|
||||
{:ok, %Event{} = event} <- Events.get_event_with_preload(event_id),
|
||||
%Actor{} = actor <- Users.get_actor_for_user(user),
|
||||
args = Map.put(args, :options, args[:options] || %{})
|
||||
|
||||
with {:ok, %Event{} = event} <- Events.get_event_with_preload(event_id),
|
||||
{:ok, args} <- verify_profile_change(args, event, user, actor),
|
||||
{:event_can_be_managed, true} <-
|
||||
{:event_can_be_managed, can_event_be_updated_by?(event, actor)},
|
||||
|
@ -319,7 +332,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Event do
|
|||
{:new_actor, _} ->
|
||||
{:error, dgettext("errors", "You can't attribute this event to this profile.")}
|
||||
|
||||
{:error, _, %Ecto.Changeset{} = error, _} ->
|
||||
{:error, %Ecto.Changeset{} = error} ->
|
||||
{:error, error}
|
||||
end
|
||||
end
|
||||
|
@ -331,13 +344,20 @@ defmodule Mobilizon.GraphQL.Resolvers.Event do
|
|||
@doc """
|
||||
Delete an event
|
||||
"""
|
||||
@spec delete_event(any(), map(), Absinthe.Resolution.t()) ::
|
||||
{:ok, Event.t()} | {:error, String.t() | Ecto.Changeset.t()}
|
||||
def delete_event(
|
||||
_parent,
|
||||
%{event_id: event_id},
|
||||
%{context: %{current_user: %User{role: role} = user}}
|
||||
%{
|
||||
context: %{
|
||||
current_user: %User{role: role},
|
||||
current_actor: %Actor{id: actor_id} = actor
|
||||
}
|
||||
}
|
||||
) do
|
||||
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
|
||||
case Events.get_event_with_preload(event_id) do
|
||||
{:ok, %Event{local: is_local} = event} ->
|
||||
cond do
|
||||
{:event_can_be_managed, true} ==
|
||||
{:event_can_be_managed, can_event_be_deleted_by?(event, actor)} ->
|
||||
|
@ -354,7 +374,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Event do
|
|||
true ->
|
||||
{:error, dgettext("errors", "You cannot delete this event")}
|
||||
end
|
||||
else
|
||||
|
||||
{:error, :event_not_found} ->
|
||||
{:error, dgettext("errors", "Event not found")}
|
||||
end
|
||||
|
@ -364,6 +384,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Event do
|
|||
{:error, dgettext("errors", "You need to be logged-in to delete an event")}
|
||||
end
|
||||
|
||||
@spec do_delete_event(Event.t(), Actor.t(), boolean()) :: {:ok, map()}
|
||||
defp do_delete_event(%Event{} = event, %Actor{} = actor, federate \\ true)
|
||||
when is_boolean(federate) do
|
||||
with {:ok, _activity, event} <- API.Events.delete_event(event, actor) do
|
||||
|
@ -371,6 +392,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Event do
|
|||
end
|
||||
end
|
||||
|
||||
@spec is_organizer_group_member?(map()) :: boolean()
|
||||
defp is_organizer_group_member?(%{
|
||||
attributed_to_id: attributed_to_id,
|
||||
organizer_actor_id: organizer_actor_id
|
||||
|
@ -382,6 +404,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Event do
|
|||
|
||||
defp is_organizer_group_member?(_), do: true
|
||||
|
||||
@spec verify_profile_change(map(), Event.t(), User.t(), Actor.t()) :: {:ok, map()}
|
||||
defp verify_profile_change(
|
||||
args,
|
||||
%Event{attributed_to: %Actor{}},
|
||||
|
|
|
@ -6,7 +6,10 @@ defmodule Mobilizon.GraphQL.Resolvers.Event.Utils do
|
|||
alias Mobilizon.Actors.Actor
|
||||
alias Mobilizon.Events.Event
|
||||
alias Mobilizon.Federation.ActivityPub.Permission
|
||||
import Mobilizon.Service.Guards, only: [is_valid_string: 1]
|
||||
|
||||
@spec can_event_be_updated_by?(%Event{id: String.t()}, Actor.t()) ::
|
||||
boolean
|
||||
def can_event_be_updated_by?(
|
||||
%Event{attributed_to: %Actor{type: :Group}} = event,
|
||||
%Actor{} = actor_member
|
||||
|
@ -21,10 +24,13 @@ defmodule Mobilizon.GraphQL.Resolvers.Event.Utils do
|
|||
Event.can_be_managed_by?(event, actor_member_id)
|
||||
end
|
||||
|
||||
@spec can_event_be_deleted_by?(%Event{id: String.t(), url: String.t()}, Actor.t()) ::
|
||||
boolean
|
||||
def can_event_be_deleted_by?(
|
||||
%Event{attributed_to: %Actor{type: :Group}} = event,
|
||||
%Event{attributed_to: %Actor{type: :Group}, id: event_id, url: event_url} = event,
|
||||
%Actor{} = actor_member
|
||||
) do
|
||||
)
|
||||
when is_valid_string(event_id) and is_valid_string(event_url) do
|
||||
Permission.can_delete_group_object?(actor_member, event)
|
||||
end
|
||||
|
||||
|
|
|
@ -4,9 +4,9 @@ defmodule Mobilizon.GraphQL.Resolvers.Followers do
|
|||
"""
|
||||
|
||||
import Mobilizon.Users.Guards
|
||||
alias Mobilizon.{Actors, Users}
|
||||
alias Mobilizon.Actors
|
||||
alias Mobilizon.Actors.{Actor, Follower}
|
||||
alias Mobilizon.Federation.ActivityPub
|
||||
alias Mobilizon.Federation.ActivityPub.Actions
|
||||
alias Mobilizon.Storage.Page
|
||||
alias Mobilizon.Users.User
|
||||
|
||||
|
@ -16,17 +16,16 @@ defmodule Mobilizon.GraphQL.Resolvers.Followers do
|
|||
%{page: page, limit: limit} = args,
|
||||
%{
|
||||
context: %{
|
||||
current_user: %User{role: user_role} = user
|
||||
current_user: %User{role: user_role},
|
||||
current_actor: %Actor{id: actor_id}
|
||||
}
|
||||
}
|
||||
) do
|
||||
with {:actor, %Actor{id: actor_id} = _actor} <- {:actor, Users.get_actor_for_user(user)},
|
||||
{:member, true} <-
|
||||
{:member, Actors.is_moderator?(actor_id, group_id) or is_moderator(user_role)} do
|
||||
if Actors.is_moderator?(actor_id, group_id) or is_moderator(user_role) do
|
||||
{:ok,
|
||||
Actors.list_paginated_followers_for_actor(group, Map.get(args, :approved), page, limit)}
|
||||
else
|
||||
_ -> {:error, :unauthorized}
|
||||
{:error, :unauthorized}
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -35,19 +34,18 @@ defmodule Mobilizon.GraphQL.Resolvers.Followers do
|
|||
@spec update_follower(any(), map(), map()) :: {:ok, Follower.t()} | {:error, any()}
|
||||
def update_follower(_, %{id: follower_id, approved: approved}, %{
|
||||
context: %{
|
||||
current_user: %User{} = user
|
||||
current_actor: %Actor{id: actor_id}
|
||||
}
|
||||
}) do
|
||||
with {:actor, %Actor{id: actor_id} = _actor} <- {:actor, Users.get_actor_for_user(user)},
|
||||
%Follower{target_actor: %Actor{type: :Group, id: group_id}} = follower <-
|
||||
with %Follower{target_actor: %Actor{type: :Group, id: group_id}} = follower <-
|
||||
Actors.get_follower(follower_id),
|
||||
{:member, true} <-
|
||||
{:member, Actors.is_moderator?(actor_id, group_id)},
|
||||
{:ok, _activity, %Follower{} = follower} <-
|
||||
(if approved do
|
||||
ActivityPub.accept(:follow, follower)
|
||||
Actions.Accept.accept(:follow, follower)
|
||||
else
|
||||
ActivityPub.reject(:follow, follower)
|
||||
Actions.Reject.reject(:follow, follower)
|
||||
end) do
|
||||
{:ok, follower}
|
||||
else
|
||||
|
|
|
@ -4,9 +4,9 @@ defmodule Mobilizon.GraphQL.Resolvers.Group do
|
|||
"""
|
||||
|
||||
import Mobilizon.Users.Guards
|
||||
alias Mobilizon.{Actors, Events, Users}
|
||||
alias Mobilizon.{Actors, Events}
|
||||
alias Mobilizon.Actors.{Actor, Member}
|
||||
alias Mobilizon.Federation.ActivityPub
|
||||
alias Mobilizon.Federation.ActivityPub.Actions
|
||||
alias Mobilizon.Federation.ActivityPub.Actor, as: ActivityPubActor
|
||||
alias Mobilizon.GraphQL.API
|
||||
alias Mobilizon.Users.User
|
||||
|
@ -15,6 +15,12 @@ defmodule Mobilizon.GraphQL.Resolvers.Group do
|
|||
|
||||
require Logger
|
||||
|
||||
@spec find_group(
|
||||
any,
|
||||
%{:preferred_username => binary, optional(any) => any},
|
||||
Absinthe.Resolution.t()
|
||||
) ::
|
||||
{:error, :group_not_found} | {:ok, Actor.t()}
|
||||
@doc """
|
||||
Find a group
|
||||
"""
|
||||
|
@ -23,34 +29,30 @@ defmodule Mobilizon.GraphQL.Resolvers.Group do
|
|||
%{preferred_username: name} = args,
|
||||
%{
|
||||
context: %{
|
||||
current_user: %User{} = user
|
||||
current_actor: %Actor{id: actor_id}
|
||||
}
|
||||
}
|
||||
) do
|
||||
with {:group, {:ok, %Actor{id: group_id, suspended: false} = group}} <-
|
||||
{:group, ActivityPubActor.find_or_make_group_from_nickname(name)},
|
||||
{:actor, %Actor{id: actor_id} = _actor} <- {:actor, Users.get_actor_for_user(user)},
|
||||
{:member, true} <- {:member, Actors.is_member?(actor_id, group_id)} do
|
||||
case ActivityPubActor.find_or_make_group_from_nickname(name) do
|
||||
{:ok, %Actor{id: group_id, suspended: false} = group} ->
|
||||
if Actors.is_member?(actor_id, group_id) do
|
||||
{:ok, group}
|
||||
else
|
||||
{:member, false} ->
|
||||
find_group(parent, args, nil)
|
||||
end
|
||||
|
||||
{:group, _} ->
|
||||
{:error, _err} ->
|
||||
{:error, :group_not_found}
|
||||
|
||||
_ ->
|
||||
{:error, :unknown}
|
||||
end
|
||||
end
|
||||
|
||||
def find_group(_parent, %{preferred_username: name}, _resolution) do
|
||||
with {:ok, %Actor{suspended: false} = actor} <-
|
||||
ActivityPubActor.find_or_make_group_from_nickname(name),
|
||||
%Actor{} = actor <- restrict_fields_for_non_member_request(actor) do
|
||||
case ActivityPubActor.find_or_make_group_from_nickname(name) do
|
||||
{:ok, %Actor{suspended: false} = actor} ->
|
||||
%Actor{} = actor = restrict_fields_for_non_member_request(actor)
|
||||
{:ok, actor}
|
||||
else
|
||||
_ ->
|
||||
|
||||
{:error, _err} ->
|
||||
{:error, :group_not_found}
|
||||
end
|
||||
end
|
||||
|
@ -58,13 +60,18 @@ defmodule Mobilizon.GraphQL.Resolvers.Group do
|
|||
@doc """
|
||||
Get a group
|
||||
"""
|
||||
@spec get_group(any(), map(), Absinthe.Resolution.t()) ::
|
||||
{:ok, Actor.t()} | {:error, String.t()}
|
||||
def get_group(_parent, %{id: id}, %{context: %{current_user: %User{role: role}}}) do
|
||||
with %Actor{type: :Group, suspended: suspended} = actor <-
|
||||
Actors.get_actor_with_preload(id, true),
|
||||
true <- suspended == false or is_moderator(role) do
|
||||
case Actors.get_actor_with_preload(id, true) do
|
||||
%Actor{type: :Group, suspended: suspended} = actor ->
|
||||
if suspended == false or is_moderator(role) do
|
||||
{:ok, actor}
|
||||
else
|
||||
_ ->
|
||||
{:error, dgettext("errors", "Group with ID %{id} not found", id: id)}
|
||||
end
|
||||
|
||||
nil ->
|
||||
{:error, dgettext("errors", "Group with ID %{id} not found", id: id)}
|
||||
end
|
||||
end
|
||||
|
@ -72,6 +79,8 @@ defmodule Mobilizon.GraphQL.Resolvers.Group do
|
|||
@doc """
|
||||
Lists all groups
|
||||
"""
|
||||
@spec list_groups(any(), map(), Absinthe.Resolution.t()) ::
|
||||
{:ok, Page.t(Actor.t())} | {:error, String.t()}
|
||||
def list_groups(
|
||||
_parent,
|
||||
%{
|
||||
|
@ -96,6 +105,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Group do
|
|||
do: {:error, dgettext("errors", "You may not list groups unless moderator.")}
|
||||
|
||||
# TODO Move me to somewhere cleaner
|
||||
@spec save_attached_pictures(map()) :: map()
|
||||
defp save_attached_pictures(args) do
|
||||
Enum.reduce([:avatar, :banner], args, fn key, args ->
|
||||
if is_map(args) && Map.has_key?(args, key) && !is_nil(args[key][:media]) do
|
||||
|
@ -114,17 +124,18 @@ defmodule Mobilizon.GraphQL.Resolvers.Group do
|
|||
@doc """
|
||||
Create a new group. The creator is automatically added as admin
|
||||
"""
|
||||
@spec create_group(any(), map(), Absinthe.Resolution.t()) ::
|
||||
{:ok, Actor.t()} | {:error, String.t()}
|
||||
def create_group(
|
||||
_parent,
|
||||
args,
|
||||
%{
|
||||
context: %{
|
||||
current_user: user
|
||||
current_actor: %Actor{id: creator_actor_id} = creator_actor
|
||||
}
|
||||
}
|
||||
) do
|
||||
with %Actor{id: creator_actor_id} = creator_actor <- Users.get_actor_for_user(user),
|
||||
args when is_map(args) <- Map.update(args, :preferred_username, "", &String.downcase/1),
|
||||
with args when is_map(args) <- Map.update(args, :preferred_username, "", &String.downcase/1),
|
||||
args when is_map(args) <- Map.put(args, :creator_actor, creator_actor),
|
||||
args when is_map(args) <- Map.put(args, :creator_actor_id, creator_actor_id),
|
||||
{:picture, args} when is_map(args) <- {:picture, save_attached_pictures(args)},
|
||||
|
@ -147,31 +158,34 @@ defmodule Mobilizon.GraphQL.Resolvers.Group do
|
|||
@doc """
|
||||
Update a group. The creator is automatically added as admin
|
||||
"""
|
||||
@spec update_group(any(), map(), Absinthe.Resolution.t()) ::
|
||||
{:ok, Actor.t()} | {:error, String.t()}
|
||||
def update_group(
|
||||
_parent,
|
||||
%{id: group_id} = args,
|
||||
%{
|
||||
context: %{
|
||||
current_user: %User{} = user
|
||||
current_actor: %Actor{} = updater_actor
|
||||
}
|
||||
}
|
||||
) do
|
||||
with %Actor{} = updater_actor <- Users.get_actor_for_user(user),
|
||||
{:administrator, true} <-
|
||||
{:administrator, Actors.is_administrator?(updater_actor.id, group_id)},
|
||||
args when is_map(args) <- Map.put(args, :updater_actor, updater_actor),
|
||||
{:picture, args} when is_map(args) <- {:picture, save_attached_pictures(args)},
|
||||
{:ok, _activity, %Actor{type: :Group} = group} <-
|
||||
API.Groups.update_group(args) do
|
||||
{:ok, group}
|
||||
else
|
||||
{:picture, {:error, :file_too_large}} ->
|
||||
if Actors.is_administrator?(updater_actor.id, group_id) do
|
||||
args = Map.put(args, :updater_actor, updater_actor)
|
||||
|
||||
case save_attached_pictures(args) do
|
||||
{:error, :file_too_large} ->
|
||||
{:error, dgettext("errors", "The provided picture is too heavy")}
|
||||
|
||||
{:error, err} when is_binary(err) ->
|
||||
{:error, err}
|
||||
map when is_map(map) ->
|
||||
case API.Groups.update_group(args) do
|
||||
{:ok, _activity, %Actor{type: :Group} = group} ->
|
||||
{:ok, group}
|
||||
|
||||
{:administrator, false} ->
|
||||
{:error, _err} ->
|
||||
{:error, dgettext("errors", "Failed to update the group")}
|
||||
end
|
||||
end
|
||||
else
|
||||
{:error, dgettext("errors", "Profile is not administrator for the group")}
|
||||
end
|
||||
end
|
||||
|
@ -183,20 +197,21 @@ defmodule Mobilizon.GraphQL.Resolvers.Group do
|
|||
@doc """
|
||||
Delete an existing group
|
||||
"""
|
||||
@spec delete_group(any(), map(), Absinthe.Resolution.t()) ::
|
||||
{:ok, %{id: integer()}} | {:error, String.t()}
|
||||
def delete_group(
|
||||
_parent,
|
||||
%{group_id: group_id},
|
||||
%{
|
||||
context: %{
|
||||
current_user: user
|
||||
current_actor: %Actor{id: actor_id} = actor
|
||||
}
|
||||
}
|
||||
) do
|
||||
with %Actor{id: actor_id} = actor <- Users.get_actor_for_user(user),
|
||||
{:ok, %Actor{} = group} <- Actors.get_group_by_actor_id(group_id),
|
||||
with {:ok, %Actor{} = group} <- Actors.get_group_by_actor_id(group_id),
|
||||
{:ok, %Member{} = member} <- Actors.get_member(actor_id, group.id),
|
||||
{:is_admin, true} <- {:is_admin, Member.is_administrator(member)},
|
||||
{:ok, _activity, group} <- ActivityPub.delete(group, actor, true) do
|
||||
{:ok, _activity, group} <- Actions.Delete.delete(group, actor, true) do
|
||||
{:ok, %{id: group.id}}
|
||||
else
|
||||
{:error, :group_not_found} ->
|
||||
|
@ -218,16 +233,17 @@ defmodule Mobilizon.GraphQL.Resolvers.Group do
|
|||
@doc """
|
||||
Join an existing group
|
||||
"""
|
||||
@spec join_group(any(), map(), Absinthe.Resolution.t()) ::
|
||||
{:ok, Member.t()} | {:error, String.t()}
|
||||
def join_group(_parent, %{group_id: group_id} = args, %{
|
||||
context: %{current_user: %User{} = user}
|
||||
context: %{current_actor: %Actor{} = actor}
|
||||
}) do
|
||||
with %Actor{} = actor <- Users.get_actor_for_user(user),
|
||||
{:ok, %Actor{type: :Group} = group} <-
|
||||
with {:ok, %Actor{type: :Group} = group} <-
|
||||
Actors.get_group_by_actor_id(group_id),
|
||||
{:error, :member_not_found} <- Actors.get_member(actor.id, group.id),
|
||||
{:is_able_to_join, true} <- {:is_able_to_join, Member.can_be_joined(group)},
|
||||
{:ok, _activity, %Member{} = member} <-
|
||||
ActivityPub.join(group, actor, true, args) do
|
||||
Actions.Join.join(group, actor, true, args) do
|
||||
{:ok, member}
|
||||
else
|
||||
{:error, :group_not_found} ->
|
||||
|
@ -248,18 +264,20 @@ defmodule Mobilizon.GraphQL.Resolvers.Group do
|
|||
@doc """
|
||||
Leave a existing group
|
||||
"""
|
||||
@spec leave_group(any(), map(), Absinthe.Resolution.t()) ::
|
||||
{:ok, Member.t()} | {:error, String.t()}
|
||||
def leave_group(
|
||||
_parent,
|
||||
%{group_id: group_id},
|
||||
%{
|
||||
context: %{
|
||||
current_user: %User{} = user
|
||||
current_actor: %Actor{} = actor
|
||||
}
|
||||
}
|
||||
) do
|
||||
with {:actor, %Actor{} = actor} <- {:actor, Users.get_actor_for_user(user)},
|
||||
{:group, %Actor{type: :Group} = group} <- {:group, Actors.get_actor(group_id)},
|
||||
{:ok, _activity, %Member{} = member} <- ActivityPub.leave(group, actor, true) do
|
||||
with {:group, %Actor{type: :Group} = group} <- {:group, Actors.get_actor(group_id)},
|
||||
{:ok, _activity, %Member{} = member} <-
|
||||
Actions.Leave.leave(group, actor, true) do
|
||||
{:ok, member}
|
||||
else
|
||||
{:error, :member_not_found} ->
|
||||
|
@ -268,7 +286,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Group do
|
|||
{:group, nil} ->
|
||||
{:error, dgettext("errors", "Group not found")}
|
||||
|
||||
{:is_not_only_admin, false} ->
|
||||
{:error, :is_not_only_admin} ->
|
||||
{:error,
|
||||
dgettext("errors", "You can't leave this group because you are the only administrator")}
|
||||
end
|
||||
|
@ -278,6 +296,8 @@ defmodule Mobilizon.GraphQL.Resolvers.Group do
|
|||
{:error, dgettext("errors", "You need to be logged-in to leave a group")}
|
||||
end
|
||||
|
||||
@spec find_events_for_group(Actor.t(), map(), Absinthe.Resolution.t()) ::
|
||||
{:ok, Page.t(Event.t())}
|
||||
def find_events_for_group(
|
||||
%Actor{id: group_id} = group,
|
||||
%{
|
||||
|
@ -286,13 +306,12 @@ defmodule Mobilizon.GraphQL.Resolvers.Group do
|
|||
} = args,
|
||||
%{
|
||||
context: %{
|
||||
current_user: %User{role: user_role} = user
|
||||
current_user: %User{role: user_role},
|
||||
current_actor: %Actor{id: actor_id}
|
||||
}
|
||||
}
|
||||
) do
|
||||
with {:actor, %Actor{id: actor_id} = _actor} <- {:actor, Users.get_actor_for_user(user)},
|
||||
{:member, true} <-
|
||||
{:member, Actors.is_member?(actor_id, group_id) or is_moderator(user_role)} do
|
||||
if Actors.is_member?(actor_id, group_id) or is_moderator(user_role) do
|
||||
# TODO : Handle public / restricted to group members events
|
||||
{:ok,
|
||||
Events.list_organized_events_for_group(
|
||||
|
@ -304,7 +323,6 @@ defmodule Mobilizon.GraphQL.Resolvers.Group do
|
|||
limit
|
||||
)}
|
||||
else
|
||||
{:member, false} ->
|
||||
find_events_for_group(group, args, nil)
|
||||
end
|
||||
end
|
||||
|
@ -328,16 +346,15 @@ defmodule Mobilizon.GraphQL.Resolvers.Group do
|
|||
)}
|
||||
end
|
||||
|
||||
@spec restrict_fields_for_non_member_request(Actor.t()) :: Actor.t()
|
||||
defp restrict_fields_for_non_member_request(%Actor{} = group) do
|
||||
Map.merge(
|
||||
group,
|
||||
%{
|
||||
followers: [],
|
||||
%Actor{
|
||||
group
|
||||
| followers: [],
|
||||
followings: [],
|
||||
organized_events: [],
|
||||
comments: [],
|
||||
feed_tokens: []
|
||||
}
|
||||
)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -4,7 +4,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Media do
|
|||
"""
|
||||
|
||||
alias Mobilizon.Actors.Actor
|
||||
alias Mobilizon.{Medias, Users}
|
||||
alias Mobilizon.Medias
|
||||
alias Mobilizon.Medias.Media
|
||||
alias Mobilizon.Users.User
|
||||
import Mobilizon.Web.Gettext
|
||||
|
@ -44,10 +44,9 @@ defmodule Mobilizon.GraphQL.Resolvers.Media do
|
|||
def upload_media(
|
||||
_parent,
|
||||
%{file: %Plug.Upload{} = file} = args,
|
||||
%{context: %{current_user: %User{} = user}}
|
||||
%{context: %{current_actor: %Actor{id: actor_id}}}
|
||||
) do
|
||||
with %Actor{id: actor_id} <- Users.get_actor_for_user(user),
|
||||
{:ok,
|
||||
with {:ok,
|
||||
%{
|
||||
name: _name,
|
||||
url: url,
|
||||
|
@ -94,6 +93,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Media do
|
|||
else
|
||||
{:media, nil} -> {:error, :not_found}
|
||||
{:is_owned, _} -> {:error, :unauthorized}
|
||||
{:error, :enofile} -> {:error, "File not found"}
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -4,9 +4,9 @@ defmodule Mobilizon.GraphQL.Resolvers.Member do
|
|||
"""
|
||||
|
||||
import Mobilizon.Users.Guards
|
||||
alias Mobilizon.{Actors, Users}
|
||||
alias Mobilizon.Actors
|
||||
alias Mobilizon.Actors.{Actor, Member}
|
||||
alias Mobilizon.Federation.ActivityPub
|
||||
alias Mobilizon.Federation.ActivityPub.Actions
|
||||
alias Mobilizon.Federation.ActivityPub.Actor, as: ActivityPubActor
|
||||
alias Mobilizon.Storage.Page
|
||||
alias Mobilizon.Users.User
|
||||
|
@ -17,16 +17,16 @@ defmodule Mobilizon.GraphQL.Resolvers.Member do
|
|||
|
||||
If actor requesting is not part of the group, we only return the number of members, not members
|
||||
"""
|
||||
@spec find_members_for_group(Actor.t(), map(), Absinthe.Resolution.t()) ::
|
||||
{:ok, Page.t(Member.t())}
|
||||
def find_members_for_group(
|
||||
%Actor{id: group_id} = group,
|
||||
%{page: page, limit: limit, roles: roles},
|
||||
%{
|
||||
context: %{current_user: %User{role: user_role} = user}
|
||||
context: %{current_user: %User{role: user_role}, current_actor: %Actor{id: actor_id}}
|
||||
} = _resolution
|
||||
) do
|
||||
with %Actor{id: actor_id} <- Users.get_actor_for_user(user),
|
||||
{:member, true} <-
|
||||
{:member, Actors.is_member?(actor_id, group_id) or is_moderator(user_role)} do
|
||||
if Actors.is_member?(actor_id, group_id) or is_moderator(user_role) do
|
||||
roles =
|
||||
case roles do
|
||||
"" ->
|
||||
|
@ -42,27 +42,25 @@ defmodule Mobilizon.GraphQL.Resolvers.Member do
|
|||
%Page{} = page = Actors.list_members_for_group(group, roles, page, limit)
|
||||
{:ok, page}
|
||||
else
|
||||
{:member, false} ->
|
||||
# Actor is not member of group, fallback to public
|
||||
with %Page{} = page <- Actors.list_members_for_group(group) do
|
||||
%Page{} = page = Actors.list_members_for_group(group)
|
||||
{:ok, %Page{page | elements: []}}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def find_members_for_group(%Actor{} = group, _args, _resolution) do
|
||||
with %Page{} = page <- Actors.list_members_for_group(group) do
|
||||
%Page{} = page = Actors.list_members_for_group(group)
|
||||
{:ok, %Page{page | elements: []}}
|
||||
end
|
||||
end
|
||||
|
||||
@spec invite_member(any(), map(), Absinthe.Resolution.t()) ::
|
||||
{:ok, Member.t()} | {:error, String.t()}
|
||||
def invite_member(
|
||||
_parent,
|
||||
%{group_id: group_id, target_actor_username: target_actor_username},
|
||||
%{context: %{current_user: %User{} = user}}
|
||||
%{context: %{current_actor: %Actor{id: actor_id} = actor}}
|
||||
) do
|
||||
with %Actor{id: actor_id} = actor <- Users.get_actor_for_user(user),
|
||||
{:ok, %Actor{} = group} <- Actors.get_group_by_actor_id(group_id),
|
||||
with {:ok, %Actor{} = group} <- Actors.get_group_by_actor_id(group_id),
|
||||
{:has_rights_to_invite, {:ok, %Member{role: role}}}
|
||||
when role in [:moderator, :administrator, :creator] <-
|
||||
{:has_rights_to_invite, Actors.get_member(actor_id, group_id)},
|
||||
|
@ -73,7 +71,8 @@ defmodule Mobilizon.GraphQL.Resolvers.Member do
|
|||
ActivityPubActor.find_or_make_actor_from_nickname(target_actor_username)},
|
||||
{:existant, true} <-
|
||||
{:existant, check_member_not_existant_or_rejected(target_actor_id, group.id)},
|
||||
{:ok, _activity, %Member{} = member} <- ActivityPub.invite(group, actor, target_actor) do
|
||||
{:ok, _activity, %Member{} = member} <-
|
||||
Actions.Invite.invite(group, actor, target_actor) do
|
||||
{:ok, member}
|
||||
else
|
||||
{:error, :group_not_found} ->
|
||||
|
@ -97,13 +96,16 @@ defmodule Mobilizon.GraphQL.Resolvers.Member do
|
|||
end
|
||||
end
|
||||
|
||||
def accept_invitation(_parent, %{id: member_id}, %{context: %{current_user: %User{} = user}}) do
|
||||
with %Actor{id: actor_id} <- Users.get_actor_for_user(user),
|
||||
%Member{actor: %Actor{id: member_actor_id}} = member <-
|
||||
@spec accept_invitation(any(), map(), Absinthe.Resolution.t()) ::
|
||||
{:ok, Member.t()} | {:error, String.t()}
|
||||
def accept_invitation(_parent, %{id: member_id}, %{
|
||||
context: %{current_actor: %Actor{id: actor_id}}
|
||||
}) do
|
||||
with %Member{actor: %Actor{id: member_actor_id}} = member <-
|
||||
Actors.get_member(member_id),
|
||||
{:is_same_actor, true} <- {:is_same_actor, member_actor_id === actor_id},
|
||||
{:is_same_actor, true} <- {:is_same_actor, member_actor_id == actor_id},
|
||||
{:ok, _activity, %Member{} = member} <-
|
||||
ActivityPub.accept(
|
||||
Actions.Accept.accept(
|
||||
:invite,
|
||||
member,
|
||||
true
|
||||
|
@ -115,13 +117,16 @@ defmodule Mobilizon.GraphQL.Resolvers.Member do
|
|||
end
|
||||
end
|
||||
|
||||
def reject_invitation(_parent, %{id: member_id}, %{context: %{current_user: %User{} = user}}) do
|
||||
with %Actor{id: actor_id} <- Users.get_actor_for_user(user),
|
||||
{:invitation_exists, %Member{actor: %Actor{id: member_actor_id}} = member} <-
|
||||
@spec reject_invitation(any(), map(), Absinthe.Resolution.t()) ::
|
||||
{:ok, Member.t()} | {:error, String.t()}
|
||||
def reject_invitation(_parent, %{id: member_id}, %{
|
||||
context: %{current_actor: %Actor{id: actor_id}}
|
||||
}) do
|
||||
with {:invitation_exists, %Member{actor: %Actor{id: member_actor_id}} = member} <-
|
||||
{:invitation_exists, Actors.get_member(member_id)},
|
||||
{:is_same_actor, true} <- {:is_same_actor, member_actor_id === actor_id},
|
||||
{:is_same_actor, true} <- {:is_same_actor, member_actor_id == actor_id},
|
||||
{:ok, _activity, %Member{} = member} <-
|
||||
ActivityPub.reject(
|
||||
Actions.Reject.reject(
|
||||
:invite,
|
||||
member,
|
||||
true
|
||||
|
@ -136,19 +141,20 @@ defmodule Mobilizon.GraphQL.Resolvers.Member do
|
|||
end
|
||||
end
|
||||
|
||||
@spec update_member(any(), map(), Absinthe.Resolution.t()) ::
|
||||
{:ok, Member.t()} | {:error, String.t()}
|
||||
def update_member(_parent, %{member_id: member_id, role: role}, %{
|
||||
context: %{current_user: %User{} = user}
|
||||
context: %{current_actor: %Actor{} = moderator}
|
||||
}) do
|
||||
with %Actor{} = moderator <- Users.get_actor_for_user(user),
|
||||
%Member{} = member <- Actors.get_member(member_id),
|
||||
with %Member{} = member <- Actors.get_member(member_id),
|
||||
{:ok, _activity, %Member{} = member} <-
|
||||
ActivityPub.update(member, %{role: role}, true, %{moderator: moderator}) do
|
||||
Actions.Update.update(member, %{role: role}, true, %{moderator: moderator}) do
|
||||
{:ok, member}
|
||||
else
|
||||
{:has_rights_to_update_role, {:error, :member_not_found}} ->
|
||||
{:error, :member_not_found} ->
|
||||
{:error, dgettext("errors", "You are not a moderator or admin for this group")}
|
||||
|
||||
{:is_only_admin, true} ->
|
||||
{:error, :only_admin_left} ->
|
||||
{:error,
|
||||
dgettext(
|
||||
"errors",
|
||||
|
@ -160,16 +166,18 @@ defmodule Mobilizon.GraphQL.Resolvers.Member do
|
|||
def update_member(_parent, _args, _resolution),
|
||||
do: {:error, "You must be logged-in to update a member"}
|
||||
|
||||
@spec remove_member(any(), map(), Absinthe.Resolution.t()) ::
|
||||
{:ok, Member.t()} | {:error, String.t()}
|
||||
def remove_member(_parent, %{member_id: member_id, group_id: group_id}, %{
|
||||
context: %{current_user: %User{} = user}
|
||||
context: %{current_actor: %Actor{id: moderator_id} = moderator}
|
||||
}) do
|
||||
with %Actor{id: moderator_id} = moderator <- Users.get_actor_for_user(user),
|
||||
%Member{role: role} = member when role != :rejected <- Actors.get_member(member_id),
|
||||
with %Member{role: role} = member when role != :rejected <- Actors.get_member(member_id),
|
||||
%Actor{type: :Group} = group <- Actors.get_actor(group_id),
|
||||
{:has_rights_to_remove, {:ok, %Member{role: role}}}
|
||||
when role in [:moderator, :administrator, :creator] <-
|
||||
{:has_rights_to_remove, Actors.get_member(moderator_id, group_id)},
|
||||
{:ok, _activity, %Member{}} <- ActivityPub.remove(member, group, moderator, true) do
|
||||
{:ok, _activity, %Member{}} <-
|
||||
Actions.Remove.remove(member, group, moderator, true) do
|
||||
{:ok, member}
|
||||
else
|
||||
%Member{role: :rejected} ->
|
||||
|
|
|
@ -2,7 +2,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Participant do
|
|||
@moduledoc """
|
||||
Handles the participation-related GraphQL calls.
|
||||
"""
|
||||
alias Mobilizon.{Actors, Config, Crypto, Events, Users}
|
||||
alias Mobilizon.{Actors, Config, Crypto, Events}
|
||||
alias Mobilizon.Actors.Actor
|
||||
alias Mobilizon.Events.{Event, Participant}
|
||||
alias Mobilizon.GraphQL.API.Participations
|
||||
|
@ -16,6 +16,8 @@ defmodule Mobilizon.GraphQL.Resolvers.Participant do
|
|||
@doc """
|
||||
Join an event for an regular or anonymous actor
|
||||
"""
|
||||
@spec actor_join_event(any(), map(), Absinthe.Resolution.t()) ::
|
||||
{:ok, Participant.t()} | {:error, String.t()}
|
||||
def actor_join_event(
|
||||
_parent,
|
||||
%{actor_id: actor_id, event_id: event_id} = args,
|
||||
|
@ -117,7 +119,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Participant do
|
|||
|> Map.put(:actor, actor) do
|
||||
{:ok, participant}
|
||||
else
|
||||
{:maximum_attendee_capacity, _} ->
|
||||
{:error, :maximum_attendee_capacity_reached} ->
|
||||
{:error, dgettext("errors", "The event has already reached its maximum capacity")}
|
||||
|
||||
{:has_event, _} ->
|
||||
|
@ -127,39 +129,53 @@ defmodule Mobilizon.GraphQL.Resolvers.Participant do
|
|||
{:error, :event_not_found} ->
|
||||
{:error, dgettext("errors", "Event id not found")}
|
||||
|
||||
{:ok, %Participant{}} ->
|
||||
{:error, :already_participant} ->
|
||||
{:error, dgettext("errors", "You are already a participant of this event")}
|
||||
end
|
||||
end
|
||||
|
||||
@spec check_anonymous_participation(String.t(), String.t()) ::
|
||||
{:ok, Event.t()} | {:error, String.t()}
|
||||
defp check_anonymous_participation(actor_id, event_id) do
|
||||
cond do
|
||||
Config.anonymous_participation?() == false ->
|
||||
{:error, dgettext("errors", "Anonymous participation is not enabled")}
|
||||
|
||||
to_string(Config.anonymous_actor_id()) != actor_id ->
|
||||
{:error, dgettext("errors", "The anonymous actor ID is invalid")}
|
||||
|
||||
true ->
|
||||
case Mobilizon.Events.get_event_with_preload(event_id) do
|
||||
{:ok, %Event{} = event} ->
|
||||
{:ok, event}
|
||||
|
||||
{:error, :event_not_found} ->
|
||||
{:error,
|
||||
dgettext("errors", "Event with this ID %{id} doesn't exist", id: inspect(event_id))}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Leave an event for an anonymous actor
|
||||
"""
|
||||
@spec actor_leave_event(any(), map(), Absinthe.Resolution.t()) ::
|
||||
{:ok, map()} | {:error, String.t()}
|
||||
def actor_leave_event(
|
||||
_parent,
|
||||
%{actor_id: actor_id, event_id: event_id, token: token},
|
||||
_resolution
|
||||
)
|
||||
when not is_nil(token) do
|
||||
with {:anonymous_participation_enabled, true} <-
|
||||
{:anonymous_participation_enabled, Config.anonymous_participation?()},
|
||||
{:anonymous_actor_id, true} <-
|
||||
{:anonymous_actor_id, to_string(Config.anonymous_actor_id()) == actor_id},
|
||||
{:has_event, {:ok, %Event{} = event}} <-
|
||||
{:has_event, Mobilizon.Events.get_event_with_preload(event_id)},
|
||||
%Actor{} = actor <- Actors.get_actor_with_preload(actor_id),
|
||||
{:ok, _activity, %Participant{id: participant_id} = _participant} <-
|
||||
Participations.leave(event, actor, %{local: false, cancellation_token: token}) do
|
||||
case check_anonymous_participation(actor_id, event_id) do
|
||||
{:ok, %Event{} = event} ->
|
||||
%Actor{} = actor = Actors.get_actor_with_preload!(actor_id)
|
||||
|
||||
case Participations.leave(event, actor, %{local: false, cancellation_token: token}) do
|
||||
{:ok, _activity, %Participant{id: participant_id} = _participant} ->
|
||||
{:ok, %{event: %{id: event_id}, actor: %{id: actor_id}, id: participant_id}}
|
||||
else
|
||||
{:has_event, _} ->
|
||||
{:error,
|
||||
dgettext("errors", "Event with this ID %{id} doesn't exist", id: inspect(event_id))}
|
||||
|
||||
{:is_owned, nil} ->
|
||||
{:error, dgettext("errors", "Profile is not owned by authenticated user")}
|
||||
|
||||
{:only_organizer, true} ->
|
||||
{:error, :is_only_organizer} ->
|
||||
{:error,
|
||||
dgettext(
|
||||
"errors",
|
||||
|
@ -168,6 +184,10 @@ defmodule Mobilizon.GraphQL.Resolvers.Participant do
|
|||
|
||||
{:error, :participant_not_found} ->
|
||||
{:error, dgettext("errors", "Participant not found")}
|
||||
|
||||
{:error, _err} ->
|
||||
{:error, dgettext("errors", "Failed to leave the event")}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -188,7 +208,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Participant do
|
|||
{:is_owned, nil} ->
|
||||
{:error, dgettext("errors", "Profile is not owned by authenticated user")}
|
||||
|
||||
{:only_organizer, true} ->
|
||||
{:error, :is_only_organizer} ->
|
||||
{:error,
|
||||
dgettext(
|
||||
"errors",
|
||||
|
@ -204,19 +224,19 @@ defmodule Mobilizon.GraphQL.Resolvers.Participant do
|
|||
{:error, dgettext("errors", "You need to be logged-in to leave an event")}
|
||||
end
|
||||
|
||||
@spec update_participation(any(), map(), Absinthe.Resolution.t()) ::
|
||||
{:ok, Participation.t()} | {:error, String.t()}
|
||||
def update_participation(
|
||||
_parent,
|
||||
%{id: participation_id, role: new_role},
|
||||
%{
|
||||
context: %{
|
||||
current_user: user
|
||||
current_actor: %Actor{} = moderator_actor
|
||||
}
|
||||
}
|
||||
) do
|
||||
# Check that moderator provided is rightly authenticated
|
||||
with %Actor{} = moderator_actor <- Users.get_actor_for_user(user),
|
||||
# Check that participation already exists
|
||||
{:has_participation, %Participant{role: old_role, event_id: event_id} = participation} <-
|
||||
with {:has_participation, %Participant{role: old_role, event_id: event_id} = participation} <-
|
||||
{:has_participation, Events.get_participant(participation_id)},
|
||||
{:same_role, false} <- {:same_role, new_role == old_role},
|
||||
# Check that moderator has right
|
||||
|
|
|
@ -12,7 +12,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Person do
|
|||
alias Mobilizon.Users.User
|
||||
import Mobilizon.Web.Gettext
|
||||
|
||||
alias Mobilizon.Federation.ActivityPub
|
||||
alias Mobilizon.Federation.ActivityPub.Actions
|
||||
alias Mobilizon.Federation.ActivityPub.Actor, as: ActivityPubActor
|
||||
require Logger
|
||||
|
||||
|
@ -21,6 +21,8 @@ defmodule Mobilizon.GraphQL.Resolvers.Person do
|
|||
@doc """
|
||||
Get a person
|
||||
"""
|
||||
@spec get_person(any(), map(), Absinthe.Resolution.t()) ::
|
||||
{:ok, Actor.t()} | {:error, String.t() | :unauthorized}
|
||||
def get_person(_parent, %{id: id}, %{context: %{current_user: %User{role: role}}}) do
|
||||
with %Actor{suspended: suspended} = actor <- Actors.get_actor_with_preload(id, true),
|
||||
true <- suspended == false or is_moderator(role) do
|
||||
|
@ -36,6 +38,8 @@ defmodule Mobilizon.GraphQL.Resolvers.Person do
|
|||
@doc """
|
||||
Find a person
|
||||
"""
|
||||
@spec fetch_person(any(), map(), Absinthe.Resolution.t()) ::
|
||||
{:ok, Actor.t()} | {:error, String.t() | :unauthorized | :unauthenticated}
|
||||
def fetch_person(_parent, %{preferred_username: preferred_username}, %{
|
||||
context: %{current_user: %User{} = user}
|
||||
}) do
|
||||
|
@ -57,6 +61,8 @@ defmodule Mobilizon.GraphQL.Resolvers.Person do
|
|||
|
||||
def fetch_person(_parent, _args, _resolution), do: {:error, :unauthenticated}
|
||||
|
||||
@spec list_persons(any(), map(), Absinthe.Resolution.t()) ::
|
||||
{:ok, Page.t(Actor.t())} | {:error, :unauthorized | :unauthenticated}
|
||||
def list_persons(
|
||||
_parent,
|
||||
%{
|
||||
|
@ -91,8 +97,14 @@ defmodule Mobilizon.GraphQL.Resolvers.Person do
|
|||
@doc """
|
||||
Returns the current actor for the currently logged-in user
|
||||
"""
|
||||
def get_current_person(_parent, _args, %{context: %{current_user: user}}) do
|
||||
{:ok, Users.get_actor_for_user(user)}
|
||||
@spec get_current_person(any, any, Absinthe.Resolution.t()) ::
|
||||
{:error, :unauthenticated | :no_current_person} | {:ok, Actor.t()}
|
||||
def get_current_person(_parent, _args, %{context: %{current_actor: %Actor{} = actor}}) do
|
||||
{:ok, actor}
|
||||
end
|
||||
|
||||
def get_current_person(_parent, _args, %{context: %{current_user: %User{}}}) do
|
||||
{:error, :no_current_person}
|
||||
end
|
||||
|
||||
def get_current_person(_parent, _args, _resolution) do
|
||||
|
@ -102,6 +114,8 @@ defmodule Mobilizon.GraphQL.Resolvers.Person do
|
|||
@doc """
|
||||
Returns the list of identities for the logged-in user
|
||||
"""
|
||||
@spec identities(any, any, Absinthe.Resolution.t()) ::
|
||||
{:error, :unauthenticated} | {:ok, list(Actor.t())}
|
||||
def identities(_parent, _args, %{context: %{current_user: user}}) do
|
||||
{:ok, Users.get_actors_for_user(user)}
|
||||
end
|
||||
|
@ -113,6 +127,8 @@ defmodule Mobilizon.GraphQL.Resolvers.Person do
|
|||
@doc """
|
||||
This function is used to create more identities from an existing user
|
||||
"""
|
||||
@spec create_person(any(), map(), Absinthe.Resolution.t()) ::
|
||||
{:ok, Actor.t()} | {:error, String.t() | :unauthenticated}
|
||||
def create_person(
|
||||
_parent,
|
||||
%{preferred_username: _preferred_username} = args,
|
||||
|
@ -140,6 +156,8 @@ defmodule Mobilizon.GraphQL.Resolvers.Person do
|
|||
@doc """
|
||||
This function is used to update an existing identity
|
||||
"""
|
||||
@spec update_person(any(), map(), Absinthe.Resolution.t()) ::
|
||||
{:ok, Actor.t()} | {:error, String.t() | :unauthenticated}
|
||||
def update_person(
|
||||
_parent,
|
||||
%{id: id} = args,
|
||||
|
@ -148,21 +166,24 @@ defmodule Mobilizon.GraphQL.Resolvers.Person do
|
|||
require Logger
|
||||
args = Map.put(args, :user_id, user.id)
|
||||
|
||||
with {:find_actor, %Actor{} = actor} <-
|
||||
{:find_actor, Actors.get_actor(id)},
|
||||
{:is_owned, %Actor{}} <- User.owns_actor(user, actor.id),
|
||||
{:picture, args} when is_map(args) <- {:picture, save_attached_pictures(args)},
|
||||
{:ok, _activity, %Actor{} = actor} <- ActivityPub.update(actor, args, true) do
|
||||
case owned_actor(user, id) do
|
||||
{:ok, %Actor{} = actor} ->
|
||||
case save_attached_pictures(args) do
|
||||
args when is_map(args) ->
|
||||
case Actions.Update.update(actor, args, true) do
|
||||
{:ok, _activity, %Actor{} = actor} ->
|
||||
{:ok, actor}
|
||||
else
|
||||
{:picture, {:error, :file_too_large}} ->
|
||||
|
||||
{:error, err} ->
|
||||
{:error, err}
|
||||
end
|
||||
|
||||
{:error, :file_too_large} ->
|
||||
{:error, dgettext("errors", "The provided picture is too heavy")}
|
||||
end
|
||||
|
||||
{:find_actor, nil} ->
|
||||
{:error, dgettext("errors", "Profile not found")}
|
||||
|
||||
{:is_owned, nil} ->
|
||||
{:error, dgettext("errors", "Profile is not owned by authenticated user")}
|
||||
{:error, err} ->
|
||||
{:error, err}
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -173,30 +194,27 @@ defmodule Mobilizon.GraphQL.Resolvers.Person do
|
|||
@doc """
|
||||
This function is used to delete an existing identity
|
||||
"""
|
||||
@spec delete_person(any(), map(), Absinthe.Resolution.t()) ::
|
||||
{:ok, Actor.t()} | {:error, String.t() | :unauthenticated}
|
||||
def delete_person(
|
||||
_parent,
|
||||
%{id: id} = _args,
|
||||
%{context: %{current_user: user}} = _resolution
|
||||
%{context: %{current_user: %User{} = user}} = _resolution
|
||||
) do
|
||||
with {:find_actor, %Actor{} = actor} <-
|
||||
{:find_actor, Actors.get_actor(id)},
|
||||
{:is_owned, %Actor{}} <- User.owns_actor(user, actor.id),
|
||||
{:last_identity, false} <- {:last_identity, last_identity?(user)},
|
||||
{:last_admin, false} <- {:last_admin, last_admin_of_a_group?(actor.id)},
|
||||
{:ok, actor} <- Actors.delete_actor(actor) do
|
||||
{:ok, actor}
|
||||
else
|
||||
{:find_actor, nil} ->
|
||||
{:error, dgettext("errors", "Profile not found")}
|
||||
|
||||
{:last_identity, true} ->
|
||||
case owned_actor(user, id) do
|
||||
{:ok, %Actor{} = actor} ->
|
||||
if last_identity?(user) do
|
||||
{:error, dgettext("errors", "Cannot remove the last identity of a user")}
|
||||
|
||||
{:last_admin, true} ->
|
||||
else
|
||||
if last_admin_of_a_group?(actor.id) do
|
||||
{:error, dgettext("errors", "Cannot remove the last administrator of a group")}
|
||||
else
|
||||
Actors.delete_actor(actor)
|
||||
end
|
||||
end
|
||||
|
||||
{:is_owned, nil} ->
|
||||
{:error, dgettext("errors", "Profile is not owned by authenticated user")}
|
||||
{:error, err} ->
|
||||
{:error, err}
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -204,20 +222,51 @@ defmodule Mobilizon.GraphQL.Resolvers.Person do
|
|||
{:error, :unauthenticated}
|
||||
end
|
||||
|
||||
@spec owned_actor(User.t(), integer() | String.t()) :: {:error, String.t()} | {:ok, Actor.t()}
|
||||
defp owned_actor(%User{} = user, actor_id) do
|
||||
with {:find_actor, %Actor{} = actor} <-
|
||||
{:find_actor, Actors.get_actor(actor_id)},
|
||||
{:is_owned, %Actor{}} <- User.owns_actor(user, actor.id) do
|
||||
{:ok, actor}
|
||||
else
|
||||
{:find_actor, nil} ->
|
||||
{:error, dgettext("errors", "Profile not found")}
|
||||
|
||||
{:is_owned, nil} ->
|
||||
{:error, dgettext("errors", "Profile is not owned by authenticated user")}
|
||||
end
|
||||
end
|
||||
|
||||
@spec last_identity?(User.t()) :: boolean
|
||||
defp last_identity?(user) do
|
||||
length(Users.get_actors_for_user(user)) <= 1
|
||||
end
|
||||
|
||||
@spec save_attached_pictures(map()) :: map() | {:error, any()}
|
||||
defp save_attached_pictures(args) do
|
||||
with args when is_map(args) <- save_attached_picture(args, :avatar),
|
||||
args when is_map(args) <- save_attached_picture(args, :banner) do
|
||||
case save_attached_picture(args, :avatar) do
|
||||
{:error, err} ->
|
||||
{:error, err}
|
||||
|
||||
args when is_map(args) ->
|
||||
case save_attached_picture(args, :banner) do
|
||||
{:error, err} ->
|
||||
{:error, err}
|
||||
|
||||
args when is_map(args) ->
|
||||
args
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@spec save_attached_picture(map(), :avatar | :banner) :: map() | {:error, any}
|
||||
defp save_attached_picture(args, key) do
|
||||
if Map.has_key?(args, key) && !is_nil(args[key][:media]) do
|
||||
with media when is_map(media) <- save_picture(args[key][:media], key) do
|
||||
case save_picture(args[key][:media], key) do
|
||||
{:error, err} ->
|
||||
{:error, err}
|
||||
|
||||
media when is_map(media) ->
|
||||
Map.put(args, key, media)
|
||||
end
|
||||
else
|
||||
|
@ -225,73 +274,82 @@ defmodule Mobilizon.GraphQL.Resolvers.Person do
|
|||
end
|
||||
end
|
||||
|
||||
@spec save_picture(map(), :avatar | :banner) :: {:ok, map()} | {:error, any()}
|
||||
defp save_picture(media, key) do
|
||||
with {:ok, %{name: name, url: url, content_type: content_type, size: size}} <-
|
||||
Upload.store(media.file, type: key, description: media.alt) do
|
||||
case Upload.store(media.file, type: key, description: media.alt) do
|
||||
{:ok, %{name: name, url: url, content_type: content_type, size: size}} ->
|
||||
%{"name" => name, "url" => url, "content_type" => content_type, "size" => size}
|
||||
|
||||
{:error, err} ->
|
||||
{:error, err}
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
This function is used to register a person afterwards the user has been created (but not activated)
|
||||
"""
|
||||
@spec register_person(any(), map(), Absinthe.Resolution.t()) ::
|
||||
{:ok, Actor.t()} | {:error, String.t()}
|
||||
def register_person(_parent, args, _resolution) do
|
||||
# When registering, email is assumed confirmed (unlike changing email)
|
||||
with {:ok, %User{} = user} <- Users.get_user_by_email(args.email, unconfirmed: false),
|
||||
user_actor <- Users.get_actor_for_user(user),
|
||||
no_actor <- is_nil(user_actor),
|
||||
{:no_actor, true} <- {:no_actor, no_actor},
|
||||
args <- Map.update(args, :preferred_username, "", &String.downcase/1),
|
||||
args <- Map.put(args, :user_id, user.id),
|
||||
{:picture, args} when is_map(args) <- {:picture, save_attached_pictures(args)},
|
||||
{:ok, %Actor{} = new_person} <- Actors.new_person(args, true) do
|
||||
{:ok, new_person}
|
||||
else
|
||||
{:picture, {:error, :file_too_large}} ->
|
||||
case Users.get_user_by_email(args.email, unconfirmed: false) do
|
||||
{:ok, %User{} = user} ->
|
||||
if is_nil(Users.get_actor_for_user(user)) do
|
||||
# No profile yet, we can create one
|
||||
case prepare_args(args, user) do
|
||||
args when is_map(args) ->
|
||||
Actors.new_person(args, true)
|
||||
|
||||
{:error, :file_too_large} ->
|
||||
{:error, dgettext("errors", "The provided picture is too heavy")}
|
||||
|
||||
{:error, _err} ->
|
||||
{:error, dgettext("errors", "Error while uploading pictures")}
|
||||
end
|
||||
else
|
||||
{:error, dgettext("errors", "You already have a profile for this user")}
|
||||
end
|
||||
|
||||
{:error, :user_not_found} ->
|
||||
{:error, dgettext("errors", "No user with this email was found")}
|
||||
|
||||
{:no_actor, _} ->
|
||||
{:error, dgettext("errors", "You already have a profile for this user")}
|
||||
|
||||
{:error, %Ecto.Changeset{} = e} ->
|
||||
{:error, e}
|
||||
end
|
||||
end
|
||||
|
||||
@spec prepare_args(map(), User.t()) :: map() | {:error, any()}
|
||||
defp prepare_args(args, %User{} = user) do
|
||||
args
|
||||
|> Map.update(:preferred_username, "", &String.downcase/1)
|
||||
|> Map.put(:user_id, user.id)
|
||||
|> save_attached_pictures()
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns the participations, optionally restricted to an event
|
||||
"""
|
||||
@spec person_participations(Actor.t(), map(), Absinthe.Resolution.t()) ::
|
||||
{:ok, Page.t(Participant.t())} | {:error, :unauthorized | String.t()}
|
||||
def person_participations(
|
||||
%Actor{id: actor_id} = person,
|
||||
%{event_id: event_id},
|
||||
%{context: %{current_user: %User{} = user}}
|
||||
) do
|
||||
with {:can_get_participations, true} <-
|
||||
{:can_get_participations, user_can_access_person_details?(person, user)},
|
||||
{:no_participant, {:ok, %Participant{} = participant}} <-
|
||||
{:no_participant, Events.get_participant(event_id, actor_id)} do
|
||||
{:ok, %Page{elements: [participant], total: 1}}
|
||||
if user_can_access_person_details?(person, user) do
|
||||
case Events.get_participant(event_id, actor_id) do
|
||||
{:ok, %Participant{} = participant} -> {:ok, %Page{elements: [participant], total: 1}}
|
||||
{:error, :participant_not_found} -> {:ok, %Page{elements: [], total: 0}}
|
||||
end
|
||||
else
|
||||
{:is_owned, nil} ->
|
||||
{:error, dgettext("errors", "Profile is not owned by authenticated user")}
|
||||
|
||||
{:no_participant, _} ->
|
||||
{:ok, %Page{elements: [], total: 0}}
|
||||
{:error, :unauthorized}
|
||||
end
|
||||
end
|
||||
|
||||
def person_participations(%Actor{} = person, %{page: page, limit: limit}, %{
|
||||
context: %{current_user: %User{} = user}
|
||||
}) do
|
||||
with {:can_get_participations, true} <-
|
||||
{:can_get_participations, user_can_access_person_details?(person, user)},
|
||||
%Page{} = page <- Events.list_event_participations_for_actor(person, page, limit) do
|
||||
if user_can_access_person_details?(person, user) do
|
||||
%Page{} = page = Events.list_event_participations_for_actor(person, page, limit)
|
||||
{:ok, page}
|
||||
else
|
||||
{:can_get_participations, false} ->
|
||||
{:error, dgettext("errors", "Profile is not owned by authenticated user")}
|
||||
end
|
||||
end
|
||||
|
@ -303,23 +361,22 @@ defmodule Mobilizon.GraphQL.Resolvers.Person do
|
|||
def person_memberships(%Actor{id: actor_id} = person, %{group: group}, %{
|
||||
context: %{current_user: %User{} = user}
|
||||
}) do
|
||||
with {:can_get_memberships, true} <-
|
||||
{:can_get_memberships, user_can_access_person_details?(person, user)},
|
||||
{:group, %Actor{id: group_id}} <- {:group, Actors.get_actor_by_name(group, :Group)},
|
||||
{:ok, %Member{} = membership} <- Actors.get_member(actor_id, group_id),
|
||||
memberships <- %Page{
|
||||
if user_can_access_person_details?(person, user) do
|
||||
with {:group, %Actor{id: group_id}} <- {:group, Actors.get_actor_by_name(group, :Group)},
|
||||
{:ok, %Member{} = membership} <- Actors.get_member(actor_id, group_id) do
|
||||
{:ok,
|
||||
%Page{
|
||||
total: 1,
|
||||
elements: [Repo.preload(membership, [:actor, :parent, :invited_by])]
|
||||
} do
|
||||
{:ok, memberships}
|
||||
}}
|
||||
else
|
||||
{:error, :member_not_found} ->
|
||||
{:ok, %Page{total: 0, elements: []}}
|
||||
|
||||
{:group, nil} ->
|
||||
{:error, :group_not_found}
|
||||
|
||||
{:can_get_memberships, _} ->
|
||||
end
|
||||
else
|
||||
{:error, dgettext("errors", "Profile is not owned by authenticated user")}
|
||||
end
|
||||
end
|
||||
|
@ -341,6 +398,8 @@ defmodule Mobilizon.GraphQL.Resolvers.Person do
|
|||
end
|
||||
end
|
||||
|
||||
@spec user_for_person(Actor.t(), map(), Absinthe.Resolution.t()) ::
|
||||
{:ok, User.t() | nil} | {:error, String.t() | nil}
|
||||
def user_for_person(%Actor{type: :Person, user_id: user_id}, _args, %{
|
||||
context: %{current_user: %User{role: role}}
|
||||
})
|
||||
|
@ -359,6 +418,8 @@ defmodule Mobilizon.GraphQL.Resolvers.Person do
|
|||
|
||||
def user_for_person(_, _args, _resolution), do: {:error, nil}
|
||||
|
||||
@spec organized_events_for_person(Actor.t(), map(), Absinthe.Resolution.t()) ::
|
||||
{:ok, Page.t(Event.t())} | {:error, :unauthorized}
|
||||
def organized_events_for_person(
|
||||
%Actor{} = person,
|
||||
%{page: page, limit: limit},
|
||||
|
@ -366,12 +427,10 @@ defmodule Mobilizon.GraphQL.Resolvers.Person do
|
|||
context: %{current_user: %User{} = user}
|
||||
}
|
||||
) do
|
||||
with {:can_get_events, true} <-
|
||||
{:can_get_events, user_can_access_person_details?(person, user)},
|
||||
%Page{} = page <- Events.list_organized_events_for_actor(person, page, limit) do
|
||||
if user_can_access_person_details?(person, user) do
|
||||
%Page{} = page = Events.list_organized_events_for_actor(person, page, limit)
|
||||
{:ok, page}
|
||||
else
|
||||
{:can_get_events, false} ->
|
||||
{:error, :unauthorized}
|
||||
end
|
||||
end
|
||||
|
|
|
@ -4,10 +4,9 @@ defmodule Mobilizon.GraphQL.Resolvers.Post do
|
|||
"""
|
||||
|
||||
import Mobilizon.Users.Guards
|
||||
alias Mobilizon.{Actors, Posts, Users}
|
||||
alias Mobilizon.{Actors, Posts}
|
||||
alias Mobilizon.Actors.Actor
|
||||
alias Mobilizon.Federation.ActivityPub
|
||||
alias Mobilizon.Federation.ActivityPub.{Permission, Utils}
|
||||
alias Mobilizon.Federation.ActivityPub.{Actions, Permission, Utils}
|
||||
alias Mobilizon.Posts.Post
|
||||
alias Mobilizon.Storage.Page
|
||||
alias Mobilizon.Users.User
|
||||
|
@ -22,22 +21,21 @@ defmodule Mobilizon.GraphQL.Resolvers.Post do
|
|||
|
||||
Returns only if actor requesting is a member of the group
|
||||
"""
|
||||
@spec find_posts_for_group(Actor.t(), map(), Absinthe.Resolution.t()) :: {:ok, Page.t(Post.t())}
|
||||
def find_posts_for_group(
|
||||
%Actor{id: group_id} = group,
|
||||
%{page: page, limit: limit} = args,
|
||||
%{
|
||||
context: %{
|
||||
current_user: %User{role: user_role} = user
|
||||
current_user: %User{role: user_role},
|
||||
current_actor: %Actor{id: actor_id}
|
||||
}
|
||||
} = _resolution
|
||||
) do
|
||||
with %Actor{id: actor_id} <- Users.get_actor_for_user(user),
|
||||
{:member, true} <-
|
||||
{:member, Actors.is_member?(actor_id, group_id) or is_moderator(user_role)},
|
||||
%Page{} = page <- Posts.get_posts_for_group(group, page, limit) do
|
||||
if Actors.is_member?(actor_id, group_id) or is_moderator(user_role) do
|
||||
%Page{} = page = Posts.get_posts_for_group(group, page, limit)
|
||||
{:ok, page}
|
||||
else
|
||||
{:member, _} ->
|
||||
find_posts_for_group(group, args, nil)
|
||||
end
|
||||
end
|
||||
|
@ -47,10 +45,9 @@ defmodule Mobilizon.GraphQL.Resolvers.Post do
|
|||
%{page: page, limit: limit},
|
||||
_resolution
|
||||
) do
|
||||
with %Page{} = page <- Posts.get_public_posts_for_group(group, page, limit) do
|
||||
%Page{} = page = Posts.get_public_posts_for_group(group, page, limit)
|
||||
{:ok, page}
|
||||
end
|
||||
end
|
||||
|
||||
def find_posts_for_group(
|
||||
_group,
|
||||
|
@ -60,18 +57,19 @@ defmodule Mobilizon.GraphQL.Resolvers.Post do
|
|||
{:ok, %Page{total: 0, elements: []}}
|
||||
end
|
||||
|
||||
@spec get_post(any(), map(), Absinthe.Resolution.t()) ::
|
||||
{:ok, Post.t()} | {:error, :post_not_found}
|
||||
def get_post(
|
||||
parent,
|
||||
%{slug: slug},
|
||||
%{
|
||||
context: %{
|
||||
current_user: %User{role: user_role} = user
|
||||
current_user: %User{role: user_role},
|
||||
current_actor: %Actor{} = current_profile
|
||||
}
|
||||
} = _resolution
|
||||
) do
|
||||
with {:current_actor, %Actor{} = current_profile} <-
|
||||
{:current_actor, Users.get_actor_for_user(user)},
|
||||
{:post, %Post{attributed_to: %Actor{}} = post} <-
|
||||
with {:post, %Post{attributed_to: %Actor{}} = post} <-
|
||||
{:post, Posts.get_post_by_slug_with_preloads(slug)},
|
||||
{:member, true} <-
|
||||
{:member,
|
||||
|
@ -102,17 +100,18 @@ defmodule Mobilizon.GraphQL.Resolvers.Post do
|
|||
{:error, :post_not_found}
|
||||
end
|
||||
|
||||
@spec create_post(any(), map(), Absinthe.Resolution.t()) ::
|
||||
{:ok, Post.t()} | {:error, String.t()}
|
||||
def create_post(
|
||||
_parent,
|
||||
%{attributed_to_id: group_id} = args,
|
||||
%{
|
||||
context: %{
|
||||
current_user: %User{} = user
|
||||
current_actor: %Actor{id: actor_id}
|
||||
}
|
||||
} = _resolution
|
||||
) do
|
||||
with %Actor{id: actor_id} <- Users.get_actor_for_user(user),
|
||||
{:member, true} <- {:member, Actors.is_member?(actor_id, group_id)},
|
||||
with {:member, true} <- {:member, Actors.is_member?(actor_id, group_id)},
|
||||
%Actor{} = group <- Actors.get_actor(group_id),
|
||||
args <-
|
||||
Map.update(args, :picture, nil, fn picture ->
|
||||
|
@ -120,7 +119,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Post do
|
|||
end),
|
||||
args <- extract_pictures_from_post_body(args, actor_id),
|
||||
{:ok, _, %Post{} = post} <-
|
||||
ActivityPub.create(
|
||||
Actions.Create.create(
|
||||
:post,
|
||||
args
|
||||
|> Map.put(:author_id, actor_id)
|
||||
|
@ -142,17 +141,18 @@ defmodule Mobilizon.GraphQL.Resolvers.Post do
|
|||
{:error, dgettext("errors", "You need to be logged-in to create posts")}
|
||||
end
|
||||
|
||||
@spec update_post(any(), map(), Absinthe.Resolution.t()) ::
|
||||
{:ok, Post.t()} | {:error, String.t()}
|
||||
def update_post(
|
||||
_parent,
|
||||
%{id: id} = args,
|
||||
%{
|
||||
context: %{
|
||||
current_user: %User{} = user
|
||||
current_actor: %Actor{id: actor_id, url: actor_url}
|
||||
}
|
||||
} = _resolution
|
||||
) do
|
||||
with {:uuid, {:ok, _uuid}} <- {:uuid, Ecto.UUID.cast(id)},
|
||||
%Actor{id: actor_id, url: actor_url} <- Users.get_actor_for_user(user),
|
||||
{:post, %Post{attributed_to: %Actor{id: group_id} = group} = post} <-
|
||||
{:post, Posts.get_post_with_preloads(id)},
|
||||
args <-
|
||||
|
@ -162,7 +162,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Post do
|
|||
args <- extract_pictures_from_post_body(args, actor_id),
|
||||
{:member, true} <- {:member, Actors.is_member?(actor_id, group_id)},
|
||||
{:ok, _, %Post{} = post} <-
|
||||
ActivityPub.update(post, args, true, %{"actor" => actor_url}) do
|
||||
Actions.Update.update(post, args, true, %{"actor" => actor_url}) do
|
||||
{:ok, post}
|
||||
else
|
||||
{:uuid, :error} ->
|
||||
|
@ -180,22 +180,23 @@ defmodule Mobilizon.GraphQL.Resolvers.Post do
|
|||
{:error, dgettext("errors", "You need to be logged-in to update posts")}
|
||||
end
|
||||
|
||||
@spec delete_post(any(), map(), Absinthe.Resolution.t()) ::
|
||||
{:ok, Post.t()} | {:error, String.t()}
|
||||
def delete_post(
|
||||
_parent,
|
||||
%{id: post_id},
|
||||
%{
|
||||
context: %{
|
||||
current_user: %User{} = user
|
||||
current_actor: %Actor{id: actor_id} = actor
|
||||
}
|
||||
} = _resolution
|
||||
) do
|
||||
with {:uuid, {:ok, _uuid}} <- {:uuid, Ecto.UUID.cast(post_id)},
|
||||
%Actor{id: actor_id} = actor <- Users.get_actor_for_user(user),
|
||||
{:post, %Post{attributed_to: %Actor{id: group_id}} = post} <-
|
||||
{:post, Posts.get_post_with_preloads(post_id)},
|
||||
{:member, true} <- {:member, Actors.is_member?(actor_id, group_id)},
|
||||
{:ok, _, %Post{} = post} <-
|
||||
ActivityPub.delete(post, actor) do
|
||||
Actions.Delete.delete(post, actor) do
|
||||
{:ok, post}
|
||||
else
|
||||
{:uuid, :error} ->
|
||||
|
@ -213,6 +214,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Post do
|
|||
{:error, dgettext("errors", "You need to be logged-in to delete posts")}
|
||||
end
|
||||
|
||||
@spec process_picture(map() | nil, Actor.t()) :: nil | map()
|
||||
defp process_picture(nil, _), do: nil
|
||||
defp process_picture(%{media_id: _picture_id} = args, _), do: args
|
||||
|
||||
|
|
|
@ -10,6 +10,8 @@ defmodule Mobilizon.GraphQL.Resolvers.PushSubscription do
|
|||
@doc """
|
||||
List all of an user's registered push subscriptions
|
||||
"""
|
||||
@spec list_user_push_subscriptions(any(), map(), Absinthe.Resolution.t()) ::
|
||||
{:ok, Page.t(PushSubscription.t())} | {:error, :unauthenticated}
|
||||
def list_user_push_subscriptions(_parent, %{page: page, limit: limit}, %{
|
||||
context: %{current_user: %User{id: user_id}}
|
||||
}) do
|
||||
|
@ -22,6 +24,8 @@ defmodule Mobilizon.GraphQL.Resolvers.PushSubscription do
|
|||
@doc """
|
||||
Register a push subscription
|
||||
"""
|
||||
@spec register_push_subscription(any(), map(), Absinthe.Resolution.t()) ::
|
||||
{:ok, String.t()} | {:error, String.t()}
|
||||
def register_push_subscription(_parent, args, %{
|
||||
context: %{current_user: %User{id: user_id}}
|
||||
}) do
|
||||
|
|
|
@ -5,7 +5,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Report do
|
|||
|
||||
import Mobilizon.Users.Guards
|
||||
|
||||
alias Mobilizon.{Actors, Config, Reports, Users}
|
||||
alias Mobilizon.{Actors, Config, Reports}
|
||||
alias Mobilizon.Actors.Actor
|
||||
alias Mobilizon.Reports.{Note, Report}
|
||||
alias Mobilizon.Users.User
|
||||
|
@ -13,6 +13,8 @@ defmodule Mobilizon.GraphQL.Resolvers.Report do
|
|||
|
||||
alias Mobilizon.GraphQL.API
|
||||
|
||||
@spec list_reports(any(), map(), Absinthe.Resolution.t()) ::
|
||||
{:ok, Page.t(Report.t())} | {:error, String.t()}
|
||||
def list_reports(
|
||||
_parent,
|
||||
%{page: page, limit: limit, status: status},
|
||||
|
@ -26,6 +28,8 @@ defmodule Mobilizon.GraphQL.Resolvers.Report do
|
|||
{:error, dgettext("errors", "You need to be logged-in and a moderator to list reports")}
|
||||
end
|
||||
|
||||
@spec get_report(any(), map(), Absinthe.Resolution.t()) ::
|
||||
{:ok, Report.t()} | {:error, String.t()}
|
||||
def get_report(_parent, %{id: id}, %{context: %{current_user: %User{role: role}}})
|
||||
when is_moderator(role) do
|
||||
case Mobilizon.Reports.get_report(id) do
|
||||
|
@ -44,16 +48,17 @@ defmodule Mobilizon.GraphQL.Resolvers.Report do
|
|||
@doc """
|
||||
Create a report, either logged-in or anonymously
|
||||
"""
|
||||
@spec create_report(any(), map(), Absinthe.Resolution.t()) ::
|
||||
{:ok, Report.t()} | {:error, String.t()}
|
||||
def create_report(
|
||||
_parent,
|
||||
args,
|
||||
%{context: %{current_user: %User{} = user}} = _resolution
|
||||
%{context: %{current_actor: %Actor{id: reporter_id}}} = _resolution
|
||||
) do
|
||||
with %Actor{id: reporter_id} <- Users.get_actor_for_user(user),
|
||||
{:ok, _, %Report{} = report} <-
|
||||
args |> Map.put(:reporter_id, reporter_id) |> API.Reports.report() do
|
||||
case args |> Map.put(:reporter_id, reporter_id) |> API.Reports.report() do
|
||||
{:ok, _, %Report{} = report} ->
|
||||
{:ok, report}
|
||||
else
|
||||
|
||||
_error ->
|
||||
{:error, dgettext("errors", "Error while saving report")}
|
||||
end
|
||||
|
@ -81,14 +86,15 @@ defmodule Mobilizon.GraphQL.Resolvers.Report do
|
|||
@doc """
|
||||
Update a report's status
|
||||
"""
|
||||
@spec update_report(any(), map(), Absinthe.Resolution.t()) ::
|
||||
{:ok, Report.t()} | {:error, String.t()}
|
||||
def update_report(
|
||||
_parent,
|
||||
%{report_id: report_id, status: status},
|
||||
%{context: %{current_user: %User{role: role} = user}}
|
||||
%{context: %{current_user: %User{role: role}, current_actor: %Actor{} = actor}}
|
||||
)
|
||||
when is_moderator(role) do
|
||||
with %Actor{} = actor <- Users.get_actor_for_user(user),
|
||||
%Report{} = report <- Mobilizon.Reports.get_report(report_id),
|
||||
with %Report{} = report <- Mobilizon.Reports.get_report(report_id),
|
||||
{:ok, %Report{} = report} <- API.Reports.update_report_status(actor, report, status) do
|
||||
{:ok, report}
|
||||
else
|
||||
|
@ -101,28 +107,28 @@ defmodule Mobilizon.GraphQL.Resolvers.Report do
|
|||
{:error, dgettext("errors", "You need to be logged-in and a moderator to update a report")}
|
||||
end
|
||||
|
||||
@spec create_report_note(any(), map(), Absinthe.Resolution.t()) :: {:ok, Note.t()}
|
||||
def create_report_note(
|
||||
_parent,
|
||||
%{report_id: report_id, content: content},
|
||||
%{context: %{current_user: %User{role: role} = user}}
|
||||
%{context: %{current_user: %User{role: role}, current_actor: %Actor{id: moderator_id}}}
|
||||
)
|
||||
when is_moderator(role) do
|
||||
with %Actor{id: moderator_id} <- Users.get_actor_for_user(user),
|
||||
%Report{} = report <- Reports.get_report(report_id),
|
||||
with %Report{} = report <- Reports.get_report(report_id),
|
||||
%Actor{} = moderator <- Actors.get_local_actor_with_preload(moderator_id),
|
||||
{:ok, %Note{} = note} <- API.Reports.create_report_note(report, moderator, content) do
|
||||
{:ok, note}
|
||||
end
|
||||
end
|
||||
|
||||
@spec delete_report_note(any(), map(), Absinthe.Resolution.t()) :: {:ok, map()}
|
||||
def delete_report_note(
|
||||
_parent,
|
||||
%{note_id: note_id},
|
||||
%{context: %{current_user: %User{role: role} = user}}
|
||||
%{context: %{current_user: %User{role: role}, current_actor: %Actor{id: moderator_id}}}
|
||||
)
|
||||
when is_moderator(role) do
|
||||
with %Actor{id: moderator_id} <- Users.get_actor_for_user(user),
|
||||
%Note{} = note <- Reports.get_note(note_id),
|
||||
with %Note{} = note <- Reports.get_note(note_id),
|
||||
%Actor{} = moderator <- Actors.get_local_actor_with_preload(moderator_id),
|
||||
{:ok, %Note{} = note} <- API.Reports.delete_report_note(note, moderator) do
|
||||
{:ok, %{id: note.id}}
|
||||
|
|
|
@ -3,9 +3,9 @@ defmodule Mobilizon.GraphQL.Resolvers.Resource do
|
|||
Handles the resources-related GraphQL calls
|
||||
"""
|
||||
|
||||
alias Mobilizon.{Actors, Resources, Users}
|
||||
alias Mobilizon.{Actors, Resources}
|
||||
alias Mobilizon.Actors.Actor
|
||||
alias Mobilizon.Federation.ActivityPub
|
||||
alias Mobilizon.Federation.ActivityPub.Actions
|
||||
alias Mobilizon.Resources.Resource
|
||||
alias Mobilizon.Resources.Resource.Metadata
|
||||
alias Mobilizon.Service.RichMedia.Parser
|
||||
|
@ -21,17 +21,18 @@ defmodule Mobilizon.GraphQL.Resolvers.Resource do
|
|||
|
||||
Returns only if actor requesting is a member of the group
|
||||
"""
|
||||
@spec find_resources_for_group(Actor.t(), map(), Absinthe.Resolution.t()) ::
|
||||
{:ok, Page.t(Resource.t())}
|
||||
def find_resources_for_group(
|
||||
%Actor{id: group_id} = group,
|
||||
%{page: page, limit: limit},
|
||||
%{
|
||||
context: %{
|
||||
current_user: %User{} = user
|
||||
current_actor: %Actor{id: actor_id}
|
||||
}
|
||||
} = _resolution
|
||||
) do
|
||||
with %Actor{id: actor_id} <- Users.get_actor_for_user(user),
|
||||
{:member, true} <- {:member, Actors.is_member?(actor_id, group_id)},
|
||||
with {:member, true} <- {:member, Actors.is_member?(actor_id, group_id)},
|
||||
%Page{} = page <- Resources.get_resources_for_group(group, page, limit) do
|
||||
{:ok, page}
|
||||
else
|
||||
|
@ -48,17 +49,18 @@ defmodule Mobilizon.GraphQL.Resolvers.Resource do
|
|||
{:ok, %Page{total: 0, elements: []}}
|
||||
end
|
||||
|
||||
@spec find_resources_for_parent(Resource.t(), map(), Absinthe.Resolution.t()) ::
|
||||
{:ok, Page.t(Resource.t())}
|
||||
def find_resources_for_parent(
|
||||
%Resource{actor_id: group_id} = parent,
|
||||
%{page: page, limit: limit},
|
||||
%{
|
||||
context: %{
|
||||
current_user: %User{} = user
|
||||
current_actor: %Actor{id: actor_id}
|
||||
}
|
||||
} = _resolution
|
||||
) do
|
||||
with %Actor{id: actor_id} <- Users.get_actor_for_user(user),
|
||||
{:member, true} <- {:member, Actors.is_member?(actor_id, group_id)},
|
||||
with {:member, true} <- {:member, Actors.is_member?(actor_id, group_id)},
|
||||
%Page{} = page <- Resources.get_resources_for_folder(parent, page, limit) do
|
||||
{:ok, page}
|
||||
end
|
||||
|
@ -67,18 +69,18 @@ defmodule Mobilizon.GraphQL.Resolvers.Resource do
|
|||
def find_resources_for_parent(_parent, _args, _resolution),
|
||||
do: {:ok, %Page{total: 0, elements: []}}
|
||||
|
||||
@spec get_resource(any(), map(), Absinthe.Resolution.t()) ::
|
||||
{:ok, Resource.t()} | {:error, :group_not_found | :resource_not_found | String.t()}
|
||||
def get_resource(
|
||||
_parent,
|
||||
%{path: path, username: username},
|
||||
%{
|
||||
context: %{
|
||||
current_user: %User{} = user
|
||||
current_actor: %Actor{id: actor_id}
|
||||
}
|
||||
} = _resolution
|
||||
) do
|
||||
with {:current_actor, %Actor{id: actor_id}} <-
|
||||
{:current_actor, Users.get_actor_for_user(user)},
|
||||
{:group, %Actor{id: group_id}} <- {:group, Actors.get_actor_by_name(username, :Group)},
|
||||
with {:group, %Actor{id: group_id}} <- {:group, Actors.get_actor_by_name(username, :Group)},
|
||||
{:member, true} <- {:member, Actors.is_member?(actor_id, group_id)},
|
||||
{:resource, %Resource{} = resource} <-
|
||||
{:resource, Resources.get_resource_by_group_and_path_with_preloads(group_id, path)} do
|
||||
|
@ -94,21 +96,22 @@ defmodule Mobilizon.GraphQL.Resolvers.Resource do
|
|||
{:error, dgettext("errors", "You need to be logged-in to access resources")}
|
||||
end
|
||||
|
||||
@spec create_resource(any(), map(), Absinthe.Resolution.t()) ::
|
||||
{:ok, Resource.t()} | {:error, String.t()}
|
||||
def create_resource(
|
||||
_parent,
|
||||
%{actor_id: group_id} = args,
|
||||
%{
|
||||
context: %{
|
||||
current_user: %User{} = user
|
||||
current_actor: %Actor{id: actor_id}
|
||||
}
|
||||
} = _resolution
|
||||
) do
|
||||
with %Actor{id: actor_id} <- Users.get_actor_for_user(user),
|
||||
{:member, true} <- {:member, Actors.is_member?(actor_id, group_id)},
|
||||
with {:member, true} <- {:member, Actors.is_member?(actor_id, group_id)},
|
||||
parent <- get_eventual_parent(args),
|
||||
{:own_check, true} <- {:own_check, check_resource_owned_by_group(parent, group_id)},
|
||||
{:ok, _, %Resource{} = resource} <-
|
||||
ActivityPub.create(
|
||||
Actions.Create.create(
|
||||
:resource,
|
||||
args
|
||||
|> Map.put(:actor_id, group_id)
|
||||
|
@ -133,50 +136,59 @@ defmodule Mobilizon.GraphQL.Resolvers.Resource do
|
|||
{:error, dgettext("errors", "You need to be logged-in to create resources")}
|
||||
end
|
||||
|
||||
@spec update_resource(any(), map(), Absinthe.Resolution.t()) ::
|
||||
{:ok, Resource.t()} | {:error, String.t()}
|
||||
def update_resource(
|
||||
_parent,
|
||||
%{id: resource_id} = args,
|
||||
%{
|
||||
context: %{
|
||||
current_user: %User{} = user
|
||||
current_actor: %Actor{id: actor_id, url: actor_url}
|
||||
}
|
||||
} = _resolution
|
||||
) do
|
||||
with %Actor{id: actor_id, url: actor_url} <- Users.get_actor_for_user(user),
|
||||
{:resource, %Resource{actor_id: group_id} = resource} <-
|
||||
{:resource, Resources.get_resource_with_preloads(resource_id)},
|
||||
{:member, true} <- {:member, Actors.is_member?(actor_id, group_id)},
|
||||
{:ok, _, %Resource{} = resource} <-
|
||||
ActivityPub.update(resource, args, true, %{"actor" => actor_url}) do
|
||||
case Resources.get_resource_with_preloads(resource_id) do
|
||||
%Resource{actor_id: group_id} = resource ->
|
||||
if Actors.is_member?(actor_id, group_id) do
|
||||
case Actions.Update.update(resource, args, true, %{"actor" => actor_url}) do
|
||||
{:ok, _, %Resource{} = resource} ->
|
||||
{:ok, resource}
|
||||
else
|
||||
{:resource, _} ->
|
||||
{:error, dgettext("errors", "Resource doesn't exist")}
|
||||
|
||||
{:member, _} ->
|
||||
{:error, %Ecto.Changeset{} = err} ->
|
||||
{:error, err}
|
||||
|
||||
{:error, err} when is_atom(err) ->
|
||||
{:error, dgettext("errors", "Unknown error while updating resource")}
|
||||
end
|
||||
else
|
||||
{:error, dgettext("errors", "Profile is not member of group")}
|
||||
end
|
||||
|
||||
nil ->
|
||||
{:error, dgettext("errors", "Resource doesn't exist")}
|
||||
end
|
||||
end
|
||||
|
||||
def update_resource(_parent, _args, _resolution) do
|
||||
{:error, dgettext("errors", "You need to be logged-in to update resources")}
|
||||
end
|
||||
|
||||
@spec delete_resource(any(), map(), Absinthe.Resolution.t()) ::
|
||||
{:ok, Resource.t()} | {:error, String.t()}
|
||||
def delete_resource(
|
||||
_parent,
|
||||
%{id: resource_id},
|
||||
%{
|
||||
context: %{
|
||||
current_user: %User{} = user
|
||||
current_actor: %Actor{id: actor_id} = actor
|
||||
}
|
||||
} = _resolution
|
||||
) do
|
||||
with %Actor{id: actor_id} = actor <- Users.get_actor_for_user(user),
|
||||
{:resource, %Resource{parent_id: _parent_id, actor_id: group_id} = resource} <-
|
||||
with {:resource, %Resource{parent_id: _parent_id, actor_id: group_id} = resource} <-
|
||||
{:resource, Resources.get_resource_with_preloads(resource_id)},
|
||||
{:member, true} <- {:member, Actors.is_member?(actor_id, group_id)},
|
||||
{:ok, _, %Resource{} = resource} <-
|
||||
ActivityPub.delete(resource, actor) do
|
||||
Actions.Delete.delete(resource, actor) do
|
||||
{:ok, resource}
|
||||
else
|
||||
{:resource, _} ->
|
||||
|
@ -191,6 +203,8 @@ defmodule Mobilizon.GraphQL.Resolvers.Resource do
|
|||
{:error, dgettext("errors", "You need to be logged-in to delete resources")}
|
||||
end
|
||||
|
||||
@spec preview_resource_link(any(), map(), Absinthe.Resolution.t()) ::
|
||||
{:ok, Metadata.t()} | {:error, String.t() | :unknown_resource}
|
||||
def preview_resource_link(
|
||||
_parent,
|
||||
%{resource_url: resource_url},
|
||||
|
@ -218,6 +232,8 @@ defmodule Mobilizon.GraphQL.Resolvers.Resource do
|
|||
{:error, dgettext("errors", "You need to be logged-in to view a resource preview")}
|
||||
end
|
||||
|
||||
@spec proxyify_pictures(Metadata.t(), map(), Absinthe.Resolution.t()) ::
|
||||
{:ok, String.t() | nil} | {:error, String.t()}
|
||||
def proxyify_pictures(%Metadata{} = metadata, _args, %{
|
||||
definition: %{schema_node: %{name: name}}
|
||||
}) do
|
||||
|
|
|
@ -2,12 +2,16 @@ defmodule Mobilizon.GraphQL.Resolvers.Search do
|
|||
@moduledoc """
|
||||
Handles the event-related GraphQL calls
|
||||
"""
|
||||
|
||||
alias Mobilizon.Actors.Actor
|
||||
alias Mobilizon.Events.Event
|
||||
alias Mobilizon.GraphQL.API.Search
|
||||
alias Mobilizon.Storage.Page
|
||||
|
||||
@doc """
|
||||
Search persons
|
||||
"""
|
||||
@spec search_persons(any(), map(), Absinthe.Resolution.t()) ::
|
||||
{:ok, Page.t(Actor.t())} | {:error, String.t()}
|
||||
def search_persons(_parent, %{page: page, limit: limit} = args, _resolution) do
|
||||
Search.search_actors(Map.put(args, :minimum_visibility, :private), page, limit, :Person)
|
||||
end
|
||||
|
@ -15,6 +19,8 @@ defmodule Mobilizon.GraphQL.Resolvers.Search do
|
|||
@doc """
|
||||
Search groups
|
||||
"""
|
||||
@spec search_groups(any(), map(), Absinthe.Resolution.t()) ::
|
||||
{:ok, Page.t(Actor.t())} | {:error, String.t()}
|
||||
def search_groups(_parent, %{page: page, limit: limit} = args, _resolution) do
|
||||
Search.search_actors(args, page, limit, :Group)
|
||||
end
|
||||
|
@ -22,10 +28,13 @@ defmodule Mobilizon.GraphQL.Resolvers.Search do
|
|||
@doc """
|
||||
Search events
|
||||
"""
|
||||
@spec search_events(any(), map(), Absinthe.Resolution.t()) ::
|
||||
{:ok, Page.t(Event.t())} | {:error, String.t()}
|
||||
def search_events(_parent, %{page: page, limit: limit} = args, _resolution) do
|
||||
Search.search_events(args, page, limit)
|
||||
end
|
||||
|
||||
@spec interact(any(), map(), Absinthe.Resolution.t()) :: {:ok, struct} | {:error, :not_found}
|
||||
def interact(_parent, %{uri: uri}, _resolution) do
|
||||
Search.interact(uri)
|
||||
end
|
||||
|
|
|
@ -8,6 +8,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Statistics do
|
|||
@doc """
|
||||
Gets config.
|
||||
"""
|
||||
@spec get_statistics(any(), any(), any()) :: {:ok, map()}
|
||||
def get_statistics(_parent, _params, _context) do
|
||||
{:ok,
|
||||
%{
|
||||
|
|
|
@ -6,9 +6,12 @@ defmodule Mobilizon.GraphQL.Resolvers.Tag do
|
|||
alias Mobilizon.{Events, Posts}
|
||||
alias Mobilizon.Events.{Event, Tag}
|
||||
alias Mobilizon.Posts.Post
|
||||
alias Mobilizon.Storage.Page
|
||||
|
||||
def list_tags(_parent, %{page: page, limit: limit}, _resolution) do
|
||||
tags = Mobilizon.Events.list_tags(page, limit)
|
||||
@spec list_tags(any(), map(), Absinthe.Resolution.t()) :: {:ok, Page.t(Tag.t())}
|
||||
def list_tags(_parent, %{page: page, limit: limit} = args, _resolution) do
|
||||
filter = Map.get(args, :filter)
|
||||
tags = Mobilizon.Events.list_tags(filter, page, limit)
|
||||
|
||||
{:ok, tags}
|
||||
end
|
||||
|
@ -18,6 +21,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Tag do
|
|||
|
||||
From an event or a struct with an url
|
||||
"""
|
||||
@spec list_tags_for_event(Event.t(), map(), Absinthe.Resolution.t()) :: {:ok, list(Tag.t())}
|
||||
def list_tags_for_event(%Event{id: id}, _args, _resolution) do
|
||||
{:ok, Events.list_tags_for_event(id)}
|
||||
end
|
||||
|
@ -32,6 +36,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Tag do
|
|||
@doc """
|
||||
Retrieve the list of tags for a post
|
||||
"""
|
||||
@spec list_tags_for_post(Post.t(), map(), Absinthe.Resolution.t()) :: {:ok, list(Tag.t())}
|
||||
def list_tags_for_post(%Post{id: id}, _args, _resolution) do
|
||||
{:ok, Posts.list_tags_for_post(id)}
|
||||
end
|
||||
|
@ -49,9 +54,8 @@ defmodule Mobilizon.GraphQL.Resolvers.Tag do
|
|||
@doc """
|
||||
Retrieve the list of related tags for a parent tag
|
||||
"""
|
||||
@spec list_tags_for_post(Tag.t(), map(), Absinthe.Resolution.t()) :: {:ok, list(Tag.t())}
|
||||
def get_related_tags(%Tag{} = tag, _args, _resolution) do
|
||||
with tags <- Events.list_tag_neighbors(tag) do
|
||||
{:ok, tags}
|
||||
end
|
||||
{:ok, Events.list_tag_neighbors(tag)}
|
||||
end
|
||||
end
|
||||
|
|
|
@ -3,12 +3,11 @@ defmodule Mobilizon.GraphQL.Resolvers.Todos do
|
|||
Handles the todos related GraphQL calls
|
||||
"""
|
||||
|
||||
alias Mobilizon.{Actors, Todos, Users}
|
||||
alias Mobilizon.{Actors, Todos}
|
||||
alias Mobilizon.Actors.Actor
|
||||
alias Mobilizon.Federation.ActivityPub
|
||||
alias Mobilizon.Federation.ActivityPub.Actions
|
||||
alias Mobilizon.Storage.Page
|
||||
alias Mobilizon.Todos.{Todo, TodoList}
|
||||
alias Mobilizon.Users.User
|
||||
import Mobilizon.Web.Gettext
|
||||
|
||||
require Logger
|
||||
|
@ -18,15 +17,16 @@ defmodule Mobilizon.GraphQL.Resolvers.Todos do
|
|||
|
||||
Returns only if actor requesting is a member of the group
|
||||
"""
|
||||
@spec find_todo_lists_for_group(Actor.t(), map(), Absinthe.Resolution.t()) ::
|
||||
{:ok, Page.t(TodoList.t())}
|
||||
def find_todo_lists_for_group(
|
||||
%Actor{id: group_id} = group,
|
||||
_args,
|
||||
%{
|
||||
context: %{current_user: %User{} = user}
|
||||
context: %{current_actor: %Actor{id: actor_id}}
|
||||
} = _resolution
|
||||
) do
|
||||
with %Actor{id: actor_id} <- Users.get_actor_for_user(user),
|
||||
{:member, true} <- {:member, Actors.is_member?(actor_id, group_id)},
|
||||
with {:member, true} <- {:member, Actors.is_member?(actor_id, group_id)},
|
||||
%Page{} = page <- Todos.get_todo_lists_for_group(group) do
|
||||
{:ok, page}
|
||||
else
|
||||
|
@ -41,15 +41,16 @@ defmodule Mobilizon.GraphQL.Resolvers.Todos do
|
|||
{:ok, %Page{total: 0, elements: []}}
|
||||
end
|
||||
|
||||
@spec find_todo_lists_for_group(TodoList.t(), map(), Absinthe.Resolution.t()) ::
|
||||
{:ok, Page.t(Todo.t())} | {:error, String.t()}
|
||||
def find_todos_for_todo_list(
|
||||
%TodoList{actor_id: group_id} = todo_list,
|
||||
_args,
|
||||
%{
|
||||
context: %{current_user: %User{} = user}
|
||||
context: %{current_actor: %Actor{id: actor_id}}
|
||||
} = _resolution
|
||||
) do
|
||||
with %Actor{id: actor_id} <- Users.get_actor_for_user(user),
|
||||
{:member, true} <- {:member, Actors.is_member?(actor_id, group_id)},
|
||||
with {:member, true} <- {:member, Actors.is_member?(actor_id, group_id)},
|
||||
%Page{} = page <- Todos.get_todos_for_todo_list(todo_list) do
|
||||
{:ok, page}
|
||||
else
|
||||
|
@ -58,15 +59,16 @@ defmodule Mobilizon.GraphQL.Resolvers.Todos do
|
|||
end
|
||||
end
|
||||
|
||||
@spec get_todo_list(any(), map(), Absinthe.Resolution.t()) ::
|
||||
{:ok, TodoList.t()} | {:error, String.t()}
|
||||
def get_todo_list(
|
||||
_parent,
|
||||
%{id: todo_list_id},
|
||||
%{
|
||||
context: %{current_user: %User{} = user}
|
||||
context: %{current_actor: %Actor{id: actor_id}}
|
||||
} = _resolution
|
||||
) do
|
||||
with {:actor, %Actor{id: actor_id}} <- {:actor, Users.get_actor_for_user(user)},
|
||||
{:todo, %TodoList{actor_id: group_id} = todo} <-
|
||||
with {:todo, %TodoList{actor_id: group_id} = todo} <-
|
||||
{:todo, Todos.get_todo_list(todo_list_id)},
|
||||
{:member, true} <- {:member, Actors.is_member?(actor_id, group_id)} do
|
||||
{:ok, todo}
|
||||
|
@ -82,19 +84,28 @@ defmodule Mobilizon.GraphQL.Resolvers.Todos do
|
|||
end
|
||||
end
|
||||
|
||||
@spec create_todo_list(any(), map(), Absinthe.Resolution.t()) ::
|
||||
{:ok, TodoList.t()} | {:error, String.t()}
|
||||
def create_todo_list(
|
||||
_parent,
|
||||
%{group_id: group_id} = args,
|
||||
%{
|
||||
context: %{current_user: %User{} = user}
|
||||
context: %{current_actor: %Actor{id: actor_id}}
|
||||
} = _resolution
|
||||
) do
|
||||
with {:actor, %Actor{id: actor_id} = _actor} <- {:actor, Users.get_actor_for_user(user)},
|
||||
{:member, true} <- {:member, Actors.is_member?(actor_id, group_id)},
|
||||
with {:member, true} <- {:member, Actors.is_member?(actor_id, group_id)},
|
||||
{:ok, _, %TodoList{} = todo_list} <-
|
||||
ActivityPub.create(:todo_list, Map.put(args, :actor_id, group_id), true, %{}) do
|
||||
Actions.Create.create(
|
||||
:todo_list,
|
||||
Map.put(args, :actor_id, group_id),
|
||||
true,
|
||||
%{}
|
||||
) do
|
||||
{:ok, todo_list}
|
||||
else
|
||||
{:actor, nil} ->
|
||||
{:error, dgettext("errors", "No profile found for user")}
|
||||
|
||||
{:member, _} ->
|
||||
{:error, dgettext("errors", "Profile is not member of group")}
|
||||
end
|
||||
|
@ -112,7 +123,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Todos do
|
|||
# {:todo_list, Todos.get_todo_list(todo_list_id)},
|
||||
# {:member, true} <- {:member, Actors.is_member?(actor_id, group_id)},
|
||||
# {:ok, _, %TodoList{} = todo} <-
|
||||
# ActivityPub.update_todo_list(todo_list, actor, true, %{}) do
|
||||
# Actions.Update.update_todo_list(todo_list, actor, true, %{}) do
|
||||
# {:ok, todo}
|
||||
# else
|
||||
# {:todo_list, _} ->
|
||||
|
@ -135,7 +146,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Todos do
|
|||
# {:todo_list, Todos.get_todo_list(todo_list_id)},
|
||||
# {:member, true} <- {:member, Actors.is_member?(actor_id, group_id)},
|
||||
# {:ok, _, %TodoList{} = todo} <-
|
||||
# ActivityPub.delete_todo_list(todo_list, actor, true, %{}) do
|
||||
# Actions.Delete.delete_todo_list(todo_list, actor, true, %{}) do
|
||||
# {:ok, todo}
|
||||
# else
|
||||
# {:todo_list, _} ->
|
||||
|
@ -146,15 +157,15 @@ defmodule Mobilizon.GraphQL.Resolvers.Todos do
|
|||
# end
|
||||
# end
|
||||
|
||||
@spec get_todo(any(), map(), Absinthe.Resolution.t()) :: {:ok, Todo.t()} | {:error, String.t()}
|
||||
def get_todo(
|
||||
_parent,
|
||||
%{id: todo_id},
|
||||
%{
|
||||
context: %{current_user: %User{} = user}
|
||||
context: %{current_actor: %Actor{id: actor_id}}
|
||||
} = _resolution
|
||||
) do
|
||||
with {:actor, %Actor{id: actor_id}} <- {:actor, Users.get_actor_for_user(user)},
|
||||
{:todo, %Todo{todo_list_id: todo_list_id} = todo} <-
|
||||
with {:todo, %Todo{todo_list_id: todo_list_id} = todo} <-
|
||||
{:todo, Todos.get_todo(todo_id)},
|
||||
{:todo_list, %TodoList{actor_id: group_id}} <-
|
||||
{:todo_list, Todos.get_todo_list(todo_list_id)},
|
||||
|
@ -172,21 +183,30 @@ defmodule Mobilizon.GraphQL.Resolvers.Todos do
|
|||
end
|
||||
end
|
||||
|
||||
@spec create_todo(any(), map(), Absinthe.Resolution.t()) ::
|
||||
{:ok, Todo.t()} | {:error, String.t()}
|
||||
def create_todo(
|
||||
_parent,
|
||||
%{todo_list_id: todo_list_id} = args,
|
||||
%{
|
||||
context: %{current_user: %User{} = user}
|
||||
context: %{current_actor: %Actor{id: actor_id}}
|
||||
} = _resolution
|
||||
) do
|
||||
with {:actor, %Actor{id: actor_id} = _actor} <- {:actor, Users.get_actor_for_user(user)},
|
||||
{:todo_list, %TodoList{actor_id: group_id} = _todo_list} <-
|
||||
with {:todo_list, %TodoList{actor_id: group_id} = _todo_list} <-
|
||||
{:todo_list, Todos.get_todo_list(todo_list_id)},
|
||||
{:member, true} <- {:member, Actors.is_member?(actor_id, group_id)},
|
||||
{:ok, _, %Todo{} = todo} <-
|
||||
ActivityPub.create(:todo, Map.put(args, :creator_id, actor_id), true, %{}) do
|
||||
Actions.Create.create(
|
||||
:todo,
|
||||
Map.put(args, :creator_id, actor_id),
|
||||
true,
|
||||
%{}
|
||||
) do
|
||||
{:ok, todo}
|
||||
else
|
||||
{:actor, nil} ->
|
||||
{:error, dgettext("errors", "No profile found for user")}
|
||||
|
||||
{:todo_list, _} ->
|
||||
{:error, dgettext("errors", "Todo list doesn't exist")}
|
||||
|
||||
|
@ -195,23 +215,27 @@ defmodule Mobilizon.GraphQL.Resolvers.Todos do
|
|||
end
|
||||
end
|
||||
|
||||
@spec update_todo(any(), map(), Absinthe.Resolution.t()) ::
|
||||
{:ok, Todo.t()} | {:error, String.t()}
|
||||
def update_todo(
|
||||
_parent,
|
||||
%{id: todo_id} = args,
|
||||
%{
|
||||
context: %{current_user: %User{} = user}
|
||||
context: %{current_actor: %Actor{id: actor_id}}
|
||||
} = _resolution
|
||||
) do
|
||||
with {:actor, %Actor{id: actor_id} = _actor} <- {:actor, Users.get_actor_for_user(user)},
|
||||
{:todo, %Todo{todo_list_id: todo_list_id} = todo} <-
|
||||
with {:todo, %Todo{todo_list_id: todo_list_id} = todo} <-
|
||||
{:todo, Todos.get_todo(todo_id)},
|
||||
{:todo_list, %TodoList{actor_id: group_id}} <-
|
||||
{:todo_list, Todos.get_todo_list(todo_list_id)},
|
||||
{:member, true} <- {:member, Actors.is_member?(actor_id, group_id)},
|
||||
{:ok, _, %Todo{} = todo} <-
|
||||
ActivityPub.update(todo, args, true, %{}) do
|
||||
Actions.Update.update(todo, args, true, %{}) do
|
||||
{:ok, todo}
|
||||
else
|
||||
{:actor, nil} ->
|
||||
{:error, dgettext("errors", "No profile found for user")}
|
||||
|
||||
{:todo_list, _} ->
|
||||
{:error, dgettext("errors", "Todo list doesn't exist")}
|
||||
|
||||
|
@ -237,7 +261,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Todos do
|
|||
# {:todo_list, Todos.get_todo_list(todo_list_id)},
|
||||
# {:member, true} <- {:member, Actors.is_member?(actor_id, group_id)},
|
||||
# {:ok, _, %Todo{} = todo} <-
|
||||
# ActivityPub.delete_todo(todo, actor, true, %{}) do
|
||||
# Actions.Delete.delete_todo(todo, actor, true, %{}) do
|
||||
# {:ok, todo}
|
||||
# else
|
||||
# {:todo_list, _} ->
|
||||
|
|
|
@ -7,8 +7,7 @@ defmodule Mobilizon.GraphQL.Resolvers.User do
|
|||
|
||||
alias Mobilizon.{Actors, Admin, Config, Events, Users}
|
||||
alias Mobilizon.Actors.Actor
|
||||
alias Mobilizon.Federation.ActivityPub
|
||||
alias Mobilizon.Federation.ActivityPub.Relay
|
||||
alias Mobilizon.Federation.ActivityPub.{Actions, Relay}
|
||||
alias Mobilizon.Service.Auth.Authenticator
|
||||
alias Mobilizon.Storage.{Page, Repo}
|
||||
alias Mobilizon.Users.{Setting, User}
|
||||
|
@ -21,6 +20,7 @@ defmodule Mobilizon.GraphQL.Resolvers.User do
|
|||
@doc """
|
||||
Find an user by its ID
|
||||
"""
|
||||
@spec find_user(any(), map(), Absinthe.Resolution.t()) :: {:ok, User.t()} | {:error, String.t()}
|
||||
def find_user(_parent, %{id: id}, %{context: %{current_user: %User{role: role}}})
|
||||
when is_moderator(role) do
|
||||
with {:ok, %User{} = user} <- Users.get_user_with_actors(id) do
|
||||
|
@ -31,6 +31,8 @@ defmodule Mobilizon.GraphQL.Resolvers.User do
|
|||
@doc """
|
||||
Return current logged-in user
|
||||
"""
|
||||
@spec get_current_user(any, map(), Absinthe.Resolution.t()) ::
|
||||
{:error, :unauthenticated} | {:ok, Mobilizon.Users.User.t()}
|
||||
def get_current_user(_parent, _args, %{context: %{current_user: %User{} = user}}) do
|
||||
{:ok, user}
|
||||
end
|
||||
|
@ -42,6 +44,8 @@ defmodule Mobilizon.GraphQL.Resolvers.User do
|
|||
@doc """
|
||||
List instance users
|
||||
"""
|
||||
@spec list_users(any(), map(), Absinthe.Resolution.t()) ::
|
||||
{:ok, Page.t(User.t())} | {:error, :unauthorized}
|
||||
def list_users(
|
||||
_parent,
|
||||
%{email: email, page: page, limit: limit, sort: sort, direction: direction},
|
||||
|
@ -58,6 +62,8 @@ defmodule Mobilizon.GraphQL.Resolvers.User do
|
|||
@doc """
|
||||
Login an user. Returns a token and the user
|
||||
"""
|
||||
@spec login_user(any(), map(), Absinthe.Resolution.t()) ::
|
||||
{:ok, map()} | {:error, :user_not_found | String.t()}
|
||||
def login_user(_parent, %{email: email, password: password}, %{context: context}) do
|
||||
with {:ok,
|
||||
%{
|
||||
|
@ -65,9 +71,8 @@ defmodule Mobilizon.GraphQL.Resolvers.User do
|
|||
refresh_token: _refresh_token,
|
||||
user: %User{} = user
|
||||
} = user_and_tokens} <- Authenticator.authenticate(email, password),
|
||||
{:ok, %User{} = user} <- update_user_login_information(user, context),
|
||||
user_and_tokens <- Map.put(user_and_tokens, :user, user) do
|
||||
{:ok, user_and_tokens}
|
||||
{:ok, %User{} = user} <- update_user_login_information(user, context) do
|
||||
{:ok, %{user_and_tokens | user: user}}
|
||||
else
|
||||
{:error, :user_not_found} ->
|
||||
{:error, :user_not_found}
|
||||
|
@ -87,6 +92,8 @@ defmodule Mobilizon.GraphQL.Resolvers.User do
|
|||
@doc """
|
||||
Refresh a token
|
||||
"""
|
||||
@spec refresh_token(any(), map(), Absinthe.Resolution.t()) ::
|
||||
{:ok, map()} | {:error, String.t()}
|
||||
def refresh_token(_parent, %{refresh_token: refresh_token}, _resolution) do
|
||||
with {:ok, user, _claims} <- Auth.Guardian.resource_from_token(refresh_token),
|
||||
{:ok, _old, {exchanged_token, _claims}} <-
|
||||
|
@ -105,6 +112,9 @@ defmodule Mobilizon.GraphQL.Resolvers.User do
|
|||
{:error, dgettext("errors", "You need to have an existing token to get a refresh token")}
|
||||
end
|
||||
|
||||
@spec logout(any(), map(), Absinthe.Resolution.t()) ::
|
||||
{:ok, String.t()}
|
||||
| {:error, :token_not_found | :unable_to_logout | :unauthenticated | :invalid_argument}
|
||||
def logout(_parent, %{refresh_token: refresh_token}, %{context: %{current_user: %User{}}}) do
|
||||
with {:ok, _claims} <- Auth.Guardian.decode_and_verify(refresh_token, %{"typ" => "refresh"}),
|
||||
{:ok, _claims} <- Auth.Guardian.revoke(refresh_token) do
|
||||
|
@ -133,7 +143,7 @@ defmodule Mobilizon.GraphQL.Resolvers.User do
|
|||
- create the user
|
||||
- send a validation email to the user
|
||||
"""
|
||||
@spec create_user(any, map, any) :: tuple
|
||||
@spec create_user(any, %{email: String.t()}, any) :: {:ok, User.t()} | {:error, String.t()}
|
||||
def create_user(_parent, %{email: email} = args, _resolution) do
|
||||
with :registration_ok <- check_registration_config(email),
|
||||
:not_deny_listed <- check_registration_denylist(email),
|
||||
|
@ -160,20 +170,22 @@ defmodule Mobilizon.GraphQL.Resolvers.User do
|
|||
end
|
||||
end
|
||||
|
||||
@spec check_registration_config(map) :: atom
|
||||
@spec check_registration_config(String.t()) ::
|
||||
:registration_ok | :registration_closed | :not_allowlisted
|
||||
defp check_registration_config(email) do
|
||||
cond do
|
||||
Config.instance_registrations_open?() ->
|
||||
:registration_ok
|
||||
|
||||
Config.instance_registrations_allowlist?() ->
|
||||
check_allow_listed_email?(email)
|
||||
check_allow_listed_email(email)
|
||||
|
||||
true ->
|
||||
:registration_closed
|
||||
end
|
||||
end
|
||||
|
||||
@spec check_registration_denylist(String.t()) :: :deny_listed | :not_deny_listed
|
||||
defp check_registration_denylist(email) do
|
||||
# Remove everything behind the +
|
||||
email = String.replace(email, ~r/(\+.*)(?=\@)/, "")
|
||||
|
@ -183,8 +195,8 @@ defmodule Mobilizon.GraphQL.Resolvers.User do
|
|||
else: :not_deny_listed
|
||||
end
|
||||
|
||||
@spec check_allow_listed_email?(String.t()) :: :registration_ok | :not_allowlisted
|
||||
defp check_allow_listed_email?(email) do
|
||||
@spec check_allow_listed_email(String.t()) :: :registration_ok | :not_allowlisted
|
||||
defp check_allow_listed_email(email) do
|
||||
if email_in_list(email, Config.instance_registrations_allowlist()),
|
||||
do: :registration_ok,
|
||||
else: :not_allowlisted
|
||||
|
@ -199,23 +211,29 @@ defmodule Mobilizon.GraphQL.Resolvers.User do
|
|||
@doc """
|
||||
Validate an user, get its actor and a token
|
||||
"""
|
||||
@spec validate_user(map(), %{token: String.t()}, map()) :: {:ok, map()} | {:error, String.t()}
|
||||
def validate_user(_parent, %{token: token}, _resolution) do
|
||||
with {:check_confirmation_token, {:ok, %User{} = user}} <-
|
||||
{:check_confirmation_token, Email.User.check_confirmation_token(token)},
|
||||
{:get_actor, actor} <- {:get_actor, Users.get_actor_for_user(user)},
|
||||
{:ok, %{access_token: access_token, refresh_token: refresh_token}} <-
|
||||
Authenticator.generate_tokens(user) do
|
||||
case Email.User.check_confirmation_token(token) do
|
||||
{:ok, %User{} = user} ->
|
||||
actor = Users.get_actor_for_user(user)
|
||||
|
||||
{:ok, %{access_token: access_token, refresh_token: refresh_token}} =
|
||||
Authenticator.generate_tokens(user)
|
||||
|
||||
{:ok,
|
||||
%{
|
||||
access_token: access_token,
|
||||
refresh_token: refresh_token,
|
||||
user: Map.put(user, :default_actor, actor)
|
||||
}}
|
||||
else
|
||||
error ->
|
||||
Logger.info("Unable to validate user with token #{token}")
|
||||
Logger.debug(inspect(error))
|
||||
|
||||
{:error, :invalid_token} ->
|
||||
Logger.info("Invalid token #{token} to validate user")
|
||||
{:error, dgettext("errors", "Unable to validate user")}
|
||||
|
||||
{:error, %Ecto.Changeset{} = err} ->
|
||||
Logger.info("Unable to validate user with token #{token}")
|
||||
Logger.debug(inspect(err))
|
||||
{:error, dgettext("errors", "Unable to validate user")}
|
||||
end
|
||||
end
|
||||
|
@ -267,12 +285,25 @@ defmodule Mobilizon.GraphQL.Resolvers.User do
|
|||
@doc """
|
||||
Reset the password from an user
|
||||
"""
|
||||
@spec reset_password(map(), %{password: String.t(), token: String.t()}, map()) ::
|
||||
{:ok, map()} | {:error, String.t()}
|
||||
def reset_password(_parent, %{password: password, token: token}, _resolution) do
|
||||
with {:ok, %User{email: email} = user} <-
|
||||
Email.User.check_reset_password_token(password, token),
|
||||
{:ok, %{access_token: access_token, refresh_token: refresh_token}} <-
|
||||
Authenticator.authenticate(email, password) do
|
||||
{:ok, %{access_token: access_token, refresh_token: refresh_token, user: user}}
|
||||
case Email.User.check_reset_password_token(password, token) do
|
||||
{:ok, %User{email: email} = user} ->
|
||||
{:ok, tokens} = Authenticator.authenticate(email, password)
|
||||
{:ok, Map.put(tokens, :user, user)}
|
||||
|
||||
{:error, %Ecto.Changeset{errors: [password: {"registration.error.password_too_short", _}]}} ->
|
||||
{:error,
|
||||
gettext(
|
||||
"The password you have choosen is too short. Please make sure your password contains at least 6 charaters."
|
||||
)}
|
||||
|
||||
{:error, _err} ->
|
||||
{:error,
|
||||
gettext(
|
||||
"The token you provided is invalid. Make sure that the URL is exactly the one provided inside the email you got."
|
||||
)}
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -280,12 +311,12 @@ defmodule Mobilizon.GraphQL.Resolvers.User do
|
|||
def change_default_actor(
|
||||
_parent,
|
||||
%{preferred_username: username},
|
||||
%{context: %{current_user: user}}
|
||||
%{context: %{current_user: %User{id: user_id} = user}}
|
||||
) do
|
||||
with %Actor{id: actor_id} <- Actors.get_local_actor_by_name(username),
|
||||
with %Actor{id: actor_id} = actor <- Actors.get_local_actor_by_name(username),
|
||||
{:user_actor, true} <-
|
||||
{:user_actor, actor_id in Enum.map(Users.get_actors_for_user(user), & &1.id)},
|
||||
%User{} = user <- Users.update_user_default_actor(user.id, actor_id) do
|
||||
%User{} = user <- Users.update_user_default_actor(user_id, actor) do
|
||||
{:ok, user}
|
||||
else
|
||||
{:user_actor, _} ->
|
||||
|
@ -369,6 +400,9 @@ defmodule Mobilizon.GraphQL.Resolvers.User do
|
|||
|> Repo.update() do
|
||||
{:ok, user}
|
||||
else
|
||||
{:can_change_password, false} ->
|
||||
{:error, dgettext("errors", "You cannot change your password.")}
|
||||
|
||||
{:current_password, _} ->
|
||||
{:error, dgettext("errors", "The current password is invalid")}
|
||||
|
||||
|
@ -408,14 +442,18 @@ defmodule Mobilizon.GraphQL.Resolvers.User do
|
|||
|
||||
{:ok, user}
|
||||
else
|
||||
{:current_password, _} ->
|
||||
{:current_password, {:error, _}} ->
|
||||
{:error, dgettext("errors", "The password provided is invalid")}
|
||||
|
||||
{:same_email, true} ->
|
||||
{:error, dgettext("errors", "The new email must be different")}
|
||||
|
||||
{:email_valid, _} ->
|
||||
{:email_valid, false} ->
|
||||
{:error, dgettext("errors", "The new email doesn't seem to be valid")}
|
||||
|
||||
{:error, %Ecto.Changeset{} = err} ->
|
||||
Logger.debug(inspect(err))
|
||||
{:error, dgettext("errors", "Failed to update user email")}
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -423,30 +461,37 @@ defmodule Mobilizon.GraphQL.Resolvers.User do
|
|||
{:error, dgettext("errors", "You need to be logged-in to change your email")}
|
||||
end
|
||||
|
||||
@spec validate_email(map(), %{token: String.t()}, map()) ::
|
||||
{:ok, User.t()} | {:error, String.t()}
|
||||
def validate_email(_parent, %{token: token}, _resolution) do
|
||||
with {:get, %User{} = user} <- {:get, Users.get_user_by_activation_token(token)},
|
||||
{:ok, %User{} = user} <- Users.validate_email(user) do
|
||||
case Users.get_user_by_activation_token(token) do
|
||||
%User{} = user ->
|
||||
case Users.validate_email(user) do
|
||||
{:ok, %User{} = user} ->
|
||||
{:ok, user}
|
||||
else
|
||||
{:get, nil} ->
|
||||
|
||||
{:error, %Ecto.Changeset{} = err} ->
|
||||
Logger.debug(inspect(err))
|
||||
{:error, dgettext("errors", "Failed to validate user email")}
|
||||
end
|
||||
|
||||
nil ->
|
||||
{:error, dgettext("errors", "Invalid activation token")}
|
||||
end
|
||||
end
|
||||
|
||||
def delete_account(_parent, %{user_id: user_id}, %{
|
||||
context: %{current_user: %User{role: role} = moderator_user}
|
||||
context: %{
|
||||
current_user: %User{role: role},
|
||||
current_actor: %Actor{} = moderator_actor
|
||||
}
|
||||
})
|
||||
when is_moderator(role) do
|
||||
with {:moderator_actor, %Actor{} = moderator_actor} <-
|
||||
{:moderator_actor, Users.get_actor_for_user(moderator_user)},
|
||||
%User{disabled: false} = user <- Users.get_user(user_id),
|
||||
with %User{disabled: false} = user <- Users.get_user(user_id),
|
||||
{:ok, %User{}} <-
|
||||
do_delete_account(%User{} = user, actor_performing: Relay.get_actor()) do
|
||||
Admin.log_action(moderator_actor, "delete", user)
|
||||
else
|
||||
{:moderator_actor, nil} ->
|
||||
{:error, dgettext("errors", "No profile found for the moderator user")}
|
||||
|
||||
%User{disabled: true} ->
|
||||
{:error, dgettext("errors", "User already disabled")}
|
||||
end
|
||||
|
@ -488,7 +533,7 @@ defmodule Mobilizon.GraphQL.Resolvers.User do
|
|||
:ok <-
|
||||
Enum.each(actors, fn actor ->
|
||||
actor_performing = Keyword.get(options, :actor_performing, actor)
|
||||
ActivityPub.delete(actor, actor_performing, true)
|
||||
Actions.Delete.delete(actor, actor_performing, true)
|
||||
end),
|
||||
# Delete user
|
||||
{:ok, user} <-
|
||||
|
@ -547,11 +592,16 @@ defmodule Mobilizon.GraphQL.Resolvers.User do
|
|||
def update_locale(_parent, %{locale: locale}, %{
|
||||
context: %{current_user: %User{locale: current_locale} = user}
|
||||
}) do
|
||||
with true <- current_locale != locale,
|
||||
{:ok, %User{} = updated_user} <- Users.update_user(user, %{locale: locale}) do
|
||||
if current_locale != locale do
|
||||
case Users.update_user(user, %{locale: locale}) do
|
||||
{:ok, %User{} = updated_user} ->
|
||||
{:ok, updated_user}
|
||||
|
||||
{:error, %Ecto.Changeset{} = err} ->
|
||||
Logger.debug(err)
|
||||
{:error, dgettext("errors", "Error while updating locale")}
|
||||
end
|
||||
else
|
||||
false ->
|
||||
{:ok, user}
|
||||
end
|
||||
end
|
||||
|
|
|
@ -4,10 +4,12 @@ defmodule Mobilizon.GraphQL.Resolvers.Users.ActivitySettings do
|
|||
"""
|
||||
|
||||
alias Mobilizon.Users
|
||||
alias Mobilizon.Users.User
|
||||
alias Mobilizon.Users.{ActivitySetting, User}
|
||||
|
||||
require Logger
|
||||
|
||||
@spec user_activity_settings(any(), map(), Absinthe.Resolution.t()) ::
|
||||
{:ok, list(ActivitySetting.t())} | {:error, :unauthenticated}
|
||||
def user_activity_settings(_parent, _args, %{context: %{current_user: %User{} = user}}) do
|
||||
{:ok, Users.activity_settings_for_user(user)}
|
||||
end
|
||||
|
@ -16,6 +18,8 @@ defmodule Mobilizon.GraphQL.Resolvers.Users.ActivitySettings do
|
|||
{:error, :unauthenticated}
|
||||
end
|
||||
|
||||
@spec upsert_user_activity_setting(any(), map(), Absinthe.Resolution.t()) ::
|
||||
{:ok, ActivitySetting.t()} | {:error, :unauthenticated}
|
||||
def upsert_user_activity_setting(_parent, args, %{context: %{current_user: %User{id: user_id}}}) do
|
||||
Users.create_activity_setting(Map.put(args, :user_id, user_id))
|
||||
end
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue