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