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