logo

pleroma

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

status_controller.ex (18584B)


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