Fork of Pleroma with site-specific changes and feature branches https://git.pleroma.social/pleroma/pleroma
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

421 line
13KB

  1. # Pleroma: A lightweight social networking server
  2. # Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
  3. # SPDX-License-Identifier: AGPL-3.0-only
  4. defmodule Pleroma.Web.MastodonAPI.StatusController do
  5. use Pleroma.Web, :controller
  6. import Pleroma.Web.ControllerHelper,
  7. only: [try_render: 3, add_link_headers: 2]
  8. require Ecto.Query
  9. alias Pleroma.Activity
  10. alias Pleroma.Bookmark
  11. alias Pleroma.Object
  12. alias Pleroma.Repo
  13. alias Pleroma.ScheduledActivity
  14. alias Pleroma.User
  15. alias Pleroma.Web.ActivityPub.ActivityPub
  16. alias Pleroma.Web.ActivityPub.Visibility
  17. alias Pleroma.Web.CommonAPI
  18. alias Pleroma.Web.MastodonAPI.AccountView
  19. alias Pleroma.Web.MastodonAPI.ScheduledActivityView
  20. alias Pleroma.Web.Plugs.OAuthScopesPlug
  21. alias Pleroma.Web.Plugs.RateLimiter
  22. plug(Pleroma.Web.ApiSpec.CastAndValidate)
  23. plug(
  24. :skip_plug,
  25. Pleroma.Web.Plugs.EnsurePublicOrAuthenticatedPlug when action in [:index, :show]
  26. )
  27. @unauthenticated_access %{fallback: :proceed_unauthenticated, scopes: []}
  28. plug(
  29. OAuthScopesPlug,
  30. %{@unauthenticated_access | scopes: ["read:statuses"]}
  31. when action in [
  32. :index,
  33. :show,
  34. :card,
  35. :context
  36. ]
  37. )
  38. plug(
  39. OAuthScopesPlug,
  40. %{scopes: ["write:statuses"]}
  41. when action in [
  42. :create,
  43. :delete,
  44. :reblog,
  45. :unreblog
  46. ]
  47. )
  48. plug(OAuthScopesPlug, %{scopes: ["read:favourites"]} when action == :favourites)
  49. plug(
  50. OAuthScopesPlug,
  51. %{scopes: ["write:favourites"]} when action in [:favourite, :unfavourite]
  52. )
  53. plug(
  54. OAuthScopesPlug,
  55. %{scopes: ["write:mutes"]} when action in [:mute_conversation, :unmute_conversation]
  56. )
  57. plug(
  58. OAuthScopesPlug,
  59. %{@unauthenticated_access | scopes: ["read:accounts"]}
  60. when action in [:favourited_by, :reblogged_by]
  61. )
  62. plug(OAuthScopesPlug, %{scopes: ["write:accounts"]} when action in [:pin, :unpin])
  63. # Note: scope not present in Mastodon: read:bookmarks
  64. plug(OAuthScopesPlug, %{scopes: ["read:bookmarks"]} when action == :bookmarks)
  65. # Note: scope not present in Mastodon: write:bookmarks
  66. plug(
  67. OAuthScopesPlug,
  68. %{scopes: ["write:bookmarks"]} when action in [:bookmark, :unbookmark]
  69. )
  70. @rate_limited_status_actions ~w(reblog unreblog favourite unfavourite create delete)a
  71. plug(
  72. RateLimiter,
  73. [name: :status_id_action, bucket_name: "status_id_action:reblog_unreblog", params: [:id]]
  74. when action in ~w(reblog unreblog)a
  75. )
  76. plug(
  77. RateLimiter,
  78. [name: :status_id_action, bucket_name: "status_id_action:fav_unfav", params: [:id]]
  79. when action in ~w(favourite unfavourite)a
  80. )
  81. plug(RateLimiter, [name: :statuses_actions] when action in @rate_limited_status_actions)
  82. action_fallback(Pleroma.Web.MastodonAPI.FallbackController)
  83. defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.StatusOperation
  84. @doc """
  85. GET `/api/v1/statuses?ids[]=1&ids[]=2`
  86. `ids` query param is required
  87. """
  88. def index(%{assigns: %{user: user}} = conn, %{ids: ids} = _params) do
  89. limit = 100
  90. activities =
  91. ids
  92. |> Enum.take(limit)
  93. |> Activity.all_by_ids_with_object()
  94. |> Enum.filter(&Visibility.visible_for_user?(&1, user))
  95. render(conn, "index.json",
  96. activities: activities,
  97. for: user,
  98. as: :activity
  99. )
  100. end
  101. @doc """
  102. POST /api/v1/statuses
  103. Creates a scheduled status when `scheduled_at` param is present and it's far enough
  104. """
  105. def create(
  106. %{
  107. assigns: %{user: user},
  108. body_params: %{status: _, scheduled_at: scheduled_at} = params
  109. } = conn,
  110. _
  111. )
  112. when not is_nil(scheduled_at) do
  113. params = Map.put(params, :in_reply_to_status_id, params[:in_reply_to_id])
  114. attrs = %{
  115. params: Map.new(params, fn {key, value} -> {to_string(key), value} end),
  116. scheduled_at: scheduled_at
  117. }
  118. with {:far_enough, true} <- {:far_enough, ScheduledActivity.far_enough?(scheduled_at)},
  119. {:ok, scheduled_activity} <- ScheduledActivity.create(user, attrs) do
  120. conn
  121. |> put_view(ScheduledActivityView)
  122. |> render("show.json", scheduled_activity: scheduled_activity)
  123. else
  124. {:far_enough, _} ->
  125. params = Map.drop(params, [:scheduled_at])
  126. create(%Plug.Conn{conn | body_params: params}, %{})
  127. error ->
  128. error
  129. end
  130. end
  131. @doc """
  132. POST /api/v1/statuses
  133. Creates a regular status
  134. """
  135. def create(%{assigns: %{user: user}, body_params: %{status: _} = params} = conn, _) do
  136. params = Map.put(params, :in_reply_to_status_id, params[:in_reply_to_id])
  137. with {:ok, activity} <- CommonAPI.post(user, params) do
  138. try_render(conn, "show.json",
  139. activity: activity,
  140. for: user,
  141. as: :activity,
  142. with_direct_conversation_id: true
  143. )
  144. else
  145. {:error, {:reject, message}} ->
  146. conn
  147. |> put_status(:unprocessable_entity)
  148. |> json(%{error: message})
  149. {:error, message} ->
  150. conn
  151. |> put_status(:unprocessable_entity)
  152. |> json(%{error: message})
  153. end
  154. end
  155. def create(%{assigns: %{user: _user}, body_params: %{media_ids: _} = params} = conn, _) do
  156. params = Map.put(params, :status, "")
  157. create(%Plug.Conn{conn | body_params: params}, %{})
  158. end
  159. @doc "GET /api/v1/statuses/:id"
  160. def show(%{assigns: %{user: user}} = conn, %{id: id}) do
  161. with %Activity{} = activity <- Activity.get_by_id_with_object(id),
  162. true <- Visibility.visible_for_user?(activity, user) do
  163. try_render(conn, "show.json",
  164. activity: activity,
  165. for: user,
  166. with_direct_conversation_id: true
  167. )
  168. else
  169. _ -> {:error, :not_found}
  170. end
  171. end
  172. @doc "DELETE /api/v1/statuses/:id"
  173. def delete(%{assigns: %{user: user}} = conn, %{id: id}) do
  174. with %Activity{} = activity <- Activity.get_by_id_with_object(id),
  175. {:ok, %Activity{}} <- CommonAPI.delete(id, user) do
  176. try_render(conn, "show.json",
  177. activity: activity,
  178. for: user,
  179. with_direct_conversation_id: true,
  180. with_source: true
  181. )
  182. else
  183. _e -> {:error, :not_found}
  184. end
  185. end
  186. @doc "POST /api/v1/statuses/:id/reblog"
  187. def reblog(%{assigns: %{user: user}, body_params: params} = conn, %{id: ap_id_or_id}) do
  188. with {:ok, announce} <- CommonAPI.repeat(ap_id_or_id, user, params),
  189. %Activity{} = announce <- Activity.normalize(announce.data) do
  190. try_render(conn, "show.json", %{activity: announce, for: user, as: :activity})
  191. end
  192. end
  193. @doc "POST /api/v1/statuses/:id/unreblog"
  194. def unreblog(%{assigns: %{user: user}} = conn, %{id: activity_id}) do
  195. with {:ok, _unannounce} <- CommonAPI.unrepeat(activity_id, user),
  196. %Activity{} = activity <- Activity.get_by_id(activity_id) do
  197. try_render(conn, "show.json", %{activity: activity, for: user, as: :activity})
  198. end
  199. end
  200. @doc "POST /api/v1/statuses/:id/favourite"
  201. def favourite(%{assigns: %{user: user}} = conn, %{id: activity_id}) do
  202. with {:ok, _fav} <- CommonAPI.favorite(user, activity_id),
  203. %Activity{} = activity <- Activity.get_by_id(activity_id) do
  204. try_render(conn, "show.json", activity: activity, for: user, as: :activity)
  205. end
  206. end
  207. @doc "POST /api/v1/statuses/:id/unfavourite"
  208. def unfavourite(%{assigns: %{user: user}} = conn, %{id: activity_id}) do
  209. with {:ok, _unfav} <- CommonAPI.unfavorite(activity_id, user),
  210. %Activity{} = activity <- Activity.get_by_id(activity_id) do
  211. try_render(conn, "show.json", activity: activity, for: user, as: :activity)
  212. end
  213. end
  214. @doc "POST /api/v1/statuses/:id/pin"
  215. def pin(%{assigns: %{user: user}} = conn, %{id: ap_id_or_id}) do
  216. with {:ok, activity} <- CommonAPI.pin(ap_id_or_id, user) do
  217. try_render(conn, "show.json", activity: activity, for: user, as: :activity)
  218. end
  219. end
  220. @doc "POST /api/v1/statuses/:id/unpin"
  221. def unpin(%{assigns: %{user: user}} = conn, %{id: ap_id_or_id}) do
  222. with {:ok, activity} <- CommonAPI.unpin(ap_id_or_id, user) do
  223. try_render(conn, "show.json", activity: activity, for: user, as: :activity)
  224. end
  225. end
  226. @doc "POST /api/v1/statuses/:id/bookmark"
  227. def bookmark(%{assigns: %{user: user}} = conn, %{id: id}) do
  228. with %Activity{} = activity <- Activity.get_by_id_with_object(id),
  229. %User{} = user <- User.get_cached_by_nickname(user.nickname),
  230. true <- Visibility.visible_for_user?(activity, user),
  231. {:ok, _bookmark} <- Bookmark.create(user.id, activity.id) do
  232. try_render(conn, "show.json", activity: activity, for: user, as: :activity)
  233. end
  234. end
  235. @doc "POST /api/v1/statuses/:id/unbookmark"
  236. def unbookmark(%{assigns: %{user: user}} = conn, %{id: id}) do
  237. with %Activity{} = activity <- Activity.get_by_id_with_object(id),
  238. %User{} = user <- User.get_cached_by_nickname(user.nickname),
  239. true <- Visibility.visible_for_user?(activity, user),
  240. {:ok, _bookmark} <- Bookmark.destroy(user.id, activity.id) do
  241. try_render(conn, "show.json", activity: activity, for: user, as: :activity)
  242. end
  243. end
  244. @doc "POST /api/v1/statuses/:id/mute"
  245. def mute_conversation(%{assigns: %{user: user}} = conn, %{id: id}) do
  246. with %Activity{} = activity <- Activity.get_by_id(id),
  247. {:ok, activity} <- CommonAPI.add_mute(user, activity) do
  248. try_render(conn, "show.json", activity: activity, for: user, as: :activity)
  249. end
  250. end
  251. @doc "POST /api/v1/statuses/:id/unmute"
  252. def unmute_conversation(%{assigns: %{user: user}} = conn, %{id: id}) do
  253. with %Activity{} = activity <- Activity.get_by_id(id),
  254. {:ok, activity} <- CommonAPI.remove_mute(user, activity) do
  255. try_render(conn, "show.json", activity: activity, for: user, as: :activity)
  256. end
  257. end
  258. @doc "GET /api/v1/statuses/:id/card"
  259. @deprecated "https://github.com/tootsuite/mastodon/pull/11213"
  260. def card(%{assigns: %{user: user}} = conn, %{id: status_id}) do
  261. with %Activity{} = activity <- Activity.get_by_id(status_id),
  262. true <- Visibility.visible_for_user?(activity, user) do
  263. data = Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity)
  264. render(conn, "card.json", data)
  265. else
  266. _ -> render_error(conn, :not_found, "Record not found")
  267. end
  268. end
  269. @doc "GET /api/v1/statuses/:id/favourited_by"
  270. def favourited_by(%{assigns: %{user: user}} = conn, %{id: id}) do
  271. with true <- Pleroma.Config.get([:instance, :show_reactions]),
  272. %Activity{} = activity <- Activity.get_by_id_with_object(id),
  273. {:visible, true} <- {:visible, Visibility.visible_for_user?(activity, user)},
  274. %Object{data: %{"likes" => likes}} <- Object.normalize(activity) do
  275. users =
  276. User
  277. |> Ecto.Query.where([u], u.ap_id in ^likes)
  278. |> Repo.all()
  279. |> Enum.filter(&(not User.blocks?(user, &1)))
  280. conn
  281. |> put_view(AccountView)
  282. |> render("index.json", for: user, users: users, as: :user)
  283. else
  284. {:visible, false} -> {:error, :not_found}
  285. _ -> json(conn, [])
  286. end
  287. end
  288. @doc "GET /api/v1/statuses/:id/reblogged_by"
  289. def reblogged_by(%{assigns: %{user: user}} = conn, %{id: id}) do
  290. with %Activity{} = activity <- Activity.get_by_id_with_object(id),
  291. {:visible, true} <- {:visible, Visibility.visible_for_user?(activity, user)},
  292. %Object{data: %{"announcements" => announces, "id" => ap_id}} <-
  293. Object.normalize(activity) do
  294. announces =
  295. "Announce"
  296. |> Activity.Queries.by_type()
  297. |> Ecto.Query.where([a], a.actor in ^announces)
  298. # this is to use the index
  299. |> Activity.Queries.by_object_id(ap_id)
  300. |> Repo.all()
  301. |> Enum.filter(&Visibility.visible_for_user?(&1, user))
  302. |> Enum.map(& &1.actor)
  303. |> Enum.uniq()
  304. users =
  305. User
  306. |> Ecto.Query.where([u], u.ap_id in ^announces)
  307. |> Repo.all()
  308. |> Enum.filter(&(not User.blocks?(user, &1)))
  309. conn
  310. |> put_view(AccountView)
  311. |> render("index.json", for: user, users: users, as: :user)
  312. else
  313. {:visible, false} -> {:error, :not_found}
  314. _ -> json(conn, [])
  315. end
  316. end
  317. @doc "GET /api/v1/statuses/:id/context"
  318. def context(%{assigns: %{user: user}} = conn, %{id: id}) do
  319. with %Activity{} = activity <- Activity.get_by_id(id) do
  320. activities =
  321. ActivityPub.fetch_activities_for_context(activity.data["context"], %{
  322. blocking_user: user,
  323. user: user,
  324. exclude_id: activity.id
  325. })
  326. render(conn, "context.json", activity: activity, activities: activities, user: user)
  327. end
  328. end
  329. @doc "GET /api/v1/favourites"
  330. def favourites(%{assigns: %{user: %User{} = user}} = conn, params) do
  331. activities = ActivityPub.fetch_favourites(user, params)
  332. conn
  333. |> add_link_headers(activities)
  334. |> render("index.json",
  335. activities: activities,
  336. for: user,
  337. as: :activity
  338. )
  339. end
  340. @doc "GET /api/v1/bookmarks"
  341. def bookmarks(%{assigns: %{user: user}} = conn, params) do
  342. user = User.get_cached_by_id(user.id)
  343. bookmarks =
  344. user.id
  345. |> Bookmark.for_user_query()
  346. |> Pleroma.Pagination.fetch_paginated(params)
  347. activities =
  348. bookmarks
  349. |> Enum.map(fn b -> Map.put(b.activity, :bookmark, Map.delete(b, :activity)) end)
  350. conn
  351. |> add_link_headers(bookmarks)
  352. |> render("index.json",
  353. activities: activities,
  354. for: user,
  355. as: :activity
  356. )
  357. end
  358. end