mastodon_api_controller.ex 39.6 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
7
  alias Pleroma.{Repo, Object, Activity, User, Notification, Stats}
lain's avatar
lain committed
8
  alias Pleroma.Web
minibikini's avatar
cleanup    
minibikini committed
9
10
11
12
13
14
15
16
17
18

  alias Pleroma.Web.MastodonAPI.{
    StatusView,
    AccountView,
    MastodonView,
    ListView,
    FilterView,
    PushSubscriptionView
  }

lain's avatar
lain committed
19
  alias Pleroma.Web.ActivityPub.ActivityPub
kaniini's avatar
kaniini committed
20
  alias Pleroma.Web.ActivityPub.Utils
lain's avatar
lain committed
21
  alias Pleroma.Web.CommonAPI
lain's avatar
lain committed
22
  alias Pleroma.Web.OAuth.{Authorization, Token, App}
23
  alias Pleroma.Web.MediaProxy
minibikini's avatar
cleanup    
minibikini committed
24

Roger Braun's avatar
Roger Braun committed
25
  import Ecto.Query
Thog's avatar
Thog committed
26
  require Logger
27

28
29
  @httpoison Application.get_env(:pleroma, :httpoison)

30
31
  action_fallback(:errors)

32
  def create_app(conn, params) do
lain's avatar
lain committed
33
34
    with cs <- App.register_changeset(%App{}, params) |> IO.inspect(),
         {:ok, app} <- Repo.insert(cs) |> IO.inspect() do
35
      res = %{
36
        id: app.id |> to_string,
37
        name: app.client_name,
38
        client_id: app.client_id,
39
        client_secret: app.client_secret,
40
        redirect_uri: app.redirect_uris,
41
        website: app.website
42
43
44
45
46
47
      }

      json(conn, res)
    end
  end

lain's avatar
lain committed
48
49
50
51
52
53
54
55
56
57
58
  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
59
      end
lain's avatar
lain committed
60
61
62
63
    else
      map
    end
  end
64

lain's avatar
lain committed
65
66
  def update_credentials(%{assigns: %{user: user}} = conn, params) do
    original_user = user
67

lain's avatar
lain committed
68
69
70
    user_params =
      %{}
      |> add_if_present(params, "display_name", :name)
Maxim Filippov's avatar
Maxim Filippov committed
71
      |> add_if_present(params, "note", :bio, fn value -> {:ok, User.parse_bio(value)} end)
lain's avatar
lain committed
72
73
74
75
      |> 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
76
        else
lain's avatar
lain committed
77
          _ -> :error
lain's avatar
lain committed
78
        end
lain's avatar
lain committed
79
      end)
lain's avatar
lain committed
80

lain's avatar
lain committed
81
82
83
84
85
86
87
    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
88
        else
lain's avatar
lain committed
89
          _ -> :error
lain's avatar
lain committed
90
        end
lain's avatar
lain committed
91
      end)
92

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

lain's avatar
lain committed
95
96
    with changeset <- User.update_changeset(user, user_params),
         changeset <- Ecto.Changeset.put_embed(changeset, :info, info_cng),
lain's avatar
lain committed
97
98
99
100
         {:ok, user} <- User.update_and_set_cache(changeset) do
      if original_user != user do
        CommonAPI.update(user)
      end
lain's avatar
lain committed
101

102
      json(conn, AccountView.render("account.json", %{user: user, for: user}))
103
104
105
106
107
108
109
110
    else
      _e ->
        conn
        |> put_status(403)
        |> json(%{error: "Invalid request"})
    end
  end

Thog's avatar
Thog committed
111
  def verify_credentials(%{assigns: %{user: user}} = conn, _) do
112
    account = AccountView.render("account.json", %{user: user, for: user})
lain's avatar
lain committed
113
114
115
    json(conn, account)
  end

116
  def user(%{assigns: %{user: for_user}} = conn, %{"id" => id}) do
117
118
    with %User{} = user <- Repo.get(User, id),
         true <- User.auth_active?(user) || user.id == for_user.id || User.superuser?(for_user) do
119
      account = AccountView.render("account.json", %{user: user, for: for_user})
Roger Braun's avatar
Roger Braun committed
120
121
      json(conn, account)
    else
lain's avatar
lain committed
122
123
124
125
      _e ->
        conn
        |> put_status(404)
        |> json(%{error: "Can't find user"})
