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