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