Roger Braun's avatar
Roger Braun committed
126
127
128
    end
  end

129
  @mastodon_api_level "2.5.0"
lain's avatar
lain committed
130

lain's avatar
lain committed
131
  def masto_instance(conn, _params) do
href's avatar
href committed
132
133
    instance = Pleroma.Config.get(:instance)

lain's avatar
lain committed
134
    response = %{
lain's avatar
lain committed
135
      uri: Web.base_url(),
href's avatar
href committed
136
137
      title: Keyword.get(instance, :name),
      description: Keyword.get(instance, :description),
href's avatar
href committed
138
      version: "#{@mastodon_api_level} (compatible; #{Pleroma.Application.named_version()})",
href's avatar
href committed
139
      email: Keyword.get(instance, :email),
lain's avatar
lain committed
140
      urls: %{
lain's avatar
lain committed
141
        streaming_api: String.replace(Pleroma.Web.Endpoint.static_url(), "http", "ws")
lain's avatar
lain committed
142
      },
lain's avatar
lain committed
143
144
      stats: Stats.get_stats(),
      thumbnail: Web.base_url() <> "/instance/thumbnail.jpeg",
href's avatar
href committed
145
      max_toot_chars: Keyword.get(instance, :limit)
146
147
    }

lain's avatar
lain committed
148
    json(conn, response)
149
  end
lain's avatar
lain committed
150

151
  def peers(conn, _params) do
lain's avatar
lain committed
152
    json(conn, Stats.get_peers())
153
154
  end

155
  defp mastodonized_emoji do
href's avatar
href committed
156
    Pleroma.Emoji.get_all()
157
    |> Enum.map(fn {shortcode, relative_url} ->
lain's avatar
lain committed
158
159
      url = to_string(URI.merge(Web.base_url(), relative_url))

160
161
162
      %{
        "shortcode" => shortcode,
        "static_url" => url,
163
        "visible_in_picker" => true,
164
165
166
        "url" => url
      }
    end)
167
168
169
170
  end

  def custom_emojis(conn, _params) do
    mastodon_emoji = mastodonized_emoji()
lain's avatar
lain committed
171
    json(conn, mastodon_emoji)
172
173
  end

174
  defp add_link_headers(conn, method, activities, param \\ nil, params \\ %{}) do
175
176
    last = List.last(activities)
    first = List.first(activities)
lain's avatar
lain committed
177

178
179
180
    if last do
      min = last.id
      max = first.id
lain's avatar
lain committed
181
182
183
184

      {next_url, prev_url} =
        if param do
          {
185
186
187
188
189
190
191
192
193
194
195
196
            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
197
198
199
          }
        else
          {
200
201
202
203
204
205
206
207
208
209
            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
210
211
212
          }
        end

213
214
215
216
217
218
219
      conn
      |> put_resp_header("link", "<#{next_url}>; rel=\"next\", <#{prev_url}>; rel=\"prev\"")
    else
      conn
    end
  end

lain's avatar
lain committed
220
  def home_timeline(%{assigns: %{user: user}} = conn, params) do
lain's avatar
lain committed
221
222
223
224
225
    params =
      params
      |> Map.put("type", ["Create", "Announce"])
      |> Map.put("blocking_user", user)
      |> Map.put("user", user)
lain's avatar
lain committed
226

lain's avatar
lain committed
227
228
    activities =
      ActivityPub.fetch_activities([user.ap_id | user.following], params)
229
      |> ActivityPub.contain_timeline(user)
lain's avatar
lain committed
230
      |> Enum.reverse()
231
232

    conn
lain's avatar
lain committed
233
    |> add_link_headers(:home_timeline, activities)
href's avatar
href committed
234
235
    |> put_view(StatusView)
    |> render("index.json", %{activities: activities, for: user, as: :activity})
lain's avatar
lain committed
236
237
238
  end

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

lain's avatar
lain committed
241
242
243
    params =
      params
      |> Map.put("type", ["Create", "Announce"])
244
      |> Map.put("local_only", local_only)
lain's avatar
lain committed
245
      |> Map.put("blocking_user", user)
lain's avatar
lain committed
246

lain's avatar
lain committed
247
248
249
    activities =
      ActivityPub.fetch_public_activities(params)
      |> Enum.reverse()
lain's avatar
lain committed
250

lain's avatar
lain committed
251
    conn
252
    |> add_link_headers(:public_timeline, activities, false, %{"local" => local_only})
