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