Merge branch 'dializer' into 'master'

Various typespec and compilation improvements

See merge request framasoft/mobilizon!1062
This commit is contained in:
Thomas Citharel 2021-09-29 15:11:43 +00:00
commit d8b64e9a19
339 changed files with 6499 additions and 4154 deletions

15
.doctor.exs Normal file
View 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
}

View file

@ -179,6 +179,8 @@ config :phoenix, :filter_parameters, ["password", "token"]
config :absinthe, schema: Mobilizon.GraphQL.Schema config :absinthe, schema: Mobilizon.GraphQL.Schema
config :absinthe, Absinthe.Logger, filter_variables: ["token", "password", "secret"] config :absinthe, Absinthe.Logger, filter_variables: ["token", "password", "secret"]
config :mobilizon, Mobilizon.Web.Gettext, one_module_per_locale: true
config :ex_cldr, config :ex_cldr,
default_locale: "en", default_locale: "en",
default_backend: Mobilizon.Cldr default_backend: Mobilizon.Cldr
@ -189,7 +191,8 @@ config :http_signatures,
config :mobilizon, :cldr, config :mobilizon, :cldr,
locales: [ locales: [
"fr", "fr",
"en" "en",
"ru"
] ]
config :mobilizon, :activitypub, config :mobilizon, :activitypub,

View file

@ -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.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 # Set a higher stacktrace during development. Avoid configuring such
# in production as building large stacktraces may be expensive. # in production as building large stacktraces may be expensive.
config :phoenix, :stacktrace_depth, 20 config :phoenix, :stacktrace_depth, 20

View file

@ -77,6 +77,8 @@ config :mobilizon, Mobilizon.Web.Auth.Guardian, secret_key: "some secret"
config :mobilizon, :activitypub, sign_object_fetches: false config :mobilizon, :activitypub, sign_object_fetches: false
config :mobilizon, Mobilizon.Web.Gettext, allowed_locales: ["fr", "en", "es", "ru"]
config :junit_formatter, report_dir: "." config :junit_formatter, report_dir: "."
if System.get_env("DOCKER", "false") == "false" && File.exists?("./config/test.secret.exs") do if System.get_env("DOCKER", "false") == "false" && File.exists?("./config/test.secret.exs") do

View file

@ -29,19 +29,28 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import { Component, Prop, Vue } from "vue-property-decorator"; import { Component, Prop, Vue } from "vue-property-decorator";
import get from "lodash/get";
import differenceBy from "lodash/differenceBy"; import differenceBy from "lodash/differenceBy";
import { ITag } from "../../types/tag.model"; 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 { export default class TagInput extends Vue {
@Prop({ required: false, default: () => [] }) data!: ITag[];
@Prop({ required: true, default: "value" }) path!: string;
@Prop({ required: true }) value!: ITag[]; @Prop({ required: true }) value!: ITag[];
filteredTags: ITag[] = []; tags!: ITag[];
text = "";
private static componentId = 0; private static componentId = 0;
@ -53,13 +62,20 @@ export default class TagInput extends Vue {
return `tag-input-${TagInput.componentId}`; return `tag-input-${TagInput.componentId}`;
} }
getFilteredTags(text: string): void { async getFilteredTags(text: string): Promise<void> {
this.filteredTags = differenceBy(this.data, this.value, "id").filter( this.text = text;
await this.$apollo.queries.tags.refetch();
}
get filteredTags(): ITag[] {
return differenceBy(this.tags, this.value, "id").filter(
(option) => (option) =>
get(option, this.path) option.title
.toString() .toString()
.toLowerCase() .toLowerCase()
.indexOf(text.toLowerCase()) >= 0 .indexOf(this.text.toLowerCase()) >= 0 ||
option.slug.toString().toLowerCase().indexOf(this.text.toLowerCase()) >=
0
); );
} }

View file

@ -9,16 +9,22 @@ export const TAG_FRAGMENT = gql`
`; `;
export const TAGS = gql` export const TAGS = gql`
query { query Tags {
tags { tags {
id
related { related {
id ...TagFragment
slug
title
} }
slug ...TagFragment
title
} }
} }
${TAG_FRAGMENT}
`;
export const FILTER_TAGS = gql`
query FilterTags($filter: String) {
tags(filter: $filter) {
...TagFragment
}
}
${TAG_FRAGMENT}
`; `;

View file

@ -21,7 +21,6 @@
"A discussion has been created or updated": "Se ha creado o actualizado una discusión", "A discussion has been created or updated": "Se ha creado o actualizado una discusión",
"A federated software": "Un software federado", "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 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 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 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", "A member has been updated": "Un miembro ha sido actualizado",

View file

@ -19,7 +19,6 @@
"A discussion has been created or updated": "Une discussion a été créée ou mise à jour", "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 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 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 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 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", "A member has been updated": "Un membre a été mis à jour",

View file

@ -31,7 +31,7 @@
/> />
</b-field> </b-field>
<tag-input v-model="event.tags" :data="tags" path="title" /> <tag-input v-model="event.tags" />
<b-field <b-field
horizontal horizontal
@ -556,8 +556,6 @@ import {
IPerson, IPerson,
usernameWithDomain, usernameWithDomain,
} from "../../types/actor"; } from "../../types/actor";
import { TAGS } from "../../graphql/tags";
import { ITag } from "../../types/tag.model";
import { import {
buildFileFromIMedia, buildFileFromIMedia,
buildFileVariable, buildFileVariable,
@ -590,7 +588,6 @@ const DEFAULT_LIMIT_NUMBER_OF_PLACES = 10;
}, },
apollo: { apollo: {
currentActor: CURRENT_ACTOR_CLIENT, currentActor: CURRENT_ACTOR_CLIENT,
tags: TAGS,
config: CONFIG, config: CONFIG,
identities: IDENTITIES, identities: IDENTITIES,
event: { event: {
@ -643,8 +640,6 @@ export default class EditEvent extends Vue {
currentActor!: IActor; currentActor!: IActor;
tags: ITag[] = [];
event: IEvent = new EventModel(); event: IEvent = new EventModel();
unmodifiedEvent: IEvent = new EventModel(); unmodifiedEvent: IEvent = new EventModel();

View file

@ -67,7 +67,7 @@
/> />
</b-field> </b-field>
<tag-input v-model="editablePost.tags" :data="tags" path="title" /> <tag-input v-model="editablePost.tags" />
<div class="field"> <div class="field">
<label class="label">{{ $t("Post") }}</label> <label class="label">{{ $t("Post") }}</label>
@ -166,7 +166,6 @@ import {
} from "@/utils/image"; } from "@/utils/image";
import GroupMixin from "@/mixins/group"; import GroupMixin from "@/mixins/group";
import { PostVisibility } from "@/types/enums"; import { PostVisibility } from "@/types/enums";
import { TAGS } from "../../graphql/tags";
import { CONFIG } from "../../graphql/config"; import { CONFIG } from "../../graphql/config";
import { import {
FETCH_POST, FETCH_POST,
@ -187,7 +186,6 @@ import { FETCH_GROUP } from "@/graphql/group";
@Component({ @Component({
apollo: { apollo: {
tags: TAGS,
config: CONFIG, config: CONFIG,
group: { group: {
query: FETCH_GROUP, query: FETCH_GROUP,

View file

@ -320,6 +320,8 @@ export default class AccountSettings extends Vue {
}, },
}); });
this.oldPassword = "";
this.newPassword = "";
this.$notifier.success( this.$notifier.success(
this.$t("The password was successfully changed") as string this.$t("The password was successfully changed") as string
); );

View file

@ -4,8 +4,10 @@ defmodule Mobilizon.ConfigProvider do
""" """
@behaviour Config.Provider @behaviour Config.Provider
@spec init(String.t()) :: String.t()
def init(path) when is_binary(path), do: path def init(path) when is_binary(path), do: path
@spec load(Keyword.t(), String.t()) :: Keyword.t()
def load(config, path) do def load(config, path) do
config_path = System.get_env("MOBILIZON_CONFIG_PATH") || path config_path = System.get_env("MOBILIZON_CONFIG_PATH") || path

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View file

@ -4,7 +4,7 @@ defmodule Mobilizon.Federation.ActivityPub.Activity do
""" """
@type t :: %__MODULE__{ @type t :: %__MODULE__{
data: String.t(), data: map(),
local: boolean, local: boolean,
actor: Actor.t(), actor: Actor.t(),
recipients: [String.t()] recipients: [String.t()]

View file

@ -12,77 +12,45 @@ defmodule Mobilizon.Federation.ActivityPub do
alias Mobilizon.{ alias Mobilizon.{
Actors, Actors,
Config,
Discussions, Discussions,
Events, Events,
Posts, Posts,
Resources, Resources
Share,
Users
} }
alias Mobilizon.Actors.{Actor, Follower, Member} alias Mobilizon.Actors.Actor
alias Mobilizon.Discussions.Comment alias Mobilizon.Discussions.Comment
alias Mobilizon.Events.{Event, Participant} alias Mobilizon.Events.Event
alias Mobilizon.Tombstone alias Mobilizon.Tombstone
alias Mobilizon.Federation.ActivityPub.{ alias Mobilizon.Federation.ActivityPub.{
Activity, Activity,
Audience,
Federator,
Fetcher, Fetcher,
Preloader, Preloader,
Refresher, Relay
Relay,
Transmogrifier,
Types,
Visibility
} }
alias Mobilizon.Federation.ActivityPub.Actor, as: ActivityPubActor
alias Mobilizon.Federation.ActivityPub.Types.{Managable, Ownable}
alias Mobilizon.Federation.ActivityStream.Convertible alias Mobilizon.Federation.ActivityStream.Convertible
alias Mobilizon.Federation.HTTPSignatures.Signature
alias Mobilizon.Service.Notifications.Scheduler
alias Mobilizon.Storage.Page alias Mobilizon.Storage.Page
alias Mobilizon.Web.Endpoint alias Mobilizon.Web.Endpoint
alias Mobilizon.Web.Email.{Admin, Group, Mailer}
require Logger require Logger
@public_ap_adress "https://www.w3.org/ns/activitystreams#Public" @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 """ @doc """
Fetch an object from an URL, from our local database of events and comments, then eventually remote Fetch an object from an URL, from our local database of events and comments, then eventually remote
""" """
# TODO: Make database calls parallel # TODO: Make database calls parallel
@spec fetch_object_from_url(String.t(), Keyword.t()) :: @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 def fetch_object_from_url(url, options \\ []) do
Logger.info("Fetching object from url #{url}") Logger.info("Fetching object from url #{url}")
with {:not_http, true} <- {:not_http, String.starts_with?(url, "http")}, if String.starts_with?(url, "http") do
{:existing, nil} <- with {:existing, nil} <-
{:existing, Tombstone.find_tombstone(url)}, {:existing, Tombstone.find_tombstone(url)},
{:existing, nil} <- {:existing, Events.get_event_by_url(url)}, {:existing, nil} <- {:existing, Events.get_event_by_url(url)},
{:existing, nil} <- {:existing, nil} <-
@ -104,16 +72,15 @@ defmodule Mobilizon.Federation.ActivityPub do
{:error, e} -> {:error, e} ->
Logger.warn("Something failed while fetching url #{url} #{inspect(e)}") Logger.warn("Something failed while fetching url #{url} #{inspect(e)}")
{:error, e} {:error, e}
end
e -> else
Logger.warn("Something failed while fetching url #{url} #{inspect(e)}") {:error, :url_not_http}
{:error, e}
end end
end end
@spec handle_existing_entity(String.t(), struct(), Keyword.t()) :: @spec handle_existing_entity(String.t(), struct(), Keyword.t()) ::
{:ok, struct()} {:ok, struct()}
| {:ok, struct()} | {:ok, atom(), struct()}
| {:error, String.t(), struct()} | {:error, String.t(), struct()}
| {:error, String.t()} | {:error, String.t()}
defp handle_existing_entity(url, entity, options) do defp handle_existing_entity(url, entity, options) do
@ -128,13 +95,13 @@ defmodule Mobilizon.Federation.ActivityPub do
{:ok, entity} = Preloader.maybe_preload(entity) {:ok, entity} = Preloader.maybe_preload(entity)
{:error, status, entity} {:error, status, entity}
err -> {:error, err} ->
err {:error, err}
end end
end end
@spec refresh_entity(String.t(), struct(), Keyword.t()) :: @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 defp refresh_entity(url, entity, options) do
force_fetch = Keyword.get(options, :force, false) force_fetch = Keyword.get(options, :force, false)
@ -145,573 +112,24 @@ defmodule Mobilizon.Federation.ActivityPub do
{:ok, _activity, entity} -> {:ok, _activity, entity} ->
{:ok, entity} {:ok, entity}
{:error, "Gone"} -> {:error, :http_gone} ->
{:error, "Gone", entity} {:error, :http_gone, entity}
{:error, "Not found"} -> {:error, :http_not_found} ->
{:error, "Not found", entity} {:error, :http_not_found, entity}
{:error, "Object origin check failed"} -> {:error, err} ->
{:error, "Object origin check failed"} {:error, err}
end end
else else
{:ok, entity} {:ok, entity}
end end
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 """ @doc """
Return all public activities (events & comments) for an actor 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 def fetch_public_activities_for_actor(%Actor{id: actor_id} = actor, page \\ 1, limit \\ 10) do
%Actor{id: relay_actor_id} = Relay.get_actor() %Actor{id: relay_actor_id} = Relay.get_actor()
@ -758,217 +176,4 @@ defmodule Mobilizon.Federation.ActivityPub do
local: local local: local
} }
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
@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 end

View file

@ -14,65 +14,57 @@ defmodule Mobilizon.Federation.ActivityPub.Actor do
@doc """ @doc """
Getting an actor from url, eventually creating it if we don't have it locally or if it needs an update 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(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 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) get_or_fetch_actor_by_url(url)
end 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 def get_or_fetch_actor_by_url(url, preload) do
with {:ok, %Actor{} = cached_actor} <- Actors.get_actor_by_url(url, preload), case Actors.get_actor_by_url(url, preload) do
false <- Actors.needs_update?(cached_actor) do {:ok, %Actor{} = cached_actor} ->
{:ok, cached_actor} if Actors.needs_update?(cached_actor) do
__MODULE__.make_actor_from_url(url, preload)
else else
_ -> {:ok, cached_actor}
# For tests, see https://github.com/jjh42/mock#not-supported---mocking-internal-function-calls and Mobilizon.Federation.ActivityPubTest end
case __MODULE__.make_actor_from_url(url, preload) do
{:ok, %Actor{} = actor} ->
{:ok, actor}
{:error, err} -> {:error, :actor_not_found} ->
Logger.debug("Could not fetch by AP id") # For tests, see https://github.com/jjh42/mock#not-supported---mocking-internal-function-calls and Mobilizon.Federation.ActivityPubTest
Logger.debug(inspect(err)) __MODULE__.make_actor_from_url(url, preload)
{:error, "Could not fetch by AP id"}
end
end end
end end
@type make_actor_errors :: Fetcher.fetch_actor_errors() | :actor_is_local
@doc """ @doc """
Create an actor locally by its URL (AP ID) Create an actor locally by its URL (AP ID)
""" """
@spec make_actor_from_url(String.t(), boolean()) :: @spec make_actor_from_url(url :: String.t(), preload :: boolean()) ::
{:ok, %Actor{}} | {:error, :actor_deleted} | {:error, :http_error} | {:error, any()} {:ok, Actor.t()} | {:error, make_actor_errors}
def make_actor_from_url(url, preload \\ false) do def make_actor_from_url(url, preload \\ false) do
if are_same_origin?(url, Endpoint.url()) do if are_same_origin?(url, Endpoint.url()) do
{:error, "Can't make a local actor from URL"} {:error, :actor_is_local}
else else
case Fetcher.fetch_and_prepare_actor_from_url(url) do case Fetcher.fetch_and_prepare_actor_from_url(url) do
# Just in case {:ok, data} when is_map(data) ->
{:ok, {:error, _e}} ->
raise ArgumentError, message: "Failed to make actor from url #{url}"
{:ok, data} ->
Actors.upsert_actor(data, preload) Actors.upsert_actor(data, preload)
# Request returned 410 # Request returned 410
{:error, :actor_deleted} -> {:error, :actor_deleted} ->
Logger.info("Actor was deleted") Logger.info("Actor #{url} was deleted")
{:error, :actor_deleted} {:error, :actor_deleted}
{:error, :http_error} -> {:error, err} when err in [:http_error, :json_decode_error] ->
{:error, :http_error} {:error, err}
{:error, e} ->
Logger.warn("Failed to make actor from url #{url}")
{:error, e}
end end
end end
end end
@ -80,8 +72,8 @@ defmodule Mobilizon.Federation.ActivityPub.Actor do
@doc """ @doc """
Find an actor in our local database or call WebFinger to find what's its AP ID is and then fetch it 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) :: @spec find_or_make_actor_from_nickname(nickname :: String.t(), type :: atom() | nil) ::
{:ok, Actor.t()} | {:error, any()} {:ok, Actor.t()} | {:error, make_actor_errors | WebFinger.finger_errors()}
def find_or_make_actor_from_nickname(nickname, type \\ nil) do def find_or_make_actor_from_nickname(nickname, type \\ nil) do
case Actors.get_actor_by_name_with_preload(nickname, type) do case Actors.get_actor_by_name_with_preload(nickname, type) do
%Actor{url: actor_url} = actor -> %Actor{url: actor_url} = actor ->
@ -96,20 +88,22 @@ defmodule Mobilizon.Federation.ActivityPub.Actor do
end end
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) def find_or_make_group_from_nickname(nick), do: find_or_make_actor_from_nickname(nick, :Group)
@doc """ @doc """
Create an actor inside our database from username, using WebFinger to find out its AP ID and then fetch it 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 def make_actor_from_nickname(nickname, preload \\ false) do
case WebFinger.finger(nickname) do case WebFinger.finger(nickname) do
{:ok, url} when is_binary(url) -> {:ok, url} when is_binary(url) ->
make_actor_from_url(url, preload) make_actor_from_url(url, preload)
_e -> {:error, e} ->
{:error, "No ActivityPub URL found in WebFinger"} {:error, e}
end end
end end
end end

View file

@ -7,6 +7,7 @@ defmodule Mobilizon.Federation.ActivityPub.Audience do
alias Mobilizon.Actors.{Actor, Member} alias Mobilizon.Actors.{Actor, Member}
alias Mobilizon.Discussions.{Comment, Discussion} alias Mobilizon.Discussions.{Comment, Discussion}
alias Mobilizon.Events.{Event, Participant} alias Mobilizon.Events.{Event, Participant}
alias Mobilizon.Federation.ActivityPub.Types.Entity
alias Mobilizon.Posts.Post alias Mobilizon.Posts.Post
alias Mobilizon.Storage.Repo alias Mobilizon.Storage.Repo
@ -99,6 +100,8 @@ defmodule Mobilizon.Federation.ActivityPub.Audience do
} }
end end
@spec get_to_and_cc(Actor.t(), list(), :direct | :private | :public | :unlisted | {:list, any}) ::
{list(), list()}
@doc """ @doc """
Determines the full audience based on mentions for an audience 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 * `to` : the mentioned actors and the eventual actor we're replying to
* `cc` : none * `cc` : none
""" """
@spec get_to_and_cc(Actor.t(), list(), String.t()) :: {list(), list()}
def get_to_and_cc(%Actor{} = actor, mentions, :public) do def get_to_and_cc(%Actor{} = actor, mentions, :public) do
to = [@ap_public | mentions] to = [@ap_public | mentions]
cc = [actor.followers_url] cc = [actor.followers_url]
@ -128,7 +130,6 @@ defmodule Mobilizon.Federation.ActivityPub.Audience do
{to, cc} {to, cc}
end end
@spec get_to_and_cc(Actor.t(), list(), String.t()) :: {list(), list()}
def get_to_and_cc(%Actor{} = actor, mentions, :unlisted) do def get_to_and_cc(%Actor{} = actor, mentions, :unlisted) do
to = [actor.followers_url | mentions] to = [actor.followers_url | mentions]
cc = [@ap_public] cc = [@ap_public]
@ -138,7 +139,6 @@ defmodule Mobilizon.Federation.ActivityPub.Audience do
{to, cc} {to, cc}
end end
@spec get_to_and_cc(Actor.t(), list(), String.t()) :: {list(), list()}
def get_to_and_cc(%Actor{} = actor, mentions, :private) do def get_to_and_cc(%Actor{} = actor, mentions, :private) do
{to, cc} = get_to_and_cc(actor, mentions, :direct) {to, cc} = get_to_and_cc(actor, mentions, :direct)
@ -147,7 +147,6 @@ defmodule Mobilizon.Federation.ActivityPub.Audience do
{to, cc} {to, cc}
end end
@spec get_to_and_cc(Actor.t(), list(), String.t()) :: {list(), list()}
def get_to_and_cc(_actor, mentions, :direct) do def get_to_and_cc(_actor, mentions, :direct) do
{mentions, []} {mentions, []}
end end
@ -156,22 +155,20 @@ defmodule Mobilizon.Federation.ActivityPub.Audience do
{mentions, []} {mentions, []}
end 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 defp maybe_add_group_members(collection, %Actor{type: :Group, members_url: members_url}) do
[members_url | collection] [members_url | collection]
end end
defp maybe_add_group_members(collection, %Actor{type: _}), do: collection 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 defp maybe_add_followers(collection, %Actor{type: :Group, followers_url: followers_url}) do
[followers_url | collection] [followers_url | collection]
end end
defp maybe_add_followers(collection, %Actor{type: _}), do: collection 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(%Comment{actor: %Actor{url: url}} = _comment), do: [url]
defp add_in_reply_to(%Event{organizer_actor: %Actor{url: url}} = _event), do: [url] defp add_in_reply_to(%Event{organizer_actor: %Actor{url: url}} = _event), do: [url]
defp add_in_reply_to(_), do: [] defp add_in_reply_to(_), do: []
@ -239,29 +236,27 @@ defmodule Mobilizon.Federation.ActivityPub.Audience do
@spec extract_actors_from_mentions(list(), Actor.t(), atom()) :: {list(), list()} @spec extract_actors_from_mentions(list(), Actor.t(), atom()) :: {list(), list()}
defp extract_actors_from_mentions(mentions, actor, visibility) do defp extract_actors_from_mentions(mentions, actor, visibility) do
with mentioned_actors <- Enum.map(mentions, &process_mention/1), get_to_and_cc(actor, Enum.map(mentions, &process_mention/1), visibility)
addressed_actors <- get_addressed_actors(mentioned_actors, nil) do
get_to_and_cc(actor, addressed_actors, visibility)
end
end end
@spec extract_actors_from_event(Event.t()) :: %{
String.t() => list(String.t())
}
defp extract_actors_from_event(%Event{} = event) do defp extract_actors_from_event(%Event{} = event) do
with {to, cc} <- {to, cc} =
extract_actors_from_mentions( extract_actors_from_mentions(
event.mentions, event.mentions,
group_or_organizer_event(event), group_or_organizer_event(event),
event.visibility event.visibility
), )
{to, cc} <-
{to, cc} =
{to, {to,
Enum.uniq( Enum.uniq(
cc ++ add_comments_authors(event.comments) ++ add_shares_actors_followers(event.url) cc ++ add_comments_authors(event.comments) ++ add_shares_actors_followers(event.url)
)} do )}
%{"to" => to, "cc" => cc} %{"to" => to, "cc" => cc}
else
_ ->
%{"to" => [], "cc" => []}
end
end end
@spec group_or_organizer_event(Event.t()) :: Actor.t() @spec group_or_organizer_event(Event.t()) :: Actor.t()

View file

@ -12,17 +12,19 @@ defmodule Mobilizon.Federation.ActivityPub.Federator do
alias Mobilizon.Actors.Actor alias Mobilizon.Actors.Actor
alias Mobilizon.Federation.ActivityPub alias Mobilizon.Federation.ActivityPub
alias Mobilizon.Federation.ActivityPub.{Activity, Transmogrifier}
alias Mobilizon.Federation.ActivityPub.Actor, as: ActivityPubActor alias Mobilizon.Federation.ActivityPub.Actor, as: ActivityPubActor
alias Mobilizon.Federation.ActivityPub.Transmogrifier
require Logger require Logger
@max_jobs 20 @max_jobs 20
@spec init(any()) :: {:ok, any()}
def init(args) do def init(args) do
{:ok, args} {:ok, args}
end end
@spec start_link(any) :: GenServer.on_start()
def start_link(_) do def start_link(_) do
spawn(fn -> spawn(fn ->
# 1 minute # 1 minute
@ -39,6 +41,8 @@ defmodule Mobilizon.Federation.ActivityPub.Federator do
) )
end 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 def handle(:publish, activity) do
Logger.debug(inspect(activity)) Logger.debug(inspect(activity))
Logger.debug(fn -> "Running publish for #{activity.data["id"]}" end) Logger.debug(fn -> "Running publish for #{activity.data["id"]}" end)
@ -46,7 +50,7 @@ defmodule Mobilizon.Federation.ActivityPub.Federator do
with {:ok, %Actor{} = actor} <- with {:ok, %Actor{} = actor} <-
ActivityPubActor.get_or_fetch_actor_by_url(activity.data["actor"]) do ActivityPubActor.get_or_fetch_actor_by_url(activity.data["actor"]) do
Logger.info(fn -> "Sending #{activity.data["id"]} out via AP" end) Logger.info(fn -> "Sending #{activity.data["id"]} out via AP" end)
ActivityPub.publish(actor, activity) ActivityPub.Publisher.publish(actor, activity)
end end
end end
@ -58,9 +62,6 @@ defmodule Mobilizon.Federation.ActivityPub.Federator do
{:ok, activity, _data} -> {:ok, activity, _data} ->
{:ok, activity} {:ok, activity}
%Activity{} ->
Logger.info("Already had #{params["id"]}")
e -> e ->
# Just drop those for now # Just drop those for now
Logger.debug("Unhandled activity") Logger.debug("Unhandled activity")
@ -70,7 +71,7 @@ defmodule Mobilizon.Federation.ActivityPub.Federator do
end end
def handle(:publish_single_ap, params) do def handle(:publish_single_ap, params) do
ActivityPub.publish_one(params) ActivityPub.Publisher.publish_one(params)
end end
def handle(type, _) do def handle(type, _) do
@ -78,6 +79,7 @@ defmodule Mobilizon.Federation.ActivityPub.Federator do
{:error, "Don't know what to do with this"} {:error, "Don't know what to do with this"}
end end
@spec enqueue(atom(), map(), pos_integer()) :: :ok | {:ok, any()} | {:error, any()}
def enqueue(type, payload, priority \\ 1) do def enqueue(type, payload, priority \\ 1) do
Logger.debug("enqueue something with type #{inspect(type)}") Logger.debug("enqueue something with type #{inspect(type)}")
@ -88,6 +90,7 @@ defmodule Mobilizon.Federation.ActivityPub.Federator do
end end
end end
@spec maybe_start_job(any(), any()) :: {any(), any()}
def maybe_start_job(running_jobs, queue) do def maybe_start_job(running_jobs, queue) do
if :sets.size(running_jobs) < @max_jobs && queue != [] do if :sets.size(running_jobs) < @max_jobs && queue != [] do
{{type, payload}, queue} = queue_pop(queue) {{type, payload}, queue} = queue_pop(queue)
@ -99,6 +102,7 @@ defmodule Mobilizon.Federation.ActivityPub.Federator do
end end
end end
@spec handle_cast(any(), any()) :: {:noreply, any()}
def handle_cast({:enqueue, type, payload, _priority}, state) def handle_cast({:enqueue, type, payload, _priority}, state)
when type in [:incoming_doc, :incoming_ap_doc] do when type in [:incoming_doc, :incoming_ap_doc] do
%{in: {i_running_jobs, i_queue}, out: {o_running_jobs, o_queue}} = state %{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} {:noreply, state}
end end
@spec handle_info({:DOWN, any(), :process, any, any()}, any) :: {:noreply, map()}
def handle_info({:DOWN, ref, :process, _pid, _reason}, state) do def handle_info({:DOWN, ref, :process, _pid, _reason}, state) do
%{in: {i_running_jobs, i_queue}, out: {o_running_jobs, o_queue}} = state %{in: {i_running_jobs, i_queue}, out: {o_running_jobs, o_queue}} = state
i_running_jobs = :sets.del_element(ref, i_running_jobs) 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}}} {:noreply, %{in: {i_running_jobs, i_queue}, out: {o_running_jobs, o_queue}}}
end end
@spec enqueue_sorted(any(), any(), pos_integer()) :: any()
def enqueue_sorted(queue, element, priority) do def enqueue_sorted(queue, element, priority) do
[%{item: element, priority: priority} | queue] [%{item: element, priority: priority} | queue]
|> Enum.sort_by(fn %{priority: priority} -> priority end) |> Enum.sort_by(fn %{priority: priority} -> priority end)
end end
@spec queue_pop(list(any())) :: {any(), list(any())}
def queue_pop([%{item: element} | queue]) do def queue_pop([%{item: element} | queue]) do
{element, queue} {element, queue}
end end

View file

@ -15,107 +15,141 @@ defmodule Mobilizon.Federation.ActivityPub.Fetcher do
import Mobilizon.Federation.ActivityPub.Utils, import Mobilizon.Federation.ActivityPub.Utils,
only: [maybe_date_fetch: 2, sign_fetch: 4, origin_check?: 2] 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 def fetch(url, options \\ []) do
on_behalf_of = Keyword.get(options, :on_behalf_of, Relay.get_actor()) on_behalf_of = Keyword.get(options, :on_behalf_of, Relay.get_actor())
date = Signature.generate_date_header()
with false <- address_invalid(url), headers =
date <- Signature.generate_date_header(),
headers <-
[{:Accept, "application/activity+json"}] [{:Accept, "application/activity+json"}]
|> maybe_date_fetch(date) |> maybe_date_fetch(date)
|> sign_fetch(on_behalf_of, url, date), |> sign_fetch(on_behalf_of, url, date)
client <-
ActivityPubClient.client(headers: headers), client = ActivityPubClient.client(headers: headers)
{:ok, %Tesla.Env{body: data, status: code}} when code in 200..299 <-
ActivityPubClient.get(client, url) do 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} {:ok, data}
else
{:ok, %Tesla.Env{status: 410}} -> {:ok, %Tesla.Env{status: 410}} ->
Logger.debug("Resource at #{url} is 410 Gone") Logger.debug("Resource at #{url} is 410 Gone")
{:error, "Gone"} {:error, :http_gone}
{:ok, %Tesla.Env{status: 404}} -> {:ok, %Tesla.Env{status: 404}} ->
Logger.debug("Resource at #{url} is 404 Gone") 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} -> {:ok, %Tesla.Env{} = res} ->
{:error, res} Logger.debug("Resource returned bad HTTP code inspect #{res}")
{:error, :http_error}
{:error, err} -> end
{:error, err} else
{:error, :invalid_url}
end end
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 def fetch_and_create(url, options \\ []) do
with {:ok, data} when is_map(data) <- fetch(url, options), case fetch(url, options) do
{:origin_check, true} <- {:origin_check, origin_check?(url, data)}, {:ok, data} when is_map(data) ->
params <- %{ if origin_check?(url, data) do
case Transmogrifier.handle_incoming(%{
"type" => "Create", "type" => "Create",
"to" => data["to"], "to" => data["to"],
"cc" => data["cc"], "cc" => data["cc"],
"actor" => data["actor"] || data["attributedTo"], "actor" => data["actor"] || data["attributedTo"],
"attributedTo" => data["attributedTo"] || data["actor"], "attributedTo" => data["attributedTo"] || data["actor"],
"object" => data "object" => data
} do }) do
Transmogrifier.handle_incoming(params) {:ok, entity, structure} ->
else {:ok, entity, structure}
{:origin_check, false} ->
Logger.warn("Object origin check failed")
{:error, "Object origin check failed"}
# Returned content is not JSON {:error, error} when is_atom(error) ->
{:ok, data} when is_binary(data) -> {:error, error}
{:error, "Failed to parse content as JSON"}
:error ->
{:error, :transmogrifier_error}
end
else
Logger.warn("Object origin check failed")
{:error, :object_origin_check_failed}
end
{:error, err} -> {:error, err} ->
{:error, err} {:error, err}
end end
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 def fetch_and_update(url, options \\ []) do
with {:ok, data} when is_map(data) <- fetch(url, options), case fetch(url, options) do
{:origin_check, true} <- {:origin_check, origin_check(url, data)}, {:ok, data} when is_map(data) ->
params <- %{ if origin_check(url, data) do
Transmogrifier.handle_incoming(%{
"type" => "Update", "type" => "Update",
"to" => data["to"], "to" => data["to"],
"cc" => data["cc"], "cc" => data["cc"],
"actor" => data["actor"] || data["attributedTo"], "actor" => data["actor"] || data["attributedTo"],
"attributedTo" => data["attributedTo"] || data["actor"], "attributedTo" => data["attributedTo"] || data["actor"],
"object" => data "object" => data
} do })
Transmogrifier.handle_incoming(params)
else else
{:origin_check, false} -> Logger.warn("Object origin check failed")
{:error, "Object origin check failed"} {:error, :object_origin_check_failed}
end
{:error, err} -> {:error, err} ->
{:error, err} {:error, err}
end end
end end
@type fetch_actor_errors ::
:json_decode_error | :actor_deleted | :http_error | :actor_not_allowed_type
@doc """ @doc """
Fetching a remote actor's information through its AP ID 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 def fetch_and_prepare_actor_from_url(url) do
Logger.debug("Fetching and preparing actor from url") Logger.debug("Fetching and preparing actor from url")
Logger.debug(inspect(url)) Logger.debug(inspect(url))
res = case Tesla.get(url,
with {:ok, %{status: 200, body: body}} <-
Tesla.get(url,
headers: [{"Accept", "application/activity+json"}], headers: [{"Accept", "application/activity+json"}],
follow_redirect: true follow_redirect: true
), ) do
:ok <- Logger.debug("response okay, now decoding json"), {:ok, %{status: 200, body: body}} ->
{:ok, data} <- Jason.decode(body) do 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") Logger.debug("Got activity+json response at actor's endpoint, now converting data")
{:ok, ActorConverter.as_to_model_data(data)}
else case ActorConverter.as_to_model_data(data) do
# Actor is gone, probably deleted {: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}} -> {:ok, %{status: 410}} ->
Logger.info("Response HTTP 410") Logger.info("Response HTTP 410")
{:error, :actor_deleted} {:error, :actor_deleted}
@ -124,16 +158,10 @@ defmodule Mobilizon.Federation.ActivityPub.Fetcher do
Logger.info("Non 200 HTTP Code") Logger.info("Non 200 HTTP Code")
{:error, :http_error} {:error, :http_error}
{:error, e} -> {:error, error} ->
Logger.warn("Could not decode actor at fetch #{url}, #{inspect(e)}") Logger.warn("Could not fetch actor at fetch #{url}, #{inspect(error)}")
{:error, e} {:error, :http_error}
e ->
Logger.warn("Could not decode actor at fetch #{url}, #{inspect(e)}")
{:error, e}
end end
res
end end
@spec origin_check(String.t(), map()) :: boolean() @spec origin_check(String.t(), map()) :: boolean()
@ -147,11 +175,9 @@ defmodule Mobilizon.Federation.ActivityPub.Fetcher do
end end
end end
@spec address_invalid(String.t()) :: false | {:error, :invalid_url} @spec address_valid?(String.t()) :: boolean
defp address_invalid(address) do defp address_valid?(address) do
with %URI{host: host, scheme: scheme} <- URI.parse(address), %URI{host: host, scheme: scheme} = URI.parse(address)
true <- is_nil(host) or is_nil(scheme) do is_valid_string(host) and is_valid_string(scheme)
{:error, :invalid_url}
end
end end
end end

View file

@ -13,6 +13,17 @@ defmodule Mobilizon.Federation.ActivityPub.Permission do
@member_roles [:member, :moderator, :administrator] @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 """ @doc """
Check that actor can access the object Check that actor can access the object
""" """
@ -66,8 +77,8 @@ defmodule Mobilizon.Federation.ActivityPub.Permission do
@spec can_manage_group_object?( @spec can_manage_group_object?(
existing_object_permissions(), existing_object_permissions(),
Actor.t(), %Actor{url: String.t()},
any() object()
) :: boolean() ) :: boolean()
defp can_manage_group_object?(permission, %Actor{url: actor_url} = actor, object) do defp can_manage_group_object?(permission, %Actor{url: actor_url} = actor, object) do
if Ownable.group_actor(object) != nil do if Ownable.group_actor(object) != nil do
@ -94,7 +105,7 @@ defmodule Mobilizon.Federation.ActivityPub.Permission do
end end
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?( defp activity_actor_is_group_member?(
%Actor{id: actor_id, url: actor_url}, %Actor{id: actor_id, url: actor_url},
object, object,

View 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

View file

@ -8,13 +8,12 @@ defmodule Mobilizon.Federation.ActivityPub.Refresher do
alias Mobilizon.Federation.ActivityPub alias Mobilizon.Federation.ActivityPub
alias Mobilizon.Federation.ActivityPub.Actor, as: ActivityPubActor alias Mobilizon.Federation.ActivityPub.Actor, as: ActivityPubActor
alias Mobilizon.Federation.ActivityPub.{Fetcher, Relay, Transmogrifier, Utils} alias Mobilizon.Federation.ActivityPub.{Fetcher, Relay, Transmogrifier, Utils}
alias Mobilizon.Service.ErrorReporting.Sentry
require Logger require Logger
@doc """ @doc """
Refresh a remote profile 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{domain: nil}), do: {:error, "Can only refresh remote actors"}
def refresh_profile(%Actor{type: :Group, url: url, id: group_id} = group) do 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() Relay.get_actor()
end 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} {:ok, group}
end end
end end
def refresh_profile(%Actor{type: type, url: url}) when type in [:Person, :Application] do def refresh_profile(%Actor{type: type, url: url}) when type in [:Person, :Application] do
with {:ok, %Actor{outbox_url: outbox_url} = actor} <- case ActivityPubActor.make_actor_from_url(url) do
ActivityPubActor.make_actor_from_url(url), {:error, error} ->
:ok <- fetch_collection(outbox_url, Relay.get_actor()) do {:error, error}
{:ok, actor}
{: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
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 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{ %Actor{
outbox_url: outbox_url, outbox_url: outbox_url,
resources_url: resources_url, resources_url: resources_url,
@ -51,9 +67,10 @@ defmodule Mobilizon.Federation.ActivityPub.Refresher do
todos_url: todos_url, todos_url: todos_url,
discussions_url: discussions_url, discussions_url: discussions_url,
events_url: events_url events_url: events_url
}} <- }} ->
ActivityPubActor.make_actor_from_url(group_url), Logger.debug("Fetched group OK, now doing collections")
:ok <- fetch_collection(outbox_url, on_behalf_of),
with :ok <- fetch_collection(outbox_url, on_behalf_of),
:ok <- fetch_collection(members_url, on_behalf_of), :ok <- fetch_collection(members_url, on_behalf_of),
:ok <- fetch_collection(resources_url, on_behalf_of), :ok <- fetch_collection(resources_url, on_behalf_of),
:ok <- fetch_collection(posts_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 <- fetch_collection(events_url, on_behalf_of) do
:ok :ok
else 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} {:error, err}
when err in [:error, :process_error, :fetch_error, :collection_url_nil] ->
err -> Logger.debug("Error while fetching actor collection")
Logger.error("Error while refreshing a group") {:error, err}
end
Sentry.capture_message("Error while refreshing a group",
extra: %{group_url: group_url}
)
Logger.debug(inspect(err))
err
end 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 def fetch_collection(collection_url, on_behalf_of) do
Logger.debug("Fetching and preparing collection from url") Logger.debug("Fetching and preparing collection from url")
Logger.debug(inspect(collection_url)) Logger.debug(inspect(collection_url))
with {:ok, data} <- Fetcher.fetch(collection_url, on_behalf_of: on_behalf_of), case Fetcher.fetch(collection_url, on_behalf_of: on_behalf_of) do
:ok <- Logger.debug("Fetch ok, passing to process_collection"), {:ok, data} when is_map(data) ->
:ok <- process_collection(data, on_behalf_of) do Logger.debug("Fetch ok, passing to process_collection")
case process_collection(data, on_behalf_of) do
:ok ->
Logger.debug("Finished processing a collection") Logger.debug("Finished processing a collection")
:ok :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
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 def fetch_element(url, %Actor{} = on_behalf_of) do
with {:ok, data} <- Fetcher.fetch(url, on_behalf_of: on_behalf_of) do with {:ok, data} <- Fetcher.fetch(url, on_behalf_of: on_behalf_of) do
case handling_element(data) do case handling_element(data) do
@ -114,6 +127,9 @@ defmodule Mobilizon.Federation.ActivityPub.Refresher do
{:ok, entity} -> {:ok, entity} ->
{:ok, entity} {:ok, entity}
:error ->
{:error, :err_fetching_element}
err -> err ->
{:error, err} {:error, err}
end end
@ -127,6 +143,7 @@ defmodule Mobilizon.Federation.ActivityPub.Refresher do
|> Enum.each(&refresh_profile/1) |> Enum.each(&refresh_profile/1)
end end
@spec process_collection(map(), any()) :: :ok | :error
defp process_collection(%{"type" => type, "orderedItems" => items}, _on_behalf_of) defp process_collection(%{"type" => type, "orderedItems" => items}, _on_behalf_of)
when type in ["OrderedCollection", "OrderedCollectionPage"] do when type in ["OrderedCollection", "OrderedCollectionPage"] do
Logger.debug( Logger.debug(
@ -168,6 +185,8 @@ defmodule Mobilizon.Federation.ActivityPub.Refresher do
defp process_collection(_, _), do: :error defp process_collection(_, _), do: :error
# If we're handling an activity # 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) defp handling_element(%{"type" => activity_type} = data)
when activity_type in ["Create", "Update", "Delete"] do when activity_type in ["Create", "Update", "Delete"] do
object = get_in(data, ["object"]) object = get_in(data, ["object"])

View file

@ -11,8 +11,7 @@ defmodule Mobilizon.Federation.ActivityPub.Relay do
alias Mobilizon.Actors alias Mobilizon.Actors
alias Mobilizon.Actors.{Actor, Follower} alias Mobilizon.Actors.{Actor, Follower}
alias Mobilizon.Federation.ActivityPub alias Mobilizon.Federation.ActivityPub.{Actions, Activity, Transmogrifier}
alias Mobilizon.Federation.ActivityPub.{Activity, Transmogrifier}
alias Mobilizon.Federation.ActivityPub.Actor, as: ActivityPubActor alias Mobilizon.Federation.ActivityPub.Actor, as: ActivityPubActor
alias Mobilizon.Federation.WebFinger alias Mobilizon.Federation.WebFinger
alias Mobilizon.Service.Workers.Background alias Mobilizon.Service.Workers.Background
@ -27,76 +26,100 @@ defmodule Mobilizon.Federation.ActivityPub.Relay do
get_actor() get_actor()
end end
@spec get_actor() :: Actor.t() | {:error, Ecto.Changeset.t()} @spec get_actor() :: Actor.t() | no_return
def get_actor do def get_actor do
with {:ok, %Actor{} = actor} <- case Actors.get_or_create_internal_actor("relay") do
Actors.get_or_create_internal_actor("relay") do {:ok, %Actor{} = actor} ->
actor actor
{:error, %Ecto.Changeset{} = _err} ->
raise("Relay actor not found")
end end
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 def follow(address) do
%Actor{} = local_actor = get_actor()
with {:ok, target_instance} <- fetch_actor(address), with {:ok, target_instance} <- fetch_actor(address),
%Actor{} = local_actor <- get_actor(),
{:ok, %Actor{} = target_actor} <- {:ok, %Actor{} = target_actor} <-
ActivityPubActor.get_or_fetch_actor_by_url(target_instance), ActivityPubActor.get_or_fetch_actor_by_url(target_instance),
{:ok, activity, follow} <- Follows.follow(local_actor, target_actor) do {:ok, activity, follow} <- Follows.follow(local_actor, target_actor) do
Logger.info("Relay: followed instance #{target_instance}; id=#{activity.data["id"]}") Logger.info("Relay: followed instance #{target_instance}; id=#{activity.data["id"]}")
{:ok, activity, follow} {:ok, activity, follow}
else else
{:error, :person_no_follow} ->
Logger.warn("Only group and instances can be followed")
{:error, :person_no_follow}
{:error, e} -> {:error, e} ->
Logger.warn("Error while following remote instance: #{inspect(e)}") Logger.warn("Error while following remote instance: #{inspect(e)}")
{:error, e} {:error, e}
e ->
Logger.warn("Error while following remote instance: #{inspect(e)}")
{:error, e}
end end
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 def unfollow(address) do
%Actor{} = local_actor = get_actor()
with {:ok, target_instance} <- fetch_actor(address), with {:ok, target_instance} <- fetch_actor(address),
%Actor{} = local_actor <- get_actor(),
{:ok, %Actor{} = target_actor} <- {:ok, %Actor{} = target_actor} <-
ActivityPubActor.get_or_fetch_actor_by_url(target_instance), ActivityPubActor.get_or_fetch_actor_by_url(target_instance),
{:ok, activity, follow} <- Follows.unfollow(local_actor, target_actor) do {:ok, activity, follow} <- Follows.unfollow(local_actor, target_actor) do
Logger.info("Relay: unfollowed instance #{target_instance}: id=#{activity.data["id"]}") Logger.info("Relay: unfollowed instance #{target_instance}: id=#{activity.data["id"]}")
{:ok, activity, follow} {:ok, activity, follow}
else else
e -> {:error, e} ->
Logger.warn("Error while unfollowing remote instance: #{inspect(e)}") Logger.warn("Error while unfollowing remote instance: #{inspect(e)}")
{:error, e} {:error, e}
end end
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 def accept(address) do
Logger.debug("We're trying to accept a relay subscription") Logger.debug("We're trying to accept a relay subscription")
%Actor{} = local_actor = get_actor()
with {:ok, target_instance} <- fetch_actor(address), with {:ok, target_instance} <- fetch_actor(address),
%Actor{} = local_actor <- get_actor(),
{:ok, %Actor{} = target_actor} <- {:ok, %Actor{} = target_actor} <-
ActivityPubActor.get_or_fetch_actor_by_url(target_instance), ActivityPubActor.get_or_fetch_actor_by_url(target_instance),
{:ok, activity, follow} <- Follows.accept(target_actor, local_actor) do {:ok, activity, follow} <- Follows.accept(target_actor, local_actor) do
{:ok, activity, follow} {:ok, activity, follow}
else
{:error, e} ->
Logger.warn("Error while accepting remote instance follow: #{inspect(e)}")
{:error, e}
end end
end end
@spec reject(String.t()) ::
{:ok, Activity.t(), Follower.t()} | {:error, atom()} | {:error, String.t()}
def reject(address) do def reject(address) do
Logger.debug("We're trying to reject a relay subscription") Logger.debug("We're trying to reject a relay subscription")
%Actor{} = local_actor = get_actor()
with {:ok, target_instance} <- fetch_actor(address), with {:ok, target_instance} <- fetch_actor(address),
%Actor{} = local_actor <- get_actor(),
{:ok, %Actor{} = target_actor} <- {:ok, %Actor{} = target_actor} <-
ActivityPubActor.get_or_fetch_actor_by_url(target_instance), ActivityPubActor.get_or_fetch_actor_by_url(target_instance),
{:ok, activity, follow} <- Follows.reject(target_actor, local_actor) do {:ok, activity, follow} <- Follows.reject(target_actor, local_actor) do
{:ok, activity, follow} {:ok, activity, follow}
else
{:error, e} ->
Logger.warn("Error while rejecting remote instance follow: #{inspect(e)}")
{:error, e}
end end
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 def refresh(address) do
Logger.debug("We're trying to refresh a remote instance") Logger.debug("We're trying to refresh a remote instance")
@ -106,6 +129,10 @@ defmodule Mobilizon.Federation.ActivityPub.Relay do
Background.enqueue("refresh_profile", %{ Background.enqueue("refresh_profile", %{
"actor_id" => target_actor_id "actor_id" => target_actor_id
}) })
else
{:error, e} ->
Logger.warn("Error while refreshing remote instance: #{inspect(e)}")
{:error, e}
end end
end end
@ -117,7 +144,7 @@ defmodule Mobilizon.Federation.ActivityPub.Relay do
{object, object_id} <- fetch_object(object), {object, object_id} <- fetch_object(object),
id <- "#{object_id}/announces/#{actor_id}" do id <- "#{object_id}/announces/#{actor_id}" do
Logger.info("Publishing activity #{id} to all relays") Logger.info("Publishing activity #{id} to all relays")
ActivityPub.announce(actor, object, id, true, false) Actions.Announce.announce(actor, object, id, true, false)
else else
e -> e ->
Logger.error("Error while getting local instance actor: #{inspect(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} 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 # Dirty hack
defp fetch_actor("https://" <> address), do: fetch_actor(address) defp fetch_actor("https://" <> address), do: fetch_actor(address)
defp fetch_actor("http://" <> 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}") check_actor("relay@#{host}")
true -> true ->
{:error, "Bad URL"} {:error, :bad_url}
end end
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 defp check_actor(username_and_domain) do
case Actors.get_actor_by_name(username_and_domain) do case Actors.get_actor_by_name(username_and_domain) do
%Actor{url: url} -> {:ok, url} %Actor{url: url} -> {:ok, url}
nil -> finger_actor(username_and_domain) nil -> WebFinger.finger(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"}
end end
end end
end end

View file

@ -17,7 +17,7 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier do
alias Mobilizon.Todos.{Todo, TodoList} alias Mobilizon.Todos.{Todo, TodoList}
alias Mobilizon.Federation.ActivityPub alias Mobilizon.Federation.ActivityPub
alias Mobilizon.Federation.ActivityPub.{Activity, Permission, Relay, Utils} alias Mobilizon.Federation.ActivityPub.{Actions, Activity, Permission, Relay, Utils}
alias Mobilizon.Federation.ActivityPub.Actor, as: ActivityPubActor alias Mobilizon.Federation.ActivityPub.Actor, as: ActivityPubActor
alias Mobilizon.Federation.ActivityPub.Types.Ownable alias Mobilizon.Federation.ActivityPub.Types.Ownable
alias Mobilizon.Federation.ActivityStream.{Converter, Convertible} alias Mobilizon.Federation.ActivityStream.{Converter, Convertible}
@ -32,6 +32,7 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier do
@doc """ @doc """
Handle incoming activities Handle incoming activities
""" """
@spec handle_incoming(map()) :: :error | {:ok, any(), struct()}
def handle_incoming(%{"id" => nil}), do: :error def handle_incoming(%{"id" => nil}), do: :error
def handle_incoming(%{"id" => ""}), do: :error def handle_incoming(%{"id" => ""}), do: :error
@ -49,7 +50,7 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier do
local: false local: false
} }
ActivityPub.flag(params, false) Actions.Flag.flag(params, false)
end end
end end
@ -76,17 +77,17 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier do
{:ok, %Activity{} = activity, entity} <- {:ok, %Activity{} = activity, entity} <-
(if is_data_for_comment_or_discussion?(object_data) do (if is_data_for_comment_or_discussion?(object_data) do
Logger.debug("Chosing to create a regular comment") Logger.debug("Chosing to create a regular comment")
ActivityPub.create(:comment, object_data, false) Actions.Create.create(:comment, object_data, false)
else else
Logger.debug("Chosing to initialize or add a comment to a conversation") 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 end) do
{:ok, activity, entity} {:ok, activity, entity}
else else
{:existing_comment, {:ok, %Comment{} = comment}} -> {:existing_comment, {:ok, %Comment{} = comment}} ->
{:ok, nil, 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") Logger.debug("Tried to reply to an event for which comments are closed")
:error :error
end end
@ -109,7 +110,7 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier do
object |> Converter.Event.as_to_model_data(), object |> Converter.Event.as_to_model_data(),
{:existing_event, nil} <- {:existing_event, Events.get_event_by_url(object_data.url)}, {:existing_event, nil} <- {:existing_event, Events.get_event_by_url(object_data.url)},
{:ok, %Activity{} = activity, %Event{} = event} <- {:ok, %Activity{} = activity, %Event{} = event} <-
ActivityPub.create(:event, object_data, false) do Actions.Create.create(:event, object_data, false) do
{:ok, activity, event} {:ok, activity, event}
else else
{:existing_event, %Event{} = event} -> {:ok, nil, event} {: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{type: :Group} = group <- Actors.get_actor(object_data.parent_id),
%Actor{} = actor <- Actors.get_actor(object_data.actor_id), %Actor{} = actor <- Actors.get_actor(object_data.actor_id),
{:ok, %Activity{} = activity, %Member{} = member} <- {:ok, %Activity{} = activity, %Member{} = member} <-
ActivityPub.join(group, actor, false, %{ Actions.Join.join(group, actor, false, %{
url: object_data.url, url: object_data.url,
metadata: %{role: object_data.role} metadata: %{role: object_data.role}
}) do }) do
@ -172,7 +173,7 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier do
{:existing_post, nil} <- {:existing_post, nil} <-
{:existing_post, Posts.get_post_by_url(object_data.url)}, {:existing_post, Posts.get_post_by_url(object_data.url)},
{:ok, %Activity{} = activity, %Post{} = post} <- {:ok, %Activity{} = activity, %Post{} = post} <-
ActivityPub.create(:post, object_data, false) do Actions.Create.create(:post, object_data, false) do
{:ok, activity, post} {:ok, activity, post}
else else
{:existing_post, %Post{} = post} -> {:existing_post, %Post{} = post} ->
@ -197,7 +198,7 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier do
{:ok, nil, comment} {:ok, nil, comment}
{:ok, entity} -> {:ok, entity} ->
ActivityPub.delete(entity, Relay.get_actor(), false) Actions.Delete.delete(entity, Relay.get_actor(), false)
end end
end end
@ -206,10 +207,15 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier do
) do ) do
with {:ok, %Actor{} = followed} <- ActivityPubActor.get_or_fetch_actor_by_url(followed, true), 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, %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} {:ok, activity, object}
else 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)}") Logger.warn("Unable to handle Follow activity #{inspect(e)}")
:error :error
end end
@ -228,7 +234,9 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier do
object_data when is_map(object_data) <- object_data when is_map(object_data) <-
object |> Converter.TodoList.as_to_model_data(), object |> Converter.TodoList.as_to_model_data(),
{:ok, %Activity{} = activity, %TodoList{} = todo_list} <- {: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} {:ok, activity, todo_list}
else else
{:error, :group_not_found} -> :error {:error, :group_not_found} -> :error
@ -247,7 +255,7 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier do
object_data <- object_data <-
object |> Converter.Todo.as_to_model_data(), object |> Converter.Todo.as_to_model_data(),
{:ok, %Activity{} = activity, %Todo{} = todo} <- {:ok, %Activity{} = activity, %Todo{} = todo} <-
ActivityPub.create(:todo, object_data, false) do Actions.Create.create(:todo, object_data, false) do
{:ok, activity, todo} {:ok, activity, todo}
else else
{:existing_todo, %Todo{} = todo} -> {:ok, nil, todo} {:existing_todo, %Todo{} = todo} -> {:ok, nil, todo}
@ -272,7 +280,7 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier do
{:member, true} <- {:member, true} <-
{:member, Actors.is_member?(object_data.creator_id, object_data.actor_id)}, {:member, Actors.is_member?(object_data.creator_id, object_data.actor_id)},
{:ok, %Activity{} = activity, %Resource{} = resource} <- {:ok, %Activity{} = activity, %Resource{} = resource} <-
ActivityPub.create(:resource, object_data, false) do Actions.Create.create(:resource, object_data, false) do
{:ok, activity, resource} {:ok, activity, resource}
else else
{:existing_resource, %Resource{} = resource} -> {:existing_resource, %Resource{} = resource} ->
@ -383,7 +391,7 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier do
object_data <- object_data <-
object |> Converter.Actor.as_to_model_data(), object |> Converter.Actor.as_to_model_data(),
{:ok, %Activity{} = activity, %Actor{} = new_actor} <- {: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} {:ok, activity, new_actor}
else else
e -> e ->
@ -411,7 +419,7 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier do
Utils.origin_check?(actor_url, update_data) || Utils.origin_check?(actor_url, update_data) ||
Permission.can_update_group_object?(actor, old_event)}, Permission.can_update_group_object?(actor, old_event)},
{:ok, %Activity{} = activity, %Event{} = new_event} <- {:ok, %Activity{} = activity, %Event{} = new_event} <-
ActivityPub.update(old_event, object_data, false) do Actions.Update.update(old_event, object_data, false) do
{:ok, activity, new_event} {:ok, activity, new_event}
else else
_e -> _e ->
@ -433,7 +441,7 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier do
{:ok, old_entity} <- object |> Utils.get_url() |> ActivityPub.fetch_object_from_url(), {:ok, old_entity} <- object |> Utils.get_url() |> ActivityPub.fetch_object_from_url(),
object_data <- transform_object_data_for_discussion(object_data), object_data <- transform_object_data_for_discussion(object_data),
{:ok, %Activity{} = activity, new_entity} <- {: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} {:ok, activity, new_entity}
else else
_e -> _e ->
@ -456,7 +464,7 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier do
Utils.origin_check?(actor_url, update_data["object"]) || Utils.origin_check?(actor_url, update_data["object"]) ||
Permission.can_update_group_object?(actor, old_post)}, Permission.can_update_group_object?(actor, old_post)},
{:ok, %Activity{} = activity, %Post{} = new_post} <- {:ok, %Activity{} = activity, %Post{} = new_post} <-
ActivityPub.update(old_post, object_data, false) do Actions.Update.update(old_post, object_data, false) do
{:ok, activity, new_post} {:ok, activity, new_post}
else else
{:origin_check, _} -> {:origin_check, _} ->
@ -484,7 +492,7 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier do
Utils.origin_check?(actor_url, update_data) || Utils.origin_check?(actor_url, update_data) ||
Permission.can_update_group_object?(actor, old_resource)}, Permission.can_update_group_object?(actor, old_resource)},
{:ok, %Activity{} = activity, %Resource{} = new_resource} <- {:ok, %Activity{} = activity, %Resource{} = new_resource} <-
ActivityPub.update(old_resource, object_data, false) do Actions.Update.update(old_resource, object_data, false) do
{:ok, activity, new_resource} {:ok, activity, new_resource}
else else
_e -> _e ->
@ -505,7 +513,7 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier do
object_data <- Converter.Member.as_to_model_data(object), object_data <- Converter.Member.as_to_model_data(object),
{:ok, old_entity} <- object |> Utils.get_url() |> ActivityPub.fetch_object_from_url(), {:ok, old_entity} <- object |> Utils.get_url() |> ActivityPub.fetch_object_from_url(),
{:ok, %Activity{} = activity, new_entity} <- {: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} {:ok, activity, new_entity}
else else
_e -> _e ->
@ -522,7 +530,7 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier do
with object_url <- Utils.get_url(object), with object_url <- Utils.get_url(object),
{:ok, entity} <- ActivityPub.fetch_object_from_url(object_url) do {: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 else
{:ok, %Tombstone{} = tombstone} -> {:ok, %Tombstone{} = tombstone} ->
{:ok, nil, 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, %Actor{} = actor} <- ActivityPubActor.get_or_fetch_actor_by_url(actor),
{:ok, object} <- fetch_obj_helper_as_activity_streams(object_id), {:ok, object} <- fetch_obj_helper_as_activity_streams(object_id),
{:ok, activity, object} <- {: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} {:ok, activity, object}
else else
_e -> :error _e -> :error
@ -563,7 +577,8 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier do
with {:ok, %Actor{domain: nil} = followed} <- with {:ok, %Actor{domain: nil} = followed} <-
ActivityPubActor.get_or_fetch_actor_by_url(followed), ActivityPubActor.get_or_fetch_actor_by_url(followed),
{:ok, %Actor{} = follower} <- ActivityPubActor.get_or_fetch_actor_by_url(follower), {: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} {:ok, activity, object}
else else
e -> e ->
@ -577,6 +592,8 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier do
def handle_incoming( def handle_incoming(
%{"type" => "Delete", "object" => object, "actor" => _actor, "id" => _id} = data %{"type" => "Delete", "object" => object, "actor" => _actor, "id" => _id} = data
) do ) do
Logger.info("Handle incoming to delete an object")
with actor_url <- Utils.get_actor(data), with actor_url <- Utils.get_actor(data),
{:actor, {:ok, %Actor{} = actor}} <- {:actor, {:ok, %Actor{} = actor}} <-
{:actor, ActivityPubActor.get_or_fetch_actor_by_url(actor_url)}, {:actor, ActivityPubActor.get_or_fetch_actor_by_url(actor_url)},
@ -586,14 +603,14 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier do
{:origin_check, {:origin_check,
Utils.origin_check_from_id?(actor_url, object_id) || Utils.origin_check_from_id?(actor_url, object_id) ||
Permission.can_delete_group_object?(actor, object)}, Permission.can_delete_group_object?(actor, object)},
{:ok, activity, object} <- ActivityPub.delete(object, actor, false) do {:ok, activity, object} <- Actions.Delete.delete(object, actor, false) do
{:ok, activity, object} {:ok, activity, object}
else else
{:origin_check, false} -> {:origin_check, false} ->
Logger.warn("Object origin check failed") Logger.warn("Object origin check failed")
:error :error
{:actor, {:error, "Could not fetch by AP id"}} -> {:actor, {:error, _err}} ->
{:error, :unknown_actor} {:error, :unknown_actor}
{:error, e} -> {:error, e} ->
@ -630,7 +647,8 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier do
{:origin_check, {:origin_check,
Utils.origin_check?(actor_url, data) || Utils.origin_check?(actor_url, data) ||
Permission.can_update_group_object?(actor, old_resource)}, Permission.can_update_group_object?(actor, old_resource)},
{:ok, activity, new_resource} <- ActivityPub.move(:resource, old_resource, object_data) do {:ok, activity, new_resource} <-
Actions.Move.move(:resource, old_resource, object_data) do
{:ok, activity, new_resource} {:ok, activity, new_resource}
else else
e -> e ->
@ -658,7 +676,7 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier do
object <- Utils.get_url(object), object <- Utils.get_url(object),
{:ok, object} <- ActivityPub.fetch_object_from_url(object), {:ok, object} <- ActivityPub.fetch_object_from_url(object),
{:ok, activity, object} <- {:ok, activity, object} <-
ActivityPub.join(object, actor, false, %{ Actions.Join.join(object, actor, false, %{
url: id, url: id,
metadata: %{message: Map.get(data, "participationMessage")} metadata: %{message: Map.get(data, "participationMessage")}
}) do }) do
@ -675,7 +693,7 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier do
{:ok, %Actor{} = actor} <- ActivityPubActor.get_or_fetch_actor_by_url(actor), {:ok, %Actor{} = actor} <- ActivityPubActor.get_or_fetch_actor_by_url(actor),
object <- Utils.get_url(object), object <- Utils.get_url(object),
{:ok, object} <- ActivityPub.fetch_object_from_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} {:ok, activity, object}
else else
{:only_organizer, true} -> {:only_organizer, true} ->
@ -707,7 +725,7 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier do
{:ok, %Actor{} = target} <- {:ok, %Actor{} = target} <-
target |> Utils.get_url() |> ActivityPubActor.get_or_fetch_actor_by_url(), target |> Utils.get_url() |> ActivityPubActor.get_or_fetch_actor_by_url(),
{:ok, activity, %Member{} = member} <- {: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} {:ok, activity, member}
end end
end end
@ -727,7 +745,7 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier do
{:is_admin, Actors.get_member(moderator_id, group_id)}, {:is_admin, Actors.get_member(moderator_id, group_id)},
{:is_member, {:ok, %Member{role: role} = member}} when role != :rejected <- {:is_member, {:ok, %Member{role: role} = member}} when role != :rejected <-
{:is_member, Actors.get_member(person_id, group_id)} do {:is_member, Actors.get_member(person_id, group_id)} do
ActivityPub.remove(member, group, moderator, false) Actions.Remove.remove(member, group, moderator, false)
else else
{:is_admin, {:ok, %Member{}}} -> {:is_admin, {:ok, %Member{}}} ->
Logger.warn( Logger.warn(
@ -779,7 +797,7 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier do
{:follow, get_follow(follow_object)}, {:follow, get_follow(follow_object)},
{:same_actor, true} <- {:same_actor, actor.id == followed.id}, {:same_actor, true} <- {:same_actor, actor.id == followed.id},
{:ok, %Activity{} = activity, %Follower{approved: true} = follow} <- {:ok, %Activity{} = activity, %Follower{approved: true} = follow} <-
ActivityPub.accept( Actions.Accept.accept(
:follow, :follow,
follow, follow,
false false
@ -817,7 +835,7 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier do
{:follow, get_follow(follow_object)}, {:follow, get_follow(follow_object)},
{:same_actor, true} <- {:same_actor, actor.id == followed.id}, {:same_actor, true} <- {:same_actor, actor.id == followed.id},
{:ok, activity, _} <- {:ok, activity, _} <-
ActivityPub.reject(:follow, follow) do Actions.Reject.reject(:follow, follow) do
{:ok, activity, follow} {:ok, activity, follow}
else else
{:follow, _err} -> {:follow, _err} ->
@ -872,7 +890,7 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier do
{:can_accept_event_join, true} <- {:can_accept_event_join, true} <-
{:can_accept_event_join, can_manage_event?(actor_accepting, event)}, {:can_accept_event_join, can_manage_event?(actor_accepting, event)},
{:ok, %Activity{} = activity, %Participant{role: :participant} = participant} <- {:ok, %Activity{} = activity, %Participant{role: :participant} = participant} <-
ActivityPub.accept( Actions.Accept.accept(
:join, :join,
participant, participant,
false false
@ -904,7 +922,7 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier do
when role in [:not_approved, :rejected, :invited] and type in [:join, :invite] do when role in [:not_approved, :rejected, :invited] and type in [:join, :invite] do
# Or maybe for groups it's the group that sends the Accept activity # Or maybe for groups it's the group that sends the Accept activity
with {:ok, %Activity{} = activity, %Member{role: :member} = member} <- with {:ok, %Activity{} = activity, %Member{role: :member} = member} <-
ActivityPub.accept( Actions.Accept.accept(
type, type,
member, member,
false false
@ -922,7 +940,7 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier do
{:can_accept_event_reject, true} <- {:can_accept_event_reject, true} <-
{:can_accept_event_reject, can_manage_event?(actor_accepting, event)}, {:can_accept_event_reject, can_manage_event?(actor_accepting, event)},
{:ok, activity, participant} <- {:ok, activity, participant} <-
ActivityPub.reject(:join, participant, false), Actions.Reject.reject(:join, participant, false),
:ok <- Participation.send_emails_to_local_user(participant) do :ok <- Participation.send_emails_to_local_user(participant) do
{:ok, activity, participant} {:ok, activity, participant}
else else
@ -951,9 +969,9 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier do
defp do_handle_incoming_reject_invite(invite_object, %Actor{} = actor_rejecting) do defp do_handle_incoming_reject_invite(invite_object, %Actor{} = actor_rejecting) do
with {:invite, {:ok, %Member{role: :invited, actor_id: actor_id} = member}} <- with {:invite, {:ok, %Member{role: :invited, actor_id: actor_id} = member}} <-
{:invite, get_member(invite_object)}, {: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} <- {:ok, activity, member} <-
ActivityPub.reject(:invite, member, false) do Actions.Reject.reject(:invite, member, false) do
{:ok, activity, member} {:ok, activity, member}
end end
end end
@ -992,7 +1010,6 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier do
end end
# Comment initiates a whole discussion only if it has full title # 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 defp is_data_a_discussion_initialization?(object_data) do
not Map.has_key?(object_data, :title) or not Map.has_key?(object_data, :title) or
is_nil(object_data.title) or object_data.title == "" is_nil(object_data.title) or object_data.title == ""
@ -1106,22 +1123,22 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier do
end end
defp is_group_object_gone(object_id) do defp is_group_object_gone(object_id) do
case ActivityPub.fetch_object_from_url(object_id, force: true) do Logger.debug("is_group_object_gone #{object_id}")
{:error, error_message, object} when error_message in ["Gone", "Not found"] ->
{:ok, object}
case ActivityPub.fetch_object_from_url(object_id, force: true) do
# comments are just emptied # comments are just emptied
{:ok, %Comment{deleted_at: deleted_at} = object} when not is_nil(deleted_at) -> {:ok, %Comment{deleted_at: deleted_at} = object} when not is_nil(deleted_at) ->
{:ok, object} {:ok, object}
{:error, :http_gone, object} ->
Logger.debug("object is really gone")
{:ok, object}
{:ok, %{url: url} = object} -> {:ok, %{url: url} = object} ->
if Utils.are_same_origin?(url, Endpoint.url()), if Utils.are_same_origin?(url, Endpoint.url()),
do: {:ok, object}, do: {:ok, object},
else: {:error, "Group object URL remote"} else: {:error, "Group object URL remote"}
{:error, {:error, err}} ->
{:error, err}
{: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 # Before 1.0.4 the object of a "Remove" activity was an actor's URL
# instead of the member's URL. # instead of the member's URL.
# TODO: Remove in 1.2 # 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 defp get_remove_object(object) do
case object |> Utils.get_url() |> ActivityPub.fetch_object_from_url() do case object |> Utils.get_url() |> ActivityPub.fetch_object_from_url() do
{:ok, %Member{actor: %Actor{id: person_id}}} -> {:ok, person_id} {: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 organizer_actor_id == actor_id
end end
defp can_manage_event?(_actor, _event) do defp can_manage_event?(%Actor{} = _actor, %Event{} = _event) do
false false
end end
end end

View file

@ -1,10 +1,10 @@
defmodule Mobilizon.Federation.ActivityPub.Types.Actors do defmodule Mobilizon.Federation.ActivityPub.Types.Actors do
@moduledoc false @moduledoc false
alias Mobilizon.Actors alias Mobilizon.Actors
alias Mobilizon.Actors.{Actor, Follower, Member} alias Mobilizon.Actors.{Actor, Follower, Member, MemberRole}
alias Mobilizon.Federation.ActivityPub alias Mobilizon.Federation.ActivityPub.{Actions, Audience, Permission, Relay}
alias Mobilizon.Federation.ActivityPub.{Audience, Permission, Relay}
alias Mobilizon.Federation.ActivityPub.Types.Entity alias Mobilizon.Federation.ActivityPub.Types.Entity
alias Mobilizon.Federation.ActivityStream
alias Mobilizon.Federation.ActivityStream.Convertible alias Mobilizon.Federation.ActivityStream.Convertible
alias Mobilizon.GraphQL.API.Utils, as: APIUtils alias Mobilizon.GraphQL.API.Utils, as: APIUtils
alias Mobilizon.Service.Activity.Group, as: GroupActivity alias Mobilizon.Service.Activity.Group, as: GroupActivity
@ -17,46 +17,57 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Actors do
@behaviour Entity @behaviour Entity
@impl 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 def create(args, additional) do
with args <- prepare_args_for_actor(args), args = prepare_args_for_actor(args)
{:ok, %Actor{} = actor} <- Actors.create_actor(args),
{:ok, _} <- case Actors.create_actor(args) do
{:ok, %Actor{} = actor} ->
GroupActivity.insert_activity(actor, GroupActivity.insert_activity(actor,
subject: "group_created", subject: "group_created",
actor_id: args.creator_actor_id actor_id: args.creator_actor_id
), )
actor_as_data <- Convertible.model_to_as(actor),
audience <- %{"to" => ["https://www.w3.org/ns/activitystreams#Public"], "cc" => []}, actor_as_data = Convertible.model_to_as(actor)
create_data <- audience = %{"to" => ["https://www.w3.org/ns/activitystreams#Public"], "cc" => []}
make_create_data(actor_as_data, Map.merge(audience, additional)) do create_data = make_create_data(actor_as_data, Map.merge(audience, additional))
{:ok, actor, create_data} {:ok, actor, create_data}
{:error, %Ecto.Changeset{} = err} ->
{:error, err}
end end
end end
@impl Entity @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 def update(%Actor{} = old_actor, args, additional) do
with {:ok, %Actor{} = new_actor} <- Actors.update_actor(old_actor, args), case Actors.update_actor(old_actor, args) do
{:ok, _} <- {:ok, %Actor{} = new_actor} ->
GroupActivity.insert_activity(new_actor, GroupActivity.insert_activity(new_actor,
subject: "group_updated", subject: "group_updated",
old_group: old_actor, old_group: old_actor,
updater_actor: Map.get(args, :updater_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}"), actor_as_data = Convertible.model_to_as(new_actor)
audience <- Cachex.del(:activity_pub, "actor_#{new_actor.preferred_username}")
Audience.get_audience(new_actor), audience = Audience.get_audience(new_actor)
additional <- Map.merge(additional, %{"actor" => old_actor.url}), additional = Map.merge(additional, %{"actor" => old_actor.url})
update_data <- make_update_data(actor_as_data, Map.merge(audience, additional)) do update_data = make_update_data(actor_as_data, Map.merge(audience, additional))
{:ok, new_actor, update_data} {:ok, new_actor, update_data}
{:error, %Ecto.Changeset{} = err} ->
{:error, err}
end end
end end
@public_ap "https://www.w3.org/ns/activitystreams#Public" @public_ap "https://www.w3.org/ns/activitystreams#Public"
@impl Entity @impl Entity
@spec delete(Actor.t(), Actor.t(), boolean, map) ::
{:ok, ActivityStream.t(), Actor.t(), Actor.t()} | {:error, Ecto.Changeset.t()}
def delete( def delete(
%Actor{ %Actor{
followers_url: followers_url, followers_url: followers_url,
@ -89,21 +100,27 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Actors do
suspension = Map.get(additionnal, :suspension, false) suspension = Map.get(additionnal, :suspension, false)
with {:ok, %Oban.Job{}} <- case Actors.delete_actor(target_actor,
Actors.delete_actor(target_actor,
# We completely delete the actor if the actor is remote # We completely delete the actor if the actor is remote
reserve_username: is_nil(domain), reserve_username: is_nil(domain),
suspension: suspension, suspension: suspension,
author_id: author_id author_id: author_id
) do ) do
{:ok, %Oban.Job{}} ->
{:ok, activity_data, actor, target_actor} {:ok, activity_data, actor, target_actor}
{:error, %Ecto.Changeset{} = err} ->
{:error, err}
end end
end end
@spec actor(Actor.t()) :: Actor.t() | nil
def actor(%Actor{} = actor), do: actor def actor(%Actor{} = actor), do: actor
@spec group_actor(Actor.t()) :: Actor.t() | nil
def group_actor(%Actor{} = actor), do: actor def group_actor(%Actor{} = actor), do: actor
@spec permissions(Actor.t()) :: Permission.t()
def permissions(%Actor{} = _group) do def permissions(%Actor{} = _group) do
%Permission{ %Permission{
access: :member, access: :member,
@ -113,14 +130,14 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Actors do
} }
end 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 def join(%Actor{type: :Group} = group, %Actor{} = actor, _local, additional) do
with role <- role =
additional additional
|> Map.get(:metadata, %{}) |> Map.get(:metadata, %{})
|> Map.get(:role, Mobilizon.Actors.get_default_member_role(group)), |> Map.get(:role, Mobilizon.Actors.get_default_member_role(group))
{:ok, %Member{} = member} <-
Mobilizon.Actors.create_member(%{ case Mobilizon.Actors.create_member(%{
role: role, role: role,
parent_id: group.id, parent_id: group.id,
actor_id: actor.id, actor_id: actor.id,
@ -129,20 +146,23 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Actors do
additional additional
|> Map.get(:metadata, %{}) |> Map.get(:metadata, %{})
|> Map.update(:message, nil, &String.trim(HTML.strip_tags(&1))) |> Map.update(:message, nil, &String.trim(HTML.strip_tags(&1)))
}), }) do
{:ok, _} <- {:ok, %Member{} = member} ->
Mobilizon.Service.Activity.Member.insert_activity(member, subject: "member_joined"), Mobilizon.Service.Activity.Member.insert_activity(member, subject: "member_joined")
Absinthe.Subscription.publish(Endpoint, actor, Absinthe.Subscription.publish(Endpoint, actor,
group_membership_changed: [Actor.preferred_username_and_domain(group), actor.id] group_membership_changed: [Actor.preferred_username_and_domain(group), actor.id]
), )
join_data <- %{
join_data = %{
"type" => "Join", "type" => "Join",
"id" => member.url, "id" => member.url,
"actor" => actor.url, "actor" => actor.url,
"object" => group.url "object" => group.url
}, }
audience <-
Audience.get_audience(member) do audience = Audience.get_audience(member)
approve_if_default_role_is_member( approve_if_default_role_is_member(
group, group,
actor, actor,
@ -150,21 +170,34 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Actors do
member, member,
role role
) )
{:error, %Ecto.Changeset{} = err} ->
{:error, err}
end end
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) def follow(%Actor{} = follower_actor, %Actor{type: type} = followed, _local, additional)
when type != :Person do when type != :Person do
with {:ok, %Follower{} = follower} <- case Mobilizon.Actors.follow(followed, follower_actor, additional["activity_id"], false) do
Mobilizon.Actors.follow(followed, follower_actor, additional["activity_id"], false), {:ok, %Follower{} = follower} ->
:ok <- FollowMailer.send_notification_to_admins(follower), FollowMailer.send_notification_to_admins(follower)
follower_as_data <- Convertible.model_to_as(follower) do follower_as_data = Convertible.model_to_as(follower)
approve_if_manually_approves_followers(follower, follower_as_data) approve_if_manually_approves_followers(follower, follower_as_data)
{:error, error} ->
{:error, error}
end end
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 defp prepare_args_for_actor(args) do
args args
|> maybe_sanitize_username() |> maybe_sanitize_username()
@ -191,8 +224,14 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Actors do
defp maybe_sanitize_summary(args), do: args defp maybe_sanitize_summary(args), do: args
# Set the participant to approved if the default role for new participants is :participant # Set the participant to approved if the default role for new participants is :participant
@spec approve_if_default_role_is_member(Actor.t(), Actor.t(), map(), Member.t(), atom()) :: @spec approve_if_default_role_is_member(
{:ok, map(), Member.t()} Actor.t(),
Actor.t(),
ActivityStreams.t(),
Member.t(),
MemberRole.t()
) ::
{:ok, ActivityStreams.t(), Member.t()}
defp approve_if_default_role_is_member( defp approve_if_default_role_is_member(
%Actor{type: :Group} = group, %Actor{type: :Group} = group,
%Actor{} = actor, %Actor{} = actor,
@ -202,17 +241,17 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Actors do
) do ) do
if is_nil(group.domain) && !is_nil(actor.domain) do if is_nil(group.domain) && !is_nil(actor.domain) do
cond do cond do
Mobilizon.Actors.get_default_member_role(group) === :member && Mobilizon.Actors.get_default_member_role(group) == :member &&
role == :member -> role == :member ->
{:accept, {:accept,
ActivityPub.accept( Actions.Accept.accept(
:join, :join,
member, member,
true, true,
%{"actor" => group.url} %{"actor" => group.url}
)} )}
Mobilizon.Actors.get_default_member_role(group) === :not_approved && Mobilizon.Actors.get_default_member_role(group) == :not_approved &&
role == :not_approved -> role == :not_approved ->
Scheduler.pending_membership_notification(group) Scheduler.pending_membership_notification(group)
{:ok, activity_data, member} {:ok, activity_data, member}
@ -225,6 +264,11 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Actors do
end end
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( defp approve_if_manually_approves_followers(
%Follower{} = follower, %Follower{} = follower,
follow_as_data 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") Logger.debug("Target doesn't manually approves followers, we can accept right away")
{:accept, {:accept,
ActivityPub.accept( Actions.Accept.accept(
:follow, :follow,
follower, follower,
true, true,

View file

@ -6,6 +6,7 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Comments do
alias Mobilizon.Events.{Event, EventOptions} alias Mobilizon.Events.{Event, EventOptions}
alias Mobilizon.Federation.ActivityPub.{Audience, Permission} alias Mobilizon.Federation.ActivityPub.{Audience, Permission}
alias Mobilizon.Federation.ActivityPub.Types.Entity alias Mobilizon.Federation.ActivityPub.Types.Entity
alias Mobilizon.Federation.ActivityStream
alias Mobilizon.Federation.ActivityStream.Converter.Utils, as: ConverterUtils alias Mobilizon.Federation.ActivityStream.Converter.Utils, as: ConverterUtils
alias Mobilizon.Federation.ActivityStream.Convertible alias Mobilizon.Federation.ActivityStream.Convertible
alias Mobilizon.GraphQL.API.Utils, as: APIUtils alias Mobilizon.GraphQL.API.Utils, as: APIUtils
@ -20,47 +21,56 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Comments do
@behaviour Entity @behaviour Entity
@impl 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 def create(args, additional) do
with args <- prepare_args_for_comment(args), args = prepare_args_for_comment(args)
:ok <- make_sure_event_allows_commenting(args),
{:ok, %Comment{discussion_id: discussion_id} = comment} <- if event_allows_commenting?(args) do
Discussions.create_comment(args), case Discussions.create_comment(args) do
{:ok, _} <- {:ok, %Comment{discussion_id: discussion_id} = comment} ->
CommentActivity.insert_activity(comment, CommentActivity.insert_activity(comment,
subject: "comment_posted" subject: "comment_posted"
), )
:ok <- maybe_publish_graphql_subscription(discussion_id),
comment_as_data <- Convertible.model_to_as(comment), maybe_publish_graphql_subscription(discussion_id)
audience <- comment_as_data = Convertible.model_to_as(comment)
Audience.get_audience(comment), audience = Audience.get_audience(comment)
create_data <- create_data = make_create_data(comment_as_data, Map.merge(audience, additional))
make_create_data(comment_as_data, Map.merge(audience, additional)) do
{:ok, comment, create_data} {:ok, comment, create_data}
end
end
@impl Entity {:error, %Ecto.Changeset{} = err} ->
@spec update(Comment.t(), map(), map()) :: {:ok, Comment.t(), Activity.t()} | any() {:error, err}
def update(%Comment{} = old_comment, args, additional) do end
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}
else else
err -> {:error, :event_not_allow_commenting}
Logger.error("Something went wrong while creating an update activity")
Logger.debug(inspect(err))
err
end end
end end
@impl Entity @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( def delete(
%Comment{url: url, id: comment_id}, %Comment{url: url, id: comment_id},
%Actor{} = actor, %Actor{} = actor,
@ -79,18 +89,21 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Comments do
force_deletion = Map.get(options, :force, false) force_deletion = Map.get(options, :force, false)
with audience <- audience = Audience.get_audience(comment)
Audience.get_audience(comment),
{:ok, %Comment{} = updated_comment} <- case Discussions.delete_comment(comment, force: force_deletion) do
Discussions.delete_comment(comment, force: force_deletion), {:ok, %Comment{} = updated_comment} ->
{:ok, true} <- Cachex.del(:activity_pub, "comment_#{comment.uuid}"), Cachex.del(:activity_pub, "comment_#{comment.uuid}")
{:ok, %Tombstone{} = _tombstone} <- Tombstone.create_tombstone(%{uri: comment.url, actor_id: actor.id})
Tombstone.create_tombstone(%{uri: comment.url, actor_id: actor.id}) do
Share.delete_all_by_uri(comment.url) Share.delete_all_by_uri(comment.url)
{:ok, Map.merge(activity_data, audience), actor, updated_comment} {:ok, Map.merge(activity_data, audience), actor, updated_comment}
{:error, %Ecto.Changeset{} = err} ->
{:error, err}
end end
end end
@spec actor(Comment.t()) :: Actor.t() | nil
def actor(%Comment{actor: %Actor{} = actor}), do: actor def actor(%Comment{actor: %Actor{} = actor}), do: actor
def actor(%Comment{actor_id: actor_id}) when not is_nil(actor_id), 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 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: %Actor{} = group}), do: group
def group_actor(%Comment{attributed_to_id: attributed_to_id}) when not is_nil(attributed_to_id), 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 def group_actor(_), do: nil
@spec permissions(Comment.t()) :: Permission.t()
def permissions(%Comment{}), def permissions(%Comment{}),
do: %Permission{ do: %Permission{
access: :member, access: :member,
@ -114,6 +129,7 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Comments do
} }
# Prepare and sanitize arguments for comments # Prepare and sanitize arguments for comments
@spec prepare_args_for_comment(map) :: map
defp prepare_args_for_comment(args) do defp prepare_args_for_comment(args) do
with in_reply_to_comment <- with in_reply_to_comment <-
args |> Map.get(:in_reply_to_comment_id) |> Discussions.get_comment_with_preload(), 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
end end
@spec prepare_args_for_comment_update(map) :: map
defp prepare_args_for_comment_update(args) do defp prepare_args_for_comment_update(args) do
with {text, mentions, tags} <- with {text, mentions, tags} <-
APIUtils.make_content_html( APIUtils.make_content_html(
@ -174,32 +191,39 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Comments do
defp handle_event_for_comment(nil), do: nil 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(nil), do: :ok
defp maybe_publish_graphql_subscription(discussion_id) do 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, Absinthe.Subscription.publish(Endpoint, discussion,
discussion_comment_changed: discussion.slug discussion_comment_changed: discussion.slug
) )
:ok :ok
nil ->
:ok
end end
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, actor_id: actor_id,
event: %Event{ event: %Event{
options: %EventOptions{comment_moderation: comment_moderation}, options: %EventOptions{comment_moderation: comment_moderation},
organizer_actor_id: organizer_actor_id organizer_actor_id: organizer_actor_id
} }
}) do }) do
if comment_moderation != :closed || comment_moderation != :closed ||
to_string(actor_id) == to_string(organizer_actor_id) do to_string(actor_id) == to_string(organizer_actor_id)
:ok
else
{:error, :event_comments_are_closed}
end
end end
defp make_sure_event_allows_commenting(_), do: :ok # Comments not attached to events
defp event_allows_commenting?(_), do: true
end end

View file

@ -6,6 +6,7 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Discussions do
alias Mobilizon.Discussions.{Comment, Discussion} alias Mobilizon.Discussions.{Comment, Discussion}
alias Mobilizon.Federation.ActivityPub.{Audience, Permission} alias Mobilizon.Federation.ActivityPub.{Audience, Permission}
alias Mobilizon.Federation.ActivityPub.Types.Entity alias Mobilizon.Federation.ActivityPub.Types.Entity
alias Mobilizon.Federation.ActivityStream
alias Mobilizon.Federation.ActivityStream.Convertible alias Mobilizon.Federation.ActivityStream.Convertible
alias Mobilizon.GraphQL.API.Utils, as: APIUtils alias Mobilizon.GraphQL.API.Utils, as: APIUtils
alias Mobilizon.Service.Activity.Discussion, as: DiscussionActivity alias Mobilizon.Service.Activity.Discussion, as: DiscussionActivity
@ -16,83 +17,100 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Discussions do
@behaviour Entity @behaviour Entity
@impl 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 def create(%{discussion_id: discussion_id} = args, additional) when not is_nil(discussion_id) do
with args <- prepare_args(args), args = prepare_args(args)
%Discussion{} = discussion <- Discussions.get_discussion(discussion_id),
{:ok, %Discussion{last_comment_id: last_comment_id} = discussion} <- case Discussions.get_discussion(discussion_id) do
Discussions.reply_to_discussion(discussion, args), %Discussion{} = discussion ->
{:ok, _} <- case Discussions.reply_to_discussion(discussion, args) do
{:ok, %Discussion{last_comment_id: last_comment_id} = discussion} ->
DiscussionActivity.insert_activity(discussion, DiscussionActivity.insert_activity(discussion,
subject: "discussion_replied", subject: "discussion_replied",
actor_id: Map.get(args, :creator_id, args.actor_id) 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), case Discussions.get_comment_with_preload(last_comment_id) do
comment_as_data <- Convertible.model_to_as(last_comment), %Comment{} = last_comment ->
audience <- maybe_publish_graphql_subscription(discussion)
Audience.get_audience(discussion), comment_as_data = Convertible.model_to_as(last_comment)
create_data <- audience = Audience.get_audience(discussion)
make_create_data(comment_as_data, Map.merge(audience, additional)) do create_data = make_create_data(comment_as_data, Map.merge(audience, additional))
{:ok, discussion, create_data} {: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
end end
@impl Entity @impl Entity
@spec create(map(), map()) :: {:ok, map()}
def create(args, additional) do def create(args, additional) do
with args <- prepare_args(args), args = prepare_args(args)
{:ok, %Discussion{} = discussion} <-
Discussions.create_discussion(args), case Discussions.create_discussion(args) do
{:ok, _} <- {:ok, %Discussion{} = discussion} ->
DiscussionActivity.insert_activity(discussion, subject: "discussion_created"), DiscussionActivity.insert_activity(discussion, subject: "discussion_created")
discussion_as_data <- Convertible.model_to_as(discussion), discussion_as_data = Convertible.model_to_as(discussion)
audience <- audience = Audience.get_audience(discussion)
Audience.get_audience(discussion), create_data = make_create_data(discussion_as_data, Map.merge(audience, additional))
create_data <-
make_create_data(discussion_as_data, Map.merge(audience, additional)) do
{:ok, discussion, create_data} {:ok, discussion, create_data}
{:error, _, %Ecto.Changeset{} = err, _} ->
{:error, err}
end end
end end
@impl Entity @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 def update(%Discussion{} = old_discussion, args, additional) do
with {:ok, %Discussion{} = new_discussion} <- case Discussions.update_discussion(old_discussion, args) do
Discussions.update_discussion(old_discussion, args), {:ok, %Discussion{} = new_discussion} ->
{:ok, _} <-
DiscussionActivity.insert_activity(new_discussion, DiscussionActivity.insert_activity(new_discussion,
subject: "discussion_renamed", subject: "discussion_renamed",
old_discussion: old_discussion old_discussion: old_discussion
), )
{:ok, true} <- Cachex.del(:activity_pub, "discussion_#{new_discussion.slug}"),
discussion_as_data <- Convertible.model_to_as(new_discussion), Cachex.del(:activity_pub, "discussion_#{new_discussion.slug}")
audience <- discussion_as_data = Convertible.model_to_as(new_discussion)
Audience.get_audience(new_discussion), audience = Audience.get_audience(new_discussion)
update_data <- make_update_data(discussion_as_data, Map.merge(audience, additional)) do update_data = make_update_data(discussion_as_data, Map.merge(audience, additional))
{:ok, new_discussion, update_data} {:ok, new_discussion, update_data}
else
err -> {:error, %Ecto.Changeset{} = err} ->
Logger.error("Something went wrong while creating an update activity") {:error, err}
Logger.debug(inspect(err))
err
end end
end end
@impl Entity @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( def delete(
%Discussion{actor: group, url: url} = discussion, %Discussion{actor: group, url: url} = discussion,
%Actor{} = actor, %Actor{} = actor,
_local, _local,
_additionnal _additionnal
) do ) do
with {:ok, _} <- Discussions.delete_discussion(discussion), case Discussions.delete_discussion(discussion) do
{:ok, _} <- {:error, _, %Ecto.Changeset{} = err, _} ->
{:error, err}
{:ok, %{comments: {_, _}}} ->
DiscussionActivity.insert_activity(discussion, DiscussionActivity.insert_activity(discussion,
subject: "discussion_deleted", subject: "discussion_deleted",
moderator: actor moderator: actor
) do )
# This is just fake # This is just fake
activity_data = %{ activity_data = %{
"type" => "Delete", "type" => "Delete",
@ -106,10 +124,13 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Discussions do
end end
end end
@spec actor(Discussion.t()) :: Actor.t() | nil
def actor(%Discussion{creator_id: creator_id}), do: Actors.get_actor(creator_id) 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) def group_actor(%Discussion{actor_id: actor_id}), do: Actors.get_actor(actor_id)
@spec permissions(Discussion.t()) :: Permission.t()
def permissions(%Discussion{}) do def permissions(%Discussion{}) do
%Permission{access: :member, create: :member, update: :moderator, delete: :moderator} %Permission{access: :member, create: :member, update: :moderator, delete: :moderator}
end end
@ -123,6 +144,7 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Discussions do
:ok :ok
end end
@spec prepare_args(map) :: map
defp prepare_args(args) do defp prepare_args(args) do
{text, _mentions, _tags} = {text, _mentions, _tags} =
APIUtils.make_content_html( APIUtils.make_content_html(

View file

@ -28,16 +28,16 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Entity do
@moduledoc """ @moduledoc """
ActivityPub entity behaviour 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()) :: @callback create(data :: any(), additionnal :: map()) ::
{:ok, t(), ActivityStream.t()} {:ok, t(), ActivityStream.t()} | {:error, any()}
@callback update(struct :: t(), attrs :: map(), additionnal :: map()) :: @callback update(structure :: t(), attrs :: map(), additionnal :: map()) ::
{:ok, t(), ActivityStream.t()} {:ok, t(), ActivityStream.t()} | {:error, any()}
@callback delete(struct :: t(), Actor.t(), local :: boolean(), map()) :: @callback delete(structure :: t(), actor :: Actor.t(), local :: boolean(), additionnal :: map()) ::
{:ok, ActivityStream.t(), Actor.t(), t()} {:ok, ActivityStream.t(), Actor.t(), t()} | {:error, any()}
end end
defprotocol Mobilizon.Federation.ActivityPub.Types.Managable do defprotocol Mobilizon.Federation.ActivityPub.Types.Managable do
@ -45,46 +45,61 @@ defprotocol Mobilizon.Federation.ActivityPub.Types.Managable do
ActivityPub entity Managable protocol. ActivityPub entity Managable protocol.
""" """
@spec update(Entity.t(), map(), map()) :: {:ok, Entity.t(), ActivityStream.t()}
@doc """ @doc """
Updates a `Managable` entity with the appropriate attributes and returns the updated entity and an activitystream representation for it 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) 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" @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) def delete(entity, actor, local, additionnal)
end end
defprotocol Mobilizon.Federation.ActivityPub.Types.Ownable do 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" @doc "Returns an eventual group for the entity"
@spec group_actor(Entity.t()) :: Actor.t() | nil
def group_actor(entity) def group_actor(entity)
@spec actor(Entity.t()) :: Actor.t() | nil
@doc "Returns the actor for the entity" @doc "Returns the actor for the entity"
@spec actor(Entity.t()) :: Actor.t() | nil
def actor(entity) def actor(entity)
@doc """
Returns the list of permissions for an entity
"""
@spec permissions(Entity.t()) :: Permission.t() @spec permissions(Entity.t()) :: Permission.t()
def permissions(entity) def permissions(entity)
end end
defimpl Managable, for: Event do 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 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 defdelegate delete(entity, actor, local, additionnal), to: Events
end end
defimpl Ownable, for: Event do defimpl Ownable, for: Event do
@spec group_actor(Event.t()) :: Actor.t() | nil
defdelegate group_actor(entity), to: Events defdelegate group_actor(entity), to: Events
@spec actor(Event.t()) :: Actor.t() | nil
defdelegate actor(entity), to: Events defdelegate actor(entity), to: Events
@spec permissions(Event.t()) :: Permission.t()
defdelegate permissions(entity), to: Events defdelegate permissions(entity), to: Events
end end
defimpl Managable, for: Comment do 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 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 defdelegate delete(entity, actor, local, additionnal), to: Comments
end end

View file

@ -3,10 +3,10 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Events do
alias Mobilizon.Actors alias Mobilizon.Actors
alias Mobilizon.Actors.Actor alias Mobilizon.Actors.Actor
alias Mobilizon.Events, as: EventsManager alias Mobilizon.Events, as: EventsManager
alias Mobilizon.Events.{Event, Participant} alias Mobilizon.Events.{Event, Participant, ParticipantRole}
alias Mobilizon.Federation.ActivityPub alias Mobilizon.Federation.ActivityPub.{Actions, Audience, Permission}
alias Mobilizon.Federation.ActivityPub.{Audience, Permission}
alias Mobilizon.Federation.ActivityPub.Types.Entity alias Mobilizon.Federation.ActivityPub.Types.Entity
alias Mobilizon.Federation.ActivityStream
alias Mobilizon.Federation.ActivityStream.Converter.Utils, as: ConverterUtils alias Mobilizon.Federation.ActivityStream.Converter.Utils, as: ConverterUtils
alias Mobilizon.Federation.ActivityStream.Convertible alias Mobilizon.Federation.ActivityStream.Convertible
alias Mobilizon.GraphQL.API.Utils, as: APIUtils alias Mobilizon.GraphQL.API.Utils, as: APIUtils
@ -22,44 +22,53 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Events do
@behaviour Entity @behaviour Entity
@impl 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 def create(args, additional) do
with args <- prepare_args_for_event(args), args = prepare_args_for_event(args)
{:ok, %Event{} = event} <- EventsManager.create_event(args),
{:ok, _} <- case EventsManager.create_event(args) do
EventActivity.insert_activity(event, subject: "event_created"), {:ok, %Event{} = event} ->
event_as_data <- Convertible.model_to_as(event), EventActivity.insert_activity(event, subject: "event_created")
audience <- event_as_data = Convertible.model_to_as(event)
Audience.get_audience(event), audience = Audience.get_audience(event)
create_data <- create_data = make_create_data(event_as_data, Map.merge(audience, additional))
make_create_data(event_as_data, Map.merge(audience, additional)) do
{:ok, event, create_data} {:ok, event, create_data}
{:error, _step, %Ecto.Changeset{} = err, _} ->
{:error, err}
{:error, %Ecto.Changeset{} = err} ->
{:error, err}
end end
end end
@impl Entity @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 def update(%Event{} = old_event, args, additional) do
with args <- prepare_args_for_event(args), args = prepare_args_for_event(args)
{:ok, %Event{} = new_event} <- EventsManager.update_event(old_event, args),
{:ok, _} <- case EventsManager.update_event(old_event, args) do
EventActivity.insert_activity(new_event, subject: "event_updated"), {:ok, %Event{} = new_event} ->
{:ok, true} <- Cachex.del(:activity_pub, "event_#{new_event.uuid}"), EventActivity.insert_activity(new_event, subject: "event_updated")
event_as_data <- Convertible.model_to_as(new_event), Cachex.del(:activity_pub, "event_#{new_event.uuid}")
audience <- event_as_data = Convertible.model_to_as(new_event)
Audience.get_audience(new_event), audience = Audience.get_audience(new_event)
update_data <- make_update_data(event_as_data, Map.merge(audience, additional)) do update_data = make_update_data(event_as_data, Map.merge(audience, additional))
{:ok, new_event, update_data} {:ok, new_event, update_data}
else
err -> {:error, _step, %Ecto.Changeset{} = err, _} ->
Logger.error("Something went wrong while creating an update activity") {:error, err}
Logger.debug(inspect(err))
err {:error, err} ->
{:error, err}
end end
end end
@impl Entity @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 def delete(%Event{url: url} = event, %Actor{} = actor, _local, _additionnal) do
activity_data = %{ activity_data = %{
"type" => "Delete", "type" => "Delete",
@ -69,19 +78,27 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Events do
"id" => url <> "/delete" "id" => url <> "/delete"
} }
with audience <- audience = Audience.get_audience(event)
Audience.get_audience(event),
{:ok, %Event{} = event} <- EventsManager.delete_event(event), case EventsManager.delete_event(event) do
{:ok, _} <- {:ok, %Event{} = event} ->
EventActivity.insert_activity(event, subject: "event_deleted"), case Tombstone.create_tombstone(%{uri: event.url, actor_id: actor.id}) do
{:ok, true} <- Cachex.del(:activity_pub, "event_#{event.uuid}"), {:ok, %Tombstone{} = _tombstone} ->
{:ok, %Tombstone{} = _tombstone} <- EventActivity.insert_activity(event, subject: "event_deleted")
Tombstone.create_tombstone(%{uri: event.url, actor_id: actor.id}) do Cachex.del(:activity_pub, "event_#{event.uuid}")
Share.delete_all_by_uri(event.url) Share.delete_all_by_uri(event.url)
{:ok, Map.merge(activity_data, audience), actor, event} {:ok, Map.merge(activity_data, audience), actor, event}
{:error, %Ecto.Changeset{} = err} ->
{:error, err}
end
{:error, %Ecto.Changeset{} = err} ->
{:error, err}
end end
end end
@spec actor(Event.t()) :: Actor.t() | nil
def actor(%Event{organizer_actor: %Actor{} = actor}), do: actor def actor(%Event{organizer_actor: %Actor{} = actor}), do: actor
def actor(%Event{organizer_actor_id: organizer_actor_id}), def actor(%Event{organizer_actor_id: organizer_actor_id}),
@ -89,6 +106,7 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Events do
def actor(_), do: nil 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: %Actor{} = group}), do: group
def group_actor(%Event{attributed_to_id: attributed_to_id}) when not is_nil(attributed_to_id), 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 def group_actor(_), do: nil
@spec permissions(Event.t()) :: Permission.t()
def permissions(%Event{draft: draft, attributed_to_id: _attributed_to_id}) do def permissions(%Event{draft: draft, attributed_to_id: _attributed_to_id}) do
%Permission{ %Permission{
access: if(draft, do: nil, else: :member), access: if(draft, do: nil, else: :member),
@ -105,15 +124,18 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Events do
} }
end 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 def join(%Event{} = event, %Actor{} = actor, _local, additional) do
with {:maximum_attendee_capacity, true} <- if check_attendee_capacity?(event) do
{:maximum_attendee_capacity, check_attendee_capacity(event)}, role =
role <-
additional additional
|> Map.get(:metadata, %{}) |> Map.get(:metadata, %{})
|> Map.get(:role, Mobilizon.Events.get_default_participant_role(event)), |> Map.get(:role, Mobilizon.Events.get_default_participant_role(event))
{:ok, %Participant{} = participant} <-
Mobilizon.Events.create_participant(%{ case Mobilizon.Events.create_participant(%{
role: role, role: role,
event_id: event.id, event_id: event.id,
actor_id: actor.id, actor_id: actor.id,
@ -122,31 +144,41 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Events do
additional additional
|> Map.get(:metadata, %{}) |> Map.get(:metadata, %{})
|> Map.update(:message, nil, &String.trim(HTML.strip_tags(&1))) |> Map.update(:message, nil, &String.trim(HTML.strip_tags(&1)))
}), }) do
join_data <- Convertible.model_to_as(participant), {:ok, %Participant{} = participant} ->
audience <- join_data = Convertible.model_to_as(participant)
Audience.get_audience(participant) do audience = Audience.get_audience(participant)
approve_if_default_role_is_participant( approve_if_default_role_is_participant(
event, event,
Map.merge(join_data, audience), Map.merge(join_data, audience),
participant, participant,
role role
) )
{:error, %Ecto.Changeset{} = err} ->
{:error, err}
end
else else
{:maximum_attendee_capacity, err} -> {:error, :maximum_attendee_capacity_reached}
{:maximum_attendee_capacity, err}
end end
end end
defp check_attendee_capacity(%Event{options: options} = event) do @spec check_attendee_capacity?(Event.t()) :: boolean
with maximum_attendee_capacity <- defp check_attendee_capacity?(%Event{options: options} = event) do
Map.get(options, :maximum_attendee_capacity) || 0 do maximum_attendee_capacity = Map.get(options, :maximum_attendee_capacity) || 0
maximum_attendee_capacity == 0 || maximum_attendee_capacity == 0 ||
Mobilizon.Events.count_participant_participants(event.id) < maximum_attendee_capacity Mobilizon.Events.count_participant_participants(event.id) < maximum_attendee_capacity
end end
end
# Set the participant to approved if the default role for new participants is :participant # Set the participant to approved if the default role for new participants is :participant
@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 defp approve_if_default_role_is_participant(event, activity_data, participant, role) do
case event do case event do
%Event{attributed_to: %Actor{id: group_id, url: group_url}} -> %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} {:ok, activity_data, participant}
end end
%Event{local: true} -> %Event{attributed_to: nil, local: true} ->
do_approve(event, activity_data, participant, role, %{ do_approve(event, activity_data, participant, role, %{
"actor" => event.organizer_actor.url "actor" => event.organizer_actor.url
}) })
_ -> %Event{} ->
{:ok, activity_data, participant} {:ok, activity_data, participant}
end end
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 defp do_approve(event, activity_data, participant, role, additionnal) do
cond do cond do
Mobilizon.Events.get_default_participant_role(event) === :participant && Mobilizon.Events.get_default_participant_role(event) == :participant &&
role == :participant -> role == :participant ->
{:accept, {:accept,
ActivityPub.accept( Actions.Accept.accept(
:join, :join,
participant, participant,
true, true,
additionnal additionnal
)} )}
Mobilizon.Events.get_default_participant_role(event) === :not_approved && Mobilizon.Events.get_default_participant_role(event) == :not_approved &&
role == :not_approved -> role == :not_approved ->
Scheduler.pending_participation_notification(event) Scheduler.pending_participation_notification(event)
{:ok, activity_data, participant} {:ok, activity_data, participant}
@ -194,6 +228,7 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Events do
end end
# Prepare and sanitize arguments for events # Prepare and sanitize arguments for events
@spec prepare_args_for_event(map) :: map
defp prepare_args_for_event(args) do defp prepare_args_for_event(args) do
# If title is not set: we are not updating it # If title is not set: we are not updating it
args = args =

View file

@ -1,14 +1,18 @@
defmodule Mobilizon.Federation.ActivityPub.Types.Members do defmodule Mobilizon.Federation.ActivityPub.Types.Members do
@moduledoc false @moduledoc false
alias Mobilizon.Actors alias Mobilizon.Actors
alias Mobilizon.Actors.{Actor, Member} alias Mobilizon.Actors.{Actor, Member, MemberRole}
alias Mobilizon.Federation.ActivityPub alias Mobilizon.Federation.ActivityPub.Actions
alias Mobilizon.Federation.ActivityStream
alias Mobilizon.Federation.ActivityStream.Convertible alias Mobilizon.Federation.ActivityStream.Convertible
alias Mobilizon.Service.Activity.Member, as: MemberActivity alias Mobilizon.Service.Activity.Member, as: MemberActivity
alias Mobilizon.Web.Endpoint alias Mobilizon.Web.Endpoint
require Logger require Logger
import Mobilizon.Federation.ActivityPub.Utils, only: [make_update_data: 2] 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( def update(
%Member{ %Member{
parent: %Actor{id: group_id} = group, parent: %Actor{id: group_id} = group,
@ -19,43 +23,51 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Members do
%{role: updated_role} = args, %{role: updated_role} = args,
%{moderator: %Actor{url: moderator_url, id: moderator_id} = moderator} = additional %{moderator: %Actor{url: moderator_url, id: moderator_id} = moderator} = additional
) do ) do
with additional <- Map.delete(additional, :moderator), additional = Map.delete(additional, :moderator)
{:has_rights_to_update_role, {:ok, %Member{role: moderator_role}}}
when moderator_role in [:moderator, :administrator, :creator] <- case Actors.get_member(moderator_id, group_id) do
{:has_rights_to_update_role, Actors.get_member(moderator_id, group_id)}, {:error, :member_not_found} ->
{:is_only_admin, false} <- {:error, :member_not_found}
{:is_only_admin, check_admins_left(member_id, group_id, current_role, updated_role)},
{:ok, %Member{} = member} <- {:ok, %Member{role: moderator_role}}
Actors.update_member(old_member, args), when moderator_role in [:moderator, :administrator, :creator] ->
{:ok, _} <- 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, MemberActivity.insert_activity(member,
old_member: old_member, old_member: old_member,
moderator: moderator, moderator: moderator,
subject: "member_updated" subject: "member_updated"
), )
Absinthe.Subscription.publish(Endpoint, actor, Absinthe.Subscription.publish(Endpoint, actor,
group_membership_changed: [Actor.preferred_username_and_domain(group), actor_id] group_membership_changed: [Actor.preferred_username_and_domain(group), actor_id]
), )
{:ok, true} <- Cachex.del(:activity_pub, "member_#{member_id}"),
member_as_data <- Cachex.del(:activity_pub, "member_#{member_id}")
Convertible.model_to_as(member), member_as_data = Convertible.model_to_as(member)
audience <- %{
audience = %{
"to" => [member.parent.members_url, member.actor.url], "to" => [member.parent.members_url, member.actor.url],
"cc" => [member.parent.url], "cc" => [member.parent.url],
"actor" => moderator_url, "actor" => moderator_url,
"attributedTo" => [member.parent.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} {:ok, member, update_data}
else end
err -> end
Logger.debug(inspect(err))
err
end end
end end
# Used only when a group is suspended # Used only when a group is suspended
@spec delete(Member.t(), Actor.t(), boolean(), map()) :: {:ok, Activity.t(), Member.t()}
def delete( def delete(
%Member{parent: %Actor{} = group, actor: %Actor{} = actor} = _member, %Member{parent: %Actor{} = group, actor: %Actor{} = actor} = _member,
%Actor{}, %Actor{},
@ -63,16 +75,24 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Members do
_additionnal _additionnal
) do ) do
Logger.debug("Deleting a member") 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 end
@spec actor(Member.t()) :: Actor.t() | nil
def actor(%Member{actor_id: actor_id}), def actor(%Member{actor_id: actor_id}),
do: Actors.get_actor(actor_id) do: Actors.get_actor(actor_id)
@spec group_actor(Member.t()) :: Actor.t() | nil
def group_actor(%Member{parent_id: parent_id}), def group_actor(%Member{parent_id: parent_id}),
do: Actors.get_actor(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 && Actors.is_only_administrator?(member_id, group_id) && current_role == :administrator &&
updated_role != :administrator updated_role != :administrator
end end

View file

@ -4,6 +4,7 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Posts do
alias Mobilizon.Actors.Actor alias Mobilizon.Actors.Actor
alias Mobilizon.Federation.ActivityPub.{Audience, Permission} alias Mobilizon.Federation.ActivityPub.{Audience, Permission}
alias Mobilizon.Federation.ActivityPub.Types.Entity alias Mobilizon.Federation.ActivityPub.Types.Entity
alias Mobilizon.Federation.ActivityStream
alias Mobilizon.Federation.ActivityStream.Converter.Utils, as: ConverterUtils alias Mobilizon.Federation.ActivityStream.Converter.Utils, as: ConverterUtils
alias Mobilizon.Federation.ActivityStream.Convertible alias Mobilizon.Federation.ActivityStream.Convertible
alias Mobilizon.Posts.Post alias Mobilizon.Posts.Post
@ -17,6 +18,7 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Posts do
@public_ap "https://www.w3.org/ns/activitystreams#Public" @public_ap "https://www.w3.org/ns/activitystreams#Public"
@impl Entity @impl Entity
@spec create(map(), map()) :: {:ok, Post.t(), ActivityStream.t()}
def create(args, additional) do def create(args, additional) do
with args <- prepare_args(args), with args <- prepare_args(args),
{:ok, %Post{attributed_to_id: group_id, author_id: creator_id} = post} <- {:ok, %Post{attributed_to_id: group_id, author_id: creator_id} = post} <-
@ -37,6 +39,7 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Posts do
end end
@impl Entity @impl Entity
@spec update(Post.t(), map(), map()) :: {:ok, Post.t(), ActivityStream.t()}
def update(%Post{} = post, args, additional) do def update(%Post{} = post, args, additional) do
with args <- prepare_args(args), with args <- prepare_args(args),
{:ok, %Post{attributed_to_id: group_id, author_id: creator_id} = post} <- {:ok, %Post{attributed_to_id: group_id, author_id: creator_id} = post} <-
@ -60,6 +63,8 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Posts do
end end
@impl Entity @impl Entity
@spec delete(Post.t(), Actor.t(), boolean, map) ::
{:ok, ActivityStream.t(), Actor.t(), Post.t()}
def delete( def delete(
%Post{ %Post{
url: url, url: url,
@ -86,12 +91,15 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Posts do
end end
end end
@spec actor(Post.t()) :: Actor.t() | nil
def actor(%Post{author_id: author_id}), def actor(%Post{author_id: author_id}),
do: Actors.get_actor(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}), def group_actor(%Post{attributed_to_id: attributed_to_id}),
do: Actors.get_actor(attributed_to_id) do: Actors.get_actor(attributed_to_id)
@spec permissions(Post.t()) :: Permission.t()
def permissions(%Post{}) do def permissions(%Post{}) do
%Permission{ %Permission{
access: :member, access: :member,
@ -101,6 +109,7 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Posts do
} }
end end
@spec prepare_args(map()) :: map
defp prepare_args(args) do defp prepare_args(args) do
args args
|> Map.update(:tags, [], &ConverterUtils.fetch_tags/1) |> Map.update(:tags, [], &ConverterUtils.fetch_tags/1)

View file

@ -1,36 +1,47 @@
defmodule Mobilizon.Federation.ActivityPub.Types.Reports do defmodule Mobilizon.Federation.ActivityPub.Types.Reports do
@moduledoc false @moduledoc false
alias Mobilizon.{Actors, Discussions, Reports} alias Mobilizon.{Actors, Discussions, Events, Reports}
alias Mobilizon.Actors.Actor alias Mobilizon.Actors.Actor
alias Mobilizon.Events.Event
alias Mobilizon.Federation.ActivityStream
alias Mobilizon.Federation.ActivityStream.Convertible alias Mobilizon.Federation.ActivityStream.Convertible
alias Mobilizon.Reports.Report alias Mobilizon.Reports.Report
alias Mobilizon.Service.Formatter.HTML alias Mobilizon.Service.Formatter.HTML
require Logger require Logger
@spec flag(map(), boolean(), map()) ::
{:ok, Report.t(), ActivityStream.t()} | {:error, Ecto.Changeset.t()}
def flag(args, local \\ false, _additional \\ %{}) do def flag(args, local \\ false, _additional \\ %{}) do
with {:build_args, args} <- {:build_args, prepare_args_for_report(args)}, with {:ok, %Report{} = report} <- args |> prepare_args_for_report() |> Reports.create_report() do
{:create_report, {:ok, %Report{} = report}} <- report_as_data = Convertible.model_to_as(report)
{:create_report, Reports.create_report(args)}, cc = if(local, do: [report.reported.url], else: [])
report_as_data <- Convertible.model_to_as(report), report_as_data = Map.merge(report_as_data, %{"to" => [], "cc" => cc})
cc <- if(local, do: [report.reported.url], else: []), {:ok, report, report_as_data}
report_as_data <- Map.merge(report_as_data, %{"to" => [], "cc" => cc}) do
{report, report_as_data}
end end
end end
@spec prepare_args_for_report(map()) :: map()
defp prepare_args_for_report(args) do defp prepare_args_for_report(args) do
with {:reporter, %Actor{} = reporter_actor} <- %Actor{} = reporter_actor = Actors.get_actor!(args.reporter_id)
{:reporter, Actors.get_actor!(args.reporter_id)}, %Actor{} = reported_actor = Actors.get_actor!(args.reported_id)
{:reported, %Actor{} = reported_actor} <- content = HTML.strip_tags(args.content)
{:reported, Actors.get_actor!(args.reported_id)},
content <- HTML.strip_tags(args.content), event_id = Map.get(args, :event_id)
event <- Discussions.get_comment(Map.get(args, :event_id)),
{:get_report_comments, comments} <- event =
{:get_report_comments, 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( Discussions.list_comments_by_actor_and_ids(
reported_actor.id, reported_actor.id,
Map.get(args, :comments_ids, []) Map.get(args, :comments_ids, [])
)} do )
Map.merge(args, %{ Map.merge(args, %{
reporter: reporter_actor, reporter: reporter_actor,
reported: reported_actor, reported: reported_actor,
@ -40,4 +51,3 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Reports do
}) })
end end
end end
end

View file

@ -4,6 +4,7 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Resources do
alias Mobilizon.Actors.Actor alias Mobilizon.Actors.Actor
alias Mobilizon.Federation.ActivityPub.Permission alias Mobilizon.Federation.ActivityPub.Permission
alias Mobilizon.Federation.ActivityPub.Types.Entity alias Mobilizon.Federation.ActivityPub.Types.Entity
alias alias Mobilizon.Federation.ActivityStream
alias Mobilizon.Federation.ActivityStream.Convertible alias Mobilizon.Federation.ActivityStream.Convertible
alias Mobilizon.Resources.Resource alias Mobilizon.Resources.Resource
alias Mobilizon.Service.Activity.Resource, as: ResourceActivity alias Mobilizon.Service.Activity.Resource, as: ResourceActivity
@ -16,6 +17,9 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Resources do
@behaviour Entity @behaviour Entity
@impl 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 def create(%{type: type} = args, additional) do
args = args =
case type do case type do
@ -35,17 +39,18 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Resources do
with {:ok, with {:ok,
%Resource{actor_id: group_id, creator_id: creator_id, parent_id: parent_id} = resource} <- %Resource{actor_id: group_id, creator_id: creator_id, parent_id: parent_id} = resource} <-
Resources.create_resource(args), Resources.create_resource(args),
{:ok, _} <- ResourceActivity.insert_activity(resource, subject: "resource_created"), {:ok, %Actor{} = group, %Actor{url: creator_url} = creator} <-
{:ok, %Actor{} = group} <- Actors.get_group_by_actor_id(group_id), group_and_creator(group_id, creator_id) do
%Actor{url: creator_url} = creator <- Actors.get_actor(creator_id), ResourceActivity.insert_activity(resource, subject: "resource_created")
resource_as_data <- resource_as_data = Convertible.model_to_as(%{resource | actor: group, creator: creator})
Convertible.model_to_as(%{resource | actor: group, creator: creator}),
audience <- %{ audience = %{
"to" => [group.members_url], "to" => [group.members_url],
"cc" => [], "cc" => [],
"actor" => creator_url, "actor" => creator_url,
"attributedTo" => [creator_url] "attributedTo" => [creator_url]
} do }
create_data = create_data =
case parent_id do case parent_id do
nil -> nil ->
@ -58,14 +63,13 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Resources do
end end
{:ok, resource, create_data} {:ok, resource, create_data}
else
err ->
Logger.debug(inspect(err))
err
end end
end end
@impl Entity @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( def update(
%Resource{parent_id: old_parent_id} = old_resource, %Resource{parent_id: old_parent_id} = old_resource,
%{parent_id: parent_id} = args, %{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 def update(%Resource{} = old_resource, %{title: title} = _args, additional) do
with {:ok, %Resource{actor_id: group_id, creator_id: creator_id} = resource} <- with {:ok, %Resource{actor_id: group_id, creator_id: creator_id} = resource} <-
Resources.update_resource(old_resource, %{title: title}), 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, ResourceActivity.insert_activity(resource,
subject: "resource_renamed", subject: "resource_renamed",
old_resource: old_resource 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})
resource_as_data <-
Convertible.model_to_as(%{resource | actor: group}), audience = %{
audience <- %{
"to" => [group.members_url], "to" => [group.members_url],
"cc" => [], "cc" => [],
"actor" => creator_url, "actor" => creator_url,
"attributedTo" => [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} {:ok, resource, update_data}
else
err ->
Logger.debug(inspect(err))
err
end end
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( def move(
%Resource{parent_id: old_parent_id} = old_resource, %Resource{parent_id: old_parent_id} = old_resource,
%{parent_id: _new_parent_id} = args, %{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{actor_id: group_id, creator_id: creator_id, parent_id: new_parent_id} =
resource} <- resource} <-
Resources.update_resource(old_resource, args), Resources.update_resource(old_resource, args),
{:ok, _} <- ResourceActivity.insert_activity(resource, subject: "resource_moved"), {:ok, old_parent, new_parent} <- parents(old_parent_id, new_parent_id),
old_parent <- Resources.get_resource(old_parent_id), {:ok, %Actor{} = group, %Actor{url: creator_url}} <-
new_parent <- Resources.get_resource(new_parent_id), group_and_creator(group_id, creator_id) do
{:ok, %Actor{} = group} <- Actors.get_group_by_actor_id(group_id), ResourceActivity.insert_activity(resource, subject: "resource_moved")
%Actor{url: creator_url} <- Actors.get_actor(creator_id), resource_as_data = Convertible.model_to_as(%{resource | actor: group})
resource_as_data <-
Convertible.model_to_as(%{resource | actor: group}), audience = %{
audience <- %{
"to" => [group.members_url], "to" => [group.members_url],
"cc" => [], "cc" => [],
"actor" => creator_url, "actor" => creator_url,
"attributedTo" => [creator_url] "attributedTo" => [creator_url]
}, }
move_data <-
move_data =
make_move_data( make_move_data(
resource_as_data, resource_as_data,
old_parent, old_parent,
new_parent, new_parent,
Map.merge(audience, additional) Map.merge(audience, additional)
) do )
{:ok, resource, move_data} {:ok, resource, move_data}
else
err ->
Logger.debug(inspect(err))
err
end end
end end
@impl Entity @impl Entity
@spec delete(Resource.t(), Actor.t(), boolean, map()) ::
{:ok, ActivityStream.t(), Actor.t(), Resource.t()} | {:error, Ecto.Changeset.t()}
def delete( def delete(
%Resource{url: url, actor: %Actor{url: group_url, members_url: members_url}} = resource, %Resource{url: url, actor: %Actor{url: group_url, members_url: members_url}} = resource,
%Actor{url: actor_url} = actor, %Actor{url: actor_url} = actor,
@ -159,19 +166,50 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Resources do
"to" => [members_url] "to" => [members_url]
} }
with {:ok, _resource} <- Resources.delete_resource(resource), case Resources.delete_resource(resource) do
{:ok, _} <- ResourceActivity.insert_activity(resource, subject: "resource_deleted"), {:ok, _resource} ->
{:ok, true} <- Cachex.del(:activity_pub, "resource_#{resource.id}") do ResourceActivity.insert_activity(resource, subject: "resource_deleted")
Cachex.del(:activity_pub, "resource_#{resource.id}")
{:ok, activity_data, actor, resource} {:ok, activity_data, actor, resource}
{:error, %Ecto.Changeset{} = err} ->
{:error, err}
end end
end end
@spec actor(Todo.t()) :: Actor.t() | nil
def actor(%Resource{creator_id: creator_id}), def actor(%Resource{creator_id: creator_id}),
do: Actors.get_actor(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) def group_actor(%Resource{actor_id: actor_id}), do: Actors.get_actor(actor_id)
@spec permissions(TodoList.t()) :: Permission.t()
def permissions(%Resource{}) do def permissions(%Resource{}) do
%Permission{access: :member, create: :member, update: :member, delete: :member} %Permission{access: :member, create: :member, update: :member, delete: :member}
end 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 end

View file

@ -13,36 +13,37 @@ defmodule Mobilizon.Federation.ActivityPub.Types.TodoLists do
@behaviour Entity @behaviour Entity
@impl 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 def create(args, additional) do
with {:ok, %TodoList{actor_id: group_id} = todo_list} <- Todos.create_todo_list(args), 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), {:ok, %Actor{} = group} <- Actors.get_group_by_actor_id(group_id) do
todo_list_as_data <- Convertible.model_to_as(%{todo_list | actor: group}), todo_list_as_data = Convertible.model_to_as(%{todo_list | actor: group})
audience <- %{"to" => [group.members_url], "cc" => []}, audience = %{"to" => [group.members_url], "cc" => []}
create_data <- create_data = make_create_data(todo_list_as_data, Map.merge(audience, additional))
make_create_data(todo_list_as_data, Map.merge(audience, additional)) do
{:ok, todo_list, create_data} {:ok, todo_list, create_data}
end end
end end
@impl Entity @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 def update(%TodoList{} = old_todo_list, args, additional) do
with {:ok, %TodoList{actor_id: group_id} = todo_list} <- with {:ok, %TodoList{actor_id: group_id} = todo_list} <-
Todos.update_todo_list(old_todo_list, args), Todos.update_todo_list(old_todo_list, args),
{:ok, %Actor{} = group} <- Actors.get_group_by_actor_id(group_id), {:ok, %Actor{} = group} <- Actors.get_group_by_actor_id(group_id) do
todo_list_as_data <- todo_list_as_data = Convertible.model_to_as(%{todo_list | actor: group})
Convertible.model_to_as(%{todo_list | actor: group}), audience = %{"to" => [group.members_url], "cc" => []}
audience <- %{"to" => [group.members_url], "cc" => []}, update_data = make_update_data(todo_list_as_data, Map.merge(audience, additional))
update_data <-
make_update_data(todo_list_as_data, Map.merge(audience, additional)) do
{:ok, todo_list, update_data} {:ok, todo_list, update_data}
end end
end end
@impl Entity @impl Entity
@spec delete(TodoList.t(), Actor.t(), boolean(), map()) :: @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( def delete(
%TodoList{url: url, actor: %Actor{url: group_url}} = todo_list, %TodoList{url: url, actor: %Actor{url: group_url}} = todo_list,
%Actor{url: actor_url} = actor, %Actor{url: actor_url} = actor,
@ -59,16 +60,23 @@ defmodule Mobilizon.Federation.ActivityPub.Types.TodoLists do
"to" => [group_url] "to" => [group_url]
} }
with {:ok, _todo_list} <- Todos.delete_todo_list(todo_list), case Todos.delete_todo_list(todo_list) do
{:ok, true} <- Cachex.del(:activity_pub, "todo_list_#{todo_list.id}") do {:ok, _todo_list} ->
Cachex.del(:activity_pub, "todo_list_#{todo_list.id}")
{:ok, activity_data, actor, todo_list} {:ok, activity_data, actor, todo_list}
{:error, %Ecto.Changeset{} = err} ->
{:error, err}
end end
end end
@spec actor(TodoList.t()) :: nil
def actor(%TodoList{}), do: 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) def group_actor(%TodoList{actor_id: actor_id}), do: Actors.get_actor(actor_id)
@spec permissions(TodoList.t()) :: Permission.t()
def permissions(%TodoList{}) do def permissions(%TodoList{}) do
%Permission{access: :member, create: :member, update: :member, delete: :member} %Permission{access: :member, create: :member, update: :member, delete: :member}
end end

View file

@ -1,9 +1,12 @@
defmodule Mobilizon.Federation.ActivityPub.Types.Todos do defmodule Mobilizon.Federation.ActivityPub.Types.Todos do
@moduledoc false @moduledoc """
ActivityPub type handler for Todos
"""
alias Mobilizon.{Actors, Todos} alias Mobilizon.{Actors, Todos}
alias Mobilizon.Actors.Actor alias Mobilizon.Actors.Actor
alias Mobilizon.Federation.ActivityPub.Permission alias Mobilizon.Federation.ActivityPub.Permission
alias Mobilizon.Federation.ActivityPub.Types.Entity alias Mobilizon.Federation.ActivityPub.Types.Entity
alias Mobilizon.Federation.ActivityStream
alias Mobilizon.Federation.ActivityStream.Convertible alias Mobilizon.Federation.ActivityStream.Convertible
alias Mobilizon.Todos.{Todo, TodoList} alias Mobilizon.Todos.{Todo, TodoList}
import Mobilizon.Federation.ActivityPub.Utils, only: [make_create_data: 2, make_update_data: 2] 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 @behaviour Entity
@impl 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 def create(args, additional) do
with {:ok, %Todo{todo_list_id: todo_list_id, creator_id: creator_id} = todo} <- with {:ok, %Todo{todo_list_id: todo_list_id, creator_id: creator_id} = todo} <-
Todos.create_todo(args), Todos.create_todo(args),
%TodoList{actor_id: group_id} = todo_list <- Todos.get_todo_list(todo_list_id), {:ok, %Actor{} = creator, %TodoList{} = todo_list, %Actor{} = group} <-
%Actor{} = creator <- Actors.get_actor(creator_id), creator_todo_list_and_group(creator_id, todo_list_id) do
{:ok, %Actor{} = group} <- Actors.get_group_by_actor_id(group_id), todo = %{todo | todo_list: %{todo_list | actor: group}, creator: creator}
todo <- %{todo | todo_list: %{todo_list | actor: group}, creator: creator}, todo_as_data = Convertible.model_to_as(todo)
todo_as_data <- audience = %{"to" => [group.members_url], "cc" => []}
Convertible.model_to_as(todo), create_data = make_create_data(todo_as_data, Map.merge(audience, additional))
audience <- %{"to" => [group.members_url], "cc" => []},
create_data <-
make_create_data(todo_as_data, Map.merge(audience, additional)) do
{:ok, todo, create_data} {:ok, todo, create_data}
end end
end end
@impl Entity @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 def update(%Todo{} = old_todo, args, additional) do
with {:ok, %Todo{todo_list_id: todo_list_id} = todo} <- Todos.update_todo(old_todo, args), 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, %TodoList{} = todo_list, %Actor{} = group} <- todo_list_and_group(todo_list_id) do
{: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}})
todo_as_data <- audience = %{"to" => [group.members_url], "cc" => []}
Convertible.model_to_as(%{todo | todo_list: %{todo_list | actor: group}}), update_data = make_update_data(todo_as_data, Map.merge(audience, additional))
audience <- %{"to" => [group.members_url], "cc" => []},
update_data <-
make_update_data(todo_as_data, Map.merge(audience, additional)) do
{:ok, todo, update_data} {:ok, todo, update_data}
end end
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 @impl Entity
@spec delete(Todo.t(), Actor.t(), boolean(), map()) :: @spec delete(Todo.t(), Actor.t(), any(), any()) ::
{:ok, ActivityStream.t(), Actor.t(), Todo.t()} {:ok, ActivityStream.t(), Actor.t(), Todo.t()} | {:error, Ecto.Changeset.t()}
def delete( def delete(
%Todo{url: url, creator: %Actor{url: group_url}} = todo, %Todo{url: url, creator: %Actor{url: group_url}} = todo,
%Actor{url: actor_url} = actor, %Actor{url: actor_url} = actor,
@ -59,18 +96,24 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Todos do
"actor" => actor_url, "actor" => actor_url,
"type" => "Delete", "type" => "Delete",
"object" => Convertible.model_to_as(url), "object" => Convertible.model_to_as(url),
"id" => url <> "/delete", "id" => "#{url}/delete",
"to" => [group_url] "to" => [group_url]
} }
with {:ok, _todo} <- Todos.delete_todo(todo), case Todos.delete_todo(todo) do
{:ok, true} <- Cachex.del(:activity_pub, "todo_#{todo.id}") do {:ok, _todo} ->
Cachex.del(:activity_pub, "todo_#{todo.id}")
{:ok, activity_data, actor, todo} {:ok, activity_data, actor, todo}
{:error, %Ecto.Changeset{} = err} ->
{:error, err}
end end
end end
@spec actor(Todo.t()) :: Actor.t() | nil
def actor(%Todo{creator_id: creator_id}), do: Actors.get_actor(creator_id) 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 def group_actor(%Todo{todo_list_id: todo_list_id}) do
case Todos.get_todo_list(todo_list_id) do case Todos.get_todo_list(todo_list_id) do
%TodoList{actor_id: group_id} -> %TodoList{actor_id: group_id} ->
@ -81,6 +124,7 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Todos do
end end
end end
@spec permissions(Todo.t()) :: Permission.t()
def permissions(%Todo{}) do def permissions(%Todo{}) do
%Permission{access: :member, create: :member, update: :member, delete: :member} %Permission{access: :member, create: :member, update: :member, delete: :member}
end end

View file

@ -4,6 +4,7 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Tombstones do
alias Mobilizon.Actors.Actor alias Mobilizon.Actors.Actor
alias Mobilizon.Federation.ActivityPub.Permission 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: %Actor{id: actor_id}}), do: Actors.get_actor(actor_id)
def actor(%Tombstone{actor_id: actor_id}) when not is_nil(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 def actor(_), do: nil
@spec group_actor(any()) :: nil
def group_actor(_), do: nil def group_actor(_), do: nil
@spec permissions(any()) :: Permission.t()
def permissions(_) do def permissions(_) do
%Permission{access: nil, create: nil, update: nil, delete: nil} %Permission{access: nil, create: nil, update: nil, delete: nil}
end end

View file

@ -12,8 +12,7 @@ defmodule Mobilizon.Federation.ActivityPub.Utils do
alias Mobilizon.Actors.Actor alias Mobilizon.Actors.Actor
alias Mobilizon.Medias.Media alias Mobilizon.Medias.Media
alias Mobilizon.Federation.ActivityPub alias Mobilizon.Federation.ActivityPub.{Actions, Activity, Federator, Relay}
alias Mobilizon.Federation.ActivityPub.{Activity, Federator, Relay}
alias Mobilizon.Federation.ActivityPub.Actor, as: ActivityPubActor alias Mobilizon.Federation.ActivityPub.Actor, as: ActivityPubActor
alias Mobilizon.Federation.ActivityStream.Converter alias Mobilizon.Federation.ActivityStream.Converter
alias Mobilizon.Federation.HTTPSignatures alias Mobilizon.Federation.HTTPSignatures
@ -23,13 +22,35 @@ defmodule Mobilizon.Federation.ActivityPub.Utils do
@actor_types ["Group", "Person", "Application"] @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, # 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. # 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" => id}), do: id
def get_url(id) when is_binary(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(ids) when is_list(ids), do: get_url(hd(ids))
def get_url(_), do: nil def get_url(_), do: nil
@spec make_json_ld_header :: map()
def make_json_ld_header do def make_json_ld_header do
%{ %{
"@context" => [ "@context" => [
@ -99,6 +120,7 @@ defmodule Mobilizon.Federation.ActivityPub.Utils do
} }
end end
@spec make_date :: String.t()
def make_date do def make_date do
DateTime.utc_now() |> DateTime.truncate(:second) |> DateTime.to_iso8601() DateTime.utc_now() |> DateTime.truncate(:second) |> DateTime.to_iso8601()
end end
@ -106,6 +128,7 @@ defmodule Mobilizon.Federation.ActivityPub.Utils do
@doc """ @doc """
Enqueues an activity for federation if it's local Enqueues an activity for federation if it's local
""" """
@spec maybe_federate(activity :: Activity.t()) :: :ok
def maybe_federate(%Activity{local: true} = activity) do def maybe_federate(%Activity{local: true} = activity) do
Logger.debug("Maybe federate an activity") 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, 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. 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(activity, attributed_to \\ nil)
def maybe_relay_if_group_activity( def maybe_relay_if_group_activity(
@ -144,7 +168,7 @@ defmodule Mobilizon.Federation.ActivityPub.Utils do
%Activity{data: %{"object" => object}}, %Activity{data: %{"object" => object}},
%Actor{url: attributed_to_url} %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) do_maybe_relay_if_group_activity(object, attributed_to_url)
end end
@ -152,6 +176,7 @@ defmodule Mobilizon.Federation.ActivityPub.Utils do
:ok :ok
end 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), 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)) 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 case Actors.get_local_group_by_url(attributed_to) do
%Actor{} = group -> %Actor{} = group ->
case ActivityPub.announce(group, object, id, true, false) do case Actions.Announce.announce(group, object, id, true, false) do
{:ok, _activity, _object} -> {:ok, _activity, _object} ->
Logger.info("Forwarded activity to external members of the group") Logger.info("Forwarded activity to external members of the group")
:ok :ok
_ -> {:error, _err} ->
Logger.info("Failed to forward activity to external members of the group") Logger.info("Failed to forward activity to external members of the group")
:error :error
end end
_ -> nil ->
:ok :ok
end end
end end
@ -198,6 +223,7 @@ defmodule Mobilizon.Federation.ActivityPub.Utils do
Adds an id and a published data if they aren't there, Adds an id and a published data if they aren't there,
also adds it to an included object also adds it to an included object
""" """
@spec lazy_put_activity_defaults(map()) :: map()
def lazy_put_activity_defaults(%{"object" => _object} = map) do def lazy_put_activity_defaults(%{"object" => _object} = map) do
if is_map(map["object"]) do if is_map(map["object"]) do
object = lazy_put_object_defaults(map["object"]) 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) Map.put_new_lazy(map, "published", &make_date/0)
end end
@spec get_actor(map()) :: String.t() | nil
def get_actor(%{"actor" => actor}) when is_binary(actor) do def get_actor(%{"actor" => actor}) when is_binary(actor) do
actor actor
end 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) 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, %{"type" => "Tombstone", "id" => tombstone_id}), do: id == tombstone_id
def origin_check?(id, %{"actor" => actor, "attributedTo" => _attributed_to} = params) 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) compare_uris?(uri_1, uri_2)
end end
@spec compare_uris?(URI.t(), URI.t()) :: boolean()
defp compare_uris?(%URI{} = id_uri, %URI{} = other_uri), defp compare_uris?(%URI{} = id_uri, %URI{} = other_uri),
do: id_uri.host == other_uri.host && id_uri.port == other_uri.port 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} -> {:ok, media} ->
media media
_ -> {:error, _err} ->
nil nil
end end
end end
@ -509,7 +538,7 @@ defmodule Mobilizon.Federation.ActivityPub.Utils do
@doc """ @doc """
Make add activity data 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 def make_add_data(object, target, additional \\ %{}) do
Logger.debug("Making add data") Logger.debug("Making add data")
Logger.debug(inspect(object)) Logger.debug(inspect(object))
@ -530,7 +559,7 @@ defmodule Mobilizon.Federation.ActivityPub.Utils do
@doc """ @doc """
Make move activity data 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 def make_move_data(object, origin, target, additional \\ %{}) do
Logger.debug("Making move data") Logger.debug("Making move data")
Logger.debug(inspect(object)) Logger.debug(inspect(object))
@ -554,6 +583,7 @@ defmodule Mobilizon.Federation.ActivityPub.Utils do
@doc """ @doc """
Converts PEM encoded keys to a public key representation 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 def pem_to_public_key(pem) do
[key_code] = :public_key.pem_decode(pem) [key_code] = :public_key.pem_decode(pem)
key = :public_key.pem_entry_decode(key_code) key = :public_key.pem_entry_decode(key_code)
@ -567,6 +597,7 @@ defmodule Mobilizon.Federation.ActivityPub.Utils do
end end
end end
@spec pem_to_public_key_pem(String.t()) :: String.t()
def pem_to_public_key_pem(pem) do def pem_to_public_key_pem(pem) do
public_key = pem_to_public_key(pem) public_key = pem_to_public_key(pem)
public_key = :public_key.pem_entry_encode(:RSAPublicKey, public_key) public_key = :public_key.pem_entry_encode(:RSAPublicKey, public_key)

View file

@ -3,5 +3,5 @@ defmodule Mobilizon.Federation.ActivityStream do
The ActivityStream Type The ActivityStream Type
""" """
@type t :: map() @type t :: %{String.t() => String.t() | list(String.t()) | map() | nil}
end end

View file

@ -29,7 +29,7 @@ defmodule Mobilizon.Federation.ActivityStream.Converter.Actor do
Converts an AP object data to our internal data structure. Converts an AP object data to our internal data structure.
""" """
@impl Converter @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 def as_to_model_data(%{"type" => type} = data) when type in @allowed_types do
avatar = avatar =
download_picture(get_in(data, ["icon", "url"]), get_in(data, ["icon", "name"]), "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 end
def as_to_model_data(_), do: :error def as_to_model_data(_), do: {:error, :actor_not_allowed_type}
@doc """ @doc """
Convert an actor struct to an ActivityStream representation. Convert an actor struct to an ActivityStream representation.
@ -135,7 +135,7 @@ defmodule Mobilizon.Federation.ActivityStream.Converter.Actor do
end end
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(nil, _name, _default_name), do: nil
defp download_picture(url, name, default_name) do defp download_picture(url, name, default_name) do

View file

@ -38,18 +38,15 @@ defmodule Mobilizon.Federation.ActivityStream.Converter.Comment do
Converts an AP object data to our internal data structure. Converts an AP object data to our internal data structure.
""" """
@impl Converter @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 def as_to_model_data(object) do
Logger.debug("We're converting raw ActivityStream data to a comment entity") Logger.debug("We're converting raw ActivityStream data to a comment entity")
Logger.debug(inspect(object)) Logger.debug(inspect(object))
with {%Actor{id: actor_id, domain: actor_domain}, attributed_to} <- tag_object = Map.get(object, "tag", [])
maybe_fetch_actor_and_attributed_to_id(object),
{:tags, tags} <- {:tags, fetch_tags(Map.get(object, "tag", []))}, case maybe_fetch_actor_and_attributed_to_id(object) do
{:mentions, mentions} <- {:ok, %Actor{id: actor_id, domain: actor_domain}, attributed_to} ->
{:mentions, fetch_mentions(Map.get(object, "tag", []))},
discussion <-
Discussions.get_discussion_by_url(Map.get(object, "context")) do
Logger.debug("Inserting full comment") Logger.debug("Inserting full comment")
Logger.debug(inspect(object)) Logger.debug(inspect(object))
@ -64,9 +61,9 @@ defmodule Mobilizon.Federation.ActivityStream.Converter.Comment do
in_reply_to_comment_id: nil, in_reply_to_comment_id: nil,
event_id: nil, event_id: nil,
uuid: object["uuid"], uuid: object["uuid"],
discussion_id: if(is_nil(discussion), do: nil, else: discussion.id), discussion_id: get_discussion_id(object),
tags: tags, tags: fetch_tags(tag_object),
mentions: mentions, mentions: fetch_mentions(tag_object),
local: is_nil(actor_domain), local: is_nil(actor_domain),
visibility: if(Visibility.is_public?(object), do: :public, else: :private), visibility: if(Visibility.is_public?(object), do: :public, else: :private),
published_at: object["published"], published_at: object["published"],
@ -81,9 +78,9 @@ defmodule Mobilizon.Federation.ActivityStream.Converter.Comment do
Logger.debug("Converted object after fetching parents") Logger.debug("Converted object after fetching parents")
Logger.debug(inspect(data)) Logger.debug(inspect(data))
data data
else
{:ok, %Actor{suspended: true}} -> {:error, err} ->
:error {:error, err}
end end
end end
@ -94,9 +91,20 @@ defmodule Mobilizon.Federation.ActivityStream.Converter.Comment do
""" """
@impl Converter @impl Converter
@spec model_to_as(CommentModel.t()) :: map @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) 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 = %{ object = %{
"type" => "Note", "type" => "Note",
"to" => to, "to" => to,
@ -104,9 +112,7 @@ defmodule Mobilizon.Federation.ActivityStream.Converter.Comment do
"content" => comment.text, "content" => comment.text,
"mediaType" => "text/html", "mediaType" => "text/html",
"actor" => comment.actor.url, "actor" => comment.actor.url,
"attributedTo" => "attributedTo" => attributed_to,
if(is_nil(comment.attributed_to), do: nil, else: comment.attributed_to.url) ||
comment.actor.url,
"uuid" => comment.uuid, "uuid" => comment.uuid,
"id" => comment.url, "id" => comment.url,
"tag" => build_mentions(comment.mentions) ++ build_tags(comment.tags), "tag" => build_mentions(comment.mentions) ++ build_tags(comment.tags),
@ -132,7 +138,6 @@ defmodule Mobilizon.Federation.ActivityStream.Converter.Comment do
end end
@impl Converter @impl Converter
@spec model_to_as(CommentModel.t()) :: map
def model_to_as(%CommentModel{} = comment) do def model_to_as(%CommentModel{} = comment) do
Convertible.model_to_as(%TombstoneModel{ Convertible.model_to_as(%TombstoneModel{
uri: comment.url, uri: comment.url,
@ -203,4 +208,13 @@ defmodule Mobilizon.Federation.ActivityStream.Converter.Comment do
data data
end end
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 end

View file

@ -8,6 +8,6 @@ defmodule Mobilizon.Federation.ActivityStream.Converter do
@type model_data :: map() @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() @callback model_to_as(model :: struct()) :: ActivityStream.t()
end end

View file

@ -12,6 +12,7 @@ defmodule Mobilizon.Federation.ActivityStream.Converter.Discussion do
alias Mobilizon.Federation.ActivityStream.{Converter, Convertible} alias Mobilizon.Federation.ActivityStream.{Converter, Convertible}
alias Mobilizon.Federation.ActivityStream.Converter.Discussion, as: DiscussionConverter alias Mobilizon.Federation.ActivityStream.Converter.Discussion, as: DiscussionConverter
alias Mobilizon.Storage.Repo alias Mobilizon.Storage.Repo
import Mobilizon.Service.Guards, only: [is_valid_string: 1]
require Logger require Logger
@ -45,20 +46,28 @@ defmodule Mobilizon.Federation.ActivityStream.Converter.Discussion do
end end
@impl Converter @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(%{"type" => "Note", "name" => name} = object) when not is_nil(name) do def as_to_model_data(%{"type" => "Note", "name" => name} = object) when is_valid_string(name) do
with creator_url <- Map.get(object, "actor"), case extract_actors(object) do
{:ok, %Actor{id: creator_id, suspended: false}} <- %{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), ActivityPubActor.get_or_fetch_actor_by_url(creator_url),
actor_url <- Map.get(object, "attributedTo"),
{:ok, %Actor{id: actor_id, suspended: false}} <- {:ok, %Actor{id: actor_id, suspended: false}} <-
ActivityPubActor.get_or_fetch_actor_by_url(actor_url) do ActivityPubActor.get_or_fetch_actor_by_url(actor_url) do
%{ %{actor_id: actor_id, creator_id: creator_id}
title: name, else
actor_id: actor_id, {:error, error} -> {:error, error}
creator_id: creator_id,
url: object["id"]
}
end end
end end
end end

View file

@ -45,19 +45,20 @@ defmodule Mobilizon.Federation.ActivityStream.Converter.Event do
Converts an AP object data to our internal data structure. Converts an AP object data to our internal data structure.
""" """
@impl Converter @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 def as_to_model_data(object) do
with {%Actor{id: actor_id}, attributed_to} <- case maybe_fetch_actor_and_attributed_to_id(object) do
maybe_fetch_actor_and_attributed_to_id(object), {:ok, %Actor{id: actor_id}, attributed_to} ->
{:address, address_id} <- address_id = get_address(object["location"])
{:address, get_address(object["location"])}, tags = fetch_tags(object["tag"])
{:tags, tags} <- {:tags, fetch_tags(object["tag"])}, mentions = fetch_mentions(object["tag"])
{:mentions, mentions} <- {:mentions, fetch_mentions(object["tag"])}, visibility = get_visibility(object)
{:visibility, visibility} <- {:visibility, get_visibility(object)}, options = get_options(object)
{:options, options} <- {:options, get_options(object)}, metadata = get_metdata(object)
{:metadata, metadata} <- {:metadata, get_metdata(object)},
[description: description, picture_id: picture_id, medias: medias] <- [description: description, picture_id: picture_id, medias: medias] =
process_pictures(object, actor_id) do process_pictures(object, actor_id)
%{ %{
title: object["name"], title: object["name"],
description: description, description: description,
@ -86,9 +87,9 @@ defmodule Mobilizon.Federation.ActivityStream.Converter.Event do
publish_at: object["published"], publish_at: object["published"],
language: object["inLanguage"] language: object["inLanguage"]
} }
else
{:ok, %Actor{suspended: true}} -> {:error, err} ->
:error {:error, err}
end end
end end

View file

@ -4,9 +4,11 @@ defmodule Mobilizon.Federation.ActivityStream.Converter.EventMetadata do
""" """
alias Mobilizon.Events.EventMetadata alias Mobilizon.Events.EventMetadata
alias Mobilizon.Federation.ActivityStream
@property_value "PropertyValue" @property_value "PropertyValue"
@spec metadata_to_as(EventMetadata.t()) :: map()
def metadata_to_as(%EventMetadata{type: :boolean, value: value, key: key}) def metadata_to_as(%EventMetadata{type: :boolean, value: value, key: key})
when value in ["true", "false"] do when value in ["true", "false"] do
%{ %{
@ -47,6 +49,7 @@ defmodule Mobilizon.Federation.ActivityStream.Converter.EventMetadata do
) )
end end
@spec as_to_metadata(ActivityStream.t()) :: map()
def as_to_metadata(%{"type" => @property_value, "propertyID" => key, "value" => value}) def as_to_metadata(%{"type" => @property_value, "propertyID" => key, "value" => value})
when is_boolean(value) do when is_boolean(value) do
%{type: :boolean, key: key, value: to_string(value)} %{type: :boolean, key: key, value: to_string(value)}

View file

@ -6,6 +6,7 @@ defmodule Mobilizon.Federation.ActivityStream.Converter.Media do
internal one, and back. internal one, and back.
""" """
alias Mobilizon.Federation.ActivityStream
alias Mobilizon.Medias alias Mobilizon.Medias
alias Mobilizon.Medias.Media, as: MediaModel alias Mobilizon.Medias.Media, as: MediaModel
@ -18,7 +19,7 @@ defmodule Mobilizon.Federation.ActivityStream.Converter.Media do
@doc """ @doc """
Convert a media struct to an ActivityStream representation. 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 def model_to_as(%MediaModel{file: file}) do
%{ %{
"type" => "Document", "type" => "Document",
@ -31,29 +32,53 @@ defmodule Mobilizon.Federation.ActivityStream.Converter.Media do
@doc """ @doc """
Save media data from raw data and return AS Link data. 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), 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( def find_or_create_media(
%{"type" => "Document", "url" => media_url, "name" => name}, %{"type" => "Document", "url" => media_url, "name" => name},
actor_id actor_id
) )
when is_binary(media_url) do when is_binary(media_url) do
with {:ok, %{body: body}} <- Tesla.get(media_url, opts: @http_options), case upload_media(media_url, name) do
{:ok, %{url: url} = uploaded} <- {:error, err} ->
Upload.store(%{body: body, name: name}), {:error, err}
{:media_exists, nil} <- {:media_exists, Medias.get_media_by_url(url)} do
{:ok, %{url: url} = uploaded} ->
case Medias.get_media_by_url(url) do
%MediaModel{file: _file} = media ->
{:ok, media}
nil ->
Medias.create_media(%{ Medias.create_media(%{
file: Map.take(uploaded, [:url, :name, :content_type, :size]), file: Map.take(uploaded, [:url, :name, :content_type, :size]),
metadata: Map.take(uploaded, [:width, :height, :blurhash]), metadata: Map.take(uploaded, [:width, :height, :blurhash]),
actor_id: actor_id actor_id: actor_id
}) })
else end
{:media_exists, %MediaModel{file: _file} = media} -> end
{:ok, media} end
err -> @spec upload_media(String.t(), String.t()) :: {:ok, map()} | {:error, atom() | String.t()}
err 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 end
end end

View file

@ -33,6 +33,7 @@ defmodule Mobilizon.Federation.ActivityStream.Converter.Member do
} }
end end
@spec as_to_model_data(map()) :: map()
def as_to_model_data(%{ def as_to_model_data(%{
"type" => "Member", "type" => "Member",
"actor" => actor, "actor" => actor,

View file

@ -18,6 +18,8 @@ defmodule Mobilizon.Federation.ActivityStream.Converter.Post do
process_pictures: 2 process_pictures: 2
] ]
import Mobilizon.Service.Guards, only: [is_valid_string: 1]
@behaviour Converter @behaviour Converter
defimpl Convertible, for: Post do 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. Converts an AP object data to our internal data structure.
""" """
@impl Converter @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( def as_to_model_data(
%{"type" => "Article", "actor" => creator, "attributedTo" => group_uri} = object %{"type" => "Article", "actor" => creator, "attributedTo" => group_uri} = object
) do ) do
with {:ok, %Actor{id: attributed_to_id} = group} <- get_actor(group_uri), with {:ok, %Actor{id: attributed_to_id} = group} <- get_actor(group_uri),
{:ok, %Actor{id: author_id}} <- get_actor(creator), {:ok, %Actor{id: author_id}} <- get_actor(creator) do
{:visibility, visibility} <- {:visibility, get_visibility(object, group)}, [description: description, picture_id: picture_id, medias: medias] =
[description: description, picture_id: picture_id, medias: medias] <- process_pictures(object, attributed_to_id)
process_pictures(object, attributed_to_id) do
%{ %{
title: object["name"], title: object["name"],
body: description, body: description,
@ -82,7 +84,7 @@ defmodule Mobilizon.Federation.ActivityStream.Converter.Post do
publish_at: object["published"], publish_at: object["published"],
picture_id: picture_id, picture_id: picture_id,
medias: medias, medias: medias,
visibility: visibility, visibility: get_visibility(object, group),
draft: object["draft"] == true draft: object["draft"] == true
} }
else else
@ -92,11 +94,12 @@ defmodule Mobilizon.Federation.ActivityStream.Converter.Post do
end end
@spec get_actor(String.t() | map() | nil) :: {:ok, Actor.t()} | {:error, String.t()} @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) when is_valid_string(actor),
defp get_actor(actor),
do: actor |> Utils.get_url() |> ActivityPubActor.get_or_fetch_actor_by_url() 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(nil), do: nil
defp to_date(%DateTime{} = date), do: DateTime.to_iso8601(date) defp to_date(%DateTime{} = date), do: DateTime.to_iso8601(date)
defp to_date(%NaiveDateTime{} = date), do: NaiveDateTime.to_iso8601(date) defp to_date(%NaiveDateTime{} = date), do: NaiveDateTime.to_iso8601(date)

View file

@ -56,18 +56,17 @@ defmodule Mobilizon.Federation.ActivityStream.Converter.Resource do
Converts an AP object data to our internal data structure. Converts an AP object data to our internal data structure.
""" """
@impl Converter @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 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), with {:ok, %Actor{id: actor_id, resources_url: resources_url}} <- get_actor(group),
{:ok, %Actor{id: creator_id}} <- get_actor(creator), {:ok, %Actor{id: creator_id}} <- get_actor(creator) do
parent_id <- get_parent_id(object["context"], resources_url) do
data = %{ data = %{
title: object["name"], title: object["name"],
summary: object["summary"], summary: object["summary"],
url: object["id"], url: object["id"],
actor_id: actor_id, actor_id: actor_id,
creator_id: creator_id, creator_id: creator_id,
parent_id: parent_id, parent_id: get_parent_id(object["context"], resources_url),
published_at: object["published"] published_at: object["published"]
} }

View file

@ -47,14 +47,14 @@ defmodule Mobilizon.Federation.ActivityStream.Converter.Todo do
Converts an AP object data to our internal data structure. Converts an AP object data to our internal data structure.
""" """
@impl Converter @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( def as_to_model_data(
%{"type" => "Todo", "actor" => actor_url, "todoList" => todo_list_url} = object %{"type" => "Todo", "actor" => actor_url, "todoList" => todo_list_url} = object
) do ) do
with {:ok, %Actor{id: creator_id} = _creator} <- case ActivityPubActor.get_or_fetch_actor_by_url(actor_url) do
ActivityPubActor.get_or_fetch_actor_by_url(actor_url), {:ok, %Actor{id: creator_id} = _creator} ->
{:todo_list, %TodoList{id: todo_list_id}} <- case Todos.get_todo_list_by_url(todo_list_url) do
{:todo_list, Todos.get_todo_list_by_url(todo_list_url)} do %TodoList{id: todo_list_id} ->
%{ %{
title: object["name"], title: object["name"],
status: object["status"], status: object["status"],
@ -63,11 +63,22 @@ defmodule Mobilizon.Federation.ActivityStream.Converter.Todo do
creator_id: creator_id, creator_id: creator_id,
published_at: object["published"] published_at: object["published"]
} }
else
{:todo_list, nil} -> nil ->
with {:ok, %TodoList{}} <- ActivityPub.fetch_object_from_url(todo_list_url) do case ActivityPub.fetch_object_from_url(todo_list_url) do
{:ok, _, %TodoList{}} ->
as_to_model_data(object) as_to_model_data(object)
end
{:ok, %TodoList{}} ->
as_to_model_data(object)
{:error, err} ->
{:error, err}
end
end
{:error, err} ->
{:error, err}
end end
end end
end end

View file

@ -37,7 +37,7 @@ defmodule Mobilizon.Federation.ActivityStream.Converter.TodoList do
Converts an AP object data to our internal data structure. Converts an AP object data to our internal data structure.
""" """
@impl Converter @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 def as_to_model_data(%{"type" => "TodoList", "actor" => actor_url} = object) do
case ActivityPubActor.get_or_fetch_actor_by_url(actor_url) do case ActivityPubActor.get_or_fetch_actor_by_url(actor_url) do
{:ok, %Actor{type: :Group, id: group_id} = _group} -> {:ok, %Actor{type: :Group, id: group_id} = _group} ->

View file

@ -111,7 +111,6 @@ defmodule Mobilizon.Federation.ActivityStream.Converter.Utils do
acc ++ [%{actor_id: actor_id}] acc ++ [%{actor_id: actor_id}]
end end
@spec create_mention(map(), list()) :: list()
defp create_mention(mention, acc) when is_map(mention) do defp create_mention(mention, acc) when is_map(mention) do
with true <- mention["type"] == "Mention", with true <- mention["type"] == "Mention",
{:ok, %Actor{id: actor_id}} <- {:ok, %Actor{id: actor_id}} <-
@ -128,22 +127,34 @@ defmodule Mobilizon.Federation.ActivityStream.Converter.Utils do
create_mention(mention, acc) create_mention(mention, acc)
end 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(%{ def maybe_fetch_actor_and_attributed_to_id(%{
"actor" => actor_url, "actor" => actor_url,
"attributedTo" => attributed_to_url "attributedTo" => attributed_to_url
}) })
when is_nil(attributed_to_url) do 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 end
@spec maybe_fetch_actor_and_attributed_to_id(map()) :: {Actor.t() | nil, Actor.t() | nil}
def maybe_fetch_actor_and_attributed_to_id(%{ def maybe_fetch_actor_and_attributed_to_id(%{
"actor" => actor_url, "actor" => actor_url,
"attributedTo" => attributed_to_url "attributedTo" => attributed_to_url
}) })
when is_nil(actor_url) do 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 end
# Only when both actor and attributedTo fields are both filled is when we can return both # 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 "attributedTo" => attributed_to_url
}) })
when actor_url != attributed_to_url do when actor_url != attributed_to_url do
with actor <- fetch_actor(actor_url), with {:ok, %Actor{} = actor} <- fetch_actor(actor_url),
attributed_to <- fetch_actor(attributed_to_url) do {:ok, %Actor{} = attributed_to} <- fetch_actor(attributed_to_url) do
{actor, attributed_to} {:ok, actor, attributed_to}
else
{:error, err} ->
{:error, err}
end end
end end
@ -162,16 +176,25 @@ defmodule Mobilizon.Federation.ActivityStream.Converter.Utils do
def maybe_fetch_actor_and_attributed_to_id(%{ def maybe_fetch_actor_and_attributed_to_id(%{
"attributedTo" => attributed_to_url "attributedTo" => attributed_to_url
}) do }) 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 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 defp fetch_actor(actor_url) do
with {:ok, %Actor{suspended: false} = actor} <- case ActivityPubActor.get_or_fetch_actor_by_url(actor_url) do
ActivityPubActor.get_or_fetch_actor_by_url(actor_url) do {:ok, %Actor{suspended: false} = actor} ->
actor {:ok, actor}
{:ok, %Actor{suspended: true} = _actor} ->
{:error, :actor_suspended}
{:error, err} ->
{:error, err}
end end
end end
@ -203,12 +226,17 @@ defmodule Mobilizon.Federation.ActivityStream.Converter.Utils do
|> Map.new() |> Map.new()
picture_id = picture_id =
with banner when is_map(banner) <- get_banner_picture(attachements), case get_banner_picture(attachements) do
{:ok, %Media{id: picture_id}} <- banner when is_map(banner) ->
MediaConverter.find_or_create_media(banner, actor_id) do case MediaConverter.find_or_create_media(banner, actor_id) do
{:error, _err} ->
nil
{:ok, %Media{id: picture_id}} ->
picture_id picture_id
else end
_err ->
_ ->
nil nil
end end

View file

@ -19,7 +19,7 @@ defmodule Mobilizon.Federation.HTTPSignatures.Signature do
@spec key_id_to_actor_url(String.t()) :: String.t() @spec key_id_to_actor_url(String.t()) :: String.t()
def key_id_to_actor_url(key_id) do def key_id_to_actor_url(key_id) do
%{path: path} = %URI{path: path} =
uri = uri =
key_id key_id
|> URI.parse() |> URI.parse()
@ -29,7 +29,7 @@ defmodule Mobilizon.Federation.HTTPSignatures.Signature do
if is_nil(path) do if is_nil(path) do
uri uri
else else
Map.put(uri, :path, String.trim_trailing(path, "/publickey")) %URI{uri | path: String.trim_trailing(path, "/publickey")}
end end
URI.to_string(uri) URI.to_string(uri)
@ -78,15 +78,25 @@ defmodule Mobilizon.Federation.HTTPSignatures.Signature do
end end
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 def fetch_public_key(conn) do
with %{"keyId" => kid} <- HTTPSignatures.signature_for_conn(conn), with %{"keyId" => kid} <- HTTPSignatures.signature_for_conn(conn),
actor_id <- key_id_to_actor_url(kid), actor_id <- key_id_to_actor_url(kid),
:ok <- Logger.debug("Fetching public key for #{actor_id}"), :ok <- Logger.debug("Fetching public key for #{actor_id}"),
{:ok, public_key} <- get_public_key_for_url(actor_id) do {:ok, public_key} <- get_public_key_for_url(actor_id) do
{:ok, public_key} {:ok, public_key}
else
{:error, err} ->
{:error, err}
end end
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 def refetch_public_key(conn) do
with %{"keyId" => kid} <- HTTPSignatures.signature_for_conn(conn), with %{"keyId" => kid} <- HTTPSignatures.signature_for_conn(conn),
actor_id <- key_id_to_actor_url(kid), 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, _actor} <- ActivityPubActor.make_actor_from_url(actor_id),
{:ok, public_key} <- get_public_key_for_url(actor_id) do {:ok, public_key} <- get_public_key_for_url(actor_id) do
{:ok, public_key} {:ok, public_key}
else
{:error, err} ->
{:error, err}
end end
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 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("Signing a payload on behalf of #{actor.url}")
Logger.debug("headers") 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}" raise ArgumentError, message: "Can't do a signature on remote actor #{url}"
end end
@spec generate_date_header :: String.t()
def generate_date_header, do: generate_date_header(NaiveDateTime.utc_now()) def generate_date_header, do: generate_date_header(NaiveDateTime.utc_now())
def generate_date_header(%NaiveDateTime{} = date) do def generate_date_header(%NaiveDateTime{} = date) do
Timex.format!(date, "{WDshort}, {0D} {Mshort} {YYYY} {h24}:{m}:{s} GMT") Timex.format!(date, "{WDshort}, {0D} {Mshort} {YYYY} {h24}:{m}:{s} GMT")
end end
@spec generate_request_target(String.t(), String.t()) :: String.t()
def generate_request_target(method, path), do: "#{method} #{path}" def generate_request_target(method, path), do: "#{method} #{path}"
@spec build_digest(String.t()) :: String.t()
def build_digest(body) do def build_digest(body) do
"SHA-256=#{:sha256 |> :crypto.hash(body) |> Base.encode64()}" "SHA-256=#{:sha256 |> :crypto.hash(body) |> Base.encode64()}"
end end

View file

@ -19,11 +19,15 @@ defmodule Mobilizon.Federation.WebFinger do
require Logger require Logger
import SweetXml 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 def host_meta do
base_url = Endpoint.url() base_url = Endpoint.url()
%URI{host: host} = URI.parse(base_url) %URI{host: host} = URI.parse(base_url)
{ XmlBuilder.to_doc({
:XRD, :XRD,
%{ %{
xmlns: "http://docs.oasis-open.org/ns/xri/xrd-1.0", xmlns: "http://docs.oasis-open.org/ns/xri/xrd-1.0",
@ -43,10 +47,13 @@ defmodule Mobilizon.Federation.WebFinger do
} }
} }
] ]
} })
|> XmlBuilder.to_doc()
end 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 def webfinger(resource, "JSON") do
host = Endpoint.host() host = Endpoint.host()
regex = ~r/(acct:)?(?<name>\w+)@#{host}/ regex = ~r/(acct:)?(?<name>\w+)@#{host}/
@ -61,15 +68,18 @@ defmodule Mobilizon.Federation.WebFinger do
{:ok, represent_actor(actor, "JSON")} {:ok, represent_actor(actor, "JSON")}
_e -> _e ->
{:error, "Couldn't find actor"} {:error, :actor_not_found}
end end
end 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") 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 def represent_actor(%Actor{} = actor, "JSON") do
links = links =
[ [
@ -89,6 +99,7 @@ defmodule Mobilizon.Federation.WebFinger do
} }
end 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 defp maybe_add_avatar(data, %Actor{avatar: avatar}) when not is_nil(avatar) do
data ++ data ++
[ [
@ -102,6 +113,7 @@ defmodule Mobilizon.Federation.WebFinger do
defp maybe_add_avatar(data, _actor), do: data 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 defp maybe_add_profile_page(data, %Actor{type: :Group, url: url}) do
data ++ data ++
[ [
@ -115,37 +127,76 @@ defmodule Mobilizon.Federation.WebFinger do
defp maybe_add_profile_page(data, _actor), do: data defp maybe_add_profile_page(data, _actor), do: data
@type finger_errors ::
:host_not_found | :address_invalid | :http_error | :webfinger_information_not_json
@doc """ @doc """
Finger an actor to retreive it's ActivityPub ID/URL 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 def finger(actor) do
actor = String.trim_leading(actor, "@") actor = String.trim_leading(actor, "@")
with address when is_binary(address) <- apply_webfinger_endpoint(actor), case validate_endpoint(actor) do
false <- address_invalid(address), {:ok, address} ->
{:ok, %{body: body, status: code}} when code in 200..299 <- case fetch_webfinger_data(address) do
WebfingerClient.get(address), {:ok, %{"url" => url}} ->
{:ok, %{"url" => url}} <- webfinger_from_json(body) do
{:ok, url} {:ok, url}
else
e -> {:error, err} ->
Logger.debug("Couldn't finger #{actor}") Logger.debug("Couldn't process webfinger data for #{actor}")
Logger.debug(inspect(e)) {:error, err}
{:error, e} end
{:error, err} ->
Logger.debug("Couldn't find webfinger endpoint for #{actor}")
{:error, err}
end end
end end
@doc """ @spec fetch_webfinger_data(String.t()) ::
Fetches the Extensible Resource Descriptor endpoint `/.well-known/host-meta` to find the Webfinger endpoint (usually `/.well-known/webfinger?resource=`) {:ok, map()} | {:error, :webfinger_information_not_json | :http_error}
""" defp fetch_webfinger_data(address) do
@spec find_webfinger_endpoint(String.t()) :: String.t() case WebfingerClient.get(address) do
def find_webfinger_endpoint(domain) when is_binary(domain) 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"), 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 link_template when is_binary(link_template) <- find_link_from_template(body) do
{:ok, link_template} {:ok, link_template}
else
{:error, :link_not_found} -> {:error, :link_not_found}
{:error, error} -> {:error, error}
end end
end end

View file

@ -5,42 +5,54 @@
defmodule Mobilizon.Federation.WebFinger.XmlBuilder do defmodule Mobilizon.Federation.WebFinger.XmlBuilder do
@moduledoc """ @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) open_tag = make_open_tag(tag, attributes)
content_xml = to_xml(content) content_xml = to_xml(content)
"<#{open_tag}>#{content_xml}</#{tag}>" "<#{open_tag}>#{content_xml}</#{tag}>"
end end
def to_xml({tag, %{} = attributes}) do defp to_xml({tag, %{} = attributes}) do
open_tag = make_open_tag(tag, attributes) open_tag = make_open_tag(tag, attributes)
"<#{open_tag} />" "<#{open_tag} />"
end 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 content
|> Enum.map(&to_xml/1) |> Enum.map(&to_xml/1)
|> Enum.join() |> Enum.join()
end end
def to_xml(%NaiveDateTime{} = time), do: NaiveDateTime.to_iso8601(time) defp 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)
@spec make_open_tag(tag :: atom, attributes :: map()) :: String.t()
defp make_open_tag(tag, attributes) do defp make_open_tag(tag, attributes) do
attributes_string = attributes_string =
attributes attributes
|> Enum.map(fn {attribute, value} -> "#{attribute}=\"#{value}\"" end) |> Enum.map(fn {attribute, value} -> "#{attribute}=\"#{value}\"" end)
|> Enum.join(" ") |> Enum.join(" ")
[tag, attributes_string] |> Enum.join(" ") |> String.trim() [to_string(tag), attributes_string] |> Enum.join(" ") |> String.trim()
end end
end end

View file

@ -5,8 +5,7 @@ defmodule Mobilizon.GraphQL.API.Comments do
alias Mobilizon.Actors.Actor alias Mobilizon.Actors.Actor
alias Mobilizon.Discussions.Comment alias Mobilizon.Discussions.Comment
alias Mobilizon.Federation.ActivityPub alias Mobilizon.Federation.ActivityPub.{Actions, Activity}
alias Mobilizon.Federation.ActivityPub.Activity
alias Mobilizon.GraphQL.API.Utils alias Mobilizon.GraphQL.API.Utils
@doc """ @doc """
@ -15,7 +14,7 @@ defmodule Mobilizon.GraphQL.API.Comments do
@spec create_comment(map) :: {:ok, Activity.t(), Comment.t()} | any @spec create_comment(map) :: {:ok, Activity.t(), Comment.t()} | any
def create_comment(args) do def create_comment(args) do
args = extract_pictures_from_comment_body(args) args = extract_pictures_from_comment_body(args)
ActivityPub.create(:comment, args, true) Actions.Create.create(:comment, args, true)
end end
@doc """ @doc """
@ -24,7 +23,7 @@ defmodule Mobilizon.GraphQL.API.Comments do
@spec update_comment(Comment.t(), map()) :: {:ok, Activity.t(), Comment.t()} | any @spec update_comment(Comment.t(), map()) :: {:ok, Activity.t(), Comment.t()} | any
def update_comment(%Comment{} = comment, args) do def update_comment(%Comment{} = comment, args) do
args = extract_pictures_from_comment_body(args) args = extract_pictures_from_comment_body(args)
ActivityPub.update(comment, args, true) Actions.Update.update(comment, args, true)
end end
@doc """ @doc """
@ -32,7 +31,7 @@ defmodule Mobilizon.GraphQL.API.Comments do
""" """
@spec delete_comment(Comment.t(), Actor.t()) :: {:ok, Activity.t(), Comment.t()} | any @spec delete_comment(Comment.t(), Actor.t()) :: {:ok, Activity.t(), Comment.t()} | any
def delete_comment(%Comment{} = comment, %Actor{} = actor) do def delete_comment(%Comment{} = comment, %Actor{} = actor) do
ActivityPub.delete(comment, actor, true) Actions.Delete.delete(comment, actor, true)
end end
@doc """ @doc """
@ -42,7 +41,7 @@ defmodule Mobilizon.GraphQL.API.Comments do
def create_discussion(args) do def create_discussion(args) do
args = extract_pictures_from_comment_body(args) args = extract_pictures_from_comment_body(args)
ActivityPub.create( Actions.Create.create(
:discussion, :discussion,
args, args,
true true

View file

@ -6,8 +6,7 @@ defmodule Mobilizon.GraphQL.API.Events do
alias Mobilizon.Actors.Actor alias Mobilizon.Actors.Actor
alias Mobilizon.Events.Event alias Mobilizon.Events.Event
alias Mobilizon.Federation.ActivityPub alias Mobilizon.Federation.ActivityPub.{Actions, Activity, Utils}
alias Mobilizon.Federation.ActivityPub.{Activity, Utils}
alias Mobilizon.GraphQL.API.Utils, as: APIUtils alias Mobilizon.GraphQL.API.Utils, as: APIUtils
@doc """ @doc """
@ -15,15 +14,8 @@ defmodule Mobilizon.GraphQL.API.Events do
""" """
@spec create_event(map) :: {:ok, Activity.t(), Event.t()} | any @spec create_event(map) :: {:ok, Activity.t(), Event.t()} | any
def create_event(args) do 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 # 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)) Actions.Create.create(:event, prepare_args(args), should_federate(args))
end
end end
@doc """ @doc """
@ -31,21 +23,26 @@ defmodule Mobilizon.GraphQL.API.Events do
""" """
@spec update_event(map, Event.t()) :: {:ok, Activity.t(), Event.t()} | any @spec update_event(map, Event.t()) :: {:ok, Activity.t(), Event.t()} | any
def update_event(args, %Event{} = event) do def update_event(args, %Event{} = event) do
with organizer_actor <- Map.get(args, :organizer_actor), Actions.Update.update(event, prepare_args(args), should_federate(args))
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
end end
@doc """ @doc """
Trigger the deletion of an event 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 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 end
defp process_picture(nil, _), do: nil 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 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), defp should_federate(%{attributed_to_id: attributed_to_id}) when not is_nil(attributed_to_id),
do: true do: true

View file

@ -6,73 +6,81 @@ defmodule Mobilizon.GraphQL.API.Follows do
alias Mobilizon.Actors alias Mobilizon.Actors
alias Mobilizon.Actors.{Actor, Follower} alias Mobilizon.Actors.{Actor, Follower}
alias Mobilizon.Federation.ActivityPub alias Mobilizon.Federation.ActivityPub.{Actions, Activity}
alias Mobilizon.Federation.ActivityPub.Activity
require Logger 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 def follow(%Actor{} = follower, %Actor{} = followed) do
case ActivityPub.follow(follower, followed) do Actions.Follow.follow(follower, followed)
{: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
end 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 def unfollow(%Actor{} = follower, %Actor{} = followed) do
case ActivityPub.unfollow(follower, followed) do Actions.Follow.unfollow(follower, followed)
{:ok, activity, follow} ->
{:ok, activity, follow}
e ->
Logger.warn("Error while unfollowing actor: #{inspect(e)}")
{:error, e}
end
end end
def accept(%Actor{} = follower, %Actor{} = followed) do @doc """
Logger.debug("We're trying to accept a follow") 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 <- case Actors.is_following(follower, followed) do
Actors.is_following(follower, followed), %Follower{approved: false} = follow ->
{:ok, %Activity{} = activity, %Follower{approved: true} = follow} <- Actions.Accept.accept(
ActivityPub.accept(
:follow, :follow,
follow, follow,
true true
) do )
{:ok, activity, follow}
else
%Follower{approved: true} -> %Follower{approved: true} ->
{:error, "Follow already accepted"} {:error, "Follow already accepted"}
nil ->
{:error, "Can't accept follow: #{follower_url} is not following #{followed_url}."}
end end
end end
def reject(%Actor{} = follower, %Actor{} = followed) do @doc """
Logger.debug("We're trying to reject a follow") 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} <- case Actors.is_following(follower, followed) do
{:follower, Actors.is_following(follower, followed)}, %Follower{approved: true} ->
{:ok, %Activity{} = activity, %Follower{} = follow} <- {:error, "Follow already accepted"}
ActivityPub.reject(
%Follower{} = follow ->
Actions.Reject.reject(
:follow, :follow,
follow, follow,
true true
) do )
{:ok, activity, follow}
else
{:follower, nil} ->
{:error, "Follow not found"}
{:follower, %Follower{approved: true}} -> nil ->
{:error, "Follow already accepted"} {:error, "Follow not found"}
end end
end end
end end

View file

@ -6,39 +6,35 @@ defmodule Mobilizon.GraphQL.API.Groups do
alias Mobilizon.Actors alias Mobilizon.Actors
alias Mobilizon.Actors.Actor alias Mobilizon.Actors.Actor
alias Mobilizon.Federation.ActivityPub alias Mobilizon.Federation.ActivityPub.{Actions, Activity}
alias Mobilizon.Federation.ActivityPub.Activity
alias Mobilizon.Service.Formatter.HTML alias Mobilizon.Service.Formatter.HTML
@doc """ @doc """
Create a group 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 def create_group(args) do
with preferred_username <- preferred_username =
args |> Map.get(:preferred_username) |> HTML.strip_tags() |> String.trim(), 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)
args <- args |> Map.put(:type, :Group),
{:ok, %Activity{} = activity, %Actor{} = group} <- case Actors.get_local_actor_by_name(preferred_username) do
ActivityPub.create(:actor, args, true, %{"actor" => args.creator_actor.url}) do nil ->
{:ok, activity, group} Actions.Create.create(:actor, args, true, %{"actor" => args.creator_actor.url})
else
{:existing_group, _} -> %Actor{} ->
{:error, "A group with this name already exists"} {:error, "A profile or group with that name already exists"}
end end
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 def update_group(%{id: id} = args) do
with {:existing_group, {:ok, %Actor{type: :Group} = group}} <- with {:ok, %Actor{type: :Group} = group} <- Actors.get_group_by_actor_id(id) do
{:existing_group, Actors.get_group_by_actor_id(id)}, Actions.Update.update(group, args, true, %{"actor" => args.updater_actor.url})
{: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"}
end end
end end
end end

View file

@ -6,28 +6,27 @@ defmodule Mobilizon.GraphQL.API.Participations do
alias Mobilizon.Actors.Actor alias Mobilizon.Actors.Actor
alias Mobilizon.Events alias Mobilizon.Events
alias Mobilizon.Events.{Event, Participant} alias Mobilizon.Events.{Event, Participant}
alias Mobilizon.Federation.ActivityPub alias Mobilizon.Federation.ActivityPub.{Actions, Activity}
alias Mobilizon.Federation.ActivityPub.Activity
alias Mobilizon.Service.Notifications.Scheduler alias Mobilizon.Service.Notifications.Scheduler
alias Mobilizon.Web.Email.Participation 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 def join(%Event{id: event_id} = event, %Actor{id: actor_id} = actor, args \\ %{}) do
with {:error, :participant_not_found} <- case Mobilizon.Events.get_participant(event_id, actor_id, args) do
Mobilizon.Events.get_participant(event_id, actor_id, args), {:ok, %Participant{}} ->
{:ok, activity, participant} <- {:error, :already_participant}
ActivityPub.join(event, actor, Map.get(args, :local, true), %{metadata: args}) do
{:ok, activity, participant} {:error, :participant_not_found} ->
Actions.Join.join(event, actor, Map.get(args, :local, true), %{metadata: args})
end end
end end
@spec leave(Event.t(), Actor.t()) :: {:ok, Activity.t(), Participant.t()} @spec leave(Event.t(), Actor.t(), map()) ::
def leave(%Event{} = event, %Actor{} = actor, args \\ %{}) do {:ok, Activity.t(), Participant.t()}
with {:ok, activity, participant} <- | {:error, :is_only_organizer | :participant_not_found | Ecto.Changeset.t()}
ActivityPub.leave(event, actor, Map.get(args, :local, true), %{metadata: args}) do def leave(%Event{} = event, %Actor{} = actor, args \\ %{}),
{:ok, activity, participant} do: Actions.Leave.leave(event, actor, Map.get(args, :local, true), %{metadata: args})
end
end
@doc """ @doc """
Update participation status Update participation status
@ -36,7 +35,6 @@ defmodule Mobilizon.GraphQL.API.Participations do
def update(%Participant{} = participation, %Actor{} = moderator, :participant), def update(%Participant{} = participation, %Actor{} = moderator, :participant),
do: accept(participation, moderator) 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 def update(%Participant{} = participation, %Actor{} = _moderator, :not_approved) do
with {:ok, %Participant{} = participant} <- with {:ok, %Participant{} = participant} <-
Events.update_participant(participation, %{role: :not_approved}) do Events.update_participant(participation, %{role: :not_approved}) do
@ -45,7 +43,6 @@ defmodule Mobilizon.GraphQL.API.Participations do
end end
end end
@spec update(Participant.t(), Actor.t(), atom()) :: {:ok, Activity.t(), Participant.t()}
def update(%Participant{} = participation, %Actor{} = moderator, :rejected), def update(%Participant{} = participation, %Actor{} = moderator, :rejected),
do: reject(participation, moderator) do: reject(participation, moderator)
@ -54,15 +51,18 @@ defmodule Mobilizon.GraphQL.API.Participations do
%Participant{} = participation, %Participant{} = participation,
%Actor{} = moderator %Actor{} = moderator
) do ) do
with {:ok, activity, %Participant{role: :participant} = participation} <- case Actions.Accept.accept(
ActivityPub.accept(
:join, :join,
participation, participation,
true, true,
%{"actor" => moderator.url} %{"actor" => moderator.url}
), ) do
:ok <- Participation.send_emails_to_local_user(participation) do {:ok, activity, %Participant{role: :participant} = participation} ->
Participation.send_emails_to_local_user(participation)
{:ok, activity, participation} {:ok, activity, participation}
{:error, err} ->
{:error, err}
end end
end end
@ -72,7 +72,7 @@ defmodule Mobilizon.GraphQL.API.Participations do
%Actor{} = moderator %Actor{} = moderator
) do ) do
with {:ok, activity, %Participant{role: :rejected} = participation} <- with {:ok, activity, %Participant{role: :rejected} = participation} <-
ActivityPub.reject( Actions.Reject.reject(
:join, :join,
participation, participation,
true, true,

View file

@ -9,48 +9,46 @@ defmodule Mobilizon.GraphQL.API.Reports do
alias Mobilizon.Reports.{Note, Report, ReportStatus} alias Mobilizon.Reports.{Note, Report, ReportStatus}
alias Mobilizon.Users.User alias Mobilizon.Users.User
alias Mobilizon.Federation.ActivityPub alias Mobilizon.Federation.ActivityPub.{Actions, Activity}
alias Mobilizon.Federation.ActivityPub.Activity
@doc """ @doc """
Create a report/flag on an actor, and optionally on an event or on comments. 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 def report(args) do
case {:make_activity, ActivityPub.flag(args, Map.get(args, :forward, false) == true)} do Actions.Flag.flag(args, Map.get(args, :forward, false) == true)
{:make_activity, {:ok, %Activity{} = activity, %Report{} = report}} ->
{:ok, activity, report}
{:make_activity, err} ->
{:error, err}
end
end end
@doc """ @doc """
Update the state of a report 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 def update_report_status(%Actor{} = actor, %Report{} = report, state) do
with {:valid_state, true} <- if ReportStatus.valid_value?(state) do
{:valid_state, ReportStatus.valid_value?(state)}, with {:ok, %Report{} = report} <- ReportsAction.update_report(report, %{"status" => state}) do
{:ok, report} <- ReportsAction.update_report(report, %{"status" => state}), Admin.log_action(actor, "update", report)
{:ok, _} <- Admin.log_action(actor, "update", report) do
{:ok, report} {:ok, report}
end
else else
{:valid_state, false} -> {:error, "Unsupported state"} {:error, "Unsupported state"}
end end
end end
@doc """ @doc """
Create a note on a report 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( def create_report_note(
%Report{id: report_id}, %Report{id: report_id},
%Actor{id: moderator_id, user_id: user_id} = moderator, %Actor{id: moderator_id, user_id: user_id} = moderator,
content content
) do ) do
with %User{role: role} <- Users.get_user!(user_id), %User{role: role} = Users.get_user!(user_id)
{:role, true} <- {:role, role in [:administrator, :moderator]},
{:ok, %Note{} = note} <- if role in [:administrator, :moderator] do
with {:ok, %Note{} = note} <-
Mobilizon.Reports.create_note(%{ Mobilizon.Reports.create_note(%{
"report_id" => report_id, "report_id" => report_id,
"moderator_id" => moderator_id, "moderator_id" => moderator_id,
@ -58,8 +56,8 @@ defmodule Mobilizon.GraphQL.API.Reports do
}), }),
{:ok, _} <- Admin.log_action(moderator, "create", note) do {:ok, _} <- Admin.log_action(moderator, "create", note) do
{:ok, note} {:ok, note}
end
else else
{:role, false} ->
{:error, "You need to be a moderator or an administrator to create a note on a report"} {:error, "You need to be a moderator or an administrator to create a note on a report"}
end end
end end
@ -67,23 +65,25 @@ defmodule Mobilizon.GraphQL.API.Reports do
@doc """ @doc """
Delete a report note 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( def delete_report_note(
%Note{moderator_id: note_moderator_id} = note, %Note{moderator_id: note_moderator_id} = note,
%Actor{id: moderator_id, user_id: user_id} = moderator %Actor{id: moderator_id, user_id: user_id} = moderator
) do ) do
with {:same_actor, true} <- {:same_actor, note_moderator_id == moderator_id}, if note_moderator_id == moderator_id do
%User{role: role} <- Users.get_user!(user_id), %User{role: role} = Users.get_user!(user_id)
{:role, true} <- {:role, role in [:administrator, :moderator]},
{:ok, %Note{} = note} <- if role in [:administrator, :moderator] do
with {:ok, %Note{} = note} <-
Mobilizon.Reports.delete_note(note), Mobilizon.Reports.delete_note(note),
{:ok, _} <- Admin.log_action(moderator, "delete", note) do {:ok, _} <- Admin.log_action(moderator, "delete", note) do
{:ok, note} {:ok, note}
end
else else
{:role, false} ->
{:error, "You need to be a moderator or an administrator to create a note on a report"} {:error, "You need to be a moderator or an administrator to create a note on a report"}
end
{:same_actor, false} -> else
{:error, "You can only remove your own notes"} {:error, "You can only remove your own notes"}
end end
end end

View file

@ -59,8 +59,8 @@ defmodule Mobilizon.GraphQL.API.Search do
@doc """ @doc """
Search events Search events
""" """
@spec search_events(String.t(), integer | nil, integer | nil) :: @spec search_events(map(), integer | nil, integer | nil) ::
{:ok, Page.t()} | {:error, String.t()} {:ok, Page.t()}
def search_events(%{term: term} = args, page \\ 1, limit \\ 10) do def search_events(%{term: term} = args, page \\ 1, limit \\ 10) do
term = String.trim(term) term = String.trim(term)
@ -78,6 +78,7 @@ defmodule Mobilizon.GraphQL.API.Search do
end end
end end
@spec interact(String.t()) :: {:ok, struct()} | {:error, :not_found}
def interact(uri) do def interact(uri) do
case ActivityPub.fetch_object_from_url(uri) do case ActivityPub.fetch_object_from_url(uri) do
{:ok, object} -> {:ok, object} ->

View file

@ -10,7 +10,7 @@ defmodule Mobilizon.GraphQL.API.Utils do
@doc """ @doc """
Creates HTML content from text and mentions 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 def make_content_html(text, additional_tags, content_type) do
with {text, mentions, tags} <- format_input(text, content_type, []) do with {text, mentions, tags} <- format_input(text, content_type, []) do
{text, mentions, additional_tags ++ Enum.map(tags, fn {_, tag} -> tag end)} {text, mentions, additional_tags ++ Enum.map(tags, fn {_, tag} -> tag end)}

View file

@ -8,11 +8,19 @@ defmodule Mobilizon.GraphQL.Error do
alias Mobilizon.Web.Gettext, as: GettextBackend alias Mobilizon.Web.Gettext, as: GettextBackend
import Mobilizon.Web.Gettext, only: [dgettext: 2] 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] defstruct [:code, :message, :status_code, :field]
@type error :: {:error, any()} | {:error, any(), any(), any()} | atom()
@doc """
Normalize an error to return `t`.
"""
# Error Tuples # Error Tuples
# ------------ # ------------
# Regular errors # Regular errors
@spec normalize(error | list(error) | String.t() | any()) :: t()
def normalize({:error, reason}) do def normalize({:error, reason}) do
handle(reason) handle(reason)
end end

View 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

View file

@ -4,7 +4,8 @@ defmodule Mobilizon.GraphQL.Resolvers.Activity do
""" """
import Mobilizon.Users.Guards import Mobilizon.Users.Guards
alias Mobilizon.{Activities, Actors, Users} alias Mobilizon.{Activities, Actors}
alias Mobilizon.Activities.Activity
alias Mobilizon.Actors.Actor alias Mobilizon.Actors.Actor
alias Mobilizon.Service.Activity.Utils alias Mobilizon.Service.Activity.Utils
alias Mobilizon.Storage.Page alias Mobilizon.Storage.Page
@ -12,11 +13,12 @@ defmodule Mobilizon.GraphQL.Resolvers.Activity do
require Logger 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, %{ 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 }) do
with {:actor, %Actor{id: actor_id} = _actor} <- {:actor, Users.get_actor_for_user(user)}, if Actors.is_member?(actor_id, group_id) or is_moderator(role) do
{:member, true} <- {:member, Actors.is_member?(actor_id, group_id) or is_moderator(role)} do
%Page{total: total, elements: elements} = %Page{total: total, elements: elements} =
Activities.list_group_activities_for_member( Activities.list_group_activities_for_member(
group_id, group_id,
@ -30,7 +32,6 @@ defmodule Mobilizon.GraphQL.Resolvers.Activity do
{:ok, %Page{total: total, elements: elements}} {:ok, %Page{total: total, elements: elements}}
else else
{:member, false} ->
{:error, :unauthorized} {:error, :unauthorized}
end end
end end

View file

@ -4,15 +4,17 @@ defmodule Mobilizon.GraphQL.Resolvers.Actor do
""" """
import Mobilizon.Users.Guards import Mobilizon.Users.Guards
alias Mobilizon.{Actors, Admin, Users} alias Mobilizon.{Actors, Admin}
alias Mobilizon.Actors.Actor alias Mobilizon.Actors.Actor
alias Mobilizon.Federation.ActivityPub alias Mobilizon.Federation.ActivityPub.Actions
alias Mobilizon.Service.Workers.Background alias Mobilizon.Service.Workers.Background
alias Mobilizon.Users.User alias Mobilizon.Users.User
import Mobilizon.Web.Gettext, only: [dgettext: 2] import Mobilizon.Web.Gettext, only: [dgettext: 2]
require Logger 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}}}) def refresh_profile(_parent, %{id: id}, %{context: %{current_user: %User{role: role}}})
when is_admin(role) do when is_admin(role) do
case Actors.get_actor(id) do case Actors.get_actor(id) do
@ -31,40 +33,38 @@ defmodule Mobilizon.GraphQL.Resolvers.Actor do
end end
end end
@spec suspend_profile(any(), map(), Absinthe.Resolution.t()) ::
{:ok, Actor.t()} | {:error, String.t()}
def suspend_profile(_parent, %{id: id}, %{ 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 when is_moderator(role) do
with {:moderator_actor, %Actor{} = moderator_actor} <- case Actors.get_actor_with_preload(id) do
{:moderator_actor, Users.get_actor_for_user(user)},
%Actor{suspended: false} = actor <- Actors.get_actor_with_preload(id) do
case actor do
# Suspend a group on this instance # 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") 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) Admin.log_action(moderator_actor, "suspend", actor)
{:ok, actor} {:ok, actor}
# Delete a remote 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") Logger.debug("We're just deleting a remote instance")
Actors.delete_actor(actor, suspension: true) Actors.delete_actor(actor, suspension: true)
Admin.log_action(moderator_actor, "suspend", actor) Admin.log_action(moderator_actor, "suspend", actor)
{:ok, actor} {:ok, actor}
%Actor{domain: nil} -> %Actor{suspended: false, domain: nil} ->
{:error, dgettext("errors", "No remote profile found with this ID")} {: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} -> %Actor{suspended: true} ->
{:error, dgettext("errors", "Profile already suspended")} {:error, dgettext("errors", "Profile already suspended")}
{:error, _} -> nil ->
{:error, dgettext("errors", "Error while performing background task")} {:error, dgettext("errors", "Profile not found")}
end end
end end
@ -72,13 +72,16 @@ defmodule Mobilizon.GraphQL.Resolvers.Actor do
{:error, dgettext("errors", "Only moderators and administrators can suspend a profile")} {:error, dgettext("errors", "Only moderators and administrators can suspend a profile")}
end end
@spec unsuspend_profile(any(), map(), Absinthe.Resolution.t()) ::
{:ok, Actor.t()} | {:error, String.t()}
def unsuspend_profile(_parent, %{id: id}, %{ 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 when is_moderator(role) do
with {:moderator_actor, %Actor{} = moderator_actor} <- with %Actor{suspended: true} = actor <-
{:moderator_actor, Users.get_actor_for_user(user)},
%Actor{suspended: true} = actor <-
Actors.get_actor_with_preload(id, true), Actors.get_actor_with_preload(id, true),
{:delete_tombstones, {_, nil}} <- {:delete_tombstones, {_, nil}} <-
{:delete_tombstones, Mobilizon.Tombstone.delete_actor_tombstones(id)}, {:delete_tombstones, Mobilizon.Tombstone.delete_actor_tombstones(id)},

View file

@ -6,14 +6,13 @@ defmodule Mobilizon.GraphQL.Resolvers.Admin do
import Mobilizon.Users.Guards import Mobilizon.Users.Guards
alias Mobilizon.{Actors, Admin, Config, Events} alias Mobilizon.{Actors, Admin, Config, Events}
alias Mobilizon.Actors.Actor alias Mobilizon.Actors.{Actor, Follower}
alias Mobilizon.Admin.{ActionLog, Setting} alias Mobilizon.Admin.{ActionLog, Setting}
alias Mobilizon.Cldr.Language alias Mobilizon.Cldr.Language
alias Mobilizon.Config alias Mobilizon.Config
alias Mobilizon.Discussions.Comment alias Mobilizon.Discussions.Comment
alias Mobilizon.Events.Event alias Mobilizon.Events.Event
alias Mobilizon.Federation.ActivityPub alias Mobilizon.Federation.ActivityPub.{Actions, Relay}
alias Mobilizon.Federation.ActivityPub.Relay
alias Mobilizon.Reports.{Note, Report} alias Mobilizon.Reports.{Note, Report}
alias Mobilizon.Service.Statistics alias Mobilizon.Service.Statistics
alias Mobilizon.Storage.Page alias Mobilizon.Storage.Page
@ -21,6 +20,8 @@ defmodule Mobilizon.GraphQL.Resolvers.Admin do
import Mobilizon.Web.Gettext import Mobilizon.Web.Gettext
require Logger require Logger
@spec list_action_logs(any(), map(), Absinthe.Resolution.t()) ::
{:ok, Page.t(ActionLog.t())} | {:error, String.t()}
def list_action_logs( def list_action_logs(
_parent, _parent,
%{page: page, limit: limit}, %{page: page, limit: limit},
@ -38,10 +39,10 @@ defmodule Mobilizon.GraphQL.Resolvers.Admin do
id: id, id: id,
inserted_at: inserted_at inserted_at: inserted_at
} = action_log -> } = action_log ->
with data when is_map(data) <- target_type
transform_action_log(String.to_existing_atom(target_type), action, action_log) do |> String.to_existing_atom()
Map.merge(data, %{actor: actor, id: id, inserted_at: inserted_at}) |> transform_action_log(action, action_log)
end |> Map.merge(%{actor: actor, id: id, inserted_at: inserted_at})
end) end)
|> Enum.filter(& &1) |> 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")} {:error, dgettext("errors", "You need to be logged-in and a moderator to list action logs")}
end end
@spec transform_action_log(module(), atom(), ActionLog.t()) :: map()
defp transform_action_log( defp transform_action_log(
Report, Report,
:update, :update,
@ -123,6 +125,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Admin do
end end
# Changes are stored as %{"key" => "value"} so we need to convert them back as struct # 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 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}), 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 data <- Map.put(data, :report, Mobilizon.Reports.get_report(data.report_id)) do
@ -143,6 +146,8 @@ defmodule Mobilizon.GraphQL.Resolvers.Admin do
end end
# datetimes are not unserialized as DateTime/NaiveDateTime so we do it manually with changeset data # 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 defp process_eventual_type(changeset, key, val) do
cond do cond do
changeset[String.to_existing_atom(key)] == :utc_datetime and not is_nil(val) -> 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
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 def get_list_of_languages(_parent, %{codes: codes}, _resolution) when is_list(codes) do
locale = Gettext.get_locale() locale = Gettext.get_locale()
locale = if Cldr.known_locale_name?(locale), do: locale, else: "en" locale = if Cldr.known_locale_name?(locale), do: locale, else: "en"
@ -187,6 +193,8 @@ defmodule Mobilizon.GraphQL.Resolvers.Admin do
end end
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}}}) def get_dashboard(_parent, _args, %{context: %{current_user: %User{role: role}}})
when is_admin(role) do when is_admin(role) do
last_public_event_published = last_public_event_published =
@ -225,6 +233,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Admin do
)} )}
end end
@spec get_settings(any(), any(), Absinthe.Resolution.t()) :: {:ok, map()} | {:error, String.t()}
def get_settings(_parent, _args, %{ def get_settings(_parent, _args, %{
context: %{current_user: %User{role: role}} 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")} dgettext("errors", "You need to be logged-in and an administrator to access admin settings")}
end end
@spec save_settings(any(), map(), Absinthe.Resolution.t()) ::
{:ok, map()} | {:error, String.t()}
def save_settings(_parent, args, %{ def save_settings(_parent, args, %{
context: %{current_user: %User{role: role}} 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")} dgettext("errors", "You need to be logged-in and an administrator to save admin settings")}
end end
@spec list_relay_followers(any(), map(), Absinthe.Resolution.t()) ::
{:ok, Page.t(Follower.t())} | {:error, :unauthorized | :unauthenticated}
def list_relay_followers( def list_relay_followers(
_parent, _parent,
%{page: page, limit: limit}, %{page: page, limit: limit},
@ -283,6 +296,8 @@ defmodule Mobilizon.GraphQL.Resolvers.Admin do
{:error, :unauthenticated} {:error, :unauthenticated}
end end
@spec list_relay_followings(any(), map(), Absinthe.Resolution.t()) ::
{:ok, Page.t(Follower.t())} | {:error, :unauthorized | :unauthenticated}
def list_relay_followings( def list_relay_followings(
_parent, _parent,
%{page: page, limit: limit}, %{page: page, limit: limit},
@ -305,28 +320,34 @@ defmodule Mobilizon.GraphQL.Resolvers.Admin do
{:error, :unauthenticated} {:error, :unauthenticated}
end 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}}}) def create_relay(_parent, %{address: address}, %{context: %{current_user: %User{role: role}}})
when is_admin(role) do when is_admin(role) do
case Relay.follow(address) do case Relay.follow(address) do
{:ok, _activity, follow} -> {:ok, _activity, follow} ->
{:ok, follow} {:ok, follow}
{:error, err} when is_binary(err) -> {:error, err} ->
{:error, err} {:error, err}
end end
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}}}) def remove_relay(_parent, %{address: address}, %{context: %{current_user: %User{role: role}}})
when is_admin(role) do when is_admin(role) do
case Relay.unfollow(address) do case Relay.unfollow(address) do
{:ok, _activity, follow} -> {:ok, _activity, follow} ->
{:ok, follow} {:ok, follow}
{:error, {:error, err}} when is_binary(err) -> {:error, err} ->
{:error, err} {:error, err}
end end
end end
@spec accept_subscription(any(), map(), Absinthe.Resolution.t()) ::
{:ok, Follower.t()} | {:error, any()}
def accept_subscription( def accept_subscription(
_parent, _parent,
%{address: address}, %{address: address},
@ -337,14 +358,13 @@ defmodule Mobilizon.GraphQL.Resolvers.Admin do
{:ok, _activity, follow} -> {:ok, _activity, follow} ->
{:ok, follow} {:ok, follow}
{:error, {:error, err}} when is_binary(err) -> {:error, err} ->
{:error, err}
{:error, err} when is_binary(err) ->
{:error, err} {:error, err}
end end
end end
@spec reject_subscription(any(), map(), Absinthe.Resolution.t()) ::
{:ok, Follower.t()} | {:error, any()}
def reject_subscription( def reject_subscription(
_parent, _parent,
%{address: address}, %{address: address},
@ -355,15 +375,12 @@ defmodule Mobilizon.GraphQL.Resolvers.Admin do
{:ok, _activity, follow} -> {:ok, _activity, follow} ->
{:ok, follow} {:ok, follow}
{:error, {:error, err}} when is_binary(err) -> {:error, err} ->
{:error, err}
{:error, err} when is_binary(err) ->
{:error, err} {:error, err}
end end
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 defp eventually_update_instance_actor(admin_setting_args) do
args = %{} args = %{}
new_instance_description = Map.get(admin_setting_args, :instance_description) 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), do: Map.put(args, :name, new_instance_name),
else: args else: args
with {:changes, true} <- {:changes, args != %{}}, if args != %{} do
%Actor{} = instance_actor <- Relay.get_actor(), %Actor{} = instance_actor = Relay.get_actor()
{:ok, _activity, _actor} <- ActivityPub.update(instance_actor, args, true) do
:ok case Actions.Update.update(instance_actor, args, true) do
else {:ok, _activity, _actor} ->
{:changes, false} ->
:ok :ok
err -> {:error, _err} ->
err {:error, :instance_actor_update_failure}
end
else
:ok
end end
end end
end end

View file

@ -3,7 +3,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Comment do
Handles the comment-related GraphQL calls. 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.Actors.Actor
alias Mobilizon.Discussions.Comment, as: CommentModel alias Mobilizon.Discussions.Comment, as: CommentModel
alias Mobilizon.Events.{Event, EventOptions} alias Mobilizon.Events.{Event, EventOptions}
@ -14,61 +14,78 @@ defmodule Mobilizon.GraphQL.Resolvers.Comment do
require Logger require Logger
@spec get_thread(any(), map(), Absinthe.Resolution.t()) :: {:ok, [CommentModel.t()]}
def get_thread(_parent, %{id: thread_id}, _context) do def get_thread(_parent, %{id: thread_id}, _context) do
{:ok, Discussions.get_thread_replies(thread_id)} {:ok, Discussions.get_thread_replies(thread_id)}
end end
@spec create_comment(any(), map(), Absinthe.Resolution.t()) ::
{:ok, CommentModel.t()} | {:error, :unauthorized | :not_found | any() | String.t()}
def create_comment( def create_comment(
_parent, _parent,
%{event_id: event_id} = args, %{event_id: event_id} = args,
%{ %{
context: %{ context: %{
current_user: %User{} = user current_actor: %Actor{id: actor_id}
} }
} }
) do ) do
with %Actor{id: actor_id} <- Users.get_actor_for_user(user), case Events.get_event(event_id) do
{:find_event,
{:ok, {:ok,
%Event{ %Event{
options: %EventOptions{comment_moderation: comment_moderation}, options: %EventOptions{comment_moderation: comment_moderation},
organizer_actor_id: organizer_actor_id organizer_actor_id: organizer_actor_id
}}} <- }} ->
{:find_event, Events.get_event(event_id)}, if comment_moderation != :closed || actor_id == organizer_actor_id do
{:allowed, true} <- args = Map.put(args, :actor_id, actor_id)
{:allowed, comment_moderation != :closed || actor_id == organizer_actor_id},
args <- Map.put(args, :actor_id, actor_id), case Comments.create_comment(args) do
{:ok, _, %CommentModel{} = comment} <- {:ok, _, %CommentModel{} = comment} ->
Comments.create_comment(args) do
{:ok, comment} {:ok, comment}
else
{:error, err} -> {:error, err} ->
{:error, err} {:error, err}
end
{:allowed, false} -> else
{:error, :unauthorized} {:error, :unauthorized}
end end
{:error, :event_not_found} ->
{:error, :not_found}
end
end end
def create_comment(_parent, _args, _context) do def create_comment(_parent, _args, _context) do
{:error, dgettext("errors", "You are not allowed to create a comment if not connected")} {:error, dgettext("errors", "You are not allowed to create a comment if not connected")}
end end
@spec update_comment(any(), map(), Absinthe.Resolution.t()) ::
{:ok, CommentModel.t()} | {:error, :unauthorized | :not_found | any() | String.t()}
def update_comment( def update_comment(
_parent, _parent,
%{text: text, comment_id: comment_id}, %{text: text, comment_id: comment_id},
%{ %{
context: %{ context: %{
current_user: %User{} = user current_actor: %Actor{id: actor_id}
} }
} }
) do ) do
with {:actor, %Actor{id: actor_id} = _actor} <- {:actor, Users.get_actor_for_user(user)}, case Mobilizon.Discussions.get_comment_with_preload(comment_id) do
%CommentModel{actor_id: comment_actor_id} = comment <- %CommentModel{actor_id: comment_actor_id} = comment ->
Mobilizon.Discussions.get_comment_with_preload(comment_id), if actor_id == comment_actor_id do
true <- actor_id === comment_actor_id, case Comments.update_comment(comment, %{text: text}) do
{:ok, _, %CommentModel{} = comment} <- Comments.update_comment(comment, %{text: text}) do {:ok, _, %CommentModel{} = comment} ->
{:ok, 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
end end
@ -81,13 +98,13 @@ defmodule Mobilizon.GraphQL.Resolvers.Comment do
%{comment_id: comment_id}, %{comment_id: comment_id},
%{ %{
context: %{ context: %{
current_user: %User{role: role} = user current_user: %User{role: role},
current_actor: %Actor{id: actor_id} = actor
} }
} }
) do ) do
with {:actor, %Actor{id: actor_id} = actor} <- {:actor, Users.get_actor_for_user(user)}, case Discussions.get_comment_with_preload(comment_id) do
%CommentModel{deleted_at: nil} = comment <- %CommentModel{deleted_at: nil} = comment ->
Discussions.get_comment_with_preload(comment_id) do
cond do cond do
{:comment_can_be_managed, true} == CommentModel.can_be_managed_by(comment, actor_id) -> {:comment_can_be_managed, true} == CommentModel.can_be_managed_by(comment, actor_id) ->
do_delete_comment(comment, actor) do_delete_comment(comment, actor)
@ -103,9 +120,12 @@ defmodule Mobilizon.GraphQL.Resolvers.Comment do
true -> true ->
{:error, dgettext("errors", "You cannot delete this comment")} {:error, dgettext("errors", "You cannot delete this comment")}
end end
else
%CommentModel{deleted_at: deleted_at} when not is_nil(deleted_at) -> %CommentModel{deleted_at: deleted_at} when not is_nil(deleted_at) ->
{:error, dgettext("errors", "Comment is already deleted")} {:error, dgettext("errors", "Comment is already deleted")}
nil ->
{:error, dgettext("errors", "Comment not found")}
end end
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")} {:error, dgettext("errors", "You are not allowed to delete a comment if not connected")}
end end
@spec do_delete_comment(CommentModel.t(), Actor.t()) ::
{:ok, CommentModel.t()} | {:error, any()}
defp do_delete_comment(%CommentModel{} = comment, %Actor{} = actor) do defp do_delete_comment(%CommentModel{} = comment, %Actor{} = actor) do
with {:ok, _, %CommentModel{} = comment} <- case Comments.delete_comment(comment, actor) do
Comments.delete_comment(comment, actor) do {:ok, _, %CommentModel{} = comment} ->
{:ok, comment} {:ok, comment}
{:error, err} ->
{:error, err}
end end
end end
end end

View file

@ -8,6 +8,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Config do
@doc """ @doc """
Gets config. Gets config.
""" """
@spec get_config(any(), map(), Absinthe.Resolution.t()) :: {:ok, map()}
def get_config(_parent, _params, %{context: %{ip: ip}}) do def get_config(_parent, _params, %{context: %{ip: ip}}) do
geolix = Geolix.lookup(ip) geolix = Geolix.lookup(ip)
@ -28,6 +29,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Config do
{:ok, data} {:ok, data}
end end
@spec terms(any(), map(), Absinthe.Resolution.t()) :: {:ok, map()}
def terms(_parent, %{locale: locale}, _resolution) do def terms(_parent, %{locale: locale}, _resolution) do
type = Config.instance_terms_type() type = Config.instance_terms_type()
@ -41,6 +43,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Config do
{:ok, %{body_html: body_html, type: type, url: url}} {:ok, %{body_html: body_html, type: type, url: url}}
end end
@spec privacy(any(), map(), Absinthe.Resolution.t()) :: {:ok, map()}
def privacy(_parent, %{locale: locale}, _resolution) do def privacy(_parent, %{locale: locale}, _resolution) do
type = Config.instance_privacy_type() type = Config.instance_privacy_type()
@ -54,6 +57,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Config do
{:ok, %{body_html: body_html, type: type, url: url}} {:ok, %{body_html: body_html, type: type, url: url}}
end end
@spec config_cache :: map()
defp config_cache do defp config_cache do
case Cachex.fetch(:config, "full_config", fn _key -> case Cachex.fetch(:config, "full_config", fn _key ->
case build_config_cache() do case build_config_cache() do
@ -62,10 +66,11 @@ defmodule Mobilizon.GraphQL.Resolvers.Config do
end end
end) do end) do
{status, value} when status in [:ok, :commit] -> value {status, value} when status in [:ok, :commit] -> value
_err -> nil _err -> %{}
end end
end end
@spec build_config_cache :: map()
defp build_config_cache do defp build_config_cache do
%{ %{
name: Config.instance_name(), name: Config.instance_name(),

View file

@ -3,26 +3,27 @@ defmodule Mobilizon.GraphQL.Resolvers.Discussion do
Handles the group-related GraphQL calls. Handles the group-related GraphQL calls.
""" """
alias Mobilizon.{Actors, Discussions, Users} alias Mobilizon.{Actors, Discussions}
alias Mobilizon.Actors.Actor alias Mobilizon.Actors.Actor
alias Mobilizon.Discussions.{Comment, Discussion} alias Mobilizon.Discussions.{Comment, Discussion}
alias Mobilizon.Federation.ActivityPub alias Mobilizon.Federation.ActivityPub.Actions
alias Mobilizon.GraphQL.API.Comments alias Mobilizon.GraphQL.API.Comments
alias Mobilizon.Storage.Page alias Mobilizon.Storage.Page
alias Mobilizon.Users.User alias Mobilizon.Users.User
import Mobilizon.Web.Gettext 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( def find_discussions_for_actor(
%Actor{id: group_id}, %Actor{id: group_id},
%{page: page, limit: limit}, %{page: page, limit: limit},
%{ %{
context: %{ context: %{
current_user: %User{} = user current_actor: %Actor{id: actor_id}
} }
} }
) do ) do
with {:actor, %Actor{id: actor_id} = _actor} <- {:actor, Users.get_actor_for_user(user)}, with {:member, true} <- {:member, Actors.is_member?(actor_id, group_id)},
{: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, %Actor{type: :Group} = group} <- Actors.get_group_by_actor_id(group_id) do
{:ok, Discussions.find_discussions_for_actor(group, page, limit)} {:ok, Discussions.find_discussions_for_actor(group, page, limit)}
else else
@ -31,30 +32,42 @@ defmodule Mobilizon.GraphQL.Resolvers.Discussion do
end end
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: []}} {:ok, %Page{total: 0, elements: []}}
end 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}, %{ def get_discussion(_parent, %{id: id}, %{
context: %{ context: %{
current_user: %User{} = user current_actor: %Actor{id: creator_id}
} }
}) do }) do
with {:actor, %Actor{id: creator_id} = _actor} <- {:actor, Users.get_actor_for_user(user)}, case Discussions.get_discussion(id) do
%Discussion{actor_id: actor_id} = discussion <- %Discussion{actor_id: actor_id} = discussion ->
Discussions.get_discussion(id), if Actors.is_member?(creator_id, actor_id) do
{:member, true} <- {:member, Actors.is_member?(creator_id, actor_id)} do
{:ok, discussion} {:ok, discussion}
else
{:error, :unauthorized}
end
nil ->
{:error, :discussion_not_found}
end end
end end
def get_discussion(_parent, %{slug: slug}, %{ def get_discussion(_parent, %{slug: slug}, %{
context: %{ context: %{
current_user: %User{} = user current_actor: %Actor{id: creator_id}
} }
}) do }) do
with {:actor, %Actor{id: creator_id} = _actor} <- {:actor, Users.get_actor_for_user(user)}, with %Discussion{actor_id: actor_id} = discussion <-
%Discussion{actor_id: actor_id} = discussion <-
Discussions.get_discussion_by_slug(slug), Discussions.get_discussion_by_slug(slug),
{:member, true} <- {:member, Actors.is_member?(creator_id, actor_id)} do {:member, true} <- {:member, Actors.is_member?(creator_id, actor_id)} do
{:ok, discussion} {:ok, discussion}
@ -76,6 +89,8 @@ defmodule Mobilizon.GraphQL.Resolvers.Discussion do
def get_discussion(_parent, _args, _resolution), def get_discussion(_parent, _args, _resolution),
do: {:error, dgettext("errors", "You need to be logged-in to access discussions")} 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( def get_comments_for_discussion(
%Discussion{id: discussion_id}, %Discussion{id: discussion_id},
%{page: page, limit: limit}, %{page: page, limit: limit},
@ -84,48 +99,54 @@ defmodule Mobilizon.GraphQL.Resolvers.Discussion do
{:ok, Discussions.get_comments_for_discussion(discussion_id, page, limit)} {:ok, Discussions.get_comments_for_discussion(discussion_id, page, limit)}
end end
@spec create_discussion(any(), map(), Absinthe.Resolution.t()) ::
{:ok, Discussion.t()}
| {:error, Ecto.Changeset.t() | String.t() | :unauthorized | :unauthenticated}
def create_discussion( def create_discussion(
_parent, _parent,
%{title: title, text: text, actor_id: group_id}, %{title: title, text: text, actor_id: group_id},
%{ %{
context: %{ context: %{
current_user: %User{} = user current_actor: %Actor{id: creator_id}
} }
} }
) do ) do
with {:actor, %Actor{id: creator_id} = _actor} <- {:actor, Users.get_actor_for_user(user)}, if Actors.is_member?(creator_id, group_id) do
{:member, true} <- {:member, Actors.is_member?(creator_id, group_id)}, case Comments.create_discussion(%{
{:ok, _activity, %Discussion{} = discussion} <-
Comments.create_discussion(%{
title: title, title: title,
text: text, text: text,
actor_id: group_id, actor_id: group_id,
creator_id: creator_id, creator_id: creator_id,
attributed_to_id: group_id attributed_to_id: group_id
}) do }) do
{:ok, _activity, %Discussion{} = discussion} ->
{:ok, discussion} {:ok, discussion}
else
{:error, type, err, _} when type in [:discussion, :comment] -> {:error, %Ecto.Changeset{} = err} ->
{:error, err} {:error, err}
{:member, false} -> {:error, _err} ->
{:error, dgettext("errors", "Error while creating a discussion")}
end
else
{:error, :unauthorized} {:error, :unauthorized}
end end
end end
def create_discussion(_, _, _), do: {:error, :unauthenticated} 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( def reply_to_discussion(
_parent, _parent,
%{text: text, discussion_id: discussion_id}, %{text: text, discussion_id: discussion_id},
%{ %{
context: %{ context: %{
current_user: %User{} = user current_actor: %Actor{id: creator_id}
} }
} }
) do ) do
with {:actor, %Actor{id: creator_id} = _actor} <- {:actor, Users.get_actor_for_user(user)}, with {:no_discussion,
{:no_discussion,
%Discussion{ %Discussion{
actor_id: actor_id, actor_id: actor_id,
last_comment: %Comment{ last_comment: %Comment{
@ -155,22 +176,22 @@ defmodule Mobilizon.GraphQL.Resolvers.Discussion do
def reply_to_discussion(_, _, _), do: {:error, :unauthenticated} 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( def update_discussion(
_parent, _parent,
%{title: title, discussion_id: discussion_id}, %{title: title, discussion_id: discussion_id},
%{ %{
context: %{ context: %{
current_user: %User{} = user current_actor: %Actor{id: creator_id}
} }
} }
) do ) do
with {:actor, %Actor{id: creator_id} = _actor} <- {:actor, Users.get_actor_for_user(user)}, with {:no_discussion, %Discussion{actor_id: actor_id} = discussion} <-
{:no_discussion, %Discussion{actor_id: actor_id} = discussion} <-
{:no_discussion, Discussions.get_discussion(discussion_id)}, {:no_discussion, Discussions.get_discussion(discussion_id)},
{:member, true} <- {:member, Actors.is_member?(creator_id, actor_id)}, {:member, true} <- {:member, Actors.is_member?(creator_id, actor_id)},
{:ok, _activity, %Discussion{} = discussion} <- {:ok, _activity, %Discussion{} = discussion} <-
ActivityPub.update( Actions.Update.update(
discussion, discussion,
%{ %{
title: title title: title
@ -185,17 +206,19 @@ defmodule Mobilizon.GraphQL.Resolvers.Discussion do
def update_discussion(_, _, _), do: {:error, :unauthenticated} 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}, %{ def delete_discussion(_parent, %{discussion_id: discussion_id}, %{
context: %{ context: %{
current_user: %User{} = user current_user: %User{},
current_actor: %Actor{id: creator_id} = actor
} }
}) do }) do
with {:actor, %Actor{id: creator_id} = actor} <- {:actor, Users.get_actor_for_user(user)}, with {:no_discussion, %Discussion{actor_id: actor_id} = discussion} <-
{:no_discussion, %Discussion{actor_id: actor_id} = discussion} <-
{:no_discussion, Discussions.get_discussion(discussion_id)}, {:no_discussion, Discussions.get_discussion(discussion_id)},
{:member, true} <- {:member, Actors.is_member?(creator_id, actor_id)}, {:member, true} <- {:member, Actors.is_member?(creator_id, actor_id)},
{:ok, _activity, %Discussion{} = discussion} <- {:ok, _activity, %Discussion{} = discussion} <-
ActivityPub.delete(discussion, actor) do Actions.Delete.delete(discussion, actor) do
{:ok, discussion} {:ok, discussion}
else else
{:no_discussion, _} -> {:no_discussion, _} ->

View file

@ -3,7 +3,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Event do
Handles the event-related GraphQL calls. Handles the event-related GraphQL calls.
""" """
alias Mobilizon.{Actors, Admin, Events, Users} alias Mobilizon.{Actors, Admin, Events}
alias Mobilizon.Actors.Actor alias Mobilizon.Actors.Actor
alias Mobilizon.Config alias Mobilizon.Config
alias Mobilizon.Events.{Event, EventParticipantStats} alias Mobilizon.Events.{Event, EventParticipantStats}
@ -21,14 +21,17 @@ defmodule Mobilizon.GraphQL.Resolvers.Event do
@event_max_limit 100 @event_max_limit 100
@number_of_related_events 3 @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( def organizer_for_event(
%Event{attributed_to_id: attributed_to_id, organizer_actor_id: organizer_actor_id}, %Event{attributed_to_id: attributed_to_id, organizer_actor_id: organizer_actor_id},
_args, _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 when not is_nil(attributed_to_id) do
with %Actor{id: group_id} <- Actors.get_actor(attributed_to_id), with %Actor{id: group_id} <- Actors.get_actor(attributed_to_id),
%Actor{id: actor_id} <- Users.get_actor_for_user(user),
{:member, true} <- {:member, true} <-
{:member, Actors.is_member?(actor_id, group_id) or is_moderator(user_role)}, {:member, Actors.is_member?(actor_id, group_id) or is_moderator(user_role)},
%Actor{} = actor <- Actors.get_actor(organizer_actor_id) do %Actor{} = actor <- Actors.get_actor(organizer_actor_id) do
@ -61,6 +64,8 @@ defmodule Mobilizon.GraphQL.Resolvers.Event do
end end
end end
@spec list_events(any(), map(), Absinthe.Resolution.t()) ::
{:ok, Page.t(Event.t())} | {:error, :events_max_limit_reached}
def list_events( def list_events(
_parent, _parent,
%{page: page, limit: limit, order_by: order_by, direction: direction}, %{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} {:error, :events_max_limit_reached}
end end
@spec find_private_event(any(), map(), Absinthe.Resolution.t()) ::
{:ok, Event.t()} | {:error, :event_not_found}
defp find_private_event( defp find_private_event(
_parent, _parent,
%{uuid: uuid}, %{uuid: uuid},
%{context: %{current_user: %User{} = user}} = _resolution %{context: %{current_actor: %Actor{} = profile}} = _resolution
) do ) do
%Actor{} = profile = Users.get_actor_for_user(user)
case Events.get_event_by_uuid_with_preload(uuid) do case Events.get_event_by_uuid_with_preload(uuid) do
# Event attributed to group # Event attributed to group
%Event{attributed_to: %Actor{}} = event -> %Event{attributed_to: %Actor{}} = event ->
@ -107,6 +112,8 @@ defmodule Mobilizon.GraphQL.Resolvers.Event do
{:error, :event_not_found} {:error, :event_not_found}
end 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 def find_event(parent, %{uuid: uuid} = args, %{context: context} = resolution) do
with {:has_event, %Event{} = event} <- with {:has_event, %Event{} = event} <-
{:has_event, Events.get_public_event_by_uuid_with_preload(uuid)}, {:has_event, Events.get_public_event_by_uuid_with_preload(uuid)},
@ -133,15 +140,15 @@ defmodule Mobilizon.GraphQL.Resolvers.Event do
@doc """ @doc """
List participants for event (through an event request) 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( def list_participants_for_event(
%Event{id: event_id} = event, %Event{id: event_id} = event,
%{page: page, limit: limit, roles: roles}, %{page: page, limit: limit, roles: roles},
%{context: %{current_user: %User{} = user}} = _resolution %{context: %{current_actor: %Actor{} = actor}} = _resolution
) do ) do
with %Actor{} = actor <- Users.get_actor_for_user(user),
# Check that moderator has right # Check that moderator has right
{:event_can_be_managed, true} <- if can_event_be_updated_by?(event, actor) do
{:event_can_be_managed, can_event_be_updated_by?(event, actor)} do
roles = roles =
case roles do case roles do
nil -> nil ->
@ -160,7 +167,6 @@ defmodule Mobilizon.GraphQL.Resolvers.Event do
participants = Events.list_participants_for_event(event_id, roles, page, limit) participants = Events.list_participants_for_event(event_id, roles, page, limit)
{:ok, participants} {:ok, participants}
else else
{:event_can_be_managed, _} ->
{:error, {:error,
dgettext("errors", "Provided profile doesn't have moderator permissions on this event")} dgettext("errors", "Provided profile doesn't have moderator permissions on this event")}
end end
@ -170,6 +176,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Event do
{:ok, %{total: 0, elements: []}} {:ok, %{total: 0, elements: []}}
end end
@spec stats_participants(Event.t(), map(), Absinthe.Resolution.t()) :: {:ok, map()}
def stats_participants( def stats_participants(
%Event{participant_stats: %EventParticipantStats{} = stats, id: event_id} = _event, %Event{participant_stats: %EventParticipantStats{} = stats, id: event_id} = _event,
_args, _args,
@ -202,6 +209,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Event do
@doc """ @doc """
List related events List related events
""" """
@spec list_related_events(Event.t(), map(), Absinthe.Resolution.t()) :: {:ok, list(Event.t())}
def list_related_events( def list_related_events(
%Event{tags: tags, organizer_actor: organizer_actor, uuid: uuid}, %Event{tags: tags, organizer_actor: organizer_actor, uuid: uuid},
_args, _args,
@ -243,11 +251,14 @@ defmodule Mobilizon.GraphQL.Resolvers.Event do
{:ok, events} {:ok, events}
end end
@spec uniq_events(list(Event.t())) :: list(Event.t())
defp uniq_events(events), do: Enum.uniq_by(events, fn event -> event.uuid end) defp uniq_events(events), do: Enum.uniq_by(events, fn event -> event.uuid end)
@doc """ @doc """
Create an event Create an event
""" """
@spec create_event(any(), map(), Absinthe.Resolution.t()) ::
{:ok, Event.t()} | {:error, String.t() | Ecto.Changeset.t()}
def create_event( def create_event(
_parent, _parent,
%{organizer_actor_id: organizer_actor_id} = args, %{organizer_actor_id: organizer_actor_id} = args,
@ -287,15 +298,17 @@ defmodule Mobilizon.GraphQL.Resolvers.Event do
@doc """ @doc """
Update an event Update an event
""" """
@spec update_event(any(), map(), Absinthe.Resolution.t()) ::
{:ok, Event.t()} | {:error, String.t() | Ecto.Changeset.t()}
def update_event( def update_event(
_parent, _parent,
%{event_id: event_id} = args, %{event_id: event_id} = args,
%{context: %{current_user: %User{} = user}} = _resolution %{context: %{current_user: %User{} = user, current_actor: %Actor{} = actor}} = _resolution
) do ) do
# See https://github.com/absinthe-graphql/absinthe/issues/490 # See https://github.com/absinthe-graphql/absinthe/issues/490
with args <- Map.put(args, :options, args[:options] || %{}), 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), with {:ok, %Event{} = event} <- Events.get_event_with_preload(event_id),
{:ok, args} <- verify_profile_change(args, event, user, actor), {:ok, args} <- verify_profile_change(args, event, user, actor),
{:event_can_be_managed, true} <- {:event_can_be_managed, true} <-
{:event_can_be_managed, can_event_be_updated_by?(event, actor)}, {:event_can_be_managed, can_event_be_updated_by?(event, actor)},
@ -319,7 +332,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Event do
{:new_actor, _} -> {:new_actor, _} ->
{:error, dgettext("errors", "You can't attribute this event to this profile.")} {:error, dgettext("errors", "You can't attribute this event to this profile.")}
{:error, _, %Ecto.Changeset{} = error, _} -> {:error, %Ecto.Changeset{} = error} ->
{:error, error} {:error, error}
end end
end end
@ -331,13 +344,20 @@ defmodule Mobilizon.GraphQL.Resolvers.Event do
@doc """ @doc """
Delete an event Delete an event
""" """
@spec delete_event(any(), map(), Absinthe.Resolution.t()) ::
{:ok, Event.t()} | {:error, String.t() | Ecto.Changeset.t()}
def delete_event( def delete_event(
_parent, _parent,
%{event_id: event_id}, %{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 ) do
with {:ok, %Event{local: is_local} = event} <- Events.get_event_with_preload(event_id), case Events.get_event_with_preload(event_id) do
%Actor{id: actor_id} = actor <- Users.get_actor_for_user(user) do {:ok, %Event{local: is_local} = event} ->
cond do cond do
{:event_can_be_managed, true} == {:event_can_be_managed, true} ==
{:event_can_be_managed, can_event_be_deleted_by?(event, actor)} -> {:event_can_be_managed, can_event_be_deleted_by?(event, actor)} ->
@ -354,7 +374,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Event do
true -> true ->
{:error, dgettext("errors", "You cannot delete this event")} {:error, dgettext("errors", "You cannot delete this event")}
end end
else
{:error, :event_not_found} -> {:error, :event_not_found} ->
{:error, dgettext("errors", "Event not found")} {:error, dgettext("errors", "Event not found")}
end end
@ -364,6 +384,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Event do
{:error, dgettext("errors", "You need to be logged-in to delete an event")} {:error, dgettext("errors", "You need to be logged-in to delete an event")}
end end
@spec do_delete_event(Event.t(), Actor.t(), boolean()) :: {:ok, map()}
defp do_delete_event(%Event{} = event, %Actor{} = actor, federate \\ true) defp do_delete_event(%Event{} = event, %Actor{} = actor, federate \\ true)
when is_boolean(federate) do when is_boolean(federate) do
with {:ok, _activity, event} <- API.Events.delete_event(event, actor) do with {:ok, _activity, event} <- API.Events.delete_event(event, actor) do
@ -371,6 +392,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Event do
end end
end end
@spec is_organizer_group_member?(map()) :: boolean()
defp is_organizer_group_member?(%{ defp is_organizer_group_member?(%{
attributed_to_id: attributed_to_id, attributed_to_id: attributed_to_id,
organizer_actor_id: organizer_actor_id organizer_actor_id: organizer_actor_id
@ -382,6 +404,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Event do
defp is_organizer_group_member?(_), do: true defp is_organizer_group_member?(_), do: true
@spec verify_profile_change(map(), Event.t(), User.t(), Actor.t()) :: {:ok, map()}
defp verify_profile_change( defp verify_profile_change(
args, args,
%Event{attributed_to: %Actor{}}, %Event{attributed_to: %Actor{}},

View file

@ -6,7 +6,10 @@ defmodule Mobilizon.GraphQL.Resolvers.Event.Utils do
alias Mobilizon.Actors.Actor alias Mobilizon.Actors.Actor
alias Mobilizon.Events.Event alias Mobilizon.Events.Event
alias Mobilizon.Federation.ActivityPub.Permission 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?( def can_event_be_updated_by?(
%Event{attributed_to: %Actor{type: :Group}} = event, %Event{attributed_to: %Actor{type: :Group}} = event,
%Actor{} = actor_member %Actor{} = actor_member
@ -21,10 +24,13 @@ defmodule Mobilizon.GraphQL.Resolvers.Event.Utils do
Event.can_be_managed_by?(event, actor_member_id) Event.can_be_managed_by?(event, actor_member_id)
end end
@spec can_event_be_deleted_by?(%Event{id: String.t(), url: String.t()}, Actor.t()) ::
boolean
def can_event_be_deleted_by?( 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 %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) Permission.can_delete_group_object?(actor_member, event)
end end

View file

@ -4,9 +4,9 @@ defmodule Mobilizon.GraphQL.Resolvers.Followers do
""" """
import Mobilizon.Users.Guards import Mobilizon.Users.Guards
alias Mobilizon.{Actors, Users} alias Mobilizon.Actors
alias Mobilizon.Actors.{Actor, Follower} alias Mobilizon.Actors.{Actor, Follower}
alias Mobilizon.Federation.ActivityPub alias Mobilizon.Federation.ActivityPub.Actions
alias Mobilizon.Storage.Page alias Mobilizon.Storage.Page
alias Mobilizon.Users.User alias Mobilizon.Users.User
@ -16,17 +16,16 @@ defmodule Mobilizon.GraphQL.Resolvers.Followers do
%{page: page, limit: limit} = args, %{page: page, limit: limit} = args,
%{ %{
context: %{ context: %{
current_user: %User{role: user_role} = user current_user: %User{role: user_role},
current_actor: %Actor{id: actor_id}
} }
} }
) do ) do
with {:actor, %Actor{id: actor_id} = _actor} <- {:actor, Users.get_actor_for_user(user)}, if Actors.is_moderator?(actor_id, group_id) or is_moderator(user_role) do
{:member, true} <-
{:member, Actors.is_moderator?(actor_id, group_id) or is_moderator(user_role)} do
{:ok, {:ok,
Actors.list_paginated_followers_for_actor(group, Map.get(args, :approved), page, limit)} Actors.list_paginated_followers_for_actor(group, Map.get(args, :approved), page, limit)}
else else
_ -> {:error, :unauthorized} {:error, :unauthorized}
end end
end end
@ -35,19 +34,18 @@ defmodule Mobilizon.GraphQL.Resolvers.Followers do
@spec update_follower(any(), map(), map()) :: {:ok, Follower.t()} | {:error, any()} @spec update_follower(any(), map(), map()) :: {:ok, Follower.t()} | {:error, any()}
def update_follower(_, %{id: follower_id, approved: approved}, %{ def update_follower(_, %{id: follower_id, approved: approved}, %{
context: %{ context: %{
current_user: %User{} = user current_actor: %Actor{id: actor_id}
} }
}) do }) do
with {:actor, %Actor{id: actor_id} = _actor} <- {:actor, Users.get_actor_for_user(user)}, with %Follower{target_actor: %Actor{type: :Group, id: group_id}} = follower <-
%Follower{target_actor: %Actor{type: :Group, id: group_id}} = follower <-
Actors.get_follower(follower_id), Actors.get_follower(follower_id),
{:member, true} <- {:member, true} <-
{:member, Actors.is_moderator?(actor_id, group_id)}, {:member, Actors.is_moderator?(actor_id, group_id)},
{:ok, _activity, %Follower{} = follower} <- {:ok, _activity, %Follower{} = follower} <-
(if approved do (if approved do
ActivityPub.accept(:follow, follower) Actions.Accept.accept(:follow, follower)
else else
ActivityPub.reject(:follow, follower) Actions.Reject.reject(:follow, follower)
end) do end) do
{:ok, follower} {:ok, follower}
else else

View file

@ -4,9 +4,9 @@ defmodule Mobilizon.GraphQL.Resolvers.Group do
""" """
import Mobilizon.Users.Guards import Mobilizon.Users.Guards
alias Mobilizon.{Actors, Events, Users} alias Mobilizon.{Actors, Events}
alias Mobilizon.Actors.{Actor, Member} alias Mobilizon.Actors.{Actor, Member}
alias Mobilizon.Federation.ActivityPub alias Mobilizon.Federation.ActivityPub.Actions
alias Mobilizon.Federation.ActivityPub.Actor, as: ActivityPubActor alias Mobilizon.Federation.ActivityPub.Actor, as: ActivityPubActor
alias Mobilizon.GraphQL.API alias Mobilizon.GraphQL.API
alias Mobilizon.Users.User alias Mobilizon.Users.User
@ -15,6 +15,12 @@ defmodule Mobilizon.GraphQL.Resolvers.Group do
require Logger require Logger
@spec find_group(
any,
%{:preferred_username => binary, optional(any) => any},
Absinthe.Resolution.t()
) ::
{:error, :group_not_found} | {:ok, Actor.t()}
@doc """ @doc """
Find a group Find a group
""" """
@ -23,34 +29,30 @@ defmodule Mobilizon.GraphQL.Resolvers.Group do
%{preferred_username: name} = args, %{preferred_username: name} = args,
%{ %{
context: %{ context: %{
current_user: %User{} = user current_actor: %Actor{id: actor_id}
} }
} }
) do ) do
with {:group, {:ok, %Actor{id: group_id, suspended: false} = group}} <- case ActivityPubActor.find_or_make_group_from_nickname(name) do
{:group, ActivityPubActor.find_or_make_group_from_nickname(name)}, {:ok, %Actor{id: group_id, suspended: false} = group} ->
{:actor, %Actor{id: actor_id} = _actor} <- {:actor, Users.get_actor_for_user(user)}, if Actors.is_member?(actor_id, group_id) do
{:member, true} <- {:member, Actors.is_member?(actor_id, group_id)} do
{:ok, group} {:ok, group}
else else
{:member, false} ->
find_group(parent, args, nil) find_group(parent, args, nil)
end
{:group, _} -> {:error, _err} ->
{:error, :group_not_found} {:error, :group_not_found}
_ ->
{:error, :unknown}
end end
end end
def find_group(_parent, %{preferred_username: name}, _resolution) do def find_group(_parent, %{preferred_username: name}, _resolution) do
with {:ok, %Actor{suspended: false} = actor} <- case ActivityPubActor.find_or_make_group_from_nickname(name) do
ActivityPubActor.find_or_make_group_from_nickname(name), {:ok, %Actor{suspended: false} = actor} ->
%Actor{} = actor <- restrict_fields_for_non_member_request(actor) do %Actor{} = actor = restrict_fields_for_non_member_request(actor)
{:ok, actor} {:ok, actor}
else
_ -> {:error, _err} ->
{:error, :group_not_found} {:error, :group_not_found}
end end
end end
@ -58,13 +60,18 @@ defmodule Mobilizon.GraphQL.Resolvers.Group do
@doc """ @doc """
Get a group 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 def get_group(_parent, %{id: id}, %{context: %{current_user: %User{role: role}}}) do
with %Actor{type: :Group, suspended: suspended} = actor <- case Actors.get_actor_with_preload(id, true) do
Actors.get_actor_with_preload(id, true), %Actor{type: :Group, suspended: suspended} = actor ->
true <- suspended == false or is_moderator(role) do if suspended == false or is_moderator(role) do
{:ok, actor} {:ok, actor}
else 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)} {:error, dgettext("errors", "Group with ID %{id} not found", id: id)}
end end
end end
@ -72,6 +79,8 @@ defmodule Mobilizon.GraphQL.Resolvers.Group do
@doc """ @doc """
Lists all groups Lists all groups
""" """
@spec list_groups(any(), map(), Absinthe.Resolution.t()) ::
{:ok, Page.t(Actor.t())} | {:error, String.t()}
def list_groups( def list_groups(
_parent, _parent,
%{ %{
@ -96,6 +105,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Group do
do: {:error, dgettext("errors", "You may not list groups unless moderator.")} do: {:error, dgettext("errors", "You may not list groups unless moderator.")}
# TODO Move me to somewhere cleaner # TODO Move me to somewhere cleaner
@spec save_attached_pictures(map()) :: map()
defp save_attached_pictures(args) do defp save_attached_pictures(args) do
Enum.reduce([:avatar, :banner], args, fn key, args -> Enum.reduce([:avatar, :banner], args, fn key, args ->
if is_map(args) && Map.has_key?(args, key) && !is_nil(args[key][:media]) do 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 """ @doc """
Create a new group. The creator is automatically added as admin 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( def create_group(
_parent, _parent,
args, args,
%{ %{
context: %{ context: %{
current_user: user current_actor: %Actor{id: creator_actor_id} = creator_actor
} }
} }
) do ) do
with %Actor{id: creator_actor_id} = creator_actor <- Users.get_actor_for_user(user), with args when is_map(args) <- Map.update(args, :preferred_username, "", &String.downcase/1),
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, creator_actor),
args when is_map(args) <- Map.put(args, :creator_actor_id, creator_actor_id), 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)}, {:picture, args} when is_map(args) <- {:picture, save_attached_pictures(args)},
@ -147,31 +158,34 @@ defmodule Mobilizon.GraphQL.Resolvers.Group do
@doc """ @doc """
Update a group. The creator is automatically added as admin 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( def update_group(
_parent, _parent,
%{id: group_id} = args, %{id: group_id} = args,
%{ %{
context: %{ context: %{
current_user: %User{} = user current_actor: %Actor{} = updater_actor
} }
} }
) do ) do
with %Actor{} = updater_actor <- Users.get_actor_for_user(user), if Actors.is_administrator?(updater_actor.id, group_id) do
{:administrator, true} <- args = Map.put(args, :updater_actor, updater_actor)
{:administrator, Actors.is_administrator?(updater_actor.id, group_id)},
args when is_map(args) <- Map.put(args, :updater_actor, updater_actor), case save_attached_pictures(args) do
{:picture, args} when is_map(args) <- {:picture, save_attached_pictures(args)}, {:error, :file_too_large} ->
{:ok, _activity, %Actor{type: :Group} = group} <-
API.Groups.update_group(args) do
{:ok, group}
else
{:picture, {:error, :file_too_large}} ->
{:error, dgettext("errors", "The provided picture is too heavy")} {:error, dgettext("errors", "The provided picture is too heavy")}
{:error, err} when is_binary(err) -> map when is_map(map) ->
{:error, err} 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")} {:error, dgettext("errors", "Profile is not administrator for the group")}
end end
end end
@ -183,20 +197,21 @@ defmodule Mobilizon.GraphQL.Resolvers.Group do
@doc """ @doc """
Delete an existing group Delete an existing group
""" """
@spec delete_group(any(), map(), Absinthe.Resolution.t()) ::
{:ok, %{id: integer()}} | {:error, String.t()}
def delete_group( def delete_group(
_parent, _parent,
%{group_id: group_id}, %{group_id: group_id},
%{ %{
context: %{ context: %{
current_user: user current_actor: %Actor{id: actor_id} = actor
} }
} }
) do ) do
with %Actor{id: actor_id} = actor <- Users.get_actor_for_user(user), with {:ok, %Actor{} = group} <- Actors.get_group_by_actor_id(group_id),
{:ok, %Actor{} = group} <- Actors.get_group_by_actor_id(group_id),
{:ok, %Member{} = member} <- Actors.get_member(actor_id, group.id), {:ok, %Member{} = member} <- Actors.get_member(actor_id, group.id),
{:is_admin, true} <- {:is_admin, Member.is_administrator(member)}, {: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}} {:ok, %{id: group.id}}
else else
{:error, :group_not_found} -> {:error, :group_not_found} ->
@ -218,16 +233,17 @@ defmodule Mobilizon.GraphQL.Resolvers.Group do
@doc """ @doc """
Join an existing group 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, %{ def join_group(_parent, %{group_id: group_id} = args, %{
context: %{current_user: %User{} = user} context: %{current_actor: %Actor{} = actor}
}) do }) do
with %Actor{} = actor <- Users.get_actor_for_user(user), with {:ok, %Actor{type: :Group} = group} <-
{:ok, %Actor{type: :Group} = group} <-
Actors.get_group_by_actor_id(group_id), Actors.get_group_by_actor_id(group_id),
{:error, :member_not_found} <- Actors.get_member(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)}, {:is_able_to_join, true} <- {:is_able_to_join, Member.can_be_joined(group)},
{:ok, _activity, %Member{} = member} <- {:ok, _activity, %Member{} = member} <-
ActivityPub.join(group, actor, true, args) do Actions.Join.join(group, actor, true, args) do
{:ok, member} {:ok, member}
else else
{:error, :group_not_found} -> {:error, :group_not_found} ->
@ -248,18 +264,20 @@ defmodule Mobilizon.GraphQL.Resolvers.Group do
@doc """ @doc """
Leave a existing group Leave a existing group
""" """
@spec leave_group(any(), map(), Absinthe.Resolution.t()) ::
{:ok, Member.t()} | {:error, String.t()}
def leave_group( def leave_group(
_parent, _parent,
%{group_id: group_id}, %{group_id: group_id},
%{ %{
context: %{ context: %{
current_user: %User{} = user current_actor: %Actor{} = actor
} }
} }
) do ) do
with {:actor, %Actor{} = actor} <- {:actor, Users.get_actor_for_user(user)}, with {:group, %Actor{type: :Group} = group} <- {:group, Actors.get_actor(group_id)},
{:group, %Actor{type: :Group} = group} <- {:group, Actors.get_actor(group_id)}, {:ok, _activity, %Member{} = member} <-
{:ok, _activity, %Member{} = member} <- ActivityPub.leave(group, actor, true) do Actions.Leave.leave(group, actor, true) do
{:ok, member} {:ok, member}
else else
{:error, :member_not_found} -> {:error, :member_not_found} ->
@ -268,7 +286,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Group do
{:group, nil} -> {:group, nil} ->
{:error, dgettext("errors", "Group not found")} {:error, dgettext("errors", "Group not found")}
{:is_not_only_admin, false} -> {:error, :is_not_only_admin} ->
{:error, {:error,
dgettext("errors", "You can't leave this group because you are the only administrator")} dgettext("errors", "You can't leave this group because you are the only administrator")}
end end
@ -278,6 +296,8 @@ defmodule Mobilizon.GraphQL.Resolvers.Group do
{:error, dgettext("errors", "You need to be logged-in to leave a group")} {:error, dgettext("errors", "You need to be logged-in to leave a group")}
end end
@spec find_events_for_group(Actor.t(), map(), Absinthe.Resolution.t()) ::
{:ok, Page.t(Event.t())}
def find_events_for_group( def find_events_for_group(
%Actor{id: group_id} = group, %Actor{id: group_id} = group,
%{ %{
@ -286,13 +306,12 @@ defmodule Mobilizon.GraphQL.Resolvers.Group do
} = args, } = args,
%{ %{
context: %{ context: %{
current_user: %User{role: user_role} = user current_user: %User{role: user_role},
current_actor: %Actor{id: actor_id}
} }
} }
) do ) do
with {:actor, %Actor{id: actor_id} = _actor} <- {:actor, Users.get_actor_for_user(user)}, if Actors.is_member?(actor_id, group_id) or is_moderator(user_role) do
{:member, true} <-
{:member, Actors.is_member?(actor_id, group_id) or is_moderator(user_role)} do
# TODO : Handle public / restricted to group members events # TODO : Handle public / restricted to group members events
{:ok, {:ok,
Events.list_organized_events_for_group( Events.list_organized_events_for_group(
@ -304,7 +323,6 @@ defmodule Mobilizon.GraphQL.Resolvers.Group do
limit limit
)} )}
else else
{:member, false} ->
find_events_for_group(group, args, nil) find_events_for_group(group, args, nil)
end end
end end
@ -328,16 +346,15 @@ defmodule Mobilizon.GraphQL.Resolvers.Group do
)} )}
end end
@spec restrict_fields_for_non_member_request(Actor.t()) :: Actor.t()
defp restrict_fields_for_non_member_request(%Actor{} = group) do defp restrict_fields_for_non_member_request(%Actor{} = group) do
Map.merge( %Actor{
group, group
%{ | followers: [],
followers: [],
followings: [], followings: [],
organized_events: [], organized_events: [],
comments: [], comments: [],
feed_tokens: [] feed_tokens: []
} }
)
end end
end end

View file

@ -4,7 +4,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Media do
""" """
alias Mobilizon.Actors.Actor alias Mobilizon.Actors.Actor
alias Mobilizon.{Medias, Users} alias Mobilizon.Medias
alias Mobilizon.Medias.Media alias Mobilizon.Medias.Media
alias Mobilizon.Users.User alias Mobilizon.Users.User
import Mobilizon.Web.Gettext import Mobilizon.Web.Gettext
@ -44,10 +44,9 @@ defmodule Mobilizon.GraphQL.Resolvers.Media do
def upload_media( def upload_media(
_parent, _parent,
%{file: %Plug.Upload{} = file} = args, %{file: %Plug.Upload{} = file} = args,
%{context: %{current_user: %User{} = user}} %{context: %{current_actor: %Actor{id: actor_id}}}
) do ) do
with %Actor{id: actor_id} <- Users.get_actor_for_user(user), with {:ok,
{:ok,
%{ %{
name: _name, name: _name,
url: url, url: url,
@ -94,6 +93,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Media do
else else
{:media, nil} -> {:error, :not_found} {:media, nil} -> {:error, :not_found}
{:is_owned, _} -> {:error, :unauthorized} {:is_owned, _} -> {:error, :unauthorized}
{:error, :enofile} -> {:error, "File not found"}
end end
end end

View file

@ -4,9 +4,9 @@ defmodule Mobilizon.GraphQL.Resolvers.Member do
""" """
import Mobilizon.Users.Guards import Mobilizon.Users.Guards
alias Mobilizon.{Actors, Users} alias Mobilizon.Actors
alias Mobilizon.Actors.{Actor, Member} alias Mobilizon.Actors.{Actor, Member}
alias Mobilizon.Federation.ActivityPub alias Mobilizon.Federation.ActivityPub.Actions
alias Mobilizon.Federation.ActivityPub.Actor, as: ActivityPubActor alias Mobilizon.Federation.ActivityPub.Actor, as: ActivityPubActor
alias Mobilizon.Storage.Page alias Mobilizon.Storage.Page
alias Mobilizon.Users.User 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 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( def find_members_for_group(
%Actor{id: group_id} = group, %Actor{id: group_id} = group,
%{page: page, limit: limit, roles: roles}, %{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 } = _resolution
) do ) do
with %Actor{id: actor_id} <- Users.get_actor_for_user(user), if Actors.is_member?(actor_id, group_id) or is_moderator(user_role) do
{:member, true} <-
{:member, Actors.is_member?(actor_id, group_id) or is_moderator(user_role)} do
roles = roles =
case roles do case roles do
"" -> "" ->
@ -42,27 +42,25 @@ defmodule Mobilizon.GraphQL.Resolvers.Member do
%Page{} = page = Actors.list_members_for_group(group, roles, page, limit) %Page{} = page = Actors.list_members_for_group(group, roles, page, limit)
{:ok, page} {:ok, page}
else else
{:member, false} ->
# Actor is not member of group, fallback to public # 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: []}} {:ok, %Page{page | elements: []}}
end end
end end
end
def find_members_for_group(%Actor{} = group, _args, _resolution) do 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: []}} {:ok, %Page{page | elements: []}}
end end
end
@spec invite_member(any(), map(), Absinthe.Resolution.t()) ::
{:ok, Member.t()} | {:error, String.t()}
def invite_member( def invite_member(
_parent, _parent,
%{group_id: group_id, target_actor_username: target_actor_username}, %{group_id: group_id, target_actor_username: target_actor_username},
%{context: %{current_user: %User{} = user}} %{context: %{current_actor: %Actor{id: actor_id} = actor}}
) do ) do
with %Actor{id: actor_id} = actor <- Users.get_actor_for_user(user), with {:ok, %Actor{} = group} <- Actors.get_group_by_actor_id(group_id),
{:ok, %Actor{} = group} <- Actors.get_group_by_actor_id(group_id),
{:has_rights_to_invite, {:ok, %Member{role: role}}} {:has_rights_to_invite, {:ok, %Member{role: role}}}
when role in [:moderator, :administrator, :creator] <- when role in [:moderator, :administrator, :creator] <-
{:has_rights_to_invite, Actors.get_member(actor_id, group_id)}, {: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)}, ActivityPubActor.find_or_make_actor_from_nickname(target_actor_username)},
{:existant, true} <- {:existant, true} <-
{:existant, check_member_not_existant_or_rejected(target_actor_id, group.id)}, {: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} {:ok, member}
else else
{:error, :group_not_found} -> {:error, :group_not_found} ->
@ -97,13 +96,16 @@ defmodule Mobilizon.GraphQL.Resolvers.Member do
end end
end end
def accept_invitation(_parent, %{id: member_id}, %{context: %{current_user: %User{} = user}}) do @spec accept_invitation(any(), map(), Absinthe.Resolution.t()) ::
with %Actor{id: actor_id} <- Users.get_actor_for_user(user), {:ok, Member.t()} | {:error, String.t()}
%Member{actor: %Actor{id: member_actor_id}} = member <- 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), 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} <- {:ok, _activity, %Member{} = member} <-
ActivityPub.accept( Actions.Accept.accept(
:invite, :invite,
member, member,
true true
@ -115,13 +117,16 @@ defmodule Mobilizon.GraphQL.Resolvers.Member do
end end
end end
def reject_invitation(_parent, %{id: member_id}, %{context: %{current_user: %User{} = user}}) do @spec reject_invitation(any(), map(), Absinthe.Resolution.t()) ::
with %Actor{id: actor_id} <- Users.get_actor_for_user(user), {:ok, Member.t()} | {:error, String.t()}
{:invitation_exists, %Member{actor: %Actor{id: member_actor_id}} = member} <- 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)}, {: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} <- {:ok, _activity, %Member{} = member} <-
ActivityPub.reject( Actions.Reject.reject(
:invite, :invite,
member, member,
true true
@ -136,19 +141,20 @@ defmodule Mobilizon.GraphQL.Resolvers.Member do
end end
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}, %{ def update_member(_parent, %{member_id: member_id, role: role}, %{
context: %{current_user: %User{} = user} context: %{current_actor: %Actor{} = moderator}
}) do }) do
with %Actor{} = moderator <- Users.get_actor_for_user(user), with %Member{} = member <- Actors.get_member(member_id),
%Member{} = member <- Actors.get_member(member_id),
{:ok, _activity, %Member{} = member} <- {: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} {:ok, member}
else 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")} {:error, dgettext("errors", "You are not a moderator or admin for this group")}
{:is_only_admin, true} -> {:error, :only_admin_left} ->
{:error, {:error,
dgettext( dgettext(
"errors", "errors",
@ -160,16 +166,18 @@ defmodule Mobilizon.GraphQL.Resolvers.Member do
def update_member(_parent, _args, _resolution), def update_member(_parent, _args, _resolution),
do: {:error, "You must be logged-in to update a member"} 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}, %{ 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 }) do
with %Actor{id: moderator_id} = moderator <- Users.get_actor_for_user(user), with %Member{role: role} = member when role != :rejected <- Actors.get_member(member_id),
%Member{role: role} = member when role != :rejected <- Actors.get_member(member_id),
%Actor{type: :Group} = group <- Actors.get_actor(group_id), %Actor{type: :Group} = group <- Actors.get_actor(group_id),
{:has_rights_to_remove, {:ok, %Member{role: role}}} {:has_rights_to_remove, {:ok, %Member{role: role}}}
when role in [:moderator, :administrator, :creator] <- when role in [:moderator, :administrator, :creator] <-
{:has_rights_to_remove, Actors.get_member(moderator_id, group_id)}, {: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} {:ok, member}
else else
%Member{role: :rejected} -> %Member{role: :rejected} ->

View file

@ -2,7 +2,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Participant do
@moduledoc """ @moduledoc """
Handles the participation-related GraphQL calls. 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.Actors.Actor
alias Mobilizon.Events.{Event, Participant} alias Mobilizon.Events.{Event, Participant}
alias Mobilizon.GraphQL.API.Participations alias Mobilizon.GraphQL.API.Participations
@ -16,6 +16,8 @@ defmodule Mobilizon.GraphQL.Resolvers.Participant do
@doc """ @doc """
Join an event for an regular or anonymous actor 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( def actor_join_event(
_parent, _parent,
%{actor_id: actor_id, event_id: event_id} = args, %{actor_id: actor_id, event_id: event_id} = args,
@ -117,7 +119,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Participant do
|> Map.put(:actor, actor) do |> Map.put(:actor, actor) do
{:ok, participant} {:ok, participant}
else else
{:maximum_attendee_capacity, _} -> {:error, :maximum_attendee_capacity_reached} ->
{:error, dgettext("errors", "The event has already reached its maximum capacity")} {:error, dgettext("errors", "The event has already reached its maximum capacity")}
{:has_event, _} -> {:has_event, _} ->
@ -127,39 +129,53 @@ defmodule Mobilizon.GraphQL.Resolvers.Participant do
{:error, :event_not_found} -> {:error, :event_not_found} ->
{:error, dgettext("errors", "Event id 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")} {:error, dgettext("errors", "You are already a participant of this event")}
end end
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 """ @doc """
Leave an event for an anonymous actor 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( def actor_leave_event(
_parent, _parent,
%{actor_id: actor_id, event_id: event_id, token: token}, %{actor_id: actor_id, event_id: event_id, token: token},
_resolution _resolution
) )
when not is_nil(token) do when not is_nil(token) do
with {:anonymous_participation_enabled, true} <- case check_anonymous_participation(actor_id, event_id) do
{:anonymous_participation_enabled, Config.anonymous_participation?()}, {:ok, %Event{} = event} ->
{:anonymous_actor_id, true} <- %Actor{} = actor = Actors.get_actor_with_preload!(actor_id)
{:anonymous_actor_id, to_string(Config.anonymous_actor_id()) == actor_id},
{:has_event, {:ok, %Event{} = event}} <- case Participations.leave(event, actor, %{local: false, cancellation_token: token}) do
{:has_event, Mobilizon.Events.get_event_with_preload(event_id)}, {:ok, _activity, %Participant{id: participant_id} = _participant} ->
%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
{:ok, %{event: %{id: event_id}, actor: %{id: actor_id}, id: participant_id}} {: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, :is_only_organizer} ->
{:error, dgettext("errors", "Profile is not owned by authenticated user")}
{:only_organizer, true} ->
{:error, {:error,
dgettext( dgettext(
"errors", "errors",
@ -168,6 +184,10 @@ defmodule Mobilizon.GraphQL.Resolvers.Participant do
{:error, :participant_not_found} -> {:error, :participant_not_found} ->
{:error, dgettext("errors", "Participant not found")} {:error, dgettext("errors", "Participant not found")}
{:error, _err} ->
{:error, dgettext("errors", "Failed to leave the event")}
end
end end
end end
@ -188,7 +208,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Participant do
{:is_owned, nil} -> {:is_owned, nil} ->
{:error, dgettext("errors", "Profile is not owned by authenticated user")} {:error, dgettext("errors", "Profile is not owned by authenticated user")}
{:only_organizer, true} -> {:error, :is_only_organizer} ->
{:error, {:error,
dgettext( dgettext(
"errors", "errors",
@ -204,19 +224,19 @@ defmodule Mobilizon.GraphQL.Resolvers.Participant do
{:error, dgettext("errors", "You need to be logged-in to leave an event")} {:error, dgettext("errors", "You need to be logged-in to leave an event")}
end end
@spec update_participation(any(), map(), Absinthe.Resolution.t()) ::
{:ok, Participation.t()} | {:error, String.t()}
def update_participation( def update_participation(
_parent, _parent,
%{id: participation_id, role: new_role}, %{id: participation_id, role: new_role},
%{ %{
context: %{ context: %{
current_user: user current_actor: %Actor{} = moderator_actor
} }
} }
) do ) do
# Check that moderator provided is rightly authenticated
with %Actor{} = moderator_actor <- Users.get_actor_for_user(user),
# Check that participation already exists # 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)}, {:has_participation, Events.get_participant(participation_id)},
{:same_role, false} <- {:same_role, new_role == old_role}, {:same_role, false} <- {:same_role, new_role == old_role},
# Check that moderator has right # Check that moderator has right

View file

@ -12,7 +12,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Person do
alias Mobilizon.Users.User alias Mobilizon.Users.User
import Mobilizon.Web.Gettext import Mobilizon.Web.Gettext
alias Mobilizon.Federation.ActivityPub alias Mobilizon.Federation.ActivityPub.Actions
alias Mobilizon.Federation.ActivityPub.Actor, as: ActivityPubActor alias Mobilizon.Federation.ActivityPub.Actor, as: ActivityPubActor
require Logger require Logger
@ -21,6 +21,8 @@ defmodule Mobilizon.GraphQL.Resolvers.Person do
@doc """ @doc """
Get a person 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 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), with %Actor{suspended: suspended} = actor <- Actors.get_actor_with_preload(id, true),
true <- suspended == false or is_moderator(role) do true <- suspended == false or is_moderator(role) do
@ -36,6 +38,8 @@ defmodule Mobilizon.GraphQL.Resolvers.Person do
@doc """ @doc """
Find a person 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}, %{ def fetch_person(_parent, %{preferred_username: preferred_username}, %{
context: %{current_user: %User{} = user} context: %{current_user: %User{} = user}
}) do }) do
@ -57,6 +61,8 @@ defmodule Mobilizon.GraphQL.Resolvers.Person do
def fetch_person(_parent, _args, _resolution), do: {:error, :unauthenticated} 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( def list_persons(
_parent, _parent,
%{ %{
@ -91,8 +97,14 @@ defmodule Mobilizon.GraphQL.Resolvers.Person do
@doc """ @doc """
Returns the current actor for the currently logged-in user Returns the current actor for the currently logged-in user
""" """
def get_current_person(_parent, _args, %{context: %{current_user: user}}) do @spec get_current_person(any, any, Absinthe.Resolution.t()) ::
{:ok, Users.get_actor_for_user(user)} {: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 end
def get_current_person(_parent, _args, _resolution) do def get_current_person(_parent, _args, _resolution) do
@ -102,6 +114,8 @@ defmodule Mobilizon.GraphQL.Resolvers.Person do
@doc """ @doc """
Returns the list of identities for the logged-in user 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 def identities(_parent, _args, %{context: %{current_user: user}}) do
{:ok, Users.get_actors_for_user(user)} {:ok, Users.get_actors_for_user(user)}
end end
@ -113,6 +127,8 @@ defmodule Mobilizon.GraphQL.Resolvers.Person do
@doc """ @doc """
This function is used to create more identities from an existing user 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( def create_person(
_parent, _parent,
%{preferred_username: _preferred_username} = args, %{preferred_username: _preferred_username} = args,
@ -140,6 +156,8 @@ defmodule Mobilizon.GraphQL.Resolvers.Person do
@doc """ @doc """
This function is used to update an existing identity 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( def update_person(
_parent, _parent,
%{id: id} = args, %{id: id} = args,
@ -148,21 +166,24 @@ defmodule Mobilizon.GraphQL.Resolvers.Person do
require Logger require Logger
args = Map.put(args, :user_id, user.id) args = Map.put(args, :user_id, user.id)
with {:find_actor, %Actor{} = actor} <- case owned_actor(user, id) do
{:find_actor, Actors.get_actor(id)}, {:ok, %Actor{} = actor} ->
{:is_owned, %Actor{}} <- User.owns_actor(user, actor.id), case save_attached_pictures(args) do
{:picture, args} when is_map(args) <- {:picture, save_attached_pictures(args)}, args when is_map(args) ->
{:ok, _activity, %Actor{} = actor} <- ActivityPub.update(actor, args, true) do case Actions.Update.update(actor, args, true) do
{:ok, _activity, %Actor{} = actor} ->
{:ok, 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")} {:error, dgettext("errors", "The provided picture is too heavy")}
end
{:find_actor, nil} -> {:error, err} ->
{:error, dgettext("errors", "Profile not found")} {:error, err}
{:is_owned, nil} ->
{:error, dgettext("errors", "Profile is not owned by authenticated user")}
end end
end end
@ -173,30 +194,27 @@ defmodule Mobilizon.GraphQL.Resolvers.Person do
@doc """ @doc """
This function is used to delete an existing identity 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( def delete_person(
_parent, _parent,
%{id: id} = _args, %{id: id} = _args,
%{context: %{current_user: user}} = _resolution %{context: %{current_user: %User{} = user}} = _resolution
) do ) do
with {:find_actor, %Actor{} = actor} <- case owned_actor(user, id) do
{:find_actor, Actors.get_actor(id)}, {:ok, %Actor{} = actor} ->
{:is_owned, %Actor{}} <- User.owns_actor(user, actor.id), if last_identity?(user) do
{: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} ->
{:error, dgettext("errors", "Cannot remove the last identity of a user")} {:error, dgettext("errors", "Cannot remove the last identity of a user")}
else
{:last_admin, true} -> if last_admin_of_a_group?(actor.id) do
{:error, dgettext("errors", "Cannot remove the last administrator of a group")} {:error, dgettext("errors", "Cannot remove the last administrator of a group")}
else
Actors.delete_actor(actor)
end
end
{:is_owned, nil} -> {:error, err} ->
{:error, dgettext("errors", "Profile is not owned by authenticated user")} {:error, err}
end end
end end
@ -204,20 +222,51 @@ defmodule Mobilizon.GraphQL.Resolvers.Person do
{:error, :unauthenticated} {:error, :unauthenticated}
end 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 defp last_identity?(user) do
length(Users.get_actors_for_user(user)) <= 1 length(Users.get_actors_for_user(user)) <= 1
end end
@spec save_attached_pictures(map()) :: map() | {:error, any()}
defp save_attached_pictures(args) do defp save_attached_pictures(args) do
with args when is_map(args) <- save_attached_picture(args, :avatar), case save_attached_picture(args, :avatar) do
args when is_map(args) <- save_attached_picture(args, :banner) 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 args
end end
end end
end
@spec save_attached_picture(map(), :avatar | :banner) :: map() | {:error, any}
defp save_attached_picture(args, key) do defp save_attached_picture(args, key) do
if Map.has_key?(args, key) && !is_nil(args[key][:media]) 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) Map.put(args, key, media)
end end
else else
@ -225,73 +274,82 @@ defmodule Mobilizon.GraphQL.Resolvers.Person do
end end
end end
@spec save_picture(map(), :avatar | :banner) :: {:ok, map()} | {:error, any()}
defp save_picture(media, key) do defp save_picture(media, key) do
with {:ok, %{name: name, url: url, content_type: content_type, size: size}} <- case Upload.store(media.file, type: key, description: media.alt) do
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} %{"name" => name, "url" => url, "content_type" => content_type, "size" => size}
{:error, err} ->
{:error, err}
end end
end end
@doc """ @doc """
This function is used to register a person afterwards the user has been created (but not activated) 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 def register_person(_parent, args, _resolution) do
# When registering, email is assumed confirmed (unlike changing email) # When registering, email is assumed confirmed (unlike changing email)
with {:ok, %User{} = user} <- Users.get_user_by_email(args.email, unconfirmed: false), case Users.get_user_by_email(args.email, unconfirmed: false) do
user_actor <- Users.get_actor_for_user(user), {:ok, %User{} = user} ->
no_actor <- is_nil(user_actor), if is_nil(Users.get_actor_for_user(user)) do
{:no_actor, true} <- {:no_actor, no_actor}, # No profile yet, we can create one
args <- Map.update(args, :preferred_username, "", &String.downcase/1), case prepare_args(args, user) do
args <- Map.put(args, :user_id, user.id), args when is_map(args) ->
{:picture, args} when is_map(args) <- {:picture, save_attached_pictures(args)}, Actors.new_person(args, true)
{:ok, %Actor{} = new_person} <- Actors.new_person(args, true) do
{:ok, new_person} {:error, :file_too_large} ->
else
{:picture, {:error, :file_too_large}} ->
{:error, dgettext("errors", "The provided picture is too heavy")} {: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, :user_not_found} ->
{:error, dgettext("errors", "No user with this email was 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
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 """ @doc """
Returns the participations, optionally restricted to an event 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( def person_participations(
%Actor{id: actor_id} = person, %Actor{id: actor_id} = person,
%{event_id: event_id}, %{event_id: event_id},
%{context: %{current_user: %User{} = user}} %{context: %{current_user: %User{} = user}}
) do ) do
with {:can_get_participations, true} <- if user_can_access_person_details?(person, user) do
{:can_get_participations, user_can_access_person_details?(person, user)}, case Events.get_participant(event_id, actor_id) do
{:no_participant, {:ok, %Participant{} = participant}} <- {:ok, %Participant{} = participant} -> {:ok, %Page{elements: [participant], total: 1}}
{:no_participant, Events.get_participant(event_id, actor_id)} do {:error, :participant_not_found} -> {:ok, %Page{elements: [], total: 0}}
{:ok, %Page{elements: [participant], total: 1}} end
else else
{:is_owned, nil} -> {:error, :unauthorized}
{:error, dgettext("errors", "Profile is not owned by authenticated user")}
{:no_participant, _} ->
{:ok, %Page{elements: [], total: 0}}
end end
end end
def person_participations(%Actor{} = person, %{page: page, limit: limit}, %{ def person_participations(%Actor{} = person, %{page: page, limit: limit}, %{
context: %{current_user: %User{} = user} context: %{current_user: %User{} = user}
}) do }) do
with {:can_get_participations, true} <- if user_can_access_person_details?(person, user) do
{:can_get_participations, user_can_access_person_details?(person, user)}, %Page{} = page = Events.list_event_participations_for_actor(person, page, limit)
%Page{} = page <- Events.list_event_participations_for_actor(person, page, limit) do
{:ok, page} {:ok, page}
else else
{:can_get_participations, false} ->
{:error, dgettext("errors", "Profile is not owned by authenticated user")} {:error, dgettext("errors", "Profile is not owned by authenticated user")}
end end
end end
@ -303,23 +361,22 @@ defmodule Mobilizon.GraphQL.Resolvers.Person do
def person_memberships(%Actor{id: actor_id} = person, %{group: group}, %{ def person_memberships(%Actor{id: actor_id} = person, %{group: group}, %{
context: %{current_user: %User{} = user} context: %{current_user: %User{} = user}
}) do }) do
with {:can_get_memberships, true} <- if user_can_access_person_details?(person, user) do
{:can_get_memberships, user_can_access_person_details?(person, user)}, with {:group, %Actor{id: group_id}} <- {:group, Actors.get_actor_by_name(group, :Group)},
{: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, %Member{} = membership} <- Actors.get_member(actor_id, group_id), {:ok,
memberships <- %Page{ %Page{
total: 1, total: 1,
elements: [Repo.preload(membership, [:actor, :parent, :invited_by])] elements: [Repo.preload(membership, [:actor, :parent, :invited_by])]
} do }}
{:ok, memberships}
else else
{:error, :member_not_found} -> {:error, :member_not_found} ->
{:ok, %Page{total: 0, elements: []}} {:ok, %Page{total: 0, elements: []}}
{:group, nil} -> {:group, nil} ->
{:error, :group_not_found} {:error, :group_not_found}
end
{:can_get_memberships, _} -> else
{:error, dgettext("errors", "Profile is not owned by authenticated user")} {:error, dgettext("errors", "Profile is not owned by authenticated user")}
end end
end end
@ -341,6 +398,8 @@ defmodule Mobilizon.GraphQL.Resolvers.Person do
end end
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, %{ def user_for_person(%Actor{type: :Person, user_id: user_id}, _args, %{
context: %{current_user: %User{role: role}} 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} 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( def organized_events_for_person(
%Actor{} = person, %Actor{} = person,
%{page: page, limit: limit}, %{page: page, limit: limit},
@ -366,12 +427,10 @@ defmodule Mobilizon.GraphQL.Resolvers.Person do
context: %{current_user: %User{} = user} context: %{current_user: %User{} = user}
} }
) do ) do
with {:can_get_events, true} <- if user_can_access_person_details?(person, user) do
{:can_get_events, user_can_access_person_details?(person, user)}, %Page{} = page = Events.list_organized_events_for_actor(person, page, limit)
%Page{} = page <- Events.list_organized_events_for_actor(person, page, limit) do
{:ok, page} {:ok, page}
else else
{:can_get_events, false} ->
{:error, :unauthorized} {:error, :unauthorized}
end end
end end

View file

@ -4,10 +4,9 @@ defmodule Mobilizon.GraphQL.Resolvers.Post do
""" """
import Mobilizon.Users.Guards import Mobilizon.Users.Guards
alias Mobilizon.{Actors, Posts, Users} alias Mobilizon.{Actors, Posts}
alias Mobilizon.Actors.Actor alias Mobilizon.Actors.Actor
alias Mobilizon.Federation.ActivityPub alias Mobilizon.Federation.ActivityPub.{Actions, Permission, Utils}
alias Mobilizon.Federation.ActivityPub.{Permission, Utils}
alias Mobilizon.Posts.Post alias Mobilizon.Posts.Post
alias Mobilizon.Storage.Page alias Mobilizon.Storage.Page
alias Mobilizon.Users.User alias Mobilizon.Users.User
@ -22,22 +21,21 @@ defmodule Mobilizon.GraphQL.Resolvers.Post do
Returns only if actor requesting is a member of the group 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( def find_posts_for_group(
%Actor{id: group_id} = group, %Actor{id: group_id} = group,
%{page: page, limit: limit} = args, %{page: page, limit: limit} = args,
%{ %{
context: %{ context: %{
current_user: %User{role: user_role} = user current_user: %User{role: user_role},
current_actor: %Actor{id: actor_id}
} }
} = _resolution } = _resolution
) do ) do
with %Actor{id: actor_id} <- Users.get_actor_for_user(user), if Actors.is_member?(actor_id, group_id) or is_moderator(user_role) do
{:member, true} <- %Page{} = page = Posts.get_posts_for_group(group, page, limit)
{:member, Actors.is_member?(actor_id, group_id) or is_moderator(user_role)},
%Page{} = page <- Posts.get_posts_for_group(group, page, limit) do
{:ok, page} {:ok, page}
else else
{:member, _} ->
find_posts_for_group(group, args, nil) find_posts_for_group(group, args, nil)
end end
end end
@ -47,10 +45,9 @@ defmodule Mobilizon.GraphQL.Resolvers.Post do
%{page: page, limit: limit}, %{page: page, limit: limit},
_resolution _resolution
) do ) 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} {:ok, page}
end end
end
def find_posts_for_group( def find_posts_for_group(
_group, _group,
@ -60,18 +57,19 @@ defmodule Mobilizon.GraphQL.Resolvers.Post do
{:ok, %Page{total: 0, elements: []}} {:ok, %Page{total: 0, elements: []}}
end end
@spec get_post(any(), map(), Absinthe.Resolution.t()) ::
{:ok, Post.t()} | {:error, :post_not_found}
def get_post( def get_post(
parent, parent,
%{slug: slug}, %{slug: slug},
%{ %{
context: %{ context: %{
current_user: %User{role: user_role} = user current_user: %User{role: user_role},
current_actor: %Actor{} = current_profile
} }
} = _resolution } = _resolution
) do ) do
with {:current_actor, %Actor{} = current_profile} <- with {:post, %Post{attributed_to: %Actor{}} = post} <-
{:current_actor, Users.get_actor_for_user(user)},
{:post, %Post{attributed_to: %Actor{}} = post} <-
{:post, Posts.get_post_by_slug_with_preloads(slug)}, {:post, Posts.get_post_by_slug_with_preloads(slug)},
{:member, true} <- {:member, true} <-
{:member, {:member,
@ -102,17 +100,18 @@ defmodule Mobilizon.GraphQL.Resolvers.Post do
{:error, :post_not_found} {:error, :post_not_found}
end end
@spec create_post(any(), map(), Absinthe.Resolution.t()) ::
{:ok, Post.t()} | {:error, String.t()}
def create_post( def create_post(
_parent, _parent,
%{attributed_to_id: group_id} = args, %{attributed_to_id: group_id} = args,
%{ %{
context: %{ context: %{
current_user: %User{} = user current_actor: %Actor{id: actor_id}
} }
} = _resolution } = _resolution
) do ) do
with %Actor{id: actor_id} <- Users.get_actor_for_user(user), with {:member, true} <- {:member, Actors.is_member?(actor_id, group_id)},
{:member, true} <- {:member, Actors.is_member?(actor_id, group_id)},
%Actor{} = group <- Actors.get_actor(group_id), %Actor{} = group <- Actors.get_actor(group_id),
args <- args <-
Map.update(args, :picture, nil, fn picture -> Map.update(args, :picture, nil, fn picture ->
@ -120,7 +119,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Post do
end), end),
args <- extract_pictures_from_post_body(args, actor_id), args <- extract_pictures_from_post_body(args, actor_id),
{:ok, _, %Post{} = post} <- {:ok, _, %Post{} = post} <-
ActivityPub.create( Actions.Create.create(
:post, :post,
args args
|> Map.put(:author_id, actor_id) |> 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")} {:error, dgettext("errors", "You need to be logged-in to create posts")}
end end
@spec update_post(any(), map(), Absinthe.Resolution.t()) ::
{:ok, Post.t()} | {:error, String.t()}
def update_post( def update_post(
_parent, _parent,
%{id: id} = args, %{id: id} = args,
%{ %{
context: %{ context: %{
current_user: %User{} = user current_actor: %Actor{id: actor_id, url: actor_url}
} }
} = _resolution } = _resolution
) do ) do
with {:uuid, {:ok, _uuid}} <- {:uuid, Ecto.UUID.cast(id)}, 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, %Post{attributed_to: %Actor{id: group_id} = group} = post} <-
{:post, Posts.get_post_with_preloads(id)}, {:post, Posts.get_post_with_preloads(id)},
args <- args <-
@ -162,7 +162,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Post do
args <- extract_pictures_from_post_body(args, actor_id), args <- extract_pictures_from_post_body(args, actor_id),
{:member, true} <- {:member, Actors.is_member?(actor_id, group_id)}, {:member, true} <- {:member, Actors.is_member?(actor_id, group_id)},
{:ok, _, %Post{} = post} <- {:ok, _, %Post{} = post} <-
ActivityPub.update(post, args, true, %{"actor" => actor_url}) do Actions.Update.update(post, args, true, %{"actor" => actor_url}) do
{:ok, post} {:ok, post}
else else
{:uuid, :error} -> {:uuid, :error} ->
@ -180,22 +180,23 @@ defmodule Mobilizon.GraphQL.Resolvers.Post do
{:error, dgettext("errors", "You need to be logged-in to update posts")} {:error, dgettext("errors", "You need to be logged-in to update posts")}
end end
@spec delete_post(any(), map(), Absinthe.Resolution.t()) ::
{:ok, Post.t()} | {:error, String.t()}
def delete_post( def delete_post(
_parent, _parent,
%{id: post_id}, %{id: post_id},
%{ %{
context: %{ context: %{
current_user: %User{} = user current_actor: %Actor{id: actor_id} = actor
} }
} = _resolution } = _resolution
) do ) do
with {:uuid, {:ok, _uuid}} <- {:uuid, Ecto.UUID.cast(post_id)}, 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, %Post{attributed_to: %Actor{id: group_id}} = post} <-
{:post, Posts.get_post_with_preloads(post_id)}, {:post, Posts.get_post_with_preloads(post_id)},
{:member, true} <- {:member, Actors.is_member?(actor_id, group_id)}, {:member, true} <- {:member, Actors.is_member?(actor_id, group_id)},
{:ok, _, %Post{} = post} <- {:ok, _, %Post{} = post} <-
ActivityPub.delete(post, actor) do Actions.Delete.delete(post, actor) do
{:ok, post} {:ok, post}
else else
{:uuid, :error} -> {:uuid, :error} ->
@ -213,6 +214,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Post do
{:error, dgettext("errors", "You need to be logged-in to delete posts")} {:error, dgettext("errors", "You need to be logged-in to delete posts")}
end end
@spec process_picture(map() | nil, Actor.t()) :: nil | map()
defp process_picture(nil, _), do: nil defp process_picture(nil, _), do: nil
defp process_picture(%{media_id: _picture_id} = args, _), do: args defp process_picture(%{media_id: _picture_id} = args, _), do: args

View file

@ -10,6 +10,8 @@ defmodule Mobilizon.GraphQL.Resolvers.PushSubscription do
@doc """ @doc """
List all of an user's registered push subscriptions 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}, %{ def list_user_push_subscriptions(_parent, %{page: page, limit: limit}, %{
context: %{current_user: %User{id: user_id}} context: %{current_user: %User{id: user_id}}
}) do }) do
@ -22,6 +24,8 @@ defmodule Mobilizon.GraphQL.Resolvers.PushSubscription do
@doc """ @doc """
Register a push subscription 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, %{ def register_push_subscription(_parent, args, %{
context: %{current_user: %User{id: user_id}} context: %{current_user: %User{id: user_id}}
}) do }) do

View file

@ -5,7 +5,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Report do
import Mobilizon.Users.Guards import Mobilizon.Users.Guards
alias Mobilizon.{Actors, Config, Reports, Users} alias Mobilizon.{Actors, Config, Reports}
alias Mobilizon.Actors.Actor alias Mobilizon.Actors.Actor
alias Mobilizon.Reports.{Note, Report} alias Mobilizon.Reports.{Note, Report}
alias Mobilizon.Users.User alias Mobilizon.Users.User
@ -13,6 +13,8 @@ defmodule Mobilizon.GraphQL.Resolvers.Report do
alias Mobilizon.GraphQL.API alias Mobilizon.GraphQL.API
@spec list_reports(any(), map(), Absinthe.Resolution.t()) ::
{:ok, Page.t(Report.t())} | {:error, String.t()}
def list_reports( def list_reports(
_parent, _parent,
%{page: page, limit: limit, status: status}, %{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")} {:error, dgettext("errors", "You need to be logged-in and a moderator to list reports")}
end 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}}}) def get_report(_parent, %{id: id}, %{context: %{current_user: %User{role: role}}})
when is_moderator(role) do when is_moderator(role) do
case Mobilizon.Reports.get_report(id) do case Mobilizon.Reports.get_report(id) do
@ -44,16 +48,17 @@ defmodule Mobilizon.GraphQL.Resolvers.Report do
@doc """ @doc """
Create a report, either logged-in or anonymously 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( def create_report(
_parent, _parent,
args, args,
%{context: %{current_user: %User{} = user}} = _resolution %{context: %{current_actor: %Actor{id: reporter_id}}} = _resolution
) do ) do
with %Actor{id: reporter_id} <- Users.get_actor_for_user(user), case args |> Map.put(:reporter_id, reporter_id) |> API.Reports.report() do
{:ok, _, %Report{} = report} <- {:ok, _, %Report{} = report} ->
args |> Map.put(:reporter_id, reporter_id) |> API.Reports.report() do
{:ok, report} {:ok, report}
else
_error -> _error ->
{:error, dgettext("errors", "Error while saving report")} {:error, dgettext("errors", "Error while saving report")}
end end
@ -81,14 +86,15 @@ defmodule Mobilizon.GraphQL.Resolvers.Report do
@doc """ @doc """
Update a report's status Update a report's status
""" """
@spec update_report(any(), map(), Absinthe.Resolution.t()) ::
{:ok, Report.t()} | {:error, String.t()}
def update_report( def update_report(
_parent, _parent,
%{report_id: report_id, status: status}, %{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 when is_moderator(role) do
with %Actor{} = actor <- Users.get_actor_for_user(user), with %Report{} = report <- Mobilizon.Reports.get_report(report_id),
%Report{} = report <- Mobilizon.Reports.get_report(report_id),
{:ok, %Report{} = report} <- API.Reports.update_report_status(actor, report, status) do {:ok, %Report{} = report} <- API.Reports.update_report_status(actor, report, status) do
{:ok, report} {:ok, report}
else 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")} {:error, dgettext("errors", "You need to be logged-in and a moderator to update a report")}
end end
@spec create_report_note(any(), map(), Absinthe.Resolution.t()) :: {:ok, Note.t()}
def create_report_note( def create_report_note(
_parent, _parent,
%{report_id: report_id, content: content}, %{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 when is_moderator(role) do
with %Actor{id: moderator_id} <- Users.get_actor_for_user(user), with %Report{} = report <- Reports.get_report(report_id),
%Report{} = report <- Reports.get_report(report_id),
%Actor{} = moderator <- Actors.get_local_actor_with_preload(moderator_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{} = note} <- API.Reports.create_report_note(report, moderator, content) do
{:ok, note} {:ok, note}
end end
end end
@spec delete_report_note(any(), map(), Absinthe.Resolution.t()) :: {:ok, map()}
def delete_report_note( def delete_report_note(
_parent, _parent,
%{note_id: note_id}, %{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 when is_moderator(role) do
with %Actor{id: moderator_id} <- Users.get_actor_for_user(user), with %Note{} = note <- Reports.get_note(note_id),
%Note{} = note <- Reports.get_note(note_id),
%Actor{} = moderator <- Actors.get_local_actor_with_preload(moderator_id), %Actor{} = moderator <- Actors.get_local_actor_with_preload(moderator_id),
{:ok, %Note{} = note} <- API.Reports.delete_report_note(note, moderator) do {:ok, %Note{} = note} <- API.Reports.delete_report_note(note, moderator) do
{:ok, %{id: note.id}} {:ok, %{id: note.id}}

View file

@ -3,9 +3,9 @@ defmodule Mobilizon.GraphQL.Resolvers.Resource do
Handles the resources-related GraphQL calls Handles the resources-related GraphQL calls
""" """
alias Mobilizon.{Actors, Resources, Users} alias Mobilizon.{Actors, Resources}
alias Mobilizon.Actors.Actor alias Mobilizon.Actors.Actor
alias Mobilizon.Federation.ActivityPub alias Mobilizon.Federation.ActivityPub.Actions
alias Mobilizon.Resources.Resource alias Mobilizon.Resources.Resource
alias Mobilizon.Resources.Resource.Metadata alias Mobilizon.Resources.Resource.Metadata
alias Mobilizon.Service.RichMedia.Parser 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 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( def find_resources_for_group(
%Actor{id: group_id} = group, %Actor{id: group_id} = group,
%{page: page, limit: limit}, %{page: page, limit: limit},
%{ %{
context: %{ context: %{
current_user: %User{} = user current_actor: %Actor{id: actor_id}
} }
} = _resolution } = _resolution
) do ) do
with %Actor{id: actor_id} <- Users.get_actor_for_user(user), with {:member, true} <- {:member, Actors.is_member?(actor_id, group_id)},
{:member, true} <- {:member, Actors.is_member?(actor_id, group_id)},
%Page{} = page <- Resources.get_resources_for_group(group, page, limit) do %Page{} = page <- Resources.get_resources_for_group(group, page, limit) do
{:ok, page} {:ok, page}
else else
@ -48,17 +49,18 @@ defmodule Mobilizon.GraphQL.Resolvers.Resource do
{:ok, %Page{total: 0, elements: []}} {:ok, %Page{total: 0, elements: []}}
end end
@spec find_resources_for_parent(Resource.t(), map(), Absinthe.Resolution.t()) ::
{:ok, Page.t(Resource.t())}
def find_resources_for_parent( def find_resources_for_parent(
%Resource{actor_id: group_id} = parent, %Resource{actor_id: group_id} = parent,
%{page: page, limit: limit}, %{page: page, limit: limit},
%{ %{
context: %{ context: %{
current_user: %User{} = user current_actor: %Actor{id: actor_id}
} }
} = _resolution } = _resolution
) do ) do
with %Actor{id: actor_id} <- Users.get_actor_for_user(user), with {:member, true} <- {:member, Actors.is_member?(actor_id, group_id)},
{:member, true} <- {:member, Actors.is_member?(actor_id, group_id)},
%Page{} = page <- Resources.get_resources_for_folder(parent, page, limit) do %Page{} = page <- Resources.get_resources_for_folder(parent, page, limit) do
{:ok, page} {:ok, page}
end end
@ -67,18 +69,18 @@ defmodule Mobilizon.GraphQL.Resolvers.Resource do
def find_resources_for_parent(_parent, _args, _resolution), def find_resources_for_parent(_parent, _args, _resolution),
do: {:ok, %Page{total: 0, elements: []}} 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( def get_resource(
_parent, _parent,
%{path: path, username: username}, %{path: path, username: username},
%{ %{
context: %{ context: %{
current_user: %User{} = user current_actor: %Actor{id: actor_id}
} }
} = _resolution } = _resolution
) do ) do
with {:current_actor, %Actor{id: actor_id}} <- with {:group, %Actor{id: group_id}} <- {:group, Actors.get_actor_by_name(username, :Group)},
{:current_actor, Users.get_actor_for_user(user)},
{:group, %Actor{id: group_id}} <- {:group, Actors.get_actor_by_name(username, :Group)},
{:member, true} <- {:member, Actors.is_member?(actor_id, group_id)}, {:member, true} <- {:member, Actors.is_member?(actor_id, group_id)},
{:resource, %Resource{} = resource} <- {:resource, %Resource{} = resource} <-
{:resource, Resources.get_resource_by_group_and_path_with_preloads(group_id, path)} do {: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")} {:error, dgettext("errors", "You need to be logged-in to access resources")}
end end
@spec create_resource(any(), map(), Absinthe.Resolution.t()) ::
{:ok, Resource.t()} | {:error, String.t()}
def create_resource( def create_resource(
_parent, _parent,
%{actor_id: group_id} = args, %{actor_id: group_id} = args,
%{ %{
context: %{ context: %{
current_user: %User{} = user current_actor: %Actor{id: actor_id}
} }
} = _resolution } = _resolution
) do ) do
with %Actor{id: actor_id} <- Users.get_actor_for_user(user), with {:member, true} <- {:member, Actors.is_member?(actor_id, group_id)},
{:member, true} <- {:member, Actors.is_member?(actor_id, group_id)},
parent <- get_eventual_parent(args), parent <- get_eventual_parent(args),
{:own_check, true} <- {:own_check, check_resource_owned_by_group(parent, group_id)}, {:own_check, true} <- {:own_check, check_resource_owned_by_group(parent, group_id)},
{:ok, _, %Resource{} = resource} <- {:ok, _, %Resource{} = resource} <-
ActivityPub.create( Actions.Create.create(
:resource, :resource,
args args
|> Map.put(:actor_id, group_id) |> 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")} {:error, dgettext("errors", "You need to be logged-in to create resources")}
end end
@spec update_resource(any(), map(), Absinthe.Resolution.t()) ::
{:ok, Resource.t()} | {:error, String.t()}
def update_resource( def update_resource(
_parent, _parent,
%{id: resource_id} = args, %{id: resource_id} = args,
%{ %{
context: %{ context: %{
current_user: %User{} = user current_actor: %Actor{id: actor_id, url: actor_url}
} }
} = _resolution } = _resolution
) do ) do
with %Actor{id: actor_id, url: actor_url} <- Users.get_actor_for_user(user), case Resources.get_resource_with_preloads(resource_id) do
{:resource, %Resource{actor_id: group_id} = resource} <- %Resource{actor_id: group_id} = resource ->
{:resource, Resources.get_resource_with_preloads(resource_id)}, if Actors.is_member?(actor_id, group_id) do
{:member, true} <- {:member, Actors.is_member?(actor_id, group_id)}, case Actions.Update.update(resource, args, true, %{"actor" => actor_url}) do
{:ok, _, %Resource{} = resource} <- {:ok, _, %Resource{} = resource} ->
ActivityPub.update(resource, args, true, %{"actor" => actor_url}) do
{:ok, 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")} {:error, dgettext("errors", "Profile is not member of group")}
end end
nil ->
{:error, dgettext("errors", "Resource doesn't exist")}
end
end end
def update_resource(_parent, _args, _resolution) do def update_resource(_parent, _args, _resolution) do
{:error, dgettext("errors", "You need to be logged-in to update resources")} {:error, dgettext("errors", "You need to be logged-in to update resources")}
end end
@spec delete_resource(any(), map(), Absinthe.Resolution.t()) ::
{:ok, Resource.t()} | {:error, String.t()}
def delete_resource( def delete_resource(
_parent, _parent,
%{id: resource_id}, %{id: resource_id},
%{ %{
context: %{ context: %{
current_user: %User{} = user current_actor: %Actor{id: actor_id} = actor
} }
} = _resolution } = _resolution
) do ) do
with %Actor{id: actor_id} = actor <- Users.get_actor_for_user(user), with {:resource, %Resource{parent_id: _parent_id, actor_id: group_id} = resource} <-
{:resource, %Resource{parent_id: _parent_id, actor_id: group_id} = resource} <-
{:resource, Resources.get_resource_with_preloads(resource_id)}, {:resource, Resources.get_resource_with_preloads(resource_id)},
{:member, true} <- {:member, Actors.is_member?(actor_id, group_id)}, {:member, true} <- {:member, Actors.is_member?(actor_id, group_id)},
{:ok, _, %Resource{} = resource} <- {:ok, _, %Resource{} = resource} <-
ActivityPub.delete(resource, actor) do Actions.Delete.delete(resource, actor) do
{:ok, resource} {:ok, resource}
else else
{:resource, _} -> {:resource, _} ->
@ -191,6 +203,8 @@ defmodule Mobilizon.GraphQL.Resolvers.Resource do
{:error, dgettext("errors", "You need to be logged-in to delete resources")} {:error, dgettext("errors", "You need to be logged-in to delete resources")}
end end
@spec preview_resource_link(any(), map(), Absinthe.Resolution.t()) ::
{:ok, Metadata.t()} | {:error, String.t() | :unknown_resource}
def preview_resource_link( def preview_resource_link(
_parent, _parent,
%{resource_url: resource_url}, %{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")} {:error, dgettext("errors", "You need to be logged-in to view a resource preview")}
end end
@spec proxyify_pictures(Metadata.t(), map(), Absinthe.Resolution.t()) ::
{:ok, String.t() | nil} | {:error, String.t()}
def proxyify_pictures(%Metadata{} = metadata, _args, %{ def proxyify_pictures(%Metadata{} = metadata, _args, %{
definition: %{schema_node: %{name: name}} definition: %{schema_node: %{name: name}}
}) do }) do

View file

@ -2,12 +2,16 @@ defmodule Mobilizon.GraphQL.Resolvers.Search do
@moduledoc """ @moduledoc """
Handles the event-related GraphQL calls Handles the event-related GraphQL calls
""" """
alias Mobilizon.Actors.Actor
alias Mobilizon.Events.Event
alias Mobilizon.GraphQL.API.Search alias Mobilizon.GraphQL.API.Search
alias Mobilizon.Storage.Page
@doc """ @doc """
Search persons 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 def search_persons(_parent, %{page: page, limit: limit} = args, _resolution) do
Search.search_actors(Map.put(args, :minimum_visibility, :private), page, limit, :Person) Search.search_actors(Map.put(args, :minimum_visibility, :private), page, limit, :Person)
end end
@ -15,6 +19,8 @@ defmodule Mobilizon.GraphQL.Resolvers.Search do
@doc """ @doc """
Search groups 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 def search_groups(_parent, %{page: page, limit: limit} = args, _resolution) do
Search.search_actors(args, page, limit, :Group) Search.search_actors(args, page, limit, :Group)
end end
@ -22,10 +28,13 @@ defmodule Mobilizon.GraphQL.Resolvers.Search do
@doc """ @doc """
Search events 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 def search_events(_parent, %{page: page, limit: limit} = args, _resolution) do
Search.search_events(args, page, limit) Search.search_events(args, page, limit)
end end
@spec interact(any(), map(), Absinthe.Resolution.t()) :: {:ok, struct} | {:error, :not_found}
def interact(_parent, %{uri: uri}, _resolution) do def interact(_parent, %{uri: uri}, _resolution) do
Search.interact(uri) Search.interact(uri)
end end

View file

@ -8,6 +8,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Statistics do
@doc """ @doc """
Gets config. Gets config.
""" """
@spec get_statistics(any(), any(), any()) :: {:ok, map()}
def get_statistics(_parent, _params, _context) do def get_statistics(_parent, _params, _context) do
{:ok, {:ok,
%{ %{

View file

@ -6,9 +6,12 @@ defmodule Mobilizon.GraphQL.Resolvers.Tag do
alias Mobilizon.{Events, Posts} alias Mobilizon.{Events, Posts}
alias Mobilizon.Events.{Event, Tag} alias Mobilizon.Events.{Event, Tag}
alias Mobilizon.Posts.Post alias Mobilizon.Posts.Post
alias Mobilizon.Storage.Page
def list_tags(_parent, %{page: page, limit: limit}, _resolution) do @spec list_tags(any(), map(), Absinthe.Resolution.t()) :: {:ok, Page.t(Tag.t())}
tags = Mobilizon.Events.list_tags(page, limit) 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} {:ok, tags}
end end
@ -18,6 +21,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Tag do
From an event or a struct with an url 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 def list_tags_for_event(%Event{id: id}, _args, _resolution) do
{:ok, Events.list_tags_for_event(id)} {:ok, Events.list_tags_for_event(id)}
end end
@ -32,6 +36,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Tag do
@doc """ @doc """
Retrieve the list of tags for a post 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 def list_tags_for_post(%Post{id: id}, _args, _resolution) do
{:ok, Posts.list_tags_for_post(id)} {:ok, Posts.list_tags_for_post(id)}
end end
@ -49,9 +54,8 @@ defmodule Mobilizon.GraphQL.Resolvers.Tag do
@doc """ @doc """
Retrieve the list of related tags for a parent tag 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 def get_related_tags(%Tag{} = tag, _args, _resolution) do
with tags <- Events.list_tag_neighbors(tag) do {:ok, Events.list_tag_neighbors(tag)}
{:ok, tags}
end
end end
end end

View file

@ -3,12 +3,11 @@ defmodule Mobilizon.GraphQL.Resolvers.Todos do
Handles the todos related GraphQL calls Handles the todos related GraphQL calls
""" """
alias Mobilizon.{Actors, Todos, Users} alias Mobilizon.{Actors, Todos}
alias Mobilizon.Actors.Actor alias Mobilizon.Actors.Actor
alias Mobilizon.Federation.ActivityPub alias Mobilizon.Federation.ActivityPub.Actions
alias Mobilizon.Storage.Page alias Mobilizon.Storage.Page
alias Mobilizon.Todos.{Todo, TodoList} alias Mobilizon.Todos.{Todo, TodoList}
alias Mobilizon.Users.User
import Mobilizon.Web.Gettext import Mobilizon.Web.Gettext
require Logger require Logger
@ -18,15 +17,16 @@ defmodule Mobilizon.GraphQL.Resolvers.Todos do
Returns only if actor requesting is a member of the group 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( def find_todo_lists_for_group(
%Actor{id: group_id} = group, %Actor{id: group_id} = group,
_args, _args,
%{ %{
context: %{current_user: %User{} = user} context: %{current_actor: %Actor{id: actor_id}}
} = _resolution } = _resolution
) do ) do
with %Actor{id: actor_id} <- Users.get_actor_for_user(user), with {:member, true} <- {:member, Actors.is_member?(actor_id, group_id)},
{:member, true} <- {:member, Actors.is_member?(actor_id, group_id)},
%Page{} = page <- Todos.get_todo_lists_for_group(group) do %Page{} = page <- Todos.get_todo_lists_for_group(group) do
{:ok, page} {:ok, page}
else else
@ -41,15 +41,16 @@ defmodule Mobilizon.GraphQL.Resolvers.Todos do
{:ok, %Page{total: 0, elements: []}} {:ok, %Page{total: 0, elements: []}}
end 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( def find_todos_for_todo_list(
%TodoList{actor_id: group_id} = todo_list, %TodoList{actor_id: group_id} = todo_list,
_args, _args,
%{ %{
context: %{current_user: %User{} = user} context: %{current_actor: %Actor{id: actor_id}}
} = _resolution } = _resolution
) do ) do
with %Actor{id: actor_id} <- Users.get_actor_for_user(user), with {:member, true} <- {:member, Actors.is_member?(actor_id, group_id)},
{:member, true} <- {:member, Actors.is_member?(actor_id, group_id)},
%Page{} = page <- Todos.get_todos_for_todo_list(todo_list) do %Page{} = page <- Todos.get_todos_for_todo_list(todo_list) do
{:ok, page} {:ok, page}
else else
@ -58,15 +59,16 @@ defmodule Mobilizon.GraphQL.Resolvers.Todos do
end end
end end
@spec get_todo_list(any(), map(), Absinthe.Resolution.t()) ::
{:ok, TodoList.t()} | {:error, String.t()}
def get_todo_list( def get_todo_list(
_parent, _parent,
%{id: todo_list_id}, %{id: todo_list_id},
%{ %{
context: %{current_user: %User{} = user} context: %{current_actor: %Actor{id: actor_id}}
} = _resolution } = _resolution
) do ) do
with {:actor, %Actor{id: actor_id}} <- {:actor, Users.get_actor_for_user(user)}, with {:todo, %TodoList{actor_id: group_id} = todo} <-
{:todo, %TodoList{actor_id: group_id} = todo} <-
{:todo, Todos.get_todo_list(todo_list_id)}, {:todo, Todos.get_todo_list(todo_list_id)},
{:member, true} <- {:member, Actors.is_member?(actor_id, group_id)} do {:member, true} <- {:member, Actors.is_member?(actor_id, group_id)} do
{:ok, todo} {:ok, todo}
@ -82,19 +84,28 @@ defmodule Mobilizon.GraphQL.Resolvers.Todos do
end end
end end
@spec create_todo_list(any(), map(), Absinthe.Resolution.t()) ::
{:ok, TodoList.t()} | {:error, String.t()}
def create_todo_list( def create_todo_list(
_parent, _parent,
%{group_id: group_id} = args, %{group_id: group_id} = args,
%{ %{
context: %{current_user: %User{} = user} context: %{current_actor: %Actor{id: actor_id}}
} = _resolution } = _resolution
) do ) do
with {:actor, %Actor{id: actor_id} = _actor} <- {:actor, Users.get_actor_for_user(user)}, with {:member, true} <- {:member, Actors.is_member?(actor_id, group_id)},
{:member, true} <- {:member, Actors.is_member?(actor_id, group_id)},
{:ok, _, %TodoList{} = todo_list} <- {: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} {:ok, todo_list}
else else
{:actor, nil} ->
{:error, dgettext("errors", "No profile found for user")}
{:member, _} -> {:member, _} ->
{:error, dgettext("errors", "Profile is not member of group")} {:error, dgettext("errors", "Profile is not member of group")}
end end
@ -112,7 +123,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Todos do
# {:todo_list, Todos.get_todo_list(todo_list_id)}, # {:todo_list, Todos.get_todo_list(todo_list_id)},
# {:member, true} <- {:member, Actors.is_member?(actor_id, group_id)}, # {:member, true} <- {:member, Actors.is_member?(actor_id, group_id)},
# {:ok, _, %TodoList{} = todo} <- # {:ok, _, %TodoList{} = todo} <-
# ActivityPub.update_todo_list(todo_list, actor, true, %{}) do # Actions.Update.update_todo_list(todo_list, actor, true, %{}) do
# {:ok, todo} # {:ok, todo}
# else # else
# {:todo_list, _} -> # {:todo_list, _} ->
@ -135,7 +146,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Todos do
# {:todo_list, Todos.get_todo_list(todo_list_id)}, # {:todo_list, Todos.get_todo_list(todo_list_id)},
# {:member, true} <- {:member, Actors.is_member?(actor_id, group_id)}, # {:member, true} <- {:member, Actors.is_member?(actor_id, group_id)},
# {:ok, _, %TodoList{} = todo} <- # {:ok, _, %TodoList{} = todo} <-
# ActivityPub.delete_todo_list(todo_list, actor, true, %{}) do # Actions.Delete.delete_todo_list(todo_list, actor, true, %{}) do
# {:ok, todo} # {:ok, todo}
# else # else
# {:todo_list, _} -> # {:todo_list, _} ->
@ -146,15 +157,15 @@ defmodule Mobilizon.GraphQL.Resolvers.Todos do
# end # end
# end # end
@spec get_todo(any(), map(), Absinthe.Resolution.t()) :: {:ok, Todo.t()} | {:error, String.t()}
def get_todo( def get_todo(
_parent, _parent,
%{id: todo_id}, %{id: todo_id},
%{ %{
context: %{current_user: %User{} = user} context: %{current_actor: %Actor{id: actor_id}}
} = _resolution } = _resolution
) do ) do
with {:actor, %Actor{id: actor_id}} <- {:actor, Users.get_actor_for_user(user)}, with {:todo, %Todo{todo_list_id: todo_list_id} = todo} <-
{:todo, %Todo{todo_list_id: todo_list_id} = todo} <-
{:todo, Todos.get_todo(todo_id)}, {:todo, Todos.get_todo(todo_id)},
{:todo_list, %TodoList{actor_id: group_id}} <- {:todo_list, %TodoList{actor_id: group_id}} <-
{:todo_list, Todos.get_todo_list(todo_list_id)}, {:todo_list, Todos.get_todo_list(todo_list_id)},
@ -172,21 +183,30 @@ defmodule Mobilizon.GraphQL.Resolvers.Todos do
end end
end end
@spec create_todo(any(), map(), Absinthe.Resolution.t()) ::
{:ok, Todo.t()} | {:error, String.t()}
def create_todo( def create_todo(
_parent, _parent,
%{todo_list_id: todo_list_id} = args, %{todo_list_id: todo_list_id} = args,
%{ %{
context: %{current_user: %User{} = user} context: %{current_actor: %Actor{id: actor_id}}
} = _resolution } = _resolution
) do ) do
with {:actor, %Actor{id: actor_id} = _actor} <- {:actor, Users.get_actor_for_user(user)}, with {:todo_list, %TodoList{actor_id: group_id} = _todo_list} <-
{:todo_list, %TodoList{actor_id: group_id} = _todo_list} <-
{:todo_list, Todos.get_todo_list(todo_list_id)}, {:todo_list, Todos.get_todo_list(todo_list_id)},
{:member, true} <- {:member, Actors.is_member?(actor_id, group_id)}, {:member, true} <- {:member, Actors.is_member?(actor_id, group_id)},
{:ok, _, %Todo{} = todo} <- {: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} {:ok, todo}
else else
{:actor, nil} ->
{:error, dgettext("errors", "No profile found for user")}
{:todo_list, _} -> {:todo_list, _} ->
{:error, dgettext("errors", "Todo list doesn't exist")} {:error, dgettext("errors", "Todo list doesn't exist")}
@ -195,23 +215,27 @@ defmodule Mobilizon.GraphQL.Resolvers.Todos do
end end
end end
@spec update_todo(any(), map(), Absinthe.Resolution.t()) ::
{:ok, Todo.t()} | {:error, String.t()}
def update_todo( def update_todo(
_parent, _parent,
%{id: todo_id} = args, %{id: todo_id} = args,
%{ %{
context: %{current_user: %User{} = user} context: %{current_actor: %Actor{id: actor_id}}
} = _resolution } = _resolution
) do ) do
with {:actor, %Actor{id: actor_id} = _actor} <- {:actor, Users.get_actor_for_user(user)}, with {:todo, %Todo{todo_list_id: todo_list_id} = todo} <-
{:todo, %Todo{todo_list_id: todo_list_id} = todo} <-
{:todo, Todos.get_todo(todo_id)}, {:todo, Todos.get_todo(todo_id)},
{:todo_list, %TodoList{actor_id: group_id}} <- {:todo_list, %TodoList{actor_id: group_id}} <-
{:todo_list, Todos.get_todo_list(todo_list_id)}, {:todo_list, Todos.get_todo_list(todo_list_id)},
{:member, true} <- {:member, Actors.is_member?(actor_id, group_id)}, {:member, true} <- {:member, Actors.is_member?(actor_id, group_id)},
{:ok, _, %Todo{} = todo} <- {:ok, _, %Todo{} = todo} <-
ActivityPub.update(todo, args, true, %{}) do Actions.Update.update(todo, args, true, %{}) do
{:ok, todo} {:ok, todo}
else else
{:actor, nil} ->
{:error, dgettext("errors", "No profile found for user")}
{:todo_list, _} -> {:todo_list, _} ->
{:error, dgettext("errors", "Todo list doesn't exist")} {: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)}, # {:todo_list, Todos.get_todo_list(todo_list_id)},
# {:member, true} <- {:member, Actors.is_member?(actor_id, group_id)}, # {:member, true} <- {:member, Actors.is_member?(actor_id, group_id)},
# {:ok, _, %Todo{} = todo} <- # {:ok, _, %Todo{} = todo} <-
# ActivityPub.delete_todo(todo, actor, true, %{}) do # Actions.Delete.delete_todo(todo, actor, true, %{}) do
# {:ok, todo} # {:ok, todo}
# else # else
# {:todo_list, _} -> # {:todo_list, _} ->

View file

@ -7,8 +7,7 @@ defmodule Mobilizon.GraphQL.Resolvers.User do
alias Mobilizon.{Actors, Admin, Config, Events, Users} alias Mobilizon.{Actors, Admin, Config, Events, Users}
alias Mobilizon.Actors.Actor alias Mobilizon.Actors.Actor
alias Mobilizon.Federation.ActivityPub alias Mobilizon.Federation.ActivityPub.{Actions, Relay}
alias Mobilizon.Federation.ActivityPub.Relay
alias Mobilizon.Service.Auth.Authenticator alias Mobilizon.Service.Auth.Authenticator
alias Mobilizon.Storage.{Page, Repo} alias Mobilizon.Storage.{Page, Repo}
alias Mobilizon.Users.{Setting, User} alias Mobilizon.Users.{Setting, User}
@ -21,6 +20,7 @@ defmodule Mobilizon.GraphQL.Resolvers.User do
@doc """ @doc """
Find an user by its ID 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}}}) def find_user(_parent, %{id: id}, %{context: %{current_user: %User{role: role}}})
when is_moderator(role) do when is_moderator(role) do
with {:ok, %User{} = user} <- Users.get_user_with_actors(id) do with {:ok, %User{} = user} <- Users.get_user_with_actors(id) do
@ -31,6 +31,8 @@ defmodule Mobilizon.GraphQL.Resolvers.User do
@doc """ @doc """
Return current logged-in user 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 def get_current_user(_parent, _args, %{context: %{current_user: %User{} = user}}) do
{:ok, user} {:ok, user}
end end
@ -42,6 +44,8 @@ defmodule Mobilizon.GraphQL.Resolvers.User do
@doc """ @doc """
List instance users List instance users
""" """
@spec list_users(any(), map(), Absinthe.Resolution.t()) ::
{:ok, Page.t(User.t())} | {:error, :unauthorized}
def list_users( def list_users(
_parent, _parent,
%{email: email, page: page, limit: limit, sort: sort, direction: direction}, %{email: email, page: page, limit: limit, sort: sort, direction: direction},
@ -58,6 +62,8 @@ defmodule Mobilizon.GraphQL.Resolvers.User do
@doc """ @doc """
Login an user. Returns a token and the user 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 def login_user(_parent, %{email: email, password: password}, %{context: context}) do
with {:ok, with {:ok,
%{ %{
@ -65,9 +71,8 @@ defmodule Mobilizon.GraphQL.Resolvers.User do
refresh_token: _refresh_token, refresh_token: _refresh_token,
user: %User{} = user user: %User{} = user
} = user_and_tokens} <- Authenticator.authenticate(email, password), } = user_and_tokens} <- Authenticator.authenticate(email, password),
{:ok, %User{} = user} <- update_user_login_information(user, context), {:ok, %User{} = user} <- update_user_login_information(user, context) do
user_and_tokens <- Map.put(user_and_tokens, :user, user) do {:ok, %{user_and_tokens | user: user}}
{:ok, user_and_tokens}
else else
{:error, :user_not_found} -> {:error, :user_not_found} ->
{:error, :user_not_found} {:error, :user_not_found}
@ -87,6 +92,8 @@ defmodule Mobilizon.GraphQL.Resolvers.User do
@doc """ @doc """
Refresh a token 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 def refresh_token(_parent, %{refresh_token: refresh_token}, _resolution) do
with {:ok, user, _claims} <- Auth.Guardian.resource_from_token(refresh_token), with {:ok, user, _claims} <- Auth.Guardian.resource_from_token(refresh_token),
{:ok, _old, {exchanged_token, _claims}} <- {: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")} {:error, dgettext("errors", "You need to have an existing token to get a refresh token")}
end 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 def logout(_parent, %{refresh_token: refresh_token}, %{context: %{current_user: %User{}}}) do
with {:ok, _claims} <- Auth.Guardian.decode_and_verify(refresh_token, %{"typ" => "refresh"}), with {:ok, _claims} <- Auth.Guardian.decode_and_verify(refresh_token, %{"typ" => "refresh"}),
{:ok, _claims} <- Auth.Guardian.revoke(refresh_token) do {:ok, _claims} <- Auth.Guardian.revoke(refresh_token) do
@ -133,7 +143,7 @@ defmodule Mobilizon.GraphQL.Resolvers.User do
- create the user - create the user
- send a validation email to 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 def create_user(_parent, %{email: email} = args, _resolution) do
with :registration_ok <- check_registration_config(email), with :registration_ok <- check_registration_config(email),
:not_deny_listed <- check_registration_denylist(email), :not_deny_listed <- check_registration_denylist(email),
@ -160,20 +170,22 @@ defmodule Mobilizon.GraphQL.Resolvers.User do
end end
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 defp check_registration_config(email) do
cond do cond do
Config.instance_registrations_open?() -> Config.instance_registrations_open?() ->
:registration_ok :registration_ok
Config.instance_registrations_allowlist?() -> Config.instance_registrations_allowlist?() ->
check_allow_listed_email?(email) check_allow_listed_email(email)
true -> true ->
:registration_closed :registration_closed
end end
end end
@spec check_registration_denylist(String.t()) :: :deny_listed | :not_deny_listed
defp check_registration_denylist(email) do defp check_registration_denylist(email) do
# Remove everything behind the + # Remove everything behind the +
email = String.replace(email, ~r/(\+.*)(?=\@)/, "") email = String.replace(email, ~r/(\+.*)(?=\@)/, "")
@ -183,8 +195,8 @@ defmodule Mobilizon.GraphQL.Resolvers.User do
else: :not_deny_listed else: :not_deny_listed
end end
@spec check_allow_listed_email?(String.t()) :: :registration_ok | :not_allowlisted @spec check_allow_listed_email(String.t()) :: :registration_ok | :not_allowlisted
defp check_allow_listed_email?(email) do defp check_allow_listed_email(email) do
if email_in_list(email, Config.instance_registrations_allowlist()), if email_in_list(email, Config.instance_registrations_allowlist()),
do: :registration_ok, do: :registration_ok,
else: :not_allowlisted else: :not_allowlisted
@ -199,23 +211,29 @@ defmodule Mobilizon.GraphQL.Resolvers.User do
@doc """ @doc """
Validate an user, get its actor and a token 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 def validate_user(_parent, %{token: token}, _resolution) do
with {:check_confirmation_token, {:ok, %User{} = user}} <- case Email.User.check_confirmation_token(token) do
{:check_confirmation_token, Email.User.check_confirmation_token(token)}, {:ok, %User{} = user} ->
{:get_actor, actor} <- {:get_actor, Users.get_actor_for_user(user)}, actor = Users.get_actor_for_user(user)
{:ok, %{access_token: access_token, refresh_token: refresh_token}} <-
Authenticator.generate_tokens(user) do {:ok, %{access_token: access_token, refresh_token: refresh_token}} =
Authenticator.generate_tokens(user)
{:ok, {:ok,
%{ %{
access_token: access_token, access_token: access_token,
refresh_token: refresh_token, refresh_token: refresh_token,
user: Map.put(user, :default_actor, actor) 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")} {:error, dgettext("errors", "Unable to validate user")}
end end
end end
@ -267,12 +285,25 @@ defmodule Mobilizon.GraphQL.Resolvers.User do
@doc """ @doc """
Reset the password from an user 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 def reset_password(_parent, %{password: password, token: token}, _resolution) do
with {:ok, %User{email: email} = user} <- case Email.User.check_reset_password_token(password, token) do
Email.User.check_reset_password_token(password, token), {:ok, %User{email: email} = user} ->
{:ok, %{access_token: access_token, refresh_token: refresh_token}} <- {:ok, tokens} = Authenticator.authenticate(email, password)
Authenticator.authenticate(email, password) do {:ok, Map.put(tokens, :user, user)}
{:ok, %{access_token: access_token, refresh_token: refresh_token, 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
end end
@ -280,12 +311,12 @@ defmodule Mobilizon.GraphQL.Resolvers.User do
def change_default_actor( def change_default_actor(
_parent, _parent,
%{preferred_username: username}, %{preferred_username: username},
%{context: %{current_user: user}} %{context: %{current_user: %User{id: user_id} = user}}
) do ) 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, true} <-
{:user_actor, actor_id in Enum.map(Users.get_actors_for_user(user), & &1.id)}, {: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} {:ok, user}
else else
{:user_actor, _} -> {:user_actor, _} ->
@ -369,6 +400,9 @@ defmodule Mobilizon.GraphQL.Resolvers.User do
|> Repo.update() do |> Repo.update() do
{:ok, user} {:ok, user}
else else
{:can_change_password, false} ->
{:error, dgettext("errors", "You cannot change your password.")}
{:current_password, _} -> {:current_password, _} ->
{:error, dgettext("errors", "The current password is invalid")} {:error, dgettext("errors", "The current password is invalid")}
@ -408,14 +442,18 @@ defmodule Mobilizon.GraphQL.Resolvers.User do
{:ok, user} {:ok, user}
else else
{:current_password, _} -> {:current_password, {:error, _}} ->
{:error, dgettext("errors", "The password provided is invalid")} {:error, dgettext("errors", "The password provided is invalid")}
{:same_email, true} -> {:same_email, true} ->
{:error, dgettext("errors", "The new email must be different")} {: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, 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
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")} {:error, dgettext("errors", "You need to be logged-in to change your email")}
end end
@spec validate_email(map(), %{token: String.t()}, map()) ::
{:ok, User.t()} | {:error, String.t()}
def validate_email(_parent, %{token: token}, _resolution) do def validate_email(_parent, %{token: token}, _resolution) do
with {:get, %User{} = user} <- {:get, Users.get_user_by_activation_token(token)}, case Users.get_user_by_activation_token(token) do
{:ok, %User{} = user} <- Users.validate_email(user) do %User{} = user ->
case Users.validate_email(user) do
{:ok, %User{} = user} ->
{:ok, 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")} {:error, dgettext("errors", "Invalid activation token")}
end end
end end
def delete_account(_parent, %{user_id: user_id}, %{ 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 when is_moderator(role) do
with {:moderator_actor, %Actor{} = moderator_actor} <- with %User{disabled: false} = user <- Users.get_user(user_id),
{:moderator_actor, Users.get_actor_for_user(moderator_user)},
%User{disabled: false} = user <- Users.get_user(user_id),
{:ok, %User{}} <- {:ok, %User{}} <-
do_delete_account(%User{} = user, actor_performing: Relay.get_actor()) do do_delete_account(%User{} = user, actor_performing: Relay.get_actor()) do
Admin.log_action(moderator_actor, "delete", user) Admin.log_action(moderator_actor, "delete", user)
else else
{:moderator_actor, nil} ->
{:error, dgettext("errors", "No profile found for the moderator user")}
%User{disabled: true} -> %User{disabled: true} ->
{:error, dgettext("errors", "User already disabled")} {:error, dgettext("errors", "User already disabled")}
end end
@ -488,7 +533,7 @@ defmodule Mobilizon.GraphQL.Resolvers.User do
:ok <- :ok <-
Enum.each(actors, fn actor -> Enum.each(actors, fn actor ->
actor_performing = Keyword.get(options, :actor_performing, actor) actor_performing = Keyword.get(options, :actor_performing, actor)
ActivityPub.delete(actor, actor_performing, true) Actions.Delete.delete(actor, actor_performing, true)
end), end),
# Delete user # Delete user
{:ok, user} <- {:ok, user} <-
@ -547,11 +592,16 @@ defmodule Mobilizon.GraphQL.Resolvers.User do
def update_locale(_parent, %{locale: locale}, %{ def update_locale(_parent, %{locale: locale}, %{
context: %{current_user: %User{locale: current_locale} = user} context: %{current_user: %User{locale: current_locale} = user}
}) do }) do
with true <- current_locale != locale, if current_locale != locale do
{:ok, %User{} = updated_user} <- Users.update_user(user, %{locale: locale}) do case Users.update_user(user, %{locale: locale}) do
{:ok, %User{} = updated_user} ->
{:ok, updated_user} {:ok, updated_user}
{:error, %Ecto.Changeset{} = err} ->
Logger.debug(err)
{:error, dgettext("errors", "Error while updating locale")}
end
else else
false ->
{:ok, user} {:ok, user}
end end
end end

View file

@ -4,10 +4,12 @@ defmodule Mobilizon.GraphQL.Resolvers.Users.ActivitySettings do
""" """
alias Mobilizon.Users alias Mobilizon.Users
alias Mobilizon.Users.User alias Mobilizon.Users.{ActivitySetting, User}
require Logger 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 def user_activity_settings(_parent, _args, %{context: %{current_user: %User{} = user}}) do
{:ok, Users.activity_settings_for_user(user)} {:ok, Users.activity_settings_for_user(user)}
end end
@ -16,6 +18,8 @@ defmodule Mobilizon.GraphQL.Resolvers.Users.ActivitySettings do
{:error, :unauthenticated} {:error, :unauthenticated}
end 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 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)) Users.create_activity_setting(Map.put(args, :user_id, user_id))
end end

Some files were not shown because too many files have changed in this diff Show more