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