href's avatar
href committed
253
254
    |> put_view(StatusView)
    |> render("index.json", %{activities: activities, for: user, as: :activity})
lain's avatar
lain committed
255
256
  end

257
258
259
  def user_statuses(%{assigns: %{user: reading_user}} = conn, params) do
    with %User{} = user <- Repo.get(User, params["id"]) do
      # Since Pleroma has no "pinned" posts feature, we'll just set an empty list here
eal's avatar
eal committed
260
261
262
263
      activities =
        if params["pinned"] == "true" do
          []
        else
264
          ActivityPub.fetch_user_activities(user, reading_user, params)
eal's avatar
eal committed
265
        end
lain's avatar
lain committed
266

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

278
  def dm_timeline(%{assigns: %{user: user}} = conn, params) do
279
    query =
280
281
282
283
      ActivityPub.fetch_activities_query(
        [user.ap_id],
        Map.merge(params, %{"type" => "Create", visibility: "direct"})
      )
284

285
286
287
    activities = Repo.all(query)

    conn
288
    |> add_link_headers(:dm_timeline, activities)
href's avatar
href committed
289
290
    |> put_view(StatusView)
    |> render("index.json", %{activities: activities, for: user, as: :activity})
291
292
  end

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

lain's avatar
lain committed
302
303
  def get_context(%{assigns: %{user: user}} = conn, %{"id" => id}) do
    with %Activity{} = activity <- Repo.get(Activity, id),
lain's avatar
lain committed
304
305
306
307
308
309
310
311
312
313
         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
314
      result = %{
lain's avatar
lain committed
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
        ancestors:
          StatusView.render(
            "index.json",
            for: user,
            activities: grouped_activities[true] || [],
            as: :activity
          )
          |> Enum.reverse(),
        descendants:
          StatusView.render(
            "index.json",
            for: user,
            activities: grouped_activities[false] || [],
            as: :activity
          )
          |> Enum.reverse()
lain's avatar
lain committed
331
332
333
334
335
336
      }

      json(conn, result)
    end
  end

337
338
339
340
341
342
343
344
345
  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
346
  def post_status(%{assigns: %{user: user}} = conn, %{"status" => _} = params) do
lain's avatar
lain committed
347
348
349
350
    params =
      params
      |> Map.put("in_reply_to_status_id", params["in_reply_to_id"])
      |> Map.put("no_attachment_links", true)
lain's avatar
lain committed
351

lain's avatar
lain committed
352
353
354
355
356
357
358
    idempotency_key =
      case get_req_header(conn, "idempotency-key") do
        [key] -> key
        _ -> Ecto.UUID.generate()
      end

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

href's avatar
href committed
361
362
363
    conn
    |> put_view(StatusView)
    |> try_render("status.json", %{activity: activity, for: user, as: :activity})
lain's avatar
lain committed
364
  end
lain's avatar
lain committed
365
366
367
368
369
370
371
372
373
374
375

  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
376
377

  def reblog_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
378
    with {:ok, announce, _activity} <- CommonAPI.repeat(ap_id_or_id, user) do
href's avatar
href committed
379
380
381
      conn
      |> put_view(StatusView)
      |> try_render("status.json", %{activity: announce, for: user, as: :activity})
lain's avatar
lain committed
382
383
    end
  end
lain's avatar
lain committed
384

normandy's avatar
normandy committed
385
  def unreblog_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
386
    with {:ok, _unannounce, %{data: %{"id" => id}}} <- CommonAPI.unrepeat(ap_id_or_id, user),
387
         %Activity{} = activity <- Activity.get_create_activity_by_object_ap_id(id) do
href's avatar
href committed
388
389
390
      conn
      |> put_view(StatusView)
      |> try_render("status.json", %{activity: activity, for: user, as: :activity})
normandy's avatar
normandy committed
391
392
393
    end
  end

lain's avatar
lain committed
394
  def fav_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
395
    with {:ok, _fav, %{data: %{"id" => id}}} <- CommonAPI.favorite(ap_id_or_id, user),
lain's avatar
lain committed
396
         %Activity{} = activity <- Activity.get_create_activity_by_object_ap_id(id) do
href's avatar
href committed
397
398
399
      conn
      |> put_view(StatusView)
      |> try_render("status.json", %{activity: activity, for: user, as: :activity})
lain's avatar
lain committed
400
401
402
403
    end
  end

  def unfav_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
