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.Activity
12 alias Pleroma.Notification
16 alias Pleroma.Web.ActivityPub.ActivityPub
17 alias Pleroma.Web.ActivityPub.Visibility
18 alias Pleroma.Web.CommonAPI
19 alias Pleroma.Web.OAuth.Token
20 alias Pleroma.Web.TwitterAPI.ActivityView
21 alias Pleroma.Web.TwitterAPI.NotificationView
22 alias Pleroma.Web.TwitterAPI.TokenView
23 alias Pleroma.Web.TwitterAPI.TwitterAPI
24 alias Pleroma.Web.TwitterAPI.UserView
28 plug(:only_if_public_instance when action in [:public_timeline, :public_and_external_timeline])
29 action_fallback(:errors)
31 def verify_credentials(%{assigns: %{user: user}} = conn, _params) do
32 token = Phoenix.Token.sign(conn, "user socket", user.id)
36 |> render("show.json", %{user: user, token: token, for: user})
39 def status_update(%{assigns: %{user: user}} = conn, %{"status" => _} = status_data) do
40 with media_ids <- extract_media_ids(status_data),
42 TwitterAPI.create_status(user, Map.put(status_data, "media_ids", media_ids)) do
44 |> json(ActivityView.render("activity.json", activity: activity, for: user))
46 _ -> empty_status_reply(conn)
50 def status_update(conn, _status_data) do
51 empty_status_reply(conn)
54 defp empty_status_reply(conn) do
55 bad_request_reply(conn, "Client must provide a 'status' parameter with a value.")
58 defp extract_media_ids(status_data) do
59 with media_ids when not is_nil(media_ids) <- status_data["media_ids"],
60 split_ids <- String.split(media_ids, ","),
61 clean_ids <- Enum.reject(split_ids, fn id -> String.length(id) == 0 end) do
68 def public_and_external_timeline(%{assigns: %{user: user}} = conn, params) do
71 |> Map.put("type", ["Create", "Announce"])
72 |> Map.put("blocking_user", user)
74 activities = ActivityPub.fetch_public_activities(params)
77 |> put_view(ActivityView)
78 |> render("index.json", %{activities: activities, for: user})
81 def public_timeline(%{assigns: %{user: user}} = conn, params) do
84 |> Map.put("type", ["Create", "Announce"])
85 |> Map.put("local_only", true)
86 |> Map.put("blocking_user", user)
88 activities = ActivityPub.fetch_public_activities(params)
91 |> put_view(ActivityView)
92 |> render("index.json", %{activities: activities, for: user})
95 def friends_timeline(%{assigns: %{user: user}} = conn, params) do
98 |> Map.put("type", ["Create", "Announce", "Follow", "Like"])
99 |> Map.put("blocking_user", user)
100 |> Map.put("user", user)
103 ActivityPub.fetch_activities([user.ap_id | user.following], params)
104 |> ActivityPub.contain_timeline(user)
107 |> put_view(ActivityView)
108 |> render("index.json", %{activities: activities, for: user})
111 def show_user(conn, params) do
112 for_user = conn.assigns.user
114 with {:ok, shown} <- TwitterAPI.get_user(params),
116 User.auth_active?(shown) ||
117 (for_user && (for_user.id == shown.id || User.superuser?(for_user))) do
120 %{user: shown, for: for_user}
126 |> put_view(UserView)
127 |> render("show.json", params)
130 bad_request_reply(conn, msg)
135 |> json(%{error: "Unconfirmed user"})
139 def user_timeline(%{assigns: %{user: user}} = conn, params) do
140 case TwitterAPI.get_user(user, params) do
141 {:ok, target_user} ->
142 # Twitter and ActivityPub use a different name and sense for this parameter.
143 {include_rts, params} = Map.pop(params, "include_rts")
147 x when x == "false" or x == "0" -> Map.put(params, "exclude_reblogs", "true")
151 activities = ActivityPub.fetch_user_activities(target_user, user, params)
154 |> put_view(ActivityView)
155 |> render("index.json", %{activities: activities, for: user})
158 bad_request_reply(conn, msg)
162 def mentions_timeline(%{assigns: %{user: user}} = conn, params) do
165 |> Map.put("type", ["Create", "Announce", "Follow", "Like"])
166 |> Map.put("blocking_user", user)
167 |> Map.put(:visibility, ~w[unlisted public private])
169 activities = ActivityPub.fetch_activities([user.ap_id], params)
172 |> put_view(ActivityView)
173 |> render("index.json", %{activities: activities, for: user})
176 def dm_timeline(%{assigns: %{user: user}} = conn, params) do
179 |> Map.put("type", "Create")
180 |> Map.put("blocking_user", user)
181 |> Map.put("user", user)
182 |> Map.put(:visibility, "direct")
185 ActivityPub.fetch_activities_query([user.ap_id], params)
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} <- CommonAPI.accept_follow_request(follower, followed) do
592 |> put_view(UserView)
593 |> render("show.json", %{user: follower, for: followed})
595 e -> bad_request_reply(conn, "Can't approve user: #{inspect(e)}")
599 def deny_friend_request(conn, %{"user_id" => uid} = _params) do
600 with followed <- conn.assigns[:user],
601 %User{} = follower <- Repo.get(User, uid),
602 {:ok, follower} <- CommonAPI.reject_follow_request(follower, followed) do
604 |> put_view(UserView)
605 |> render("show.json", %{user: follower, for: followed})
607 e -> bad_request_reply(conn, "Can't deny user: #{inspect(e)}")
611 def friends_ids(%{assigns: %{user: user}} = conn, _params) do
612 with {:ok, friends} <- User.get_friends(user) do
615 |> Enum.map(fn x -> x.id end)
620 _e -> bad_request_reply(conn, "Can't get friends")
624 def empty_array(conn, _params) do
625 json(conn, Jason.encode!([]))
628 def raw_empty_array(conn, _params) do
632 defp build_info_cng(user, params) do
634 ["no_rich_text", "locked", "hide_followers", "hide_follows", "show_role"]
635 |> Enum.reduce(%{}, fn key, res ->
636 if value = params[key] do
637 Map.put(res, key, value == "true")
644 if value = params["default_scope"] do
645 Map.put(info_params, "default_scope", value)
650 User.Info.profile_update(user.info, info_params)
653 defp parse_profile_bio(user, params) do
654 if bio = params["description"] do
655 Map.put(params, "bio", User.parse_bio(bio, user))
661 def update_profile(%{assigns: %{user: user}} = conn, params) do
662 params = parse_profile_bio(user, params)
663 info_cng = build_info_cng(user, params)
665 with changeset <- User.update_changeset(user, params),
666 changeset <- Ecto.Changeset.put_embed(changeset, :info, info_cng),
667 {:ok, user} <- User.update_and_set_cache(changeset) do
668 CommonAPI.update(user)
671 |> put_view(UserView)
672 |> render("user.json", %{user: user, for: user})
675 Logger.debug("Can't update user: #{inspect(error)}")
676 bad_request_reply(conn, "Can't update user")
680 def search(%{assigns: %{user: user}} = conn, %{"q" => _query} = params) do
681 activities = TwitterAPI.search(user, params)
684 |> put_view(ActivityView)
685 |> render("index.json", %{activities: activities, for: user})
688 def search_user(%{assigns: %{user: user}} = conn, %{"query" => query}) do
689 users = User.search(query, resolve: true, for_user: user)
692 |> put_view(UserView)
693 |> render("index.json", %{users: users, for: user})
696 defp bad_request_reply(conn, error_message) do
697 json = error_json(conn, error_message)
698 json_reply(conn, 400, json)
701 defp json_reply(conn, status, json) do
703 |> put_resp_content_type("application/json")
704 |> send_resp(status, json)
707 defp forbidden_json_reply(conn, error_message) do
708 json = error_json(conn, error_message)
709 json_reply(conn, 403, json)
712 def only_if_public_instance(%{assigns: %{user: %User{}}} = conn, _), do: conn
714 def only_if_public_instance(conn, _) do
715 if Keyword.get(Application.get_env(:pleroma, :instance), :public) do
719 |> forbidden_json_reply("Invalid credentials.")
724 defp error_json(conn, error_message) do
725 %{"error" => error_message, "request" => conn.request_path} |> Jason.encode!()
728 def errors(conn, {:param_cast, _}) do
731 |> json("Invalid parameters")
734 def errors(conn, _) do
737 |> json("Something went wrong")