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