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")
185 |> Map.put(:order, :desc)
188 ActivityPub.fetch_activities_query([user.ap_id], params)
192 |> put_view(ActivityView)
193 |> render("index.json", %{activities: activities, for: user})
196 def notifications(%{assigns: %{user: user}} = conn, params) do
197 notifications = Notification.for_user(user, params)
200 |> put_view(NotificationView)
201 |> render("notification.json", %{notifications: notifications, for: user})
204 def notifications_read(%{assigns: %{user: user}} = conn, %{"latest_id" => latest_id} = params) do
205 Notification.set_read_up_to(user, latest_id)
207 notifications = Notification.for_user(user, params)
210 |> put_view(NotificationView)
211 |> render("notification.json", %{notifications: notifications, for: user})
214 def notifications_read(%{assigns: %{user: _user}} = conn, _) do
215 bad_request_reply(conn, "You need to specify latest_id")
218 def follow(%{assigns: %{user: user}} = conn, params) do
219 case TwitterAPI.follow(user, params) do
220 {:ok, user, followed, _activity} ->
222 |> put_view(UserView)
223 |> render("show.json", %{user: followed, for: user})
226 forbidden_json_reply(conn, msg)
230 def block(%{assigns: %{user: user}} = conn, params) do
231 case TwitterAPI.block(user, params) do
232 {:ok, user, blocked} ->
234 |> put_view(UserView)
235 |> render("show.json", %{user: blocked, for: user})
238 forbidden_json_reply(conn, msg)
242 def unblock(%{assigns: %{user: user}} = conn, params) do
243 case TwitterAPI.unblock(user, params) do
244 {:ok, user, blocked} ->
246 |> put_view(UserView)
247 |> render("show.json", %{user: blocked, for: user})
250 forbidden_json_reply(conn, msg)
254 def delete_post(%{assigns: %{user: user}} = conn, %{"id" => id}) do
255 with {:ok, activity} <- TwitterAPI.delete(user, id) do
257 |> put_view(ActivityView)
258 |> render("activity.json", %{activity: activity, for: user})
262 def unfollow(%{assigns: %{user: user}} = conn, params) do
263 case TwitterAPI.unfollow(user, params) do
264 {:ok, user, unfollowed} ->
266 |> put_view(UserView)
267 |> render("show.json", %{user: unfollowed, for: user})
270 forbidden_json_reply(conn, msg)
274 def fetch_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
275 with %Activity{} = activity <- Activity.get_by_id(id),
276 true <- Visibility.visible_for_user?(activity, user) do
278 |> put_view(ActivityView)
279 |> render("activity.json", %{activity: activity, for: user})
283 def fetch_conversation(%{assigns: %{user: user}} = conn, %{"id" => id}) do
284 with context when is_binary(context) <- Utils.conversation_id_to_context(id),
286 ActivityPub.fetch_activities_for_context(context, %{
287 "blocking_user" => user,
291 |> put_view(ActivityView)
292 |> render("index.json", %{activities: activities, for: user})
297 Updates metadata of uploaded media object.
298 Derived from [Twitter API endpoint](https://developer.twitter.com/en/docs/media/upload-media/api-reference/post-media-metadata-create).
300 def update_media(%{assigns: %{user: user}} = conn, %{"media_id" => id} = data) do
301 object = Repo.get(Object, id)
302 description = get_in(data, ["alt_text", "text"]) || data["name"] || data["description"]
304 {conn, status, response_body} =
307 {halt(conn), :not_found, ""}
309 !Object.authorize_mutation(object, user) ->
310 {halt(conn), :forbidden, "You can only update your own uploads."}
312 !is_binary(description) ->
313 {conn, :not_modified, ""}
316 new_data = Map.put(object.data, "name", description)
320 |> Object.change(%{data: new_data})
323 {conn, :no_content, ""}
327 |> put_status(status)
328 |> json(response_body)
331 def upload(%{assigns: %{user: user}} = conn, %{"media" => media}) do
332 response = TwitterAPI.upload(media, user)
335 |> put_resp_content_type("application/atom+xml")
336 |> send_resp(200, response)
339 def upload_json(%{assigns: %{user: user}} = conn, %{"media" => media}) do
340 response = TwitterAPI.upload(media, user, "json")
343 |> json_reply(200, response)
346 def get_by_id_or_ap_id(id) do
347 activity = Activity.get_by_id(id) || Activity.get_create_by_object_ap_id(id)
349 if activity.data["type"] == "Create" do
352 Activity.get_create_by_object_ap_id(activity.data["object"])
356 def favorite(%{assigns: %{user: user}} = conn, %{"id" => id}) do
357 with {:ok, activity} <- TwitterAPI.fav(user, id) do
359 |> put_view(ActivityView)
360 |> render("activity.json", %{activity: activity, for: user})
362 _ -> json_reply(conn, 400, Jason.encode!(%{}))
366 def unfavorite(%{assigns: %{user: user}} = conn, %{"id" => id}) do
367 with {:ok, activity} <- TwitterAPI.unfav(user, id) do
369 |> put_view(ActivityView)
370 |> render("activity.json", %{activity: activity, for: user})
372 _ -> json_reply(conn, 400, Jason.encode!(%{}))
376 def retweet(%{assigns: %{user: user}} = conn, %{"id" => id}) do
377 with {:ok, activity} <- TwitterAPI.repeat(user, id) do
379 |> put_view(ActivityView)
380 |> render("activity.json", %{activity: activity, for: user})
382 _ -> json_reply(conn, 400, Jason.encode!(%{}))
386 def unretweet(%{assigns: %{user: user}} = conn, %{"id" => id}) do
387 with {:ok, activity} <- TwitterAPI.unrepeat(user, id) do
389 |> put_view(ActivityView)
390 |> render("activity.json", %{activity: activity, for: user})
392 _ -> json_reply(conn, 400, Jason.encode!(%{}))
396 def pin(%{assigns: %{user: user}} = conn, %{"id" => id}) do
397 with {:ok, activity} <- TwitterAPI.pin(user, id) do
399 |> put_view(ActivityView)
400 |> render("activity.json", %{activity: activity, for: user})
402 {:error, message} -> bad_request_reply(conn, message)
407 def unpin(%{assigns: %{user: user}} = conn, %{"id" => id}) do
408 with {:ok, activity} <- TwitterAPI.unpin(user, id) do
410 |> put_view(ActivityView)
411 |> render("activity.json", %{activity: activity, for: user})
413 {:error, message} -> bad_request_reply(conn, message)
418 def register(conn, params) do
419 with {:ok, user} <- TwitterAPI.register_user(params) do
421 |> put_view(UserView)
422 |> render("show.json", %{user: user})
426 |> json_reply(400, Jason.encode!(errors))
430 def password_reset(conn, params) do
431 nickname_or_email = params["email"] || params["nickname"]
433 with {:ok, _} <- TwitterAPI.password_reset(nickname_or_email) do
434 json_response(conn, :no_content, "")
438 def confirm_email(conn, %{"user_id" => uid, "token" => token}) do
439 with %User{} = user <- User.get_cached_by_id(uid),
441 true <- user.info.confirmation_pending,
442 true <- user.info.confirmation_token == token,
443 info_change <- User.Info.confirmation_changeset(user.info, :confirmed),
444 changeset <- Changeset.change(user) |> Changeset.put_embed(:info, info_change),
445 {:ok, _} <- User.update_and_set_cache(changeset) do
451 def resend_confirmation_email(conn, params) do
452 nickname_or_email = params["email"] || params["nickname"]
454 with %User{} = user <- User.get_by_nickname_or_email(nickname_or_email),
455 {:ok, _} <- User.try_send_confirmation_email(user) do
457 |> json_response(:no_content, "")
461 def update_avatar(%{assigns: %{user: user}} = conn, params) do
462 {:ok, object} = ActivityPub.upload(params, type: :avatar)
463 change = Changeset.change(user, %{avatar: object.data})
464 {:ok, user} = User.update_and_set_cache(change)
465 CommonAPI.update(user)
468 |> put_view(UserView)
469 |> render("show.json", %{user: user, for: user})
472 def update_banner(%{assigns: %{user: user}} = conn, params) do
473 with {:ok, object} <- ActivityPub.upload(%{"img" => params["banner"]}, type: :banner),
474 new_info <- %{"banner" => object.data},
475 info_cng <- User.Info.profile_update(user.info, new_info),
476 changeset <- Ecto.Changeset.change(user) |> Ecto.Changeset.put_embed(:info, info_cng),
477 {:ok, user} <- User.update_and_set_cache(changeset) do
478 CommonAPI.update(user)
479 %{"url" => [%{"href" => href} | _]} = object.data
480 response = %{url: href} |> Jason.encode!()
483 |> json_reply(200, response)
487 def update_background(%{assigns: %{user: user}} = conn, params) do
488 with {:ok, object} <- ActivityPub.upload(params, type: :background),
489 new_info <- %{"background" => object.data},
490 info_cng <- User.Info.profile_update(user.info, new_info),
491 changeset <- Ecto.Changeset.change(user) |> Ecto.Changeset.put_embed(:info, info_cng),
492 {:ok, _user} <- User.update_and_set_cache(changeset) do
493 %{"url" => [%{"href" => href} | _]} = object.data
494 response = %{url: href} |> Jason.encode!()
497 |> json_reply(200, response)
501 def external_profile(%{assigns: %{user: current_user}} = conn, %{"profileurl" => uri}) do
502 with {:ok, user_map} <- TwitterAPI.get_external_profile(current_user, uri),
503 response <- Jason.encode!(user_map) do
505 |> json_reply(200, response)
510 |> json(%{error: "Can't find user"})
514 def followers(%{assigns: %{user: for_user}} = conn, params) do
515 {:ok, page} = Ecto.Type.cast(:integer, params["page"] || 1)
517 with {:ok, user} <- TwitterAPI.get_user(for_user, params),
518 {:ok, followers} <- User.get_followers(user, page) do
521 for_user && user.id == for_user.id -> followers
522 user.info.hide_followers -> []
527 |> put_view(UserView)
528 |> render("index.json", %{users: followers, for: conn.assigns[:user]})
530 _e -> bad_request_reply(conn, "Can't get followers")
534 def friends(%{assigns: %{user: for_user}} = conn, params) do
535 {:ok, page} = Ecto.Type.cast(:integer, params["page"] || 1)
536 {:ok, export} = Ecto.Type.cast(:boolean, params["all"] || false)
538 page = if export, do: nil, else: page
540 with {:ok, user} <- TwitterAPI.get_user(conn.assigns[:user], params),
541 {:ok, friends} <- User.get_friends(user, page) do
544 for_user && user.id == for_user.id -> friends
545 user.info.hide_follows -> []
550 |> put_view(UserView)
551 |> render("index.json", %{users: friends, for: conn.assigns[:user]})
553 _e -> bad_request_reply(conn, "Can't get friends")
557 def oauth_tokens(%{assigns: %{user: user}} = conn, _params) do
558 with oauth_tokens <- Token.get_user_tokens(user) do
560 |> put_view(TokenView)
561 |> render("index.json", %{tokens: oauth_tokens})
565 def revoke_token(%{assigns: %{user: user}} = conn, %{"id" => id} = _params) do
566 Token.delete_user_token(user, id)
568 json_reply(conn, 201, "")
571 def blocks(%{assigns: %{user: user}} = conn, _params) do
572 with blocked_users <- User.blocked_users(user) do
574 |> put_view(UserView)
575 |> render("index.json", %{users: blocked_users, for: user})
579 def friend_requests(conn, params) do
580 with {:ok, user} <- TwitterAPI.get_user(conn.assigns[:user], params),
581 {:ok, friend_requests} <- User.get_follow_requests(user) do
583 |> put_view(UserView)
584 |> render("index.json", %{users: friend_requests, for: conn.assigns[:user]})
586 _e -> bad_request_reply(conn, "Can't get friend requests")
590 def approve_friend_request(conn, %{"user_id" => uid} = _params) do
591 with followed <- conn.assigns[:user],
592 %User{} = follower <- User.get_cached_by_id(uid),
593 {:ok, follower} <- CommonAPI.accept_follow_request(follower, followed) do
595 |> put_view(UserView)
596 |> render("show.json", %{user: follower, for: followed})
598 e -> bad_request_reply(conn, "Can't approve user: #{inspect(e)}")
602 def deny_friend_request(conn, %{"user_id" => uid} = _params) do
603 with followed <- conn.assigns[:user],
604 %User{} = follower <- User.get_cached_by_id(uid),
605 {:ok, follower} <- CommonAPI.reject_follow_request(follower, followed) do
607 |> put_view(UserView)
608 |> render("show.json", %{user: follower, for: followed})
610 e -> bad_request_reply(conn, "Can't deny user: #{inspect(e)}")
614 def friends_ids(%{assigns: %{user: user}} = conn, _params) do
615 with {:ok, friends} <- User.get_friends(user) do
618 |> Enum.map(fn x -> x.id end)
623 _e -> bad_request_reply(conn, "Can't get friends")
627 def empty_array(conn, _params) do
628 json(conn, Jason.encode!([]))
631 def raw_empty_array(conn, _params) do
635 defp build_info_cng(user, params) do
637 ["no_rich_text", "locked", "hide_followers", "hide_follows", "hide_favorites", "show_role"]
638 |> Enum.reduce(%{}, fn key, res ->
639 if value = params[key] do
640 Map.put(res, key, value == "true")
647 if value = params["default_scope"] do
648 Map.put(info_params, "default_scope", value)
653 User.Info.profile_update(user.info, info_params)
656 defp parse_profile_bio(user, params) do
657 if bio = params["description"] do
658 emojis_text = (params["description"] || "") <> " " <> (params["name"] || "")
661 ((user.info.emoji || []) ++ Formatter.get_emoji_map(emojis_text))
672 |> Map.put("bio", User.parse_bio(bio, user))
673 |> Map.put("info", user_info)
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")