mastodon_api_controller.ex 45.2 KB
Newer Older
1
# Pleroma: A lightweight social networking server
kaniini's avatar
kaniini committed
2
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
3 4
# SPDX-License-Identifier: AGPL-3.0-only

5 6
defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
  use Pleroma.Web, :controller
Haelwenn's avatar
Haelwenn committed
7 8 9 10 11 12 13 14
  alias Pleroma.Activity
  alias Pleroma.Config
  alias Pleroma.Filter
  alias Pleroma.Notification
  alias Pleroma.Object
  alias Pleroma.Repo
  alias Pleroma.Stats
  alias Pleroma.User
lain's avatar
lain committed
15
  alias Pleroma.Web
lain's avatar
lain committed
16
  alias Pleroma.Web.CommonAPI
17
  alias Pleroma.Web.MediaProxy
Haelwenn's avatar
Haelwenn committed
18
  alias Pleroma.Web.Push
Haelwenn's avatar
Haelwenn committed
19
  alias Push.Subscription
minibikini's avatar
cleanup  
minibikini committed
20

Haelwenn's avatar
Haelwenn committed
21 22 23 24 25 26 27 28 29 30 31
  alias Pleroma.Web.MastodonAPI.AccountView
  alias Pleroma.Web.MastodonAPI.FilterView
  alias Pleroma.Web.MastodonAPI.ListView
  alias Pleroma.Web.MastodonAPI.MastodonView
  alias Pleroma.Web.MastodonAPI.PushSubscriptionView
  alias Pleroma.Web.MastodonAPI.StatusView
  alias Pleroma.Web.ActivityPub.ActivityPub
  alias Pleroma.Web.ActivityPub.Utils
  alias Pleroma.Web.OAuth.App
  alias Pleroma.Web.OAuth.Authorization
  alias Pleroma.Web.OAuth.Token
minibikini's avatar
cleanup  
minibikini committed
32

Roger Braun's avatar
Roger Braun committed
33
  import Ecto.Query
Thog's avatar
Thog committed
34
  require Logger
35

36
  @httpoison Application.get_env(:pleroma, :httpoison)
37
  @local_mastodon_name "Mastodon-Local"
38

39 40
  action_fallback(:errors)

41
  def create_app(conn, params) do
42 43 44
    with cs <- App.register_changeset(%App{}, params),
         false <- cs.changes[:client_name] == @local_mastodon_name,
         {:ok, app} <- Repo.insert(cs) do
45
      res = %{
46
        id: app.id |> to_string,
47
        name: app.client_name,
48
        client_id: app.client_id,
49
        client_secret: app.client_secret,
50
        redirect_uri: app.redirect_uris,
51
        website: app.website
52 53 54 55 56 57
      }

      json(conn, res)
    end
  end

lain's avatar
lain committed
58 59 60 61 62 63 64 65 66 67 68
  defp add_if_present(
         map,
         params,
         params_field,
         map_field,
         value_function \\ fn x -> {:ok, x} end
       ) do
    if Map.has_key?(params, params_field) do
      case value_function.(params[params_field]) do
        {:ok, new_value} -> Map.put(map, map_field, new_value)
        :error -> map
lain's avatar
lain committed
69
      end
lain's avatar
lain committed
70 71 72 73
    else
      map
    end
  end
74

lain's avatar
lain committed
75 76
  def update_credentials(%{assigns: %{user: user}} = conn, params) do
    original_user = user
77

lain's avatar
lain committed
78 79 80
    user_params =
      %{}
      |> add_if_present(params, "display_name", :name)
Maxim Filippov's avatar
Maxim Filippov committed
81
      |> add_if_present(params, "note", :bio, fn value -> {:ok, User.parse_bio(value)} end)
lain's avatar
lain committed
82 83 84 85
      |> add_if_present(params, "avatar", :avatar, fn value ->
        with %Plug.Upload{} <- value,
             {:ok, object} <- ActivityPub.upload(value, type: :avatar) do
          {:ok, object.data}
lain's avatar
lain committed
86
        else
lain's avatar
lain committed
87
          _ -> :error
lain's avatar
lain committed
88
        end
lain's avatar
lain committed
89
      end)
lain's avatar
lain committed
90

lain's avatar
lain committed
91 92 93 94 95 96 97
    info_params =
      %{}
      |> add_if_present(params, "locked", :locked, fn value -> {:ok, value == "true"} end)
      |> add_if_present(params, "header", :banner, fn value ->
        with %Plug.Upload{} <- value,
             {:ok, object} <- ActivityPub.upload(value, type: :banner) do
          {:ok, object.data}
lain's avatar
lain committed
98
        else
lain's avatar
lain committed
99
          _ -> :error
lain's avatar
lain committed
100
        end
lain's avatar
lain committed
101
      end)
102

lain's avatar
lain committed
103
    info_cng = User.Info.mastodon_profile_update(user.info, info_params)
