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

lain's avatar
lain committed
5
defmodule Pleroma.Web.TwitterAPI.TwitterAPI do
Haelwenn's avatar
Haelwenn committed
6
  alias Pleroma.Activity
7
8
9
  alias Pleroma.Mailer
  alias Pleroma.Repo
  alias Pleroma.User
Haelwenn's avatar
Haelwenn committed
10
  alias Pleroma.UserEmail
11
  alias Pleroma.UserInviteToken
lain's avatar
lain committed
12
  alias Pleroma.Web.ActivityPub.ActivityPub
Maksim's avatar
Maksim committed
13
  alias Pleroma.Web.CommonAPI
14
  alias Pleroma.Web.TwitterAPI.UserView
15

lain's avatar
lain committed
16
  import Ecto.Query
lain's avatar
lain committed
17

Thog's avatar
Thog committed
18
  def create_status(%User{} = user, %{"status" => _} = data) do
19
    CommonAPI.post(user, data)
lain's avatar
lain committed
20
21
  end

normandy's avatar
normandy committed
22
  def delete(%User{} = user, id) do
23
    with %Activity{data: %{"type" => _type}} <- Activity.get_by_id(id),
24
25
         {:ok, activity} <- CommonAPI.delete(id, user) do
      {:ok, activity}
normandy's avatar
normandy committed
26
27
28
    end
  end

dtluna's avatar
dtluna committed
29
  def follow(%User{} = follower, params) do
30
31
    with {:ok, %User{} = followed} <- get_user(params) do
      CommonAPI.follow(follower, followed)
lain's avatar
lain committed
32
33
34
    end
  end

dtluna's avatar
dtluna committed
35
  def unfollow(%User{} = follower, params) do
lain's avatar
lain committed
36
    with {:ok, %User{} = unfollowed} <- get_user(params),
37
         {:ok, follower} <- CommonAPI.unfollow(follower, unfollowed) do
lain's avatar
lain committed
38
      {:ok, follower, unfollowed}
lain's avatar
lain committed
39
40
41
    end
  end

eal's avatar
eal committed
42
43
  def block(%User{} = blocker, params) do
    with {:ok, %User{} = blocked} <- get_user(params),
normandy's avatar
normandy committed
44
45
         {:ok, blocker} <- User.block(blocker, blocked),
         {:ok, _activity} <- ActivityPub.block(blocker, blocked) do
eal's avatar
eal committed
46
47
48
49
50
51
52
53
      {:ok, blocker, blocked}
    else
      err -> err
    end
  end

  def unblock(%User{} = blocker, params) do
    with {:ok, %User{} = blocked} <- get_user(params),
normandy's avatar
normandy committed
54
55
         {:ok, blocker} <- User.unblock(blocker, blocked),
         {:ok, _activity} <- ActivityPub.unblock(blocker, blocked) do
eal's avatar
eal committed
56
57
58
59
60
61
      {:ok, blocker, blocked}
    else
      err -> err
    end
  end

62
  def subscribe(%User{} = subscriber, params) do
63
64
65
    with {:ok, %User{} = subscribed} <- get_user(params),
         {:ok, subscriber} <- User.subscribe(subscriber, subscribed) do
      {:ok, subscriber, subscribed}
66
67
68
69
    end
  end

  def unsubscribe(%User{} = unsubscriber, params) do
70
71
72
    with {:ok, %User{} = unsubscribed} <- get_user(params),
         {:ok, unsubscriber} <- User.unsubscribe(unsubscriber, unsubscribed) do
      {:ok, unsubscriber, unsubscribed}
73
74
75
    end
  end

lain's avatar
lain committed
76
  def repeat(%User{} = user, ap_id_or_id) do
77
    with {:ok, _announce, %{data: %{"id" => id}}} <- CommonAPI.repeat(ap_id_or_id, user),
78
         %Activity{} = activity <- Activity.get_create_by_object_ap_id(id) do
79
      {:ok, activity}
lain's avatar
lain committed
80
    end
lain's avatar
lain committed
81
82
  end

83
84
  def unrepeat(%User{} = user, ap_id_or_id) do
    with {:ok, _unannounce, %{data: %{"id" => id}}} <- CommonAPI.unrepeat(ap_id_or_id, user),
85
         %Activity{} = activity <- Activity.get_create_by_object_ap_id(id) do
normandy's avatar
normandy committed
86
87
88
89
      {:ok, activity}
    end
  end

90
91
92
93
94
95
96
97
  def pin(%User{} = user, ap_id_or_id) do
    CommonAPI.pin(ap_id_or_id, user)
  end

  def unpin(%User{} = user, ap_id_or_id) do
    CommonAPI.unpin(ap_id_or_id, user)
  end

