1 # Pleroma: A lightweight social networking server
2 # Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
3 # SPDX-License-Identifier: AGPL-3.0-only
5 defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
6 use Pleroma.Web, :controller
8 import Pleroma.Web.ControllerHelper, only: [json_response: 3]
11 alias Pleroma.Activity
12 alias Pleroma.Bookmark
14 alias Pleroma.Conversation.Participation
16 alias Pleroma.Formatter
18 alias Pleroma.Notification
20 alias Pleroma.Pagination
21 alias Pleroma.Plugs.RateLimiter
23 alias Pleroma.ScheduledActivity
27 alias Pleroma.Web.ActivityPub.ActivityPub
28 alias Pleroma.Web.ActivityPub.Visibility
29 alias Pleroma.Web.CommonAPI
30 alias Pleroma.Web.MastodonAPI.AccountView
31 alias Pleroma.Web.MastodonAPI.AppView
32 alias Pleroma.Web.MastodonAPI.ConversationView
33 alias Pleroma.Web.MastodonAPI.FilterView
34 alias Pleroma.Web.MastodonAPI.ListView
35 alias Pleroma.Web.MastodonAPI.MastodonAPI
36 alias Pleroma.Web.MastodonAPI.MastodonView
37 alias Pleroma.Web.MastodonAPI.NotificationView
38 alias Pleroma.Web.MastodonAPI.ReportView
39 alias Pleroma.Web.MastodonAPI.ScheduledActivityView
40 alias Pleroma.Web.MastodonAPI.StatusView
41 alias Pleroma.Web.MediaProxy
42 alias Pleroma.Web.OAuth.App
43 alias Pleroma.Web.OAuth.Authorization
44 alias Pleroma.Web.OAuth.Scopes
45 alias Pleroma.Web.OAuth.Token
46 alias Pleroma.Web.TwitterAPI.TwitterAPI
48 alias Pleroma.Web.ControllerHelper
52 require Pleroma.Constants
54 @rate_limited_relations_actions ~w(follow unfollow)a
56 @rate_limited_status_actions ~w(reblog_status unreblog_status fav_status unfav_status
57 post_status delete_status)a
61 {:status_id_action, bucket_name: "status_id_action:reblog_unreblog", params: ["id"]}
62 when action in ~w(reblog_status unreblog_status)a
67 {:status_id_action, bucket_name: "status_id_action:fav_unfav", params: ["id"]}
68 when action in ~w(fav_status unfav_status)a
73 {:relations_id_action, params: ["id", "uri"]} when action in @rate_limited_relations_actions
76 plug(RateLimiter, :relations_actions when action in @rate_limited_relations_actions)
77 plug(RateLimiter, :statuses_actions when action in @rate_limited_status_actions)
78 plug(RateLimiter, :app_account_creation when action == :account_register)
79 plug(RateLimiter, :search when action in [:search, :search2, :account_search])
80 plug(RateLimiter, :password_reset when action == :password_reset)
81 plug(RateLimiter, :account_confirmation_resend when action == :account_confirmation_resend)
83 @local_mastodon_name "Mastodon-Local"
85 action_fallback(:errors)
87 def create_app(conn, params) do
88 scopes = Scopes.fetch_scopes(params, ["read"])
92 |> Map.drop(["scope", "scopes"])
93 |> Map.put("scopes", scopes)
95 with cs <- App.register_changeset(%App{}, app_attrs),
96 false <- cs.changes[:client_name] == @local_mastodon_name,
97 {:ok, app} <- Repo.insert(cs) do
100 |> render("show.json", %{app: app})
109 value_function \\ fn x -> {:ok, x} end
111 if Map.has_key?(params, params_field) do
112 case value_function.(params[params_field]) do
113 {:ok, new_value} -> Map.put(map, map_field, new_value)
121 def update_credentials(%{assigns: %{user: user}} = conn, params) do
126 |> add_if_present(params, "display_name", :name)
127 |> add_if_present(params, "note", :bio, fn value -> {:ok, User.parse_bio(value, user)} end)
128 |> add_if_present(params, "avatar", :avatar, fn value ->
129 with %Plug.Upload{} <- value,
130 {:ok, object} <- ActivityPub.upload(value, type: :avatar) do
137 emojis_text = (user_params["display_name"] || "") <> (user_params["note"] || "")
140 ((user.info.emoji || []) ++ Formatter.get_emoji_map(emojis_text))
151 :skip_thread_containment
153 |> Enum.reduce(%{}, fn key, acc ->
154 add_if_present(acc, params, to_string(key), key, fn value ->
155 {:ok, ControllerHelper.truthy_param?(value)}
158 |> add_if_present(params, "default_scope", :default_scope)
159 |> add_if_present(params, "pleroma_settings_store", :pleroma_settings_store, fn value ->
160 {:ok, Map.merge(user.info.pleroma_settings_store, value)}
162 |> add_if_present(params, "header", :banner, fn value ->
163 with %Plug.Upload{} <- value,
164 {:ok, object} <- ActivityPub.upload(value, type: :banner) do
170 |> add_if_present(params, "pleroma_background_image", :background, fn value ->
171 with %Plug.Upload{} <- value,
172 {:ok, object} <- ActivityPub.upload(value, type: :background) do
178 |> Map.put(:emoji, user_info_emojis)
180 info_cng = User.Info.profile_update(user.info, info_params)
182 with changeset <- User.update_changeset(user, user_params),
183 changeset <- Ecto.Changeset.put_embed(changeset, :info, info_cng),
184 {:ok, user} <- User.update_and_set_cache(changeset) do
185 if original_user != user do
186 CommonAPI.update(user)
191 AccountView.render("account.json", %{user: user, for: user, with_pleroma_settings: true})
194 _e -> render_error(conn, :forbidden, "Invalid request")
198 def update_avatar(%{assigns: %{user: user}} = conn, %{"img" => ""}) do
199 change = Changeset.change(user, %{avatar: nil})
200 {:ok, user} = User.update_and_set_cache(change)
201 CommonAPI.update(user)
203 json(conn, %{url: nil})
206 def update_avatar(%{assigns: %{user: user}} = conn, params) do
207 {:ok, object} = ActivityPub.upload(params, type: :avatar)
208 change = Changeset.change(user, %{avatar: object.data})
209 {:ok, user} = User.update_and_set_cache(change)
210 CommonAPI.update(user)
211 %{"url" => [%{"href" => href} | _]} = object.data
213 json(conn, %{url: href})
216 def update_banner(%{assigns: %{user: user}} = conn, %{"banner" => ""}) do
217 with new_info <- %{"banner" => %{}},
218 info_cng <- User.Info.profile_update(user.info, new_info),
219 changeset <- Ecto.Changeset.change(user) |> Ecto.Changeset.put_embed(:info, info_cng),
220 {:ok, user} <- User.update_and_set_cache(changeset) do
221 CommonAPI.update(user)
223 json(conn, %{url: nil})
227 def update_banner(%{assigns: %{user: user}} = conn, params) do
228 with {:ok, object} <- ActivityPub.upload(%{"img" => params["banner"]}, type: :banner),
229 new_info <- %{"banner" => object.data},
230 info_cng <- User.Info.profile_update(user.info, new_info),
231 changeset <- Ecto.Changeset.change(user) |> Ecto.Changeset.put_embed(:info, info_cng),
232 {:ok, user} <- User.update_and_set_cache(changeset) do
233 CommonAPI.update(user)
234 %{"url" => [%{"href" => href} | _]} = object.data
236 json(conn, %{url: href})
240 def update_background(%{assigns: %{user: user}} = conn, %{"img" => ""}) do
241 with new_info <- %{"background" => %{}},
242 info_cng <- User.Info.profile_update(user.info, new_info),
243 changeset <- Ecto.Changeset.change(user) |> Ecto.Changeset.put_embed(:info, info_cng),
244 {:ok, _user} <- User.update_and_set_cache(changeset) do
245 json(conn, %{url: nil})
249 def update_background(%{assigns: %{user: user}} = conn, params) do
250 with {:ok, object} <- ActivityPub.upload(params, type: :background),
251 new_info <- %{"background" => object.data},
252 info_cng <- User.Info.profile_update(user.info, new_info),
253 changeset <- Ecto.Changeset.change(user) |> Ecto.Changeset.put_embed(:info, info_cng),
254 {:ok, _user} <- User.update_and_set_cache(changeset) do
255 %{"url" => [%{"href" => href} | _]} = object.data
257 json(conn, %{url: href})
261 def verify_credentials(%{assigns: %{user: user}} = conn, _) do
262 chat_token = Phoenix.Token.sign(conn, "user socket", user.id)
265 AccountView.render("account.json", %{
268 with_pleroma_settings: true,
269 with_chat_token: chat_token
275 def verify_app_credentials(%{assigns: %{user: _user, token: token}} = conn, _) do
276 with %Token{app: %App{} = app} <- Repo.preload(token, :app) do
279 |> render("short.json", %{app: app})
283 def user(%{assigns: %{user: for_user}} = conn, %{"id" => nickname_or_id}) do
284 with %User{} = user <- User.get_cached_by_nickname_or_id(nickname_or_id),
285 true <- User.auth_active?(user) || user.id == for_user.id || User.superuser?(for_user) do
286 account = AccountView.render("account.json", %{user: user, for: for_user})
289 _e -> render_error(conn, :not_found, "Can't find user")
293 @mastodon_api_level "2.7.2"
295 def masto_instance(conn, _params) do
296 instance = Config.get(:instance)
300 title: Keyword.get(instance, :name),
301 description: Keyword.get(instance, :description),
302 version: "#{@mastodon_api_level} (compatible; #{Pleroma.Application.named_version()})",
303 email: Keyword.get(instance, :email),
305 streaming_api: Pleroma.Web.Endpoint.websocket_url()
307 stats: Stats.get_stats(),
308 thumbnail: Web.base_url() <> "/instance/thumbnail.jpeg",
310 registrations: Pleroma.Config.get([:instance, :registrations_open]),
311 # Extra (not present in Mastodon):
312 max_toot_chars: Keyword.get(instance, :limit),
313 poll_limits: Keyword.get(instance, :poll_limits)
319 def peers(conn, _params) do
320 json(conn, Stats.get_peers())
323 defp mastodonized_emoji do
324 Pleroma.Emoji.get_all()
325 |> Enum.map(fn {shortcode, relative_url, tags} ->
326 url = to_string(URI.merge(Web.base_url(), relative_url))
329 "shortcode" => shortcode,
331 "visible_in_picker" => true,
334 # Assuming that a comma is authorized in the category name
335 "category" => (tags -- ["Custom"]) |> Enum.join(",")
340 def custom_emojis(conn, _params) do
341 mastodon_emoji = mastodonized_emoji()
342 json(conn, mastodon_emoji)
345 defp add_link_headers(conn, method, activities, param \\ nil, params \\ %{}) do
348 |> Map.drop(["since_id", "max_id", "min_id"])
351 last = List.last(activities)
358 |> Map.get("limit", "20")
359 |> String.to_integer()
362 if length(activities) <= limit do
368 |> Enum.at(limit * -1)
372 {next_url, prev_url} =
376 Pleroma.Web.Endpoint,
379 Map.merge(params, %{max_id: max_id})
382 Pleroma.Web.Endpoint,
385 Map.merge(params, %{min_id: min_id})
391 Pleroma.Web.Endpoint,
393 Map.merge(params, %{max_id: max_id})
396 Pleroma.Web.Endpoint,
398 Map.merge(params, %{min_id: min_id})
404 |> put_resp_header("link", "<#{next_url}>; rel=\"next\", <#{prev_url}>; rel=\"prev\"")
410 def home_timeline(%{assigns: %{user: user}} = conn, params) do
413 |> Map.put("type", ["Create", "Announce"])
414 |> Map.put("blocking_user", user)
415 |> Map.put("muting_user", user)
416 |> Map.put("user", user)
419 [user.ap_id | user.following]
420 |> ActivityPub.fetch_activities(params)
424 |> add_link_headers(:home_timeline, activities)
425 |> put_view(StatusView)
426 |> render("index.json", %{activities: activities, for: user, as: :activity})
429 def public_timeline(%{assigns: %{user: user}} = conn, params) do
430 local_only = params["local"] in [true, "True", "true", "1"]
434 |> Map.put("type", ["Create", "Announce"])
435 |> Map.put("local_only", local_only)
436 |> Map.put("blocking_user", user)
437 |> Map.put("muting_user", user)
438 |> Map.put("user", user)
439 |> ActivityPub.fetch_public_activities()
443 |> add_link_headers(:public_timeline, activities, false, %{"local" => local_only})
444 |> put_view(StatusView)
445 |> render("index.json", %{activities: activities, for: user, as: :activity})
448 def user_statuses(%{assigns: %{user: reading_user}} = conn, params) do
449 with %User{} = user <- User.get_cached_by_nickname_or_id(params["id"]) do
452 |> Map.put("tag", params["tagged"])
454 activities = ActivityPub.fetch_user_activities(user, reading_user, params)
457 |> add_link_headers(:user_statuses, activities, params["id"])
458 |> put_view(StatusView)
459 |> render("index.json", %{
460 activities: activities,
467 def dm_timeline(%{assigns: %{user: user}} = conn, params) do
470 |> Map.put("type", "Create")
471 |> Map.put("blocking_user", user)
472 |> Map.put("user", user)
473 |> Map.put(:visibility, "direct")
477 |> ActivityPub.fetch_activities_query(params)
478 |> Pagination.fetch_paginated(params)
481 |> add_link_headers(:dm_timeline, activities)
482 |> put_view(StatusView)
483 |> render("index.json", %{activities: activities, for: user, as: :activity})
486 def get_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
487 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
488 true <- Visibility.visible_for_user?(activity, user) do
490 |> put_view(StatusView)
491 |> try_render("status.json", %{activity: activity, for: user})
495 def get_context(%{assigns: %{user: user}} = conn, %{"id" => id}) do
496 with %Activity{} = activity <- Activity.get_by_id(id),
498 ActivityPub.fetch_activities_for_context(activity.data["context"], %{
499 "blocking_user" => user,
503 activities |> Enum.filter(fn %{id: aid} -> to_string(aid) != to_string(id) end),
505 activities |> Enum.filter(fn %{data: %{"type" => type}} -> type == "Create" end),
506 grouped_activities <- Enum.group_by(activities, fn %{id: id} -> id < activity.id end) do
512 activities: grouped_activities[true] || [],
516 # credo:disable-for-previous-line Credo.Check.Refactor.PipeChainStart
521 activities: grouped_activities[false] || [],
525 # credo:disable-for-previous-line Credo.Check.Refactor.PipeChainStart
532 def get_poll(%{assigns: %{user: user}} = conn, %{"id" => id}) do
533 with %Object{} = object <- Object.get_by_id(id),
534 %Activity{} = activity <- Activity.get_create_by_object_ap_id(object.data["id"]),
535 true <- Visibility.visible_for_user?(activity, user) do
537 |> put_view(StatusView)
538 |> try_render("poll.json", %{object: object, for: user})
540 error when is_nil(error) or error == false ->
541 render_error(conn, :not_found, "Record not found")
545 defp get_cached_vote_or_vote(user, object, choices) do
546 idempotency_key = "polls:#{user.id}:#{object.data["id"]}"
549 Cachex.fetch(:idempotency_cache, idempotency_key, fn _ ->
550 case CommonAPI.vote(user, object, choices) do
551 {:error, _message} = res -> {:ignore, res}
552 res -> {:commit, res}
559 def poll_vote(%{assigns: %{user: user}} = conn, %{"id" => id, "choices" => choices}) do
560 with %Object{} = object <- Object.get_by_id(id),
561 true <- object.data["type"] == "Question",
562 %Activity{} = activity <- Activity.get_create_by_object_ap_id(object.data["id"]),
563 true <- Visibility.visible_for_user?(activity, user),
564 {:ok, _activities, object} <- get_cached_vote_or_vote(user, object, choices) do
566 |> put_view(StatusView)
567 |> try_render("poll.json", %{object: object, for: user})
570 render_error(conn, :not_found, "Record not found")
573 render_error(conn, :not_found, "Record not found")
577 |> put_status(:unprocessable_entity)
578 |> json(%{error: message})
582 def scheduled_statuses(%{assigns: %{user: user}} = conn, params) do
583 with scheduled_activities <- MastodonAPI.get_scheduled_activities(user, params) do
585 |> add_link_headers(:scheduled_statuses, scheduled_activities)
586 |> put_view(ScheduledActivityView)
587 |> render("index.json", %{scheduled_activities: scheduled_activities})
591 def show_scheduled_status(%{assigns: %{user: user}} = conn, %{"id" => scheduled_activity_id}) do
592 with %ScheduledActivity{} = scheduled_activity <-
593 ScheduledActivity.get(user, scheduled_activity_id) do
595 |> put_view(ScheduledActivityView)
596 |> render("show.json", %{scheduled_activity: scheduled_activity})
598 _ -> {:error, :not_found}
602 def update_scheduled_status(
603 %{assigns: %{user: user}} = conn,
604 %{"id" => scheduled_activity_id} = params
606 with %ScheduledActivity{} = scheduled_activity <-
607 ScheduledActivity.get(user, scheduled_activity_id),
608 {:ok, scheduled_activity} <- ScheduledActivity.update(scheduled_activity, params) do
610 |> put_view(ScheduledActivityView)
611 |> render("show.json", %{scheduled_activity: scheduled_activity})
613 nil -> {:error, :not_found}
618 def delete_scheduled_status(%{assigns: %{user: user}} = conn, %{"id" => scheduled_activity_id}) do
619 with %ScheduledActivity{} = scheduled_activity <-
620 ScheduledActivity.get(user, scheduled_activity_id),
621 {:ok, scheduled_activity} <- ScheduledActivity.delete(scheduled_activity) do
623 |> put_view(ScheduledActivityView)
624 |> render("show.json", %{scheduled_activity: scheduled_activity})
626 nil -> {:error, :not_found}
631 def post_status(%{assigns: %{user: user}} = conn, %{"status" => _} = params) do
634 |> Map.put("in_reply_to_status_id", params["in_reply_to_id"])
636 scheduled_at = params["scheduled_at"]
638 if scheduled_at && ScheduledActivity.far_enough?(scheduled_at) do
639 with {:ok, scheduled_activity} <-
640 ScheduledActivity.create(user, %{"params" => params, "scheduled_at" => scheduled_at}) do
642 |> put_view(ScheduledActivityView)
643 |> render("show.json", %{scheduled_activity: scheduled_activity})
646 params = Map.drop(params, ["scheduled_at"])
648 case CommonAPI.post(user, params) do
651 |> put_status(:unprocessable_entity)
652 |> json(%{error: message})
656 |> put_view(StatusView)
657 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
662 def delete_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
663 with {:ok, %Activity{}} <- CommonAPI.delete(id, user) do
666 _e -> render_error(conn, :forbidden, "Can't delete this post")
670 def reblog_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
671 with {:ok, announce, _activity} <- CommonAPI.repeat(ap_id_or_id, user),
672 %Activity{} = announce <- Activity.normalize(announce.data) do
674 |> put_view(StatusView)
675 |> try_render("status.json", %{activity: announce, for: user, as: :activity})
679 def unreblog_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
680 with {:ok, _unannounce, %{data: %{"id" => id}}} <- CommonAPI.unrepeat(ap_id_or_id, user),
681 %Activity{} = activity <- Activity.get_create_by_object_ap_id_with_object(id) do
683 |> put_view(StatusView)
684 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
688 def fav_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
689 with {:ok, _fav, %{data: %{"id" => id}}} <- CommonAPI.favorite(ap_id_or_id, user),
690 %Activity{} = activity <- Activity.get_create_by_object_ap_id(id) do
692 |> put_view(StatusView)
693 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
697 def unfav_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
698 with {:ok, _, _, %{data: %{"id" => id}}} <- CommonAPI.unfavorite(ap_id_or_id, user),
699 %Activity{} = activity <- Activity.get_create_by_object_ap_id(id) do
701 |> put_view(StatusView)
702 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
706 def pin_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
707 with {:ok, activity} <- CommonAPI.pin(ap_id_or_id, user) do
709 |> put_view(StatusView)
710 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
714 def unpin_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
715 with {:ok, activity} <- CommonAPI.unpin(ap_id_or_id, user) do
717 |> put_view(StatusView)
718 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
722 def bookmark_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
723 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
724 %User{} = user <- User.get_cached_by_nickname(user.nickname),
725 true <- Visibility.visible_for_user?(activity, user),
726 {:ok, _bookmark} <- Bookmark.create(user.id, activity.id) do
728 |> put_view(StatusView)
729 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
733 def unbookmark_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
734 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
735 %User{} = user <- User.get_cached_by_nickname(user.nickname),
736 true <- Visibility.visible_for_user?(activity, user),
737 {:ok, _bookmark} <- Bookmark.destroy(user.id, activity.id) do
739 |> put_view(StatusView)
740 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
744 def mute_conversation(%{assigns: %{user: user}} = conn, %{"id" => id}) do
745 activity = Activity.get_by_id(id)
747 with {:ok, activity} <- CommonAPI.add_mute(user, activity) do
749 |> put_view(StatusView)
750 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
754 def unmute_conversation(%{assigns: %{user: user}} = conn, %{"id" => id}) do
755 activity = Activity.get_by_id(id)
757 with {:ok, activity} <- CommonAPI.remove_mute(user, activity) do
759 |> put_view(StatusView)
760 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
764 def notifications(%{assigns: %{user: user}} = conn, params) do
765 notifications = MastodonAPI.get_notifications(user, params)
768 |> add_link_headers(:notifications, notifications)
769 |> put_view(NotificationView)
770 |> render("index.json", %{notifications: notifications, for: user})
773 def get_notification(%{assigns: %{user: user}} = conn, %{"id" => id} = _params) do
774 with {:ok, notification} <- Notification.get(user, id) do
776 |> put_view(NotificationView)
777 |> render("show.json", %{notification: notification, for: user})
781 |> put_status(:forbidden)
782 |> json(%{"error" => reason})
786 def clear_notifications(%{assigns: %{user: user}} = conn, _params) do
787 Notification.clear(user)
791 def dismiss_notification(%{assigns: %{user: user}} = conn, %{"id" => id} = _params) do
792 with {:ok, _notif} <- Notification.dismiss(user, id) do
797 |> put_status(:forbidden)
798 |> json(%{"error" => reason})
802 def destroy_multiple(%{assigns: %{user: user}} = conn, %{"ids" => ids} = _params) do
803 Notification.destroy_multiple(user, ids)
807 def relationships(%{assigns: %{user: user}} = conn, %{"id" => id}) do
809 q = from(u in User, where: u.id in ^id)
810 targets = Repo.all(q)
813 |> put_view(AccountView)
814 |> render("relationships.json", %{user: user, targets: targets})
817 # Instead of returning a 400 when no "id" params is present, Mastodon returns an empty array.
818 def relationships(%{assigns: %{user: _user}} = conn, _), do: json(conn, [])
820 def update_media(%{assigns: %{user: user}} = conn, data) do
821 with %Object{} = object <- Repo.get(Object, data["id"]),
822 true <- Object.authorize_mutation(object, user),
823 true <- is_binary(data["description"]),
824 description <- data["description"] do
825 new_data = %{object.data | "name" => description}
829 |> Object.change(%{data: new_data})
832 attachment_data = Map.put(new_data, "id", object.id)
835 |> put_view(StatusView)
836 |> render("attachment.json", %{attachment: attachment_data})
840 def upload(%{assigns: %{user: user}} = conn, %{"file" => file} = data) do
841 with {:ok, object} <-
844 actor: User.ap_id(user),
845 description: Map.get(data, "description")
847 attachment_data = Map.put(object.data, "id", object.id)
850 |> put_view(StatusView)
851 |> render("attachment.json", %{attachment: attachment_data})
855 def set_mascot(%{assigns: %{user: user}} = conn, %{"file" => file}) do
856 with {:ok, object} <- ActivityPub.upload(file, actor: User.ap_id(user)),
857 %{} = attachment_data <- Map.put(object.data, "id", object.id),
858 %{type: type} = rendered <-
859 StatusView.render("attachment.json", %{attachment: attachment_data}) do
860 # Reject if not an image
861 if type == "image" do
863 # Save to the user's info
864 info_changeset = User.Info.mascot_update(user.info, rendered)
868 |> Ecto.Changeset.change()
869 |> Ecto.Changeset.put_embed(:info, info_changeset)
871 {:ok, _user} = User.update_and_set_cache(user_changeset)
876 render_error(conn, :unsupported_media_type, "mascots can only be images")
881 def get_mascot(%{assigns: %{user: user}} = conn, _params) do
882 mascot = User.get_mascot(user)
888 def favourited_by(%{assigns: %{user: user}} = conn, %{"id" => id}) do
889 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
890 %Object{data: %{"likes" => likes}} <- Object.normalize(activity) do
891 q = from(u in User, where: u.ap_id in ^likes)
895 |> Enum.filter(&(not User.blocks?(user, &1)))
898 |> put_view(AccountView)
899 |> render("accounts.json", %{for: user, users: users, as: :user})
905 def reblogged_by(%{assigns: %{user: user}} = conn, %{"id" => id}) do
906 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
907 %Object{data: %{"announcements" => announces}} <- Object.normalize(activity) do
908 q = from(u in User, where: u.ap_id in ^announces)
912 |> Enum.filter(&(not User.blocks?(user, &1)))
915 |> put_view(AccountView)
916 |> render("accounts.json", %{for: user, users: users, as: :user})
922 def hashtag_timeline(%{assigns: %{user: user}} = conn, params) do
923 local_only = params["local"] in [true, "True", "true", "1"]
926 [params["tag"], params["any"]]
930 |> Enum.map(&String.downcase(&1))
935 |> Enum.map(&String.downcase(&1))
940 |> Enum.map(&String.downcase(&1))
944 |> Map.put("type", "Create")
945 |> Map.put("local_only", local_only)
946 |> Map.put("blocking_user", user)
947 |> Map.put("muting_user", user)
948 |> Map.put("user", user)
949 |> Map.put("tag", tags)
950 |> Map.put("tag_all", tag_all)
951 |> Map.put("tag_reject", tag_reject)
952 |> ActivityPub.fetch_public_activities()
956 |> add_link_headers(:hashtag_timeline, activities, params["tag"], %{"local" => local_only})
957 |> put_view(StatusView)
958 |> render("index.json", %{activities: activities, for: user, as: :activity})
961 def followers(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
962 with %User{} = user <- User.get_cached_by_id(id),
963 followers <- MastodonAPI.get_followers(user, params) do
966 for_user && user.id == for_user.id -> followers
967 user.info.hide_followers -> []
972 |> add_link_headers(:followers, followers, user)
973 |> put_view(AccountView)
974 |> render("accounts.json", %{for: for_user, users: followers, as: :user})
978 def following(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
979 with %User{} = user <- User.get_cached_by_id(id),
980 followers <- MastodonAPI.get_friends(user, params) do
983 for_user && user.id == for_user.id -> followers
984 user.info.hide_follows -> []
989 |> add_link_headers(:following, followers, user)
990 |> put_view(AccountView)
991 |> render("accounts.json", %{for: for_user, users: followers, as: :user})
995 def follow_requests(%{assigns: %{user: followed}} = conn, _params) do
996 with {:ok, follow_requests} <- User.get_follow_requests(followed) do
998 |> put_view(AccountView)
999 |> render("accounts.json", %{for: followed, users: follow_requests, as: :user})
1003 def authorize_follow_request(%{assigns: %{user: followed}} = conn, %{"id" => id}) do
1004 with %User{} = follower <- User.get_cached_by_id(id),
1005 {:ok, follower} <- CommonAPI.accept_follow_request(follower, followed) do
1007 |> put_view(AccountView)
1008 |> render("relationship.json", %{user: followed, target: follower})
1010 {:error, message} ->
1012 |> put_status(:forbidden)
1013 |> json(%{error: message})
1017 def reject_follow_request(%{assigns: %{user: followed}} = conn, %{"id" => id}) do
1018 with %User{} = follower <- User.get_cached_by_id(id),
1019 {:ok, follower} <- CommonAPI.reject_follow_request(follower, followed) do
1021 |> put_view(AccountView)
1022 |> render("relationship.json", %{user: followed, target: follower})
1024 {:error, message} ->
1026 |> put_status(:forbidden)
1027 |> json(%{error: message})
1031 def follow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
1032 with {_, %User{} = followed} <- {:followed, User.get_cached_by_id(id)},
1033 {_, true} <- {:followed, follower.id != followed.id},
1034 {:ok, follower} <- MastodonAPI.follow(follower, followed, conn.params) do
1036 |> put_view(AccountView)
1037 |> render("relationship.json", %{user: follower, target: followed})
1040 {:error, :not_found}
1042 {:error, message} ->
1044 |> put_status(:forbidden)
1045 |> json(%{error: message})
1049 def follow(%{assigns: %{user: follower}} = conn, %{"uri" => uri}) do
1050 with {_, %User{} = followed} <- {:followed, User.get_cached_by_nickname(uri)},
1051 {_, true} <- {:followed, follower.id != followed.id},
1052 {:ok, follower, followed, _} <- CommonAPI.follow(follower, followed) do
1054 |> put_view(AccountView)
1055 |> render("account.json", %{user: followed, for: follower})
1058 {:error, :not_found}
1060 {:error, message} ->
1062 |> put_status(:forbidden)
1063 |> json(%{error: message})
1067 def unfollow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
1068 with {_, %User{} = followed} <- {:followed, User.get_cached_by_id(id)},
1069 {_, true} <- {:followed, follower.id != followed.id},
1070 {:ok, follower} <- CommonAPI.unfollow(follower, followed) do
1072 |> put_view(AccountView)
1073 |> render("relationship.json", %{user: follower, target: followed})
1076 {:error, :not_found}
1083 def mute(%{assigns: %{user: muter}} = conn, %{"id" => id} = params) do
1085 if Map.has_key?(params, "notifications"),
1086 do: params["notifications"] in [true, "True", "true", "1"],
1089 with %User{} = muted <- User.get_cached_by_id(id),
1090 {:ok, muter} <- User.mute(muter, muted, notifications) do
1092 |> put_view(AccountView)
1093 |> render("relationship.json", %{user: muter, target: muted})
1095 {:error, message} ->
1097 |> put_status(:forbidden)
1098 |> json(%{error: message})
1102 def unmute(%{assigns: %{user: muter}} = conn, %{"id" => id}) do
1103 with %User{} = muted <- User.get_cached_by_id(id),
1104 {:ok, muter} <- User.unmute(muter, muted) do
1106 |> put_view(AccountView)
1107 |> render("relationship.json", %{user: muter, target: muted})
1109 {:error, message} ->
1111 |> put_status(:forbidden)
1112 |> json(%{error: message})
1116 def mutes(%{assigns: %{user: user}} = conn, _) do
1117 with muted_accounts <- User.muted_users(user) do
1118 res = AccountView.render("accounts.json", users: muted_accounts, for: user, as: :user)
1123 def block(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
1124 with %User{} = blocked <- User.get_cached_by_id(id),
1125 {:ok, blocker} <- User.block(blocker, blocked),
1126 {:ok, _activity} <- ActivityPub.block(blocker, blocked) do
1128 |> put_view(AccountView)
1129 |> render("relationship.json", %{user: blocker, target: blocked})
1131 {:error, message} ->
1133 |> put_status(:forbidden)
1134 |> json(%{error: message})
1138 def unblock(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
1139 with %User{} = blocked <- User.get_cached_by_id(id),
1140 {:ok, blocker} <- User.unblock(blocker, blocked),
1141 {:ok, _activity} <- ActivityPub.unblock(blocker, blocked) do
1143 |> put_view(AccountView)
1144 |> render("relationship.json", %{user: blocker, target: blocked})
1146 {:error, message} ->
1148 |> put_status(:forbidden)
1149 |> json(%{error: message})
1153 def blocks(%{assigns: %{user: user}} = conn, _) do
1154 with blocked_accounts <- User.blocked_users(user) do
1155 res = AccountView.render("accounts.json", users: blocked_accounts, for: user, as: :user)
1160 def domain_blocks(%{assigns: %{user: %{info: info}}} = conn, _) do
1161 json(conn, info.domain_blocks || [])
1164 def block_domain(%{assigns: %{user: blocker}} = conn, %{"domain" => domain}) do
1165 User.block_domain(blocker, domain)
1169 def unblock_domain(%{assigns: %{user: blocker}} = conn, %{"domain" => domain}) do
1170 User.unblock_domain(blocker, domain)
1174 def subscribe(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1175 with %User{} = subscription_target <- User.get_cached_by_id(id),
1176 {:ok, subscription_target} = User.subscribe(user, subscription_target) do
1178 |> put_view(AccountView)
1179 |> render("relationship.json", %{user: user, target: subscription_target})
1181 {:error, message} ->
1183 |> put_status(:forbidden)
1184 |> json(%{error: message})
1188 def unsubscribe(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1189 with %User{} = subscription_target <- User.get_cached_by_id(id),
1190 {:ok, subscription_target} = User.unsubscribe(user, subscription_target) do
1192 |> put_view(AccountView)
1193 |> render("relationship.json", %{user: user, target: subscription_target})
1195 {:error, message} ->
1197 |> put_status(:forbidden)
1198 |> json(%{error: message})
1202 def favourites(%{assigns: %{user: user}} = conn, params) do
1205 |> Map.put("type", "Create")
1206 |> Map.put("favorited_by", user.ap_id)
1207 |> Map.put("blocking_user", user)
1210 ActivityPub.fetch_activities([], params)
1214 |> add_link_headers(:favourites, activities)
1215 |> put_view(StatusView)
1216 |> render("index.json", %{activities: activities, for: user, as: :activity})
1219 def user_favourites(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
1220 with %User{} = user <- User.get_by_id(id),
1221 false <- user.info.hide_favorites do
1224 |> Map.put("type", "Create")
1225 |> Map.put("favorited_by", user.ap_id)
1226 |> Map.put("blocking_user", for_user)
1230 [Pleroma.Constants.as_public()] ++ [for_user.ap_id | for_user.following]
1232 [Pleroma.Constants.as_public()]
1237 |> ActivityPub.fetch_activities(params)
1241 |> add_link_headers(:favourites, activities)
1242 |> put_view(StatusView)
1243 |> render("index.json", %{activities: activities, for: for_user, as: :activity})
1245 nil -> {:error, :not_found}
1246 true -> render_error(conn, :forbidden, "Can't get favorites")
1250 def bookmarks(%{assigns: %{user: user}} = conn, params) do
1251 user = User.get_cached_by_id(user.id)
1254 Bookmark.for_user_query(user.id)
1255 |> Pagination.fetch_paginated(params)
1259 |> Enum.map(fn b -> Map.put(b.activity, :bookmark, Map.delete(b, :activity)) end)
1262 |> add_link_headers(:bookmarks, bookmarks)
1263 |> put_view(StatusView)
1264 |> render("index.json", %{activities: activities, for: user, as: :activity})
1267 def get_lists(%{assigns: %{user: user}} = conn, opts) do
1268 lists = Pleroma.List.for_user(user, opts)
1269 res = ListView.render("lists.json", lists: lists)
1273 def get_list(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1274 with %Pleroma.List{} = list <- Pleroma.List.get(id, user) do
1275 res = ListView.render("list.json", list: list)
1278 _e -> render_error(conn, :not_found, "Record not found")
1282 def account_lists(%{assigns: %{user: user}} = conn, %{"id" => account_id}) do
1283 lists = Pleroma.List.get_lists_account_belongs(user, account_id)
1284 res = ListView.render("lists.json", lists: lists)
1288 def delete_list(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1289 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1290 {:ok, _list} <- Pleroma.List.delete(list) do
1294 json(conn, dgettext("errors", "error"))
1298 def create_list(%{assigns: %{user: user}} = conn, %{"title" => title}) do
1299 with {:ok, %Pleroma.List{} = list} <- Pleroma.List.create(title, user) do
1300 res = ListView.render("list.json", list: list)
1305 def add_to_list(%{assigns: %{user: user}} = conn, %{"id" => id, "account_ids" => accounts}) do
1307 |> Enum.each(fn account_id ->
1308 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1309 %User{} = followed <- User.get_cached_by_id(account_id) do
1310 Pleroma.List.follow(list, followed)
1317 def remove_from_list(%{assigns: %{user: user}} = conn, %{"id" => id, "account_ids" => accounts}) do
1319 |> Enum.each(fn account_id ->
1320 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1321 %User{} = followed <- User.get_cached_by_id(account_id) do
1322 Pleroma.List.unfollow(list, followed)
1329 def list_accounts(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1330 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1331 {:ok, users} = Pleroma.List.get_following(list) do
1333 |> put_view(AccountView)
1334 |> render("accounts.json", %{for: user, users: users, as: :user})
1338 def rename_list(%{assigns: %{user: user}} = conn, %{"id" => id, "title" => title}) do
1339 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1340 {:ok, list} <- Pleroma.List.rename(list, title) do
1341 res = ListView.render("list.json", list: list)
1345 json(conn, dgettext("errors", "error"))
1349 def list_timeline(%{assigns: %{user: user}} = conn, %{"list_id" => id} = params) do
1350 with %Pleroma.List{title: _title, following: following} <- Pleroma.List.get(id, user) do
1353 |> Map.put("type", "Create")
1354 |> Map.put("blocking_user", user)
1355 |> Map.put("user", user)
1356 |> Map.put("muting_user", user)
1358 # we must filter the following list for the user to avoid leaking statuses the user
1359 # does not actually have permission to see (for more info, peruse security issue #270).
1362 |> Enum.filter(fn x -> x in user.following end)
1363 |> ActivityPub.fetch_activities_bounded(following, params)
1367 |> put_view(StatusView)
1368 |> render("index.json", %{activities: activities, for: user, as: :activity})
1370 _e -> render_error(conn, :forbidden, "Error.")
1374 def index(%{assigns: %{user: user}} = conn, _params) do
1375 token = get_session(conn, :oauth_token)
1378 mastodon_emoji = mastodonized_emoji()
1380 limit = Config.get([:instance, :limit])
1383 Map.put(%{}, user.id, AccountView.render("account.json", %{user: user, for: user}))
1388 streaming_api_base_url: Pleroma.Web.Endpoint.websocket_url(),
1389 access_token: token,
1391 domain: Pleroma.Web.Endpoint.host(),
1394 unfollow_modal: false,
1397 auto_play_gif: false,
1398 display_sensitive_media: false,
1399 reduce_motion: false,
1400 max_toot_chars: limit,
1401 mascot: User.get_mascot(user)["url"]
1403 poll_limits: Config.get([:instance, :poll_limits]),
1405 delete_others_notice: present?(user.info.is_moderator),
1406 admin: present?(user.info.is_admin)
1410 default_privacy: user.info.default_scope,
1411 default_sensitive: false,
1412 allow_content_types: Config.get([:instance, :allowed_post_formats])
1414 media_attachments: %{
1415 accept_content_types: [
1431 user.info.settings ||
1461 push_subscription: nil,
1463 custom_emojis: mastodon_emoji,
1469 |> put_layout(false)
1470 |> put_view(MastodonView)
1471 |> render("index.html", %{initial_state: initial_state})
1474 |> put_session(:return_to, conn.request_path)
1475 |> redirect(to: "/web/login")
1479 def put_settings(%{assigns: %{user: user}} = conn, %{"data" => settings} = _params) do
1480 info_cng = User.Info.mastodon_settings_update(user.info, settings)
1482 with changeset <- Ecto.Changeset.change(user),
1483 changeset <- Ecto.Changeset.put_embed(changeset, :info, info_cng),
1484 {:ok, _user} <- User.update_and_set_cache(changeset) do
1489 |> put_status(:internal_server_error)
1490 |> json(%{error: inspect(e)})
1494 def login(%{assigns: %{user: %User{}}} = conn, _params) do
1495 redirect(conn, to: local_mastodon_root_path(conn))
1498 @doc "Local Mastodon FE login init action"
1499 def login(conn, %{"code" => auth_token}) do
1500 with {:ok, app} <- get_or_make_app(),
1501 %Authorization{} = auth <- Repo.get_by(Authorization, token: auth_token, app_id: app.id),
1502 {:ok, token} <- Token.exchange_token(app, auth) do
1504 |> put_session(:oauth_token, token.token)
1505 |> redirect(to: local_mastodon_root_path(conn))
1509 @doc "Local Mastodon FE callback action"
1510 def login(conn, _) do
1511 with {:ok, app} <- get_or_make_app() do
1516 response_type: "code",
1517 client_id: app.client_id,
1519 scope: Enum.join(app.scopes, " ")
1522 redirect(conn, to: path)
1526 defp local_mastodon_root_path(conn) do
1527 case get_session(conn, :return_to) do
1529 mastodon_api_path(conn, :index, ["getting-started"])
1532 delete_session(conn, :return_to)
1537 defp get_or_make_app do
1538 find_attrs = %{client_name: @local_mastodon_name, redirect_uris: "."}
1539 scopes = ["read", "write", "follow", "push"]
1541 with %App{} = app <- Repo.get_by(App, find_attrs) do
1543 if app.scopes == scopes do
1547 |> Ecto.Changeset.change(%{scopes: scopes})
1555 App.register_changeset(
1557 Map.put(find_attrs, :scopes, scopes)
1564 def logout(conn, _) do
1567 |> redirect(to: "/")
1570 def relationship_noop(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1571 Logger.debug("Unimplemented, returning unmodified relationship")
1573 with %User{} = target <- User.get_cached_by_id(id) do
1575 |> put_view(AccountView)
1576 |> render("relationship.json", %{user: user, target: target})
1580 def empty_array(conn, _) do
1581 Logger.debug("Unimplemented, returning an empty array")
1585 def empty_object(conn, _) do
1586 Logger.debug("Unimplemented, returning an empty object")
1590 def get_filters(%{assigns: %{user: user}} = conn, _) do
1591 filters = Filter.get_filters(user)
1592 res = FilterView.render("filters.json", filters: filters)
1597 %{assigns: %{user: user}} = conn,
1598 %{"phrase" => phrase, "context" => context} = params
1604 hide: Map.get(params, "irreversible", false),
1605 whole_word: Map.get(params, "boolean", true)
1609 {:ok, response} = Filter.create(query)
1610 res = FilterView.render("filter.json", filter: response)
1614 def get_filter(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do
1615 filter = Filter.get(filter_id, user)
1616 res = FilterView.render("filter.json", filter: filter)
1621 %{assigns: %{user: user}} = conn,
1622 %{"phrase" => phrase, "context" => context, "id" => filter_id} = params
1626 filter_id: filter_id,
1629 hide: Map.get(params, "irreversible", nil),
1630 whole_word: Map.get(params, "boolean", true)
1634 {:ok, response} = Filter.update(query)
1635 res = FilterView.render("filter.json", filter: response)
1639 def delete_filter(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do
1642 filter_id: filter_id
1645 {:ok, _} = Filter.delete(query)
1651 def errors(conn, {:error, %Changeset{} = changeset}) do
1654 |> Changeset.traverse_errors(fn {message, _opt} -> message end)
1655 |> Enum.map_join(", ", fn {_k, v} -> v end)
1658 |> put_status(:unprocessable_entity)
1659 |> json(%{error: error_message})
1662 def errors(conn, {:error, :not_found}) do
1663 render_error(conn, :not_found, "Record not found")
1666 def errors(conn, {:error, error_message}) do
1668 |> put_status(:bad_request)
1669 |> json(%{error: error_message})
1672 def errors(conn, _) do
1674 |> put_status(:internal_server_error)
1675 |> json(dgettext("errors", "Something went wrong"))
1678 def suggestions(%{assigns: %{user: user}} = conn, _) do
1679 suggestions = Config.get(:suggestions)
1681 if Keyword.get(suggestions, :enabled, false) do
1682 api = Keyword.get(suggestions, :third_party_engine, "")
1683 timeout = Keyword.get(suggestions, :timeout, 5000)
1684 limit = Keyword.get(suggestions, :limit, 23)
1686 host = Config.get([Pleroma.Web.Endpoint, :url, :host])
1688 user = user.nickname
1692 |> String.replace("{{host}}", host)
1693 |> String.replace("{{user}}", user)
1695 with {:ok, %{status: 200, body: body}} <-
1696 HTTP.get(url, [], adapter: [recv_timeout: timeout, pool: :default]),
1697 {:ok, data} <- Jason.decode(body) do
1700 |> Enum.slice(0, limit)
1703 |> Map.put("id", fetch_suggestion_id(x))
1704 |> Map.put("avatar", MediaProxy.url(x["avatar"]))
1705 |> Map.put("avatar_static", MediaProxy.url(x["avatar_static"]))
1711 Logger.error("Could not retrieve suggestions at fetch #{url}, #{inspect(e)}")
1718 defp fetch_suggestion_id(attrs) do
1719 case User.get_or_fetch(attrs["acct"]) do
1720 {:ok, %User{id: id}} -> id
1725 def status_card(%{assigns: %{user: user}} = conn, %{"id" => status_id}) do
1726 with %Activity{} = activity <- Activity.get_by_id(status_id),
1727 true <- Visibility.visible_for_user?(activity, user) do
1731 Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity)
1741 def reports(%{assigns: %{user: user}} = conn, params) do
1742 case CommonAPI.report(user, params) do
1745 |> put_view(ReportView)
1746 |> try_render("report.json", %{activity: activity})
1750 |> put_status(:bad_request)
1751 |> json(%{error: err})
1755 def account_register(
1756 %{assigns: %{app: app}} = conn,
1757 %{"username" => nickname, "email" => _, "password" => _, "agreement" => true} = params
1765 "captcha_answer_data",
1769 |> Map.put("nickname", nickname)
1770 |> Map.put("fullname", params["fullname"] || nickname)
1771 |> Map.put("bio", params["bio"] || "")
1772 |> Map.put("confirm", params["password"])
1774 with {:ok, user} <- TwitterAPI.register_user(params, need_confirmation: true),
1775 {:ok, token} <- Token.create_token(app, user, %{scopes: app.scopes}) do
1777 token_type: "Bearer",
1778 access_token: token.token,
1780 created_at: Token.Utils.format_created_at(token)
1785 |> put_status(:bad_request)
1790 def account_register(%{assigns: %{app: _app}} = conn, _params) do
1791 render_error(conn, :bad_request, "Missing parameters")
1794 def account_register(conn, _) do
1795 render_error(conn, :forbidden, "Invalid credentials")
1798 def conversations(%{assigns: %{user: user}} = conn, params) do
1799 participations = Participation.for_user_with_last_activity_id(user, params)
1802 Enum.map(participations, fn participation ->
1803 ConversationView.render("participation.json", %{participation: participation, user: user})
1807 |> add_link_headers(:conversations, participations)
1808 |> json(conversations)
1811 def conversation_read(%{assigns: %{user: user}} = conn, %{"id" => participation_id}) do
1812 with %Participation{} = participation <-
1813 Repo.get_by(Participation, id: participation_id, user_id: user.id),
1814 {:ok, participation} <- Participation.mark_as_read(participation) do
1815 participation_view =
1816 ConversationView.render("participation.json", %{participation: participation, user: user})
1819 |> json(participation_view)
1823 def password_reset(conn, params) do
1824 nickname_or_email = params["email"] || params["nickname"]
1826 with {:ok, _} <- TwitterAPI.password_reset(nickname_or_email) do
1828 |> put_status(:no_content)
1831 {:error, "unknown user"} ->
1832 send_resp(conn, :not_found, "")
1835 send_resp(conn, :bad_request, "")
1839 def account_confirmation_resend(conn, params) do
1840 nickname_or_email = params["email"] || params["nickname"]
1842 with %User{} = user <- User.get_by_nickname_or_email(nickname_or_email),
1843 {:ok, _} <- User.try_send_confirmation_email(user) do
1845 |> json_response(:no_content, "")
1849 def try_render(conn, target, params)
1850 when is_binary(target) do
1851 case render(conn, target, params) do
1852 nil -> render_error(conn, :not_implemented, "Can't display this activity")
1857 def try_render(conn, _, _) do
1858 render_error(conn, :not_implemented, "Can't display this activity")
1861 defp present?(nil), do: false
1862 defp present?(false), do: false
1863 defp present?(_), do: true