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