lain's avatar
lain committed
98
  def fav(%User{} = user, ap_id_or_id) do
99
    with {:ok, _fav, %{data: %{"id" => id}}} <- CommonAPI.favorite(ap_id_or_id, user),
100
         %Activity{} = activity <- Activity.get_create_by_object_ap_id(id) do
101
      {:ok, activity}
lain's avatar
lain committed
102
103
104
    end
  end

lain's avatar
lain committed
105
  def unfav(%User{} = user, ap_id_or_id) do
106
    with {:ok, _unfav, _fav, %{data: %{"id" => id}}} <- CommonAPI.unfavorite(ap_id_or_id, user),
107
         %Activity{} = activity <- Activity.get_create_by_object_ap_id(id) do
108
      {:ok, activity}
lain's avatar
lain committed
109
110
111
    end
  end

112
  def upload(%Plug.Upload{} = file, %User{} = user, format \\ "xml") do
113
    {:ok, object} = ActivityPub.upload(file, actor: User.ap_id(user))
lain's avatar
lain committed
114

lain's avatar
lain committed
115
    url = List.first(object.data["url"])
href's avatar
href committed
116
    href = url["href"]
lain's avatar
lain committed
117
118
    type = url["mediaType"]

119
120
121
122
123
124
125
126
127
128
129
130
131
132
    case format do
      "xml" ->
        # Fake this as good as possible...
        """
        <?xml version="1.0" encoding="UTF-8"?>
        <rsp stat="ok" xmlns:atom="http://www.w3.org/2005/Atom">
        <mediaid>#{object.id}</mediaid>
        <media_id>#{object.id}</media_id>
        <media_id_string>#{object.id}</media_id_string>
        <media_url>#{href}</media_url>
        <mediaurl>#{href}</mediaurl>
        <atom:link rel="enclosure" href="#{href}" type="#{type}"></atom:link>
        </rsp>
        """
lain's avatar
lain committed
133

134
135
136
137
138
139
      "json" ->
        %{
          media_id: object.id,
          media_id_string: "#{object.id}}",
          media_url: href,
          size: 0
lain's avatar
lain committed
140
141
        }
        |> Jason.encode!()
142
    end
lain's avatar
lain committed
143
144
  end

lain's avatar
lain committed
145
  def register_user(params) do
146
    token_string = params["token"]
HJ's avatar
HJ committed
147

lain's avatar
lain committed
148
149
150
    params = %{
      nickname: params["nickname"],
      name: params["fullname"],
Maxim Filippov's avatar
Maxim Filippov committed
151
      bio: User.parse_bio(params["bio"]),
lain's avatar
lain committed
152
153
      email: params["email"],
      password: params["password"],
154
155
      password_confirmation: params["confirm"],
      captcha_solution: params["captcha_solution"],
vaartis's avatar
vaartis committed
156
157
      captcha_token: params["captcha_token"],
      captcha_answer_data: params["captcha_answer_data"]
lain's avatar
lain committed
158
159
    }

vaartis's avatar
vaartis committed
160
161
    captcha_enabled = Pleroma.Config.get([Pleroma.Captcha, :enabled])
    # true if captcha is disabled or enabled and valid, false otherwise
vaartis's avatar
vaartis committed
162
163
    captcha_ok =
      if !captcha_enabled do
vaartis's avatar
vaartis committed
164
        :ok
vaartis's avatar
vaartis committed
165
      else
vaartis's avatar
vaartis committed
166
167
168
169
170
        Pleroma.Captcha.validate(
          params[:captcha_token],
          params[:captcha_solution],
          params[:captcha_answer_data]
        )
vaartis's avatar
vaartis committed
171
      end
vaartis's avatar
vaartis committed
172

173
    # Captcha invalid
vaartis's avatar
vaartis committed
174
175
    if captcha_ok != :ok do
      {:error, error} = captcha_ok
176
      # I have no idea how this error handling works
vaartis's avatar
vaartis committed
177
      {:error, %{error: Jason.encode!(%{captcha: [error]})}}
178
179
    else
      registrations_open = Pleroma.Config.get([:instance, :registrations_open])
href's avatar
href committed
180

181
182
      # no need to query DB if registration is open
      token =
183
184
        unless registrations_open || is_nil(token_string) do
          Repo.get_by(UserInviteToken, %{token: token_string})
vaartis's avatar
vaartis committed
185
        end
lain's avatar
lain committed
186

