Merge branch 'develop' into csaurus/pleroma-feature/mstdn-direct-api
[akkoma] / lib / pleroma / web / mastodon_api / mastodon_api_controller.ex
1 defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
2 use Pleroma.Web, :controller
3 alias Pleroma.{Repo, Activity, User, Notification, Stats}
4 alias Pleroma.Web
5 alias Pleroma.Web.MastodonAPI.{StatusView, AccountView, MastodonView, ListView}
6 alias Pleroma.Web.ActivityPub.ActivityPub
7 alias Pleroma.Web.{CommonAPI, OStatus}
8 alias Pleroma.Web.OAuth.{Authorization, Token, App}
9 alias Comeonin.Pbkdf2
10 import Ecto.Query
11 require Logger
12
13 def create_app(conn, params) do
14 with cs <- App.register_changeset(%App{}, params) |> IO.inspect(),
15 {:ok, app} <- Repo.insert(cs) |> IO.inspect() do
16 res = %{
17 id: app.id,
18 client_id: app.client_id,
19 client_secret: app.client_secret
20 }
21
22 json(conn, res)
23 end
24 end
25
26 def update_credentials(%{assigns: %{user: user}} = conn, params) do
27 original_user = user
28
29 params =
30 if bio = params["note"] do
31 Map.put(params, "bio", bio)
32 else
33 params
34 end
35
36 params =
37 if name = params["display_name"] do
38 Map.put(params, "name", name)
39 else
40 params
41 end
42
43 user =
44 if avatar = params["avatar"] do
45 with %Plug.Upload{} <- avatar,
46 {:ok, object} <- ActivityPub.upload(avatar),
47 change = Ecto.Changeset.change(user, %{avatar: object.data}),
48 {:ok, user} = User.update_and_set_cache(change) do
49 user
50 else
51 _e -> user
52 end
53 else
54 user
55 end
56
57 user =
58 if banner = params["header"] do
59 with %Plug.Upload{} <- banner,
60 {:ok, object} <- ActivityPub.upload(banner),
61 new_info <- Map.put(user.info, "banner", object.data),
62 change <- User.info_changeset(user, %{info: new_info}),
63 {:ok, user} <- User.update_and_set_cache(change) do
64 user
65 else
66 _e -> user
67 end
68 else
69 user
70 end
71
72 with changeset <- User.update_changeset(user, params),
73 {:ok, user} <- User.update_and_set_cache(changeset) do
74 if original_user != user do
75 CommonAPI.update(user)
76 end
77
78 json(conn, AccountView.render("account.json", %{user: user}))
79 else
80 _e ->
81 conn
82 |> put_status(403)
83 |> json(%{error: "Invalid request"})
84 end
85 end
86
87 def verify_credentials(%{assigns: %{user: user}} = conn, _) do
88 account = AccountView.render("account.json", %{user: user})
89 json(conn, account)
90 end
91
92 def user(conn, %{"id" => id}) do
93 with %User{} = user <- Repo.get(User, id) do
94 account = AccountView.render("account.json", %{user: user})
95 json(conn, account)
96 else
97 _e ->
98 conn
99 |> put_status(404)
100 |> json(%{error: "Can't find user"})
101 end
102 end
103
104 @instance Application.get_env(:pleroma, :instance)
105 @mastodon_api_level "2.3.3"
106
107 def masto_instance(conn, _params) do
108 response = %{
109 uri: Web.base_url(),
110 title: Keyword.get(@instance, :name),
111 description: "A Pleroma instance, an alternative fediverse server",
112 version: "#{@mastodon_api_level} (compatible; #{Keyword.get(@instance, :version)})",
113 email: Keyword.get(@instance, :email),
114 urls: %{
115 streaming_api: String.replace(Pleroma.Web.Endpoint.static_url(), "http", "ws")
116 },
117 stats: Stats.get_stats(),
118 thumbnail: Web.base_url() <> "/instance/thumbnail.jpeg",
119 max_toot_chars: Keyword.get(@instance, :limit)
120 }
121
122 json(conn, response)
123 end
124
125 def peers(conn, _params) do
126 json(conn, Stats.get_peers())
127 end
128
129 defp mastodonized_emoji do
130 Pleroma.Formatter.get_custom_emoji()
131 |> Enum.map(fn {shortcode, relative_url} ->
132 url = to_string(URI.merge(Web.base_url(), relative_url))
133
134 %{
135 "shortcode" => shortcode,
136 "static_url" => url,
137 "url" => url
138 }
139 end)
140 end
141
142 def custom_emojis(conn, _params) do
143 mastodon_emoji = mastodonized_emoji()
144 json(conn, mastodon_emoji)
145 end
146
147 defp add_link_headers(conn, method, activities, param \\ false) do
148 last = List.last(activities)
149 first = List.first(activities)
150
151 if last do
152 min = last.id
153 max = first.id
154
155 {next_url, prev_url} =
156 if param do
157 {
158 mastodon_api_url(Pleroma.Web.Endpoint, method, param, max_id: min),
159 mastodon_api_url(Pleroma.Web.Endpoint, method, param, since_id: max)
160 }
161 else
162 {
163 mastodon_api_url(Pleroma.Web.Endpoint, method, max_id: min),
164 mastodon_api_url(Pleroma.Web.Endpoint, method, since_id: max)
165 }
166 end
167
168 conn
169 |> put_resp_header("link", "<#{next_url}>; rel=\"next\", <#{prev_url}>; rel=\"prev\"")
170 else
171 conn
172 end
173 end
174
175 def home_timeline(%{assigns: %{user: user}} = conn, params) do
176 params =
177 params
178 |> Map.put("type", ["Create", "Announce"])
179 |> Map.put("blocking_user", user)
180 |> Map.put("user", user)
181
182 activities =
183 ActivityPub.fetch_activities([user.ap_id | user.following], params)
184 |> Enum.reverse()
185
186 conn
187 |> add_link_headers(:home_timeline, activities)
188 |> render(StatusView, "index.json", %{activities: activities, for: user, as: :activity})
189 end
190
191 def public_timeline(%{assigns: %{user: user}} = conn, params) do
192 params =
193 params
194 |> Map.put("type", ["Create", "Announce"])
195 |> Map.put("local_only", params["local"] in [true, "True", "true", "1"])
196 |> Map.put("blocking_user", user)
197
198 activities =
199 ActivityPub.fetch_public_activities(params)
200 |> Enum.reverse()
201
202 conn
203 |> add_link_headers(:public_timeline, activities)
204 |> render(StatusView, "index.json", %{activities: activities, for: user, as: :activity})
205 end
206
207 def user_statuses(%{assigns: %{user: reading_user}} = conn, params) do
208 with %User{} = user <- Repo.get(User, params["id"]) do
209 # Since Pleroma has no "pinned" posts feature, we'll just set an empty list here
210 activities =
211 if params["pinned"] == "true" do
212 []
213 else
214 ActivityPub.fetch_user_activities(user, reading_user, params)
215 end
216
217 conn
218 |> add_link_headers(:user_statuses, activities, params["id"])
219 |> render(StatusView, "index.json", %{activities: activities, for: user, as: :activity})
220 end
221 end
222
223 def dm_timeline(%{assigns: %{user: user}} = conn, params) do
224 query = ActivityPub.fetch_activities_query([user.ap_id], %{visibility: "direct"})
225 activities = Repo.all(query)
226
227 conn
228 |> add_link_headers(:user_statuses, activities, user.ap_id)
229 |> render(StatusView, "index.json", %{activities: activities, for: user, as: :activity})
230 end
231
232 def get_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
233 with %Activity{} = activity <- Repo.get(Activity, id),
234 true <- ActivityPub.visible_for_user?(activity, user) do
235 render(conn, StatusView, "status.json", %{activity: activity, for: user})
236 end
237 end
238
239 def get_context(%{assigns: %{user: user}} = conn, %{"id" => id}) do
240 with %Activity{} = activity <- Repo.get(Activity, id),
241 activities <-
242 ActivityPub.fetch_activities_for_context(activity.data["context"], %{
243 "blocking_user" => user,
244 "user" => user
245 }),
246 activities <-
247 activities |> Enum.filter(fn %{id: aid} -> to_string(aid) != to_string(id) end),
248 activities <-
249 activities |> Enum.filter(fn %{data: %{"type" => type}} -> type == "Create" end),
250 grouped_activities <- Enum.group_by(activities, fn %{id: id} -> id < activity.id end) do
251 result = %{
252 ancestors:
253 StatusView.render(
254 "index.json",
255 for: user,
256 activities: grouped_activities[true] || [],
257 as: :activity
258 )
259 |> Enum.reverse(),
260 descendants:
261 StatusView.render(
262 "index.json",
263 for: user,
264 activities: grouped_activities[false] || [],
265 as: :activity
266 )
267 |> Enum.reverse()
268 }
269
270 json(conn, result)
271 end
272 end
273
274 def post_status(%{assigns: %{user: user}} = conn, %{"status" => _} = params) do
275 params =
276 params
277 |> Map.put("in_reply_to_status_id", params["in_reply_to_id"])
278 |> Map.put("no_attachment_links", true)
279
280 idempotency_key =
281 case get_req_header(conn, "idempotency-key") do
282 [key] -> key
283 _ -> Ecto.UUID.generate()
284 end
285
286 {:ok, activity} =
287 Cachex.fetch!(:idempotency_cache, idempotency_key, fn _ -> CommonAPI.post(user, params) end)
288
289 render(conn, StatusView, "status.json", %{activity: activity, for: user, as: :activity})
290 end
291
292 def delete_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
293 with {:ok, %Activity{}} <- CommonAPI.delete(id, user) do
294 json(conn, %{})
295 else
296 _e ->
297 conn
298 |> put_status(403)
299 |> json(%{error: "Can't delete this post"})
300 end
301 end
302
303 def reblog_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
304 with {:ok, announce, _activity} = CommonAPI.repeat(ap_id_or_id, user) do
305 render(conn, StatusView, "status.json", %{activity: announce, for: user, as: :activity})
306 end
307 end
308
309 def unreblog_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
310 with {:ok, _, _, %{data: %{"id" => id}}} = CommonAPI.unrepeat(ap_id_or_id, user),
311 %Activity{} = activity <- Activity.get_create_activity_by_object_ap_id(id) do
312 render(conn, StatusView, "status.json", %{activity: activity, for: user, as: :activity})
313 end
314 end
315
316 def fav_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
317 with {:ok, _fav, %{data: %{"id" => id}}} = CommonAPI.favorite(ap_id_or_id, user),
318 %Activity{} = activity <- Activity.get_create_activity_by_object_ap_id(id) do
319 render(conn, StatusView, "status.json", %{activity: activity, for: user, as: :activity})
320 end
321 end
322
323 def unfav_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
324 with {:ok, _, _, %{data: %{"id" => id}}} = CommonAPI.unfavorite(ap_id_or_id, user),
325 %Activity{} = activity <- Activity.get_create_activity_by_object_ap_id(id) do
326 render(conn, StatusView, "status.json", %{activity: activity, for: user, as: :activity})
327 end
328 end
329
330 def notifications(%{assigns: %{user: user}} = conn, params) do
331 notifications = Notification.for_user(user, params)
332
333 result =
334 Enum.map(notifications, fn x ->
335 render_notification(user, x)
336 end)
337 |> Enum.filter(& &1)
338
339 conn
340 |> add_link_headers(:notifications, notifications)
341 |> json(result)
342 end
343
344 def get_notification(%{assigns: %{user: user}} = conn, %{"id" => id} = _params) do
345 with {:ok, notification} <- Notification.get(user, id) do
346 json(conn, render_notification(user, notification))
347 else
348 {:error, reason} ->
349 conn
350 |> put_resp_content_type("application/json")
351 |> send_resp(403, Jason.encode!(%{"error" => reason}))
352 end
353 end
354
355 def clear_notifications(%{assigns: %{user: user}} = conn, _params) do
356 Notification.clear(user)
357 json(conn, %{})
358 end
359
360 def dismiss_notification(%{assigns: %{user: user}} = conn, %{"id" => id} = _params) do
361 with {:ok, _notif} <- Notification.dismiss(user, id) do
362 json(conn, %{})
363 else
364 {:error, reason} ->
365 conn
366 |> put_resp_content_type("application/json")
367 |> send_resp(403, Jason.encode!(%{"error" => reason}))
368 end
369 end
370
371 def relationships(%{assigns: %{user: user}} = conn, %{"id" => id}) do
372 id = List.wrap(id)
373 q = from(u in User, where: u.id in ^id)
374 targets = Repo.all(q)
375 render(conn, AccountView, "relationships.json", %{user: user, targets: targets})
376 end
377
378 def upload(%{assigns: %{user: _}} = conn, %{"file" => file}) do
379 with {:ok, object} <- ActivityPub.upload(file) do
380 data =
381 object.data
382 |> Map.put("id", object.id)
383
384 render(conn, StatusView, "attachment.json", %{attachment: data})
385 end
386 end
387
388 def favourited_by(conn, %{"id" => id}) do
389 with %Activity{data: %{"object" => %{"likes" => likes}}} <- Repo.get(Activity, id) do
390 q = from(u in User, where: u.ap_id in ^likes)
391 users = Repo.all(q)
392 render(conn, AccountView, "accounts.json", %{users: users, as: :user})
393 else
394 _ -> json(conn, [])
395 end
396 end
397
398 def reblogged_by(conn, %{"id" => id}) do
399 with %Activity{data: %{"object" => %{"announcements" => announces}}} <- Repo.get(Activity, id) do
400 q = from(u in User, where: u.ap_id in ^announces)
401 users = Repo.all(q)
402 render(conn, AccountView, "accounts.json", %{users: users, as: :user})
403 else
404 _ -> json(conn, [])
405 end
406 end
407
408 def hashtag_timeline(%{assigns: %{user: user}} = conn, params) do
409 params =
410 params
411 |> Map.put("type", "Create")
412 |> Map.put("local_only", !!params["local"])
413 |> Map.put("blocking_user", user)
414
415 activities =
416 ActivityPub.fetch_public_activities(params)
417 |> Enum.reverse()
418
419 conn
420 |> add_link_headers(:hashtag_timeline, activities, params["tag"])
421 |> render(StatusView, "index.json", %{activities: activities, for: user, as: :activity})
422 end
423
424 # TODO: Pagination
425 def followers(conn, %{"id" => id}) do
426 with %User{} = user <- Repo.get(User, id),
427 {:ok, followers} <- User.get_followers(user) do
428 render(conn, AccountView, "accounts.json", %{users: followers, as: :user})
429 end
430 end
431
432 def following(conn, %{"id" => id}) do
433 with %User{} = user <- Repo.get(User, id),
434 {:ok, followers} <- User.get_friends(user) do
435 render(conn, AccountView, "accounts.json", %{users: followers, as: :user})
436 end
437 end
438
439 def follow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
440 with %User{} = followed <- Repo.get(User, id),
441 {:ok, follower} <- User.maybe_direct_follow(follower, followed),
442 {:ok, _activity} <- ActivityPub.follow(follower, followed) do
443 render(conn, AccountView, "relationship.json", %{user: follower, target: followed})
444 else
445 {:error, message} ->
446 conn
447 |> put_resp_content_type("application/json")
448 |> send_resp(403, Jason.encode!(%{"error" => message}))
449 end
450 end
451
452 def follow(%{assigns: %{user: follower}} = conn, %{"uri" => uri}) do
453 with %User{} = followed <- Repo.get_by(User, nickname: uri),
454 {:ok, follower} <- User.maybe_direct_follow(follower, followed),
455 {:ok, _activity} <- ActivityPub.follow(follower, followed) do
456 render(conn, AccountView, "account.json", %{user: followed})
457 else
458 {:error, message} ->
459 conn
460 |> put_resp_content_type("application/json")
461 |> send_resp(403, Jason.encode!(%{"error" => message}))
462 end
463 end
464
465 def unfollow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
466 with %User{} = followed <- Repo.get(User, id),
467 {:ok, _activity} <- ActivityPub.unfollow(follower, followed),
468 {:ok, follower, _} <- User.unfollow(follower, followed) do
469 render(conn, AccountView, "relationship.json", %{user: follower, target: followed})
470 end
471 end
472
473 def block(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
474 with %User{} = blocked <- Repo.get(User, id),
475 {:ok, blocker} <- User.block(blocker, blocked),
476 {:ok, _activity} <- ActivityPub.block(blocker, blocked) do
477 render(conn, AccountView, "relationship.json", %{user: blocker, target: blocked})
478 else
479 {:error, message} ->
480 conn
481 |> put_resp_content_type("application/json")
482 |> send_resp(403, Jason.encode!(%{"error" => message}))
483 end
484 end
485
486 def unblock(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
487 with %User{} = blocked <- Repo.get(User, id),
488 {:ok, blocker} <- User.unblock(blocker, blocked),
489 {:ok, _activity} <- ActivityPub.unblock(blocker, blocked) do
490 render(conn, AccountView, "relationship.json", %{user: blocker, target: blocked})
491 else
492 {:error, message} ->
493 conn
494 |> put_resp_content_type("application/json")
495 |> send_resp(403, Jason.encode!(%{"error" => message}))
496 end
497 end
498
499 # TODO: Use proper query
500 def blocks(%{assigns: %{user: user}} = conn, _) do
501 with blocked_users <- user.info["blocks"] || [],
502 accounts <- Enum.map(blocked_users, fn ap_id -> User.get_cached_by_ap_id(ap_id) end) do
503 res = AccountView.render("accounts.json", users: accounts, for: user, as: :user)
504 json(conn, res)
505 end
506 end
507
508 def search(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do
509 accounts = User.search(query, params["resolve"] == "true")
510
511 fetched =
512 if Regex.match?(~r/https?:/, query) do
513 with {:ok, activities} <- OStatus.fetch_activity_from_url(query) do
514 activities
515 |> Enum.filter(fn
516 %{data: %{"type" => "Create"}} -> true
517 _ -> false
518 end)
519 else
520 _e -> []
521 end
522 end || []
523
524 q =
525 from(
526 a in Activity,
527 where: fragment("?->>'type' = 'Create'", a.data),
528 where: "https://www.w3.org/ns/activitystreams#Public" in a.recipients,
529 where:
530 fragment(
531 "to_tsvector('english', ?->'object'->>'content') @@ plainto_tsquery('english', ?)",
532 a.data,
533 ^query
534 ),
535 limit: 20,
536 order_by: [desc: :id]
537 )
538
539 statuses = Repo.all(q) ++ fetched
540
541 tags =
542 String.split(query)
543 |> Enum.uniq()
544 |> Enum.filter(fn tag -> String.starts_with?(tag, "#") end)
545 |> Enum.map(fn tag -> String.slice(tag, 1..-1) end)
546
547 res = %{
548 "accounts" => AccountView.render("accounts.json", users: accounts, for: user, as: :user),
549 "statuses" =>
550 StatusView.render("index.json", activities: statuses, for: user, as: :activity),
551 "hashtags" => tags
552 }
553
554 json(conn, res)
555 end
556
557 def account_search(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do
558 accounts = User.search(query, params["resolve"] == "true")
559
560 res = AccountView.render("accounts.json", users: accounts, for: user, as: :user)
561
562 json(conn, res)
563 end
564
565 def favourites(%{assigns: %{user: user}} = conn, _) do
566 params =
567 %{}
568 |> Map.put("type", "Create")
569 |> Map.put("favorited_by", user.ap_id)
570 |> Map.put("blocking_user", user)
571
572 activities =
573 ActivityPub.fetch_public_activities(params)
574 |> Enum.reverse()
575
576 conn
577 |> render(StatusView, "index.json", %{activities: activities, for: user, as: :activity})
578 end
579
580 def get_lists(%{assigns: %{user: user}} = conn, opts) do
581 lists = Pleroma.List.for_user(user, opts)
582 res = ListView.render("lists.json", lists: lists)
583 json(conn, res)
584 end
585
586 def get_list(%{assigns: %{user: user}} = conn, %{"id" => id}) do
587 with %Pleroma.List{} = list <- Pleroma.List.get(id, user) do
588 res = ListView.render("list.json", list: list)
589 json(conn, res)
590 else
591 _e -> json(conn, "error")
592 end
593 end
594
595 def delete_list(%{assigns: %{user: user}} = conn, %{"id" => id}) do
596 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
597 {:ok, _list} <- Pleroma.List.delete(list) do
598 json(conn, %{})
599 else
600 _e ->
601 json(conn, "error")
602 end
603 end
604
605 def create_list(%{assigns: %{user: user}} = conn, %{"title" => title}) do
606 with {:ok, %Pleroma.List{} = list} <- Pleroma.List.create(title, user) do
607 res = ListView.render("list.json", list: list)
608 json(conn, res)
609 end
610 end
611
612 def add_to_list(%{assigns: %{user: user}} = conn, %{"id" => id, "account_ids" => accounts}) do
613 accounts
614 |> Enum.each(fn account_id ->
615 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
616 %User{} = followed <- Repo.get(User, account_id) do
617 Pleroma.List.follow(list, followed)
618 end
619 end)
620
621 json(conn, %{})
622 end
623
624 def remove_from_list(%{assigns: %{user: user}} = conn, %{"id" => id, "account_ids" => accounts}) do
625 accounts
626 |> Enum.each(fn account_id ->
627 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
628 %User{} = followed <- Repo.get(Pleroma.User, account_id) do
629 Pleroma.List.unfollow(list, followed)
630 end
631 end)
632
633 json(conn, %{})
634 end
635
636 def list_accounts(%{assigns: %{user: user}} = conn, %{"id" => id}) do
637 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
638 {:ok, users} = Pleroma.List.get_following(list) do
639 render(conn, AccountView, "accounts.json", %{users: users, as: :user})
640 end
641 end
642
643 def rename_list(%{assigns: %{user: user}} = conn, %{"id" => id, "title" => title}) do
644 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
645 {:ok, list} <- Pleroma.List.rename(list, title) do
646 res = ListView.render("list.json", list: list)
647 json(conn, res)
648 else
649 _e ->
650 json(conn, "error")
651 end
652 end
653
654 def list_timeline(%{assigns: %{user: user}} = conn, %{"list_id" => id} = params) do
655 with %Pleroma.List{title: title, following: following} <- Pleroma.List.get(id, user) do
656 params =
657 params
658 |> Map.put("type", "Create")
659 |> Map.put("blocking_user", user)
660
661 # adding title is a hack to not make empty lists function like a public timeline
662 activities =
663 ActivityPub.fetch_activities([title | following], params)
664 |> Enum.reverse()
665
666 conn
667 |> render(StatusView, "index.json", %{activities: activities, for: user, as: :activity})
668 else
669 _e ->
670 conn
671 |> put_status(403)
672 |> json(%{error: "Error."})
673 end
674 end
675
676 def index(%{assigns: %{user: user}} = conn, _params) do
677 token =
678 conn
679 |> get_session(:oauth_token)
680
681 if user && token do
682 mastodon_emoji = mastodonized_emoji()
683 accounts = Map.put(%{}, user.id, AccountView.render("account.json", %{user: user}))
684
685 initial_state =
686 %{
687 meta: %{
688 streaming_api_base_url:
689 String.replace(Pleroma.Web.Endpoint.static_url(), "http", "ws"),
690 access_token: token,
691 locale: "en",
692 domain: Pleroma.Web.Endpoint.host(),
693 admin: "1",
694 me: "#{user.id}",
695 unfollow_modal: false,
696 boost_modal: false,
697 delete_modal: true,
698 auto_play_gif: false,
699 reduce_motion: false
700 },
701 compose: %{
702 me: "#{user.id}",
703 default_privacy: "public",
704 default_sensitive: false
705 },
706 media_attachments: %{
707 accept_content_types: [
708 ".jpg",
709 ".jpeg",
710 ".png",
711 ".gif",
712 ".webm",
713 ".mp4",
714 ".m4v",
715 "image\/jpeg",
716 "image\/png",
717 "image\/gif",
718 "video\/webm",
719 "video\/mp4"
720 ]
721 },
722 settings:
723 Map.get(user.info, "settings") ||
724 %{
725 onboarded: true,
726 home: %{
727 shows: %{
728 reblog: true,
729 reply: true
730 }
731 },
732 notifications: %{
733 alerts: %{
734 follow: true,
735 favourite: true,
736 reblog: true,
737 mention: true
738 },
739 shows: %{
740 follow: true,
741 favourite: true,
742 reblog: true,
743 mention: true
744 },
745 sounds: %{
746 follow: true,
747 favourite: true,
748 reblog: true,
749 mention: true
750 }
751 }
752 },
753 push_subscription: nil,
754 accounts: accounts,
755 custom_emojis: mastodon_emoji,
756 char_limit: Keyword.get(@instance, :limit)
757 }
758 |> Jason.encode!()
759
760 conn
761 |> put_layout(false)
762 |> render(MastodonView, "index.html", %{initial_state: initial_state})
763 else
764 conn
765 |> redirect(to: "/web/login")
766 end
767 end
768
769 def put_settings(%{assigns: %{user: user}} = conn, %{"data" => settings} = _params) do
770 with new_info <- Map.put(user.info, "settings", settings),
771 change <- User.info_changeset(user, %{info: new_info}),
772 {:ok, _user} <- User.update_and_set_cache(change) do
773 conn
774 |> json(%{})
775 else
776 e ->
777 conn
778 |> json(%{error: inspect(e)})
779 end
780 end
781
782 def login(conn, _) do
783 conn
784 |> render(MastodonView, "login.html", %{error: false})
785 end
786
787 defp get_or_make_app() do
788 with %App{} = app <- Repo.get_by(App, client_name: "Mastodon-Local") do
789 {:ok, app}
790 else
791 _e ->
792 cs =
793 App.register_changeset(%App{}, %{
794 client_name: "Mastodon-Local",
795 redirect_uris: ".",
796 scopes: "read,write,follow"
797 })
798
799 Repo.insert(cs)
800 end
801 end
802
803 def login_post(conn, %{"authorization" => %{"name" => name, "password" => password}}) do
804 with %User{} = user <- User.get_by_nickname_or_email(name),
805 true <- Pbkdf2.checkpw(password, user.password_hash),
806 {:ok, app} <- get_or_make_app(),
807 {:ok, auth} <- Authorization.create_authorization(app, user),
808 {:ok, token} <- Token.exchange_token(app, auth) do
809 conn
810 |> put_session(:oauth_token, token.token)
811 |> redirect(to: "/web/getting-started")
812 else
813 _e ->
814 conn
815 |> render(MastodonView, "login.html", %{error: "Wrong username or password"})
816 end
817 end
818
819 def logout(conn, _) do
820 conn
821 |> clear_session
822 |> redirect(to: "/")
823 end
824
825 def relationship_noop(%{assigns: %{user: user}} = conn, %{"id" => id}) do
826 Logger.debug("Unimplemented, returning unmodified relationship")
827
828 with %User{} = target <- Repo.get(User, id) do
829 render(conn, AccountView, "relationship.json", %{user: user, target: target})
830 end
831 end
832
833 def empty_array(conn, _) do
834 Logger.debug("Unimplemented, returning an empty array")
835 json(conn, [])
836 end
837
838 def empty_object(conn, _) do
839 Logger.debug("Unimplemented, returning an empty object")
840 json(conn, %{})
841 end
842
843 def render_notification(user, %{id: id, activity: activity, inserted_at: created_at} = _params) do
844 actor = User.get_cached_by_ap_id(activity.data["actor"])
845
846 created_at =
847 NaiveDateTime.to_iso8601(created_at)
848 |> String.replace(~r/(\.\d+)?$/, ".000Z", global: false)
849
850 case activity.data["type"] do
851 "Create" ->
852 %{
853 id: id,
854 type: "mention",
855 created_at: created_at,
856 account: AccountView.render("account.json", %{user: actor}),
857 status: StatusView.render("status.json", %{activity: activity, for: user})
858 }
859
860 "Like" ->
861 liked_activity = Activity.get_create_activity_by_object_ap_id(activity.data["object"])
862
863 %{
864 id: id,
865 type: "favourite",
866 created_at: created_at,
867 account: AccountView.render("account.json", %{user: actor}),
868 status: StatusView.render("status.json", %{activity: liked_activity, for: user})
869 }
870
871 "Announce" ->
872 announced_activity = Activity.get_create_activity_by_object_ap_id(activity.data["object"])
873
874 %{
875 id: id,
876 type: "reblog",
877 created_at: created_at,
878 account: AccountView.render("account.json", %{user: actor}),
879 status: StatusView.render("status.json", %{activity: announced_activity, for: user})
880 }
881
882 "Follow" ->
883 %{
884 id: id,
885 type: "follow",
886 created_at: created_at,
887 account: AccountView.render("account.json", %{user: actor})
888 }
889
890 _ ->
891 nil
892 end
893 end
894 end