aboutsummaryrefslogtreecommitdiff
path: root/lib/pleroma/web/mastodon_api
diff options
context:
space:
mode:
authorkPherox <admin@mail.kr-kp.com>2019-10-01 01:39:22 +0900
committerkPherox <admin@mail.kr-kp.com>2019-10-01 01:40:33 +0900
commita0f101ee806af06bcd4271cd8d57d11ff85ea11a (patch)
tree2cd3c5d1a645be1395c57ec5616f31afb4b68da7 /lib/pleroma/web/mastodon_api
parent8ca4f145a51e92c9f3a6c374ceddfac22ea300d9 (diff)
parent7c9b023a918c84b60ae6547289a083c671a3659b (diff)
downloadpleroma-a0f101ee806af06bcd4271cd8d57d11ff85ea11a.tar.gz
Merge remote-tracking branch 'upstream/develop' into fix-prameter-name-of-accounts-update-credentials
Diffstat (limited to 'lib/pleroma/web/mastodon_api')
-rw-r--r--lib/pleroma/web/mastodon_api/controllers/account_controller.ex323
-rw-r--r--lib/pleroma/web/mastodon_api/controllers/conversation_controller.ex32
-rw-r--r--lib/pleroma/web/mastodon_api/controllers/domain_block_controller.ex26
-rw-r--r--lib/pleroma/web/mastodon_api/controllers/fallback_controller.ex34
-rw-r--r--lib/pleroma/web/mastodon_api/controllers/filter_controller.ex72
-rw-r--r--lib/pleroma/web/mastodon_api/controllers/follow_request_controller.ex49
-rw-r--r--lib/pleroma/web/mastodon_api/controllers/list_controller.ex84
-rw-r--r--lib/pleroma/web/mastodon_api/controllers/mastodon_api_controller.ex529
-rw-r--r--lib/pleroma/web/mastodon_api/controllers/notification_controller.ex57
-rw-r--r--lib/pleroma/web/mastodon_api/controllers/report_controller.ex16
-rw-r--r--lib/pleroma/web/mastodon_api/controllers/scheduled_activity_controller.ex51
-rw-r--r--lib/pleroma/web/mastodon_api/controllers/search_controller.ex (renamed from lib/pleroma/web/mastodon_api/search_controller.ex)7
-rw-r--r--lib/pleroma/web/mastodon_api/controllers/status_controller.ex274
-rw-r--r--lib/pleroma/web/mastodon_api/controllers/subscription_controller.ex (renamed from lib/pleroma/web/mastodon_api/subscription_controller.ex)4
-rw-r--r--lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex136
-rw-r--r--lib/pleroma/web/mastodon_api/mastodon_api_controller.ex1824
-rw-r--r--lib/pleroma/web/mastodon_api/views/account_view.ex46
-rw-r--r--lib/pleroma/web/mastodon_api/views/conversation_view.ex21
-rw-r--r--lib/pleroma/web/mastodon_api/views/list_view.ex6
-rw-r--r--lib/pleroma/web/mastodon_api/views/notification_view.ex10
-rw-r--r--lib/pleroma/web/mastodon_api/views/report_view.ex2
-rw-r--r--lib/pleroma/web/mastodon_api/views/scheduled_activity_view.ex23
-rw-r--r--lib/pleroma/web/mastodon_api/views/status_view.ex112
-rw-r--r--lib/pleroma/web/mastodon_api/websocket_handler.ex7
24 files changed, 1834 insertions, 1911 deletions
diff --git a/lib/pleroma/web/mastodon_api/controllers/account_controller.ex b/lib/pleroma/web/mastodon_api/controllers/account_controller.ex
new file mode 100644
index 000000000..5b99605eb
--- /dev/null
+++ b/lib/pleroma/web/mastodon_api/controllers/account_controller.ex
@@ -0,0 +1,323 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.MastodonAPI.AccountController do
+ use Pleroma.Web, :controller
+
+ import Pleroma.Web.ControllerHelper,
+ only: [add_link_headers: 2, truthy_param?: 1, assign_account_by_id: 2, json_response: 3]
+
+ alias Pleroma.Emoji
+ alias Pleroma.Plugs.RateLimiter
+ alias Pleroma.User
+ alias Pleroma.Web.ActivityPub.ActivityPub
+ alias Pleroma.Web.CommonAPI
+ alias Pleroma.Web.MastodonAPI.ListView
+ alias Pleroma.Web.MastodonAPI.MastodonAPI
+ alias Pleroma.Web.MastodonAPI.StatusView
+ alias Pleroma.Web.OAuth.Token
+ alias Pleroma.Web.TwitterAPI.TwitterAPI
+
+ @relations [:follow, :unfollow]
+ @needs_account ~W(followers following lists follow unfollow mute unmute block unblock)a
+
+ plug(RateLimiter, {:relations_id_action, params: ["id", "uri"]} when action in @relations)
+ plug(RateLimiter, :relations_actions when action in @relations)
+ plug(RateLimiter, :app_account_creation when action == :create)
+ plug(:assign_account_by_id when action in @needs_account)
+
+ action_fallback(Pleroma.Web.MastodonAPI.FallbackController)
+
+ @doc "POST /api/v1/accounts"
+ def create(
+ %{assigns: %{app: app}} = conn,
+ %{"username" => nickname, "email" => _, "password" => _, "agreement" => true} = params
+ ) do
+ params =
+ params
+ |> Map.take([
+ "email",
+ "captcha_solution",
+ "captcha_token",
+ "captcha_answer_data",
+ "token",
+ "password"
+ ])
+ |> Map.put("nickname", nickname)
+ |> Map.put("fullname", params["fullname"] || nickname)
+ |> Map.put("bio", params["bio"] || "")
+ |> Map.put("confirm", params["password"])
+
+ with {:ok, user} <- TwitterAPI.register_user(params, need_confirmation: true),
+ {:ok, token} <- Token.create_token(app, user, %{scopes: app.scopes}) do
+ json(conn, %{
+ token_type: "Bearer",
+ access_token: token.token,
+ scope: app.scopes,
+ created_at: Token.Utils.format_created_at(token)
+ })
+ else
+ {:error, errors} -> json_response(conn, :bad_request, errors)
+ end
+ end
+
+ def create(%{assigns: %{app: _app}} = conn, _) do
+ render_error(conn, :bad_request, "Missing parameters")
+ end
+
+ def create(conn, _) do
+ render_error(conn, :forbidden, "Invalid credentials")
+ end
+
+ @doc "GET /api/v1/accounts/verify_credentials"
+ def verify_credentials(%{assigns: %{user: user}} = conn, _) do
+ chat_token = Phoenix.Token.sign(conn, "user socket", user.id)
+
+ render(conn, "show.json",
+ user: user,
+ for: user,
+ with_pleroma_settings: true,
+ with_chat_token: chat_token
+ )
+ end
+
+ @doc "PATCH /api/v1/accounts/update_credentials"
+ def update_credentials(%{assigns: %{user: original_user}} = conn, params) do
+ user = original_user
+
+ user_params =
+ %{}
+ |> add_if_present(params, "display_name", :name)
+ |> add_if_present(params, "note", :bio, fn value -> {:ok, User.parse_bio(value, user)} end)
+ |> add_if_present(params, "avatar", :avatar, fn value ->
+ with %Plug.Upload{} <- value,
+ {:ok, object} <- ActivityPub.upload(value, type: :avatar) do
+ {:ok, object.data}
+ end
+ end)
+
+ emojis_text = (user_params["display_name"] || "") <> (user_params["note"] || "")
+
+ user_info_emojis =
+ user.info
+ |> Map.get(:emoji, [])
+ |> Enum.concat(Emoji.Formatter.get_emoji_map(emojis_text))
+ |> Enum.dedup()
+
+ params =
+ if Map.has_key?(params, "fields_attributes") do
+ Map.update!(params, "fields_attributes", fn fields ->
+ fields
+ |> normalize_fields_attributes()
+ |> Enum.filter(fn %{"name" => n} -> n != "" end)
+ end)
+ else
+ params
+ end
+
+ info_params =
+ [
+ :no_rich_text,
+ :locked,
+ :hide_followers_count,
+ :hide_follows_count,
+ :hide_followers,
+ :hide_follows,
+ :hide_favorites,
+ :show_role,
+ :skip_thread_containment,
+ :discoverable
+ ]
+ |> Enum.reduce(%{}, fn key, acc ->
+ add_if_present(acc, params, to_string(key), key, &{:ok, truthy_param?(&1)})
+ end)
+ |> add_if_present(params, "default_scope", :default_scope)
+ |> add_if_present(params, "fields_attributes", :fields, fn fields ->
+ fields = Enum.map(fields, fn f -> Map.update!(f, "value", &AutoLinker.link(&1)) end)
+
+ {:ok, fields}
+ end)
+ |> add_if_present(params, "fields_attributes", :raw_fields)
+ |> add_if_present(params, "pleroma_settings_store", :pleroma_settings_store, fn value ->
+ {:ok, Map.merge(user.info.pleroma_settings_store, value)}
+ end)
+ |> add_if_present(params, "header", :banner, fn value ->
+ with %Plug.Upload{} <- value,
+ {:ok, object} <- ActivityPub.upload(value, type: :banner) do
+ {:ok, object.data}
+ end
+ end)
+ |> add_if_present(params, "pleroma_background_image", :background, fn value ->
+ with %Plug.Upload{} <- value,
+ {:ok, object} <- ActivityPub.upload(value, type: :background) do
+ {:ok, object.data}
+ end
+ end)
+ |> Map.put(:emoji, user_info_emojis)
+
+ changeset =
+ user
+ |> User.update_changeset(user_params)
+ |> User.change_info(&User.Info.profile_update(&1, info_params))
+
+ with {:ok, user} <- User.update_and_set_cache(changeset) do
+ if original_user != user, do: CommonAPI.update(user)
+
+ render(conn, "show.json", user: user, for: user, with_pleroma_settings: true)
+ else
+ _e -> render_error(conn, :forbidden, "Invalid request")
+ end
+ end
+
+ defp add_if_present(map, params, params_field, map_field, value_function \\ &{:ok, &1}) do
+ with true <- Map.has_key?(params, params_field),
+ {:ok, new_value} <- value_function.(params[params_field]) do
+ Map.put(map, map_field, new_value)
+ else
+ _ -> map
+ end
+ end
+
+ defp normalize_fields_attributes(fields) do
+ if Enum.all?(fields, &is_tuple/1) do
+ Enum.map(fields, fn {_, v} -> v end)
+ else
+ fields
+ end
+ end
+
+ @doc "GET /api/v1/accounts/relationships"
+ def relationships(%{assigns: %{user: user}} = conn, %{"id" => id}) do
+ targets = User.get_all_by_ids(List.wrap(id))
+
+ render(conn, "relationships.json", user: user, targets: targets)
+ end
+
+ # Instead of returning a 400 when no "id" params is present, Mastodon returns an empty array.
+ def relationships(%{assigns: %{user: _user}} = conn, _), do: json(conn, [])
+
+ @doc "GET /api/v1/accounts/:id"
+ def show(%{assigns: %{user: for_user}} = conn, %{"id" => nickname_or_id}) do
+ with %User{} = user <- User.get_cached_by_nickname_or_id(nickname_or_id, for: for_user),
+ true <- User.auth_active?(user) || user.id == for_user.id || User.superuser?(for_user) do
+ render(conn, "show.json", user: user, for: for_user)
+ else
+ _e -> render_error(conn, :not_found, "Can't find user")
+ end
+ end
+
+ @doc "GET /api/v1/accounts/:id/statuses"
+ def statuses(%{assigns: %{user: reading_user}} = conn, params) do
+ with %User{} = user <- User.get_cached_by_nickname_or_id(params["id"], for: reading_user) do
+ params = Map.put(params, "tag", params["tagged"])
+ activities = ActivityPub.fetch_user_activities(user, reading_user, params)
+
+ conn
+ |> add_link_headers(activities)
+ |> put_view(StatusView)
+ |> render("index.json", activities: activities, for: reading_user, as: :activity)
+ end
+ end
+
+ @doc "GET /api/v1/accounts/:id/followers"
+ def followers(%{assigns: %{user: for_user, account: user}} = conn, params) do
+ followers =
+ cond do
+ for_user && user.id == for_user.id -> MastodonAPI.get_followers(user, params)
+ user.info.hide_followers -> []
+ true -> MastodonAPI.get_followers(user, params)
+ end
+
+ conn
+ |> add_link_headers(followers)
+ |> render("index.json", for: for_user, users: followers, as: :user)
+ end
+
+ @doc "GET /api/v1/accounts/:id/following"
+ def following(%{assigns: %{user: for_user, account: user}} = conn, params) do
+ followers =
+ cond do
+ for_user && user.id == for_user.id -> MastodonAPI.get_friends(user, params)
+ user.info.hide_follows -> []
+ true -> MastodonAPI.get_friends(user, params)
+ end
+
+ conn
+ |> add_link_headers(followers)
+ |> render("index.json", for: for_user, users: followers, as: :user)
+ end
+
+ @doc "GET /api/v1/accounts/:id/lists"
+ def lists(%{assigns: %{user: user, account: account}} = conn, _params) do
+ lists = Pleroma.List.get_lists_account_belongs(user, account)
+
+ conn
+ |> put_view(ListView)
+ |> render("index.json", lists: lists)
+ end
+
+ @doc "POST /api/v1/accounts/:id/follow"
+ def follow(%{assigns: %{user: %{id: id}, account: %{id: id}}}, _params) do
+ {:error, :not_found}
+ end
+
+ def follow(%{assigns: %{user: follower, account: followed}} = conn, _params) do
+ with {:ok, follower} <- MastodonAPI.follow(follower, followed, conn.params) do
+ render(conn, "relationship.json", user: follower, target: followed)
+ else
+ {:error, message} -> json_response(conn, :forbidden, %{error: message})
+ end
+ end
+
+ @doc "POST /api/v1/accounts/:id/unfollow"
+ def unfollow(%{assigns: %{user: %{id: id}, account: %{id: id}}}, _params) do
+ {:error, :not_found}
+ end
+
+ def unfollow(%{assigns: %{user: follower, account: followed}} = conn, _params) do
+ with {:ok, follower} <- CommonAPI.unfollow(follower, followed) do
+ render(conn, "relationship.json", user: follower, target: followed)
+ end
+ end
+
+ @doc "POST /api/v1/accounts/:id/mute"
+ def mute(%{assigns: %{user: muter, account: muted}} = conn, params) do
+ notifications? = params |> Map.get("notifications", true) |> truthy_param?()
+
+ with {:ok, muter} <- User.mute(muter, muted, notifications?) do
+ render(conn, "relationship.json", user: muter, target: muted)
+ else
+ {:error, message} -> json_response(conn, :forbidden, %{error: message})
+ end
+ end
+
+ @doc "POST /api/v1/accounts/:id/unmute"
+ def unmute(%{assigns: %{user: muter, account: muted}} = conn, _params) do
+ with {:ok, muter} <- User.unmute(muter, muted) do
+ render(conn, "relationship.json", user: muter, target: muted)
+ else
+ {:error, message} -> json_response(conn, :forbidden, %{error: message})
+ end
+ end
+
+ @doc "POST /api/v1/accounts/:id/block"
+ def block(%{assigns: %{user: blocker, account: blocked}} = conn, _params) do
+ with {:ok, blocker} <- User.block(blocker, blocked),
+ {:ok, _activity} <- ActivityPub.block(blocker, blocked) do
+ render(conn, "relationship.json", user: blocker, target: blocked)
+ else
+ {:error, message} -> json_response(conn, :forbidden, %{error: message})
+ end
+ end
+
+ @doc "POST /api/v1/accounts/:id/unblock"
+ def unblock(%{assigns: %{user: blocker, account: blocked}} = conn, _params) do
+ with {:ok, blocker} <- User.unblock(blocker, blocked),
+ {:ok, _activity} <- ActivityPub.unblock(blocker, blocked) do
+ render(conn, "relationship.json", user: blocker, target: blocked)
+ else
+ {:error, message} -> json_response(conn, :forbidden, %{error: message})
+ end
+ end
+end
diff --git a/lib/pleroma/web/mastodon_api/controllers/conversation_controller.ex b/lib/pleroma/web/mastodon_api/controllers/conversation_controller.ex
new file mode 100644
index 000000000..ea1e36a12
--- /dev/null
+++ b/lib/pleroma/web/mastodon_api/controllers/conversation_controller.ex
@@ -0,0 +1,32 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.MastodonAPI.ConversationController do
+ use Pleroma.Web, :controller
+
+ import Pleroma.Web.ControllerHelper, only: [add_link_headers: 2]
+
+ alias Pleroma.Conversation.Participation
+ alias Pleroma.Repo
+
+ action_fallback(Pleroma.Web.MastodonAPI.FallbackController)
+
+ @doc "GET /api/v1/conversations"
+ def index(%{assigns: %{user: user}} = conn, params) do
+ participations = Participation.for_user_with_last_activity_id(user, params)
+
+ conn
+ |> add_link_headers(participations)
+ |> render("participations.json", participations: participations, for: user)
+ end
+
+ @doc "POST /api/v1/conversations/:id/read"
+ def read(%{assigns: %{user: user}} = conn, %{"id" => participation_id}) do
+ with %Participation{} = participation <-
+ Repo.get_by(Participation, id: participation_id, user_id: user.id),
+ {:ok, participation} <- Participation.mark_as_read(participation) do
+ render(conn, "participation.json", participation: participation, for: user)
+ end
+ end
+end
diff --git a/lib/pleroma/web/mastodon_api/controllers/domain_block_controller.ex b/lib/pleroma/web/mastodon_api/controllers/domain_block_controller.ex
new file mode 100644
index 000000000..03db6c9b8
--- /dev/null
+++ b/lib/pleroma/web/mastodon_api/controllers/domain_block_controller.ex
@@ -0,0 +1,26 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.MastodonAPI.DomainBlockController do
+ use Pleroma.Web, :controller
+
+ alias Pleroma.User
+
+ @doc "GET /api/v1/domain_blocks"
+ def index(%{assigns: %{user: %{info: info}}} = conn, _) do
+ json(conn, Map.get(info, :domain_blocks, []))
+ end
+
+ @doc "POST /api/v1/domain_blocks"
+ def create(%{assigns: %{user: blocker}} = conn, %{"domain" => domain}) do
+ User.block_domain(blocker, domain)
+ json(conn, %{})
+ end
+
+ @doc "DELETE /api/v1/domain_blocks"
+ def delete(%{assigns: %{user: blocker}} = conn, %{"domain" => domain}) do
+ User.unblock_domain(blocker, domain)
+ json(conn, %{})
+ end
+end
diff --git a/lib/pleroma/web/mastodon_api/controllers/fallback_controller.ex b/lib/pleroma/web/mastodon_api/controllers/fallback_controller.ex
new file mode 100644
index 000000000..41243d5e7
--- /dev/null
+++ b/lib/pleroma/web/mastodon_api/controllers/fallback_controller.ex
@@ -0,0 +1,34 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.MastodonAPI.FallbackController do
+ use Pleroma.Web, :controller
+
+ def call(conn, {:error, %Ecto.Changeset{} = changeset}) do
+ error_message =
+ changeset
+ |> Ecto.Changeset.traverse_errors(fn {message, _opt} -> message end)
+ |> Enum.map_join(", ", fn {_k, v} -> v end)
+
+ conn
+ |> put_status(:unprocessable_entity)
+ |> json(%{error: error_message})
+ end
+
+ def call(conn, {:error, :not_found}) do
+ render_error(conn, :not_found, "Record not found")
+ end
+
+ def call(conn, {:error, error_message}) do
+ conn
+ |> put_status(:bad_request)
+ |> json(%{error: error_message})
+ end
+
+ def call(conn, _) do
+ conn
+ |> put_status(:internal_server_error)
+ |> json(dgettext("errors", "Something went wrong"))
+ end
+end
diff --git a/lib/pleroma/web/mastodon_api/controllers/filter_controller.ex b/lib/pleroma/web/mastodon_api/controllers/filter_controller.ex
new file mode 100644
index 000000000..19041304e
--- /dev/null
+++ b/lib/pleroma/web/mastodon_api/controllers/filter_controller.ex
@@ -0,0 +1,72 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.MastodonAPI.FilterController do
+ use Pleroma.Web, :controller
+
+ alias Pleroma.Filter
+
+ @doc "GET /api/v1/filters"
+ def index(%{assigns: %{user: user}} = conn, _) do
+ filters = Filter.get_filters(user)
+
+ render(conn, "filters.json", filters: filters)
+ end
+
+ @doc "POST /api/v1/filters"
+ def create(
+ %{assigns: %{user: user}} = conn,
+ %{"phrase" => phrase, "context" => context} = params
+ ) do
+ query = %Filter{
+ user_id: user.id,
+ phrase: phrase,
+ context: context,
+ hide: Map.get(params, "irreversible", false),
+ whole_word: Map.get(params, "boolean", true)
+ # expires_at
+ }
+
+ {:ok, response} = Filter.create(query)
+
+ render(conn, "filter.json", filter: response)
+ end
+
+ @doc "GET /api/v1/filters/:id"
+ def show(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do
+ filter = Filter.get(filter_id, user)
+
+ render(conn, "filter.json", filter: filter)
+ end
+
+ @doc "PUT /api/v1/filters/:id"
+ def update(
+ %{assigns: %{user: user}} = conn,
+ %{"phrase" => phrase, "context" => context, "id" => filter_id} = params
+ ) do
+ query = %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} = Filter.update(query)
+ render(conn, "filter.json", filter: response)
+ end
+
+ @doc "DELETE /api/v1/filters/:id"
+ def delete(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do
+ query = %Filter{
+ user_id: user.id,
+ filter_id: filter_id
+ }
+
+ {:ok, _} = Filter.delete(query)
+ json(conn, %{})
+ end
+end
diff --git a/lib/pleroma/web/mastodon_api/controllers/follow_request_controller.ex b/lib/pleroma/web/mastodon_api/controllers/follow_request_controller.ex
new file mode 100644
index 000000000..ce7b625ee
--- /dev/null
+++ b/lib/pleroma/web/mastodon_api/controllers/follow_request_controller.ex
@@ -0,0 +1,49 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.MastodonAPI.FollowRequestController do
+ use Pleroma.Web, :controller
+
+ alias Pleroma.User
+ alias Pleroma.Web.CommonAPI
+
+ plug(:put_view, Pleroma.Web.MastodonAPI.AccountView)
+ plug(:assign_follower when action != :index)
+
+ action_fallback(:errors)
+
+ @doc "GET /api/v1/follow_requests"
+ def index(%{assigns: %{user: followed}} = conn, _params) do
+ follow_requests = User.get_follow_requests(followed)
+
+ render(conn, "index.json", for: followed, users: follow_requests, as: :user)
+ end
+
+ @doc "POST /api/v1/follow_requests/:id/authorize"
+ def authorize(%{assigns: %{user: followed, follower: follower}} = conn, _params) do
+ with {:ok, follower} <- CommonAPI.accept_follow_request(follower, followed) do
+ render(conn, "relationship.json", user: followed, target: follower)
+ end
+ end
+
+ @doc "POST /api/v1/follow_requests/:id/reject"
+ def reject(%{assigns: %{user: followed, follower: follower}} = conn, _params) do
+ with {:ok, follower} <- CommonAPI.reject_follow_request(follower, followed) do
+ render(conn, "relationship.json", user: followed, target: follower)
+ end
+ end
+
+ defp assign_follower(%{params: %{"id" => id}} = conn, _) do
+ case User.get_cached_by_id(id) do
+ %User{} = follower -> assign(conn, :follower, follower)
+ nil -> Pleroma.Web.MastodonAPI.FallbackController.call(conn, {:error, :not_found}) |> halt()
+ end
+ end
+
+ defp errors(conn, {:error, message}) do
+ conn
+ |> put_status(:forbidden)
+ |> json(%{error: message})
+ end
+end
diff --git a/lib/pleroma/web/mastodon_api/controllers/list_controller.ex b/lib/pleroma/web/mastodon_api/controllers/list_controller.ex
new file mode 100644
index 000000000..50f42bee5
--- /dev/null
+++ b/lib/pleroma/web/mastodon_api/controllers/list_controller.ex
@@ -0,0 +1,84 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.MastodonAPI.ListController do
+ use Pleroma.Web, :controller
+
+ alias Pleroma.User
+ alias Pleroma.Web.MastodonAPI.AccountView
+
+ plug(:list_by_id_and_user when action not in [:index, :create])
+
+ action_fallback(Pleroma.Web.MastodonAPI.FallbackController)
+
+ # GET /api/v1/lists
+ def index(%{assigns: %{user: user}} = conn, opts) do
+ lists = Pleroma.List.for_user(user, opts)
+ render(conn, "index.json", lists: lists)
+ end
+
+ # POST /api/v1/lists
+ def create(%{assigns: %{user: user}} = conn, %{"title" => title}) do
+ with {:ok, %Pleroma.List{} = list} <- Pleroma.List.create(title, user) do
+ render(conn, "show.json", list: list)
+ end
+ end
+
+ # GET /api/v1/lists/:id
+ def show(%{assigns: %{list: list}} = conn, _) do
+ render(conn, "show.json", list: list)
+ end
+
+ # PUT /api/v1/lists/:id
+ def update(%{assigns: %{list: list}} = conn, %{"title" => title}) do
+ with {:ok, list} <- Pleroma.List.rename(list, title) do
+ render(conn, "show.json", list: list)
+ end
+ end
+
+ # DELETE /api/v1/lists/:id
+ def delete(%{assigns: %{list: list}} = conn, _) do
+ with {:ok, _list} <- Pleroma.List.delete(list) do
+ json(conn, %{})
+ end
+ end
+
+ # GET /api/v1/lists/:id/accounts
+ def list_accounts(%{assigns: %{user: user, list: list}} = conn, _) do
+ with {:ok, users} <- Pleroma.List.get_following(list) do
+ conn
+ |> put_view(AccountView)
+ |> render("index.json", for: user, users: users, as: :user)
+ end
+ end
+
+ # POST /api/v1/lists/:id/accounts
+ def add_to_list(%{assigns: %{list: list}} = conn, %{"account_ids" => account_ids}) do
+ Enum.each(account_ids, fn account_id ->
+ with %User{} = followed <- User.get_cached_by_id(account_id) do
+ Pleroma.List.follow(list, followed)
+ end
+ end)
+
+ json(conn, %{})
+ end
+
+ # DELETE /api/v1/lists/:id/accounts
+ def remove_from_list(%{assigns: %{list: list}} = conn, %{"account_ids" => account_ids}) do
+ Enum.each(account_ids, fn account_id ->
+ with %User{} = followed <- User.get_cached_by_id(account_id) do
+ Pleroma.List.unfollow(list, followed)
+ end
+ end)
+
+ json(conn, %{})
+ end
+
+ defp list_by_id_and_user(%{assigns: %{user: user}, params: %{"id" => id}} = conn, _) do
+ case Pleroma.List.get(id, user) do
+ %Pleroma.List{} = list -> assign(conn, :list, list)
+ nil -> conn |> render_error(:not_found, "List not found") |> halt()
+ end
+ end
+end
diff --git a/lib/pleroma/web/mastodon_api/controllers/mastodon_api_controller.ex b/lib/pleroma/web/mastodon_api/controllers/mastodon_api_controller.ex
new file mode 100644
index 000000000..1484a0174
--- /dev/null
+++ b/lib/pleroma/web/mastodon_api/controllers/mastodon_api_controller.ex
@@ -0,0 +1,529 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
+ use Pleroma.Web, :controller
+
+ import Pleroma.Web.ControllerHelper, only: [add_link_headers: 2]
+
+ alias Pleroma.Activity
+ alias Pleroma.Bookmark
+ alias Pleroma.Config
+ alias Pleroma.HTTP
+ alias Pleroma.Object
+ alias Pleroma.Pagination
+ alias Pleroma.Plugs.RateLimiter
+ alias Pleroma.Repo
+ alias Pleroma.Stats
+ alias Pleroma.User
+ alias Pleroma.Web
+ alias Pleroma.Web.ActivityPub.ActivityPub
+ alias Pleroma.Web.ActivityPub.Visibility
+ alias Pleroma.Web.CommonAPI
+ alias Pleroma.Web.MastodonAPI.AccountView
+ alias Pleroma.Web.MastodonAPI.AppView
+ alias Pleroma.Web.MastodonAPI.MastodonView
+ alias Pleroma.Web.MastodonAPI.StatusView
+ alias Pleroma.Web.MediaProxy
+ alias Pleroma.Web.OAuth.App
+ alias Pleroma.Web.OAuth.Authorization
+ alias Pleroma.Web.OAuth.Scopes
+ alias Pleroma.Web.OAuth.Token
+ alias Pleroma.Web.TwitterAPI.TwitterAPI
+
+ require Logger
+
+ plug(RateLimiter, :password_reset when action == :password_reset)
+
+ @local_mastodon_name "Mastodon-Local"
+
+ action_fallback(Pleroma.Web.MastodonAPI.FallbackController)
+
+ def create_app(conn, params) do
+ scopes = Scopes.fetch_scopes(params, ["read"])
+
+ app_attrs =
+ params
+ |> Map.drop(["scope", "scopes"])
+ |> Map.put("scopes", scopes)
+
+ with cs <- App.register_changeset(%App{}, app_attrs),
+ false <- cs.changes[:client_name] == @local_mastodon_name,
+ {:ok, app} <- Repo.insert(cs) do
+ conn
+ |> put_view(AppView)
+ |> render("show.json", %{app: app})
+ end
+ end
+
+ def verify_app_credentials(%{assigns: %{user: _user, token: token}} = conn, _) do
+ with %Token{app: %App{} = app} <- Repo.preload(token, :app) do
+ conn
+ |> put_view(AppView)
+ |> render("short.json", %{app: app})
+ end
+ end
+
+ @mastodon_api_level "2.7.2"
+
+ def masto_instance(conn, _params) do
+ instance = Config.get(:instance)
+
+ response = %{
+ uri: Web.base_url(),
+ title: Keyword.get(instance, :name),
+ description: Keyword.get(instance, :description),
+ version: "#{@mastodon_api_level} (compatible; #{Pleroma.Application.named_version()})",
+ email: Keyword.get(instance, :email),
+ urls: %{
+ streaming_api: Pleroma.Web.Endpoint.websocket_url()
+ },
+ stats: Stats.get_stats(),
+ thumbnail: Web.base_url() <> "/instance/thumbnail.jpeg",
+ languages: ["en"],
+ registrations: Pleroma.Config.get([:instance, :registrations_open]),
+ # Extra (not present in Mastodon):
+ max_toot_chars: Keyword.get(instance, :limit),
+ poll_limits: Keyword.get(instance, :poll_limits)
+ }
+
+ json(conn, response)
+ end
+
+ def peers(conn, _params) do
+ json(conn, Stats.get_peers())
+ end
+
+ defp mastodonized_emoji do
+ Pleroma.Emoji.get_all()
+ |> Enum.map(fn {shortcode, %Pleroma.Emoji{file: relative_url, tags: tags}} ->
+ url = to_string(URI.merge(Web.base_url(), relative_url))
+
+ %{
+ "shortcode" => shortcode,
+ "static_url" => url,
+ "visible_in_picker" => true,
+ "url" => url,
+ "tags" => tags,
+ # Assuming that a comma is authorized in the category name
+ "category" => (tags -- ["Custom"]) |> Enum.join(",")
+ }
+ end)
+ end
+
+ def custom_emojis(conn, _params) do
+ mastodon_emoji = mastodonized_emoji()
+ json(conn, mastodon_emoji)
+ end
+
+ def get_poll(%{assigns: %{user: user}} = conn, %{"id" => id}) do
+ with %Object{} = object <- Object.get_by_id_and_maybe_refetch(id, interval: 60),
+ %Activity{} = activity <- Activity.get_create_by_object_ap_id(object.data["id"]),
+ true <- Visibility.visible_for_user?(activity, user) do
+ conn
+ |> put_view(StatusView)
+ |> try_render("poll.json", %{object: object, for: user})
+ else
+ error when is_nil(error) or error == false ->
+ render_error(conn, :not_found, "Record not found")
+ end
+ end
+
+ defp get_cached_vote_or_vote(user, object, choices) do
+ idempotency_key = "polls:#{user.id}:#{object.data["id"]}"
+
+ {_, res} =
+ Cachex.fetch(:idempotency_cache, idempotency_key, fn _ ->
+ case CommonAPI.vote(user, object, choices) do
+ {:error, _message} = res -> {:ignore, res}
+ res -> {:commit, res}
+ end
+ end)
+
+ res
+ end
+
+ def poll_vote(%{assigns: %{user: user}} = conn, %{"id" => id, "choices" => choices}) do
+ with %Object{} = object <- Object.get_by_id(id),
+ true <- object.data["type"] == "Question",
+ %Activity{} = activity <- Activity.get_create_by_object_ap_id(object.data["id"]),
+ true <- Visibility.visible_for_user?(activity, user),
+ {:ok, _activities, object} <- get_cached_vote_or_vote(user, object, choices) do
+ conn
+ |> put_view(StatusView)
+ |> try_render("poll.json", %{object: object, for: user})
+ else
+ nil ->
+ render_error(conn, :not_found, "Record not found")
+
+ false ->
+ render_error(conn, :not_found, "Record not found")
+
+ {:error, message} ->
+ conn
+ |> put_status(:unprocessable_entity)
+ |> json(%{error: message})
+ end
+ end
+
+ def update_media(
+ %{assigns: %{user: user}} = conn,
+ %{"id" => id, "description" => description} = _
+ )
+ when is_binary(description) do
+ with %Object{} = object <- Repo.get(Object, id),
+ true <- Object.authorize_mutation(object, user),
+ {:ok, %Object{data: data}} <- Object.update_data(object, %{"name" => description}) do
+ attachment_data = Map.put(data, "id", object.id)
+
+ conn
+ |> put_view(StatusView)
+ |> render("attachment.json", %{attachment: attachment_data})
+ end
+ end
+
+ def update_media(_conn, _data), do: {:error, :bad_request}
+
+ def upload(%{assigns: %{user: user}} = conn, %{"file" => file} = data) do
+ with {:ok, object} <-
+ ActivityPub.upload(
+ file,
+ actor: User.ap_id(user),
+ description: Map.get(data, "description")
+ ) do
+ attachment_data = Map.put(object.data, "id", object.id)
+
+ conn
+ |> put_view(StatusView)
+ |> render("attachment.json", %{attachment: attachment_data})
+ end
+ end
+
+ def follows(%{assigns: %{user: follower}} = conn, %{"uri" => uri}) do
+ with {_, %User{} = followed} <- {:followed, User.get_cached_by_nickname(uri)},
+ {_, true} <- {:followed, follower.id != followed.id},
+ {:ok, follower, followed, _} <- CommonAPI.follow(follower, followed) do
+ conn
+ |> put_view(AccountView)
+ |> render("show.json", %{user: followed, for: follower})
+ else
+ {:followed, _} ->
+ {:error, :not_found}
+
+ {:error, message} ->
+ conn
+ |> put_status(:forbidden)
+ |> json(%{error: message})
+ end
+ end
+
+ def mutes(%{assigns: %{user: user}} = conn, _) do
+ with muted_accounts <- User.muted_users(user) do
+ res = AccountView.render("index.json", users: muted_accounts, for: user, as: :user)
+ json(conn, res)
+ end
+ end
+
+ def blocks(%{assigns: %{user: user}} = conn, _) do
+ with blocked_accounts <- User.blocked_users(user) do
+ res = AccountView.render("index.json", users: blocked_accounts, for: user, as: :user)
+ json(conn, res)
+ end
+ end
+
+ def favourites(%{assigns: %{user: user}} = conn, params) do
+ params =
+ params
+ |> Map.put("type", "Create")
+ |> Map.put("favorited_by", user.ap_id)
+ |> Map.put("blocking_user", user)
+
+ activities =
+ ActivityPub.fetch_activities([], params)
+ |> Enum.reverse()
+
+ conn
+ |> add_link_headers(activities)
+ |> put_view(StatusView)
+ |> render("index.json", %{activities: activities, for: user, as: :activity})
+ end
+
+ def bookmarks(%{assigns: %{user: user}} = conn, params) do
+ user = User.get_cached_by_id(user.id)
+
+ bookmarks =
+ Bookmark.for_user_query(user.id)
+ |> Pagination.fetch_paginated(params)
+
+ activities =
+ bookmarks
+ |> Enum.map(fn b -> Map.put(b.activity, :bookmark, Map.delete(b, :activity)) end)
+
+ conn
+ |> add_link_headers(bookmarks)
+ |> put_view(StatusView)
+ |> render("index.json", %{activities: activities, for: user, as: :activity})
+ end
+
+ def index(%{assigns: %{user: user}} = conn, _params) do
+ token = get_session(conn, :oauth_token)
+
+ if user && token do
+ mastodon_emoji = mastodonized_emoji()
+
+ limit = Config.get([:instance, :limit])
+
+ accounts = Map.put(%{}, user.id, AccountView.render("show.json", %{user: user, for: user}))
+
+ initial_state =
+ %{
+ meta: %{
+ streaming_api_base_url: Pleroma.Web.Endpoint.websocket_url(),
+ 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,
+ display_sensitive_media: false,
+ reduce_motion: false,
+ max_toot_chars: limit,
+ mascot: User.get_mascot(user)["url"]
+ },
+ poll_limits: Config.get([:instance, :poll_limits]),
+ rights: %{
+ delete_others_notice: present?(user.info.is_moderator),
+ admin: present?(user.info.is_admin)
+ },
+ compose: %{
+ me: "#{user.id}",
+ default_privacy: user.info.default_scope,
+ default_sensitive: false,
+ allow_content_types: Config.get([:instance, :allowed_post_formats])
+ },
+ media_attachments: %{
+ accept_content_types: [
+ ".jpg",
+ ".jpeg",
+ ".png",
+ ".gif",
+ ".webm",
+ ".mp4",
+ ".m4v",
+ "image\/jpeg",
+ "image\/png",
+ "image\/gif",
+ "video\/webm",
+ "video\/mp4"
+ ]
+ },
+ settings:
+ 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: limit
+ }
+ |> Jason.encode!()
+
+ conn
+ |> put_layout(false)
+ |> put_view(MastodonView)
+ |> render("index.html", %{initial_state: initial_state})
+ else
+ conn
+ |> put_session(:return_to, conn.request_path)
+ |> redirect(to: "/web/login")
+ end
+ end
+
+ def put_settings(%{assigns: %{user: user}} = conn, %{"data" => settings} = _params) do
+ with {:ok, _} <- User.update_info(user, &User.Info.mastodon_settings_update(&1, settings)) do
+ json(conn, %{})
+ else
+ e ->
+ conn
+ |> put_status(:internal_server_error)
+ |> json(%{error: inspect(e)})
+ end
+ end
+
+ def login(%{assigns: %{user: %User{}}} = conn, _params) do
+ redirect(conn, to: local_mastodon_root_path(conn))
+ end
+
+ @doc "Local Mastodon FE login init action"
+ def login(conn, %{"code" => auth_token}) do
+ with {:ok, app} <- get_or_make_app(),
+ {:ok, auth} <- Authorization.get_by_token(app, auth_token),
+ {:ok, token} <- Token.exchange_token(app, auth) do
+ conn
+ |> put_session(:oauth_token, token.token)
+ |> redirect(to: local_mastodon_root_path(conn))
+ end
+ end
+
+ @doc "Local Mastodon FE callback action"
+ def login(conn, _) do
+ with {:ok, app} <- get_or_make_app() do
+ path =
+ o_auth_path(conn, :authorize,
+ response_type: "code",
+ client_id: app.client_id,
+ redirect_uri: ".",
+ scope: Enum.join(app.scopes, " ")
+ )
+
+ redirect(conn, to: path)
+ end
+ end
+
+ defp local_mastodon_root_path(conn) do
+ case get_session(conn, :return_to) do
+ nil ->
+ mastodon_api_path(conn, :index, ["getting-started"])
+
+ return_to ->
+ delete_session(conn, :return_to)
+ return_to
+ end
+ end
+
+ @spec get_or_make_app() :: {:ok, App.t()} | {:error, Ecto.Changeset.t()}
+ defp get_or_make_app do
+ App.get_or_make(
+ %{client_name: @local_mastodon_name, redirect_uris: "."},
+ ["read", "write", "follow", "push"]
+ )
+ end
+
+ def logout(conn, _) do
+ conn
+ |> clear_session
+ |> redirect(to: "/")
+ end
+
+ # Stubs for unimplemented mastodon api
+ #
+ 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 suggestions(%{assigns: %{user: user}} = conn, _) do
+ suggestions = Config.get(:suggestions)
+
+ 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 = Config.get([Pleroma.Web.Endpoint, :url, :host])
+
+ user = user.nickname
+
+ url =
+ api
+ |> String.replace("{{host}}", host)
+ |> String.replace("{{user}}", user)
+
+ with {:ok, %{status: 200, body: body}} <-
+ HTTP.get(url, [], adapter: [recv_timeout: timeout, pool: :default]),
+ {:ok, data} <- Jason.decode(body) do
+ data =
+ data
+ |> Enum.slice(0, limit)
+ |> Enum.map(fn x ->
+ x
+ |> Map.put("id", fetch_suggestion_id(x))
+ |> Map.put("avatar", MediaProxy.url(x["avatar"]))
+ |> Map.put("avatar_static", MediaProxy.url(x["avatar_static"]))
+ end)
+
+ json(conn, data)
+ else
+ e ->
+ Logger.error("Could not retrieve suggestions at fetch #{url}, #{inspect(e)}")
+ end
+ else
+ json(conn, [])
+ end
+ end
+
+ defp fetch_suggestion_id(attrs) do
+ case User.get_or_fetch(attrs["acct"]) do
+ {:ok, %User{id: id}} -> id
+ _ -> 0
+ end
+ end
+
+ def password_reset(conn, params) do
+ nickname_or_email = params["email"] || params["nickname"]
+
+ with {:ok, _} <- TwitterAPI.password_reset(nickname_or_email) do
+ conn
+ |> put_status(:no_content)
+ |> json("")
+ else
+ {:error, "unknown user"} ->
+ send_resp(conn, :not_found, "")
+
+ {:error, _} ->
+ send_resp(conn, :bad_request, "")
+ end
+ end
+
+ def try_render(conn, target, params)
+ when is_binary(target) do
+ case render(conn, target, params) do
+ nil -> render_error(conn, :not_implemented, "Can't display this activity")
+ res -> res
+ end
+ end
+
+ def try_render(conn, _, _) do
+ render_error(conn, :not_implemented, "Can't display this activity")
+ end
+
+ defp present?(nil), do: false
+ defp present?(false), do: false
+ defp present?(_), do: true
+end
diff --git a/lib/pleroma/web/mastodon_api/controllers/notification_controller.ex b/lib/pleroma/web/mastodon_api/controllers/notification_controller.ex
new file mode 100644
index 000000000..7e4d7297c
--- /dev/null
+++ b/lib/pleroma/web/mastodon_api/controllers/notification_controller.ex
@@ -0,0 +1,57 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.MastodonAPI.NotificationController do
+ use Pleroma.Web, :controller
+
+ import Pleroma.Web.ControllerHelper, only: [add_link_headers: 2]
+
+ alias Pleroma.Notification
+ alias Pleroma.Web.MastodonAPI.MastodonAPI
+
+ # GET /api/v1/notifications
+ def index(%{assigns: %{user: user}} = conn, params) do
+ notifications = MastodonAPI.get_notifications(user, params)
+
+ conn
+ |> add_link_headers(notifications)
+ |> render("index.json", notifications: notifications, for: user)
+ end
+
+ # GET /api/v1/notifications/:id
+ def show(%{assigns: %{user: user}} = conn, %{"id" => id}) do
+ with {:ok, notification} <- Notification.get(user, id) do
+ render(conn, "show.json", notification: notification, for: user)
+ else
+ {:error, reason} ->
+ conn
+ |> put_status(:forbidden)
+ |> json(%{"error" => reason})
+ end
+ end
+
+ # POST /api/v1/notifications/clear
+ def clear(%{assigns: %{user: user}} = conn, _params) do
+ Notification.clear(user)
+ json(conn, %{})
+ end
+
+ # POST /api/v1/notifications/dismiss
+ def dismiss(%{assigns: %{user: user}} = conn, %{"id" => id} = _params) do
+ with {:ok, _notif} <- Notification.dismiss(user, id) do
+ json(conn, %{})
+ else
+ {:error, reason} ->
+ conn
+ |> put_status(:forbidden)
+ |> json(%{"error" => reason})
+ end
+ end
+
+ # DELETE /api/v1/notifications/destroy_multiple
+ def destroy_multiple(%{assigns: %{user: user}} = conn, %{"ids" => ids} = _params) do
+ Notification.destroy_multiple(user, ids)
+ json(conn, %{})
+ end
+end
diff --git a/lib/pleroma/web/mastodon_api/controllers/report_controller.ex b/lib/pleroma/web/mastodon_api/controllers/report_controller.ex
new file mode 100644
index 000000000..1c084b740
--- /dev/null
+++ b/lib/pleroma/web/mastodon_api/controllers/report_controller.ex
@@ -0,0 +1,16 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.MastodonAPI.ReportController do
+ use Pleroma.Web, :controller
+
+ action_fallback(Pleroma.Web.MastodonAPI.FallbackController)
+
+ @doc "POST /api/v1/reports"
+ def create(%{assigns: %{user: user}} = conn, params) do
+ with {:ok, activity} <- Pleroma.Web.CommonAPI.report(user, params) do
+ render(conn, "show.json", activity: activity)
+ end
+ end
+end
diff --git a/lib/pleroma/web/mastodon_api/controllers/scheduled_activity_controller.ex b/lib/pleroma/web/mastodon_api/controllers/scheduled_activity_controller.ex
new file mode 100644
index 000000000..0a56b10b6
--- /dev/null
+++ b/lib/pleroma/web/mastodon_api/controllers/scheduled_activity_controller.ex
@@ -0,0 +1,51 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.MastodonAPI.ScheduledActivityController do
+ use Pleroma.Web, :controller
+
+ import Pleroma.Web.ControllerHelper, only: [add_link_headers: 2]
+
+ alias Pleroma.ScheduledActivity
+ alias Pleroma.Web.MastodonAPI.MastodonAPI
+
+ plug(:assign_scheduled_activity when action != :index)
+
+ action_fallback(Pleroma.Web.MastodonAPI.FallbackController)
+
+ @doc "GET /api/v1/scheduled_statuses"
+ def index(%{assigns: %{user: user}} = conn, params) do
+ with scheduled_activities <- MastodonAPI.get_scheduled_activities(user, params) do
+ conn
+ |> add_link_headers(scheduled_activities)
+ |> render("index.json", scheduled_activities: scheduled_activities)
+ end
+ end
+
+ @doc "GET /api/v1/scheduled_statuses/:id"
+ def show(%{assigns: %{scheduled_activity: scheduled_activity}} = conn, _params) do
+ render(conn, "show.json", scheduled_activity: scheduled_activity)
+ end
+
+ @doc "PUT /api/v1/scheduled_statuses/:id"
+ def update(%{assigns: %{scheduled_activity: scheduled_activity}} = conn, params) do
+ with {:ok, scheduled_activity} <- ScheduledActivity.update(scheduled_activity, params) do
+ render(conn, "show.json", scheduled_activity: scheduled_activity)
+ end
+ end
+
+ @doc "DELETE /api/v1/scheduled_statuses/:id"
+ def delete(%{assigns: %{scheduled_activity: scheduled_activity}} = conn, _params) do
+ with {:ok, scheduled_activity} <- ScheduledActivity.delete(scheduled_activity) do
+ render(conn, "show.json", scheduled_activity: scheduled_activity)
+ end
+ end
+
+ defp assign_scheduled_activity(%{assigns: %{user: user}, params: %{"id" => id}} = conn, _) do
+ case ScheduledActivity.get(user, id) do
+ %ScheduledActivity{} = activity -> assign(conn, :scheduled_activity, activity)
+ nil -> Pleroma.Web.MastodonAPI.FallbackController.call(conn, {:error, :not_found}) |> halt()
+ end
+ end
+end
diff --git a/lib/pleroma/web/mastodon_api/search_controller.ex b/lib/pleroma/web/mastodon_api/controllers/search_controller.ex
index 9072aa7a4..3fc89d645 100644
--- a/lib/pleroma/web/mastodon_api/search_controller.ex
+++ b/lib/pleroma/web/mastodon_api/controllers/search_controller.ex
@@ -19,9 +19,10 @@ defmodule Pleroma.Web.MastodonAPI.SearchController do
def account_search(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do
accounts = User.search(query, search_options(params, user))
- res = AccountView.render("accounts.json", users: accounts, for: user, as: :user)
- json(conn, res)
+ conn
+ |> put_view(AccountView)
+ |> render("index.json", users: accounts, for: user, as: :user)
end
def search2(conn, params), do: do_search(:v2, conn, params)
@@ -71,7 +72,7 @@ defmodule Pleroma.Web.MastodonAPI.SearchController do
defp resource_search(_, "accounts", query, options) do
accounts = with_fallback(fn -> User.search(query, options) end)
- AccountView.render("accounts.json", users: accounts, for: options[:for_user], as: :user)
+ AccountView.render("index.json", users: accounts, for: options[:for_user], as: :user)
end
defp resource_search(_, "statuses", query, options) do
diff --git a/lib/pleroma/web/mastodon_api/controllers/status_controller.ex b/lib/pleroma/web/mastodon_api/controllers/status_controller.ex
new file mode 100644
index 000000000..3c6987a5f
--- /dev/null
+++ b/lib/pleroma/web/mastodon_api/controllers/status_controller.ex
@@ -0,0 +1,274 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.MastodonAPI.StatusController do
+ use Pleroma.Web, :controller
+
+ import Pleroma.Web.MastodonAPI.MastodonAPIController, only: [try_render: 3]
+
+ require Ecto.Query
+
+ alias Pleroma.Activity
+ alias Pleroma.Bookmark
+ alias Pleroma.Object
+ alias Pleroma.Plugs.RateLimiter
+ alias Pleroma.Repo
+ alias Pleroma.ScheduledActivity
+ alias Pleroma.User
+ alias Pleroma.Web.ActivityPub.ActivityPub
+ alias Pleroma.Web.ActivityPub.Visibility
+ alias Pleroma.Web.CommonAPI
+ alias Pleroma.Web.MastodonAPI.AccountView
+ alias Pleroma.Web.MastodonAPI.ScheduledActivityView
+
+ @rate_limited_status_actions ~w(reblog unreblog favourite unfavourite create delete)a
+
+ plug(
+ RateLimiter,
+ {:status_id_action, bucket_name: "status_id_action:reblog_unreblog", params: ["id"]}
+ when action in ~w(reblog unreblog)a
+ )
+
+ plug(
+ RateLimiter,
+ {:status_id_action, bucket_name: "status_id_action:fav_unfav", params: ["id"]}
+ when action in ~w(favourite unfavourite)a
+ )
+
+ plug(RateLimiter, :statuses_actions when action in @rate_limited_status_actions)
+
+ action_fallback(Pleroma.Web.MastodonAPI.FallbackController)
+
+ @doc """
+ GET `/api/v1/statuses?ids[]=1&ids[]=2`
+
+ `ids` query param is required
+ """
+ def index(%{assigns: %{user: user}} = conn, %{"ids" => ids}) do
+ limit = 100
+
+ activities =
+ ids
+ |> Enum.take(limit)
+ |> Activity.all_by_ids_with_object()
+ |> Enum.filter(&Visibility.visible_for_user?(&1, user))
+
+ render(conn, "index.json", activities: activities, for: user, as: :activity)
+ end
+
+ @doc """
+ POST /api/v1/statuses
+
+ Creates a scheduled status when `scheduled_at` param is present and it's far enough
+ """
+ def create(
+ %{assigns: %{user: user}} = conn,
+ %{"status" => _, "scheduled_at" => scheduled_at} = params
+ ) do
+ params = Map.put(params, "in_reply_to_status_id", params["in_reply_to_id"])
+
+ if ScheduledActivity.far_enough?(scheduled_at) do
+ with {:ok, scheduled_activity} <-
+ ScheduledActivity.create(user, %{"params" => params, "scheduled_at" => scheduled_at}) do
+ conn
+ |> put_view(ScheduledActivityView)
+ |> render("show.json", scheduled_activity: scheduled_activity)
+ end
+ else
+ create(conn, Map.drop(params, ["scheduled_at"]))
+ end
+ end
+
+ @doc """
+ POST /api/v1/statuses
+
+ Creates a regular status
+ """
+ def create(%{assigns: %{user: user}} = conn, %{"status" => _} = params) do
+ params = Map.put(params, "in_reply_to_status_id", params["in_reply_to_id"])
+
+ with {:ok, activity} <- CommonAPI.post(user, params) do
+ try_render(conn, "show.json",
+ activity: activity,
+ for: user,
+ as: :activity,
+ with_direct_conversation_id: true
+ )
+ else
+ {:error, message} ->
+ conn
+ |> put_status(:unprocessable_entity)
+ |> json(%{error: message})
+ end
+ end
+
+ def create(%{assigns: %{user: _user}} = conn, %{"media_ids" => _} = params) do
+ create(conn, Map.put(params, "status", ""))
+ end
+
+ @doc "GET /api/v1/statuses/:id"
+ def show(%{assigns: %{user: user}} = conn, %{"id" => id}) do
+ with %Activity{} = activity <- Activity.get_by_id_with_object(id),
+ true <- Visibility.visible_for_user?(activity, user) do
+ try_render(conn, "show.json", activity: activity, for: user)
+ end
+ end
+
+ @doc "DELETE /api/v1/statuses/:id"
+ def delete(%{assigns: %{user: user}} = conn, %{"id" => id}) do
+ with {:ok, %Activity{}} <- CommonAPI.delete(id, user) do
+ json(conn, %{})
+ else
+ _e -> render_error(conn, :forbidden, "Can't delete this post")
+ end
+ end
+
+ @doc "POST /api/v1/statuses/:id/reblog"
+ def reblog(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
+ with {:ok, announce, _activity} <- CommonAPI.repeat(ap_id_or_id, user),
+ %Activity{} = announce <- Activity.normalize(announce.data) do
+ try_render(conn, "show.json", %{activity: announce, for: user, as: :activity})
+ end
+ end
+
+ @doc "POST /api/v1/statuses/:id/unreblog"
+ def unreblog(%{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_by_object_ap_id_with_object(id) do
+ try_render(conn, "show.json", %{activity: activity, for: user, as: :activity})
+ end
+ end
+
+ @doc "POST /api/v1/statuses/:id/favourite"
+ def favourite(%{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_by_object_ap_id(id) do
+ try_render(conn, "show.json", activity: activity, for: user, as: :activity)
+ end
+ end
+
+ @doc "POST /api/v1/statuses/:id/unfavourite"
+ def unfavourite(%{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_by_object_ap_id(id) do
+ try_render(conn, "show.json", activity: activity, for: user, as: :activity)
+ end
+ end
+
+ @doc "POST /api/v1/statuses/:id/pin"
+ def pin(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
+ with {:ok, activity} <- CommonAPI.pin(ap_id_or_id, user) do
+ try_render(conn, "show.json", activity: activity, for: user, as: :activity)
+ end
+ end
+
+ @doc "POST /api/v1/statuses/:id/unpin"
+ def unpin(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
+ with {:ok, activity} <- CommonAPI.unpin(ap_id_or_id, user) do
+ try_render(conn, "show.json", activity: activity, for: user, as: :activity)
+ end
+ end
+
+ @doc "POST /api/v1/statuses/:id/bookmark"
+ def bookmark(%{assigns: %{user: user}} = conn, %{"id" => id}) do
+ with %Activity{} = activity <- Activity.get_by_id_with_object(id),
+ %User{} = user <- User.get_cached_by_nickname(user.nickname),
+ true <- Visibility.visible_for_user?(activity, user),
+ {:ok, _bookmark} <- Bookmark.create(user.id, activity.id) do
+ try_render(conn, "show.json", activity: activity, for: user, as: :activity)
+ end
+ end
+
+ @doc "POST /api/v1/statuses/:id/unbookmark"
+ def unbookmark(%{assigns: %{user: user}} = conn, %{"id" => id}) do
+ with %Activity{} = activity <- Activity.get_by_id_with_object(id),
+ %User{} = user <- User.get_cached_by_nickname(user.nickname),
+ true <- Visibility.visible_for_user?(activity, user),
+ {:ok, _bookmark} <- Bookmark.destroy(user.id, activity.id) do
+ try_render(conn, "show.json", activity: activity, for: user, as: :activity)
+ end
+ end
+
+ @doc "POST /api/v1/statuses/:id/mute"
+ def mute_conversation(%{assigns: %{user: user}} = conn, %{"id" => id}) do
+ with %Activity{} = activity <- Activity.get_by_id(id),
+ {:ok, activity} <- CommonAPI.add_mute(user, activity) do
+ try_render(conn, "show.json", activity: activity, for: user, as: :activity)
+ end
+ end
+
+ @doc "POST /api/v1/statuses/:id/unmute"
+ def unmute_conversation(%{assigns: %{user: user}} = conn, %{"id" => id}) do
+ with %Activity{} = activity <- Activity.get_by_id(id),
+ {:ok, activity} <- CommonAPI.remove_mute(user, activity) do
+ try_render(conn, "show.json", activity: activity, for: user, as: :activity)
+ end
+ end
+
+ @doc "GET /api/v1/statuses/:id/card"
+ @deprecated "https://github.com/tootsuite/mastodon/pull/11213"
+ def card(%{assigns: %{user: user}} = conn, %{"id" => status_id}) do
+ with %Activity{} = activity <- Activity.get_by_id(status_id),
+ true <- Visibility.visible_for_user?(activity, user) do
+ data = Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity)
+ render(conn, "card.json", data)
+ else
+ _ -> render_error(conn, :not_found, "Record not found")
+ end
+ end
+
+ @doc "GET /api/v1/statuses/:id/favourited_by"
+ def favourited_by(%{assigns: %{user: user}} = conn, %{"id" => id}) do
+ with %Activity{} = activity <- Activity.get_by_id_with_object(id),
+ {:visible, true} <- {:visible, Visibility.visible_for_user?(activity, user)},
+ %Object{data: %{"likes" => likes}} <- Object.normalize(activity) do
+ users =
+ User
+ |> Ecto.Query.where([u], u.ap_id in ^likes)
+ |> Repo.all()
+ |> Enum.filter(&(not User.blocks?(user, &1)))
+
+ conn
+ |> put_view(AccountView)
+ |> render("index.json", for: user, users: users, as: :user)
+ else
+ {:visible, false} -> {:error, :not_found}
+ _ -> json(conn, [])
+ end
+ end
+
+ @doc "GET /api/v1/statuses/:id/reblogged_by"
+ def reblogged_by(%{assigns: %{user: user}} = conn, %{"id" => id}) do
+ with %Activity{} = activity <- Activity.get_by_id_with_object(id),
+ {:visible, true} <- {:visible, Visibility.visible_for_user?(activity, user)},
+ %Object{data: %{"announcements" => announces}} <- Object.normalize(activity) do
+ users =
+ User
+ |> Ecto.Query.where([u], u.ap_id in ^announces)
+ |> Repo.all()
+ |> Enum.filter(&(not User.blocks?(user, &1)))
+
+ conn
+ |> put_view(AccountView)
+ |> render("index.json", for: user, users: users, as: :user)
+ else
+ {:visible, false} -> {:error, :not_found}
+ _ -> json(conn, [])
+ end
+ end
+
+ @doc "GET /api/v1/statuses/:id/context"
+ def context(%{assigns: %{user: user}} = conn, %{"id" => id}) do
+ with %Activity{} = activity <- Activity.get_by_id(id) do
+ activities =
+ ActivityPub.fetch_activities_for_context(activity.data["context"], %{
+ "blocking_user" => user,
+ "user" => user,
+ "exclude_id" => activity.id
+ })
+
+ render(conn, "context.json", activity: activity, activities: activities, user: user)
+ end
+ end
+end
diff --git a/lib/pleroma/web/mastodon_api/subscription_controller.ex b/lib/pleroma/web/mastodon_api/controllers/subscription_controller.ex
index 255ee2f18..e2b17aab1 100644
--- a/lib/pleroma/web/mastodon_api/subscription_controller.ex
+++ b/lib/pleroma/web/mastodon_api/controllers/subscription_controller.ex
@@ -64,8 +64,6 @@ defmodule Pleroma.Web.MastodonAPI.SubscriptionController do
end
def errors(conn, _) do
- conn
- |> put_status(:internal_server_error)
- |> json(dgettext("errors", "Something went wrong"))
+ Pleroma.Web.MastodonAPI.FallbackController.call(conn, nil)
end
end
diff --git a/lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex b/lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex
new file mode 100644
index 000000000..bb8b0eb32
--- /dev/null
+++ b/lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex
@@ -0,0 +1,136 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.MastodonAPI.TimelineController do
+ use Pleroma.Web, :controller
+
+ import Pleroma.Web.ControllerHelper,
+ only: [add_link_headers: 2, add_link_headers: 3, truthy_param?: 1]
+
+ alias Pleroma.Pagination
+ alias Pleroma.Web.ActivityPub.ActivityPub
+
+ plug(:put_view, Pleroma.Web.MastodonAPI.StatusView)
+
+ # GET /api/v1/timelines/home
+ def home(%{assigns: %{user: user}} = conn, params) do
+ params =
+ params
+ |> Map.put("type", ["Create", "Announce"])
+ |> Map.put("blocking_user", user)
+ |> Map.put("muting_user", user)
+ |> Map.put("user", user)
+
+ recipients = [user.ap_id | user.following]
+
+ activities =
+ recipients
+ |> ActivityPub.fetch_activities(params)
+ |> Enum.reverse()
+
+ conn
+ |> add_link_headers(activities)
+ |> render("index.json", activities: activities, for: user, as: :activity)
+ end
+
+ # GET /api/v1/timelines/direct
+ def direct(%{assigns: %{user: user}} = conn, params) do
+ params =
+ params
+ |> Map.put("type", "Create")
+ |> Map.put("blocking_user", user)
+ |> Map.put("user", user)
+ |> Map.put(:visibility, "direct")
+
+ activities =
+ [user.ap_id]
+ |> ActivityPub.fetch_activities_query(params)
+ |> Pagination.fetch_paginated(params)
+
+ conn
+ |> add_link_headers(activities)
+ |> render("index.json", activities: activities, for: user, as: :activity)
+ end
+
+ # GET /api/v1/timelines/public
+ def public(%{assigns: %{user: user}} = conn, params) do
+ local_only = truthy_param?(params["local"])
+
+ activities =
+ params
+ |> Map.put("type", ["Create", "Announce"])
+ |> Map.put("local_only", local_only)
+ |> Map.put("blocking_user", user)
+ |> Map.put("muting_user", user)
+ |> ActivityPub.fetch_public_activities()
+ |> Enum.reverse()
+
+ conn
+ |> add_link_headers(activities, %{"local" => local_only})
+ |> render("index.json", activities: activities, for: user, as: :activity)
+ end
+
+ # GET /api/v1/timelines/tag/:tag
+ def hashtag(%{assigns: %{user: user}} = conn, params) do
+ local_only = truthy_param?(params["local"])
+
+ tags =
+ [params["tag"], params["any"]]
+ |> List.flatten()
+ |> Enum.uniq()
+ |> Enum.filter(& &1)
+ |> Enum.map(&String.downcase(&1))
+
+ tag_all =
+ params
+ |> Map.get("all", [])
+ |> Enum.map(&String.downcase(&1))
+
+ tag_reject =
+ params
+ |> Map.get("none", [])
+ |> Enum.map(&String.downcase(&1))
+
+ activities =
+ params
+ |> Map.put("type", "Create")
+ |> Map.put("local_only", local_only)
+ |> Map.put("blocking_user", user)
+ |> Map.put("muting_user", user)
+ |> Map.put("user", user)
+ |> Map.put("tag", tags)
+ |> Map.put("tag_all", tag_all)
+ |> Map.put("tag_reject", tag_reject)
+ |> ActivityPub.fetch_public_activities()
+ |> Enum.reverse()
+
+ conn
+ |> add_link_headers(activities, %{"local" => local_only})
+ |> render("index.json", activities: activities, for: user, as: :activity)
+ end
+
+ # GET /api/v1/timelines/list/:list_id
+ def list(%{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)
+ |> Map.put("user", user)
+ |> Map.put("muting_user", user)
+
+ # 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).
+ activities =
+ following
+ |> Enum.filter(fn x -> x in user.following end)
+ |> ActivityPub.fetch_activities_bounded(following, params)
+ |> Enum.reverse()
+
+ render(conn, "index.json", activities: activities, for: user, as: :activity)
+ else
+ _e -> render_error(conn, :forbidden, "Error.")
+ end
+ end
+end
diff --git a/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex b/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex
deleted file mode 100644
index 3ca1630f4..000000000
--- a/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex
+++ /dev/null
@@ -1,1824 +0,0 @@
-# Pleroma: A lightweight social networking server
-# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
-# SPDX-License-Identifier: AGPL-3.0-only
-
-defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
- use Pleroma.Web, :controller
-
- import Pleroma.Web.ControllerHelper,
- only: [json_response: 3, add_link_headers: 5, add_link_headers: 4, add_link_headers: 3]
-
- alias Ecto.Changeset
- alias Pleroma.Activity
- alias Pleroma.Bookmark
- alias Pleroma.Config
- alias Pleroma.Conversation.Participation
- alias Pleroma.Filter
- alias Pleroma.Formatter
- alias Pleroma.HTTP
- alias Pleroma.Notification
- alias Pleroma.Object
- alias Pleroma.Pagination
- alias Pleroma.Plugs.RateLimiter
- alias Pleroma.Repo
- alias Pleroma.ScheduledActivity
- alias Pleroma.Stats
- alias Pleroma.User
- alias Pleroma.Web
- alias Pleroma.Web.ActivityPub.ActivityPub
- alias Pleroma.Web.ActivityPub.Visibility
- alias Pleroma.Web.CommonAPI
- alias Pleroma.Web.MastodonAPI.AccountView
- alias Pleroma.Web.MastodonAPI.AppView
- alias Pleroma.Web.MastodonAPI.ConversationView
- alias Pleroma.Web.MastodonAPI.FilterView
- alias Pleroma.Web.MastodonAPI.ListView
- alias Pleroma.Web.MastodonAPI.MastodonAPI
- alias Pleroma.Web.MastodonAPI.MastodonView
- alias Pleroma.Web.MastodonAPI.NotificationView
- alias Pleroma.Web.MastodonAPI.ReportView
- alias Pleroma.Web.MastodonAPI.ScheduledActivityView
- alias Pleroma.Web.MastodonAPI.StatusView
- alias Pleroma.Web.MediaProxy
- alias Pleroma.Web.OAuth.App
- alias Pleroma.Web.OAuth.Authorization
- alias Pleroma.Web.OAuth.Scopes
- alias Pleroma.Web.OAuth.Token
- alias Pleroma.Web.TwitterAPI.TwitterAPI
-
- alias Pleroma.Web.ControllerHelper
- import Ecto.Query
-
- require Logger
- require Pleroma.Constants
-
- @rate_limited_relations_actions ~w(follow unfollow)a
-
- @rate_limited_status_actions ~w(reblog_status unreblog_status fav_status unfav_status
- post_status delete_status)a
-
- plug(
- RateLimiter,
- {:status_id_action, bucket_name: "status_id_action:reblog_unreblog", params: ["id"]}
- when action in ~w(reblog_status unreblog_status)a
- )
-
- plug(
- RateLimiter,
- {:status_id_action, bucket_name: "status_id_action:fav_unfav", params: ["id"]}
- when action in ~w(fav_status unfav_status)a
- )
-
- plug(
- RateLimiter,
- {:relations_id_action, params: ["id", "uri"]} when action in @rate_limited_relations_actions
- )
-
- plug(RateLimiter, :relations_actions when action in @rate_limited_relations_actions)
- plug(RateLimiter, :statuses_actions when action in @rate_limited_status_actions)
- plug(RateLimiter, :app_account_creation when action == :account_register)
- plug(RateLimiter, :search when action in [:search, :search2, :account_search])
- plug(RateLimiter, :password_reset when action == :password_reset)
- plug(RateLimiter, :account_confirmation_resend when action == :account_confirmation_resend)
-
- @local_mastodon_name "Mastodon-Local"
-
- action_fallback(:errors)
-
- def create_app(conn, params) do
- scopes = Scopes.fetch_scopes(params, ["read"])
-
- app_attrs =
- params
- |> Map.drop(["scope", "scopes"])
- |> Map.put("scopes", scopes)
-
- with cs <- App.register_changeset(%App{}, app_attrs),
- false <- cs.changes[:client_name] == @local_mastodon_name,
- {:ok, app} <- Repo.insert(cs) do
- conn
- |> put_view(AppView)
- |> render("show.json", %{app: app})
- end
- end
-
- defp add_if_present(
- map,
- params,
- params_field,
- map_field,
- value_function \\ fn x -> {:ok, x} end
- ) do
- if Map.has_key?(params, params_field) do
- case value_function.(params[params_field]) do
- {:ok, new_value} -> Map.put(map, map_field, new_value)
- :error -> map
- end
- else
- map
- end
- end
-
- defp normalize_fields_attributes(fields) do
- if Enum.all?(fields, &is_tuple/1) do
- Enum.map(fields, fn {_, v} -> v end)
- else
- fields
- end
- end
-
- def update_credentials(%{assigns: %{user: user}} = conn, params) do
- original_user = user
-
- user_params =
- %{}
- |> add_if_present(params, "display_name", :name)
- |> add_if_present(params, "note", :bio, fn value -> {:ok, User.parse_bio(value, user)} end)
- |> add_if_present(params, "avatar", :avatar, fn value ->
- with %Plug.Upload{} <- value,
- {:ok, object} <- ActivityPub.upload(value, type: :avatar) do
- {:ok, object.data}
- else
- _ -> :error
- end
- end)
-
- emojis_text = (user_params["display_name"] || "") <> (user_params["note"] || "")
-
- user_info_emojis =
- user.info
- |> Map.get(:emoji, [])
- |> Enum.concat(Formatter.get_emoji_map(emojis_text))
- |> Enum.dedup()
-
- params =
- if Map.has_key?(params, "fields_attributes") do
- Map.update!(params, "fields_attributes", fn fields ->
- fields
- |> normalize_fields_attributes()
- |> Enum.filter(fn %{"name" => n} -> n != "" end)
- end)
- else
- params
- end
-
- info_params =
- [
- :no_rich_text,
- :locked,
- :hide_followers,
- :hide_follows,
- :hide_favorites,
- :show_role,
- :skip_thread_containment
- ]
- |> Enum.reduce(%{}, fn key, acc ->
- add_if_present(acc, params, to_string(key), key, fn value ->
- {:ok, ControllerHelper.truthy_param?(value)}
- end)
- end)
- |> add_if_present(params, "default_scope", :default_scope)
- |> add_if_present(params, "fields_attributes", :fields, fn fields ->
- fields = Enum.map(fields, fn f -> Map.update!(f, "value", &AutoLinker.link(&1)) end)
-
- {:ok, fields}
- end)
- |> add_if_present(params, "fields_attributes", :raw_fields)
- |> add_if_present(params, "pleroma_settings_store", :pleroma_settings_store, fn value ->
- {:ok, Map.merge(user.info.pleroma_settings_store, value)}
- end)
- |> add_if_present(params, "header", :banner, fn value ->
- with %Plug.Upload{} <- value,
- {:ok, object} <- ActivityPub.upload(value, type: :banner) do
- {:ok, object.data}
- else
- _ -> :error
- end
- end)
- |> add_if_present(params, "pleroma_background_image", :background, fn value ->
- with %Plug.Upload{} <- value,
- {:ok, object} <- ActivityPub.upload(value, type: :background) do
- {:ok, object.data}
- else
- _ -> :error
- end
- end)
- |> Map.put(:emoji, user_info_emojis)
-
- info_cng = User.Info.profile_update(user.info, info_params)
-
- with changeset <- User.update_changeset(user, user_params),
- changeset <- Ecto.Changeset.put_embed(changeset, :info, info_cng),
- {: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, for: user, with_pleroma_settings: true})
- )
- else
- _e -> render_error(conn, :forbidden, "Invalid request")
- end
- end
-
- def update_avatar(%{assigns: %{user: user}} = conn, %{"img" => ""}) do
- change = Changeset.change(user, %{avatar: nil})
- {:ok, user} = User.update_and_set_cache(change)
- CommonAPI.update(user)
-
- json(conn, %{url: nil})
- end
-
- def update_avatar(%{assigns: %{user: user}} = conn, params) do
- {:ok, object} = ActivityPub.upload(params, type: :avatar)
- change = Changeset.change(user, %{avatar: object.data})
- {:ok, user} = User.update_and_set_cache(change)
- CommonAPI.update(user)
- %{"url" => [%{"href" => href} | _]} = object.data
-
- json(conn, %{url: href})
- end
-
- def update_banner(%{assigns: %{user: user}} = conn, %{"banner" => ""}) do
- with new_info <- %{"banner" => %{}},
- info_cng <- User.Info.profile_update(user.info, new_info),
- changeset <- Ecto.Changeset.change(user) |> Ecto.Changeset.put_embed(:info, info_cng),
- {:ok, user} <- User.update_and_set_cache(changeset) do
- CommonAPI.update(user)
-
- json(conn, %{url: nil})
- end
- end
-
- def update_banner(%{assigns: %{user: user}} = conn, params) do
- with {:ok, object} <- ActivityPub.upload(%{"img" => params["banner"]}, type: :banner),
- new_info <- %{"banner" => object.data},
- info_cng <- User.Info.profile_update(user.info, new_info),
- changeset <- Ecto.Changeset.change(user) |> Ecto.Changeset.put_embed(:info, info_cng),
- {:ok, user} <- User.update_and_set_cache(changeset) do
- CommonAPI.update(user)
- %{"url" => [%{"href" => href} | _]} = object.data
-
- json(conn, %{url: href})
- end
- end
-
- def update_background(%{assigns: %{user: user}} = conn, %{"img" => ""}) do
- with new_info <- %{"background" => %{}},
- info_cng <- User.Info.profile_update(user.info, new_info),
- changeset <- Ecto.Changeset.change(user) |> Ecto.Changeset.put_embed(:info, info_cng),
- {:ok, _user} <- User.update_and_set_cache(changeset) do
- json(conn, %{url: nil})
- end
- end
-
- def update_background(%{assigns: %{user: user}} = conn, params) do
- with {:ok, object} <- ActivityPub.upload(params, type: :background),
- new_info <- %{"background" => object.data},
- info_cng <- User.Info.profile_update(user.info, new_info),
- changeset <- Ecto.Changeset.change(user) |> Ecto.Changeset.put_embed(:info, info_cng),
- {:ok, _user} <- User.update_and_set_cache(changeset) do
- %{"url" => [%{"href" => href} | _]} = object.data
-
- json(conn, %{url: href})
- end
- end
-
- def verify_credentials(%{assigns: %{user: user}} = conn, _) do
- chat_token = Phoenix.Token.sign(conn, "user socket", user.id)
-
- account =
- AccountView.render("account.json", %{
- user: user,
- for: user,
- with_pleroma_settings: true,
- with_chat_token: chat_token
- })
-
- json(conn, account)
- end
-
- def verify_app_credentials(%{assigns: %{user: _user, token: token}} = conn, _) do
- with %Token{app: %App{} = app} <- Repo.preload(token, :app) do
- conn
- |> put_view(AppView)
- |> render("short.json", %{app: app})
- end
- end
-
- def user(%{assigns: %{user: for_user}} = conn, %{"id" => nickname_or_id}) do
- with %User{} = user <- User.get_cached_by_nickname_or_id(nickname_or_id),
- true <- User.auth_active?(user) || user.id == for_user.id || User.superuser?(for_user) do
- account = AccountView.render("account.json", %{user: user, for: for_user})
- json(conn, account)
- else
- _e -> render_error(conn, :not_found, "Can't find user")
- end
- end
-
- @mastodon_api_level "2.7.2"
-
- def masto_instance(conn, _params) do
- instance = Config.get(:instance)
-
- response = %{
- uri: Web.base_url(),
- title: Keyword.get(instance, :name),
- description: Keyword.get(instance, :description),
- version: "#{@mastodon_api_level} (compatible; #{Pleroma.Application.named_version()})",
- email: Keyword.get(instance, :email),
- urls: %{
- streaming_api: Pleroma.Web.Endpoint.websocket_url()
- },
- stats: Stats.get_stats(),
- thumbnail: Web.base_url() <> "/instance/thumbnail.jpeg",
- languages: ["en"],
- registrations: Pleroma.Config.get([:instance, :registrations_open]),
- # Extra (not present in Mastodon):
- max_toot_chars: Keyword.get(instance, :limit),
- poll_limits: Keyword.get(instance, :poll_limits)
- }
-
- json(conn, response)
- end
-
- def peers(conn, _params) do
- json(conn, Stats.get_peers())
- end
-
- defp mastodonized_emoji do
- Pleroma.Emoji.get_all()
- |> Enum.map(fn {shortcode, relative_url, tags} ->
- url = to_string(URI.merge(Web.base_url(), relative_url))
-
- %{
- "shortcode" => shortcode,
- "static_url" => url,
- "visible_in_picker" => true,
- "url" => url,
- "tags" => tags,
- # Assuming that a comma is authorized in the category name
- "category" => (tags -- ["Custom"]) |> Enum.join(",")
- }
- end)
- end
-
- def custom_emojis(conn, _params) do
- mastodon_emoji = mastodonized_emoji()
- json(conn, mastodon_emoji)
- end
-
- def home_timeline(%{assigns: %{user: user}} = conn, params) do
- params =
- params
- |> Map.put("type", ["Create", "Announce"])
- |> Map.put("blocking_user", user)
- |> Map.put("muting_user", user)
- |> Map.put("user", user)
-
- activities =
- [user.ap_id | user.following]
- |> ActivityPub.fetch_activities(params)
- |> Enum.reverse()
-
- conn
- |> add_link_headers(:home_timeline, activities)
- |> put_view(StatusView)
- |> render("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"]
-
- activities =
- params
- |> Map.put("type", ["Create", "Announce"])
- |> Map.put("local_only", local_only)
- |> Map.put("blocking_user", user)
- |> Map.put("muting_user", user)
- |> Map.put("user", user)
- |> ActivityPub.fetch_public_activities()
- |> Enum.reverse()
-
- conn
- |> add_link_headers(:public_timeline, activities, false, %{"local" => local_only})
- |> put_view(StatusView)
- |> render("index.json", %{activities: activities, for: user, as: :activity})
- end
-
- def user_statuses(%{assigns: %{user: reading_user}} = conn, params) do
- with %User{} = user <- User.get_cached_by_nickname_or_id(params["id"]) do
- params =
- params
- |> Map.put("tag", params["tagged"])
-
- activities = ActivityPub.fetch_user_activities(user, reading_user, params)
-
- conn
- |> add_link_headers(:user_statuses, activities, params["id"])
- |> put_view(StatusView)
- |> render("index.json", %{
- activities: activities,
- for: reading_user,
- as: :activity
- })
- end
- end
-
- def dm_timeline(%{assigns: %{user: user}} = conn, params) do
- params =
- params
- |> Map.put("type", "Create")
- |> Map.put("blocking_user", user)
- |> Map.put("user", user)
- |> Map.put(:visibility, "direct")
-
- activities =
- [user.ap_id]
- |> ActivityPub.fetch_activities_query(params)
- |> Pagination.fetch_paginated(params)
-
- conn
- |> add_link_headers(:dm_timeline, activities)
- |> put_view(StatusView)
- |> render("index.json", %{activities: activities, for: user, as: :activity})
- end
-
- def get_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
- with %Activity{} = activity <- Activity.get_by_id_with_object(id),
- true <- Visibility.visible_for_user?(activity, user) do
- conn
- |> put_view(StatusView)
- |> try_render("status.json", %{activity: activity, for: user})
- end
- end
-
- def get_context(%{assigns: %{user: user}} = conn, %{"id" => id}) do
- with %Activity{} = activity <- Activity.get_by_id(id),
- activities <-
- ActivityPub.fetch_activities_for_context(activity.data["context"], %{
- "blocking_user" => user,
- "user" => user,
- "exclude_id" => activity.id
- }),
- 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(),
- # credo:disable-for-previous-line Credo.Check.Refactor.PipeChainStart
- descendants:
- StatusView.render(
- "index.json",
- for: user,
- activities: grouped_activities[false] || [],
- as: :activity
- )
- |> Enum.reverse()
- # credo:disable-for-previous-line Credo.Check.Refactor.PipeChainStart
- }
-
- json(conn, result)
- end
- end
-
- def get_poll(%{assigns: %{user: user}} = conn, %{"id" => id}) do
- with %Object{} = object <- Object.get_by_id(id),
- %Activity{} = activity <- Activity.get_create_by_object_ap_id(object.data["id"]),
- true <- Visibility.visible_for_user?(activity, user) do
- conn
- |> put_view(StatusView)
- |> try_render("poll.json", %{object: object, for: user})
- else
- error when is_nil(error) or error == false ->
- render_error(conn, :not_found, "Record not found")
- end
- end
-
- defp get_cached_vote_or_vote(user, object, choices) do
- idempotency_key = "polls:#{user.id}:#{object.data["id"]}"
-
- {_, res} =
- Cachex.fetch(:idempotency_cache, idempotency_key, fn _ ->
- case CommonAPI.vote(user, object, choices) do
- {:error, _message} = res -> {:ignore, res}
- res -> {:commit, res}
- end
- end)
-
- res
- end
-
- def poll_vote(%{assigns: %{user: user}} = conn, %{"id" => id, "choices" => choices}) do
- with %Object{} = object <- Object.get_by_id(id),
- true <- object.data["type"] == "Question",
- %Activity{} = activity <- Activity.get_create_by_object_ap_id(object.data["id"]),
- true <- Visibility.visible_for_user?(activity, user),
- {:ok, _activities, object} <- get_cached_vote_or_vote(user, object, choices) do
- conn
- |> put_view(StatusView)
- |> try_render("poll.json", %{object: object, for: user})
- else
- nil ->
- render_error(conn, :not_found, "Record not found")
-
- false ->
- render_error(conn, :not_found, "Record not found")
-
- {:error, message} ->
- conn
- |> put_status(:unprocessable_entity)
- |> json(%{error: message})
- end
- end
-
- def scheduled_statuses(%{assigns: %{user: user}} = conn, params) do
- with scheduled_activities <- MastodonAPI.get_scheduled_activities(user, params) do
- conn
- |> add_link_headers(:scheduled_statuses, scheduled_activities)
- |> put_view(ScheduledActivityView)
- |> render("index.json", %{scheduled_activities: scheduled_activities})
- end
- end
-
- def show_scheduled_status(%{assigns: %{user: user}} = conn, %{"id" => scheduled_activity_id}) do
- with %ScheduledActivity{} = scheduled_activity <-
- ScheduledActivity.get(user, scheduled_activity_id) do
- conn
- |> put_view(ScheduledActivityView)
- |> render("show.json", %{scheduled_activity: scheduled_activity})
- else
- _ -> {:error, :not_found}
- end
- end
-
- def update_scheduled_status(
- %{assigns: %{user: user}} = conn,
- %{"id" => scheduled_activity_id} = params
- ) do
- with %ScheduledActivity{} = scheduled_activity <-
- ScheduledActivity.get(user, scheduled_activity_id),
- {:ok, scheduled_activity} <- ScheduledActivity.update(scheduled_activity, params) do
- conn
- |> put_view(ScheduledActivityView)
- |> render("show.json", %{scheduled_activity: scheduled_activity})
- else
- nil -> {:error, :not_found}
- error -> error
- end
- end
-
- def delete_scheduled_status(%{assigns: %{user: user}} = conn, %{"id" => scheduled_activity_id}) do
- with %ScheduledActivity{} = scheduled_activity <-
- ScheduledActivity.get(user, scheduled_activity_id),
- {:ok, scheduled_activity} <- ScheduledActivity.delete(scheduled_activity) do
- conn
- |> put_view(ScheduledActivityView)
- |> render("show.json", %{scheduled_activity: scheduled_activity})
- else
- nil -> {:error, :not_found}
- error -> error
- end
- 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"])
-
- scheduled_at = params["scheduled_at"]
-
- if scheduled_at && ScheduledActivity.far_enough?(scheduled_at) do
- with {:ok, scheduled_activity} <-
- ScheduledActivity.create(user, %{"params" => params, "scheduled_at" => scheduled_at}) do
- conn
- |> put_view(ScheduledActivityView)
- |> render("show.json", %{scheduled_activity: scheduled_activity})
- end
- else
- params = Map.drop(params, ["scheduled_at"])
-
- case CommonAPI.post(user, params) do
- {:error, message} ->
- conn
- |> put_status(:unprocessable_entity)
- |> json(%{error: message})
-
- {:ok, activity} ->
- conn
- |> put_view(StatusView)
- |> try_render("status.json", %{activity: activity, for: user, as: :activity})
- end
- end
- end
-
- def delete_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
- with {:ok, %Activity{}} <- CommonAPI.delete(id, user) do
- json(conn, %{})
- else
- _e -> render_error(conn, :forbidden, "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),
- %Activity{} = announce <- Activity.normalize(announce.data) do
- conn
- |> put_view(StatusView)
- |> try_render("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_by_object_ap_id_with_object(id) do
- conn
- |> put_view(StatusView)
- |> try_render("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_by_object_ap_id(id) do
- conn
- |> put_view(StatusView)
- |> try_render("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_by_object_ap_id(id) do
- conn
- |> put_view(StatusView)
- |> try_render("status.json", %{activity: activity, for: user, as: :activity})
- end
- end
-
- def pin_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
- with {:ok, activity} <- CommonAPI.pin(ap_id_or_id, user) do
- conn
- |> put_view(StatusView)
- |> try_render("status.json", %{activity: activity, for: user, as: :activity})
- end
- end
-
- def unpin_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
- with {:ok, activity} <- CommonAPI.unpin(ap_id_or_id, user) do
- conn
- |> put_view(StatusView)
- |> try_render("status.json", %{activity: activity, for: user, as: :activity})
- end
- end
-
- def bookmark_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
- with %Activity{} = activity <- Activity.get_by_id_with_object(id),
- %User{} = user <- User.get_cached_by_nickname(user.nickname),
- true <- Visibility.visible_for_user?(activity, user),
- {:ok, _bookmark} <- Bookmark.create(user.id, activity.id) do
- conn
- |> put_view(StatusView)
- |> try_render("status.json", %{activity: activity, for: user, as: :activity})
- end
- end
-
- def unbookmark_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
- with %Activity{} = activity <- Activity.get_by_id_with_object(id),
- %User{} = user <- User.get_cached_by_nickname(user.nickname),
- true <- Visibility.visible_for_user?(activity, user),
- {:ok, _bookmark} <- Bookmark.destroy(user.id, activity.id) do
- conn
- |> put_view(StatusView)
- |> try_render("status.json", %{activity: activity, for: user, as: :activity})
- end
- end
-
- def mute_conversation(%{assigns: %{user: user}} = conn, %{"id" => id}) do
- activity = Activity.get_by_id(id)
-
- with {:ok, activity} <- CommonAPI.add_mute(user, activity) do
- conn
- |> put_view(StatusView)
- |> try_render("status.json", %{activity: activity, for: user, as: :activity})
- end
- end
-
- def unmute_conversation(%{assigns: %{user: user}} = conn, %{"id" => id}) do
- activity = Activity.get_by_id(id)
-
- with {:ok, activity} <- CommonAPI.remove_mute(user, activity) do
- conn
- |> put_view(StatusView)
- |> try_render("status.json", %{activity: activity, for: user, as: :activity})
- end
- end
-
- def notifications(%{assigns: %{user: user}} = conn, params) do
- notifications = MastodonAPI.get_notifications(user, params)
-
- conn
- |> add_link_headers(:notifications, notifications)
- |> put_view(NotificationView)
- |> render("index.json", %{notifications: notifications, for: user})
- end
-
- def get_notification(%{assigns: %{user: user}} = conn, %{"id" => id} = _params) do
- with {:ok, notification} <- Notification.get(user, id) do
- conn
- |> put_view(NotificationView)
- |> render("show.json", %{notification: notification, for: user})
- else
- {:error, reason} ->
- conn
- |> put_status(:forbidden)
- |> json(%{"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_status(:forbidden)
- |> json(%{"error" => reason})
- end
- end
-
- def destroy_multiple(%{assigns: %{user: user}} = conn, %{"ids" => ids} = _params) do
- Notification.destroy_multiple(user, ids)
- json(conn, %{})
- 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)
-
- conn
- |> put_view(AccountView)
- |> render("relationships.json", %{user: user, targets: targets})
- end
-
- # Instead of returning a 400 when no "id" params is present, Mastodon returns an empty array.
- def relationships(%{assigns: %{user: _user}} = conn, _), do: json(conn, [])
-
- def update_media(%{assigns: %{user: user}} = conn, data) do
- with %Object{} = object <- Repo.get(Object, data["id"]),
- true <- Object.authorize_mutation(object, user),
- true <- is_binary(data["description"]),
- description <- data["description"] do
- new_data = %{object.data | "name" => description}
-
- {:ok, _} =
- object
- |> Object.change(%{data: new_data})
- |> Repo.update()
-
- attachment_data = Map.put(new_data, "id", object.id)
-
- conn
- |> put_view(StatusView)
- |> render("attachment.json", %{attachment: attachment_data})
- end
- end
-
- def upload(%{assigns: %{user: user}} = conn, %{"file" => file} = data) do
- with {:ok, object} <-
- ActivityPub.upload(
- file,
- actor: User.ap_id(user),
- description: Map.get(data, "description")
- ) do
- attachment_data = Map.put(object.data, "id", object.id)
-
- conn
- |> put_view(StatusView)
- |> render("attachment.json", %{attachment: attachment_data})
- end
- end
-
- def set_mascot(%{assigns: %{user: user}} = conn, %{"file" => file}) do
- with {:ok, object} <- ActivityPub.upload(file, actor: User.ap_id(user)),
- %{} = attachment_data <- Map.put(object.data, "id", object.id),
- %{type: type} = rendered <-
- StatusView.render("attachment.json", %{attachment: attachment_data}) do
- # Reject if not an image
- if type == "image" do
- # Sure!
- # Save to the user's info
- info_changeset = User.Info.mascot_update(user.info, rendered)
-
- user_changeset =
- user
- |> Ecto.Changeset.change()
- |> Ecto.Changeset.put_embed(:info, info_changeset)
-
- {:ok, _user} = User.update_and_set_cache(user_changeset)
-
- conn
- |> json(rendered)
- else
- render_error(conn, :unsupported_media_type, "mascots can only be images")
- end
- end
- end
-
- def get_mascot(%{assigns: %{user: user}} = conn, _params) do
- mascot = User.get_mascot(user)
-
- conn
- |> json(mascot)
- end
-
- def favourited_by(%{assigns: %{user: user}} = conn, %{"id" => id}) do
- with %Activity{} = activity <- Activity.get_by_id_with_object(id),
- %Object{data: %{"likes" => likes}} <- Object.normalize(activity) do
- q = from(u in User, where: u.ap_id in ^likes)
-
- users =
- Repo.all(q)
- |> Enum.filter(&(not User.blocks?(user, &1)))
-
- conn
- |> put_view(AccountView)
- |> render("accounts.json", %{for: user, users: users, as: :user})
- else
- _ -> json(conn, [])
- end
- end
-
- def reblogged_by(%{assigns: %{user: user}} = conn, %{"id" => id}) do
- with %Activity{} = activity <- Activity.get_by_id_with_object(id),
- %Object{data: %{"announcements" => announces}} <- Object.normalize(activity) do
- q = from(u in User, where: u.ap_id in ^announces)
-
- users =
- Repo.all(q)
- |> Enum.filter(&(not User.blocks?(user, &1)))
-
- conn
- |> put_view(AccountView)
- |> render("accounts.json", %{for: user, 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"]
-
- tags =
- [params["tag"], params["any"]]
- |> List.flatten()
- |> Enum.uniq()
- |> Enum.filter(& &1)
- |> Enum.map(&String.downcase(&1))
-
- tag_all =
- params["all"] ||
- []
- |> Enum.map(&String.downcase(&1))
-
- tag_reject =
- params["none"] ||
- []
- |> Enum.map(&String.downcase(&1))
-
- activities =
- params
- |> Map.put("type", "Create")
- |> Map.put("local_only", local_only)
- |> Map.put("blocking_user", user)
- |> Map.put("muting_user", user)
- |> Map.put("user", user)
- |> Map.put("tag", tags)
- |> Map.put("tag_all", tag_all)
- |> Map.put("tag_reject", tag_reject)
- |> ActivityPub.fetch_public_activities()
- |> Enum.reverse()
-
- conn
- |> add_link_headers(:hashtag_timeline, activities, params["tag"], %{"local" => local_only})
- |> put_view(StatusView)
- |> render("index.json", %{activities: activities, for: user, as: :activity})
- end
-
- def followers(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
- with %User{} = user <- User.get_cached_by_id(id),
- followers <- MastodonAPI.get_followers(user, params) do
- followers =
- cond do
- for_user && user.id == for_user.id -> followers
- user.info.hide_followers -> []
- true -> followers
- end
-
- conn
- |> add_link_headers(:followers, followers, user)
- |> put_view(AccountView)
- |> render("accounts.json", %{for: for_user, users: followers, as: :user})
- end
- end
-
- def following(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
- with %User{} = user <- User.get_cached_by_id(id),
- followers <- MastodonAPI.get_friends(user, params) do
- followers =
- cond do
- for_user && user.id == for_user.id -> followers
- user.info.hide_follows -> []
- true -> followers
- end
-
- conn
- |> add_link_headers(:following, followers, user)
- |> put_view(AccountView)
- |> render("accounts.json", %{for: for_user, 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
- conn
- |> put_view(AccountView)
- |> render("accounts.json", %{for: followed, users: follow_requests, as: :user})
- end
- end
-
- def authorize_follow_request(%{assigns: %{user: followed}} = conn, %{"id" => id}) do
- with %User{} = follower <- User.get_cached_by_id(id),
- {:ok, follower} <- CommonAPI.accept_follow_request(follower, followed) do
- conn
- |> put_view(AccountView)
- |> render("relationship.json", %{user: followed, target: follower})
- else
- {:error, message} ->
- conn
- |> put_status(:forbidden)
- |> json(%{error: message})
- end
- end
-
- def reject_follow_request(%{assigns: %{user: followed}} = conn, %{"id" => id}) do
- with %User{} = follower <- User.get_cached_by_id(id),
- {:ok, follower} <- CommonAPI.reject_follow_request(follower, followed) do
- conn
- |> put_view(AccountView)
- |> render("relationship.json", %{user: followed, target: follower})
- else
- {:error, message} ->
- conn
- |> put_status(:forbidden)
- |> json(%{error: message})
- end
- end
-
- def follow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
- with {_, %User{} = followed} <- {:followed, User.get_cached_by_id(id)},
- {_, true} <- {:followed, follower.id != followed.id},
- {:ok, follower} <- MastodonAPI.follow(follower, followed, conn.params) do
- conn
- |> put_view(AccountView)
- |> render("relationship.json", %{user: follower, target: followed})
- else
- {:followed, _} ->
- {:error, :not_found}
-
- {:error, message} ->
- conn
- |> put_status(:forbidden)
- |> json(%{error: message})
- end
- end
-
- def follow(%{assigns: %{user: follower}} = conn, %{"uri" => uri}) do
- with {_, %User{} = followed} <- {:followed, User.get_cached_by_nickname(uri)},
- {_, true} <- {:followed, follower.id != followed.id},
- {:ok, follower, followed, _} <- CommonAPI.follow(follower, followed) do
- conn
- |> put_view(AccountView)
- |> render("account.json", %{user: followed, for: follower})
- else
- {:followed, _} ->
- {:error, :not_found}
-
- {:error, message} ->
- conn
- |> put_status(:forbidden)
- |> json(%{error: message})
- end
- end
-
- def unfollow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
- with {_, %User{} = followed} <- {:followed, User.get_cached_by_id(id)},
- {_, true} <- {:followed, follower.id != followed.id},
- {:ok, follower} <- CommonAPI.unfollow(follower, followed) do
- conn
- |> put_view(AccountView)
- |> render("relationship.json", %{user: follower, target: followed})
- else
- {:followed, _} ->
- {:error, :not_found}
-
- error ->
- error
- end
- end
-
- def mute(%{assigns: %{user: muter}} = conn, %{"id" => id} = params) do
- notifications =
- if Map.has_key?(params, "notifications"),
- do: params["notifications"] in [true, "True", "true", "1"],
- else: true
-
- with %User{} = muted <- User.get_cached_by_id(id),
- {:ok, muter} <- User.mute(muter, muted, notifications) do
- conn
- |> put_view(AccountView)
- |> render("relationship.json", %{user: muter, target: muted})
- else
- {:error, message} ->
- conn
- |> put_status(:forbidden)
- |> json(%{error: message})
- end
- end
-
- def unmute(%{assigns: %{user: muter}} = conn, %{"id" => id}) do
- with %User{} = muted <- User.get_cached_by_id(id),
- {:ok, muter} <- User.unmute(muter, muted) do
- conn
- |> put_view(AccountView)
- |> render("relationship.json", %{user: muter, target: muted})
- else
- {:error, message} ->
- conn
- |> put_status(:forbidden)
- |> json(%{error: message})
- end
- end
-
- def mutes(%{assigns: %{user: user}} = conn, _) do
- with muted_accounts <- User.muted_users(user) do
- res = AccountView.render("accounts.json", users: muted_accounts, for: user, as: :user)
- json(conn, res)
- end
- end
-
- def block(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
- with %User{} = blocked <- User.get_cached_by_id(id),
- {:ok, blocker} <- User.block(blocker, blocked),
- {:ok, _activity} <- ActivityPub.block(blocker, blocked) do
- conn
- |> put_view(AccountView)
- |> render("relationship.json", %{user: blocker, target: blocked})
- else
- {:error, message} ->
- conn
- |> put_status(:forbidden)
- |> json(%{error: message})
- end
- end
-
- def unblock(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
- with %User{} = blocked <- User.get_cached_by_id(id),
- {:ok, blocker} <- User.unblock(blocker, blocked),
- {:ok, _activity} <- ActivityPub.unblock(blocker, blocked) do
- conn
- |> put_view(AccountView)
- |> render("relationship.json", %{user: blocker, target: blocked})
- else
- {:error, message} ->
- conn
- |> put_status(:forbidden)
- |> json(%{error: message})
- end
- end
-
- def blocks(%{assigns: %{user: user}} = conn, _) do
- with blocked_accounts <- User.blocked_users(user) do
- res = AccountView.render("accounts.json", users: blocked_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 subscribe(%{assigns: %{user: user}} = conn, %{"id" => id}) do
- with %User{} = subscription_target <- User.get_cached_by_id(id),
- {:ok, subscription_target} = User.subscribe(user, subscription_target) do
- conn
- |> put_view(AccountView)
- |> render("relationship.json", %{user: user, target: subscription_target})
- else
- {:error, message} ->
- conn
- |> put_status(:forbidden)
- |> json(%{error: message})
- end
- end
-
- def unsubscribe(%{assigns: %{user: user}} = conn, %{"id" => id}) do
- with %User{} = subscription_target <- User.get_cached_by_id(id),
- {:ok, subscription_target} = User.unsubscribe(user, subscription_target) do
- conn
- |> put_view(AccountView)
- |> render("relationship.json", %{user: user, target: subscription_target})
- else
- {:error, message} ->
- conn
- |> put_status(:forbidden)
- |> json(%{error: message})
- end
- end
-
- def favourites(%{assigns: %{user: user}} = conn, params) do
- params =
- params
- |> Map.put("type", "Create")
- |> Map.put("favorited_by", user.ap_id)
- |> Map.put("blocking_user", user)
-
- activities =
- ActivityPub.fetch_activities([], params)
- |> Enum.reverse()
-
- conn
- |> add_link_headers(:favourites, activities)
- |> put_view(StatusView)
- |> render("index.json", %{activities: activities, for: user, as: :activity})
- end
-
- def user_favourites(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
- with %User{} = user <- User.get_by_id(id),
- false <- user.info.hide_favorites do
- params =
- params
- |> Map.put("type", "Create")
- |> Map.put("favorited_by", user.ap_id)
- |> Map.put("blocking_user", for_user)
-
- recipients =
- if for_user do
- [Pleroma.Constants.as_public()] ++ [for_user.ap_id | for_user.following]
- else
- [Pleroma.Constants.as_public()]
- end
-
- activities =
- recipients
- |> ActivityPub.fetch_activities(params)
- |> Enum.reverse()
-
- conn
- |> add_link_headers(:favourites, activities)
- |> put_view(StatusView)
- |> render("index.json", %{activities: activities, for: for_user, as: :activity})
- else
- nil -> {:error, :not_found}
- true -> render_error(conn, :forbidden, "Can't get favorites")
- end
- end
-
- def bookmarks(%{assigns: %{user: user}} = conn, params) do
- user = User.get_cached_by_id(user.id)
-
- bookmarks =
- Bookmark.for_user_query(user.id)
- |> Pagination.fetch_paginated(params)
-
- activities =
- bookmarks
- |> Enum.map(fn b -> Map.put(b.activity, :bookmark, Map.delete(b, :activity)) end)
-
- conn
- |> add_link_headers(:bookmarks, bookmarks)
- |> put_view(StatusView)
- |> render("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 -> render_error(conn, :not_found, "Record not found")
- 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
- json(conn, %{})
- else
- _e ->
- json(conn, dgettext("errors", "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 <- User.get_cached_by_id(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 <- User.get_cached_by_id(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
- conn
- |> put_view(AccountView)
- |> render("accounts.json", %{for: user, 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, dgettext("errors", "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)
- |> Map.put("user", user)
- |> Map.put("muting_user", user)
-
- # 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).
- activities =
- following
- |> Enum.filter(fn x -> x in user.following end)
- |> ActivityPub.fetch_activities_bounded(following, params)
- |> Enum.reverse()
-
- conn
- |> put_view(StatusView)
- |> render("index.json", %{activities: activities, for: user, as: :activity})
- else
- _e -> render_error(conn, :forbidden, "Error.")
- end
- end
-
- def index(%{assigns: %{user: user}} = conn, _params) do
- token = get_session(conn, :oauth_token)
-
- if user && token do
- mastodon_emoji = mastodonized_emoji()
-
- limit = Config.get([:instance, :limit])
-
- accounts =
- Map.put(%{}, user.id, AccountView.render("account.json", %{user: user, for: user}))
-
- initial_state =
- %{
- meta: %{
- streaming_api_base_url: Pleroma.Web.Endpoint.websocket_url(),
- 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,
- display_sensitive_media: false,
- reduce_motion: false,
- max_toot_chars: limit,
- mascot: User.get_mascot(user)["url"]
- },
- poll_limits: Config.get([:instance, :poll_limits]),
- rights: %{
- delete_others_notice: present?(user.info.is_moderator),
- admin: present?(user.info.is_admin)
- },
- compose: %{
- me: "#{user.id}",
- default_privacy: user.info.default_scope,
- default_sensitive: false,
- allow_content_types: Config.get([:instance, :allowed_post_formats])
- },
- media_attachments: %{
- accept_content_types: [
- ".jpg",
- ".jpeg",
- ".png",
- ".gif",
- ".webm",
- ".mp4",
- ".m4v",
- "image\/jpeg",
- "image\/png",
- "image\/gif",
- "video\/webm",
- "video\/mp4"
- ]
- },
- settings:
- 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: limit
- }
- |> Jason.encode!()
-
- conn
- |> put_layout(false)
- |> put_view(MastodonView)
- |> render("index.html", %{initial_state: initial_state})
- else
- conn
- |> put_session(:return_to, conn.request_path)
- |> redirect(to: "/web/login")
- end
- end
-
- def put_settings(%{assigns: %{user: user}} = conn, %{"data" => settings} = _params) do
- info_cng = User.Info.mastodon_settings_update(user.info, settings)
-
- with changeset <- Ecto.Changeset.change(user),
- changeset <- Ecto.Changeset.put_embed(changeset, :info, info_cng),
- {:ok, _user} <- User.update_and_set_cache(changeset) do
- json(conn, %{})
- else
- e ->
- conn
- |> put_status(:internal_server_error)
- |> json(%{error: inspect(e)})
- end
- end
-
- def login(%{assigns: %{user: %User{}}} = conn, _params) do
- redirect(conn, to: local_mastodon_root_path(conn))
- end
-
- @doc "Local Mastodon FE login init action"
- def login(conn, %{"code" => auth_token}) do
- with {:ok, app} <- get_or_make_app(),
- %Authorization{} = auth <- Repo.get_by(Authorization, token: auth_token, app_id: app.id),
- {:ok, token} <- Token.exchange_token(app, auth) do
- conn
- |> put_session(:oauth_token, token.token)
- |> redirect(to: local_mastodon_root_path(conn))
- end
- end
-
- @doc "Local Mastodon FE callback action"
- def login(conn, _) do
- with {:ok, app} <- get_or_make_app() do
- path =
- o_auth_path(
- conn,
- :authorize,
- response_type: "code",
- client_id: app.client_id,
- redirect_uri: ".",
- scope: Enum.join(app.scopes, " ")
- )
-
- redirect(conn, to: path)
- end
- end
-
- defp local_mastodon_root_path(conn) do
- case get_session(conn, :return_to) do
- nil ->
- mastodon_api_path(conn, :index, ["getting-started"])
-
- return_to ->
- delete_session(conn, :return_to)
- return_to
- end
- end
-
- defp get_or_make_app do
- find_attrs = %{client_name: @local_mastodon_name, redirect_uris: "."}
- scopes = ["read", "write", "follow", "push"]
-
- with %App{} = app <- Repo.get_by(App, find_attrs) do
- {:ok, app} =
- if app.scopes == scopes do
- {:ok, app}
- else
- app
- |> Ecto.Changeset.change(%{scopes: scopes})
- |> Repo.update()
- end
-
- {:ok, app}
- else
- _e ->
- cs =
- App.register_changeset(
- %App{},
- Map.put(find_attrs, :scopes, scopes)
- )
-
- Repo.insert(cs)
- 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 <- User.get_cached_by_id(id) do
- conn
- |> put_view(AccountView)
- |> render("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 get_filters(%{assigns: %{user: user}} = conn, _) do
- filters = 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 = %Filter{
- user_id: user.id,
- phrase: phrase,
- context: context,
- hide: Map.get(params, "irreversible", false),
- whole_word: Map.get(params, "boolean", true)
- # expires_at
- }
-
- {:ok, response} = 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 = 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 = %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} = 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 = %Filter{
- user_id: user.id,
- filter_id: filter_id
- }
-
- {:ok, _} = Filter.delete(query)
- json(conn, %{})
- end
-
- # fallback action
- #
- def errors(conn, {:error, %Changeset{} = changeset}) do
- error_message =
- changeset
- |> Changeset.traverse_errors(fn {message, _opt} -> message end)
- |> Enum.map_join(", ", fn {_k, v} -> v end)
-
- conn
- |> put_status(:unprocessable_entity)
- |> json(%{error: error_message})
- end
-
- def errors(conn, {:error, :not_found}) do
- render_error(conn, :not_found, "Record not found")
- end
-
- def errors(conn, {:error, error_message}) do
- conn
- |> put_status(:bad_request)
- |> json(%{error: error_message})
- end
-
- def errors(conn, _) do
- conn
- |> put_status(:internal_server_error)
- |> json(dgettext("errors", "Something went wrong"))
- end
-
- def suggestions(%{assigns: %{user: user}} = conn, _) do
- suggestions = Config.get(:suggestions)
-
- 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 = Config.get([Pleroma.Web.Endpoint, :url, :host])
-
- user = user.nickname
-
- url =
- api
- |> String.replace("{{host}}", host)
- |> String.replace("{{user}}", user)
-
- with {:ok, %{status: 200, body: body}} <-
- HTTP.get(url, [], adapter: [recv_timeout: timeout, pool: :default]),
- {:ok, data} <- Jason.decode(body) do
- data =
- data
- |> Enum.slice(0, limit)
- |> Enum.map(fn x ->
- x
- |> Map.put("id", fetch_suggestion_id(x))
- |> Map.put("avatar", MediaProxy.url(x["avatar"]))
- |> Map.put("avatar_static", MediaProxy.url(x["avatar_static"]))
- end)
-
- json(conn, data)
- else
- e ->
- Logger.error("Could not retrieve suggestions at fetch #{url}, #{inspect(e)}")
- end
- else
- json(conn, [])
- end
- end
-
- defp fetch_suggestion_id(attrs) do
- case User.get_or_fetch(attrs["acct"]) do
- {:ok, %User{id: id}} -> id
- _ -> 0
- end
- end
-
- def status_card(%{assigns: %{user: user}} = conn, %{"id" => status_id}) do
- with %Activity{} = activity <- Activity.get_by_id(status_id),
- true <- Visibility.visible_for_user?(activity, user) do
- data =
- StatusView.render(
- "card.json",
- Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity)
- )
-
- json(conn, data)
- else
- _e ->
- %{}
- end
- end
-
- def reports(%{assigns: %{user: user}} = conn, params) do
- case CommonAPI.report(user, params) do
- {:ok, activity} ->
- conn
- |> put_view(ReportView)
- |> try_render("report.json", %{activity: activity})
-
- {:error, err} ->
- conn
- |> put_status(:bad_request)
- |> json(%{error: err})
- end
- end
-
- def account_register(
- %{assigns: %{app: app}} = conn,
- %{"username" => nickname, "email" => _, "password" => _, "agreement" => true} = params
- ) do
- params =
- params
- |> Map.take([
- "email",
- "captcha_solution",
- "captcha_token",
- "captcha_answer_data",
- "token",
- "password"
- ])
- |> Map.put("nickname", nickname)
- |> Map.put("fullname", params["fullname"] || nickname)
- |> Map.put("bio", params["bio"] || "")
- |> Map.put("confirm", params["password"])
-
- with {:ok, user} <- TwitterAPI.register_user(params, need_confirmation: true),
- {:ok, token} <- Token.create_token(app, user, %{scopes: app.scopes}) do
- json(conn, %{
- token_type: "Bearer",
- access_token: token.token,
- scope: app.scopes,
- created_at: Token.Utils.format_created_at(token)
- })
- else
- {:error, errors} ->
- conn
- |> put_status(:bad_request)
- |> json(errors)
- end
- end
-
- def account_register(%{assigns: %{app: _app}} = conn, _params) do
- render_error(conn, :bad_request, "Missing parameters")
- end
-
- def account_register(conn, _) do
- render_error(conn, :forbidden, "Invalid credentials")
- end
-
- def conversations(%{assigns: %{user: user}} = conn, params) do
- participations = Participation.for_user_with_last_activity_id(user, params)
-
- conversations =
- Enum.map(participations, fn participation ->
- ConversationView.render("participation.json", %{participation: participation, for: user})
- end)
-
- conn
- |> add_link_headers(:conversations, participations)
- |> json(conversations)
- end
-
- def conversation_read(%{assigns: %{user: user}} = conn, %{"id" => participation_id}) do
- with %Participation{} = participation <-
- Repo.get_by(Participation, id: participation_id, user_id: user.id),
- {:ok, participation} <- Participation.mark_as_read(participation) do
- participation_view =
- ConversationView.render("participation.json", %{participation: participation, for: user})
-
- conn
- |> json(participation_view)
- end
- end
-
- def password_reset(conn, params) do
- nickname_or_email = params["email"] || params["nickname"]
-
- with {:ok, _} <- TwitterAPI.password_reset(nickname_or_email) do
- conn
- |> put_status(:no_content)
- |> json("")
- else
- {:error, "unknown user"} ->
- send_resp(conn, :not_found, "")
-
- {:error, _} ->
- send_resp(conn, :bad_request, "")
- end
- end
-
- def account_confirmation_resend(conn, params) do
- nickname_or_email = params["email"] || params["nickname"]
-
- with %User{} = user <- User.get_by_nickname_or_email(nickname_or_email),
- {:ok, _} <- User.try_send_confirmation_email(user) do
- conn
- |> json_response(:no_content, "")
- end
- end
-
- def try_render(conn, target, params)
- when is_binary(target) do
- case render(conn, target, params) do
- nil -> render_error(conn, :not_implemented, "Can't display this activity")
- res -> res
- end
- end
-
- def try_render(conn, _, _) do
- render_error(conn, :not_implemented, "Can't display this activity")
- end
-
- defp present?(nil), do: false
- defp present?(false), do: false
- defp present?(_), do: true
-end
diff --git a/lib/pleroma/web/mastodon_api/views/account_view.ex b/lib/pleroma/web/mastodon_api/views/account_view.ex
index 169116d0d..99169ef95 100644
--- a/lib/pleroma/web/mastodon_api/views/account_view.ex
+++ b/lib/pleroma/web/mastodon_api/views/account_view.ex
@@ -11,15 +11,15 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do
alias Pleroma.Web.MastodonAPI.AccountView
alias Pleroma.Web.MediaProxy
- def render("accounts.json", %{users: users} = opts) do
+ def render("index.json", %{users: users} = opts) do
users
- |> render_many(AccountView, "account.json", opts)
+ |> render_many(AccountView, "show.json", opts)
|> Enum.filter(&Enum.any?/1)
end
- def render("account.json", %{user: user} = opts) do
+ def render("show.json", %{user: user} = opts) do
if User.visible_for?(user, opts[:for]),
- do: do_render("account.json", opts),
+ do: do_render("show.json", opts),
else: %{}
end
@@ -66,7 +66,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do
render_many(targets, AccountView, "relationship.json", user: user, as: :target)
end
- defp do_render("account.json", %{user: user} = opts) do
+ defp do_render("show.json", %{user: user} = opts) do
display_name = HTML.strip_tags(user.name || user.nickname)
image = User.avatar_url(user) |> MediaProxy.url()
@@ -74,10 +74,18 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do
user_info = User.get_cached_user_info(user)
following_count =
- ((!user.info.hide_follows or opts[:for] == user) && user_info.following_count) || 0
+ if !user.info.hide_follows_count or !user.info.hide_follows or opts[:for] == user do
+ user_info.following_count
+ else
+ 0
+ end
followers_count =
- ((!user.info.hide_followers or opts[:for] == user) && user_info.follower_count) || 0
+ if !user.info.hide_followers_count or !user.info.hide_followers or opts[:for] == user do
+ user_info.follower_count
+ else
+ 0
+ end
bot = (user.info.source_data["type"] || "Person") in ["Application", "Service"]
@@ -108,6 +116,8 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do
bio = HTML.filter_tags(user.bio, User.html_filter_policy(opts[:for]))
relationship = render("relationship.json", %{user: opts[:for], target: user})
+ discoverable = user.info.discoverable
+
%{
id: to_string(user.id),
username: username_from_nickname(user.nickname),
@@ -131,13 +141,17 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do
note: HTML.strip_tags((user.bio || "") |> String.replace("<br>", "\n")),
sensitive: false,
fields: raw_fields,
- pleroma: %{}
+ pleroma: %{
+ discoverable: discoverable
+ }
},
# Pleroma extension
pleroma: %{
confirmation_pending: user_info.confirmation_pending,
tags: user.tags,
+ hide_followers_count: user.info.hide_followers_count,
+ hide_follows_count: user.info.hide_follows_count,
hide_followers: user.info.hide_followers,
hide_follows: user.info.hide_follows,
hide_favorites: user.info.hide_favorites,
@@ -152,6 +166,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do
|> maybe_put_settings_store(user, opts[:for], opts)
|> maybe_put_chat_token(user, opts[:for], opts)
|> maybe_put_activation_status(user, opts[:for])
+ |> maybe_put_follow_requests_count(user, opts[:for])
end
defp username_from_nickname(string) when is_binary(string) do
@@ -160,6 +175,21 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do
defp username_from_nickname(_), do: nil
+ defp maybe_put_follow_requests_count(
+ data,
+ %User{id: user_id} = user,
+ %User{id: user_id}
+ ) do
+ count =
+ User.get_follow_requests(user)
+ |> length()
+
+ data
+ |> Kernel.put_in([:follow_requests_count], count)
+ end
+
+ defp maybe_put_follow_requests_count(data, _, _), do: data
+
defp maybe_put_settings(
data,
%User{id: user_id} = user,
diff --git a/lib/pleroma/web/mastodon_api/views/conversation_view.ex b/lib/pleroma/web/mastodon_api/views/conversation_view.ex
index 40acc07b3..e9d2735b3 100644
--- a/lib/pleroma/web/mastodon_api/views/conversation_view.ex
+++ b/lib/pleroma/web/mastodon_api/views/conversation_view.ex
@@ -11,6 +11,10 @@ defmodule Pleroma.Web.MastodonAPI.ConversationView do
alias Pleroma.Web.MastodonAPI.AccountView
alias Pleroma.Web.MastodonAPI.StatusView
+ def render("participations.json", %{participations: participations, for: user}) do
+ render_many(participations, __MODULE__, "participation.json", as: :participation, for: user)
+ end
+
def render("participation.json", %{participation: participation, for: user}) do
participation = Repo.preload(participation, conversation: [], recipients: [])
@@ -23,25 +27,14 @@ defmodule Pleroma.Web.MastodonAPI.ConversationView do
end
activity = Activity.get_by_id_with_object(last_activity_id)
-
- last_status = StatusView.render("status.json", %{activity: activity, for: user})
-
# Conversations return all users except the current user.
- users =
- participation.recipients
- |> Enum.reject(&(&1.id == user.id))
-
- accounts =
- AccountView.render("accounts.json", %{
- users: users,
- as: :user
- })
+ users = Enum.reject(participation.recipients, &(&1.id == user.id))
%{
id: participation.id |> to_string(),
- accounts: accounts,
+ accounts: render(AccountView, "index.json", users: users, as: :user),
unread: !participation.read,
- last_status: last_status
+ last_status: render(StatusView, "show.json", activity: activity, for: user)
}
end
end
diff --git a/lib/pleroma/web/mastodon_api/views/list_view.ex b/lib/pleroma/web/mastodon_api/views/list_view.ex
index 0f86e2512..bfda6f5b3 100644
--- a/lib/pleroma/web/mastodon_api/views/list_view.ex
+++ b/lib/pleroma/web/mastodon_api/views/list_view.ex
@@ -6,11 +6,11 @@ defmodule Pleroma.Web.MastodonAPI.ListView do
use Pleroma.Web, :view
alias Pleroma.Web.MastodonAPI.ListView
- def render("lists.json", %{lists: lists} = opts) do
- render_many(lists, ListView, "list.json", opts)
+ def render("index.json", %{lists: lists} = opts) do
+ render_many(lists, ListView, "show.json", opts)
end
- def render("list.json", %{list: list}) do
+ def render("show.json", %{list: list}) do
%{
id: to_string(list.id),
title: list.title
diff --git a/lib/pleroma/web/mastodon_api/views/notification_view.ex b/lib/pleroma/web/mastodon_api/views/notification_view.ex
index 27e9cab06..60b58dc90 100644
--- a/lib/pleroma/web/mastodon_api/views/notification_view.ex
+++ b/lib/pleroma/web/mastodon_api/views/notification_view.ex
@@ -14,7 +14,7 @@ defmodule Pleroma.Web.MastodonAPI.NotificationView do
alias Pleroma.Web.MastodonAPI.StatusView
def render("index.json", %{notifications: notifications, for: user}) do
- render_many(notifications, NotificationView, "show.json", %{for: user})
+ safe_render_many(notifications, NotificationView, "show.json", %{for: user})
end
def render("show.json", %{
@@ -29,7 +29,7 @@ defmodule Pleroma.Web.MastodonAPI.NotificationView do
id: to_string(notification.id),
type: mastodon_type,
created_at: CommonAPI.Utils.to_masto_date(notification.inserted_at),
- account: AccountView.render("account.json", %{user: actor, for: user}),
+ account: AccountView.render("show.json", %{user: actor, for: user}),
pleroma: %{
is_seen: notification.seen
}
@@ -39,19 +39,19 @@ defmodule Pleroma.Web.MastodonAPI.NotificationView do
"mention" ->
response
|> Map.merge(%{
- status: StatusView.render("status.json", %{activity: activity, for: user})
+ status: StatusView.render("show.json", %{activity: activity, for: user})
})
"favourite" ->
response
|> Map.merge(%{
- status: StatusView.render("status.json", %{activity: parent_activity, for: user})
+ status: StatusView.render("show.json", %{activity: parent_activity, for: user})
})
"reblog" ->
response
|> Map.merge(%{
- status: StatusView.render("status.json", %{activity: parent_activity, for: user})
+ status: StatusView.render("show.json", %{activity: parent_activity, for: user})
})
"follow" ->
diff --git a/lib/pleroma/web/mastodon_api/views/report_view.ex b/lib/pleroma/web/mastodon_api/views/report_view.ex
index a16e7ff10..9da2dd740 100644
--- a/lib/pleroma/web/mastodon_api/views/report_view.ex
+++ b/lib/pleroma/web/mastodon_api/views/report_view.ex
@@ -5,7 +5,7 @@
defmodule Pleroma.Web.MastodonAPI.ReportView do
use Pleroma.Web, :view
- def render("report.json", %{activity: activity}) do
+ def render("show.json", %{activity: activity}) do
%{
id: to_string(activity.id),
action_taken: false
diff --git a/lib/pleroma/web/mastodon_api/views/scheduled_activity_view.ex b/lib/pleroma/web/mastodon_api/views/scheduled_activity_view.ex
index 0aae15ab9..fc042a276 100644
--- a/lib/pleroma/web/mastodon_api/views/scheduled_activity_view.ex
+++ b/lib/pleroma/web/mastodon_api/views/scheduled_activity_view.ex
@@ -7,11 +7,10 @@ defmodule Pleroma.Web.MastodonAPI.ScheduledActivityView do
alias Pleroma.ScheduledActivity
alias Pleroma.Web.CommonAPI
- alias Pleroma.Web.MastodonAPI.ScheduledActivityView
alias Pleroma.Web.MastodonAPI.StatusView
def render("index.json", %{scheduled_activities: scheduled_activities}) do
- render_many(scheduled_activities, ScheduledActivityView, "show.json")
+ render_many(scheduled_activities, __MODULE__, "show.json")
end
def render("show.json", %{scheduled_activity: %ScheduledActivity{} = scheduled_activity}) do
@@ -24,12 +23,8 @@ defmodule Pleroma.Web.MastodonAPI.ScheduledActivityView do
end
defp with_media_attachments(data, %{params: %{"media_attachments" => media_attachments}}) do
- try do
- attachments = render_many(media_attachments, StatusView, "attachment.json", as: :attachment)
- Map.put(data, :media_attachments, attachments)
- rescue
- _ -> data
- end
+ attachments = render_many(media_attachments, StatusView, "attachment.json", as: :attachment)
+ Map.put(data, :media_attachments, attachments)
end
defp with_media_attachments(data, _), do: data
@@ -45,13 +40,9 @@ defmodule Pleroma.Web.MastodonAPI.ScheduledActivityView do
in_reply_to_id: params["in_reply_to_id"]
}
- data =
- if media_ids = params["media_ids"] do
- Map.put(data, :media_ids, media_ids)
- else
- data
- end
-
- data
+ case params["media_ids"] do
+ nil -> data
+ media_ids -> Map.put(data, :media_ids, media_ids)
+ end
end
end
diff --git a/lib/pleroma/web/mastodon_api/views/status_view.ex b/lib/pleroma/web/mastodon_api/views/status_view.ex
index 42fbdf51b..bc527ad1b 100644
--- a/lib/pleroma/web/mastodon_api/views/status_view.ex
+++ b/lib/pleroma/web/mastodon_api/views/status_view.ex
@@ -8,6 +8,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
require Pleroma.Constants
alias Pleroma.Activity
+ alias Pleroma.ActivityExpiration
alias Pleroma.Conversation
alias Pleroma.Conversation.Participation
alias Pleroma.HTML
@@ -72,19 +73,13 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
def render("index.json", opts) do
replied_to_activities = get_replied_to_activities(opts.activities)
- parallel = unless is_nil(opts[:parallel]), do: opts[:parallel], else: true
-
- opts.activities
- |> safe_render_many(
- StatusView,
- "status.json",
- Map.put(opts, :replied_to_activities, replied_to_activities),
- parallel
- )
+ opts = Map.put(opts, :replied_to_activities, replied_to_activities)
+
+ safe_render_many(opts.activities, StatusView, "show.json", opts)
end
def render(
- "status.json",
+ "show.json",
%{activity: %{data: %{"type" => "Announce", "object" => _object}} = activity} = opts
) do
user = get_user(activity.data["actor"])
@@ -97,7 +92,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
|> Activity.with_set_thread_muted_field(opts[:for])
|> Repo.one()
- reblogged = render("status.json", Map.put(opts, :activity, reblogged_activity))
+ reblogged = render("show.json", Map.put(opts, :activity, reblogged_activity))
favorited = opts[:for] && opts[:for].ap_id in (activity_object.data["likes"] || [])
@@ -113,7 +108,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
id: to_string(activity.id),
uri: activity_object.data["id"],
url: activity_object.data["id"],
- account: AccountView.render("account.json", %{user: user, for: opts[:for]}),
+ account: AccountView.render("show.json", %{user: user, for: opts[:for]}),
in_reply_to_id: nil,
in_reply_to_account_id: nil,
reblog: reblogged,
@@ -145,7 +140,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
}
end
- def render("status.json", %{activity: %{data: %{"object" => _object}} = activity} = opts) do
+ def render("show.json", %{activity: %{data: %{"object" => _object}} = activity} = opts) do
object = Object.normalize(activity)
user = get_user(activity.data["actor"])
@@ -177,6 +172,15 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
bookmarked = Activity.get_bookmark(activity, opts[:for]) != nil
+ client_posted_this_activity = opts[:for] && user.id == opts[:for].id
+
+ expires_at =
+ with true <- client_posted_this_activity,
+ expiration when not is_nil(expiration) <-
+ ActivityExpiration.get_by_activity_id(activity.id) do
+ expiration.scheduled_at
+ end
+
thread_muted? =
case activity.thread_muted? do
thread_muted? when is_boolean(thread_muted?) -> thread_muted?
@@ -254,7 +258,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
id: to_string(activity.id),
uri: object.data["id"],
url: url,
- account: AccountView.render("account.json", %{user: user, for: opts[:for]}),
+ account: AccountView.render("show.json", %{user: user, for: opts[:for]}),
in_reply_to_id: reply_to && to_string(reply_to.id),
in_reply_to_account_id: reply_to_user && to_string(reply_to_user.id),
reblog: nil,
@@ -288,12 +292,14 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
in_reply_to_account_acct: reply_to_user && reply_to_user.nickname,
content: %{"text/plain" => content_plaintext},
spoiler_text: %{"text/plain" => summary_plaintext},
- direct_conversation_id: direct_conversation_id
+ expires_at: expires_at,
+ direct_conversation_id: direct_conversation_id,
+ thread_muted: thread_muted?
}
}
end
- def render("status.json", _) do
+ def render("show.json", _) do
nil
end
@@ -333,9 +339,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
}
end
- def render("card.json", _) do
- nil
- end
+ def render("card.json", _), do: nil
def render("attachment.json", %{attachment: attachment}) do
[attachment_url | _] = attachment["url"]
@@ -364,6 +368,27 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
}
end
+ def render("listen.json", %{activity: %Activity{data: %{"type" => "Listen"}} = activity} = opts) do
+ object = Object.normalize(activity)
+
+ user = get_user(activity.data["actor"])
+ created_at = Utils.to_masto_date(activity.data["published"])
+
+ %{
+ id: activity.id,
+ account: AccountView.render("show.json", %{user: user, for: opts[:for]}),
+ created_at: created_at,
+ title: object.data["title"] |> HTML.strip_tags(),
+ artist: object.data["artist"] |> HTML.strip_tags(),
+ album: object.data["album"] |> HTML.strip_tags(),
+ length: object.data["length"]
+ }
+ end
+
+ def render("listens.json", opts) do
+ safe_render_many(opts.activities, StatusView, "listen.json", opts)
+ end
+
def render("poll.json", %{object: object} = opts) do
{multiple, options} =
case object.data do
@@ -373,16 +398,27 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
end
if options do
- end_time =
- (object.data["closed"] || object.data["endTime"])
- |> NaiveDateTime.from_iso8601!()
-
- expired =
- end_time
- |> NaiveDateTime.compare(NaiveDateTime.utc_now())
- |> case do
- :lt -> true
- _ -> false
+ {end_time, expired} =
+ case object.data["closed"] || object.data["endTime"] do
+ end_time when is_binary(end_time) ->
+ end_time =
+ (object.data["closed"] || object.data["endTime"])
+ |> NaiveDateTime.from_iso8601!()
+
+ expired =
+ end_time
+ |> NaiveDateTime.compare(NaiveDateTime.utc_now())
+ |> case do
+ :lt -> true
+ _ -> false
+ end
+
+ end_time = Utils.to_masto_date(end_time)
+
+ {end_time, expired}
+
+ _ ->
+ {nil, false}
end
voted =
@@ -409,7 +445,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
# Mastodon uses separate ids for polls, but an object can't have
# more than one poll embedded so object id is fine
id: to_string(object.id),
- expires_at: Utils.to_masto_date(end_time),
+ expires_at: end_time,
expired: expired,
multiple: multiple,
votes_count: votes_count,
@@ -422,6 +458,20 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
end
end
+ def render("context.json", %{activity: activity, activities: activities, user: user}) do
+ %{ancestors: ancestors, descendants: descendants} =
+ activities
+ |> Enum.reverse()
+ |> Enum.group_by(fn %{id: id} -> if id < activity.id, do: :ancestors, else: :descendants end)
+ |> Map.put_new(:ancestors, [])
+ |> Map.put_new(:descendants, [])
+
+ %{
+ ancestors: render("index.json", for: user, activities: ancestors, as: :activity),
+ descendants: render("index.json", for: user, activities: descendants, as: :activity)
+ }
+ end
+
def get_reply_to(activity, %{replied_to_activities: replied_to_activities}) do
object = Object.normalize(activity)
@@ -476,7 +526,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
object_tags = for tag when is_binary(tag) <- object_tags, do: tag
Enum.reduce(object_tags, [], fn tag, tags ->
- tags ++ [%{name: tag, url: "/tag/#{tag}"}]
+ tags ++ [%{name: tag, url: "/tag/#{URI.encode(tag)}"}]
end)
end
diff --git a/lib/pleroma/web/mastodon_api/websocket_handler.ex b/lib/pleroma/web/mastodon_api/websocket_handler.ex
index dbd3542ea..3c26eb406 100644
--- a/lib/pleroma/web/mastodon_api/websocket_handler.ex
+++ b/lib/pleroma/web/mastodon_api/websocket_handler.ex
@@ -8,6 +8,7 @@ defmodule Pleroma.Web.MastodonAPI.WebsocketHandler do
alias Pleroma.Repo
alias Pleroma.User
alias Pleroma.Web.OAuth.Token
+ alias Pleroma.Web.Streamer
@behaviour :cowboy_websocket
@@ -24,7 +25,7 @@ defmodule Pleroma.Web.MastodonAPI.WebsocketHandler do
]
@anonymous_streams ["public", "public:local", "hashtag"]
- # Handled by periodic keepalive in Pleroma.Web.Streamer.
+ # Handled by periodic keepalive in Pleroma.Web.Streamer.Ping.
@timeout :infinity
def init(%{qs: qs} = req, state) do
@@ -65,7 +66,7 @@ defmodule Pleroma.Web.MastodonAPI.WebsocketHandler do
}, topic #{state.topic}"
)
- Pleroma.Web.Streamer.add_socket(state.topic, streamer_socket(state))
+ Streamer.add_socket(state.topic, streamer_socket(state))
{:ok, state}
end
@@ -80,7 +81,7 @@ defmodule Pleroma.Web.MastodonAPI.WebsocketHandler do
}, topic #{state.topic || "?"}: #{inspect(reason)}"
)
- Pleroma.Web.Streamer.remove_socket(state.topic, streamer_socket(state))
+ Streamer.remove_socket(state.topic, streamer_socket(state))
:ok
end