104

lain's avatar
lain committed
105 106
    with changeset <- User.update_changeset(user, user_params),
         changeset <- Ecto.Changeset.put_embed(changeset, :info, info_cng),
lain's avatar
lain committed
107 108 109 110
         {:ok, user} <- User.update_and_set_cache(changeset) do
      if original_user != user do
        CommonAPI.update(user)
      end
lain's avatar
lain committed
111

112
      json(conn, AccountView.render("account.json", %{user: user, for: user}))
113 114 115 116 117 118 119 120
    else
      _e ->
        conn
        |> put_status(403)
        |> json(%{error: "Invalid request"})
    end
  end

Thog's avatar
Thog committed
121
  def verify_credentials(%{assigns: %{user: user}} = conn, _) do
122
    account = AccountView.render("account.json", %{user: user, for: user})
lain's avatar
lain committed
123 124 125
    json(conn, account)
  end

126
  def user(%{assigns: %{user: for_user}} = conn, %{"id" => id}) do
127 128
    with %User{} = user <- Repo.get(User, id),
         true <- User.auth_active?(user) || user.id == for_user.id || User.superuser?(for_user) do
129
      account = AccountView.render("account.json", %{user: user, for: for_user})
Roger Braun's avatar
Roger Braun committed
130 131
      json(conn, account)
    else
lain's avatar
lain committed
132 133 134 135
      _e ->
        conn
        |> put_status(404)
        |> json(%{error: "Can't find user"})
Roger Braun's avatar
Roger Braun committed
136 137 138
    end
  end

139
  @mastodon_api_level "2.5.0"
lain's avatar
lain committed
140

lain's avatar
lain committed
141
  def masto_instance(conn, _params) do
Haelwenn's avatar
Haelwenn committed
142
    instance = Config.get(:instance)
href's avatar
href committed
143

lain's avatar
lain committed
144
    response = %{
lain's avatar
lain committed
145
      uri: Web.base_url(),
href's avatar
href committed
146 147
      title: Keyword.get(instance, :name),
      description: Keyword.get(instance, :description),
href's avatar
href committed
148
      version: "#{@mastodon_api_level} (compatible; #{Pleroma.Application.named_version()})",
href's avatar
href committed
149
      email: Keyword.get(instance, :email),
lain's avatar
lain committed
150
      urls: %{
151
        streaming_api: Pleroma.Web.Endpoint.websocket_url()
lain's avatar
lain committed
152
      },
lain's avatar
lain committed
153 154
      stats: Stats.get_stats(),
      thumbnail: Web.base_url() <> "/instance/thumbnail.jpeg",
href's avatar
href committed
155
      max_toot_chars: Keyword.get(instance, :limit)
156 157
    }

lain's avatar
lain committed
158
    json(conn, response)
159
  end
lain's avatar
lain committed
160

161
  def peers(conn, _params) do
lain's avatar
lain committed
162
    json(conn, Stats.get_peers())
163 164
  end

165
  defp mastodonized_emoji do
href's avatar
href committed
166
    Pleroma.Emoji.get_all()
167
    |> Enum.map(fn {shortcode, relative_url} ->
lain's avatar
lain committed
168 169
      url = to_string(URI.merge(Web.base_url(), relative_url))

170 171 172
      %{
        "shortcode" => shortcode,
        "static_url" => url,
173
        "visible_in_picker" => true,
174 175 176
        "url" => url
      }
    end)
177 178 179 180
  end

  def custom_emojis(conn, _params) do
    mastodon_emoji = mastodonized_emoji()
lain's avatar
lain committed
181
    json(conn, mastodon_emoji)
182 183
  end

184
  defp add_link_headers(conn, method, activities, param \\ nil, params \\ %{}) do
185 186
    last = List.last(activities)
    first = List.first(activities)
lain's avatar
lain committed
187

188 189 190
    if last do
      min = last.id
      max = first.id
lain's avatar
lain committed
191 192 193 194

      {next_url, prev_url} =
        if param do
          {
195 196 197 198 199 200 201 202 203 204 205 206
            mastodon_api_url(
              Pleroma.Web.Endpoint,
              method,
              param,
              Map.merge(params, %{max_id: min})
            ),
            mastodon_api_url(
              Pleroma.Web.Endpoint,
              method,
              param,
              Map.merge(params, %{since_id: max})
            )
lain's avatar
lain committed
207 208 209
          }
        else
          {
210 211 212 213 214 215 216 217 218 219
            mastodon_api_url(
              Pleroma.Web.Endpoint,
              method,
              Map.merge(params, %{max_id: min})
            ),
            mastodon_api_url(
              Pleroma.Web.Endpoint,
              method,
              Map.merge(params, %{since_id: max})
            )
lain's avatar
lain committed
220 221 222
          }
        end

223 224 225 226 227 228 229
      conn
      |> put_resp_header("link", "<#{next_url}>; rel=\"next\", <#{prev_url}>; rel=\"prev\"")
    else
      conn
    end
  end

