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