Merge branch 'develop' into tests/mastodon_api_controller.ex
[akkoma] / lib / pleroma / web / ostatus / ostatus_controller.ex
1 # Pleroma: A lightweight social networking server
2 # Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
3 # SPDX-License-Identifier: AGPL-3.0-only
4
5 defmodule Pleroma.Web.OStatus.OStatusController do
6 use Pleroma.Web, :controller
7
8 alias Fallback.RedirectController
9 alias Pleroma.Activity
10 alias Pleroma.Object
11 alias Pleroma.User
12 alias Pleroma.Web.ActivityPub.ActivityPub
13 alias Pleroma.Web.ActivityPub.ActivityPubController
14 alias Pleroma.Web.ActivityPub.ObjectView
15 alias Pleroma.Web.ActivityPub.Visibility
16 alias Pleroma.Web.Endpoint
17 alias Pleroma.Web.Federator
18 alias Pleroma.Web.Metadata.PlayerView
19 alias Pleroma.Web.OStatus
20 alias Pleroma.Web.OStatus.ActivityRepresenter
21 alias Pleroma.Web.OStatus.FeedRepresenter
22 alias Pleroma.Web.Router
23 alias Pleroma.Web.XML
24
25 plug(
26 Pleroma.Plugs.RateLimiter,
27 {:ap_routes, params: ["uuid"]} when action in [:object, :activity]
28 )
29
30 plug(Pleroma.Web.FederatingPlug when action in [:salmon_incoming])
31
32 plug(
33 Pleroma.Plugs.SetFormatPlug
34 when action in [:feed_redirect, :object, :activity, :notice]
35 )
36
37 action_fallback(:errors)
38
39 def feed_redirect(%{assigns: %{format: "html"}} = conn, %{"nickname" => nickname}) do
40 with {_, %User{} = user} <- {:fetch_user, User.get_cached_by_nickname_or_id(nickname)} do
41 RedirectController.redirector_with_meta(conn, %{user: user})
42 end
43 end
44
45 def feed_redirect(%{assigns: %{format: format}} = conn, _params)
46 when format in ["json", "activity+json"] do
47 ActivityPubController.call(conn, :user)
48 end
49
50 def feed_redirect(conn, %{"nickname" => nickname}) do
51 with {_, %User{} = user} <- {:fetch_user, User.get_cached_by_nickname(nickname)} do
52 redirect(conn, external: OStatus.feed_path(user))
53 end
54 end
55
56 def feed(conn, %{"nickname" => nickname} = params) do
57 with {_, %User{} = user} <- {:fetch_user, User.get_cached_by_nickname(nickname)} do
58 activities =
59 params
60 |> Map.take(["max_id"])
61 |> Map.merge(%{"whole_db" => true, "actor_id" => user.ap_id})
62 |> ActivityPub.fetch_public_activities()
63 |> Enum.reverse()
64
65 response =
66 user
67 |> FeedRepresenter.to_simple_form(activities, [user])
68 |> :xmerl.export_simple(:xmerl_xml)
69 |> to_string
70
71 conn
72 |> put_resp_content_type("application/atom+xml")
73 |> send_resp(200, response)
74 end
75 end
76
77 defp decode_or_retry(body) do
78 with {:ok, magic_key} <- Pleroma.Web.Salmon.fetch_magic_key(body),
79 {:ok, doc} <- Pleroma.Web.Salmon.decode_and_validate(magic_key, body) do
80 {:ok, doc}
81 else
82 _e ->
83 with [decoded | _] <- Pleroma.Web.Salmon.decode(body),
84 doc <- XML.parse_document(decoded),
85 uri when not is_nil(uri) <- XML.string_from_xpath("/entry/author[1]/uri", doc),
86 {:ok, _} <- Pleroma.Web.OStatus.make_user(uri, true),
87 {:ok, magic_key} <- Pleroma.Web.Salmon.fetch_magic_key(body),
88 {:ok, doc} <- Pleroma.Web.Salmon.decode_and_validate(magic_key, body) do
89 {:ok, doc}
90 end
91 end
92 end
93
94 def salmon_incoming(conn, _) do
95 {:ok, body, _conn} = read_body(conn)
96 {:ok, doc} = decode_or_retry(body)
97
98 Federator.incoming_doc(doc)
99
100 send_resp(conn, 200, "")
101 end
102
103 def object(%{assigns: %{format: format}} = conn, %{"uuid" => _uuid})
104 when format in ["json", "activity+json"] do
105 ActivityPubController.call(conn, :object)
106 end
107
108 def object(%{assigns: %{format: format}} = conn, %{"uuid" => uuid}) do
109 with id <- o_status_url(conn, :object, uuid),
110 {_, %Activity{} = activity} <-
111 {:activity, Activity.get_create_by_object_ap_id_with_object(id)},
112 {_, true} <- {:public?, Visibility.is_public?(activity)},
113 %User{} = user <- User.get_cached_by_ap_id(activity.data["actor"]) do
114 case format do
115 "html" -> redirect(conn, to: "/notice/#{activity.id}")
116 _ -> represent_activity(conn, nil, activity, user)
117 end
118 else
119 reason when reason in [{:public?, false}, {:activity, nil}] ->
120 {:error, :not_found}
121
122 e ->
123 e
124 end
125 end
126
127 def activity(%{assigns: %{format: format}} = conn, %{"uuid" => _uuid})
128 when format in ["json", "activity+json"] do
129 ActivityPubController.call(conn, :activity)
130 end
131
132 def activity(%{assigns: %{format: format}} = conn, %{"uuid" => uuid}) do
133 with id <- o_status_url(conn, :activity, uuid),
134 {_, %Activity{} = activity} <- {:activity, Activity.normalize(id)},
135 {_, true} <- {:public?, Visibility.is_public?(activity)},
136 %User{} = user <- User.get_cached_by_ap_id(activity.data["actor"]) do
137 case format do
138 "html" -> redirect(conn, to: "/notice/#{activity.id}")
139 _ -> represent_activity(conn, format, activity, user)
140 end
141 else
142 reason when reason in [{:public?, false}, {:activity, nil}] ->
143 {:error, :not_found}
144
145 e ->
146 e
147 end
148 end
149
150 def notice(%{assigns: %{format: format}} = conn, %{"id" => id}) do
151 with {_, %Activity{} = activity} <- {:activity, Activity.get_by_id_with_object(id)},
152 {_, true} <- {:public?, Visibility.is_public?(activity)},
153 %User{} = user <- User.get_cached_by_ap_id(activity.data["actor"]) do
154 cond do
155 format == "html" && activity.data["type"] == "Create" ->
156 %Object{} = object = Object.normalize(activity)
157
158 RedirectController.redirector_with_meta(
159 conn,
160 %{
161 activity_id: activity.id,
162 object: object,
163 url: Router.Helpers.o_status_url(Endpoint, :notice, activity.id),
164 user: user
165 }
166 )
167
168 format == "html" ->
169 RedirectController.redirector(conn, nil)
170
171 true ->
172 represent_activity(conn, format, activity, user)
173 end
174 else
175 reason when reason in [{:public?, false}, {:activity, nil}] ->
176 conn
177 |> put_status(404)
178 |> RedirectController.redirector(nil, 404)
179
180 e ->
181 e
182 end
183 end
184
185 # Returns an HTML embedded <audio> or <video> player suitable for embed iframes.
186 def notice_player(conn, %{"id" => id}) do
187 with %Activity{data: %{"type" => "Create"}} = activity <- Activity.get_by_id_with_object(id),
188 true <- Visibility.is_public?(activity),
189 %Object{} = object <- Object.normalize(activity),
190 %{data: %{"attachment" => [%{"url" => [url | _]} | _]}} <- object,
191 true <- String.starts_with?(url["mediaType"], ["audio", "video"]) do
192 conn
193 |> put_layout(:metadata_player)
194 |> put_resp_header("x-frame-options", "ALLOW")
195 |> put_resp_header(
196 "content-security-policy",
197 "default-src 'none';style-src 'self' 'unsafe-inline';img-src 'self' data: https:; media-src 'self' https:;"
198 )
199 |> put_view(PlayerView)
200 |> render("player.html", url)
201 else
202 _error ->
203 conn
204 |> put_status(404)
205 |> RedirectController.redirector(nil, 404)
206 end
207 end
208
209 defp represent_activity(
210 conn,
211 "activity+json",
212 %Activity{data: %{"type" => "Create"}} = activity,
213 _user
214 ) do
215 object = Object.normalize(activity)
216
217 conn
218 |> put_resp_header("content-type", "application/activity+json")
219 |> json(ObjectView.render("object.json", %{object: object}))
220 end
221
222 defp represent_activity(_conn, "activity+json", _, _) do
223 {:error, :not_found}
224 end
225
226 defp represent_activity(conn, _, activity, user) do
227 response =
228 activity
229 |> ActivityRepresenter.to_simple_form(user, true)
230 |> ActivityRepresenter.wrap_with_entry()
231 |> :xmerl.export_simple(:xmerl_xml)
232 |> to_string
233
234 conn
235 |> put_resp_content_type("application/atom+xml")
236 |> send_resp(200, response)
237 end
238
239 def errors(conn, {:error, :not_found}) do
240 render_error(conn, :not_found, "Not found")
241 end
242
243 def errors(conn, {:fetch_user, nil}), do: errors(conn, {:error, :not_found})
244
245 def errors(conn, _) do
246 render_error(conn, :internal_server_error, "Something went wrong")
247 end
248 end