mastodon_api_controller.ex 37.5 KB
Newer Older
1
2
defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
  use Pleroma.Web, :controller
3
  alias Pleroma.{Repo, Object, Activity, User, Notification, Stats}
lain's avatar
lain committed
4
  alias Pleroma.Web
Haelwenn's avatar
Haelwenn committed
5
  alias Pleroma.Web.MastodonAPI.{StatusView, AccountView, MastodonView, ListView, FilterView}
lain's avatar
lain committed
6
  alias Pleroma.Web.ActivityPub.ActivityPub
kaniini's avatar
kaniini committed
7
  alias Pleroma.Web.ActivityPub.Utils
lain's avatar
lain committed
8
  alias Pleroma.Web.CommonAPI
lain's avatar
lain committed
9
  alias Pleroma.Web.OAuth.{Authorization, Token, App}
10
  alias Pleroma.Web.MediaProxy
lain's avatar
lain committed
11
  alias Comeonin.Pbkdf2
Roger Braun's avatar
Roger Braun committed
12
  import Ecto.Query
Thog's avatar
Thog committed
13
  require Logger
14

15
16
  @httpoison Application.get_env(:pleroma, :httpoison)

17
18
  action_fallback(:errors)

19
  def create_app(conn, params) do
lain's avatar
lain committed
20
21
    with cs <- App.register_changeset(%App{}, params) |> IO.inspect(),
         {:ok, app} <- Repo.insert(cs) |> IO.inspect() do
22
      res = %{
23
        id: app.id |> to_string,
24
        name: app.client_name,
25
        client_id: app.client_id,
26
        client_secret: app.client_secret,
27
        redirect_uri: app.redirect_uris,
28
        website: app.website
29
30
31
32
33
34
      }

      json(conn, res)
    end
  end

lain's avatar
lain committed
35
36
37
38
39
40
41
42
43
44
45
  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
46
      end
lain's avatar
lain committed
47
48
49
50
    else
      map
    end
  end
51

lain's avatar
lain committed
52
53
  def update_credentials(%{assigns: %{user: user}} = conn, params) do
    original_user = user
54

lain's avatar
lain committed
55
56
57
    user_params =
      %{}
      |> add_if_present(params, "display_name", :name)
Maxim Filippov's avatar
Maxim Filippov committed
58
      |> add_if_present(params, "note", :bio, fn value -> {:ok, User.parse_bio(value)} end)
lain's avatar
lain committed
59
60
61
62
      |> 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
63
        else
lain's avatar
lain committed
64
          _ -> :error
lain's avatar
lain committed
65
        end
lain's avatar
lain committed
66
      end)
lain's avatar
lain committed
67

lain's avatar
lain committed
68
69
70
71
72
73
74
75
76
77
78
    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}
        else
          _ -> :error
        end
      end)
lain's avatar
lain committed
79

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

lain's avatar
lain committed
82
83
    with changeset <- User.update_changeset(user, user_params),
         changeset <- Ecto.Changeset.put_embed(changeset, :info, info_cng),
lain's avatar
lain committed
84
85
86
87
         {:ok, user} <- User.update_and_set_cache(changeset) do
      if original_user != user do
        CommonAPI.update(user)
      end
lain's avatar
lain committed
88

89
      json(conn, AccountView.render("account.json", %{user: user, for: user}))
90
91
92
93
94
95
96
97
    else
      _e ->
        conn
        |> put_status(403)
        |> json(%{error: "Invalid request"})
    end
  end

Thog's avatar
Thog committed
98
  def verify_credentials(%{assigns: %{user: user}} = conn, _) do
99
    account = AccountView.render("account.json", %{user: user, for: user})
lain's avatar
lain committed
100
101
102
    json(conn, account)
  end

103
  def user(%{assigns: %{user: for_user}} = conn, %{"id" => id}) do
Roger Braun's avatar
Roger Braun committed
104
    with %User{} = user <- Repo.get(User, id) do
105
      account = AccountView.render("account.json", %{user: user, for: for_user})