lain's avatar
lain committed
230
  def home_timeline(%{assigns: %{user: user}} = conn, params) do
lain's avatar
lain committed
231 232 233 234
    params =
      params
      |> Map.put("type", ["Create", "Announce"])
      |> Map.put("blocking_user", user)
235
      |> Map.put("muting_user", user)
lain's avatar
lain committed
236
      |> Map.put("user", user)
lain's avatar
lain committed
237

lain's avatar
lain committed
238
    activities =
Haelwenn's avatar
Haelwenn committed
239 240
      [user.ap_id | user.following]
      |> ActivityPub.fetch_activities(params)
241
      |> ActivityPub.contain_timeline(user)
lain's avatar
lain committed
242
      |> Enum.reverse()
243 244

    conn
lain's avatar
lain committed
245
    |> add_link_headers(:home_timeline, activities)
href's avatar
href committed
246 247
    |> put_view(StatusView)
    |> render("index.json", %{activities: activities, for: user, as: :activity})
lain's avatar
lain committed
248 249 250
  end

  def public_timeline(%{assigns: %{user: user}} = conn, params) do
251 252
    local_only = params["local"] in [true, "True", "true", "1"]

Haelwenn's avatar
Haelwenn committed
253
    activities =
lain's avatar
lain committed
254 255
      params
      |> Map.put("type", ["Create", "Announce"])
256
      |> Map.put("local_only", local_only)
lain's avatar
lain committed
257
      |> Map.put("blocking_user", user)
258
      |> Map.put("muting_user", user)
Haelwenn's avatar
Haelwenn committed
259
      |> ActivityPub.fetch_public_activities()
lain's avatar
lain committed
260
      |> Enum.reverse()
lain's avatar
lain committed
261

lain's avatar
lain committed
262
    conn
263
    |> add_link_headers(:public_timeline, activities, false, %{"local" => local_only})
href's avatar
href committed
264 265
    |> put_view(StatusView)
    |> render("index.json", %{activities: activities, for: user, as: :activity})
lain's avatar
lain committed
266 267
  end

268 269
  def user_statuses(%{assigns: %{user: reading_user}} = conn, params) do
    with %User{} = user <- Repo.get(User, params["id"]) do
minibikini's avatar
minibikini committed
270
      activities = ActivityPub.fetch_user_activities(user, reading_user, params)
lain's avatar
lain committed
271

272 273
      conn
      |> add_link_headers(:user_statuses, activities, params["id"])
href's avatar
href committed
274 275
      |> put_view(StatusView)
      |> render("index.json", %{
276 277 278 279
        activities: activities,
        for: reading_user,
        as: :activity
      })
lain's avatar
lain committed
280 281 282
    end
  end

283
  def dm_timeline(%{assigns: %{user: user}} = conn, params) do
284
    query =
285 286 287 288
      ActivityPub.fetch_activities_query(
        [user.ap_id],
        Map.merge(params, %{"type" => "Create", visibility: "direct"})
      )
289

290 291 292
    activities = Repo.all(query)

    conn
293
    |> add_link_headers(:dm_timeline, activities)
href's avatar
href committed
294 295
    |> put_view(StatusView)
    |> render("index.json", %{activities: activities, for: user, as: :activity})
296 297
  end

lain's avatar
lain committed
298
  def get_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
lain's avatar
lain committed
299 300
    with %Activity{} = activity <- Repo.get(Activity, id),
         true <- ActivityPub.visible_for_user?(activity, user) do
href's avatar
href committed
301 302 303
      conn
      |> put_view(StatusView)
      |> try_render("status.json", %{activity: activity, for: user})
lain's avatar
lain committed
304 305 306
    end
  end

lain's avatar
lain committed
307 308
  def get_context(%{assigns: %{user: user}} = conn, %{"id" => id}) do
    with %Activity{} = activity <- Repo.get(Activity, id),
lain's avatar
lain committed
309 310 311 312 313 314 315 316 317 318
         activities <-
           ActivityPub.fetch_activities_for_context(activity.data["context"], %{
             "blocking_user" => user,
             "user" => user
           }),
         activities <-
           activities |> Enum.filter(fn %{id: aid} -> to_string(aid) != to_string(id) end),
         activities <-
           activities |> Enum.filter(fn %{data: %{"type" => type}} -> type == "Create" end),
         grouped_activities <- Enum.group_by(activities, fn %{id: id} -> id < activity.id end) do
