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, params) do
460 {:ok, object} = ActivityPub.upload(params, type: :avatar)
461 change = Changeset.change(user, %{avatar: object.data})
462 {:ok, user} = User.update_and_set_cache(change)
463 CommonAPI.update(user)
466 |> put_view(UserView)
467 |> render("show.json", %{user: user, for: user})
470 def update_banner(%{assigns: %{user: user}} = conn, params) do
471 with {:ok, object} <- ActivityPub.upload(%{"img" => params["banner"]}, type: :banner),
472 new_info <- %{"banner" => object.data},
473 info_cng <- User.Info.profile_update(user.info, new_info),
474 changeset <- Ecto.Changeset.change(user) |> Ecto.Changeset.put_embed(:info, info_cng),
475 {:ok, user} <- User.update_and_set_cache(changeset) do
476 CommonAPI.update(user)
477 %{"url" => [%{"href" => href} | _]} = object.data
478 response = %{url: href} |> Jason.encode!()
481 |> json_reply(200, response)
485 def update_background(%{assigns: %{user: user}} = conn, params) do
486 with {:ok, object} <- ActivityPub.upload(params, type: :background),
487 new_info <- %{"background" => object.data},
488 info_cng <- User.Info.profile_update(user.info, new_info),
489 changeset <- Ecto.Changeset.change(user) |> Ecto.Changeset.put_embed(:info, info_cng),
490 {:ok, _user} <- User.update_and_set_cache(changeset) do
491 %{"url" => [%{"href" => href} | _]} = object.data
492 response = %{url: href} |> Jason.encode!()
495 |> json_reply(200, response)
499 def external_profile(%{assigns: %{user: current_user}} = conn, %{"profileurl" => uri}) do
500 with {:ok, user_map} <- TwitterAPI.get_external_profile(current_user, uri),
501 response <- Jason.encode!(user_map) do
503 |> json_reply(200, response)
508 |> json(%{error: "Can't find user"})
512 def followers(%{assigns: %{user: for_user}} = conn, params) do
513 {:ok, page} = Ecto.Type.cast(:integer, params["page"] || 1)
515 with {:ok, user} <- TwitterAPI.get_user(for_user, params),
516 {:ok, followers} <- User.get_followers(user, page) do
519 for_user && user.id == for_user.id -> followers
520 user.info.hide_followers -> []
525 |> put_view(UserView)
526 |> render("index.json", %{users: followers, for: conn.assigns[:user]})
528 _e -> bad_request_reply(conn, "Can't get followers")
532 def friends(%{assigns: %{user: for_user}} = conn, params) do
533 {:ok, page} = Ecto.Type.cast(:integer, params["page"] || 1)
534 {:ok, export} = Ecto.Type.cast(:boolean, params["all"] || false)
536 page = if export, do: nil, else: page
538 with {:ok, user} <- TwitterAPI.get_user(conn.assigns[:user], params),
539 {:ok, friends} <- User.get_friends(user, page) do
542 for_user && user.id == for_user.id -> friends
543 user.info.hide_follows -> []
548 |> put_view(UserView)
549 |> render("index.json", %{users: friends, for: conn.assigns[:user]})
551 _e -> bad_request_reply(conn, "Can't get friends")
555 def oauth_tokens(%{assigns: %{user: user}} = conn, _params) do
556 with oauth_tokens <- Token.get_user_tokens(user) do
558 |> put_view(TokenView)
559 |> render("index.json", %{tokens: oauth_tokens})
563 def revoke_token(%{assigns: %{user: user}} = conn, %{"id" => id} = _params) do
564 Token.delete_user_token(user, id)
566 json_reply(conn, 201, "")
569 def blocks(%{assigns: %{user: user}} = conn, _params) do
570 with blocked_users <- User.blocked_users(user) do
572 |> put_view(UserView)
573 |> render("index.json", %{users: blocked_users, for: user})
577 def friend_requests(conn, params) do
578 with {:ok, user} <- TwitterAPI.get_user(conn.assigns[:user], params),
579 {:ok, friend_requests} <- User.get_follow_requests(user) do
581 |> put_view(UserView)
582 |> render("index.json", %{users: friend_requests, for: conn.assigns[:user]})
584 _e -> bad_request_reply(conn, "Can't get friend requests")
588 def approve_friend_request(conn, %{"user_id" => uid} = _params) do
589 with followed <- conn.assigns[:user],
590 %User{} = follower <- User.get_cached_by_id(uid),
591 {:ok, follower} <- CommonAPI.accept_follow_request(follower, followed) do
593 |> put_view(UserView)
594 |> render("show.json", %{user: follower, for: followed})
596 e -> bad_request_reply(conn, "Can't approve user: #{inspect(e)}")
600 def deny_friend_request(conn, %{"user_id" => uid} = _params) do
601 with followed <- conn.assigns[:user],
602 %User{} = follower <- User.get_cached_by_id(uid),
603 {:ok, follower} <- CommonAPI.reject_follow_request(follower, followed) do
605 |> put_view(UserView)
606 |> render("show.json", %{user: follower, for: followed})
608 e -> bad_request_reply(conn, "Can't deny user: #{inspect(e)}")
612 def friends_ids(%{assigns: %{user: user}} = conn, _params) do
613 with {:ok, friends} <- User.get_friends(user) do
616 |> Enum.map(fn x -> x.id end)
621 _e -> bad_request_reply(conn, "Can't get friends")
625 def empty_array(conn, _params) do
626 json(conn, Jason.encode!([]))
629 def raw_empty_array(conn, _params) do
633 defp build_info_cng(user, params) do
642 "skip_thread_containment"
644 |> Enum.reduce(%{}, fn key, res ->
645 if value = params[key] do
646 Map.put(res, key, value == "true")
653 if value = params["default_scope"] do
654 Map.put(info_params, "default_scope", value)
659 User.Info.profile_update(user.info, info_params)
662 defp parse_profile_bio(user, params) do
663 if bio = params["description"] do
664 emojis_text = (params["description"] || "") <> " " <> (params["name"] || "")
667 ((user.info.emoji || []) ++ Formatter.get_emoji_map(emojis_text))
678 |> Map.put("bio", User.parse_bio(bio, user))
679 |> Map.put("info", user_info)
685 def update_profile(%{assigns: %{user: user}} = conn, params) do
686 params = parse_profile_bio(user, params)
687 info_cng = build_info_cng(user, params)
689 with changeset <- User.update_changeset(user, params),
690 changeset <- Ecto.Changeset.put_embed(changeset, :info, info_cng),
691 {:ok, user} <- User.update_and_set_cache(changeset) do
692 CommonAPI.update(user)
695 |> put_view(UserView)
696 |> render("user.json", %{user: user, for: user})
699 Logger.debug("Can't update user: #{inspect(error)}")
700 bad_request_reply(conn, "Can't update user")
704 def search(%{assigns: %{user: user}} = conn, %{"q" => _query} = params) do
705 activities = TwitterAPI.search(user, params)
708 |> put_view(ActivityView)
709 |> render("index.json", %{activities: activities, for: user})
712 def search_user(%{assigns: %{user: user}} = conn, %{"query" => query}) do
713 users = User.search(query, resolve: true, for_user: user)
716 |> put_view(UserView)
717 |> render("index.json", %{users: users, for: user})
720 defp bad_request_reply(conn, error_message) do
721 json = error_json(conn, error_message)
722 json_reply(conn, 400, json)
725 defp json_reply(conn, status, json) do
727 |> put_resp_content_type("application/json")
728 |> send_resp(status, json)
731 defp forbidden_json_reply(conn, error_message) do
732 json = error_json(conn, error_message)
733 json_reply(conn, 403, json)
736 def only_if_public_instance(%{assigns: %{user: %User{}}} = conn, _), do: conn
738 def only_if_public_instance(conn, _) do
739 if Pleroma.Config.get([:instance, :public]) do
743 |> forbidden_json_reply("Invalid credentials.")
748 defp error_json(conn, error_message) do
749 %{"error" => error_message, "request" => conn.request_path} |> Jason.encode!()
752 def errors(conn, {:param_cast, _}) do
755 |> json("Invalid parameters")
758 def errors(conn, _) do
761 |> json("Something went wrong")