Fork of Pleroma with site-specific changes and feature branches https://git.pleroma.social/pleroma/pleroma
選択できるのは25トピックまでです。 トピックは、先頭が英数字で、英数字とダッシュ('-')を使用した35文字以内のものにしてください。

462 行
15KB

  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.AccountController do
  5. use Pleroma.Web, :controller
  6. import Pleroma.Web.ControllerHelper,
  7. only: [
  8. add_link_headers: 2,
  9. truthy_param?: 1,
  10. assign_account_by_id: 2,
  11. embed_relationships?: 1,
  12. json_response: 3
  13. ]
  14. alias Pleroma.Maps
  15. alias Pleroma.User
  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.OAuth.OAuthView
  26. alias Pleroma.Web.Plugs.EnsurePublicOrAuthenticatedPlug
  27. alias Pleroma.Web.Plugs.OAuthScopesPlug
  28. alias Pleroma.Web.Plugs.RateLimiter
  29. alias Pleroma.Web.TwitterAPI.TwitterAPI
  30. plug(Pleroma.Web.ApiSpec.CastAndValidate)
  31. plug(:skip_plug, [OAuthScopesPlug, EnsurePublicOrAuthenticatedPlug] when action == :create)
  32. plug(:skip_plug, EnsurePublicOrAuthenticatedPlug when action in [:show, :statuses])
  33. plug(
  34. OAuthScopesPlug,
  35. %{fallback: :proceed_unauthenticated, scopes: ["read:accounts"]}
  36. when action in [:show, :followers, :following]
  37. )
  38. plug(
  39. OAuthScopesPlug,
  40. %{fallback: :proceed_unauthenticated, scopes: ["read:statuses"]}
  41. when action == :statuses
  42. )
  43. plug(
  44. OAuthScopesPlug,
  45. %{scopes: ["read:accounts"]}
  46. when action in [:verify_credentials, :endorsements, :identity_proofs]
  47. )
  48. plug(OAuthScopesPlug, %{scopes: ["write:accounts"]} when action == :update_credentials)
  49. plug(OAuthScopesPlug, %{scopes: ["read:lists"]} when action == :lists)
  50. plug(
  51. OAuthScopesPlug,
  52. %{scopes: ["follow", "read:blocks"]} when action == :blocks
  53. )
  54. plug(
  55. OAuthScopesPlug,
  56. %{scopes: ["follow", "write:blocks"]} when action in [:block, :unblock]
  57. )
  58. plug(OAuthScopesPlug, %{scopes: ["read:follows"]} when action == :relationships)
  59. plug(
  60. OAuthScopesPlug,
  61. %{scopes: ["follow", "write:follows"]} when action in [:follow_by_uri, :follow, :unfollow]
  62. )
  63. plug(OAuthScopesPlug, %{scopes: ["follow", "read:mutes"]} when action == :mutes)
  64. plug(OAuthScopesPlug, %{scopes: ["follow", "write:mutes"]} when action in [:mute, :unmute])
  65. @relationship_actions [:follow, :unfollow]
  66. @needs_account ~W(followers following lists follow unfollow mute unmute block unblock)a
  67. plug(
  68. RateLimiter,
  69. [name: :relation_id_action, params: [:id, :uri]] when action in @relationship_actions
  70. )
  71. plug(RateLimiter, [name: :relations_actions] when action in @relationship_actions)
  72. plug(RateLimiter, [name: :app_account_creation] when action == :create)
  73. plug(:assign_account_by_id when action in @needs_account)
  74. action_fallback(Pleroma.Web.MastodonAPI.FallbackController)
  75. defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.AccountOperation
  76. @doc "POST /api/v1/accounts"
  77. def create(%{assigns: %{app: app}, body_params: params} = conn, _params) do
  78. with :ok <- validate_email_param(params),
  79. :ok <- TwitterAPI.validate_captcha(app, params),
  80. {:ok, user} <- TwitterAPI.register_user(params),
  81. {_, {:ok, token}} <-
  82. {:login, OAuthController.login(user, app, app.scopes)} do
  83. json(conn, OAuthView.render("token.json", %{user: user, token: token}))
  84. else
  85. {:login, {:account_status, :confirmation_pending}} ->
  86. json_response(conn, :ok, %{
  87. message: "You have been registered. Please check your email for further instructions.",
  88. identifier: "missing_confirmed_email"
  89. })
  90. {:login, {:account_status, :approval_pending}} ->
  91. json_response(conn, :ok, %{
  92. message:
  93. "You have been registered. You'll be able to log in once your account is approved.",
  94. identifier: "awaiting_approval"
  95. })
  96. {:login, _} ->
  97. json_response(conn, :ok, %{
  98. message:
  99. "You have been registered. Some post-registration steps may be pending. " <>
  100. "Please log in manually.",
  101. identifier: "manual_login_required"
  102. })
  103. {:error, error} ->
  104. json_response(conn, :bad_request, %{error: error})
  105. end
  106. end
  107. def create(%{assigns: %{app: _app}} = conn, _) do
  108. render_error(conn, :bad_request, "Missing parameters")
  109. end
  110. def create(conn, _) do
  111. render_error(conn, :forbidden, "Invalid credentials")
  112. end
  113. defp validate_email_param(%{email: email}) when not is_nil(email), do: :ok
  114. defp validate_email_param(_) do
  115. case Pleroma.Config.get([:instance, :account_activation_required]) do
  116. true -> {:error, dgettext("errors", "Missing parameter: %{name}", name: "email")}
  117. _ -> :ok
  118. end
  119. end
  120. @doc "GET /api/v1/accounts/verify_credentials"
  121. def verify_credentials(%{assigns: %{user: user}} = conn, _) do
  122. chat_token = Phoenix.Token.sign(conn, "user socket", user.id)
  123. render(conn, "show.json",
  124. user: user,
  125. for: user,
  126. with_pleroma_settings: true,
  127. with_chat_token: chat_token
  128. )
  129. end
  130. @doc "PATCH /api/v1/accounts/update_credentials"
  131. def update_credentials(%{assigns: %{user: user}, body_params: params} = conn, _params) do
  132. params =
  133. params
  134. |> Enum.filter(fn {_, value} -> not is_nil(value) end)
  135. |> Enum.into(%{})
  136. # We use an empty string as a special value to reset
  137. # avatars, banners, backgrounds
  138. user_image_value = fn
  139. "" -> {:ok, nil}
  140. value -> {:ok, value}
  141. end
  142. user_params =
  143. [
  144. :no_rich_text,
  145. :locked,
  146. :hide_followers_count,
  147. :hide_follows_count,
  148. :hide_followers,
  149. :hide_follows,
  150. :hide_favorites,
  151. :show_role,
  152. :skip_thread_containment,
  153. :allow_following_move,
  154. :discoverable,
  155. :accepts_chat_messages
  156. ]
  157. |> Enum.reduce(%{}, fn key, acc ->
  158. Maps.put_if_present(acc, key, params[key], &{:ok, truthy_param?(&1)})
  159. end)
  160. |> Maps.put_if_present(:name, params[:display_name])
  161. |> Maps.put_if_present(:bio, params[:note])
  162. |> Maps.put_if_present(:raw_bio, params[:note])
  163. |> Maps.put_if_present(:avatar, params[:avatar], user_image_value)
  164. |> Maps.put_if_present(:banner, params[:header], user_image_value)
  165. |> Maps.put_if_present(:background, params[:pleroma_background_image], user_image_value)
  166. |> Maps.put_if_present(
  167. :raw_fields,
  168. params[:fields_attributes],
  169. &{:ok, normalize_fields_attributes(&1)}
  170. )
  171. |> Maps.put_if_present(:pleroma_settings_store, params[:pleroma_settings_store])
  172. |> Maps.put_if_present(:default_scope, params[:default_scope])
  173. |> Maps.put_if_present(:default_scope, params["source"]["privacy"])
  174. |> Maps.put_if_present(:actor_type, params[:bot], fn bot ->
  175. if bot, do: {:ok, "Service"}, else: {:ok, "Person"}
  176. end)
  177. |> Maps.put_if_present(:actor_type, params[:actor_type])
  178. # What happens here:
  179. #
  180. # We want to update the user through the pipeline, but the ActivityPub
  181. # update information is not quite enough for this, because this also
  182. # contains local settings that don't federate and don't even appear
  183. # in the Update activity.
  184. #
  185. # So we first build the normal local changeset, then apply it to the
  186. # user data, but don't persist it. With this, we generate the object
  187. # data for our update activity. We feed this and the changeset as meta
  188. # inforation into the pipeline, where they will be properly updated and
  189. # federated.
  190. with changeset <- User.update_changeset(user, user_params),
  191. {:ok, unpersisted_user} <- Ecto.Changeset.apply_action(changeset, :update),
  192. updated_object <-
  193. Pleroma.Web.ActivityPub.UserView.render("user.json", user: unpersisted_user)
  194. |> Map.delete("@context"),
  195. {:ok, update_data, []} <- Builder.update(user, updated_object),
  196. {:ok, _update, _} <-
  197. Pipeline.common_pipeline(update_data,
  198. local: true,
  199. user_update_changeset: changeset
  200. ) do
  201. render(conn, "show.json",
  202. user: unpersisted_user,
  203. for: unpersisted_user,
  204. with_pleroma_settings: true
  205. )
  206. else
  207. _e -> render_error(conn, :forbidden, "Invalid request")
  208. end
  209. end
  210. defp normalize_fields_attributes(fields) do
  211. if Enum.all?(fields, &is_tuple/1) do
  212. Enum.map(fields, fn {_, v} -> v end)
  213. else
  214. Enum.map(fields, fn
  215. %{} = field -> %{"name" => field.name, "value" => field.value}
  216. field -> field
  217. end)
  218. end
  219. end
  220. @doc "GET /api/v1/accounts/relationships"
  221. def relationships(%{assigns: %{user: user}} = conn, %{id: id}) do
  222. targets = User.get_all_by_ids(List.wrap(id))
  223. render(conn, "relationships.json", user: user, targets: targets)
  224. end
  225. # Instead of returning a 400 when no "id" params is present, Mastodon returns an empty array.
  226. def relationships(%{assigns: %{user: _user}} = conn, _), do: json(conn, [])
  227. @doc "GET /api/v1/accounts/:id"
  228. def show(%{assigns: %{user: for_user}} = conn, %{id: nickname_or_id}) do
  229. with %User{} = user <- User.get_cached_by_nickname_or_id(nickname_or_id, for: for_user),
  230. :visible <- User.visible_for(user, for_user) do
  231. render(conn, "show.json", user: user, for: for_user)
  232. else
  233. error -> user_visibility_error(conn, error)
  234. end
  235. end
  236. @doc "GET /api/v1/accounts/:id/statuses"
  237. def statuses(%{assigns: %{user: reading_user}} = conn, params) do
  238. with %User{} = user <- User.get_cached_by_nickname_or_id(params.id, for: reading_user),
  239. :visible <- User.visible_for(user, reading_user) do
  240. params =
  241. params
  242. |> Map.delete(:tagged)
  243. |> Map.put(:tag, params[:tagged])
  244. activities = ActivityPub.fetch_user_activities(user, reading_user, params)
  245. conn
  246. |> add_link_headers(activities)
  247. |> put_view(StatusView)
  248. |> render("index.json",
  249. activities: activities,
  250. for: reading_user,
  251. as: :activity
  252. )
  253. else
  254. error -> user_visibility_error(conn, error)
  255. end
  256. end
  257. defp user_visibility_error(conn, error) do
  258. case error do
  259. :restrict_unauthenticated ->
  260. render_error(conn, :unauthorized, "This API requires an authenticated user")
  261. _ ->
  262. render_error(conn, :not_found, "Can't find user")
  263. end
  264. end
  265. @doc "GET /api/v1/accounts/:id/followers"
  266. def followers(%{assigns: %{user: for_user, account: user}} = conn, params) do
  267. params =
  268. params
  269. |> Enum.map(fn {key, value} -> {to_string(key), value} end)
  270. |> Enum.into(%{})
  271. followers =
  272. cond do
  273. for_user && user.id == for_user.id -> MastodonAPI.get_followers(user, params)
  274. user.hide_followers -> []
  275. true -> MastodonAPI.get_followers(user, params)
  276. end
  277. conn
  278. |> add_link_headers(followers)
  279. # https://git.pleroma.social/pleroma/pleroma-fe/-/issues/838#note_59223
  280. |> render("index.json",
  281. for: for_user,
  282. users: followers,
  283. as: :user,
  284. embed_relationships: embed_relationships?(params)
  285. )
  286. end
  287. @doc "GET /api/v1/accounts/:id/following"
  288. def following(%{assigns: %{user: for_user, account: user}} = conn, params) do
  289. params =
  290. params
  291. |> Enum.map(fn {key, value} -> {to_string(key), value} end)
  292. |> Enum.into(%{})
  293. followers =
  294. cond do
  295. for_user && user.id == for_user.id -> MastodonAPI.get_friends(user, params)
  296. user.hide_follows -> []
  297. true -> MastodonAPI.get_friends(user, params)
  298. end
  299. conn
  300. |> add_link_headers(followers)
  301. # https://git.pleroma.social/pleroma/pleroma-fe/-/issues/838#note_59223
  302. |> render("index.json",
  303. for: for_user,
  304. users: followers,
  305. as: :user,
  306. embed_relationships: embed_relationships?(params)
  307. )
  308. end
  309. @doc "GET /api/v1/accounts/:id/lists"
  310. def lists(%{assigns: %{user: user, account: account}} = conn, _params) do
  311. lists = Pleroma.List.get_lists_account_belongs(user, account)
  312. conn
  313. |> put_view(ListView)
  314. |> render("index.json", lists: lists)
  315. end
  316. @doc "POST /api/v1/accounts/:id/follow"
  317. def follow(%{assigns: %{user: %{id: id}, account: %{id: id}}}, _params) do
  318. {:error, "Can not follow yourself"}
  319. end
  320. def follow(%{body_params: params, assigns: %{user: follower, account: followed}} = conn, _) do
  321. with {:ok, follower} <- MastodonAPI.follow(follower, followed, params) do
  322. render(conn, "relationship.json", user: follower, target: followed)
  323. else
  324. {:error, message} -> json_response(conn, :forbidden, %{error: message})
  325. end
  326. end
  327. @doc "POST /api/v1/accounts/:id/unfollow"
  328. def unfollow(%{assigns: %{user: %{id: id}, account: %{id: id}}}, _params) do
  329. {:error, "Can not unfollow yourself"}
  330. end
  331. def unfollow(%{assigns: %{user: follower, account: followed}} = conn, _params) do
  332. with {:ok, follower} <- CommonAPI.unfollow(follower, followed) do
  333. render(conn, "relationship.json", user: follower, target: followed)
  334. end
  335. end
  336. @doc "POST /api/v1/accounts/:id/mute"
  337. def mute(%{assigns: %{user: muter, account: muted}, body_params: params} = conn, _params) do
  338. with {:ok, _user_relationships} <- User.mute(muter, muted, params.notifications) do
  339. render(conn, "relationship.json", user: muter, target: muted)
  340. else
  341. {:error, message} -> json_response(conn, :forbidden, %{error: message})
  342. end
  343. end
  344. @doc "POST /api/v1/accounts/:id/unmute"
  345. def unmute(%{assigns: %{user: muter, account: muted}} = conn, _params) do
  346. with {:ok, _user_relationships} <- User.unmute(muter, muted) do
  347. render(conn, "relationship.json", user: muter, target: muted)
  348. else
  349. {:error, message} -> json_response(conn, :forbidden, %{error: message})
  350. end
  351. end
  352. @doc "POST /api/v1/accounts/:id/block"
  353. def block(%{assigns: %{user: blocker, account: blocked}} = conn, _params) do
  354. with {:ok, _activity} <- CommonAPI.block(blocker, blocked) do
  355. render(conn, "relationship.json", user: blocker, target: blocked)
  356. else
  357. {:error, message} -> json_response(conn, :forbidden, %{error: message})
  358. end
  359. end
  360. @doc "POST /api/v1/accounts/:id/unblock"
  361. def unblock(%{assigns: %{user: blocker, account: blocked}} = conn, _params) do
  362. with {:ok, _activity} <- CommonAPI.unblock(blocker, blocked) do
  363. render(conn, "relationship.json", user: blocker, target: blocked)
  364. else
  365. {:error, message} -> json_response(conn, :forbidden, %{error: message})
  366. end
  367. end
  368. @doc "POST /api/v1/follows"
  369. def follow_by_uri(%{body_params: %{uri: uri}} = conn, _) do
  370. case User.get_cached_by_nickname(uri) do
  371. %User{} = user ->
  372. conn
  373. |> assign(:account, user)
  374. |> follow(%{})
  375. nil ->
  376. {:error, :not_found}
  377. end
  378. end
  379. @doc "GET /api/v1/mutes"
  380. def mutes(%{assigns: %{user: user}} = conn, _) do
  381. users = User.muted_users(user, _restrict_deactivated = true)
  382. render(conn, "index.json", users: users, for: user, as: :user)
  383. end
  384. @doc "GET /api/v1/blocks"
  385. def blocks(%{assigns: %{user: user}} = conn, _) do
  386. users = User.blocked_users(user, _restrict_deactivated = true)
  387. render(conn, "index.json", users: users, for: user, as: :user)
  388. end
  389. @doc "GET /api/v1/endorsements"
  390. def endorsements(conn, params), do: MastodonAPIController.empty_array(conn, params)
  391. @doc "GET /api/v1/identity_proofs"
  392. def identity_proofs(conn, params), do: MastodonAPIController.empty_array(conn, params)
  393. end