twitter_api_controller.ex 17.2 KB
Newer Older
1
2
defmodule Pleroma.Web.TwitterAPI.Controller do
  use Pleroma.Web, :controller
3
  alias Pleroma.Formatter
4
  alias Pleroma.Web.TwitterAPI.{TwitterAPI, UserView, ActivityView, NotificationView}
lain's avatar
lain committed
5
  alias Pleroma.Web.CommonAPI
6
  alias Pleroma.Web.CommonAPI.Utils, as: CommonUtils
7
  alias Pleroma.{Repo, Activity, User, Notification}
lain's avatar
lain committed
8
  alias Pleroma.Web.ActivityPub.ActivityPub
9
  alias Pleroma.Web.ActivityPub.Utils
10
  alias Ecto.Changeset
11

lain's avatar
lain committed
12
13
  require Logger

href's avatar
href committed
14
  plug(:only_if_public_instance when action in [:public_timeline, :public_and_external_timeline])
15
16
  action_fallback(:errors)

17
  def verify_credentials(%{assigns: %{user: user}} = conn, _params) do
lain's avatar
lain committed
18
19
    token = Phoenix.Token.sign(conn, "user socket", user.id)
    render(conn, UserView, "show.json", %{user: user, token: token})
20
21
  end

Thog's avatar
Thog committed
22
  def status_update(%{assigns: %{user: user}} = conn, %{"status" => _} = status_data) do
23
    with media_ids <- extract_media_ids(status_data),
lain's avatar
lain committed
24
25
         {:ok, activity} <-
           TwitterAPI.create_status(user, Map.put(status_data, "media_ids", media_ids)) do
dtluna's avatar
dtluna committed
26
      conn
lain's avatar
lain committed
27
      |> json(ActivityView.render("activity.json", activity: activity, for: user))
dtluna's avatar
dtluna committed
28
    else
29
      _ -> empty_status_reply(conn)
dtluna's avatar
dtluna committed
30
    end
lain's avatar
lain committed
31
32
  end

dtluna's avatar
dtluna committed
33
34
35
36
37
38
39
40
  def status_update(conn, _status_data) do
    empty_status_reply(conn)
  end

  defp empty_status_reply(conn) do
    bad_request_reply(conn, "Client must provide a 'status' parameter with a value.")
  end

lain's avatar
lain committed
41
42
43
  defp extract_media_ids(status_data) do
    with media_ids when not is_nil(media_ids) <- status_data["media_ids"],
         split_ids <- String.split(media_ids, ","),
lain's avatar
lain committed
44
45
46
47
         clean_ids <- Enum.reject(split_ids, fn id -> String.length(id) == 0 end) do
      clean_ids
    else
      _e -> []
lain's avatar
lain committed
48
49
50
    end
  end

lain's avatar
lain committed
51
  def public_and_external_timeline(%{assigns: %{user: user}} = conn, params) do
lain's avatar
lain committed
52
53
    params =
      params
54
      |> Map.put("type", ["Create", "Announce"])
lain's avatar
lain committed
55
56
57
      |> Map.put("blocking_user", user)

    activities = ActivityPub.fetch_public_activities(params)
lain's avatar
lain committed
58
59

    conn
lain's avatar
lain committed
60
    |> render(ActivityView, "index.json", %{activities: activities, for: user})
lain's avatar
lain committed
61
62
  end

63
  def public_timeline(%{assigns: %{user: user}} = conn, params) do
lain's avatar
lain committed
64
65
    params =
      params
66
      |> Map.put("type", ["Create", "Announce"])
lain's avatar
lain committed
67
68
69
70
      |> Map.put("local_only", true)
      |> Map.put("blocking_user", user)

    activities = ActivityPub.fetch_public_activities(params)
lain's avatar
lain committed
71
72

    conn
lain's avatar
lain committed
73
    |> render(ActivityView, "index.json", %{activities: activities, for: user})
lain's avatar
lain committed
74
75
  end

lain's avatar
lain committed
76
  def friends_timeline(%{assigns: %{user: user}} = conn, params) do
lain's avatar
lain committed
77
78
79
80
81
82
    params =
      params
      |> Map.put("type", ["Create", "Announce", "Follow", "Like"])
      |> Map.put("blocking_user", user)
      |> Map.put("user", user)

