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