mastodon_api_controller.ex 38.7 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
minibikini's avatar
cleanup    
minibikini committed
5
6
7
8
9
10
11
12
13
14

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

lain's avatar
lain committed
15
  alias Pleroma.Web.ActivityPub.ActivityPub
kaniini's avatar
kaniini committed
16
  alias Pleroma.Web.ActivityPub.Utils
lain's avatar
lain committed
17
  alias Pleroma.Web.CommonAPI
lain's avatar
lain committed
18
  alias Pleroma.Web.OAuth.{Authorization, Token, App}
19
  alias Pleroma.Web.MediaProxy
minibikini's avatar
cleanup    
minibikini committed
20

Roger Braun's avatar
Roger Braun committed
21
  import Ecto.Query
Thog's avatar
Thog committed
22
  require Logger
23

24
25
  @httpoison Application.get_env(:pleroma, :httpoison)

26
27
  action_fallback(:errors)

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

      json(conn, res)
    end
  end

lain's avatar
lain committed
44
45
46
47
48
49
50
51
52
53
54
  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
55
      end
lain's avatar
lain committed
56
57
58
59
    else
      map
    end
  end
60

lain's avatar
lain committed
61
62
  def update_credentials(%{assigns: %{user: user}} = conn, params) do
    original_user = user
63

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

lain's avatar
lain committed
77
78
79
80
81
82
83
    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
84
        else
lain's avatar
lain committed
85
          _ -> :error
lain's avatar
lain committed
86
        end
lain's avatar
lain committed
87
      end)
88

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

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

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

Thog's avatar
Thog committed
107
  def verify_credentials(%{assigns: %{user: user}} = conn, _) do
108
    account = AccountView.render("account.json", %{user: user, for: user})
lain's avatar
lain committed
109
110
111
    json(conn, account)
  end

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

124
  @mastodon_api_level "2.5.0"
lain's avatar
lain committed
125

lain's avatar
lain committed
126
  def masto_instance(conn, _params) do
href's avatar
href committed
127
128
    instance = Pleroma.Config.get(:instance)

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

lain's avatar
lain committed
143
    json(conn, response)
144
  end
lain's avatar
lain committed
145

146
  def peers(conn, _params) do
lain's avatar
lain committed
147
    json(conn, Stats.get_peers())
148
149
  end

150
  defp mastodonized_emoji do
href's avatar
href committed
151
    Pleroma.Emoji.get_all()
152
    |> Enum.map(fn {shortcode, relative_url} ->
lain's avatar
lain committed
153
154
      url = to_string(URI.merge(Web.base_url(), relative_url))

155
156
157
      %{
        "shortcode" => shortcode,
        "static_url" => url,
158
        "visible_in_picker" => true,
159
160
161
        "url" => url
      }
    end)
162
163
164
165
  end

  def custom_emojis(conn, _params) do
    mastodon_emoji = mastodonized_emoji()
lain's avatar
lain committed
166
    json(conn, mastodon_emoji)
167
168
  end

169
  defp add_link_headers(conn, method, activities, param \\ nil, params \\ %{}) do
170
171
    last = List.last(activities)
    first = List.first(activities)
lain's avatar
lain committed
172

173
174
175
    if last do
      min = last.id
      max = first.id
lain's avatar
lain committed
176
177
178
179

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

208
209
210
211
212
213
214
      conn
      |> put_resp_header("link", "<#{next_url}>; rel=\"next\", <#{prev_url}>; rel=\"prev\"")
    else
      conn
    end
  end

lain's avatar
lain committed
215
  def home_timeline(%{assigns: %{user: user}} = conn, params) do
lain's avatar
lain committed
216
217
218
219
220
    params =
      params
      |> Map.put("type", ["Create", "Announce"])
      |> Map.put("blocking_user", user)
      |> Map.put("user", user)
lain's avatar
lain committed
221

lain's avatar
lain committed
222
223
    activities =
      ActivityPub.fetch_activities([user.ap_id | user.following], params)
224
      |> ActivityPub.contain_timeline(user)
lain's avatar
lain committed
225
      |> Enum.reverse()
226
227

    conn
lain's avatar
lain committed
228
    |> add_link_headers(:home_timeline, activities)
lain's avatar
lain committed
229
    |> render(StatusView, "index.json", %{activities: activities, for: user, as: :activity})
lain's avatar
lain committed
230
231
232
  end

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

lain's avatar
lain committed
235
236
237
    params =
      params
      |> Map.put("type", ["Create", "Announce"])
