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