AP UserView: Refactor banner / avatar display code, add test.
[akkoma] / lib / pleroma / user.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.User do
6 use Ecto.Schema
7
8 import Ecto.Changeset
9 import Ecto.Query
10
11 alias Comeonin.Pbkdf2
12 alias Pleroma.Activity
13 alias Pleroma.Formatter
14 alias Pleroma.Notification
15 alias Pleroma.Object
16 alias Pleroma.Repo
17 alias Pleroma.User
18 alias Pleroma.Web
19 alias Pleroma.Web.ActivityPub.ActivityPub
20 alias Pleroma.Web.ActivityPub.Utils
21 alias Pleroma.Web.CommonAPI.Utils, as: CommonUtils
22 alias Pleroma.Web.OAuth
23 alias Pleroma.Web.OStatus
24 alias Pleroma.Web.RelMe
25 alias Pleroma.Web.Websub
26
27 require Logger
28
29 @type t :: %__MODULE__{}
30
31 @primary_key {:id, Pleroma.FlakeId, autogenerate: true}
32
33 # credo:disable-for-next-line Credo.Check.Readability.MaxLineLength
34 @email_regex ~r/^[a-zA-Z0-9.!#$%&'*+\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/
35
36 @strict_local_nickname_regex ~r/^[a-zA-Z\d]+$/
37 @extended_local_nickname_regex ~r/^[a-zA-Z\d_-]+$/
38
39 schema "users" do
40 field(:bio, :string)
41 field(:email, :string)
42 field(:name, :string)
43 field(:nickname, :string)
44 field(:password_hash, :string)
45 field(:password, :string, virtual: true)
46 field(:password_confirmation, :string, virtual: true)
47 field(:following, {:array, :string}, default: [])
48 field(:ap_id, :string)
49 field(:avatar, :map)
50 field(:local, :boolean, default: true)
51 field(:follower_address, :string)
52 field(:search_rank, :float, virtual: true)
53 field(:search_type, :integer, virtual: true)
54 field(:tags, {:array, :string}, default: [])
55 field(:bookmarks, {:array, :string}, default: [])
56 field(:last_refreshed_at, :naive_datetime_usec)
57 has_many(:notifications, Notification)
58 embeds_one(:info, Pleroma.User.Info)
59
60 timestamps()
61 end
62
63 def auth_active?(%User{local: false}), do: true
64
65 def auth_active?(%User{info: %User.Info{confirmation_pending: false}}), do: true
66
67 def auth_active?(%User{info: %User.Info{confirmation_pending: true}}),
68 do: !Pleroma.Config.get([:instance, :account_activation_required])
69
70 def auth_active?(_), do: false
71
72 def visible_for?(user, for_user \\ nil)
73
74 def visible_for?(%User{id: user_id}, %User{id: for_id}) when user_id == for_id, do: true
75
76 def visible_for?(%User{} = user, for_user) do
77 auth_active?(user) || superuser?(for_user)
78 end
79
80 def visible_for?(_, _), do: false
81
82 def superuser?(%User{local: true, info: %User.Info{is_admin: true}}), do: true
83 def superuser?(%User{local: true, info: %User.Info{is_moderator: true}}), do: true
84 def superuser?(_), do: false
85
86 def avatar_url(user, options \\ []) do
87 case user.avatar do
88 %{"url" => [%{"href" => href} | _]} -> href
89 _ -> !options[:no_default] && "#{Web.base_url()}/images/avi.png"
90 end
91 end
92
93 def banner_url(user, options \\ []) do
94 case user.info.banner do
95 %{"url" => [%{"href" => href} | _]} -> href
96 _ -> !options[:no_default] && "#{Web.base_url()}/images/banner.png"
97 end
98 end
99
100 def profile_url(%User{info: %{source_data: %{"url" => url}}}), do: url
101 def profile_url(%User{ap_id: ap_id}), do: ap_id
102 def profile_url(_), do: nil
103
104 def ap_id(%User{nickname: nickname}) do
105 "#{Web.base_url()}/users/#{nickname}"
106 end
107
108 def ap_followers(%User{follower_address: fa}) when is_binary(fa), do: fa
109 def ap_followers(%User{} = user), do: "#{ap_id(user)}/followers"
110
111 def user_info(%User{} = user) do
112 oneself = if user.local, do: 1, else: 0
113
114 %{
115 following_count: length(user.following) - oneself,
116 note_count: user.info.note_count,
117 follower_count: user.info.follower_count,
118 locked: user.info.locked,
119 confirmation_pending: user.info.confirmation_pending,
120 default_scope: user.info.default_scope
121 }
122 end
123
124 def remote_user_creation(params) do
125 params =
126 params
127 |> Map.put(:info, params[:info] || %{})
128
129 info_cng = User.Info.remote_user_creation(%User.Info{}, params[:info])
130
131 changes =
132 %User{}
133 |> cast(params, [:bio, :name, :ap_id, :nickname, :avatar])
134 |> validate_required([:name, :ap_id])
135 |> unique_constraint(:nickname)
136 |> validate_format(:nickname, @email_regex)
137 |> validate_length(:bio, max: 5000)
138 |> validate_length(:name, max: 100)
139 |> put_change(:local, false)
140 |> put_embed(:info, info_cng)
141
142 if changes.valid? do
143 case info_cng.changes[:source_data] do
144 %{"followers" => followers} ->
145 changes
146 |> put_change(:follower_address, followers)
147
148 _ ->
149 followers = User.ap_followers(%User{nickname: changes.changes[:nickname]})
150
151 changes
152 |> put_change(:follower_address, followers)
153 end
154 else
155 changes
156 end
157 end
158
159 def update_changeset(struct, params \\ %{}) do
160 struct
161 |> cast(params, [:bio, :name, :avatar])
162 |> unique_constraint(:nickname)
163 |> validate_format(:nickname, local_nickname_regex())
164 |> validate_length(:bio, max: 5000)
165 |> validate_length(:name, min: 1, max: 100)
166 end
167
168 def upgrade_changeset(struct, params \\ %{}) do
169 params =
170 params
171 |> Map.put(:last_refreshed_at, NaiveDateTime.utc_now())
172
173 info_cng =
174 struct.info
175 |> User.Info.user_upgrade(params[:info])
176
177 struct
178 |> cast(params, [:bio, :name, :follower_address, :avatar, :last_refreshed_at])
179 |> unique_constraint(:nickname)
180 |> validate_format(:nickname, local_nickname_regex())
181 |> validate_length(:bio, max: 5000)
182 |> validate_length(:name, max: 100)
183 |> put_embed(:info, info_cng)
184 end
185
186 def password_update_changeset(struct, params) do
187 changeset =
188 struct
189 |> cast(params, [:password, :password_confirmation])
190 |> validate_required([:password, :password_confirmation])
191 |> validate_confirmation(:password)
192
193 OAuth.Token.delete_user_tokens(struct)
194 OAuth.Authorization.delete_user_authorizations(struct)
195
196 if changeset.valid? do
197 hashed = Pbkdf2.hashpwsalt(changeset.changes[:password])
198
199 changeset
200 |> put_change(:password_hash, hashed)
201 else
202 changeset
203 end
204 end
205
206 def reset_password(user, data) do
207 update_and_set_cache(password_update_changeset(user, data))
208 end
209
210 def register_changeset(struct, params \\ %{}, opts \\ []) do
211 confirmation_status =
212 if opts[:confirmed] || !Pleroma.Config.get([:instance, :account_activation_required]) do
213 :confirmed
214 else
215 :unconfirmed
216 end
217
218 info_change = User.Info.confirmation_changeset(%User.Info{}, confirmation_status)
219
220 changeset =
221 struct
222 |> cast(params, [:bio, :email, :name, :nickname, :password, :password_confirmation])
223 |> validate_required([:email, :name, :nickname, :password, :password_confirmation])
224 |> validate_confirmation(:password)
225 |> unique_constraint(:email)
226 |> unique_constraint(:nickname)
227 |> validate_exclusion(:nickname, Pleroma.Config.get([Pleroma.User, :restricted_nicknames]))
228 |> validate_format(:nickname, local_nickname_regex())
229 |> validate_format(:email, @email_regex)
230 |> validate_length(:bio, max: 1000)
231 |> validate_length(:name, min: 1, max: 100)
232 |> put_change(:info, info_change)
233
234 if changeset.valid? do
235 hashed = Pbkdf2.hashpwsalt(changeset.changes[:password])
236 ap_id = User.ap_id(%User{nickname: changeset.changes[:nickname]})
237 followers = User.ap_followers(%User{nickname: changeset.changes[:nickname]})
238
239 changeset
240 |> put_change(:password_hash, hashed)
241 |> put_change(:ap_id, ap_id)
242 |> unique_constraint(:ap_id)
243 |> put_change(:following, [followers])
244 |> put_change(:follower_address, followers)
245 else
246 changeset
247 end
248 end
249
250 defp autofollow_users(user) do
251 candidates = Pleroma.Config.get([:instance, :autofollowed_nicknames])
252
253 autofollowed_users =
254 from(u in User,
255 where: u.local == true,
256 where: u.nickname in ^candidates
257 )
258 |> Repo.all()
259
260 follow_all(user, autofollowed_users)
261 end
262
263 @doc "Inserts provided changeset, performs post-registration actions (confirmation email sending etc.)"
264 def register(%Ecto.Changeset{} = changeset) do
265 with {:ok, user} <- Repo.insert(changeset),
266 {:ok, user} <- autofollow_users(user),
267 {:ok, _} <- Pleroma.User.WelcomeMessage.post_welcome_message_to_user(user),
268 {:ok, _} <- try_send_confirmation_email(user) do
269 {:ok, user}
270 end
271 end
272
273 def try_send_confirmation_email(%User{} = user) do
274 if user.info.confirmation_pending &&
275 Pleroma.Config.get([:instance, :account_activation_required]) do
276 user
277 |> Pleroma.UserEmail.account_confirmation_email()
278 |> Pleroma.Mailer.deliver_async()
279 else
280 {:ok, :noop}
281 end
282 end
283
284 def needs_update?(%User{local: true}), do: false
285
286 def needs_update?(%User{local: false, last_refreshed_at: nil}), do: true
287
288 def needs_update?(%User{local: false} = user) do
289 NaiveDateTime.diff(NaiveDateTime.utc_now(), user.last_refreshed_at) >= 86_400
290 end
291
292 def needs_update?(_), do: true
293
294 def maybe_direct_follow(%User{} = follower, %User{local: true, info: %{locked: true}}) do
295 {:ok, follower}
296 end
297
298 def maybe_direct_follow(%User{} = follower, %User{local: true} = followed) do
299 follow(follower, followed)
300 end
301
302 def maybe_direct_follow(%User{} = follower, %User{} = followed) do
303 if not User.ap_enabled?(followed) do
304 follow(follower, followed)
305 else
306 {:ok, follower}
307 end
308 end
309
310 def maybe_follow(%User{} = follower, %User{info: _info} = followed) do
311 if not following?(follower, followed) do
312 follow(follower, followed)
313 else
314 {:ok, follower}
315 end
316 end
317
318 @doc "A mass follow for local users. Respects blocks in both directions but does not create activities."
319 @spec follow_all(User.t(), list(User.t())) :: {atom(), User.t()}
320 def follow_all(follower, followeds) do
321 followed_addresses =
322 followeds
323 |> Enum.reject(fn followed -> blocks?(follower, followed) || blocks?(followed, follower) end)
324 |> Enum.map(fn %{follower_address: fa} -> fa end)
325
326 q =
327 from(u in User,
328 where: u.id == ^follower.id,
329 update: [
330 set: [
331 following:
332 fragment(
333 "array(select distinct unnest (array_cat(?, ?)))",
334 u.following,
335 ^followed_addresses
336 )
337 ]
338 ],
339 select: u
340 )
341
342 {1, [follower]} = Repo.update_all(q, [])
343
344 Enum.each(followeds, fn followed ->
345 update_follower_count(followed)
346 end)
347
348 set_cache(follower)
349 end
350
351 def follow(%User{} = follower, %User{info: info} = followed) do
352 user_config = Application.get_env(:pleroma, :user)
353 deny_follow_blocked = Keyword.get(user_config, :deny_follow_blocked)
354
355 ap_followers = followed.follower_address
356
357 cond do
358 following?(follower, followed) or info.deactivated ->
359 {:error, "Could not follow user: #{followed.nickname} is already on your list."}
360
361 deny_follow_blocked and blocks?(followed, follower) ->
362 {:error, "Could not follow user: #{followed.nickname} blocked you."}
363
364 true ->
365 if !followed.local && follower.local && !ap_enabled?(followed) do
366 Websub.subscribe(follower, followed)
367 end
368
369 q =
370 from(u in User,
371 where: u.id == ^follower.id,
372 update: [push: [following: ^ap_followers]],
373 select: u
374 )
375
376 {1, [follower]} = Repo.update_all(q, [])
377
378 {:ok, _} = update_follower_count(followed)
379
380 set_cache(follower)
381 end
382 end
383
384 def unfollow(%User{} = follower, %User{} = followed) do
385 ap_followers = followed.follower_address
386
387 if following?(follower, followed) and follower.ap_id != followed.ap_id do
388 q =
389 from(u in User,
390 where: u.id == ^follower.id,
391 update: [pull: [following: ^ap_followers]],
392 select: u
393 )
394
395 {1, [follower]} = Repo.update_all(q, [])
396
397 {:ok, followed} = update_follower_count(followed)
398
399 set_cache(follower)
400
401 {:ok, follower, Utils.fetch_latest_follow(follower, followed)}
402 else
403 {:error, "Not subscribed!"}
404 end
405 end
406
407 @spec following?(User.t(), User.t()) :: boolean
408 def following?(%User{} = follower, %User{} = followed) do
409 Enum.member?(follower.following, followed.follower_address)
410 end
411
412 def follow_import(%User{} = follower, followed_identifiers)
413 when is_list(followed_identifiers) do
414 Enum.map(
415 followed_identifiers,
416 fn followed_identifier ->
417 with %User{} = followed <- get_or_fetch(followed_identifier),
418 {:ok, follower} <- maybe_direct_follow(follower, followed),
419 {:ok, _} <- ActivityPub.follow(follower, followed) do
420 followed
421 else
422 err ->
423 Logger.debug("follow_import failed for #{followed_identifier} with: #{inspect(err)}")
424 err
425 end
426 end
427 )
428 end
429
430 def locked?(%User{} = user) do
431 user.info.locked || false
432 end
433
434 def get_by_id(id) do
435 Repo.get_by(User, id: id)
436 end
437
438 def get_by_ap_id(ap_id) do
439 Repo.get_by(User, ap_id: ap_id)
440 end
441
442 # This is mostly an SPC migration fix. This guesses the user nickname by taking the last part
443 # of the ap_id and the domain and tries to get that user
444 def get_by_guessed_nickname(ap_id) do
445 domain = URI.parse(ap_id).host
446 name = List.last(String.split(ap_id, "/"))
447 nickname = "#{name}@#{domain}"
448
449 get_by_nickname(nickname)
450 end
451
452 def set_cache(user) do
453 Cachex.put(:user_cache, "ap_id:#{user.ap_id}", user)
454 Cachex.put(:user_cache, "nickname:#{user.nickname}", user)
455 Cachex.put(:user_cache, "user_info:#{user.id}", user_info(user))
456 {:ok, user}
457 end
458
459 def update_and_set_cache(changeset) do
460 with {:ok, user} <- Repo.update(changeset) do
461 set_cache(user)
462 else
463 e -> e
464 end
465 end
466
467 def invalidate_cache(user) do
468 Cachex.del(:user_cache, "ap_id:#{user.ap_id}")
469 Cachex.del(:user_cache, "nickname:#{user.nickname}")
470 Cachex.del(:user_cache, "user_info:#{user.id}")
471 end
472
473 def get_cached_by_ap_id(ap_id) do
474 key = "ap_id:#{ap_id}"
475 Cachex.fetch!(:user_cache, key, fn _ -> get_by_ap_id(ap_id) end)
476 end
477
478 def get_cached_by_id(id) do
479 key = "id:#{id}"
480
481 ap_id =
482 Cachex.fetch!(:user_cache, key, fn _ ->
483 user = get_by_id(id)
484
485 if user do
486 Cachex.put(:user_cache, "ap_id:#{user.ap_id}", user)
487 {:commit, user.ap_id}
488 else
489 {:ignore, ""}
490 end
491 end)
492
493 get_cached_by_ap_id(ap_id)
494 end
495
496 def get_cached_by_nickname(nickname) do
497 key = "nickname:#{nickname}"
498 Cachex.fetch!(:user_cache, key, fn _ -> get_or_fetch_by_nickname(nickname) end)
499 end
500
501 def get_cached_by_nickname_or_id(nickname_or_id) do
502 get_cached_by_id(nickname_or_id) || get_cached_by_nickname(nickname_or_id)
503 end
504
505 def get_by_nickname(nickname) do
506 Repo.get_by(User, nickname: nickname) ||
507 if Regex.match?(~r(@#{Pleroma.Web.Endpoint.host()})i, nickname) do
508 Repo.get_by(User, nickname: local_nickname(nickname))
509 end
510 end
511
512 def get_by_nickname_or_email(nickname_or_email) do
513 case user = Repo.get_by(User, nickname: nickname_or_email) do
514 %User{} -> user
515 nil -> Repo.get_by(User, email: nickname_or_email)
516 end
517 end
518
519 def get_cached_user_info(user) do
520 key = "user_info:#{user.id}"
521 Cachex.fetch!(:user_cache, key, fn _ -> user_info(user) end)
522 end
523
524 def fetch_by_nickname(nickname) do
525 ap_try = ActivityPub.make_user_from_nickname(nickname)
526
527 case ap_try do
528 {:ok, user} -> {:ok, user}
529 _ -> OStatus.make_user(nickname)
530 end
531 end
532
533 def get_or_fetch_by_nickname(nickname) do
534 with %User{} = user <- get_by_nickname(nickname) do
535 user
536 else
537 _e ->
538 with [_nick, _domain] <- String.split(nickname, "@"),
539 {:ok, user} <- fetch_by_nickname(nickname) do
540 if Pleroma.Config.get([:fetch_initial_posts, :enabled]) do
541 {:ok, _} = Task.start(__MODULE__, :fetch_initial_posts, [user])
542 end
543
544 user
545 else
546 _e -> nil
547 end
548 end
549 end
550
551 @doc "Fetch some posts when the user has just been federated with"
552 def fetch_initial_posts(user) do
553 pages = Pleroma.Config.get!([:fetch_initial_posts, :pages])
554
555 Enum.each(
556 # Insert all the posts in reverse order, so they're in the right order on the timeline
557 Enum.reverse(Utils.fetch_ordered_collection(user.info.source_data["outbox"], pages)),
558 &Pleroma.Web.Federator.incoming_ap_doc/1
559 )
560 end
561
562 def get_followers_query(%User{id: id, follower_address: follower_address}, nil) do
563 from(
564 u in User,
565 where: fragment("? <@ ?", ^[follower_address], u.following),
566 where: u.id != ^id
567 )
568 end
569
570 def get_followers_query(user, page) do
571 from(u in get_followers_query(user, nil))
572 |> paginate(page, 20)
573 end
574
575 def get_followers_query(user), do: get_followers_query(user, nil)
576
577 def get_followers(user, page \\ nil) do
578 q = get_followers_query(user, page)
579
580 {:ok, Repo.all(q)}
581 end
582
583 def get_followers_ids(user, page \\ nil) do
584 q = get_followers_query(user, page)
585
586 Repo.all(from(u in q, select: u.id))
587 end
588
589 def get_friends_query(%User{id: id, following: following}, nil) do
590 from(
591 u in User,
592 where: u.follower_address in ^following,
593 where: u.id != ^id
594 )
595 end
596
597 def get_friends_query(user, page) do
598 from(u in get_friends_query(user, nil))
599 |> paginate(page, 20)
600 end
601
602 def get_friends_query(user), do: get_friends_query(user, nil)
603
604 def get_friends(user, page \\ nil) do
605 q = get_friends_query(user, page)
606
607 {:ok, Repo.all(q)}
608 end
609
610 def get_friends_ids(user, page \\ nil) do
611 q = get_friends_query(user, page)
612
613 Repo.all(from(u in q, select: u.id))
614 end
615
616 def get_follow_requests_query(%User{} = user) do
617 from(
618 a in Activity,
619 where:
620 fragment(
621 "? ->> 'type' = 'Follow'",
622 a.data
623 ),
624 where:
625 fragment(
626 "? ->> 'state' = 'pending'",
627 a.data
628 ),
629 where:
630 fragment(
631 "coalesce((?)->'object'->>'id', (?)->>'object') = ?",
632 a.data,
633 a.data,
634 ^user.ap_id
635 )
636 )
637 end
638
639 def get_follow_requests(%User{} = user) do
640 users =
641 user
642 |> User.get_follow_requests_query()
643 |> join(:inner, [a], u in User, on: a.actor == u.ap_id)
644 |> where([a, u], not fragment("? @> ?", u.following, ^[user.follower_address]))
645 |> group_by([a, u], u.id)
646 |> select([a, u], u)
647 |> Repo.all()
648
649 {:ok, users}
650 end
651
652 def increase_note_count(%User{} = user) do
653 User
654 |> where(id: ^user.id)
655 |> update([u],
656 set: [
657 info:
658 fragment(
659 "jsonb_set(?, '{note_count}', ((?->>'note_count')::int + 1)::varchar::jsonb, true)",
660 u.info,
661 u.info
662 )
663 ]
664 )
665 |> select([u], u)
666 |> Repo.update_all([])
667 |> case do
668 {1, [user]} -> set_cache(user)
669 _ -> {:error, user}
670 end
671 end
672
673 def decrease_note_count(%User{} = user) do
674 User
675 |> where(id: ^user.id)
676 |> update([u],
677 set: [
678 info:
679 fragment(
680 "jsonb_set(?, '{note_count}', (greatest(0, (?->>'note_count')::int - 1))::varchar::jsonb, true)",
681 u.info,
682 u.info
683 )
684 ]
685 )
686 |> select([u], u)
687 |> Repo.update_all([])
688 |> case do
689 {1, [user]} -> set_cache(user)
690 _ -> {:error, user}
691 end
692 end
693
694 def update_note_count(%User{} = user) do
695 note_count_query =
696 from(
697 a in Object,
698 where: fragment("?->>'actor' = ? and ?->>'type' = 'Note'", a.data, ^user.ap_id, a.data),
699 select: count(a.id)
700 )
701
702 note_count = Repo.one(note_count_query)
703
704 info_cng = User.Info.set_note_count(user.info, note_count)
705
706 cng =
707 change(user)
708 |> put_embed(:info, info_cng)
709
710 update_and_set_cache(cng)
711 end
712
713 def update_follower_count(%User{} = user) do
714 follower_count_query =
715 User
716 |> where([u], ^user.follower_address in u.following)
717 |> where([u], u.id != ^user.id)
718 |> select([u], %{count: count(u.id)})
719
720 User
721 |> where(id: ^user.id)
722 |> join(:inner, [u], s in subquery(follower_count_query))
723 |> update([u, s],
724 set: [
725 info:
726 fragment(
727 "jsonb_set(?, '{follower_count}', ?::varchar::jsonb, true)",
728 u.info,
729 s.count
730 )
731 ]
732 )
733 |> select([u], u)
734 |> Repo.update_all([])
735 |> case do
736 {1, [user]} -> set_cache(user)
737 _ -> {:error, user}
738 end
739 end
740
741 def get_users_from_set_query(ap_ids, false) do
742 from(
743 u in User,
744 where: u.ap_id in ^ap_ids
745 )
746 end
747
748 def get_users_from_set_query(ap_ids, true) do
749 query = get_users_from_set_query(ap_ids, false)
750
751 from(
752 u in query,
753 where: u.local == true
754 )
755 end
756
757 def get_users_from_set(ap_ids, local_only \\ true) do
758 get_users_from_set_query(ap_ids, local_only)
759 |> Repo.all()
760 end
761
762 def get_recipients_from_activity(%Activity{recipients: to}) do
763 query =
764 from(
765 u in User,
766 where: u.ap_id in ^to,
767 or_where: fragment("? && ?", u.following, ^to)
768 )
769
770 query = from(u in query, where: u.local == true)
771
772 Repo.all(query)
773 end
774
775 @spec search_for_admin(%{
776 local: boolean(),
777 page: number(),
778 page_size: number()
779 }) :: {:ok, [Pleroma.User.t()], number()}
780 def search_for_admin(%{query: nil, local: local, page: page, page_size: page_size}) do
781 query =
782 from(u in User, order_by: u.nickname)
783 |> maybe_local_user_query(local)
784
785 paginated_query =
786 query
787 |> paginate(page, page_size)
788
789 count =
790 query
791 |> Repo.aggregate(:count, :id)
792
793 {:ok, Repo.all(paginated_query), count}
794 end
795
796 @spec search_for_admin(%{
797 query: binary(),
798 local: boolean(),
799 page: number(),
800 page_size: number()
801 }) :: {:ok, [Pleroma.User.t()], number()}
802 def search_for_admin(%{
803 query: term,
804 local: local,
805 page: page,
806 page_size: page_size
807 }) do
808 maybe_local_query = User |> maybe_local_user_query(local)
809
810 search_query = from(u in maybe_local_query, where: ilike(u.nickname, ^"%#{term}%"))
811 count = search_query |> Repo.aggregate(:count, :id)
812
813 results =
814 search_query
815 |> paginate(page, page_size)
816 |> Repo.all()
817
818 {:ok, results, count}
819 end
820
821 def search(query, resolve \\ false, for_user \\ nil) do
822 # Strip the beginning @ off if there is a query
823 query = String.trim_leading(query, "@")
824
825 if resolve, do: get_or_fetch(query)
826
827 {:ok, results} =
828 Repo.transaction(fn ->
829 Ecto.Adapters.SQL.query(Repo, "select set_limit(0.25)", [])
830 Repo.all(search_query(query, for_user))
831 end)
832
833 results
834 end
835
836 def search_query(query, for_user) do
837 fts_subquery = fts_search_subquery(query)
838 trigram_subquery = trigram_search_subquery(query)
839 union_query = from(s in trigram_subquery, union_all: ^fts_subquery)
840 distinct_query = from(s in subquery(union_query), order_by: s.search_type, distinct: s.id)
841
842 from(s in subquery(boost_search_rank_query(distinct_query, for_user)),
843 order_by: [desc: s.search_rank],
844 limit: 20
845 )
846 end
847
848 defp boost_search_rank_query(query, nil), do: query
849
850 defp boost_search_rank_query(query, for_user) do
851 friends_ids = get_friends_ids(for_user)
852 followers_ids = get_followers_ids(for_user)
853
854 from(u in subquery(query),
855 select_merge: %{
856 search_rank:
857 fragment(
858 """
859 CASE WHEN (?) THEN (?) * 1.3
860 WHEN (?) THEN (?) * 1.2
861 WHEN (?) THEN (?) * 1.1
862 ELSE (?) END
863 """,
864 u.id in ^friends_ids and u.id in ^followers_ids,
865 u.search_rank,
866 u.id in ^friends_ids,
867 u.search_rank,
868 u.id in ^followers_ids,
869 u.search_rank,
870 u.search_rank
871 )
872 }
873 )
874 end
875
876 defp fts_search_subquery(term, query \\ User) do
877 processed_query =
878 term
879 |> String.replace(~r/\W+/, " ")
880 |> String.trim()
881 |> String.split()
882 |> Enum.map(&(&1 <> ":*"))
883 |> Enum.join(" | ")
884
885 from(
886 u in query,
887 select_merge: %{
888 search_type: ^0,
889 search_rank:
890 fragment(
891 """
892 ts_rank_cd(
893 setweight(to_tsvector('simple', regexp_replace(?, '\\W', ' ', 'g')), 'A') ||
894 setweight(to_tsvector('simple', regexp_replace(coalesce(?, ''), '\\W', ' ', 'g')), 'B'),
895 to_tsquery('simple', ?),
896 32
897 )
898 """,
899 u.nickname,
900 u.name,
901 ^processed_query
902 )
903 },
904 where:
905 fragment(
906 """
907 (setweight(to_tsvector('simple', regexp_replace(?, '\\W', ' ', 'g')), 'A') ||
908 setweight(to_tsvector('simple', regexp_replace(coalesce(?, ''), '\\W', ' ', 'g')), 'B')) @@ to_tsquery('simple', ?)
909 """,
910 u.nickname,
911 u.name,
912 ^processed_query
913 )
914 )
915 end
916
917 defp trigram_search_subquery(term) do
918 from(
919 u in User,
920 select_merge: %{
921 # ^1 gives 'Postgrex expected a binary, got 1' for some weird reason
922 search_type: fragment("?", 1),
923 search_rank:
924 fragment(
925 "similarity(?, trim(? || ' ' || coalesce(?, '')))",
926 ^term,
927 u.nickname,
928 u.name
929 )
930 },
931 where: fragment("trim(? || ' ' || coalesce(?, '')) % ?", u.nickname, u.name, ^term)
932 )
933 end
934
935 def blocks_import(%User{} = blocker, blocked_identifiers) when is_list(blocked_identifiers) do
936 Enum.map(
937 blocked_identifiers,
938 fn blocked_identifier ->
939 with %User{} = blocked <- get_or_fetch(blocked_identifier),
940 {:ok, blocker} <- block(blocker, blocked),
941 {:ok, _} <- ActivityPub.block(blocker, blocked) do
942 blocked
943 else
944 err ->
945 Logger.debug("blocks_import failed for #{blocked_identifier} with: #{inspect(err)}")
946 err
947 end
948 end
949 )
950 end
951
952 def mute(muter, %User{ap_id: ap_id}) do
953 info_cng =
954 muter.info
955 |> User.Info.add_to_mutes(ap_id)
956
957 cng =
958 change(muter)
959 |> put_embed(:info, info_cng)
960
961 update_and_set_cache(cng)
962 end
963
964 def unmute(muter, %{ap_id: ap_id}) do
965 info_cng =
966 muter.info
967 |> User.Info.remove_from_mutes(ap_id)
968
969 cng =
970 change(muter)
971 |> put_embed(:info, info_cng)
972
973 update_and_set_cache(cng)
974 end
975
976 def block(blocker, %User{ap_id: ap_id} = blocked) do
977 # sever any follow relationships to prevent leaks per activitypub (Pleroma issue #213)
978 blocker =
979 if following?(blocker, blocked) do
980 {:ok, blocker, _} = unfollow(blocker, blocked)
981 blocker
982 else
983 blocker
984 end
985
986 if following?(blocked, blocker) do
987 unfollow(blocked, blocker)
988 end
989
990 info_cng =
991 blocker.info
992 |> User.Info.add_to_block(ap_id)
993
994 cng =
995 change(blocker)
996 |> put_embed(:info, info_cng)
997
998 update_and_set_cache(cng)
999 end
1000
1001 # helper to handle the block given only an actor's AP id
1002 def block(blocker, %{ap_id: ap_id}) do
1003 block(blocker, User.get_by_ap_id(ap_id))
1004 end
1005
1006 def unblock(blocker, %{ap_id: ap_id}) do
1007 info_cng =
1008 blocker.info
1009 |> User.Info.remove_from_block(ap_id)
1010
1011 cng =
1012 change(blocker)
1013 |> put_embed(:info, info_cng)
1014
1015 update_and_set_cache(cng)
1016 end
1017
1018 def mutes?(nil, _), do: false
1019 def mutes?(user, %{ap_id: ap_id}), do: Enum.member?(user.info.mutes, ap_id)
1020
1021 def blocks?(user, %{ap_id: ap_id}) do
1022 blocks = user.info.blocks
1023 domain_blocks = user.info.domain_blocks
1024 %{host: host} = URI.parse(ap_id)
1025
1026 Enum.member?(blocks, ap_id) ||
1027 Enum.any?(domain_blocks, fn domain ->
1028 host == domain
1029 end)
1030 end
1031
1032 def muted_users(user),
1033 do: Repo.all(from(u in User, where: u.ap_id in ^user.info.mutes))
1034
1035 def blocked_users(user),
1036 do: Repo.all(from(u in User, where: u.ap_id in ^user.info.blocks))
1037
1038 def block_domain(user, domain) do
1039 info_cng =
1040 user.info
1041 |> User.Info.add_to_domain_block(domain)
1042
1043 cng =
1044 change(user)
1045 |> put_embed(:info, info_cng)
1046
1047 update_and_set_cache(cng)
1048 end
1049
1050 def unblock_domain(user, domain) do
1051 info_cng =
1052 user.info
1053 |> User.Info.remove_from_domain_block(domain)
1054
1055 cng =
1056 change(user)
1057 |> put_embed(:info, info_cng)
1058
1059 update_and_set_cache(cng)
1060 end
1061
1062 def maybe_local_user_query(query, local) do
1063 if local, do: local_user_query(query), else: query
1064 end
1065
1066 def local_user_query(query \\ User) do
1067 from(
1068 u in query,
1069 where: u.local == true,
1070 where: not is_nil(u.nickname)
1071 )
1072 end
1073
1074 def active_local_user_query do
1075 from(
1076 u in local_user_query(),
1077 where: fragment("not (?->'deactivated' @> 'true')", u.info)
1078 )
1079 end
1080
1081 def moderator_user_query do
1082 from(
1083 u in User,
1084 where: u.local == true,
1085 where: fragment("?->'is_moderator' @> 'true'", u.info)
1086 )
1087 end
1088
1089 def deactivate(%User{} = user, status \\ true) do
1090 info_cng = User.Info.set_activation_status(user.info, status)
1091
1092 cng =
1093 change(user)
1094 |> put_embed(:info, info_cng)
1095
1096 update_and_set_cache(cng)
1097 end
1098
1099 def delete(%User{} = user) do
1100 {:ok, user} = User.deactivate(user)
1101
1102 # Remove all relationships
1103 {:ok, followers} = User.get_followers(user)
1104
1105 followers
1106 |> Enum.each(fn follower -> User.unfollow(follower, user) end)
1107
1108 {:ok, friends} = User.get_friends(user)
1109
1110 friends
1111 |> Enum.each(fn followed -> User.unfollow(user, followed) end)
1112
1113 query =
1114 from(a in Activity, where: a.actor == ^user.ap_id)
1115 |> Activity.with_preloaded_object()
1116
1117 Repo.all(query)
1118 |> Enum.each(fn activity ->
1119 case activity.data["type"] do
1120 "Create" ->
1121 ActivityPub.delete(Object.normalize(activity))
1122
1123 # TODO: Do something with likes, follows, repeats.
1124 _ ->
1125 "Doing nothing"
1126 end
1127 end)
1128
1129 {:ok, user}
1130 end
1131
1132 def html_filter_policy(%User{info: %{no_rich_text: true}}) do
1133 Pleroma.HTML.Scrubber.TwitterText
1134 end
1135
1136 @default_scrubbers Pleroma.Config.get([:markup, :scrub_policy])
1137
1138 def html_filter_policy(_), do: @default_scrubbers
1139
1140 def fetch_by_ap_id(ap_id) do
1141 ap_try = ActivityPub.make_user_from_ap_id(ap_id)
1142
1143 case ap_try do
1144 {:ok, user} ->
1145 user
1146
1147 _ ->
1148 case OStatus.make_user(ap_id) do
1149 {:ok, user} -> user
1150 _ -> {:error, "Could not fetch by AP id"}
1151 end
1152 end
1153 end
1154
1155 def get_or_fetch_by_ap_id(ap_id) do
1156 user = get_by_ap_id(ap_id)
1157
1158 if !is_nil(user) and !User.needs_update?(user) do
1159 user
1160 else
1161 # Whether to fetch initial posts for the user (if it's a new user & the fetching is enabled)
1162 should_fetch_initial = is_nil(user) and Pleroma.Config.get([:fetch_initial_posts, :enabled])
1163
1164 user = fetch_by_ap_id(ap_id)
1165
1166 if should_fetch_initial do
1167 with %User{} = user do
1168 {:ok, _} = Task.start(__MODULE__, :fetch_initial_posts, [user])
1169 end
1170 end
1171
1172 user
1173 end
1174 end
1175
1176 def get_or_create_instance_user do
1177 relay_uri = "#{Pleroma.Web.Endpoint.url()}/relay"
1178
1179 if user = get_by_ap_id(relay_uri) do
1180 user
1181 else
1182 changes =
1183 %User{info: %User.Info{}}
1184 |> cast(%{}, [:ap_id, :nickname, :local])
1185 |> put_change(:ap_id, relay_uri)
1186 |> put_change(:nickname, nil)
1187 |> put_change(:local, true)
1188 |> put_change(:follower_address, relay_uri <> "/followers")
1189
1190 {:ok, user} = Repo.insert(changes)
1191 user
1192 end
1193 end
1194
1195 # AP style
1196 def public_key_from_info(%{
1197 source_data: %{"publicKey" => %{"publicKeyPem" => public_key_pem}}
1198 }) do
1199 key =
1200 public_key_pem
1201 |> :public_key.pem_decode()
1202 |> hd()
1203 |> :public_key.pem_entry_decode()
1204
1205 {:ok, key}
1206 end
1207
1208 # OStatus Magic Key
1209 def public_key_from_info(%{magic_key: magic_key}) do
1210 {:ok, Pleroma.Web.Salmon.decode_key(magic_key)}
1211 end
1212
1213 def get_public_key_for_ap_id(ap_id) do
1214 with %User{} = user <- get_or_fetch_by_ap_id(ap_id),
1215 {:ok, public_key} <- public_key_from_info(user.info) do
1216 {:ok, public_key}
1217 else
1218 _ -> :error
1219 end
1220 end
1221
1222 defp blank?(""), do: nil
1223 defp blank?(n), do: n
1224
1225 def insert_or_update_user(data) do
1226 data =
1227 data
1228 |> Map.put(:name, blank?(data[:name]) || data[:nickname])
1229
1230 cs = User.remote_user_creation(data)
1231
1232 Repo.insert(cs, on_conflict: :replace_all, conflict_target: :nickname)
1233 end
1234
1235 def ap_enabled?(%User{local: true}), do: true
1236 def ap_enabled?(%User{info: info}), do: info.ap_enabled
1237 def ap_enabled?(_), do: false
1238
1239 @doc "Gets or fetch a user by uri or nickname."
1240 @spec get_or_fetch(String.t()) :: User.t()
1241 def get_or_fetch("http" <> _host = uri), do: get_or_fetch_by_ap_id(uri)
1242 def get_or_fetch(nickname), do: get_or_fetch_by_nickname(nickname)
1243
1244 # wait a period of time and return newest version of the User structs
1245 # this is because we have synchronous follow APIs and need to simulate them
1246 # with an async handshake
1247 def wait_and_refresh(_, %User{local: true} = a, %User{local: true} = b) do
1248 with %User{} = a <- Repo.get(User, a.id),
1249 %User{} = b <- Repo.get(User, b.id) do
1250 {:ok, a, b}
1251 else
1252 _e ->
1253 :error
1254 end
1255 end
1256
1257 def wait_and_refresh(timeout, %User{} = a, %User{} = b) do
1258 with :ok <- :timer.sleep(timeout),
1259 %User{} = a <- Repo.get(User, a.id),
1260 %User{} = b <- Repo.get(User, b.id) do
1261 {:ok, a, b}
1262 else
1263 _e ->
1264 :error
1265 end
1266 end
1267
1268 def parse_bio(bio, user \\ %User{info: %{source_data: %{}}})
1269 def parse_bio(nil, _user), do: ""
1270 def parse_bio(bio, _user) when bio == "", do: bio
1271
1272 def parse_bio(bio, user) do
1273 emoji =
1274 (user.info.source_data["tag"] || [])
1275 |> Enum.filter(fn %{"type" => t} -> t == "Emoji" end)
1276 |> Enum.map(fn %{"icon" => %{"url" => url}, "name" => name} ->
1277 {String.trim(name, ":"), url}
1278 end)
1279
1280 # TODO: get profile URLs other than user.ap_id
1281 profile_urls = [user.ap_id]
1282
1283 bio
1284 |> CommonUtils.format_input("text/plain",
1285 mentions_format: :full,
1286 rel: &RelMe.maybe_put_rel_me(&1, profile_urls)
1287 )
1288 |> elem(0)
1289 |> Formatter.emojify(emoji)
1290 end
1291
1292 def tag(user_identifiers, tags) when is_list(user_identifiers) do
1293 Repo.transaction(fn ->
1294 for user_identifier <- user_identifiers, do: tag(user_identifier, tags)
1295 end)
1296 end
1297
1298 def tag(nickname, tags) when is_binary(nickname),
1299 do: tag(User.get_by_nickname(nickname), tags)
1300
1301 def tag(%User{} = user, tags),
1302 do: update_tags(user, Enum.uniq((user.tags || []) ++ normalize_tags(tags)))
1303
1304 def untag(user_identifiers, tags) when is_list(user_identifiers) do
1305 Repo.transaction(fn ->
1306 for user_identifier <- user_identifiers, do: untag(user_identifier, tags)
1307 end)
1308 end
1309
1310 def untag(nickname, tags) when is_binary(nickname),
1311 do: untag(User.get_by_nickname(nickname), tags)
1312
1313 def untag(%User{} = user, tags),
1314 do: update_tags(user, (user.tags || []) -- normalize_tags(tags))
1315
1316 defp update_tags(%User{} = user, new_tags) do
1317 {:ok, updated_user} =
1318 user
1319 |> change(%{tags: new_tags})
1320 |> update_and_set_cache()
1321
1322 updated_user
1323 end
1324
1325 def bookmark(%User{} = user, status_id) do
1326 bookmarks = Enum.uniq(user.bookmarks ++ [status_id])
1327 update_bookmarks(user, bookmarks)
1328 end
1329
1330 def unbookmark(%User{} = user, status_id) do
1331 bookmarks = Enum.uniq(user.bookmarks -- [status_id])
1332 update_bookmarks(user, bookmarks)
1333 end
1334
1335 def update_bookmarks(%User{} = user, bookmarks) do
1336 user
1337 |> change(%{bookmarks: bookmarks})
1338 |> update_and_set_cache
1339 end
1340
1341 defp normalize_tags(tags) do
1342 [tags]
1343 |> List.flatten()
1344 |> Enum.map(&String.downcase(&1))
1345 end
1346
1347 defp local_nickname_regex do
1348 if Pleroma.Config.get([:instance, :extended_nickname_format]) do
1349 @extended_local_nickname_regex
1350 else
1351 @strict_local_nickname_regex
1352 end
1353 end
1354
1355 def local_nickname(nickname_or_mention) do
1356 nickname_or_mention
1357 |> full_nickname()
1358 |> String.split("@")
1359 |> hd()
1360 end
1361
1362 def full_nickname(nickname_or_mention),
1363 do: String.trim_leading(nickname_or_mention, "@")
1364
1365 def error_user(ap_id) do
1366 %User{
1367 name: ap_id,
1368 ap_id: ap_id,
1369 info: %User.Info{},
1370 nickname: "erroruser@example.com",
1371 inserted_at: NaiveDateTime.utc_now()
1372 }
1373 end
1374
1375 def all_superusers do
1376 from(
1377 u in User,
1378 where: u.local == true,
1379 where: fragment("?->'is_admin' @> 'true' OR ?->'is_moderator' @> 'true'", u.info, u.info)
1380 )
1381 |> Repo.all()
1382 end
1383
1384 defp paginate(query, page, page_size) do
1385 from(u in query,
1386 limit: ^page_size,
1387 offset: ^((page - 1) * page_size)
1388 )
1389 end
1390
1391 def showing_reblogs?(%User{} = user, %User{} = target) do
1392 target.ap_id not in user.info.muted_reblogs
1393 end
1394 end