7e4ee317c24b67d2ba1cf80947c5e216ff3e2c23
[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 {:ok, export} = Ecto.Type.cast(:boolean, params["all"] || false)
528
529 page = if export, do: nil, else: page
530
531 with {:ok, user} <- TwitterAPI.get_user(conn.assigns[:user], params),
532 {:ok, friends} <- User.get_friends(user, page) do
533 friends =
534 cond do
535 for_user && user.id == for_user.id -> friends
536 user.info.hide_follows -> []
537 true -> friends
538 end
539
540 conn
541 |> put_view(UserView)
542 |> render("index.json", %{users: friends, for: conn.assigns[:user]})
543 else
544 _e -> bad_request_reply(conn, "Can't get friends")
545 end
546 end
547
548 def blocks(%{assigns: %{user: user}} = conn, _params) do
549 with blocked_users <- User.blocked_users(user) do
550 conn
551 |> put_view(UserView)
552 |> render("index.json", %{users: blocked_users, for: user})
553 end
554 end
555
556 def friend_requests(conn, params) do
557 with {:ok, user} <- TwitterAPI.get_user(conn.assigns[:user], params),
558 {:ok, friend_requests} <- User.get_follow_requests(user) do
559 conn
560 |> put_view(UserView)
561 |> render("index.json", %{users: friend_requests, for: conn.assigns[:user]})
562 else
563 _e -> bad_request_reply(conn, "Can't get friend requests")
564 end
565 end
566
567 def approve_friend_request(conn, %{"user_id" => uid} = _params) do
568 with followed <- conn.assigns[:user],
569 %User{} = follower <- Repo.get(User, uid),
570 {:ok, follower} <- User.maybe_follow(follower, followed),
571 %Activity{} = follow_activity <- Utils.fetch_latest_follow(follower, followed),
572 {:ok, follow_activity} <- Utils.update_follow_state(follow_activity, "accept"),
573 {:ok, _activity} <-
574 ActivityPub.accept(%{
575 to: [follower.ap_id],
576 actor: followed,
577 object: follow_activity.data["id"],
578 type: "Accept"
579 }) do
580 conn
581 |> put_view(UserView)
582 |> render("show.json", %{user: follower, for: followed})
583 else
584 e -> bad_request_reply(conn, "Can't approve user: #{inspect(e)}")
585 end
586 end
587
588 def deny_friend_request(conn, %{"user_id" => uid} = _params) do
589 with followed <- conn.assigns[:user],
590 %User{} = follower <- Repo.get(User, uid),
591 %Activity{} = follow_activity <- Utils.fetch_latest_follow(follower, followed),
592 {:ok, follow_activity} <- Utils.update_follow_state(follow_activity, "reject"),
593 {:ok, _activity} <-
594 ActivityPub.reject(%{
595 to: [follower.ap_id],
596 actor: followed,
597 object: follow_activity.data["id"],
598 type: "Reject"
599 }) do
600 conn
601 |> put_view(UserView)
602 |> render("show.json", %{user: follower, for: followed})
603 else
604 e -> bad_request_reply(conn, "Can't deny user: #{inspect(e)}")
605 end
606 end
607
608 def friends_ids(%{assigns: %{user: user}} = conn, _params) do
609 with {:ok, friends} <- User.get_friends(user) do
610 ids =
611 friends
612 |> Enum.map(fn x -> x.id end)
613 |> Jason.encode!()
614
615 json(conn, ids)
616 else
617 _e -> bad_request_reply(conn, "Can't get friends")
618 end
619 end
620
621 def empty_array(conn, _params) do
622 json(conn, Jason.encode!([]))
623 end
624
625 def raw_empty_array(conn, _params) do
626 json(conn, [])
627 end
628
629 defp build_info_cng(user, params) do
630 info_params =
631 ["no_rich_text", "locked", "hide_followers", "hide_follows", "show_role"]
632 |> Enum.reduce(%{}, fn key, res ->
633 if value = params[key] do
634 Map.put(res, key, value == "true")
635 else
636 res
637 end
638 end)
639
640 info_params =
641 if value = params["default_scope"] do
642 Map.put(info_params, "default_scope", value)
643 else
644 info_params
645 end
646
647 User.Info.profile_update(user.info, info_params)
648 end
649
650 defp parse_profile_bio(user, params) do
651 if bio = params["description"] do
652 Map.put(params, "bio", User.parse_bio(bio, user))
653 else
654 params
655 end
656 end
657
658 def update_profile(%{assigns: %{user: user}} = conn, params) do
659 params = parse_profile_bio(user, params)
660 info_cng = build_info_cng(user, params)
661
662 with changeset <- User.update_changeset(user, params),
663 changeset <- Ecto.Changeset.put_embed(changeset, :info, info_cng),
664 {:ok, user} <- User.update_and_set_cache(changeset) do
665 CommonAPI.update(user)
666
667 conn
668 |> put_view(UserView)
669 |> render("user.json", %{user: user, for: user})
670 else
671 error ->
672 Logger.debug("Can't update user: #{inspect(error)}")
673 bad_request_reply(conn, "Can't update user")
674 end
675 end
676
677 def search(%{assigns: %{user: user}} = conn, %{"q" => _query} = params) do
678 activities = TwitterAPI.search(user, params)
679
680 conn
681 |> put_view(ActivityView)
682 |> render("index.json", %{activities: activities, for: user})
683 end
684
685 def search_user(%{assigns: %{user: user}} = conn, %{"query" => query}) do
686 users = User.search(query, true, user)
687
688 conn
689 |> put_view(UserView)
690 |> render("index.json", %{users: users, for: user})
691 end
692
693 defp bad_request_reply(conn, error_message) do
694 json = error_json(conn, error_message)
695 json_reply(conn, 400, json)
696 end
697
698 defp json_reply(conn, status, json) do
699 conn
700 |> put_resp_content_type("application/json")
701 |> send_resp(status, json)
702 end
703
704 defp forbidden_json_reply(conn, error_message) do
705 json = error_json(conn, error_message)
706 json_reply(conn, 403, json)
707 end
708
709 def only_if_public_instance(%{assigns: %{user: %User{}}} = conn, _), do: conn
710
711 def only_if_public_instance(conn, _) do
712 if Keyword.get(Application.get_env(:pleroma, :instance), :public) do
713 conn
714 else
715 conn
716 |> forbidden_json_reply("Invalid credentials.")
717 |> halt()
718 end
719 end
720
721 defp error_json(conn, error_message) do
722 %{"error" => error_message, "request" => conn.request_path} |> Jason.encode!()
723 end
724
725 def errors(conn, {:param_cast, _}) do
726 conn
727 |> put_status(400)
728 |> json("Invalid parameters")
729 end
730
731 def errors(conn, _) do
732 conn
733 |> put_status(500)
734 |> json("Something went wrong")
735 end
736 end