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