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(:only_if_public_instance when action in [:public_timeline, :public_and_external_timeline])
31 action_fallback(:errors)
33 def verify_credentials(%{assigns: %{user: user}} = conn, _params) do
34 token = Phoenix.Token.sign(conn, "user socket", user.id)
38 |> render("show.json", %{user: user, token: token, for: user})
41 def status_update(%{assigns: %{user: user}} = conn, %{"status" => _} = status_data) do
42 with media_ids <- extract_media_ids(status_data),
44 TwitterAPI.create_status(user, Map.put(status_data, "media_ids", media_ids)) do
46 |> json(ActivityView.render("activity.json", activity: activity, for: user))
48 _ -> empty_status_reply(conn)
52 def status_update(conn, _status_data) do
53 empty_status_reply(conn)
56 defp empty_status_reply(conn) do
57 bad_request_reply(conn, "Client must provide a 'status' parameter with a value.")
60 defp extract_media_ids(status_data) do
61 with media_ids when not is_nil(media_ids) <- status_data["media_ids"],
62 split_ids <- String.split(media_ids, ","),
63 clean_ids <- Enum.reject(split_ids, fn id -> String.length(id) == 0 end) do
70 def public_and_external_timeline(%{assigns: %{user: user}} = conn, params) do
73 |> Map.put("type", ["Create", "Announce"])
74 |> Map.put("blocking_user", user)
76 activities = ActivityPub.fetch_public_activities(params)
79 |> put_view(ActivityView)
80 |> render("index.json", %{activities: activities, for: user})
83 def public_timeline(%{assigns: %{user: user}} = conn, params) do
86 |> Map.put("type", ["Create", "Announce"])
87 |> Map.put("local_only", true)
88 |> Map.put("blocking_user", user)
90 activities = ActivityPub.fetch_public_activities(params)
93 |> put_view(ActivityView)
94 |> render("index.json", %{activities: activities, for: user})
97 def friends_timeline(%{assigns: %{user: user}} = conn, params) do
100 |> Map.put("type", ["Create", "Announce", "Follow", "Like"])
101 |> Map.put("blocking_user", user)
102 |> Map.put("user", user)
104 activities = ActivityPub.fetch_activities([user.ap_id | user.following], params)
107 |> put_view(ActivityView)
108 |> render("index.json", %{activities: activities, for: user})
111 def show_user(conn, params) do
112 for_user = conn.assigns.user
114 with {:ok, shown} <- TwitterAPI.get_user(params),
116 User.auth_active?(shown) ||
117 (for_user && (for_user.id == shown.id || User.superuser?(for_user))) do
120 %{user: shown, for: for_user}
126 |> put_view(UserView)
127 |> render("show.json", params)
130 bad_request_reply(conn, msg)
135 |> json(%{error: "Unconfirmed user"})
139 def user_timeline(%{assigns: %{user: user}} = conn, params) do
140 case TwitterAPI.get_user(user, params) do
141 {:ok, target_user} ->
142 # Twitter and ActivityPub use a different name and sense for this parameter.
143 {include_rts, params} = Map.pop(params, "include_rts")
147 x when x == "false" or x == "0" -> Map.put(params, "exclude_reblogs", "true")
151 activities = ActivityPub.fetch_user_activities(target_user, user, params)
154 |> put_view(ActivityView)
155 |> render("index.json", %{activities: activities, for: user})
158 bad_request_reply(conn, msg)
162 def mentions_timeline(%{assigns: %{user: user}} = conn, params) do
165 |> Map.put("type", ["Create", "Announce", "Follow", "Like"])
166 |> Map.put("blocking_user", user)
167 |> Map.put(:visibility, ~w[unlisted public private])
169 activities = ActivityPub.fetch_activities([user.ap_id], params)
172 |> put_view(ActivityView)
173 |> render("index.json", %{activities: activities, for: user})
176 def dm_timeline(%{assigns: %{user: user}} = conn, params) do
179 |> Map.put("type", "Create")
180 |> Map.put("blocking_user", user)
181 |> Map.put("user", user)
182 |> Map.put(:visibility, "direct")
183 |> Map.put(:order, :desc)
186 ActivityPub.fetch_activities_query([user.ap_id], params)
190 |> put_view(ActivityView)
191 |> render("index.json", %{activities: activities, for: user})
194 def notifications(%{assigns: %{user: user}} = conn, params) do
195 notifications = Notification.for_user(user, params)
198 |> put_view(NotificationView)
199 |> render("notification.json", %{notifications: notifications, for: user})
202 def notifications_read(%{assigns: %{user: user}} = conn, %{"latest_id" => latest_id} = params) do
203 Notification.set_read_up_to(user, latest_id)
205 notifications = Notification.for_user(user, params)
208 |> put_view(NotificationView)
209 |> render("notification.json", %{notifications: notifications, for: user})
212 def notifications_read(%{assigns: %{user: _user}} = conn, _) do
213 bad_request_reply(conn, "You need to specify latest_id")
216 def follow(%{assigns: %{user: user}} = conn, params) do
217 case TwitterAPI.follow(user, params) do
218 {:ok, user, followed, _activity} ->
220 |> put_view(UserView)
221 |> render("show.json", %{user: followed, for: user})
224 forbidden_json_reply(conn, msg)
228 def block(%{assigns: %{user: user}} = conn, params) do
229 case TwitterAPI.block(user, params) do
230 {:ok, user, blocked} ->
232 |> put_view(UserView)
233 |> render("show.json", %{user: blocked, for: user})
236 forbidden_json_reply(conn, msg)
240 def unblock(%{assigns: %{user: user}} = conn, params) do
241 case TwitterAPI.unblock(user, params) do
242 {:ok, user, blocked} ->
244 |> put_view(UserView)
245 |> render("show.json", %{user: blocked, for: user})
248 forbidden_json_reply(conn, msg)
252 def delete_post(%{assigns: %{user: user}} = conn, %{"id" => id}) do
253 with {:ok, activity} <- TwitterAPI.delete(user, id) do
255 |> put_view(ActivityView)
256 |> render("activity.json", %{activity: activity, for: user})
260 def unfollow(%{assigns: %{user: user}} = conn, params) do
261 case TwitterAPI.unfollow(user, params) do
262 {:ok, user, unfollowed} ->
264 |> put_view(UserView)
265 |> render("show.json", %{user: unfollowed, for: user})
268 forbidden_json_reply(conn, msg)
272 def fetch_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
273 with %Activity{} = activity <- Activity.get_by_id(id),
274 true <- Visibility.visible_for_user?(activity, user) do
276 |> put_view(ActivityView)
277 |> render("activity.json", %{activity: activity, for: user})
281 def fetch_conversation(%{assigns: %{user: user}} = conn, %{"id" => id}) do
282 with context when is_binary(context) <- Utils.conversation_id_to_context(id),
284 ActivityPub.fetch_activities_for_context(context, %{
285 "blocking_user" => user,
289 |> put_view(ActivityView)
290 |> render("index.json", %{activities: activities, for: user})
295 Updates metadata of uploaded media object.
296 Derived from [Twitter API endpoint](https://developer.twitter.com/en/docs/media/upload-media/api-reference/post-media-metadata-create).
298 def update_media(%{assigns: %{user: user}} = conn, %{"media_id" => id} = data) do
299 object = Repo.get(Object, id)
300 description = get_in(data, ["alt_text", "text"]) || data["name"] || data["description"]
302 {conn, status, response_body} =
305 {halt(conn), :not_found, ""}
307 !Object.authorize_mutation(object, user) ->
308 {halt(conn), :forbidden, "You can only update your own uploads."}
310 !is_binary(description) ->
311 {conn, :not_modified, ""}
314 new_data = Map.put(object.data, "name", description)
318 |> Object.change(%{data: new_data})
321 {conn, :no_content, ""}
325 |> put_status(status)
326 |> json(response_body)
329 def upload(%{assigns: %{user: user}} = conn, %{"media" => media}) do
330 response = TwitterAPI.upload(media, user)
333 |> put_resp_content_type("application/atom+xml")
334 |> send_resp(200, response)
337 def upload_json(%{assigns: %{user: user}} = conn, %{"media" => media}) do
338 response = TwitterAPI.upload(media, user, "json")
341 |> json_reply(200, response)
344 def get_by_id_or_ap_id(id) do
345 activity = Activity.get_by_id(id) || Activity.get_create_by_object_ap_id(id)
347 if activity.data["type"] == "Create" do
350 Activity.get_create_by_object_ap_id(activity.data["object"])
354 def favorite(%{assigns: %{user: user}} = conn, %{"id" => id}) do
355 with {:ok, activity} <- TwitterAPI.fav(user, id) do
357 |> put_view(ActivityView)
358 |> render("activity.json", %{activity: activity, for: user})
360 _ -> json_reply(conn, 400, Jason.encode!(%{}))
364 def unfavorite(%{assigns: %{user: user}} = conn, %{"id" => id}) do
365 with {:ok, activity} <- TwitterAPI.unfav(user, id) do
367 |> put_view(ActivityView)
368 |> render("activity.json", %{activity: activity, for: user})
370 _ -> json_reply(conn, 400, Jason.encode!(%{}))
374 def retweet(%{assigns: %{user: user}} = conn, %{"id" => id}) do
375 with {:ok, activity} <- TwitterAPI.repeat(user, id) do
377 |> put_view(ActivityView)
378 |> render("activity.json", %{activity: activity, for: user})
380 _ -> json_reply(conn, 400, Jason.encode!(%{}))
384 def unretweet(%{assigns: %{user: user}} = conn, %{"id" => id}) do
385 with {:ok, activity} <- TwitterAPI.unrepeat(user, id) do
387 |> put_view(ActivityView)
388 |> render("activity.json", %{activity: activity, for: user})
390 _ -> json_reply(conn, 400, Jason.encode!(%{}))
394 def pin(%{assigns: %{user: user}} = conn, %{"id" => id}) do
395 with {:ok, activity} <- TwitterAPI.pin(user, id) do
397 |> put_view(ActivityView)
398 |> render("activity.json", %{activity: activity, for: user})
400 {:error, message} -> bad_request_reply(conn, message)
405 def unpin(%{assigns: %{user: user}} = conn, %{"id" => id}) do
406 with {:ok, activity} <- TwitterAPI.unpin(user, id) do
408 |> put_view(ActivityView)
409 |> render("activity.json", %{activity: activity, for: user})
411 {:error, message} -> bad_request_reply(conn, message)
416 def register(conn, params) do
417 with {:ok, user} <- TwitterAPI.register_user(params) do
419 |> put_view(UserView)
420 |> render("show.json", %{user: user})
424 |> json_reply(400, Jason.encode!(errors))
428 def password_reset(conn, params) do
429 nickname_or_email = params["email"] || params["nickname"]
431 with {:ok, _} <- TwitterAPI.password_reset(nickname_or_email) do
432 json_response(conn, :no_content, "")
436 def confirm_email(conn, %{"user_id" => uid, "token" => token}) do
437 with %User{} = user <- User.get_cached_by_id(uid),
439 true <- user.info.confirmation_pending,
440 true <- user.info.confirmation_token == token,
441 info_change <- User.Info.confirmation_changeset(user.info, need_confirmation: false),
442 changeset <- Changeset.change(user) |> Changeset.put_embed(:info, info_change),
443 {:ok, _} <- User.update_and_set_cache(changeset) do
449 def resend_confirmation_email(conn, params) do
450 nickname_or_email = params["email"] || params["nickname"]
452 with %User{} = user <- User.get_by_nickname_or_email(nickname_or_email),
453 {:ok, _} <- User.try_send_confirmation_email(user) do
455 |> json_response(:no_content, "")
459 def update_avatar(%{assigns: %{user: user}} = conn, %{"img" => ""}) do
460 change = Changeset.change(user, %{avatar: nil})
461 {:ok, user} = User.update_and_set_cache(change)
462 CommonAPI.update(user)
465 |> put_view(UserView)
466 |> render("show.json", %{user: user, for: user})
469 def update_avatar(%{assigns: %{user: user}} = conn, params) do
470 {:ok, object} = ActivityPub.upload(params, type: :avatar)
471 change = Changeset.change(user, %{avatar: object.data})
472 {:ok, user} = User.update_and_set_cache(change)
473 CommonAPI.update(user)
476 |> put_view(UserView)
477 |> render("show.json", %{user: user, for: user})
480 def update_banner(%{assigns: %{user: user}} = conn, %{"banner" => ""}) do
481 with new_info <- %{"banner" => %{}},
482 info_cng <- User.Info.profile_update(user.info, new_info),
483 changeset <- Ecto.Changeset.change(user) |> Ecto.Changeset.put_embed(:info, info_cng),
484 {:ok, user} <- User.update_and_set_cache(changeset) do
485 CommonAPI.update(user)
486 response = %{url: nil} |> Jason.encode!()
489 |> json_reply(200, response)
493 def update_banner(%{assigns: %{user: user}} = conn, params) do
494 with {:ok, object} <- ActivityPub.upload(%{"img" => params["banner"]}, type: :banner),
495 new_info <- %{"banner" => object.data},
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 %{"url" => [%{"href" => href} | _]} = object.data
501 response = %{url: href} |> Jason.encode!()
504 |> json_reply(200, response)
508 def update_background(%{assigns: %{user: user}} = conn, %{"img" => ""}) do
509 with new_info <- %{"background" => %{}},
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 response = %{url: nil} |> Jason.encode!()
516 |> json_reply(200, response)
520 def update_background(%{assigns: %{user: user}} = conn, params) do
521 with {:ok, object} <- ActivityPub.upload(params, type: :background),
522 new_info <- %{"background" => object.data},
523 info_cng <- User.Info.profile_update(user.info, new_info),
524 changeset <- Ecto.Changeset.change(user) |> Ecto.Changeset.put_embed(:info, info_cng),
525 {:ok, _user} <- User.update_and_set_cache(changeset) do
526 %{"url" => [%{"href" => href} | _]} = object.data
527 response = %{url: href} |> Jason.encode!()
530 |> json_reply(200, response)
534 def external_profile(%{assigns: %{user: current_user}} = conn, %{"profileurl" => uri}) do
535 with {:ok, user_map} <- TwitterAPI.get_external_profile(current_user, uri),
536 response <- Jason.encode!(user_map) do
538 |> json_reply(200, response)
543 |> json(%{error: "Can't find user"})
547 def followers(%{assigns: %{user: for_user}} = conn, params) do
548 {:ok, page} = Ecto.Type.cast(:integer, params["page"] || 1)
550 with {:ok, user} <- TwitterAPI.get_user(for_user, params),
551 {:ok, followers} <- User.get_followers(user, page) do
554 for_user && user.id == for_user.id -> followers
555 user.info.hide_followers -> []
560 |> put_view(UserView)
561 |> render("index.json", %{users: followers, for: conn.assigns[:user]})
563 _e -> bad_request_reply(conn, "Can't get followers")
567 def friends(%{assigns: %{user: for_user}} = conn, params) do
568 {:ok, page} = Ecto.Type.cast(:integer, params["page"] || 1)
569 {:ok, export} = Ecto.Type.cast(:boolean, params["all"] || false)
571 page = if export, do: nil, else: page
573 with {:ok, user} <- TwitterAPI.get_user(conn.assigns[:user], params),
574 {:ok, friends} <- User.get_friends(user, page) do
577 for_user && user.id == for_user.id -> friends
578 user.info.hide_follows -> []
583 |> put_view(UserView)
584 |> render("index.json", %{users: friends, for: conn.assigns[:user]})
586 _e -> bad_request_reply(conn, "Can't get friends")
590 def oauth_tokens(%{assigns: %{user: user}} = conn, _params) do
591 with oauth_tokens <- Token.get_user_tokens(user) do
593 |> put_view(TokenView)
594 |> render("index.json", %{tokens: oauth_tokens})
598 def revoke_token(%{assigns: %{user: user}} = conn, %{"id" => id} = _params) do
599 Token.delete_user_token(user, id)
601 json_reply(conn, 201, "")
604 def blocks(%{assigns: %{user: user}} = conn, _params) do
605 with blocked_users <- User.blocked_users(user) do
607 |> put_view(UserView)
608 |> render("index.json", %{users: blocked_users, for: user})
612 def friend_requests(conn, params) do
613 with {:ok, user} <- TwitterAPI.get_user(conn.assigns[:user], params),
614 {:ok, friend_requests} <- User.get_follow_requests(user) do
616 |> put_view(UserView)
617 |> render("index.json", %{users: friend_requests, for: conn.assigns[:user]})
619 _e -> bad_request_reply(conn, "Can't get friend requests")
623 def approve_friend_request(conn, %{"user_id" => uid} = _params) do
624 with followed <- conn.assigns[:user],
625 %User{} = follower <- User.get_cached_by_id(uid),
626 {:ok, follower} <- CommonAPI.accept_follow_request(follower, followed) do
628 |> put_view(UserView)
629 |> render("show.json", %{user: follower, for: followed})
631 e -> bad_request_reply(conn, "Can't approve user: #{inspect(e)}")
635 def deny_friend_request(conn, %{"user_id" => uid} = _params) do
636 with followed <- conn.assigns[:user],
637 %User{} = follower <- User.get_cached_by_id(uid),
638 {:ok, follower} <- CommonAPI.reject_follow_request(follower, followed) do
640 |> put_view(UserView)
641 |> render("show.json", %{user: follower, for: followed})
643 e -> bad_request_reply(conn, "Can't deny user: #{inspect(e)}")
647 def friends_ids(%{assigns: %{user: user}} = conn, _params) do
648 with {:ok, friends} <- User.get_friends(user) do
651 |> Enum.map(fn x -> x.id end)
656 _e -> bad_request_reply(conn, "Can't get friends")
660 def empty_array(conn, _params) do
661 json(conn, Jason.encode!([]))
664 def raw_empty_array(conn, _params) do
668 defp build_info_cng(user, params) do
670 ["no_rich_text", "locked", "hide_followers", "hide_follows", "hide_favorites", "show_role"]
671 |> Enum.reduce(%{}, fn key, res ->
672 if value = params[key] do
673 Map.put(res, key, value == "true")
680 if value = params["default_scope"] do
681 Map.put(info_params, "default_scope", value)
686 User.Info.profile_update(user.info, info_params)
689 defp parse_profile_bio(user, params) do
690 if bio = params["description"] do
691 emojis_text = (params["description"] || "") <> " " <> (params["name"] || "")
694 ((user.info.emoji || []) ++ Formatter.get_emoji_map(emojis_text))
705 |> Map.put("bio", User.parse_bio(bio, user))
706 |> Map.put("info", user_info)
712 def update_profile(%{assigns: %{user: user}} = conn, params) do
713 params = parse_profile_bio(user, params)
714 info_cng = build_info_cng(user, params)
716 with changeset <- User.update_changeset(user, params),
717 changeset <- Ecto.Changeset.put_embed(changeset, :info, info_cng),
718 {:ok, user} <- User.update_and_set_cache(changeset) do
719 CommonAPI.update(user)
722 |> put_view(UserView)
723 |> render("user.json", %{user: user, for: user})
726 Logger.debug("Can't update user: #{inspect(error)}")
727 bad_request_reply(conn, "Can't update user")
731 def search(%{assigns: %{user: user}} = conn, %{"q" => _query} = params) do
732 activities = TwitterAPI.search(user, params)
735 |> put_view(ActivityView)
736 |> render("index.json", %{activities: activities, for: user})
739 def search_user(%{assigns: %{user: user}} = conn, %{"query" => query}) do
740 users = User.search(query, resolve: true, for_user: user)
743 |> put_view(UserView)
744 |> render("index.json", %{users: users, for: user})
747 defp bad_request_reply(conn, error_message) do
748 json = error_json(conn, error_message)
749 json_reply(conn, 400, json)
752 defp json_reply(conn, status, json) do
754 |> put_resp_content_type("application/json")
755 |> send_resp(status, json)
758 defp forbidden_json_reply(conn, error_message) do
759 json = error_json(conn, error_message)
760 json_reply(conn, 403, json)
763 def only_if_public_instance(%{assigns: %{user: %User{}}} = conn, _), do: conn
765 def only_if_public_instance(conn, _) do
766 if Keyword.get(Application.get_env(:pleroma, :instance), :public) do
770 |> forbidden_json_reply("Invalid credentials.")
775 defp error_json(conn, error_message) do
776 %{"error" => error_message, "request" => conn.request_path} |> Jason.encode!()
779 def errors(conn, {:param_cast, _}) do
782 |> json("Invalid parameters")
785 def errors(conn, _) do
788 |> json("Something went wrong")