[#468] Merged `upstream/develop`, resolved conflicts.
[akkoma] / lib / pleroma / web / mastodon_api / mastodon_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.MastodonAPI.MastodonAPIController do
6 use Pleroma.Web, :controller
7 alias Pleroma.Activity
8 alias Pleroma.Config
9 alias Pleroma.Filter
10 alias Pleroma.Notification
11 alias Pleroma.Object
12 alias Pleroma.Repo
13 alias Pleroma.Stats
14 alias Pleroma.User
15 alias Pleroma.Web
16 alias Pleroma.Web.CommonAPI
17 alias Pleroma.Web.MediaProxy
18 alias Pleroma.Web.Push
19 alias Push.Subscription
20
21 alias Pleroma.Web.MastodonAPI.AccountView
22 alias Pleroma.Web.MastodonAPI.FilterView
23 alias Pleroma.Web.MastodonAPI.ListView
24 alias Pleroma.Web.MastodonAPI.MastodonView
25 alias Pleroma.Web.MastodonAPI.PushSubscriptionView
26 alias Pleroma.Web.MastodonAPI.StatusView
27 alias Pleroma.Web.ActivityPub.ActivityPub
28 alias Pleroma.Web.ActivityPub.Utils
29 alias Pleroma.Web.OAuth.App
30 alias Pleroma.Web.OAuth.Authorization
31 alias Pleroma.Web.OAuth.Token
32
33 import Pleroma.Web.ControllerHelper, only: [oauth_scopes: 2]
34 import Ecto.Query
35
36 require Logger
37
38 @httpoison Application.get_env(:pleroma, :httpoison)
39 @local_mastodon_name "Mastodon-Local"
40
41 action_fallback(:errors)
42
43 def create_app(conn, params) do
44 scopes = oauth_scopes(params, ["read"])
45
46 app_attrs =
47 params
48 |> Map.drop(["scope", "scopes"])
49 |> Map.put("scopes", scopes)
50
51 with cs <- App.register_changeset(%App{}, app_attrs),
52 false <- cs.changes[:client_name] == @local_mastodon_name,
53 {:ok, app} <- Repo.insert(cs) do
54 res = %{
55 id: app.id |> to_string,
56 name: app.client_name,
57 client_id: app.client_id,
58 client_secret: app.client_secret,
59 redirect_uri: app.redirect_uris,
60 website: app.website
61 }
62
63 json(conn, res)
64 end
65 end
66
67 defp add_if_present(
68 map,
69 params,
70 params_field,
71 map_field,
72 value_function \\ fn x -> {:ok, x} end
73 ) do
74 if Map.has_key?(params, params_field) do
75 case value_function.(params[params_field]) do
76 {:ok, new_value} -> Map.put(map, map_field, new_value)
77 :error -> map
78 end
79 else
80 map
81 end
82 end
83
84 def update_credentials(%{assigns: %{user: user}} = conn, params) do
85 original_user = user
86
87 user_params =
88 %{}
89 |> add_if_present(params, "display_name", :name)
90 |> add_if_present(params, "note", :bio, fn value -> {:ok, User.parse_bio(value)} end)
91 |> add_if_present(params, "avatar", :avatar, fn value ->
92 with %Plug.Upload{} <- value,
93 {:ok, object} <- ActivityPub.upload(value, type: :avatar) do
94 {:ok, object.data}
95 else
96 _ -> :error
97 end
98 end)
99
100 info_params =
101 %{}
102 |> add_if_present(params, "locked", :locked, fn value -> {:ok, value == "true"} end)
103 |> add_if_present(params, "header", :banner, fn value ->
104 with %Plug.Upload{} <- value,
105 {:ok, object} <- ActivityPub.upload(value, type: :banner) do
106 {:ok, object.data}
107 else
108 _ -> :error
109 end
110 end)
111
112 info_cng = User.Info.mastodon_profile_update(user.info, info_params)
113
114 with changeset <- User.update_changeset(user, user_params),
115 changeset <- Ecto.Changeset.put_embed(changeset, :info, info_cng),
116 {:ok, user} <- User.update_and_set_cache(changeset) do
117 if original_user != user do
118 CommonAPI.update(user)
119 end
120
121 json(conn, AccountView.render("account.json", %{user: user, for: user}))
122 else
123 _e ->
124 conn
125 |> put_status(403)
126 |> json(%{error: "Invalid request"})
127 end
128 end
129
130 def verify_credentials(%{assigns: %{user: user}} = conn, _) do
131 account = AccountView.render("account.json", %{user: user, for: user})
132 json(conn, account)
133 end
134
135 def user(%{assigns: %{user: for_user}} = conn, %{"id" => id}) do
136 with %User{} = user <- Repo.get(User, id),
137 true <- User.auth_active?(user) || user.id == for_user.id || User.superuser?(for_user) do
138 account = AccountView.render("account.json", %{user: user, for: for_user})
139 json(conn, account)
140 else
141 _e ->
142 conn
143 |> put_status(404)
144 |> json(%{error: "Can't find user"})
145 end
146 end
147
148 @mastodon_api_level "2.5.0"
149
150 def masto_instance(conn, _params) do
151 instance = Config.get(:instance)
152
153 response = %{
154 uri: Web.base_url(),
155 title: Keyword.get(instance, :name),
156 description: Keyword.get(instance, :description),
157 version: "#{@mastodon_api_level} (compatible; #{Pleroma.Application.named_version()})",
158 email: Keyword.get(instance, :email),
159 urls: %{
160 streaming_api: Pleroma.Web.Endpoint.websocket_url()
161 },
162 stats: Stats.get_stats(),
163 thumbnail: Web.base_url() <> "/instance/thumbnail.jpeg",
164 max_toot_chars: Keyword.get(instance, :limit)
165 }
166
167 json(conn, response)
168 end
169
170 def peers(conn, _params) do
171 json(conn, Stats.get_peers())
172 end
173
174 defp mastodonized_emoji do
175 Pleroma.Emoji.get_all()
176 |> Enum.map(fn {shortcode, relative_url} ->
177 url = to_string(URI.merge(Web.base_url(), relative_url))
178
179 %{
180 "shortcode" => shortcode,
181 "static_url" => url,
182 "visible_in_picker" => true,
183 "url" => url
184 }
185 end)
186 end
187
188 def custom_emojis(conn, _params) do
189 mastodon_emoji = mastodonized_emoji()
190 json(conn, mastodon_emoji)
191 end
192
193 defp add_link_headers(conn, method, activities, param \\ nil, params \\ %{}) do
194 last = List.last(activities)
195 first = List.first(activities)
196
197 if last do
198 min = last.id
199 max = first.id
200
201 {next_url, prev_url} =
202 if param do
203 {
204 mastodon_api_url(
205 Pleroma.Web.Endpoint,
206 method,
207 param,
208 Map.merge(params, %{max_id: min})
209 ),
210 mastodon_api_url(
211 Pleroma.Web.Endpoint,
212 method,
213 param,
214 Map.merge(params, %{since_id: max})
215 )
216 }
217 else
218 {
219 mastodon_api_url(
220 Pleroma.Web.Endpoint,
221 method,
222 Map.merge(params, %{max_id: min})
223 ),
224 mastodon_api_url(
225 Pleroma.Web.Endpoint,
226 method,
227 Map.merge(params, %{since_id: max})
228 )
229 }
230 end
231
232 conn
233 |> put_resp_header("link", "<#{next_url}>; rel=\"next\", <#{prev_url}>; rel=\"prev\"")
234 else
235 conn
236 end
237 end
238
239 def home_timeline(%{assigns: %{user: user}} = conn, params) do
240 params =
241 params
242 |> Map.put("type", ["Create", "Announce"])
243 |> Map.put("blocking_user", user)
244 |> Map.put("user", user)
245
246 activities =
247 [user.ap_id | user.following]
248 |> ActivityPub.fetch_activities(params)
249 |> ActivityPub.contain_timeline(user)
250 |> Enum.reverse()
251
252 conn
253 |> add_link_headers(:home_timeline, activities)
254 |> put_view(StatusView)
255 |> render("index.json", %{activities: activities, for: user, as: :activity})
256 end
257
258 def public_timeline(%{assigns: %{user: user}} = conn, params) do
259 local_only = params["local"] in [true, "True", "true", "1"]
260
261 activities =
262 params
263 |> Map.put("type", ["Create", "Announce"])
264 |> Map.put("local_only", local_only)
265 |> Map.put("blocking_user", user)
266 |> ActivityPub.fetch_public_activities()
267 |> Enum.reverse()
268
269 conn
270 |> add_link_headers(:public_timeline, activities, false, %{"local" => local_only})
271 |> put_view(StatusView)
272 |> render("index.json", %{activities: activities, for: user, as: :activity})
273 end
274
275 def user_statuses(%{assigns: %{user: reading_user}} = conn, params) do
276 with %User{} = user <- Repo.get(User, params["id"]) do
277 activities = ActivityPub.fetch_user_activities(user, reading_user, params)
278
279 conn
280 |> add_link_headers(:user_statuses, activities, params["id"])
281 |> put_view(StatusView)
282 |> render("index.json", %{
283 activities: activities,
284 for: reading_user,
285 as: :activity
286 })
287 end
288 end
289
290 def dm_timeline(%{assigns: %{user: user}} = conn, params) do
291 query =
292 ActivityPub.fetch_activities_query(
293 [user.ap_id],
294 Map.merge(params, %{"type" => "Create", visibility: "direct"})
295 )
296
297 activities = Repo.all(query)
298
299 conn
300 |> add_link_headers(:dm_timeline, activities)
301 |> put_view(StatusView)
302 |> render("index.json", %{activities: activities, for: user, as: :activity})
303 end
304
305 def get_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
306 with %Activity{} = activity <- Repo.get(Activity, id),
307 true <- ActivityPub.visible_for_user?(activity, user) do
308 conn
309 |> put_view(StatusView)
310 |> try_render("status.json", %{activity: activity, for: user})
311 end
312 end
313
314 def get_context(%{assigns: %{user: user}} = conn, %{"id" => id}) do
315 with %Activity{} = activity <- Repo.get(Activity, id),
316 activities <-
317 ActivityPub.fetch_activities_for_context(activity.data["context"], %{
318 "blocking_user" => user,
319 "user" => user
320 }),
321 activities <-
322 activities |> Enum.filter(fn %{id: aid} -> to_string(aid) != to_string(id) end),
323 activities <-
324 activities |> Enum.filter(fn %{data: %{"type" => type}} -> type == "Create" end),
325 grouped_activities <- Enum.group_by(activities, fn %{id: id} -> id < activity.id end) do
326 result = %{
327 ancestors:
328 StatusView.render(
329 "index.json",
330 for: user,
331 activities: grouped_activities[true] || [],
332 as: :activity
333 )
334 |> Enum.reverse(),
335 # credo:disable-for-previous-line Credo.Check.Refactor.PipeChainStart
336 descendants:
337 StatusView.render(
338 "index.json",
339 for: user,
340 activities: grouped_activities[false] || [],
341 as: :activity
342 )
343 |> Enum.reverse()
344 # credo:disable-for-previous-line Credo.Check.Refactor.PipeChainStart
345 }
346
347 json(conn, result)
348 end
349 end
350
351 def post_status(conn, %{"status" => "", "media_ids" => media_ids} = params)
352 when length(media_ids) > 0 do
353 params =
354 params
355 |> Map.put("status", ".")
356
357 post_status(conn, params)
358 end
359
360 def post_status(%{assigns: %{user: user}} = conn, %{"status" => _} = params) do
361 params =
362 params
363 |> Map.put("in_reply_to_status_id", params["in_reply_to_id"])
364
365 idempotency_key =
366 case get_req_header(conn, "idempotency-key") do
367 [key] -> key
368 _ -> Ecto.UUID.generate()
369 end
370
371 {:ok, activity} =
372 Cachex.fetch!(:idempotency_cache, idempotency_key, fn _ -> CommonAPI.post(user, params) end)
373
374 conn
375 |> put_view(StatusView)
376 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
377 end
378
379 def delete_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
380 with {:ok, %Activity{}} <- CommonAPI.delete(id, user) do
381 json(conn, %{})
382 else
383 _e ->
384 conn
385 |> put_status(403)
386 |> json(%{error: "Can't delete this post"})
387 end
388 end
389
390 def reblog_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
391 with {:ok, announce, _activity} <- CommonAPI.repeat(ap_id_or_id, user) do
392 conn
393 |> put_view(StatusView)
394 |> try_render("status.json", %{activity: announce, for: user, as: :activity})
395 end
396 end
397
398 def unreblog_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
399 with {:ok, _unannounce, %{data: %{"id" => id}}} <- CommonAPI.unrepeat(ap_id_or_id, user),
400 %Activity{} = activity <- Activity.get_create_by_object_ap_id(id) do
401 conn
402 |> put_view(StatusView)
403 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
404 end
405 end
406
407 def fav_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
408 with {:ok, _fav, %{data: %{"id" => id}}} <- CommonAPI.favorite(ap_id_or_id, user),
409 %Activity{} = activity <- Activity.get_create_by_object_ap_id(id) do
410 conn
411 |> put_view(StatusView)
412 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
413 end
414 end
415
416 def unfav_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
417 with {:ok, _, _, %{data: %{"id" => id}}} <- CommonAPI.unfavorite(ap_id_or_id, user),
418 %Activity{} = activity <- Activity.get_create_by_object_ap_id(id) do
419 conn
420 |> put_view(StatusView)
421 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
422 end
423 end
424
425 def pin_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
426 with {:ok, activity} <- CommonAPI.pin(ap_id_or_id, user) do
427 conn
428 |> put_view(StatusView)
429 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
430 else
431 {:error, reason} ->
432 conn
433 |> put_resp_content_type("application/json")
434 |> send_resp(:bad_request, Jason.encode!(%{"error" => reason}))
435 end
436 end
437
438 def unpin_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
439 with {:ok, activity} <- CommonAPI.unpin(ap_id_or_id, user) do
440 conn
441 |> put_view(StatusView)
442 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
443 end
444 end
445
446 def bookmark_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
447 with %Activity{} = activity <- Repo.get(Activity, id),
448 %User{} = user <- User.get_by_nickname(user.nickname),
449 true <- ActivityPub.visible_for_user?(activity, user),
450 {:ok, user} <- User.bookmark(user, activity.data["object"]["id"]) do
451 conn
452 |> put_view(StatusView)
453 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
454 end
455 end
456
457 def unbookmark_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
458 with %Activity{} = activity <- Repo.get(Activity, id),
459 %User{} = user <- User.get_by_nickname(user.nickname),
460 true <- ActivityPub.visible_for_user?(activity, user),
461 {:ok, user} <- User.unbookmark(user, activity.data["object"]["id"]) do
462 conn
463 |> put_view(StatusView)
464 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
465 end
466 end
467
468 def mute_conversation(%{assigns: %{user: user}} = conn, %{"id" => id}) do
469 activity = Activity.get_by_id(id)
470
471 with {:ok, activity} <- CommonAPI.add_mute(user, activity) do
472 conn
473 |> put_view(StatusView)
474 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
475 else
476 {:error, reason} ->
477 conn
478 |> put_resp_content_type("application/json")
479 |> send_resp(:bad_request, Jason.encode!(%{"error" => reason}))
480 end
481 end
482
483 def unmute_conversation(%{assigns: %{user: user}} = conn, %{"id" => id}) do
484 activity = Activity.get_by_id(id)
485
486 with {:ok, activity} <- CommonAPI.remove_mute(user, activity) do
487 conn
488 |> put_view(StatusView)
489 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
490 end
491 end
492
493 def notifications(%{assigns: %{user: user}} = conn, params) do
494 notifications = Notification.for_user(user, params)
495
496 result =
497 notifications
498 |> Enum.map(fn x -> render_notification(user, x) end)
499 |> Enum.filter(& &1)
500
501 conn
502 |> add_link_headers(:notifications, notifications)
503 |> json(result)
504 end
505
506 def get_notification(%{assigns: %{user: user}} = conn, %{"id" => id} = _params) do
507 with {:ok, notification} <- Notification.get(user, id) do
508 json(conn, render_notification(user, notification))
509 else
510 {:error, reason} ->
511 conn
512 |> put_resp_content_type("application/json")
513 |> send_resp(403, Jason.encode!(%{"error" => reason}))
514 end
515 end
516
517 def clear_notifications(%{assigns: %{user: user}} = conn, _params) do
518 Notification.clear(user)
519 json(conn, %{})
520 end
521
522 def dismiss_notification(%{assigns: %{user: user}} = conn, %{"id" => id} = _params) do
523 with {:ok, _notif} <- Notification.dismiss(user, id) do
524 json(conn, %{})
525 else
526 {:error, reason} ->
527 conn
528 |> put_resp_content_type("application/json")
529 |> send_resp(403, Jason.encode!(%{"error" => reason}))
530 end
531 end
532
533 def relationships(%{assigns: %{user: user}} = conn, %{"id" => id}) do
534 id = List.wrap(id)
535 q = from(u in User, where: u.id in ^id)
536 targets = Repo.all(q)
537
538 conn
539 |> put_view(AccountView)
540 |> render("relationships.json", %{user: user, targets: targets})
541 end
542
543 # Instead of returning a 400 when no "id" params is present, Mastodon returns an empty array.
544 def relationships(%{assigns: %{user: _user}} = conn, _), do: json(conn, [])
545
546 def update_media(%{assigns: %{user: user}} = conn, data) do
547 with %Object{} = object <- Repo.get(Object, data["id"]),
548 true <- Object.authorize_mutation(object, user),
549 true <- is_binary(data["description"]),
550 description <- data["description"] do
551 new_data = %{object.data | "name" => description}
552
553 {:ok, _} =
554 object
555 |> Object.change(%{data: new_data})
556 |> Repo.update()
557
558 attachment_data = Map.put(new_data, "id", object.id)
559
560 conn
561 |> put_view(StatusView)
562 |> render("attachment.json", %{attachment: attachment_data})
563 end
564 end
565
566 def upload(%{assigns: %{user: user}} = conn, %{"file" => file} = data) do
567 with {:ok, object} <-
568 ActivityPub.upload(
569 file,
570 actor: User.ap_id(user),
571 description: Map.get(data, "description")
572 ) do
573 attachment_data = Map.put(object.data, "id", object.id)
574
575 conn
576 |> put_view(StatusView)
577 |> render("attachment.json", %{attachment: attachment_data})
578 end
579 end
580
581 def favourited_by(conn, %{"id" => id}) do
582 with %Activity{data: %{"object" => %{"likes" => likes}}} <- Repo.get(Activity, id) do
583 q = from(u in User, where: u.ap_id in ^likes)
584 users = Repo.all(q)
585
586 conn
587 |> put_view(AccountView)
588 |> render(AccountView, "accounts.json", %{users: users, as: :user})
589 else
590 _ -> json(conn, [])
591 end
592 end
593
594 def reblogged_by(conn, %{"id" => id}) do
595 with %Activity{data: %{"object" => %{"announcements" => announces}}} <- Repo.get(Activity, id) do
596 q = from(u in User, where: u.ap_id in ^announces)
597 users = Repo.all(q)
598
599 conn
600 |> put_view(AccountView)
601 |> render("accounts.json", %{users: users, as: :user})
602 else
603 _ -> json(conn, [])
604 end
605 end
606
607 def hashtag_timeline(%{assigns: %{user: user}} = conn, params) do
608 local_only = params["local"] in [true, "True", "true", "1"]
609
610 tags =
611 [params["tag"], params["any"]]
612 |> List.flatten()
613 |> Enum.uniq()
614 |> Enum.filter(& &1)
615 |> Enum.map(&String.downcase(&1))
616
617 tag_all =
618 params["all"] ||
619 []
620 |> Enum.map(&String.downcase(&1))
621
622 tag_reject =
623 params["none"] ||
624 []
625 |> Enum.map(&String.downcase(&1))
626
627 activities =
628 params
629 |> Map.put("type", "Create")
630 |> Map.put("local_only", local_only)
631 |> Map.put("blocking_user", user)
632 |> Map.put("tag", tags)
633 |> Map.put("tag_all", tag_all)
634 |> Map.put("tag_reject", tag_reject)
635 |> ActivityPub.fetch_public_activities()
636 |> Enum.reverse()
637
638 conn
639 |> add_link_headers(:hashtag_timeline, activities, params["tag"], %{"local" => local_only})
640 |> put_view(StatusView)
641 |> render("index.json", %{activities: activities, for: user, as: :activity})
642 end
643
644 def followers(%{assigns: %{user: for_user}} = conn, %{"id" => id}) do
645 with %User{} = user <- Repo.get(User, id),
646 {:ok, followers} <- User.get_followers(user) do
647 followers =
648 cond do
649 for_user && user.id == for_user.id -> followers
650 user.info.hide_followers -> []
651 true -> followers
652 end
653
654 conn
655 |> put_view(AccountView)
656 |> render("accounts.json", %{users: followers, as: :user})
657 end
658 end
659
660 def following(%{assigns: %{user: for_user}} = conn, %{"id" => id}) do
661 with %User{} = user <- Repo.get(User, id),
662 {:ok, followers} <- User.get_friends(user) do
663 followers =
664 cond do
665 for_user && user.id == for_user.id -> followers
666 user.info.hide_follows -> []
667 true -> followers
668 end
669
670 conn
671 |> put_view(AccountView)
672 |> render("accounts.json", %{users: followers, as: :user})
673 end
674 end
675
676 def follow_requests(%{assigns: %{user: followed}} = conn, _params) do
677 with {:ok, follow_requests} <- User.get_follow_requests(followed) do
678 conn
679 |> put_view(AccountView)
680 |> render("accounts.json", %{users: follow_requests, as: :user})
681 end
682 end
683
684 def authorize_follow_request(%{assigns: %{user: followed}} = conn, %{"id" => id}) do
685 with %User{} = follower <- Repo.get(User, id),
686 {:ok, follower} <- User.maybe_follow(follower, followed),
687 %Activity{} = follow_activity <- Utils.fetch_latest_follow(follower, followed),
688 {:ok, follow_activity} <- Utils.update_follow_state(follow_activity, "accept"),
689 {:ok, _activity} <-
690 ActivityPub.accept(%{
691 to: [follower.ap_id],
692 actor: followed.ap_id,
693 object: follow_activity.data["id"],
694 type: "Accept"
695 }) do
696 conn
697 |> put_view(AccountView)
698 |> render("relationship.json", %{user: followed, target: follower})
699 else
700 {:error, message} ->
701 conn
702 |> put_resp_content_type("application/json")
703 |> send_resp(403, Jason.encode!(%{"error" => message}))
704 end
705 end
706
707 def reject_follow_request(%{assigns: %{user: followed}} = conn, %{"id" => id}) do
708 with %User{} = follower <- Repo.get(User, id),
709 %Activity{} = follow_activity <- Utils.fetch_latest_follow(follower, followed),
710 {:ok, follow_activity} <- Utils.update_follow_state(follow_activity, "reject"),
711 {:ok, _activity} <-
712 ActivityPub.reject(%{
713 to: [follower.ap_id],
714 actor: followed.ap_id,
715 object: follow_activity.data["id"],
716 type: "Reject"
717 }) do
718 conn
719 |> put_view(AccountView)
720 |> render("relationship.json", %{user: followed, target: follower})
721 else
722 {:error, message} ->
723 conn
724 |> put_resp_content_type("application/json")
725 |> send_resp(403, Jason.encode!(%{"error" => message}))
726 end
727 end
728
729 def follow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
730 with %User{} = followed <- Repo.get(User, id),
731 {:ok, follower} <- User.maybe_direct_follow(follower, followed),
732 {:ok, _activity} <- ActivityPub.follow(follower, followed),
733 {:ok, follower, followed} <-
734 User.wait_and_refresh(
735 Config.get([:activitypub, :follow_handshake_timeout]),
736 follower,
737 followed
738 ) do
739 conn
740 |> put_view(AccountView)
741 |> render("relationship.json", %{user: follower, target: followed})
742 else
743 {:error, message} ->
744 conn
745 |> put_resp_content_type("application/json")
746 |> send_resp(403, Jason.encode!(%{"error" => message}))
747 end
748 end
749
750 def follow(%{assigns: %{user: follower}} = conn, %{"uri" => uri}) do
751 with %User{} = followed <- Repo.get_by(User, nickname: uri),
752 {:ok, follower} <- User.maybe_direct_follow(follower, followed),
753 {:ok, _activity} <- ActivityPub.follow(follower, followed) do
754 conn
755 |> put_view(AccountView)
756 |> render("account.json", %{user: followed, for: follower})
757 else
758 {:error, message} ->
759 conn
760 |> put_resp_content_type("application/json")
761 |> send_resp(403, Jason.encode!(%{"error" => message}))
762 end
763 end
764
765 def unfollow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
766 with %User{} = followed <- Repo.get(User, id),
767 {:ok, _activity} <- ActivityPub.unfollow(follower, followed),
768 {:ok, follower, _} <- User.unfollow(follower, followed) do
769 conn
770 |> put_view(AccountView)
771 |> render("relationship.json", %{user: follower, target: followed})
772 end
773 end
774
775 def block(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
776 with %User{} = blocked <- Repo.get(User, id),
777 {:ok, blocker} <- User.block(blocker, blocked),
778 {:ok, _activity} <- ActivityPub.block(blocker, blocked) do
779 conn
780 |> put_view(AccountView)
781 |> render("relationship.json", %{user: blocker, target: blocked})
782 else
783 {:error, message} ->
784 conn
785 |> put_resp_content_type("application/json")
786 |> send_resp(403, Jason.encode!(%{"error" => message}))
787 end
788 end
789
790 def unblock(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
791 with %User{} = blocked <- Repo.get(User, id),
792 {:ok, blocker} <- User.unblock(blocker, blocked),
793 {:ok, _activity} <- ActivityPub.unblock(blocker, blocked) do
794 conn
795 |> put_view(AccountView)
796 |> render("relationship.json", %{user: blocker, target: blocked})
797 else
798 {:error, message} ->
799 conn
800 |> put_resp_content_type("application/json")
801 |> send_resp(403, Jason.encode!(%{"error" => message}))
802 end
803 end
804
805 def blocks(%{assigns: %{user: user}} = conn, _) do
806 with blocked_accounts <- User.blocked_users(user) do
807 res = AccountView.render("accounts.json", users: blocked_accounts, for: user, as: :user)
808 json(conn, res)
809 end
810 end
811
812 def domain_blocks(%{assigns: %{user: %{info: info}}} = conn, _) do
813 json(conn, info.domain_blocks || [])
814 end
815
816 def block_domain(%{assigns: %{user: blocker}} = conn, %{"domain" => domain}) do
817 User.block_domain(blocker, domain)
818 json(conn, %{})
819 end
820
821 def unblock_domain(%{assigns: %{user: blocker}} = conn, %{"domain" => domain}) do
822 User.unblock_domain(blocker, domain)
823 json(conn, %{})
824 end
825
826 def status_search(user, query) do
827 fetched =
828 if Regex.match?(~r/https?:/, query) do
829 with {:ok, object} <- ActivityPub.fetch_object_from_id(query),
830 %Activity{} = activity <- Activity.get_create_by_object_ap_id(object.data["id"]),
831 true <- ActivityPub.visible_for_user?(activity, user) do
832 [activity]
833 else
834 _e -> []
835 end
836 end || []
837
838 q =
839 from(
840 a in Activity,
841 where: fragment("?->>'type' = 'Create'", a.data),
842 where: "https://www.w3.org/ns/activitystreams#Public" in a.recipients,
843 where:
844 fragment(
845 "to_tsvector('english', ?->'object'->>'content') @@ plainto_tsquery('english', ?)",
846 a.data,
847 ^query
848 ),
849 limit: 20,
850 order_by: [desc: :id]
851 )
852
853 Repo.all(q) ++ fetched
854 end
855
856 def search2(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do
857 accounts = User.search(query, params["resolve"] == "true", user)
858
859 statuses = status_search(user, query)
860
861 tags_path = Web.base_url() <> "/tag/"
862
863 tags =
864 query
865 |> String.split()
866 |> Enum.uniq()
867 |> Enum.filter(fn tag -> String.starts_with?(tag, "#") end)
868 |> Enum.map(fn tag -> String.slice(tag, 1..-1) end)
869 |> Enum.map(fn tag -> %{name: tag, url: tags_path <> tag} end)
870
871 res = %{
872 "accounts" => AccountView.render("accounts.json", users: accounts, for: user, as: :user),
873 "statuses" =>
874 StatusView.render("index.json", activities: statuses, for: user, as: :activity),
875 "hashtags" => tags
876 }
877
878 json(conn, res)
879 end
880
881 def search(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do
882 accounts = User.search(query, params["resolve"] == "true", user)
883
884 statuses = status_search(user, query)
885
886 tags =
887 query
888 |> String.split()
889 |> Enum.uniq()
890 |> Enum.filter(fn tag -> String.starts_with?(tag, "#") end)
891 |> Enum.map(fn tag -> String.slice(tag, 1..-1) end)
892
893 res = %{
894 "accounts" => AccountView.render("accounts.json", users: accounts, for: user, as: :user),
895 "statuses" =>
896 StatusView.render("index.json", activities: statuses, for: user, as: :activity),
897 "hashtags" => tags
898 }
899
900 json(conn, res)
901 end
902
903 def account_search(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do
904 accounts = User.search(query, params["resolve"] == "true", user)
905
906 res = AccountView.render("accounts.json", users: accounts, for: user, as: :user)
907
908 json(conn, res)
909 end
910
911 def favourites(%{assigns: %{user: user}} = conn, params) do
912 activities =
913 params
914 |> Map.put("type", "Create")
915 |> Map.put("favorited_by", user.ap_id)
916 |> Map.put("blocking_user", user)
917 |> ActivityPub.fetch_public_activities()
918 |> Enum.reverse()
919
920 conn
921 |> add_link_headers(:favourites, activities)
922 |> put_view(StatusView)
923 |> render("index.json", %{activities: activities, for: user, as: :activity})
924 end
925
926 def bookmarks(%{assigns: %{user: user}} = conn, _) do
927 user = Repo.get(User, user.id)
928
929 activities =
930 user.bookmarks
931 |> Enum.map(fn id -> Activity.get_create_by_object_ap_id(id) end)
932 |> Enum.reverse()
933
934 conn
935 |> put_view(StatusView)
936 |> render("index.json", %{activities: activities, for: user, as: :activity})
937 end
938
939 def get_lists(%{assigns: %{user: user}} = conn, opts) do
940 lists = Pleroma.List.for_user(user, opts)
941 res = ListView.render("lists.json", lists: lists)
942 json(conn, res)
943 end
944
945 def get_list(%{assigns: %{user: user}} = conn, %{"id" => id}) do
946 with %Pleroma.List{} = list <- Pleroma.List.get(id, user) do
947 res = ListView.render("list.json", list: list)
948 json(conn, res)
949 else
950 _e ->
951 conn
952 |> put_status(404)
953 |> json(%{error: "Record not found"})
954 end
955 end
956
957 def account_lists(%{assigns: %{user: user}} = conn, %{"id" => account_id}) do
958 lists = Pleroma.List.get_lists_account_belongs(user, account_id)
959 res = ListView.render("lists.json", lists: lists)
960 json(conn, res)
961 end
962
963 def delete_list(%{assigns: %{user: user}} = conn, %{"id" => id}) do
964 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
965 {:ok, _list} <- Pleroma.List.delete(list) do
966 json(conn, %{})
967 else
968 _e ->
969 json(conn, "error")
970 end
971 end
972
973 def create_list(%{assigns: %{user: user}} = conn, %{"title" => title}) do
974 with {:ok, %Pleroma.List{} = list} <- Pleroma.List.create(title, user) do
975 res = ListView.render("list.json", list: list)
976 json(conn, res)
977 end
978 end
979
980 def add_to_list(%{assigns: %{user: user}} = conn, %{"id" => id, "account_ids" => accounts}) do
981 accounts
982 |> Enum.each(fn account_id ->
983 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
984 %User{} = followed <- Repo.get(User, account_id) do
985 Pleroma.List.follow(list, followed)
986 end
987 end)
988
989 json(conn, %{})
990 end
991
992 def remove_from_list(%{assigns: %{user: user}} = conn, %{"id" => id, "account_ids" => accounts}) do
993 accounts
994 |> Enum.each(fn account_id ->
995 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
996 %User{} = followed <- Repo.get(Pleroma.User, account_id) do
997 Pleroma.List.unfollow(list, followed)
998 end
999 end)
1000
1001 json(conn, %{})
1002 end
1003
1004 def list_accounts(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1005 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1006 {:ok, users} = Pleroma.List.get_following(list) do
1007 conn
1008 |> put_view(AccountView)
1009 |> render("accounts.json", %{users: users, as: :user})
1010 end
1011 end
1012
1013 def rename_list(%{assigns: %{user: user}} = conn, %{"id" => id, "title" => title}) do
1014 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1015 {:ok, list} <- Pleroma.List.rename(list, title) do
1016 res = ListView.render("list.json", list: list)
1017 json(conn, res)
1018 else
1019 _e ->
1020 json(conn, "error")
1021 end
1022 end
1023
1024 def list_timeline(%{assigns: %{user: user}} = conn, %{"list_id" => id} = params) do
1025 with %Pleroma.List{title: _title, following: following} <- Pleroma.List.get(id, user) do
1026 params =
1027 params
1028 |> Map.put("type", "Create")
1029 |> Map.put("blocking_user", user)
1030
1031 # we must filter the following list for the user to avoid leaking statuses the user
1032 # does not actually have permission to see (for more info, peruse security issue #270).
1033 activities =
1034 following
1035 |> Enum.filter(fn x -> x in user.following end)
1036 |> ActivityPub.fetch_activities_bounded(following, params)
1037 |> Enum.reverse()
1038
1039 conn
1040 |> put_view(StatusView)
1041 |> render("index.json", %{activities: activities, for: user, as: :activity})
1042 else
1043 _e ->
1044 conn
1045 |> put_status(403)
1046 |> json(%{error: "Error."})
1047 end
1048 end
1049
1050 def index(%{assigns: %{user: user}} = conn, _params) do
1051 token =
1052 conn
1053 |> get_session(:oauth_token)
1054
1055 if user && token do
1056 mastodon_emoji = mastodonized_emoji()
1057
1058 limit = Config.get([:instance, :limit])
1059
1060 accounts =
1061 Map.put(%{}, user.id, AccountView.render("account.json", %{user: user, for: user}))
1062
1063 initial_state =
1064 %{
1065 meta: %{
1066 streaming_api_base_url:
1067 String.replace(Pleroma.Web.Endpoint.static_url(), "http", "ws"),
1068 access_token: token,
1069 locale: "en",
1070 domain: Pleroma.Web.Endpoint.host(),
1071 admin: "1",
1072 me: "#{user.id}",
1073 unfollow_modal: false,
1074 boost_modal: false,
1075 delete_modal: true,
1076 auto_play_gif: false,
1077 display_sensitive_media: false,
1078 reduce_motion: false,
1079 max_toot_chars: limit
1080 },
1081 rights: %{
1082 delete_others_notice: present?(user.info.is_moderator),
1083 admin: present?(user.info.is_admin)
1084 },
1085 compose: %{
1086 me: "#{user.id}",
1087 default_privacy: user.info.default_scope,
1088 default_sensitive: false
1089 },
1090 media_attachments: %{
1091 accept_content_types: [
1092 ".jpg",
1093 ".jpeg",
1094 ".png",
1095 ".gif",
1096 ".webm",
1097 ".mp4",
1098 ".m4v",
1099 "image\/jpeg",
1100 "image\/png",
1101 "image\/gif",
1102 "video\/webm",
1103 "video\/mp4"
1104 ]
1105 },
1106 settings:
1107 user.info.settings ||
1108 %{
1109 onboarded: true,
1110 home: %{
1111 shows: %{
1112 reblog: true,
1113 reply: true
1114 }
1115 },
1116 notifications: %{
1117 alerts: %{
1118 follow: true,
1119 favourite: true,
1120 reblog: true,
1121 mention: true
1122 },
1123 shows: %{
1124 follow: true,
1125 favourite: true,
1126 reblog: true,
1127 mention: true
1128 },
1129 sounds: %{
1130 follow: true,
1131 favourite: true,
1132 reblog: true,
1133 mention: true
1134 }
1135 }
1136 },
1137 push_subscription: nil,
1138 accounts: accounts,
1139 custom_emojis: mastodon_emoji,
1140 char_limit: limit
1141 }
1142 |> Jason.encode!()
1143
1144 conn
1145 |> put_layout(false)
1146 |> put_view(MastodonView)
1147 |> render("index.html", %{initial_state: initial_state})
1148 else
1149 conn
1150 |> redirect(to: "/web/login")
1151 end
1152 end
1153
1154 def put_settings(%{assigns: %{user: user}} = conn, %{"data" => settings} = _params) do
1155 info_cng = User.Info.mastodon_settings_update(user.info, settings)
1156
1157 with changeset <- Ecto.Changeset.change(user),
1158 changeset <- Ecto.Changeset.put_embed(changeset, :info, info_cng),
1159 {:ok, _user} <- User.update_and_set_cache(changeset) do
1160 json(conn, %{})
1161 else
1162 e ->
1163 conn
1164 |> put_resp_content_type("application/json")
1165 |> send_resp(500, Jason.encode!(%{"error" => inspect(e)}))
1166 end
1167 end
1168
1169 def login(conn, %{"code" => code}) do
1170 with {:ok, app} <- get_or_make_app(),
1171 %Authorization{} = auth <- Repo.get_by(Authorization, token: code, app_id: app.id),
1172 {:ok, token} <- Token.exchange_token(app, auth) do
1173 conn
1174 |> put_session(:oauth_token, token.token)
1175 |> redirect(to: "/web/getting-started")
1176 end
1177 end
1178
1179 def login(conn, _) do
1180 with {:ok, app} <- get_or_make_app() do
1181 path =
1182 o_auth_path(
1183 conn,
1184 :authorize,
1185 response_type: "code",
1186 client_id: app.client_id,
1187 redirect_uri: ".",
1188 scope: Enum.join(app.scopes, " ")
1189 )
1190
1191 conn
1192 |> redirect(to: path)
1193 end
1194 end
1195
1196 defp get_or_make_app() do
1197 find_attrs = %{client_name: @local_mastodon_name, redirect_uris: "."}
1198
1199 with %App{} = app <- Repo.get_by(App, find_attrs) do
1200 {:ok, app}
1201 else
1202 _e ->
1203 cs =
1204 App.register_changeset(
1205 %App{},
1206 Map.put(find_attrs, :scopes, ["read", "write", "follow"])
1207 )
1208
1209 Repo.insert(cs)
1210 end
1211 end
1212
1213 def logout(conn, _) do
1214 conn
1215 |> clear_session
1216 |> redirect(to: "/")
1217 end
1218
1219 def relationship_noop(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1220 Logger.debug("Unimplemented, returning unmodified relationship")
1221
1222 with %User{} = target <- Repo.get(User, id) do
1223 conn
1224 |> put_view(AccountView)
1225 |> render("relationship.json", %{user: user, target: target})
1226 end
1227 end
1228
1229 def empty_array(conn, _) do
1230 Logger.debug("Unimplemented, returning an empty array")
1231 json(conn, [])
1232 end
1233
1234 def empty_object(conn, _) do
1235 Logger.debug("Unimplemented, returning an empty object")
1236 json(conn, %{})
1237 end
1238
1239 def render_notification(user, %{id: id, activity: activity, inserted_at: created_at} = _params) do
1240 actor = User.get_cached_by_ap_id(activity.data["actor"])
1241 parent_activity = Activity.get_create_by_object_ap_id(activity.data["object"])
1242 mastodon_type = Activity.mastodon_notification_type(activity)
1243
1244 response = %{
1245 id: to_string(id),
1246 type: mastodon_type,
1247 created_at: CommonAPI.Utils.to_masto_date(created_at),
1248 account: AccountView.render("account.json", %{user: actor, for: user})
1249 }
1250
1251 case mastodon_type do
1252 "mention" ->
1253 response
1254 |> Map.merge(%{
1255 status: StatusView.render("status.json", %{activity: activity, for: user})
1256 })
1257
1258 "favourite" ->
1259 response
1260 |> Map.merge(%{
1261 status: StatusView.render("status.json", %{activity: parent_activity, for: user})
1262 })
1263
1264 "reblog" ->
1265 response
1266 |> Map.merge(%{
1267 status: StatusView.render("status.json", %{activity: parent_activity, for: user})
1268 })
1269
1270 "follow" ->
1271 response
1272
1273 _ ->
1274 nil
1275 end
1276 end
1277
1278 def get_filters(%{assigns: %{user: user}} = conn, _) do
1279 filters = Filter.get_filters(user)
1280 res = FilterView.render("filters.json", filters: filters)
1281 json(conn, res)
1282 end
1283
1284 def create_filter(
1285 %{assigns: %{user: user}} = conn,
1286 %{"phrase" => phrase, "context" => context} = params
1287 ) do
1288 query = %Filter{
1289 user_id: user.id,
1290 phrase: phrase,
1291 context: context,
1292 hide: Map.get(params, "irreversible", nil),
1293 whole_word: Map.get(params, "boolean", true)
1294 # expires_at
1295 }
1296
1297 {:ok, response} = Filter.create(query)
1298 res = FilterView.render("filter.json", filter: response)
1299 json(conn, res)
1300 end
1301
1302 def get_filter(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do
1303 filter = Filter.get(filter_id, user)
1304 res = FilterView.render("filter.json", filter: filter)
1305 json(conn, res)
1306 end
1307
1308 def update_filter(
1309 %{assigns: %{user: user}} = conn,
1310 %{"phrase" => phrase, "context" => context, "id" => filter_id} = params
1311 ) do
1312 query = %Filter{
1313 user_id: user.id,
1314 filter_id: filter_id,
1315 phrase: phrase,
1316 context: context,
1317 hide: Map.get(params, "irreversible", nil),
1318 whole_word: Map.get(params, "boolean", true)
1319 # expires_at
1320 }
1321
1322 {:ok, response} = Filter.update(query)
1323 res = FilterView.render("filter.json", filter: response)
1324 json(conn, res)
1325 end
1326
1327 def delete_filter(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do
1328 query = %Filter{
1329 user_id: user.id,
1330 filter_id: filter_id
1331 }
1332
1333 {:ok, _} = Filter.delete(query)
1334 json(conn, %{})
1335 end
1336
1337 def create_push_subscription(%{assigns: %{user: user, token: token}} = conn, params) do
1338 true = Push.enabled()
1339 Subscription.delete_if_exists(user, token)
1340 {:ok, subscription} = Subscription.create(user, token, params)
1341 view = PushSubscriptionView.render("push_subscription.json", subscription: subscription)
1342 json(conn, view)
1343 end
1344
1345 def get_push_subscription(%{assigns: %{user: user, token: token}} = conn, _params) do
1346 true = Push.enabled()
1347 subscription = Subscription.get(user, token)
1348 view = PushSubscriptionView.render("push_subscription.json", subscription: subscription)
1349 json(conn, view)
1350 end
1351
1352 def update_push_subscription(
1353 %{assigns: %{user: user, token: token}} = conn,
1354 params
1355 ) do
1356 true = Push.enabled()
1357 {:ok, subscription} = Subscription.update(user, token, params)
1358 view = PushSubscriptionView.render("push_subscription.json", subscription: subscription)
1359 json(conn, view)
1360 end
1361
1362 def delete_push_subscription(%{assigns: %{user: user, token: token}} = conn, _params) do
1363 true = Push.enabled()
1364 {:ok, _response} = Subscription.delete(user, token)
1365 json(conn, %{})
1366 end
1367
1368 def errors(conn, _) do
1369 conn
1370 |> put_status(500)
1371 |> json("Something went wrong")
1372 end
1373
1374 def suggestions(%{assigns: %{user: user}} = conn, _) do
1375 suggestions = Config.get(:suggestions)
1376
1377 if Keyword.get(suggestions, :enabled, false) do
1378 api = Keyword.get(suggestions, :third_party_engine, "")
1379 timeout = Keyword.get(suggestions, :timeout, 5000)
1380 limit = Keyword.get(suggestions, :limit, 23)
1381
1382 host = Config.get([Pleroma.Web.Endpoint, :url, :host])
1383
1384 user = user.nickname
1385
1386 url =
1387 api
1388 |> String.replace("{{host}}", host)
1389 |> String.replace("{{user}}", user)
1390
1391 with {:ok, %{status: 200, body: body}} <-
1392 @httpoison.get(
1393 url,
1394 [],
1395 adapter: [
1396 timeout: timeout,
1397 recv_timeout: timeout,
1398 pool: :default
1399 ]
1400 ),
1401 {:ok, data} <- Jason.decode(body) do
1402 data =
1403 data
1404 |> Enum.slice(0, limit)
1405 |> Enum.map(fn x ->
1406 Map.put(
1407 x,
1408 "id",
1409 case User.get_or_fetch(x["acct"]) do
1410 %{id: id} -> id
1411 _ -> 0
1412 end
1413 )
1414 end)
1415 |> Enum.map(fn x ->
1416 Map.put(x, "avatar", MediaProxy.url(x["avatar"]))
1417 end)
1418 |> Enum.map(fn x ->
1419 Map.put(x, "avatar_static", MediaProxy.url(x["avatar_static"]))
1420 end)
1421
1422 conn
1423 |> json(data)
1424 else
1425 e -> Logger.error("Could not retrieve suggestions at fetch #{url}, #{inspect(e)}")
1426 end
1427 else
1428 json(conn, [])
1429 end
1430 end
1431
1432 def status_card(conn, %{"id" => status_id}) do
1433 with %Activity{} = activity <- Repo.get(Activity, status_id),
1434 true <- ActivityPub.is_public?(activity) do
1435 data =
1436 StatusView.render(
1437 "card.json",
1438 Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity)
1439 )
1440
1441 json(conn, data)
1442 else
1443 _e ->
1444 %{}
1445 end
1446 end
1447
1448 def try_render(conn, target, params)
1449 when is_binary(target) do
1450 res = render(conn, target, params)
1451
1452 if res == nil do
1453 conn
1454 |> put_status(501)
1455 |> json(%{error: "Can't display this activity"})
1456 else
1457 res
1458 end
1459 end
1460
1461 def try_render(conn, _, _) do
1462 conn
1463 |> put_status(501)
1464 |> json(%{error: "Can't display this activity"})
1465 end
1466
1467 defp present?(nil), do: false
1468 defp present?(false), do: false
1469 defp present?(_), do: true
1470 end