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.Utils
17 alias Pleroma.Web.CommonAPI
18 alias Pleroma.Web.TwitterAPI.ActivityView
19 alias Pleroma.Web.TwitterAPI.NotificationView
20 alias Pleroma.Web.TwitterAPI.TwitterAPI
21 alias Pleroma.Web.TwitterAPI.UserView
22 alias Pleroma.Activity
24 alias Pleroma.Notification
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)
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
179 ActivityPub.fetch_activities_query(
181 Map.merge(params, %{"type" => "Create", "user" => user, visibility: "direct"})
184 activities = Repo.all(query)
187 |> put_view(ActivityView)
188 |> render("index.json", %{activities: activities, for: user})
191 def notifications(%{assigns: %{user: user}} = conn, params) do
192 notifications = Notification.for_user(user, params)
195 |> put_view(NotificationView)
196 |> render("notification.json", %{notifications: notifications, for: user})
199 def notifications_read(%{assigns: %{user: user}} = conn, %{"latest_id" => latest_id} = params) do
200 Notification.set_read_up_to(user, latest_id)
202 notifications = Notification.for_user(user, params)
205 |> put_view(NotificationView)
206 |> render("notification.json", %{notifications: notifications, for: user})
209 def notifications_read(%{assigns: %{user: _user}} = conn, _) do
210 bad_request_reply(conn, "You need to specify latest_id")
213 def follow(%{assigns: %{user: user}} = conn, params) do
214 case TwitterAPI.follow(user, params) do
215 {:ok, user, followed, _activity} ->
217 |> put_view(UserView)
218 |> render("show.json", %{user: followed, for: user})
221 forbidden_json_reply(conn, msg)
225 def block(%{assigns: %{user: user}} = conn, params) do
226 case TwitterAPI.block(user, params) do
227 {:ok, user, blocked} ->
229 |> put_view(UserView)
230 |> render("show.json", %{user: blocked, for: user})
233 forbidden_json_reply(conn, msg)
237 def unblock(%{assigns: %{user: user}} = conn, params) do
238 case TwitterAPI.unblock(user, params) do
239 {:ok, user, blocked} ->
241 |> put_view(UserView)
242 |> render("show.json", %{user: blocked, for: user})
245 forbidden_json_reply(conn, msg)
249 def delete_post(%{assigns: %{user: user}} = conn, %{"id" => id}) do
250 with {:ok, activity} <- TwitterAPI.delete(user, id) do
252 |> put_view(ActivityView)
253 |> render("activity.json", %{activity: activity, for: user})
257 def unfollow(%{assigns: %{user: user}} = conn, params) do
258 case TwitterAPI.unfollow(user, params) do
259 {:ok, user, unfollowed} ->
261 |> put_view(UserView)
262 |> render("show.json", %{user: unfollowed, for: user})
265 forbidden_json_reply(conn, msg)
269 def fetch_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
270 with %Activity{} = activity <- Repo.get(Activity, id),
271 true <- ActivityPub.visible_for_user?(activity, user) do
273 |> put_view(ActivityView)
274 |> render("activity.json", %{activity: activity, for: user})
278 def fetch_conversation(%{assigns: %{user: user}} = conn, %{"id" => id}) do
279 with context when is_binary(context) <- TwitterAPI.conversation_id_to_context(id),
281 ActivityPub.fetch_activities_for_context(context, %{
282 "blocking_user" => user,
286 |> put_view(ActivityView)
287 |> render("index.json", %{activities: activities, for: user})
292 Updates metadata of uploaded media object.
293 Derived from [Twitter API endpoint](https://developer.twitter.com/en/docs/media/upload-media/api-reference/post-media-metadata-create).
295 def update_media(%{assigns: %{user: user}} = conn, %{"media_id" => id} = data) do
296 object = Repo.get(Object, id)
297 description = get_in(data, ["alt_text", "text"]) || data["name"] || data["description"]
299 {conn, status, response_body} =
302 {halt(conn), :not_found, ""}
304 !Object.authorize_mutation(object, user) ->
305 {halt(conn), :forbidden, "You can only update your own uploads."}
307 !is_binary(description) ->
308 {conn, :not_modified, ""}
311 new_data = Map.put(object.data, "name", description)
315 |> Object.change(%{data: new_data})
318 {conn, :no_content, ""}
322 |> put_status(status)
323 |> json(response_body)
326 def upload(%{assigns: %{user: user}} = conn, %{"media" => media}) do
327 response = TwitterAPI.upload(media, user)
330 |> put_resp_content_type("application/atom+xml")
331 |> send_resp(200, response)
334 def upload_json(%{assigns: %{user: user}} = conn, %{"media" => media}) do
335 response = TwitterAPI.upload(media, user, "json")
338 |> json_reply(200, response)
341 def get_by_id_or_ap_id(id) do
342 activity = Repo.get(Activity, id) || Activity.get_create_by_object_ap_id(id)
344 if activity.data["type"] == "Create" do
347 Activity.get_create_by_object_ap_id(activity.data["object"])
351 def favorite(%{assigns: %{user: user}} = conn, %{"id" => id}) do
352 with {:ok, activity} <- TwitterAPI.fav(user, id) do
354 |> put_view(ActivityView)
355 |> render("activity.json", %{activity: activity, for: user})
357 _ -> json_reply(conn, 400, Jason.encode!(%{}))
361 def unfavorite(%{assigns: %{user: user}} = conn, %{"id" => id}) do
362 with {:ok, activity} <- TwitterAPI.unfav(user, id) do
364 |> put_view(ActivityView)
365 |> render("activity.json", %{activity: activity, for: user})
367 _ -> json_reply(conn, 400, Jason.encode!(%{}))
371 def retweet(%{assigns: %{user: user}} = conn, %{"id" => id}) do
372 with {:ok, activity} <- TwitterAPI.repeat(user, id) do
374 |> put_view(ActivityView)
375 |> render("activity.json", %{activity: activity, for: user})
377 _ -> json_reply(conn, 400, Jason.encode!(%{}))
381 def unretweet(%{assigns: %{user: user}} = conn, %{"id" => id}) do
382 with {:ok, activity} <- TwitterAPI.unrepeat(user, id) do
384 |> put_view(ActivityView)
385 |> render("activity.json", %{activity: activity, for: user})
387 _ -> json_reply(conn, 400, Jason.encode!(%{}))
391 def pin(%{assigns: %{user: user}} = conn, %{"id" => id}) do
392 with {:ok, activity} <- TwitterAPI.pin(user, id) do
394 |> put_view(ActivityView)
395 |> render("activity.json", %{activity: activity, for: user})
397 {:error, message} -> bad_request_reply(conn, message)
402 def unpin(%{assigns: %{user: user}} = conn, %{"id" => id}) do
403 with {:ok, activity} <- TwitterAPI.unpin(user, id) do
405 |> put_view(ActivityView)
406 |> render("activity.json", %{activity: activity, for: user})
408 {:error, message} -> bad_request_reply(conn, message)
413 def register(conn, params) do
414 with {:ok, user} <- TwitterAPI.register_user(params) do
416 |> put_view(UserView)
417 |> render("show.json", %{user: user})
421 |> json_reply(400, Jason.encode!(errors))
425 def password_reset(conn, params) do
426 nickname_or_email = params["email"] || params["nickname"]
428 with {:ok, _} <- TwitterAPI.password_reset(nickname_or_email) do
429 json_response(conn, :no_content, "")
433 def confirm_email(conn, %{"user_id" => uid, "token" => token}) do
434 with %User{} = user <- Repo.get(User, uid),
436 true <- user.info.confirmation_pending,
437 true <- user.info.confirmation_token == token,
438 info_change <- User.Info.confirmation_changeset(user.info, :confirmed),
439 changeset <- Changeset.change(user) |> Changeset.put_embed(:info, info_change),
440 {:ok, _} <- User.update_and_set_cache(changeset) do
446 def resend_confirmation_email(conn, params) do
447 nickname_or_email = params["email"] || params["nickname"]
449 with %User{} = user <- User.get_by_nickname_or_email(nickname_or_email),
450 {:ok, _} <- User.try_send_confirmation_email(user) do
452 |> json_response(:no_content, "")
456 def update_avatar(%{assigns: %{user: user}} = conn, params) do
457 {:ok, object} = ActivityPub.upload(params, type: :avatar)
458 change = Changeset.change(user, %{avatar: object.data})
459 {:ok, user} = User.update_and_set_cache(change)
460 CommonAPI.update(user)
463 |> put_view(UserView)
464 |> render("show.json", %{user: user, for: user})
467 def update_banner(%{assigns: %{user: user}} = conn, params) do
468 with {:ok, object} <- ActivityPub.upload(%{"img" => params["banner"]}, type: :banner),
469 new_info <- %{"banner" => object.data},
470 info_cng <- User.Info.profile_update(user.info, new_info),
471 changeset <- Ecto.Changeset.change(user) |> Ecto.Changeset.put_embed(:info, info_cng),
472 {:ok, user} <- User.update_and_set_cache(changeset) do
473 CommonAPI.update(user)
474 %{"url" => [%{"href" => href} | _]} = object.data
475 response = %{url: href} |> Jason.encode!()
478 |> json_reply(200, response)
482 def update_background(%{assigns: %{user: user}} = conn, params) do
483 with {:ok, object} <- ActivityPub.upload(params, type: :background),
484 new_info <- %{"background" => object.data},
485 info_cng <- User.Info.profile_update(user.info, new_info),
486 changeset <- Ecto.Changeset.change(user) |> Ecto.Changeset.put_embed(:info, info_cng),
487 {:ok, _user} <- User.update_and_set_cache(changeset) do
488 %{"url" => [%{"href" => href} | _]} = object.data
489 response = %{url: href} |> Jason.encode!()
492 |> json_reply(200, response)
496 def external_profile(%{assigns: %{user: current_user}} = conn, %{"profileurl" => uri}) do
497 with {:ok, user_map} <- TwitterAPI.get_external_profile(current_user, uri),
498 response <- Jason.encode!(user_map) do
500 |> json_reply(200, response)
505 |> json(%{error: "Can't find user"})
509 def followers(%{assigns: %{user: for_user}} = conn, params) do
510 {:ok, page} = Ecto.Type.cast(:integer, params["page"] || 1)
512 with {:ok, user} <- TwitterAPI.get_user(for_user, params),
513 {:ok, followers} <- User.get_followers(user, page) do
516 for_user && user.id == for_user.id -> followers
517 user.info.hide_followers -> []
522 |> put_view(UserView)
523 |> render("index.json", %{users: followers, for: conn.assigns[:user]})
525 _e -> bad_request_reply(conn, "Can't get followers")
529 def friends(%{assigns: %{user: for_user}} = conn, params) do
530 {:ok, page} = Ecto.Type.cast(:integer, params["page"] || 1)
531 {:ok, export} = Ecto.Type.cast(:boolean, params["all"] || false)
533 page = if export, do: nil, else: page
535 with {:ok, user} <- TwitterAPI.get_user(conn.assigns[:user], params),
536 {:ok, friends} <- User.get_friends(user, page) do
539 for_user && user.id == for_user.id -> friends
540 user.info.hide_follows -> []
545 |> put_view(UserView)
546 |> render("index.json", %{users: friends, for: conn.assigns[:user]})
548 _e -> bad_request_reply(conn, "Can't get friends")
552 def oauth_tokens(%{assigns: %{user: user}} = conn, _params) do
553 with oauth_tokens <- Token.get_user_tokens(user) do
555 |> put_view(TokenView)
556 |> render("index.json", %{tokens: oauth_tokens})
560 def revoke_token(%{assigns: %{user: user}} = conn, %{"id" => id} = _params) do
561 Token.delete_user_token(user, id)
563 json_reply(conn, 201, "")
566 def blocks(%{assigns: %{user: user}} = conn, _params) do
567 with blocked_users <- User.blocked_users(user) do
569 |> put_view(UserView)
570 |> render("index.json", %{users: blocked_users, for: user})
574 def friend_requests(conn, params) do
575 with {:ok, user} <- TwitterAPI.get_user(conn.assigns[:user], params),
576 {:ok, friend_requests} <- User.get_follow_requests(user) do
578 |> put_view(UserView)
579 |> render("index.json", %{users: friend_requests, for: conn.assigns[:user]})
581 _e -> bad_request_reply(conn, "Can't get friend requests")
585 def approve_friend_request(conn, %{"user_id" => uid} = _params) do
586 with followed <- conn.assigns[:user],
587 %User{} = follower <- Repo.get(User, uid),
588 {:ok, follower} <- User.maybe_follow(follower, followed),
589 %Activity{} = follow_activity <- Utils.fetch_latest_follow(follower, followed),
590 {:ok, follow_activity} <- Utils.update_follow_state(follow_activity, "accept"),
592 ActivityPub.accept(%{
593 to: [follower.ap_id],
595 object: follow_activity.data["id"],
599 |> put_view(UserView)
600 |> render("show.json", %{user: follower, for: followed})
602 e -> bad_request_reply(conn, "Can't approve user: #{inspect(e)}")
606 def deny_friend_request(conn, %{"user_id" => uid} = _params) do
607 with followed <- conn.assigns[:user],
608 %User{} = follower <- Repo.get(User, uid),
609 %Activity{} = follow_activity <- Utils.fetch_latest_follow(follower, followed),
610 {:ok, follow_activity} <- Utils.update_follow_state(follow_activity, "reject"),
612 ActivityPub.reject(%{
613 to: [follower.ap_id],
615 object: follow_activity.data["id"],
619 |> put_view(UserView)
620 |> render("show.json", %{user: follower, for: followed})
622 e -> bad_request_reply(conn, "Can't deny user: #{inspect(e)}")
626 def friends_ids(%{assigns: %{user: user}} = conn, _params) do
627 with {:ok, friends} <- User.get_friends(user) do
630 |> Enum.map(fn x -> x.id end)
635 _e -> bad_request_reply(conn, "Can't get friends")
639 def empty_array(conn, _params) do
640 json(conn, Jason.encode!([]))
643 def raw_empty_array(conn, _params) do
647 defp build_info_cng(user, params) do
649 ["no_rich_text", "locked", "hide_followers", "hide_follows", "show_role"]
650 |> Enum.reduce(%{}, fn key, res ->
651 if value = params[key] do
652 Map.put(res, key, value == "true")
659 if value = params["default_scope"] do
660 Map.put(info_params, "default_scope", value)
665 User.Info.profile_update(user.info, info_params)
668 defp parse_profile_bio(user, params) do
669 if bio = params["description"] do
670 Map.put(params, "bio", User.parse_bio(bio, user))
676 def update_profile(%{assigns: %{user: user}} = conn, params) do
677 params = parse_profile_bio(user, params)
678 info_cng = build_info_cng(user, params)
680 with changeset <- User.update_changeset(user, params),
681 changeset <- Ecto.Changeset.put_embed(changeset, :info, info_cng),
682 {:ok, user} <- User.update_and_set_cache(changeset) do
683 CommonAPI.update(user)
686 |> put_view(UserView)
687 |> render("user.json", %{user: user, for: user})
690 Logger.debug("Can't update user: #{inspect(error)}")
691 bad_request_reply(conn, "Can't update user")
695 def search(%{assigns: %{user: user}} = conn, %{"q" => _query} = params) do
696 activities = TwitterAPI.search(user, params)
699 |> put_view(ActivityView)
700 |> render("index.json", %{activities: activities, for: user})
703 def search_user(%{assigns: %{user: user}} = conn, %{"query" => query}) do
704 users = User.search(query, true, user)
707 |> put_view(UserView)
708 |> render("index.json", %{users: users, for: user})
711 defp bad_request_reply(conn, error_message) do
712 json = error_json(conn, error_message)
713 json_reply(conn, 400, json)
716 defp json_reply(conn, status, json) do
718 |> put_resp_content_type("application/json")
719 |> send_resp(status, json)
722 defp forbidden_json_reply(conn, error_message) do
723 json = error_json(conn, error_message)
724 json_reply(conn, 403, json)
727 def only_if_public_instance(%{assigns: %{user: %User{}}} = conn, _), do: conn
729 def only_if_public_instance(conn, _) do
730 if Keyword.get(Application.get_env(:pleroma, :instance), :public) do
734 |> forbidden_json_reply("Invalid credentials.")
739 defp error_json(conn, error_message) do
740 %{"error" => error_message, "request" => conn.request_path} |> Jason.encode!()
743 def errors(conn, {:param_cast, _}) do
746 |> json("Invalid parameters")
749 def errors(conn, _) do
752 |> json("Something went wrong")