36012ef6c1
If we didn't put some kind of lifetime requirement on these, I guess you could annoy people by sending large numbers of ephemeral posts that provoke notifications but then disappear before anyone can read them.
467 lines
15 KiB
Elixir
467 lines
15 KiB
Elixir
# Pleroma: A lightweight social networking server
|
|
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
|
|
# SPDX-License-Identifier: AGPL-3.0-only
|
|
|
|
defmodule Pleroma.Web.CommonAPI do
|
|
alias Pleroma.Activity
|
|
alias Pleroma.ActivityExpiration
|
|
alias Pleroma.Formatter
|
|
alias Pleroma.Object
|
|
alias Pleroma.ThreadMute
|
|
alias Pleroma.User
|
|
alias Pleroma.Web.ActivityPub.ActivityPub
|
|
alias Pleroma.Web.ActivityPub.Utils
|
|
alias Pleroma.Web.ActivityPub.Visibility
|
|
|
|
import Pleroma.Web.Gettext
|
|
import Pleroma.Web.CommonAPI.Utils
|
|
|
|
def follow(follower, followed) do
|
|
with {:ok, follower} <- User.maybe_direct_follow(follower, followed),
|
|
{:ok, activity} <- ActivityPub.follow(follower, followed),
|
|
{:ok, follower, followed} <-
|
|
User.wait_and_refresh(
|
|
Pleroma.Config.get([:activitypub, :follow_handshake_timeout]),
|
|
follower,
|
|
followed
|
|
) do
|
|
{:ok, follower, followed, activity}
|
|
end
|
|
end
|
|
|
|
def unfollow(follower, unfollowed) do
|
|
with {:ok, follower, _follow_activity} <- User.unfollow(follower, unfollowed),
|
|
{:ok, _activity} <- ActivityPub.unfollow(follower, unfollowed),
|
|
{:ok, _unfollowed} <- User.unsubscribe(follower, unfollowed) do
|
|
{:ok, follower}
|
|
end
|
|
end
|
|
|
|
def accept_follow_request(follower, followed) do
|
|
with {:ok, follower} <- User.follow(follower, followed),
|
|
%Activity{} = follow_activity <- Utils.fetch_latest_follow(follower, followed),
|
|
{:ok, follow_activity} <- Utils.update_follow_state_for_all(follow_activity, "accept"),
|
|
{:ok, _activity} <-
|
|
ActivityPub.accept(%{
|
|
to: [follower.ap_id],
|
|
actor: followed,
|
|
object: follow_activity.data["id"],
|
|
type: "Accept"
|
|
}) do
|
|
{:ok, follower}
|
|
end
|
|
end
|
|
|
|
def reject_follow_request(follower, followed) do
|
|
with %Activity{} = follow_activity <- Utils.fetch_latest_follow(follower, followed),
|
|
{:ok, follow_activity} <- Utils.update_follow_state_for_all(follow_activity, "reject"),
|
|
{:ok, _activity} <-
|
|
ActivityPub.reject(%{
|
|
to: [follower.ap_id],
|
|
actor: followed,
|
|
object: follow_activity.data["id"],
|
|
type: "Reject"
|
|
}) do
|
|
{:ok, follower}
|
|
end
|
|
end
|
|
|
|
def delete(activity_id, user) do
|
|
with %Activity{data: %{"object" => _}} = activity <-
|
|
Activity.get_by_id_with_object(activity_id),
|
|
%Object{} = object <- Object.normalize(activity),
|
|
true <- User.superuser?(user) || user.ap_id == object.data["actor"],
|
|
{:ok, _} <- unpin(activity_id, user),
|
|
{:ok, delete} <- ActivityPub.delete(object) do
|
|
{:ok, delete}
|
|
else
|
|
_ ->
|
|
{:error, dgettext("errors", "Could not delete")}
|
|
end
|
|
end
|
|
|
|
def repeat(id_or_ap_id, user) do
|
|
with %Activity{} = activity <- get_by_id_or_ap_id(id_or_ap_id),
|
|
object <- Object.normalize(activity),
|
|
nil <- Utils.get_existing_announce(user.ap_id, object) do
|
|
ActivityPub.announce(user, object)
|
|
else
|
|
_ ->
|
|
{:error, dgettext("errors", "Could not repeat")}
|
|
end
|
|
end
|
|
|
|
def unrepeat(id_or_ap_id, user) do
|
|
with %Activity{} = activity <- get_by_id_or_ap_id(id_or_ap_id),
|
|
object <- Object.normalize(activity) do
|
|
ActivityPub.unannounce(user, object)
|
|
else
|
|
_ ->
|
|
{:error, dgettext("errors", "Could not unrepeat")}
|
|
end
|
|
end
|
|
|
|
def favorite(id_or_ap_id, user) do
|
|
with %Activity{} = activity <- get_by_id_or_ap_id(id_or_ap_id),
|
|
object <- Object.normalize(activity),
|
|
nil <- Utils.get_existing_like(user.ap_id, object) do
|
|
ActivityPub.like(user, object)
|
|
else
|
|
_ ->
|
|
{:error, dgettext("errors", "Could not favorite")}
|
|
end
|
|
end
|
|
|
|
def unfavorite(id_or_ap_id, user) do
|
|
with %Activity{} = activity <- get_by_id_or_ap_id(id_or_ap_id),
|
|
object <- Object.normalize(activity) do
|
|
ActivityPub.unlike(user, object)
|
|
else
|
|
_ ->
|
|
{:error, dgettext("errors", "Could not unfavorite")}
|
|
end
|
|
end
|
|
|
|
def vote(user, object, choices) do
|
|
with "Question" <- object.data["type"],
|
|
{:author, false} <- {:author, object.data["actor"] == user.ap_id},
|
|
{:existing_votes, []} <- {:existing_votes, Utils.get_existing_votes(user.ap_id, object)},
|
|
{options, max_count} <- get_options_and_max_count(object),
|
|
option_count <- Enum.count(options),
|
|
{:choice_check, {choices, true}} <-
|
|
{:choice_check, normalize_and_validate_choice_indices(choices, option_count)},
|
|
{:count_check, true} <- {:count_check, Enum.count(choices) <= max_count} do
|
|
answer_activities =
|
|
Enum.map(choices, fn index ->
|
|
answer_data = make_answer_data(user, object, Enum.at(options, index)["name"])
|
|
|
|
{:ok, activity} =
|
|
ActivityPub.create(%{
|
|
to: answer_data["to"],
|
|
actor: user,
|
|
context: object.data["context"],
|
|
object: answer_data,
|
|
additional: %{"cc" => answer_data["cc"]}
|
|
})
|
|
|
|
activity
|
|
end)
|
|
|
|
object = Object.get_cached_by_ap_id(object.data["id"])
|
|
{:ok, answer_activities, object}
|
|
else
|
|
{:author, _} -> {:error, dgettext("errors", "Poll's author can't vote")}
|
|
{:existing_votes, _} -> {:error, dgettext("errors", "Already voted")}
|
|
{:choice_check, {_, false}} -> {:error, dgettext("errors", "Invalid indices")}
|
|
{:count_check, false} -> {:error, dgettext("errors", "Too many choices")}
|
|
end
|
|
end
|
|
|
|
defp get_options_and_max_count(object) do
|
|
if Map.has_key?(object.data, "anyOf") do
|
|
{object.data["anyOf"], Enum.count(object.data["anyOf"])}
|
|
else
|
|
{object.data["oneOf"], 1}
|
|
end
|
|
end
|
|
|
|
defp normalize_and_validate_choice_indices(choices, count) do
|
|
Enum.map_reduce(choices, true, fn index, valid ->
|
|
index = if is_binary(index), do: String.to_integer(index), else: index
|
|
{index, if(valid, do: index < count, else: valid)}
|
|
end)
|
|
end
|
|
|
|
def get_visibility(%{"visibility" => visibility}, in_reply_to)
|
|
when visibility in ~w{public unlisted private direct},
|
|
do: {visibility, get_replied_to_visibility(in_reply_to)}
|
|
|
|
def get_visibility(%{"visibility" => "list:" <> list_id}, in_reply_to) do
|
|
visibility = {:list, String.to_integer(list_id)}
|
|
{visibility, get_replied_to_visibility(in_reply_to)}
|
|
end
|
|
|
|
def get_visibility(_, in_reply_to) when not is_nil(in_reply_to) do
|
|
visibility = get_replied_to_visibility(in_reply_to)
|
|
{visibility, visibility}
|
|
end
|
|
|
|
def get_visibility(_, in_reply_to), do: {"public", get_replied_to_visibility(in_reply_to)}
|
|
|
|
def get_replied_to_visibility(nil), do: nil
|
|
|
|
def get_replied_to_visibility(activity) do
|
|
with %Object{} = object <- Object.normalize(activity) do
|
|
Pleroma.Web.ActivityPub.Visibility.get_visibility(object)
|
|
end
|
|
end
|
|
|
|
defp check_expiry_date(expiry_str) do
|
|
{:ok, expiry} = Ecto.Type.cast(:naive_datetime, expiry_str)
|
|
|
|
if is_nil(expiry) || ActivityExpiration.expires_late_enough?(expiry) do
|
|
{:ok, expiry}
|
|
else
|
|
{:error, "Expiry date is too soon"}
|
|
end
|
|
end
|
|
|
|
def post(user, %{"status" => status} = data) do
|
|
limit = Pleroma.Config.get([:instance, :limit])
|
|
|
|
with status <- String.trim(status),
|
|
attachments <- attachments_from_ids(data),
|
|
in_reply_to <- get_replied_to_activity(data["in_reply_to_status_id"]),
|
|
{visibility, in_reply_to_visibility} <- get_visibility(data, in_reply_to),
|
|
{_, false} <-
|
|
{:private_to_public, in_reply_to_visibility == "direct" && visibility != "direct"},
|
|
{content_html, mentions, tags} <-
|
|
make_content_html(
|
|
status,
|
|
attachments,
|
|
data,
|
|
visibility
|
|
),
|
|
mentioned_users <- for({_, mentioned_user} <- mentions, do: mentioned_user.ap_id),
|
|
addressed_users <- get_addressed_users(mentioned_users, data["to"]),
|
|
{poll, poll_emoji} <- make_poll_data(data),
|
|
{to, cc} <- get_to_and_cc(user, addressed_users, in_reply_to, visibility),
|
|
context <- make_context(in_reply_to),
|
|
cw <- data["spoiler_text"] || "",
|
|
sensitive <- data["sensitive"] || Enum.member?(tags, {"#nsfw", "nsfw"}),
|
|
{:ok, expires_at} <- check_expiry_date(data["expires_at"]),
|
|
full_payload <- String.trim(status <> cw),
|
|
:ok <- validate_character_limit(full_payload, attachments, limit),
|
|
object <-
|
|
make_note_data(
|
|
user.ap_id,
|
|
to,
|
|
context,
|
|
content_html,
|
|
attachments,
|
|
in_reply_to,
|
|
tags,
|
|
cw,
|
|
cc,
|
|
sensitive,
|
|
poll
|
|
),
|
|
object <-
|
|
Map.put(
|
|
object,
|
|
"emoji",
|
|
Map.merge(Formatter.get_emoji_map(full_payload), poll_emoji)
|
|
) do
|
|
preview? = Pleroma.Web.ControllerHelper.truthy_param?(data["preview"]) || false
|
|
direct? = visibility == "direct"
|
|
|
|
result =
|
|
%{
|
|
to: to,
|
|
actor: user,
|
|
context: context,
|
|
object: object,
|
|
additional: %{"cc" => cc, "directMessage" => direct?}
|
|
}
|
|
|> maybe_add_list_data(user, visibility)
|
|
|> ActivityPub.create(preview?)
|
|
|
|
if expires_at do
|
|
with {:ok, activity} <- result do
|
|
{:ok, _} = ActivityExpiration.create(activity, expires_at)
|
|
end
|
|
end
|
|
|
|
result
|
|
else
|
|
{:private_to_public, true} ->
|
|
{:error, dgettext("errors", "The message visibility must be direct")}
|
|
|
|
{:error, _} = e ->
|
|
e
|
|
|
|
e ->
|
|
{:error, e}
|
|
end
|
|
end
|
|
|
|
# Updates the emojis for a user based on their profile
|
|
def update(user) do
|
|
user =
|
|
with emoji <- emoji_from_profile(user),
|
|
source_data <- (user.info.source_data || %{}) |> Map.put("tag", emoji),
|
|
info_cng <- User.Info.set_source_data(user.info, source_data),
|
|
change <- Ecto.Changeset.change(user) |> Ecto.Changeset.put_embed(:info, info_cng),
|
|
{:ok, user} <- User.update_and_set_cache(change) do
|
|
user
|
|
else
|
|
_e ->
|
|
user
|
|
end
|
|
|
|
ActivityPub.update(%{
|
|
local: true,
|
|
to: [user.follower_address],
|
|
cc: [],
|
|
actor: user.ap_id,
|
|
object: Pleroma.Web.ActivityPub.UserView.render("user.json", %{user: user})
|
|
})
|
|
end
|
|
|
|
def pin(id_or_ap_id, %{ap_id: user_ap_id} = user) do
|
|
with %Activity{
|
|
actor: ^user_ap_id,
|
|
data: %{
|
|
"type" => "Create"
|
|
},
|
|
object: %Object{
|
|
data: %{
|
|
"type" => "Note"
|
|
}
|
|
}
|
|
} = activity <- get_by_id_or_ap_id(id_or_ap_id),
|
|
true <- Visibility.is_public?(activity),
|
|
%{valid?: true} = info_changeset <-
|
|
User.Info.add_pinnned_activity(user.info, activity),
|
|
changeset <-
|
|
Ecto.Changeset.change(user) |> Ecto.Changeset.put_embed(:info, info_changeset),
|
|
{:ok, _user} <- User.update_and_set_cache(changeset) do
|
|
{:ok, activity}
|
|
else
|
|
%{errors: [pinned_activities: {err, _}]} ->
|
|
{:error, err}
|
|
|
|
_ ->
|
|
{:error, dgettext("errors", "Could not pin")}
|
|
end
|
|
end
|
|
|
|
def unpin(id_or_ap_id, user) do
|
|
with %Activity{} = activity <- get_by_id_or_ap_id(id_or_ap_id),
|
|
%{valid?: true} = info_changeset <-
|
|
User.Info.remove_pinnned_activity(user.info, activity),
|
|
changeset <-
|
|
Ecto.Changeset.change(user) |> Ecto.Changeset.put_embed(:info, info_changeset),
|
|
{:ok, _user} <- User.update_and_set_cache(changeset) do
|
|
{:ok, activity}
|
|
else
|
|
%{errors: [pinned_activities: {err, _}]} ->
|
|
{:error, err}
|
|
|
|
_ ->
|
|
{:error, dgettext("errors", "Could not unpin")}
|
|
end
|
|
end
|
|
|
|
def add_mute(user, activity) do
|
|
with {:ok, _} <- ThreadMute.add_mute(user.id, activity.data["context"]) do
|
|
{:ok, activity}
|
|
else
|
|
{:error, _} -> {:error, dgettext("errors", "conversation is already muted")}
|
|
end
|
|
end
|
|
|
|
def remove_mute(user, activity) do
|
|
ThreadMute.remove_mute(user.id, activity.data["context"])
|
|
{:ok, activity}
|
|
end
|
|
|
|
def thread_muted?(%{id: nil} = _user, _activity), do: false
|
|
|
|
def thread_muted?(user, activity) do
|
|
with [] <- ThreadMute.check_muted(user.id, activity.data["context"]) do
|
|
false
|
|
else
|
|
_ -> true
|
|
end
|
|
end
|
|
|
|
def report(user, data) do
|
|
with {:account_id, %{"account_id" => account_id}} <- {:account_id, data},
|
|
{:account, %User{} = account} <- {:account, User.get_cached_by_id(account_id)},
|
|
{:ok, {content_html, _, _}} <- make_report_content_html(data["comment"]),
|
|
{:ok, statuses} <- get_report_statuses(account, data),
|
|
{:ok, activity} <-
|
|
ActivityPub.flag(%{
|
|
context: Utils.generate_context_id(),
|
|
actor: user,
|
|
account: account,
|
|
statuses: statuses,
|
|
content: content_html,
|
|
forward: data["forward"] || false
|
|
}) do
|
|
{:ok, activity}
|
|
else
|
|
{:error, err} -> {:error, err}
|
|
{:account_id, %{}} -> {:error, dgettext("errors", "Valid `account_id` required")}
|
|
{:account, nil} -> {:error, dgettext("errors", "Account not found")}
|
|
end
|
|
end
|
|
|
|
def update_report_state(activity_id, state) do
|
|
with %Activity{} = activity <- Activity.get_by_id(activity_id),
|
|
{:ok, activity} <- Utils.update_report_state(activity, state) do
|
|
{:ok, activity}
|
|
else
|
|
nil -> {:error, :not_found}
|
|
{:error, reason} -> {:error, reason}
|
|
_ -> {:error, dgettext("errors", "Could not update state")}
|
|
end
|
|
end
|
|
|
|
def update_activity_scope(activity_id, opts \\ %{}) do
|
|
with %Activity{} = activity <- Activity.get_by_id_with_object(activity_id),
|
|
{:ok, activity} <- toggle_sensitive(activity, opts),
|
|
{:ok, activity} <- set_visibility(activity, opts) do
|
|
{:ok, activity}
|
|
else
|
|
nil -> {:error, :not_found}
|
|
{:error, reason} -> {:error, reason}
|
|
end
|
|
end
|
|
|
|
defp toggle_sensitive(activity, %{"sensitive" => sensitive}) when sensitive in ~w(true false) do
|
|
toggle_sensitive(activity, %{"sensitive" => String.to_existing_atom(sensitive)})
|
|
end
|
|
|
|
defp toggle_sensitive(%Activity{object: object} = activity, %{"sensitive" => sensitive})
|
|
when is_boolean(sensitive) do
|
|
new_data = Map.put(object.data, "sensitive", sensitive)
|
|
|
|
{:ok, object} =
|
|
object
|
|
|> Object.change(%{data: new_data})
|
|
|> Object.update_and_set_cache()
|
|
|
|
{:ok, Map.put(activity, :object, object)}
|
|
end
|
|
|
|
defp toggle_sensitive(activity, _), do: {:ok, activity}
|
|
|
|
defp set_visibility(activity, %{"visibility" => visibility}) do
|
|
Utils.update_activity_visibility(activity, visibility)
|
|
end
|
|
|
|
defp set_visibility(activity, _), do: {:ok, activity}
|
|
|
|
def hide_reblogs(user, muted) do
|
|
ap_id = muted.ap_id
|
|
|
|
if ap_id not in user.info.muted_reblogs do
|
|
info_changeset = User.Info.add_reblog_mute(user.info, ap_id)
|
|
changeset = Ecto.Changeset.change(user) |> Ecto.Changeset.put_embed(:info, info_changeset)
|
|
User.update_and_set_cache(changeset)
|
|
end
|
|
end
|
|
|
|
def show_reblogs(user, muted) do
|
|
ap_id = muted.ap_id
|
|
|
|
if ap_id in user.info.muted_reblogs do
|
|
info_changeset = User.Info.remove_reblog_mute(user.info, ap_id)
|
|
changeset = Ecto.Changeset.change(user) |> Ecto.Changeset.put_embed(:info, info_changeset)
|
|
User.update_and_set_cache(changeset)
|
|
end
|
|
end
|
|
end
|