Roger Braun's avatar
Roger Braun committed
106
107
      json(conn, account)
    else
lain's avatar
lain committed
108
109
110
111
      _e ->
        conn
        |> put_status(404)
        |> json(%{error: "Can't find user"})
Roger Braun's avatar
Roger Braun committed
112
113
114
    end
  end

115
  @mastodon_api_level "2.5.0"
lain's avatar
lain committed
116

lain's avatar
lain committed
117
  def masto_instance(conn, _params) do
href's avatar
href committed
118
119
    instance = Pleroma.Config.get(:instance)

lain's avatar
lain committed
120
    response = %{
lain's avatar
lain committed
121
      uri: Web.base_url(),
href's avatar
href committed
122
123
      title: Keyword.get(instance, :name),
      description: Keyword.get(instance, :description),
href's avatar
href committed
124
      version: "#{@mastodon_api_level} (compatible; #{Pleroma.Application.named_version()})",
href's avatar
href committed
125
      email: Keyword.get(instance, :email),
lain's avatar
lain committed
126
      urls: %{
lain's avatar
lain committed
127
        streaming_api: String.replace(Pleroma.Web.Endpoint.static_url(), "http", "ws")
lain's avatar
lain committed
128
      },
lain's avatar
lain committed
129
130
      stats: Stats.get_stats(),
      thumbnail: Web.base_url() <> "/instance/thumbnail.jpeg",
href's avatar
href committed
131
      max_toot_chars: Keyword.get(instance, :limit)
132
133
    }

lain's avatar
lain committed
134
    json(conn, response)
135
  end
lain's avatar
lain committed
136

137
  def peers(conn, _params) do
lain's avatar
lain committed
138
    json(conn, Stats.get_peers())
139
140
  end

141
  defp mastodonized_emoji do
href's avatar
href committed
142
    Pleroma.Emoji.get_all()
143
    |> Enum.map(fn {shortcode, relative_url} ->
lain's avatar
lain committed
144
145
      url = to_string(URI.merge(Web.base_url(), relative_url))

146
147
148
      %{
        "shortcode" => shortcode,
        "static_url" => url,
149
        "visible_in_picker" => true,
150
151
152
        "url" => url
      }
    end)
153
154
155
156
  end

  def custom_emojis(conn, _params) do
    mastodon_emoji = mastodonized_emoji()
lain's avatar
lain committed
157
    json(conn, mastodon_emoji)
158
159
  end

160
  defp add_link_headers(conn, method, activities, param \\ nil, params \\ %{}) do
161
162
    last = List.last(activities)
    first = List.first(activities)
lain's avatar
lain committed
163

164
165
166
    if last do
      min = last.id
      max = first.id
lain's avatar
lain committed
167
168
169
170

      {next_url, prev_url} =
        if param do
          {
171
172
173
174
175
176
177
178
179
180
181
182
            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
183
184
185
          }
        else
          {
186
187
188
189
190
191
192
193
194
195
            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
196
197
198
          }
        end

199
200
201
202
203
204
205
      conn
      |> put_resp_header("link", "<#{next_url}>; rel=\"next\", <#{prev_url}>; rel=\"prev\"")
    else
      conn
    end
  end

lain's avatar
lain committed
206
  def home_timeline(%{assigns: %{user: user}} = conn, params) do
lain's avatar
lain committed
207
208
209
210
211
    params =
      params
      |> Map.put("type", ["Create", "Announce"])
      |> Map.put("blocking_user", user)
      |> Map.put("user", user)
lain's avatar
lain committed
212

lain's avatar
lain committed
213
214
    activities =
      ActivityPub.fetch_activities([user.ap_id | user.following], params)
215
      |> ActivityPub.contain_timeline(user)
lain's avatar
lain committed
216
      |> Enum.reverse()
217
218

    conn
lain's avatar
lain committed
219
    |> add_link_headers(:home_timeline, activities)
lain's avatar
lain committed
220
    |> render(StatusView, "index.json", %{activities: activities, for: user, as: :activity})
