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