83
84
85
    activities =
      ActivityPub.fetch_activities([user.ap_id | user.following], params)
      |> ActivityPub.contain_timeline(user)
lain's avatar
lain committed
86
87

    conn
lain's avatar
lain committed
88
    |> render(ActivityView, "index.json", %{activities: activities, for: user})
lain's avatar
lain committed
89
90
  end

eal's avatar
eal committed
91
92
93
  def show_user(conn, params) do
    with {:ok, shown} <- TwitterAPI.get_user(params) do
      if user = conn.assigns.user do
lain's avatar
lain committed
94
        render(conn, UserView, "show.json", %{user: shown, for: user})
eal's avatar
eal committed
95
      else
lain's avatar
lain committed
96
        render(conn, UserView, "show.json", %{user: shown})
eal's avatar
eal committed
97
98
99
100
101
102
103
      end
    else
      {:error, msg} ->
        bad_request_reply(conn, msg)
    end
  end

dtluna's avatar
dtluna committed
104
  def user_timeline(%{assigns: %{user: user}} = conn, params) do
105
106
    case TwitterAPI.get_user(user, params) do
      {:ok, target_user} ->
107
        activities = ActivityPub.fetch_user_activities(target_user, user, params)
lain's avatar
lain committed
108

109
        conn
lain's avatar
lain committed
110
        |> render(ActivityView, "index.json", %{activities: activities, for: user})
lain's avatar
lain committed
111

112
113
114
      {:error, msg} ->
        bad_request_reply(conn, msg)
    end
dtluna's avatar
dtluna committed
115
116
  end

dtluna's avatar
dtluna committed
117
  def mentions_timeline(%{assigns: %{user: user}} = conn, params) do
118
119
120
121
122
    params =
      params
      |> Map.put("type", ["Create", "Announce", "Follow", "Like"])
      |> Map.put("blocking_user", user)

lain's avatar
lain committed
123
    activities = ActivityPub.fetch_activities([user.ap_id], params)
dtluna's avatar
dtluna committed
124
125

    conn
lain's avatar
lain committed
126
127
128
129
130
131
132
    |> render(ActivityView, "index.json", %{activities: activities, for: user})
  end

  def dm_timeline(%{assigns: %{user: user}} = conn, params) do
    query =
      ActivityPub.fetch_activities_query(
        [user.ap_id],
133
        Map.merge(params, %{"type" => "Create", "user" => user, visibility: "direct"})
lain's avatar
lain committed
134
135
136
137
138
      )

    activities = Repo.all(query)

    conn
lain's avatar
lain committed
139
    |> render(ActivityView, "index.json", %{activities: activities, for: user})
dtluna's avatar
dtluna committed
140
141
  end

142
143
144
145
146
147
148
  def notifications(%{assigns: %{user: user}} = conn, params) do
    notifications = Notification.for_user(user, params)

    conn
    |> render(NotificationView, "notification.json", %{notifications: notifications, for: user})
  end

149
150
151
152
153
154
155
156
157
158
159
160
161
  def notifications_read(%{assigns: %{user: user}} = conn, %{"latest_id" => latest_id} = params) do
    Notification.set_read_up_to(user, latest_id)

    notifications = Notification.for_user(user, params)

    conn
    |> render(NotificationView, "notification.json", %{notifications: notifications, for: user})
  end

  def notifications_read(%{assigns: %{user: user}} = conn, _) do
    bad_request_reply(conn, "You need to specify latest_id")
  end

162
163
  def follow(%{assigns: %{user: user}} = conn, params) do
    case TwitterAPI.follow(user, params) do
164
      {:ok, user, followed, _activity} ->
dtluna's avatar
dtluna committed
165
        render(conn, UserView, "show.json", %{user: followed, for: user})
lain's avatar
lain committed
166
167
168

      {:error, msg} ->
        forbidden_json_reply(conn, msg)
169
    end
lain's avatar
lain committed
170
171
  end

eal's avatar
eal committed
172
173
174
  def block(%{assigns: %{user: user}} = conn, params) do
    case TwitterAPI.block(user, params) do
      {:ok, user, blocked} ->