lain's avatar
lain committed
221
222
223
  end

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

lain's avatar
lain committed
226
227
228
    params =
      params
      |> Map.put("type", ["Create", "Announce"])
229
      |> Map.put("local_only", local_only)
lain's avatar
lain committed
230
      |> Map.put("blocking_user", user)
lain's avatar
lain committed
231

lain's avatar
lain committed
232
233
234
    activities =
      ActivityPub.fetch_public_activities(params)
      |> Enum.reverse()
lain's avatar
lain committed
235

lain's avatar
lain committed
236
    conn
237
    |> add_link_headers(:public_timeline, activities, false, %{"local" => local_only})
lain's avatar
lain committed
238
    |> render(StatusView, "index.json", %{activities: activities, for: user, as: :activity})
lain's avatar
lain committed
239
240
  end

241
242
243
  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
244
245
246
247
      activities =
        if params["pinned"] == "true" do
          []
        else
248
          ActivityPub.fetch_user_activities(user, reading_user, params)
eal's avatar
eal committed
249
        end
lain's avatar
lain committed
250

251
252
      conn
      |> add_link_headers(:user_statuses, activities, params["id"])
253
254
255
256
257
      |> render(StatusView, "index.json", %{
        activities: activities,
        for: reading_user,
        as: :activity
      })
lain's avatar
lain committed
258
259
260
    end
  end

261
  def dm_timeline(%{assigns: %{user: user}} = conn, params) do
262
    query =
263
264
265
266
      ActivityPub.fetch_activities_query(
        [user.ap_id],
        Map.merge(params, %{"type" => "Create", visibility: "direct"})
      )
267

268
269
270
    activities = Repo.all(query)

    conn
271
    |> add_link_headers(:dm_timeline, activities)
272
273
274
    |> render(StatusView, "index.json", %{activities: activities, for: user, as: :activity})
  end

lain's avatar
lain committed
275
  def get_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
lain's avatar
lain committed
276
277
    with %Activity{} = activity <- Repo.get(Activity, id),
         true <- ActivityPub.visible_for_user?(activity, user) do
278
      try_render(conn, StatusView, "status.json", %{activity: activity, for: user})
lain's avatar
lain committed
279
280
281
    end
  end

lain's avatar
lain committed
282
283
  def get_context(%{assigns: %{user: user}} = conn, %{"id" => id}) do
    with %Activity{} = activity <- Repo.get(Activity, id),
lain's avatar
lain committed
284
285
286
287
288
289
290
291
292
293
         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
294
      result = %{
lain's avatar
lain committed
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
        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
311
312
313
314
315
316
      }

      json(conn, result)
    end
  end

317
318
319
320
321
322
323
324
325
  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
326
  def post_status(%{assigns: %{user: user}} = conn, %{"status" => _} = params) do
lain's avatar
lain committed
327
328
329
330
    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
331

lain's avatar
lain committed
332
333
334
335
336
337
338
    idempotency_key =
      case get_req_header(conn, "idempotency-key") do
        [key] -> key
        _ -> Ecto.UUID.generate()
      end

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

341
    try_render(conn, StatusView, "status.json", %{activity: activity, for: user, as: :activity})
lain's avatar
lain committed
342
  end
lain's avatar
lain committed
343
344
345
346
347
348
349
350
351
352
353

  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
354
355

  def reblog_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
356
    with {:ok, announce, _activity} <- CommonAPI.repeat(ap_id_or_id, user) do
357
      try_render(conn, StatusView, "status.json", %{activity: announce, for: user, as: :activity})
lain's avatar
lain committed
358
359
    end
  end
lain's avatar
lain committed
360

normandy's avatar
normandy committed
361
  def unreblog_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
362
    with {:ok, _unannounce, %{data: %{"id" => id}}} <- CommonAPI.unrepeat(ap_id_or_id, user),
363
         %Activity{} = activity <- Activity.get_create_activity_by_object_ap_id(id) do
364
      try_render(conn, StatusView, "status.json", %{activity: activity, for: user, as: :activity})
