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.Web.TwitterAPI.{TwitterAPI, UserView, ActivityView, NotificationView, TokenView}
12 alias Pleroma.Web.CommonAPI
13 alias Pleroma.{Repo, Activity, Object, User, Notification}
14 alias Pleroma.Web.OAuth.Token
15 alias Pleroma.Web.ActivityPub.ActivityPub
16 alias Pleroma.Web.ActivityPub.Visibility
17 alias Pleroma.Web.ActivityPub.Utils
18 alias Pleroma.Web.CommonAPI
19 alias Pleroma.Web.TwitterAPI.ActivityView
20 alias Pleroma.Web.TwitterAPI.NotificationView
21 alias Pleroma.Web.TwitterAPI.TwitterAPI
22 alias Pleroma.Web.TwitterAPI.UserView
23 alias Pleroma.Activity
25 alias Pleroma.Notification
31 plug(:only_if_public_instance when action in [:public_timeline, :public_and_external_timeline])
32 action_fallback(:errors)
34 def verify_credentials(%{assigns: %{user: user}} = conn, _params) do
35 token = Phoenix.Token.sign(conn, "user socket", user.id)
39 |> render("show.json", %{user: user, token: token, for: user})
42 def status_update(%{assigns: %{user: user}} = conn, %{"status" => _} = status_data) do
43 with media_ids <- extract_media_ids(status_data),
45 TwitterAPI.create_status(user, Map.put(status_data, "media_ids", media_ids)) do
47 |> json(ActivityView.render("activity.json", activity: activity, for: user))
49 _ -> empty_status_reply(conn)
53 def status_update(conn, _status_data) do
54 empty_status_reply(conn)
57 defp empty_status_reply(conn) do
58 bad_request_reply(conn, "Client must provide a 'status' parameter with a value.")
61 defp extract_media_ids(status_data) do
62 with media_ids when not is_nil(media_ids) <- status_data["media_ids"],
63 split_ids <- String.split(media_ids, ","),
64 clean_ids <- Enum.reject(split_ids, fn id -> String.length(id) == 0 end) do
71 def public_and_external_timeline(%{assigns: %{user: user}} = conn, params) do
74 |> Map.put("type", ["Create", "Announce"])
75 |> Map.put("blocking_user", user)
77 activities = ActivityPub.fetch_public_activities(params)
80 |> put_view(ActivityView)
81 |> render("index.json", %{activities: activities, for: user})
84 def public_timeline(%{assigns: %{user: user}} = conn, params) do
87 |> Map.put("type", ["Create", "Announce"])
88 |> Map.put("local_only", true)
89 |> Map.put("blocking_user", user)
91 activities = ActivityPub.fetch_public_activities(params)
94 |> put_view(ActivityView)
95 |> render("index.json", %{activities: activities, for: user})
98 def friends_timeline(%{assigns: %{user: user}} = conn, params) do
101 |> Map.put("type", ["Create", "Announce", "Follow", "Like"])
102 |> Map.put("blocking_user", user)
103 |> Map.put("user", user)
106 ActivityPub.fetch_activities([user.ap_id | user.following], params)
107 |> ActivityPub.contain_timeline(user)
110 |> put_view(ActivityView)
111 |> render("index.json", %{activities: activities, for: user})
114 def show_user(conn, params) do
115 for_user = conn.assigns.user
117 with {:ok, shown} <- TwitterAPI.get_user(params),
119 User.auth_active?(shown) ||
120 (for_user && (for_user.id == shown.id || User.superuser?(for_user))) do
123 %{user: shown, for: for_user}
129 |> put_view(UserView)
130 |> render("show.json", params)
133 bad_request_reply(conn, msg)
138 |> json(%{error: "Unconfirmed user"})
142 def user_timeline(%{assigns: %{user: user}} = conn, params) do
143 case TwitterAPI.get_user(user, params) do
144 {:ok, target_user} ->
145 # Twitter and ActivityPub use a different name and sense for this parameter.
146 {include_rts, params} = Map.pop(params, "include_rts")
150 x when x == "false" or x == "0" -> Map.put(params, "exclude_reblogs", "true")
154 activities = ActivityPub.fetch_user_activities(target_user, user, params)
157 |> put_view(ActivityView)
158 |> render("index.json", %{activities: activities, for: user})
161 bad_request_reply(conn, msg)
165 def mentions_timeline(%{assigns: %{user: user}} = conn, params) do
168 |> Map.put("type", ["Create", "Announce", "Follow", "Like"])
169 |> Map.put("blocking_user", user)
170 |> Map.put(:visibility, ~w[unlisted public private])
172 activities = ActivityPub.fetch_activities([user.ap_id], params)
175 |> put_view(ActivityView)
176 |> render("index.json", %{activities: activities, for: user})
179 def dm_timeline(%{assigns: %{user: user}} = conn, params) do
181 ActivityPub.fetch_activities_query(
183 Map.merge(params, %{"type" => "Create", "user" => user, visibility: "direct"})
186 activities = Repo.all(query)
189 |> put_view(ActivityView)
190 |> render("index.json", %{activities: activities, for: user})
193 def notifications(%{assigns: %{user: user}} = conn, params) do
194 notifications = Notification.for_user(user, params)
197 |> put_view(NotificationView)
198 |> render("notification.json", %{notifications: notifications, for: user})
201 def notifications_read(%{assigns: %{user: user}} = conn, %{"latest_id" => latest_id} = params) do
202 Notification.set_read_up_to(user, latest_id)
204 notifications = Notification.for_user(user, params)
207 |> put_view(NotificationView)
208 |> render("notification.json", %{notifications: notifications, for: user})
211 def notifications_read(%{assigns: %{user: _user}} = conn, _) do
212 bad_request_reply(conn, "You need to specify latest_id")
215 def follow(%{assigns: %{user: user}} = conn, params) do
216 case TwitterAPI.follow(user, params) do
217 {:ok, user, followed, _activity} ->
219 |> put_view(UserView)
220 |> render("show.json", %{user: followed, for: user})
223 forbidden_json_reply(conn, msg)
227 def block(%{assigns: %{user: user}} = conn, params) do
228 case TwitterAPI.block(user, params) do
229 {:ok, user, blocked} ->
231 |> put_view(UserView)
232 |> render("show.json", %{user: blocked, for: user})
235 forbidden_json_reply(conn, msg)
239 def unblock(%{assigns: %{user: user}} = conn, params) do
240 case TwitterAPI.unblock(user, params) do
241 {:ok, user, blocked} ->
243 |> put_view(UserView)
244 |> render("show.json", %{user: blocked, for: user})
247 forbidden_json_reply(conn, msg)
251 def delete_post(%{assigns: %{user: user}} = conn, %{"id" => id}) do
252 with {:ok, activity} <- TwitterAPI.delete(user, id) do
254 |> put_view(ActivityView)
255 |> render("activity.json", %{activity: activity, for: user})
259 def unfollow(%{assigns: %{user: user}} = conn, params) do
260 case TwitterAPI.unfollow(user, params) do
261 {:ok, user, unfollowed} ->
263 |> put_view(UserView)
264 |> render("show.json", %{user: unfollowed, for: user})
267 forbidden_json_reply(conn, msg)
271 def fetch_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
272 with %Activity{} = activity <- Repo.get(Activity, id),
273 true <- Visibility.visible_for_user?(activity, user) do
275 |> put_view(ActivityView)
276 |> render("activity.json", %{activity: activity, for: user})
280 def fetch_conversation(%{assigns: %{user: user}} = conn, %{"id" => id}) do
281 with context when is_binary(context) <- TwitterAPI.conversation_id_to_context(id),
283 ActivityPub.fetch_activities_for_context(context, %{
284 "blocking_user" => user,
288 |> put_view(ActivityView)
289 |> render("index.json", %{activities: activities, for: user})
294 Updates metadata of uploaded media object.
295 Derived from [Twitter API endpoint](https://developer.twitter.com/en/docs/media/upload-media/api-reference/post-media-metadata-create).
297 def update_media(%{assigns: %{user: user}} = conn, %{"media_id" => id} = data) do
298 object = Repo.get(Object, id)
299 description = get_in(data, ["alt_text", "text"]) || data["name"] || data["description"]
301 {conn, status, response_body} =
304 {halt(conn), :not_found, ""}
306 !Object.authorize_mutation(object, user) ->
307 {halt(conn), :forbidden, "You can only update your own uploads."}
309 !is_binary(description) ->
310 {conn, :not_modified, ""}
313 new_data = Map.put(object.data, "name", description)
317 |> Object.change(%{data: new_data})
320 {conn, :no_content, ""}
324 |> put_status(status)
325 |> json(response_body)
328 def upload(%{assigns: %{user: user}} = conn, %{"media" => media}) do
329 response = TwitterAPI.upload(media, user)
332 |> put_resp_content_type("application/atom+xml")
333 |> send_resp(200, response)
336 def upload_json(%{assigns: %{user: user}} = conn, %{"media" => media}) do
337 response = TwitterAPI.upload(media, user, "json")
340 |> json_reply(200, response)
343 def get_by_id_or_ap_id(id) do
344 activity = Repo.get(Activity, id) || Activity.get_create_by_object_ap_id(id)
346 if activity.data["type"] == "Create" do
349 Activity.get_create_by_object_ap_id(activity.data["object"])
353 def favorite(%{assigns: %{user: user}} = conn, %{"id" => id}) do
354 with {:ok, activity} <- TwitterAPI.fav(user, id) do
356 |> put_view(ActivityView)
357 |> render("activity.json", %{activity: activity, for: user})
359 _ -> json_reply(conn, 400, Jason.encode!(%{}))
363 def unfavorite(%{assigns: %{user: user}} = conn, %{"id" => id}) do
364 with {:ok, activity} <- TwitterAPI.unfav(user, id) do
366 |> put_view(ActivityView)
367 |> render("activity.json", %{activity: activity, for: user})
369 _ -> json_reply(conn, 400, Jason.encode!(%{}))
373 def retweet(%{assigns: %{user: user}} = conn, %{"id" => id}) do
374 with {:ok, activity} <- TwitterAPI.repeat(user, id) do
376 |> put_view(ActivityView)
377 |> render("activity.json", %{activity: activity, for: user})
379 _ -> json_reply(conn, 400, Jason.encode!(%{}))
383 def unretweet(%{assigns: %{user: user}} = conn, %{"id" => id}) do
384 with {:ok, activity} <- TwitterAPI.unrepeat(user, id) do
386 |> put_view(ActivityView)
387 |> render("activity.json", %{activity: activity, for: user})
389 _ -> json_reply(conn, 400, Jason.encode!(%{}))
393 def pin(%{assigns: %{user: user}} = conn, %{"id" => id}) do
394 with {:ok, activity} <- TwitterAPI.pin(user, id) do
396 |> put_view(ActivityView)
397 |> render("activity.json", %{activity: activity, for: user})
399 {:error, message} -> bad_request_reply(conn, message)
404 def unpin(%{assigns: %{user: user}} = conn, %{"id" => id}) do
405 with {:ok, activity} <- TwitterAPI.unpin(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 register(conn, params) do
416 with {:ok, user} <- TwitterAPI.register_user(params) do
418 |> put_view(UserView)
419 |> render("show.json", %{user: user})
423 |> json_reply(400, Jason.encode!(errors))
427 def password_reset(conn, params) do
428 nickname_or_email = params["email"] || params["nickname"]
430 with {:ok, _} <- TwitterAPI.password_reset(nickname_or_email) do
431 json_response(conn, :no_content, "")
435 def confirm_email(conn, %{"user_id" => uid, "token" => token}) do
436 with %User{} = user <- Repo.get(User, uid),
438 true <- user.info.confirmation_pending,
439 true <- user.info.confirmation_token == token,
440 info_change <- User.Info.confirmation_changeset(user.info, :confirmed),
441 changeset <- Changeset.change(user) |> Changeset.put_embed(:info, info_change),
442 {:ok, _} <- User.update_and_set_cache(changeset) do
448 def resend_confirmation_email(conn, params) do
449 nickname_or_email = params["email"] || params["nickname"]
451 with %User{} = user <- User.get_by_nickname_or_email(nickname_or_email),
452 {:ok, _} <- User.try_send_confirmation_email(user) do
454 |> json_response(:no_content, "")
458 def update_avatar(%{assigns: %{user: user}} = conn, params) do
459 {:ok, object} = ActivityPub.upload(params, type: :avatar)
460 change = Changeset.change(user, %{avatar: object.data})
461 {:ok, user} = User.update_and_set_cache(change)
462 CommonAPI.update(user)
465 |> put_view(UserView)
466 |> render("show.json", %{user: user, for: user})
469 def update_banner(%{assigns: %{user: user}} = conn, params) do
470 with {:ok, object} <- ActivityPub.upload(%{"img" => params["banner"]}, type: :banner),
471 new_info <- %{"banner" => object.data},
472 info_cng <- User.Info.profile_update(user.info, new_info),
473 changeset <- Ecto.Changeset.change(user) |> Ecto.Changeset.put_embed(:info, info_cng),
474 {:ok, user} <- User.update_and_set_cache(changeset) do
475 CommonAPI.update(user)
476 %{"url" => [%{"href" => href} | _]} = object.data
477 response = %{url: href} |> Jason.encode!()
480 |> json_reply(200, response)
484 def update_background(%{assigns: %{user: user}} = conn, params) do
485 with {:ok, object} <- ActivityPub.upload(params, type: :background),
486 new_info <- %{"background" => object.data},
487 info_cng <- User.Info.profile_update(user.info, new_info),
488 changeset <- Ecto.Changeset.change(user) |> Ecto.Changeset.put_embed(:info, info_cng),
489 {:ok, _user} <- User.update_and_set_cache(changeset) do
490 %{"url" => [%{"href" => href} | _]} = object.data
491 response = %{url: href} |> Jason.encode!()
494 |> json_reply(200, response)
498 def external_profile(%{assigns: %{user: current_user}} = conn, %{"profileurl" => uri}) do
499 with {:ok, user_map} <- TwitterAPI.get_external_profile(current_user, uri),
500 response <- Jason.encode!(user_map) do
502 |> json_reply(200, response)
507 |> json(%{error: "Can't find user"})
511 def followers(%{assigns: %{user: for_user}} = conn, params) do
512 {:ok, page} = Ecto.Type.cast(:integer, params["page"] || 1)
514 with {:ok, user} <- TwitterAPI.get_user(for_user, params),
515 {:ok, followers} <- User.get_followers(user, page) do
518 for_user && user.id == for_user.id -> followers
519 user.info.hide_followers -> []
524 |> put_view(UserView)
525 |> render("index.json", %{users: followers, for: conn.assigns[:user]})
527 _e -> bad_request_reply(conn, "Can't get followers")
531 def friends(%{assigns: %{user: for_user}} = conn, params) do
532 {:ok, page} = Ecto.Type.cast(:integer, params["page"] || 1)
533 {:ok, export} = Ecto.Type.cast(:boolean, params["all"] || false)
535 page = if export, do: nil, else: page
537 with {:ok, user} <- TwitterAPI.get_user(conn.assigns[:user], params),
538 {:ok, friends} <- User.get_friends(user, page) do
541 for_user && user.id == for_user.id -> friends
542 user.info.hide_follows -> []
547 |> put_view(UserView)
548 |> render("index.json", %{users: friends, for: conn.assigns[:user]})
550 _e -> bad_request_reply(conn, "Can't get friends")
554 def oauth_tokens(%{assigns: %{user: user}} = conn, _params) do
555 with oauth_tokens <- Token.get_user_tokens(user) do
557 |> put_view(TokenView)
558 |> render("index.json", %{tokens: oauth_tokens})
562 def revoke_token(%{assigns: %{user: user}} = conn, %{"id" => id} = _params) do
563 Token.delete_user_token(user, id)
565 json_reply(conn, 201, "")
568 def blocks(%{assigns: %{user: user}} = conn, _params) do
569 with blocked_users <- User.blocked_users(user) do
571 |> put_view(UserView)
572 |> render("index.json", %{users: blocked_users, for: user})
576 def friend_requests(conn, params) do
577 with {:ok, user} <- TwitterAPI.get_user(conn.assigns[:user], params),
578 {:ok, friend_requests} <- User.get_follow_requests(user) do
580 |> put_view(UserView)
581 |> render("index.json", %{users: friend_requests, for: conn.assigns[:user]})
583 _e -> bad_request_reply(conn, "Can't get friend requests")
587 def approve_friend_request(conn, %{"user_id" => uid} = _params) do
588 with followed <- conn.assigns[:user],
589 %User{} = follower <- Repo.get(User, uid),
590 {:ok, follower} <- User.maybe_follow(follower, followed),
591 %Activity{} = follow_activity <- Utils.fetch_latest_follow(follower, followed),
592 {:ok, follow_activity} <- Utils.update_follow_state(follow_activity, "accept"),
594 ActivityPub.accept(%{
595 to: [follower.ap_id],
597 object: follow_activity.data["id"],
601 |> put_view(UserView)
602 |> render("show.json", %{user: follower, for: followed})
604 e -> bad_request_reply(conn, "Can't approve user: #{inspect(e)}")
608 def deny_friend_request(conn, %{"user_id" => uid} = _params) do
609 with followed <- conn.assigns[:user],
610 %User{} = follower <- Repo.get(User, uid),
611 %Activity{} = follow_activity <- Utils.fetch_latest_follow(follower, followed),
612 {:ok, follow_activity} <- Utils.update_follow_state(follow_activity, "reject"),
614 ActivityPub.reject(%{
615 to: [follower.ap_id],
617 object: follow_activity.data["id"],
621 |> put_view(UserView)
622 |> render("show.json", %{user: follower, for: followed})
624 e -> bad_request_reply(conn, "Can't deny user: #{inspect(e)}")
628 def friends_ids(%{assigns: %{user: user}} = conn, _params) do
629 with {:ok, friends} <- User.get_friends(user) do
632 |> Enum.map(fn x -> x.id end)
637 _e -> bad_request_reply(conn, "Can't get friends")
641 def empty_array(conn, _params) do
642 json(conn, Jason.encode!([]))
645 def raw_empty_array(conn, _params) do
649 defp build_info_cng(user, params) do
651 ["no_rich_text", "locked", "hide_followers", "hide_follows", "show_role"]
652 |> Enum.reduce(%{}, fn key, res ->
653 if value = params[key] do
654 Map.put(res, key, value == "true")
661 if value = params["default_scope"] do
662 Map.put(info_params, "default_scope", value)
667 User.Info.profile_update(user.info, info_params)
670 defp parse_profile_bio(user, params) do
671 if bio = params["description"] do
672 Map.put(params, "bio", User.parse_bio(bio, user))
678 def update_profile(%{assigns: %{user: user}} = conn, params) do
679 params = parse_profile_bio(user, params)
680 info_cng = build_info_cng(user, params)
682 with changeset <- User.update_changeset(user, params),
683 changeset <- Ecto.Changeset.put_embed(changeset, :info, info_cng),
684 {:ok, user} <- User.update_and_set_cache(changeset) do
685 CommonAPI.update(user)
688 |> put_view(UserView)
689 |> render("user.json", %{user: user, for: user})
692 Logger.debug("Can't update user: #{inspect(error)}")
693 bad_request_reply(conn, "Can't update user")
697 def search(%{assigns: %{user: user}} = conn, %{"q" => _query} = params) do
698 activities = TwitterAPI.search(user, params)
701 |> put_view(ActivityView)
702 |> render("index.json", %{activities: activities, for: user})
705 def search_user(%{assigns: %{user: user}} = conn, %{"query" => query}) do
706 users = User.search(query, resolve: true, for_user: user)
709 |> put_view(UserView)
710 |> render("index.json", %{users: users, for: user})
713 defp bad_request_reply(conn, error_message) do
714 json = error_json(conn, error_message)
715 json_reply(conn, 400, json)
718 defp json_reply(conn, status, json) do
720 |> put_resp_content_type("application/json")
721 |> send_resp(status, json)
724 defp forbidden_json_reply(conn, error_message) do
725 json = error_json(conn, error_message)
726 json_reply(conn, 403, json)
729 def only_if_public_instance(%{assigns: %{user: %User{}}} = conn, _), do: conn
731 def only_if_public_instance(conn, _) do
732 if Keyword.get(Application.get_env(:pleroma, :instance), :public) do
736 |> forbidden_json_reply("Invalid credentials.")
741 defp error_json(conn, error_message) do
742 %{"error" => error_message, "request" => conn.request_path} |> Jason.encode!()
745 def errors(conn, {:param_cast, _}) do
748 |> json("Invalid parameters")
751 def errors(conn, _) do
754 |> json("Something went wrong")