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