lain's avatar
lain committed
319
      result = %{
lain's avatar
lain committed
320 321 322 323 324 325 326 327
        ancestors:
          StatusView.render(
            "index.json",
            for: user,
            activities: grouped_activities[true] || [],
            as: :activity
          )
          |> Enum.reverse(),
Haelwenn's avatar
Haelwenn committed
328
        # credo:disable-for-previous-line Credo.Check.Refactor.PipeChainStart
lain's avatar
lain committed
329 330 331 332 333 334 335 336
        descendants:
          StatusView.render(
            "index.json",
            for: user,
            activities: grouped_activities[false] || [],
            as: :activity
          )
          |> Enum.reverse()
Haelwenn's avatar
Haelwenn committed
337
        # credo:disable-for-previous-line Credo.Check.Refactor.PipeChainStart
lain's avatar
lain committed
338 339 340 341 342 343
      }

      json(conn, result)
    end
  end

344 345 346 347 348 349 350 351 352
  def post_status(conn, %{"status" => "", "media_ids" => media_ids} = params)
      when length(media_ids) > 0 do
    params =
      params
      |> Map.put("status", ".")

    post_status(conn, params)
  end

Thog's avatar
Thog committed
353
  def post_status(%{assigns: %{user: user}} = conn, %{"status" => _} = params) do
lain's avatar
lain committed
354 355 356
    params =
      params
      |> Map.put("in_reply_to_status_id", params["in_reply_to_id"])
lain's avatar
lain committed
357

lain's avatar
lain committed
358 359 360 361 362 363 364
    idempotency_key =
      case get_req_header(conn, "idempotency-key") do
        [key] -> key
        _ -> Ecto.UUID.generate()
      end

    {:ok, activity} =
Thog's avatar
Thog committed
365
      Cachex.fetch!(:idempotency_cache, idempotency_key, fn _ -> CommonAPI.post(user, params) end)
lain's avatar
lain committed
366

href's avatar
href committed
367 368 369
    conn
    |> put_view(StatusView)
    |> try_render("status.json", %{activity: activity, for: user, as: :activity})
lain's avatar
lain committed
370
  end
lain's avatar
lain committed
371 372 373 374 375 376 377 378 379 380 381

  def delete_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
    with {:ok, %Activity{}} <- CommonAPI.delete(id, user) do
      json(conn, %{})
    else
      _e ->
        conn
        |> put_status(403)
        |> json(%{error: "Can't delete this post"})
    end
  end
lain's avatar
lain committed
382 383

  def reblog_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
384
    with {:ok, announce, _activity} <- CommonAPI.repeat(ap_id_or_id, user) do
href's avatar
href committed
385 386 387
      conn
      |> put_view(StatusView)
      |> try_render("status.json", %{activity: announce, for: user, as: :activity})
lain's avatar
lain committed
388 389
    end
  end
lain's avatar
lain committed
390

normandy's avatar
normandy committed
391
  def unreblog_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
392
    with {:ok, _unannounce, %{data: %{"id" => id}}} <- CommonAPI.unrepeat(ap_id_or_id, user),
393
         %Activity{} = activity <- Activity.get_create_by_object_ap_id(id) do
href's avatar
href committed
394 395 396
      conn
      |> put_view(StatusView)
      |> try_render("status.json", %{activity: activity, for: user, as: :activity})
normandy's avatar
normandy committed
397 398 399
    end
  end

lain's avatar
lain committed
400
  def fav_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
401
    with {:ok, _fav, %{data: %{"id" => id}}} <- CommonAPI.favorite(ap_id_or_id, user),
402
         %Activity{} = activity <- Activity.get_create_by_object_ap_id(id) do
href's avatar
href committed
403 404 405
      conn
      |> put_view(StatusView)
      |> try_render("status.json", %{activity: activity, for: user, as: :activity})
lain's avatar
lain committed
406 407 408 409
    end
  end

  def unfav_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
410
    with {:ok, _, _, %{data: %{"id" => id}}} <- CommonAPI.unfavorite(ap_id_or_id, user),
411
         %Activity{} = activity <- Activity.get_create_by_object_ap_id(id) do
href's avatar
href committed
412 413 414
      conn
      |> put_view(StatusView)
      |> try_render("status.json", %{activity: activity, for: user, as: :activity})
lain's avatar
lain committed
415 416
    end
  end
417

minibikini's avatar
minibikini committed
418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438
  def pin_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
    with {:ok, activity} <- CommonAPI.pin(ap_id_or_id, user) do
      conn
      |> put_view(StatusView)
      |> try_render("status.json", %{activity: activity, for: user, as: :activity})
    else
      {:error, reason} ->
        conn
        |> put_resp_content_type("application/json")
        |> send_resp(:bad_request, Jason.encode!(%{"error" => reason}))
    end
  end

  def unpin_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
    with {:ok, activity} <- CommonAPI.unpin(ap_id_or_id, user) do
      conn
      |> put_view(StatusView)
      |> try_render("status.json", %{activity: activity, for: user, as: :activity})
    end
  end

