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