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