a79da4dd8f974a34ee0e590750103ab48d12c6be
[akkoma] / 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 follow_import(%User{} = follower, followed_identifiers)
417 when is_list(followed_identifiers) do
418 Enum.map(
419 followed_identifiers,
420 fn followed_identifier ->
421 with {:ok, %User{} = followed} <- get_or_fetch(followed_identifier),
422 {:ok, follower} <- maybe_direct_follow(follower, followed),
423 {:ok, _} <- ActivityPub.follow(follower, followed) do
424 followed
425 else
426 err ->
427 Logger.debug("follow_import failed for #{followed_identifier} with: #{inspect(err)}")
428 err
429 end
430 end
431 )
432 end
433
434 def locked?(%User{} = user) do
435 user.info.locked || false
436 end
437
438 def get_by_id(id) do
439 Repo.get_by(User, id: id)
440 end
441
442 def get_by_ap_id(ap_id) do
443 Repo.get_by(User, ap_id: ap_id)
444 end
445
446 # This is mostly an SPC migration fix. This guesses the user nickname by taking the last part
447 # of the ap_id and the domain and tries to get that user
448 def get_by_guessed_nickname(ap_id) do
449 domain = URI.parse(ap_id).host
450 name = List.last(String.split(ap_id, "/"))
451 nickname = "#{name}@#{domain}"
452
453 get_cached_by_nickname(nickname)
454 end
455
456 def set_cache({:ok, user}), do: set_cache(user)
457 def set_cache({:error, err}), do: {:error, err}
458
459 def set_cache(%User{} = user) do
460 Cachex.put(:user_cache, "ap_id:#{user.ap_id}", user)
461 Cachex.put(:user_cache, "nickname:#{user.nickname}", user)
462 Cachex.put(:user_cache, "user_info:#{user.id}", user_info(user))
463 {:ok, user}
464 end
465
466 def update_and_set_cache(changeset) do
467 with {:ok, user} <- Repo.update(changeset) do
468 set_cache(user)
469 else
470 e -> e
471 end
472 end
473
474 def invalidate_cache(user) do
475 Cachex.del(:user_cache, "ap_id:#{user.ap_id}")
476 Cachex.del(:user_cache, "nickname:#{user.nickname}")
477 Cachex.del(:user_cache, "user_info:#{user.id}")
478 end
479
480 def get_cached_by_ap_id(ap_id) do
481 key = "ap_id:#{ap_id}"
482 Cachex.fetch!(:user_cache, key, fn _ -> get_by_ap_id(ap_id) end)
483 end
484
485 def get_cached_by_id(id) do
486 key = "id:#{id}"
487
488 ap_id =
489 Cachex.fetch!(:user_cache, key, fn _ ->
490 user = get_by_id(id)
491
492 if user do
493 Cachex.put(:user_cache, "ap_id:#{user.ap_id}", user)
494 {:commit, user.ap_id}
495 else
496 {:ignore, ""}
497 end
498 end)
499
500 get_cached_by_ap_id(ap_id)
501 end
502
503 def get_cached_by_nickname(nickname) do
504 key = "nickname:#{nickname}"
505
506 Cachex.fetch!(:user_cache, key, fn ->
507 user_result = get_or_fetch_by_nickname(nickname)
508
509 case user_result do
510 {:ok, user} -> {:commit, user}
511 {:error, _error} -> {:ignore, nil}
512 end
513 end)
514 end
515
516 def get_cached_by_nickname_or_id(nickname_or_id) do
517 get_cached_by_id(nickname_or_id) || get_cached_by_nickname(nickname_or_id)
518 end
519
520 def get_by_nickname(nickname) do
521 Repo.get_by(User, nickname: nickname) ||
522 if Regex.match?(~r(@#{Pleroma.Web.Endpoint.host()})i, nickname) do
523 Repo.get_by(User, nickname: local_nickname(nickname))
524 end
525 end
526
527 def get_by_email(email), do: Repo.get_by(User, email: email)
528
529 def get_by_nickname_or_email(nickname_or_email) do
530 get_by_nickname(nickname_or_email) || get_by_email(nickname_or_email)
531 end
532
533 def get_cached_user_info(user) do
534 key = "user_info:#{user.id}"
535 Cachex.fetch!(:user_cache, key, fn _ -> user_info(user) end)
536 end
537
538 def fetch_by_nickname(nickname) do
539 ap_try = ActivityPub.make_user_from_nickname(nickname)
540
541 case ap_try do
542 {:ok, user} -> {:ok, user}
543 _ -> OStatus.make_user(nickname)
544 end
545 end
546
547 def get_or_fetch_by_nickname(nickname) do
548 with %User{} = user <- get_by_nickname(nickname) do
549 {:ok, user}
550 else
551 _e ->
552 with [_nick, _domain] <- String.split(nickname, "@"),
553 {:ok, user} <- fetch_by_nickname(nickname) do
554 if Pleroma.Config.get([:fetch_initial_posts, :enabled]) do
555 fetch_initial_posts(user)
556 end
557
558 {:ok, user}
559 else
560 _e -> {:error, "not found " <> nickname}
561 end
562 end
563 end
564
565 @doc "Fetch some posts when the user has just been federated with"
566 def fetch_initial_posts(user),
567 do: PleromaJobQueue.enqueue(:background, __MODULE__, [:fetch_initial_posts, user])
568
569 @spec get_followers_query(User.t(), pos_integer() | nil) :: Ecto.Query.t()
570 def get_followers_query(%User{} = user, nil) do
571 User.Query.build(%{followers: user})
572 end
573
574 def get_followers_query(user, page) do
575 from(u in get_followers_query(user, nil))
576 |> User.Query.paginate(page, 20)
577 end
578
579 @spec get_followers_query(User.t()) :: Ecto.Query.t()
580 def get_followers_query(user), do: get_followers_query(user, nil)
581
582 def get_followers(user, page \\ nil) do
583 q = get_followers_query(user, page)
584
585 {:ok, Repo.all(q)}
586 end
587
588 def get_followers_ids(user, page \\ nil) do
589 q = get_followers_query(user, page)
590
591 Repo.all(from(u in q, select: u.id))
592 end
593
594 @spec get_friends_query(User.t(), pos_integer() | nil) :: Ecto.Query.t()
595 def get_friends_query(%User{} = user, nil) do
596 User.Query.build(%{friends: user})
597 end
598
599 def get_friends_query(user, page) do
600 from(u in get_friends_query(user, nil))
601 |> User.Query.paginate(page, 20)
602 end
603
604 @spec get_friends_query(User.t()) :: Ecto.Query.t()
605 def get_friends_query(user), do: get_friends_query(user, nil)
606
607 def get_friends(user, page \\ nil) do
608 q = get_friends_query(user, page)
609
610 {:ok, Repo.all(q)}
611 end
612
613 def get_friends_ids(user, page \\ nil) do
614 q = get_friends_query(user, page)
615
616 Repo.all(from(u in q, select: u.id))
617 end
618
619 @spec get_follow_requests(User.t()) :: {:ok, [User.t()]}
620 def get_follow_requests(%User{} = user) do
621 users =
622 Activity.follow_requests_for_actor(user)
623 |> join(:inner, [a], u in User, on: a.actor == u.ap_id)
624 |> where([a, u], not fragment("? @> ?", u.following, ^[user.follower_address]))
625 |> group_by([a, u], u.id)
626 |> select([a, u], u)
627 |> Repo.all()
628
629 {:ok, users}
630 end
631
632 def increase_note_count(%User{} = user) do
633 User
634 |> where(id: ^user.id)
635 |> update([u],
636 set: [
637 info:
638 fragment(
639 "jsonb_set(?, '{note_count}', ((?->>'note_count')::int + 1)::varchar::jsonb, true)",
640 u.info,
641 u.info
642 )
643 ]
644 )
645 |> select([u], u)
646 |> Repo.update_all([])
647 |> case do
648 {1, [user]} -> set_cache(user)
649 _ -> {:error, user}
650 end
651 end
652
653 def decrease_note_count(%User{} = user) do
654 User
655 |> where(id: ^user.id)
656 |> update([u],
657 set: [
658 info:
659 fragment(
660 "jsonb_set(?, '{note_count}', (greatest(0, (?->>'note_count')::int - 1))::varchar::jsonb, true)",
661 u.info,
662 u.info
663 )
664 ]
665 )
666 |> select([u], u)
667 |> Repo.update_all([])
668 |> case do
669 {1, [user]} -> set_cache(user)
670 _ -> {:error, user}
671 end
672 end
673
674 def update_note_count(%User{} = user) do
675 note_count_query =
676 from(
677 a in Object,
678 where: fragment("?->>'actor' = ? and ?->>'type' = 'Note'", a.data, ^user.ap_id, a.data),
679 select: count(a.id)
680 )
681
682 note_count = Repo.one(note_count_query)
683
684 info_cng = User.Info.set_note_count(user.info, note_count)
685
686 cng =
687 change(user)
688 |> put_embed(:info, info_cng)
689
690 update_and_set_cache(cng)
691 end
692
693 def update_follower_count(%User{} = user) do
694 follower_count_query =
695 User.Query.build(%{followers: user}) |> select([u], %{count: count(u.id)})
696
697 User
698 |> where(id: ^user.id)
699 |> join(:inner, [u], s in subquery(follower_count_query))
700 |> update([u, s],
701 set: [
702 info:
703 fragment(
704 "jsonb_set(?, '{follower_count}', ?::varchar::jsonb, true)",
705 u.info,
706 s.count
707 )
708 ]
709 )
710 |> select([u], u)
711 |> Repo.update_all([])
712 |> case do
713 {1, [user]} -> set_cache(user)
714 _ -> {:error, user}
715 end
716 end
717
718 @spec get_users_from_set([String.t()], boolean()) :: [User.t()]
719 def get_users_from_set(ap_ids, local_only \\ true) do
720 criteria = %{ap_id: ap_ids}
721 criteria = if local_only, do: Map.put(criteria, :local, true), else: criteria
722
723 User.Query.build(criteria)
724 |> Repo.all()
725 end
726
727 @spec get_recipients_from_activity(Activity.t()) :: [User.t()]
728 def get_recipients_from_activity(%Activity{recipients: to}) do
729 User.Query.build(%{recipients_from_activity: to, local: true})
730 |> Repo.all()
731 end
732
733 def search(query, resolve \\ false, for_user \\ nil) do
734 # Strip the beginning @ off if there is a query
735 query = String.trim_leading(query, "@")
736
737 if resolve, do: get_or_fetch(query)
738
739 {:ok, results} =
740 Repo.transaction(fn ->
741 Ecto.Adapters.SQL.query(Repo, "select set_limit(0.25)", [])
742 Repo.all(search_query(query, for_user))
743 end)
744
745 results
746 end
747
748 def search_query(query, for_user) do
749 fts_subquery = fts_search_subquery(query)
750 trigram_subquery = trigram_search_subquery(query)
751 union_query = from(s in trigram_subquery, union_all: ^fts_subquery)
752 distinct_query = from(s in subquery(union_query), order_by: s.search_type, distinct: s.id)
753
754 from(s in subquery(boost_search_rank_query(distinct_query, for_user)),
755 order_by: [desc: s.search_rank],
756 limit: 20
757 )
758 end
759
760 defp boost_search_rank_query(query, nil), do: query
761
762 defp boost_search_rank_query(query, for_user) do
763 friends_ids = get_friends_ids(for_user)
764 followers_ids = get_followers_ids(for_user)
765
766 from(u in subquery(query),
767 select_merge: %{
768 search_rank:
769 fragment(
770 """
771 CASE WHEN (?) THEN (?) * 1.3
772 WHEN (?) THEN (?) * 1.2
773 WHEN (?) THEN (?) * 1.1
774 ELSE (?) END
775 """,
776 u.id in ^friends_ids and u.id in ^followers_ids,
777 u.search_rank,
778 u.id in ^friends_ids,
779 u.search_rank,
780 u.id in ^followers_ids,
781 u.search_rank,
782 u.search_rank
783 )
784 }
785 )
786 end
787
788 defp fts_search_subquery(term, query \\ User) do
789 processed_query =
790 term
791 |> String.replace(~r/\W+/, " ")
792 |> String.trim()
793 |> String.split()
794 |> Enum.map(&(&1 <> ":*"))
795 |> Enum.join(" | ")
796
797 from(
798 u in query,
799 select_merge: %{
800 search_type: ^0,
801 search_rank:
802 fragment(
803 """
804 ts_rank_cd(
805 setweight(to_tsvector('simple', regexp_replace(?, '\\W', ' ', 'g')), 'A') ||
806 setweight(to_tsvector('simple', regexp_replace(coalesce(?, ''), '\\W', ' ', 'g')), 'B'),
807 to_tsquery('simple', ?),
808 32
809 )
810 """,
811 u.nickname,
812 u.name,
813 ^processed_query
814 )
815 },
816 where:
817 fragment(
818 """
819 (setweight(to_tsvector('simple', regexp_replace(?, '\\W', ' ', 'g')), 'A') ||
820 setweight(to_tsvector('simple', regexp_replace(coalesce(?, ''), '\\W', ' ', 'g')), 'B')) @@ to_tsquery('simple', ?)
821 """,
822 u.nickname,
823 u.name,
824 ^processed_query
825 )
826 )
827 end
828
829 defp trigram_search_subquery(term) do
830 from(
831 u in User,
832 select_merge: %{
833 # ^1 gives 'Postgrex expected a binary, got 1' for some weird reason
834 search_type: fragment("?", 1),
835 search_rank:
836 fragment(
837 "similarity(?, trim(? || ' ' || coalesce(?, '')))",
838 ^term,
839 u.nickname,
840 u.name
841 )
842 },
843 where: fragment("trim(? || ' ' || coalesce(?, '')) % ?", u.nickname, u.name, ^term)
844 )
845 end
846
847 def blocks_import(%User{} = blocker, blocked_identifiers) when is_list(blocked_identifiers) do
848 Enum.map(
849 blocked_identifiers,
850 fn blocked_identifier ->
851 with {:ok, %User{} = blocked} <- get_or_fetch(blocked_identifier),
852 {:ok, blocker} <- block(blocker, blocked),
853 {:ok, _} <- ActivityPub.block(blocker, blocked) do
854 blocked
855 else
856 err ->
857 Logger.debug("blocks_import failed for #{blocked_identifier} with: #{inspect(err)}")
858 err
859 end
860 end
861 )
862 end
863
864 def mute(muter, %User{ap_id: ap_id}) do
865 info_cng =
866 muter.info
867 |> User.Info.add_to_mutes(ap_id)
868
869 cng =
870 change(muter)
871 |> put_embed(:info, info_cng)
872
873 update_and_set_cache(cng)
874 end
875
876 def unmute(muter, %{ap_id: ap_id}) do
877 info_cng =
878 muter.info
879 |> User.Info.remove_from_mutes(ap_id)
880
881 cng =
882 change(muter)
883 |> put_embed(:info, info_cng)
884
885 update_and_set_cache(cng)
886 end
887
888 def subscribe(subscriber, %{ap_id: ap_id}) do
889 deny_follow_blocked = Pleroma.Config.get([:user, :deny_follow_blocked])
890
891 with %User{} = subscribed <- get_cached_by_ap_id(ap_id) do
892 blocked = blocks?(subscribed, subscriber) and deny_follow_blocked
893
894 if blocked do
895 {:error, "Could not subscribe: #{subscribed.nickname} is blocking you"}
896 else
897 info_cng =
898 subscribed.info
899 |> User.Info.add_to_subscribers(subscriber.ap_id)
900
901 change(subscribed)
902 |> put_embed(:info, info_cng)
903 |> update_and_set_cache()
904 end
905 end
906 end
907
908 def unsubscribe(unsubscriber, %{ap_id: ap_id}) do
909 with %User{} = user <- get_cached_by_ap_id(ap_id) do
910 info_cng =
911 user.info
912 |> User.Info.remove_from_subscribers(unsubscriber.ap_id)
913
914 change(user)
915 |> put_embed(:info, info_cng)
916 |> update_and_set_cache()
917 end
918 end
919
920 def block(blocker, %User{ap_id: ap_id} = blocked) do
921 # sever any follow relationships to prevent leaks per activitypub (Pleroma issue #213)
922 blocker =
923 if following?(blocker, blocked) do
924 {:ok, blocker, _} = unfollow(blocker, blocked)
925 blocker
926 else
927 blocker
928 end
929
930 blocker =
931 if subscribed_to?(blocked, blocker) do
932 {:ok, blocker} = unsubscribe(blocked, blocker)
933 blocker
934 else
935 blocker
936 end
937
938 if following?(blocked, blocker) do
939 unfollow(blocked, blocker)
940 end
941
942 {:ok, blocker} = update_follower_count(blocker)
943
944 info_cng =
945 blocker.info
946 |> User.Info.add_to_block(ap_id)
947
948 cng =
949 change(blocker)
950 |> put_embed(:info, info_cng)
951
952 update_and_set_cache(cng)
953 end
954
955 # helper to handle the block given only an actor's AP id
956 def block(blocker, %{ap_id: ap_id}) do
957 block(blocker, get_cached_by_ap_id(ap_id))
958 end
959
960 def unblock(blocker, %{ap_id: ap_id}) do
961 info_cng =
962 blocker.info
963 |> User.Info.remove_from_block(ap_id)
964
965 cng =
966 change(blocker)
967 |> put_embed(:info, info_cng)
968
969 update_and_set_cache(cng)
970 end
971
972 def mutes?(nil, _), do: false
973 def mutes?(user, %{ap_id: ap_id}), do: Enum.member?(user.info.mutes, ap_id)
974
975 def blocks?(user, %{ap_id: ap_id}) do
976 blocks = user.info.blocks
977 domain_blocks = user.info.domain_blocks
978 %{host: host} = URI.parse(ap_id)
979
980 Enum.member?(blocks, ap_id) ||
981 Enum.any?(domain_blocks, fn domain ->
982 host == domain
983 end)
984 end
985
986 def subscribed_to?(user, %{ap_id: ap_id}) do
987 with %User{} = target <- get_cached_by_ap_id(ap_id) do
988 Enum.member?(target.info.subscribers, user.ap_id)
989 end
990 end
991
992 @spec muted_users(User.t()) :: [User.t()]
993 def muted_users(user) do
994 User.Query.build(%{ap_id: user.info.mutes})
995 |> Repo.all()
996 end
997
998 @spec blocked_users(User.t()) :: [User.t()]
999 def blocked_users(user) do
1000 User.Query.build(%{ap_id: user.info.blocks})
1001 |> Repo.all()
1002 end
1003
1004 @spec subscribers(User.t()) :: [User.t()]
1005 def subscribers(user) do
1006 User.Query.build(%{ap_id: user.info.subscribers})
1007 |> Repo.all()
1008 end
1009
1010 def block_domain(user, domain) do
1011 info_cng =
1012 user.info
1013 |> User.Info.add_to_domain_block(domain)
1014
1015 cng =
1016 change(user)
1017 |> put_embed(:info, info_cng)
1018
1019 update_and_set_cache(cng)
1020 end
1021
1022 def unblock_domain(user, domain) do
1023 info_cng =
1024 user.info
1025 |> User.Info.remove_from_domain_block(domain)
1026
1027 cng =
1028 change(user)
1029 |> put_embed(:info, info_cng)
1030
1031 update_and_set_cache(cng)
1032 end
1033
1034 def deactivate(%User{} = user, status \\ true) do
1035 info_cng = User.Info.set_activation_status(user.info, status)
1036
1037 cng =
1038 change(user)
1039 |> put_embed(:info, info_cng)
1040
1041 update_and_set_cache(cng)
1042 end
1043
1044 def update_notification_settings(%User{} = user, settings \\ %{}) do
1045 info_changeset = User.Info.update_notification_settings(user.info, settings)
1046
1047 change(user)
1048 |> put_embed(:info, info_changeset)
1049 |> update_and_set_cache()
1050 end
1051
1052 @spec delete(User.t()) :: :ok
1053 def delete(%User{} = user),
1054 do: PleromaJobQueue.enqueue(:background, __MODULE__, [:delete, user])
1055
1056 @spec perform(atom(), User.t()) :: {:ok, User.t()}
1057 def perform(:delete, %User{} = user) do
1058 {:ok, user} = User.deactivate(user)
1059
1060 # Remove all relationships
1061 {:ok, followers} = User.get_followers(user)
1062
1063 Enum.each(followers, fn follower -> User.unfollow(follower, user) end)
1064
1065 {:ok, friends} = User.get_friends(user)
1066
1067 Enum.each(friends, fn followed -> User.unfollow(user, followed) end)
1068
1069 delete_user_activities(user)
1070 end
1071
1072 @spec perform(atom(), User.t()) :: {:ok, User.t()}
1073 def perform(:fetch_initial_posts, %User{} = user) do
1074 pages = Pleroma.Config.get!([:fetch_initial_posts, :pages])
1075
1076 Enum.each(
1077 # Insert all the posts in reverse order, so they're in the right order on the timeline
1078 Enum.reverse(Utils.fetch_ordered_collection(user.info.source_data["outbox"], pages)),
1079 &Pleroma.Web.Federator.incoming_ap_doc/1
1080 )
1081
1082 {:ok, user}
1083 end
1084
1085 def delete_user_activities(%User{ap_id: ap_id} = user) do
1086 stream =
1087 ap_id
1088 |> Activity.query_by_actor()
1089 |> Activity.with_preloaded_object()
1090 |> Repo.stream()
1091
1092 Repo.transaction(fn -> Enum.each(stream, &delete_activity(&1)) end, timeout: :infinity)
1093
1094 {:ok, user}
1095 end
1096
1097 defp delete_activity(%{data: %{"type" => "Create"}} = activity) do
1098 Object.normalize(activity) |> ActivityPub.delete()
1099 end
1100
1101 defp delete_activity(_activity), do: "Doing nothing"
1102
1103 def html_filter_policy(%User{info: %{no_rich_text: true}}) do
1104 Pleroma.HTML.Scrubber.TwitterText
1105 end
1106
1107 @default_scrubbers Pleroma.Config.get([:markup, :scrub_policy])
1108
1109 def html_filter_policy(_), do: @default_scrubbers
1110
1111 def fetch_by_ap_id(ap_id) do
1112 ap_try = ActivityPub.make_user_from_ap_id(ap_id)
1113
1114 case ap_try do
1115 {:ok, user} ->
1116 {:ok, user}
1117
1118 _ ->
1119 case OStatus.make_user(ap_id) do
1120 {:ok, user} -> {:ok, user}
1121 _ -> {:error, "Could not fetch by AP id"}
1122 end
1123 end
1124 end
1125
1126 def get_or_fetch_by_ap_id(ap_id) do
1127 user = get_cached_by_ap_id(ap_id)
1128
1129 if !is_nil(user) and !User.needs_update?(user) do
1130 {:ok, user}
1131 else
1132 # Whether to fetch initial posts for the user (if it's a new user & the fetching is enabled)
1133 should_fetch_initial = is_nil(user) and Pleroma.Config.get([:fetch_initial_posts, :enabled])
1134
1135 resp = fetch_by_ap_id(ap_id)
1136
1137 if should_fetch_initial do
1138 with {:ok, %User{} = user} <- resp do
1139 fetch_initial_posts(user)
1140 end
1141 end
1142
1143 resp
1144 end
1145 end
1146
1147 def get_or_create_instance_user do
1148 relay_uri = "#{Pleroma.Web.Endpoint.url()}/relay"
1149
1150 if user = get_cached_by_ap_id(relay_uri) do
1151 user
1152 else
1153 changes =
1154 %User{info: %User.Info{}}
1155 |> cast(%{}, [:ap_id, :nickname, :local])
1156 |> put_change(:ap_id, relay_uri)
1157 |> put_change(:nickname, nil)
1158 |> put_change(:local, true)
1159 |> put_change(:follower_address, relay_uri <> "/followers")
1160
1161 {:ok, user} = Repo.insert(changes)
1162 user
1163 end
1164 end
1165
1166 # AP style
1167 def public_key_from_info(%{
1168 source_data: %{"publicKey" => %{"publicKeyPem" => public_key_pem}}
1169 }) do
1170 key =
1171 public_key_pem
1172 |> :public_key.pem_decode()
1173 |> hd()
1174 |> :public_key.pem_entry_decode()
1175
1176 {:ok, key}
1177 end
1178
1179 # OStatus Magic Key
1180 def public_key_from_info(%{magic_key: magic_key}) do
1181 {:ok, Pleroma.Web.Salmon.decode_key(magic_key)}
1182 end
1183
1184 def get_public_key_for_ap_id(ap_id) do
1185 with {:ok, %User{} = user} <- get_or_fetch_by_ap_id(ap_id),
1186 {:ok, public_key} <- public_key_from_info(user.info) do
1187 {:ok, public_key}
1188 else
1189 _ -> :error
1190 end
1191 end
1192
1193 defp blank?(""), do: nil
1194 defp blank?(n), do: n
1195
1196 def insert_or_update_user(data) do
1197 data
1198 |> Map.put(:name, blank?(data[:name]) || data[:nickname])
1199 |> remote_user_creation()
1200 |> Repo.insert(on_conflict: :replace_all, conflict_target: :nickname)
1201 |> set_cache()
1202 end
1203
1204 def ap_enabled?(%User{local: true}), do: true
1205 def ap_enabled?(%User{info: info}), do: info.ap_enabled
1206 def ap_enabled?(_), do: false
1207
1208 @doc "Gets or fetch a user by uri or nickname."
1209 @spec get_or_fetch(String.t()) :: {:ok, User.t()} | {:error, String.t()}
1210 def get_or_fetch("http" <> _host = uri), do: get_or_fetch_by_ap_id(uri)
1211 def get_or_fetch(nickname), do: get_or_fetch_by_nickname(nickname)
1212
1213 # wait a period of time and return newest version of the User structs
1214 # this is because we have synchronous follow APIs and need to simulate them
1215 # with an async handshake
1216 def wait_and_refresh(_, %User{local: true} = a, %User{local: true} = b) do
1217 with %User{} = a <- User.get_cached_by_id(a.id),
1218 %User{} = b <- User.get_cached_by_id(b.id) do
1219 {:ok, a, b}
1220 else
1221 _e ->
1222 :error
1223 end
1224 end
1225
1226 def wait_and_refresh(timeout, %User{} = a, %User{} = b) do
1227 with :ok <- :timer.sleep(timeout),
1228 %User{} = a <- User.get_cached_by_id(a.id),
1229 %User{} = b <- User.get_cached_by_id(b.id) do
1230 {:ok, a, b}
1231 else
1232 _e ->
1233 :error
1234 end
1235 end
1236
1237 def parse_bio(bio) when is_binary(bio) and bio != "" do
1238 bio
1239 |> CommonUtils.format_input("text/plain", mentions_format: :full)
1240 |> elem(0)
1241 end
1242
1243 def parse_bio(_), do: ""
1244
1245 def parse_bio(bio, user) when is_binary(bio) and bio != "" do
1246 # TODO: get profile URLs other than user.ap_id
1247 profile_urls = [user.ap_id]
1248
1249 bio
1250 |> CommonUtils.format_input("text/plain",
1251 mentions_format: :full,
1252 rel: &RelMe.maybe_put_rel_me(&1, profile_urls)
1253 )
1254 |> elem(0)
1255 end
1256
1257 def parse_bio(_, _), do: ""
1258
1259 def tag(user_identifiers, tags) when is_list(user_identifiers) do
1260 Repo.transaction(fn ->
1261 for user_identifier <- user_identifiers, do: tag(user_identifier, tags)
1262 end)
1263 end
1264
1265 def tag(nickname, tags) when is_binary(nickname),
1266 do: tag(get_by_nickname(nickname), tags)
1267
1268 def tag(%User{} = user, tags),
1269 do: update_tags(user, Enum.uniq((user.tags || []) ++ normalize_tags(tags)))
1270
1271 def untag(user_identifiers, tags) when is_list(user_identifiers) do
1272 Repo.transaction(fn ->
1273 for user_identifier <- user_identifiers, do: untag(user_identifier, tags)
1274 end)
1275 end
1276
1277 def untag(nickname, tags) when is_binary(nickname),
1278 do: untag(get_by_nickname(nickname), tags)
1279
1280 def untag(%User{} = user, tags),
1281 do: update_tags(user, (user.tags || []) -- normalize_tags(tags))
1282
1283 defp update_tags(%User{} = user, new_tags) do
1284 {:ok, updated_user} =
1285 user
1286 |> change(%{tags: new_tags})
1287 |> update_and_set_cache()
1288
1289 updated_user
1290 end
1291
1292 defp normalize_tags(tags) do
1293 [tags]
1294 |> List.flatten()
1295 |> Enum.map(&String.downcase(&1))
1296 end
1297
1298 defp local_nickname_regex do
1299 if Pleroma.Config.get([:instance, :extended_nickname_format]) do
1300 @extended_local_nickname_regex
1301 else
1302 @strict_local_nickname_regex
1303 end
1304 end
1305
1306 def local_nickname(nickname_or_mention) do
1307 nickname_or_mention
1308 |> full_nickname()
1309 |> String.split("@")
1310 |> hd()
1311 end
1312
1313 def full_nickname(nickname_or_mention),
1314 do: String.trim_leading(nickname_or_mention, "@")
1315
1316 def error_user(ap_id) do
1317 %User{
1318 name: ap_id,
1319 ap_id: ap_id,
1320 info: %User.Info{},
1321 nickname: "erroruser@example.com",
1322 inserted_at: NaiveDateTime.utc_now()
1323 }
1324 end
1325
1326 @spec all_superusers() :: [User.t()]
1327 def all_superusers do
1328 User.Query.build(%{super_users: true, local: true})
1329 |> Repo.all()
1330 end
1331
1332 def showing_reblogs?(%User{} = user, %User{} = target) do
1333 target.ap_id not in user.info.muted_reblogs
1334 end
1335 end