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