404
    with {:ok, _, _, %{data: %{"id" => id}}} <- CommonAPI.unfavorite(ap_id_or_id, user),
lain's avatar
lain committed
405
         %Activity{} = activity <- Activity.get_create_activity_by_object_ap_id(id) do
href's avatar
href committed
406
407
408
      conn
      |> put_view(StatusView)
      |> try_render("status.json", %{activity: activity, for: user, as: :activity})
lain's avatar
lain committed
409
410
    end
  end
411

412
413
  def notifications(%{assigns: %{user: user}} = conn, params) do
    notifications = Notification.for_user(user, params)
lain's avatar
lain committed
414
415
416
417
418
419

    result =
      Enum.map(notifications, fn x ->
        render_notification(user, x)
      end)
      |> Enum.filter(& &1)
420

lain's avatar
lain committed
421
422
423
    conn
    |> add_link_headers(:notifications, notifications)
    |> json(result)
424
425
  end

426
427
428
429
430
431
432
  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
433
        |> send_resp(403, Jason.encode!(%{"error" => reason}))
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
    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
449
        |> send_resp(403, Jason.encode!(%{"error" => reason}))
450
451
452
    end
  end

Roger Braun's avatar
Roger Braun committed
453
454
  def relationships(%{assigns: %{user: user}} = conn, %{"id" => id}) do
    id = List.wrap(id)
lain's avatar
lain committed
455
    q = from(u in User, where: u.id in ^id)
Roger Braun's avatar
Roger Braun committed
456
    targets = Repo.all(q)
href's avatar
href committed
457
458
459
460

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

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

466
  def update_media(%{assigns: %{user: user}} = conn, data) do
467
    with %Object{} = object <- Repo.get(Object, data["id"]),
468
         true <- Object.authorize_mutation(object, user),
469
470
471
472
         true <- is_binary(data["description"]),
         description <- data["description"] do
      new_data = %{object.data | "name" => description}

473
474
475
476
      {:ok, _} =
        object
        |> Object.change(%{data: new_data})
        |> Repo.update()
lain's avatar
lain committed
477

478
      attachment_data = Map.put(new_data, "id", object.id)
href's avatar
href committed
479
480
481
482

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

