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.Utils
18 alias Pleroma.Web.ActivityPub.Visibility
19 alias Pleroma.Web.CommonAPI
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) <- TwitterAPI.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} <- User.maybe_follow(follower, followed),
592 %Activity{} = follow_activity <- Utils.fetch_latest_follow(follower, followed),
593 {:ok, follow_activity} <- Utils.update_follow_state(follow_activity, "accept"),
595 ActivityPub.accept(%{
596 to: [follower.ap_id],
598 object: follow_activity.data["id"],
602 |> put_view(UserView)
603 |> render("show.json", %{user: follower, for: followed})
605 e -> bad_request_reply(conn, "Can't approve user: #{inspect(e)}")
609 def deny_friend_request(conn, %{"user_id" => uid} = _params) do
610 with followed <- conn.assigns[:user],
611 %User{} = follower <- Repo.get(User, uid),
612 %Activity{} = follow_activity <- Utils.fetch_latest_follow(follower, followed),
613 {:ok, follow_activity} <- Utils.update_follow_state(follow_activity, "reject"),
615 ActivityPub.reject(%{
616 to: [follower.ap_id],
618 object: follow_activity.data["id"],
622 |> put_view(UserView)
623 |> render("show.json", %{user: follower, for: followed})
625 e -> bad_request_reply(conn, "Can't deny user: #{inspect(e)}")
629 def friends_ids(%{assigns: %{user: user}} = conn, _params) do
630 with {:ok, friends} <- User.get_friends(user) do
633 |> Enum.map(fn x -> x.id end)
638 _e -> bad_request_reply(conn, "Can't get friends")
642 def empty_array(conn, _params) do
643 json(conn, Jason.encode!([]))
646 def raw_empty_array(conn, _params) do
650 defp build_info_cng(user, params) do
652 ["no_rich_text", "locked", "hide_followers", "hide_follows", "show_role"]
653 |> Enum.reduce(%{}, fn key, res ->
654 if value = params[key] do
655 Map.put(res, key, value == "true")
662 if value = params["default_scope"] do
663 Map.put(info_params, "default_scope", value)
668 User.Info.profile_update(user.info, info_params)
671 defp parse_profile_bio(user, params) do
672 if bio = params["description"] do
673 Map.put(params, "bio", User.parse_bio(bio, user))
679 def update_profile(%{assigns: %{user: user}} = conn, params) do
680 params = parse_profile_bio(user, params)
681 info_cng = build_info_cng(user, params)
683 with changeset <- User.update_changeset(user, params),
684 changeset <- Ecto.Changeset.put_embed(changeset, :info, info_cng),
685 {:ok, user} <- User.update_and_set_cache(changeset) do
686 CommonAPI.update(user)
689 |> put_view(UserView)
690 |> render("user.json", %{user: user, for: user})
693 Logger.debug("Can't update user: #{inspect(error)}")
694 bad_request_reply(conn, "Can't update user")
698 def search(%{assigns: %{user: user}} = conn, %{"q" => _query} = params) do
699 activities = TwitterAPI.search(user, params)
702 |> put_view(ActivityView)
703 |> render("index.json", %{activities: activities, for: user})
706 def search_user(%{assigns: %{user: user}} = conn, %{"query" => query}) do
707 users = User.search(query, resolve: true, for_user: user)
710 |> put_view(UserView)
711 |> render("index.json", %{users: users, for: user})
714 defp bad_request_reply(conn, error_message) do
715 json = error_json(conn, error_message)
716 json_reply(conn, 400, json)
719 defp json_reply(conn, status, json) do
721 |> put_resp_content_type("application/json")
722 |> send_resp(status, json)
725 defp forbidden_json_reply(conn, error_message) do
726 json = error_json(conn, error_message)
727 json_reply(conn, 403, json)
730 def only_if_public_instance(%{assigns: %{user: %User{}}} = conn, _), do: conn
732 def only_if_public_instance(conn, _) do
733 if Keyword.get(Application.get_env(:pleroma, :instance), :public) do
737 |> forbidden_json_reply("Invalid credentials.")
742 defp error_json(conn, error_message) do
743 %{"error" => error_message, "request" => conn.request_path} |> Jason.encode!()
746 def errors(conn, {:param_cast, _}) do
749 |> json("Invalid parameters")
752 def errors(conn, _) do
755 |> json("Something went wrong")