normandy's avatar
normandy committed
365
366
367
    end
  end

lain's avatar
lain committed
368
  def fav_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
369
    with {:ok, _fav, %{data: %{"id" => id}}} <- CommonAPI.favorite(ap_id_or_id, user),
lain's avatar
lain committed
370
         %Activity{} = activity <- Activity.get_create_activity_by_object_ap_id(id) do
371
      try_render(conn, StatusView, "status.json", %{activity: activity, for: user, as: :activity})
lain's avatar
lain committed
372
373
374
375
    end
  end

  def unfav_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
376
    with {:ok, _, _, %{data: %{"id" => id}}} <- CommonAPI.unfavorite(ap_id_or_id, user),
lain's avatar
lain committed
377
         %Activity{} = activity <- Activity.get_create_activity_by_object_ap_id(id) do
378
      try_render(conn, StatusView, "status.json", %{activity: activity, for: user, as: :activity})
lain's avatar
lain committed
379
380
    end
  end
381

382
383
  def notifications(%{assigns: %{user: user}} = conn, params) do
    notifications = Notification.for_user(user, params)
lain's avatar
lain committed
384
385
386
387
388
389

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

lain's avatar
lain committed
391
392
393
    conn
    |> add_link_headers(:notifications, notifications)
    |> json(result)
394
395
  end

396
397
398
399
400
401
402
  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
403
        |> send_resp(403, Jason.encode!(%{"error" => reason}))
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
    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
419
        |> send_resp(403, Jason.encode!(%{"error" => reason}))
420
421
422
    end
  end

Roger Braun's avatar
Roger Braun committed
423
424
  def relationships(%{assigns: %{user: user}} = conn, %{"id" => id}) do
    id = List.wrap(id)
lain's avatar
lain committed
425
    q = from(u in User, where: u.id in ^id)
Roger Braun's avatar
Roger Braun committed
426
    targets = Repo.all(q)
lain's avatar
lain committed
427
    render(conn, AccountView, "relationships.json", %{user: user, targets: targets})
Roger Braun's avatar
Roger Braun committed
428
429
  end

430
431
432
433
434
435
  # Instead of returning a 400 when no "id" params is present, Mastodon returns an empty array.
  def relationships(%{assigns: %{user: user}} = conn, _) do
    conn
    |> json([])
  end

436
437
438
439
440
441
442
  def update_media(%{assigns: %{user: _}} = conn, data) do
    with %Object{} = object <- Repo.get(Object, data["id"]),
         true <- is_binary(data["description"]),
         description <- data["description"] do
      new_data = %{object.data | "name" => description}

      change = Object.change(object, %{data: new_data})
443
      {:ok, _} = Repo.update(change)
444

lain's avatar
lain committed
445
      data =
446
        new_data
lain's avatar
lain committed
447
        |> Map.put("id", object.id)
lain's avatar
lain committed
448

lain's avatar
lain committed
449
      render(conn, StatusView, "attachment.json", %{attachment: data})
lain's avatar
lain committed
450
451
452
    end
  end

453
  def upload(%{assigns: %{user: _}} = conn, %{"file" => file} = data) do
href's avatar
href committed
454
455
    with {:ok, object} <- ActivityPub.upload(file, description: Map.get(data, "description")) do
      change = Object.change(object, %{data: object.data})
456
457
458
      {:ok, object} = Repo.update(change)

      objdata =
href's avatar
href committed
459
        object.data
460
461
462
463
464
465
        |> Map.put("id", object.id)

      render(conn, StatusView, "attachment.json", %{attachment: objdata})
    end
  end

466
  def favourited_by(conn, %{"id" => id}) do
Thog's avatar
Thog committed
467
    with %Activity{data: %{"object" => %{"likes" => likes}}} <- Repo.get(Activity, id) do
lain's avatar
lain committed
468
      q = from(u in User, where: u.ap_id in ^likes)
469
      users = Repo.all(q)
lain's avatar
lain committed
470
      render(conn, AccountView, "accounts.json", %{users: users, as: :user})
