62cce18dc2bd78a7da18aef9816920ef6c20483c
[akkoma] / lib / pleroma / web / twitter_api / twitter_api_controller.ex
1 # Pleroma: A lightweight social networking server
2 # Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
3 # SPDX-License-Identifier: AGPL-3.0-only
4
5 defmodule Pleroma.Web.TwitterAPI.Controller do
6 use Pleroma.Web, :controller
7
8 import Pleroma.Web.ControllerHelper, only: [json_response: 3]
9
10 alias Ecto.Changeset
11 alias Pleroma.Activity
12 alias Pleroma.Notification
13 alias Pleroma.Object
14 alias Pleroma.Repo
15 alias Pleroma.User
16 alias Pleroma.Web.ActivityPub.ActivityPub
17 alias Pleroma.Web.ActivityPub.Visibility
18 alias Pleroma.Web.CommonAPI
19 alias Pleroma.Web.CommonAPI.Utils
20 alias Pleroma.Web.OAuth.Token
21 alias Pleroma.Web.TwitterAPI.ActivityView
22 alias Pleroma.Web.TwitterAPI.NotificationView
23 alias Pleroma.Web.TwitterAPI.TokenView
24 alias Pleroma.Web.TwitterAPI.TwitterAPI
25 alias Pleroma.Web.TwitterAPI.UserView
26
27 require Logger
28
29 plug(:only_if_public_instance when action in [:public_timeline, :public_and_external_timeline])
30 action_fallback(:errors)
31
32 def verify_credentials(%{assigns: %{user: user}} = conn, _params) do
33 token = Phoenix.Token.sign(conn, "user socket", user.id)
34
35 conn
36 |> put_view(UserView)
37 |> render("show.json", %{user: user, token: token, for: user})
38 end
39
40 def status_update(%{assigns: %{user: user}} = conn, %{"status" => _} = status_data) do
41 with media_ids <- extract_media_ids(status_data),
42 {:ok, activity} <-
43 TwitterAPI.create_status(user, Map.put(status_data, "media_ids", media_ids)) do
44 conn
45 |> json(ActivityView.render("activity.json", activity: activity, for: user))
46 else
47 _ -> empty_status_reply(conn)
48 end
49 end
50
51 def status_update(conn, _status_data) do
52 empty_status_reply(conn)
53 end
54
55 defp empty_status_reply(conn) do
56 bad_request_reply(conn, "Client must provide a 'status' parameter with a value.")
57 end
58
59 defp extract_media_ids(status_data) do
60 with media_ids when not is_nil(media_ids) <- status_data["media_ids"],
61 split_ids <- String.split(media_ids, ","),
62 clean_ids <- Enum.reject(split_ids, fn id -> String.length(id) == 0 end) do
63 clean_ids
64 else
65 _e -> []
66 end
67 end
68
69 def public_and_external_timeline(%{assigns: %{user: user}} = conn, params) do
70 params =
71 params
72 |> Map.put("type", ["Create", "Announce"])
73 |> Map.put("blocking_user", user)
74
75 activities = ActivityPub.fetch_public_activities(params)
76
77 conn
78 |> put_view(ActivityView)
79 |> render("index.json", %{activities: activities, for: user})
80 end
81
82 def public_timeline(%{assigns: %{user: user}} = conn, params) do
83 params =
84 params
85 |> Map.put("type", ["Create", "Announce"])
86 |> Map.put("local_only", true)
87 |> Map.put("blocking_user", user)
88
89 activities = ActivityPub.fetch_public_activities(params)
90
91 conn
92 |> put_view(ActivityView)
93 |> render("index.json", %{activities: activities, for: user})
94 end
95
96 def friends_timeline(%{assigns: %{user: user}} = conn, params) do
97 params =
98 params
99 |> Map.put("type", ["Create", "Announce", "Follow", "Like"])
100 |> Map.put("blocking_user", user)
101 |> Map.put("user", user)
102
103 activities =
104 ActivityPub.fetch_activities([user.ap_id | user.following], params)
105 |> ActivityPub.contain_timeline(user)
106
107 conn
108 |> put_view(ActivityView)
109 |> render("index.json", %{activities: activities, for: user})
110 end
111
112 def show_user(conn, params) do
113 for_user = conn.assigns.user
114
115 with {:ok, shown} <- TwitterAPI.get_user(params),
116 true <-
117 User.auth_active?(shown) ||
118 (for_user && (for_user.id == shown.id || User.superuser?(for_user))) do
119 params =
120 if for_user do
121 %{user: shown, for: for_user}
122 else
123 %{user: shown}
124 end
125
126 conn
127 |> put_view(UserView)
128 |> render("show.json", params)
129 else
130 {:error, msg} ->
131 bad_request_reply(conn, msg)
132
133 false ->
134 conn
135 |> put_status(404)
136 |> json(%{error: "Unconfirmed user"})
137 end
138 end
139
140 def user_timeline(%{assigns: %{user: user}} = conn, params) do
141 case TwitterAPI.get_user(user, params) do
142 {:ok, target_user} ->
143 # Twitter and ActivityPub use a different name and sense for this parameter.
144 {include_rts, params} = Map.pop(params, "include_rts")
145
146 params =
147 case include_rts do
148 x when x == "false" or x == "0" -> Map.put(params, "exclude_reblogs", "true")
149 _ -> params
150 end
151
152 activities = ActivityPub.fetch_user_activities(target_user, user, params)
153
154 conn
155 |> put_view(ActivityView)
156 |> render("index.json", %{activities: activities, for: user})
157
158 {:error, msg} ->
159 bad_request_reply(conn, msg)
160 end
161 end
162
163 def mentions_timeline(%{assigns: %{user: user}} = conn, params) do
164 params =
165 params
166 |> Map.put("type", ["Create", "Announce", "Follow", "Like"])
167 |> Map.put("blocking_user", user)
168 |> Map.put(:visibility, ~w[unlisted public private])
169
170 activities = ActivityPub.fetch_activities([user.ap_id], params)
171
172 conn
173 |> put_view(ActivityView)
174 |> render("index.json", %{activities: activities, for: user})
175 end
176
177 def dm_timeline(%{assigns: %{user: user}} = conn, params) do
178 params =
179 params
180 |> Map.put("type", "Create")
181 |> Map.put("blocking_user", user)
182 |> Map.put("user", user)
183 |> Map.put(:visibility, "direct")
184
185 activities =
186 ActivityPub.fetch_activities_query([user.ap_id], params)
187 |> Repo.all()
188
189 conn
190 |> put_view(ActivityView)
191 |> render("index.json", %{activities: activities, for: user})
192 end
193
194 def notifications(%{assigns: %{user: user}} = conn, params) do
195 notifications = Notification.for_user(user, params)
196
197 conn
198 |> put_view(NotificationView)
199 |> render("notification.json", %{notifications: notifications, for: user})
200 end
201
202 def notifications_read(%{assigns: %{user: user}} = conn, %{"latest_id" => latest_id} = params) do
203 Notification.set_read_up_to(user, latest_id)
204
205 notifications = Notification.for_user(user, params)
206
207 conn
208 |> put_view(NotificationView)
209 |> render("notification.json", %{notifications: notifications, for: user})
210 end
211
212 def notifications_read(%{assigns: %{user: _user}} = conn, _) do
213 bad_request_reply(conn, "You need to specify latest_id")
214 end
215
216 def follow(%{assigns: %{user: user}} = conn, params) do
217 case TwitterAPI.follow(user, params) do
218 {:ok, user, followed, _activity} ->
219 conn
220 |> put_view(UserView)
221 |> render("show.json", %{user: followed, for: user})
222
223 {:error, msg} ->
224 forbidden_json_reply(conn, msg)
225 end
226 end
227
228 def block(%{assigns: %{user: user}} = conn, params) do
229 case TwitterAPI.block(user, params) do
230 {:ok, user, blocked} ->
231 conn
232 |> put_view(UserView)
233 |> render("show.json", %{user: blocked, for: user})
234
235 {:error, msg} ->
236 forbidden_json_reply(conn, msg)
237 end
238 end
239
240 def unblock(%{assigns: %{user: user}} = conn, params) do
241 case TwitterAPI.unblock(user, params) do
242 {:ok, user, blocked} ->
243 conn
244 |> put_view(UserView)
245 |> render("show.json", %{user: blocked, for: user})
246
247 {:error, msg} ->
248 forbidden_json_reply(conn, msg)
249 end
250 end
251
252 def delete_post(%{assigns: %{user: user}} = conn, %{"id" => id}) do
253 with {:ok, activity} <- TwitterAPI.delete(user, id) do
254 conn
255 |> put_view(ActivityView)
256 |> render("activity.json", %{activity: activity, for: user})
257 end
258 end
259
260 def unfollow(%{assigns: %{user: user}} = conn, params) do
261 case TwitterAPI.unfollow(user, params) do
262 {:ok, user, unfollowed} ->
263 conn
264 |> put_view(UserView)
265 |> render("show.json", %{user: unfollowed, for: user})
266
267 {:error, msg} ->
268 forbidden_json_reply(conn, msg)
269 end
270 end
271
272 def fetch_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
273 with %Activity{} = activity <- Repo.get(Activity, id),
274 true <- Visibility.visible_for_user?(activity, user) do
275 conn
276 |> put_view(ActivityView)
277 |> render("activity.json", %{activity: activity, for: user})
278 end
279 end
280
281 def fetch_conversation(%{assigns: %{user: user}} = conn, %{"id" => id}) do
282 with context when is_binary(context) <- Utils.conversation_id_to_context(id),
283 activities <-
284 ActivityPub.fetch_activities_for_context(context, %{
285 "blocking_user" => user,
286 "user" => user
287 }) do
288 conn
289 |> put_view(ActivityView)
290 |> render("index.json", %{activities: activities, for: user})
291 end
292 end
293
294 @doc """
295 Updates metadata of uploaded media object.
296 Derived from [Twitter API endpoint](https://developer.twitter.com/en/docs/media/upload-media/api-reference/post-media-metadata-create).
297 """
298 def update_media(%{assigns: %{user: user}} = conn, %{"media_id" => id} = data) do
299 object = Repo.get(Object, id)
300 description = get_in(data, ["alt_text", "text"]) || data["name"] || data["description"]
301
302 {conn, status, response_body} =
303 cond do
304 !object ->
305 {halt(conn), :not_found, ""}
306
307 !Object.authorize_mutation(object, user) ->
308 {halt(conn), :forbidden, "You can only update your own uploads."}
309
310 !is_binary(description) ->
311 {conn, :not_modified, ""}
312
313 true ->
314 new_data = Map.put(object.data, "name", description)
315
316 {:ok, _} =
317 object
318 |> Object.change(%{data: new_data})
319 |> Repo.update()
320
321 {conn, :no_content, ""}
322 end
323
324 conn
325 |> put_status(status)
326 |> json(response_body)
327 end
328
329 def upload(%{assigns: %{user: user}} = conn, %{"media" => media}) do
330 response = TwitterAPI.upload(media, user)
331
332 conn
333 |> put_resp_content_type("application/atom+xml")
334 |> send_resp(200, response)
335 end
336
337 def upload_json(%{assigns: %{user: user}} = conn, %{"media" => media}) do
338 response = TwitterAPI.upload(media, user, "json")
339
340 conn
341 |> json_reply(200, response)
342 end
343
344 def get_by_id_or_ap_id(id) do
345 activity = Repo.get(Activity, id) || Activity.get_create_by_object_ap_id(id)
346
347 if activity.data["type"] == "Create" do
348 activity
349 else
350 Activity.get_create_by_object_ap_id(activity.data["object"])
351 end
352 end
353
354 def favorite(%{assigns: %{user: user}} = conn, %{"id" => id}) do
355 with {:ok, activity} <- TwitterAPI.fav(user, id) do
356 conn
357 |> put_view(ActivityView)
358 |> render("activity.json", %{activity: activity, for: user})
359 else
360 _ -> json_reply(conn, 400, Jason.encode!(%{}))
361 end
362 end
363
364 def unfavorite(%{assigns: %{user: user}} = conn, %{"id" => id}) do
365 with {:ok, activity} <- TwitterAPI.unfav(user, id) do
366 conn
367 |> put_view(ActivityView)
368 |> render("activity.json", %{activity: activity, for: user})
369 else
370 _ -> json_reply(conn, 400, Jason.encode!(%{}))
371 end
372 end
373
374 def retweet(%{assigns: %{user: user}} = conn, %{"id" => id}) do
375 with {:ok, activity} <- TwitterAPI.repeat(user, id) do
376 conn
377 |> put_view(ActivityView)
378 |> render("activity.json", %{activity: activity, for: user})
379 else
380 _ -> json_reply(conn, 400, Jason.encode!(%{}))
381 end
382 end
383
384 def unretweet(%{assigns: %{user: user}} = conn, %{"id" => id}) do
385 with {:ok, activity} <- TwitterAPI.unrepeat(user, id) do
386 conn
387 |> put_view(ActivityView)
388 |> render("activity.json", %{activity: activity, for: user})
389 else
390 _ -> json_reply(conn, 400, Jason.encode!(%{}))
391 end
392 end
393
394 def pin(%{assigns: %{user: user}} = conn, %{"id" => id}) do
395 with {:ok, activity} <- TwitterAPI.pin(user, id) do
396 conn
397 |> put_view(ActivityView)
398 |> render("activity.json", %{activity: activity, for: user})
399 else
400 {:error, message} -> bad_request_reply(conn, message)
401 err -> err
402 end
403 end
404
405 def unpin(%{assigns: %{user: user}} = conn, %{"id" => id}) do
406 with {:ok, activity} <- TwitterAPI.unpin(user, id) do
407 conn
408 |> put_view(ActivityView)
409 |> render("activity.json", %{activity: activity, for: user})
410 else
411 {:error, message} -> bad_request_reply(conn, message)
412 err -> err
413 end
414 end
415
416 def register(conn, params) do
417 with {:ok, user} <- TwitterAPI.register_user(params) do
418 conn
419 |> put_view(UserView)
420 |> render("show.json", %{user: user})
421 else
422 {:error, errors} ->
423 conn
424 |> json_reply(400, Jason.encode!(errors))
425 end
426 end
427
428 def password_reset(conn, params) do
429 nickname_or_email = params["email"] || params["nickname"]
430
431 with {:ok, _} <- TwitterAPI.password_reset(nickname_or_email) do
432 json_response(conn, :no_content, "")
433 end
434 end
435
436 def confirm_email(conn, %{"user_id" => uid, "token" => token}) do
437 with %User{} = user <- Repo.get(User, uid),
438 true <- user.local,
439 true <- user.info.confirmation_pending,
440 true <- user.info.confirmation_token == token,
441 info_change <- User.Info.confirmation_changeset(user.info, :confirmed),
442 changeset <- Changeset.change(user) |> Changeset.put_embed(:info, info_change),
443 {:ok, _} <- User.update_and_set_cache(changeset) do
444 conn
445 |> redirect(to: "/")
446 end
447 end
448
449 def resend_confirmation_email(conn, params) do
450 nickname_or_email = params["email"] || params["nickname"]
451
452 with %User{} = user <- User.get_by_nickname_or_email(nickname_or_email),
453 {:ok, _} <- User.try_send_confirmation_email(user) do
454 conn
455 |> json_response(:no_content, "")
456 end
457 end
458
459 def update_avatar(%{assigns: %{user: user}} = conn, params) do
460 {:ok, object} = ActivityPub.upload(params, type: :avatar)
461 change = Changeset.change(user, %{avatar: object.data})
462 {:ok, user} = User.update_and_set_cache(change)
463 CommonAPI.update(user)
464
465 conn
466 |> put_view(UserView)
467 |> render("show.json", %{user: user, for: user})
468 end
469
470 def update_banner(%{assigns: %{user: user}} = conn, params) do
471 with {:ok, object} <- ActivityPub.upload(%{"img" => params["banner"]}, type: :banner),
472 new_info <- %{"banner" => object.data},
473 info_cng <- User.Info.profile_update(user.info, new_info),
474 changeset <- Ecto.Changeset.change(user) |> Ecto.Changeset.put_embed(:info, info_cng),
475 {:ok, user} <- User.update_and_set_cache(changeset) do
476 CommonAPI.update(user)
477 %{"url" => [%{"href" => href} | _]} = object.data
478 response = %{url: href} |> Jason.encode!()
479
480 conn
481 |> json_reply(200, response)
482 end
483 end
484
485 def update_background(%{assigns: %{user: user}} = conn, params) do
486 with {:ok, object} <- ActivityPub.upload(params, type: :background),
487 new_info <- %{"background" => object.data},
488 info_cng <- User.Info.profile_update(user.info, new_info),
489 changeset <- Ecto.Changeset.change(user) |> Ecto.Changeset.put_embed(:info, info_cng),
490 {:ok, _user} <- User.update_and_set_cache(changeset) do
491 %{"url" => [%{"href" => href} | _]} = object.data
492 response = %{url: href} |> Jason.encode!()
493
494 conn
495 |> json_reply(200, response)
496 end
497 end
498
499 def external_profile(%{assigns: %{user: current_user}} = conn, %{"profileurl" => uri}) do
500 with {:ok, user_map} <- TwitterAPI.get_external_profile(current_user, uri),
501 response <- Jason.encode!(user_map) do
502 conn
503 |> json_reply(200, response)
504 else
505 _e ->
506 conn
507 |> put_status(404)
508 |> json(%{error: "Can't find user"})
509 end
510 end
511
512 def followers(%{assigns: %{user: for_user}} = conn, params) do
513 {:ok, page} = Ecto.Type.cast(:integer, params["page"] || 1)
514
515 with {:ok, user} <- TwitterAPI.get_user(for_user, params),
516 {:ok, followers} <- User.get_followers(user, page) do
517 followers =
518 cond do
519 for_user && user.id == for_user.id -> followers
520 user.info.hide_followers -> []
521 true -> followers
522 end
523
524 conn
525 |> put_view(UserView)
526 |> render("index.json", %{users: followers, for: conn.assigns[:user]})
527 else
528 _e -> bad_request_reply(conn, "Can't get followers")
529 end
530 end
531
532 def friends(%{assigns: %{user: for_user}} = conn, params) do
533 {:ok, page} = Ecto.Type.cast(:integer, params["page"] || 1)
534 {:ok, export} = Ecto.Type.cast(:boolean, params["all"] || false)
535
536 page = if export, do: nil, else: page
537
538 with {:ok, user} <- TwitterAPI.get_user(conn.assigns[:user], params),
539 {:ok, friends} <- User.get_friends(user, page) do
540 friends =
541 cond do
542 for_user && user.id == for_user.id -> friends
543 user.info.hide_follows -> []
544 true -> friends
545 end
546
547 conn
548 |> put_view(UserView)
549 |> render("index.json", %{users: friends, for: conn.assigns[:user]})
550 else
551 _e -> bad_request_reply(conn, "Can't get friends")
552 end
553 end
554
555 def oauth_tokens(%{assigns: %{user: user}} = conn, _params) do
556 with oauth_tokens <- Token.get_user_tokens(user) do
557 conn
558 |> put_view(TokenView)
559 |> render("index.json", %{tokens: oauth_tokens})
560 end
561 end
562
563 def revoke_token(%{assigns: %{user: user}} = conn, %{"id" => id} = _params) do
564 Token.delete_user_token(user, id)
565
566 json_reply(conn, 201, "")
567 end
568
569 def blocks(%{assigns: %{user: user}} = conn, _params) do
570 with blocked_users <- User.blocked_users(user) do
571 conn
572 |> put_view(UserView)
573 |> render("index.json", %{users: blocked_users, for: user})
574 end
575 end
576
577 def friend_requests(conn, params) do
578 with {:ok, user} <- TwitterAPI.get_user(conn.assigns[:user], params),
579 {:ok, friend_requests} <- User.get_follow_requests(user) do
580 conn
581 |> put_view(UserView)
582 |> render("index.json", %{users: friend_requests, for: conn.assigns[:user]})
583 else
584 _e -> bad_request_reply(conn, "Can't get friend requests")
585 end
586 end
587
588 def approve_friend_request(conn, %{"user_id" => uid} = _params) do
589 with followed <- conn.assigns[:user],
590 %User{} = follower <- Repo.get(User, uid),
591 {:ok, follower} <- CommonAPI.accept_follow_request(follower, followed) do
592 conn
593 |> put_view(UserView)
594 |> render("show.json", %{user: follower, for: followed})
595 else
596 e -> bad_request_reply(conn, "Can't approve user: #{inspect(e)}")
597 end
598 end
599
600 def deny_friend_request(conn, %{"user_id" => uid} = _params) do
601 with followed <- conn.assigns[:user],
602 %User{} = follower <- Repo.get(User, uid),
603 {:ok, follower} <- CommonAPI.reject_follow_request(follower, followed) do
604 conn
605 |> put_view(UserView)
606 |> render("show.json", %{user: follower, for: followed})
607 else
608 e -> bad_request_reply(conn, "Can't deny user: #{inspect(e)}")
609 end
610 end
611
612 def friends_ids(%{assigns: %{user: user}} = conn, _params) do
613 with {:ok, friends} <- User.get_friends(user) do
614 ids =
615 friends
616 |> Enum.map(fn x -> x.id end)
617 |> Jason.encode!()
618
619 json(conn, ids)
620 else
621 _e -> bad_request_reply(conn, "Can't get friends")
622 end
623 end
624
625 def empty_array(conn, _params) do
626 json(conn, Jason.encode!([]))
627 end
628
629 def raw_empty_array(conn, _params) do
630 json(conn, [])
631 end
632
633 defp build_info_cng(user, params) do
634 info_params =
635 ["no_rich_text", "locked", "hide_followers", "hide_follows", "show_role"]
636 |> Enum.reduce(%{}, fn key, res ->
637 if value = params[key] do
638 Map.put(res, key, value == "true")
639 else
640 res
641 end
642 end)
643
644 info_params =
645 if value = params["default_scope"] do
646 Map.put(info_params, "default_scope", value)
647 else
648 info_params
649 end
650
651 User.Info.profile_update(user.info, info_params)
652 end
653
654 defp parse_profile_bio(user, params) do
655 if bio = params["description"] do
656 Map.put(params, "bio", User.parse_bio(bio, user))
657 else
658 params
659 end
660 end
661
662 def update_profile(%{assigns: %{user: user}} = conn, params) do
663 params = parse_profile_bio(user, params)
664 info_cng = build_info_cng(user, params)
665
666 with changeset <- User.update_changeset(user, params),
667 changeset <- Ecto.Changeset.put_embed(changeset, :info, info_cng),
668 {:ok, user} <- User.update_and_set_cache(changeset) do
669 CommonAPI.update(user)
670
671 conn
672 |> put_view(UserView)
673 |> render("user.json", %{user: user, for: user})
674 else
675 error ->
676 Logger.debug("Can't update user: #{inspect(error)}")
677 bad_request_reply(conn, "Can't update user")
678 end
679 end
680
681 def search(%{assigns: %{user: user}} = conn, %{"q" => _query} = params) do
682 activities = TwitterAPI.search(user, params)
683
684 conn
685 |> put_view(ActivityView)
686 |> render("index.json", %{activities: activities, for: user})
687 end
688
689 def search_user(%{assigns: %{user: user}} = conn, %{"query" => query}) do
690 users = User.search(query, resolve: true, for_user: user)
691
692 conn
693 |> put_view(UserView)
694 |> render("index.json", %{users: users, for: user})
695 end
696
697 defp bad_request_reply(conn, error_message) do
698 json = error_json(conn, error_message)
699 json_reply(conn, 400, json)
700 end
701
702 defp json_reply(conn, status, json) do
703 conn
704 |> put_resp_content_type("application/json")
705 |> send_resp(status, json)
706 end
707
708 defp forbidden_json_reply(conn, error_message) do
709 json = error_json(conn, error_message)
710 json_reply(conn, 403, json)
711 end
712
713 def only_if_public_instance(%{assigns: %{user: %User{}}} = conn, _), do: conn
714
715 def only_if_public_instance(conn, _) do
716 if Keyword.get(Application.get_env(:pleroma, :instance), :public) do
717 conn
718 else
719 conn
720 |> forbidden_json_reply("Invalid credentials.")
721 |> halt()
722 end
723 end
724
725 defp error_json(conn, error_message) do
726 %{"error" => error_message, "request" => conn.request_path} |> Jason.encode!()
727 end
728
729 def errors(conn, {:param_cast, _}) do
730 conn
731 |> put_status(400)
732 |> json("Invalid parameters")
733 end
734
735 def errors(conn, _) do
736 conn
737 |> put_status(500)
738 |> json("Something went wrong")
739 end
740 end