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