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