Merge branch 'features/validators-note' into 'develop'
[akkoma] / lib / pleroma / web / activity_pub / views / user_view.ex
1 # Pleroma: A lightweight social networking server
2 # Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
3 # SPDX-License-Identifier: AGPL-3.0-only
4
5 defmodule Pleroma.Web.ActivityPub.UserView do
6 use Pleroma.Web, :view
7
8 alias Pleroma.Keys
9 alias Pleroma.Object
10 alias Pleroma.Repo
11 alias Pleroma.User
12 alias Pleroma.Web.ActivityPub.ObjectView
13 alias Pleroma.Web.ActivityPub.Transmogrifier
14 alias Pleroma.Web.ActivityPub.Utils
15 alias Pleroma.Web.Endpoint
16 alias Pleroma.Web.Router.Helpers
17
18 import Ecto.Query
19
20 def render("endpoints.json", %{user: %User{nickname: nil, local: true} = _user}) do
21 %{"sharedInbox" => Helpers.activity_pub_url(Endpoint, :inbox)}
22 end
23
24 def render("endpoints.json", %{user: %User{local: true} = _user}) do
25 %{
26 "oauthAuthorizationEndpoint" => Helpers.o_auth_url(Endpoint, :authorize),
27 "oauthRegistrationEndpoint" => Helpers.app_url(Endpoint, :create),
28 "oauthTokenEndpoint" => Helpers.o_auth_url(Endpoint, :token_exchange),
29 "sharedInbox" => Helpers.activity_pub_url(Endpoint, :inbox),
30 "uploadMedia" => Helpers.activity_pub_url(Endpoint, :upload_media)
31 }
32 end
33
34 def render("endpoints.json", _), do: %{}
35
36 def render("service.json", %{user: user}) do
37 {:ok, user} = User.ensure_keys_present(user)
38 {:ok, _, public_key} = Keys.keys_from_pem(user.keys)
39 public_key = :public_key.pem_entry_encode(:SubjectPublicKeyInfo, public_key)
40 public_key = :public_key.pem_encode([public_key])
41
42 endpoints = render("endpoints.json", %{user: user})
43
44 %{
45 "id" => user.ap_id,
46 "type" => "Application",
47 "following" => "#{user.ap_id}/following",
48 "followers" => "#{user.ap_id}/followers",
49 "inbox" => "#{user.ap_id}/inbox",
50 "name" => "Pleroma",
51 "summary" =>
52 "An internal service actor for this Pleroma instance. No user-serviceable parts inside.",
53 "url" => user.ap_id,
54 "manuallyApprovesFollowers" => false,
55 "publicKey" => %{
56 "id" => "#{user.ap_id}#main-key",
57 "owner" => user.ap_id,
58 "publicKeyPem" => public_key
59 },
60 "endpoints" => endpoints,
61 "invisible" => User.invisible?(user)
62 }
63 |> Map.merge(Utils.make_json_ld_header())
64 end
65
66 # the instance itself is not a Person, but instead an Application
67 def render("user.json", %{user: %User{nickname: nil} = user}),
68 do: render("service.json", %{user: user})
69
70 def render("user.json", %{user: %User{nickname: "internal." <> _} = user}),
71 do: render("service.json", %{user: user}) |> Map.put("preferredUsername", user.nickname)
72
73 def render("user.json", %{user: user}) do
74 {:ok, user} = User.ensure_keys_present(user)
75 {:ok, _, public_key} = Keys.keys_from_pem(user.keys)
76 public_key = :public_key.pem_entry_encode(:SubjectPublicKeyInfo, public_key)
77 public_key = :public_key.pem_encode([public_key])
78 user = User.sanitize_html(user)
79
80 endpoints = render("endpoints.json", %{user: user})
81
82 emoji_tags = Transmogrifier.take_emoji_tags(user)
83
84 fields = Enum.map(user.fields, &Map.put(&1, "type", "PropertyValue"))
85
86 capabilities =
87 if is_boolean(user.accepts_chat_messages) do
88 %{
89 "acceptsChatMessages" => user.accepts_chat_messages
90 }
91 else
92 %{}
93 end
94
95 %{
96 "id" => user.ap_id,
97 "type" => user.actor_type,
98 "following" => "#{user.ap_id}/following",
99 "followers" => "#{user.ap_id}/followers",
100 "inbox" => "#{user.ap_id}/inbox",
101 "outbox" => "#{user.ap_id}/outbox",
102 "featured" => "#{user.ap_id}/collections/featured",
103 "preferredUsername" => user.nickname,
104 "name" => user.name,
105 "summary" => user.bio,
106 "url" => user.ap_id,
107 "manuallyApprovesFollowers" => user.is_locked,
108 "publicKey" => %{
109 "id" => "#{user.ap_id}#main-key",
110 "owner" => user.ap_id,
111 "publicKeyPem" => public_key
112 },
113 "endpoints" => endpoints,
114 "attachment" => fields,
115 "tag" => emoji_tags,
116 # Note: key name is indeed "discoverable" (not an error)
117 "discoverable" => user.is_discoverable,
118 "capabilities" => capabilities,
119 "alsoKnownAs" => user.also_known_as
120 }
121 |> Map.merge(maybe_make_image(&User.avatar_url/2, "icon", user))
122 |> Map.merge(maybe_make_image(&User.banner_url/2, "image", user))
123 |> Map.merge(Utils.make_json_ld_header())
124 end
125
126 def render("following.json", %{user: user, page: page} = opts) do
127 showing_items = (opts[:for] && opts[:for] == user) || !user.hide_follows
128 showing_count = showing_items || !user.hide_follows_count
129
130 query = User.get_friends_query(user)
131 query = from(user in query, select: [:ap_id])
132 following = Repo.all(query)
133
134 total =
135 if showing_count do
136 length(following)
137 else
138 0
139 end
140
141 collection(following, "#{user.ap_id}/following", page, showing_items, total)
142 |> Map.merge(Utils.make_json_ld_header())
143 end
144
145 def render("following.json", %{user: user} = opts) do
146 showing_items = (opts[:for] && opts[:for] == user) || !user.hide_follows
147 showing_count = showing_items || !user.hide_follows_count
148
149 query = User.get_friends_query(user)
150 query = from(user in query, select: [:ap_id])
151 following = Repo.all(query)
152
153 total =
154 if showing_count do
155 length(following)
156 else
157 0
158 end
159
160 %{
161 "id" => "#{user.ap_id}/following",
162 "type" => "OrderedCollection",
163 "totalItems" => total,
164 "first" =>
165 if showing_items do
166 collection(following, "#{user.ap_id}/following", 1, !user.hide_follows)
167 else
168 "#{user.ap_id}/following?page=1"
169 end
170 }
171 |> Map.merge(Utils.make_json_ld_header())
172 end
173
174 def render("followers.json", %{user: user, page: page} = opts) do
175 showing_items = (opts[:for] && opts[:for] == user) || !user.hide_followers
176 showing_count = showing_items || !user.hide_followers_count
177
178 query = User.get_followers_query(user)
179 query = from(user in query, select: [:ap_id])
180 followers = Repo.all(query)
181
182 total =
183 if showing_count do
184 length(followers)
185 else
186 0
187 end
188
189 collection(followers, "#{user.ap_id}/followers", page, showing_items, total)
190 |> Map.merge(Utils.make_json_ld_header())
191 end
192
193 def render("followers.json", %{user: user} = opts) do
194 showing_items = (opts[:for] && opts[:for] == user) || !user.hide_followers
195 showing_count = showing_items || !user.hide_followers_count
196
197 query = User.get_followers_query(user)
198 query = from(user in query, select: [:ap_id])
199 followers = Repo.all(query)
200
201 total =
202 if showing_count do
203 length(followers)
204 else
205 0
206 end
207
208 %{
209 "id" => "#{user.ap_id}/followers",
210 "type" => "OrderedCollection",
211 "first" =>
212 if showing_items do
213 collection(followers, "#{user.ap_id}/followers", 1, showing_items, total)
214 else
215 "#{user.ap_id}/followers?page=1"
216 end
217 }
218 |> maybe_put_total_items(showing_count, total)
219 |> Map.merge(Utils.make_json_ld_header())
220 end
221
222 def render("activity_collection.json", %{iri: iri}) do
223 %{
224 "id" => iri,
225 "type" => "OrderedCollection",
226 "first" => "#{iri}?page=true"
227 }
228 |> Map.merge(Utils.make_json_ld_header())
229 end
230
231 def render("activity_collection_page.json", %{
232 activities: activities,
233 iri: iri,
234 pagination: pagination
235 }) do
236 collection =
237 Enum.map(activities, fn activity ->
238 {:ok, data} = Transmogrifier.prepare_outgoing(activity.data)
239 data
240 end)
241
242 %{
243 "type" => "OrderedCollectionPage",
244 "partOf" => iri,
245 "orderedItems" => collection
246 }
247 |> Map.merge(Utils.make_json_ld_header())
248 |> Map.merge(pagination)
249 end
250
251 def render("featured.json", %{
252 user: %{featured_address: featured_address, pinned_objects: pinned_objects}
253 }) do
254 objects =
255 pinned_objects
256 |> Enum.sort_by(fn {_, pinned_at} -> pinned_at end, &>=/2)
257 |> Enum.map(fn {id, _} ->
258 ObjectView.render("object.json", %{object: Object.get_cached_by_ap_id(id)})
259 end)
260
261 %{
262 "id" => featured_address,
263 "type" => "OrderedCollection",
264 "orderedItems" => objects,
265 "totalItems" => length(objects)
266 }
267 |> Map.merge(Utils.make_json_ld_header())
268 end
269
270 defp maybe_put_total_items(map, false, _total), do: map
271
272 defp maybe_put_total_items(map, true, total) do
273 Map.put(map, "totalItems", total)
274 end
275
276 def collection(collection, iri, page, show_items \\ true, total \\ nil) do
277 offset = (page - 1) * 10
278 items = Enum.slice(collection, offset, 10)
279 items = Enum.map(items, fn user -> user.ap_id end)
280 total = total || length(collection)
281
282 map = %{
283 "id" => "#{iri}?page=#{page}",
284 "type" => "OrderedCollectionPage",
285 "partOf" => iri,
286 "totalItems" => total,
287 "orderedItems" => if(show_items, do: items, else: [])
288 }
289
290 if offset < total do
291 Map.put(map, "next", "#{iri}?page=#{page + 1}")
292 else
293 map
294 end
295 end
296
297 defp maybe_make_image(func, key, user) do
298 if image = func.(user, no_default: true) do
299 %{
300 key => %{
301 "type" => "Image",
302 "url" => image
303 }
304 }
305 else
306 %{}
307 end
308 end
309 end