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