Page Menu
Home
Phorge
Search
Configure Global Search
Log In
Files
F85831593
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Flag For Later
Award Token
Authored By
Unknown
Size
77 KB
Referenced Files
None
Subscribers
None
View Options
diff --git a/lib/pleroma/web/activity_pub/transmogrifier.ex b/lib/pleroma/web/activity_pub/transmogrifier.ex
index ab744f6a2..0ebb49dc0 100644
--- a/lib/pleroma/web/activity_pub/transmogrifier.ex
+++ b/lib/pleroma/web/activity_pub/transmogrifier.ex
@@ -1,652 +1,673 @@
defmodule Pleroma.Web.ActivityPub.Transmogrifier do
@moduledoc """
A module to handle coding from internal to wire ActivityPub and back.
"""
alias Pleroma.User
alias Pleroma.Object
alias Pleroma.Activity
alias Pleroma.Repo
alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.Web.ActivityPub.Utils
import Ecto.Query
require Logger
@doc """
Modifies an incoming AP object (mastodon format) to our internal format.
"""
def fix_object(object) do
object
|> Map.put("actor", object["attributedTo"])
|> fix_attachments
|> fix_context
|> fix_in_reply_to
|> fix_emoji
|> fix_tag
end
def fix_in_reply_to(%{"inReplyTo" => in_reply_to_id} = object)
when not is_nil(in_reply_to_id) do
case ActivityPub.fetch_object_from_id(in_reply_to_id) do
{:ok, replied_object} ->
activity = Activity.get_create_activity_by_object_ap_id(replied_object.data["id"])
object
|> Map.put("inReplyTo", replied_object.data["id"])
|> Map.put("inReplyToAtomUri", object["inReplyToAtomUri"] || in_reply_to_id)
|> Map.put("inReplyToStatusId", activity.id)
|> Map.put("conversation", replied_object.data["context"] || object["conversation"])
|> Map.put("context", replied_object.data["context"] || object["conversation"])
e ->
Logger.error("Couldn't fetch #{object["inReplyTo"]} #{inspect(e)}")
object
end
end
def fix_in_reply_to(object), do: object
def fix_context(object) do
object
|> Map.put("context", object["conversation"])
end
def fix_attachments(object) do
attachments =
(object["attachment"] || [])
|> Enum.map(fn data ->
url = [%{"type" => "Link", "mediaType" => data["mediaType"], "href" => data["url"]}]
Map.put(data, "url", url)
end)
object
|> Map.put("attachment", attachments)
end
def fix_emoji(object) do
tags = object["tag"] || []
emoji = tags |> Enum.filter(fn data -> data["type"] == "Emoji" and data["icon"] end)
emoji =
emoji
|> Enum.reduce(%{}, fn data, mapping ->
name = data["name"]
name =
if String.starts_with?(name, ":") do
name |> String.slice(1..-2)
else
name
end
mapping |> Map.put(name, data["icon"]["url"])
end)
# we merge mastodon and pleroma emoji into a single mapping, to allow for both wire formats
emoji = Map.merge(object["emoji"] || %{}, emoji)
object
|> Map.put("emoji", emoji)
end
def fix_tag(object) do
tags =
(object["tag"] || [])
|> Enum.filter(fn data -> data["type"] == "Hashtag" and data["name"] end)
|> Enum.map(fn data -> String.slice(data["name"], 1..-1) end)
combined = (object["tag"] || []) ++ tags
object
|> Map.put("tag", combined)
end
# TODO: validate those with a Ecto scheme
# - tags
# - emoji
def handle_incoming(%{"type" => "Create", "object" => %{"type" => "Note"} = object} = data) do
with nil <- Activity.get_create_activity_by_object_ap_id(object["id"]),
%User{} = user <- User.get_or_fetch_by_ap_id(data["actor"]) do
object = fix_object(data["object"])
params = %{
to: data["to"],
object: object,
actor: user,
context: object["conversation"],
local: false,
published: data["published"],
additional:
Map.take(data, [
"cc",
"id"
])
}
ActivityPub.create(params)
else
%Activity{} = activity -> {:ok, activity}
_e -> :error
end
end
def handle_incoming(
%{"type" => "Follow", "object" => followed, "actor" => follower, "id" => id} = data
) do
with %User{local: true} = followed <- User.get_cached_by_ap_id(followed),
%User{} = follower <- User.get_or_fetch_by_ap_id(follower),
{:ok, activity} <- ActivityPub.follow(follower, followed, id, false) do
if not User.locked?(followed) do
ActivityPub.accept(%{to: [follower.ap_id], actor: followed.ap_id, object: data, local: true})
User.follow(follower, followed)
end
{:ok, activity}
else
_e -> :error
end
end
defp mastodon_follow_hack(%{"id" => id, "actor" => follower_id}, followed) do
with true <- id =~ "follows",
%User{local: true} = follower <- User.get_cached_by_ap_id(follower_id),
%Activity{} = activity <- Utils.fetch_latest_follow(follower, followed) do
{:ok, activity}
else
_ -> {:error, nil}
end
end
defp mastodon_follow_hack(_), do: {:error, nil}
defp get_follow_activity(follow_object, followed) do
with object_id when not is_nil(object_id) <- Utils.get_ap_id(follow_object),
{_, %Activity{} = activity} <- {:activity, Activity.get_by_ap_id(object_id)} do
{:ok, activity}
else
# Can't find the activity. This might a Mastodon 2.3 "Accept"
{:activity, nil} ->
mastodon_follow_hack(follow_object, followed)
_ ->
{:error, nil}
end
end
def handle_incoming(
%{"type" => "Accept", "object" => follow_object, "actor" => actor, "id" => id} = data
) do
with %User{} = followed <- User.get_or_fetch_by_ap_id(actor),
{:ok, follow_activity} <- get_follow_activity(follow_object, followed),
%User{local: true} = follower <- User.get_cached_by_ap_id(follow_activity.data["actor"]),
{:ok, activity} <-
ActivityPub.accept(%{
to: follow_activity.data["to"],
type: "Accept",
actor: followed.ap_id,
object: follow_activity.data["id"],
local: false
}) do
if not User.following?(follower, followed) do
{:ok, follower} = User.follow(follower, followed)
end
{:ok, activity}
else
_e -> :error
end
end
def handle_incoming(
%{"type" => "Reject", "object" => follow_object, "actor" => actor, "id" => id} = data
) do
with %User{} = followed <- User.get_or_fetch_by_ap_id(actor),
{:ok, follow_activity} <- get_follow_activity(follow_object, followed),
%User{local: true} = follower <- User.get_cached_by_ap_id(follow_activity.data["actor"]),
{:ok, activity} <-
ActivityPub.accept(%{
to: follow_activity.data["to"],
type: "Accept",
actor: followed.ap_id,
object: follow_activity.data["id"],
local: false
}) do
User.unfollow(follower, followed)
{:ok, activity}
else
_e -> :error
end
end
def handle_incoming(
%{"type" => "Like", "object" => object_id, "actor" => actor, "id" => id} = _data
) do
with %User{} = actor <- User.get_or_fetch_by_ap_id(actor),
{:ok, object} <-
get_obj_helper(object_id) || ActivityPub.fetch_object_from_id(object_id),
{:ok, activity, _object} <- ActivityPub.like(actor, object, id, false) do
{:ok, activity}
else
_e -> :error
end
end
def handle_incoming(
%{"type" => "Announce", "object" => object_id, "actor" => actor, "id" => id} = _data
) do
with %User{} = actor <- User.get_or_fetch_by_ap_id(actor),
{:ok, object} <-
get_obj_helper(object_id) || ActivityPub.fetch_object_from_id(object_id),
{:ok, activity, _object} <- ActivityPub.announce(actor, object, id, false) do
{:ok, activity}
else
_e -> :error
end
end
def handle_incoming(
%{"type" => "Update", "object" => %{"type" => "Person"} = object, "actor" => actor_id} =
data
) do
with %User{ap_id: ^actor_id} = actor <- User.get_by_ap_id(object["id"]) do
{:ok, new_user_data} = ActivityPub.user_data_from_user_object(object)
banner = new_user_data[:info]["banner"]
locked = new_user_data[:info]["locked"] || false
update_data =
new_user_data
|> Map.take([:name, :bio, :avatar])
|> Map.put(:info, Map.merge(actor.info, %{"banner" => banner, "locked" => locked}))
actor
|> User.upgrade_changeset(update_data)
|> User.update_and_set_cache()
ActivityPub.update(%{
local: false,
to: data["to"] || [],
cc: data["cc"] || [],
object: object,
actor: actor_id
})
else
e ->
Logger.error(e)
:error
end
end
# TODO: Make secure.
def handle_incoming(
%{"type" => "Delete", "object" => object_id, "actor" => actor, "id" => _id} = _data
) do
object_id = Utils.get_ap_id(object_id)
with %User{} = _actor <- User.get_or_fetch_by_ap_id(actor),
{:ok, object} <-
get_obj_helper(object_id) || ActivityPub.fetch_object_from_id(object_id),
{:ok, activity} <- ActivityPub.delete(object, false) do
{:ok, activity}
else
_e -> :error
end
end
def handle_incoming(
%{
"type" => "Undo",
"object" => %{"type" => "Announce", "object" => object_id},
"actor" => actor,
"id" => id
} = _data
) do
with %User{} = actor <- User.get_or_fetch_by_ap_id(actor),
{:ok, object} <-
get_obj_helper(object_id) || ActivityPub.fetch_object_from_id(object_id),
{:ok, activity, _, _} <- ActivityPub.unannounce(actor, object, id, false) do
{:ok, activity}
else
_e -> :error
end
end
def handle_incoming(
%{
"type" => "Undo",
"object" => %{"type" => "Follow", "object" => followed},
"actor" => follower,
"id" => id
} = _data
) do
with %User{local: true} = followed <- User.get_cached_by_ap_id(followed),
%User{} = follower <- User.get_or_fetch_by_ap_id(follower),
{:ok, activity} <- ActivityPub.unfollow(follower, followed, id, false) do
User.unfollow(follower, followed)
{:ok, activity}
else
e -> :error
end
end
@ap_config Application.get_env(:pleroma, :activitypub)
@accept_blocks Keyword.get(@ap_config, :accept_blocks)
def handle_incoming(
%{
"type" => "Undo",
"object" => %{"type" => "Block", "object" => blocked},
"actor" => blocker,
"id" => id
} = _data
) do
with true <- @accept_blocks,
%User{local: true} = blocked <- User.get_cached_by_ap_id(blocked),
%User{} = blocker <- User.get_or_fetch_by_ap_id(blocker),
{:ok, activity} <- ActivityPub.unblock(blocker, blocked, id, false) do
User.unblock(blocker, blocked)
{:ok, activity}
else
e -> :error
end
end
def handle_incoming(
%{"type" => "Block", "object" => blocked, "actor" => blocker, "id" => id} = data
) do
with true <- @accept_blocks,
%User{local: true} = blocked = User.get_cached_by_ap_id(blocked),
%User{} = blocker = User.get_or_fetch_by_ap_id(blocker),
{:ok, activity} <- ActivityPub.block(blocker, blocked, id, false) do
User.unfollow(blocker, blocked)
User.block(blocker, blocked)
{:ok, activity}
else
e -> :error
end
end
def handle_incoming(
%{
"type" => "Undo",
"object" => %{"type" => "Like", "object" => object_id},
"actor" => actor,
"id" => id
} = _data
) do
with %User{} = actor <- User.get_or_fetch_by_ap_id(actor),
{:ok, object} <-
get_obj_helper(object_id) || ActivityPub.fetch_object_from_id(object_id),
{:ok, activity, _, _} <- ActivityPub.unlike(actor, object, id, false) do
{:ok, activity}
else
_e -> :error
end
end
def handle_incoming(_), do: :error
def get_obj_helper(id) do
if object = Object.get_by_ap_id(id), do: {:ok, object}, else: nil
end
def set_reply_to_uri(%{"inReplyTo" => inReplyTo} = object) do
with false <- String.starts_with?(inReplyTo, "http"),
{:ok, %{data: replied_to_object}} <- get_obj_helper(inReplyTo) do
Map.put(object, "inReplyTo", replied_to_object["external_url"] || inReplyTo)
else
_e -> object
end
end
def set_reply_to_uri(obj), do: obj
# Prepares the object of an outgoing create activity.
def prepare_object(object) do
object
|> set_sensitive
|> add_hashtags
|> add_mention_tags
|> add_emoji_tags
|> add_attributed_to
|> prepare_attachments
|> set_conversation
|> set_reply_to_uri
end
# @doc
# """
# internal -> Mastodon
# """
def prepare_outgoing(%{"type" => "Create", "object" => %{"type" => "Note"} = object} = data) do
object =
object
|> prepare_object
data =
data
|> Map.put("object", object)
|> Map.put("@context", "https://www.w3.org/ns/activitystreams")
{:ok, data}
end
+ # Mastodon Accept/Reject requires a non-normalized object containing the actor URIs,
+ # because of course it does.
+ def prepare_outgoing(%{"type" => "Accept"} = data) do
+ with follow_activity <- Activity.get_by_ap_id(data["object"]) do
+ object = %{
+ "actor" => follow_activity.actor,
+ "object" => follow_activity.data["object"],
+ "id" => follow_activity.data["id"],
+ "type" => "Follow"
+ }
+
+ data =
+ data
+ |> Map.put("object", object)
+
+ IO.inspect(data)
+
+ {:ok, data}
+ end
+ end
+
def prepare_outgoing(%{"type" => _type} = data) do
data =
data
|> maybe_fix_object_url
|> Map.put("@context", "https://www.w3.org/ns/activitystreams")
{:ok, data}
end
def maybe_fix_object_url(data) do
if is_binary(data["object"]) and not String.starts_with?(data["object"], "http") do
case ActivityPub.fetch_object_from_id(data["object"]) do
{:ok, relative_object} ->
if relative_object.data["external_url"] do
_data =
data
|> Map.put("object", relative_object.data["external_url"])
else
data
end
e ->
Logger.error("Couldn't fetch #{data["object"]} #{inspect(e)}")
data
end
else
data
end
end
def add_hashtags(object) do
tags =
(object["tag"] || [])
|> Enum.map(fn tag ->
%{
"href" => Pleroma.Web.Endpoint.url() <> "/tags/#{tag}",
"name" => "##{tag}",
"type" => "Hashtag"
}
end)
object
|> Map.put("tag", tags)
end
def add_mention_tags(object) do
recipients = object["to"] ++ (object["cc"] || [])
mentions =
recipients
|> Enum.map(fn ap_id -> User.get_cached_by_ap_id(ap_id) end)
|> Enum.filter(& &1)
|> Enum.map(fn user ->
%{"type" => "Mention", "href" => user.ap_id, "name" => "@#{user.nickname}"}
end)
tags = object["tag"] || []
object
|> Map.put("tag", tags ++ mentions)
end
# TODO: we should probably send mtime instead of unix epoch time for updated
def add_emoji_tags(object) do
tags = object["tag"] || []
emoji = object["emoji"] || []
out =
emoji
|> Enum.map(fn {name, url} ->
%{
"icon" => %{"url" => url, "type" => "Image"},
"name" => ":" <> name <> ":",
"type" => "Emoji",
"updated" => "1970-01-01T00:00:00Z",
"id" => url
}
end)
object
|> Map.put("tag", tags ++ out)
end
def set_conversation(object) do
Map.put(object, "conversation", object["context"])
end
def set_sensitive(object) do
tags = object["tag"] || []
Map.put(object, "sensitive", "nsfw" in tags)
end
def add_attributed_to(object) do
attributedTo = object["attributedTo"] || object["actor"]
object
|> Map.put("attributedTo", attributedTo)
end
def prepare_attachments(object) do
attachments =
(object["attachment"] || [])
|> Enum.map(fn data ->
[%{"mediaType" => media_type, "href" => href} | _] = data["url"]
%{"url" => href, "mediaType" => media_type, "name" => data["name"], "type" => "Document"}
end)
object
|> Map.put("attachment", attachments)
end
defp user_upgrade_task(user) do
old_follower_address = User.ap_followers(user)
q =
from(
u in User,
where: ^old_follower_address in u.following,
update: [
set: [
following:
fragment(
"array_replace(?,?,?)",
u.following,
^old_follower_address,
^user.follower_address
)
]
]
)
Repo.update_all(q, [])
maybe_retire_websub(user.ap_id)
# Only do this for recent activties, don't go through the whole db.
# Only look at the last 1000 activities.
since = (Repo.aggregate(Activity, :max, :id) || 0) - 1_000
q =
from(
a in Activity,
where: ^old_follower_address in a.recipients,
where: a.id > ^since,
update: [
set: [
recipients:
fragment(
"array_replace(?,?,?)",
a.recipients,
^old_follower_address,
^user.follower_address
)
]
]
)
Repo.update_all(q, [])
end
def upgrade_user_from_ap_id(ap_id, async \\ true) do
with %User{local: false} = user <- User.get_by_ap_id(ap_id),
{:ok, data} <- ActivityPub.fetch_and_prepare_user_from_ap_id(ap_id) do
data =
data
|> Map.put(:info, Map.merge(user.info, data[:info]))
already_ap = User.ap_enabled?(user)
{:ok, user} =
User.upgrade_changeset(user, data)
|> Repo.update()
if !already_ap do
# This could potentially take a long time, do it in the background
if async do
Task.start(fn ->
user_upgrade_task(user)
end)
else
user_upgrade_task(user)
end
end
{:ok, user}
else
e -> e
end
end
def maybe_retire_websub(ap_id) do
# some sanity checks
if is_binary(ap_id) && String.length(ap_id) > 8 do
q =
from(
ws in Pleroma.Web.Websub.WebsubClientSubscription,
where: fragment("? like ?", ws.topic, ^"#{ap_id}%")
)
Repo.delete_all(q)
end
end
def maybe_fix_user_url(data) do
if is_map(data["url"]) do
Map.put(data, "url", data["url"]["href"])
else
data
end
end
def maybe_fix_user_object(data) do
data
|> maybe_fix_user_url
end
end
diff --git a/lib/pleroma/web/activity_pub/utils.ex b/lib/pleroma/web/activity_pub/utils.ex
index 3229949c0..6ce954cd0 100644
--- a/lib/pleroma/web/activity_pub/utils.ex
+++ b/lib/pleroma/web/activity_pub/utils.ex
@@ -1,425 +1,441 @@
defmodule Pleroma.Web.ActivityPub.Utils do
alias Pleroma.{Repo, Web, Object, Activity, User}
alias Pleroma.Web.Router.Helpers
alias Pleroma.Web.Endpoint
alias Ecto.{Changeset, UUID}
import Ecto.Query
+ require Logger
# 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.
def get_ap_id(object) do
case object do
%{"id" => id} -> id
id -> id
end
end
def normalize_params(params) do
Map.put(params, "actor", get_ap_id(params["actor"]))
end
def make_json_ld_header do
%{
"@context" => [
"https://www.w3.org/ns/activitystreams",
"https://w3id.org/security/v1",
%{
"manuallyApprovesFollowers" => "as:manuallyApprovesFollowers",
"sensitive" => "as:sensitive",
"Hashtag" => "as:Hashtag",
"ostatus" => "http://ostatus.org#",
"atomUri" => "ostatus:atomUri",
"inReplyToAtomUri" => "ostatus:inReplyToAtomUri",
"conversation" => "ostatus:conversation",
"toot" => "http://joinmastodon.org/ns#",
"Emoji" => "toot:Emoji"
}
]
}
end
def make_date do
DateTime.utc_now() |> DateTime.to_iso8601()
end
def generate_activity_id do
generate_id("activities")
end
def generate_context_id do
generate_id("contexts")
end
def generate_object_id do
Helpers.o_status_url(Endpoint, :object, UUID.generate())
end
def generate_id(type) do
"#{Web.base_url()}/#{type}/#{UUID.generate()}"
end
def create_context(context) do
context = context || generate_id("contexts")
changeset = Object.context_mapping(context)
case Repo.insert(changeset) do
{:ok, object} ->
object
# This should be solved by an upsert, but it seems ecto
# has problems accessing the constraint inside the jsonb.
{:error, _} ->
Object.get_cached_by_ap_id(context)
end
end
@doc """
Enqueues an activity for federation if it's local
"""
def maybe_federate(%Activity{local: true} = activity) do
priority =
case activity.data["type"] do
"Delete" -> 10
"Create" -> 1
_ -> 5
end
Pleroma.Web.Federator.enqueue(:publish, activity, priority)
:ok
end
def maybe_federate(_), do: :ok
@doc """
Adds an id and a published data if they aren't there,
also adds it to an included object
"""
def lazy_put_activity_defaults(map) do
%{data: %{"id" => context}, id: context_id} = create_context(map["context"])
map =
map
|> Map.put_new_lazy("id", &generate_activity_id/0)
|> Map.put_new_lazy("published", &make_date/0)
|> Map.put_new("context", context)
|> Map.put_new("context_id", context_id)
if is_map(map["object"]) do
object = lazy_put_object_defaults(map["object"], map)
%{map | "object" => object}
else
map
end
end
@doc """
Adds an id and published date if they aren't there.
"""
def lazy_put_object_defaults(map, activity \\ %{}) do
map
|> Map.put_new_lazy("id", &generate_object_id/0)
|> Map.put_new_lazy("published", &make_date/0)
|> Map.put_new("context", activity["context"])
|> Map.put_new("context_id", activity["context_id"])
end
@doc """
Inserts a full object if it is contained in an activity.
"""
def insert_full_object(%{"object" => %{"type" => type} = object_data})
when is_map(object_data) and type in ["Note"] do
with {:ok, _} <- Object.create(object_data) do
:ok
end
end
def insert_full_object(_), do: :ok
def update_object_in_activities(%{data: %{"id" => id}} = object) do
# TODO
# Update activities that already had this. Could be done in a seperate process.
# Alternatively, just don't do this and fetch the current object each time. Most
# could probably be taken from cache.
relevant_activities = Activity.all_by_object_ap_id(id)
Enum.map(relevant_activities, fn activity ->
new_activity_data = activity.data |> Map.put("object", object.data)
changeset = Changeset.change(activity, data: new_activity_data)
Repo.update(changeset)
end)
end
#### Like-related helpers
@doc """
Returns an existing like if a user already liked an object
"""
def get_existing_like(actor, %{data: %{"id" => id}}) do
query =
from(
activity in Activity,
where: fragment("(?)->>'actor' = ?", activity.data, ^actor),
# this is to use the index
where:
fragment(
"coalesce((?)->'object'->>'id', (?)->>'object') = ?",
activity.data,
activity.data,
^id
),
where: fragment("(?)->>'type' = 'Like'", activity.data)
)
Repo.one(query)
end
def make_like_data(%User{ap_id: ap_id} = actor, %{data: %{"id" => id}} = object, activity_id) do
data = %{
"type" => "Like",
"actor" => ap_id,
"object" => id,
"to" => [actor.follower_address, object.data["actor"]],
"cc" => ["https://www.w3.org/ns/activitystreams#Public"],
"context" => object.data["context"]
}
if activity_id, do: Map.put(data, "id", activity_id), else: data
end
def update_element_in_object(property, element, object) do
with new_data <-
object.data
|> Map.put("#{property}_count", length(element))
|> Map.put("#{property}s", element),
changeset <- Changeset.change(object, data: new_data),
{:ok, object} <- Repo.update(changeset),
_ <- update_object_in_activities(object) do
{:ok, object}
end
end
def update_likes_in_object(likes, object) do
update_element_in_object("like", likes, object)
end
def add_like_to_object(%Activity{data: %{"actor" => actor}}, object) do
with likes <- [actor | object.data["likes"] || []] |> Enum.uniq() do
update_likes_in_object(likes, object)
end
end
def remove_like_from_object(%Activity{data: %{"actor" => actor}}, object) do
with likes <- (object.data["likes"] || []) |> List.delete(actor) do
update_likes_in_object(likes, object)
end
end
#### Follow-related helpers
+ @doc """
+ Updates a follow activity's state (for locked accounts).
+ """
+ def update_follow_state(%Activity{} = activity, state) do
+ with new_data <-
+ activity.data
+ |> Map.put("state", state),
+ changeset <- Changeset.change(activity, data: new_data),
+ {:ok, activity} <- Repo.update(changeset) do
+ {:ok, activity}
+ end
+ end
+
@doc """
Makes a follow activity data for the given follower and followed
"""
def make_follow_data(%User{ap_id: follower_id}, %User{ap_id: followed_id} = followed, activity_id) do
data = %{
"type" => "Follow",
"actor" => follower_id,
"to" => [followed_id],
"cc" => ["https://www.w3.org/ns/activitystreams#Public"],
"object" => followed_id
}
- if activity_id, do: Map.put(data, "id", activity_id), else: data
- if User.locked?(followed), do: Map.put(data, "state", "pending"), else: data
+ data = if activity_id, do: Map.put(data, "id", activity_id), else: data
+ data = if User.locked?(followed), do: Map.put(data, "state", "pending"), else: data
+
+ data
end
def fetch_latest_follow(%User{ap_id: follower_id}, %User{ap_id: followed_id}) do
query =
from(
activity in Activity,
where:
fragment(
"? ->> 'type' = 'Follow'",
activity.data
),
where: activity.actor == ^follower_id,
where:
fragment(
"? @> ?",
activity.data,
^%{object: followed_id}
),
order_by: [desc: :id],
limit: 1
)
Repo.one(query)
end
#### Announce-related helpers
@doc """
Retruns an existing announce activity if the notice has already been announced
"""
def get_existing_announce(actor, %{data: %{"id" => id}}) do
query =
from(
activity in Activity,
where: activity.actor == ^actor,
# this is to use the index
where:
fragment(
"coalesce((?)->'object'->>'id', (?)->>'object') = ?",
activity.data,
activity.data,
^id
),
where: fragment("(?)->>'type' = 'Announce'", activity.data)
)
Repo.one(query)
end
@doc """
Make announce activity data for the given actor and object
"""
def make_announce_data(
%User{ap_id: ap_id} = user,
%Object{data: %{"id" => id}} = object,
activity_id
) do
data = %{
"type" => "Announce",
"actor" => ap_id,
"object" => id,
"to" => [user.follower_address, object.data["actor"]],
"cc" => ["https://www.w3.org/ns/activitystreams#Public"],
"context" => object.data["context"]
}
if activity_id, do: Map.put(data, "id", activity_id), else: data
end
@doc """
Make unannounce activity data for the given actor and object
"""
def make_unannounce_data(
%User{ap_id: ap_id} = user,
%Activity{data: %{"context" => context}} = activity,
activity_id
) do
data = %{
"type" => "Undo",
"actor" => ap_id,
"object" => activity.data,
"to" => [user.follower_address, activity.data["actor"]],
"cc" => ["https://www.w3.org/ns/activitystreams#Public"],
"context" => context
}
if activity_id, do: Map.put(data, "id", activity_id), else: data
end
def make_unlike_data(
%User{ap_id: ap_id} = user,
%Activity{data: %{"context" => context}} = activity,
activity_id
) do
data = %{
"type" => "Undo",
"actor" => ap_id,
"object" => activity.data,
"to" => [user.follower_address, activity.data["actor"]],
"cc" => ["https://www.w3.org/ns/activitystreams#Public"],
"context" => context
}
if activity_id, do: Map.put(data, "id", activity_id), else: data
end
def add_announce_to_object(%Activity{data: %{"actor" => actor}}, object) do
with announcements <- [actor | object.data["announcements"] || []] |> Enum.uniq() do
update_element_in_object("announcement", announcements, object)
end
end
def remove_announce_from_object(%Activity{data: %{"actor" => actor}}, object) do
with announcements <- (object.data["announcements"] || []) |> List.delete(actor) do
update_element_in_object("announcement", announcements, object)
end
end
#### Unfollow-related helpers
def make_unfollow_data(follower, followed, follow_activity, activity_id) do
data = %{
"type" => "Undo",
"actor" => follower.ap_id,
"to" => [followed.ap_id],
"object" => follow_activity.data
}
if activity_id, do: Map.put(data, "id", activity_id), else: data
end
#### Block-related helpers
def fetch_latest_block(%User{ap_id: blocker_id}, %User{ap_id: blocked_id}) do
query =
from(
activity in Activity,
where:
fragment(
"? ->> 'type' = 'Block'",
activity.data
),
where: activity.actor == ^blocker_id,
where:
fragment(
"? @> ?",
activity.data,
^%{object: blocked_id}
),
order_by: [desc: :id],
limit: 1
)
Repo.one(query)
end
def make_block_data(blocker, blocked, activity_id) do
data = %{
"type" => "Block",
"actor" => blocker.ap_id,
"to" => [blocked.ap_id],
"object" => blocked.ap_id
}
if activity_id, do: Map.put(data, "id", activity_id), else: data
end
def make_unblock_data(blocker, blocked, block_activity, activity_id) do
data = %{
"type" => "Undo",
"actor" => blocker.ap_id,
"to" => [blocked.ap_id],
"object" => block_activity.data
}
if activity_id, do: Map.put(data, "id", activity_id), else: data
end
#### Create-related helpers
def make_create_data(params, additional) do
published = params.published || make_date()
%{
"type" => "Create",
"to" => params.to |> Enum.uniq(),
"actor" => params.actor.ap_id,
"object" => params.object,
"published" => published,
"context" => params.context
}
|> Map.merge(additional)
end
end
diff --git a/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex b/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex
index 90b0da8da..cae81c43d 100644
--- a/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex
+++ b/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex
@@ -1,984 +1,1002 @@
defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
use Pleroma.Web, :controller
alias Pleroma.{Repo, Activity, User, Notification, Stats}
alias Pleroma.Web
alias Pleroma.Web.MastodonAPI.{StatusView, AccountView, MastodonView, ListView}
alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.Web.ActivityPub.Utils
alias Pleroma.Web.{CommonAPI, OStatus}
alias Pleroma.Web.OAuth.{Authorization, Token, App}
alias Comeonin.Pbkdf2
import Ecto.Query
require Logger
action_fallback(:errors)
def create_app(conn, params) do
with cs <- App.register_changeset(%App{}, params) |> IO.inspect(),
{:ok, app} <- Repo.insert(cs) |> IO.inspect() do
res = %{
id: app.id,
client_id: app.client_id,
client_secret: app.client_secret
}
json(conn, res)
end
end
def update_credentials(%{assigns: %{user: user}} = conn, params) do
original_user = user
params =
if bio = params["note"] do
Map.put(params, "bio", bio)
else
params
end
params =
if name = params["display_name"] do
Map.put(params, "name", name)
else
params
end
user =
if avatar = params["avatar"] do
with %Plug.Upload{} <- avatar,
{:ok, object} <- ActivityPub.upload(avatar),
change = Ecto.Changeset.change(user, %{avatar: object.data}),
{:ok, user} = User.update_and_set_cache(change) do
user
else
_e -> user
end
else
user
end
user =
if banner = params["header"] do
with %Plug.Upload{} <- banner,
{:ok, object} <- ActivityPub.upload(banner),
new_info <- Map.put(user.info, "banner", object.data),
change <- User.info_changeset(user, %{info: new_info}),
{:ok, user} <- User.update_and_set_cache(change) do
user
else
_e -> user
end
else
user
end
with changeset <- User.update_changeset(user, params),
{:ok, user} <- User.update_and_set_cache(changeset) do
if original_user != user do
CommonAPI.update(user)
end
json(conn, AccountView.render("account.json", %{user: user}))
else
_e ->
conn
|> put_status(403)
|> json(%{error: "Invalid request"})
end
end
def verify_credentials(%{assigns: %{user: user}} = conn, _) do
account = AccountView.render("account.json", %{user: user})
json(conn, account)
end
def user(conn, %{"id" => id}) do
with %User{} = user <- Repo.get(User, id) do
account = AccountView.render("account.json", %{user: user})
json(conn, account)
else
_e ->
conn
|> put_status(404)
|> json(%{error: "Can't find user"})
end
end
@instance Application.get_env(:pleroma, :instance)
@mastodon_api_level "2.3.3"
def masto_instance(conn, _params) do
response = %{
uri: Web.base_url(),
title: Keyword.get(@instance, :name),
description: "A Pleroma instance, an alternative fediverse server",
version: "#{@mastodon_api_level} (compatible; #{Keyword.get(@instance, :version)})",
email: Keyword.get(@instance, :email),
urls: %{
streaming_api: String.replace(Pleroma.Web.Endpoint.static_url(), "http", "ws")
},
stats: Stats.get_stats(),
thumbnail: Web.base_url() <> "/instance/thumbnail.jpeg",
max_toot_chars: Keyword.get(@instance, :limit)
}
json(conn, response)
end
def peers(conn, _params) do
json(conn, Stats.get_peers())
end
defp mastodonized_emoji do
Pleroma.Formatter.get_custom_emoji()
|> Enum.map(fn {shortcode, relative_url} ->
url = to_string(URI.merge(Web.base_url(), relative_url))
%{
"shortcode" => shortcode,
"static_url" => url,
"visible_in_picker" => true,
"url" => url
}
end)
end
def custom_emojis(conn, _params) do
mastodon_emoji = mastodonized_emoji()
json(conn, mastodon_emoji)
end
defp add_link_headers(conn, method, activities, param \\ nil, params \\ %{}) do
last = List.last(activities)
first = List.first(activities)
if last do
min = last.id
max = first.id
{next_url, prev_url} =
if param do
{
mastodon_api_url(
Pleroma.Web.Endpoint,
method,
param,
Map.merge(params, %{max_id: min})
),
mastodon_api_url(
Pleroma.Web.Endpoint,
method,
param,
Map.merge(params, %{since_id: max})
)
}
else
{
mastodon_api_url(
Pleroma.Web.Endpoint,
method,
Map.merge(params, %{max_id: min})
),
mastodon_api_url(
Pleroma.Web.Endpoint,
method,
Map.merge(params, %{since_id: max})
)
}
end
conn
|> put_resp_header("link", "<#{next_url}>; rel=\"next\", <#{prev_url}>; rel=\"prev\"")
else
conn
end
end
def home_timeline(%{assigns: %{user: user}} = conn, params) do
params =
params
|> Map.put("type", ["Create", "Announce"])
|> Map.put("blocking_user", user)
|> Map.put("user", user)
activities =
ActivityPub.fetch_activities([user.ap_id | user.following], params)
|> Enum.reverse()
conn
|> add_link_headers(:home_timeline, activities)
|> render(StatusView, "index.json", %{activities: activities, for: user, as: :activity})
end
def public_timeline(%{assigns: %{user: user}} = conn, params) do
local_only = params["local"] in [true, "True", "true", "1"]
params =
params
|> Map.put("type", ["Create", "Announce"])
|> Map.put("local_only", local_only)
|> Map.put("blocking_user", user)
activities =
ActivityPub.fetch_public_activities(params)
|> Enum.reverse()
conn
|> add_link_headers(:public_timeline, activities, false, %{"local" => local_only})
|> render(StatusView, "index.json", %{activities: activities, for: user, as: :activity})
end
def user_statuses(%{assigns: %{user: reading_user}} = conn, params) do
with %User{} = user <- Repo.get(User, params["id"]) do
# Since Pleroma has no "pinned" posts feature, we'll just set an empty list here
activities =
if params["pinned"] == "true" do
[]
else
ActivityPub.fetch_user_activities(user, reading_user, params)
end
conn
|> add_link_headers(:user_statuses, activities, params["id"])
|> render(StatusView, "index.json", %{
activities: activities,
for: reading_user,
as: :activity
})
end
end
def dm_timeline(%{assigns: %{user: user}} = conn, _params) do
query =
ActivityPub.fetch_activities_query([user.ap_id], %{"type" => "Create", visibility: "direct"})
activities = Repo.all(query)
conn
|> add_link_headers(:dm_timeline, activities)
|> render(StatusView, "index.json", %{activities: activities, for: user, as: :activity})
end
def get_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
with %Activity{} = activity <- Repo.get(Activity, id),
true <- ActivityPub.visible_for_user?(activity, user) do
render(conn, StatusView, "status.json", %{activity: activity, for: user})
end
end
def get_context(%{assigns: %{user: user}} = conn, %{"id" => id}) do
with %Activity{} = activity <- Repo.get(Activity, id),
activities <-
ActivityPub.fetch_activities_for_context(activity.data["context"], %{
"blocking_user" => user,
"user" => user
}),
activities <-
activities |> Enum.filter(fn %{id: aid} -> to_string(aid) != to_string(id) end),
activities <-
activities |> Enum.filter(fn %{data: %{"type" => type}} -> type == "Create" end),
grouped_activities <- Enum.group_by(activities, fn %{id: id} -> id < activity.id end) do
result = %{
ancestors:
StatusView.render(
"index.json",
for: user,
activities: grouped_activities[true] || [],
as: :activity
)
|> Enum.reverse(),
descendants:
StatusView.render(
"index.json",
for: user,
activities: grouped_activities[false] || [],
as: :activity
)
|> Enum.reverse()
}
json(conn, result)
end
end
def post_status(conn, %{"status" => "", "media_ids" => media_ids} = params)
when length(media_ids) > 0 do
params =
params
|> Map.put("status", ".")
post_status(conn, params)
end
def post_status(%{assigns: %{user: user}} = conn, %{"status" => _} = params) do
params =
params
|> Map.put("in_reply_to_status_id", params["in_reply_to_id"])
|> Map.put("no_attachment_links", true)
idempotency_key =
case get_req_header(conn, "idempotency-key") do
[key] -> key
_ -> Ecto.UUID.generate()
end
{:ok, activity} =
Cachex.fetch!(:idempotency_cache, idempotency_key, fn _ -> CommonAPI.post(user, params) end)
render(conn, StatusView, "status.json", %{activity: activity, for: user, as: :activity})
end
def delete_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
with {:ok, %Activity{}} <- CommonAPI.delete(id, user) do
json(conn, %{})
else
_e ->
conn
|> put_status(403)
|> json(%{error: "Can't delete this post"})
end
end
def reblog_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
with {:ok, announce, _activity} <- CommonAPI.repeat(ap_id_or_id, user) do
render(conn, StatusView, "status.json", %{activity: announce, for: user, as: :activity})
end
end
def unreblog_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
with {:ok, _, _, %{data: %{"id" => id}}} <- CommonAPI.unrepeat(ap_id_or_id, user),
%Activity{} = activity <- Activity.get_create_activity_by_object_ap_id(id) do
render(conn, StatusView, "status.json", %{activity: activity, for: user, as: :activity})
end
end
def fav_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
with {:ok, _fav, %{data: %{"id" => id}}} <- CommonAPI.favorite(ap_id_or_id, user),
%Activity{} = activity <- Activity.get_create_activity_by_object_ap_id(id) do
render(conn, StatusView, "status.json", %{activity: activity, for: user, as: :activity})
end
end
def unfav_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
with {:ok, _, _, %{data: %{"id" => id}}} <- CommonAPI.unfavorite(ap_id_or_id, user),
%Activity{} = activity <- Activity.get_create_activity_by_object_ap_id(id) do
render(conn, StatusView, "status.json", %{activity: activity, for: user, as: :activity})
end
end
def notifications(%{assigns: %{user: user}} = conn, params) do
notifications = Notification.for_user(user, params)
result =
Enum.map(notifications, fn x ->
render_notification(user, x)
end)
|> Enum.filter(& &1)
conn
|> add_link_headers(:notifications, notifications)
|> json(result)
end
def get_notification(%{assigns: %{user: user}} = conn, %{"id" => id} = _params) do
with {:ok, notification} <- Notification.get(user, id) do
json(conn, render_notification(user, notification))
else
{:error, reason} ->
conn
|> put_resp_content_type("application/json")
|> send_resp(403, Jason.encode!(%{"error" => reason}))
end
end
def clear_notifications(%{assigns: %{user: user}} = conn, _params) do
Notification.clear(user)
json(conn, %{})
end
def dismiss_notification(%{assigns: %{user: user}} = conn, %{"id" => id} = _params) do
with {:ok, _notif} <- Notification.dismiss(user, id) do
json(conn, %{})
else
{:error, reason} ->
conn
|> put_resp_content_type("application/json")
|> send_resp(403, Jason.encode!(%{"error" => reason}))
end
end
def relationships(%{assigns: %{user: user}} = conn, %{"id" => id}) do
id = List.wrap(id)
q = from(u in User, where: u.id in ^id)
targets = Repo.all(q)
render(conn, AccountView, "relationships.json", %{user: user, targets: targets})
end
def upload(%{assigns: %{user: _}} = conn, %{"file" => file}) do
with {:ok, object} <- ActivityPub.upload(file) do
data =
object.data
|> Map.put("id", object.id)
render(conn, StatusView, "attachment.json", %{attachment: data})
end
end
def favourited_by(conn, %{"id" => id}) do
with %Activity{data: %{"object" => %{"likes" => likes}}} <- Repo.get(Activity, id) do
q = from(u in User, where: u.ap_id in ^likes)
users = Repo.all(q)
render(conn, AccountView, "accounts.json", %{users: users, as: :user})
else
_ -> json(conn, [])
end
end
def reblogged_by(conn, %{"id" => id}) do
with %Activity{data: %{"object" => %{"announcements" => announces}}} <- Repo.get(Activity, id) do
q = from(u in User, where: u.ap_id in ^announces)
users = Repo.all(q)
render(conn, AccountView, "accounts.json", %{users: users, as: :user})
else
_ -> json(conn, [])
end
end
def hashtag_timeline(%{assigns: %{user: user}} = conn, params) do
local_only = params["local"] in [true, "True", "true", "1"]
params =
params
|> Map.put("type", "Create")
|> Map.put("local_only", local_only)
|> Map.put("blocking_user", user)
activities =
ActivityPub.fetch_public_activities(params)
|> Enum.reverse()
conn
|> add_link_headers(:hashtag_timeline, activities, params["tag"], %{"local" => local_only})
|> render(StatusView, "index.json", %{activities: activities, for: user, as: :activity})
end
# TODO: Pagination
def followers(conn, %{"id" => id}) do
with %User{} = user <- Repo.get(User, id),
{:ok, followers} <- User.get_followers(user) do
render(conn, AccountView, "accounts.json", %{users: followers, as: :user})
end
end
def following(conn, %{"id" => id}) do
with %User{} = user <- Repo.get(User, id),
{:ok, followers} <- User.get_friends(user) do
render(conn, AccountView, "accounts.json", %{users: followers, as: :user})
end
end
def follow_requests(%{assigns: %{user: followed}} = conn, _params) do
with {:ok, follow_requests} <- User.get_follow_requests(followed) do
render(conn, AccountView, "accounts.json", %{users: follow_requests, as: :user})
end
end
def authorize_follow_request(%{assigns: %{user: followed}} = conn, %{"id" => id}) do
with %User{} = follower <- Repo.get(User, id),
{:ok, follower} <- User.follow(follower, followed),
%Activity{} = follow_activity <- Utils.fetch_latest_follow(follower, followed),
+ {:ok, follow_activity} <- Utils.update_follow_state(follow_activity, "accept"),
{:ok, _activity} <-
ActivityPub.accept(%{
- to: follower.ap_id,
+ to: [follower.ap_id],
actor: followed.ap_id,
object: follow_activity.data["id"],
type: "Accept"
}) do
render(conn, AccountView, "relationship.json", %{user: followed, target: follower})
else
{:error, message} ->
conn
|> put_resp_content_type("application/json")
|> send_resp(403, Jason.encode!(%{"error" => message}))
end
end
- # def reject_follow_request(%{assigns: %{user: followed}} = conn, %{"id" => id}) do
- # end
+ def reject_follow_request(%{assigns: %{user: followed}} = conn, %{"id" => id}) do
+ with %User{} = follower <- Repo.get(User, id),
+ %Activity{} = follow_activity <- Utils.fetch_latest_follow(follower, followed),
+ {:ok, follow_activity} <- Utils.update_follow_state(follow_activity, "reject"),
+ {:ok, _activity} <-
+ ActivityPub.reject(%{
+ to: [follower.ap_id],
+ actor: followed.ap_id,
+ object: follow_activity.data["id"],
+ type: "Reject"
+ }) do
+ render(conn, AccountView, "relationship.json", %{user: followed, target: follower})
+ else
+ {:error, message} ->
+ conn
+ |> put_resp_content_type("application/json")
+ |> send_resp(403, Jason.encode!(%{"error" => message}))
+ end
+ end
def follow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
with %User{} = followed <- Repo.get(User, id),
{:ok, follower} <- User.maybe_direct_follow(follower, followed),
{:ok, _activity} <- ActivityPub.follow(follower, followed) do
render(conn, AccountView, "relationship.json", %{user: follower, target: followed})
else
{:error, message} ->
conn
|> put_resp_content_type("application/json")
|> send_resp(403, Jason.encode!(%{"error" => message}))
end
end
def follow(%{assigns: %{user: follower}} = conn, %{"uri" => uri}) do
with %User{} = followed <- Repo.get_by(User, nickname: uri),
{:ok, follower} <- User.maybe_direct_follow(follower, followed),
{:ok, _activity} <- ActivityPub.follow(follower, followed) do
render(conn, AccountView, "account.json", %{user: followed})
else
{:error, message} ->
conn
|> put_resp_content_type("application/json")
|> send_resp(403, Jason.encode!(%{"error" => message}))
end
end
def unfollow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
with %User{} = followed <- Repo.get(User, id),
{:ok, _activity} <- ActivityPub.unfollow(follower, followed),
{:ok, follower, _} <- User.unfollow(follower, followed) do
render(conn, AccountView, "relationship.json", %{user: follower, target: followed})
end
end
def block(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
with %User{} = blocked <- Repo.get(User, id),
{:ok, blocker} <- User.block(blocker, blocked),
{:ok, _activity} <- ActivityPub.block(blocker, blocked) do
render(conn, AccountView, "relationship.json", %{user: blocker, target: blocked})
else
{:error, message} ->
conn
|> put_resp_content_type("application/json")
|> send_resp(403, Jason.encode!(%{"error" => message}))
end
end
def unblock(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
with %User{} = blocked <- Repo.get(User, id),
{:ok, blocker} <- User.unblock(blocker, blocked),
{:ok, _activity} <- ActivityPub.unblock(blocker, blocked) do
render(conn, AccountView, "relationship.json", %{user: blocker, target: blocked})
else
{:error, message} ->
conn
|> put_resp_content_type("application/json")
|> send_resp(403, Jason.encode!(%{"error" => message}))
end
end
# TODO: Use proper query
def blocks(%{assigns: %{user: user}} = conn, _) do
with blocked_users <- user.info["blocks"] || [],
accounts <- Enum.map(blocked_users, fn ap_id -> User.get_cached_by_ap_id(ap_id) end) do
res = AccountView.render("accounts.json", users: accounts, for: user, as: :user)
json(conn, res)
end
end
def domain_blocks(%{assigns: %{user: %{info: info}}} = conn, _) do
json(conn, info["domain_blocks"] || [])
end
def block_domain(%{assigns: %{user: blocker}} = conn, %{"domain" => domain}) do
User.block_domain(blocker, domain)
json(conn, %{})
end
def unblock_domain(%{assigns: %{user: blocker}} = conn, %{"domain" => domain}) do
User.unblock_domain(blocker, domain)
json(conn, %{})
end
def search(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do
accounts = User.search(query, params["resolve"] == "true")
fetched =
if Regex.match?(~r/https?:/, query) do
with {:ok, activities} <- OStatus.fetch_activity_from_url(query) do
activities
|> Enum.filter(fn
%{data: %{"type" => "Create"}} -> true
_ -> false
end)
else
_e -> []
end
end || []
q =
from(
a in Activity,
where: fragment("?->>'type' = 'Create'", a.data),
where: "https://www.w3.org/ns/activitystreams#Public" in a.recipients,
where:
fragment(
"to_tsvector('english', ?->'object'->>'content') @@ plainto_tsquery('english', ?)",
a.data,
^query
),
limit: 20,
order_by: [desc: :id]
)
statuses = Repo.all(q) ++ fetched
tags =
String.split(query)
|> Enum.uniq()
|> Enum.filter(fn tag -> String.starts_with?(tag, "#") end)
|> Enum.map(fn tag -> String.slice(tag, 1..-1) end)
res = %{
"accounts" => AccountView.render("accounts.json", users: accounts, for: user, as: :user),
"statuses" =>
StatusView.render("index.json", activities: statuses, for: user, as: :activity),
"hashtags" => tags
}
json(conn, res)
end
def account_search(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do
accounts = User.search(query, params["resolve"] == "true")
res = AccountView.render("accounts.json", users: accounts, for: user, as: :user)
json(conn, res)
end
def favourites(%{assigns: %{user: user}} = conn, _) do
params =
%{}
|> Map.put("type", "Create")
|> Map.put("favorited_by", user.ap_id)
|> Map.put("blocking_user", user)
activities =
ActivityPub.fetch_public_activities(params)
|> Enum.reverse()
conn
|> render(StatusView, "index.json", %{activities: activities, for: user, as: :activity})
end
def get_lists(%{assigns: %{user: user}} = conn, opts) do
lists = Pleroma.List.for_user(user, opts)
res = ListView.render("lists.json", lists: lists)
json(conn, res)
end
def get_list(%{assigns: %{user: user}} = conn, %{"id" => id}) do
with %Pleroma.List{} = list <- Pleroma.List.get(id, user) do
res = ListView.render("list.json", list: list)
json(conn, res)
else
_e -> json(conn, "error")
end
end
def delete_list(%{assigns: %{user: user}} = conn, %{"id" => id}) do
with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
{:ok, _list} <- Pleroma.List.delete(list) do
json(conn, %{})
else
_e ->
json(conn, "error")
end
end
def create_list(%{assigns: %{user: user}} = conn, %{"title" => title}) do
with {:ok, %Pleroma.List{} = list} <- Pleroma.List.create(title, user) do
res = ListView.render("list.json", list: list)
json(conn, res)
end
end
def add_to_list(%{assigns: %{user: user}} = conn, %{"id" => id, "account_ids" => accounts}) do
accounts
|> Enum.each(fn account_id ->
with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
%User{} = followed <- Repo.get(User, account_id) do
Pleroma.List.follow(list, followed)
end
end)
json(conn, %{})
end
def remove_from_list(%{assigns: %{user: user}} = conn, %{"id" => id, "account_ids" => accounts}) do
accounts
|> Enum.each(fn account_id ->
with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
%User{} = followed <- Repo.get(Pleroma.User, account_id) do
Pleroma.List.unfollow(list, followed)
end
end)
json(conn, %{})
end
def list_accounts(%{assigns: %{user: user}} = conn, %{"id" => id}) do
with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
{:ok, users} = Pleroma.List.get_following(list) do
render(conn, AccountView, "accounts.json", %{users: users, as: :user})
end
end
def rename_list(%{assigns: %{user: user}} = conn, %{"id" => id, "title" => title}) do
with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
{:ok, list} <- Pleroma.List.rename(list, title) do
res = ListView.render("list.json", list: list)
json(conn, res)
else
_e ->
json(conn, "error")
end
end
def list_timeline(%{assigns: %{user: user}} = conn, %{"list_id" => id} = params) do
with %Pleroma.List{title: title, following: following} <- Pleroma.List.get(id, user) do
params =
params
|> Map.put("type", "Create")
|> Map.put("blocking_user", user)
# adding title is a hack to not make empty lists function like a public timeline
activities =
ActivityPub.fetch_activities([title | following], params)
|> Enum.reverse()
conn
|> render(StatusView, "index.json", %{activities: activities, for: user, as: :activity})
else
_e ->
conn
|> put_status(403)
|> json(%{error: "Error."})
end
end
def index(%{assigns: %{user: user}} = conn, _params) do
token =
conn
|> get_session(:oauth_token)
if user && token do
mastodon_emoji = mastodonized_emoji()
accounts = Map.put(%{}, user.id, AccountView.render("account.json", %{user: user}))
initial_state =
%{
meta: %{
streaming_api_base_url:
String.replace(Pleroma.Web.Endpoint.static_url(), "http", "ws"),
access_token: token,
locale: "en",
domain: Pleroma.Web.Endpoint.host(),
admin: "1",
me: "#{user.id}",
unfollow_modal: false,
boost_modal: false,
delete_modal: true,
auto_play_gif: false,
reduce_motion: false
},
compose: %{
me: "#{user.id}",
default_privacy: "public",
default_sensitive: false
},
media_attachments: %{
accept_content_types: [
".jpg",
".jpeg",
".png",
".gif",
".webm",
".mp4",
".m4v",
"image\/jpeg",
"image\/png",
"image\/gif",
"video\/webm",
"video\/mp4"
]
},
settings:
Map.get(user.info, "settings") ||
%{
onboarded: true,
home: %{
shows: %{
reblog: true,
reply: true
}
},
notifications: %{
alerts: %{
follow: true,
favourite: true,
reblog: true,
mention: true
},
shows: %{
follow: true,
favourite: true,
reblog: true,
mention: true
},
sounds: %{
follow: true,
favourite: true,
reblog: true,
mention: true
}
}
},
push_subscription: nil,
accounts: accounts,
custom_emojis: mastodon_emoji,
char_limit: Keyword.get(@instance, :limit)
}
|> Jason.encode!()
conn
|> put_layout(false)
|> render(MastodonView, "index.html", %{initial_state: initial_state})
else
conn
|> redirect(to: "/web/login")
end
end
def put_settings(%{assigns: %{user: user}} = conn, %{"data" => settings} = _params) do
with new_info <- Map.put(user.info, "settings", settings),
change <- User.info_changeset(user, %{info: new_info}),
{:ok, _user} <- User.update_and_set_cache(change) do
conn
|> json(%{})
else
e ->
conn
|> json(%{error: inspect(e)})
end
end
def login(conn, _) do
conn
|> render(MastodonView, "login.html", %{error: false})
end
defp get_or_make_app() do
with %App{} = app <- Repo.get_by(App, client_name: "Mastodon-Local") do
{:ok, app}
else
_e ->
cs =
App.register_changeset(%App{}, %{
client_name: "Mastodon-Local",
redirect_uris: ".",
scopes: "read,write,follow"
})
Repo.insert(cs)
end
end
def login_post(conn, %{"authorization" => %{"name" => name, "password" => password}}) do
with %User{} = user <- User.get_by_nickname_or_email(name),
true <- Pbkdf2.checkpw(password, user.password_hash),
{:ok, app} <- get_or_make_app(),
{:ok, auth} <- Authorization.create_authorization(app, user),
{:ok, token} <- Token.exchange_token(app, auth) do
conn
|> put_session(:oauth_token, token.token)
|> redirect(to: "/web/getting-started")
else
_e ->
conn
|> render(MastodonView, "login.html", %{error: "Wrong username or password"})
end
end
def logout(conn, _) do
conn
|> clear_session
|> redirect(to: "/")
end
def relationship_noop(%{assigns: %{user: user}} = conn, %{"id" => id}) do
Logger.debug("Unimplemented, returning unmodified relationship")
with %User{} = target <- Repo.get(User, id) do
render(conn, AccountView, "relationship.json", %{user: user, target: target})
end
end
def empty_array(conn, _) do
Logger.debug("Unimplemented, returning an empty array")
json(conn, [])
end
def empty_object(conn, _) do
Logger.debug("Unimplemented, returning an empty object")
json(conn, %{})
end
def render_notification(user, %{id: id, activity: activity, inserted_at: created_at} = _params) do
actor = User.get_cached_by_ap_id(activity.data["actor"])
created_at =
NaiveDateTime.to_iso8601(created_at)
|> String.replace(~r/(\.\d+)?$/, ".000Z", global: false)
case activity.data["type"] do
"Create" ->
%{
id: id,
type: "mention",
created_at: created_at,
account: AccountView.render("account.json", %{user: actor}),
status: StatusView.render("status.json", %{activity: activity, for: user})
}
"Like" ->
liked_activity = Activity.get_create_activity_by_object_ap_id(activity.data["object"])
%{
id: id,
type: "favourite",
created_at: created_at,
account: AccountView.render("account.json", %{user: actor}),
status: StatusView.render("status.json", %{activity: liked_activity, for: user})
}
"Announce" ->
announced_activity = Activity.get_create_activity_by_object_ap_id(activity.data["object"])
%{
id: id,
type: "reblog",
created_at: created_at,
account: AccountView.render("account.json", %{user: actor}),
status: StatusView.render("status.json", %{activity: announced_activity, for: user})
}
"Follow" ->
%{
id: id,
type: "follow",
created_at: created_at,
account: AccountView.render("account.json", %{user: actor})
}
_ ->
nil
end
end
def errors(conn, _) do
conn
|> put_status(500)
|> json("Something went wrong")
end
end
diff --git a/lib/pleroma/web/router.ex b/lib/pleroma/web/router.ex
index e517510b8..b37c8168f 100644
--- a/lib/pleroma/web/router.ex
+++ b/lib/pleroma/web/router.ex
@@ -1,358 +1,360 @@
defmodule Pleroma.Web.Router do
use Pleroma.Web, :router
alias Pleroma.{Repo, User, Web.Router}
@instance Application.get_env(:pleroma, :instance)
@federating Keyword.get(@instance, :federating)
@public Keyword.get(@instance, :public)
@registrations_open Keyword.get(@instance, :registrations_open)
def user_fetcher(username) do
{:ok, Repo.get_by(User, %{nickname: username})}
end
pipeline :api do
plug(:accepts, ["json"])
plug(:fetch_session)
plug(Pleroma.Plugs.OAuthPlug)
plug(Pleroma.Plugs.AuthenticationPlug, %{fetcher: &Router.user_fetcher/1, optional: true})
end
pipeline :authenticated_api do
plug(:accepts, ["json"])
plug(:fetch_session)
plug(Pleroma.Plugs.OAuthPlug)
plug(Pleroma.Plugs.AuthenticationPlug, %{fetcher: &Router.user_fetcher/1})
end
pipeline :mastodon_html do
plug(:accepts, ["html"])
plug(:fetch_session)
plug(Pleroma.Plugs.OAuthPlug)
plug(Pleroma.Plugs.AuthenticationPlug, %{fetcher: &Router.user_fetcher/1, optional: true})
end
pipeline :pleroma_html do
plug(:accepts, ["html"])
plug(:fetch_session)
plug(Pleroma.Plugs.OAuthPlug)
plug(Pleroma.Plugs.AuthenticationPlug, %{fetcher: &Router.user_fetcher/1, optional: true})
end
pipeline :well_known do
plug(:accepts, ["xml", "xrd+xml", "json", "jrd+json"])
end
pipeline :config do
plug(:accepts, ["json", "xml"])
end
pipeline :oauth do
plug(:accepts, ["html", "json"])
end
pipeline :pleroma_api do
plug(:accepts, ["html", "json"])
end
scope "/api/pleroma", Pleroma.Web.TwitterAPI do
pipe_through(:pleroma_api)
get("/password_reset/:token", UtilController, :show_password_reset)
post("/password_reset", UtilController, :password_reset)
get("/emoji", UtilController, :emoji)
end
scope "/", Pleroma.Web.TwitterAPI do
pipe_through(:pleroma_html)
get("/ostatus_subscribe", UtilController, :remote_follow)
post("/ostatus_subscribe", UtilController, :do_remote_follow)
post("/main/ostatus", UtilController, :remote_subscribe)
end
scope "/api/pleroma", Pleroma.Web.TwitterAPI do
pipe_through(:authenticated_api)
post("/follow_import", UtilController, :follow_import)
post("/change_password", UtilController, :change_password)
post("/delete_account", UtilController, :delete_account)
end
scope "/oauth", Pleroma.Web.OAuth do
get("/authorize", OAuthController, :authorize)
post("/authorize", OAuthController, :create_authorization)
post("/token", OAuthController, :token_exchange)
end
scope "/api/v1", Pleroma.Web.MastodonAPI do
pipe_through(:authenticated_api)
patch("/accounts/update_credentials", MastodonAPIController, :update_credentials)
get("/accounts/verify_credentials", MastodonAPIController, :verify_credentials)
get("/accounts/relationships", MastodonAPIController, :relationships)
get("/accounts/search", MastodonAPIController, :account_search)
post("/accounts/:id/follow", MastodonAPIController, :follow)
post("/accounts/:id/unfollow", MastodonAPIController, :unfollow)
post("/accounts/:id/block", MastodonAPIController, :block)
post("/accounts/:id/unblock", MastodonAPIController, :unblock)
post("/accounts/:id/mute", MastodonAPIController, :relationship_noop)
post("/accounts/:id/unmute", MastodonAPIController, :relationship_noop)
get("/follow_requests", MastodonAPIController, :follow_requests)
+ post("/follow_requests/:id/authorize", MastodonAPIController, :authorize_follow_request)
+ post("/follow_requests/:id/reject", MastodonAPIController, :reject_follow_request)
post("/follows", MastodonAPIController, :follow)
get("/blocks", MastodonAPIController, :blocks)
get("/domain_blocks", MastodonAPIController, :empty_array)
get("/mutes", MastodonAPIController, :empty_array)
get("/timelines/home", MastodonAPIController, :home_timeline)
get("/timelines/direct", MastodonAPIController, :dm_timeline)
get("/favourites", MastodonAPIController, :favourites)
post("/statuses", MastodonAPIController, :post_status)
delete("/statuses/:id", MastodonAPIController, :delete_status)
post("/statuses/:id/reblog", MastodonAPIController, :reblog_status)
post("/statuses/:id/unreblog", MastodonAPIController, :unreblog_status)
post("/statuses/:id/favourite", MastodonAPIController, :fav_status)
post("/statuses/:id/unfavourite", MastodonAPIController, :unfav_status)
post("/notifications/clear", MastodonAPIController, :clear_notifications)
post("/notifications/dismiss", MastodonAPIController, :dismiss_notification)
get("/notifications", MastodonAPIController, :notifications)
get("/notifications/:id", MastodonAPIController, :get_notification)
post("/media", MastodonAPIController, :upload)
get("/lists", MastodonAPIController, :get_lists)
get("/lists/:id", MastodonAPIController, :get_list)
delete("/lists/:id", MastodonAPIController, :delete_list)
post("/lists", MastodonAPIController, :create_list)
put("/lists/:id", MastodonAPIController, :rename_list)
get("/lists/:id/accounts", MastodonAPIController, :list_accounts)
post("/lists/:id/accounts", MastodonAPIController, :add_to_list)
delete("/lists/:id/accounts", MastodonAPIController, :remove_from_list)
get("/domain_blocks", MastodonAPIController, :domain_blocks)
post("/domain_blocks", MastodonAPIController, :block_domain)
delete("/domain_blocks", MastodonAPIController, :unblock_domain)
end
scope "/api/web", Pleroma.Web.MastodonAPI do
pipe_through(:authenticated_api)
put("/settings", MastodonAPIController, :put_settings)
end
scope "/api/v1", Pleroma.Web.MastodonAPI do
pipe_through(:api)
get("/instance", MastodonAPIController, :masto_instance)
get("/instance/peers", MastodonAPIController, :peers)
post("/apps", MastodonAPIController, :create_app)
get("/custom_emojis", MastodonAPIController, :custom_emojis)
get("/timelines/public", MastodonAPIController, :public_timeline)
get("/timelines/tag/:tag", MastodonAPIController, :hashtag_timeline)
get("/timelines/list/:list_id", MastodonAPIController, :list_timeline)
get("/statuses/:id", MastodonAPIController, :get_status)
get("/statuses/:id/context", MastodonAPIController, :get_context)
get("/statuses/:id/card", MastodonAPIController, :empty_object)
get("/statuses/:id/favourited_by", MastodonAPIController, :favourited_by)
get("/statuses/:id/reblogged_by", MastodonAPIController, :reblogged_by)
get("/accounts/:id/statuses", MastodonAPIController, :user_statuses)
get("/accounts/:id/followers", MastodonAPIController, :followers)
get("/accounts/:id/following", MastodonAPIController, :following)
get("/accounts/:id", MastodonAPIController, :user)
get("/search", MastodonAPIController, :search)
end
scope "/api", Pleroma.Web do
pipe_through(:config)
get("/help/test", TwitterAPI.UtilController, :help_test)
post("/help/test", TwitterAPI.UtilController, :help_test)
get("/statusnet/config", TwitterAPI.UtilController, :config)
get("/statusnet/version", TwitterAPI.UtilController, :version)
end
scope "/api", Pleroma.Web do
pipe_through(:api)
get("/statuses/user_timeline", TwitterAPI.Controller, :user_timeline)
get("/qvitter/statuses/user_timeline", TwitterAPI.Controller, :user_timeline)
get("/users/show", TwitterAPI.Controller, :show_user)
get("/statuses/followers", TwitterAPI.Controller, :followers)
get("/statuses/friends", TwitterAPI.Controller, :friends)
get("/statuses/show/:id", TwitterAPI.Controller, :fetch_status)
get("/statusnet/conversation/:id", TwitterAPI.Controller, :fetch_conversation)
if @registrations_open do
post("/account/register", TwitterAPI.Controller, :register)
end
get("/search", TwitterAPI.Controller, :search)
get("/statusnet/tags/timeline/:tag", TwitterAPI.Controller, :public_and_external_timeline)
end
scope "/api", Pleroma.Web do
if @public do
pipe_through(:api)
else
pipe_through(:authenticated_api)
end
get("/statuses/public_timeline", TwitterAPI.Controller, :public_timeline)
get(
"/statuses/public_and_external_timeline",
TwitterAPI.Controller,
:public_and_external_timeline
)
get("/statuses/networkpublic_timeline", TwitterAPI.Controller, :public_and_external_timeline)
end
scope "/api", Pleroma.Web do
pipe_through(:authenticated_api)
get("/account/verify_credentials", TwitterAPI.Controller, :verify_credentials)
post("/account/verify_credentials", TwitterAPI.Controller, :verify_credentials)
post("/account/update_profile", TwitterAPI.Controller, :update_profile)
post("/account/update_profile_banner", TwitterAPI.Controller, :update_banner)
post("/qvitter/update_background_image", TwitterAPI.Controller, :update_background)
post(
"/account/most_recent_notification",
TwitterAPI.Controller,
:update_most_recent_notification
)
get("/statuses/home_timeline", TwitterAPI.Controller, :friends_timeline)
get("/statuses/friends_timeline", TwitterAPI.Controller, :friends_timeline)
get("/statuses/mentions", TwitterAPI.Controller, :mentions_timeline)
get("/statuses/mentions_timeline", TwitterAPI.Controller, :mentions_timeline)
get("/qvitter/statuses/notifications", TwitterAPI.Controller, :notifications)
post("/statuses/update", TwitterAPI.Controller, :status_update)
post("/statuses/retweet/:id", TwitterAPI.Controller, :retweet)
post("/statuses/destroy/:id", TwitterAPI.Controller, :delete_post)
post("/friendships/create", TwitterAPI.Controller, :follow)
post("/friendships/destroy", TwitterAPI.Controller, :unfollow)
post("/blocks/create", TwitterAPI.Controller, :block)
post("/blocks/destroy", TwitterAPI.Controller, :unblock)
post("/statusnet/media/upload", TwitterAPI.Controller, :upload)
post("/media/upload", TwitterAPI.Controller, :upload_json)
post("/favorites/create/:id", TwitterAPI.Controller, :favorite)
post("/favorites/create", TwitterAPI.Controller, :favorite)
post("/favorites/destroy/:id", TwitterAPI.Controller, :unfavorite)
post("/qvitter/update_avatar", TwitterAPI.Controller, :update_avatar)
get("/friends/ids", TwitterAPI.Controller, :friends_ids)
get("/friendships/no_retweets/ids", TwitterAPI.Controller, :empty_array)
get("/mutes/users/ids", TwitterAPI.Controller, :empty_array)
get("/externalprofile/show", TwitterAPI.Controller, :external_profile)
end
pipeline :ostatus do
plug(:accepts, ["xml", "atom", "html", "activity+json"])
end
scope "/", Pleroma.Web do
pipe_through(:ostatus)
get("/objects/:uuid", OStatus.OStatusController, :object)
get("/activities/:uuid", OStatus.OStatusController, :activity)
get("/notice/:id", OStatus.OStatusController, :notice)
get("/users/:nickname/feed", OStatus.OStatusController, :feed)
get("/users/:nickname", OStatus.OStatusController, :feed_redirect)
if @federating do
post("/users/:nickname/salmon", OStatus.OStatusController, :salmon_incoming)
post("/push/hub/:nickname", Websub.WebsubController, :websub_subscription_request)
get("/push/subscriptions/:id", Websub.WebsubController, :websub_subscription_confirmation)
post("/push/subscriptions/:id", Websub.WebsubController, :websub_incoming)
end
end
pipeline :activitypub do
plug(:accepts, ["activity+json"])
plug(Pleroma.Web.Plugs.HTTPSignaturePlug)
end
scope "/", Pleroma.Web.ActivityPub do
# XXX: not really ostatus
pipe_through(:ostatus)
get("/users/:nickname/followers", ActivityPubController, :followers)
get("/users/:nickname/following", ActivityPubController, :following)
get("/users/:nickname/outbox", ActivityPubController, :outbox)
end
if @federating do
scope "/", Pleroma.Web.ActivityPub do
pipe_through(:activitypub)
post("/users/:nickname/inbox", ActivityPubController, :inbox)
post("/inbox", ActivityPubController, :inbox)
end
scope "/.well-known", Pleroma.Web do
pipe_through(:well_known)
get("/host-meta", WebFinger.WebFingerController, :host_meta)
get("/webfinger", WebFinger.WebFingerController, :webfinger)
get("/nodeinfo", Nodeinfo.NodeinfoController, :schemas)
end
scope "/nodeinfo", Pleroma.Web do
get("/:version", Nodeinfo.NodeinfoController, :nodeinfo)
end
end
scope "/", Pleroma.Web.MastodonAPI do
pipe_through(:mastodon_html)
get("/web/login", MastodonAPIController, :login)
post("/web/login", MastodonAPIController, :login_post)
get("/web/*path", MastodonAPIController, :index)
delete("/auth/sign_out", MastodonAPIController, :logout)
end
pipeline :remote_media do
plug(:accepts, ["html"])
end
scope "/proxy/", Pleroma.Web.MediaProxy do
pipe_through(:remote_media)
get("/:sig/:url", MediaProxyController, :remote)
end
scope "/", Fallback do
get("/*path", RedirectController, :redirector)
end
end
defmodule Fallback.RedirectController do
use Pleroma.Web, :controller
def redirector(conn, _params) do
if Mix.env() != :test do
conn
|> put_resp_content_type("text/html")
|> send_file(200, "priv/static/index.html")
end
end
end
File Metadata
Details
Attached
Mime Type
text/x-diff
Expires
Sun, Nov 1, 6:15 PM (11 h, 38 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
1803240
Default Alt Text
(77 KB)
Attached To
Mode
rPUBE pleroma-upstream
Attached
Detach File
Event Timeline