486
487
488
489
490
491
492
  def upload(%{assigns: %{user: user}} = conn, %{"file" => file} = data) do
    with {:ok, object} <-
           ActivityPub.upload(file,
             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
493
494
495
496

      conn
      |> put_view(StatusView)
      |> render("attachment.json", %{attachment: attachment_data})
497
498
499
    end
  end

500
  def favourited_by(conn, %{"id" => id}) do
Thog's avatar
Thog committed
501
    with %Activity{data: %{"object" => %{"likes" => likes}}} <- Repo.get(Activity, id) do
lain's avatar
lain committed
502
      q = from(u in User, where: u.ap_id in ^likes)
503
      users = Repo.all(q)
href's avatar
href committed
504
505
506
507

      conn
      |> put_view(AccountView)
      |> render(AccountView, "accounts.json", %{users: users, as: :user})
508
509
510
511
512
513
514
    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
515
      q = from(u in User, where: u.ap_id in ^announces)
516
      users = Repo.all(q)
href's avatar
href committed
517
518
519
520

      conn
      |> put_view(AccountView)
      |> render("accounts.json", %{users: users, as: :user})
521
522
523
524
525
    else
      _ -> json(conn, [])
    end
  end

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

lain's avatar
lain committed
529
530
531
    params =
      params
      |> Map.put("type", "Create")
532
      |> Map.put("local_only", local_only)
lain's avatar
lain committed
533
      |> Map.put("blocking_user", user)
feld's avatar
feld committed
534
      |> Map.put("tag", String.downcase(params["tag"]))
Roger Braun's avatar
Roger Braun committed
535

lain's avatar
lain committed
536
537
538
    activities =
      ActivityPub.fetch_public_activities(params)
      |> Enum.reverse()
Roger Braun's avatar
Roger Braun committed
539
540

    conn
541
    |> add_link_headers(:hashtag_timeline, activities, params["tag"], %{"local" => local_only})
href's avatar
href committed
542
543
    |> put_view(StatusView)
    |> render("index.json", %{activities: activities, for: user, as: :activity})
Roger Braun's avatar
Roger Braun committed
544
545
  end

546
  def followers(%{assigns: %{user: for_user}} = conn, %{"id" => id}) do
547
548
    with %User{} = user <- Repo.get(User, id),
         {:ok, followers} <- User.get_followers(user) do
549
550
551
552
553
554
555
      followers =
        cond do
          for_user && user.id == for_user.id -> followers
          user.info.hide_network -> []
          true -> followers
        end

href's avatar
href committed
556
557
558
      conn
      |> put_view(AccountView)
      |> render("accounts.json", %{users: followers, as: :user})
559
560
561
    end
  end

562
  def following(%{assigns: %{user: for_user}} = conn, %{"id" => id}) do
563
564
    with %User{} = user <- Repo.get(User, id),
         {:ok, followers} <- User.get_friends(user) do
565
566
567
568
569
570
571
      followers =
        cond do
          for_user && user.id == for_user.id -> followers
          user.info.hide_network -> []
          true -> followers
        end

href's avatar
href committed
572
573
574
      conn
      |> put_view(AccountView)
      |> render("accounts.json", %{users: followers, as: :user})
575
576
577
    end
  end

578
579
  def follow_requests(%{assigns: %{user: followed}} = conn, _params) do
    with {:ok, follow_requests} <- User.get_follow_requests(followed) do
href's avatar
href committed
580
581
582
      conn
      |> put_view(AccountView)
      |> render("accounts.json", %{users: follow_requests, as: :user})
583
584
585
    end
  end

kaniini's avatar
kaniini committed
586
587
  def authorize_follow_request(%{assigns: %{user: followed}} = conn, %{"id" => id}) do
    with %User{} = follower <- Repo.get(User, id),
588
         {:ok, follower} <- User.maybe_follow(follower, followed),
kaniini's avatar
kaniini committed
589
         %Activity{} = follow_activity <- Utils.fetch_latest_follow(follower, followed),
kaniini's avatar
kaniini committed
590
         {:ok, follow_activity} <- Utils.update_follow_state(follow_activity, "accept"),
kaniini's avatar
kaniini committed
591
592
         {:ok, _activity} <-
           ActivityPub.accept(%{
kaniini's avatar
kaniini committed
593
             to: [follower.ap_id],
kaniini's avatar
kaniini committed
594
595
596
597
             actor: followed.ap_id,
             object: follow_activity.data["id"],
             type: "Accept"
           }) do
href's avatar
href committed
598
599
600
      conn
      |> put_view(AccountView)
      |> render("relationship.json", %{user: followed, target: follower})
kaniini's avatar
kaniini committed
601
602
603
604
605
606
607
608
    else
      {:error, message} ->
        conn
        |> put_resp_content_type("application/json")
        |> send_resp(403, Jason.encode!(%{"error" => message}))
    end
  end

kaniini's avatar
kaniini committed
609
610
611
612
613
614
615
616
617
618
619
  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],
             actor: followed.ap_id,
             object: follow_activity.data["id"],
             type: "Reject"
           }) do
href's avatar
href committed
620
621
622
      conn
      |> put_view(AccountView)
      |> render("relationship.json", %{user: followed, target: follower})
kaniini's avatar
kaniini committed
623
624
625
626
627
628
629
    else
      {:error, message} ->
        conn
        |> put_resp_content_type("application/json")
        |> send_resp(403, Jason.encode!(%{"error" => message}))
    end
  end
kaniini's avatar
kaniini committed
630

eal's avatar
eal committed
631
632
  def follow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
    with %User{} = followed <- Repo.get(User, id),
633
         {:ok, follower} <- User.maybe_direct_follow(follower, followed),
634
         {:ok, _activity} <- ActivityPub.follow(follower, followed),
635
         {:ok, follower, followed} <-
href's avatar
href committed
636
637
638
639
640
           User.wait_and_refresh(
             Pleroma.Config.get([:activitypub, :follow_handshake_timeout]),
             follower,
             followed
           ) do
href's avatar
href committed
641
642
643
      conn
      |> put_view(AccountView)
      |> render("relationship.json", %{user: follower, target: followed})
eal's avatar
eal committed
644
    else
Thog's avatar
Thog committed
645
      {:error, message} ->
eal's avatar
eal committed
646
647
        conn
        |> put_resp_content_type("application/json")
lain's avatar
lain committed
648
        |> send_resp(403, Jason.encode!(%{"error" => message}))
649
650
651
    end
  end

