This feature was both conceptually broken and through bitrotting the implementation was also buggy with the handling of certain list-post interactions just crashing. Remote servers had no way to know who belongs to a list and thus posts basically showed just up as weird DM threads with different participants on each instance. And while on the local instance addition and removal from a listed grated and revoked post access retroactively, it never acted retroactively on remotes. Notably our "activity_visibility" database function also didn’t know about "list visibility" instead treating them as direct messages. Furthermore no known client actualy allows creating such messages and the lack of complaints about the accumulutaed bugs supports the absence of any users. Given this there seems no point in fixing the implementation. To reduce complexity of visibility handling it will be dropped instead. Note, a similar effect with less federation weirdness can already be achieved client-side using the explicit-addressing feature originally introduced in https://git.pleroma.social/pleroma/pleroma/-/merge_requests/1239. Ref: https://akkoma.dev/AkkomaGang/akkoma/issues/812
576 lines
19 KiB
Elixir
576 lines
19 KiB
Elixir
# Pleroma: A lightweight social networking server
|
|
# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
|
|
# SPDX-License-Identifier: AGPL-3.0-only
|
|
|
|
defmodule Pleroma.Web.CommonAPI do
|
|
alias Pleroma.Activity
|
|
alias Pleroma.Conversation.Participation
|
|
alias Pleroma.Object
|
|
alias Pleroma.ThreadMute
|
|
alias Pleroma.User
|
|
alias Pleroma.UserRelationship
|
|
alias Pleroma.Web.ActivityPub.ActivityPub
|
|
alias Pleroma.Web.ActivityPub.Builder
|
|
alias Pleroma.Web.ActivityPub.Pipeline
|
|
alias Pleroma.Web.ActivityPub.Utils
|
|
alias Pleroma.Web.ActivityPub.Visibility
|
|
alias Pleroma.Web.CommonAPI.ActivityDraft
|
|
|
|
use Gettext,
|
|
backend: Pleroma.Web.Gettext
|
|
|
|
import Pleroma.Web.CommonAPI.Utils
|
|
|
|
require Pleroma.Constants
|
|
require Logger
|
|
|
|
def block(blocker, blocked) do
|
|
with {:ok, block_data, _} <- Builder.block(blocker, blocked),
|
|
{:ok, block, _} <- Pipeline.common_pipeline(block_data, local: true) do
|
|
{:ok, block}
|
|
end
|
|
end
|
|
|
|
def unblock(blocker, blocked) do
|
|
with {_, %Activity{} = block} <- {:fetch_block, Utils.fetch_latest_block(blocker, blocked)},
|
|
{:ok, unblock_data, _} <- Builder.undo(blocker, block),
|
|
{:ok, unblock, _} <- Pipeline.common_pipeline(unblock_data, local: true) do
|
|
{:ok, unblock}
|
|
else
|
|
{:fetch_block, nil} ->
|
|
if User.blocks?(blocker, blocked) do
|
|
User.unblock(blocker, blocked)
|
|
{:ok, :no_activity}
|
|
else
|
|
{:error, :not_blocking}
|
|
end
|
|
|
|
e ->
|
|
e
|
|
end
|
|
end
|
|
|
|
def follow(follower, followed) do
|
|
timeout = Pleroma.Config.get([:activitypub, :follow_handshake_timeout])
|
|
|
|
with {:ok, follow_data, _} <- Builder.follow(follower, followed),
|
|
{:ok, activity, _} <- Pipeline.common_pipeline(follow_data, local: true),
|
|
{:ok, follower, followed} <- User.wait_and_refresh(timeout, follower, followed) do
|
|
if activity.data["state"] == "reject" do
|
|
{:error, :rejected}
|
|
else
|
|
{:ok, follower, followed, activity}
|
|
end
|
|
end
|
|
end
|
|
|
|
def unfollow(follower, unfollowed) do
|
|
with {:ok, follower, _follow_activity} <- User.unfollow(follower, unfollowed),
|
|
{:ok, _activity} <- ActivityPub.unfollow(follower, unfollowed),
|
|
{:ok, _subscription} <- User.unsubscribe(follower, unfollowed) do
|
|
{:ok, follower}
|
|
end
|
|
end
|
|
|
|
def accept_follow_request(follower, followed) do
|
|
with %Activity{} = follow_activity <- Utils.fetch_latest_follow(follower, followed),
|
|
{:ok, accept_data, _} <- Builder.accept(followed, follow_activity),
|
|
{:ok, _activity, _} <- Pipeline.common_pipeline(accept_data, local: true) do
|
|
{:ok, follower}
|
|
end
|
|
end
|
|
|
|
def reject_follow_request(follower, followed) do
|
|
with %Activity{} = follow_activity <- Utils.fetch_latest_follow(follower, followed),
|
|
{:ok, reject_data, _} <- Builder.reject(followed, follow_activity),
|
|
{:ok, _activity, _} <- Pipeline.common_pipeline(reject_data, local: true) do
|
|
{:ok, follower}
|
|
end
|
|
end
|
|
|
|
def delete(activity_id, user) do
|
|
with {_, %Activity{data: %{"object" => _, "type" => "Create"}} = activity} <-
|
|
{:find_activity, Activity.get_by_id(activity_id, filter: [])},
|
|
{_, %Object{} = object, _} <-
|
|
{:find_object, Object.normalize(activity, fetch: false), activity},
|
|
true <- User.superuser?(user) || user.ap_id == object.data["actor"],
|
|
{:ok, delete_data, _} <- Builder.delete(user, object.data["id"]),
|
|
{:ok, delete, _} <- Pipeline.common_pipeline(delete_data, local: true) do
|
|
{:ok, delete}
|
|
else
|
|
{:find_activity, _} ->
|
|
{:error, :not_found}
|
|
|
|
{:find_object, nil, %Activity{data: %{"actor" => actor, "object" => object}}} ->
|
|
# We have the create activity, but not the object, it was probably pruned.
|
|
# Insert a tombstone and try again
|
|
with {:ok, tombstone_data, _} <- Builder.tombstone(actor, object),
|
|
{:ok, _tombstone} <- Object.create(tombstone_data) do
|
|
delete(activity_id, user)
|
|
else
|
|
_ ->
|
|
Logger.error(
|
|
"Could not insert tombstone for missing object on deletion. Object is #{object}."
|
|
)
|
|
|
|
{:error, dgettext("errors", "Could not delete")}
|
|
end
|
|
|
|
_ ->
|
|
{:error, dgettext("errors", "Could not delete")}
|
|
end
|
|
end
|
|
|
|
def repeat(id, user, params \\ %{}) do
|
|
with %Activity{data: %{"type" => "Create"}} = activity <- Activity.get_by_id(id),
|
|
object = %Object{} <- Object.normalize(activity, fetch: false),
|
|
{_, nil} <- {:existing_announce, Utils.get_existing_announce(user.ap_id, object)},
|
|
visibility = announce_visibility(object, params),
|
|
{:ok, announce, _} <- Builder.announce(user, object, visibility: visibility),
|
|
{:ok, activity, _} <- Pipeline.common_pipeline(announce, local: true) do
|
|
{:ok, activity}
|
|
else
|
|
{:existing_announce, %Activity{} = announce} ->
|
|
{:ok, announce}
|
|
|
|
_ ->
|
|
{:error, :not_found}
|
|
end
|
|
end
|
|
|
|
def unrepeat(id, user) do
|
|
with {_, %Activity{data: %{"type" => "Create"}} = activity} <-
|
|
{:find_activity, Activity.get_by_id(id)},
|
|
%Object{} = note <- Object.normalize(activity, fetch: false),
|
|
%Activity{} = announce <- Utils.get_existing_announce(user.ap_id, note),
|
|
{:ok, undo, _} <- Builder.undo(user, announce),
|
|
{:ok, activity, _} <- Pipeline.common_pipeline(undo, local: true) do
|
|
{:ok, activity}
|
|
else
|
|
{:find_activity, _} -> {:error, :not_found}
|
|
_ -> {:error, dgettext("errors", "Could not unrepeat")}
|
|
end
|
|
end
|
|
|
|
@spec favorite(User.t(), binary()) :: {:ok, Activity.t() | :already_liked} | {:error, any()}
|
|
def favorite(%User{} = user, id) do
|
|
case favorite_helper(user, id) do
|
|
{:ok, _} = res ->
|
|
res
|
|
|
|
{:error, :not_found} = res ->
|
|
res
|
|
|
|
{:error, e} ->
|
|
Logger.error("Could not favorite #{id}. Error: #{inspect(e, pretty: true)}")
|
|
{:error, dgettext("errors", "Could not favorite")}
|
|
end
|
|
end
|
|
|
|
def favorite_helper(user, id) do
|
|
with {_, %Activity{object: object}} <- {:find_object, Activity.get_by_id_with_object(id)},
|
|
{_, {:ok, like_object, meta}} <- {:build_object, Builder.like(user, object)},
|
|
{_, {:ok, %Activity{} = activity, _meta}} <-
|
|
{:common_pipeline,
|
|
Pipeline.common_pipeline(like_object, Keyword.put(meta, :local, true))} do
|
|
{:ok, activity}
|
|
else
|
|
{:find_object, _} ->
|
|
{:error, :not_found}
|
|
|
|
{:common_pipeline, {:error, {:validate, {:error, changeset}}}} = e ->
|
|
if {:object, {"already liked by this actor", []}} in changeset.errors do
|
|
{:ok, :already_liked}
|
|
else
|
|
{:error, e}
|
|
end
|
|
|
|
e ->
|
|
{:error, e}
|
|
end
|
|
end
|
|
|
|
def unfavorite(id, user) do
|
|
with {_, %Activity{data: %{"type" => "Create"}} = activity} <-
|
|
{:find_activity, Activity.get_by_id(id)},
|
|
%Object{} = note <- Object.normalize(activity, fetch: false),
|
|
%Activity{} = like <- Utils.get_existing_like(user.ap_id, note),
|
|
{:ok, undo, _} <- Builder.undo(user, like),
|
|
{:ok, activity, _} <- Pipeline.common_pipeline(undo, local: true) do
|
|
{:ok, activity}
|
|
else
|
|
{:find_activity, _} -> {:error, :not_found}
|
|
_ -> {:error, dgettext("errors", "Could not unfavorite")}
|
|
end
|
|
end
|
|
|
|
def react_with_emoji(id, user, emoji) do
|
|
with %Activity{} = activity <- Activity.get_by_id(id),
|
|
object <- Object.normalize(activity, fetch: false),
|
|
{:ok, emoji_react, _} <- Builder.emoji_react(user, object, emoji),
|
|
{:ok, activity, _} <- Pipeline.common_pipeline(emoji_react, local: true) do
|
|
{:ok, activity}
|
|
else
|
|
_ ->
|
|
{:error, dgettext("errors", "Could not add reaction emoji")}
|
|
end
|
|
end
|
|
|
|
def unreact_with_emoji(id, user, emoji) do
|
|
with %Activity{} = reaction_activity <- Utils.get_latest_reaction(id, user, emoji),
|
|
{:ok, undo, _} <- Builder.undo(user, reaction_activity),
|
|
{:ok, activity, _} <- Pipeline.common_pipeline(undo, local: true) do
|
|
{:ok, activity}
|
|
else
|
|
_ ->
|
|
{:error, dgettext("errors", "Could not remove reaction emoji")}
|
|
end
|
|
end
|
|
|
|
def vote(user, %{data: %{"type" => "Question"}} = object, choices) do
|
|
with :ok <- validate_not_author(object, user),
|
|
:ok <- validate_existing_votes(user, object),
|
|
{:ok, options, choices} <- normalize_and_validate_choices(choices, object) do
|
|
answer_activities =
|
|
Enum.map(choices, fn index ->
|
|
{:ok, answer_object, _meta} =
|
|
Builder.answer(user, object, Enum.at(options, index)["name"])
|
|
|
|
{:ok, activity_data, _meta} = Builder.create(user, answer_object, [])
|
|
|
|
{:ok, activity, _meta} =
|
|
activity_data
|
|
|> Map.put("cc", answer_object["cc"])
|
|
|> Map.put("context", answer_object["context"])
|
|
|> Pipeline.common_pipeline(local: true)
|
|
|
|
# TODO: Do preload of Pleroma.Object in Pipeline
|
|
Activity.normalize(activity.data)
|
|
end)
|
|
|
|
object = Object.get_cached_by_ap_id(object.data["id"])
|
|
{:ok, answer_activities, object}
|
|
end
|
|
end
|
|
|
|
defp validate_not_author(%{data: %{"actor" => ap_id}}, %{ap_id: ap_id}),
|
|
do: {:error, dgettext("errors", "Poll's author can't vote")}
|
|
|
|
defp validate_not_author(_, _), do: :ok
|
|
|
|
defp validate_existing_votes(%{ap_id: ap_id}, object) do
|
|
if Utils.get_existing_votes(ap_id, object) == [] do
|
|
:ok
|
|
else
|
|
{:error, dgettext("errors", "Already voted")}
|
|
end
|
|
end
|
|
|
|
defp get_options_and_max_count(%{data: %{"anyOf" => any_of}})
|
|
when is_list(any_of) and any_of != [],
|
|
do: {any_of, Enum.count(any_of)}
|
|
|
|
defp get_options_and_max_count(%{data: %{"oneOf" => one_of}})
|
|
when is_list(one_of) and one_of != [],
|
|
do: {one_of, 1}
|
|
|
|
defp normalize_and_validate_choices(choices, object) do
|
|
choices = Enum.map(choices, fn i -> if is_binary(i), do: String.to_integer(i), else: i end)
|
|
{options, max_count} = get_options_and_max_count(object)
|
|
count = Enum.count(options)
|
|
|
|
with {_, true} <- {:valid_choice, Enum.all?(choices, &(&1 < count))},
|
|
{_, true} <- {:count_check, Enum.count(choices) <= max_count} do
|
|
{:ok, options, choices}
|
|
else
|
|
{:valid_choice, _} -> {:error, dgettext("errors", "Invalid indices")}
|
|
{:count_check, _} -> {:error, dgettext("errors", "Too many choices")}
|
|
end
|
|
end
|
|
|
|
def announce_visibility(_, %{visibility: visibility})
|
|
when visibility in ~w{public unlisted private direct local},
|
|
do: visibility
|
|
|
|
def announce_visibility(object, _), do: Visibility.get_visibility(object)
|
|
|
|
def get_visibility(_, _, %Participation{}), do: {"direct", "direct"}
|
|
|
|
def get_visibility(%{visibility: visibility}, in_reply_to, _)
|
|
when visibility in ~w{public local unlisted private direct},
|
|
do: {visibility, get_replied_to_visibility(in_reply_to)}
|
|
|
|
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, fetch: false) do
|
|
Visibility.get_visibility(object)
|
|
end
|
|
end
|
|
|
|
def get_quoted_visibility(nil), do: nil
|
|
|
|
def get_quoted_visibility(activity), do: get_replied_to_visibility(activity)
|
|
|
|
def check_expiry_date({:ok, nil} = res), do: res
|
|
|
|
def check_expiry_date({:ok, in_seconds}) do
|
|
expiry = DateTime.add(DateTime.utc_now(), in_seconds)
|
|
|
|
if Pleroma.Workers.PurgeExpiredActivity.expires_late_enough?(expiry) do
|
|
{:ok, expiry}
|
|
else
|
|
{:error, "Expiry date is too soon"}
|
|
end
|
|
end
|
|
|
|
def check_expiry_date(expiry_str) do
|
|
Ecto.Type.cast(:integer, expiry_str)
|
|
|> check_expiry_date()
|
|
end
|
|
|
|
def post(user, %{status: _} = data) do
|
|
with {:ok, draft} <- ActivityDraft.create(user, data) do
|
|
ActivityPub.create(draft.changes, draft.preview?)
|
|
end
|
|
end
|
|
|
|
def update(user, orig_activity, changes) do
|
|
with orig_object <- Object.normalize(orig_activity),
|
|
{:ok, new_object} <- make_update_data(user, orig_object, changes),
|
|
{:ok, update_data, _} <- Builder.update(user, new_object),
|
|
{:ok, update, _} <- Pipeline.common_pipeline(update_data, local: true) do
|
|
{:ok, update}
|
|
else
|
|
_ -> {:error, nil}
|
|
end
|
|
end
|
|
|
|
defp make_update_data(user, orig_object, changes) do
|
|
kept_params = %{
|
|
visibility: Visibility.get_visibility(orig_object),
|
|
in_reply_to_id:
|
|
with replied_id when is_binary(replied_id) <- orig_object.data["inReplyTo"],
|
|
%Activity{id: activity_id} <- Activity.get_create_by_object_ap_id(replied_id) do
|
|
activity_id
|
|
else
|
|
_ -> nil
|
|
end
|
|
}
|
|
|
|
params = Map.merge(changes, kept_params)
|
|
|
|
with {:ok, draft} <- ActivityDraft.create(user, params) do
|
|
change =
|
|
Object.Updater.make_update_object_data(orig_object.data, draft.object, Utils.make_date())
|
|
|
|
{:ok, change}
|
|
else
|
|
_ -> {:error, nil}
|
|
end
|
|
end
|
|
|
|
@spec pin(String.t(), User.t()) :: {:ok, Activity.t()} | {:error, term()}
|
|
def pin(id, %User{} = user) do
|
|
with %Activity{} = activity <- create_activity_by_id(id),
|
|
true <- activity_belongs_to_actor(activity, user.ap_id),
|
|
true <- object_type_is_allowed_for_pin(activity.object),
|
|
true <- activity_is_public(activity),
|
|
{:ok, pin_data, _} <- Builder.pin(user, activity.object),
|
|
{:ok, _pin, _} <-
|
|
Pipeline.common_pipeline(pin_data,
|
|
local: true,
|
|
activity_id: id
|
|
) do
|
|
{:ok, activity}
|
|
else
|
|
{:error, {:side_effects, error}} -> error
|
|
error -> error
|
|
end
|
|
end
|
|
|
|
defp create_activity_by_id(id) do
|
|
with nil <- Activity.create_by_id_with_object(id) do
|
|
{:error, :not_found}
|
|
end
|
|
end
|
|
|
|
defp activity_belongs_to_actor(%{actor: actor}, actor), do: true
|
|
defp activity_belongs_to_actor(_, _), do: {:error, :ownership_error}
|
|
|
|
defp object_type_is_allowed_for_pin(%{data: %{"type" => type}}) do
|
|
with false <- type in ["Note", "Article", "Question"] do
|
|
{:error, :not_allowed}
|
|
end
|
|
end
|
|
|
|
defp activity_is_public(activity) do
|
|
with false <- Visibility.is_public?(activity) do
|
|
{:error, :visibility_error}
|
|
end
|
|
end
|
|
|
|
@spec unpin(String.t(), User.t()) :: {:ok, User.t()} | {:error, term()}
|
|
def unpin(id, user) do
|
|
with %Activity{} = activity <- create_activity_by_id(id),
|
|
{:ok, unpin_data, _} <- Builder.unpin(user, activity.object),
|
|
{:ok, _unpin, _} <-
|
|
Pipeline.common_pipeline(unpin_data,
|
|
local: true,
|
|
activity_id: activity.id,
|
|
expires_at: activity.data["expires_at"],
|
|
featured_address: user.featured_address
|
|
) do
|
|
{:ok, activity}
|
|
end
|
|
end
|
|
|
|
def add_mute(user, activity, params \\ %{}) do
|
|
expires_in = Map.get(params, :expires_in, 0)
|
|
|
|
with {:ok, _} <- ThreadMute.add_mute(user.id, activity.data["context"]),
|
|
_ <- Pleroma.Notification.mark_context_as_read(user, activity.data["context"]) do
|
|
if expires_in > 0 do
|
|
Pleroma.Workers.MuteExpireWorker.enqueue(
|
|
"unmute_conversation",
|
|
%{"user_id" => user.id, "activity_id" => activity.id},
|
|
schedule_in: expires_in
|
|
)
|
|
end
|
|
|
|
{:ok, activity}
|
|
else
|
|
{:error, _} -> {:error, dgettext("errors", "conversation is already muted")}
|
|
end
|
|
end
|
|
|
|
def remove_mute(%User{} = user, %Activity{} = activity) do
|
|
ThreadMute.remove_mute(user.id, activity.data["context"])
|
|
{:ok, activity}
|
|
end
|
|
|
|
def remove_mute(user_id, activity_id) do
|
|
with {:user, %User{} = user} <- {:user, User.get_by_id(user_id)},
|
|
{:activity, %Activity{} = activity} <- {:activity, Activity.get_by_id(activity_id)} do
|
|
remove_mute(user, activity)
|
|
else
|
|
{what, result} = error ->
|
|
Logger.warning(
|
|
"CommonAPI.remove_mute/2 failed. #{what}: #{result}, user_id: #{user_id}, activity_id: #{activity_id}"
|
|
)
|
|
|
|
{:error, error}
|
|
end
|
|
end
|
|
|
|
def thread_muted?(%User{id: user_id}, %{data: %{"context" => context}})
|
|
when is_binary(context) do
|
|
ThreadMute.exists?(user_id, context)
|
|
end
|
|
|
|
def thread_muted?(_, _), do: false
|
|
|
|
def report(user, data) do
|
|
with {:ok, account} <- get_reported_account(data.account_id),
|
|
{:ok, {content_html, _, _}} <- make_report_content_html(data[:comment]),
|
|
{:ok, statuses} <- get_report_statuses(account, data) do
|
|
ActivityPub.flag(%{
|
|
context: Utils.generate_context_id(),
|
|
actor: user,
|
|
account: account,
|
|
statuses: statuses,
|
|
content: content_html,
|
|
forward: Map.get(data, :forward, false)
|
|
})
|
|
end
|
|
end
|
|
|
|
defp get_reported_account(account_id) do
|
|
case User.get_cached_by_id(account_id) do
|
|
%User{} = account -> {:ok, account}
|
|
_ -> {:error, dgettext("errors", "Account not found")}
|
|
end
|
|
end
|
|
|
|
def update_report_state(activity_ids, state) when is_list(activity_ids) do
|
|
case Utils.update_report_state(activity_ids, state) do
|
|
:ok -> {:ok, activity_ids}
|
|
_ -> {:error, dgettext("errors", "Could not update state")}
|
|
end
|
|
end
|
|
|
|
def update_report_state(activity_id, state) do
|
|
with %Activity{} = activity <- Activity.get_by_id(activity_id) do
|
|
Utils.update_report_state(activity, state)
|
|
else
|
|
nil -> {:error, :not_found}
|
|
_ -> {: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) do
|
|
set_visibility(activity, opts)
|
|
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{} = user, %User{} = target) do
|
|
UserRelationship.create_reblog_mute(user, target)
|
|
end
|
|
|
|
def show_reblogs(%User{} = user, %User{} = target) do
|
|
UserRelationship.delete_reblog_mute(user, target)
|
|
end
|
|
|
|
def get_user(ap_id, fake_record_fallback \\ true) do
|
|
cond do
|
|
user = User.get_cached_by_ap_id(ap_id) ->
|
|
user
|
|
|
|
user = User.get_by_guessed_nickname(ap_id) ->
|
|
user
|
|
|
|
fake_record_fallback ->
|
|
# TODO: refactor (fake records is never a good idea)
|
|
User.error_user(ap_id)
|
|
|
|
true ->
|
|
nil
|
|
end
|
|
end
|
|
end
|