lain's avatar
lain committed
175
176
177
178
        render(conn, UserView, "show.json", %{user: blocked, for: user})

      {:error, msg} ->
        forbidden_json_reply(conn, msg)
eal's avatar
eal committed
179
180
181
182
183
184
    end
  end

  def unblock(%{assigns: %{user: user}} = conn, params) do
    case TwitterAPI.unblock(user, params) do
      {:ok, user, blocked} ->
lain's avatar
lain committed
185
186
187
188
        render(conn, UserView, "show.json", %{user: blocked, for: user})

      {:error, msg} ->
        forbidden_json_reply(conn, msg)
eal's avatar
eal committed
189
190
191
    end
  end

lain's avatar
lain committed
192
  def delete_post(%{assigns: %{user: user}} = conn, %{"id" => id}) do
normandy's avatar
normandy committed
193
    with {:ok, activity} <- TwitterAPI.delete(user, id) do
normandy's avatar
normandy committed
194
      render(conn, ActivityView, "activity.json", %{activity: activity, for: user})
lain's avatar
lain committed
195
196
197
    end
  end

198
  def unfollow(%{assigns: %{user: user}} = conn, params) do
199
    case TwitterAPI.unfollow(user, params) do
200
      {:ok, user, unfollowed} ->
dtluna's avatar
dtluna committed
201
        render(conn, UserView, "show.json", %{user: unfollowed, for: user})
lain's avatar
lain committed
202
203
204

      {:error, msg} ->
        forbidden_json_reply(conn, msg)
205
    end
lain's avatar
lain committed
206
207
  end

208
  def fetch_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
lain's avatar
lain committed
209
210
    with %Activity{} = activity <- Repo.get(Activity, id),
         true <- ActivityPub.visible_for_user?(activity, user) do
lain's avatar
lain committed
211
      render(conn, ActivityView, "activity.json", %{activity: activity, for: user})
lain's avatar
lain committed
212
    end
lain's avatar
lain committed
213
214
  end

215
  def fetch_conversation(%{assigns: %{user: user}} = conn, %{"id" => id}) do
216
217
    id = String.to_integer(id)

lain's avatar
lain committed
218
219
220
221
222
223
224
225
226
    with context when is_binary(context) <- TwitterAPI.conversation_id_to_context(id),
         activities <-
           ActivityPub.fetch_activities_for_context(context, %{
             "blocking_user" => user,
             "user" => user
           }) do
      conn
      |> render(ActivityView, "index.json", %{activities: activities, for: user})
    end
227
228
  end

lain's avatar
lain committed
229
230
  def upload(conn, %{"media" => media}) do
    response = TwitterAPI.upload(media)
lain's avatar
lain committed
231

lain's avatar
lain committed
232
233
234
235
    conn
    |> put_resp_content_type("application/atom+xml")
    |> send_resp(200, response)
  end
236

237
238
  def upload_json(conn, %{"media" => media}) do
    response = TwitterAPI.upload(media, "json")
lain's avatar
lain committed
239

240
241
242
243
    conn
    |> json_reply(200, response)
  end

lain's avatar
lain committed
244
  def get_by_id_or_ap_id(id) do
245
    activity = Repo.get(Activity, id) || Activity.get_create_activity_by_object_ap_id(id)
lain's avatar
lain committed
246

247
248
249
250
251
    if activity.data["type"] == "Create" do
      activity
    else
      Activity.get_create_activity_by_object_ap_id(activity.data["object"])
    end
lain's avatar
lain committed
252
253
  end

lain's avatar
lain committed
254
  def favorite(%{assigns: %{user: user}} = conn, %{"id" => id}) do
255
256
    with {_, {:ok, id}} <- {:param_cast, Ecto.Type.cast(:integer, id)},
         {:ok, activity} <- TwitterAPI.fav(user, id) do
257
      render(conn, ActivityView, "activity.json", %{activity: activity, for: user})
lain's avatar
lain committed
258
    end
lain's avatar
lain committed
259
260
  end

lain's avatar
lain committed
261
  def unfavorite(%{assigns: %{user: user}} = conn, %{"id" => id}) do
