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
196 if Map.has_key?(params, "with_muted") do
197 Map.put(params, :with_muted, params["with_muted"] in [true, "True", "true", "1"])
202 notifications = Notification.for_user(user, params)
205 |> put_view(NotificationView)
206 |> render("notification.json", %{notifications: notifications, for: user})
209 def notifications_read(%{assigns: %{user: user}} = conn, %{"latest_id" => latest_id} = params) do
210 Notification.set_read_up_to(user, latest_id)
212 notifications = Notification.for_user(user, params)
215 |> put_view(NotificationView)
216 |> render("notification.json", %{notifications: notifications, for: user})
219 def notifications_read(%{assigns: %{user: _user}} = conn, _) do
220 bad_request_reply(conn, "You need to specify latest_id")
223 def follow(%{assigns: %{user: user}} = conn, params) do
224 case TwitterAPI.follow(user, params) do
225 {:ok, user, followed, _activity} ->
227 |> put_view(UserView)
228 |> render("show.json", %{user: followed, for: user})
231 forbidden_json_reply(conn, msg)
235 def block(%{assigns: %{user: user}} = conn, params) do
236 case TwitterAPI.block(user, params) do
237 {:ok, user, blocked} ->
239 |> put_view(UserView)
240 |> render("show.json", %{user: blocked, for: user})
243 forbidden_json_reply(conn, msg)
247 def unblock(%{assigns: %{user: user}} = conn, params) do
248 case TwitterAPI.unblock(user, params) do
249 {:ok, user, blocked} ->
251 |> put_view(UserView)
252 |> render("show.json", %{user: blocked, for: user})
255 forbidden_json_reply(conn, msg)
259 def delete_post(%{assigns: %{user: user}} = conn, %{"id" => id}) do
260 with {:ok, activity} <- TwitterAPI.delete(user, id) do
262 |> put_view(ActivityView)
263 |> render("activity.json", %{activity: activity, for: user})
267 def unfollow(%{assigns: %{user: user}} = conn, params) do
268 case TwitterAPI.unfollow(user, params) do
269 {:ok, user, unfollowed} ->
271 |> put_view(UserView)
272 |> render("show.json", %{user: unfollowed, for: user})
275 forbidden_json_reply(conn, msg)
279 def fetch_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
280 with %Activity{} = activity <- Activity.get_by_id(id),
281 true <- Visibility.visible_for_user?(activity, user) do
283 |> put_view(ActivityView)
284 |> render("activity.json", %{activity: activity, for: user})
288 def fetch_conversation(%{assigns: %{user: user}} = conn, %{"id" => id}) do
289 with context when is_binary(context) <- Utils.conversation_id_to_context(id),
291 ActivityPub.fetch_activities_for_context(context, %{
292 "blocking_user" => user,
296 |> put_view(ActivityView)
297 |> render("index.json", %{activities: activities, for: user})
302 Updates metadata of uploaded media object.
303 Derived from [Twitter API endpoint](https://developer.twitter.com/en/docs/media/upload-media/api-reference/post-media-metadata-create).
305 def update_media(%{assigns: %{user: user}} = conn, %{"media_id" => id} = data) do
306 object = Repo.get(Object, id)
307 description = get_in(data, ["alt_text", "text"]) || data["name"] || data["description"]
309 {conn, status, response_body} =
312 {halt(conn), :not_found, ""}
314 !Object.authorize_mutation(object, user) ->
315 {halt(conn), :forbidden, "You can only update your own uploads."}
317 !is_binary(description) ->
318 {conn, :not_modified, ""}
321 new_data = Map.put(object.data, "name", description)
325 |> Object.change(%{data: new_data})
328 {conn, :no_content, ""}
332 |> put_status(status)
333 |> json(response_body)
336 def upload(%{assigns: %{user: user}} = conn, %{"media" => media}) do
337 response = TwitterAPI.upload(media, user)
340 |> put_resp_content_type("application/atom+xml")
341 |> send_resp(200, response)
344 def upload_json(%{assigns: %{user: user}} = conn, %{"media" => media}) do
345 response = TwitterAPI.upload(media, user, "json")
348 |> json_reply(200, response)
351 def get_by_id_or_ap_id(id) do
352 activity = Activity.get_by_id(id) || Activity.get_create_by_object_ap_id(id)
354 if activity.data["type"] == "Create" do
357 Activity.get_create_by_object_ap_id(activity.data["object"])
361 def favorite(%{assigns: %{user: user}} = conn, %{"id" => id}) do
362 with {:ok, activity} <- TwitterAPI.fav(user, id) do
364 |> put_view(ActivityView)
365 |> render("activity.json", %{activity: activity, for: user})
367 _ -> json_reply(conn, 400, Jason.encode!(%{}))
371 def unfavorite(%{assigns: %{user: user}} = conn, %{"id" => id}) do
372 with {:ok, activity} <- TwitterAPI.unfav(user, id) do
374 |> put_view(ActivityView)
375 |> render("activity.json", %{activity: activity, for: user})
377 _ -> json_reply(conn, 400, Jason.encode!(%{}))
381 def retweet(%{assigns: %{user: user}} = conn, %{"id" => id}) do
382 with {:ok, activity} <- TwitterAPI.repeat(user, id) do
384 |> put_view(ActivityView)
385 |> render("activity.json", %{activity: activity, for: user})
387 _ -> json_reply(conn, 400, Jason.encode!(%{}))
391 def unretweet(%{assigns: %{user: user}} = conn, %{"id" => id}) do
392 with {:ok, activity} <- TwitterAPI.unrepeat(user, id) do
394 |> put_view(ActivityView)
395 |> render("activity.json", %{activity: activity, for: user})
397 _ -> json_reply(conn, 400, Jason.encode!(%{}))
401 def pin(%{assigns: %{user: user}} = conn, %{"id" => id}) do
402 with {:ok, activity} <- TwitterAPI.pin(user, id) do
404 |> put_view(ActivityView)
405 |> render("activity.json", %{activity: activity, for: user})
407 {:error, message} -> bad_request_reply(conn, message)
412 def unpin(%{assigns: %{user: user}} = conn, %{"id" => id}) do
413 with {:ok, activity} <- TwitterAPI.unpin(user, id) do
415 |> put_view(ActivityView)
416 |> render("activity.json", %{activity: activity, for: user})
418 {:error, message} -> bad_request_reply(conn, message)
423 def register(conn, params) do
424 with {:ok, user} <- TwitterAPI.register_user(params) do
426 |> put_view(UserView)
427 |> render("show.json", %{user: user})
431 |> json_reply(400, Jason.encode!(errors))
435 def password_reset(conn, params) do
436 nickname_or_email = params["email"] || params["nickname"]
438 with {:ok, _} <- TwitterAPI.password_reset(nickname_or_email) do
439 json_response(conn, :no_content, "")
443 def confirm_email(conn, %{"user_id" => uid, "token" => token}) do
444 with %User{} = user <- User.get_cached_by_id(uid),
446 true <- user.info.confirmation_pending,
447 true <- user.info.confirmation_token == token,
448 info_change <- User.Info.confirmation_changeset(user.info, need_confirmation: false),
449 changeset <- Changeset.change(user) |> Changeset.put_embed(:info, info_change),
450 {:ok, _} <- User.update_and_set_cache(changeset) do
456 def resend_confirmation_email(conn, params) do
457 nickname_or_email = params["email"] || params["nickname"]
459 with %User{} = user <- User.get_by_nickname_or_email(nickname_or_email),
460 {:ok, _} <- User.try_send_confirmation_email(user) do
462 |> json_response(:no_content, "")
466 def update_avatar(%{assigns: %{user: user}} = conn, %{"img" => ""}) do
467 change = Changeset.change(user, %{avatar: nil})
468 {:ok, user} = User.update_and_set_cache(change)
469 CommonAPI.update(user)
472 |> put_view(UserView)
473 |> render("show.json", %{user: user, for: user})
476 def update_avatar(%{assigns: %{user: user}} = conn, params) do
477 {:ok, object} = ActivityPub.upload(params, type: :avatar)
478 change = Changeset.change(user, %{avatar: object.data})
479 {:ok, user} = User.update_and_set_cache(change)
480 CommonAPI.update(user)
483 |> put_view(UserView)
484 |> render("show.json", %{user: user, for: user})
487 def update_banner(%{assigns: %{user: user}} = conn, %{"banner" => ""}) do
488 with new_info <- %{"banner" => %{}},
489 info_cng <- User.Info.profile_update(user.info, new_info),
490 changeset <- Ecto.Changeset.change(user) |> Ecto.Changeset.put_embed(:info, info_cng),
491 {:ok, user} <- User.update_and_set_cache(changeset) do
492 CommonAPI.update(user)
493 response = %{url: nil} |> Jason.encode!()
496 |> json_reply(200, response)
500 def update_banner(%{assigns: %{user: user}} = conn, params) do
501 with {:ok, object} <- ActivityPub.upload(%{"img" => params["banner"]}, type: :banner),
502 new_info <- %{"banner" => object.data},
503 info_cng <- User.Info.profile_update(user.info, new_info),
504 changeset <- Ecto.Changeset.change(user) |> Ecto.Changeset.put_embed(:info, info_cng),
505 {:ok, user} <- User.update_and_set_cache(changeset) do
506 CommonAPI.update(user)
507 %{"url" => [%{"href" => href} | _]} = object.data
508 response = %{url: href} |> Jason.encode!()
511 |> json_reply(200, response)
515 def update_background(%{assigns: %{user: user}} = conn, %{"img" => ""}) do
516 with new_info <- %{"background" => %{}},
517 info_cng <- User.Info.profile_update(user.info, new_info),
518 changeset <- Ecto.Changeset.change(user) |> Ecto.Changeset.put_embed(:info, info_cng),
519 {:ok, _user} <- User.update_and_set_cache(changeset) do
520 response = %{url: nil} |> Jason.encode!()
523 |> json_reply(200, response)
527 def update_background(%{assigns: %{user: user}} = conn, params) do
528 with {:ok, object} <- ActivityPub.upload(params, type: :background),
529 new_info <- %{"background" => object.data},
530 info_cng <- User.Info.profile_update(user.info, new_info),
531 changeset <- Ecto.Changeset.change(user) |> Ecto.Changeset.put_embed(:info, info_cng),
532 {:ok, _user} <- User.update_and_set_cache(changeset) do
533 %{"url" => [%{"href" => href} | _]} = object.data
534 response = %{url: href} |> Jason.encode!()
537 |> json_reply(200, response)
541 def external_profile(%{assigns: %{user: current_user}} = conn, %{"profileurl" => uri}) do
542 with {:ok, user_map} <- TwitterAPI.get_external_profile(current_user, uri),
543 response <- Jason.encode!(user_map) do
545 |> json_reply(200, response)
550 |> json(%{error: "Can't find user"})
554 def followers(%{assigns: %{user: for_user}} = conn, params) do
555 {:ok, page} = Ecto.Type.cast(:integer, params["page"] || 1)
557 with {:ok, user} <- TwitterAPI.get_user(for_user, params),
558 {:ok, followers} <- User.get_followers(user, page) do
561 for_user && user.id == for_user.id -> followers
562 user.info.hide_followers -> []
567 |> put_view(UserView)
568 |> render("index.json", %{users: followers, for: conn.assigns[:user]})
570 _e -> bad_request_reply(conn, "Can't get followers")
574 def friends(%{assigns: %{user: for_user}} = conn, params) do
575 {:ok, page} = Ecto.Type.cast(:integer, params["page"] || 1)
576 {:ok, export} = Ecto.Type.cast(:boolean, params["all"] || false)
578 page = if export, do: nil, else: page
580 with {:ok, user} <- TwitterAPI.get_user(conn.assigns[:user], params),
581 {:ok, friends} <- User.get_friends(user, page) do
584 for_user && user.id == for_user.id -> friends
585 user.info.hide_follows -> []
590 |> put_view(UserView)
591 |> render("index.json", %{users: friends, for: conn.assigns[:user]})
593 _e -> bad_request_reply(conn, "Can't get friends")
597 def oauth_tokens(%{assigns: %{user: user}} = conn, _params) do
598 with oauth_tokens <- Token.get_user_tokens(user) do
600 |> put_view(TokenView)
601 |> render("index.json", %{tokens: oauth_tokens})
605 def revoke_token(%{assigns: %{user: user}} = conn, %{"id" => id} = _params) do
606 Token.delete_user_token(user, id)
608 json_reply(conn, 201, "")
611 def blocks(%{assigns: %{user: user}} = conn, _params) do
612 with blocked_users <- User.blocked_users(user) do
614 |> put_view(UserView)
615 |> render("index.json", %{users: blocked_users, for: user})
619 def friend_requests(conn, params) do
620 with {:ok, user} <- TwitterAPI.get_user(conn.assigns[:user], params),
621 {:ok, friend_requests} <- User.get_follow_requests(user) do
623 |> put_view(UserView)
624 |> render("index.json", %{users: friend_requests, for: conn.assigns[:user]})
626 _e -> bad_request_reply(conn, "Can't get friend requests")
630 def approve_friend_request(conn, %{"user_id" => uid} = _params) do
631 with followed <- conn.assigns[:user],
632 %User{} = follower <- User.get_cached_by_id(uid),
633 {:ok, follower} <- CommonAPI.accept_follow_request(follower, followed) do
635 |> put_view(UserView)
636 |> render("show.json", %{user: follower, for: followed})
638 e -> bad_request_reply(conn, "Can't approve user: #{inspect(e)}")
642 def deny_friend_request(conn, %{"user_id" => uid} = _params) do
643 with followed <- conn.assigns[:user],
644 %User{} = follower <- User.get_cached_by_id(uid),
645 {:ok, follower} <- CommonAPI.reject_follow_request(follower, followed) do
647 |> put_view(UserView)
648 |> render("show.json", %{user: follower, for: followed})
650 e -> bad_request_reply(conn, "Can't deny user: #{inspect(e)}")
654 def friends_ids(%{assigns: %{user: user}} = conn, _params) do
655 with {:ok, friends} <- User.get_friends(user) do
658 |> Enum.map(fn x -> x.id end)
663 _e -> bad_request_reply(conn, "Can't get friends")
667 def empty_array(conn, _params) do
668 json(conn, Jason.encode!([]))
671 def raw_empty_array(conn, _params) do
675 defp build_info_cng(user, params) do
684 "skip_thread_containment"
686 |> Enum.reduce(%{}, fn key, res ->
687 if value = params[key] do
688 Map.put(res, key, value == "true")
695 if value = params["default_scope"] do
696 Map.put(info_params, "default_scope", value)
701 User.Info.profile_update(user.info, info_params)
704 defp parse_profile_bio(user, params) do
705 if bio = params["description"] do
706 emojis_text = (params["description"] || "") <> " " <> (params["name"] || "")
709 ((user.info.emoji || []) ++ Formatter.get_emoji_map(emojis_text))
720 |> Map.put("bio", User.parse_bio(bio, user))
721 |> Map.put("info", user_info)
727 def update_profile(%{assigns: %{user: user}} = conn, params) do
728 params = parse_profile_bio(user, params)
729 info_cng = build_info_cng(user, params)
731 with changeset <- User.update_changeset(user, params),
732 changeset <- Ecto.Changeset.put_embed(changeset, :info, info_cng),
733 {:ok, user} <- User.update_and_set_cache(changeset) do
734 CommonAPI.update(user)
737 |> put_view(UserView)
738 |> render("user.json", %{user: user, for: user})
741 Logger.debug("Can't update user: #{inspect(error)}")
742 bad_request_reply(conn, "Can't update user")
746 def search(%{assigns: %{user: user}} = conn, %{"q" => _query} = params) do
747 activities = TwitterAPI.search(user, params)
750 |> put_view(ActivityView)
751 |> render("index.json", %{activities: activities, for: user})
754 def search_user(%{assigns: %{user: user}} = conn, %{"query" => query}) do
755 users = User.search(query, resolve: true, for_user: user)
758 |> put_view(UserView)
759 |> render("index.json", %{users: users, for: user})
762 defp bad_request_reply(conn, error_message) do
763 json = error_json(conn, error_message)
764 json_reply(conn, 400, json)
767 defp json_reply(conn, status, json) do
769 |> put_resp_content_type("application/json")
770 |> send_resp(status, json)
773 defp forbidden_json_reply(conn, error_message) do
774 json = error_json(conn, error_message)
775 json_reply(conn, 403, json)
778 def only_if_public_instance(%{assigns: %{user: %User{}}} = conn, _), do: conn
780 def only_if_public_instance(conn, _) do
781 if Pleroma.Config.get([:instance, :public]) do
785 |> forbidden_json_reply("Invalid credentials.")
790 defp error_json(conn, error_message) do
791 %{"error" => error_message, "request" => conn.request_path} |> Jason.encode!()
794 def errors(conn, {:param_cast, _}) do
797 |> json("Invalid parameters")
800 def errors(conn, _) do
803 |> json("Something went wrong")