704960b3c1
The "expires_at" parameter accepts an ISO8601-formatted date which defines when the activity will expire. At this point the API will not give you any feedback about if your post will expire or not.
456 lines
15 KiB
Elixir
456 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
|
|
|
|
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} <- Ecto.Type.cast(:naive_datetime, 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
|
|
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
|