238
      |> Map.put("local_only", local_only)
lain's avatar
lain committed
239
      |> Map.put("blocking_user", user)
lain's avatar
lain committed
240

lain's avatar
lain committed
241
242
243
    activities =
      ActivityPub.fetch_public_activities(params)
      |> Enum.reverse()
lain's avatar
lain committed
244

lain's avatar
lain committed
245
    conn
246
    |> add_link_headers(:public_timeline, activities, false, %{"local" => local_only})
lain's avatar
lain committed
247
    |> render(StatusView, "index.json", %{activities: activities, for: user, as: :activity})
lain's avatar
lain committed
248
249
  end

250
251
252
  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
253
254
255
256
      activities =
        if params["pinned"] == "true" do
          []
        else
257
          ActivityPub.fetch_user_activities(user, reading_user, params)
eal's avatar
eal committed
258
        end
lain's avatar
lain committed
259

260
261
      conn
      |> add_link_headers(:user_statuses, activities, params["id"])
262
263
264
265
266
      |> render(StatusView, "index.json", %{
        activities: activities,
        for: reading_user,
        as: :activity
      })
lain's avatar
lain committed
267
268
269
    end
  end

270
  def dm_timeline(%{assigns: %{user: user}} = conn, params) do
271
    query =
272
273
274
275
      ActivityPub.fetch_activities_query(
        [user.ap_id],
        Map.merge(params, %{"type" => "Create", visibility: "direct"})
      )
276

277
278
279
    activities = Repo.all(query)

    conn
280
    |> add_link_headers(:dm_timeline, activities)
281
282
283
    |> render(StatusView, "index.json", %{activities: activities, for: user, as: :activity})
  end

lain's avatar
lain committed
284
  def get_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
lain's avatar
lain committed
285
286
    with %Activity{} = activity <- Repo.get(Activity, id),
         true <- ActivityPub.visible_for_user?(activity, user) do
287
      try_render(conn, StatusView, "status.json", %{activity: activity, for: user})
lain's avatar
lain committed
288
289
290
    end
  end

lain's avatar
lain committed
291
292
  def get_context(%{assigns: %{user: user}} = conn, %{"id" => id}) do
    with %Activity{} = activity <- Repo.get(Activity, id),
lain's avatar
lain committed
293
294
295
296
297
298
299
300
301
302
         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
303
      result = %{
lain's avatar
lain committed
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
        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
320
321
322
323
324
325
      }

      json(conn, result)
    end
  end

326
327
328
329
330
331
332
333
334
  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
335
  def post_status(%{assigns: %{user: user}} = conn, %{"status" => _} = params) do
lain's avatar
lain committed
336
337
338
339
    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
340

lain's avatar
lain committed
341
342
343
344
345
346
347
    idempotency_key =
      case get_req_header(conn, "idempotency-key") do
        [key] -> key
        _ -> Ecto.UUID.generate()
      end

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

350
    try_render(conn, StatusView, "status.json", %{activity: activity, for: user, as: :activity})
lain's avatar
lain committed
351
  end
lain's avatar
lain committed
352
353
354
355
356
357
358
359
360
361
362

  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
363
364

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

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

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

  def unfav_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
385
    with {:ok, _, _, %{data: %{"id" => id}}} <- CommonAPI.unfavorite(ap_id_or_id, user),
lain's avatar
lain committed
386
         %Activity{} = activity <- Activity.get_create_activity_by_object_ap_id(id) do
387
      try_render(conn, StatusView, "status.json", %{activity: activity, for: user, as: :activity})
lain's avatar
lain committed
388
389
    end
  end
390

391
392
  def notifications(%{assigns: %{user: user}} = conn, params) do
    notifications = Notification.for_user(user, params)
lain's avatar
lain committed
393
394
395
396
397
398

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

lain's avatar
lain committed
400
401
402
    conn
    |> add_link_headers(:notifications, notifications)
    |> json(result)
403
404
  end

405
406
407
408
409
410
411
  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
412
        |> send_resp(403, Jason.encode!(%{"error" => reason}))
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
    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
428
        |> send_resp(403, Jason.encode!(%{"error" => reason}))
429
430
431
    end
  end

Roger Braun's avatar
Roger Braun committed
432
433
  def relationships(%{assigns: %{user: user}} = conn, %{"id" => id}) do
    id = List.wrap(id)
lain's avatar
lain committed
434
    q = from(u in User, where: u.id in ^id)
