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.

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