471
472
473
474
475
476
477
    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
478
      q = from(u in User, where: u.ap_id in ^announces)
479
      users = Repo.all(q)
lain's avatar
lain committed
480
      render(conn, AccountView, "accounts.json", %{users: users, as: :user})
481
482
483
484
485
    else
      _ -> json(conn, [])
    end
  end

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

lain's avatar
lain committed
489
490
491
    params =
      params
      |> Map.put("type", "Create")
492
      |> Map.put("local_only", local_only)
lain's avatar
lain committed
493
      |> Map.put("blocking_user", user)
feld's avatar
feld committed
494
      |> Map.put("tag", String.downcase(params["tag"]))
Roger Braun's avatar
Roger Braun committed
495

lain's avatar
lain committed
496
497
498
    activities =
      ActivityPub.fetch_public_activities(params)
      |> Enum.reverse()
Roger Braun's avatar
Roger Braun committed
499
500

    conn
501
    |> add_link_headers(:hashtag_timeline, activities, params["tag"], %{"local" => local_only})
Roger Braun's avatar
Roger Braun committed
502
503
504
    |> render(StatusView, "index.json", %{activities: activities, for: user, as: :activity})
  end

505
  def followers(%{assigns: %{user: for_user}} = conn, %{"id" => id}) do
506
507
    with %User{} = user <- Repo.get(User, id),
         {:ok, followers} <- User.get_followers(user) do
508
509
510
511
512
513
514
      followers =
        cond do
          for_user && user.id == for_user.id -> followers
          user.info.hide_network -> []
          true -> followers
        end

lain's avatar
lain committed
515
      render(conn, AccountView, "accounts.json", %{users: followers, as: :user})
516
517
518
    end
  end

519
  def following(%{assigns: %{user: for_user}} = conn, %{"id" => id}) do
520
521
    with %User{} = user <- Repo.get(User, id),
         {:ok, followers} <- User.get_friends(user) do
522
523
524
525
526
527
528
      followers =
        cond do
          for_user && user.id == for_user.id -> followers
          user.info.hide_network -> []
          true -> followers
        end

lain's avatar
lain committed
529
      render(conn, AccountView, "accounts.json", %{users: followers, as: :user})
530
531
532
    end
  end

533
534
535
536
537
538
  def follow_requests(%{assigns: %{user: followed}} = conn, _params) do
    with {:ok, follow_requests} <- User.get_follow_requests(followed) do
      render(conn, AccountView, "accounts.json", %{users: follow_requests, as: :user})
    end
  end

kaniini's avatar
kaniini committed
539
540
  def authorize_follow_request(%{assigns: %{user: followed}} = conn, %{"id" => id}) do
    with %User{} = follower <- Repo.get(User, id),
541
         {:ok, follower} <- User.maybe_follow(follower, followed),
kaniini's avatar
kaniini committed
542
         %Activity{} = follow_activity <- Utils.fetch_latest_follow(follower, followed),
kaniini's avatar
kaniini committed
543
         {:ok, follow_activity} <- Utils.update_follow_state(follow_activity, "accept"),
kaniini's avatar
kaniini committed
544
545
         {:ok, _activity} <-
           ActivityPub.accept(%{
kaniini's avatar
kaniini committed
546
             to: [follower.ap_id],
kaniini's avatar
kaniini committed
547
548
549
550
551
552
553
554
555
556
557
558
559
             actor: followed.ap_id,
             object: follow_activity.data["id"],
             type: "Accept"
           }) do
      render(conn, AccountView, "relationship.json", %{user: followed, target: follower})
    else
      {:error, message} ->
        conn
        |> put_resp_content_type("application/json")
        |> send_resp(403, Jason.encode!(%{"error" => message}))
    end
  end

kaniini's avatar
kaniini committed
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
  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
      render(conn, AccountView, "relationship.json", %{user: followed, target: follower})
    else
      {:error, message} ->
        conn
        |> put_resp_content_type("application/json")
        |> send_resp(403, Jason.encode!(%{"error" => message}))
    end
  end
