Merge branch 'notifications-is-seen' 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
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 {:ok, follower, followed, _} <- CommonAPI.follow(follower, followed) do
727 conn
728 |> put_view(AccountView)
729 |> render("relationship.json", %{user: follower, target: followed})
730 else
731 {:error, message} ->
732 conn
733 |> put_resp_content_type("application/json")
734 |> send_resp(403, Jason.encode!(%{"error" => message}))
735 end
736 end
737
738 def follow(%{assigns: %{user: follower}} = conn, %{"uri" => uri}) do
739 with %User{} = followed <- Repo.get_by(User, nickname: uri),
740 {:ok, follower, followed, _} <- CommonAPI.follow(follower, followed) do
741 conn
742 |> put_view(AccountView)
743 |> render("account.json", %{user: followed, for: follower})
744 else
745 {:error, message} ->
746 conn
747 |> put_resp_content_type("application/json")
748 |> send_resp(403, Jason.encode!(%{"error" => message}))
749 end
750 end
751
752 def unfollow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
753 with %User{} = followed <- Repo.get(User, id),
754 {:ok, follower} <- CommonAPI.unfollow(follower, followed) do
755 conn
756 |> put_view(AccountView)
757 |> render("relationship.json", %{user: follower, target: followed})
758 end
759 end
760
761 def mute(%{assigns: %{user: muter}} = conn, %{"id" => id}) do
762 with %User{} = muted <- Repo.get(User, id),
763 {:ok, muter} <- User.mute(muter, muted) do
764 conn
765 |> put_view(AccountView)
766 |> render("relationship.json", %{user: muter, target: muted})
767 else
768 {:error, message} ->
769 conn
770 |> put_resp_content_type("application/json")
771 |> send_resp(403, Jason.encode!(%{"error" => message}))
772 end
773 end
774
775 def unmute(%{assigns: %{user: muter}} = conn, %{"id" => id}) do
776 with %User{} = muted <- Repo.get(User, id),
777 {:ok, muter} <- User.unmute(muter, muted) do
778 conn
779 |> put_view(AccountView)
780 |> render("relationship.json", %{user: muter, target: muted})
781 else
782 {:error, message} ->
783 conn
784 |> put_resp_content_type("application/json")
785 |> send_resp(403, Jason.encode!(%{"error" => message}))
786 end
787 end
788
789 def mutes(%{assigns: %{user: user}} = conn, _) do
790 with muted_accounts <- User.muted_users(user) do
791 res = AccountView.render("accounts.json", users: muted_accounts, for: user, as: :user)
792 json(conn, res)
793 end
794 end
795
796 def block(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
797 with %User{} = blocked <- Repo.get(User, id),
798 {:ok, blocker} <- User.block(blocker, blocked),
799 {:ok, _activity} <- ActivityPub.block(blocker, blocked) do
800 conn
801 |> put_view(AccountView)
802 |> render("relationship.json", %{user: blocker, target: blocked})
803 else
804 {:error, message} ->
805 conn
806 |> put_resp_content_type("application/json")
807 |> send_resp(403, Jason.encode!(%{"error" => message}))
808 end
809 end
810
811 def unblock(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
812 with %User{} = blocked <- Repo.get(User, id),
813 {:ok, blocker} <- User.unblock(blocker, blocked),
814 {:ok, _activity} <- ActivityPub.unblock(blocker, blocked) do
815 conn
816 |> put_view(AccountView)
817 |> render("relationship.json", %{user: blocker, target: blocked})
818 else
819 {:error, message} ->
820 conn
821 |> put_resp_content_type("application/json")
822 |> send_resp(403, Jason.encode!(%{"error" => message}))
823 end
824 end
825
826 def blocks(%{assigns: %{user: user}} = conn, _) do
827 with blocked_accounts <- User.blocked_users(user) do
828 res = AccountView.render("accounts.json", users: blocked_accounts, for: user, as: :user)
829 json(conn, res)
830 end
831 end
832
833 def domain_blocks(%{assigns: %{user: %{info: info}}} = conn, _) do
834 json(conn, info.domain_blocks || [])
835 end
836
837 def block_domain(%{assigns: %{user: blocker}} = conn, %{"domain" => domain}) do
838 User.block_domain(blocker, domain)
839 json(conn, %{})
840 end
841
842 def unblock_domain(%{assigns: %{user: blocker}} = conn, %{"domain" => domain}) do
843 User.unblock_domain(blocker, domain)
844 json(conn, %{})
845 end
846
847 def status_search(user, query) do
848 fetched =
849 if Regex.match?(~r/https?:/, query) do
850 with {:ok, object} <- ActivityPub.fetch_object_from_id(query),
851 %Activity{} = activity <- Activity.get_create_by_object_ap_id(object.data["id"]),
852 true <- Visibility.visible_for_user?(activity, user) do
853 [activity]
854 else
855 _e -> []
856 end
857 end || []
858
859 q =
860 from(
861 a in Activity,
862 where: fragment("?->>'type' = 'Create'", a.data),
863 where: "https://www.w3.org/ns/activitystreams#Public" in a.recipients,
864 where:
865 fragment(
866 "to_tsvector('english', ?->'object'->>'content') @@ plainto_tsquery('english', ?)",
867 a.data,
868 ^query
869 ),
870 limit: 20,
871 order_by: [desc: :id]
872 )
873
874 Repo.all(q) ++ fetched
875 end
876
877 def search2(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do
878 accounts = User.search(query, resolve: params["resolve"] == "true", for_user: user)
879
880 statuses = status_search(user, query)
881
882 tags_path = Web.base_url() <> "/tag/"
883
884 tags =
885 query
886 |> String.split()
887 |> Enum.uniq()
888 |> Enum.filter(fn tag -> String.starts_with?(tag, "#") end)
889 |> Enum.map(fn tag -> String.slice(tag, 1..-1) end)
890 |> Enum.map(fn tag -> %{name: tag, url: tags_path <> tag} end)
891
892 res = %{
893 "accounts" => AccountView.render("accounts.json", users: accounts, for: user, as: :user),
894 "statuses" =>
895 StatusView.render("index.json", activities: statuses, for: user, as: :activity),
896 "hashtags" => tags
897 }
898
899 json(conn, res)
900 end
901
902 def search(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do
903 accounts = User.search(query, resolve: params["resolve"] == "true", for_user: user)
904
905 statuses = status_search(user, query)
906
907 tags =
908 query
909 |> String.split()
910 |> Enum.uniq()
911 |> Enum.filter(fn tag -> String.starts_with?(tag, "#") end)
912 |> Enum.map(fn tag -> String.slice(tag, 1..-1) end)
913
914 res = %{
915 "accounts" => AccountView.render("accounts.json", users: accounts, for: user, as: :user),
916 "statuses" =>
917 StatusView.render("index.json", activities: statuses, for: user, as: :activity),
918 "hashtags" => tags
919 }
920
921 json(conn, res)
922 end
923
924 def account_search(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do
925 accounts = User.search(query, resolve: params["resolve"] == "true", for_user: user)
926
927 res = AccountView.render("accounts.json", users: accounts, for: user, as: :user)
928
929 json(conn, res)
930 end
931
932 def favourites(%{assigns: %{user: user}} = conn, params) do
933 activities =
934 params
935 |> Map.put("type", "Create")
936 |> Map.put("favorited_by", user.ap_id)
937 |> Map.put("blocking_user", user)
938 |> ActivityPub.fetch_public_activities()
939 |> Enum.reverse()
940
941 conn
942 |> add_link_headers(:favourites, activities)
943 |> put_view(StatusView)
944 |> render("index.json", %{activities: activities, for: user, as: :activity})
945 end
946
947 def bookmarks(%{assigns: %{user: user}} = conn, _) do
948 user = Repo.get(User, user.id)
949
950 activities =
951 user.bookmarks
952 |> Enum.map(fn id -> Activity.get_create_by_object_ap_id(id) end)
953 |> Enum.reverse()
954
955 conn
956 |> put_view(StatusView)
957 |> render("index.json", %{activities: activities, for: user, as: :activity})
958 end
959
960 def get_lists(%{assigns: %{user: user}} = conn, opts) do
961 lists = Pleroma.List.for_user(user, opts)
962 res = ListView.render("lists.json", lists: lists)
963 json(conn, res)
964 end
965
966 def get_list(%{assigns: %{user: user}} = conn, %{"id" => id}) do
967 with %Pleroma.List{} = list <- Pleroma.List.get(id, user) do
968 res = ListView.render("list.json", list: list)
969 json(conn, res)
970 else
971 _e ->
972 conn
973 |> put_status(404)
974 |> json(%{error: "Record not found"})
975 end
976 end
977
978 def account_lists(%{assigns: %{user: user}} = conn, %{"id" => account_id}) do
979 lists = Pleroma.List.get_lists_account_belongs(user, account_id)
980 res = ListView.render("lists.json", lists: lists)
981 json(conn, res)
982 end
983
984 def delete_list(%{assigns: %{user: user}} = conn, %{"id" => id}) do
985 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
986 {:ok, _list} <- Pleroma.List.delete(list) do
987 json(conn, %{})
988 else
989 _e ->
990 json(conn, "error")
991 end
992 end
993
994 def create_list(%{assigns: %{user: user}} = conn, %{"title" => title}) do
995 with {:ok, %Pleroma.List{} = list} <- Pleroma.List.create(title, user) do
996 res = ListView.render("list.json", list: list)
997 json(conn, res)
998 end
999 end
1000
1001 def add_to_list(%{assigns: %{user: user}} = conn, %{"id" => id, "account_ids" => accounts}) do
1002 accounts
1003 |> Enum.each(fn account_id ->
1004 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1005 %User{} = followed <- Repo.get(User, account_id) do
1006 Pleroma.List.follow(list, followed)
1007 end
1008 end)
1009
1010 json(conn, %{})
1011 end
1012
1013 def remove_from_list(%{assigns: %{user: user}} = conn, %{"id" => id, "account_ids" => accounts}) do
1014 accounts
1015 |> Enum.each(fn account_id ->
1016 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1017 %User{} = followed <- Repo.get(Pleroma.User, account_id) do
1018 Pleroma.List.unfollow(list, followed)
1019 end
1020 end)
1021
1022 json(conn, %{})
1023 end
1024
1025 def list_accounts(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1026 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1027 {:ok, users} = Pleroma.List.get_following(list) do
1028 conn
1029 |> put_view(AccountView)
1030 |> render("accounts.json", %{users: users, as: :user})
1031 end
1032 end
1033
1034 def rename_list(%{assigns: %{user: user}} = conn, %{"id" => id, "title" => title}) do
1035 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1036 {:ok, list} <- Pleroma.List.rename(list, title) do
1037 res = ListView.render("list.json", list: list)
1038 json(conn, res)
1039 else
1040 _e ->
1041 json(conn, "error")
1042 end
1043 end
1044
1045 def list_timeline(%{assigns: %{user: user}} = conn, %{"list_id" => id} = params) do
1046 with %Pleroma.List{title: _title, following: following} <- Pleroma.List.get(id, user) do
1047 params =
1048 params
1049 |> Map.put("type", "Create")
1050 |> Map.put("blocking_user", user)
1051 |> Map.put("muting_user", user)
1052
1053 # we must filter the following list for the user to avoid leaking statuses the user
1054 # does not actually have permission to see (for more info, peruse security issue #270).
1055 activities =
1056 following
1057 |> Enum.filter(fn x -> x in user.following end)
1058 |> ActivityPub.fetch_activities_bounded(following, params)
1059 |> Enum.reverse()
1060
1061 conn
1062 |> put_view(StatusView)
1063 |> render("index.json", %{activities: activities, for: user, as: :activity})
1064 else
1065 _e ->
1066 conn
1067 |> put_status(403)
1068 |> json(%{error: "Error."})
1069 end
1070 end
1071
1072 def index(%{assigns: %{user: user}} = conn, _params) do
1073 token =
1074 conn
1075 |> get_session(:oauth_token)
1076
1077 if user && token do
1078 mastodon_emoji = mastodonized_emoji()
1079
1080 limit = Config.get([:instance, :limit])
1081
1082 accounts =
1083 Map.put(%{}, user.id, AccountView.render("account.json", %{user: user, for: user}))
1084
1085 flavour = get_user_flavour(user)
1086
1087 initial_state =
1088 %{
1089 meta: %{
1090 streaming_api_base_url:
1091 String.replace(Pleroma.Web.Endpoint.static_url(), "http", "ws"),
1092 access_token: token,
1093 locale: "en",
1094 domain: Pleroma.Web.Endpoint.host(),
1095 admin: "1",
1096 me: "#{user.id}",
1097 unfollow_modal: false,
1098 boost_modal: false,
1099 delete_modal: true,
1100 auto_play_gif: false,
1101 display_sensitive_media: false,
1102 reduce_motion: false,
1103 max_toot_chars: limit
1104 },
1105 rights: %{
1106 delete_others_notice: present?(user.info.is_moderator),
1107 admin: present?(user.info.is_admin)
1108 },
1109 compose: %{
1110 me: "#{user.id}",
1111 default_privacy: user.info.default_scope,
1112 default_sensitive: false,
1113 allow_content_types: Config.get([:instance, :allowed_post_formats])
1114 },
1115 media_attachments: %{
1116 accept_content_types: [
1117 ".jpg",
1118 ".jpeg",
1119 ".png",
1120 ".gif",
1121 ".webm",
1122 ".mp4",
1123 ".m4v",
1124 "image\/jpeg",
1125 "image\/png",
1126 "image\/gif",
1127 "video\/webm",
1128 "video\/mp4"
1129 ]
1130 },
1131 settings:
1132 user.info.settings ||
1133 %{
1134 onboarded: true,
1135 home: %{
1136 shows: %{
1137 reblog: true,
1138 reply: true
1139 }
1140 },
1141 notifications: %{
1142 alerts: %{
1143 follow: true,
1144 favourite: true,
1145 reblog: true,
1146 mention: true
1147 },
1148 shows: %{
1149 follow: true,
1150 favourite: true,
1151 reblog: true,
1152 mention: true
1153 },
1154 sounds: %{
1155 follow: true,
1156 favourite: true,
1157 reblog: true,
1158 mention: true
1159 }
1160 }
1161 },
1162 push_subscription: nil,
1163 accounts: accounts,
1164 custom_emojis: mastodon_emoji,
1165 char_limit: limit
1166 }
1167 |> Jason.encode!()
1168
1169 conn
1170 |> put_layout(false)
1171 |> put_view(MastodonView)
1172 |> render("index.html", %{initial_state: initial_state, flavour: flavour})
1173 else
1174 conn
1175 |> redirect(to: "/web/login")
1176 end
1177 end
1178
1179 def put_settings(%{assigns: %{user: user}} = conn, %{"data" => settings} = _params) do
1180 info_cng = User.Info.mastodon_settings_update(user.info, settings)
1181
1182 with changeset <- Ecto.Changeset.change(user),
1183 changeset <- Ecto.Changeset.put_embed(changeset, :info, info_cng),
1184 {:ok, _user} <- User.update_and_set_cache(changeset) do
1185 json(conn, %{})
1186 else
1187 e ->
1188 conn
1189 |> put_resp_content_type("application/json")
1190 |> send_resp(500, Jason.encode!(%{"error" => inspect(e)}))
1191 end
1192 end
1193
1194 @supported_flavours ["glitch", "vanilla"]
1195
1196 def set_flavour(%{assigns: %{user: user}} = conn, %{"flavour" => flavour} = _params)
1197 when flavour in @supported_flavours do
1198 flavour_cng = User.Info.mastodon_flavour_update(user.info, flavour)
1199
1200 with changeset <- Ecto.Changeset.change(user),
1201 changeset <- Ecto.Changeset.put_embed(changeset, :info, flavour_cng),
1202 {:ok, user} <- User.update_and_set_cache(changeset),
1203 flavour <- user.info.flavour do
1204 json(conn, flavour)
1205 else
1206 e ->
1207 conn
1208 |> put_resp_content_type("application/json")
1209 |> send_resp(500, Jason.encode!(%{"error" => inspect(e)}))
1210 end
1211 end
1212
1213 def set_flavour(conn, _params) do
1214 conn
1215 |> put_status(400)
1216 |> json(%{error: "Unsupported flavour"})
1217 end
1218
1219 def get_flavour(%{assigns: %{user: user}} = conn, _params) do
1220 json(conn, get_user_flavour(user))
1221 end
1222
1223 defp get_user_flavour(%User{info: %{flavour: flavour}}) when flavour in @supported_flavours do
1224 flavour
1225 end
1226
1227 defp get_user_flavour(_) do
1228 "glitch"
1229 end
1230
1231 def login(conn, %{"code" => code}) do
1232 with {:ok, app} <- get_or_make_app(),
1233 %Authorization{} = auth <- Repo.get_by(Authorization, token: code, app_id: app.id),
1234 {:ok, token} <- Token.exchange_token(app, auth) do
1235 conn
1236 |> put_session(:oauth_token, token.token)
1237 |> redirect(to: "/web/getting-started")
1238 end
1239 end
1240
1241 def login(conn, _) do
1242 with {:ok, app} <- get_or_make_app() do
1243 path =
1244 o_auth_path(
1245 conn,
1246 :authorize,
1247 response_type: "code",
1248 client_id: app.client_id,
1249 redirect_uri: ".",
1250 scope: Enum.join(app.scopes, " ")
1251 )
1252
1253 conn
1254 |> redirect(to: path)
1255 end
1256 end
1257
1258 defp get_or_make_app do
1259 find_attrs = %{client_name: @local_mastodon_name, redirect_uris: "."}
1260 scopes = ["read", "write", "follow", "push"]
1261
1262 with %App{} = app <- Repo.get_by(App, find_attrs) do
1263 {:ok, app} =
1264 if app.scopes == scopes do
1265 {:ok, app}
1266 else
1267 app
1268 |> Ecto.Changeset.change(%{scopes: scopes})
1269 |> Repo.update()
1270 end
1271
1272 {:ok, app}
1273 else
1274 _e ->
1275 cs =
1276 App.register_changeset(
1277 %App{},
1278 Map.put(find_attrs, :scopes, scopes)
1279 )
1280
1281 Repo.insert(cs)
1282 end
1283 end
1284
1285 def logout(conn, _) do
1286 conn
1287 |> clear_session
1288 |> redirect(to: "/")
1289 end
1290
1291 def relationship_noop(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1292 Logger.debug("Unimplemented, returning unmodified relationship")
1293
1294 with %User{} = target <- Repo.get(User, id) do
1295 conn
1296 |> put_view(AccountView)
1297 |> render("relationship.json", %{user: user, target: target})
1298 end
1299 end
1300
1301 def empty_array(conn, _) do
1302 Logger.debug("Unimplemented, returning an empty array")
1303 json(conn, [])
1304 end
1305
1306 def empty_object(conn, _) do
1307 Logger.debug("Unimplemented, returning an empty object")
1308 json(conn, %{})
1309 end
1310
1311 def get_filters(%{assigns: %{user: user}} = conn, _) do
1312 filters = Filter.get_filters(user)
1313 res = FilterView.render("filters.json", filters: filters)
1314 json(conn, res)
1315 end
1316
1317 def create_filter(
1318 %{assigns: %{user: user}} = conn,
1319 %{"phrase" => phrase, "context" => context} = params
1320 ) do
1321 query = %Filter{
1322 user_id: user.id,
1323 phrase: phrase,
1324 context: context,
1325 hide: Map.get(params, "irreversible", nil),
1326 whole_word: Map.get(params, "boolean", true)
1327 # expires_at
1328 }
1329
1330 {:ok, response} = Filter.create(query)
1331 res = FilterView.render("filter.json", filter: response)
1332 json(conn, res)
1333 end
1334
1335 def get_filter(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do
1336 filter = Filter.get(filter_id, user)
1337 res = FilterView.render("filter.json", filter: filter)
1338 json(conn, res)
1339 end
1340
1341 def update_filter(
1342 %{assigns: %{user: user}} = conn,
1343 %{"phrase" => phrase, "context" => context, "id" => filter_id} = params
1344 ) do
1345 query = %Filter{
1346 user_id: user.id,
1347 filter_id: filter_id,
1348 phrase: phrase,
1349 context: context,
1350 hide: Map.get(params, "irreversible", nil),
1351 whole_word: Map.get(params, "boolean", true)
1352 # expires_at
1353 }
1354
1355 {:ok, response} = Filter.update(query)
1356 res = FilterView.render("filter.json", filter: response)
1357 json(conn, res)
1358 end
1359
1360 def delete_filter(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do
1361 query = %Filter{
1362 user_id: user.id,
1363 filter_id: filter_id
1364 }
1365
1366 {:ok, _} = Filter.delete(query)
1367 json(conn, %{})
1368 end
1369
1370 # fallback action
1371 #
1372 def errors(conn, _) do
1373 conn
1374 |> put_status(500)
1375 |> json("Something went wrong")
1376 end
1377
1378 def suggestions(%{assigns: %{user: user}} = conn, _) do
1379 suggestions = Config.get(:suggestions)
1380
1381 if Keyword.get(suggestions, :enabled, false) do
1382 api = Keyword.get(suggestions, :third_party_engine, "")
1383 timeout = Keyword.get(suggestions, :timeout, 5000)
1384 limit = Keyword.get(suggestions, :limit, 23)
1385
1386 host = Config.get([Pleroma.Web.Endpoint, :url, :host])
1387
1388 user = user.nickname
1389
1390 url =
1391 api
1392 |> String.replace("{{host}}", host)
1393 |> String.replace("{{user}}", user)
1394
1395 with {:ok, %{status: 200, body: body}} <-
1396 @httpoison.get(
1397 url,
1398 [],
1399 adapter: [
1400 recv_timeout: timeout,
1401 pool: :default
1402 ]
1403 ),
1404 {:ok, data} <- Jason.decode(body) do
1405 data =
1406 data
1407 |> Enum.slice(0, limit)
1408 |> Enum.map(fn x ->
1409 Map.put(
1410 x,
1411 "id",
1412 case User.get_or_fetch(x["acct"]) do
1413 %{id: id} -> id
1414 _ -> 0
1415 end
1416 )
1417 end)
1418 |> Enum.map(fn x ->
1419 Map.put(x, "avatar", MediaProxy.url(x["avatar"]))
1420 end)
1421 |> Enum.map(fn x ->
1422 Map.put(x, "avatar_static", MediaProxy.url(x["avatar_static"]))
1423 end)
1424
1425 conn
1426 |> json(data)
1427 else
1428 e -> Logger.error("Could not retrieve suggestions at fetch #{url}, #{inspect(e)}")
1429 end
1430 else
1431 json(conn, [])
1432 end
1433 end
1434
1435 def status_card(%{assigns: %{user: user}} = conn, %{"id" => status_id}) do
1436 with %Activity{} = activity <- Repo.get(Activity, status_id),
1437 true <- Visibility.visible_for_user?(activity, user) do
1438 data =
1439 StatusView.render(
1440 "card.json",
1441 Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity)
1442 )
1443
1444 json(conn, data)
1445 else
1446 _e ->
1447 %{}
1448 end
1449 end
1450
1451 def reports(%{assigns: %{user: user}} = conn, params) do
1452 case CommonAPI.report(user, params) do
1453 {:ok, activity} ->
1454 conn
1455 |> put_view(ReportView)
1456 |> try_render("report.json", %{activity: activity})
1457
1458 {:error, err} ->
1459 conn
1460 |> put_status(:bad_request)
1461 |> json(%{error: err})
1462 end
1463 end
1464
1465 def try_render(conn, target, params)
1466 when is_binary(target) do
1467 res = render(conn, target, params)
1468
1469 if res == nil do
1470 conn
1471 |> put_status(501)
1472 |> json(%{error: "Can't display this activity"})
1473 else
1474 res
1475 end
1476 end
1477
1478 def try_render(conn, _, _) do
1479 conn
1480 |> put_status(501)
1481 |> json(%{error: "Can't display this activity"})
1482 end
1483
1484 defp present?(nil), do: false
1485 defp present?(false), do: false
1486 defp present?(_), do: true
1487 end