394599146b8c71d689a8375545845f17fe309eaa
[akkoma] / lib / pleroma / web / mastodon_api / controllers / mastodon_api_controller.ex
1 # Pleroma: A lightweight social networking server
2 # Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
3 # SPDX-License-Identifier: AGPL-3.0-only
4
5 defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
6 use Pleroma.Web, :controller
7
8 import Pleroma.Web.ControllerHelper,
9 only: [json_response: 3, add_link_headers: 2, truthy_param?: 1]
10
11 alias Ecto.Changeset
12 alias Pleroma.Activity
13 alias Pleroma.Bookmark
14 alias Pleroma.Config
15 alias Pleroma.Emoji
16 alias Pleroma.HTTP
17 alias Pleroma.Object
18 alias Pleroma.Pagination
19 alias Pleroma.Plugs.RateLimiter
20 alias Pleroma.Repo
21 alias Pleroma.Stats
22 alias Pleroma.User
23 alias Pleroma.Web
24 alias Pleroma.Web.ActivityPub.ActivityPub
25 alias Pleroma.Web.ActivityPub.Visibility
26 alias Pleroma.Web.CommonAPI
27 alias Pleroma.Web.MastodonAPI.AccountView
28 alias Pleroma.Web.MastodonAPI.AppView
29 alias Pleroma.Web.MastodonAPI.MastodonView
30 alias Pleroma.Web.MastodonAPI.StatusView
31 alias Pleroma.Web.MediaProxy
32 alias Pleroma.Web.OAuth.App
33 alias Pleroma.Web.OAuth.Authorization
34 alias Pleroma.Web.OAuth.Scopes
35 alias Pleroma.Web.OAuth.Token
36 alias Pleroma.Web.TwitterAPI.TwitterAPI
37
38 require Logger
39
40 plug(RateLimiter, :app_account_creation when action == :account_register)
41 plug(RateLimiter, :search when action in [:search, :search2, :account_search])
42 plug(RateLimiter, :password_reset when action == :password_reset)
43 plug(RateLimiter, :account_confirmation_resend when action == :account_confirmation_resend)
44
45 @local_mastodon_name "Mastodon-Local"
46
47 action_fallback(Pleroma.Web.MastodonAPI.FallbackController)
48
49 def create_app(conn, params) do
50 scopes = Scopes.fetch_scopes(params, ["read"])
51
52 app_attrs =
53 params
54 |> Map.drop(["scope", "scopes"])
55 |> Map.put("scopes", scopes)
56
57 with cs <- App.register_changeset(%App{}, app_attrs),
58 false <- cs.changes[:client_name] == @local_mastodon_name,
59 {:ok, app} <- Repo.insert(cs) do
60 conn
61 |> put_view(AppView)
62 |> render("show.json", %{app: app})
63 end
64 end
65
66 defp add_if_present(
67 map,
68 params,
69 params_field,
70 map_field,
71 value_function \\ fn x -> {:ok, x} end
72 ) do
73 if Map.has_key?(params, params_field) do
74 case value_function.(params[params_field]) do
75 {:ok, new_value} -> Map.put(map, map_field, new_value)
76 :error -> map
77 end
78 else
79 map
80 end
81 end
82
83 def update_credentials(%{assigns: %{user: user}} = conn, params) do
84 original_user = user
85
86 user_params =
87 %{}
88 |> add_if_present(params, "display_name", :name)
89 |> add_if_present(params, "note", :bio, fn value -> {:ok, User.parse_bio(value, user)} end)
90 |> add_if_present(params, "avatar", :avatar, fn value ->
91 with %Plug.Upload{} <- value,
92 {:ok, object} <- ActivityPub.upload(value, type: :avatar) do
93 {:ok, object.data}
94 else
95 _ -> :error
96 end
97 end)
98
99 emojis_text = (user_params["display_name"] || "") <> (user_params["note"] || "")
100
101 user_info_emojis =
102 user.info
103 |> Map.get(:emoji, [])
104 |> Enum.concat(Emoji.Formatter.get_emoji_map(emojis_text))
105 |> Enum.dedup()
106
107 info_params =
108 [
109 :no_rich_text,
110 :locked,
111 :hide_followers_count,
112 :hide_follows_count,
113 :hide_followers,
114 :hide_follows,
115 :hide_favorites,
116 :show_role,
117 :skip_thread_containment,
118 :discoverable
119 ]
120 |> Enum.reduce(%{}, fn key, acc ->
121 add_if_present(acc, params, to_string(key), key, fn value ->
122 {:ok, truthy_param?(value)}
123 end)
124 end)
125 |> add_if_present(params, "default_scope", :default_scope)
126 |> add_if_present(params, "fields", :fields, fn fields ->
127 fields = Enum.map(fields, fn f -> Map.update!(f, "value", &AutoLinker.link(&1)) end)
128
129 {:ok, fields}
130 end)
131 |> add_if_present(params, "fields", :raw_fields)
132 |> add_if_present(params, "pleroma_settings_store", :pleroma_settings_store, fn value ->
133 {:ok, Map.merge(user.info.pleroma_settings_store, value)}
134 end)
135 |> add_if_present(params, "header", :banner, fn value ->
136 with %Plug.Upload{} <- value,
137 {:ok, object} <- ActivityPub.upload(value, type: :banner) do
138 {:ok, object.data}
139 else
140 _ -> :error
141 end
142 end)
143 |> add_if_present(params, "pleroma_background_image", :background, fn value ->
144 with %Plug.Upload{} <- value,
145 {:ok, object} <- ActivityPub.upload(value, type: :background) do
146 {:ok, object.data}
147 else
148 _ -> :error
149 end
150 end)
151 |> Map.put(:emoji, user_info_emojis)
152
153 changeset =
154 user
155 |> User.update_changeset(user_params)
156 |> User.change_info(&User.Info.profile_update(&1, info_params))
157
158 with {:ok, user} <- User.update_and_set_cache(changeset) do
159 if original_user != user, do: CommonAPI.update(user)
160
161 json(
162 conn,
163 AccountView.render("show.json", %{user: user, for: user, with_pleroma_settings: true})
164 )
165 else
166 _e -> render_error(conn, :forbidden, "Invalid request")
167 end
168 end
169
170 def update_avatar(%{assigns: %{user: user}} = conn, %{"img" => ""}) do
171 change = Changeset.change(user, %{avatar: nil})
172 {:ok, user} = User.update_and_set_cache(change)
173 CommonAPI.update(user)
174
175 json(conn, %{url: nil})
176 end
177
178 def update_avatar(%{assigns: %{user: user}} = conn, params) do
179 {:ok, object} = ActivityPub.upload(params, type: :avatar)
180 change = Changeset.change(user, %{avatar: object.data})
181 {:ok, user} = User.update_and_set_cache(change)
182 CommonAPI.update(user)
183 %{"url" => [%{"href" => href} | _]} = object.data
184
185 json(conn, %{url: href})
186 end
187
188 def update_banner(%{assigns: %{user: user}} = conn, %{"banner" => ""}) do
189 new_info = %{"banner" => %{}}
190
191 with {:ok, user} <- User.update_info(user, &User.Info.profile_update(&1, new_info)) do
192 CommonAPI.update(user)
193 json(conn, %{url: nil})
194 end
195 end
196
197 def update_banner(%{assigns: %{user: user}} = conn, params) do
198 with {:ok, object} <- ActivityPub.upload(%{"img" => params["banner"]}, type: :banner),
199 new_info <- %{"banner" => object.data},
200 {:ok, user} <- User.update_info(user, &User.Info.profile_update(&1, new_info)) do
201 CommonAPI.update(user)
202 %{"url" => [%{"href" => href} | _]} = object.data
203
204 json(conn, %{url: href})
205 end
206 end
207
208 def update_background(%{assigns: %{user: user}} = conn, %{"img" => ""}) do
209 new_info = %{"background" => %{}}
210
211 with {:ok, _user} <- User.update_info(user, &User.Info.profile_update(&1, new_info)) do
212 json(conn, %{url: nil})
213 end
214 end
215
216 def update_background(%{assigns: %{user: user}} = conn, params) do
217 with {:ok, object} <- ActivityPub.upload(params, type: :background),
218 new_info <- %{"background" => object.data},
219 {:ok, _user} <- User.update_info(user, &User.Info.profile_update(&1, new_info)) do
220 %{"url" => [%{"href" => href} | _]} = object.data
221
222 json(conn, %{url: href})
223 end
224 end
225
226 def verify_credentials(%{assigns: %{user: user}} = conn, _) do
227 chat_token = Phoenix.Token.sign(conn, "user socket", user.id)
228
229 account =
230 AccountView.render("show.json", %{
231 user: user,
232 for: user,
233 with_pleroma_settings: true,
234 with_chat_token: chat_token
235 })
236
237 json(conn, account)
238 end
239
240 def verify_app_credentials(%{assigns: %{user: _user, token: token}} = conn, _) do
241 with %Token{app: %App{} = app} <- Repo.preload(token, :app) do
242 conn
243 |> put_view(AppView)
244 |> render("short.json", %{app: app})
245 end
246 end
247
248 @mastodon_api_level "2.7.2"
249
250 def masto_instance(conn, _params) do
251 instance = Config.get(:instance)
252
253 response = %{
254 uri: Web.base_url(),
255 title: Keyword.get(instance, :name),
256 description: Keyword.get(instance, :description),
257 version: "#{@mastodon_api_level} (compatible; #{Pleroma.Application.named_version()})",
258 email: Keyword.get(instance, :email),
259 urls: %{
260 streaming_api: Pleroma.Web.Endpoint.websocket_url()
261 },
262 stats: Stats.get_stats(),
263 thumbnail: Web.base_url() <> "/instance/thumbnail.jpeg",
264 languages: ["en"],
265 registrations: Pleroma.Config.get([:instance, :registrations_open]),
266 # Extra (not present in Mastodon):
267 max_toot_chars: Keyword.get(instance, :limit),
268 poll_limits: Keyword.get(instance, :poll_limits)
269 }
270
271 json(conn, response)
272 end
273
274 def peers(conn, _params) do
275 json(conn, Stats.get_peers())
276 end
277
278 defp mastodonized_emoji do
279 Pleroma.Emoji.get_all()
280 |> Enum.map(fn {shortcode, %Pleroma.Emoji{file: relative_url, tags: tags}} ->
281 url = to_string(URI.merge(Web.base_url(), relative_url))
282
283 %{
284 "shortcode" => shortcode,
285 "static_url" => url,
286 "visible_in_picker" => true,
287 "url" => url,
288 "tags" => tags,
289 # Assuming that a comma is authorized in the category name
290 "category" => (tags -- ["Custom"]) |> Enum.join(",")
291 }
292 end)
293 end
294
295 def custom_emojis(conn, _params) do
296 mastodon_emoji = mastodonized_emoji()
297 json(conn, mastodon_emoji)
298 end
299
300 def get_poll(%{assigns: %{user: user}} = conn, %{"id" => id}) do
301 with %Object{} = object <- Object.get_by_id_and_maybe_refetch(id, interval: 60),
302 %Activity{} = activity <- Activity.get_create_by_object_ap_id(object.data["id"]),
303 true <- Visibility.visible_for_user?(activity, user) do
304 conn
305 |> put_view(StatusView)
306 |> try_render("poll.json", %{object: object, for: user})
307 else
308 error when is_nil(error) or error == false ->
309 render_error(conn, :not_found, "Record not found")
310 end
311 end
312
313 defp get_cached_vote_or_vote(user, object, choices) do
314 idempotency_key = "polls:#{user.id}:#{object.data["id"]}"
315
316 {_, res} =
317 Cachex.fetch(:idempotency_cache, idempotency_key, fn _ ->
318 case CommonAPI.vote(user, object, choices) do
319 {:error, _message} = res -> {:ignore, res}
320 res -> {:commit, res}
321 end
322 end)
323
324 res
325 end
326
327 def poll_vote(%{assigns: %{user: user}} = conn, %{"id" => id, "choices" => choices}) do
328 with %Object{} = object <- Object.get_by_id(id),
329 true <- object.data["type"] == "Question",
330 %Activity{} = activity <- Activity.get_create_by_object_ap_id(object.data["id"]),
331 true <- Visibility.visible_for_user?(activity, user),
332 {:ok, _activities, object} <- get_cached_vote_or_vote(user, object, choices) do
333 conn
334 |> put_view(StatusView)
335 |> try_render("poll.json", %{object: object, for: user})
336 else
337 nil ->
338 render_error(conn, :not_found, "Record not found")
339
340 false ->
341 render_error(conn, :not_found, "Record not found")
342
343 {:error, message} ->
344 conn
345 |> put_status(:unprocessable_entity)
346 |> json(%{error: message})
347 end
348 end
349
350 def relationships(%{assigns: %{user: user}} = conn, %{"id" => id}) do
351 targets = User.get_all_by_ids(List.wrap(id))
352
353 conn
354 |> put_view(AccountView)
355 |> render("relationships.json", %{user: user, targets: targets})
356 end
357
358 # Instead of returning a 400 when no "id" params is present, Mastodon returns an empty array.
359 def relationships(%{assigns: %{user: _user}} = conn, _), do: json(conn, [])
360
361 def update_media(
362 %{assigns: %{user: user}} = conn,
363 %{"id" => id, "description" => description} = _
364 )
365 when is_binary(description) do
366 with %Object{} = object <- Repo.get(Object, id),
367 true <- Object.authorize_mutation(object, user),
368 {:ok, %Object{data: data}} <- Object.update_data(object, %{"name" => description}) do
369 attachment_data = Map.put(data, "id", object.id)
370
371 conn
372 |> put_view(StatusView)
373 |> render("attachment.json", %{attachment: attachment_data})
374 end
375 end
376
377 def update_media(_conn, _data), do: {:error, :bad_request}
378
379 def upload(%{assigns: %{user: user}} = conn, %{"file" => file} = data) do
380 with {:ok, object} <-
381 ActivityPub.upload(
382 file,
383 actor: User.ap_id(user),
384 description: Map.get(data, "description")
385 ) do
386 attachment_data = Map.put(object.data, "id", object.id)
387
388 conn
389 |> put_view(StatusView)
390 |> render("attachment.json", %{attachment: attachment_data})
391 end
392 end
393
394 def set_mascot(%{assigns: %{user: user}} = conn, %{"file" => file}) do
395 with {:ok, object} <- ActivityPub.upload(file, actor: User.ap_id(user)),
396 %{} = attachment_data <- Map.put(object.data, "id", object.id),
397 # Reject if not an image
398 %{type: "image"} = rendered <-
399 StatusView.render("attachment.json", %{attachment: attachment_data}) do
400 # Sure!
401 # Save to the user's info
402 {:ok, _user} = User.update_info(user, &User.Info.mascot_update(&1, rendered))
403
404 json(conn, rendered)
405 else
406 %{type: _} -> render_error(conn, :unsupported_media_type, "mascots can only be images")
407 end
408 end
409
410 def get_mascot(%{assigns: %{user: user}} = conn, _params) do
411 mascot = User.get_mascot(user)
412
413 json(conn, mascot)
414 end
415
416 def follows(%{assigns: %{user: follower}} = conn, %{"uri" => uri}) do
417 with {_, %User{} = followed} <- {:followed, User.get_cached_by_nickname(uri)},
418 {_, true} <- {:followed, follower.id != followed.id},
419 {:ok, follower, followed, _} <- CommonAPI.follow(follower, followed) do
420 conn
421 |> put_view(AccountView)
422 |> render("show.json", %{user: followed, for: follower})
423 else
424 {:followed, _} ->
425 {:error, :not_found}
426
427 {:error, message} ->
428 conn
429 |> put_status(:forbidden)
430 |> json(%{error: message})
431 end
432 end
433
434 def mutes(%{assigns: %{user: user}} = conn, _) do
435 with muted_accounts <- User.muted_users(user) do
436 res = AccountView.render("index.json", users: muted_accounts, for: user, as: :user)
437 json(conn, res)
438 end
439 end
440
441 def blocks(%{assigns: %{user: user}} = conn, _) do
442 with blocked_accounts <- User.blocked_users(user) do
443 res = AccountView.render("index.json", users: blocked_accounts, for: user, as: :user)
444 json(conn, res)
445 end
446 end
447
448 def favourites(%{assigns: %{user: user}} = conn, params) do
449 params =
450 params
451 |> Map.put("type", "Create")
452 |> Map.put("favorited_by", user.ap_id)
453 |> Map.put("blocking_user", user)
454
455 activities =
456 ActivityPub.fetch_activities([], params)
457 |> Enum.reverse()
458
459 conn
460 |> add_link_headers(activities)
461 |> put_view(StatusView)
462 |> render("index.json", %{activities: activities, for: user, as: :activity})
463 end
464
465 def bookmarks(%{assigns: %{user: user}} = conn, params) do
466 user = User.get_cached_by_id(user.id)
467
468 bookmarks =
469 Bookmark.for_user_query(user.id)
470 |> Pagination.fetch_paginated(params)
471
472 activities =
473 bookmarks
474 |> Enum.map(fn b -> Map.put(b.activity, :bookmark, Map.delete(b, :activity)) end)
475
476 conn
477 |> add_link_headers(bookmarks)
478 |> put_view(StatusView)
479 |> render("index.json", %{activities: activities, for: user, as: :activity})
480 end
481
482 def index(%{assigns: %{user: user}} = conn, _params) do
483 token = get_session(conn, :oauth_token)
484
485 if user && token do
486 mastodon_emoji = mastodonized_emoji()
487
488 limit = Config.get([:instance, :limit])
489
490 accounts = Map.put(%{}, user.id, AccountView.render("show.json", %{user: user, for: user}))
491
492 initial_state =
493 %{
494 meta: %{
495 streaming_api_base_url: Pleroma.Web.Endpoint.websocket_url(),
496 access_token: token,
497 locale: "en",
498 domain: Pleroma.Web.Endpoint.host(),
499 admin: "1",
500 me: "#{user.id}",
501 unfollow_modal: false,
502 boost_modal: false,
503 delete_modal: true,
504 auto_play_gif: false,
505 display_sensitive_media: false,
506 reduce_motion: false,
507 max_toot_chars: limit,
508 mascot: User.get_mascot(user)["url"]
509 },
510 poll_limits: Config.get([:instance, :poll_limits]),
511 rights: %{
512 delete_others_notice: present?(user.info.is_moderator),
513 admin: present?(user.info.is_admin)
514 },
515 compose: %{
516 me: "#{user.id}",
517 default_privacy: user.info.default_scope,
518 default_sensitive: false,
519 allow_content_types: Config.get([:instance, :allowed_post_formats])
520 },
521 media_attachments: %{
522 accept_content_types: [
523 ".jpg",
524 ".jpeg",
525 ".png",
526 ".gif",
527 ".webm",
528 ".mp4",
529 ".m4v",
530 "image\/jpeg",
531 "image\/png",
532 "image\/gif",
533 "video\/webm",
534 "video\/mp4"
535 ]
536 },
537 settings:
538 user.info.settings ||
539 %{
540 onboarded: true,
541 home: %{
542 shows: %{
543 reblog: true,
544 reply: true
545 }
546 },
547 notifications: %{
548 alerts: %{
549 follow: true,
550 favourite: true,
551 reblog: true,
552 mention: true
553 },
554 shows: %{
555 follow: true,
556 favourite: true,
557 reblog: true,
558 mention: true
559 },
560 sounds: %{
561 follow: true,
562 favourite: true,
563 reblog: true,
564 mention: true
565 }
566 }
567 },
568 push_subscription: nil,
569 accounts: accounts,
570 custom_emojis: mastodon_emoji,
571 char_limit: limit
572 }
573 |> Jason.encode!()
574
575 conn
576 |> put_layout(false)
577 |> put_view(MastodonView)
578 |> render("index.html", %{initial_state: initial_state})
579 else
580 conn
581 |> put_session(:return_to, conn.request_path)
582 |> redirect(to: "/web/login")
583 end
584 end
585
586 def put_settings(%{assigns: %{user: user}} = conn, %{"data" => settings} = _params) do
587 with {:ok, _} <- User.update_info(user, &User.Info.mastodon_settings_update(&1, settings)) do
588 json(conn, %{})
589 else
590 e ->
591 conn
592 |> put_status(:internal_server_error)
593 |> json(%{error: inspect(e)})
594 end
595 end
596
597 def login(%{assigns: %{user: %User{}}} = conn, _params) do
598 redirect(conn, to: local_mastodon_root_path(conn))
599 end
600
601 @doc "Local Mastodon FE login init action"
602 def login(conn, %{"code" => auth_token}) do
603 with {:ok, app} <- get_or_make_app(),
604 {:ok, auth} <- Authorization.get_by_token(app, auth_token),
605 {:ok, token} <- Token.exchange_token(app, auth) do
606 conn
607 |> put_session(:oauth_token, token.token)
608 |> redirect(to: local_mastodon_root_path(conn))
609 end
610 end
611
612 @doc "Local Mastodon FE callback action"
613 def login(conn, _) do
614 with {:ok, app} <- get_or_make_app() do
615 path =
616 o_auth_path(conn, :authorize,
617 response_type: "code",
618 client_id: app.client_id,
619 redirect_uri: ".",
620 scope: Enum.join(app.scopes, " ")
621 )
622
623 redirect(conn, to: path)
624 end
625 end
626
627 defp local_mastodon_root_path(conn) do
628 case get_session(conn, :return_to) do
629 nil ->
630 mastodon_api_path(conn, :index, ["getting-started"])
631
632 return_to ->
633 delete_session(conn, :return_to)
634 return_to
635 end
636 end
637
638 @spec get_or_make_app() :: {:ok, App.t()} | {:error, Ecto.Changeset.t()}
639 defp get_or_make_app do
640 App.get_or_make(
641 %{client_name: @local_mastodon_name, redirect_uris: "."},
642 ["read", "write", "follow", "push"]
643 )
644 end
645
646 def logout(conn, _) do
647 conn
648 |> clear_session
649 |> redirect(to: "/")
650 end
651
652 # Stubs for unimplemented mastodon api
653 #
654 def empty_array(conn, _) do
655 Logger.debug("Unimplemented, returning an empty array")
656 json(conn, [])
657 end
658
659 def empty_object(conn, _) do
660 Logger.debug("Unimplemented, returning an empty object")
661 json(conn, %{})
662 end
663
664 def suggestions(%{assigns: %{user: user}} = conn, _) do
665 suggestions = Config.get(:suggestions)
666
667 if Keyword.get(suggestions, :enabled, false) do
668 api = Keyword.get(suggestions, :third_party_engine, "")
669 timeout = Keyword.get(suggestions, :timeout, 5000)
670 limit = Keyword.get(suggestions, :limit, 23)
671
672 host = Config.get([Pleroma.Web.Endpoint, :url, :host])
673
674 user = user.nickname
675
676 url =
677 api
678 |> String.replace("{{host}}", host)
679 |> String.replace("{{user}}", user)
680
681 with {:ok, %{status: 200, body: body}} <-
682 HTTP.get(url, [], adapter: [recv_timeout: timeout, pool: :default]),
683 {:ok, data} <- Jason.decode(body) do
684 data =
685 data
686 |> Enum.slice(0, limit)
687 |> Enum.map(fn x ->
688 x
689 |> Map.put("id", fetch_suggestion_id(x))
690 |> Map.put("avatar", MediaProxy.url(x["avatar"]))
691 |> Map.put("avatar_static", MediaProxy.url(x["avatar_static"]))
692 end)
693
694 json(conn, data)
695 else
696 e ->
697 Logger.error("Could not retrieve suggestions at fetch #{url}, #{inspect(e)}")
698 end
699 else
700 json(conn, [])
701 end
702 end
703
704 defp fetch_suggestion_id(attrs) do
705 case User.get_or_fetch(attrs["acct"]) do
706 {:ok, %User{id: id}} -> id
707 _ -> 0
708 end
709 end
710
711 def account_register(
712 %{assigns: %{app: app}} = conn,
713 %{"username" => nickname, "email" => _, "password" => _, "agreement" => true} = params
714 ) do
715 params =
716 params
717 |> Map.take([
718 "email",
719 "captcha_solution",
720 "captcha_token",
721 "captcha_answer_data",
722 "token",
723 "password"
724 ])
725 |> Map.put("nickname", nickname)
726 |> Map.put("fullname", params["fullname"] || nickname)
727 |> Map.put("bio", params["bio"] || "")
728 |> Map.put("confirm", params["password"])
729
730 with {:ok, user} <- TwitterAPI.register_user(params, need_confirmation: true),
731 {:ok, token} <- Token.create_token(app, user, %{scopes: app.scopes}) do
732 json(conn, %{
733 token_type: "Bearer",
734 access_token: token.token,
735 scope: app.scopes,
736 created_at: Token.Utils.format_created_at(token)
737 })
738 else
739 {:error, errors} ->
740 conn
741 |> put_status(:bad_request)
742 |> json(errors)
743 end
744 end
745
746 def account_register(%{assigns: %{app: _app}} = conn, _) do
747 render_error(conn, :bad_request, "Missing parameters")
748 end
749
750 def account_register(conn, _) do
751 render_error(conn, :forbidden, "Invalid credentials")
752 end
753
754 def password_reset(conn, params) do
755 nickname_or_email = params["email"] || params["nickname"]
756
757 with {:ok, _} <- TwitterAPI.password_reset(nickname_or_email) do
758 conn
759 |> put_status(:no_content)
760 |> json("")
761 else
762 {:error, "unknown user"} ->
763 send_resp(conn, :not_found, "")
764
765 {:error, _} ->
766 send_resp(conn, :bad_request, "")
767 end
768 end
769
770 def account_confirmation_resend(conn, params) do
771 nickname_or_email = params["email"] || params["nickname"]
772
773 with %User{} = user <- User.get_by_nickname_or_email(nickname_or_email),
774 {:ok, _} <- User.try_send_confirmation_email(user) do
775 conn
776 |> json_response(:no_content, "")
777 end
778 end
779
780 def try_render(conn, target, params)
781 when is_binary(target) do
782 case render(conn, target, params) do
783 nil -> render_error(conn, :not_implemented, "Can't display this activity")
784 res -> res
785 end
786 end
787
788 def try_render(conn, _, _) do
789 render_error(conn, :not_implemented, "Can't display this activity")
790 end
791
792 defp present?(nil), do: false
793 defp present?(false), do: false
794 defp present?(_), do: true
795 end