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.Activity
13 alias Pleroma.Notification
14 alias Pleroma.Notification
21 alias Pleroma.Web.ActivityPub.ActivityPub
22 alias Pleroma.Web.ActivityPub.Utils
23 alias Pleroma.Web.ActivityPub.Visibility
24 alias Pleroma.Web.CommonAPI
25 alias Pleroma.Web.CommonAPI
26 alias Pleroma.Web.OAuth.Token
27 alias Pleroma.Web.TwitterAPI.ActivityView
28 alias Pleroma.Web.TwitterAPI.ActivityView
29 alias Pleroma.Web.TwitterAPI.NotificationView
30 alias Pleroma.Web.TwitterAPI.NotificationView
31 alias Pleroma.Web.TwitterAPI.TokenView
32 alias Pleroma.Web.TwitterAPI.TwitterAPI
33 alias Pleroma.Web.TwitterAPI.TwitterAPI
34 alias Pleroma.Web.TwitterAPI.UserView
35 alias Pleroma.Web.TwitterAPI.UserView
39 plug(:only_if_public_instance when action in [:public_timeline, :public_and_external_timeline])
40 action_fallback(:errors)
42 def verify_credentials(%{assigns: %{user: user}} = conn, _params) do
43 token = Phoenix.Token.sign(conn, "user socket", user.id)
47 |> render("show.json", %{user: user, token: token, for: user})
50 def status_update(%{assigns: %{user: user}} = conn, %{"status" => _} = status_data) do
51 with media_ids <- extract_media_ids(status_data),
53 TwitterAPI.create_status(user, Map.put(status_data, "media_ids", media_ids)) do
55 |> json(ActivityView.render("activity.json", activity: activity, for: user))
57 _ -> empty_status_reply(conn)
61 def status_update(conn, _status_data) do
62 empty_status_reply(conn)
65 defp empty_status_reply(conn) do
66 bad_request_reply(conn, "Client must provide a 'status' parameter with a value.")
69 defp extract_media_ids(status_data) do
70 with media_ids when not is_nil(media_ids) <- status_data["media_ids"],
71 split_ids <- String.split(media_ids, ","),
72 clean_ids <- Enum.reject(split_ids, fn id -> String.length(id) == 0 end) do
79 def public_and_external_timeline(%{assigns: %{user: user}} = conn, params) do
82 |> Map.put("type", ["Create", "Announce"])
83 |> Map.put("blocking_user", user)
85 activities = ActivityPub.fetch_public_activities(params)
88 |> put_view(ActivityView)
89 |> render("index.json", %{activities: activities, for: user})
92 def public_timeline(%{assigns: %{user: user}} = conn, params) do
95 |> Map.put("type", ["Create", "Announce"])
96 |> Map.put("local_only", true)
97 |> Map.put("blocking_user", user)
99 activities = ActivityPub.fetch_public_activities(params)
102 |> put_view(ActivityView)
103 |> render("index.json", %{activities: activities, for: user})
106 def friends_timeline(%{assigns: %{user: user}} = conn, params) do
109 |> Map.put("type", ["Create", "Announce", "Follow", "Like"])
110 |> Map.put("blocking_user", user)
111 |> Map.put("user", user)
114 ActivityPub.fetch_activities([user.ap_id | user.following], params)
115 |> ActivityPub.contain_timeline(user)
118 |> put_view(ActivityView)
119 |> render("index.json", %{activities: activities, for: user})
122 def show_user(conn, params) do
123 for_user = conn.assigns.user
125 with {:ok, shown} <- TwitterAPI.get_user(params),
127 User.auth_active?(shown) ||
128 (for_user && (for_user.id == shown.id || User.superuser?(for_user))) do
131 %{user: shown, for: for_user}
137 |> put_view(UserView)
138 |> render("show.json", params)
141 bad_request_reply(conn, msg)
146 |> json(%{error: "Unconfirmed user"})
150 def user_timeline(%{assigns: %{user: user}} = conn, params) do
151 case TwitterAPI.get_user(user, params) do
152 {:ok, target_user} ->
153 # Twitter and ActivityPub use a different name and sense for this parameter.
154 {include_rts, params} = Map.pop(params, "include_rts")
158 x when x == "false" or x == "0" -> Map.put(params, "exclude_reblogs", "true")
162 activities = ActivityPub.fetch_user_activities(target_user, user, params)
165 |> put_view(ActivityView)
166 |> render("index.json", %{activities: activities, for: user})
169 bad_request_reply(conn, msg)
173 def mentions_timeline(%{assigns: %{user: user}} = conn, params) do
176 |> Map.put("type", ["Create", "Announce", "Follow", "Like"])
177 |> Map.put("blocking_user", user)
178 |> Map.put(:visibility, ~w[unlisted public private])
180 activities = ActivityPub.fetch_activities([user.ap_id], params)
183 |> put_view(ActivityView)
184 |> render("index.json", %{activities: activities, for: user})
187 def dm_timeline(%{assigns: %{user: user}} = conn, params) do
190 |> Map.put("type", "Create")
191 |> Map.put("blocking_user", user)
192 |> Map.put("user", user)
193 |> Map.put(:visibility, "direct")
196 ActivityPub.fetch_activities_query([user.ap_id], params)
200 |> put_view(ActivityView)
201 |> render("index.json", %{activities: activities, for: user})
204 def notifications(%{assigns: %{user: user}} = conn, params) do
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, %{"latest_id" => latest_id} = params) do
213 Notification.set_read_up_to(user, latest_id)
215 notifications = Notification.for_user(user, params)
218 |> put_view(NotificationView)
219 |> render("notification.json", %{notifications: notifications, for: user})
222 def notifications_read(%{assigns: %{user: _user}} = conn, _) do
223 bad_request_reply(conn, "You need to specify latest_id")
226 def follow(%{assigns: %{user: user}} = conn, params) do
227 case TwitterAPI.follow(user, params) do
228 {:ok, user, followed, _activity} ->
230 |> put_view(UserView)
231 |> render("show.json", %{user: followed, for: user})
234 forbidden_json_reply(conn, msg)
238 def block(%{assigns: %{user: user}} = conn, params) do
239 case TwitterAPI.block(user, params) do
240 {:ok, user, blocked} ->
242 |> put_view(UserView)
243 |> render("show.json", %{user: blocked, for: user})
246 forbidden_json_reply(conn, msg)
250 def unblock(%{assigns: %{user: user}} = conn, params) do
251 case TwitterAPI.unblock(user, params) do
252 {:ok, user, blocked} ->
254 |> put_view(UserView)
255 |> render("show.json", %{user: blocked, for: user})
258 forbidden_json_reply(conn, msg)
262 def delete_post(%{assigns: %{user: user}} = conn, %{"id" => id}) do
263 with {:ok, activity} <- TwitterAPI.delete(user, id) do
265 |> put_view(ActivityView)
266 |> render("activity.json", %{activity: activity, for: user})
270 def unfollow(%{assigns: %{user: user}} = conn, params) do
271 case TwitterAPI.unfollow(user, params) do
272 {:ok, user, unfollowed} ->
274 |> put_view(UserView)
275 |> render("show.json", %{user: unfollowed, for: user})
278 forbidden_json_reply(conn, msg)
282 def fetch_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
283 with %Activity{} = activity <- Repo.get(Activity, id),
284 true <- Visibility.visible_for_user?(activity, user) do
286 |> put_view(ActivityView)
287 |> render("activity.json", %{activity: activity, for: user})
291 def fetch_conversation(%{assigns: %{user: user}} = conn, %{"id" => id}) do
292 with context when is_binary(context) <- TwitterAPI.conversation_id_to_context(id),
294 ActivityPub.fetch_activities_for_context(context, %{
295 "blocking_user" => user,
299 |> put_view(ActivityView)
300 |> render("index.json", %{activities: activities, for: user})
305 Updates metadata of uploaded media object.
306 Derived from [Twitter API endpoint](https://developer.twitter.com/en/docs/media/upload-media/api-reference/post-media-metadata-create).
308 def update_media(%{assigns: %{user: user}} = conn, %{"media_id" => id} = data) do
309 object = Repo.get(Object, id)
310 description = get_in(data, ["alt_text", "text"]) || data["name"] || data["description"]
312 {conn, status, response_body} =
315 {halt(conn), :not_found, ""}
317 !Object.authorize_mutation(object, user) ->
318 {halt(conn), :forbidden, "You can only update your own uploads."}
320 !is_binary(description) ->
321 {conn, :not_modified, ""}
324 new_data = Map.put(object.data, "name", description)
328 |> Object.change(%{data: new_data})
331 {conn, :no_content, ""}
335 |> put_status(status)
336 |> json(response_body)
339 def upload(%{assigns: %{user: user}} = conn, %{"media" => media}) do
340 response = TwitterAPI.upload(media, user)
343 |> put_resp_content_type("application/atom+xml")
344 |> send_resp(200, response)
347 def upload_json(%{assigns: %{user: user}} = conn, %{"media" => media}) do
348 response = TwitterAPI.upload(media, user, "json")
351 |> json_reply(200, response)
354 def get_by_id_or_ap_id(id) do
355 activity = Repo.get(Activity, id) || Activity.get_create_by_object_ap_id(id)
357 if activity.data["type"] == "Create" do
360 Activity.get_create_by_object_ap_id(activity.data["object"])
364 def favorite(%{assigns: %{user: user}} = conn, %{"id" => id}) do
365 with {:ok, activity} <- TwitterAPI.fav(user, id) do
367 |> put_view(ActivityView)
368 |> render("activity.json", %{activity: activity, for: user})
370 _ -> json_reply(conn, 400, Jason.encode!(%{}))
374 def unfavorite(%{assigns: %{user: user}} = conn, %{"id" => id}) do
375 with {:ok, activity} <- TwitterAPI.unfav(user, id) do
377 |> put_view(ActivityView)
378 |> render("activity.json", %{activity: activity, for: user})
380 _ -> json_reply(conn, 400, Jason.encode!(%{}))
384 def retweet(%{assigns: %{user: user}} = conn, %{"id" => id}) do
385 with {:ok, activity} <- TwitterAPI.repeat(user, id) do
387 |> put_view(ActivityView)
388 |> render("activity.json", %{activity: activity, for: user})
390 _ -> json_reply(conn, 400, Jason.encode!(%{}))
394 def unretweet(%{assigns: %{user: user}} = conn, %{"id" => id}) do
395 with {:ok, activity} <- TwitterAPI.unrepeat(user, id) do
397 |> put_view(ActivityView)
398 |> render("activity.json", %{activity: activity, for: user})
400 _ -> json_reply(conn, 400, Jason.encode!(%{}))
404 def pin(%{assigns: %{user: user}} = conn, %{"id" => id}) do
405 with {:ok, activity} <- TwitterAPI.pin(user, id) do
407 |> put_view(ActivityView)
408 |> render("activity.json", %{activity: activity, for: user})
410 {:error, message} -> bad_request_reply(conn, message)
415 def unpin(%{assigns: %{user: user}} = conn, %{"id" => id}) do
416 with {:ok, activity} <- TwitterAPI.unpin(user, id) do
418 |> put_view(ActivityView)
419 |> render("activity.json", %{activity: activity, for: user})
421 {:error, message} -> bad_request_reply(conn, message)
426 def register(conn, params) do
427 with {:ok, user} <- TwitterAPI.register_user(params) do
429 |> put_view(UserView)
430 |> render("show.json", %{user: user})
434 |> json_reply(400, Jason.encode!(errors))
438 def password_reset(conn, params) do
439 nickname_or_email = params["email"] || params["nickname"]
441 with {:ok, _} <- TwitterAPI.password_reset(nickname_or_email) do
442 json_response(conn, :no_content, "")
446 def confirm_email(conn, %{"user_id" => uid, "token" => token}) do
447 with %User{} = user <- Repo.get(User, uid),
449 true <- user.info.confirmation_pending,
450 true <- user.info.confirmation_token == token,
451 info_change <- User.Info.confirmation_changeset(user.info, :confirmed),
452 changeset <- Changeset.change(user) |> Changeset.put_embed(:info, info_change),
453 {:ok, _} <- User.update_and_set_cache(changeset) do
459 def resend_confirmation_email(conn, params) do
460 nickname_or_email = params["email"] || params["nickname"]
462 with %User{} = user <- User.get_by_nickname_or_email(nickname_or_email),
463 {:ok, _} <- User.try_send_confirmation_email(user) do
465 |> json_response(:no_content, "")
469 def update_avatar(%{assigns: %{user: user}} = conn, params) do
470 {:ok, object} = ActivityPub.upload(params, type: :avatar)
471 change = Changeset.change(user, %{avatar: object.data})
472 {:ok, user} = User.update_and_set_cache(change)
473 CommonAPI.update(user)
476 |> put_view(UserView)
477 |> render("show.json", %{user: user, for: user})
480 def update_banner(%{assigns: %{user: user}} = conn, params) do
481 with {:ok, object} <- ActivityPub.upload(%{"img" => params["banner"]}, type: :banner),
482 new_info <- %{"banner" => object.data},
483 info_cng <- User.Info.profile_update(user.info, new_info),
484 changeset <- Ecto.Changeset.change(user) |> Ecto.Changeset.put_embed(:info, info_cng),
485 {:ok, user} <- User.update_and_set_cache(changeset) do
486 CommonAPI.update(user)
487 %{"url" => [%{"href" => href} | _]} = object.data
488 response = %{url: href} |> Jason.encode!()
491 |> json_reply(200, response)
495 def update_background(%{assigns: %{user: user}} = conn, params) do
496 with {:ok, object} <- ActivityPub.upload(params, type: :background),
497 new_info <- %{"background" => object.data},
498 info_cng <- User.Info.profile_update(user.info, new_info),
499 changeset <- Ecto.Changeset.change(user) |> Ecto.Changeset.put_embed(:info, info_cng),
500 {:ok, _user} <- User.update_and_set_cache(changeset) do
501 %{"url" => [%{"href" => href} | _]} = object.data
502 response = %{url: href} |> Jason.encode!()
505 |> json_reply(200, response)
509 def external_profile(%{assigns: %{user: current_user}} = conn, %{"profileurl" => uri}) do
510 with {:ok, user_map} <- TwitterAPI.get_external_profile(current_user, uri),
511 response <- Jason.encode!(user_map) do
513 |> json_reply(200, response)
518 |> json(%{error: "Can't find user"})
522 def followers(%{assigns: %{user: for_user}} = conn, params) do
523 {:ok, page} = Ecto.Type.cast(:integer, params["page"] || 1)
525 with {:ok, user} <- TwitterAPI.get_user(for_user, params),
526 {:ok, followers} <- User.get_followers(user, page) do
529 for_user && user.id == for_user.id -> followers
530 user.info.hide_followers -> []
535 |> put_view(UserView)
536 |> render("index.json", %{users: followers, for: conn.assigns[:user]})
538 _e -> bad_request_reply(conn, "Can't get followers")
542 def friends(%{assigns: %{user: for_user}} = conn, params) do
543 {:ok, page} = Ecto.Type.cast(:integer, params["page"] || 1)
544 {:ok, export} = Ecto.Type.cast(:boolean, params["all"] || false)
546 page = if export, do: nil, else: page
548 with {:ok, user} <- TwitterAPI.get_user(conn.assigns[:user], params),
549 {:ok, friends} <- User.get_friends(user, page) do
552 for_user && user.id == for_user.id -> friends
553 user.info.hide_follows -> []
558 |> put_view(UserView)
559 |> render("index.json", %{users: friends, for: conn.assigns[:user]})
561 _e -> bad_request_reply(conn, "Can't get friends")
565 def oauth_tokens(%{assigns: %{user: user}} = conn, _params) do
566 with oauth_tokens <- Token.get_user_tokens(user) do
568 |> put_view(TokenView)
569 |> render("index.json", %{tokens: oauth_tokens})
573 def revoke_token(%{assigns: %{user: user}} = conn, %{"id" => id} = _params) do
574 Token.delete_user_token(user, id)
576 json_reply(conn, 201, "")
579 def blocks(%{assigns: %{user: user}} = conn, _params) do
580 with blocked_users <- User.blocked_users(user) do
582 |> put_view(UserView)
583 |> render("index.json", %{users: blocked_users, for: user})
587 def friend_requests(conn, params) do
588 with {:ok, user} <- TwitterAPI.get_user(conn.assigns[:user], params),
589 {:ok, friend_requests} <- User.get_follow_requests(user) do
591 |> put_view(UserView)
592 |> render("index.json", %{users: friend_requests, for: conn.assigns[:user]})
594 _e -> bad_request_reply(conn, "Can't get friend requests")
598 def approve_friend_request(conn, %{"user_id" => uid} = _params) do
599 with followed <- conn.assigns[:user],
600 %User{} = follower <- Repo.get(User, uid),
601 {:ok, follower} <- User.maybe_follow(follower, followed),
602 %Activity{} = follow_activity <- Utils.fetch_latest_follow(follower, followed),
603 {:ok, follow_activity} <- Utils.update_follow_state(follow_activity, "accept"),
605 ActivityPub.accept(%{
606 to: [follower.ap_id],
608 object: follow_activity.data["id"],
612 |> put_view(UserView)
613 |> render("show.json", %{user: follower, for: followed})
615 e -> bad_request_reply(conn, "Can't approve user: #{inspect(e)}")
619 def deny_friend_request(conn, %{"user_id" => uid} = _params) do
620 with followed <- conn.assigns[:user],
621 %User{} = follower <- Repo.get(User, uid),
622 %Activity{} = follow_activity <- Utils.fetch_latest_follow(follower, followed),
623 {:ok, follow_activity} <- Utils.update_follow_state(follow_activity, "reject"),
625 ActivityPub.reject(%{
626 to: [follower.ap_id],
628 object: follow_activity.data["id"],
632 |> put_view(UserView)
633 |> render("show.json", %{user: follower, for: followed})
635 e -> bad_request_reply(conn, "Can't deny user: #{inspect(e)}")
639 def friends_ids(%{assigns: %{user: user}} = conn, _params) do
640 with {:ok, friends} <- User.get_friends(user) do
643 |> Enum.map(fn x -> x.id end)
648 _e -> bad_request_reply(conn, "Can't get friends")
652 def empty_array(conn, _params) do
653 json(conn, Jason.encode!([]))
656 def raw_empty_array(conn, _params) do
660 defp build_info_cng(user, params) do
662 ["no_rich_text", "locked", "hide_followers", "hide_follows", "show_role"]
663 |> Enum.reduce(%{}, fn key, res ->
664 if value = params[key] do
665 Map.put(res, key, value == "true")
672 if value = params["default_scope"] do
673 Map.put(info_params, "default_scope", value)
678 User.Info.profile_update(user.info, info_params)
681 defp parse_profile_bio(user, params) do
682 if bio = params["description"] do
683 Map.put(params, "bio", User.parse_bio(bio, user))
689 def update_profile(%{assigns: %{user: user}} = conn, params) do
690 params = parse_profile_bio(user, params)
691 info_cng = build_info_cng(user, params)
693 with changeset <- User.update_changeset(user, params),
694 changeset <- Ecto.Changeset.put_embed(changeset, :info, info_cng),
695 {:ok, user} <- User.update_and_set_cache(changeset) do
696 CommonAPI.update(user)
699 |> put_view(UserView)
700 |> render("user.json", %{user: user, for: user})
703 Logger.debug("Can't update user: #{inspect(error)}")
704 bad_request_reply(conn, "Can't update user")
708 def search(%{assigns: %{user: user}} = conn, %{"q" => _query} = params) do
709 activities = TwitterAPI.search(user, params)
712 |> put_view(ActivityView)
713 |> render("index.json", %{activities: activities, for: user})
716 def search_user(%{assigns: %{user: user}} = conn, %{"query" => query}) do
717 users = User.search(query, resolve: true, for_user: user)
720 |> put_view(UserView)
721 |> render("index.json", %{users: users, for: user})
724 defp bad_request_reply(conn, error_message) do
725 json = error_json(conn, error_message)
726 json_reply(conn, 400, json)
729 defp json_reply(conn, status, json) do
731 |> put_resp_content_type("application/json")
732 |> send_resp(status, json)
735 defp forbidden_json_reply(conn, error_message) do
736 json = error_json(conn, error_message)
737 json_reply(conn, 403, json)
740 def only_if_public_instance(%{assigns: %{user: %User{}}} = conn, _), do: conn
742 def only_if_public_instance(conn, _) do
743 if Keyword.get(Application.get_env(:pleroma, :instance), :public) do
747 |> forbidden_json_reply("Invalid credentials.")
752 defp error_json(conn, error_message) do
753 %{"error" => error_message, "request" => conn.request_path} |> Jason.encode!()
756 def errors(conn, {:param_cast, _}) do
759 |> json("Invalid parameters")
762 def errors(conn, _) do
765 |> json("Something went wrong")