Haelwenn's avatar
Haelwenn committed
439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457
  def bookmark_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
    with %Activity{} = activity <- Repo.get(Activity, id),
         %User{} = user <- User.get_by_nickname(user.nickname),
         true <- ActivityPub.visible_for_user?(activity, user),
         {:ok, user} <- User.bookmark(user, activity.data["object"]["id"]) do
      conn
      |> put_view(StatusView)
      |> try_render("status.json", %{activity: activity, for: user, as: :activity})
    end
  end

  def unbookmark_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
    with %Activity{} = activity <- Repo.get(Activity, id),
         %User{} = user <- User.get_by_nickname(user.nickname),
         true <- ActivityPub.visible_for_user?(activity, user),
         {:ok, user} <- User.unbookmark(user, activity.data["object"]["id"]) do
      conn
      |> put_view(StatusView)
      |> try_render("status.json", %{activity: activity, for: user, as: :activity})
458 459 460 461
    end
  end

  def mute_conversation(%{assigns: %{user: user}} = conn, %{"id" => id}) do
462
    activity = Activity.get_by_id(id)
463

464
    with {:ok, activity} <- CommonAPI.add_mute(user, activity) do
465 466 467
      conn
      |> put_view(StatusView)
      |> try_render("status.json", %{activity: activity, for: user, as: :activity})
468 469 470 471 472
    else
      {:error, reason} ->
        conn
        |> put_resp_content_type("application/json")
        |> send_resp(:bad_request, Jason.encode!(%{"error" => reason}))
473 474 475 476
    end
  end

  def unmute_conversation(%{assigns: %{user: user}} = conn, %{"id" => id}) do
477
    activity = Activity.get_by_id(id)
478

479
    with {:ok, activity} <- CommonAPI.remove_mute(user, activity) do
480 481 482
      conn
      |> put_view(StatusView)
      |> try_render("status.json", %{activity: activity, for: user, as: :activity})
Haelwenn's avatar
Haelwenn committed
483 484 485
    end
  end

486 487
  def notifications(%{assigns: %{user: user}} = conn, params) do
    notifications = Notification.for_user(user, params)
lain's avatar
lain committed
488 489

    result =
Haelwenn's avatar
Haelwenn committed
490 491
      notifications
      |> Enum.map(fn x -> render_notification(user, x) end)
lain's avatar
lain committed
492
      |> Enum.filter(& &1)
493

lain's avatar
lain committed
494 495 496
    conn
    |> add_link_headers(:notifications, notifications)
    |> json(result)
497 498
  end

499 500 501 502 503 504 505
  def get_notification(%{assigns: %{user: user}} = conn, %{"id" => id} = _params) do
    with {:ok, notification} <- Notification.get(user, id) do
      json(conn, render_notification(user, notification))
    else
      {:error, reason} ->
        conn
        |> put_resp_content_type("application/json")
lain's avatar
lain committed
506
        |> send_resp(403, Jason.encode!(%{"error" => reason}))
507 508 509 510 511 512 513 514 515 516 517 518 519 520 521
    end
  end

  def clear_notifications(%{assigns: %{user: user}} = conn, _params) do
    Notification.clear(user)
    json(conn, %{})
  end

  def dismiss_notification(%{assigns: %{user: user}} = conn, %{"id" => id} = _params) do
    with {:ok, _notif} <- Notification.dismiss(user, id) do
      json(conn, %{})
    else
      {:error, reason} ->
        conn
        |> put_resp_content_type("application/json")
lain's avatar
lain committed
522
        |> send_resp(403, Jason.encode!(%{"error" => reason}))
523 524 525
    end
  end

Roger Braun's avatar
Roger Braun committed
526 527
  def relationships(%{assigns: %{user: user}} = conn, %{"id" => id}) do
    id = List.wrap(id)
lain's avatar
lain committed
528
    q = from(u in User, where: u.id in ^id)
Roger Braun's avatar
Roger Braun committed
529
    targets = Repo.all(q)
href's avatar
href committed
530 531 532 533

    conn
    |> put_view(AccountView)
    |> render("relationships.json", %{user: user, targets: targets})
Roger Braun's avatar
Roger Braun committed
534 535
  end

536
  # Instead of returning a 400 when no "id" params is present, Mastodon returns an empty array.
Maksim's avatar
Maksim committed
537
  def relationships(%{assigns: %{user: _user}} = conn, _), do: json(conn, [])
538

539
  def update_media(%{assigns: %{user: user}} = conn, data) do
540
    with %Object{} = object <- Repo.get(Object, data["id"]),
541
         true <- Object.authorize_mutation(object, user),
542 543 544 545
         true <- is_binary(data["description"]),
         description <- data["description"] do
      new_data = %{object.data | "name" => description}

546 547 548 549
      {:ok, _} =
        object
        |> Object.change(%{data: new_data})
        |> Repo.update()
lain's avatar
lain committed
550

551
      attachment_data = Map.put(new_data, "id", object.id)
href's avatar
href committed
552 553 554 555

      conn
      |> put_view(StatusView)
      |> render("attachment.json", %{attachment: attachment_data})