eal's avatar
eal committed
652
  def follow(%{assigns: %{user: follower}} = conn, %{"uri" => uri}) do
eal's avatar
eal committed
653
    with %User{} = followed <- Repo.get_by(User, nickname: uri),
654
         {:ok, follower} <- User.maybe_direct_follow(follower, followed),
Thog's avatar
Thog committed
655
         {:ok, _activity} <- ActivityPub.follow(follower, followed) do
href's avatar
href committed
656
657
658
      conn
      |> put_view(AccountView)
      |> render("account.json", %{user: followed, for: follower})
eal's avatar
eal committed
659
    else
Thog's avatar
Thog committed
660
      {:error, message} ->
eal's avatar
eal committed
661
662
        conn
        |> put_resp_content_type("application/json")
lain's avatar
lain committed
663
        |> send_resp(403, Jason.encode!(%{"error" => message}))
eal's avatar
eal committed
664
665
666
    end
  end

667
668
  def unfollow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
    with %User{} = followed <- Repo.get(User, id),
669
670
         {:ok, _activity} <- ActivityPub.unfollow(follower, followed),
         {:ok, follower, _} <- User.unfollow(follower, followed) do
href's avatar
href committed
671
672
673
      conn
      |> put_view(AccountView)
      |> render("relationship.json", %{user: follower, target: followed})
674
675
676
    end
  end

lain's avatar
lain committed
677
678
  def block(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
    with %User{} = blocked <- Repo.get(User, id),
679
680
         {:ok, blocker} <- User.block(blocker, blocked),
         {:ok, _activity} <- ActivityPub.block(blocker, blocked) do
href's avatar
href committed
681
682
683
      conn
      |> put_view(AccountView)
      |> render("relationship.json", %{user: blocker, target: blocked})
lain's avatar
lain committed
684
    else
Thog's avatar
Thog committed
685
      {:error, message} ->
lain's avatar
lain committed
686
687
        conn
        |> put_resp_content_type("application/json")
lain's avatar
lain committed
688
        |> send_resp(403, Jason.encode!(%{"error" => message}))
lain's avatar
lain committed
689
690
691
692
693
    end
  end

  def unblock(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
    with %User{} = blocked <- Repo.get(User, id),
694
695
         {:ok, blocker} <- User.unblock(blocker, blocked),
         {:ok, _activity} <- ActivityPub.unblock(blocker, blocked) do
href's avatar
href committed
696
697
698
      conn
      |> put_view(AccountView)
      |> render("relationship.json", %{user: blocker, target: blocked})
lain's avatar
lain committed
699
    else
Thog's avatar
Thog committed
700
      {:error, message} ->
lain's avatar
lain committed
701
702
        conn
        |> put_resp_content_type("application/json")
lain's avatar
lain committed
703
        |> send_resp(403, Jason.encode!(%{"error" => message}))
lain's avatar
lain committed
704
705
706
    end
  end

lain's avatar
lain committed
707
  def blocks(%{assigns: %{user: user}} = conn, _) do
708
709
    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
710
711
712
713
      json(conn, res)
    end
  end

eal's avatar
eal committed
714
  def domain_blocks(%{assigns: %{user: %{info: info}}} = conn, _) do
lain's avatar
lain committed
715
    json(conn, info.domain_blocks || [])
eal's avatar
eal committed
716
717
718
719
720
721
722
723
724
725
726
727
  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

728
  def status_search(user, query) do
729
730
    fetched =
      if Regex.match?(~r/https?:/, query) do
731
732
733
734
735
        with {:ok, object} <- ActivityPub.fetch_object_from_id(query),
             %Activity{} = activity <-
               Activity.get_create_activity_by_object_ap_id(object.data["id"]),
             true <- ActivityPub.visible_for_user?(activity, user) do
          [activity]
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
        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]
      )

756
757
758
759
760
761
    Repo.all(q) ++ fetched
  end

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

762
    statuses = status_search(user, query)
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782

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

    tags =
      String.split(query)
      |> Enum.uniq()
      |> Enum.filter(fn tag -> String.starts_with?(tag, "#") end)
      |> Enum.map(fn tag -> String.slice(tag, 1..-1) end)
      |> Enum.map(fn tag -> %{name: tag, url: tags_path <> tag} end)

    res = %{
      "accounts" => AccountView.render("accounts.json", users: accounts, for: user, as: :user),
      "statuses" =>
        StatusView.render("index.json", activities: statuses, for: user, as: :activity),
      "hashtags" => tags
    }

    json(conn, res)
  end

