More TwAPI fixes.
[akkoma] / lib / pleroma / web / twitter_api / twitter_api.ex
1 defmodule Pleroma.Web.TwitterAPI.TwitterAPI do
2 alias Pleroma.{User, Activity, Repo, Object}
3 alias Pleroma.Web.ActivityPub.ActivityPub
4 alias Pleroma.Web.TwitterAPI.Representers.ActivityRepresenter
5 alias Pleroma.Web.TwitterAPI.UserView
6 alias Pleroma.Web.{OStatus, CommonAPI}
7 import Ecto.Query
8
9 @httpoison Application.get_env(:pleroma, :httpoison)
10
11 def create_status(%User{} = user, %{"status" => _} = data) do
12 CommonAPI.post(user, data)
13 end
14
15 def fetch_friend_statuses(user, opts \\ %{}) do
16 opts = opts
17 |> Map.put("blocking_user", user)
18 |> Map.put("user", user)
19 |> Map.put("type", ["Create", "Announce", "Follow", "Like"])
20
21 ActivityPub.fetch_activities([user.ap_id | user.following], opts)
22 |> activities_to_statuses(%{for: user})
23 end
24
25 def fetch_public_statuses(user, opts \\ %{}) do
26 opts = opts
27 |> Map.put("local_only", true)
28 |> Map.put("blocking_user", user)
29 |> Map.put("type", ["Create", "Announce", "Follow"])
30
31 ActivityPub.fetch_public_activities(opts)
32 |> activities_to_statuses(%{for: user})
33 end
34
35 def fetch_public_and_external_statuses(user, opts \\ %{}) do
36 opts = opts
37 |> Map.put("blocking_user", user)
38 |> Map.put("type", ["Create", "Announce", "Follow"])
39
40 ActivityPub.fetch_public_activities(opts)
41 |> activities_to_statuses(%{for: user})
42 end
43
44 def fetch_user_statuses(user, opts \\ %{}) do
45 opts = opts
46 |> Map.put("type", ["Create", "Announce", "Follow"])
47 ActivityPub.fetch_activities([], opts)
48 |> activities_to_statuses(%{for: user})
49 end
50
51 def fetch_mentions(user, opts \\ %{}) do
52 ActivityPub.fetch_activities([user.ap_id], opts)
53 |> activities_to_statuses(%{for: user})
54 end
55
56 def fetch_conversation(user, id) do
57 with context when is_binary(context) <- conversation_id_to_context(id),
58 activities <- ActivityPub.fetch_activities_for_context(context, %{"blocking_user" => user, "user" => user}),
59 statuses <- activities |> activities_to_statuses(%{for: user})
60 do
61 statuses
62 else _e ->
63 []
64 end
65 end
66
67 def fetch_status(user, id) do
68 with %Activity{} = activity <- Repo.get(Activity, id),
69 true <- ActivityPub.visible_for_user?(activity, user) do
70 activity_to_status(activity, %{for: user})
71 end
72 end
73
74 def follow(%User{} = follower, params) do
75 with {:ok, %User{} = followed} <- get_user(params),
76 {:ok, follower} <- User.follow(follower, followed),
77 {:ok, activity} <- ActivityPub.follow(follower, followed)
78 do
79 {:ok, follower, followed, activity}
80 else
81 err -> err
82 end
83 end
84
85 def unfollow(%User{} = follower, params) do
86 with { :ok, %User{} = unfollowed } <- get_user(params),
87 { :ok, follower, follow_activity } <- User.unfollow(follower, unfollowed),
88 { :ok, _activity } <- ActivityPub.insert(%{
89 "type" => "Undo",
90 "actor" => follower.ap_id,
91 "object" => follow_activity.data["id"], # get latest Follow for these users
92 "published" => make_date()
93 })
94 do
95 { :ok, follower, unfollowed }
96 else
97 err -> err
98 end
99 end
100
101 def block(%User{} = blocker, params) do
102 with {:ok, %User{} = blocked} <- get_user(params),
103 {:ok, blocker} <- User.block(blocker, blocked)
104 do
105 {:ok, blocker, blocked}
106 else
107 err -> err
108 end
109 end
110
111 def unblock(%User{} = blocker, params) do
112 with {:ok, %User{} = blocked} <- get_user(params),
113 {:ok, blocker} <- User.unblock(blocker, blocked)
114 do
115 {:ok, blocker, blocked}
116 else
117 err -> err
118 end
119 end
120
121 def repeat(%User{} = user, ap_id_or_id) do
122 with {:ok, _announce, %{data: %{"id" => id}}} = CommonAPI.repeat(ap_id_or_id, user),
123 %Activity{} = activity <- Activity.get_create_activity_by_object_ap_id(id),
124 status <- activity_to_status(activity, %{for: user}) do
125 {:ok, status}
126 end
127 end
128
129 def fav(%User{} = user, ap_id_or_id) do
130 with {:ok, _announce, %{data: %{"id" => id}}} = CommonAPI.favorite(ap_id_or_id, user),
131 %Activity{} = activity <- Activity.get_create_activity_by_object_ap_id(id),
132 status <- activity_to_status(activity, %{for: user}) do
133 {:ok, status}
134 end
135 end
136
137 def unfav(%User{} = user, ap_id_or_id) do
138 with {:ok, %{data: %{"id" => id}}} = CommonAPI.unfavorite(ap_id_or_id, user),
139 %Activity{} = activity <- Activity.get_create_activity_by_object_ap_id(id),
140 status <- activity_to_status(activity, %{for: user}) do
141 {:ok, status}
142 end
143 end
144
145 def upload(%Plug.Upload{} = file, format \\ "xml") do
146 {:ok, object} = ActivityPub.upload(file)
147
148 url = List.first(object.data["url"])
149 href = url["href"]
150 type = url["mediaType"]
151
152 case format do
153 "xml" ->
154 # Fake this as good as possible...
155 """
156 <?xml version="1.0" encoding="UTF-8"?>
157 <rsp stat="ok" xmlns:atom="http://www.w3.org/2005/Atom">
158 <mediaid>#{object.id}</mediaid>
159 <media_id>#{object.id}</media_id>
160 <media_id_string>#{object.id}</media_id_string>
161 <media_url>#{href}</media_url>
162 <mediaurl>#{href}</mediaurl>
163 <atom:link rel="enclosure" href="#{href}" type="#{type}"></atom:link>
164 </rsp>
165 """
166 "json" ->
167 %{
168 media_id: object.id,
169 media_id_string: "#{object.id}}",
170 media_url: href,
171 size: 0
172 } |> Poison.encode!
173 end
174 end
175
176 def register_user(params) do
177 params = %{
178 nickname: params["nickname"],
179 name: params["fullname"],
180 bio: params["bio"],
181 email: params["email"],
182 password: params["password"],
183 password_confirmation: params["confirm"]
184 }
185
186 changeset = User.register_changeset(%User{}, params)
187
188 with {:ok, user} <- Repo.insert(changeset) do
189 {:ok, user}
190 else
191 {:error, changeset} ->
192 errors = Ecto.Changeset.traverse_errors(changeset, fn {msg, _opts} -> msg end)
193 |> Poison.encode!
194 {:error, %{error: errors}}
195 end
196 end
197
198 def get_by_id_or_nickname(id_or_nickname) do
199 if !is_integer(id_or_nickname) && :error == Integer.parse(id_or_nickname) do
200 Repo.get_by(User, nickname: id_or_nickname)
201 else
202 Repo.get(User, id_or_nickname)
203 end
204 end
205
206 def get_user(user \\ nil, params) do
207 case params do
208 %{"user_id" => user_id} ->
209 case target = get_by_id_or_nickname(user_id) do
210 nil ->
211 {:error, "No user with such user_id"}
212 _ ->
213 {:ok, target}
214 end
215 %{"screen_name" => nickname} ->
216 case target = Repo.get_by(User, nickname: nickname) do
217 nil ->
218 {:error, "No user with such screen_name"}
219 _ ->
220 {:ok, target}
221 end
222 _ ->
223 if user do
224 {:ok, user}
225 else
226 {:error, "You need to specify screen_name or user_id"}
227 end
228 end
229 end
230
231 defp parse_int(string, default)
232 defp parse_int(string, default) when is_binary(string) do
233 with {n, _} <- Integer.parse(string) do
234 n
235 else
236 _e -> default
237 end
238 end
239 defp parse_int(_, default), do: default
240
241 def search(user, %{"q" => query} = params) do
242 limit = parse_int(params["rpp"], 20)
243 page = parse_int(params["page"], 1)
244 offset = (page - 1) * limit
245
246 q = from a in Activity,
247 where: fragment("?->>'type' = 'Create'", a.data),
248 where: fragment("to_tsvector('english', ?->'object'->>'content') @@ plainto_tsquery('english', ?)", a.data, ^query),
249 limit: ^limit,
250 offset: ^offset,
251 order_by: [desc: :inserted_at] # this one isn't indexed so psql won't take the wrong index.
252
253 activities = Repo.all(q)
254 activities_to_statuses(activities, %{for: user})
255 end
256
257 defp activities_to_statuses(activities, opts) do
258 Enum.map(activities, fn(activity) ->
259 activity_to_status(activity, opts)
260 end)
261 end
262
263 # For likes, fetch the liked activity, too.
264 defp activity_to_status(%Activity{data: %{"type" => "Like"}} = activity, opts) do
265 actor = get_in(activity.data, ["actor"])
266 user = User.get_cached_by_ap_id(actor)
267 [liked_activity] = Activity.all_by_object_ap_id(activity.data["object"])
268
269 ActivityRepresenter.to_map(activity, Map.merge(opts, %{user: user, liked_activity: liked_activity}))
270 end
271
272 # For announces, fetch the announced activity and the user.
273 defp activity_to_status(%Activity{data: %{"type" => "Announce"}} = activity, opts) do
274 actor = get_in(activity.data, ["actor"])
275 user = User.get_cached_by_ap_id(actor)
276 [announced_activity] = Activity.all_by_object_ap_id(activity.data["object"])
277 announced_actor = User.get_cached_by_ap_id(announced_activity.data["actor"])
278
279 ActivityRepresenter.to_map(activity, Map.merge(opts, %{users: [user, announced_actor], announced_activity: announced_activity}))
280 end
281
282 defp activity_to_status(%Activity{data: %{"type" => "Delete"}} = activity, opts) do
283 actor = get_in(activity.data, ["actor"])
284 user = User.get_cached_by_ap_id(actor)
285 ActivityRepresenter.to_map(activity, Map.merge(opts, %{user: user}))
286 end
287
288 defp activity_to_status(activity, opts) do
289 actor = get_in(activity.data, ["actor"])
290 user = User.get_cached_by_ap_id(actor)
291 # mentioned_users = Repo.all(from user in User, where: user.ap_id in ^activity.data["to"])
292 mentioned_users = Enum.map(activity.recipients || [], fn (ap_id) ->
293 if ap_id do
294 User.get_cached_by_ap_id(ap_id)
295 else
296 nil
297 end
298 end)
299 |> Enum.filter(&(&1))
300
301 ActivityRepresenter.to_map(activity, Map.merge(opts, %{user: user, mentioned: mentioned_users}))
302 end
303
304 defp make_date do
305 DateTime.utc_now() |> DateTime.to_iso8601
306 end
307
308 def context_to_conversation_id(context) do
309 with %Object{id: id} <- Object.get_cached_by_ap_id(context) do
310 id
311 else _e ->
312 changeset = Object.context_mapping(context)
313 case Repo.insert(changeset) do
314 {:ok, %{id: id}} -> id
315 # This should be solved by an upsert, but it seems ecto
316 # has problems accessing the constraint inside the jsonb.
317 {:error, _} -> Object.get_cached_by_ap_id(context).id
318 end
319 end
320 end
321
322 def conversation_id_to_context(id) do
323 with %Object{data: %{"id" => context}} <- Repo.get(Object, id) do
324 context
325 else _e ->
326 {:error, "No such conversation"}
327 end
328 end
329
330 def get_external_profile(for_user, uri) do
331 with {:ok, %User{} = user} <- OStatus.find_or_make_user(uri) do
332 spawn(fn ->
333 with url <- user.info["topic"],
334 {:ok, %{body: body}} <- @httpoison.get(url, [], follow_redirect: true, timeout: 10000, recv_timeout: 20000) do
335 OStatus.handle_incoming(body)
336 end
337 end)
338 {:ok, UserView.render("show.json", %{user: user, for: for_user})}
339 else _e ->
340 {:error, "Couldn't find user"}
341 end
342 end
343 end