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)
105 ActivityPub.fetch_activities([user.ap_id | user.following], params)
106 |> ActivityPub.contain_timeline(user)
109 |> put_view(ActivityView)
110 |> render("index.json", %{activities: activities, for: user})
113 def show_user(conn, params) do
114 for_user = conn.assigns.user
116 with {:ok, shown} <- TwitterAPI.get_user(params),
118 User.auth_active?(shown) ||
119 (for_user && (for_user.id == shown.id || User.superuser?(for_user))) do
122 %{user: shown, for: for_user}
128 |> put_view(UserView)
129 |> render("show.json", params)
132 bad_request_reply(conn, msg)
137 |> json(%{error: "Unconfirmed user"})
141 def user_timeline(%{assigns: %{user: user}} = conn, params) do
142 case TwitterAPI.get_user(user, params) do
143 {:ok, target_user} ->
144 # Twitter and ActivityPub use a different name and sense for this parameter.
145 {include_rts, params} = Map.pop(params, "include_rts")
149 x when x == "false" or x == "0" -> Map.put(params, "exclude_reblogs", "true")
153 activities = ActivityPub.fetch_user_activities(target_user, user, params)
156 |> put_view(ActivityView)
157 |> render("index.json", %{activities: activities, for: user})
160 bad_request_reply(conn, msg)
164 def mentions_timeline(%{assigns: %{user: user}} = conn, params) do
167 |> Map.put("type", ["Create", "Announce", "Follow", "Like"])
168 |> Map.put("blocking_user", user)
169 |> Map.put(:visibility, ~w[unlisted public private])
171 activities = ActivityPub.fetch_activities([user.ap_id], params)
174 |> put_view(ActivityView)
175 |> render("index.json", %{activities: activities, for: user})
178 def dm_timeline(%{assigns: %{user: user}} = conn, params) do
181 |> Map.put("type", "Create")
182 |> Map.put("blocking_user", user)
183 |> Map.put("user", user)
184 |> Map.put(:visibility, "direct")
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
196 notifications = Notification.for_user(user, params)
199 |> put_view(NotificationView)
200 |> render("notification.json", %{notifications: notifications, for: user})
203 def notifications_read(%{assigns: %{user: user}} = conn, %{"latest_id" => latest_id} = params) do
204 Notification.set_read_up_to(user, latest_id)
206 notifications = Notification.for_user(user, params)
209 |> put_view(NotificationView)
210 |> render("notification.json", %{notifications: notifications, for: user})
213 def notifications_read(%{assigns: %{user: _user}} = conn, _) do
214 bad_request_reply(conn, "You need to specify latest_id")
217 def follow(%{assigns: %{user: user}} = conn, params) do
218 case TwitterAPI.follow(user, params) do
219 {:ok, user, followed, _activity} ->
221 |> put_view(UserView)
222 |> render("show.json", %{user: followed, for: user})
225 forbidden_json_reply(conn, msg)
229 def block(%{assigns: %{user: user}} = conn, params) do
230 case TwitterAPI.block(user, params) do
231 {:ok, user, blocked} ->
233 |> put_view(UserView)
234 |> render("show.json", %{user: blocked, for: user})
237 forbidden_json_reply(conn, msg)
241 def unblock(%{assigns: %{user: user}} = conn, params) do
242 case TwitterAPI.unblock(user, params) do
243 {:ok, user, blocked} ->
245 |> put_view(UserView)
246 |> render("show.json", %{user: blocked, for: user})
249 forbidden_json_reply(conn, msg)
253 def delete_post(%{assigns: %{user: user}} = conn, %{"id" => id}) do
254 with {:ok, activity} <- TwitterAPI.delete(user, id) do
256 |> put_view(ActivityView)
257 |> render("activity.json", %{activity: activity, for: user})
261 def unfollow(%{assigns: %{user: user}} = conn, params) do
262 case TwitterAPI.unfollow(user, params) do
263 {:ok, user, unfollowed} ->
265 |> put_view(UserView)
266 |> render("show.json", %{user: unfollowed, for: user})
269 forbidden_json_reply(conn, msg)
273 def fetch_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
274 with %Activity{} = activity <- Activity.get_by_id(id),
275 true <- Visibility.visible_for_user?(activity, user) do
277 |> put_view(ActivityView)
278 |> render("activity.json", %{activity: activity, for: user})
282 def fetch_conversation(%{assigns: %{user: user}} = conn, %{"id" => id}) do
283 with context when is_binary(context) <- Utils.conversation_id_to_context(id),
285 ActivityPub.fetch_activities_for_context(context, %{
286 "blocking_user" => user,
290 |> put_view(ActivityView)
291 |> render("index.json", %{activities: activities, for: user})
296 Updates metadata of uploaded media object.
297 Derived from [Twitter API endpoint](https://developer.twitter.com/en/docs/media/upload-media/api-reference/post-media-metadata-create).
299 def update_media(%{assigns: %{user: user}} = conn, %{"media_id" => id} = data) do
300 object = Repo.get(Object, id)
301 description = get_in(data, ["alt_text", "text"]) || data["name"] || data["description"]
303 {conn, status, response_body} =
306 {halt(conn), :not_found, ""}
308 !Object.authorize_mutation(object, user) ->
309 {halt(conn), :forbidden, "You can only update your own uploads."}
311 !is_binary(description) ->
312 {conn, :not_modified, ""}
315 new_data = Map.put(object.data, "name", description)
319 |> Object.change(%{data: new_data})
322 {conn, :no_content, ""}
326 |> put_status(status)
327 |> json(response_body)
330 def upload(%{assigns: %{user: user}} = conn, %{"media" => media}) do
331 response = TwitterAPI.upload(media, user)
334 |> put_resp_content_type("application/atom+xml")
335 |> send_resp(200, response)
338 def upload_json(%{assigns: %{user: user}} = conn, %{"media" => media}) do
339 response = TwitterAPI.upload(media, user, "json")
342 |> json_reply(200, response)
345 def get_by_id_or_ap_id(id) do
346 activity = Activity.get_by_id(id) || Activity.get_create_by_object_ap_id(id)
348 if activity.data["type"] == "Create" do
351 Activity.get_create_by_object_ap_id(activity.data["object"])
355 def favorite(%{assigns: %{user: user}} = conn, %{"id" => id}) do
356 with {:ok, activity} <- TwitterAPI.fav(user, id) do
358 |> put_view(ActivityView)
359 |> render("activity.json", %{activity: activity, for: user})
361 _ -> json_reply(conn, 400, Jason.encode!(%{}))
365 def unfavorite(%{assigns: %{user: user}} = conn, %{"id" => id}) do
366 with {:ok, activity} <- TwitterAPI.unfav(user, id) do
368 |> put_view(ActivityView)
369 |> render("activity.json", %{activity: activity, for: user})
371 _ -> json_reply(conn, 400, Jason.encode!(%{}))
375 def retweet(%{assigns: %{user: user}} = conn, %{"id" => id}) do
376 with {:ok, activity} <- TwitterAPI.repeat(user, id) do
378 |> put_view(ActivityView)
379 |> render("activity.json", %{activity: activity, for: user})
381 _ -> json_reply(conn, 400, Jason.encode!(%{}))
385 def unretweet(%{assigns: %{user: user}} = conn, %{"id" => id}) do
386 with {:ok, activity} <- TwitterAPI.unrepeat(user, id) do
388 |> put_view(ActivityView)
389 |> render("activity.json", %{activity: activity, for: user})
391 _ -> json_reply(conn, 400, Jason.encode!(%{}))
395 def pin(%{assigns: %{user: user}} = conn, %{"id" => id}) do
396 with {:ok, activity} <- TwitterAPI.pin(user, id) do
398 |> put_view(ActivityView)
399 |> render("activity.json", %{activity: activity, for: user})
401 {:error, message} -> bad_request_reply(conn, message)
406 def unpin(%{assigns: %{user: user}} = conn, %{"id" => id}) do
407 with {:ok, activity} <- TwitterAPI.unpin(user, id) do
409 |> put_view(ActivityView)
410 |> render("activity.json", %{activity: activity, for: user})
412 {:error, message} -> bad_request_reply(conn, message)
417 def register(conn, params) do
418 with {:ok, user} <- TwitterAPI.register_user(params) do
420 |> put_view(UserView)
421 |> render("show.json", %{user: user})
425 |> json_reply(400, Jason.encode!(errors))
429 def password_reset(conn, params) do
430 nickname_or_email = params["email"] || params["nickname"]
432 with {:ok, _} <- TwitterAPI.password_reset(nickname_or_email) do
433 json_response(conn, :no_content, "")
437 def confirm_email(conn, %{"user_id" => uid, "token" => token}) do
438 with %User{} = user <- User.get_cached_by_id(uid),
440 true <- user.info.confirmation_pending,
441 true <- user.info.confirmation_token == token,
442 info_change <- User.Info.confirmation_changeset(user.info, :confirmed),
443 changeset <- Changeset.change(user) |> Changeset.put_embed(:info, info_change),
444 {:ok, _} <- User.update_and_set_cache(changeset) do
450 def resend_confirmation_email(conn, params) do
451 nickname_or_email = params["email"] || params["nickname"]
453 with %User{} = user <- User.get_by_nickname_or_email(nickname_or_email),
454 {:ok, _} <- User.try_send_confirmation_email(user) do
456 |> json_response(:no_content, "")
460 def update_avatar(%{assigns: %{user: user}} = conn, params) do
461 {:ok, object} = ActivityPub.upload(params, type: :avatar)
462 change = Changeset.change(user, %{avatar: object.data})
463 {:ok, user} = User.update_and_set_cache(change)
464 CommonAPI.update(user)
467 |> put_view(UserView)
468 |> render("show.json", %{user: user, for: user})
471 def update_banner(%{assigns: %{user: user}} = conn, params) do
472 with {:ok, object} <- ActivityPub.upload(%{"img" => params["banner"]}, type: :banner),
473 new_info <- %{"banner" => object.data},
474 info_cng <- User.Info.profile_update(user.info, new_info),
475 changeset <- Ecto.Changeset.change(user) |> Ecto.Changeset.put_embed(:info, info_cng),
476 {:ok, user} <- User.update_and_set_cache(changeset) do
477 CommonAPI.update(user)
478 %{"url" => [%{"href" => href} | _]} = object.data
479 response = %{url: href} |> Jason.encode!()
482 |> json_reply(200, response)
486 def update_background(%{assigns: %{user: user}} = conn, params) do
487 with {:ok, object} <- ActivityPub.upload(params, type: :background),
488 new_info <- %{"background" => object.data},
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 %{"url" => [%{"href" => href} | _]} = object.data
493 response = %{url: href} |> Jason.encode!()
496 |> json_reply(200, response)
500 def external_profile(%{assigns: %{user: current_user}} = conn, %{"profileurl" => uri}) do
501 with {:ok, user_map} <- TwitterAPI.get_external_profile(current_user, uri),
502 response <- Jason.encode!(user_map) do
504 |> json_reply(200, response)
509 |> json(%{error: "Can't find user"})
513 def followers(%{assigns: %{user: for_user}} = conn, params) do
514 {:ok, page} = Ecto.Type.cast(:integer, params["page"] || 1)
516 with {:ok, user} <- TwitterAPI.get_user(for_user, params),
517 {:ok, followers} <- User.get_followers(user, page) do
520 for_user && user.id == for_user.id -> followers
521 user.info.hide_followers -> []
526 |> put_view(UserView)
527 |> render("index.json", %{users: followers, for: conn.assigns[:user]})
529 _e -> bad_request_reply(conn, "Can't get followers")
533 def friends(%{assigns: %{user: for_user}} = conn, params) do
534 {:ok, page} = Ecto.Type.cast(:integer, params["page"] || 1)
535 {:ok, export} = Ecto.Type.cast(:boolean, params["all"] || false)
537 page = if export, do: nil, else: page
539 with {:ok, user} <- TwitterAPI.get_user(conn.assigns[:user], params),
540 {:ok, friends} <- User.get_friends(user, page) do
543 for_user && user.id == for_user.id -> friends
544 user.info.hide_follows -> []
549 |> put_view(UserView)
550 |> render("index.json", %{users: friends, for: conn.assigns[:user]})
552 _e -> bad_request_reply(conn, "Can't get friends")
556 def oauth_tokens(%{assigns: %{user: user}} = conn, _params) do
557 with oauth_tokens <- Token.get_user_tokens(user) do
559 |> put_view(TokenView)
560 |> render("index.json", %{tokens: oauth_tokens})
564 def revoke_token(%{assigns: %{user: user}} = conn, %{"id" => id} = _params) do
565 Token.delete_user_token(user, id)
567 json_reply(conn, 201, "")
570 def blocks(%{assigns: %{user: user}} = conn, _params) do
571 with blocked_users <- User.blocked_users(user) do
573 |> put_view(UserView)
574 |> render("index.json", %{users: blocked_users, for: user})
578 def friend_requests(conn, params) do
579 with {:ok, user} <- TwitterAPI.get_user(conn.assigns[:user], params),
580 {:ok, friend_requests} <- User.get_follow_requests(user) do
582 |> put_view(UserView)
583 |> render("index.json", %{users: friend_requests, for: conn.assigns[:user]})
585 _e -> bad_request_reply(conn, "Can't get friend requests")
589 def approve_friend_request(conn, %{"user_id" => uid} = _params) do
590 with followed <- conn.assigns[:user],
591 %User{} = follower <- User.get_cached_by_id(uid),
592 {:ok, follower} <- CommonAPI.accept_follow_request(follower, followed) do
594 |> put_view(UserView)
595 |> render("show.json", %{user: follower, for: followed})
597 e -> bad_request_reply(conn, "Can't approve user: #{inspect(e)}")
601 def deny_friend_request(conn, %{"user_id" => uid} = _params) do
602 with followed <- conn.assigns[:user],
603 %User{} = follower <- User.get_cached_by_id(uid),
604 {:ok, follower} <- CommonAPI.reject_follow_request(follower, followed) do
606 |> put_view(UserView)
607 |> render("show.json", %{user: follower, for: followed})
609 e -> bad_request_reply(conn, "Can't deny user: #{inspect(e)}")
613 def friends_ids(%{assigns: %{user: user}} = conn, _params) do
614 with {:ok, friends} <- User.get_friends(user) do
617 |> Enum.map(fn x -> x.id end)
622 _e -> bad_request_reply(conn, "Can't get friends")
626 def empty_array(conn, _params) do
627 json(conn, Jason.encode!([]))
630 def raw_empty_array(conn, _params) do
634 defp build_info_cng(user, params) do
636 ["no_rich_text", "locked", "hide_followers", "hide_follows", "hide_favorites", "show_role"]
637 |> Enum.reduce(%{}, fn key, res ->
638 if value = params[key] do
639 Map.put(res, key, value == "true")
646 if value = params["default_scope"] do
647 Map.put(info_params, "default_scope", value)
652 User.Info.profile_update(user.info, info_params)
655 defp parse_profile_bio(user, params) do
656 if bio = params["description"] do
657 emojis_text = (params["description"] || "") <> " " <> (params["name"] || "")
660 ((user.info.emoji || []) ++ Formatter.get_emoji_map(emojis_text))
671 |> Map.put("bio", User.parse_bio(bio, user))
672 |> Map.put("info", user_info)
678 def update_profile(%{assigns: %{user: user}} = conn, params) do
679 params = parse_profile_bio(user, params)
680 info_cng = build_info_cng(user, params)
682 with changeset <- User.update_changeset(user, params),
683 changeset <- Ecto.Changeset.put_embed(changeset, :info, info_cng),
684 {:ok, user} <- User.update_and_set_cache(changeset) do
685 CommonAPI.update(user)
688 |> put_view(UserView)
689 |> render("user.json", %{user: user, for: user})
692 Logger.debug("Can't update user: #{inspect(error)}")
693 bad_request_reply(conn, "Can't update user")
697 def search(%{assigns: %{user: user}} = conn, %{"q" => _query} = params) do
698 activities = TwitterAPI.search(user, params)
701 |> put_view(ActivityView)
702 |> render("index.json", %{activities: activities, for: user})
705 def search_user(%{assigns: %{user: user}} = conn, %{"query" => query}) do
706 users = User.search(query, resolve: true, for_user: user)
709 |> put_view(UserView)
710 |> render("index.json", %{users: users, for: user})
713 defp bad_request_reply(conn, error_message) do
714 json = error_json(conn, error_message)
715 json_reply(conn, 400, json)
718 defp json_reply(conn, status, json) do
720 |> put_resp_content_type("application/json")
721 |> send_resp(status, json)
724 defp forbidden_json_reply(conn, error_message) do
725 json = error_json(conn, error_message)
726 json_reply(conn, 403, json)
729 def only_if_public_instance(%{assigns: %{user: %User{}}} = conn, _), do: conn
731 def only_if_public_instance(conn, _) do
732 if Keyword.get(Application.get_env(:pleroma, :instance), :public) do
736 |> forbidden_json_reply("Invalid credentials.")
741 defp error_json(conn, error_message) do
742 %{"error" => error_message, "request" => conn.request_path} |> Jason.encode!()
745 def errors(conn, {:param_cast, _}) do
748 |> json("Invalid parameters")
751 def errors(conn, _) do
754 |> json("Something went wrong")