262
263
    with {_, {:ok, id}} <- {:param_cast, Ecto.Type.cast(:integer, id)},
         {:ok, activity} <- TwitterAPI.unfav(user, id) do
264
      render(conn, ActivityView, "activity.json", %{activity: activity, for: user})
lain's avatar
lain committed
265
    end
lain's avatar
lain committed
266
267
  end

lain's avatar
lain committed
268
  def retweet(%{assigns: %{user: user}} = conn, %{"id" => id}) do
269
270
    with {_, {:ok, id}} <- {:param_cast, Ecto.Type.cast(:integer, id)},
         {:ok, activity} <- TwitterAPI.repeat(user, id) do
271
      render(conn, ActivityView, "activity.json", %{activity: activity, for: user})
dtluna's avatar
dtluna committed
272
    end
lain's avatar
lain committed
273
274
  end

275
276
277
278
279
280
281
  def unretweet(%{assigns: %{user: user}} = conn, %{"id" => id}) do
    with {_, {:ok, id}} <- {:param_cast, Ecto.Type.cast(:integer, id)},
         {:ok, activity} <- TwitterAPI.unrepeat(user, id) do
      render(conn, ActivityView, "activity.json", %{activity: activity, for: user})
    end
  end

282
283
  def register(conn, params) do
    with {:ok, user} <- TwitterAPI.register_user(params) do
dtluna's avatar
dtluna committed
284
      render(conn, UserView, "show.json", %{user: user})
285
286
    else
      {:error, errors} ->
lain's avatar
lain committed
287
288
        conn
        |> json_reply(400, Jason.encode!(errors))
289
290
291
    end
  end

lain's avatar
lain committed
292
  def update_avatar(%{assigns: %{user: user}} = conn, params) do
href's avatar
href committed
293
    {:ok, object} = ActivityPub.upload(params, type: :avatar)
294
    change = Changeset.change(user, %{avatar: object.data})
lain's avatar
lain committed
295
    {:ok, user} = User.update_and_set_cache(change)
lain's avatar
lain committed
296
    CommonAPI.update(user)
lain's avatar
lain committed
297

dtluna's avatar
dtluna committed
298
    render(conn, UserView, "show.json", %{user: user, for: user})
lain's avatar
lain committed
299
300
  end

lain's avatar
lain committed
301
  def update_banner(%{assigns: %{user: user}} = conn, params) do
href's avatar
href committed
302
    with {:ok, object} <- ActivityPub.upload(%{"img" => params["banner"]}, type: :banner),
303
304
305
306
         new_info <- %{"banner" => object.data},
         info_cng <- User.Info.profile_update(user.info, new_info),
         changeset <- Ecto.Changeset.change(user) |> Ecto.Changeset.put_embed(:info, info_cng),
         {:ok, user} <- User.update_and_set_cache(changeset) do
lain's avatar
lain committed
307
      CommonAPI.update(user)
lain's avatar
lain committed
308
309
310
      %{"url" => [%{"href" => href} | _]} = object.data
      response = %{url: href} |> Jason.encode!()

lain's avatar
lain committed
311
312
313
314
315
316
      conn
      |> json_reply(200, response)
    end
  end

  def update_background(%{assigns: %{user: user}} = conn, params) do
href's avatar
href committed
317
    with {:ok, object} <- ActivityPub.upload(params, type: :background),
318
319
320
321
         new_info <- %{"background" => object.data},
         info_cng <- User.Info.profile_update(user.info, new_info),
         changeset <- Ecto.Changeset.change(user) |> Ecto.Changeset.put_embed(:info, info_cng),
         {:ok, _user} <- User.update_and_set_cache(changeset) do
lain's avatar
lain committed
322
323
324
      %{"url" => [%{"href" => href} | _]} = object.data
      response = %{url: href} |> Jason.encode!()

lain's avatar
lain committed
325
326
327
328
329
      conn
      |> json_reply(200, response)
    end
  end

lain's avatar
lain committed
330
331
  def external_profile(%{assigns: %{user: current_user}} = conn, %{"profileurl" => uri}) do
    with {:ok, user_map} <- TwitterAPI.get_external_profile(current_user, uri),
