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)
532 with {:ok, user} <- TwitterAPI.get_user(conn.assigns[:user], params),
533 {:ok, friends} <- User.get_friends(user, page) do
536 for_user && user.id == for_user.id -> friends
537 user.info.hide_follows -> []
542 |> put_view(UserView)
543 |> render("index.json", %{users: friends, for: conn.assigns[:user]})
545 _e -> bad_request_reply(conn, "Can't get friends")
549 def oauth_tokens(%{assigns: %{user: user}} = conn, _params) do
550 with oauth_tokens <- Token.get_user_tokens(user) do
552 |> put_view(TokenView)
553 |> render("index.json", %{tokens: oauth_tokens})
557 def blocks(%{assigns: %{user: user}} = conn, _params) do
558 with blocked_users <- User.blocked_users(user) do
560 |> put_view(UserView)
561 |> render("index.json", %{users: blocked_users, for: user})
565 def friend_requests(conn, params) do
566 with {:ok, user} <- TwitterAPI.get_user(conn.assigns[:user], params),
567 {:ok, friend_requests} <- User.get_follow_requests(user) do
569 |> put_view(UserView)
570 |> render("index.json", %{users: friend_requests, for: conn.assigns[:user]})
572 _e -> bad_request_reply(conn, "Can't get friend requests")
576 def approve_friend_request(conn, %{"user_id" => uid} = _params) do
577 with followed <- conn.assigns[:user],
578 %User{} = follower <- Repo.get(User, uid),
579 {:ok, follower} <- User.maybe_follow(follower, followed),
580 %Activity{} = follow_activity <- Utils.fetch_latest_follow(follower, followed),
581 {:ok, follow_activity} <- Utils.update_follow_state(follow_activity, "accept"),
583 ActivityPub.accept(%{
584 to: [follower.ap_id],
585 actor: followed.ap_id,
586 object: follow_activity.data["id"],
590 |> put_view(UserView)
591 |> render("show.json", %{user: follower, for: followed})
593 e -> bad_request_reply(conn, "Can't approve user: #{inspect(e)}")
597 def deny_friend_request(conn, %{"user_id" => uid} = _params) do
598 with followed <- conn.assigns[:user],
599 %User{} = follower <- Repo.get(User, uid),
600 %Activity{} = follow_activity <- Utils.fetch_latest_follow(follower, followed),
601 {:ok, follow_activity} <- Utils.update_follow_state(follow_activity, "reject"),
603 ActivityPub.reject(%{
604 to: [follower.ap_id],
605 actor: followed.ap_id,
606 object: follow_activity.data["id"],
610 |> put_view(UserView)
611 |> render("show.json", %{user: follower, for: followed})
613 e -> bad_request_reply(conn, "Can't deny user: #{inspect(e)}")
617 def friends_ids(%{assigns: %{user: user}} = conn, _params) do
618 with {:ok, friends} <- User.get_friends(user) do
621 |> Enum.map(fn x -> x.id end)
626 _e -> bad_request_reply(conn, "Can't get friends")
630 def empty_array(conn, _params) do
631 json(conn, Jason.encode!([]))
634 def raw_empty_array(conn, _params) do
638 defp build_info_cng(user, params) do
640 ["no_rich_text", "locked", "hide_followers", "hide_follows", "show_role"]
641 |> Enum.reduce(%{}, fn key, res ->
642 if value = params[key] do
643 Map.put(res, key, value == "true")
650 if value = params["default_scope"] do
651 Map.put(info_params, "default_scope", value)
656 User.Info.profile_update(user.info, info_params)
659 defp parse_profile_bio(user, params) do
660 if bio = params["description"] do
661 Map.put(params, "bio", User.parse_bio(bio, user))
667 def update_profile(%{assigns: %{user: user}} = conn, params) do
668 params = parse_profile_bio(user, params)
669 info_cng = build_info_cng(user, params)
671 with changeset <- User.update_changeset(user, params),
672 changeset <- Ecto.Changeset.put_embed(changeset, :info, info_cng),
673 {:ok, user} <- User.update_and_set_cache(changeset) do
674 CommonAPI.update(user)
677 |> put_view(UserView)
678 |> render("user.json", %{user: user, for: user})
681 Logger.debug("Can't update user: #{inspect(error)}")
682 bad_request_reply(conn, "Can't update user")
686 def search(%{assigns: %{user: user}} = conn, %{"q" => _query} = params) do
687 activities = TwitterAPI.search(user, params)
690 |> put_view(ActivityView)
691 |> render("index.json", %{activities: activities, for: user})
694 def search_user(%{assigns: %{user: user}} = conn, %{"query" => query}) do
695 users = User.search(query, true, user)
698 |> put_view(UserView)
699 |> render("index.json", %{users: users, for: user})
702 defp bad_request_reply(conn, error_message) do
703 json = error_json(conn, error_message)
704 json_reply(conn, 400, json)
707 defp json_reply(conn, status, json) do
709 |> put_resp_content_type("application/json")
710 |> send_resp(status, json)
713 defp forbidden_json_reply(conn, error_message) do
714 json = error_json(conn, error_message)
715 json_reply(conn, 403, json)
718 def only_if_public_instance(%{assigns: %{user: %User{}}} = conn, _), do: conn
720 def only_if_public_instance(conn, _) do
721 if Keyword.get(Application.get_env(:pleroma, :instance), :public) do
725 |> forbidden_json_reply("Invalid credentials.")
730 defp error_json(conn, error_message) do
731 %{"error" => error_message, "request" => conn.request_path} |> Jason.encode!()
734 def errors(conn, {:param_cast, _}) do
737 |> json("Invalid parameters")
740 def errors(conn, _) do
743 |> json("Something went wrong")