783
  def search(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do
lain's avatar
lain committed
784
    accounts = User.search(query, params["resolve"] == "true")
lain's avatar
lain committed
785

786
    statuses = status_search(user, query)
lain's avatar
lain committed
787
788
789
790
791
792

    tags =
      String.split(query)
      |> Enum.uniq()
      |> Enum.filter(fn tag -> String.starts_with?(tag, "#") end)
      |> Enum.map(fn tag -> String.slice(tag, 1..-1) end)
lain's avatar
lain committed
793
794
795

    res = %{
      "accounts" => AccountView.render("accounts.json", users: accounts, for: user, as: :user),
lain's avatar
lain committed
796
797
      "statuses" =>
        StatusView.render("index.json", activities: statuses, for: user, as: :activity),
798
      "hashtags" => tags
lain's avatar
lain committed
799
800
801
802
803
    }

    json(conn, res)
  end

lain's avatar
lain committed
804
805
  def account_search(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do
    accounts = User.search(query, params["resolve"] == "true")
806
807
808
809
810
811

    res = AccountView.render("accounts.json", users: accounts, for: user, as: :user)

    json(conn, res)
  end

Thog's avatar
Thog committed
812
  def favourites(%{assigns: %{user: user}} = conn, _) do
lain's avatar
lain committed
813
814
815
816
817
    params =
      %{}
      |> Map.put("type", "Create")
      |> Map.put("favorited_by", user.ap_id)
      |> Map.put("blocking_user", user)
818

lain's avatar
lain committed
819
820
821
    activities =
      ActivityPub.fetch_public_activities(params)
      |> Enum.reverse()
822
823

    conn
href's avatar
href committed
824
825
    |> put_view(StatusView)
    |> render("index.json", %{activities: activities, for: user, as: :activity})
826
827
  end

eal's avatar
eal committed
828
829
830
831
832
833
834
  def get_lists(%{assigns: %{user: user}} = conn, opts) do
    lists = Pleroma.List.for_user(user, opts)
    res = ListView.render("lists.json", lists: lists)
    json(conn, res)
  end

  def get_list(%{assigns: %{user: user}} = conn, %{"id" => id}) do
eal's avatar
eal committed
835
    with %Pleroma.List{} = list <- Pleroma.List.get(id, user) do
eal's avatar
eal committed
836
837
838
839
840
841
842
      res = ListView.render("list.json", list: list)
      json(conn, res)
    else
      _e -> json(conn, "error")
    end
  end

843
844
845
846
847
848
  def account_lists(%{assigns: %{user: user}} = conn, %{"id" => account_id}) do
    lists = Pleroma.List.get_lists_account_belongs(user, account_id)
    res = ListView.render("lists.json", lists: lists)
    json(conn, res)
  end

eal's avatar
eal committed
849
  def delete_list(%{assigns: %{user: user}} = conn, %{"id" => id}) do
eal's avatar
eal committed
850
    with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
eal's avatar
eal committed
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
         {:ok, _list} <- Pleroma.List.delete(list) do
      json(conn, %{})
    else
      _e ->
        json(conn, "error")
    end
  end

  def create_list(%{assigns: %{user: user}} = conn, %{"title" => title}) do
    with {:ok, %Pleroma.List{} = list} <- Pleroma.List.create(title, user) do
      res = ListView.render("list.json", list: list)
      json(conn, res)
    end
  end

  def add_to_list(%{assigns: %{user: user}} = conn, %{"id" => id, "account_ids" => accounts}) do
    accounts
    |> Enum.each(fn account_id ->
eal's avatar
eal committed
869
      with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
eal's avatar
eal committed
870
           %User{} = followed <- Repo.get(User, account_id) do
eal's avatar
eal committed
871
        Pleroma.List.follow(list, followed)
eal's avatar
eal committed
872
873
874
875
876
877
878
879
880
      end
    end)

    json(conn, %{})
  end

  def remove_from_list(%{assigns: %{user: user}} = conn, %{"id" => id, "account_ids" => accounts}) do
    accounts
    |> Enum.each(fn account_id ->
eal's avatar
eal committed
881
      with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
eal's avatar
eal committed
882
883
884
885
886
887
888
889
890
           %User{} = followed <- Repo.get(Pleroma.User, account_id) do
        Pleroma.List.unfollow(list, followed)
      end
    end)

    json(conn, %{})
  end

  def list_accounts(%{assigns: %{user: user}} = conn, %{"id" => id}) do
eal's avatar
eal committed
891
    with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
eal's avatar
eal committed
892
         {:ok, users} = Pleroma.List.get_following(list) do
href's avatar
href committed
893
894
895
      conn
      |> put_view(AccountView)
      |> render("accounts.json", %{users: users, as: :user})
eal's avatar
eal committed
896
897
898
899
    end
  end

  def rename_list(%{assigns: %{user: user}} = conn, %{"id" => id, "title" => title}) do
eal's avatar
eal committed
900
    with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
eal's avatar
eal committed
901
902
903
904
905
906
907
908
909
910
         {:ok, list} <- Pleroma.List.rename(list, title) do
      res = ListView.render("list.json", list: list)
      json(conn, res)
    else
      _e ->
        json(conn, "error")
    end
  end

  def list_timeline(%{assigns: %{user: user}} = conn, %{"list_id" => id} = params) do
Maksim's avatar
Maksim committed
911
    with %Pleroma.List{title: _title, following: following} <- Pleroma.List.get(id, user) do
eal's avatar
eal committed
912
913
914
915
916
      params =
        params
        |> Map.put("type", "Create")
        |> Map.put("blocking_user", user)

917
918
919
920
921
922
      # we must filter the following list for the user to avoid leaking statuses the user
      # does not actually have permission to see (for more info, peruse security issue #270).
      following_to =
        following
        |> Enum.filter(fn x -> x in user.following end)

eal's avatar
eal committed
923
      activities =
924
        ActivityPub.fetch_activities_bounded(following_to, following, params)
eal's avatar
eal committed
925
926
927
        |> Enum.reverse()

      conn
href's avatar
href committed
928
929
      |> put_view(StatusView)
      |> render("index.json", %{activities: activities, for: user, as: :activity})
eal's avatar
eal committed
930
931
932
933
934
935
936
937
    else
      _e ->
        conn
        |> put_status(403)
        |> json(%{error: "Error."})
    end
  end

lain's avatar
lain committed
938
  def index(%{assigns: %{user: user}} = conn, _params) do
lain's avatar
lain committed
939
940
941
    token =
      conn
      |> get_session(:oauth_token)
lain's avatar
lain committed
942
943

    if user && token do
944
      mastodon_emoji = mastodonized_emoji()
kaniini's avatar
kaniini committed
945

href's avatar
href committed
946
947
      limit = Pleroma.Config.get([:instance, :limit])

kaniini's avatar
kaniini committed
948
949
      accounts =
        Map.put(%{}, user.id, AccountView.render("account.json", %{user: user, for: user}))
lain's avatar
lain committed
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964

      initial_state =
        %{
          meta: %{
            streaming_api_base_url:
              String.replace(Pleroma.Web.Endpoint.static_url(), "http", "ws"),
            access_token: token,
            locale: "en",
            domain: Pleroma.Web.Endpoint.host(),
            admin: "1",
            me: "#{user.id}",
            unfollow_modal: false,
            boost_modal: false,
            delete_modal: true,
            auto_play_gif: false,
Haelwenn's avatar
Haelwenn committed
965
            display_sensitive_media: false,
966
            reduce_motion: false,
href's avatar
href committed
967
            max_toot_chars: limit
lain's avatar
lain committed
968
          },
969
          rights: %{
970
971
            delete_others_notice: !!user.info.is_moderator,
            admin: !!user.info.is_admin
972
          },
lain's avatar
lain committed
973
974
          compose: %{
            me: "#{user.id}",
lain's avatar
lain committed
975
            default_privacy: user.info.default_scope,
lain's avatar
lain committed
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
            default_sensitive: false
          },
          media_attachments: %{
            accept_content_types: [
              ".jpg",
              ".jpeg",
              ".png",
              ".gif",
              ".webm",
              ".mp4",
              ".m4v",
              "image\/jpeg",
              "image\/png",
              "image\/gif",
              "video\/webm",
              "video\/mp4"
            ]
          },
lain's avatar
lain committed
994
          settings:
eal's avatar
eal committed
995
            user.info.settings ||
lain's avatar
lain committed
996
997
998
999
1000
              %{
                onboarded: true,
                home: %{
                  shows: %{
                    reblog: true,
For faster browsing, not all history is shown. View entire blame