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.Notification
16 alias Pleroma.Web.ActivityPub.ActivityPub
17 alias Pleroma.Web.ActivityPub.Visibility
18 alias Pleroma.Web.CommonAPI
19 alias Pleroma.Web.CommonAPI.Utils
20 alias Pleroma.Web.OAuth.Token
21 alias Pleroma.Web.TwitterAPI.ActivityView
22 alias Pleroma.Web.TwitterAPI.NotificationView
23 alias Pleroma.Web.TwitterAPI.TokenView
24 alias Pleroma.Web.TwitterAPI.TwitterAPI
25 alias Pleroma.Web.TwitterAPI.UserView
29 plug(:only_if_public_instance when action in [:public_timeline, :public_and_external_timeline])
30 action_fallback(:errors)
32 def verify_credentials(%{assigns: %{user: user}} = conn, _params) do
33 token = Phoenix.Token.sign(conn, "user socket", user.id)
37 |> render("show.json", %{user: user, token: token, for: user})
40 def status_update(%{assigns: %{user: user}} = conn, %{"status" => _} = status_data) do
41 with media_ids <- extract_media_ids(status_data),
43 TwitterAPI.create_status(user, Map.put(status_data, "media_ids", media_ids)) do
45 |> json(ActivityView.render("activity.json", activity: activity, for: user))
47 _ -> empty_status_reply(conn)
51 def status_update(conn, _status_data) do
52 empty_status_reply(conn)
55 defp empty_status_reply(conn) do
56 bad_request_reply(conn, "Client must provide a 'status' parameter with a value.")
59 defp extract_media_ids(status_data) do
60 with media_ids when not is_nil(media_ids) <- status_data["media_ids"],
61 split_ids <- String.split(media_ids, ","),
62 clean_ids <- Enum.reject(split_ids, fn id -> String.length(id) == 0 end) do
69 def public_and_external_timeline(%{assigns: %{user: user}} = conn, params) do
72 |> Map.put("type", ["Create", "Announce"])
73 |> Map.put("blocking_user", user)
75 activities = ActivityPub.fetch_public_activities(params)
78 |> put_view(ActivityView)
79 |> render("index.json", %{activities: activities, for: user})
82 def public_timeline(%{assigns: %{user: user}} = conn, params) do
85 |> Map.put("type", ["Create", "Announce"])
86 |> Map.put("local_only", true)
87 |> Map.put("blocking_user", user)
89 activities = ActivityPub.fetch_public_activities(params)
92 |> put_view(ActivityView)
93 |> render("index.json", %{activities: activities, for: user})
96 def friends_timeline(%{assigns: %{user: user}} = conn, params) do
99 |> Map.put("type", ["Create", "Announce", "Follow", "Like"])
100 |> Map.put("blocking_user", user)
101 |> Map.put("user", user)
104 ActivityPub.fetch_activities([user.ap_id | user.following], params)
105 |> ActivityPub.contain_timeline(user)
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")
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 <- Repo.get(Activity, 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 = Repo.get(Activity, 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 <- Repo.get(User, uid),
439 true <- user.info.confirmation_pending,
440 true <- user.info.confirmation_token == token,
441 info_change <- User.Info.confirmation_changeset(user.info, :confirmed),
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 <- Repo.get(User, 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 <- Repo.get(User, 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
635 ["no_rich_text", "locked", "hide_followers", "hide_follows", "show_role"]
636 |> Enum.reduce(%{}, fn key, res ->
637 if value = params[key] do
638 Map.put(res, key, value == "true")
645 if value = params["default_scope"] do
646 Map.put(info_params, "default_scope", value)
651 User.Info.profile_update(user.info, info_params)
654 defp parse_profile_bio(user, params) do
655 if bio = params["description"] do
656 Map.put(params, "bio", User.parse_bio(bio, user))
662 def update_profile(%{assigns: %{user: user}} = conn, params) do
663 params = parse_profile_bio(user, params)
664 info_cng = build_info_cng(user, params)
666 with changeset <- User.update_changeset(user, params),
667 changeset <- Ecto.Changeset.put_embed(changeset, :info, info_cng),
668 {:ok, user} <- User.update_and_set_cache(changeset) do
669 CommonAPI.update(user)
672 |> put_view(UserView)
673 |> render("user.json", %{user: user, for: user})
676 Logger.debug("Can't update user: #{inspect(error)}")
677 bad_request_reply(conn, "Can't update user")
681 def search(%{assigns: %{user: user}} = conn, %{"q" => _query} = params) do
682 activities = TwitterAPI.search(user, params)
685 |> put_view(ActivityView)
686 |> render("index.json", %{activities: activities, for: user})
689 def search_user(%{assigns: %{user: user}} = conn, %{"query" => query}) do
690 users = User.search(query, resolve: true, for_user: user)
693 |> put_view(UserView)
694 |> render("index.json", %{users: users, for: user})
697 defp bad_request_reply(conn, error_message) do
698 json = error_json(conn, error_message)
699 json_reply(conn, 400, json)
702 defp json_reply(conn, status, json) do
704 |> put_resp_content_type("application/json")
705 |> send_resp(status, json)
708 defp forbidden_json_reply(conn, error_message) do
709 json = error_json(conn, error_message)
710 json_reply(conn, 403, json)
713 def only_if_public_instance(%{assigns: %{user: %User{}}} = conn, _), do: conn
715 def only_if_public_instance(conn, _) do
716 if Keyword.get(Application.get_env(:pleroma, :instance), :public) do
720 |> forbidden_json_reply("Invalid credentials.")
725 defp error_json(conn, error_message) do
726 %{"error" => error_message, "request" => conn.request_path} |> Jason.encode!()
729 def errors(conn, {:param_cast, _}) do
732 |> json("Invalid parameters")
735 def errors(conn, _) do
738 |> json("Something went wrong")