Merge branch 'bugfix/missing-announces' 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 = MastodonAPI.get_notifications(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 params =
948 params
949 |> Map.put("type", "Create")
950 |> Map.put("favorited_by", user.ap_id)
951 |> Map.put("blocking_user", user)
952
953 activities =
954 ActivityPub.fetch_activities([], params)
955 |> Enum.reverse()
956
957 conn
958 |> add_link_headers(:favourites, activities)
959 |> put_view(StatusView)
960 |> render("index.json", %{activities: activities, for: user, as: :activity})
961 end
962
963 def bookmarks(%{assigns: %{user: user}} = conn, _) do
964 user = Repo.get(User, user.id)
965
966 activities =
967 user.bookmarks
968 |> Enum.map(fn id -> Activity.get_create_by_object_ap_id(id) end)
969 |> Enum.reverse()
970
971 conn
972 |> put_view(StatusView)
973 |> render("index.json", %{activities: activities, for: user, as: :activity})
974 end
975
976 def get_lists(%{assigns: %{user: user}} = conn, opts) do
977 lists = Pleroma.List.for_user(user, opts)
978 res = ListView.render("lists.json", lists: lists)
979 json(conn, res)
980 end
981
982 def get_list(%{assigns: %{user: user}} = conn, %{"id" => id}) do
983 with %Pleroma.List{} = list <- Pleroma.List.get(id, user) do
984 res = ListView.render("list.json", list: list)
985 json(conn, res)
986 else
987 _e ->
988 conn
989 |> put_status(404)
990 |> json(%{error: "Record not found"})
991 end
992 end
993
994 def account_lists(%{assigns: %{user: user}} = conn, %{"id" => account_id}) do
995 lists = Pleroma.List.get_lists_account_belongs(user, account_id)
996 res = ListView.render("lists.json", lists: lists)
997 json(conn, res)
998 end
999
1000 def delete_list(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1001 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1002 {:ok, _list} <- Pleroma.List.delete(list) do
1003 json(conn, %{})
1004 else
1005 _e ->
1006 json(conn, "error")
1007 end
1008 end
1009
1010 def create_list(%{assigns: %{user: user}} = conn, %{"title" => title}) do
1011 with {:ok, %Pleroma.List{} = list} <- Pleroma.List.create(title, user) do
1012 res = ListView.render("list.json", list: list)
1013 json(conn, res)
1014 end
1015 end
1016
1017 def add_to_list(%{assigns: %{user: user}} = conn, %{"id" => id, "account_ids" => accounts}) do
1018 accounts
1019 |> Enum.each(fn account_id ->
1020 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1021 %User{} = followed <- Repo.get(User, account_id) do
1022 Pleroma.List.follow(list, followed)
1023 end
1024 end)
1025
1026 json(conn, %{})
1027 end
1028
1029 def remove_from_list(%{assigns: %{user: user}} = conn, %{"id" => id, "account_ids" => accounts}) do
1030 accounts
1031 |> Enum.each(fn account_id ->
1032 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1033 %User{} = followed <- Repo.get(Pleroma.User, account_id) do
1034 Pleroma.List.unfollow(list, followed)
1035 end
1036 end)
1037
1038 json(conn, %{})
1039 end
1040
1041 def list_accounts(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1042 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1043 {:ok, users} = Pleroma.List.get_following(list) do
1044 conn
1045 |> put_view(AccountView)
1046 |> render("accounts.json", %{users: users, as: :user})
1047 end
1048 end
1049
1050 def rename_list(%{assigns: %{user: user}} = conn, %{"id" => id, "title" => title}) do
1051 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1052 {:ok, list} <- Pleroma.List.rename(list, title) do
1053 res = ListView.render("list.json", list: list)
1054 json(conn, res)
1055 else
1056 _e ->
1057 json(conn, "error")
1058 end
1059 end
1060
1061 def list_timeline(%{assigns: %{user: user}} = conn, %{"list_id" => id} = params) do
1062 with %Pleroma.List{title: _title, following: following} <- Pleroma.List.get(id, user) do
1063 params =
1064 params
1065 |> Map.put("type", "Create")
1066 |> Map.put("blocking_user", user)
1067 |> Map.put("muting_user", user)
1068
1069 # we must filter the following list for the user to avoid leaking statuses the user
1070 # does not actually have permission to see (for more info, peruse security issue #270).
1071 activities =
1072 following
1073 |> Enum.filter(fn x -> x in user.following end)
1074 |> ActivityPub.fetch_activities_bounded(following, params)
1075 |> Enum.reverse()
1076
1077 conn
1078 |> put_view(StatusView)
1079 |> render("index.json", %{activities: activities, for: user, as: :activity})
1080 else
1081 _e ->
1082 conn
1083 |> put_status(403)
1084 |> json(%{error: "Error."})
1085 end
1086 end
1087
1088 def index(%{assigns: %{user: user}} = conn, _params) do
1089 token =
1090 conn
1091 |> get_session(:oauth_token)
1092
1093 if user && token do
1094 mastodon_emoji = mastodonized_emoji()
1095
1096 limit = Config.get([:instance, :limit])
1097
1098 accounts =
1099 Map.put(%{}, user.id, AccountView.render("account.json", %{user: user, for: user}))
1100
1101 flavour = get_user_flavour(user)
1102
1103 initial_state =
1104 %{
1105 meta: %{
1106 streaming_api_base_url:
1107 String.replace(Pleroma.Web.Endpoint.static_url(), "http", "ws"),
1108 access_token: token,
1109 locale: "en",
1110 domain: Pleroma.Web.Endpoint.host(),
1111 admin: "1",
1112 me: "#{user.id}",
1113 unfollow_modal: false,
1114 boost_modal: false,
1115 delete_modal: true,
1116 auto_play_gif: false,
1117 display_sensitive_media: false,
1118 reduce_motion: false,
1119 max_toot_chars: limit
1120 },
1121 rights: %{
1122 delete_others_notice: present?(user.info.is_moderator),
1123 admin: present?(user.info.is_admin)
1124 },
1125 compose: %{
1126 me: "#{user.id}",
1127 default_privacy: user.info.default_scope,
1128 default_sensitive: false,
1129 allow_content_types: Config.get([:instance, :allowed_post_formats])
1130 },
1131 media_attachments: %{
1132 accept_content_types: [
1133 ".jpg",
1134 ".jpeg",
1135 ".png",
1136 ".gif",
1137 ".webm",
1138 ".mp4",
1139 ".m4v",
1140 "image\/jpeg",
1141 "image\/png",
1142 "image\/gif",
1143 "video\/webm",
1144 "video\/mp4"
1145 ]
1146 },
1147 settings:
1148 user.info.settings ||
1149 %{
1150 onboarded: true,
1151 home: %{
1152 shows: %{
1153 reblog: true,
1154 reply: true
1155 }
1156 },
1157 notifications: %{
1158 alerts: %{
1159 follow: true,
1160 favourite: true,
1161 reblog: true,
1162 mention: true
1163 },
1164 shows: %{
1165 follow: true,
1166 favourite: true,
1167 reblog: true,
1168 mention: true
1169 },
1170 sounds: %{
1171 follow: true,
1172 favourite: true,
1173 reblog: true,
1174 mention: true
1175 }
1176 }
1177 },
1178 push_subscription: nil,
1179 accounts: accounts,
1180 custom_emojis: mastodon_emoji,
1181 char_limit: limit
1182 }
1183 |> Jason.encode!()
1184
1185 conn
1186 |> put_layout(false)
1187 |> put_view(MastodonView)
1188 |> render("index.html", %{initial_state: initial_state, flavour: flavour})
1189 else
1190 conn
1191 |> redirect(to: "/web/login")
1192 end
1193 end
1194
1195 def put_settings(%{assigns: %{user: user}} = conn, %{"data" => settings} = _params) do
1196 info_cng = User.Info.mastodon_settings_update(user.info, settings)
1197
1198 with changeset <- Ecto.Changeset.change(user),
1199 changeset <- Ecto.Changeset.put_embed(changeset, :info, info_cng),
1200 {:ok, _user} <- User.update_and_set_cache(changeset) do
1201 json(conn, %{})
1202 else
1203 e ->
1204 conn
1205 |> put_resp_content_type("application/json")
1206 |> send_resp(500, Jason.encode!(%{"error" => inspect(e)}))
1207 end
1208 end
1209
1210 @supported_flavours ["glitch", "vanilla"]
1211
1212 def set_flavour(%{assigns: %{user: user}} = conn, %{"flavour" => flavour} = _params)
1213 when flavour in @supported_flavours do
1214 flavour_cng = User.Info.mastodon_flavour_update(user.info, flavour)
1215
1216 with changeset <- Ecto.Changeset.change(user),
1217 changeset <- Ecto.Changeset.put_embed(changeset, :info, flavour_cng),
1218 {:ok, user} <- User.update_and_set_cache(changeset),
1219 flavour <- user.info.flavour do
1220 json(conn, flavour)
1221 else
1222 e ->
1223 conn
1224 |> put_resp_content_type("application/json")
1225 |> send_resp(500, Jason.encode!(%{"error" => inspect(e)}))
1226 end
1227 end
1228
1229 def set_flavour(conn, _params) do
1230 conn
1231 |> put_status(400)
1232 |> json(%{error: "Unsupported flavour"})
1233 end
1234
1235 def get_flavour(%{assigns: %{user: user}} = conn, _params) do
1236 json(conn, get_user_flavour(user))
1237 end
1238
1239 defp get_user_flavour(%User{info: %{flavour: flavour}}) when flavour in @supported_flavours do
1240 flavour
1241 end
1242
1243 defp get_user_flavour(_) do
1244 "glitch"
1245 end
1246
1247 def login(conn, %{"code" => code}) do
1248 with {:ok, app} <- get_or_make_app(),
1249 %Authorization{} = auth <- Repo.get_by(Authorization, token: code, app_id: app.id),
1250 {:ok, token} <- Token.exchange_token(app, auth) do
1251 conn
1252 |> put_session(:oauth_token, token.token)
1253 |> redirect(to: "/web/getting-started")
1254 end
1255 end
1256
1257 def login(conn, _) do
1258 with {:ok, app} <- get_or_make_app() do
1259 path =
1260 o_auth_path(
1261 conn,
1262 :authorize,
1263 response_type: "code",
1264 client_id: app.client_id,
1265 redirect_uri: ".",
1266 scope: Enum.join(app.scopes, " ")
1267 )
1268
1269 conn
1270 |> redirect(to: path)
1271 end
1272 end
1273
1274 defp get_or_make_app do
1275 find_attrs = %{client_name: @local_mastodon_name, redirect_uris: "."}
1276 scopes = ["read", "write", "follow", "push"]
1277
1278 with %App{} = app <- Repo.get_by(App, find_attrs) do
1279 {:ok, app} =
1280 if app.scopes == scopes do
1281 {:ok, app}
1282 else
1283 app
1284 |> Ecto.Changeset.change(%{scopes: scopes})
1285 |> Repo.update()
1286 end
1287
1288 {:ok, app}
1289 else
1290 _e ->
1291 cs =
1292 App.register_changeset(
1293 %App{},
1294 Map.put(find_attrs, :scopes, scopes)
1295 )
1296
1297 Repo.insert(cs)
1298 end
1299 end
1300
1301 def logout(conn, _) do
1302 conn
1303 |> clear_session
1304 |> redirect(to: "/")
1305 end
1306
1307 def relationship_noop(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1308 Logger.debug("Unimplemented, returning unmodified relationship")
1309
1310 with %User{} = target <- Repo.get(User, id) do
1311 conn
1312 |> put_view(AccountView)
1313 |> render("relationship.json", %{user: user, target: target})
1314 end
1315 end
1316
1317 def empty_array(conn, _) do
1318 Logger.debug("Unimplemented, returning an empty array")
1319 json(conn, [])
1320 end
1321
1322 def empty_object(conn, _) do
1323 Logger.debug("Unimplemented, returning an empty object")
1324 json(conn, %{})
1325 end
1326
1327 def get_filters(%{assigns: %{user: user}} = conn, _) do
1328 filters = Filter.get_filters(user)
1329 res = FilterView.render("filters.json", filters: filters)
1330 json(conn, res)
1331 end
1332
1333 def create_filter(
1334 %{assigns: %{user: user}} = conn,
1335 %{"phrase" => phrase, "context" => context} = params
1336 ) do
1337 query = %Filter{
1338 user_id: user.id,
1339 phrase: phrase,
1340 context: context,
1341 hide: Map.get(params, "irreversible", nil),
1342 whole_word: Map.get(params, "boolean", true)
1343 # expires_at
1344 }
1345
1346 {:ok, response} = Filter.create(query)
1347 res = FilterView.render("filter.json", filter: response)
1348 json(conn, res)
1349 end
1350
1351 def get_filter(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do
1352 filter = Filter.get(filter_id, user)
1353 res = FilterView.render("filter.json", filter: filter)
1354 json(conn, res)
1355 end
1356
1357 def update_filter(
1358 %{assigns: %{user: user}} = conn,
1359 %{"phrase" => phrase, "context" => context, "id" => filter_id} = params
1360 ) do
1361 query = %Filter{
1362 user_id: user.id,
1363 filter_id: filter_id,
1364 phrase: phrase,
1365 context: context,
1366 hide: Map.get(params, "irreversible", nil),
1367 whole_word: Map.get(params, "boolean", true)
1368 # expires_at
1369 }
1370
1371 {:ok, response} = Filter.update(query)
1372 res = FilterView.render("filter.json", filter: response)
1373 json(conn, res)
1374 end
1375
1376 def delete_filter(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do
1377 query = %Filter{
1378 user_id: user.id,
1379 filter_id: filter_id
1380 }
1381
1382 {:ok, _} = Filter.delete(query)
1383 json(conn, %{})
1384 end
1385
1386 # fallback action
1387 #
1388 def errors(conn, _) do
1389 conn
1390 |> put_status(500)
1391 |> json("Something went wrong")
1392 end
1393
1394 def suggestions(%{assigns: %{user: user}} = conn, _) do
1395 suggestions = Config.get(:suggestions)
1396
1397 if Keyword.get(suggestions, :enabled, false) do
1398 api = Keyword.get(suggestions, :third_party_engine, "")
1399 timeout = Keyword.get(suggestions, :timeout, 5000)
1400 limit = Keyword.get(suggestions, :limit, 23)
1401
1402 host = Config.get([Pleroma.Web.Endpoint, :url, :host])
1403
1404 user = user.nickname
1405
1406 url =
1407 api
1408 |> String.replace("{{host}}", host)
1409 |> String.replace("{{user}}", user)
1410
1411 with {:ok, %{status: 200, body: body}} <-
1412 @httpoison.get(
1413 url,
1414 [],
1415 adapter: [
1416 recv_timeout: timeout,
1417 pool: :default
1418 ]
1419 ),
1420 {:ok, data} <- Jason.decode(body) do
1421 data =
1422 data
1423 |> Enum.slice(0, limit)
1424 |> Enum.map(fn x ->
1425 Map.put(
1426 x,
1427 "id",
1428 case User.get_or_fetch(x["acct"]) do
1429 %{id: id} -> id
1430 _ -> 0
1431 end
1432 )
1433 end)
1434 |> Enum.map(fn x ->
1435 Map.put(x, "avatar", MediaProxy.url(x["avatar"]))
1436 end)
1437 |> Enum.map(fn x ->
1438 Map.put(x, "avatar_static", MediaProxy.url(x["avatar_static"]))
1439 end)
1440
1441 conn
1442 |> json(data)
1443 else
1444 e -> Logger.error("Could not retrieve suggestions at fetch #{url}, #{inspect(e)}")
1445 end
1446 else
1447 json(conn, [])
1448 end
1449 end
1450
1451 def status_card(%{assigns: %{user: user}} = conn, %{"id" => status_id}) do
1452 with %Activity{} = activity <- Repo.get(Activity, status_id),
1453 true <- Visibility.visible_for_user?(activity, user) do
1454 data =
1455 StatusView.render(
1456 "card.json",
1457 Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity)
1458 )
1459
1460 json(conn, data)
1461 else
1462 _e ->
1463 %{}
1464 end
1465 end
1466
1467 def reports(%{assigns: %{user: user}} = conn, params) do
1468 case CommonAPI.report(user, params) do
1469 {:ok, activity} ->
1470 conn
1471 |> put_view(ReportView)
1472 |> try_render("report.json", %{activity: activity})
1473
1474 {:error, err} ->
1475 conn
1476 |> put_status(:bad_request)
1477 |> json(%{error: err})
1478 end
1479 end
1480
1481 def try_render(conn, target, params)
1482 when is_binary(target) do
1483 res = render(conn, target, params)
1484
1485 if res == nil do
1486 conn
1487 |> put_status(501)
1488 |> json(%{error: "Can't display this activity"})
1489 else
1490 res
1491 end
1492 end
1493
1494 def try_render(conn, _, _) do
1495 conn
1496 |> put_status(501)
1497 |> json(%{error: "Can't display this activity"})
1498 end
1499
1500 defp present?(nil), do: false
1501 defp present?(false), do: false
1502 defp present?(_), do: true
1503 end