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
182 |> Map.put("type", "Create")
183 |> Map.put("blocking_user", user)
184 |> Map.put("user", user)
185 |> Map.put(:visibility, "direct")
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 <- Repo.get(Activity, 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) <- TwitterAPI.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 = Repo.get(Activity, 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 <- Repo.get(User, 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 <- Repo.get(User, uid),
593 {:ok, follower} <- User.maybe_follow(follower, followed),
594 %Activity{} = follow_activity <- Utils.fetch_latest_follow(follower, followed),
595 {:ok, follow_activity} <- Utils.update_follow_state(follow_activity, "accept"),
597 ActivityPub.accept(%{
598 to: [follower.ap_id],
600 object: follow_activity.data["id"],
604 |> put_view(UserView)
605 |> render("show.json", %{user: follower, for: followed})
607 e -> bad_request_reply(conn, "Can't approve user: #{inspect(e)}")
611 def deny_friend_request(conn, %{"user_id" => uid} = _params) do
612 with followed <- conn.assigns[:user],
613 %User{} = follower <- Repo.get(User, uid),
614 %Activity{} = follow_activity <- Utils.fetch_latest_follow(follower, followed),
615 {:ok, follow_activity} <- Utils.update_follow_state(follow_activity, "reject"),
617 ActivityPub.reject(%{
618 to: [follower.ap_id],
620 object: follow_activity.data["id"],
624 |> put_view(UserView)
625 |> render("show.json", %{user: follower, for: followed})
627 e -> bad_request_reply(conn, "Can't deny user: #{inspect(e)}")
631 def friends_ids(%{assigns: %{user: user}} = conn, _params) do
632 with {:ok, friends} <- User.get_friends(user) do
635 |> Enum.map(fn x -> x.id end)
640 _e -> bad_request_reply(conn, "Can't get friends")
644 def empty_array(conn, _params) do
645 json(conn, Jason.encode!([]))
648 def raw_empty_array(conn, _params) do
652 defp build_info_cng(user, params) do
654 ["no_rich_text", "locked", "hide_followers", "hide_follows", "show_role"]
655 |> Enum.reduce(%{}, fn key, res ->
656 if value = params[key] do
657 Map.put(res, key, value == "true")
664 if value = params["default_scope"] do
665 Map.put(info_params, "default_scope", value)
670 User.Info.profile_update(user.info, info_params)
673 defp parse_profile_bio(user, params) do
674 if bio = params["description"] do
675 Map.put(params, "bio", User.parse_bio(bio, user))
681 def update_profile(%{assigns: %{user: user}} = conn, params) do
682 params = parse_profile_bio(user, params)
683 info_cng = build_info_cng(user, params)
685 with changeset <- User.update_changeset(user, params),
686 changeset <- Ecto.Changeset.put_embed(changeset, :info, info_cng),
687 {:ok, user} <- User.update_and_set_cache(changeset) do
688 CommonAPI.update(user)
691 |> put_view(UserView)
692 |> render("user.json", %{user: user, for: user})
695 Logger.debug("Can't update user: #{inspect(error)}")
696 bad_request_reply(conn, "Can't update user")
700 def search(%{assigns: %{user: user}} = conn, %{"q" => _query} = params) do
701 activities = TwitterAPI.search(user, params)
704 |> put_view(ActivityView)
705 |> render("index.json", %{activities: activities, for: user})
708 def search_user(%{assigns: %{user: user}} = conn, %{"query" => query}) do
709 users = User.search(query, resolve: true, for_user: user)
712 |> put_view(UserView)
713 |> render("index.json", %{users: users, for: user})
716 defp bad_request_reply(conn, error_message) do
717 json = error_json(conn, error_message)
718 json_reply(conn, 400, json)
721 defp json_reply(conn, status, json) do
723 |> put_resp_content_type("application/json")
724 |> send_resp(status, json)
727 defp forbidden_json_reply(conn, error_message) do
728 json = error_json(conn, error_message)
729 json_reply(conn, 403, json)
732 def only_if_public_instance(%{assigns: %{user: %User{}}} = conn, _), do: conn
734 def only_if_public_instance(conn, _) do
735 if Keyword.get(Application.get_env(:pleroma, :instance), :public) do
739 |> forbidden_json_reply("Invalid credentials.")
744 defp error_json(conn, error_message) do
745 %{"error" => error_message, "request" => conn.request_path} |> Jason.encode!()
748 def errors(conn, {:param_cast, _}) do
751 |> json("Invalid parameters")
754 def errors(conn, _) do
757 |> json("Something went wrong")