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 subscribe(%{assigns: %{user: user}} = conn, params) do
273 case TwitterAPI.subscribe(user, params) do
274 {:ok, user, subscribed} ->
276 |> put_view(UserView)
277 |> render("show.json", %{user: subscribed, for: user})
280 forbidden_json_reply(conn, msg)
284 def unsubscribe(%{assigns: %{user: user}} = conn, params) do
285 case TwitterAPI.unsubscribe(user, params) do
286 {:ok, user, unsubscribed} ->
288 |> put_view(UserView)
289 |> render("show.json", %{user: unsubscribed, for: user})
292 forbidden_json_reply(conn, msg)
296 def fetch_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
297 with %Activity{} = activity <- Activity.get_by_id(id),
298 true <- Visibility.visible_for_user?(activity, user) do
300 |> put_view(ActivityView)
301 |> render("activity.json", %{activity: activity, for: user})
305 def fetch_conversation(%{assigns: %{user: user}} = conn, %{"id" => id}) do
306 with context when is_binary(context) <- Utils.conversation_id_to_context(id),
308 ActivityPub.fetch_activities_for_context(context, %{
309 "blocking_user" => user,
313 |> put_view(ActivityView)
314 |> render("index.json", %{activities: activities, for: user})
319 Updates metadata of uploaded media object.
320 Derived from [Twitter API endpoint](https://developer.twitter.com/en/docs/media/upload-media/api-reference/post-media-metadata-create).
322 def update_media(%{assigns: %{user: user}} = conn, %{"media_id" => id} = data) do
323 object = Repo.get(Object, id)
324 description = get_in(data, ["alt_text", "text"]) || data["name"] || data["description"]
326 {conn, status, response_body} =
329 {halt(conn), :not_found, ""}
331 !Object.authorize_mutation(object, user) ->
332 {halt(conn), :forbidden, "You can only update your own uploads."}
334 !is_binary(description) ->
335 {conn, :not_modified, ""}
338 new_data = Map.put(object.data, "name", description)
342 |> Object.change(%{data: new_data})
345 {conn, :no_content, ""}
349 |> put_status(status)
350 |> json(response_body)
353 def upload(%{assigns: %{user: user}} = conn, %{"media" => media}) do
354 response = TwitterAPI.upload(media, user)
357 |> put_resp_content_type("application/atom+xml")
358 |> send_resp(200, response)
361 def upload_json(%{assigns: %{user: user}} = conn, %{"media" => media}) do
362 response = TwitterAPI.upload(media, user, "json")
365 |> json_reply(200, response)
368 def get_by_id_or_ap_id(id) do
369 activity = Activity.get_by_id(id) || Activity.get_create_by_object_ap_id(id)
371 if activity.data["type"] == "Create" do
374 Activity.get_create_by_object_ap_id(activity.data["object"])
378 def favorite(%{assigns: %{user: user}} = conn, %{"id" => id}) do
379 with {:ok, activity} <- TwitterAPI.fav(user, id) do
381 |> put_view(ActivityView)
382 |> render("activity.json", %{activity: activity, for: user})
384 _ -> json_reply(conn, 400, Jason.encode!(%{}))
388 def unfavorite(%{assigns: %{user: user}} = conn, %{"id" => id}) do
389 with {:ok, activity} <- TwitterAPI.unfav(user, id) do
391 |> put_view(ActivityView)
392 |> render("activity.json", %{activity: activity, for: user})
394 _ -> json_reply(conn, 400, Jason.encode!(%{}))
398 def retweet(%{assigns: %{user: user}} = conn, %{"id" => id}) do
399 with {:ok, activity} <- TwitterAPI.repeat(user, id) do
401 |> put_view(ActivityView)
402 |> render("activity.json", %{activity: activity, for: user})
404 _ -> json_reply(conn, 400, Jason.encode!(%{}))
408 def unretweet(%{assigns: %{user: user}} = conn, %{"id" => id}) do
409 with {:ok, activity} <- TwitterAPI.unrepeat(user, id) do
411 |> put_view(ActivityView)
412 |> render("activity.json", %{activity: activity, for: user})
414 _ -> json_reply(conn, 400, Jason.encode!(%{}))
418 def pin(%{assigns: %{user: user}} = conn, %{"id" => id}) do
419 with {:ok, activity} <- TwitterAPI.pin(user, id) do
421 |> put_view(ActivityView)
422 |> render("activity.json", %{activity: activity, for: user})
424 {:error, message} -> bad_request_reply(conn, message)
429 def unpin(%{assigns: %{user: user}} = conn, %{"id" => id}) do
430 with {:ok, activity} <- TwitterAPI.unpin(user, id) do
432 |> put_view(ActivityView)
433 |> render("activity.json", %{activity: activity, for: user})
435 {:error, message} -> bad_request_reply(conn, message)
440 def register(conn, params) do
441 with {:ok, user} <- TwitterAPI.register_user(params) do
443 |> put_view(UserView)
444 |> render("show.json", %{user: user})
448 |> json_reply(400, Jason.encode!(errors))
452 def password_reset(conn, params) do
453 nickname_or_email = params["email"] || params["nickname"]
455 with {:ok, _} <- TwitterAPI.password_reset(nickname_or_email) do
456 json_response(conn, :no_content, "")
460 def confirm_email(conn, %{"user_id" => uid, "token" => token}) do
461 with %User{} = user <- User.get_by_id(uid),
463 true <- user.info.confirmation_pending,
464 true <- user.info.confirmation_token == token,
465 info_change <- User.Info.confirmation_changeset(user.info, :confirmed),
466 changeset <- Changeset.change(user) |> Changeset.put_embed(:info, info_change),
467 {:ok, _} <- User.update_and_set_cache(changeset) do
473 def resend_confirmation_email(conn, params) do
474 nickname_or_email = params["email"] || params["nickname"]
476 with %User{} = user <- User.get_by_nickname_or_email(nickname_or_email),
477 {:ok, _} <- User.try_send_confirmation_email(user) do
479 |> json_response(:no_content, "")
483 def update_avatar(%{assigns: %{user: user}} = conn, params) do
484 {:ok, object} = ActivityPub.upload(params, type: :avatar)
485 change = Changeset.change(user, %{avatar: object.data})
486 {:ok, user} = User.update_and_set_cache(change)
487 CommonAPI.update(user)
490 |> put_view(UserView)
491 |> render("show.json", %{user: user, for: user})
494 def update_banner(%{assigns: %{user: user}} = conn, params) do
495 with {:ok, object} <- ActivityPub.upload(%{"img" => params["banner"]}, type: :banner),
496 new_info <- %{"banner" => object.data},
497 info_cng <- User.Info.profile_update(user.info, new_info),
498 changeset <- Ecto.Changeset.change(user) |> Ecto.Changeset.put_embed(:info, info_cng),
499 {:ok, user} <- User.update_and_set_cache(changeset) do
500 CommonAPI.update(user)
501 %{"url" => [%{"href" => href} | _]} = object.data
502 response = %{url: href} |> Jason.encode!()
505 |> json_reply(200, response)
509 def update_background(%{assigns: %{user: user}} = conn, params) do
510 with {:ok, object} <- ActivityPub.upload(params, type: :background),
511 new_info <- %{"background" => object.data},
512 info_cng <- User.Info.profile_update(user.info, new_info),
513 changeset <- Ecto.Changeset.change(user) |> Ecto.Changeset.put_embed(:info, info_cng),
514 {:ok, _user} <- User.update_and_set_cache(changeset) do
515 %{"url" => [%{"href" => href} | _]} = object.data
516 response = %{url: href} |> Jason.encode!()
519 |> json_reply(200, response)
523 def external_profile(%{assigns: %{user: current_user}} = conn, %{"profileurl" => uri}) do
524 with {:ok, user_map} <- TwitterAPI.get_external_profile(current_user, uri),
525 response <- Jason.encode!(user_map) do
527 |> json_reply(200, response)
532 |> json(%{error: "Can't find user"})
536 def followers(%{assigns: %{user: for_user}} = conn, params) do
537 {:ok, page} = Ecto.Type.cast(:integer, params["page"] || 1)
539 with {:ok, user} <- TwitterAPI.get_user(for_user, params),
540 {:ok, followers} <- User.get_followers(user, page) do
543 for_user && user.id == for_user.id -> followers
544 user.info.hide_followers -> []
549 |> put_view(UserView)
550 |> render("index.json", %{users: followers, for: conn.assigns[:user]})
552 _e -> bad_request_reply(conn, "Can't get followers")
556 def friends(%{assigns: %{user: for_user}} = conn, params) do
557 {:ok, page} = Ecto.Type.cast(:integer, params["page"] || 1)
558 {:ok, export} = Ecto.Type.cast(:boolean, params["all"] || false)
560 page = if export, do: nil, else: page
562 with {:ok, user} <- TwitterAPI.get_user(conn.assigns[:user], params),
563 {:ok, friends} <- User.get_friends(user, page) do
566 for_user && user.id == for_user.id -> friends
567 user.info.hide_follows -> []
572 |> put_view(UserView)
573 |> render("index.json", %{users: friends, for: conn.assigns[:user]})
575 _e -> bad_request_reply(conn, "Can't get friends")
579 def oauth_tokens(%{assigns: %{user: user}} = conn, _params) do
580 with oauth_tokens <- Token.get_user_tokens(user) do
582 |> put_view(TokenView)
583 |> render("index.json", %{tokens: oauth_tokens})
587 def revoke_token(%{assigns: %{user: user}} = conn, %{"id" => id} = _params) do
588 Token.delete_user_token(user, id)
590 json_reply(conn, 201, "")
593 def blocks(%{assigns: %{user: user}} = conn, _params) do
594 with blocked_users <- User.blocked_users(user) do
596 |> put_view(UserView)
597 |> render("index.json", %{users: blocked_users, for: user})
601 def friend_requests(conn, params) do
602 with {:ok, user} <- TwitterAPI.get_user(conn.assigns[:user], params),
603 {:ok, friend_requests} <- User.get_follow_requests(user) do
605 |> put_view(UserView)
606 |> render("index.json", %{users: friend_requests, for: conn.assigns[:user]})
608 _e -> bad_request_reply(conn, "Can't get friend requests")
612 def approve_friend_request(conn, %{"user_id" => uid} = _params) do
613 with followed <- conn.assigns[:user],
614 %User{} = follower <- User.get_by_id(uid),
615 {:ok, follower} <- CommonAPI.accept_follow_request(follower, followed) do
617 |> put_view(UserView)
618 |> render("show.json", %{user: follower, for: followed})
620 e -> bad_request_reply(conn, "Can't approve user: #{inspect(e)}")
624 def deny_friend_request(conn, %{"user_id" => uid} = _params) do
625 with followed <- conn.assigns[:user],
626 %User{} = follower <- User.get_by_id(uid),
627 {:ok, follower} <- CommonAPI.reject_follow_request(follower, followed) do
629 |> put_view(UserView)
630 |> render("show.json", %{user: follower, for: followed})
632 e -> bad_request_reply(conn, "Can't deny user: #{inspect(e)}")
636 def friends_ids(%{assigns: %{user: user}} = conn, _params) do
637 with {:ok, friends} <- User.get_friends(user) do
640 |> Enum.map(fn x -> x.id end)
645 _e -> bad_request_reply(conn, "Can't get friends")
649 def empty_array(conn, _params) do
650 json(conn, Jason.encode!([]))
653 def raw_empty_array(conn, _params) do
657 defp build_info_cng(user, params) do
659 ["no_rich_text", "locked", "hide_followers", "hide_follows", "show_role"]
660 |> Enum.reduce(%{}, fn key, res ->
661 if value = params[key] do
662 Map.put(res, key, value == "true")
669 if value = params["default_scope"] do
670 Map.put(info_params, "default_scope", value)
675 User.Info.profile_update(user.info, info_params)
678 defp parse_profile_bio(user, params) do
679 if bio = params["description"] do
680 Map.put(params, "bio", User.parse_bio(bio, user))
686 def update_profile(%{assigns: %{user: user}} = conn, params) do
687 params = parse_profile_bio(user, params)
688 info_cng = build_info_cng(user, params)
690 with changeset <- User.update_changeset(user, params),
691 changeset <- Ecto.Changeset.put_embed(changeset, :info, info_cng),
692 {:ok, user} <- User.update_and_set_cache(changeset) do
693 CommonAPI.update(user)
696 |> put_view(UserView)
697 |> render("user.json", %{user: user, for: user})
700 Logger.debug("Can't update user: #{inspect(error)}")
701 bad_request_reply(conn, "Can't update user")
705 def search(%{assigns: %{user: user}} = conn, %{"q" => _query} = params) do
706 activities = TwitterAPI.search(user, params)
709 |> put_view(ActivityView)
710 |> render("index.json", %{activities: activities, for: user})
713 def search_user(%{assigns: %{user: user}} = conn, %{"query" => query}) do
714 users = User.search(query, resolve: true, for_user: user)
717 |> put_view(UserView)
718 |> render("index.json", %{users: users, for: user})
721 defp bad_request_reply(conn, error_message) do
722 json = error_json(conn, error_message)
723 json_reply(conn, 400, json)
726 defp json_reply(conn, status, json) do
728 |> put_resp_content_type("application/json")
729 |> send_resp(status, json)
732 defp forbidden_json_reply(conn, error_message) do
733 json = error_json(conn, error_message)
734 json_reply(conn, 403, json)
737 def only_if_public_instance(%{assigns: %{user: %User{}}} = conn, _), do: conn
739 def only_if_public_instance(conn, _) do
740 if Keyword.get(Application.get_env(:pleroma, :instance), :public) do
744 |> forbidden_json_reply("Invalid credentials.")
749 defp error_json(conn, error_message) do
750 %{"error" => error_message, "request" => conn.request_path} |> Jason.encode!()
753 def errors(conn, {:param_cast, _}) do
756 |> json("Invalid parameters")
759 def errors(conn, _) do
762 |> json("Something went wrong")