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