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