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