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