lain's avatar
lain committed
332
         response <- Jason.encode!(user_map) do
lain's avatar
lain committed
333
334
      conn
      |> json_reply(200, response)
lain's avatar
lain committed
335
336
337
338
339
    else
      _e ->
        conn
        |> put_status(404)
        |> json(%{error: "Can't find user"})
lain's avatar
lain committed
340
341
342
    end
  end

eal's avatar
eal committed
343
  def followers(conn, params) do
344
    with {:ok, user} <- TwitterAPI.get_user(conn.assigns[:user], params),
eal's avatar
eal committed
345
         {:ok, followers} <- User.get_followers(user) do
346
      render(conn, UserView, "index.json", %{users: followers, for: conn.assigns[:user]})
lain's avatar
lain committed
347
348
349
350
351
    else
      _e -> bad_request_reply(conn, "Can't get followers")
    end
  end

eal's avatar
eal committed
352
  def friends(conn, params) do
353
    with {:ok, user} <- TwitterAPI.get_user(conn.assigns[:user], params),
eal's avatar
eal committed
354
         {:ok, friends} <- User.get_friends(user) do
355
      render(conn, UserView, "index.json", %{users: friends, for: conn.assigns[:user]})
lain's avatar
lain committed
356
357
358
359
360
    else
      _e -> bad_request_reply(conn, "Can't get friends")
    end
  end

361
  def friend_requests(conn, params) do
362
    with {:ok, user} <- TwitterAPI.get_user(conn.assigns[:user], params),
363
         {:ok, friend_requests} <- User.get_follow_requests(user) do
364
      render(conn, UserView, "index.json", %{users: friend_requests, for: conn.assigns[:user]})
365
366
367
368
369
    else
      _e -> bad_request_reply(conn, "Can't get friend requests")
    end
  end

370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
  def approve_friend_request(conn, %{"user_id" => uid} = params) do
    with followed <- conn.assigns[:user],
         uid when is_number(uid) <- String.to_integer(uid),
         %User{} = follower <- Repo.get(User, uid),
         {:ok, follower} <- User.maybe_follow(follower, followed),
         %Activity{} = follow_activity <- Utils.fetch_latest_follow(follower, followed),
         {:ok, follow_activity} <- Utils.update_follow_state(follow_activity, "accept"),
         {:ok, _activity} <-
           ActivityPub.accept(%{
             to: [follower.ap_id],
             actor: followed.ap_id,
             object: follow_activity.data["id"],
             type: "Accept"
           }) do
      render(conn, UserView, "show.json", %{user: follower, for: followed})
    else
      e -> bad_request_reply(conn, "Can't approve user: #{inspect(e)}")
    end
  end

  def deny_friend_request(conn, %{"user_id" => uid} = params) do
    with followed <- conn.assigns[:user],
         uid when is_number(uid) <- String.to_integer(uid),
         %User{} = follower <- Repo.get(User, uid),
         %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, UserView, "show.json", %{user: follower, for: followed})
    else
      e -> bad_request_reply(conn, "Can't deny user: #{inspect(e)}")
    end
  end

eal's avatar
eal committed
409
410
  def friends_ids(%{assigns: %{user: user}} = conn, _params) do
    with {:ok, friends} <- User.get_friends(user) do
lain's avatar
lain committed
411
412
413
414
      ids =
        friends
        |> Enum.map(fn x -> x.id end)
        |> Jason.encode!()
415

eal's avatar
eal committed
416
417
418
419
420
421
      json(conn, ids)
    else
      _e -> bad_request_reply(conn, "Can't get friends")
    end
  end

422
  def empty_array(conn, _params) do
lain's avatar
lain committed
423
    json(conn, Jason.encode!([]))
424
  end
eal's avatar
eal committed
425

426
427
428
429
  def raw_empty_array(conn, _params) do
    json(conn, [])
  end

lain's avatar
lain committed
430
431
432
433
434
435
  defp build_info_cng(user, params) do
    info_params =
      ["no_rich_text", "locked"]
      |> Enum.reduce(%{}, fn key, res ->
        if value = params[key] do
          Map.put(res, key, value == "true")
436
        else
lain's avatar
lain committed
437
          res
438
        end
lain's avatar
lain committed
439
      end)
