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