logo

pleroma

My custom branche(s) on git.pleroma.social/pleroma/pleroma

mastodon_api_controller.ex (57215B)


      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: 5, add_link_headers: 4, add_link_headers: 3]
     10 
     11   alias Ecto.Changeset
     12   alias Pleroma.Activity
     13   alias Pleroma.Bookmark
     14   alias Pleroma.Config
     15   alias Pleroma.Conversation.Participation
     16   alias Pleroma.Filter
     17   alias Pleroma.Formatter
     18   alias Pleroma.HTTP
     19   alias Pleroma.Notification
     20   alias Pleroma.Object
     21   alias Pleroma.Pagination
     22   alias Pleroma.Plugs.RateLimiter
     23   alias Pleroma.Repo
     24   alias Pleroma.ScheduledActivity
     25   alias Pleroma.Stats
     26   alias Pleroma.User
     27   alias Pleroma.Web
     28   alias Pleroma.Web.ActivityPub.ActivityPub
     29   alias Pleroma.Web.ActivityPub.Visibility
     30   alias Pleroma.Web.CommonAPI
     31   alias Pleroma.Web.MastodonAPI.AccountView
     32   alias Pleroma.Web.MastodonAPI.AppView
     33   alias Pleroma.Web.MastodonAPI.ConversationView
     34   alias Pleroma.Web.MastodonAPI.FilterView
     35   alias Pleroma.Web.MastodonAPI.ListView
     36   alias Pleroma.Web.MastodonAPI.MastodonAPI
     37   alias Pleroma.Web.MastodonAPI.MastodonView
     38   alias Pleroma.Web.MastodonAPI.NotificationView
     39   alias Pleroma.Web.MastodonAPI.ReportView
     40   alias Pleroma.Web.MastodonAPI.ScheduledActivityView
     41   alias Pleroma.Web.MastodonAPI.StatusView
     42   alias Pleroma.Web.MediaProxy
     43   alias Pleroma.Web.OAuth.App
     44   alias Pleroma.Web.OAuth.Authorization
     45   alias Pleroma.Web.OAuth.Scopes
     46   alias Pleroma.Web.OAuth.Token
     47   alias Pleroma.Web.TwitterAPI.TwitterAPI
     48 
     49   alias Pleroma.Web.ControllerHelper
     50   import Ecto.Query
     51 
     52   require Logger
     53   require Pleroma.Constants
     54 
     55   @rate_limited_relations_actions ~w(follow unfollow)a
     56 
     57   @rate_limited_status_actions ~w(reblog_status unreblog_status fav_status unfav_status
     58     post_status delete_status)a
     59 
     60   plug(
     61     RateLimiter,
     62     {:status_id_action, bucket_name: "status_id_action:reblog_unreblog", params: ["id"]}
     63     when action in ~w(reblog_status unreblog_status)a
     64   )
     65 
     66   plug(
     67     RateLimiter,
     68     {:status_id_action, bucket_name: "status_id_action:fav_unfav", params: ["id"]}
     69     when action in ~w(fav_status unfav_status)a
     70   )
     71 
     72   plug(
     73     RateLimiter,
     74     {:relations_id_action, params: ["id", "uri"]} when action in @rate_limited_relations_actions
     75   )
     76 
     77   plug(RateLimiter, :relations_actions when action in @rate_limited_relations_actions)
     78   plug(RateLimiter, :statuses_actions when action in @rate_limited_status_actions)
     79   plug(RateLimiter, :app_account_creation when action == :account_register)
     80   plug(RateLimiter, :search when action in [:search, :search2, :account_search])
     81   plug(RateLimiter, :password_reset when action == :password_reset)
     82   plug(RateLimiter, :account_confirmation_resend when action == :account_confirmation_resend)
     83 
     84   @local_mastodon_name "Mastodon-Local"
     85 
     86   action_fallback(:errors)
     87 
     88   def create_app(conn, params) do
     89     scopes = Scopes.fetch_scopes(params, ["read"])
     90 
     91     app_attrs =
     92       params
     93       |> Map.drop(["scope", "scopes"])
     94       |> Map.put("scopes", scopes)
     95 
     96     with cs <- App.register_changeset(%App{}, app_attrs),
     97          false <- cs.changes[:client_name] == @local_mastodon_name,
     98          {:ok, app} <- Repo.insert(cs) do
     99       conn
    100       |> put_view(AppView)
    101       |> render("show.json", %{app: app})
    102     end
    103   end
    104 
    105   defp add_if_present(
    106          map,
    107          params,
    108          params_field,
    109          map_field,
    110          value_function \\ fn x -> {:ok, x} end
    111        ) do
    112     if Map.has_key?(params, params_field) do
    113       case value_function.(params[params_field]) do
    114         {:ok, new_value} -> Map.put(map, map_field, new_value)
    115         :error -> map
    116       end
    117     else
    118       map
    119     end
    120   end
    121 
    122   def update_credentials(%{assigns: %{user: user}} = conn, params) do
    123     original_user = user
    124 
    125     user_params =
    126       %{}
    127       |> add_if_present(params, "display_name", :name)
    128       |> add_if_present(params, "note", :bio, fn value -> {:ok, User.parse_bio(value, user)} end)
    129       |> add_if_present(params, "avatar", :avatar, fn value ->
    130         with %Plug.Upload{} <- value,
    131              {:ok, object} <- ActivityPub.upload(value, type: :avatar) do
    132           {:ok, object.data}
    133         else
    134           _ -> :error
    135         end
    136       end)
    137 
    138     emojis_text = (user_params["display_name"] || "") <> (user_params["note"] || "")
    139 
    140     user_info_emojis =
    141       user.info
    142       |> Map.get(:emoji, [])
    143       |> Enum.concat(Formatter.get_emoji_map(emojis_text))
    144       |> Enum.dedup()
    145 
    146     info_params =
    147       [
    148         :no_rich_text,
    149         :locked,
    150         :hide_followers,
    151         :hide_follows,
    152         :hide_favorites,
    153         :show_role,
    154         :skip_thread_containment
    155       ]
    156       |> Enum.reduce(%{}, fn key, acc ->
    157         add_if_present(acc, params, to_string(key), key, fn value ->
    158           {:ok, ControllerHelper.truthy_param?(value)}
    159         end)
    160       end)
    161       |> add_if_present(params, "default_scope", :default_scope)
    162       |> add_if_present(params, "fields", :fields, fn fields ->
    163         fields = Enum.map(fields, fn f -> Map.update!(f, "value", &AutoLinker.link(&1)) end)
    164 
    165         {:ok, fields}
    166       end)
    167       |> add_if_present(params, "fields", :raw_fields)
    168       |> add_if_present(params, "pleroma_settings_store", :pleroma_settings_store, fn value ->
    169         {:ok, Map.merge(user.info.pleroma_settings_store, value)}
    170       end)
    171       |> add_if_present(params, "header", :banner, fn value ->
    172         with %Plug.Upload{} <- value,
    173              {:ok, object} <- ActivityPub.upload(value, type: :banner) do
    174           {:ok, object.data}
    175         else
    176           _ -> :error
    177         end
    178       end)
    179       |> add_if_present(params, "pleroma_background_image", :background, fn value ->
    180         with %Plug.Upload{} <- value,
    181              {:ok, object} <- ActivityPub.upload(value, type: :background) do
    182           {:ok, object.data}
    183         else
    184           _ -> :error
    185         end
    186       end)
    187       |> Map.put(:emoji, user_info_emojis)
    188 
    189     info_cng = User.Info.profile_update(user.info, info_params)
    190 
    191     with changeset <- User.update_changeset(user, user_params),
    192          changeset <- Ecto.Changeset.put_embed(changeset, :info, info_cng),
    193          {:ok, user} <- User.update_and_set_cache(changeset) do
    194       if original_user != user do
    195         CommonAPI.update(user)
    196       end
    197 
    198       json(
    199         conn,
    200         AccountView.render("account.json", %{user: user, for: user, with_pleroma_settings: true})
    201       )
    202     else
    203       _e -> render_error(conn, :forbidden, "Invalid request")
    204     end
    205   end
    206 
    207   def update_avatar(%{assigns: %{user: user}} = conn, %{"img" => ""}) do
    208     change = Changeset.change(user, %{avatar: nil})
    209     {:ok, user} = User.update_and_set_cache(change)
    210     CommonAPI.update(user)
    211 
    212     json(conn, %{url: nil})
    213   end
    214 
    215   def update_avatar(%{assigns: %{user: user}} = conn, params) do
    216     {:ok, object} = ActivityPub.upload(params, type: :avatar)
    217     change = Changeset.change(user, %{avatar: object.data})
    218     {:ok, user} = User.update_and_set_cache(change)
    219     CommonAPI.update(user)
    220     %{"url" => [%{"href" => href} | _]} = object.data
    221 
    222     json(conn, %{url: href})
    223   end
    224 
    225   def update_banner(%{assigns: %{user: user}} = conn, %{"banner" => ""}) do
    226     with new_info <- %{"banner" => %{}},
    227          info_cng <- User.Info.profile_update(user.info, new_info),
    228          changeset <- Ecto.Changeset.change(user) |> Ecto.Changeset.put_embed(:info, info_cng),
    229          {:ok, user} <- User.update_and_set_cache(changeset) do
    230       CommonAPI.update(user)
    231 
    232       json(conn, %{url: nil})
    233     end
    234   end
    235 
    236   def update_banner(%{assigns: %{user: user}} = conn, params) do
    237     with {:ok, object} <- ActivityPub.upload(%{"img" => params["banner"]}, type: :banner),
    238          new_info <- %{"banner" => object.data},
    239          info_cng <- User.Info.profile_update(user.info, new_info),
    240          changeset <- Ecto.Changeset.change(user) |> Ecto.Changeset.put_embed(:info, info_cng),
    241          {:ok, user} <- User.update_and_set_cache(changeset) do
    242       CommonAPI.update(user)
    243       %{"url" => [%{"href" => href} | _]} = object.data
    244 
    245       json(conn, %{url: href})
    246     end
    247   end
    248 
    249   def update_background(%{assigns: %{user: user}} = conn, %{"img" => ""}) do
    250     with new_info <- %{"background" => %{}},
    251          info_cng <- User.Info.profile_update(user.info, new_info),
    252          changeset <- Ecto.Changeset.change(user) |> Ecto.Changeset.put_embed(:info, info_cng),
    253          {:ok, _user} <- User.update_and_set_cache(changeset) do
    254       json(conn, %{url: nil})
    255     end
    256   end
    257 
    258   def update_background(%{assigns: %{user: user}} = conn, params) do
    259     with {:ok, object} <- ActivityPub.upload(params, type: :background),
    260          new_info <- %{"background" => object.data},
    261          info_cng <- User.Info.profile_update(user.info, new_info),
    262          changeset <- Ecto.Changeset.change(user) |> Ecto.Changeset.put_embed(:info, info_cng),
    263          {:ok, _user} <- User.update_and_set_cache(changeset) do
    264       %{"url" => [%{"href" => href} | _]} = object.data
    265 
    266       json(conn, %{url: href})
    267     end
    268   end
    269 
    270   def verify_credentials(%{assigns: %{user: user}} = conn, _) do
    271     chat_token = Phoenix.Token.sign(conn, "user socket", user.id)
    272 
    273     account =
    274       AccountView.render("account.json", %{
    275         user: user,
    276         for: user,
    277         with_pleroma_settings: true,
    278         with_chat_token: chat_token
    279       })
    280 
    281     json(conn, account)
    282   end
    283 
    284   def verify_app_credentials(%{assigns: %{user: _user, token: token}} = conn, _) do
    285     with %Token{app: %App{} = app} <- Repo.preload(token, :app) do
    286       conn
    287       |> put_view(AppView)
    288       |> render("short.json", %{app: app})
    289     end
    290   end
    291 
    292   def user(%{assigns: %{user: for_user}} = conn, %{"id" => nickname_or_id}) do
    293     with %User{} = user <- User.get_cached_by_nickname_or_id(nickname_or_id),
    294          true <- User.auth_active?(user) || user.id == for_user.id || User.superuser?(for_user) do
    295       account = AccountView.render("account.json", %{user: user, for: for_user})
    296       json(conn, account)
    297     else
    298       _e -> render_error(conn, :not_found, "Can't find user")
    299     end
    300   end
    301 
    302   @mastodon_api_level "2.7.2"
    303 
    304   def masto_instance(conn, _params) do
    305     instance = Config.get(:instance)
    306 
    307     response = %{
    308       uri: Web.base_url(),
    309       title: Keyword.get(instance, :name),
    310       description: Keyword.get(instance, :description),
    311       version: "#{@mastodon_api_level} (compatible; #{Pleroma.Application.named_version()})",
    312       email: Keyword.get(instance, :email),
    313       urls: %{
    314         streaming_api: Pleroma.Web.Endpoint.websocket_url()
    315       },
    316       stats: Stats.get_stats(),
    317       thumbnail: Web.base_url() <> "/instance/thumbnail.jpeg",
    318       languages: ["en"],
    319       registrations: Pleroma.Config.get([:instance, :registrations_open]),
    320       # Extra (not present in Mastodon):
    321       max_toot_chars: Keyword.get(instance, :limit),
    322       poll_limits: Keyword.get(instance, :poll_limits)
    323     }
    324 
    325     json(conn, response)
    326   end
    327 
    328   def peers(conn, _params) do
    329     json(conn, Stats.get_peers())
    330   end
    331 
    332   defp mastodonized_emoji do
    333     Pleroma.Emoji.get_all()
    334     |> Enum.map(fn {shortcode, relative_url, tags} ->
    335       url = to_string(URI.merge(Web.base_url(), relative_url))
    336 
    337       %{
    338         "shortcode" => shortcode,
    339         "static_url" => url,
    340         "visible_in_picker" => true,
    341         "url" => url,
    342         "tags" => tags,
    343         # Assuming that a comma is authorized in the category name
    344         "category" => (tags -- ["Custom"]) |> Enum.join(",")
    345       }
    346     end)
    347   end
    348 
    349   def custom_emojis(conn, _params) do
    350     mastodon_emoji = mastodonized_emoji()
    351     json(conn, mastodon_emoji)
    352   end
    353 
    354   def home_timeline(%{assigns: %{user: user}} = conn, params) do
    355     params =
    356       params
    357       |> Map.put("type", ["Create", "Announce"])
    358       |> Map.put("blocking_user", user)
    359       |> Map.put("muting_user", user)
    360       |> Map.put("user", user)
    361 
    362     activities =
    363       [user.ap_id | user.following]
    364       |> ActivityPub.fetch_activities(params)
    365       |> Enum.reverse()
    366 
    367     conn
    368     |> add_link_headers(:home_timeline, activities)
    369     |> put_view(StatusView)
    370     |> render("index.json", %{activities: activities, for: user, as: :activity})
    371   end
    372 
    373   def public_timeline(%{assigns: %{user: user}} = conn, params) do
    374     local_only = params["local"] in [true, "True", "true", "1"]
    375 
    376     activities =
    377       params
    378       |> Map.put("type", ["Create", "Announce"])
    379       |> Map.put("local_only", local_only)
    380       |> Map.put("blocking_user", user)
    381       |> Map.put("muting_user", user)
    382       |> Map.put("user", user)
    383       |> ActivityPub.fetch_public_activities()
    384       |> Enum.reverse()
    385 
    386     conn
    387     |> add_link_headers(:public_timeline, activities, false, %{"local" => local_only})
    388     |> put_view(StatusView)
    389     |> render("index.json", %{activities: activities, for: user, as: :activity})
    390   end
    391 
    392   def user_statuses(%{assigns: %{user: reading_user}} = conn, params) do
    393     with %User{} = user <- User.get_cached_by_nickname_or_id(params["id"]) do
    394       params =
    395         params
    396         |> Map.put("tag", params["tagged"])
    397 
    398       activities = ActivityPub.fetch_user_activities(user, reading_user, params)
    399 
    400       conn
    401       |> add_link_headers(:user_statuses, activities, params["id"])
    402       |> put_view(StatusView)
    403       |> render("index.json", %{
    404         activities: activities,
    405         for: reading_user,
    406         as: :activity
    407       })
    408     end
    409   end
    410 
    411   def dm_timeline(%{assigns: %{user: user}} = conn, params) do
    412     params =
    413       params
    414       |> Map.put("type", "Create")
    415       |> Map.put("blocking_user", user)
    416       |> Map.put("user", user)
    417       |> Map.put(:visibility, "direct")
    418 
    419     activities =
    420       [user.ap_id]
    421       |> ActivityPub.fetch_activities_query(params)
    422       |> Pagination.fetch_paginated(params)
    423 
    424     conn
    425     |> add_link_headers(:dm_timeline, activities)
    426     |> put_view(StatusView)
    427     |> render("index.json", %{activities: activities, for: user, as: :activity})
    428   end
    429 
    430   def get_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
    431     with %Activity{} = activity <- Activity.get_by_id_with_object(id),
    432          true <- Visibility.visible_for_user?(activity, user) do
    433       conn
    434       |> put_view(StatusView)
    435       |> try_render("status.json", %{activity: activity, for: user})
    436     end
    437   end
    438 
    439   def get_context(%{assigns: %{user: user}} = conn, %{"id" => id}) do
    440     with %Activity{} = activity <- Activity.get_by_id(id),
    441          activities <-
    442            ActivityPub.fetch_activities_for_context(activity.data["context"], %{
    443              "blocking_user" => user,
    444              "user" => user,
    445              "exclude_id" => activity.id
    446            }),
    447          grouped_activities <- Enum.group_by(activities, fn %{id: id} -> id < activity.id end) do
    448       result = %{
    449         ancestors:
    450           StatusView.render(
    451             "index.json",
    452             for: user,
    453             activities: grouped_activities[true] || [],
    454             as: :activity
    455           )
    456           |> Enum.reverse(),
    457         # credo:disable-for-previous-line Credo.Check.Refactor.PipeChainStart
    458         descendants:
    459           StatusView.render(
    460             "index.json",
    461             for: user,
    462             activities: grouped_activities[false] || [],
    463             as: :activity
    464           )
    465           |> Enum.reverse()
    466         # credo:disable-for-previous-line Credo.Check.Refactor.PipeChainStart
    467       }
    468 
    469       json(conn, result)
    470     end
    471   end
    472 
    473   def get_poll(%{assigns: %{user: user}} = conn, %{"id" => id}) do
    474     with %Object{} = object <- Object.get_by_id(id),
    475          %Activity{} = activity <- Activity.get_create_by_object_ap_id(object.data["id"]),
    476          true <- Visibility.visible_for_user?(activity, user) do
    477       conn
    478       |> put_view(StatusView)
    479       |> try_render("poll.json", %{object: object, for: user})
    480     else
    481       error when is_nil(error) or error == false ->
    482         render_error(conn, :not_found, "Record not found")
    483     end
    484   end
    485 
    486   defp get_cached_vote_or_vote(user, object, choices) do
    487     idempotency_key = "polls:#{user.id}:#{object.data["id"]}"
    488 
    489     {_, res} =
    490       Cachex.fetch(:idempotency_cache, idempotency_key, fn _ ->
    491         case CommonAPI.vote(user, object, choices) do
    492           {:error, _message} = res -> {:ignore, res}
    493           res -> {:commit, res}
    494         end
    495       end)
    496 
    497     res
    498   end
    499 
    500   def poll_vote(%{assigns: %{user: user}} = conn, %{"id" => id, "choices" => choices}) do
    501     with %Object{} = object <- Object.get_by_id(id),
    502          true <- object.data["type"] == "Question",
    503          %Activity{} = activity <- Activity.get_create_by_object_ap_id(object.data["id"]),
    504          true <- Visibility.visible_for_user?(activity, user),
    505          {:ok, _activities, object} <- get_cached_vote_or_vote(user, object, choices) do
    506       conn
    507       |> put_view(StatusView)
    508       |> try_render("poll.json", %{object: object, for: user})
    509     else
    510       nil ->
    511         render_error(conn, :not_found, "Record not found")
    512 
    513       false ->
    514         render_error(conn, :not_found, "Record not found")
    515 
    516       {:error, message} ->
    517         conn
    518         |> put_status(:unprocessable_entity)
    519         |> json(%{error: message})
    520     end
    521   end
    522 
    523   def scheduled_statuses(%{assigns: %{user: user}} = conn, params) do
    524     with scheduled_activities <- MastodonAPI.get_scheduled_activities(user, params) do
    525       conn
    526       |> add_link_headers(:scheduled_statuses, scheduled_activities)
    527       |> put_view(ScheduledActivityView)
    528       |> render("index.json", %{scheduled_activities: scheduled_activities})
    529     end
    530   end
    531 
    532   def show_scheduled_status(%{assigns: %{user: user}} = conn, %{"id" => scheduled_activity_id}) do
    533     with %ScheduledActivity{} = scheduled_activity <-
    534            ScheduledActivity.get(user, scheduled_activity_id) do
    535       conn
    536       |> put_view(ScheduledActivityView)
    537       |> render("show.json", %{scheduled_activity: scheduled_activity})
    538     else
    539       _ -> {:error, :not_found}
    540     end
    541   end
    542 
    543   def update_scheduled_status(
    544         %{assigns: %{user: user}} = conn,
    545         %{"id" => scheduled_activity_id} = params
    546       ) do
    547     with %ScheduledActivity{} = scheduled_activity <-
    548            ScheduledActivity.get(user, scheduled_activity_id),
    549          {:ok, scheduled_activity} <- ScheduledActivity.update(scheduled_activity, params) do
    550       conn
    551       |> put_view(ScheduledActivityView)
    552       |> render("show.json", %{scheduled_activity: scheduled_activity})
    553     else
    554       nil -> {:error, :not_found}
    555       error -> error
    556     end
    557   end
    558 
    559   def delete_scheduled_status(%{assigns: %{user: user}} = conn, %{"id" => scheduled_activity_id}) do
    560     with %ScheduledActivity{} = scheduled_activity <-
    561            ScheduledActivity.get(user, scheduled_activity_id),
    562          {:ok, scheduled_activity} <- ScheduledActivity.delete(scheduled_activity) do
    563       conn
    564       |> put_view(ScheduledActivityView)
    565       |> render("show.json", %{scheduled_activity: scheduled_activity})
    566     else
    567       nil -> {:error, :not_found}
    568       error -> error
    569     end
    570   end
    571 
    572   def post_status(%{assigns: %{user: user}} = conn, %{"status" => _} = params) do
    573     params =
    574       params
    575       |> Map.put("in_reply_to_status_id", params["in_reply_to_id"])
    576 
    577     scheduled_at = params["scheduled_at"]
    578 
    579     if scheduled_at && ScheduledActivity.far_enough?(scheduled_at) do
    580       with {:ok, scheduled_activity} <-
    581              ScheduledActivity.create(user, %{"params" => params, "scheduled_at" => scheduled_at}) do
    582         conn
    583         |> put_view(ScheduledActivityView)
    584         |> render("show.json", %{scheduled_activity: scheduled_activity})
    585       end
    586     else
    587       params = Map.drop(params, ["scheduled_at"])
    588 
    589       case CommonAPI.post(user, params) do
    590         {:error, message} ->
    591           conn
    592           |> put_status(:unprocessable_entity)
    593           |> json(%{error: message})
    594 
    595         {:ok, activity} ->
    596           conn
    597           |> put_view(StatusView)
    598           |> try_render("status.json", %{activity: activity, for: user, as: :activity})
    599       end
    600     end
    601   end
    602 
    603   def delete_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
    604     with {:ok, %Activity{}} <- CommonAPI.delete(id, user) do
    605       json(conn, %{})
    606     else
    607       _e -> render_error(conn, :forbidden, "Can't delete this post")
    608     end
    609   end
    610 
    611   def reblog_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
    612     with {:ok, announce, _activity} <- CommonAPI.repeat(ap_id_or_id, user),
    613          %Activity{} = announce <- Activity.normalize(announce.data) do
    614       conn
    615       |> put_view(StatusView)
    616       |> try_render("status.json", %{activity: announce, for: user, as: :activity})
    617     end
    618   end
    619 
    620   def unreblog_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
    621     with {:ok, _unannounce, %{data: %{"id" => id}}} <- CommonAPI.unrepeat(ap_id_or_id, user),
    622          %Activity{} = activity <- Activity.get_create_by_object_ap_id_with_object(id) do
    623       conn
    624       |> put_view(StatusView)
    625       |> try_render("status.json", %{activity: activity, for: user, as: :activity})
    626     end
    627   end
    628 
    629   def fav_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
    630     with {:ok, _fav, %{data: %{"id" => id}}} <- CommonAPI.favorite(ap_id_or_id, user),
    631          %Activity{} = activity <- Activity.get_create_by_object_ap_id(id) do
    632       conn
    633       |> put_view(StatusView)
    634       |> try_render("status.json", %{activity: activity, for: user, as: :activity})
    635     end
    636   end
    637 
    638   def unfav_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
    639     with {:ok, _, _, %{data: %{"id" => id}}} <- CommonAPI.unfavorite(ap_id_or_id, user),
    640          %Activity{} = activity <- Activity.get_create_by_object_ap_id(id) do
    641       conn
    642       |> put_view(StatusView)
    643       |> try_render("status.json", %{activity: activity, for: user, as: :activity})
    644     end
    645   end
    646 
    647   def pin_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
    648     with {:ok, activity} <- CommonAPI.pin(ap_id_or_id, user) do
    649       conn
    650       |> put_view(StatusView)
    651       |> try_render("status.json", %{activity: activity, for: user, as: :activity})
    652     end
    653   end
    654 
    655   def unpin_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
    656     with {:ok, activity} <- CommonAPI.unpin(ap_id_or_id, user) do
    657       conn
    658       |> put_view(StatusView)
    659       |> try_render("status.json", %{activity: activity, for: user, as: :activity})
    660     end
    661   end
    662 
    663   def bookmark_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
    664     with %Activity{} = activity <- Activity.get_by_id_with_object(id),
    665          %User{} = user <- User.get_cached_by_nickname(user.nickname),
    666          true <- Visibility.visible_for_user?(activity, user),
    667          {:ok, _bookmark} <- Bookmark.create(user.id, activity.id) do
    668       conn
    669       |> put_view(StatusView)
    670       |> try_render("status.json", %{activity: activity, for: user, as: :activity})
    671     end
    672   end
    673 
    674   def unbookmark_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
    675     with %Activity{} = activity <- Activity.get_by_id_with_object(id),
    676          %User{} = user <- User.get_cached_by_nickname(user.nickname),
    677          true <- Visibility.visible_for_user?(activity, user),
    678          {:ok, _bookmark} <- Bookmark.destroy(user.id, activity.id) do
    679       conn
    680       |> put_view(StatusView)
    681       |> try_render("status.json", %{activity: activity, for: user, as: :activity})
    682     end
    683   end
    684 
    685   def mute_conversation(%{assigns: %{user: user}} = conn, %{"id" => id}) do
    686     activity = Activity.get_by_id(id)
    687 
    688     with {:ok, activity} <- CommonAPI.add_mute(user, activity) do
    689       conn
    690       |> put_view(StatusView)
    691       |> try_render("status.json", %{activity: activity, for: user, as: :activity})
    692     end
    693   end
    694 
    695   def unmute_conversation(%{assigns: %{user: user}} = conn, %{"id" => id}) do
    696     activity = Activity.get_by_id(id)
    697 
    698     with {:ok, activity} <- CommonAPI.remove_mute(user, activity) do
    699       conn
    700       |> put_view(StatusView)
    701       |> try_render("status.json", %{activity: activity, for: user, as: :activity})
    702     end
    703   end
    704 
    705   def notifications(%{assigns: %{user: user}} = conn, params) do
    706     notifications = MastodonAPI.get_notifications(user, params)
    707 
    708     conn
    709     |> add_link_headers(:notifications, notifications)
    710     |> put_view(NotificationView)
    711     |> render("index.json", %{notifications: notifications, for: user})
    712   end
    713 
    714   def get_notification(%{assigns: %{user: user}} = conn, %{"id" => id} = _params) do
    715     with {:ok, notification} <- Notification.get(user, id) do
    716       conn
    717       |> put_view(NotificationView)
    718       |> render("show.json", %{notification: notification, for: user})
    719     else
    720       {:error, reason} ->
    721         conn
    722         |> put_status(:forbidden)
    723         |> json(%{"error" => reason})
    724     end
    725   end
    726 
    727   def clear_notifications(%{assigns: %{user: user}} = conn, _params) do
    728     Notification.clear(user)
    729     json(conn, %{})
    730   end
    731 
    732   def dismiss_notification(%{assigns: %{user: user}} = conn, %{"id" => id} = _params) do
    733     with {:ok, _notif} <- Notification.dismiss(user, id) do
    734       json(conn, %{})
    735     else
    736       {:error, reason} ->
    737         conn
    738         |> put_status(:forbidden)
    739         |> json(%{"error" => reason})
    740     end
    741   end
    742 
    743   def destroy_multiple(%{assigns: %{user: user}} = conn, %{"ids" => ids} = _params) do
    744     Notification.destroy_multiple(user, ids)
    745     json(conn, %{})
    746   end
    747 
    748   def relationships(%{assigns: %{user: user}} = conn, %{"id" => id}) do
    749     id = List.wrap(id)
    750     q = from(u in User, where: u.id in ^id)
    751     targets = Repo.all(q)
    752 
    753     conn
    754     |> put_view(AccountView)
    755     |> render("relationships.json", %{user: user, targets: targets})
    756   end
    757 
    758   # Instead of returning a 400 when no "id" params is present, Mastodon returns an empty array.
    759   def relationships(%{assigns: %{user: _user}} = conn, _), do: json(conn, [])
    760 
    761   def update_media(%{assigns: %{user: user}} = conn, data) do
    762     with %Object{} = object <- Repo.get(Object, data["id"]),
    763          true <- Object.authorize_mutation(object, user),
    764          true <- is_binary(data["description"]),
    765          description <- data["description"] do
    766       new_data = %{object.data | "name" => description}
    767 
    768       {:ok, _} =
    769         object
    770         |> Object.change(%{data: new_data})
    771         |> Repo.update()
    772 
    773       attachment_data = Map.put(new_data, "id", object.id)
    774 
    775       conn
    776       |> put_view(StatusView)
    777       |> render("attachment.json", %{attachment: attachment_data})
    778     end
    779   end
    780 
    781   def upload(%{assigns: %{user: user}} = conn, %{"file" => file} = data) do
    782     with {:ok, object} <-
    783            ActivityPub.upload(
    784              file,
    785              actor: User.ap_id(user),
    786              description: Map.get(data, "description")
    787            ) do
    788       attachment_data = Map.put(object.data, "id", object.id)
    789 
    790       conn
    791       |> put_view(StatusView)
    792       |> render("attachment.json", %{attachment: attachment_data})
    793     end
    794   end
    795 
    796   def set_mascot(%{assigns: %{user: user}} = conn, %{"file" => file}) do
    797     with {:ok, object} <- ActivityPub.upload(file, actor: User.ap_id(user)),
    798          %{} = attachment_data <- Map.put(object.data, "id", object.id),
    799          %{type: type} = rendered <-
    800            StatusView.render("attachment.json", %{attachment: attachment_data}) do
    801       # Reject if not an image
    802       if type == "image" do
    803         # Sure!
    804         # Save to the user's info
    805         info_changeset = User.Info.mascot_update(user.info, rendered)
    806 
    807         user_changeset =
    808           user
    809           |> Ecto.Changeset.change()
    810           |> Ecto.Changeset.put_embed(:info, info_changeset)
    811 
    812         {:ok, _user} = User.update_and_set_cache(user_changeset)
    813 
    814         conn
    815         |> json(rendered)
    816       else
    817         render_error(conn, :unsupported_media_type, "mascots can only be images")
    818       end
    819     end
    820   end
    821 
    822   def get_mascot(%{assigns: %{user: user}} = conn, _params) do
    823     mascot = User.get_mascot(user)
    824 
    825     conn
    826     |> json(mascot)
    827   end
    828 
    829   def favourited_by(%{assigns: %{user: user}} = conn, %{"id" => id}) do
    830     with %Activity{} = activity <- Activity.get_by_id_with_object(id),
    831          %Object{data: %{"likes" => likes}} <- Object.normalize(activity) do
    832       q = from(u in User, where: u.ap_id in ^likes)
    833 
    834       users =
    835         Repo.all(q)
    836         |> Enum.filter(&(not User.blocks?(user, &1)))
    837 
    838       conn
    839       |> put_view(AccountView)
    840       |> render("accounts.json", %{for: user, users: users, as: :user})
    841     else
    842       _ -> json(conn, [])
    843     end
    844   end
    845 
    846   def reblogged_by(%{assigns: %{user: user}} = conn, %{"id" => id}) do
    847     with %Activity{} = activity <- Activity.get_by_id_with_object(id),
    848          %Object{data: %{"announcements" => announces}} <- Object.normalize(activity) do
    849       q = from(u in User, where: u.ap_id in ^announces)
    850 
    851       users =
    852         Repo.all(q)
    853         |> Enum.filter(&(not User.blocks?(user, &1)))
    854 
    855       conn
    856       |> put_view(AccountView)
    857       |> render("accounts.json", %{for: user, users: users, as: :user})
    858     else
    859       _ -> json(conn, [])
    860     end
    861   end
    862 
    863   def hashtag_timeline(%{assigns: %{user: user}} = conn, params) do
    864     local_only = params["local"] in [true, "True", "true", "1"]
    865 
    866     tags =
    867       [params["tag"], params["any"]]
    868       |> List.flatten()
    869       |> Enum.uniq()
    870       |> Enum.filter(& &1)
    871       |> Enum.map(&String.downcase(&1))
    872 
    873     tag_all =
    874       params["all"] ||
    875         []
    876         |> Enum.map(&String.downcase(&1))
    877 
    878     tag_reject =
    879       params["none"] ||
    880         []
    881         |> Enum.map(&String.downcase(&1))
    882 
    883     activities =
    884       params
    885       |> Map.put("type", "Create")
    886       |> Map.put("local_only", local_only)
    887       |> Map.put("blocking_user", user)
    888       |> Map.put("muting_user", user)
    889       |> Map.put("user", user)
    890       |> Map.put("tag", tags)
    891       |> Map.put("tag_all", tag_all)
    892       |> Map.put("tag_reject", tag_reject)
    893       |> ActivityPub.fetch_public_activities()
    894       |> Enum.reverse()
    895 
    896     conn
    897     |> add_link_headers(:hashtag_timeline, activities, params["tag"], %{"local" => local_only})
    898     |> put_view(StatusView)
    899     |> render("index.json", %{activities: activities, for: user, as: :activity})
    900   end
    901 
    902   def followers(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
    903     with %User{} = user <- User.get_cached_by_id(id),
    904          followers <- MastodonAPI.get_followers(user, params) do
    905       followers =
    906         cond do
    907           for_user && user.id == for_user.id -> followers
    908           user.info.hide_followers -> []
    909           true -> followers
    910         end
    911 
    912       conn
    913       |> add_link_headers(:followers, followers, user)
    914       |> put_view(AccountView)
    915       |> render("accounts.json", %{for: for_user, users: followers, as: :user})
    916     end
    917   end
    918 
    919   def following(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
    920     with %User{} = user <- User.get_cached_by_id(id),
    921          followers <- MastodonAPI.get_friends(user, params) do
    922       followers =
    923         cond do
    924           for_user && user.id == for_user.id -> followers
    925           user.info.hide_follows -> []
    926           true -> followers
    927         end
    928 
    929       conn
    930       |> add_link_headers(:following, followers, user)
    931       |> put_view(AccountView)
    932       |> render("accounts.json", %{for: for_user, users: followers, as: :user})
    933     end
    934   end
    935 
    936   def follow_requests(%{assigns: %{user: followed}} = conn, _params) do
    937     with {:ok, follow_requests} <- User.get_follow_requests(followed) do
    938       conn
    939       |> put_view(AccountView)
    940       |> render("accounts.json", %{for: followed, users: follow_requests, as: :user})
    941     end
    942   end
    943 
    944   def authorize_follow_request(%{assigns: %{user: followed}} = conn, %{"id" => id}) do
    945     with %User{} = follower <- User.get_cached_by_id(id),
    946          {:ok, follower} <- CommonAPI.accept_follow_request(follower, followed) do
    947       conn
    948       |> put_view(AccountView)
    949       |> render("relationship.json", %{user: followed, target: follower})
    950     else
    951       {:error, message} ->
    952         conn
    953         |> put_status(:forbidden)
    954         |> json(%{error: message})
    955     end
    956   end
    957 
    958   def reject_follow_request(%{assigns: %{user: followed}} = conn, %{"id" => id}) do
    959     with %User{} = follower <- User.get_cached_by_id(id),
    960          {:ok, follower} <- CommonAPI.reject_follow_request(follower, followed) do
    961       conn
    962       |> put_view(AccountView)
    963       |> render("relationship.json", %{user: followed, target: follower})
    964     else
    965       {:error, message} ->
    966         conn
    967         |> put_status(:forbidden)
    968         |> json(%{error: message})
    969     end
    970   end
    971 
    972   def follow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
    973     with {_, %User{} = followed} <- {:followed, User.get_cached_by_id(id)},
    974          {_, true} <- {:followed, follower.id != followed.id},
    975          {:ok, follower} <- MastodonAPI.follow(follower, followed, conn.params) do
    976       conn
    977       |> put_view(AccountView)
    978       |> render("relationship.json", %{user: follower, target: followed})
    979     else
    980       {:followed, _} ->
    981         {:error, :not_found}
    982 
    983       {:error, message} ->
    984         conn
    985         |> put_status(:forbidden)
    986         |> json(%{error: message})
    987     end
    988   end
    989 
    990   def follow(%{assigns: %{user: follower}} = conn, %{"uri" => uri}) do
    991     with {_, %User{} = followed} <- {:followed, User.get_cached_by_nickname(uri)},
    992          {_, true} <- {:followed, follower.id != followed.id},
    993          {:ok, follower, followed, _} <- CommonAPI.follow(follower, followed) do
    994       conn
    995       |> put_view(AccountView)
    996       |> render("account.json", %{user: followed, for: follower})
    997     else
    998       {:followed, _} ->
    999         {:error, :not_found}
   1000 
   1001       {:error, message} ->
   1002         conn
   1003         |> put_status(:forbidden)
   1004         |> json(%{error: message})
   1005     end
   1006   end
   1007 
   1008   def unfollow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
   1009     with {_, %User{} = followed} <- {:followed, User.get_cached_by_id(id)},
   1010          {_, true} <- {:followed, follower.id != followed.id},
   1011          {:ok, follower} <- CommonAPI.unfollow(follower, followed) do
   1012       conn
   1013       |> put_view(AccountView)
   1014       |> render("relationship.json", %{user: follower, target: followed})
   1015     else
   1016       {:followed, _} ->
   1017         {:error, :not_found}
   1018 
   1019       error ->
   1020         error
   1021     end
   1022   end
   1023 
   1024   def mute(%{assigns: %{user: muter}} = conn, %{"id" => id} = params) do
   1025     notifications =
   1026       if Map.has_key?(params, "notifications"),
   1027         do: params["notifications"] in [true, "True", "true", "1"],
   1028         else: true
   1029 
   1030     with %User{} = muted <- User.get_cached_by_id(id),
   1031          {:ok, muter} <- User.mute(muter, muted, notifications) do
   1032       conn
   1033       |> put_view(AccountView)
   1034       |> render("relationship.json", %{user: muter, target: muted})
   1035     else
   1036       {:error, message} ->
   1037         conn
   1038         |> put_status(:forbidden)
   1039         |> json(%{error: message})
   1040     end
   1041   end
   1042 
   1043   def unmute(%{assigns: %{user: muter}} = conn, %{"id" => id}) do
   1044     with %User{} = muted <- User.get_cached_by_id(id),
   1045          {:ok, muter} <- User.unmute(muter, muted) do
   1046       conn
   1047       |> put_view(AccountView)
   1048       |> render("relationship.json", %{user: muter, target: muted})
   1049     else
   1050       {:error, message} ->
   1051         conn
   1052         |> put_status(:forbidden)
   1053         |> json(%{error: message})
   1054     end
   1055   end
   1056 
   1057   def mutes(%{assigns: %{user: user}} = conn, _) do
   1058     with muted_accounts <- User.muted_users(user) do
   1059       res = AccountView.render("accounts.json", users: muted_accounts, for: user, as: :user)
   1060       json(conn, res)
   1061     end
   1062   end
   1063 
   1064   def block(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
   1065     with %User{} = blocked <- User.get_cached_by_id(id),
   1066          {:ok, blocker} <- User.block(blocker, blocked),
   1067          {:ok, _activity} <- ActivityPub.block(blocker, blocked) do
   1068       conn
   1069       |> put_view(AccountView)
   1070       |> render("relationship.json", %{user: blocker, target: blocked})
   1071     else
   1072       {:error, message} ->
   1073         conn
   1074         |> put_status(:forbidden)
   1075         |> json(%{error: message})
   1076     end
   1077   end
   1078 
   1079   def unblock(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
   1080     with %User{} = blocked <- User.get_cached_by_id(id),
   1081          {:ok, blocker} <- User.unblock(blocker, blocked),
   1082          {:ok, _activity} <- ActivityPub.unblock(blocker, blocked) do
   1083       conn
   1084       |> put_view(AccountView)
   1085       |> render("relationship.json", %{user: blocker, target: blocked})
   1086     else
   1087       {:error, message} ->
   1088         conn
   1089         |> put_status(:forbidden)
   1090         |> json(%{error: message})
   1091     end
   1092   end
   1093 
   1094   def blocks(%{assigns: %{user: user}} = conn, _) do
   1095     with blocked_accounts <- User.blocked_users(user) do
   1096       res = AccountView.render("accounts.json", users: blocked_accounts, for: user, as: :user)
   1097       json(conn, res)
   1098     end
   1099   end
   1100 
   1101   def domain_blocks(%{assigns: %{user: %{info: info}}} = conn, _) do
   1102     json(conn, info.domain_blocks || [])
   1103   end
   1104 
   1105   def block_domain(%{assigns: %{user: blocker}} = conn, %{"domain" => domain}) do
   1106     User.block_domain(blocker, domain)
   1107     json(conn, %{})
   1108   end
   1109 
   1110   def unblock_domain(%{assigns: %{user: blocker}} = conn, %{"domain" => domain}) do
   1111     User.unblock_domain(blocker, domain)
   1112     json(conn, %{})
   1113   end
   1114 
   1115   def subscribe(%{assigns: %{user: user}} = conn, %{"id" => id}) do
   1116     with %User{} = subscription_target <- User.get_cached_by_id(id),
   1117          {:ok, subscription_target} = User.subscribe(user, subscription_target) do
   1118       conn
   1119       |> put_view(AccountView)
   1120       |> render("relationship.json", %{user: user, target: subscription_target})
   1121     else
   1122       {:error, message} ->
   1123         conn
   1124         |> put_status(:forbidden)
   1125         |> json(%{error: message})
   1126     end
   1127   end
   1128 
   1129   def unsubscribe(%{assigns: %{user: user}} = conn, %{"id" => id}) do
   1130     with %User{} = subscription_target <- User.get_cached_by_id(id),
   1131          {:ok, subscription_target} = User.unsubscribe(user, subscription_target) do
   1132       conn
   1133       |> put_view(AccountView)
   1134       |> render("relationship.json", %{user: user, target: subscription_target})
   1135     else
   1136       {:error, message} ->
   1137         conn
   1138         |> put_status(:forbidden)
   1139         |> json(%{error: message})
   1140     end
   1141   end
   1142 
   1143   def favourites(%{assigns: %{user: user}} = conn, params) do
   1144     params =
   1145       params
   1146       |> Map.put("type", "Create")
   1147       |> Map.put("favorited_by", user.ap_id)
   1148       |> Map.put("blocking_user", user)
   1149 
   1150     activities =
   1151       ActivityPub.fetch_activities([], params)
   1152       |> Enum.reverse()
   1153 
   1154     conn
   1155     |> add_link_headers(:favourites, activities)
   1156     |> put_view(StatusView)
   1157     |> render("index.json", %{activities: activities, for: user, as: :activity})
   1158   end
   1159 
   1160   def user_favourites(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
   1161     with %User{} = user <- User.get_by_id(id),
   1162          false <- user.info.hide_favorites do
   1163       params =
   1164         params
   1165         |> Map.put("type", "Create")
   1166         |> Map.put("favorited_by", user.ap_id)
   1167         |> Map.put("blocking_user", for_user)
   1168 
   1169       recipients =
   1170         if for_user do
   1171           [Pleroma.Constants.as_public()] ++ [for_user.ap_id | for_user.following]
   1172         else
   1173           [Pleroma.Constants.as_public()]
   1174         end
   1175 
   1176       activities =
   1177         recipients
   1178         |> ActivityPub.fetch_activities(params)
   1179         |> Enum.reverse()
   1180 
   1181       conn
   1182       |> add_link_headers(:favourites, activities)
   1183       |> put_view(StatusView)
   1184       |> render("index.json", %{activities: activities, for: for_user, as: :activity})
   1185     else
   1186       nil -> {:error, :not_found}
   1187       true -> render_error(conn, :forbidden, "Can't get favorites")
   1188     end
   1189   end
   1190 
   1191   def bookmarks(%{assigns: %{user: user}} = conn, params) do
   1192     user = User.get_cached_by_id(user.id)
   1193 
   1194     bookmarks =
   1195       Bookmark.for_user_query(user.id)
   1196       |> Pagination.fetch_paginated(params)
   1197 
   1198     activities =
   1199       bookmarks
   1200       |> Enum.map(fn b -> Map.put(b.activity, :bookmark, Map.delete(b, :activity)) end)
   1201 
   1202     conn
   1203     |> add_link_headers(:bookmarks, bookmarks)
   1204     |> put_view(StatusView)
   1205     |> render("index.json", %{activities: activities, for: user, as: :activity})
   1206   end
   1207 
   1208   def get_lists(%{assigns: %{user: user}} = conn, opts) do
   1209     lists = Pleroma.List.for_user(user, opts)
   1210     res = ListView.render("lists.json", lists: lists)
   1211     json(conn, res)
   1212   end
   1213 
   1214   def get_list(%{assigns: %{user: user}} = conn, %{"id" => id}) do
   1215     with %Pleroma.List{} = list <- Pleroma.List.get(id, user) do
   1216       res = ListView.render("list.json", list: list)
   1217       json(conn, res)
   1218     else
   1219       _e -> render_error(conn, :not_found, "Record not found")
   1220     end
   1221   end
   1222 
   1223   def account_lists(%{assigns: %{user: user}} = conn, %{"id" => account_id}) do
   1224     lists = Pleroma.List.get_lists_account_belongs(user, account_id)
   1225     res = ListView.render("lists.json", lists: lists)
   1226     json(conn, res)
   1227   end
   1228 
   1229   def delete_list(%{assigns: %{user: user}} = conn, %{"id" => id}) do
   1230     with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
   1231          {:ok, _list} <- Pleroma.List.delete(list) do
   1232       json(conn, %{})
   1233     else
   1234       _e ->
   1235         json(conn, dgettext("errors", "error"))
   1236     end
   1237   end
   1238 
   1239   def create_list(%{assigns: %{user: user}} = conn, %{"title" => title}) do
   1240     with {:ok, %Pleroma.List{} = list} <- Pleroma.List.create(title, user) do
   1241       res = ListView.render("list.json", list: list)
   1242       json(conn, res)
   1243     end
   1244   end
   1245 
   1246   def add_to_list(%{assigns: %{user: user}} = conn, %{"id" => id, "account_ids" => accounts}) do
   1247     accounts
   1248     |> Enum.each(fn account_id ->
   1249       with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
   1250            %User{} = followed <- User.get_cached_by_id(account_id) do
   1251         Pleroma.List.follow(list, followed)
   1252       end
   1253     end)
   1254 
   1255     json(conn, %{})
   1256   end
   1257 
   1258   def remove_from_list(%{assigns: %{user: user}} = conn, %{"id" => id, "account_ids" => accounts}) do
   1259     accounts
   1260     |> Enum.each(fn account_id ->
   1261       with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
   1262            %User{} = followed <- User.get_cached_by_id(account_id) do
   1263         Pleroma.List.unfollow(list, followed)
   1264       end
   1265     end)
   1266 
   1267     json(conn, %{})
   1268   end
   1269 
   1270   def list_accounts(%{assigns: %{user: user}} = conn, %{"id" => id}) do
   1271     with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
   1272          {:ok, users} = Pleroma.List.get_following(list) do
   1273       conn
   1274       |> put_view(AccountView)
   1275       |> render("accounts.json", %{for: user, users: users, as: :user})
   1276     end
   1277   end
   1278 
   1279   def rename_list(%{assigns: %{user: user}} = conn, %{"id" => id, "title" => title}) do
   1280     with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
   1281          {:ok, list} <- Pleroma.List.rename(list, title) do
   1282       res = ListView.render("list.json", list: list)
   1283       json(conn, res)
   1284     else
   1285       _e ->
   1286         json(conn, dgettext("errors", "error"))
   1287     end
   1288   end
   1289 
   1290   def list_timeline(%{assigns: %{user: user}} = conn, %{"list_id" => id} = params) do
   1291     with %Pleroma.List{title: _title, following: following} <- Pleroma.List.get(id, user) do
   1292       params =
   1293         params
   1294         |> Map.put("type", "Create")
   1295         |> Map.put("blocking_user", user)
   1296         |> Map.put("user", user)
   1297         |> Map.put("muting_user", user)
   1298 
   1299       # we must filter the following list for the user to avoid leaking statuses the user
   1300       # does not actually have permission to see (for more info, peruse security issue #270).
   1301       activities =
   1302         following
   1303         |> Enum.filter(fn x -> x in user.following end)
   1304         |> ActivityPub.fetch_activities_bounded(following, params)
   1305         |> Enum.reverse()
   1306 
   1307       conn
   1308       |> put_view(StatusView)
   1309       |> render("index.json", %{activities: activities, for: user, as: :activity})
   1310     else
   1311       _e -> render_error(conn, :forbidden, "Error.")
   1312     end
   1313   end
   1314 
   1315   def index(%{assigns: %{user: user}} = conn, _params) do
   1316     token = get_session(conn, :oauth_token)
   1317 
   1318     if user && token do
   1319       mastodon_emoji = mastodonized_emoji()
   1320 
   1321       limit = Config.get([:instance, :limit])
   1322 
   1323       accounts =
   1324         Map.put(%{}, user.id, AccountView.render("account.json", %{user: user, for: user}))
   1325 
   1326       initial_state =
   1327         %{
   1328           meta: %{
   1329             streaming_api_base_url: Pleroma.Web.Endpoint.websocket_url(),
   1330             access_token: token,
   1331             locale: "en",
   1332             domain: Pleroma.Web.Endpoint.host(),
   1333             admin: "1",
   1334             me: "#{user.id}",
   1335             unfollow_modal: false,
   1336             boost_modal: false,
   1337             delete_modal: true,
   1338             auto_play_gif: false,
   1339             display_sensitive_media: false,
   1340             reduce_motion: false,
   1341             max_toot_chars: limit,
   1342             mascot: User.get_mascot(user)["url"]
   1343           },
   1344           poll_limits: Config.get([:instance, :poll_limits]),
   1345           rights: %{
   1346             delete_others_notice: present?(user.info.is_moderator),
   1347             admin: present?(user.info.is_admin)
   1348           },
   1349           compose: %{
   1350             me: "#{user.id}",
   1351             default_privacy: user.info.default_scope,
   1352             default_sensitive: false,
   1353             allow_content_types: Config.get([:instance, :allowed_post_formats])
   1354           },
   1355           media_attachments: %{
   1356             accept_content_types: [
   1357               ".jpg",
   1358               ".jpeg",
   1359               ".png",
   1360               ".gif",
   1361               ".webm",
   1362               ".mp4",
   1363               ".m4v",
   1364               "image\/jpeg",
   1365               "image\/png",
   1366               "image\/gif",
   1367               "video\/webm",
   1368               "video\/mp4"
   1369             ]
   1370           },
   1371           settings:
   1372             user.info.settings ||
   1373               %{
   1374                 onboarded: true,
   1375                 home: %{
   1376                   shows: %{
   1377                     reblog: true,
   1378                     reply: true
   1379                   }
   1380                 },
   1381                 notifications: %{
   1382                   alerts: %{
   1383                     follow: true,
   1384                     favourite: true,
   1385                     reblog: true,
   1386                     mention: true
   1387                   },
   1388                   shows: %{
   1389                     follow: true,
   1390                     favourite: true,
   1391                     reblog: true,
   1392                     mention: true
   1393                   },
   1394                   sounds: %{
   1395                     follow: true,
   1396                     favourite: true,
   1397                     reblog: true,
   1398                     mention: true
   1399                   }
   1400                 }
   1401               },
   1402           push_subscription: nil,
   1403           accounts: accounts,
   1404           custom_emojis: mastodon_emoji,
   1405           char_limit: limit
   1406         }
   1407         |> Jason.encode!()
   1408 
   1409       conn
   1410       |> put_layout(false)
   1411       |> put_view(MastodonView)
   1412       |> render("index.html", %{initial_state: initial_state})
   1413     else
   1414       conn
   1415       |> put_session(:return_to, conn.request_path)
   1416       |> redirect(to: "/web/login")
   1417     end
   1418   end
   1419 
   1420   def put_settings(%{assigns: %{user: user}} = conn, %{"data" => settings} = _params) do
   1421     info_cng = User.Info.mastodon_settings_update(user.info, settings)
   1422 
   1423     with changeset <- Ecto.Changeset.change(user),
   1424          changeset <- Ecto.Changeset.put_embed(changeset, :info, info_cng),
   1425          {:ok, _user} <- User.update_and_set_cache(changeset) do
   1426       json(conn, %{})
   1427     else
   1428       e ->
   1429         conn
   1430         |> put_status(:internal_server_error)
   1431         |> json(%{error: inspect(e)})
   1432     end
   1433   end
   1434 
   1435   def login(%{assigns: %{user: %User{}}} = conn, _params) do
   1436     redirect(conn, to: local_mastodon_root_path(conn))
   1437   end
   1438 
   1439   @doc "Local Mastodon FE login init action"
   1440   def login(conn, %{"code" => auth_token}) do
   1441     with {:ok, app} <- get_or_make_app(),
   1442          %Authorization{} = auth <- Repo.get_by(Authorization, token: auth_token, app_id: app.id),
   1443          {:ok, token} <- Token.exchange_token(app, auth) do
   1444       conn
   1445       |> put_session(:oauth_token, token.token)
   1446       |> redirect(to: local_mastodon_root_path(conn))
   1447     end
   1448   end
   1449 
   1450   @doc "Local Mastodon FE callback action"
   1451   def login(conn, _) do
   1452     with {:ok, app} <- get_or_make_app() do
   1453       path =
   1454         o_auth_path(
   1455           conn,
   1456           :authorize,
   1457           response_type: "code",
   1458           client_id: app.client_id,
   1459           redirect_uri: ".",
   1460           scope: Enum.join(app.scopes, " ")
   1461         )
   1462 
   1463       redirect(conn, to: path)
   1464     end
   1465   end
   1466 
   1467   defp local_mastodon_root_path(conn) do
   1468     case get_session(conn, :return_to) do
   1469       nil ->
   1470         mastodon_api_path(conn, :index, ["getting-started"])
   1471 
   1472       return_to ->
   1473         delete_session(conn, :return_to)
   1474         return_to
   1475     end
   1476   end
   1477 
   1478   defp get_or_make_app do
   1479     find_attrs = %{client_name: @local_mastodon_name, redirect_uris: "."}
   1480     scopes = ["read", "write", "follow", "push"]
   1481 
   1482     with %App{} = app <- Repo.get_by(App, find_attrs) do
   1483       {:ok, app} =
   1484         if app.scopes == scopes do
   1485           {:ok, app}
   1486         else
   1487           app
   1488           |> Ecto.Changeset.change(%{scopes: scopes})
   1489           |> Repo.update()
   1490         end
   1491 
   1492       {:ok, app}
   1493     else
   1494       _e ->
   1495         cs =
   1496           App.register_changeset(
   1497             %App{},
   1498             Map.put(find_attrs, :scopes, scopes)
   1499           )
   1500 
   1501         Repo.insert(cs)
   1502     end
   1503   end
   1504 
   1505   def logout(conn, _) do
   1506     conn
   1507     |> clear_session
   1508     |> redirect(to: "/")
   1509   end
   1510 
   1511   def relationship_noop(%{assigns: %{user: user}} = conn, %{"id" => id}) do
   1512     Logger.debug("Unimplemented, returning unmodified relationship")
   1513 
   1514     with %User{} = target <- User.get_cached_by_id(id) do
   1515       conn
   1516       |> put_view(AccountView)
   1517       |> render("relationship.json", %{user: user, target: target})
   1518     end
   1519   end
   1520 
   1521   def empty_array(conn, _) do
   1522     Logger.debug("Unimplemented, returning an empty array")
   1523     json(conn, [])
   1524   end
   1525 
   1526   def empty_object(conn, _) do
   1527     Logger.debug("Unimplemented, returning an empty object")
   1528     json(conn, %{})
   1529   end
   1530 
   1531   def get_filters(%{assigns: %{user: user}} = conn, _) do
   1532     filters = Filter.get_filters(user)
   1533     res = FilterView.render("filters.json", filters: filters)
   1534     json(conn, res)
   1535   end
   1536 
   1537   def create_filter(
   1538         %{assigns: %{user: user}} = conn,
   1539         %{"phrase" => phrase, "context" => context} = params
   1540       ) do
   1541     query = %Filter{
   1542       user_id: user.id,
   1543       phrase: phrase,
   1544       context: context,
   1545       hide: Map.get(params, "irreversible", false),
   1546       whole_word: Map.get(params, "boolean", true)
   1547       # expires_at
   1548     }
   1549 
   1550     {:ok, response} = Filter.create(query)
   1551     res = FilterView.render("filter.json", filter: response)
   1552     json(conn, res)
   1553   end
   1554 
   1555   def get_filter(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do
   1556     filter = Filter.get(filter_id, user)
   1557     res = FilterView.render("filter.json", filter: filter)
   1558     json(conn, res)
   1559   end
   1560 
   1561   def update_filter(
   1562         %{assigns: %{user: user}} = conn,
   1563         %{"phrase" => phrase, "context" => context, "id" => filter_id} = params
   1564       ) do
   1565     query = %Filter{
   1566       user_id: user.id,
   1567       filter_id: filter_id,
   1568       phrase: phrase,
   1569       context: context,
   1570       hide: Map.get(params, "irreversible", nil),
   1571       whole_word: Map.get(params, "boolean", true)
   1572       # expires_at
   1573     }
   1574 
   1575     {:ok, response} = Filter.update(query)
   1576     res = FilterView.render("filter.json", filter: response)
   1577     json(conn, res)
   1578   end
   1579 
   1580   def delete_filter(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do
   1581     query = %Filter{
   1582       user_id: user.id,
   1583       filter_id: filter_id
   1584     }
   1585 
   1586     {:ok, _} = Filter.delete(query)
   1587     json(conn, %{})
   1588   end
   1589 
   1590   # fallback action
   1591   #
   1592   def errors(conn, {:error, %Changeset{} = changeset}) do
   1593     error_message =
   1594       changeset
   1595       |> Changeset.traverse_errors(fn {message, _opt} -> message end)
   1596       |> Enum.map_join(", ", fn {_k, v} -> v end)
   1597 
   1598     conn
   1599     |> put_status(:unprocessable_entity)
   1600     |> json(%{error: error_message})
   1601   end
   1602 
   1603   def errors(conn, {:error, :not_found}) do
   1604     render_error(conn, :not_found, "Record not found")
   1605   end
   1606 
   1607   def errors(conn, {:error, error_message}) do
   1608     conn
   1609     |> put_status(:bad_request)
   1610     |> json(%{error: error_message})
   1611   end
   1612 
   1613   def errors(conn, _) do
   1614     conn
   1615     |> put_status(:internal_server_error)
   1616     |> json(dgettext("errors", "Something went wrong"))
   1617   end
   1618 
   1619   def suggestions(%{assigns: %{user: user}} = conn, _) do
   1620     suggestions = Config.get(:suggestions)
   1621 
   1622     if Keyword.get(suggestions, :enabled, false) do
   1623       api = Keyword.get(suggestions, :third_party_engine, "")
   1624       timeout = Keyword.get(suggestions, :timeout, 5000)
   1625       limit = Keyword.get(suggestions, :limit, 23)
   1626 
   1627       host = Config.get([Pleroma.Web.Endpoint, :url, :host])
   1628 
   1629       user = user.nickname
   1630 
   1631       url =
   1632         api
   1633         |> String.replace("{{host}}", host)
   1634         |> String.replace("{{user}}", user)
   1635 
   1636       with {:ok, %{status: 200, body: body}} <-
   1637              HTTP.get(url, [], adapter: [recv_timeout: timeout, pool: :default]),
   1638            {:ok, data} <- Jason.decode(body) do
   1639         data =
   1640           data
   1641           |> Enum.slice(0, limit)
   1642           |> Enum.map(fn x ->
   1643             x
   1644             |> Map.put("id", fetch_suggestion_id(x))
   1645             |> Map.put("avatar", MediaProxy.url(x["avatar"]))
   1646             |> Map.put("avatar_static", MediaProxy.url(x["avatar_static"]))
   1647           end)
   1648 
   1649         json(conn, data)
   1650       else
   1651         e ->
   1652           Logger.error("Could not retrieve suggestions at fetch #{url}, #{inspect(e)}")
   1653       end
   1654     else
   1655       json(conn, [])
   1656     end
   1657   end
   1658 
   1659   defp fetch_suggestion_id(attrs) do
   1660     case User.get_or_fetch(attrs["acct"]) do
   1661       {:ok, %User{id: id}} -> id
   1662       _ -> 0
   1663     end
   1664   end
   1665 
   1666   def status_card(%{assigns: %{user: user}} = conn, %{"id" => status_id}) do
   1667     with %Activity{} = activity <- Activity.get_by_id(status_id),
   1668          true <- Visibility.visible_for_user?(activity, user) do
   1669       data =
   1670         StatusView.render(
   1671           "card.json",
   1672           Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity)
   1673         )
   1674 
   1675       json(conn, data)
   1676     else
   1677       _e ->
   1678         %{}
   1679     end
   1680   end
   1681 
   1682   def reports(%{assigns: %{user: user}} = conn, params) do
   1683     case CommonAPI.report(user, params) do
   1684       {:ok, activity} ->
   1685         conn
   1686         |> put_view(ReportView)
   1687         |> try_render("report.json", %{activity: activity})
   1688 
   1689       {:error, err} ->
   1690         conn
   1691         |> put_status(:bad_request)
   1692         |> json(%{error: err})
   1693     end
   1694   end
   1695 
   1696   def account_register(
   1697         %{assigns: %{app: app}} = conn,
   1698         %{"username" => nickname, "email" => _, "password" => _, "agreement" => true} = params
   1699       ) do
   1700     params =
   1701       params
   1702       |> Map.take([
   1703         "email",
   1704         "captcha_solution",
   1705         "captcha_token",
   1706         "captcha_answer_data",
   1707         "token",
   1708         "password"
   1709       ])
   1710       |> Map.put("nickname", nickname)
   1711       |> Map.put("fullname", params["fullname"] || nickname)
   1712       |> Map.put("bio", params["bio"] || "")
   1713       |> Map.put("confirm", params["password"])
   1714 
   1715     with {:ok, user} <- TwitterAPI.register_user(params, need_confirmation: true),
   1716          {:ok, token} <- Token.create_token(app, user, %{scopes: app.scopes}) do
   1717       json(conn, %{
   1718         token_type: "Bearer",
   1719         access_token: token.token,
   1720         scope: app.scopes,
   1721         created_at: Token.Utils.format_created_at(token)
   1722       })
   1723     else
   1724       {:error, errors} ->
   1725         conn
   1726         |> put_status(:bad_request)
   1727         |> json(errors)
   1728     end
   1729   end
   1730 
   1731   def account_register(%{assigns: %{app: _app}} = conn, _params) do
   1732     render_error(conn, :bad_request, "Missing parameters")
   1733   end
   1734 
   1735   def account_register(conn, _) do
   1736     render_error(conn, :forbidden, "Invalid credentials")
   1737   end
   1738 
   1739   def conversations(%{assigns: %{user: user}} = conn, params) do
   1740     participations = Participation.for_user_with_last_activity_id(user, params)
   1741 
   1742     conversations =
   1743       Enum.map(participations, fn participation ->
   1744         ConversationView.render("participation.json", %{participation: participation, for: user})
   1745       end)
   1746 
   1747     conn
   1748     |> add_link_headers(:conversations, participations)
   1749     |> json(conversations)
   1750   end
   1751 
   1752   def conversation_read(%{assigns: %{user: user}} = conn, %{"id" => participation_id}) do
   1753     with %Participation{} = participation <-
   1754            Repo.get_by(Participation, id: participation_id, user_id: user.id),
   1755          {:ok, participation} <- Participation.mark_as_read(participation) do
   1756       participation_view =
   1757         ConversationView.render("participation.json", %{participation: participation, for: user})
   1758 
   1759       conn
   1760       |> json(participation_view)
   1761     end
   1762   end
   1763 
   1764   def password_reset(conn, params) do
   1765     nickname_or_email = params["email"] || params["nickname"]
   1766 
   1767     with {:ok, _} <- TwitterAPI.password_reset(nickname_or_email) do
   1768       conn
   1769       |> put_status(:no_content)
   1770       |> json("")
   1771     else
   1772       {:error, "unknown user"} ->
   1773         send_resp(conn, :not_found, "")
   1774 
   1775       {:error, _} ->
   1776         send_resp(conn, :bad_request, "")
   1777     end
   1778   end
   1779 
   1780   def account_confirmation_resend(conn, params) do
   1781     nickname_or_email = params["email"] || params["nickname"]
   1782 
   1783     with %User{} = user <- User.get_by_nickname_or_email(nickname_or_email),
   1784          {:ok, _} <- User.try_send_confirmation_email(user) do
   1785       conn
   1786       |> json_response(:no_content, "")
   1787     end
   1788   end
   1789 
   1790   def try_render(conn, target, params)
   1791       when is_binary(target) do
   1792     case render(conn, target, params) do
   1793       nil -> render_error(conn, :not_implemented, "Can't display this activity")
   1794       res -> res
   1795     end
   1796   end
   1797 
   1798   def try_render(conn, _, _) do
   1799     render_error(conn, :not_implemented, "Can't display this activity")
   1800   end
   1801 
   1802   defp present?(nil), do: false
   1803   defp present?(false), do: false
   1804   defp present?(_), do: true
   1805 end