lain's avatar
lain committed
556 557 558
    end
  end

559 560
  def upload(%{assigns: %{user: user}} = conn, %{"file" => file} = data) do
    with {:ok, object} <-
kaniini's avatar
kaniini committed
561 562
           ActivityPub.upload(
             file,
563 564 565 566
             actor: User.ap_id(user),
             description: Map.get(data, "description")
           ) do
      attachment_data = Map.put(object.data, "id", object.id)
href's avatar
href committed
567 568 569 570

      conn
      |> put_view(StatusView)
      |> render("attachment.json", %{attachment: attachment_data})
571 572 573
    end
  end

574
  def favourited_by(conn, %{"id" => id}) do
Thog's avatar
Thog committed
575
    with %Activity{data: %{"object" => %{"likes" => likes}}} <- Repo.get(Activity, id) do
lain's avatar
lain committed
576
      q = from(u in User, where: u.ap_id in ^likes)
577
      users = Repo.all(q)
href's avatar
href committed
578 579 580 581

      conn
      |> put_view(AccountView)
      |> render(AccountView, "accounts.json", %{users: users, as: :user})
582 583 584 585 586 587 588
    else
      _ -> json(conn, [])
    end
  end

  def reblogged_by(conn, %{"id" => id}) do
    with %Activity{data: %{"object" => %{"announcements" => announces}}} <- Repo.get(Activity, id) do
lain's avatar
lain committed
589
      q = from(u in User, where: u.ap_id in ^announces)
590
      users = Repo.all(q)
href's avatar
href committed
591 592 593 594

      conn
      |> put_view(AccountView)
      |> render("accounts.json", %{users: users, as: :user})
595 596 597 598 599
    else
      _ -> json(conn, [])
    end
  end

Roger Braun's avatar
Roger Braun committed
600
  def hashtag_timeline(%{assigns: %{user: user}} = conn, params) do
601 602
    local_only = params["local"] in [true, "True", "true", "1"]

Haelwenn's avatar
Haelwenn committed
603
    tags =
604 605
      [params["tag"], params["any"]]
      |> List.flatten()
Haelwenn's avatar
Haelwenn committed
606 607 608 609
      |> Enum.uniq()
      |> Enum.filter(& &1)
      |> Enum.map(&String.downcase(&1))

Haelwenn's avatar
Haelwenn committed
610 611 612 613 614
    tag_all =
      params["all"] ||
        []
        |> Enum.map(&String.downcase(&1))

Haelwenn's avatar
Haelwenn committed
615 616 617 618 619
    tag_reject =
      params["none"] ||
        []
        |> Enum.map(&String.downcase(&1))

Haelwenn's avatar
Haelwenn committed
620
    activities =
lain's avatar
lain committed
621 622
      params
      |> Map.put("type", "Create")
623
      |> Map.put("local_only", local_only)
lain's avatar
lain committed
624
      |> Map.put("blocking_user", user)
625
      |> Map.put("muting_user", user)
Haelwenn's avatar
Haelwenn committed
626
      |> Map.put("tag", tags)
Haelwenn's avatar
Haelwenn committed
627
      |> Map.put("tag_all", tag_all)
Haelwenn's avatar
Haelwenn committed
628
      |> Map.put("tag_reject", tag_reject)
Haelwenn's avatar
Haelwenn committed
629
      |> ActivityPub.fetch_public_activities()
lain's avatar
lain committed
630
      |> Enum.reverse()
Roger Braun's avatar
Roger Braun committed
631 632

    conn
633
    |> add_link_headers(:hashtag_timeline, activities, params["tag"], %{"local" => local_only})
href's avatar
href committed
634 635
    |> put_view(StatusView)
    |> render("index.json", %{activities: activities, for: user, as: :activity})
Roger Braun's avatar
Roger Braun committed
636 637
  end

638
  def followers(%{assigns: %{user: for_user}} = conn, %{"id" => id}) do
639 640
    with %User{} = user <- Repo.get(User, id),
         {:ok, followers} <- User.get_followers(user) do
641 642 643
      followers =
        cond do
          for_user && user.id == for_user.id -> followers
644
          user.info.hide_followers -> []
645 646 647
          true -> followers
        end

href's avatar
href committed
648 649 650
      conn
      |> put_view(AccountView)
      |> render("accounts.json", %{users: followers, as: :user})
651 652 653
    end
  end

654
  def following(%{assigns: %{user: for_user}} = conn, %{"id" => id}) do
655 656
    with %User{} = user <- Repo.get(User, id),
         {:ok, followers} <- User.get_friends(user) do
657 658 659
      followers =
        cond do
          for_user && user.id == for_user.id -> followers
660
          user.info.hide_follows -> []
661 662 663
          true -> followers
        end

href's avatar
href committed
664 665 666
      conn
      |> put_view(AccountView)
      |> render("accounts.json", %{users: followers, as: :user})
667 668 669
    end
  end

