logo

pleroma

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

account_controller.ex (21015B)


  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.AccountController do
  5. use Pleroma.Web, :controller
  6. import Pleroma.Web.ControllerHelper,
  7. only: [
  8. add_link_headers: 2,
  9. assign_account_by_id: 2,
  10. embed_relationships?: 1,
  11. json_response: 3
  12. ]
  13. alias Pleroma.Maps
  14. alias Pleroma.User
  15. alias Pleroma.UserNote
  16. alias Pleroma.Web.ActivityPub.ActivityPub
  17. alias Pleroma.Web.ActivityPub.Builder
  18. alias Pleroma.Web.ActivityPub.Pipeline
  19. alias Pleroma.Web.CommonAPI
  20. alias Pleroma.Web.MastodonAPI.ListView
  21. alias Pleroma.Web.MastodonAPI.MastodonAPI
  22. alias Pleroma.Web.MastodonAPI.MastodonAPIController
  23. alias Pleroma.Web.MastodonAPI.StatusView
  24. alias Pleroma.Web.OAuth.OAuthController
  25. alias Pleroma.Web.Plugs.OAuthScopesPlug
  26. alias Pleroma.Web.Plugs.RateLimiter
  27. alias Pleroma.Web.TwitterAPI.TwitterAPI
  28. alias Pleroma.Web.Utils.Params
  29. plug(Pleroma.Web.ApiSpec.CastAndValidate, replace_params: false)
  30. plug(:skip_auth when action in [:create, :lookup])
  31. plug(:skip_public_check when action in [:show, :statuses])
  32. plug(
  33. OAuthScopesPlug,
  34. %{fallback: :proceed_unauthenticated, scopes: ["read:accounts"]}
  35. when action in [:show, :followers, :following]
  36. )
  37. plug(
  38. OAuthScopesPlug,
  39. %{fallback: :proceed_unauthenticated, scopes: ["read:statuses"]}
  40. when action == :statuses
  41. )
  42. plug(
  43. OAuthScopesPlug,
  44. %{scopes: ["read:accounts"]}
  45. when action in [:verify_credentials, :endorsements, :identity_proofs]
  46. )
  47. plug(
  48. OAuthScopesPlug,
  49. %{scopes: ["write:accounts"]}
  50. when action in [:update_credentials, :note, :endorse, :unendorse]
  51. )
  52. plug(OAuthScopesPlug, %{scopes: ["read:lists"]} when action == :lists)
  53. plug(
  54. OAuthScopesPlug,
  55. %{scopes: ["follow", "read:blocks"]} when action == :blocks
  56. )
  57. plug(
  58. OAuthScopesPlug,
  59. %{scopes: ["follow", "write:blocks"]} when action in [:block, :unblock]
  60. )
  61. plug(OAuthScopesPlug, %{scopes: ["read:follows"]} when action == :relationships)
  62. plug(
  63. OAuthScopesPlug,
  64. %{scopes: ["follow", "write:follows"]}
  65. when action in [:follow_by_uri, :follow, :unfollow, :remove_from_followers]
  66. )
  67. plug(OAuthScopesPlug, %{scopes: ["follow", "read:mutes"]} when action == :mutes)
  68. plug(OAuthScopesPlug, %{scopes: ["follow", "write:mutes"]} when action in [:mute, :unmute])
  69. @relationship_actions [:follow, :unfollow, :remove_from_followers]
  70. @needs_account ~W(
  71. followers following lists follow unfollow mute unmute block unblock
  72. note endorse unendorse remove_from_followers
  73. )a
  74. plug(
  75. RateLimiter,
  76. [name: :relation_id_action, params: ["id", "uri"]] when action in @relationship_actions
  77. )
  78. plug(RateLimiter, [name: :relations_actions] when action in @relationship_actions)
  79. plug(RateLimiter, [name: :app_account_creation] when action == :create)
  80. plug(:assign_account_by_id when action in @needs_account)
  81. action_fallback(Pleroma.Web.MastodonAPI.FallbackController)
  82. defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.AccountOperation
  83. @doc "POST /api/v1/accounts"
  84. def create(
  85. %{assigns: %{app: app}, private: %{open_api_spex: %{body_params: params}}} = conn,
  86. _params
  87. ) do
  88. with :ok <- validate_email_param(params),
  89. :ok <- TwitterAPI.validate_captcha(app, params),
  90. {:ok, user} <- TwitterAPI.register_user(params),
  91. {_, {:ok, token}} <-
  92. {:login, OAuthController.login(user, app, app.scopes)} do
  93. OAuthController.after_token_exchange(conn, %{user: user, token: token})
  94. else
  95. {:login, {:account_status, :confirmation_pending}} ->
  96. json_response(conn, :ok, %{
  97. message: "You have been registered. Please check your email for further instructions.",
  98. identifier: "missing_confirmed_email"
  99. })
  100. {:login, {:account_status, :approval_pending}} ->
  101. json_response(conn, :ok, %{
  102. message:
  103. "You have been registered. You'll be able to log in once your account is approved.",
  104. identifier: "awaiting_approval"
  105. })
  106. {:login, _} ->
  107. json_response(conn, :ok, %{
  108. message:
  109. "You have been registered. Some post-registration steps may be pending. " <>
  110. "Please log in manually.",
  111. identifier: "manual_login_required"
  112. })
  113. {:error, error} ->
  114. json_response(conn, :bad_request, %{error: error})
  115. end
  116. end
  117. def create(%{assigns: %{app: _app}} = conn, _) do
  118. render_error(conn, :bad_request, "Missing parameters")
  119. end
  120. def create(conn, _) do
  121. render_error(conn, :forbidden, "Invalid credentials")
  122. end
  123. defp validate_email_param(%{email: email}) when not is_nil(email), do: :ok
  124. defp validate_email_param(_) do
  125. case Pleroma.Config.get([:instance, :account_activation_required]) do
  126. true -> {:error, dgettext("errors", "Missing parameter: %{name}", name: "email")}
  127. _ -> :ok
  128. end
  129. end
  130. @doc "GET /api/v1/accounts/verify_credentials"
  131. def verify_credentials(%{assigns: %{user: user}} = conn, _) do
  132. chat_token = Phoenix.Token.sign(conn, "user socket", user.id)
  133. render(conn, "show.json",
  134. user: user,
  135. for: user,
  136. with_pleroma_settings: true,
  137. with_chat_token: chat_token
  138. )
  139. end
  140. @doc "PATCH /api/v1/accounts/update_credentials"
  141. def update_credentials(
  142. %{assigns: %{user: user}, private: %{open_api_spex: %{body_params: params}}} = conn,
  143. _params
  144. ) do
  145. params =
  146. params
  147. |> Enum.filter(fn {_, value} -> not is_nil(value) end)
  148. |> Enum.into(%{})
  149. # We use an empty string as a special value to reset
  150. # avatars, banners, backgrounds
  151. user_image_value = fn
  152. "" -> {:ok, nil}
  153. value -> {:ok, value}
  154. end
  155. user_params =
  156. [
  157. :no_rich_text,
  158. :hide_followers_count,
  159. :hide_follows_count,
  160. :hide_followers,
  161. :hide_follows,
  162. :hide_favorites,
  163. :show_role,
  164. :skip_thread_containment,
  165. :allow_following_move,
  166. :also_known_as,
  167. :accepts_chat_messages,
  168. :show_birthday
  169. ]
  170. |> Enum.reduce(%{}, fn key, acc ->
  171. Maps.put_if_present(acc, key, params[key], &{:ok, Params.truthy_param?(&1)})
  172. end)
  173. |> Maps.put_if_present(:name, params[:display_name])
  174. |> Maps.put_if_present(:bio, params[:note])
  175. |> Maps.put_if_present(:raw_bio, params[:note])
  176. |> Maps.put_if_present(:avatar, params[:avatar], user_image_value)
  177. |> Maps.put_if_present(:banner, params[:header], user_image_value)
  178. |> Maps.put_if_present(:background, params[:pleroma_background_image], user_image_value)
  179. |> Maps.put_if_present(
  180. :raw_fields,
  181. params[:fields_attributes],
  182. &{:ok, normalize_fields_attributes(&1)}
  183. )
  184. |> Maps.put_if_present(:pleroma_settings_store, params[:pleroma_settings_store])
  185. |> Maps.put_if_present(:default_scope, params[:default_scope])
  186. |> Maps.put_if_present(:default_scope, params["source"]["privacy"])
  187. |> Maps.put_if_present(:actor_type, params[:bot], fn bot ->
  188. if bot, do: {:ok, "Service"}, else: {:ok, "Person"}
  189. end)
  190. |> Maps.put_if_present(:actor_type, params[:actor_type])
  191. |> Maps.put_if_present(:also_known_as, params[:also_known_as])
  192. # Note: param name is indeed :locked (not an error)
  193. |> Maps.put_if_present(:is_locked, params[:locked])
  194. # Note: param name is indeed :discoverable (not an error)
  195. |> Maps.put_if_present(:is_discoverable, params[:discoverable])
  196. |> Maps.put_if_present(:birthday, params[:birthday])
  197. |> Maps.put_if_present(:language, Pleroma.Web.Gettext.normalize_locale(params[:language]))
  198. # What happens here:
  199. #
  200. # We want to update the user through the pipeline, but the ActivityPub
  201. # update information is not quite enough for this, because this also
  202. # contains local settings that don't federate and don't even appear
  203. # in the Update activity.
  204. #
  205. # So we first build the normal local changeset, then apply it to the
  206. # user data, but don't persist it. With this, we generate the object
  207. # data for our update activity. We feed this and the changeset as meta
  208. # information into the pipeline, where they will be properly updated and
  209. # federated.
  210. with changeset <- User.update_changeset(user, user_params),
  211. {:ok, unpersisted_user} <- Ecto.Changeset.apply_action(changeset, :update),
  212. updated_object <-
  213. Pleroma.Web.ActivityPub.UserView.render("user.json", user: unpersisted_user)
  214. |> Map.delete("@context"),
  215. {:ok, update_data, []} <- Builder.update(user, updated_object),
  216. {:ok, _update, _} <-
  217. Pipeline.common_pipeline(update_data,
  218. local: true,
  219. user_update_changeset: changeset
  220. ) do
  221. render(conn, "show.json",
  222. user: unpersisted_user,
  223. for: unpersisted_user,
  224. with_pleroma_settings: true
  225. )
  226. else
  227. {:error, %Ecto.Changeset{errors: [avatar: {"file is too large", _}]}} ->
  228. render_error(conn, :request_entity_too_large, "File is too large")
  229. {:error, %Ecto.Changeset{errors: [banner: {"file is too large", _}]}} ->
  230. render_error(conn, :request_entity_too_large, "File is too large")
  231. {:error, %Ecto.Changeset{errors: [background: {"file is too large", _}]}} ->
  232. render_error(conn, :request_entity_too_large, "File is too large")
  233. {:error, %Ecto.Changeset{errors: [{:bio, {_, _}} | _]}} ->
  234. render_error(conn, :request_entity_too_large, "Bio is too long")
  235. {:error, %Ecto.Changeset{errors: [{:name, {_, _}} | _]}} ->
  236. render_error(conn, :request_entity_too_large, "Name is too long")
  237. {:error, %Ecto.Changeset{errors: [{:fields, {"invalid", _}} | _]}} ->
  238. render_error(conn, :request_entity_too_large, "One or more field entries are too long")
  239. {:error, %Ecto.Changeset{errors: [{:fields, {_, _}} | _]}} ->
  240. render_error(conn, :request_entity_too_large, "Too many field entries")
  241. _e ->
  242. render_error(conn, :forbidden, "Invalid request")
  243. end
  244. end
  245. defp normalize_fields_attributes(fields) do
  246. if(Enum.all?(fields, &is_tuple/1), do: Enum.map(fields, fn {_, v} -> v end), else: fields)
  247. |> Enum.map(fn
  248. %{} = field -> %{"name" => field.name, "value" => field.value}
  249. field -> field
  250. end)
  251. end
  252. @doc "GET /api/v1/accounts/relationships"
  253. def relationships(
  254. %{assigns: %{user: user}, private: %{open_api_spex: %{params: %{id: id}}}} = conn,
  255. _
  256. ) do
  257. targets = User.get_all_by_ids(List.wrap(id))
  258. render(conn, "relationships.json", user: user, targets: targets)
  259. end
  260. # Instead of returning a 400 when no "id" params is present, Mastodon returns an empty array.
  261. def relationships(%{assigns: %{user: _user}} = conn, _), do: json(conn, [])
  262. @doc "GET /api/v1/accounts/:id"
  263. def show(
  264. %{
  265. assigns: %{user: for_user},
  266. private: %{open_api_spex: %{params: %{id: nickname_or_id} = params}}
  267. } = conn,
  268. _params
  269. ) do
  270. with %User{} = user <- User.get_cached_by_nickname_or_id(nickname_or_id, for: for_user),
  271. :visible <- User.visible_for(user, for_user) do
  272. render(conn, "show.json",
  273. user: user,
  274. for: for_user,
  275. embed_relationships: embed_relationships?(params)
  276. )
  277. else
  278. error -> user_visibility_error(conn, error)
  279. end
  280. end
  281. @doc "GET /api/v1/accounts/:id/statuses"
  282. def statuses(
  283. %{assigns: %{user: reading_user}, private: %{open_api_spex: %{params: params}}} = conn,
  284. _params
  285. ) do
  286. with %User{} = user <- User.get_cached_by_nickname_or_id(params.id, for: reading_user),
  287. :visible <- User.visible_for(user, reading_user) do
  288. params =
  289. params
  290. |> Map.delete(:tagged)
  291. |> Map.put(:tag, params[:tagged])
  292. activities = ActivityPub.fetch_user_activities(user, reading_user, params)
  293. conn
  294. |> add_link_headers(activities)
  295. |> put_view(StatusView)
  296. |> render("index.json",
  297. activities: activities,
  298. for: reading_user,
  299. as: :activity,
  300. with_muted: Map.get(params, :with_muted, false)
  301. )
  302. else
  303. error -> user_visibility_error(conn, error)
  304. end
  305. end
  306. defp user_visibility_error(conn, error) do
  307. case error do
  308. :restrict_unauthenticated ->
  309. render_error(conn, :unauthorized, "This API requires an authenticated user")
  310. _ ->
  311. render_error(conn, :not_found, "Can't find user")
  312. end
  313. end
  314. @doc "GET /api/v1/accounts/:id/followers"
  315. def followers(
  316. %{assigns: %{user: for_user, account: user}, private: %{open_api_spex: %{params: params}}} =
  317. conn,
  318. _params
  319. ) do
  320. params =
  321. params
  322. |> Enum.map(fn {key, value} -> {to_string(key), value} end)
  323. |> Enum.into(%{})
  324. followers =
  325. cond do
  326. for_user && user.id == for_user.id -> MastodonAPI.get_followers(user, params)
  327. user.hide_followers -> []
  328. true -> MastodonAPI.get_followers(user, params)
  329. end
  330. conn
  331. |> add_link_headers(followers)
  332. # https://git.pleroma.social/pleroma/pleroma-fe/-/issues/838#note_59223
  333. |> render("index.json",
  334. for: for_user,
  335. users: followers,
  336. as: :user,
  337. embed_relationships: embed_relationships?(params)
  338. )
  339. end
  340. @doc "GET /api/v1/accounts/:id/following"
  341. def following(
  342. %{assigns: %{user: for_user, account: user}, private: %{open_api_spex: %{params: params}}} =
  343. conn,
  344. _params
  345. ) do
  346. params =
  347. params
  348. |> Enum.map(fn {key, value} -> {to_string(key), value} end)
  349. |> Enum.into(%{})
  350. followers =
  351. cond do
  352. for_user && user.id == for_user.id -> MastodonAPI.get_friends(user, params)
  353. user.hide_follows -> []
  354. true -> MastodonAPI.get_friends(user, params)
  355. end
  356. conn
  357. |> add_link_headers(followers)
  358. # https://git.pleroma.social/pleroma/pleroma-fe/-/issues/838#note_59223
  359. |> render("index.json",
  360. for: for_user,
  361. users: followers,
  362. as: :user,
  363. embed_relationships: embed_relationships?(params)
  364. )
  365. end
  366. @doc "GET /api/v1/accounts/:id/lists"
  367. def lists(%{assigns: %{user: user, account: account}} = conn, _params) do
  368. lists = Pleroma.List.get_lists_account_belongs(user, account)
  369. conn
  370. |> put_view(ListView)
  371. |> render("index.json", lists: lists)
  372. end
  373. @doc "POST /api/v1/accounts/:id/follow"
  374. def follow(%{assigns: %{user: %{id: id}, account: %{id: id}}}, _params) do
  375. {:error, "Can not follow yourself"}
  376. end
  377. def follow(
  378. %{
  379. assigns: %{user: follower, account: followed},
  380. private: %{open_api_spex: %{body_params: params}}
  381. } = conn,
  382. _
  383. ) do
  384. with {:ok, follower} <- MastodonAPI.follow(follower, followed, params) do
  385. render(conn, "relationship.json", user: follower, target: followed)
  386. else
  387. {:error, message} -> json_response(conn, :forbidden, %{error: message})
  388. end
  389. end
  390. @doc "POST /api/v1/accounts/:id/unfollow"
  391. def unfollow(%{assigns: %{user: %{id: id}, account: %{id: id}}}, _params) do
  392. {:error, "Can not unfollow yourself"}
  393. end
  394. def unfollow(%{assigns: %{user: follower, account: followed}} = conn, _params) do
  395. with {:ok, follower} <- CommonAPI.unfollow(follower, followed) do
  396. render(conn, "relationship.json", user: follower, target: followed)
  397. end
  398. end
  399. @doc "POST /api/v1/accounts/:id/mute"
  400. def mute(
  401. %{
  402. assigns: %{user: muter, account: muted},
  403. private: %{open_api_spex: %{body_params: params}}
  404. } = conn,
  405. _params
  406. ) do
  407. params =
  408. params
  409. |> Map.put_new(:duration, Map.get(params, :expires_in, 0))
  410. with {:ok, _user_relationships} <- User.mute(muter, muted, params) do
  411. render(conn, "relationship.json", user: muter, target: muted)
  412. else
  413. {:error, message} -> json_response(conn, :forbidden, %{error: message})
  414. end
  415. end
  416. @doc "POST /api/v1/accounts/:id/unmute"
  417. def unmute(%{assigns: %{user: muter, account: muted}} = conn, _params) do
  418. with {:ok, _user_relationships} <- User.unmute(muter, muted) do
  419. render(conn, "relationship.json", user: muter, target: muted)
  420. else
  421. {:error, message} -> json_response(conn, :forbidden, %{error: message})
  422. end
  423. end
  424. @doc "POST /api/v1/accounts/:id/block"
  425. def block(%{assigns: %{user: blocker, account: blocked}} = conn, _params) do
  426. with {:ok, _activity} <- CommonAPI.block(blocker, blocked) do
  427. render(conn, "relationship.json", user: blocker, target: blocked)
  428. else
  429. {:error, message} -> json_response(conn, :forbidden, %{error: message})
  430. end
  431. end
  432. @doc "POST /api/v1/accounts/:id/unblock"
  433. def unblock(%{assigns: %{user: blocker, account: blocked}} = conn, _params) do
  434. with {:ok, _activity} <- CommonAPI.unblock(blocker, blocked) do
  435. render(conn, "relationship.json", user: blocker, target: blocked)
  436. else
  437. {:error, message} -> json_response(conn, :forbidden, %{error: message})
  438. end
  439. end
  440. @doc "POST /api/v1/accounts/:id/note"
  441. def note(
  442. %{
  443. assigns: %{user: noter, account: target},
  444. private: %{open_api_spex: %{body_params: %{comment: comment}}}
  445. } = conn,
  446. _params
  447. ) do
  448. with {:ok, _user_note} <- UserNote.create(noter, target, comment) do
  449. render(conn, "relationship.json", user: noter, target: target)
  450. end
  451. end
  452. @doc "POST /api/v1/accounts/:id/pin"
  453. def endorse(%{assigns: %{user: endorser, account: endorsed}} = conn, _params) do
  454. with {:ok, _user_relationships} <- User.endorse(endorser, endorsed) do
  455. render(conn, "relationship.json", user: endorser, target: endorsed)
  456. else
  457. {:error, message} -> json_response(conn, :bad_request, %{error: message})
  458. end
  459. end
  460. @doc "POST /api/v1/accounts/:id/unpin"
  461. def unendorse(%{assigns: %{user: endorser, account: endorsed}} = conn, _params) do
  462. with {:ok, _user_relationships} <- User.unendorse(endorser, endorsed) do
  463. render(conn, "relationship.json", user: endorser, target: endorsed)
  464. else
  465. {:error, message} -> json_response(conn, :forbidden, %{error: message})
  466. end
  467. end
  468. @doc "POST /api/v1/accounts/:id/remove_from_followers"
  469. def remove_from_followers(%{assigns: %{user: %{id: id}, account: %{id: id}}}, _params) do
  470. {:error, "Can not unfollow yourself"}
  471. end
  472. def remove_from_followers(%{assigns: %{user: followed, account: follower}} = conn, _params) do
  473. with {:ok, follower} <- CommonAPI.reject_follow_request(follower, followed) do
  474. render(conn, "relationship.json", user: followed, target: follower)
  475. else
  476. nil ->
  477. render_error(conn, :not_found, "Record not found")
  478. end
  479. end
  480. @doc "POST /api/v1/follows"
  481. def follow_by_uri(%{private: %{open_api_spex: %{body_params: %{uri: uri}}}} = conn, _) do
  482. case User.get_cached_by_nickname(uri) do
  483. %User{} = user ->
  484. conn
  485. |> assign(:account, user)
  486. |> follow(%{})
  487. nil ->
  488. {:error, :not_found}
  489. end
  490. end
  491. @doc "GET /api/v1/mutes"
  492. def mutes(%{assigns: %{user: user}} = conn, params) do
  493. users =
  494. user
  495. |> User.muted_users_relation(_restrict_deactivated = true)
  496. |> Pleroma.Pagination.fetch_paginated(params)
  497. conn
  498. |> add_link_headers(users)
  499. |> render("index.json",
  500. users: users,
  501. for: user,
  502. as: :user,
  503. embed_relationships: embed_relationships?(params),
  504. mutes: true
  505. )
  506. end
  507. @doc "GET /api/v1/blocks"
  508. def blocks(%{assigns: %{user: user}} = conn, params) do
  509. users =
  510. user
  511. |> User.blocked_users_relation(_restrict_deactivated = true)
  512. |> Pleroma.Pagination.fetch_paginated(params)
  513. conn
  514. |> add_link_headers(users)
  515. |> render("index.json",
  516. users: users,
  517. for: user,
  518. as: :user,
  519. embed_relationships: embed_relationships?(params)
  520. )
  521. end
  522. @doc "GET /api/v1/accounts/lookup"
  523. def lookup(%{private: %{open_api_spex: %{params: %{acct: nickname}}}} = conn, _params) do
  524. with %User{} = user <- User.get_by_nickname(nickname) do
  525. render(conn, "show.json",
  526. user: user,
  527. skip_visibility_check: true
  528. )
  529. else
  530. error -> user_visibility_error(conn, error)
  531. end
  532. end
  533. @doc "GET /api/v1/endorsements"
  534. def endorsements(%{assigns: %{user: user}} = conn, params) do
  535. users =
  536. user
  537. |> User.endorsed_users_relation(_restrict_deactivated = true)
  538. |> Pleroma.Repo.all()
  539. conn
  540. |> render("index.json",
  541. users: users,
  542. for: user,
  543. as: :user,
  544. embed_relationships: embed_relationships?(params)
  545. )
  546. end
  547. @doc "GET /api/v1/identity_proofs"
  548. def identity_proofs(conn, params), do: MastodonAPIController.empty_array(conn, params)
  549. end