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