Merge branch 'fix/credo-issues' 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 notifications(%{assigns: %{user: user}} = conn, params) do
460 notifications = Notification.for_user(user, params)
461
462 result =
463 notifications
464 |> Enum.map(fn x -> render_notification(user, x) end)
465 |> Enum.filter(& &1)
466
467 conn
468 |> add_link_headers(:notifications, notifications)
469 |> json(result)
470 end
471
472 def get_notification(%{assigns: %{user: user}} = conn, %{"id" => id} = _params) do
473 with {:ok, notification} <- Notification.get(user, id) do
474 json(conn, render_notification(user, notification))
475 else
476 {:error, reason} ->
477 conn
478 |> put_resp_content_type("application/json")
479 |> send_resp(403, Jason.encode!(%{"error" => reason}))
480 end
481 end
482
483 def clear_notifications(%{assigns: %{user: user}} = conn, _params) do
484 Notification.clear(user)
485 json(conn, %{})
486 end
487
488 def dismiss_notification(%{assigns: %{user: user}} = conn, %{"id" => id} = _params) do
489 with {:ok, _notif} <- Notification.dismiss(user, id) do
490 json(conn, %{})
491 else
492 {:error, reason} ->
493 conn
494 |> put_resp_content_type("application/json")
495 |> send_resp(403, Jason.encode!(%{"error" => reason}))
496 end
497 end
498
499 def relationships(%{assigns: %{user: user}} = conn, %{"id" => id}) do
500 id = List.wrap(id)
501 q = from(u in User, where: u.id in ^id)
502 targets = Repo.all(q)
503
504 conn
505 |> put_view(AccountView)
506 |> render("relationships.json", %{user: user, targets: targets})
507 end
508
509 # Instead of returning a 400 when no "id" params is present, Mastodon returns an empty array.
510 def relationships(%{assigns: %{user: _user}} = conn, _), do: json(conn, [])
511
512 def update_media(%{assigns: %{user: user}} = conn, data) do
513 with %Object{} = object <- Repo.get(Object, data["id"]),
514 true <- Object.authorize_mutation(object, user),
515 true <- is_binary(data["description"]),
516 description <- data["description"] do
517 new_data = %{object.data | "name" => description}
518
519 {:ok, _} =
520 object
521 |> Object.change(%{data: new_data})
522 |> Repo.update()
523
524 attachment_data = Map.put(new_data, "id", object.id)
525
526 conn
527 |> put_view(StatusView)
528 |> render("attachment.json", %{attachment: attachment_data})
529 end
530 end
531
532 def upload(%{assigns: %{user: user}} = conn, %{"file" => file} = data) do
533 with {:ok, object} <-
534 ActivityPub.upload(
535 file,
536 actor: User.ap_id(user),
537 description: Map.get(data, "description")
538 ) do
539 attachment_data = Map.put(object.data, "id", object.id)
540
541 conn
542 |> put_view(StatusView)
543 |> render("attachment.json", %{attachment: attachment_data})
544 end
545 end
546
547 def favourited_by(conn, %{"id" => id}) do
548 with %Activity{data: %{"object" => %{"likes" => likes}}} <- Repo.get(Activity, id) do
549 q = from(u in User, where: u.ap_id in ^likes)
550 users = Repo.all(q)
551
552 conn
553 |> put_view(AccountView)
554 |> render(AccountView, "accounts.json", %{users: users, as: :user})
555 else
556 _ -> json(conn, [])
557 end
558 end
559
560 def reblogged_by(conn, %{"id" => id}) do
561 with %Activity{data: %{"object" => %{"announcements" => announces}}} <- Repo.get(Activity, id) do
562 q = from(u in User, where: u.ap_id in ^announces)
563 users = Repo.all(q)
564
565 conn
566 |> put_view(AccountView)
567 |> render("accounts.json", %{users: users, as: :user})
568 else
569 _ -> json(conn, [])
570 end
571 end
572
573 def hashtag_timeline(%{assigns: %{user: user}} = conn, params) do
574 local_only = params["local"] in [true, "True", "true", "1"]
575
576 tags =
577 [params["tag"], params["any"]]
578 |> List.flatten()
579 |> Enum.uniq()
580 |> Enum.filter(& &1)
581 |> Enum.map(&String.downcase(&1))
582
583 tag_all =
584 params["all"] ||
585 []
586 |> Enum.map(&String.downcase(&1))
587
588 tag_reject =
589 params["none"] ||
590 []
591 |> Enum.map(&String.downcase(&1))
592
593 activities =
594 params
595 |> Map.put("type", "Create")
596 |> Map.put("local_only", local_only)
597 |> Map.put("blocking_user", user)
598 |> Map.put("tag", tags)
599 |> Map.put("tag_all", tag_all)
600 |> Map.put("tag_reject", tag_reject)
601 |> ActivityPub.fetch_public_activities()
602 |> Enum.reverse()
603
604 conn
605 |> add_link_headers(:hashtag_timeline, activities, params["tag"], %{"local" => local_only})
606 |> put_view(StatusView)
607 |> render("index.json", %{activities: activities, for: user, as: :activity})
608 end
609
610 def followers(%{assigns: %{user: for_user}} = conn, %{"id" => id}) do
611 with %User{} = user <- Repo.get(User, id),
612 {:ok, followers} <- User.get_followers(user) do
613 followers =
614 cond do
615 for_user && user.id == for_user.id -> followers
616 user.info.hide_followers -> []
617 true -> followers
618 end
619
620 conn
621 |> put_view(AccountView)
622 |> render("accounts.json", %{users: followers, as: :user})
623 end
624 end
625
626 def following(%{assigns: %{user: for_user}} = conn, %{"id" => id}) do
627 with %User{} = user <- Repo.get(User, id),
628 {:ok, followers} <- User.get_friends(user) do
629 followers =
630 cond do
631 for_user && user.id == for_user.id -> followers
632 user.info.hide_follows -> []
633 true -> followers
634 end
635
636 conn
637 |> put_view(AccountView)
638 |> render("accounts.json", %{users: followers, as: :user})
639 end
640 end
641
642 def follow_requests(%{assigns: %{user: followed}} = conn, _params) do
643 with {:ok, follow_requests} <- User.get_follow_requests(followed) do
644 conn
645 |> put_view(AccountView)
646 |> render("accounts.json", %{users: follow_requests, as: :user})
647 end
648 end
649
650 def authorize_follow_request(%{assigns: %{user: followed}} = conn, %{"id" => id}) do
651 with %User{} = follower <- Repo.get(User, id),
652 {:ok, follower} <- User.maybe_follow(follower, followed),
653 %Activity{} = follow_activity <- Utils.fetch_latest_follow(follower, followed),
654 {:ok, follow_activity} <- Utils.update_follow_state(follow_activity, "accept"),
655 {:ok, _activity} <-
656 ActivityPub.accept(%{
657 to: [follower.ap_id],
658 actor: followed.ap_id,
659 object: follow_activity.data["id"],
660 type: "Accept"
661 }) do
662 conn
663 |> put_view(AccountView)
664 |> render("relationship.json", %{user: followed, target: follower})
665 else
666 {:error, message} ->
667 conn
668 |> put_resp_content_type("application/json")
669 |> send_resp(403, Jason.encode!(%{"error" => message}))
670 end
671 end
672
673 def reject_follow_request(%{assigns: %{user: followed}} = conn, %{"id" => id}) do
674 with %User{} = follower <- Repo.get(User, id),
675 %Activity{} = follow_activity <- Utils.fetch_latest_follow(follower, followed),
676 {:ok, follow_activity} <- Utils.update_follow_state(follow_activity, "reject"),
677 {:ok, _activity} <-
678 ActivityPub.reject(%{
679 to: [follower.ap_id],
680 actor: followed.ap_id,
681 object: follow_activity.data["id"],
682 type: "Reject"
683 }) do
684 conn
685 |> put_view(AccountView)
686 |> render("relationship.json", %{user: followed, target: follower})
687 else
688 {:error, message} ->
689 conn
690 |> put_resp_content_type("application/json")
691 |> send_resp(403, Jason.encode!(%{"error" => message}))
692 end
693 end
694
695 def follow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
696 with %User{} = followed <- Repo.get(User, id),
697 {:ok, follower} <- User.maybe_direct_follow(follower, followed),
698 {:ok, _activity} <- ActivityPub.follow(follower, followed),
699 {:ok, follower, followed} <-
700 User.wait_and_refresh(
701 Config.get([:activitypub, :follow_handshake_timeout]),
702 follower,
703 followed
704 ) do
705 conn
706 |> put_view(AccountView)
707 |> render("relationship.json", %{user: follower, target: followed})
708 else
709 {:error, message} ->
710 conn
711 |> put_resp_content_type("application/json")
712 |> send_resp(403, Jason.encode!(%{"error" => message}))
713 end
714 end
715
716 def follow(%{assigns: %{user: follower}} = conn, %{"uri" => uri}) do
717 with %User{} = followed <- Repo.get_by(User, nickname: uri),
718 {:ok, follower} <- User.maybe_direct_follow(follower, followed),
719 {:ok, _activity} <- ActivityPub.follow(follower, followed) do
720 conn
721 |> put_view(AccountView)
722 |> render("account.json", %{user: followed, for: follower})
723 else
724 {:error, message} ->
725 conn
726 |> put_resp_content_type("application/json")
727 |> send_resp(403, Jason.encode!(%{"error" => message}))
728 end
729 end
730
731 def unfollow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
732 with %User{} = followed <- Repo.get(User, id),
733 {:ok, _activity} <- ActivityPub.unfollow(follower, followed),
734 {:ok, follower, _} <- User.unfollow(follower, followed) do
735 conn
736 |> put_view(AccountView)
737 |> render("relationship.json", %{user: follower, target: followed})
738 end
739 end
740
741 def block(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
742 with %User{} = blocked <- Repo.get(User, id),
743 {:ok, blocker} <- User.block(blocker, blocked),
744 {:ok, _activity} <- ActivityPub.block(blocker, blocked) do
745 conn
746 |> put_view(AccountView)
747 |> render("relationship.json", %{user: blocker, target: blocked})
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 unblock(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
757 with %User{} = blocked <- Repo.get(User, id),
758 {:ok, blocker} <- User.unblock(blocker, blocked),
759 {:ok, _activity} <- ActivityPub.unblock(blocker, blocked) do
760 conn
761 |> put_view(AccountView)
762 |> render("relationship.json", %{user: blocker, target: blocked})
763 else
764 {:error, message} ->
765 conn
766 |> put_resp_content_type("application/json")
767 |> send_resp(403, Jason.encode!(%{"error" => message}))
768 end
769 end
770
771 def blocks(%{assigns: %{user: user}} = conn, _) do
772 with blocked_accounts <- User.blocked_users(user) do
773 res = AccountView.render("accounts.json", users: blocked_accounts, for: user, as: :user)
774 json(conn, res)
775 end
776 end
777
778 def domain_blocks(%{assigns: %{user: %{info: info}}} = conn, _) do
779 json(conn, info.domain_blocks || [])
780 end
781
782 def block_domain(%{assigns: %{user: blocker}} = conn, %{"domain" => domain}) do
783 User.block_domain(blocker, domain)
784 json(conn, %{})
785 end
786
787 def unblock_domain(%{assigns: %{user: blocker}} = conn, %{"domain" => domain}) do
788 User.unblock_domain(blocker, domain)
789 json(conn, %{})
790 end
791
792 def status_search(user, query) do
793 fetched =
794 if Regex.match?(~r/https?:/, query) do
795 with {:ok, object} <- ActivityPub.fetch_object_from_id(query),
796 %Activity{} = activity <- Activity.get_create_by_object_ap_id(object.data["id"]),
797 true <- ActivityPub.visible_for_user?(activity, user) do
798 [activity]
799 else
800 _e -> []
801 end
802 end || []
803
804 q =
805 from(
806 a in Activity,
807 where: fragment("?->>'type' = 'Create'", a.data),
808 where: "https://www.w3.org/ns/activitystreams#Public" in a.recipients,
809 where:
810 fragment(
811 "to_tsvector('english', ?->'object'->>'content') @@ plainto_tsquery('english', ?)",
812 a.data,
813 ^query
814 ),
815 limit: 20,
816 order_by: [desc: :id]
817 )
818
819 Repo.all(q) ++ fetched
820 end
821
822 def search2(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do
823 accounts = User.search(query, params["resolve"] == "true", user)
824
825 statuses = status_search(user, query)
826
827 tags_path = Web.base_url() <> "/tag/"
828
829 tags =
830 query
831 |> String.split()
832 |> Enum.uniq()
833 |> Enum.filter(fn tag -> String.starts_with?(tag, "#") end)
834 |> Enum.map(fn tag -> String.slice(tag, 1..-1) end)
835 |> Enum.map(fn tag -> %{name: tag, url: tags_path <> tag} end)
836
837 res = %{
838 "accounts" => AccountView.render("accounts.json", users: accounts, for: user, as: :user),
839 "statuses" =>
840 StatusView.render("index.json", activities: statuses, for: user, as: :activity),
841 "hashtags" => tags
842 }
843
844 json(conn, res)
845 end
846
847 def search(%{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 =
853 query
854 |> String.split()
855 |> Enum.uniq()
856 |> Enum.filter(fn tag -> String.starts_with?(tag, "#") end)
857 |> Enum.map(fn tag -> String.slice(tag, 1..-1) end)
858
859 res = %{
860 "accounts" => AccountView.render("accounts.json", users: accounts, for: user, as: :user),
861 "statuses" =>
862 StatusView.render("index.json", activities: statuses, for: user, as: :activity),
863 "hashtags" => tags
864 }
865
866 json(conn, res)
867 end
868
869 def account_search(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do
870 accounts = User.search(query, params["resolve"] == "true", user)
871
872 res = AccountView.render("accounts.json", users: accounts, for: user, as: :user)
873
874 json(conn, res)
875 end
876
877 def favourites(%{assigns: %{user: user}} = conn, params) do
878 activities =
879 params
880 |> Map.put("type", "Create")
881 |> Map.put("favorited_by", user.ap_id)
882 |> Map.put("blocking_user", user)
883 |> ActivityPub.fetch_public_activities()
884 |> Enum.reverse()
885
886 conn
887 |> add_link_headers(:favourites, activities)
888 |> put_view(StatusView)
889 |> render("index.json", %{activities: activities, for: user, as: :activity})
890 end
891
892 def bookmarks(%{assigns: %{user: user}} = conn, _) do
893 user = Repo.get(User, user.id)
894
895 activities =
896 user.bookmarks
897 |> Enum.map(fn id -> Activity.get_create_by_object_ap_id(id) end)
898 |> Enum.reverse()
899
900 conn
901 |> put_view(StatusView)
902 |> render("index.json", %{activities: activities, for: user, as: :activity})
903 end
904
905 def get_lists(%{assigns: %{user: user}} = conn, opts) do
906 lists = Pleroma.List.for_user(user, opts)
907 res = ListView.render("lists.json", lists: lists)
908 json(conn, res)
909 end
910
911 def get_list(%{assigns: %{user: user}} = conn, %{"id" => id}) do
912 with %Pleroma.List{} = list <- Pleroma.List.get(id, user) do
913 res = ListView.render("list.json", list: list)
914 json(conn, res)
915 else
916 _e ->
917 conn
918 |> put_status(404)
919 |> json(%{error: "Record not found"})
920 end
921 end
922
923 def account_lists(%{assigns: %{user: user}} = conn, %{"id" => account_id}) do
924 lists = Pleroma.List.get_lists_account_belongs(user, account_id)
925 res = ListView.render("lists.json", lists: lists)
926 json(conn, res)
927 end
928
929 def delete_list(%{assigns: %{user: user}} = conn, %{"id" => id}) do
930 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
931 {:ok, _list} <- Pleroma.List.delete(list) do
932 json(conn, %{})
933 else
934 _e ->
935 json(conn, "error")
936 end
937 end
938
939 def create_list(%{assigns: %{user: user}} = conn, %{"title" => title}) do
940 with {:ok, %Pleroma.List{} = list} <- Pleroma.List.create(title, user) do
941 res = ListView.render("list.json", list: list)
942 json(conn, res)
943 end
944 end
945
946 def add_to_list(%{assigns: %{user: user}} = conn, %{"id" => id, "account_ids" => accounts}) do
947 accounts
948 |> Enum.each(fn account_id ->
949 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
950 %User{} = followed <- Repo.get(User, account_id) do
951 Pleroma.List.follow(list, followed)
952 end
953 end)
954
955 json(conn, %{})
956 end
957
958 def remove_from_list(%{assigns: %{user: user}} = conn, %{"id" => id, "account_ids" => accounts}) do
959 accounts
960 |> Enum.each(fn account_id ->
961 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
962 %User{} = followed <- Repo.get(Pleroma.User, account_id) do
963 Pleroma.List.unfollow(list, followed)
964 end
965 end)
966
967 json(conn, %{})
968 end
969
970 def list_accounts(%{assigns: %{user: user}} = conn, %{"id" => id}) do
971 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
972 {:ok, users} = Pleroma.List.get_following(list) do
973 conn
974 |> put_view(AccountView)
975 |> render("accounts.json", %{users: users, as: :user})
976 end
977 end
978
979 def rename_list(%{assigns: %{user: user}} = conn, %{"id" => id, "title" => title}) do
980 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
981 {:ok, list} <- Pleroma.List.rename(list, title) do
982 res = ListView.render("list.json", list: list)
983 json(conn, res)
984 else
985 _e ->
986 json(conn, "error")
987 end
988 end
989
990 def list_timeline(%{assigns: %{user: user}} = conn, %{"list_id" => id} = params) do
991 with %Pleroma.List{title: _title, following: following} <- Pleroma.List.get(id, user) do
992 params =
993 params
994 |> Map.put("type", "Create")
995 |> Map.put("blocking_user", user)
996
997 # we must filter the following list for the user to avoid leaking statuses the user
998 # does not actually have permission to see (for more info, peruse security issue #270).
999 activities =
1000 following
1001 |> Enum.filter(fn x -> x in user.following end)
1002 |> ActivityPub.fetch_activities_bounded(following, params)
1003 |> Enum.reverse()
1004
1005 conn
1006 |> put_view(StatusView)
1007 |> render("index.json", %{activities: activities, for: user, as: :activity})
1008 else
1009 _e ->
1010 conn
1011 |> put_status(403)
1012 |> json(%{error: "Error."})
1013 end
1014 end
1015
1016 def index(%{assigns: %{user: user}} = conn, _params) do
1017 token =
1018 conn
1019 |> get_session(:oauth_token)
1020
1021 if user && token do
1022 mastodon_emoji = mastodonized_emoji()
1023
1024 limit = Config.get([:instance, :limit])
1025
1026 accounts =
1027 Map.put(%{}, user.id, AccountView.render("account.json", %{user: user, for: user}))
1028
1029 initial_state =
1030 %{
1031 meta: %{
1032 streaming_api_base_url:
1033 String.replace(Pleroma.Web.Endpoint.static_url(), "http", "ws"),
1034 access_token: token,
1035 locale: "en",
1036 domain: Pleroma.Web.Endpoint.host(),
1037 admin: "1",
1038 me: "#{user.id}",
1039 unfollow_modal: false,
1040 boost_modal: false,
1041 delete_modal: true,
1042 auto_play_gif: false,
1043 display_sensitive_media: false,
1044 reduce_motion: false,
1045 max_toot_chars: limit
1046 },
1047 rights: %{
1048 delete_others_notice: present?(user.info.is_moderator),
1049 admin: present?(user.info.is_admin)
1050 },
1051 compose: %{
1052 me: "#{user.id}",
1053 default_privacy: user.info.default_scope,
1054 default_sensitive: false
1055 },
1056 media_attachments: %{
1057 accept_content_types: [
1058 ".jpg",
1059 ".jpeg",
1060 ".png",
1061 ".gif",
1062 ".webm",
1063 ".mp4",
1064 ".m4v",
1065 "image\/jpeg",
1066 "image\/png",
1067 "image\/gif",
1068 "video\/webm",
1069 "video\/mp4"
1070 ]
1071 },
1072 settings:
1073 user.info.settings ||
1074 %{
1075 onboarded: true,
1076 home: %{
1077 shows: %{
1078 reblog: true,
1079 reply: true
1080 }
1081 },
1082 notifications: %{
1083 alerts: %{
1084 follow: true,
1085 favourite: true,
1086 reblog: true,
1087 mention: true
1088 },
1089 shows: %{
1090 follow: true,
1091 favourite: true,
1092 reblog: true,
1093 mention: true
1094 },
1095 sounds: %{
1096 follow: true,
1097 favourite: true,
1098 reblog: true,
1099 mention: true
1100 }
1101 }
1102 },
1103 push_subscription: nil,
1104 accounts: accounts,
1105 custom_emojis: mastodon_emoji,
1106 char_limit: limit
1107 }
1108 |> Jason.encode!()
1109
1110 conn
1111 |> put_layout(false)
1112 |> put_view(MastodonView)
1113 |> render("index.html", %{initial_state: initial_state})
1114 else
1115 conn
1116 |> redirect(to: "/web/login")
1117 end
1118 end
1119
1120 def put_settings(%{assigns: %{user: user}} = conn, %{"data" => settings} = _params) do
1121 info_cng = User.Info.mastodon_settings_update(user.info, settings)
1122
1123 with changeset <- Ecto.Changeset.change(user),
1124 changeset <- Ecto.Changeset.put_embed(changeset, :info, info_cng),
1125 {:ok, _user} <- User.update_and_set_cache(changeset) do
1126 json(conn, %{})
1127 else
1128 e ->
1129 conn
1130 |> put_resp_content_type("application/json")
1131 |> send_resp(500, Jason.encode!(%{"error" => inspect(e)}))
1132 end
1133 end
1134
1135 def login(conn, %{"code" => code}) do
1136 with {:ok, app} <- get_or_make_app(),
1137 %Authorization{} = auth <- Repo.get_by(Authorization, token: code, app_id: app.id),
1138 {:ok, token} <- Token.exchange_token(app, auth) do
1139 conn
1140 |> put_session(:oauth_token, token.token)
1141 |> redirect(to: "/web/getting-started")
1142 end
1143 end
1144
1145 def login(conn, _) do
1146 with {:ok, app} <- get_or_make_app() do
1147 path =
1148 o_auth_path(
1149 conn,
1150 :authorize,
1151 response_type: "code",
1152 client_id: app.client_id,
1153 redirect_uri: ".",
1154 scope: app.scopes
1155 )
1156
1157 conn
1158 |> redirect(to: path)
1159 end
1160 end
1161
1162 defp get_or_make_app() do
1163 find_attrs = %{client_name: @local_mastodon_name, redirect_uris: "."}
1164
1165 with %App{} = app <- Repo.get_by(App, find_attrs) do
1166 {:ok, app}
1167 else
1168 _e ->
1169 cs = App.register_changeset(%App{}, Map.put(find_attrs, :scopes, "read,write,follow"))
1170
1171 Repo.insert(cs)
1172 end
1173 end
1174
1175 def logout(conn, _) do
1176 conn
1177 |> clear_session
1178 |> redirect(to: "/")
1179 end
1180
1181 def relationship_noop(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1182 Logger.debug("Unimplemented, returning unmodified relationship")
1183
1184 with %User{} = target <- Repo.get(User, id) do
1185 conn
1186 |> put_view(AccountView)
1187 |> render("relationship.json", %{user: user, target: target})
1188 end
1189 end
1190
1191 def empty_array(conn, _) do
1192 Logger.debug("Unimplemented, returning an empty array")
1193 json(conn, [])
1194 end
1195
1196 def empty_object(conn, _) do
1197 Logger.debug("Unimplemented, returning an empty object")
1198 json(conn, %{})
1199 end
1200
1201 def render_notification(user, %{id: id, activity: activity, inserted_at: created_at} = _params) do
1202 actor = User.get_cached_by_ap_id(activity.data["actor"])
1203 parent_activity = Activity.get_create_by_object_ap_id(activity.data["object"])
1204 mastodon_type = Activity.mastodon_notification_type(activity)
1205
1206 response = %{
1207 id: to_string(id),
1208 type: mastodon_type,
1209 created_at: CommonAPI.Utils.to_masto_date(created_at),
1210 account: AccountView.render("account.json", %{user: actor, for: user})
1211 }
1212
1213 case mastodon_type do
1214 "mention" ->
1215 response
1216 |> Map.merge(%{
1217 status: StatusView.render("status.json", %{activity: activity, for: user})
1218 })
1219
1220 "favourite" ->
1221 response
1222 |> Map.merge(%{
1223 status: StatusView.render("status.json", %{activity: parent_activity, for: user})
1224 })
1225
1226 "reblog" ->
1227 response
1228 |> Map.merge(%{
1229 status: StatusView.render("status.json", %{activity: parent_activity, for: user})
1230 })
1231
1232 "follow" ->
1233 response
1234
1235 _ ->
1236 nil
1237 end
1238 end
1239
1240 def get_filters(%{assigns: %{user: user}} = conn, _) do
1241 filters = Filter.get_filters(user)
1242 res = FilterView.render("filters.json", filters: filters)
1243 json(conn, res)
1244 end
1245
1246 def create_filter(
1247 %{assigns: %{user: user}} = conn,
1248 %{"phrase" => phrase, "context" => context} = params
1249 ) do
1250 query = %Filter{
1251 user_id: user.id,
1252 phrase: phrase,
1253 context: context,
1254 hide: Map.get(params, "irreversible", nil),
1255 whole_word: Map.get(params, "boolean", true)
1256 # expires_at
1257 }
1258
1259 {:ok, response} = Filter.create(query)
1260 res = FilterView.render("filter.json", filter: response)
1261 json(conn, res)
1262 end
1263
1264 def get_filter(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do
1265 filter = Filter.get(filter_id, user)
1266 res = FilterView.render("filter.json", filter: filter)
1267 json(conn, res)
1268 end
1269
1270 def update_filter(
1271 %{assigns: %{user: user}} = conn,
1272 %{"phrase" => phrase, "context" => context, "id" => filter_id} = params
1273 ) do
1274 query = %Filter{
1275 user_id: user.id,
1276 filter_id: filter_id,
1277 phrase: phrase,
1278 context: context,
1279 hide: Map.get(params, "irreversible", nil),
1280 whole_word: Map.get(params, "boolean", true)
1281 # expires_at
1282 }
1283
1284 {:ok, response} = Filter.update(query)
1285 res = FilterView.render("filter.json", filter: response)
1286 json(conn, res)
1287 end
1288
1289 def delete_filter(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do
1290 query = %Filter{
1291 user_id: user.id,
1292 filter_id: filter_id
1293 }
1294
1295 {:ok, _} = Filter.delete(query)
1296 json(conn, %{})
1297 end
1298
1299 def create_push_subscription(%{assigns: %{user: user, token: token}} = conn, params) do
1300 true = Push.enabled()
1301 Subscription.delete_if_exists(user, token)
1302 {:ok, subscription} = Subscription.create(user, token, params)
1303 view = PushSubscriptionView.render("push_subscription.json", subscription: subscription)
1304 json(conn, view)
1305 end
1306
1307 def get_push_subscription(%{assigns: %{user: user, token: token}} = conn, _params) do
1308 true = Push.enabled()
1309 subscription = Subscription.get(user, token)
1310 view = PushSubscriptionView.render("push_subscription.json", subscription: subscription)
1311 json(conn, view)
1312 end
1313
1314 def update_push_subscription(
1315 %{assigns: %{user: user, token: token}} = conn,
1316 params
1317 ) do
1318 true = Push.enabled()
1319 {:ok, subscription} = Subscription.update(user, token, params)
1320 view = PushSubscriptionView.render("push_subscription.json", subscription: subscription)
1321 json(conn, view)
1322 end
1323
1324 def delete_push_subscription(%{assigns: %{user: user, token: token}} = conn, _params) do
1325 true = Push.enabled()
1326 {:ok, _response} = Subscription.delete(user, token)
1327 json(conn, %{})
1328 end
1329
1330 def errors(conn, _) do
1331 conn
1332 |> put_status(500)
1333 |> json("Something went wrong")
1334 end
1335
1336 def suggestions(%{assigns: %{user: user}} = conn, _) do
1337 suggestions = Config.get(:suggestions)
1338
1339 if Keyword.get(suggestions, :enabled, false) do
1340 api = Keyword.get(suggestions, :third_party_engine, "")
1341 timeout = Keyword.get(suggestions, :timeout, 5000)
1342 limit = Keyword.get(suggestions, :limit, 23)
1343
1344 host = Config.get([Pleroma.Web.Endpoint, :url, :host])
1345
1346 user = user.nickname
1347
1348 url =
1349 api
1350 |> String.replace("{{host}}", host)
1351 |> String.replace("{{user}}", user)
1352
1353 with {:ok, %{status: 200, body: body}} <-
1354 @httpoison.get(
1355 url,
1356 [],
1357 adapter: [
1358 timeout: timeout,
1359 recv_timeout: timeout,
1360 pool: :default
1361 ]
1362 ),
1363 {:ok, data} <- Jason.decode(body) do
1364 data =
1365 data
1366 |> Enum.slice(0, limit)
1367 |> Enum.map(fn x ->
1368 Map.put(
1369 x,
1370 "id",
1371 case User.get_or_fetch(x["acct"]) do
1372 %{id: id} -> id
1373 _ -> 0
1374 end
1375 )
1376 end)
1377 |> Enum.map(fn x ->
1378 Map.put(x, "avatar", MediaProxy.url(x["avatar"]))
1379 end)
1380 |> Enum.map(fn x ->
1381 Map.put(x, "avatar_static", MediaProxy.url(x["avatar_static"]))
1382 end)
1383
1384 conn
1385 |> json(data)
1386 else
1387 e -> Logger.error("Could not retrieve suggestions at fetch #{url}, #{inspect(e)}")
1388 end
1389 else
1390 json(conn, [])
1391 end
1392 end
1393
1394 def status_card(conn, %{"id" => status_id}) do
1395 with %Activity{} = activity <- Repo.get(Activity, status_id),
1396 true <- ActivityPub.is_public?(activity) do
1397 data =
1398 StatusView.render(
1399 "card.json",
1400 Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity)
1401 )
1402
1403 json(conn, data)
1404 else
1405 _e ->
1406 %{}
1407 end
1408 end
1409
1410 def try_render(conn, target, params)
1411 when is_binary(target) do
1412 res = render(conn, target, params)
1413
1414 if res == nil do
1415 conn
1416 |> put_status(501)
1417 |> json(%{error: "Can't display this activity"})
1418 else
1419 res
1420 end
1421 end
1422
1423 def try_render(conn, _, _) do
1424 conn
1425 |> put_status(501)
1426 |> json(%{error: "Can't display this activity"})
1427 end
1428
1429 defp present?(nil), do: false
1430 defp present?(false), do: false
1431 defp present?(_), do: true
1432 end