logo

pleroma

My custom branche(s) on git.pleroma.social/pleroma/pleroma git clone https://hacktivis.me/git/pleroma.git

status_controller.ex (18750B)


  1. # Pleroma: A lightweight social networking server
  2. # Copyright © 2017-2022 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, replace_params: false)
  24. plug(:skip_public_check when action in [:index, :show])
  25. @unauthenticated_access %{fallback: :proceed_unauthenticated, scopes: []}
  26. plug(
  27. OAuthScopesPlug,
  28. %{@unauthenticated_access | scopes: ["read:statuses"]}
  29. when action in [
  30. :index,
  31. :show,
  32. :card,
  33. :context,
  34. :show_history,
  35. :show_source
  36. ]
  37. )
  38. plug(
  39. OAuthScopesPlug,
  40. %{scopes: ["write:statuses"]}
  41. when action in [
  42. :create,
  43. :delete,
  44. :reblog,
  45. :unreblog,
  46. :update
  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(
  90. %{assigns: %{user: user}, private: %{open_api_spex: %{params: %{ids: ids} = params}}} =
  91. conn,
  92. _
  93. ) do
  94. limit = 100
  95. activities =
  96. ids
  97. |> Enum.take(limit)
  98. |> Activity.all_by_ids_with_object()
  99. |> Enum.filter(&Visibility.visible_for_user?(&1, user))
  100. render(conn, "index.json",
  101. activities: activities,
  102. for: user,
  103. as: :activity,
  104. with_muted: Map.get(params, :with_muted, false)
  105. )
  106. end
  107. @doc """
  108. POST /api/v1/statuses
  109. """
  110. # Creates a scheduled status when `scheduled_at` param is present and it's far enough
  111. def create(
  112. %{
  113. assigns: %{user: user},
  114. private: %{
  115. open_api_spex: %{body_params: %{status: _, scheduled_at: scheduled_at} = params}
  116. }
  117. } = conn,
  118. _
  119. )
  120. when not is_nil(scheduled_at) do
  121. params =
  122. Map.put(params, :in_reply_to_status_id, params[:in_reply_to_id])
  123. |> put_application(conn)
  124. attrs = %{
  125. params: Map.new(params, fn {key, value} -> {to_string(key), value} end),
  126. scheduled_at: scheduled_at
  127. }
  128. with {:far_enough, true} <- {:far_enough, ScheduledActivity.far_enough?(scheduled_at)},
  129. {:ok, scheduled_activity} <- ScheduledActivity.create(user, attrs) do
  130. conn
  131. |> put_view(ScheduledActivityView)
  132. |> render("show.json", scheduled_activity: scheduled_activity)
  133. else
  134. {:far_enough, _} ->
  135. params = Map.drop(params, [:scheduled_at])
  136. put_in(
  137. conn,
  138. [Access.key(:private), Access.key(:open_api_spex), Access.key(:body_params)],
  139. params
  140. )
  141. |> do_create
  142. error ->
  143. error
  144. end
  145. end
  146. # Creates a regular status
  147. def create(
  148. %{
  149. private: %{open_api_spex: %{body_params: %{status: _}}}
  150. } = conn,
  151. _
  152. ) do
  153. do_create(conn)
  154. end
  155. def create(
  156. %{
  157. assigns: %{user: _user},
  158. private: %{open_api_spex: %{body_params: %{media_ids: _} = params}}
  159. } = conn,
  160. _
  161. ) do
  162. params = Map.put(params, :status, "")
  163. put_in(
  164. conn,
  165. [Access.key(:private), Access.key(:open_api_spex), Access.key(:body_params)],
  166. params
  167. )
  168. |> do_create
  169. end
  170. defp do_create(
  171. %{assigns: %{user: user}, private: %{open_api_spex: %{body_params: params}}} = conn
  172. ) do
  173. params =
  174. Map.put(params, :in_reply_to_status_id, params[:in_reply_to_id])
  175. |> put_application(conn)
  176. with {:ok, activity} <- CommonAPI.post(user, params) do
  177. try_render(conn, "show.json",
  178. activity: activity,
  179. for: user,
  180. as: :activity,
  181. with_direct_conversation_id: true
  182. )
  183. else
  184. {:error, {:reject, message}} ->
  185. conn
  186. |> put_status(:unprocessable_entity)
  187. |> json(%{error: message})
  188. {:error, message} ->
  189. conn
  190. |> put_status(:unprocessable_entity)
  191. |> json(%{error: message})
  192. end
  193. end
  194. @doc "GET /api/v1/statuses/:id/history"
  195. def show_history(
  196. %{assigns: assigns, private: %{open_api_spex: %{params: %{id: id} = params}}} = conn,
  197. _
  198. ) do
  199. with user = assigns[:user],
  200. %Activity{} = activity <- Activity.get_by_id_with_object(id),
  201. true <- Visibility.visible_for_user?(activity, user) do
  202. try_render(conn, "history.json",
  203. activity: activity,
  204. for: user,
  205. with_direct_conversation_id: true,
  206. with_muted: Map.get(params, :with_muted, false)
  207. )
  208. else
  209. _ -> {:error, :not_found}
  210. end
  211. end
  212. @doc "GET /api/v1/statuses/:id/source"
  213. def show_source(%{assigns: assigns, private: %{open_api_spex: %{params: %{id: id}}}} = conn, _) do
  214. with user = assigns[:user],
  215. %Activity{} = activity <- Activity.get_by_id_with_object(id),
  216. true <- Visibility.visible_for_user?(activity, user) do
  217. try_render(conn, "source.json",
  218. activity: activity,
  219. for: user
  220. )
  221. else
  222. _ -> {:error, :not_found}
  223. end
  224. end
  225. @doc "PUT /api/v1/statuses/:id"
  226. def update(
  227. %{
  228. assigns: %{user: user},
  229. private: %{open_api_spex: %{body_params: body_params, params: %{id: id} = params}}
  230. } = conn,
  231. _
  232. ) do
  233. with {_, %Activity{}} = {_, activity} <- {:activity, Activity.get_by_id_with_object(id)},
  234. {_, true} <- {:visible, Visibility.visible_for_user?(activity, user)},
  235. {_, true} <- {:is_create, activity.data["type"] == "Create"},
  236. actor <- Activity.user_actor(activity),
  237. {_, true} <- {:own_status, actor.id == user.id},
  238. changes <- body_params |> put_application(conn),
  239. {_, {:ok, _update_activity}} <- {:pipeline, CommonAPI.update(user, activity, changes)},
  240. {_, %Activity{}} = {_, activity} <- {:refetched, Activity.get_by_id_with_object(id)} do
  241. try_render(conn, "show.json",
  242. activity: activity,
  243. for: user,
  244. with_direct_conversation_id: true,
  245. with_muted: Map.get(params, :with_muted, false)
  246. )
  247. else
  248. {:own_status, _} -> {:error, :forbidden}
  249. {:pipeline, _} -> {:error, :internal_server_error}
  250. _ -> {:error, :not_found}
  251. end
  252. end
  253. @doc "GET /api/v1/statuses/:id"
  254. def show(
  255. %{assigns: %{user: user}, private: %{open_api_spex: %{params: %{id: id} = params}}} =
  256. conn,
  257. _
  258. ) do
  259. with %Activity{} = activity <- Activity.get_by_id_with_object(id),
  260. true <- Visibility.visible_for_user?(activity, user) do
  261. try_render(conn, "show.json",
  262. activity: activity,
  263. for: user,
  264. with_direct_conversation_id: true,
  265. with_muted: Map.get(params, :with_muted, false)
  266. )
  267. else
  268. _ -> {:error, :not_found}
  269. end
  270. end
  271. @doc "DELETE /api/v1/statuses/:id"
  272. def delete(%{assigns: %{user: user}, private: %{open_api_spex: %{params: %{id: id}}}} = conn, _) do
  273. with %Activity{} = activity <- Activity.get_by_id_with_object(id),
  274. {:ok, %Activity{}} <- CommonAPI.delete(id, user) do
  275. try_render(conn, "show.json",
  276. activity: activity,
  277. for: user,
  278. with_direct_conversation_id: true,
  279. with_source: true
  280. )
  281. else
  282. _e -> {:error, :not_found}
  283. end
  284. end
  285. @doc "POST /api/v1/statuses/:id/reblog"
  286. def reblog(
  287. %{
  288. assigns: %{user: user},
  289. private: %{open_api_spex: %{body_params: params, params: %{id: ap_id_or_id}}}
  290. } = conn,
  291. _
  292. ) do
  293. with {:ok, announce} <- CommonAPI.repeat(ap_id_or_id, user, params),
  294. %Activity{} = announce <- Activity.normalize(announce.data) do
  295. try_render(conn, "show.json", %{activity: announce, for: user, as: :activity})
  296. end
  297. end
  298. @doc "POST /api/v1/statuses/:id/unreblog"
  299. def unreblog(
  300. %{assigns: %{user: user}, private: %{open_api_spex: %{params: %{id: activity_id}}}} =
  301. conn,
  302. _
  303. ) do
  304. with {:ok, _unannounce} <- CommonAPI.unrepeat(activity_id, user),
  305. %Activity{} = activity <- Activity.get_by_id(activity_id) do
  306. try_render(conn, "show.json", %{activity: activity, for: user, as: :activity})
  307. end
  308. end
  309. @doc "POST /api/v1/statuses/:id/favourite"
  310. def favourite(
  311. %{assigns: %{user: user}, private: %{open_api_spex: %{params: %{id: activity_id}}}} =
  312. conn,
  313. _
  314. ) do
  315. with {:ok, _fav} <- CommonAPI.favorite(user, activity_id),
  316. %Activity{} = activity <- Activity.get_by_id(activity_id) do
  317. try_render(conn, "show.json", activity: activity, for: user, as: :activity)
  318. end
  319. end
  320. @doc "POST /api/v1/statuses/:id/unfavourite"
  321. def unfavourite(
  322. %{assigns: %{user: user}, private: %{open_api_spex: %{params: %{id: activity_id}}}} =
  323. conn,
  324. _
  325. ) do
  326. with {:ok, _unfav} <- CommonAPI.unfavorite(activity_id, user),
  327. %Activity{} = activity <- Activity.get_by_id(activity_id) do
  328. try_render(conn, "show.json", activity: activity, for: user, as: :activity)
  329. end
  330. end
  331. @doc "POST /api/v1/statuses/:id/pin"
  332. def pin(
  333. %{assigns: %{user: user}, private: %{open_api_spex: %{params: %{id: ap_id_or_id}}}} =
  334. conn,
  335. _
  336. ) do
  337. with {:ok, activity} <- CommonAPI.pin(ap_id_or_id, user) do
  338. try_render(conn, "show.json", activity: activity, for: user, as: :activity)
  339. else
  340. {:error, :pinned_statuses_limit_reached} ->
  341. {:error, "You have already pinned the maximum number of statuses"}
  342. {:error, :ownership_error} ->
  343. {:error, :unprocessable_entity, "Someone else's status cannot be pinned"}
  344. {:error, :visibility_error} ->
  345. {:error, :unprocessable_entity, "Non-public status cannot be pinned"}
  346. error ->
  347. error
  348. end
  349. end
  350. @doc "POST /api/v1/statuses/:id/unpin"
  351. def unpin(
  352. %{assigns: %{user: user}, private: %{open_api_spex: %{params: %{id: ap_id_or_id}}}} =
  353. conn,
  354. _
  355. ) do
  356. with {:ok, activity} <- CommonAPI.unpin(ap_id_or_id, user) do
  357. try_render(conn, "show.json", activity: activity, for: user, as: :activity)
  358. end
  359. end
  360. @doc "POST /api/v1/statuses/:id/bookmark"
  361. def bookmark(
  362. %{assigns: %{user: user}, private: %{open_api_spex: %{params: %{id: id}}}} = conn,
  363. _
  364. ) do
  365. with %Activity{} = activity <- Activity.get_by_id_with_object(id),
  366. %User{} = user <- User.get_cached_by_nickname(user.nickname),
  367. true <- Visibility.visible_for_user?(activity, user),
  368. {:ok, _bookmark} <- Bookmark.create(user.id, activity.id) do
  369. try_render(conn, "show.json", activity: activity, for: user, as: :activity)
  370. end
  371. end
  372. @doc "POST /api/v1/statuses/:id/unbookmark"
  373. def unbookmark(
  374. %{assigns: %{user: user}, private: %{open_api_spex: %{params: %{id: id}}}} = conn,
  375. _
  376. ) do
  377. with %Activity{} = activity <- Activity.get_by_id_with_object(id),
  378. %User{} = user <- User.get_cached_by_nickname(user.nickname),
  379. true <- Visibility.visible_for_user?(activity, user),
  380. {:ok, _bookmark} <- Bookmark.destroy(user.id, activity.id) do
  381. try_render(conn, "show.json", activity: activity, for: user, as: :activity)
  382. end
  383. end
  384. @doc "POST /api/v1/statuses/:id/mute"
  385. def mute_conversation(
  386. %{
  387. assigns: %{user: user},
  388. private: %{open_api_spex: %{body_params: params, params: %{id: id}}}
  389. } = conn,
  390. _
  391. ) do
  392. with %Activity{} = activity <- Activity.get_by_id(id),
  393. {:ok, activity} <- CommonAPI.add_mute(user, activity, params) do
  394. try_render(conn, "show.json", activity: activity, for: user, as: :activity)
  395. end
  396. end
  397. @doc "POST /api/v1/statuses/:id/unmute"
  398. def unmute_conversation(
  399. %{
  400. assigns: %{user: user},
  401. private: %{open_api_spex: %{params: %{id: id}}}
  402. } = conn,
  403. _
  404. ) do
  405. with %Activity{} = activity <- Activity.get_by_id(id),
  406. {:ok, activity} <- CommonAPI.remove_mute(user, activity) do
  407. try_render(conn, "show.json", activity: activity, for: user, as: :activity)
  408. end
  409. end
  410. @doc "GET /api/v1/statuses/:id/card"
  411. @deprecated "https://github.com/tootsuite/mastodon/pull/11213"
  412. def card(
  413. %{assigns: %{user: user}, private: %{open_api_spex: %{params: %{id: status_id}}}} = conn,
  414. _
  415. ) do
  416. with %Activity{} = activity <- Activity.get_by_id(status_id),
  417. true <- Visibility.visible_for_user?(activity, user) do
  418. data = Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity)
  419. render(conn, "card.json", data)
  420. else
  421. _ -> render_error(conn, :not_found, "Record not found")
  422. end
  423. end
  424. @doc "GET /api/v1/statuses/:id/favourited_by"
  425. def favourited_by(
  426. %{assigns: %{user: user}, private: %{open_api_spex: %{params: %{id: id}}}} = conn,
  427. _
  428. ) do
  429. with true <- Pleroma.Config.get([:instance, :show_reactions]),
  430. %Activity{} = activity <- Activity.get_by_id_with_object(id),
  431. {:visible, true} <- {:visible, Visibility.visible_for_user?(activity, user)},
  432. %Object{data: %{"likes" => likes}} <- Object.normalize(activity, fetch: false) do
  433. users =
  434. User
  435. |> Ecto.Query.where([u], u.ap_id in ^likes)
  436. |> Repo.all()
  437. |> Enum.filter(&(not User.blocks?(user, &1)))
  438. conn
  439. |> put_view(AccountView)
  440. |> render("index.json", for: user, users: users, as: :user)
  441. else
  442. {:visible, false} -> {:error, :not_found}
  443. _ -> json(conn, [])
  444. end
  445. end
  446. @doc "GET /api/v1/statuses/:id/reblogged_by"
  447. def reblogged_by(
  448. %{assigns: %{user: user}, private: %{open_api_spex: %{params: %{id: id}}}} = conn,
  449. _
  450. ) do
  451. with %Activity{} = activity <- Activity.get_by_id_with_object(id),
  452. {:visible, true} <- {:visible, Visibility.visible_for_user?(activity, user)},
  453. %Object{data: %{"announcements" => announces, "id" => ap_id}} <-
  454. Object.normalize(activity, fetch: false) do
  455. announces =
  456. "Announce"
  457. |> Activity.Queries.by_type()
  458. |> Ecto.Query.where([a], a.actor in ^announces)
  459. # this is to use the index
  460. |> Activity.Queries.by_object_id(ap_id)
  461. |> Repo.all()
  462. |> Enum.filter(&Visibility.visible_for_user?(&1, user))
  463. |> Enum.map(& &1.actor)
  464. |> Enum.uniq()
  465. users =
  466. User
  467. |> Ecto.Query.where([u], u.ap_id in ^announces)
  468. |> Repo.all()
  469. |> Enum.filter(&(not User.blocks?(user, &1)))
  470. conn
  471. |> put_view(AccountView)
  472. |> render("index.json", for: user, users: users, as: :user)
  473. else
  474. {:visible, false} -> {:error, :not_found}
  475. _ -> json(conn, [])
  476. end
  477. end
  478. @doc "GET /api/v1/statuses/:id/context"
  479. def context(
  480. %{assigns: %{user: user}, private: %{open_api_spex: %{params: %{id: id}}}} = conn,
  481. _
  482. ) do
  483. with %Activity{} = activity <- Activity.get_by_id(id) do
  484. activities =
  485. ActivityPub.fetch_activities_for_context(activity.data["context"], %{
  486. blocking_user: user,
  487. user: user,
  488. exclude_id: activity.id
  489. })
  490. render(conn, "context.json", activity: activity, activities: activities, user: user)
  491. end
  492. end
  493. @doc "GET /api/v1/favourites"
  494. def favourites(
  495. %{assigns: %{user: %User{} = user}, private: %{open_api_spex: %{params: params}}} = conn,
  496. _
  497. ) do
  498. activities = ActivityPub.fetch_favourites(user, params)
  499. conn
  500. |> add_link_headers(activities)
  501. |> render("index.json",
  502. activities: activities,
  503. for: user,
  504. as: :activity
  505. )
  506. end
  507. @doc "GET /api/v1/bookmarks"
  508. def bookmarks(%{assigns: %{user: user}, private: %{open_api_spex: %{params: params}}} = conn, _) do
  509. user = User.get_cached_by_id(user.id)
  510. bookmarks =
  511. user.id
  512. |> Bookmark.for_user_query()
  513. |> Pleroma.Pagination.fetch_paginated(params)
  514. activities =
  515. bookmarks
  516. |> Enum.map(fn b -> Map.put(b.activity, :bookmark, Map.delete(b, :activity)) end)
  517. conn
  518. |> add_link_headers(bookmarks)
  519. |> render("index.json",
  520. activities: activities,
  521. for: user,
  522. as: :activity
  523. )
  524. end
  525. defp put_application(params, %{assigns: %{token: %Token{user: %User{} = user} = token}} = _conn) do
  526. if user.disclose_client do
  527. %{client_name: client_name, website: website} = Repo.preload(token, :app).app
  528. Map.put(params, :generator, %{type: "Application", name: client_name, url: website})
  529. else
  530. Map.put(params, :generator, nil)
  531. end
  532. end
  533. defp put_application(params, _), do: Map.put(params, :generator, nil)
  534. end