440

lain's avatar
lain committed
441
442
443
    info_params =
      if value = params["default_scope"] do
        Map.put(info_params, "default_scope", value)
444
      else
lain's avatar
lain committed
445
        info_params
446
447
      end

lain's avatar
lain committed
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
    User.Info.profile_update(user.info, info_params)
  end

  defp add_profile_emoji(user, params) do
    if bio = params["description"] do
      mentions = Formatter.parse_mentions(bio)
      tags = Formatter.parse_tags(bio)

      emoji =
        (user.info.source_data["tag"] || [])
        |> Enum.filter(fn %{"type" => t} -> t == "Emoji" end)
        |> Enum.map(fn %{"icon" => %{"url" => url}, "name" => name} ->
          {String.trim(name, ":"), url}
        end)

      bio_html = CommonUtils.format_input(bio, mentions, tags, "text/plain")
      Map.put(params, "bio", bio_html |> Formatter.emojify(emoji))
    else
      params
    end
  end

  def update_profile(%{assigns: %{user: user}} = conn, params) do
    params = add_profile_emoji(user, params)
    info_cng = build_info_cng(user, params)
473

lain's avatar
lain committed
474
    with changeset <- User.update_changeset(user, params),
lain's avatar
lain committed
475
         changeset <- Ecto.Changeset.put_embed(changeset, :info, info_cng),
lain's avatar
lain committed
476
         {:ok, user} <- User.update_and_set_cache(changeset) do
lain's avatar
lain committed
477
      CommonAPI.update(user)
lain's avatar
lain committed
478
479
480
481
482
483
484
485
      render(conn, UserView, "user.json", %{user: user, for: user})
    else
      error ->
        Logger.debug("Can't update user: #{inspect(error)}")
        bad_request_reply(conn, "Can't update user")
    end
  end

Thog's avatar
Thog committed
486
  def search(%{assigns: %{user: user}} = conn, %{"q" => _query} = params) do
lain's avatar
lain committed
487
488
    activities = TwitterAPI.search(user, params)

lain's avatar
lain committed
489
    conn
lain's avatar
lain committed
490
    |> render(ActivityView, "index.json", %{activities: activities, for: user})
lain's avatar
lain committed
491
492
  end

493
494
495
496
497
498
499
  def search_user(%{assigns: %{user: user}} = conn, %{"query" => query}) do
    users = User.search(query, true)

    conn
    |> render(UserView, "index.json", %{users: users, for: user})
  end

500
  defp bad_request_reply(conn, error_message) do
dtluna's avatar
dtluna committed
501
    json = error_json(conn, error_message)
502
503
504
    json_reply(conn, 400, json)
  end

505
506
507
508
509
  defp json_reply(conn, status, json) do
    conn
    |> put_resp_content_type("application/json")
    |> send_resp(status, json)
  end
510
511

  defp forbidden_json_reply(conn, error_message) do
dtluna's avatar
dtluna committed
512
    json = error_json(conn, error_message)
513
514
    json_reply(conn, 403, json)
  end
dtluna's avatar
dtluna committed
515

href's avatar
href committed
516
517
518
519
520
521
522
523
  def only_if_public_instance(conn = %{conn: %{assigns: %{user: _user}}}, _), do: conn

  def only_if_public_instance(conn, _) do
    if Keyword.get(Application.get_env(:pleroma, :instance), :public) do
      conn
    else
      conn
      |> forbidden_json_reply("Invalid credentials.")
href's avatar
href committed
524
      |> halt()
href's avatar
href committed
525
526
527
    end
  end

dtluna's avatar
dtluna committed
528
  defp error_json(conn, error_message) do
lain's avatar
lain committed
529
    %{"error" => error_message, "request" => conn.request_path} |> Jason.encode!()
dtluna's avatar
dtluna committed
530
  end
531
532
533
534
535
536
537
538
539
540
541
542

  def errors(conn, {:param_cast, _}) do
    conn
    |> put_status(400)
    |> json("Invalid parameters")
  end

  def errors(conn, _) do
    conn
    |> put_status(500)
    |> json("Something went wrong")
  end
543
end