Fork of Pleroma with site-specific changes and feature branches https://git.pleroma.social/pleroma/pleroma
25개 이상의 토픽을 선택하실 수 없습니다. Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

446 lines
14KB

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