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