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