Roger Braun's avatar
Roger Braun committed
435
    targets = Repo.all(q)
lain's avatar
lain committed
436
    render(conn, AccountView, "relationships.json", %{user: user, targets: targets})
Roger Braun's avatar
Roger Braun committed
437
438
  end

439
440
441
442
443
444
  # 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

445
  def update_media(%{assigns: %{user: user}} = conn, data) do
446
    with %Object{} = object <- Repo.get(Object, data["id"]),
447
         true <- Object.authorize_mutation(object, user),
448
449
450
451
         true <- is_binary(data["description"]),
         description <- data["description"] do
      new_data = %{object.data | "name" => description}

452
453
454
455
      {:ok, _} =
        object
        |> Object.change(%{data: new_data})
        |> Repo.update()
lain's avatar
lain committed
456

457
458
      attachment_data = Map.put(new_data, "id", object.id)
      render(conn, StatusView, "attachment.json", %{attachment: attachment_data})
lain's avatar
lain committed
459
460
461
    end
  end

462
463
464
465
466
467
468
469
  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)
      render(conn, StatusView, "attachment.json", %{attachment: attachment_data})
470
471
472
    end
  end

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

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

lain's avatar
lain committed
496
497
498
    params =
      params
      |> Map.put("type", "Create")
499
      |> Map.put("local_only", local_only)
lain's avatar
lain committed
500
      |> Map.put("blocking_user", user)
feld's avatar
feld committed
501
      |> Map.put("tag", String.downcase(params["tag"]))
Roger Braun's avatar
Roger Braun committed
502

lain's avatar
lain committed
503
504
505
    activities =
      ActivityPub.fetch_public_activities(params)
      |> Enum.reverse()
Roger Braun's avatar
Roger Braun committed
506
507

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

512
  def followers(%{assigns: %{user: for_user}} = conn, %{"id" => id}) do
513
514
    with %User{} = user <- Repo.get(User, id),
         {:ok, followers} <- User.get_followers(user) do
515
516
517
518
519
520
521
      followers =
        cond do
          for_user && user.id == for_user.id -> followers
          user.info.hide_network -> []
          true -> followers
        end

lain's avatar
lain committed
522
      render(conn, AccountView, "accounts.json", %{users: followers, as: :user})
523
524
525
    end
  end

526
  def following(%{assigns: %{user: for_user}} = conn, %{"id" => id}) do
527
528
    with %User{} = user <- Repo.get(User, id),
         {:ok, followers} <- User.get_friends(user) do
529
530
531
532
533
534
535
      followers =
        cond do
          for_user && user.id == for_user.id -> followers
          user.info.hide_network -> []
          true -> followers
        end

lain's avatar
lain committed
536
      render(conn, AccountView, "accounts.json", %{users: followers, as: :user})
537
538
539
    end
  end

540
541
542
543
544
545
  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
546
547
  def authorize_follow_request(%{assigns: %{user: followed}} = conn, %{"id" => id}) do
    with %User{} = follower <- Repo.get(User, id),
548
         {:ok, follower} <- User.maybe_follow(follower, followed),
kaniini's avatar
kaniini committed
549
         %Activity{} = follow_activity <- Utils.fetch_latest_follow(follower, followed),
kaniini's avatar
kaniini committed
550
         {:ok, follow_activity} <- Utils.update_follow_state(follow_activity, "accept"),