670 671
  def follow_requests(%{assigns: %{user: followed}} = conn, _params) do
    with {:ok, follow_requests} <- User.get_follow_requests(followed) do
href's avatar
href committed
672 673 674
      conn
      |> put_view(AccountView)
      |> render("accounts.json", %{users: follow_requests, as: :user})
675 676 677
    end
  end

kaniini's avatar
kaniini committed
678 679
  def authorize_follow_request(%{assigns: %{user: followed}} = conn, %{"id" => id}) do
    with %User{} = follower <- Repo.get(User, id),
680
         {:ok, follower} <- User.maybe_follow(follower, followed),
kaniini's avatar
kaniini committed
681
         %Activity{} = follow_activity <- Utils.fetch_latest_follow(follower, followed),
kaniini's avatar
kaniini committed
682
         {:ok, follow_activity} <- Utils.update_follow_state(follow_activity, "accept"),
kaniini's avatar
kaniini committed
683 684
         {:ok, _activity} <-
           ActivityPub.accept(%{
kaniini's avatar
kaniini committed
685
             to: [follower.ap_id],
686
             actor: followed,
kaniini's avatar
kaniini committed
687 688 689
             object: follow_activity.data["id"],
             type: "Accept"
           }) do
href's avatar
href committed
690 691 692
      conn
      |> put_view(AccountView)
      |> render("relationship.json", %{user: followed, target: follower})
kaniini's avatar
kaniini committed
693 694 695 696 697 698 699 700
    else
      {:error, message} ->
        conn
        |> put_resp_content_type("application/json")
        |> send_resp(403, Jason.encode!(%{"error" => message}))
    end
  end

kaniini's avatar
kaniini committed
701 702 703 704 705 706 707
  def reject_follow_request(%{assigns: %{user: followed}} = conn, %{"id" => id}) do
    with %User{} = follower <- Repo.get(User, id),
         %Activity{} = follow_activity <- Utils.fetch_latest_follow(follower, followed),
         {:ok, follow_activity} <- Utils.update_follow_state(follow_activity, "reject"),
         {:ok, _activity} <-
           ActivityPub.reject(%{
             to: [follower.ap_id],
708
             actor: followed,
kaniini's avatar
kaniini committed
709 710 711
             object: follow_activity.data["id"],
             type: "Reject"
           }) do
href's avatar
href committed
712 713 714
      conn
      |> put_view(AccountView)
      |> render("relationship.json", %{user: followed, target: follower})
kaniini's avatar
kaniini committed
715 716 717 718 719 720 721
    else
      {:error, message} ->
        conn
        |> put_resp_content_type("application/json")
        |> send_resp(403, Jason.encode!(%{"error" => message}))
    end
  end
kaniini's avatar
kaniini committed
722

eal's avatar
eal committed
723 724
  def follow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
    with %User{} = followed <- Repo.get(User, id),
725
         {:ok, follower} <- User.maybe_direct_follow(follower, followed),
726
         {:ok, _activity} <- ActivityPub.follow(follower, followed),
727
         {:ok, follower, followed} <-
href's avatar
href committed
728
           User.wait_and_refresh(
Haelwenn's avatar
Haelwenn committed
729
             Config.get([:activitypub, :follow_handshake_timeout]),
href's avatar
href committed
730 731 732
             follower,
             followed
           ) do
href's avatar
href committed
733 734 735
      conn
      |> put_view(AccountView)
      |> render("relationship.json", %{user: follower, target: followed})
eal's avatar
eal committed
736
    else
Thog's avatar
Thog committed
737
      {:error, message} ->
eal's avatar
eal committed
738 739
        conn
        |> put_resp_content_type("application/json")
lain's avatar
lain committed
740
        |> send_resp(403, Jason.encode!(%{"error" => message}))
741 742 743
    end
  end

eal's avatar
eal committed
744
  def follow(%{assigns: %{user: follower}} = conn, %{"uri" => uri}) do
eal's avatar
eal committed
745
    with %User{} = followed <- Repo.get_by(User, nickname: uri),
746
         {:ok, follower} <- User.maybe_direct_follow(follower, followed),
Thog's avatar
Thog committed
747
         {:ok, _activity} <- ActivityPub.follow(follower, followed) do
href's avatar
href committed
748 749 750
      conn
      |> put_view(AccountView)
      |> render("account.json", %{user: followed, for: follower})
eal's avatar
eal committed
751
    else
Thog's avatar
Thog committed
752
      {:error, message} ->
eal's avatar
eal committed
753 754
        conn
        |> put_resp_content_type("application/json")
lain's avatar
lain committed
755
        |> send_resp(403, Jason.encode!(%{"error" => message}))
eal's avatar
eal committed
756 757 758
    end
  end

759 760
  def unfollow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
    with %User{} = followed <- Repo.get(User, id),
761 762
         {:ok, _activity} <- ActivityPub.unfollow(follower, followed),
         {:ok, follower, _} <- User.unfollow(follower, followed) do
