total rebase
[anni] / lib / pleroma / web / activity_pub / views / user_view.ex
1 # Pleroma: A lightweight social networking server
2 # Copyright © 2017-2022 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, _, public_key} = Keys.keys_from_pem(user.keys)
38     public_key = :public_key.pem_entry_encode(:SubjectPublicKeyInfo, public_key)
39     public_key = :public_key.pem_encode([public_key])
40
41     endpoints = render("endpoints.json", %{user: user})
42
43     %{
44       "id" => user.ap_id,
45       "type" => "Application",
46       "following" => "#{user.ap_id}/following",
47       "followers" => "#{user.ap_id}/followers",
48       "inbox" => "#{user.ap_id}/inbox",
49       "outbox" => "#{user.ap_id}/outbox",
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}) do
71     render("service.json", %{user: user})
72     |> Map.merge(%{
73       "preferredUsername" => user.nickname,
74       "webfinger" => "acct:#{User.full_nickname(user)}"
75     })
76   end
77
78   def render("user.json", %{user: user}) do
79     {:ok, _, public_key} = Keys.keys_from_pem(user.keys)
80     public_key = :public_key.pem_entry_encode(:SubjectPublicKeyInfo, public_key)
81     public_key = :public_key.pem_encode([public_key])
82     user = User.sanitize_html(user)
83
84     endpoints = render("endpoints.json", %{user: user})
85
86     emoji_tags = Transmogrifier.take_emoji_tags(user)
87
88     fields = Enum.map(user.fields, &Map.put(&1, "type", "PropertyValue"))
89
90     capabilities =
91       if is_boolean(user.accepts_chat_messages) do
92         %{
93           "acceptsChatMessages" => user.accepts_chat_messages
94         }
95       else
96         %{}
97       end
98
99     birthday =
100       if user.show_birthday && user.birthday,
101         do: Date.to_iso8601(user.birthday),
102         else: nil
103
104     %{
105       "id" => user.ap_id,
106       "type" => user.actor_type,
107       "following" => "#{user.ap_id}/following",
108       "followers" => "#{user.ap_id}/followers",
109       "inbox" => "#{user.ap_id}/inbox",
110       "outbox" => "#{user.ap_id}/outbox",
111       "featured" => "#{user.ap_id}/collections/featured",
112       "preferredUsername" => user.nickname,
113       "name" => user.name,
114       "summary" => user.bio,
115       "url" => user.ap_id,
116       "manuallyApprovesFollowers" => user.is_locked,
117       "publicKey" => %{
118         "id" => "#{user.ap_id}#main-key",
119         "owner" => user.ap_id,
120         "publicKeyPem" => public_key
121       },
122       "endpoints" => endpoints,
123       "attachment" => fields,
124       "tag" => emoji_tags,
125       # Note: key name is indeed "discoverable" (not an error)
126       "discoverable" => user.is_discoverable,
127       "capabilities" => capabilities,
128       "alsoKnownAs" => user.also_known_as,
129       "vcard:bday" => birthday,
130       "webfinger" => "acct:#{User.full_nickname(user)}"
131     }
132     |> Map.merge(maybe_make_image(&User.avatar_url/2, "icon", user))
133     |> Map.merge(maybe_make_image(&User.banner_url/2, "image", user))
134     |> Map.merge(Utils.make_json_ld_header())
135   end
136
137   def render("following.json", %{user: user, page: page} = opts) do
138     showing_items = (opts[:for] && opts[:for] == user) || !user.hide_follows
139     showing_count = showing_items || !user.hide_follows_count
140
141     query = User.get_friends_query(user)
142     query = from(user in query, select: [:ap_id])
143     following = Repo.all(query)
144
145     total =
146       if showing_count do
147         length(following)
148       else
149         0
150       end
151
152     collection(following, "#{user.ap_id}/following", page, showing_items, total)
153     |> Map.merge(Utils.make_json_ld_header())
154   end
155
156   def render("following.json", %{user: user} = opts) do
157     showing_items = (opts[:for] && opts[:for] == user) || !user.hide_follows
158     showing_count = showing_items || !user.hide_follows_count
159
160     query = User.get_friends_query(user)
161     query = from(user in query, select: [:ap_id])
162     following = Repo.all(query)
163
164     total =
165       if showing_count do
166         length(following)
167       else
168         0
169       end
170
171     %{
172       "id" => "#{user.ap_id}/following",
173       "type" => "OrderedCollection",
174       "totalItems" => total,
175       "first" =>
176         if showing_items do
177           collection(following, "#{user.ap_id}/following", 1, !user.hide_follows)
178         else
179           "#{user.ap_id}/following?page=1"
180         end
181     }
182     |> Map.merge(Utils.make_json_ld_header())
183   end
184
185   def render("followers.json", %{user: user, page: page} = opts) do
186     showing_items = (opts[:for] && opts[:for] == user) || !user.hide_followers
187     showing_count = showing_items || !user.hide_followers_count
188
189     query = User.get_followers_query(user)
190     query = from(user in query, select: [:ap_id])
191     followers = Repo.all(query)
192
193     total =
194       if showing_count do
195         length(followers)
196       else
197         0
198       end
199
200     collection(followers, "#{user.ap_id}/followers", page, showing_items, total)
201     |> Map.merge(Utils.make_json_ld_header())
202   end
203
204   def render("followers.json", %{user: user} = opts) do
205     showing_items = (opts[:for] && opts[:for] == user) || !user.hide_followers
206     showing_count = showing_items || !user.hide_followers_count
207
208     query = User.get_followers_query(user)
209     query = from(user in query, select: [:ap_id])
210     followers = Repo.all(query)
211
212     total =
213       if showing_count do
214         length(followers)
215       else
216         0
217       end
218
219     %{
220       "id" => "#{user.ap_id}/followers",
221       "type" => "OrderedCollection",
222       "first" =>
223         if showing_items do
224           collection(followers, "#{user.ap_id}/followers", 1, showing_items, total)
225         else
226           "#{user.ap_id}/followers?page=1"
227         end
228     }
229     |> maybe_put_total_items(showing_count, total)
230     |> Map.merge(Utils.make_json_ld_header())
231   end
232
233   def render("activity_collection.json", %{iri: iri}) do
234     %{
235       "id" => iri,
236       "type" => "OrderedCollection",
237       "first" => "#{iri}?page=true"
238     }
239     |> Map.merge(Utils.make_json_ld_header())
240   end
241
242   def render("activity_collection_page.json", %{
243         activities: activities,
244         iri: iri,
245         pagination: pagination
246       }) do
247     collection =
248       Enum.map(activities, fn activity ->
249         {:ok, data} = Transmogrifier.prepare_outgoing(activity.data)
250         data
251       end)
252
253     %{
254       "type" => "OrderedCollectionPage",
255       "partOf" => iri,
256       "orderedItems" => collection
257     }
258     |> Map.merge(Utils.make_json_ld_header())
259     |> Map.merge(pagination)
260   end
261
262   def render("featured.json", %{
263         user: %{featured_address: featured_address, pinned_objects: pinned_objects}
264       }) do
265     objects =
266       pinned_objects
267       |> Enum.sort_by(fn {_, pinned_at} -> pinned_at end, &>=/2)
268       |> Enum.map(fn {id, _} ->
269         ObjectView.render("object.json", %{object: Object.get_cached_by_ap_id(id)})
270       end)
271
272     %{
273       "id" => featured_address,
274       "type" => "OrderedCollection",
275       "orderedItems" => objects,
276       "totalItems" => length(objects)
277     }
278     |> Map.merge(Utils.make_json_ld_header())
279   end
280
281   defp maybe_put_total_items(map, false, _total), do: map
282
283   defp maybe_put_total_items(map, true, total) do
284     Map.put(map, "totalItems", total)
285   end
286
287   def collection(collection, iri, page, show_items \\ true, total \\ nil) do
288     offset = (page - 1) * 10
289     items = Enum.slice(collection, offset, 10)
290     items = Enum.map(items, fn user -> user.ap_id end)
291     total = total || length(collection)
292
293     map = %{
294       "id" => "#{iri}?page=#{page}",
295       "type" => "OrderedCollectionPage",
296       "partOf" => iri,
297       "totalItems" => total,
298       "orderedItems" => if(show_items, do: items, else: [])
299     }
300
301     if offset < total do
302       Map.put(map, "next", "#{iri}?page=#{page + 1}")
303     else
304       map
305     end
306   end
307
308   defp maybe_make_image(func, key, user) do
309     if image = func.(user, no_default: true) do
310       %{
311         key => %{
312           "type" => "Image",
313           "url" => image
314         }
315       }
316     else
317       %{}
318     end
319   end
320 end