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