kaniini's avatar
kaniini committed
551
552
         {:ok, _activity} <-
           ActivityPub.accept(%{
kaniini's avatar
kaniini committed
553
             to: [follower.ap_id],
kaniini's avatar
kaniini committed
554
555
556
557
558
559
560
561
562
563
564
565
566
             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
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
  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
586

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

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

619
620
  def unfollow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
    with %User{} = followed <- Repo.get(User, id),
621
622
         {:ok, _activity} <- ActivityPub.unfollow(follower, followed),
         {:ok, follower, _} <- User.unfollow(follower, followed) do
lain's avatar
lain committed
623
      render(conn, AccountView, "relationship.json", %{user: follower, target: followed})
624
625
626
    end
  end

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

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

lain's avatar
lain committed
653
654
  # TODO: Use proper query
  def blocks(%{assigns: %{user: user}} = conn, _) do
lain's avatar
lain committed
655
    with blocked_users <- user.info.blocks || [],
lain's avatar
lain committed
656
         accounts <- Enum.map(blocked_users, fn ap_id -> User.get_cached_by_ap_id(ap_id) end) do
lain's avatar
lain committed
657
658
659
660
661
      res = AccountView.render("accounts.json", users: accounts, for: user, as: :user)
      json(conn, res)
    end
  end

eal's avatar
eal committed
662
  def domain_blocks(%{assigns: %{user: %{info: info}}} = conn, _) do
lain's avatar
lain committed
663
    json(conn, info.domain_blocks || [])
eal's avatar
eal committed
664
665
666
667
668
669
670
671
672
673
674
675
  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

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

701
702
703
704
705
706
707
    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)
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727

    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

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

731
    statuses = status_search(query)
lain's avatar
lain committed
732
733
734
735
736
737

    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
738
739
740

    res = %{
      "accounts" => AccountView.render("accounts.json", users: accounts, for: user, as: :user),
lain's avatar
lain committed
741
742
      "statuses" =>
        StatusView.render("index.json", activities: statuses, for: user, as: :activity),
743
      "hashtags" => tags
lain's avatar
lain committed
744
745
746
747
748
    }

    json(conn, res)
  end

lain's avatar
lain committed
749
750
  def account_search(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do
    accounts = User.search(query, params["resolve"] == "true")
751
752
753
754
755
756

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

    json(conn, res)
  end

Thog's avatar
Thog committed
757
  def favourites(%{assigns: %{user: user}} = conn, _) do
lain's avatar
lain committed
758
759
760
761
762
    params =
      %{}
      |> Map.put("type", "Create")
      |> Map.put("favorited_by", user.ap_id)
      |> Map.put("blocking_user", user)
763

lain's avatar
lain committed
764
765
766
    activities =
      ActivityPub.fetch_public_activities(params)
      |> Enum.reverse()
767
768
769
770
771

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

eal's avatar
eal committed
772
773
774
775
776
777
778
  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
779
    with %Pleroma.List{} = list <- Pleroma.List.get(id, user) do
eal's avatar
eal committed
780
781
782
783
784
785
786
      res = ListView.render("list.json", list: list)
      json(conn, res)
    else
      _e -> json(conn, "error")
    end
  end

787
788
789
790
791
792
  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
793
  def delete_list(%{assigns: %{user: user}} = conn, %{"id" => id}) do
eal's avatar
eal committed
794
    with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
eal's avatar
eal committed
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
         {: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
813
      with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
eal's avatar
eal committed
814
           %User{} = followed <- Repo.get(User, account_id) do
eal's avatar
eal committed
815
        Pleroma.List.follow(list, followed)
eal's avatar
eal committed
816
817
818
819
820
821
822
823
824
      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
825
      with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
eal's avatar
eal committed
826
827
828
829
830
831
832
833
834
           %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
835
    with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
eal's avatar
eal committed
836
837
838
839
840
841
         {: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
842
    with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
eal's avatar
eal committed
843
844
845
846
847
848
849
850
851
852
         {: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
853
    with %Pleroma.List{title: title, following: following} <- Pleroma.List.get(id, user) do
eal's avatar
eal committed
854
855
856
857
858
      params =
        params
        |> Map.put("type", "Create")
        |> Map.put("blocking_user", user)

859
860
861
862
863
864
      # 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
865
      activities =
866
        ActivityPub.fetch_activities_bounded(following_to, following, params)
eal's avatar
eal committed
867
868
869
870
871
872
873
874
875
876
877
878
        |> 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
879
  def index(%{assigns: %{user: user}} = conn, _params) do
lain's avatar
lain committed
880
881
882
    token =
      conn
      |> get_session(:oauth_token)
lain's avatar
lain committed
883
884

    if user && token do
885
      mastodon_emoji = mastodonized_emoji()
kaniini's avatar
kaniini committed
886

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

kaniini's avatar
kaniini committed
889
890
      accounts =
        Map.put(%{}, user.id, AccountView.render("account.json", %{user: user, for: user}))
lain's avatar
lain committed
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905

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

lain's avatar
lain committed
972
973
974
975
976
977
978
979
980
      conn
      |> put_layout(false)
      |> render(MastodonView, "index.html", %{initial_state: initial_state})
    else
      conn
      |> redirect(to: "/web/login")
    end
  end

981
982
983
984
985
986
  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
987
988
    else
      e ->
989
990
991
992
993
        conn
        |> json(%{error: inspect(e)})
    end
  end

lain's avatar
lain committed
994
995
996
997
998
999
1000
  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")
For faster browsing, not all history is shown. View entire blame