Ivan Tashkinov's avatar
Ivan Tashkinov committed
187
188
189
190
191
192
193
194
195
196
197
198
199
      cond do
        registrations_open || (!is_nil(token) && !token.used) ->
          changeset = User.register_changeset(%User{}, params)

          with {:ok, user} <- User.register(changeset) do
            !registrations_open && UserInviteToken.mark_as_used(token.token)

            {:ok, user}
          else
            {:error, changeset} ->
              errors =
                Ecto.Changeset.traverse_errors(changeset, fn {msg, _opts} -> msg end)
                |> Jason.encode!()
HJ's avatar
HJ committed
200

vaartis's avatar
vaartis committed
201
              {:error, %{error: errors}}
202
203
204
          end

        !registrations_open && is_nil(token) ->
vaartis's avatar
vaartis committed
205
          {:error, "Invalid token"}
lain's avatar
lain committed
206

207
        !registrations_open && token.used ->
vaartis's avatar
vaartis committed
208
          {:error, "Expired token"}
209
      end
lain's avatar
lain committed
210
211
212
    end
  end

213
214
215
216
217
  def password_reset(nickname_or_email) do
    with true <- is_binary(nickname_or_email),
         %User{local: true} = user <- User.get_by_nickname_or_email(nickname_or_email),
         {:ok, token_record} <- Pleroma.PasswordResetToken.create_token(user) do
      user
218
      |> UserEmail.password_reset_email(token_record.token)
minibikini's avatar
Reports    
minibikini committed
219
      |> Mailer.deliver_async()
220
221
222
223
224
225
226
227
228
229
230
231
    else
      false ->
        {:error, "bad user identifier"}

      %User{local: false} ->
        {:error, "remote user"}

      nil ->
        {:error, "unknown user"}
    end
  end

232
  def get_user(user \\ nil, params) do
233
    case params do
lain's avatar
lain committed
234
      %{"user_id" => user_id} ->
235
        case target = User.get_cached_by_nickname_or_id(user_id) do
236
237
          nil ->
            {:error, "No user with such user_id"}
lain's avatar
lain committed
238

239
240
241
          _ ->
            {:ok, target}
        end
lain's avatar
lain committed
242

lain's avatar
lain committed
243
      %{"screen_name" => nickname} ->
244
245
246
        case User.get_by_nickname(nickname) do
          nil -> {:error, "No user with such screen_name"}
          target -> {:ok, target}
247
        end
lain's avatar
lain committed
248

249
250
251
252
      _ ->
        if user do
          {:ok, user}
        else
dtluna's avatar
dtluna committed
253
          {:error, "You need to specify screen_name or user_id"}
254
255
256
257
        end
    end
  end

Thog's avatar
Thog committed
258
  defp parse_int(string, default)
lain's avatar
lain committed
259

lain's avatar
lain committed
260
261
262
263
264
265
266
  defp parse_int(string, default) when is_binary(string) do
    with {n, _} <- Integer.parse(string) do
      n
    else
      _e -> default
    end
  end
lain's avatar
lain committed
267

lain's avatar
lain committed
268
269
  defp parse_int(_, default), do: default

feld's avatar
feld committed
270
  def search(_user, %{"q" => query} = params) do
lain's avatar
lain committed
271
272
273
274
    limit = parse_int(params["rpp"], 20)
    page = parse_int(params["page"], 1)
    offset = (page - 1) * limit

lain's avatar
lain committed
275
276
277
278
    q =
      from(
        a in Activity,
        where: fragment("?->>'type' = 'Create'", a.data),
lain's avatar
lain committed
279
        where: "https://www.w3.org/ns/activitystreams#Public" in a.recipients,
lain's avatar
lain committed
280
281
282
283
284
285
286
287
288
289
290
        where:
          fragment(
            "to_tsvector('english', ?->'object'->>'content') @@ plainto_tsquery('english', ?)",
            a.data,
            ^query
          ),
        limit: ^limit,
        offset: ^offset,
        # this one isn't indexed so psql won't take the wrong index.
        order_by: [desc: :inserted_at]
      )
lain's avatar
lain committed
291

feld's avatar
feld committed
292
    _activities = Repo.all(q)
lain's avatar
lain committed
293
294
  end

lain's avatar
lain committed
295
  def get_external_profile(for_user, uri) do
lain's avatar
lain committed
296
    with %User{} = user <- User.get_or_fetch(uri) do
dtluna's avatar
dtluna committed
297
      {:ok, UserView.render("show.json", %{user: user, for: for_user})}
lain's avatar
lain committed
298
299
    else
      _e ->
300
        {:error, "Couldn't find user"}
lain's avatar
lain committed
301
302
    end
  end
lain's avatar
lain committed
303
end