Merge branch 'feature/move-search-to-db' into 'develop'
[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 {:ok, results} =
827 Repo.transaction(fn ->
828 Ecto.Adapters.SQL.query(Repo, "select set_limit(0.25)", [])
829 Repo.all(search_query(query, for_user))
830 end)
831
832 results
833 end
834
835 def search_query(query, for_user) do
836 fts_subquery = fts_search_subquery(query)
837 trigram_subquery = trigram_search_subquery(query)
838 union_query = from(s in trigram_subquery, union: ^fts_subquery)
839 distinct_query = from(s in subquery(union_query), distinct: s.id)
840
841 from(s in subquery(boost_search_rank_query(distinct_query, for_user)),
842 order_by: [desc: s.search_rank],
843 limit: 20
844 )
845 end
846
847 defp boost_search_rank_query(query, nil), do: query
848
849 defp boost_search_rank_query(query, for_user) do
850 friends_ids = get_friends_ids(for_user)
851 followers_ids = get_followers_ids(for_user)
852
853 from(u in subquery(query),
854 select_merge: %{
855 search_rank:
856 fragment(
857 """
858 CASE WHEN (?) THEN (?) * 1.3
859 WHEN (?) THEN (?) * 1.2
860 WHEN (?) THEN (?) * 1.1
861 ELSE (?) END
862 """,
863 u.id in ^friends_ids and u.id in ^followers_ids,
864 u.search_rank,
865 u.id in ^friends_ids,
866 u.search_rank,
867 u.id in ^followers_ids,
868 u.search_rank,
869 u.search_rank
870 )
871 }
872 )
873 end
874
875 defp fts_search_subquery(term, query \\ User) do
876 processed_query =
877 term
878 |> String.replace(~r/\W+/, " ")
879 |> String.trim()
880 |> String.split()
881 |> Enum.map(&(&1 <> ":*"))
882 |> Enum.join(" | ")
883
884 from(
885 u in query,
886 select_merge: %{
887 search_rank:
888 fragment(
889 """
890 ts_rank_cd(
891 setweight(to_tsvector('simple', regexp_replace(?, '\\W', ' ', 'g')), 'A') ||
892 setweight(to_tsvector('simple', regexp_replace(coalesce(?, ''), '\\W', ' ', 'g')), 'B'),
893 to_tsquery('simple', ?),
894 32
895 )
896 """,
897 u.nickname,
898 u.name,
899 ^processed_query
900 )
901 },
902 where:
903 fragment(
904 """
905 (setweight(to_tsvector('simple', regexp_replace(?, '\\W', ' ', 'g')), 'A') ||
906 setweight(to_tsvector('simple', regexp_replace(coalesce(?, ''), '\\W', ' ', 'g')), 'B')) @@ to_tsquery('simple', ?)
907 """,
908 u.nickname,
909 u.name,
910 ^processed_query
911 )
912 )
913 end
914
915 defp trigram_search_subquery(term) do
916 from(
917 u in User,
918 select_merge: %{
919 search_rank:
920 fragment(
921 "similarity(?, trim(? || ' ' || coalesce(?, '')))",
922 ^term,
923 u.nickname,
924 u.name
925 )
926 },
927 where: fragment("trim(? || ' ' || coalesce(?, '')) % ?", u.nickname, u.name, ^term)
928 )
929 end
930
931 def blocks_import(%User{} = blocker, blocked_identifiers) when is_list(blocked_identifiers) do
932 Enum.map(
933 blocked_identifiers,
934 fn blocked_identifier ->
935 with %User{} = blocked <- get_or_fetch(blocked_identifier),
936 {:ok, blocker} <- block(blocker, blocked),
937 {:ok, _} <- ActivityPub.block(blocker, blocked) do
938 blocked
939 else
940 err ->
941 Logger.debug("blocks_import failed for #{blocked_identifier} with: #{inspect(err)}")
942 err
943 end
944 end
945 )
946 end
947
948 def mute(muter, %User{ap_id: ap_id}) do
949 info_cng =
950 muter.info
951 |> User.Info.add_to_mutes(ap_id)
952
953 cng =
954 change(muter)
955 |> put_embed(:info, info_cng)
956
957 update_and_set_cache(cng)
958 end
959
960 def unmute(muter, %{ap_id: ap_id}) do
961 info_cng =
962 muter.info
963 |> User.Info.remove_from_mutes(ap_id)
964
965 cng =
966 change(muter)
967 |> put_embed(:info, info_cng)
968
969 update_and_set_cache(cng)
970 end
971
972 def block(blocker, %User{ap_id: ap_id} = blocked) do
973 # sever any follow relationships to prevent leaks per activitypub (Pleroma issue #213)
974 blocker =
975 if following?(blocker, blocked) do
976 {:ok, blocker, _} = unfollow(blocker, blocked)
977 blocker
978 else
979 blocker
980 end
981
982 if following?(blocked, blocker) do
983 unfollow(blocked, blocker)
984 end
985
986 info_cng =
987 blocker.info
988 |> User.Info.add_to_block(ap_id)
989
990 cng =
991 change(blocker)
992 |> put_embed(:info, info_cng)
993
994 update_and_set_cache(cng)
995 end
996
997 # helper to handle the block given only an actor's AP id
998 def block(blocker, %{ap_id: ap_id}) do
999 block(blocker, User.get_by_ap_id(ap_id))
1000 end
1001
1002 def unblock(blocker, %{ap_id: ap_id}) do
1003 info_cng =
1004 blocker.info
1005 |> User.Info.remove_from_block(ap_id)
1006
1007 cng =
1008 change(blocker)
1009 |> put_embed(:info, info_cng)
1010
1011 update_and_set_cache(cng)
1012 end
1013
1014 def mutes?(nil, _), do: false
1015 def mutes?(user, %{ap_id: ap_id}), do: Enum.member?(user.info.mutes, ap_id)
1016
1017 def blocks?(user, %{ap_id: ap_id}) do
1018 blocks = user.info.blocks
1019 domain_blocks = user.info.domain_blocks
1020 %{host: host} = URI.parse(ap_id)
1021
1022 Enum.member?(blocks, ap_id) ||
1023 Enum.any?(domain_blocks, fn domain ->
1024 host == domain
1025 end)
1026 end
1027
1028 def muted_users(user),
1029 do: Repo.all(from(u in User, where: u.ap_id in ^user.info.mutes))
1030
1031 def blocked_users(user),
1032 do: Repo.all(from(u in User, where: u.ap_id in ^user.info.blocks))
1033
1034 def block_domain(user, domain) do
1035 info_cng =
1036 user.info
1037 |> User.Info.add_to_domain_block(domain)
1038
1039 cng =
1040 change(user)
1041 |> put_embed(:info, info_cng)
1042
1043 update_and_set_cache(cng)
1044 end
1045
1046 def unblock_domain(user, domain) do
1047 info_cng =
1048 user.info
1049 |> User.Info.remove_from_domain_block(domain)
1050
1051 cng =
1052 change(user)
1053 |> put_embed(:info, info_cng)
1054
1055 update_and_set_cache(cng)
1056 end
1057
1058 def maybe_local_user_query(query, local) do
1059 if local, do: local_user_query(query), else: query
1060 end
1061
1062 def local_user_query(query \\ User) do
1063 from(
1064 u in query,
1065 where: u.local == true,
1066 where: not is_nil(u.nickname)
1067 )
1068 end
1069
1070 def active_local_user_query do
1071 from(
1072 u in local_user_query(),
1073 where: fragment("not (?->'deactivated' @> 'true')", u.info)
1074 )
1075 end
1076
1077 def moderator_user_query do
1078 from(
1079 u in User,
1080 where: u.local == true,
1081 where: fragment("?->'is_moderator' @> 'true'", u.info)
1082 )
1083 end
1084
1085 def deactivate(%User{} = user, status \\ true) do
1086 info_cng = User.Info.set_activation_status(user.info, status)
1087
1088 cng =
1089 change(user)
1090 |> put_embed(:info, info_cng)
1091
1092 update_and_set_cache(cng)
1093 end
1094
1095 def delete(%User{} = user) do
1096 {:ok, user} = User.deactivate(user)
1097
1098 # Remove all relationships
1099 {:ok, followers} = User.get_followers(user)
1100
1101 followers
1102 |> Enum.each(fn follower -> User.unfollow(follower, user) end)
1103
1104 {:ok, friends} = User.get_friends(user)
1105
1106 friends
1107 |> Enum.each(fn followed -> User.unfollow(user, followed) end)
1108
1109 query = from(a in Activity, where: a.actor == ^user.ap_id)
1110
1111 Repo.all(query)
1112 |> Enum.each(fn activity ->
1113 case activity.data["type"] do
1114 "Create" ->
1115 ActivityPub.delete(Object.normalize(activity.data["object"]))
1116
1117 # TODO: Do something with likes, follows, repeats.
1118 _ ->
1119 "Doing nothing"
1120 end
1121 end)
1122
1123 {:ok, user}
1124 end
1125
1126 def html_filter_policy(%User{info: %{no_rich_text: true}}) do
1127 Pleroma.HTML.Scrubber.TwitterText
1128 end
1129
1130 @default_scrubbers Pleroma.Config.get([:markup, :scrub_policy])
1131
1132 def html_filter_policy(_), do: @default_scrubbers
1133
1134 def fetch_by_ap_id(ap_id) do
1135 ap_try = ActivityPub.make_user_from_ap_id(ap_id)
1136
1137 case ap_try do
1138 {:ok, user} ->
1139 user
1140
1141 _ ->
1142 case OStatus.make_user(ap_id) do
1143 {:ok, user} -> user
1144 _ -> {:error, "Could not fetch by AP id"}
1145 end
1146 end
1147 end
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 # Whether to fetch initial posts for the user (if it's a new user & the fetching is enabled)
1156 should_fetch_initial = is_nil(user) and Pleroma.Config.get([:fetch_initial_posts, :enabled])
1157
1158 user = fetch_by_ap_id(ap_id)
1159
1160 if should_fetch_initial do
1161 with %User{} = user do
1162 {:ok, _} = Task.start(__MODULE__, :fetch_initial_posts, [user])
1163 end
1164 end
1165
1166 user
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
1385 def showing_reblogs?(%User{} = user, %User{} = target) do
1386 target.ap_id not in user.info.muted_reblogs
1387 end
1388 end