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