kaniini's avatar
kaniini committed
579

eal's avatar
eal committed
580
581
  def follow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
    with %User{} = followed <- Repo.get(User, id),
582
         {:ok, follower} <- User.maybe_direct_follow(follower, followed),
583
         {:ok, _activity} <- ActivityPub.follow(follower, followed),
584
         {:ok, follower, followed} <-
href's avatar
href committed
585
586
587
588
589
           User.wait_and_refresh(
             Pleroma.Config.get([:activitypub, :follow_handshake_timeout]),
             follower,
             followed
           ) do
lain's avatar
lain committed
590
      render(conn, AccountView, "relationship.json", %{user: follower, target: followed})
eal's avatar
eal committed
591
    else
Thog's avatar
Thog committed
592
      {:error, message} ->
eal's avatar
eal committed
593
594
        conn
        |> put_resp_content_type("application/json")
lain's avatar
lain committed
595
        |> send_resp(403, Jason.encode!(%{"error" => message}))
596
597
598
    end
  end

eal's avatar
eal committed
599
  def follow(%{assigns: %{user: follower}} = conn, %{"uri" => uri}) do
eal's avatar
eal committed
600
    with %User{} = followed <- Repo.get_by(User, nickname: uri),
601
         {:ok, follower} <- User.maybe_direct_follow(follower, followed),
Thog's avatar
Thog committed
602
         {:ok, _activity} <- ActivityPub.follow(follower, followed) do
603
      render(conn, AccountView, "account.json", %{user: followed, for: follower})
eal's avatar
eal committed
604
    else
Thog's avatar
Thog committed
605
      {:error, message} ->
eal's avatar
eal committed
606
607
        conn
        |> put_resp_content_type("application/json")
lain's avatar
lain committed
608
        |> send_resp(403, Jason.encode!(%{"error" => message}))
eal's avatar
eal committed
609
610
611
    end
  end

612
613
  def unfollow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
    with %User{} = followed <- Repo.get(User, id),
614
615
         {:ok, _activity} <- ActivityPub.unfollow(follower, followed),
         {:ok, follower, _} <- User.unfollow(follower, followed) do
lain's avatar
lain committed
616
      render(conn, AccountView, "relationship.json", %{user: follower, target: followed})
617
618
619
    end
  end

lain's avatar
lain committed
620
621
  def block(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
    with %User{} = blocked <- Repo.get(User, id),
622
623
         {:ok, blocker} <- User.block(blocker, blocked),
         {:ok, _activity} <- ActivityPub.block(blocker, blocked) do
lain's avatar
lain committed
624
      render(conn, AccountView, "relationship.json", %{user: blocker, target: blocked})
lain's avatar
lain committed
625
    else
Thog's avatar
Thog committed
626
      {:error, message} ->
lain's avatar
lain committed
627
628
        conn
        |> put_resp_content_type("application/json")
lain's avatar
lain committed
629
        |> send_resp(403, Jason.encode!(%{"error" => message}))
lain's avatar
lain committed
630
631
632
633
634
    end
  end

  def unblock(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
    with %User{} = blocked <- Repo.get(User, id),
635
636
         {:ok, blocker} <- User.unblock(blocker, blocked),
         {:ok, _activity} <- ActivityPub.unblock(blocker, blocked) do
lain's avatar
lain committed
637
      render(conn, AccountView, "relationship.json", %{user: blocker, target: blocked})
lain's avatar
lain committed
638
    else
Thog's avatar
Thog committed
639
      {:error, message} ->
lain's avatar
lain committed
640
641
        conn
        |> put_resp_content_type("application/json")
lain's avatar
lain committed
642
        |> send_resp(403, Jason.encode!(%{"error" => message}))
lain's avatar
lain committed
643
644
645
    end
  end

lain's avatar
lain committed
646
647
  # TODO: Use proper query
  def blocks(%{assigns: %{user: user}} = conn, _) do
lain's avatar
lain committed
648
    with blocked_users <- user.info.blocks || [],
lain's avatar
lain committed
649
         accounts <- Enum.map(blocked_users, fn ap_id -> User.get_cached_by_ap_id(ap_id) end) do
lain's avatar
lain committed
650
651
652
653
654
      res = AccountView.render("accounts.json", users: accounts, for: user, as: :user)
      json(conn, res)
    end
  end

eal's avatar
eal committed
655
  def domain_blocks(%{assigns: %{user: %{info: info}}} = conn, _) do
lain's avatar
lain committed
656
    json(conn, info.domain_blocks || [])
eal's avatar
eal committed
657
658
659
660
661
662
663
664
665
666
667
668
  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

669
  def status_search(query) do
670
671
    fetched =
      if Regex.match?(~r/https?:/, query) do
lain's avatar
lain committed
672
673
        with {:ok, object} <- ActivityPub.fetch_object_from_id(query) do
          [Activity.get_create_activity_by_object_ap_id(object.data["id"])]
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
        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]
      )

