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)
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
180 ActivityPub.fetch_activities_query(
182 Map.merge(params, %{"type" => "Create", "user" => user, visibility: "direct"})
185 activities = Repo.all(query)
188 |> put_view(ActivityView)
189 |> render("index.json", %{activities: activities, for: user})
192 def notifications(%{assigns: %{user: user}} = conn, params) do
193 notifications = Notification.for_user(user, params)
196 |> put_view(NotificationView)
197 |> render("notification.json", %{notifications: notifications, for: user})
200 def notifications_read(%{assigns: %{user: user}} = conn, %{"latest_id" => latest_id} = params) do
201 Notification.set_read_up_to(user, latest_id)
203 notifications = Notification.for_user(user, params)
206 |> put_view(NotificationView)
207 |> render("notification.json", %{notifications: notifications, for: user})
210 def notifications_read(%{assigns: %{user: _user}} = conn, _) do
211 bad_request_reply(conn, "You need to specify latest_id")
214 def follow(%{assigns: %{user: user}} = conn, params) do
215 case TwitterAPI.follow(user, params) do
216 {:ok, user, followed, _activity} ->
218 |> put_view(UserView)
219 |> render("show.json", %{user: followed, for: user})
222 forbidden_json_reply(conn, msg)
226 def block(%{assigns: %{user: user}} = conn, params) do
227 case TwitterAPI.block(user, params) do
228 {:ok, user, blocked} ->
230 |> put_view(UserView)
231 |> render("show.json", %{user: blocked, for: user})
234 forbidden_json_reply(conn, msg)
238 def unblock(%{assigns: %{user: user}} = conn, params) do
239 case TwitterAPI.unblock(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 delete_post(%{assigns: %{user: user}} = conn, %{"id" => id}) do
251 with {:ok, activity} <- TwitterAPI.delete(user, id) do
253 |> put_view(ActivityView)
254 |> render("activity.json", %{activity: activity, for: user})
258 def unfollow(%{assigns: %{user: user}} = conn, params) do
259 case TwitterAPI.unfollow(user, params) do
260 {:ok, user, unfollowed} ->
262 |> put_view(UserView)
263 |> render("show.json", %{user: unfollowed, for: user})
266 forbidden_json_reply(conn, msg)
270 def fetch_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
271 with %Activity{} = activity <- Repo.get(Activity, id),
272 true <- Visibility.visible_for_user?(activity, user) do
274 |> put_view(ActivityView)
275 |> render("activity.json", %{activity: activity, for: user})
279 def fetch_conversation(%{assigns: %{user: user}} = conn, %{"id" => id}) do
280 with context when is_binary(context) <- TwitterAPI.conversation_id_to_context(id),
282 ActivityPub.fetch_activities_for_context(context, %{
283 "blocking_user" => user,
287 |> put_view(ActivityView)
288 |> render("index.json", %{activities: activities, for: user})
293 Updates metadata of uploaded media object.
294 Derived from [Twitter API endpoint](https://developer.twitter.com/en/docs/media/upload-media/api-reference/post-media-metadata-create).
296 def update_media(%{assigns: %{user: user}} = conn, %{"media_id" => id} = data) do
297 object = Repo.get(Object, id)
298 description = get_in(data, ["alt_text", "text"]) || data["name"] || data["description"]
300 {conn, status, response_body} =
303 {halt(conn), :not_found, ""}
305 !Object.authorize_mutation(object, user) ->
306 {halt(conn), :forbidden, "You can only update your own uploads."}
308 !is_binary(description) ->
309 {conn, :not_modified, ""}
312 new_data = Map.put(object.data, "name", description)
316 |> Object.change(%{data: new_data})
319 {conn, :no_content, ""}
323 |> put_status(status)
324 |> json(response_body)
327 def upload(%{assigns: %{user: user}} = conn, %{"media" => media}) do
328 response = TwitterAPI.upload(media, user)
331 |> put_resp_content_type("application/atom+xml")
332 |> send_resp(200, response)
335 def upload_json(%{assigns: %{user: user}} = conn, %{"media" => media}) do
336 response = TwitterAPI.upload(media, user, "json")
339 |> json_reply(200, response)
342 def get_by_id_or_ap_id(id) do
343 activity = Repo.get(Activity, id) || Activity.get_create_by_object_ap_id(id)
345 if activity.data["type"] == "Create" do
348 Activity.get_create_by_object_ap_id(activity.data["object"])
352 def favorite(%{assigns: %{user: user}} = conn, %{"id" => id}) do
353 with {:ok, activity} <- TwitterAPI.fav(user, id) do
355 |> put_view(ActivityView)
356 |> render("activity.json", %{activity: activity, for: user})
358 _ -> json_reply(conn, 400, Jason.encode!(%{}))
362 def unfavorite(%{assigns: %{user: user}} = conn, %{"id" => id}) do
363 with {:ok, activity} <- TwitterAPI.unfav(user, id) do
365 |> put_view(ActivityView)
366 |> render("activity.json", %{activity: activity, for: user})
368 _ -> json_reply(conn, 400, Jason.encode!(%{}))
372 def retweet(%{assigns: %{user: user}} = conn, %{"id" => id}) do
373 with {:ok, activity} <- TwitterAPI.repeat(user, id) do
375 |> put_view(ActivityView)
376 |> render("activity.json", %{activity: activity, for: user})
378 _ -> json_reply(conn, 400, Jason.encode!(%{}))
382 def unretweet(%{assigns: %{user: user}} = conn, %{"id" => id}) do
383 with {:ok, activity} <- TwitterAPI.unrepeat(user, id) do
385 |> put_view(ActivityView)
386 |> render("activity.json", %{activity: activity, for: user})
388 _ -> json_reply(conn, 400, Jason.encode!(%{}))
392 def pin(%{assigns: %{user: user}} = conn, %{"id" => id}) do
393 with {:ok, activity} <- TwitterAPI.pin(user, id) do
395 |> put_view(ActivityView)
396 |> render("activity.json", %{activity: activity, for: user})
398 {:error, message} -> bad_request_reply(conn, message)
403 def unpin(%{assigns: %{user: user}} = conn, %{"id" => id}) do
404 with {:ok, activity} <- TwitterAPI.unpin(user, id) do
406 |> put_view(ActivityView)
407 |> render("activity.json", %{activity: activity, for: user})
409 {:error, message} -> bad_request_reply(conn, message)
414 def register(conn, params) do
415 with {:ok, user} <- TwitterAPI.register_user(params) do
417 |> put_view(UserView)
418 |> render("show.json", %{user: user})
422 |> json_reply(400, Jason.encode!(errors))
426 def password_reset(conn, params) do
427 nickname_or_email = params["email"] || params["nickname"]
429 with {:ok, _} <- TwitterAPI.password_reset(nickname_or_email) do
430 json_response(conn, :no_content, "")
434 def confirm_email(conn, %{"user_id" => uid, "token" => token}) do
435 with %User{} = user <- Repo.get(User, uid),
437 true <- user.info.confirmation_pending,
438 true <- user.info.confirmation_token == token,
439 info_change <- User.Info.confirmation_changeset(user.info, :confirmed),
440 changeset <- Changeset.change(user) |> Changeset.put_embed(:info, info_change),
441 {:ok, _} <- User.update_and_set_cache(changeset) do
447 def resend_confirmation_email(conn, params) do
448 nickname_or_email = params["email"] || params["nickname"]
450 with %User{} = user <- User.get_by_nickname_or_email(nickname_or_email),
451 {:ok, _} <- User.try_send_confirmation_email(user) do
453 |> json_response(:no_content, "")
457 def update_avatar(%{assigns: %{user: user}} = conn, params) do
458 {:ok, object} = ActivityPub.upload(params, type: :avatar)
459 change = Changeset.change(user, %{avatar: object.data})
460 {:ok, user} = User.update_and_set_cache(change)
461 CommonAPI.update(user)
464 |> put_view(UserView)
465 |> render("show.json", %{user: user, for: user})
468 def update_banner(%{assigns: %{user: user}} = conn, params) do
469 with {:ok, object} <- ActivityPub.upload(%{"img" => params["banner"]}, type: :banner),
470 new_info <- %{"banner" => object.data},
471 info_cng <- User.Info.profile_update(user.info, new_info),
472 changeset <- Ecto.Changeset.change(user) |> Ecto.Changeset.put_embed(:info, info_cng),
473 {:ok, user} <- User.update_and_set_cache(changeset) do
474 CommonAPI.update(user)
475 %{"url" => [%{"href" => href} | _]} = object.data
476 response = %{url: href} |> Jason.encode!()
479 |> json_reply(200, response)
483 def update_background(%{assigns: %{user: user}} = conn, params) do
484 with {:ok, object} <- ActivityPub.upload(params, type: :background),
485 new_info <- %{"background" => object.data},
486 info_cng <- User.Info.profile_update(user.info, new_info),
487 changeset <- Ecto.Changeset.change(user) |> Ecto.Changeset.put_embed(:info, info_cng),
488 {:ok, _user} <- User.update_and_set_cache(changeset) do
489 %{"url" => [%{"href" => href} | _]} = object.data
490 response = %{url: href} |> Jason.encode!()
493 |> json_reply(200, response)
497 def external_profile(%{assigns: %{user: current_user}} = conn, %{"profileurl" => uri}) do
498 with {:ok, user_map} <- TwitterAPI.get_external_profile(current_user, uri),
499 response <- Jason.encode!(user_map) do
501 |> json_reply(200, response)
506 |> json(%{error: "Can't find user"})
510 def followers(%{assigns: %{user: for_user}} = conn, params) do
511 {:ok, page} = Ecto.Type.cast(:integer, params["page"] || 1)
513 with {:ok, user} <- TwitterAPI.get_user(for_user, params),
514 {:ok, followers} <- User.get_followers(user, page) do
517 for_user && user.id == for_user.id -> followers
518 user.info.hide_followers -> []
523 |> put_view(UserView)
524 |> render("index.json", %{users: followers, for: conn.assigns[:user]})
526 _e -> bad_request_reply(conn, "Can't get followers")
530 def friends(%{assigns: %{user: for_user}} = conn, params) do
531 {:ok, page} = Ecto.Type.cast(:integer, params["page"] || 1)
532 {:ok, export} = Ecto.Type.cast(:boolean, params["all"] || false)
534 page = if export, do: nil, else: page
536 with {:ok, user} <- TwitterAPI.get_user(conn.assigns[:user], params),
537 {:ok, friends} <- User.get_friends(user, page) do
540 for_user && user.id == for_user.id -> friends
541 user.info.hide_follows -> []
546 |> put_view(UserView)
547 |> render("index.json", %{users: friends, for: conn.assigns[:user]})
549 _e -> bad_request_reply(conn, "Can't get friends")
553 def oauth_tokens(%{assigns: %{user: user}} = conn, _params) do
554 with oauth_tokens <- Token.get_user_tokens(user) do
556 |> put_view(TokenView)
557 |> render("index.json", %{tokens: oauth_tokens})
561 def revoke_token(%{assigns: %{user: user}} = conn, %{"id" => id} = _params) do
562 Token.delete_user_token(user, id)
564 json_reply(conn, 201, "")
567 def blocks(%{assigns: %{user: user}} = conn, _params) do
568 with blocked_users <- User.blocked_users(user) do
570 |> put_view(UserView)
571 |> render("index.json", %{users: blocked_users, for: user})
575 def friend_requests(conn, params) do
576 with {:ok, user} <- TwitterAPI.get_user(conn.assigns[:user], params),
577 {:ok, friend_requests} <- User.get_follow_requests(user) do
579 |> put_view(UserView)
580 |> render("index.json", %{users: friend_requests, for: conn.assigns[:user]})
582 _e -> bad_request_reply(conn, "Can't get friend requests")
586 def approve_friend_request(conn, %{"user_id" => uid} = _params) do
587 with followed <- conn.assigns[:user],
588 %User{} = follower <- Repo.get(User, uid),
589 {:ok, follower} <- User.maybe_follow(follower, followed),
590 %Activity{} = follow_activity <- Utils.fetch_latest_follow(follower, followed),
591 {:ok, follow_activity} <- Utils.update_follow_state(follow_activity, "accept"),
593 ActivityPub.accept(%{
594 to: [follower.ap_id],
596 object: follow_activity.data["id"],
600 |> put_view(UserView)
601 |> render("show.json", %{user: follower, for: followed})
603 e -> bad_request_reply(conn, "Can't approve user: #{inspect(e)}")
607 def deny_friend_request(conn, %{"user_id" => uid} = _params) do
608 with followed <- conn.assigns[:user],
609 %User{} = follower <- Repo.get(User, uid),
610 %Activity{} = follow_activity <- Utils.fetch_latest_follow(follower, followed),
611 {:ok, follow_activity} <- Utils.update_follow_state(follow_activity, "reject"),
613 ActivityPub.reject(%{
614 to: [follower.ap_id],
616 object: follow_activity.data["id"],
620 |> put_view(UserView)
621 |> render("show.json", %{user: follower, for: followed})
623 e -> bad_request_reply(conn, "Can't deny user: #{inspect(e)}")
627 def friends_ids(%{assigns: %{user: user}} = conn, _params) do
628 with {:ok, friends} <- User.get_friends(user) do
631 |> Enum.map(fn x -> x.id end)
636 _e -> bad_request_reply(conn, "Can't get friends")
640 def empty_array(conn, _params) do
641 json(conn, Jason.encode!([]))
644 def raw_empty_array(conn, _params) do
648 defp build_info_cng(user, params) do
650 ["no_rich_text", "locked", "hide_followers", "hide_follows", "show_role"]
651 |> Enum.reduce(%{}, fn key, res ->
652 if value = params[key] do
653 Map.put(res, key, value == "true")
660 if value = params["default_scope"] do
661 Map.put(info_params, "default_scope", value)
666 User.Info.profile_update(user.info, info_params)
669 defp parse_profile_bio(user, params) do
670 if bio = params["description"] do
671 Map.put(params, "bio", User.parse_bio(bio, user))
677 def update_profile(%{assigns: %{user: user}} = conn, params) do
678 params = parse_profile_bio(user, params)
679 info_cng = build_info_cng(user, params)
681 with changeset <- User.update_changeset(user, params),
682 changeset <- Ecto.Changeset.put_embed(changeset, :info, info_cng),
683 {:ok, user} <- User.update_and_set_cache(changeset) do
684 CommonAPI.update(user)
687 |> put_view(UserView)
688 |> render("user.json", %{user: user, for: user})
691 Logger.debug("Can't update user: #{inspect(error)}")
692 bad_request_reply(conn, "Can't update user")
696 def search(%{assigns: %{user: user}} = conn, %{"q" => _query} = params) do
697 activities = TwitterAPI.search(user, params)
700 |> put_view(ActivityView)
701 |> render("index.json", %{activities: activities, for: user})
704 def search_user(%{assigns: %{user: user}} = conn, %{"query" => query}) do
705 users = User.search(query, true, user)
708 |> put_view(UserView)
709 |> render("index.json", %{users: users, for: user})
712 defp bad_request_reply(conn, error_message) do
713 json = error_json(conn, error_message)
714 json_reply(conn, 400, json)
717 defp json_reply(conn, status, json) do
719 |> put_resp_content_type("application/json")
720 |> send_resp(status, json)
723 defp forbidden_json_reply(conn, error_message) do
724 json = error_json(conn, error_message)
725 json_reply(conn, 403, json)
728 def only_if_public_instance(%{assigns: %{user: %User{}}} = conn, _), do: conn
730 def only_if_public_instance(conn, _) do
731 if Keyword.get(Application.get_env(:pleroma, :instance), :public) do
735 |> forbidden_json_reply("Invalid credentials.")
740 defp error_json(conn, error_message) do
741 %{"error" => error_message, "request" => conn.request_path} |> Jason.encode!()
744 def errors(conn, {:param_cast, _}) do
747 |> json("Invalid parameters")
750 def errors(conn, _) do
753 |> json("Something went wrong")