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