694
695
696
697
698
699
700
    Repo.all(q) ++ fetched
  end

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

    statuses = status_search(query)
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720

    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

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

724
    statuses = status_search(query)
lain's avatar
lain committed
725
726
727
728
729
730

    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
731
732
733

    res = %{
      "accounts" => AccountView.render("accounts.json", users: accounts, for: user, as: :user),
lain's avatar
lain committed
734
735
      "statuses" =>
        StatusView.render("index.json", activities: statuses, for: user, as: :activity),
736
      "hashtags" => tags
lain's avatar
lain committed
737
738
739
740
741
    }

    json(conn, res)
  end

lain's avatar
lain committed
742
743
  def account_search(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do
    accounts = User.search(query, params["resolve"] == "true")
744
745
746
747
748
749

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

    json(conn, res)
  end

Thog's avatar
Thog committed
750
  def favourites(%{assigns: %{user: user}} = conn, _) do
lain's avatar
lain committed
751
752
753
754
755
    params =
      %{}
      |> Map.put("type", "Create")
      |> Map.put("favorited_by", user.ap_id)
      |> Map.put("blocking_user", user)
756

lain's avatar
lain committed
757
758
759
    activities =
      ActivityPub.fetch_public_activities(params)
      |> Enum.reverse()
760
761
762
763
764

    conn
    |> render(StatusView, "index.json", %{activities: activities, for: user, as: :activity})
  end

eal's avatar
eal committed
765
766
767
768
769
770
771
  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
772
    with %Pleroma.List{} = list <- Pleroma.List.get(id, user) do
eal's avatar
eal committed
773
774
775
776
777
778
779
      res = ListView.render("list.json", list: list)
      json(conn, res)
    else
      _e -> json(conn, "error")
    end
  end

780
781
782
783
784
785
  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
786
  def delete_list(%{assigns: %{user: user}} = conn, %{"id" => id}) do
eal's avatar
eal committed
787
    with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
eal's avatar
eal committed
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
         {: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
806
      with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
eal's avatar
eal committed
807
           %User{} = followed <- Repo.get(User, account_id) do
eal's avatar
eal committed
808
        Pleroma.List.follow(list, followed)
eal's avatar
eal committed
809
810
811
812
813
814
815
816
817
      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
818
      with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
eal's avatar
eal committed
819
820
821
822
823
824
825
826
827
           %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
828
    with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
eal's avatar
eal committed
829
830
831
832
833
834
         {:ok, users} = Pleroma.List.get_following(list) do
      render(conn, AccountView, "accounts.json", %{users: users, as: :user})
    end
  end

  def rename_list(%{assigns: %{user: user}} = conn, %{"id" => id, "title" => title}) do
eal's avatar
eal committed
835
    with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
eal's avatar
eal committed
836
837
838
839
840
841
842
843
844
845
         {: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
eal's avatar
eal committed
846
    with %Pleroma.List{title: title, following: following} <- Pleroma.List.get(id, user) do
eal's avatar
eal committed
847
848
849
850
851
      params =
        params
        |> Map.put("type", "Create")
        |> Map.put("blocking_user", user)

852
853
854
855
856
857
      # 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
858
      activities =
859
        ActivityPub.fetch_activities_bounded(following_to, following, params)
eal's avatar
eal committed
860
861
862
863
864
865
866
867
868
869
870
871
        |> Enum.reverse()

      conn
      |> render(StatusView, "index.json", %{activities: activities, for: user, as: :activity})
    else
      _e ->
        conn
        |> put_status(403)
        |> json(%{error: "Error."})
    end
  end

lain's avatar
lain committed
872
  def index(%{assigns: %{user: user}} = conn, _params) do
lain's avatar
lain committed
873
874
875
    token =
      conn
      |> get_session(:oauth_token)
lain's avatar
lain committed
876
877

    if user && token do
878
      mastodon_emoji = mastodonized_emoji()
kaniini's avatar
kaniini committed
879

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

kaniini's avatar
kaniini committed
882
883
      accounts =
        Map.put(%{}, user.id, AccountView.render("account.json", %{user: user, for: user}))
lain's avatar
lain committed
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898

      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
899
            display_sensitive_media: false,
900
            reduce_motion: false,
href's avatar
href committed
901
            max_toot_chars: limit
lain's avatar
lain committed
902
          },
903
          rights: %{
lain's avatar
lain committed
904
            delete_others_notice: !!user.info.is_moderator
905
          },
lain's avatar
lain committed
906
907
          compose: %{
            me: "#{user.id}",
lain's avatar
lain committed
908
            default_privacy: user.info.default_scope,
lain's avatar
lain committed
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
            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
927
          settings:
lain's avatar
lain committed
928
            Map.get(user.info, :settings) ||
lain's avatar
lain committed
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
              %{
                onboarded: true,
                home: %{
                  shows: %{
                    reblog: true,
                    reply: true
                  }
                },
                notifications: %{
                  alerts: %{
                    follow: true,
                    favourite: true,
                    reblog: true,
                    mention: true
                  },
                  shows: %{
                    follow: true,
                    favourite: true,
                    reblog: true,
                    mention: true
                  },
                  sounds: %{
                    follow: true,
                    favourite: true,
                    reblog: true,
                    mention: true
                  }
                }
lain's avatar
lain committed
957
958
959
960
              },
          push_subscription: nil,
          accounts: accounts,
          custom_emojis: mastodon_emoji,
href's avatar
href committed
961
          char_limit: limit
lain's avatar
lain committed
962
963
964
        }
        |> Jason.encode!()

lain's avatar
lain committed
965
966
967
968
969
970
971
972
973
      conn
      |> put_layout(false)
      |> render(MastodonView, "index.html", %{initial_state: initial_state})
    else
      conn
      |> redirect(to: "/web/login")
    end
  end

974
975
976
977
978
979
  def put_settings(%{assigns: %{user: user}} = conn, %{"data" => settings} = _params) do
    with new_info <- Map.put(user.info, "settings", settings),
         change <- User.info_changeset(user, %{info: new_info}),
         {:ok, _user} <- User.update_and_set_cache(change) do
      conn
      |> json(%{})
lain's avatar
lain committed
980
981
    else
      e ->
982
983
984
985
986
        conn
        |> json(%{error: inspect(e)})
    end
  end

lain's avatar
lain committed
987
988
989
990
991
992
993
994
995
996
  def login(conn, %{"code" => code}) do
    with {:ok, app} <- get_or_make_app(),
         %Authorization{} = auth <- Repo.get_by(Authorization, token: code, app_id: app.id),
         {:ok, token} <- Token.exchange_token(app, auth) do
      conn
      |> put_session(:oauth_token, token.token)
      |> redirect(to: "/web/getting-started")
    end
  end

Thog's avatar
Thog committed
997
  def login(conn, _) do
lain's avatar
lain committed
998
999
1000
    with {:ok, app} <- get_or_make_app() do
      path =
        o_auth_path(conn, :authorize,
For faster browsing, not all history is shown. View entire blame