defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
use Pleroma.Web, :controller
- alias Pleroma.{Repo, Activity, User, Notification, Stats}
+ alias Pleroma.{Repo, Object, Activity, User, Notification, Stats}
alias Pleroma.Web
- alias Pleroma.Web.MastodonAPI.{StatusView, AccountView, MastodonView, ListView}
+ alias Pleroma.Web.MastodonAPI.{StatusView, AccountView, MastodonView, ListView, FilterView}
alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.Web.ActivityPub.Utils
- alias Pleroma.Web.{CommonAPI, OStatus}
+ alias Pleroma.Web.CommonAPI
alias Pleroma.Web.OAuth.{Authorization, Token, App}
+ alias Pleroma.Web.MediaProxy
alias Comeonin.Pbkdf2
import Ecto.Query
require Logger
+ @httpoison Application.get_env(:pleroma, :httpoison)
+
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,
+ id: app.id |> to_string,
+ name: app.client_name,
client_id: app.client_id,
- client_secret: app.client_secret
+ client_secret: app.client_secret,
+ redirect_uri: app.redirect_uris,
+ website: app.website
}
json(conn, res)
def update_credentials(%{assigns: %{user: user}} = conn, params) do
original_user = user
+ avatar_upload_limit =
+ Application.get_env(:pleroma, :instance)
+ |> Keyword.fetch(:avatar_upload_limit)
+
+ banner_upload_limit =
+ Application.get_env(:pleroma, :instance)
+ |> Keyword.fetch(:banner_upload_limit)
+
params =
if bio = params["note"] do
Map.put(params, "bio", bio)
user =
if avatar = params["avatar"] do
with %Plug.Upload{} <- avatar,
- {:ok, object} <- ActivityPub.upload(avatar),
+ {:ok, object} <- ActivityPub.upload(avatar, avatar_upload_limit),
change = Ecto.Changeset.change(user, %{avatar: object.data}),
{:ok, user} = User.update_and_set_cache(change) do
user
user =
if banner = params["header"] do
with %Plug.Upload{} <- banner,
- {:ok, object} <- ActivityPub.upload(banner),
+ {:ok, object} <- ActivityPub.upload(banner, banner_upload_limit),
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
CommonAPI.update(user)
end
- json(conn, AccountView.render("account.json", %{user: user}))
+ json(conn, AccountView.render("account.json", %{user: user, for: user}))
else
_e ->
conn
end
def verify_credentials(%{assigns: %{user: user}} = conn, _) do
- account = AccountView.render("account.json", %{user: user})
+ account = AccountView.render("account.json", %{user: user, for: user})
json(conn, account)
end
- def user(conn, %{"id" => id}) do
+ def user(%{assigns: %{user: for_user}} = conn, %{"id" => id}) do
with %User{} = user <- Repo.get(User, id) do
- account = AccountView.render("account.json", %{user: user})
+ account = AccountView.render("account.json", %{user: user, for: for_user})
json(conn, account)
else
_e ->
end
@instance Application.get_env(:pleroma, :instance)
- @mastodon_api_level "2.3.3"
+ @mastodon_api_level "2.5.0"
def masto_instance(conn, _params) do
response = %{
uri: Web.base_url(),
title: Keyword.get(@instance, :name),
- description: "A Pleroma instance, an alternative fediverse server",
+ description: Keyword.get(@instance, :description),
version: "#{@mastodon_api_level} (compatible; #{Keyword.get(@instance, :version)})",
email: Keyword.get(@instance, :email),
urls: %{
end
defp mastodonized_emoji do
- Pleroma.Formatter.get_custom_emoji()
+ Pleroma.Emoji.get_all()
|> Enum.map(fn {shortcode, relative_url} ->
url = to_string(URI.merge(Web.base_url(), relative_url))
activities =
ActivityPub.fetch_activities([user.ap_id | user.following], params)
+ |> ActivityPub.contain_timeline(user)
|> Enum.reverse()
conn
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})
+ try_render(conn, StatusView, "status.json", %{activity: activity, for: user})
end
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})
+ try_render(conn, StatusView, "status.json", %{activity: activity, for: user, as: :activity})
end
def delete_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
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})
+ try_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, _unannounce, %{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})
+ try_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})
+ try_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})
+ try_render(conn, StatusView, "status.json", %{activity: activity, for: user, as: :activity})
end
end
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
+ def update_media(%{assigns: %{user: _}} = conn, data) do
+ with %Object{} = object <- Repo.get(Object, data["id"]),
+ true <- is_binary(data["description"]),
+ description <- data["description"] do
+ new_data = %{object.data | "name" => description}
+
+ change = Object.change(object, %{data: new_data})
+ {:ok, _} = Repo.update(change)
+
data =
- object.data
+ new_data
|> Map.put("id", object.id)
render(conn, StatusView, "attachment.json", %{attachment: data})
end
end
+ def upload(%{assigns: %{user: _}} = conn, %{"file" => file} = data) do
+ with {:ok, object} <- ActivityPub.upload(file) do
+ objdata =
+ if Map.has_key?(data, "description") do
+ Map.put(object.data, "name", data["description"])
+ else
+ object.data
+ end
+
+ change = Object.change(object, %{data: objdata})
+ {:ok, object} = Repo.update(change)
+
+ objdata =
+ objdata
+ |> Map.put("id", object.id)
+
+ render(conn, StatusView, "attachment.json", %{attachment: objdata})
+ 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)
|> Map.put("type", "Create")
|> Map.put("local_only", local_only)
|> Map.put("blocking_user", user)
+ |> Map.put("tag", String.downcase(params["tag"]))
activities =
ActivityPub.fetch_public_activities(params)
end
end
+ @activitypub Application.get_env(:pleroma, :activitypub)
+ @follow_handshake_timeout Keyword.get(@activitypub, :follow_handshake_timeout)
+
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
+ {:ok, _activity} <- ActivityPub.follow(follower, followed),
+ {:ok, follower, followed} <-
+ User.wait_and_refresh(@follow_handshake_timeout, follower, followed) do
render(conn, AccountView, "relationship.json", %{user: follower, target: followed})
else
{:error, message} ->
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})
+ render(conn, AccountView, "account.json", %{user: followed, for: follower})
else
{:error, message} ->
conn
json(conn, %{})
end
- def search2(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do
- accounts = User.search(query, params["resolve"] == "true")
-
+ def status_search(query) do
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)
+ with {:ok, object} <- ActivityPub.fetch_object_from_id(query) do
+ [Activity.get_create_activity_by_object_ap_id(object.data["id"])]
else
_e -> []
end
order_by: [desc: :id]
)
- statuses = Repo.all(q) ++ fetched
+ Repo.all(q) ++ fetched
+ end
+
+ def search2(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do
+ accounts = User.search(query, params["resolve"] == "true")
+
+ statuses = status_search(query)
tags_path = Web.base_url() <> "/tag/"
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
+ statuses = status_search(query)
tags =
String.split(query)
end
end
+ def account_lists(%{assigns: %{user: user}} = conn, %{"id" => account_id}) do
+ lists = Pleroma.List.get_lists_account_belongs(user, account_id)
+ res = ListView.render("lists.json", lists: lists)
+ json(conn, res)
+ 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
|> Map.put("type", "Create")
|> Map.put("blocking_user", user)
- # adding title is a hack to not make empty lists function like a public timeline
+ # we must filter the following list for the user to avoid leaking statuses the user
+ # does not actually have permission to see (for more info, peruse security issue #270).
+ following_to =
+ following
+ |> Enum.filter(fn x -> x in user.following end)
+
activities =
- ActivityPub.fetch_activities([title | following], params)
+ ActivityPub.fetch_activities_bounded(following_to, following, params)
|> Enum.reverse()
conn
if user && token do
mastodon_emoji = mastodonized_emoji()
- accounts = Map.put(%{}, user.id, AccountView.render("account.json", %{user: user}))
+
+ accounts =
+ Map.put(%{}, user.id, AccountView.render("account.json", %{user: user, for: user}))
initial_state =
%{
NaiveDateTime.to_iso8601(created_at)
|> String.replace(~r/(\.\d+)?$/, ".000Z", global: false)
+ id = id |> to_string
+
case activity.data["type"] do
"Create" ->
%{
id: id,
type: "mention",
created_at: created_at,
- account: AccountView.render("account.json", %{user: actor}),
+ account: AccountView.render("account.json", %{user: actor, for: user}),
status: StatusView.render("status.json", %{activity: activity, for: user})
}
id: id,
type: "favourite",
created_at: created_at,
- account: AccountView.render("account.json", %{user: actor}),
+ account: AccountView.render("account.json", %{user: actor, for: user}),
status: StatusView.render("status.json", %{activity: liked_activity, for: user})
}
id: id,
type: "reblog",
created_at: created_at,
- account: AccountView.render("account.json", %{user: actor}),
+ account: AccountView.render("account.json", %{user: actor, for: user}),
status: StatusView.render("status.json", %{activity: announced_activity, for: user})
}
id: id,
type: "follow",
created_at: created_at,
- account: AccountView.render("account.json", %{user: actor})
+ account: AccountView.render("account.json", %{user: actor, for: user})
}
_ ->
end
end
+ def get_filters(%{assigns: %{user: user}} = conn, _) do
+ filters = Pleroma.Filter.get_filters(user)
+ res = FilterView.render("filters.json", filters: filters)
+ json(conn, res)
+ end
+
+ def create_filter(
+ %{assigns: %{user: user}} = conn,
+ %{"phrase" => phrase, "context" => context} = params
+ ) do
+ query = %Pleroma.Filter{
+ user_id: user.id,
+ phrase: phrase,
+ context: context,
+ hide: Map.get(params, "irreversible", nil),
+ whole_word: Map.get(params, "boolean", true)
+ # expires_at
+ }
+
+ {:ok, response} = Pleroma.Filter.create(query)
+ res = FilterView.render("filter.json", filter: response)
+ json(conn, res)
+ end
+
+ def get_filter(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do
+ filter = Pleroma.Filter.get(filter_id, user)
+ res = FilterView.render("filter.json", filter: filter)
+ json(conn, res)
+ end
+
+ def update_filter(
+ %{assigns: %{user: user}} = conn,
+ %{"phrase" => phrase, "context" => context, "id" => filter_id} = params
+ ) do
+ query = %Pleroma.Filter{
+ user_id: user.id,
+ filter_id: filter_id,
+ phrase: phrase,
+ context: context,
+ hide: Map.get(params, "irreversible", nil),
+ whole_word: Map.get(params, "boolean", true)
+ # expires_at
+ }
+
+ {:ok, response} = Pleroma.Filter.update(query)
+ res = FilterView.render("filter.json", filter: response)
+ json(conn, res)
+ end
+
+ def delete_filter(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do
+ query = %Pleroma.Filter{
+ user_id: user.id,
+ filter_id: filter_id
+ }
+
+ {:ok, _} = Pleroma.Filter.delete(query)
+ json(conn, %{})
+ end
+
def errors(conn, _) do
conn
|> put_status(500)
|> json("Something went wrong")
end
+ @suggestions Application.get_env(:pleroma, :suggestions)
+
def suggestions(%{assigns: %{user: user}} = conn, _) do
- res = %{
- host: (String.replace Web.base_url(), "https://", ""),
- user: user.nickname
- }
+ if Keyword.get(@suggestions, :enabled, false) do
+ api = Keyword.get(@suggestions, :third_party_engine, "")
+ timeout = Keyword.get(@suggestions, :timeout, 5000)
+ limit = Keyword.get(@suggestions, :limit, 23)
+
+ host =
+ Application.get_env(:pleroma, Pleroma.Web.Endpoint)
+ |> Keyword.get(:url)
+ |> Keyword.get(:host)
+
+ user = user.nickname
+ url = String.replace(api, "{{host}}", host) |> String.replace("{{user}}", user)
+
+ with {:ok, %{status_code: 200, body: body}} <-
+ @httpoison.get(url, [], timeout: timeout, recv_timeout: timeout),
+ {:ok, data} <- Jason.decode(body) do
+ data2 =
+ Enum.slice(data, 0, limit)
+ |> Enum.map(fn x ->
+ Map.put(
+ x,
+ "id",
+ case User.get_or_fetch(x["acct"]) do
+ %{id: id} -> id
+ _ -> 0
+ end
+ )
+ end)
+ |> Enum.map(fn x ->
+ Map.put(x, "avatar", MediaProxy.url(x["avatar"]))
+ end)
+ |> Enum.map(fn x ->
+ Map.put(x, "avatar_static", MediaProxy.url(x["avatar_static"]))
+ end)
+
+ conn
+ |> json(data2)
+ else
+ e -> Logger.error("Could not retrieve suggestions at fetch #{url}, #{inspect(e)}")
+ end
+ else
+ json(conn, [])
+ end
+ end
+
+ def try_render(conn, renderer, target, params)
+ when is_binary(target) do
+ res = render(conn, renderer, target, params)
+
+ if res == nil do
+ conn
+ |> put_status(501)
+ |> json(%{error: "Can't display this activity"})
+ else
+ res
+ end
+ end
+
+ def try_render(conn, _, _, _) do
conn
- |> json(res)
+ |> put_status(501)
+ |> json(%{error: "Can't display this activity"})
end
end