1 # Pleroma: A lightweight social networking server
2 # Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
3 # SPDX-License-Identifier: AGPL-3.0-only
5 defmodule Pleroma.Web.TwitterAPI.Controller do
6 use Pleroma.Web, :controller
8 import Pleroma.Web.ControllerHelper, only: [json_response: 3]
11 alias Pleroma.Activity
12 alias Pleroma.Formatter
13 alias Pleroma.Notification
17 alias Pleroma.Web.ActivityPub.ActivityPub
18 alias Pleroma.Web.ActivityPub.Visibility
19 alias Pleroma.Web.CommonAPI
20 alias Pleroma.Web.CommonAPI.Utils
21 alias Pleroma.Web.OAuth.Token
22 alias Pleroma.Web.TwitterAPI.ActivityView
23 alias Pleroma.Web.TwitterAPI.NotificationView
24 alias Pleroma.Web.TwitterAPI.TokenView
25 alias Pleroma.Web.TwitterAPI.TwitterAPI
26 alias Pleroma.Web.TwitterAPI.UserView
30 plug(Pleroma.Plugs.RateLimiter, :password_reset when action == :password_reset)
31 plug(:only_if_public_instance when action in [:public_timeline, :public_and_external_timeline])
32 action_fallback(:errors)
34 def verify_credentials(%{assigns: %{user: user}} = conn, _params) do
35 token = Phoenix.Token.sign(conn, "user socket", user.id)
39 |> render("show.json", %{user: user, token: token, for: user})
42 def status_update(%{assigns: %{user: user}} = conn, %{"status" => _} = status_data) do
43 with media_ids <- extract_media_ids(status_data),
45 TwitterAPI.create_status(user, Map.put(status_data, "media_ids", media_ids)) do
47 |> json(ActivityView.render("activity.json", activity: activity, for: user))
49 _ -> empty_status_reply(conn)
53 def status_update(conn, _status_data) do
54 empty_status_reply(conn)
57 defp empty_status_reply(conn) do
58 bad_request_reply(conn, "Client must provide a 'status' parameter with a value.")
61 defp extract_media_ids(status_data) do
62 with media_ids when not is_nil(media_ids) <- status_data["media_ids"],
63 split_ids <- String.split(media_ids, ","),
64 clean_ids <- Enum.reject(split_ids, fn id -> String.length(id) == 0 end) do
71 def public_and_external_timeline(%{assigns: %{user: user}} = conn, params) do
74 |> Map.put("type", ["Create", "Announce"])
75 |> Map.put("blocking_user", user)
77 activities = ActivityPub.fetch_public_activities(params)
80 |> put_view(ActivityView)
81 |> render("index.json", %{activities: activities, for: user})
84 def public_timeline(%{assigns: %{user: user}} = conn, params) do
87 |> Map.put("type", ["Create", "Announce"])
88 |> Map.put("local_only", true)
89 |> Map.put("blocking_user", user)
91 activities = ActivityPub.fetch_public_activities(params)
94 |> put_view(ActivityView)
95 |> render("index.json", %{activities: activities, for: user})
98 def friends_timeline(%{assigns: %{user: user}} = conn, params) do
101 |> Map.put("type", ["Create", "Announce", "Follow", "Like"])
102 |> Map.put("blocking_user", user)
103 |> Map.put("user", user)
105 activities = ActivityPub.fetch_activities([user.ap_id | user.following], params)
108 |> put_view(ActivityView)
109 |> render("index.json", %{activities: activities, for: user})
112 def show_user(conn, params) do
113 for_user = conn.assigns.user
115 with {:ok, shown} <- TwitterAPI.get_user(params),
117 User.auth_active?(shown) ||
118 (for_user && (for_user.id == shown.id || User.superuser?(for_user))) do
121 %{user: shown, for: for_user}
127 |> put_view(UserView)
128 |> render("show.json", params)
131 bad_request_reply(conn, msg)
136 |> json(%{error: "Unconfirmed user"})
140 def user_timeline(%{assigns: %{user: user}} = conn, params) do
141 case TwitterAPI.get_user(user, params) do
142 {:ok, target_user} ->
143 # Twitter and ActivityPub use a different name and sense for this parameter.
144 {include_rts, params} = Map.pop(params, "include_rts")
148 x when x == "false" or x == "0" -> Map.put(params, "exclude_reblogs", "true")
152 activities = ActivityPub.fetch_user_activities(target_user, user, params)
155 |> put_view(ActivityView)
156 |> render("index.json", %{activities: activities, for: user})
159 bad_request_reply(conn, msg)
163 def mentions_timeline(%{assigns: %{user: user}} = conn, params) do
166 |> Map.put("type", ["Create", "Announce", "Follow", "Like"])
167 |> Map.put("blocking_user", user)
168 |> Map.put(:visibility, ~w[unlisted public private])
170 activities = ActivityPub.fetch_activities([user.ap_id], params)
173 |> put_view(ActivityView)
174 |> render("index.json", %{activities: activities, for: user})
177 def dm_timeline(%{assigns: %{user: user}} = conn, params) do
180 |> Map.put("type", "Create")
181 |> Map.put("blocking_user", user)
182 |> Map.put("user", user)
183 |> Map.put(:visibility, "direct")
184 |> Map.put(:order, :desc)
187 ActivityPub.fetch_activities_query([user.ap_id], params)
191 |> put_view(ActivityView)
192 |> render("index.json", %{activities: activities, for: user})
195 def notifications(%{assigns: %{user: user}} = conn, params) do
197 if Map.has_key?(params, "with_muted") do
198 Map.put(params, :with_muted, params["with_muted"] in [true, "True", "true", "1"])
203 notifications = Notification.for_user(user, params)
206 |> put_view(NotificationView)
207 |> render("notification.json", %{notifications: notifications, for: user})
210 def notifications_read(%{assigns: %{user: user}} = conn, %{"latest_id" => latest_id} = params) do
211 Notification.set_read_up_to(user, latest_id)
213 notifications = Notification.for_user(user, params)
216 |> put_view(NotificationView)
217 |> render("notification.json", %{notifications: notifications, for: user})
220 def notifications_read(%{assigns: %{user: _user}} = conn, _) do
221 bad_request_reply(conn, "You need to specify latest_id")
224 def follow(%{assigns: %{user: user}} = conn, params) do
225 case TwitterAPI.follow(user, params) do
226 {:ok, user, followed, _activity} ->
228 |> put_view(UserView)
229 |> render("show.json", %{user: followed, for: user})
232 forbidden_json_reply(conn, msg)
236 def block(%{assigns: %{user: user}} = conn, params) do
237 case TwitterAPI.block(user, params) do
238 {:ok, user, blocked} ->
240 |> put_view(UserView)
241 |> render("show.json", %{user: blocked, for: user})
244 forbidden_json_reply(conn, msg)
248 def unblock(%{assigns: %{user: user}} = conn, params) do
249 case TwitterAPI.unblock(user, params) do
250 {:ok, user, blocked} ->
252 |> put_view(UserView)
253 |> render("show.json", %{user: blocked, for: user})
256 forbidden_json_reply(conn, msg)
260 def delete_post(%{assigns: %{user: user}} = conn, %{"id" => id}) do
261 with {:ok, activity} <- TwitterAPI.delete(user, id) do
263 |> put_view(ActivityView)
264 |> render("activity.json", %{activity: activity, for: user})
268 def unfollow(%{assigns: %{user: user}} = conn, params) do
269 case TwitterAPI.unfollow(user, params) do
270 {:ok, user, unfollowed} ->
272 |> put_view(UserView)
273 |> render("show.json", %{user: unfollowed, for: user})
276 forbidden_json_reply(conn, msg)
280 def fetch_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
281 with %Activity{} = activity <- Activity.get_by_id(id),
282 true <- Visibility.visible_for_user?(activity, user) do
284 |> put_view(ActivityView)
285 |> render("activity.json", %{activity: activity, for: user})
289 def fetch_conversation(%{assigns: %{user: user}} = conn, %{"id" => id}) do
290 with context when is_binary(context) <- Utils.conversation_id_to_context(id),
292 ActivityPub.fetch_activities_for_context(context, %{
293 "blocking_user" => user,
297 |> put_view(ActivityView)
298 |> render("index.json", %{activities: activities, for: user})
303 Updates metadata of uploaded media object.
304 Derived from [Twitter API endpoint](https://developer.twitter.com/en/docs/media/upload-media/api-reference/post-media-metadata-create).
306 def update_media(%{assigns: %{user: user}} = conn, %{"media_id" => id} = data) do
307 object = Repo.get(Object, id)
308 description = get_in(data, ["alt_text", "text"]) || data["name"] || data["description"]
310 {conn, status, response_body} =
313 {halt(conn), :not_found, ""}
315 !Object.authorize_mutation(object, user) ->
316 {halt(conn), :forbidden, "You can only update your own uploads."}
318 !is_binary(description) ->
319 {conn, :not_modified, ""}
322 new_data = Map.put(object.data, "name", description)
326 |> Object.change(%{data: new_data})
329 {conn, :no_content, ""}
333 |> put_status(status)
334 |> json(response_body)
337 def upload(%{assigns: %{user: user}} = conn, %{"media" => media}) do
338 response = TwitterAPI.upload(media, user)
341 |> put_resp_content_type("application/atom+xml")
342 |> send_resp(200, response)
345 def upload_json(%{assigns: %{user: user}} = conn, %{"media" => media}) do
346 response = TwitterAPI.upload(media, user, "json")
349 |> json_reply(200, response)
352 def get_by_id_or_ap_id(id) do
353 activity = Activity.get_by_id(id) || Activity.get_create_by_object_ap_id(id)
355 if activity.data["type"] == "Create" do
358 Activity.get_create_by_object_ap_id(activity.data["object"])
362 def favorite(%{assigns: %{user: user}} = conn, %{"id" => id}) do
363 with {:ok, activity} <- TwitterAPI.fav(user, id) do
365 |> put_view(ActivityView)
366 |> render("activity.json", %{activity: activity, for: user})
368 _ -> json_reply(conn, 400, Jason.encode!(%{}))
372 def unfavorite(%{assigns: %{user: user}} = conn, %{"id" => id}) do
373 with {:ok, activity} <- TwitterAPI.unfav(user, id) do
375 |> put_view(ActivityView)
376 |> render("activity.json", %{activity: activity, for: user})
378 _ -> json_reply(conn, 400, Jason.encode!(%{}))
382 def retweet(%{assigns: %{user: user}} = conn, %{"id" => id}) do
383 with {:ok, activity} <- TwitterAPI.repeat(user, id) do
385 |> put_view(ActivityView)
386 |> render("activity.json", %{activity: activity, for: user})
388 _ -> json_reply(conn, 400, Jason.encode!(%{}))
392 def unretweet(%{assigns: %{user: user}} = conn, %{"id" => id}) do
393 with {:ok, activity} <- TwitterAPI.unrepeat(user, id) do
395 |> put_view(ActivityView)
396 |> render("activity.json", %{activity: activity, for: user})
398 _ -> json_reply(conn, 400, Jason.encode!(%{}))
402 def pin(%{assigns: %{user: user}} = conn, %{"id" => id}) do
403 with {:ok, activity} <- TwitterAPI.pin(user, id) do
405 |> put_view(ActivityView)
406 |> render("activity.json", %{activity: activity, for: user})
408 {:error, message} -> bad_request_reply(conn, message)
413 def unpin(%{assigns: %{user: user}} = conn, %{"id" => id}) do
414 with {:ok, activity} <- TwitterAPI.unpin(user, id) do
416 |> put_view(ActivityView)
417 |> render("activity.json", %{activity: activity, for: user})
419 {:error, message} -> bad_request_reply(conn, message)
424 def register(conn, params) do
425 with {:ok, user} <- TwitterAPI.register_user(params) do
427 |> put_view(UserView)
428 |> render("show.json", %{user: user})
432 |> json_reply(400, Jason.encode!(errors))
436 def password_reset(conn, params) do
437 nickname_or_email = params["email"] || params["nickname"]
439 with {:ok, _} <- TwitterAPI.password_reset(nickname_or_email) do
440 json_response(conn, :no_content, "")
442 {:error, "unknown user"} ->
443 send_resp(conn, :not_found, "")
446 send_resp(conn, :bad_request, "")
450 def confirm_email(conn, %{"user_id" => uid, "token" => token}) do
451 with %User{} = user <- User.get_cached_by_id(uid),
453 true <- user.info.confirmation_pending,
454 true <- user.info.confirmation_token == token,
455 info_change <- User.Info.confirmation_changeset(user.info, need_confirmation: false),
456 changeset <- Changeset.change(user) |> Changeset.put_embed(:info, info_change),
457 {:ok, _} <- User.update_and_set_cache(changeset) do
463 def resend_confirmation_email(conn, params) do
464 nickname_or_email = params["email"] || params["nickname"]
466 with %User{} = user <- User.get_by_nickname_or_email(nickname_or_email),
467 {:ok, _} <- User.try_send_confirmation_email(user) do
469 |> json_response(:no_content, "")
473 def update_avatar(%{assigns: %{user: user}} = conn, %{"img" => ""}) do
474 change = Changeset.change(user, %{avatar: nil})
475 {:ok, user} = User.update_and_set_cache(change)
476 CommonAPI.update(user)
479 |> put_view(UserView)
480 |> render("show.json", %{user: user, for: user})
483 def update_avatar(%{assigns: %{user: user}} = conn, params) do
484 {:ok, object} = ActivityPub.upload(params, type: :avatar)
485 change = Changeset.change(user, %{avatar: object.data})
486 {:ok, user} = User.update_and_set_cache(change)
487 CommonAPI.update(user)
490 |> put_view(UserView)
491 |> render("show.json", %{user: user, for: user})
494 def update_banner(%{assigns: %{user: user}} = conn, %{"banner" => ""}) do
495 with new_info <- %{"banner" => %{}},
496 info_cng <- User.Info.profile_update(user.info, new_info),
497 changeset <- Ecto.Changeset.change(user) |> Ecto.Changeset.put_embed(:info, info_cng),
498 {:ok, user} <- User.update_and_set_cache(changeset) do
499 CommonAPI.update(user)
500 response = %{url: nil} |> Jason.encode!()
503 |> json_reply(200, response)
507 def update_banner(%{assigns: %{user: user}} = conn, params) do
508 with {:ok, object} <- ActivityPub.upload(%{"img" => params["banner"]}, type: :banner),
509 new_info <- %{"banner" => object.data},
510 info_cng <- User.Info.profile_update(user.info, new_info),
511 changeset <- Ecto.Changeset.change(user) |> Ecto.Changeset.put_embed(:info, info_cng),
512 {:ok, user} <- User.update_and_set_cache(changeset) do
513 CommonAPI.update(user)
514 %{"url" => [%{"href" => href} | _]} = object.data
515 response = %{url: href} |> Jason.encode!()
518 |> json_reply(200, response)
522 def update_background(%{assigns: %{user: user}} = conn, %{"img" => ""}) do
523 with new_info <- %{"background" => %{}},
524 info_cng <- User.Info.profile_update(user.info, new_info),
525 changeset <- Ecto.Changeset.change(user) |> Ecto.Changeset.put_embed(:info, info_cng),
526 {:ok, _user} <- User.update_and_set_cache(changeset) do
527 response = %{url: nil} |> Jason.encode!()
530 |> json_reply(200, response)
534 def update_background(%{assigns: %{user: user}} = conn, params) do
535 with {:ok, object} <- ActivityPub.upload(params, type: :background),
536 new_info <- %{"background" => object.data},
537 info_cng <- User.Info.profile_update(user.info, new_info),
538 changeset <- Ecto.Changeset.change(user) |> Ecto.Changeset.put_embed(:info, info_cng),
539 {:ok, _user} <- User.update_and_set_cache(changeset) do
540 %{"url" => [%{"href" => href} | _]} = object.data
541 response = %{url: href} |> Jason.encode!()
544 |> json_reply(200, response)
548 def external_profile(%{assigns: %{user: current_user}} = conn, %{"profileurl" => uri}) do
549 with {:ok, user_map} <- TwitterAPI.get_external_profile(current_user, uri),
550 response <- Jason.encode!(user_map) do
552 |> json_reply(200, response)
557 |> json(%{error: "Can't find user"})
561 def followers(%{assigns: %{user: for_user}} = conn, params) do
562 {:ok, page} = Ecto.Type.cast(:integer, params["page"] || 1)
564 with {:ok, user} <- TwitterAPI.get_user(for_user, params),
565 {:ok, followers} <- User.get_followers(user, page) do
568 for_user && user.id == for_user.id -> followers
569 user.info.hide_followers -> []
574 |> put_view(UserView)
575 |> render("index.json", %{users: followers, for: conn.assigns[:user]})
577 _e -> bad_request_reply(conn, "Can't get followers")
581 def friends(%{assigns: %{user: for_user}} = conn, params) do
582 {:ok, page} = Ecto.Type.cast(:integer, params["page"] || 1)
583 {:ok, export} = Ecto.Type.cast(:boolean, params["all"] || false)
585 page = if export, do: nil, else: page
587 with {:ok, user} <- TwitterAPI.get_user(conn.assigns[:user], params),
588 {:ok, friends} <- User.get_friends(user, page) do
591 for_user && user.id == for_user.id -> friends
592 user.info.hide_follows -> []
597 |> put_view(UserView)
598 |> render("index.json", %{users: friends, for: conn.assigns[:user]})
600 _e -> bad_request_reply(conn, "Can't get friends")
604 def oauth_tokens(%{assigns: %{user: user}} = conn, _params) do
605 with oauth_tokens <- Token.get_user_tokens(user) do
607 |> put_view(TokenView)
608 |> render("index.json", %{tokens: oauth_tokens})
612 def revoke_token(%{assigns: %{user: user}} = conn, %{"id" => id} = _params) do
613 Token.delete_user_token(user, id)
615 json_reply(conn, 201, "")
618 def blocks(%{assigns: %{user: user}} = conn, _params) do
619 with blocked_users <- User.blocked_users(user) do
621 |> put_view(UserView)
622 |> render("index.json", %{users: blocked_users, for: user})
626 def friend_requests(conn, params) do
627 with {:ok, user} <- TwitterAPI.get_user(conn.assigns[:user], params),
628 {:ok, friend_requests} <- User.get_follow_requests(user) do
630 |> put_view(UserView)
631 |> render("index.json", %{users: friend_requests, for: conn.assigns[:user]})
633 _e -> bad_request_reply(conn, "Can't get friend requests")
637 def approve_friend_request(conn, %{"user_id" => uid} = _params) do
638 with followed <- conn.assigns[:user],
639 %User{} = follower <- User.get_cached_by_id(uid),
640 {:ok, follower} <- CommonAPI.accept_follow_request(follower, followed) do
642 |> put_view(UserView)
643 |> render("show.json", %{user: follower, for: followed})
645 e -> bad_request_reply(conn, "Can't approve user: #{inspect(e)}")
649 def deny_friend_request(conn, %{"user_id" => uid} = _params) do
650 with followed <- conn.assigns[:user],
651 %User{} = follower <- User.get_cached_by_id(uid),
652 {:ok, follower} <- CommonAPI.reject_follow_request(follower, followed) do
654 |> put_view(UserView)
655 |> render("show.json", %{user: follower, for: followed})
657 e -> bad_request_reply(conn, "Can't deny user: #{inspect(e)}")
661 def friends_ids(%{assigns: %{user: user}} = conn, _params) do
662 with {:ok, friends} <- User.get_friends(user) do
665 |> Enum.map(fn x -> x.id end)
670 _e -> bad_request_reply(conn, "Can't get friends")
674 def empty_array(conn, _params) do
675 json(conn, Jason.encode!([]))
678 def raw_empty_array(conn, _params) do
682 defp build_info_cng(user, params) do
691 "skip_thread_containment"
693 |> Enum.reduce(%{}, fn key, res ->
694 if value = params[key] do
695 Map.put(res, key, value == "true")
702 if value = params["default_scope"] do
703 Map.put(info_params, "default_scope", value)
708 User.Info.profile_update(user.info, info_params)
711 defp parse_profile_bio(user, params) do
712 if bio = params["description"] do
713 emojis_text = (params["description"] || "") <> " " <> (params["name"] || "")
716 ((user.info.emoji || []) ++ Formatter.get_emoji_map(emojis_text))
727 |> Map.put("bio", User.parse_bio(bio, user))
728 |> Map.put("info", user_info)
734 def update_profile(%{assigns: %{user: user}} = conn, params) do
735 params = parse_profile_bio(user, params)
736 info_cng = build_info_cng(user, params)
738 with changeset <- User.update_changeset(user, params),
739 changeset <- Ecto.Changeset.put_embed(changeset, :info, info_cng),
740 {:ok, user} <- User.update_and_set_cache(changeset) do
741 CommonAPI.update(user)
744 |> put_view(UserView)
745 |> render("user.json", %{user: user, for: user})
748 Logger.debug("Can't update user: #{inspect(error)}")
749 bad_request_reply(conn, "Can't update user")
753 def search(%{assigns: %{user: user}} = conn, %{"q" => _query} = params) do
754 activities = TwitterAPI.search(user, params)
757 |> put_view(ActivityView)
758 |> render("index.json", %{activities: activities, for: user})
761 def search_user(%{assigns: %{user: user}} = conn, %{"query" => query}) do
762 users = User.search(query, resolve: true, for_user: user)
765 |> put_view(UserView)
766 |> render("index.json", %{users: users, for: user})
769 defp bad_request_reply(conn, error_message) do
770 json = error_json(conn, error_message)
771 json_reply(conn, 400, json)
774 defp json_reply(conn, status, json) do
776 |> put_resp_content_type("application/json")
777 |> send_resp(status, json)
780 defp forbidden_json_reply(conn, error_message) do
781 json = error_json(conn, error_message)
782 json_reply(conn, 403, json)
785 def only_if_public_instance(%{assigns: %{user: %User{}}} = conn, _), do: conn
787 def only_if_public_instance(conn, _) do
788 if Pleroma.Config.get([:instance, :public]) do
792 |> forbidden_json_reply("Invalid credentials.")
797 defp error_json(conn, error_message) do
798 %{"error" => error_message, "request" => conn.request_path} |> Jason.encode!()
801 def errors(conn, {:param_cast, _}) do
804 |> json("Invalid parameters")
807 def errors(conn, _) do
810 |> json("Something went wrong")