forked from potsda.mn/mobilizon
da532c7059
Closes #1344 Signed-off-by: Thomas Citharel <tcit@tcit.fr>
348 lines
12 KiB
Elixir
348 lines
12 KiB
Elixir
defmodule Mobilizon.Federation.ActivityPub.Types.Events do
|
|
@moduledoc false
|
|
alias Mobilizon.Actors
|
|
alias Mobilizon.Actors.Actor
|
|
alias Mobilizon.Events, as: EventsManager
|
|
alias Mobilizon.Events.{Event, Participant}
|
|
alias Mobilizon.Federation.ActivityPub.{Actions, Audience, Permission}
|
|
alias Mobilizon.Federation.ActivityPub.Types.Entity
|
|
alias Mobilizon.Federation.ActivityStream
|
|
alias Mobilizon.Federation.ActivityStream.Converter.Utils, as: ConverterUtils
|
|
alias Mobilizon.Federation.ActivityStream.Convertible
|
|
alias Mobilizon.GraphQL.API.Utils, as: APIUtils
|
|
alias Mobilizon.Service.Activity.Event, as: EventActivity
|
|
alias Mobilizon.Service.Formatter.HTML
|
|
alias Mobilizon.Service.LanguageDetection
|
|
alias Mobilizon.Service.Notifications.Scheduler
|
|
alias Mobilizon.Service.Workers.EventDelayedNotificationWorker
|
|
alias Mobilizon.Share
|
|
alias Mobilizon.Tombstone
|
|
import Mobilizon.Events.Utils, only: [calculate_notification_time: 1]
|
|
import Mobilizon.Federation.ActivityPub.Utils, only: [make_create_data: 2, make_update_data: 2]
|
|
require Logger
|
|
|
|
@behaviour Entity
|
|
|
|
@impl Entity
|
|
@spec create(map(), map()) ::
|
|
{:ok, Event.t(), ActivityStream.t()} | {:error, Ecto.Changeset.t()}
|
|
def create(args, additional) do
|
|
args = prepare_args_for_event(args)
|
|
|
|
case EventsManager.create_event(args) do
|
|
{:ok, %Event{uuid: event_uuid, begins_on: begins_on} = event} ->
|
|
EventActivity.insert_activity(event, subject: "event_created")
|
|
clear_caches(event)
|
|
|
|
%{action: :notify_of_new_event, event_uuid: event_uuid}
|
|
|> EventDelayedNotificationWorker.new(
|
|
scheduled_at: calculate_notification_time(begins_on)
|
|
)
|
|
|> Oban.insert()
|
|
|
|
event_as_data = Convertible.model_to_as(event)
|
|
audience = Audience.get_audience(event)
|
|
create_data = make_create_data(event_as_data, Map.merge(audience, additional))
|
|
{:ok, event, create_data}
|
|
|
|
{:error, _step, %Ecto.Changeset{} = err, _} ->
|
|
{:error, err}
|
|
|
|
{:error, %Ecto.Changeset{} = err} ->
|
|
{:error, err}
|
|
end
|
|
end
|
|
|
|
@impl Entity
|
|
@spec update(Event.t(), map(), map()) ::
|
|
{:ok, Event.t(), ActivityStream.t()} | {:error, Ecto.Changeset.t()}
|
|
def update(%Event{} = old_event, args, additional) do
|
|
args = prepare_args_for_event(args)
|
|
|
|
case EventsManager.update_event(old_event, args) do
|
|
{:ok, %Event{} = new_event} ->
|
|
EventActivity.insert_activity(new_event, subject: "event_updated")
|
|
clear_caches(new_event)
|
|
event_as_data = Convertible.model_to_as(new_event)
|
|
audience = Audience.get_audience(new_event)
|
|
update_data = make_update_data(event_as_data, Map.merge(audience, additional))
|
|
{:ok, new_event, update_data}
|
|
|
|
{:error, _step, %Ecto.Changeset{} = err, _} ->
|
|
{:error, err}
|
|
|
|
{:error, err} ->
|
|
{:error, err}
|
|
end
|
|
end
|
|
|
|
@impl Entity
|
|
@spec delete(Event.t(), Actor.t(), boolean, map()) ::
|
|
{:ok, ActivityStream.t(), Actor.t(), Event.t()} | {:error, Ecto.Changeset.t()}
|
|
def delete(%Event{url: url} = event, %Actor{} = actor, _local, _additionnal) do
|
|
activity_data = %{
|
|
"type" => "Delete",
|
|
"actor" => actor.url,
|
|
"object" => %{
|
|
"type" => "Tombstone",
|
|
"formerType" => "Event",
|
|
"deleted" => DateTime.utc_now(),
|
|
"id" => url
|
|
},
|
|
"to" => [actor.url <> "/followers", "https://www.w3.org/ns/activitystreams#Public"],
|
|
"id" => url <> "/delete"
|
|
}
|
|
|
|
audience = Audience.get_audience(event)
|
|
|
|
case EventsManager.delete_event(event) do
|
|
{:ok, %Event{} = event} ->
|
|
case Tombstone.create_tombstone(%{uri: event.url, actor_id: actor.id}) do
|
|
{:ok, %Tombstone{} = _tombstone} ->
|
|
EventActivity.insert_activity(event, subject: "event_deleted")
|
|
clear_caches(event)
|
|
Share.delete_all_by_uri(event.url)
|
|
{:ok, Map.merge(activity_data, audience), actor, event}
|
|
|
|
{:error, %Ecto.Changeset{} = err} ->
|
|
{:error, err}
|
|
end
|
|
|
|
{:error, %Ecto.Changeset{} = err} ->
|
|
{:error, err}
|
|
end
|
|
end
|
|
|
|
@spec actor(Event.t()) :: Actor.t() | nil
|
|
def actor(%Event{organizer_actor: %Actor{} = actor}), do: actor
|
|
|
|
def actor(%Event{organizer_actor_id: organizer_actor_id}),
|
|
do: Actors.get_actor(organizer_actor_id)
|
|
|
|
def actor(_), do: nil
|
|
|
|
@spec group_actor(Event.t()) :: Actor.t() | nil
|
|
def group_actor(%Event{attributed_to: %Actor{} = group}), do: group
|
|
|
|
def group_actor(%Event{attributed_to_id: attributed_to_id}) when not is_nil(attributed_to_id),
|
|
do: Actors.get_actor(attributed_to_id)
|
|
|
|
def group_actor(_), do: nil
|
|
|
|
@spec permissions(Event.t()) :: Permission.t()
|
|
def permissions(%Event{draft: draft, attributed_to_id: _attributed_to_id}) do
|
|
%Permission{
|
|
access: if(draft, do: :moderator, else: :member),
|
|
create: :moderator,
|
|
update: :moderator,
|
|
delete: :moderator
|
|
}
|
|
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
|
|
if check_attendee_capacity?(event) do
|
|
role =
|
|
additional
|
|
|> Map.get(:metadata, %{})
|
|
|> Map.get(:role, Mobilizon.Events.get_default_participant_role(event))
|
|
|
|
case Mobilizon.Events.create_participant(%{
|
|
role: role,
|
|
event_id: event.id,
|
|
actor_id: actor.id,
|
|
url: Map.get(additional, :url),
|
|
metadata:
|
|
additional
|
|
|> Map.get(:metadata, %{})
|
|
|> Map.update(:message, nil, &String.trim(HTML.strip_tags(&1)))
|
|
}) do
|
|
{:ok, %Participant{} = participant} ->
|
|
join_data = Convertible.model_to_as(participant)
|
|
audience = Audience.get_audience(participant)
|
|
|
|
approve_if_default_role_is_participant(
|
|
event,
|
|
Map.merge(join_data, audience),
|
|
participant,
|
|
role
|
|
)
|
|
|
|
{:error, _, %Ecto.Changeset{} = err, _} ->
|
|
{:error, err}
|
|
end
|
|
else
|
|
{:error, :maximum_attendee_capacity_reached}
|
|
end
|
|
end
|
|
|
|
@spec check_attendee_capacity?(Event.t()) :: boolean
|
|
defp check_attendee_capacity?(%Event{options: options} = event) do
|
|
maximum_attendee_capacity = Map.get(options, :maximum_attendee_capacity) || 0
|
|
|
|
maximum_attendee_capacity == 0 ||
|
|
Mobilizon.Events.count_participant_participants(event.id) < maximum_attendee_capacity
|
|
end
|
|
|
|
# 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(),
|
|
atom()
|
|
) :: {:ok, ActivityStreams.t(), Participant.t()} | {:accept, any()}
|
|
defp approve_if_default_role_is_participant(event, activity_data, participant, role) do
|
|
case event do
|
|
%Event{attributed_to: %Actor{id: group_id, url: group_url}} ->
|
|
case Actors.get_single_group_moderator_actor(group_id) do
|
|
%Actor{} = actor ->
|
|
do_approve(event, activity_data, participant, role, %{
|
|
"actor" => actor.url,
|
|
"attributedTo" => group_url
|
|
})
|
|
|
|
_ ->
|
|
{:ok, activity_data, participant}
|
|
end
|
|
|
|
%Event{attributed_to: nil, local: true} ->
|
|
do_approve(event, activity_data, participant, role, %{
|
|
"actor" => event.organizer_actor.url
|
|
})
|
|
|
|
%Event{} ->
|
|
{:ok, activity_data, participant}
|
|
end
|
|
end
|
|
|
|
@spec do_approve(Event.t(), ActivityStreams.t(), Particpant.t(), atom(), map()) ::
|
|
{:accept, any} | {:ok, ActivityStreams.t(), Participant.t()}
|
|
defp do_approve(event, activity_data, participant, role, additionnal) do
|
|
cond do
|
|
Mobilizon.Events.get_default_participant_role(event) == :participant &&
|
|
role == :participant ->
|
|
Mobilizon.Service.Activity.Participant.insert_activity(participant,
|
|
subject: "event_new_participation"
|
|
)
|
|
|
|
{:accept,
|
|
Actions.Accept.accept(
|
|
:join,
|
|
participant,
|
|
true,
|
|
additionnal
|
|
)}
|
|
|
|
Mobilizon.Events.get_default_participant_role(event) == :not_approved &&
|
|
role == :not_approved ->
|
|
Logger.debug("Scheduling a notification to notify of a new pending participation")
|
|
Scheduler.pending_participation_notification(event)
|
|
{:ok, activity_data, participant}
|
|
|
|
true ->
|
|
{:ok, activity_data, participant}
|
|
end
|
|
end
|
|
|
|
# Prepare and sanitize arguments for events
|
|
@spec prepare_args_for_event(map) :: map
|
|
defp prepare_args_for_event(args) do
|
|
# If title is not set: we are not updating it
|
|
args =
|
|
if Map.has_key?(args, :title) && !is_nil(args.title),
|
|
do: Map.update(args, :title, "", &String.trim/1),
|
|
else: args
|
|
|
|
# If we've been given a description (we might not get one if updating)
|
|
# sanitize it, HTML it, and extract tags & mentions from it
|
|
args =
|
|
if Map.has_key?(args, :description) && !is_nil(args.description) do
|
|
{description, mentions, tags} =
|
|
APIUtils.make_content_html(
|
|
String.trim(args.description),
|
|
Map.get(args, :tags, []),
|
|
"text/html"
|
|
)
|
|
|
|
mentions = ConverterUtils.fetch_mentions(Map.get(args, :mentions, []) ++ mentions)
|
|
|
|
Map.merge(args, %{
|
|
description: description,
|
|
mentions: mentions,
|
|
# Exclude tags with length > 40
|
|
tags: Enum.filter(tags, &exclude_too_long_tags/1)
|
|
})
|
|
else
|
|
args
|
|
end
|
|
|
|
# Make sure we don't have duplicate (with different casing) tags
|
|
args =
|
|
Map.update(
|
|
args,
|
|
:tags,
|
|
[],
|
|
&Enum.uniq_by(&1, fn tag -> tag |> tag_to_string() |> String.downcase() end)
|
|
)
|
|
|
|
# Check that we can only allow anonymous participation if our instance allows it
|
|
{_, options} =
|
|
Map.get_and_update(
|
|
Map.get(args, :options, %{anonymous_participation: false}),
|
|
:anonymous_participation,
|
|
fn value ->
|
|
{value, value && Mobilizon.Config.anonymous_participation?()}
|
|
end
|
|
)
|
|
|
|
args
|
|
|> Map.put(:options, options)
|
|
|> Map.put_new(:language, "und")
|
|
|> Map.update!(:language, fn lang ->
|
|
if lang == "und", do: LanguageDetection.detect(:event, args), else: lang
|
|
end)
|
|
|> Map.update(:tags, [], &ConverterUtils.fetch_tags/1)
|
|
|> Map.update(:contacts, [], &ConverterUtils.fetch_actors/1)
|
|
end
|
|
|
|
@spec exclude_too_long_tags(%{title: String.t()} | String.t()) :: boolean()
|
|
defp exclude_too_long_tags(tag) do
|
|
tag
|
|
|> tag_to_string()
|
|
|> String.length()
|
|
|> Kernel.<(40)
|
|
end
|
|
|
|
@spec tag_to_string(%{title: String.t()} | String.t()) :: String.t()
|
|
defp tag_to_string(%{title: tag}), do: tag
|
|
defp tag_to_string(tag), do: tag
|
|
|
|
defp clear_caches(event) do
|
|
clear_ap_cache(event)
|
|
clear_feeds_cache(event)
|
|
clear_category_statistics_cache()
|
|
end
|
|
|
|
defp clear_ap_cache(event), do: Cachex.del(:activity_pub, "event_#{event.uuid}")
|
|
|
|
defp clear_category_statistics_cache, do: Cachex.del(:statistics, :categories)
|
|
|
|
defp clear_feeds_cache(event) do
|
|
if event.attributed_to do
|
|
clear_actor_feeds_cache(event.attributed_to)
|
|
end
|
|
|
|
clear_event_ics_cache(event)
|
|
end
|
|
|
|
defp clear_actor_feeds_cache(actor) do
|
|
Cachex.del(:ics, "actor_#{actor.preferred_username}")
|
|
Cachex.del(:feed, "actor_#{actor.preferred_username}")
|
|
end
|
|
|
|
defp clear_event_ics_cache(event), do: Cachex.del(:ics, "event_#{event.uuid}")
|
|
end
|