href's avatar
href committed
763 764 765
      conn
      |> put_view(AccountView)
      |> render("relationship.json", %{user: follower, target: followed})
766 767 768
    end
  end

769 770 771 772 773 774 775 776 777 778 779 780 781 782
  def mute(%{assigns: %{user: muter}} = conn, %{"id" => id}) do
    with %User{} = muted <- Repo.get(User, id),
         {:ok, muter} <- User.mute(muter, muted) do
      render(conn, AccountView, "relationship.json", %{user: muter, target: muted})
    end
  end

  def unmute(%{assigns: %{user: muter}} = conn, %{"id" => id}) do
    with %User{} = muted <- Repo.get(User, id),
         {:ok, muter} <- User.unmute(muter, muted) do
      render(conn, AccountView, "relationship.json", %{user: muter, target: muted})
    end
  end

lain's avatar
lain committed
783 784
  def block(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
    with %User{} = blocked <- Repo.get(User, id),
785 786
         {:ok, blocker} <- User.block(blocker, blocked),
         {:ok, _activity} <- ActivityPub.block(blocker, blocked) do
href's avatar
href committed
787 788 789
      conn
      |> put_view(AccountView)
      |> render("relationship.json", %{user: blocker, target: blocked})
lain's avatar
lain committed
790
    else
Thog's avatar
Thog committed
791
      {:error, message} ->
lain's avatar
lain committed
792 793
        conn
        |> put_resp_content_type("application/json")
lain's avatar
lain committed
794
        |> send_resp(403, Jason.encode!(%{"error" => message}))
lain's avatar
lain committed
795 796 797 798 799
    end
  end

  def unblock(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
    with %User{} = blocked <- Repo.get(User, id),
800 801
         {:ok, blocker} <- User.unblock(blocker, blocked),
         {:ok, _activity} <- ActivityPub.unblock(blocker, blocked) do
href's avatar
href committed
802 803 804
      conn
      |> put_view(AccountView)
      |> render("relationship.json", %{user: blocker, target: blocked})
lain's avatar
lain committed
805
    else
Thog's avatar
Thog committed
806
      {:error, message} ->
lain's avatar
lain committed
807 808
        conn
        |> put_resp_content_type("application/json")
lain's avatar
lain committed
809
        |> send_resp(403, Jason.encode!(%{"error" => message}))
lain's avatar
lain committed
810 811 812
    end
  end

lain's avatar
lain committed
813
  def blocks(%{assigns: %{user: user}} = conn, _) do
814 815
    with blocked_accounts <- User.blocked_users(user) do
      res = AccountView.render("accounts.json", users: blocked_accounts, for: user, as: :user)
lain's avatar
lain committed
816 817 818 819
      json(conn, res)
    end
  end

eal's avatar
eal committed
820
  def domain_blocks(%{assigns: %{user: %{info: info}}} = conn, _) do
lain's avatar
lain committed
821
    json(conn, info.domain_blocks || [])
eal's avatar
eal committed
822 823 824 825 826 827 828 829 830 831 832 833
  end

  def block_domain(%{assigns: %{user: blocker}} = conn, %{"domain" => domain}) do
    User.block_domain(blocker, domain)
    json(conn, %{})
  end

  def unblock_domain(%{assigns: %{user: blocker}} = conn, %{"domain" => domain}) do
    User.unblock_domain(blocker, domain)
    json(conn, %{})
  end

834
  def status_search(user, query) do
835 836
    fetched =
      if Regex.match?(~r/https?:/, query) do
837
        with {:ok, object} <- ActivityPub.fetch_object_from_id(query),
838
             %Activity{} = activity <- Activity.get_create_by_object_ap_id(object.data["id"]),
839 840
             true <- ActivityPub.visible_for_user?(activity, user) do
          [activity]
841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860
        else
          _e -> []
        end
      end || []

    q =
      from(
        a in Activity,
        where: fragment("?->>'type' = 'Create'", a.data),
        where: "https://www.w3.org/ns/activitystreams#Public" in a.recipients,
        where:
          fragment(
            "to_tsvector('english', ?->'object'->>'content') @@ plainto_tsquery('english', ?)",
            a.data,
            ^query
          ),
        limit: 20,
        order_by: [desc: :id]
      )

861 862 863 864
    Repo.all(q) ++ fetched
  end

  def search2(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do
865
    accounts = User.search(query, params["resolve"] == "true", user)
866

867
    statuses = status_search(user, query)
868 869 870 871

    tags_path = Web.base_url() <> "/tag/"

    tags =
Haelwenn's avatar
Haelwenn committed
872 873
      query
      |> String.split()
874 875 876 877 878 879 880 881 882 883 884 885 886 887 888
      |> Enum.uniq()
      |> Enum.filter(fn tag -> String.starts_with?(tag, "#") end)
      |> Enum.