8df276ae0a5843edd84337d3017dc82caa9898b6
[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)
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 )
339
340 {1, [follower]} = Repo.update_all(q, [], returning: true)
341
342 Enum.each(followeds, fn followed ->
343 update_follower_count(followed)
344 end)
345
346 set_cache(follower)
347 end
348
349 def follow(%User{} = follower, %User{info: info} = followed) do
350 user_config = Application.get_env(:pleroma, :user)
351 deny_follow_blocked = Keyword.get(user_config, :deny_follow_blocked)
352
353 ap_followers = followed.follower_address
354
355 cond do
356 following?(follower, followed) or info.deactivated ->
357 {:error, "Could not follow user: #{followed.nickname} is already on your list."}
358
359 deny_follow_blocked and blocks?(followed, follower) ->
360 {:error, "Could not follow user: #{followed.nickname} blocked you."}
361
362 true ->
363 if !followed.local && follower.local && !ap_enabled?(followed) do
364 Websub.subscribe(follower, followed)
365 end
366
367 q =
368 from(u in User,
369 where: u.id == ^follower.id,
370 update: [push: [following: ^ap_followers]]
371 )
372
373 {1, [follower]} = Repo.update_all(q, [], returning: true)
374
375 {:ok, _} = update_follower_count(followed)
376
377 set_cache(follower)
378 end
379 end
380
381 def unfollow(%User{} = follower, %User{} = followed) do
382 ap_followers = followed.follower_address
383
384 if following?(follower, followed) and follower.ap_id != followed.ap_id do
385 q =
386 from(u in User,
387 where: u.id == ^follower.id,
388 update: [pull: [following: ^ap_followers]]
389 )
390
391 {1, [follower]} = Repo.update_all(q, [], returning: true)
392
393 {:ok, followed} = update_follower_count(followed)
394
395 set_cache(follower)
396
397 {:ok, follower, Utils.fetch_latest_follow(follower, followed)}
398 else
399 {:error, "Not subscribed!"}
400 end
401 end
402
403 @spec following?(User.t(), User.t()) :: boolean
404 def following?(%User{} = follower, %User{} = followed) do
405 Enum.member?(follower.following, followed.follower_address)
406 end
407
408 def follow_import(%User{} = follower, followed_identifiers)
409 when is_list(followed_identifiers) do
410 Enum.map(
411 followed_identifiers,
412 fn followed_identifier ->
413 with %User{} = followed <- get_or_fetch(followed_identifier),
414 {:ok, follower} <- maybe_direct_follow(follower, followed),
415 {:ok, _} <- ActivityPub.follow(follower, followed) do
416 followed
417 else
418 err ->
419 Logger.debug("follow_import failed for #{followed_identifier} with: #{inspect(err)}")
420 err
421 end
422 end
423 )
424 end
425
426 def locked?(%User{} = user) do
427 user.info.locked || false
428 end
429
430 def get_by_id(id) do
431 Repo.get_by(User, id: id)
432 end
433
434 def get_by_ap_id(ap_id) do
435 Repo.get_by(User, ap_id: ap_id)
436 end
437
438 # This is mostly an SPC migration fix. This guesses the user nickname by taking the last part
439 # of the ap_id and the domain and tries to get that user
440 def get_by_guessed_nickname(ap_id) do
441 domain = URI.parse(ap_id).host
442 name = List.last(String.split(ap_id, "/"))
443 nickname = "#{name}@#{domain}"
444
445 get_by_nickname(nickname)
446 end
447
448 def set_cache(user) do
449 Cachex.put(:user_cache, "ap_id:#{user.ap_id}", user)
450 Cachex.put(:user_cache, "nickname:#{user.nickname}", user)
451 Cachex.put(:user_cache, "user_info:#{user.id}", user_info(user))
452 {:ok, user}
453 end
454
455 def update_and_set_cache(changeset) do
456 with {:ok, user} <- Repo.update(changeset) do
457 set_cache(user)
458 else
459 e -> e
460 end
461 end
462
463 def invalidate_cache(user) do
464 Cachex.del(:user_cache, "ap_id:#{user.ap_id}")
465 Cachex.del(:user_cache, "nickname:#{user.nickname}")
466 Cachex.del(:user_cache, "user_info:#{user.id}")
467 end
468
469 def get_cached_by_ap_id(ap_id) do
470 key = "ap_id:#{ap_id}"
471 Cachex.fetch!(:user_cache, key, fn _ -> get_by_ap_id(ap_id) end)
472 end
473
474 def get_cached_by_id(id) do
475 key = "id:#{id}"
476
477 ap_id =
478 Cachex.fetch!(:user_cache, key, fn _ ->
479 user = get_by_id(id)
480
481 if user do
482 Cachex.put(:user_cache, "ap_id:#{user.ap_id}", user)
483 {:commit, user.ap_id}
484 else
485 {:ignore, ""}
486 end
487 end)
488
489 get_cached_by_ap_id(ap_id)
490 end
491
492 def get_cached_by_nickname(nickname) do
493 key = "nickname:#{nickname}"
494 Cachex.fetch!(:user_cache, key, fn _ -> get_or_fetch_by_nickname(nickname) end)
495 end
496
497 def get_cached_by_nickname_or_id(nickname_or_id) do
498 get_cached_by_id(nickname_or_id) || get_cached_by_nickname(nickname_or_id)
499 end
500
501 def get_by_nickname(nickname) do
502 Repo.get_by(User, nickname: nickname) ||
503 if Regex.match?(~r(@#{Pleroma.Web.Endpoint.host()})i, nickname) do
504 Repo.get_by(User, nickname: local_nickname(nickname))
505 end
506 end
507
508 def get_by_nickname_or_email(nickname_or_email) do
509 case user = Repo.get_by(User, nickname: nickname_or_email) do
510 %User{} -> user
511 nil -> Repo.get_by(User, email: nickname_or_email)
512 end
513 end
514
515 def get_cached_user_info(user) do
516 key = "user_info:#{user.id}"
517 Cachex.fetch!(:user_cache, key, fn _ -> user_info(user) end)
518 end
519
520 def fetch_by_nickname(nickname) do
521 ap_try = ActivityPub.make_user_from_nickname(nickname)
522
523 case ap_try do
524 {:ok, user} -> {:ok, user}
525 _ -> OStatus.make_user(nickname)
526 end
527 end
528
529 def get_or_fetch_by_nickname(nickname) do
530 with %User{} = user <- get_by_nickname(nickname) do
531 user
532 else
533 _e ->
534 with [_nick, _domain] <- String.split(nickname, "@"),
535 {:ok, user} <- fetch_by_nickname(nickname) do
536 if Pleroma.Config.get([:fetch_initial_posts, :enabled]) do
537 {:ok, _} = Task.start(__MODULE__, :fetch_initial_posts, [user])
538 end
539
540 user
541 else
542 _e -> nil
543 end
544 end
545 end
546
547 @doc "Fetch some posts when the user has just been federated with"
548 def fetch_initial_posts(user) do
549 pages = Pleroma.Config.get!([:fetch_initial_posts, :pages])
550
551 Enum.each(
552 # Insert all the posts in reverse order, so they're in the right order on the timeline
553 Enum.reverse(Utils.fetch_ordered_collection(user.info.source_data["outbox"], pages)),
554 &Pleroma.Web.Federator.incoming_ap_doc/1
555 )
556 end
557
558 def get_followers_query(%User{id: id, follower_address: follower_address}, nil) do
559 from(
560 u in User,
561 where: fragment("? <@ ?", ^[follower_address], u.following),
562 where: u.id != ^id
563 )
564 end
565
566 def get_followers_query(user, page) do
567 from(u in get_followers_query(user, nil))
568 |> paginate(page, 20)
569 end
570
571 def get_followers_query(user), do: get_followers_query(user, nil)
572
573 def get_followers(user, page \\ nil) do
574 q = get_followers_query(user, page)
575
576 {:ok, Repo.all(q)}
577 end
578
579 def get_followers_ids(user, page \\ nil) do
580 q = get_followers_query(user, page)
581
582 Repo.all(from(u in q, select: u.id))
583 end
584
585 def get_friends_query(%User{id: id, following: following}, nil) do
586 from(
587 u in User,
588 where: u.follower_address in ^following,
589 where: u.id != ^id
590 )
591 end
592
593 def get_friends_query(user, page) do
594 from(u in get_friends_query(user, nil))
595 |> paginate(page, 20)
596 end
597
598 def get_friends_query(user), do: get_friends_query(user, nil)
599
600 def get_friends(user, page \\ nil) do
601 q = get_friends_query(user, page)
602
603 {:ok, Repo.all(q)}
604 end
605
606 def get_friends_ids(user, page \\ nil) do
607 q = get_friends_query(user, page)
608
609 Repo.all(from(u in q, select: u.id))
610 end
611
612 def get_follow_requests_query(%User{} = user) do
613 from(
614 a in Activity,
615 where:
616 fragment(
617 "? ->> 'type' = 'Follow'",
618 a.data
619 ),
620 where:
621 fragment(
622 "? ->> 'state' = 'pending'",
623 a.data
624 ),
625 where:
626 fragment(
627 "coalesce((?)->'object'->>'id', (?)->>'object') = ?",
628 a.data,
629 a.data,
630 ^user.ap_id
631 )
632 )
633 end
634
635 def get_follow_requests(%User{} = user) do
636 users =
637 user
638 |> User.get_follow_requests_query()
639 |> join(:inner, [a], u in User, a.actor == u.ap_id)
640 |> where([a, u], not fragment("? @> ?", u.following, ^[user.follower_address]))
641 |> group_by([a, u], u.id)
642 |> select([a, u], u)
643 |> Repo.all()
644
645 {:ok, users}
646 end
647
648 def increase_note_count(%User{} = user) do
649 User
650 |> where(id: ^user.id)
651 |> update([u],
652 set: [
653 info:
654 fragment(
655 "jsonb_set(?, '{note_count}', ((?->>'note_count')::int + 1)::varchar::jsonb, true)",
656 u.info,
657 u.info
658 )
659 ]
660 )
661 |> Repo.update_all([], returning: true)
662 |> case do
663 {1, [user]} -> set_cache(user)
664 _ -> {:error, user}
665 end
666 end
667
668 def decrease_note_count(%User{} = user) do
669 User
670 |> where(id: ^user.id)
671 |> update([u],
672 set: [
673 info:
674 fragment(
675 "jsonb_set(?, '{note_count}', (greatest(0, (?->>'note_count')::int - 1))::varchar::jsonb, true)",
676 u.info,
677 u.info
678 )
679 ]
680 )
681 |> Repo.update_all([], returning: true)
682 |> case do
683 {1, [user]} -> set_cache(user)
684 _ -> {:error, user}
685 end
686 end
687
688 def update_note_count(%User{} = user) do
689 note_count_query =
690 from(
691 a in Object,
692 where: fragment("?->>'actor' = ? and ?->>'type' = 'Note'", a.data, ^user.ap_id, a.data),
693 select: count(a.id)
694 )
695
696 note_count = Repo.one(note_count_query)
697
698 info_cng = User.Info.set_note_count(user.info, note_count)
699
700 cng =
701 change(user)
702 |> put_embed(:info, info_cng)
703
704 update_and_set_cache(cng)
705 end
706
707 def update_follower_count(%User{} = user) do
708 follower_count_query =
709 User
710 |> where([u], ^user.follower_address in u.following)
711 |> where([u], u.id != ^user.id)
712 |> select([u], %{count: count(u.id)})
713
714 User
715 |> where(id: ^user.id)
716 |> join(:inner, [u], s in subquery(follower_count_query))
717 |> update([u, s],
718 set: [
719 info:
720 fragment(
721 "jsonb_set(?, '{follower_count}', ?::varchar::jsonb, true)",
722 u.info,
723 s.count
724 )
725 ]
726 )
727 |> Repo.update_all([], returning: true)
728 |> case do
729 {1, [user]} -> set_cache(user)
730 _ -> {:error, user}
731 end
732 end
733
734 def get_users_from_set_query(ap_ids, false) do
735 from(
736 u in User,
737 where: u.ap_id in ^ap_ids
738 )
739 end
740
741 def get_users_from_set_query(ap_ids, true) do
742 query = get_users_from_set_query(ap_ids, false)
743
744 from(
745 u in query,
746 where: u.local == true
747 )
748 end
749
750 def get_users_from_set(ap_ids, local_only \\ true) do
751 get_users_from_set_query(ap_ids, local_only)
752 |> Repo.all()
753 end
754
755 def get_recipients_from_activity(%Activity{recipients: to}) do
756 query =
757 from(
758 u in User,
759 where: u.ap_id in ^to,
760 or_where: fragment("? && ?", u.following, ^to)
761 )
762
763 query = from(u in query, where: u.local == true)
764
765 Repo.all(query)
766 end
767
768 @spec search_for_admin(%{
769 local: boolean(),
770 page: number(),
771 page_size: number()
772 }) :: {:ok, [Pleroma.User.t()], number()}
773 def search_for_admin(%{query: nil, local: local, page: page, page_size: page_size}) do
774 query =
775 from(u in User, order_by: u.nickname)
776 |> maybe_local_user_query(local)
777
778 paginated_query =
779 query
780 |> paginate(page, page_size)
781
782 count =
783 query
784 |> Repo.aggregate(:count, :id)
785
786 {:ok, Repo.all(paginated_query), count}
787 end
788
789 @spec search_for_admin(%{
790 query: binary(),
791 admin: Pleroma.User.t(),
792 local: boolean(),
793 page: number(),
794 page_size: number()
795 }) :: {:ok, [Pleroma.User.t()], number()}
796 def search_for_admin(%{
797 query: term,
798 admin: admin,
799 local: local,
800 page: page,
801 page_size: page_size
802 }) do
803 term = String.trim_leading(term, "@")
804
805 local_paginated_query =
806 User
807 |> maybe_local_user_query(local)
808 |> paginate(page, page_size)
809
810 search_query = fts_search_subquery(term, local_paginated_query)
811
812 count =
813 term
814 |> fts_search_subquery()
815 |> maybe_local_user_query(local)
816 |> Repo.aggregate(:count, :id)
817
818 {:ok, do_search(search_query, admin), 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 fts_results = do_search(fts_search_subquery(query), for_user)
828
829 {:ok, trigram_results} =
830 Repo.transaction(fn ->
831 Ecto.Adapters.SQL.query(Repo, "select set_limit(0.25)", [])
832 do_search(trigram_search_subquery(query), for_user)
833 end)
834
835 Enum.uniq_by(fts_results ++ trigram_results, & &1.id)
836 end
837
838 defp do_search(subquery, for_user, options \\ []) do
839 q =
840 from(
841 s in subquery(subquery),
842 order_by: [desc: s.search_rank],
843 limit: ^(options[:limit] || 20)
844 )
845
846 results =
847 q
848 |> Repo.all()
849 |> Enum.filter(&(&1.search_rank > 0))
850
851 boost_search_results(results, for_user)
852 end
853
854 defp fts_search_subquery(term, query \\ User) do
855 processed_query =
856 term
857 |> String.replace(~r/\W+/, " ")
858 |> String.trim()
859 |> String.split()
860 |> Enum.map(&(&1 <> ":*"))
861 |> Enum.join(" | ")
862
863 from(
864 u in query,
865 select_merge: %{
866 search_rank:
867 fragment(
868 """
869 ts_rank_cd(
870 setweight(to_tsvector('simple', regexp_replace(?, '\\W', ' ', 'g')), 'A') ||
871 setweight(to_tsvector('simple', regexp_replace(coalesce(?, ''), '\\W', ' ', 'g')), 'B'),
872 to_tsquery('simple', ?),
873 32
874 )
875 """,
876 u.nickname,
877 u.name,
878 ^processed_query
879 )
880 },
881 where:
882 fragment(
883 """
884 (setweight(to_tsvector('simple', regexp_replace(?, '\\W', ' ', 'g')), 'A') ||
885 setweight(to_tsvector('simple', regexp_replace(coalesce(?, ''), '\\W', ' ', 'g')), 'B')) @@ to_tsquery('simple', ?)
886 """,
887 u.nickname,
888 u.name,
889 ^processed_query
890 )
891 )
892 end
893
894 defp trigram_search_subquery(term) do
895 from(
896 u in User,
897 select_merge: %{
898 search_rank:
899 fragment(
900 "similarity(?, trim(? || ' ' || coalesce(?, '')))",
901 ^term,
902 u.nickname,
903 u.name
904 )
905 },
906 where: fragment("trim(? || ' ' || coalesce(?, '')) % ?", u.nickname, u.name, ^term)
907 )
908 end
909
910 defp boost_search_results(results, nil), do: results
911
912 defp boost_search_results(results, for_user) do
913 friends_ids = get_friends_ids(for_user)
914 followers_ids = get_followers_ids(for_user)
915
916 Enum.map(
917 results,
918 fn u ->
919 search_rank_coef =
920 cond do
921 u.id in friends_ids ->
922 1.2
923
924 u.id in followers_ids ->
925 1.1
926
927 true ->
928 1
929 end
930
931 Map.put(u, :search_rank, u.search_rank * search_rank_coef)
932 end
933 )
934 |> Enum.sort_by(&(-&1.search_rank))
935 end
936
937 def blocks_import(%User{} = blocker, blocked_identifiers) when is_list(blocked_identifiers) do
938 Enum.map(
939 blocked_identifiers,
940 fn blocked_identifier ->
941 with %User{} = blocked <- get_or_fetch(blocked_identifier),
942 {:ok, blocker} <- block(blocker, blocked),
943 {:ok, _} <- ActivityPub.block(blocker, blocked) do
944 blocked
945 else
946 err ->
947 Logger.debug("blocks_import failed for #{blocked_identifier} with: #{inspect(err)}")
948 err
949 end
950 end
951 )
952 end
953
954 def mute(muter, %User{ap_id: ap_id}) do
955 info_cng =
956 muter.info
957 |> User.Info.add_to_mutes(ap_id)
958
959 cng =
960 change(muter)
961 |> put_embed(:info, info_cng)
962
963 update_and_set_cache(cng)
964 end
965
966 def unmute(muter, %{ap_id: ap_id}) do
967 info_cng =
968 muter.info
969 |> User.Info.remove_from_mutes(ap_id)
970
971 cng =
972 change(muter)
973 |> put_embed(:info, info_cng)
974
975 update_and_set_cache(cng)
976 end
977
978 def block(blocker, %User{ap_id: ap_id} = blocked) do
979 # sever any follow relationships to prevent leaks per activitypub (Pleroma issue #213)
980 blocker =
981 if following?(blocker, blocked) do
982 {:ok, blocker, _} = unfollow(blocker, blocked)
983 blocker
984 else
985 blocker
986 end
987
988 if following?(blocked, blocker) do
989 unfollow(blocked, blocker)
990 end
991
992 info_cng =
993 blocker.info
994 |> User.Info.add_to_block(ap_id)
995
996 cng =
997 change(blocker)
998 |> put_embed(:info, info_cng)
999
1000 update_and_set_cache(cng)
1001 end
1002
1003 # helper to handle the block given only an actor's AP id
1004 def block(blocker, %{ap_id: ap_id}) do
1005 block(blocker, User.get_by_ap_id(ap_id))
1006 end
1007
1008 def unblock(blocker, %{ap_id: ap_id}) do
1009 info_cng =
1010 blocker.info
1011 |> User.Info.remove_from_block(ap_id)
1012
1013 cng =
1014 change(blocker)
1015 |> put_embed(:info, info_cng)
1016
1017 update_and_set_cache(cng)
1018 end
1019
1020 def mutes?(nil, _), do: false
1021 def mutes?(user, %{ap_id: ap_id}), do: Enum.member?(user.info.mutes, ap_id)
1022
1023 def blocks?(user, %{ap_id: ap_id}) do
1024 blocks = user.info.blocks
1025 domain_blocks = user.info.domain_blocks
1026 %{host: host} = URI.parse(ap_id)
1027
1028 Enum.member?(blocks, ap_id) ||
1029 Enum.any?(domain_blocks, fn domain ->
1030 host == domain
1031 end)
1032 end
1033
1034 def muted_users(user),
1035 do: Repo.all(from(u in User, where: u.ap_id in ^user.info.mutes))
1036
1037 def blocked_users(user),
1038 do: Repo.all(from(u in User, where: u.ap_id in ^user.info.blocks))
1039
1040 def block_domain(user, domain) do
1041 info_cng =
1042 user.info
1043 |> User.Info.add_to_domain_block(domain)
1044
1045 cng =
1046 change(user)
1047 |> put_embed(:info, info_cng)
1048
1049 update_and_set_cache(cng)
1050 end
1051
1052 def unblock_domain(user, domain) do
1053 info_cng =
1054 user.info
1055 |> User.Info.remove_from_domain_block(domain)
1056
1057 cng =
1058 change(user)
1059 |> put_embed(:info, info_cng)
1060
1061 update_and_set_cache(cng)
1062 end
1063
1064 def maybe_local_user_query(query, local) do
1065 if local, do: local_user_query(query), else: query
1066 end
1067
1068 def local_user_query(query \\ User) do
1069 from(
1070 u in query,
1071 where: u.local == true,
1072 where: not is_nil(u.nickname)
1073 )
1074 end
1075
1076 def active_local_user_query do
1077 from(
1078 u in local_user_query(),
1079 where: fragment("not (?->'deactivated' @> 'true')", u.info)
1080 )
1081 end
1082
1083 def moderator_user_query do
1084 from(
1085 u in User,
1086 where: u.local == true,
1087 where: fragment("?->'is_moderator' @> 'true'", u.info)
1088 )
1089 end
1090
1091 def deactivate(%User{} = user, status \\ true) do
1092 info_cng = User.Info.set_activation_status(user.info, status)
1093
1094 cng =
1095 change(user)
1096 |> put_embed(:info, info_cng)
1097
1098 update_and_set_cache(cng)
1099 end
1100
1101 def delete(%User{} = user) do
1102 {:ok, user} = User.deactivate(user)
1103
1104 # Remove all relationships
1105 {:ok, followers} = User.get_followers(user)
1106
1107 followers
1108 |> Enum.each(fn follower -> User.unfollow(follower, user) end)
1109
1110 {:ok, friends} = User.get_friends(user)
1111
1112 friends
1113 |> Enum.each(fn followed -> User.unfollow(user, followed) end)
1114
1115 query = from(a in Activity, where: a.actor == ^user.ap_id)
1116
1117 Repo.all(query)
1118 |> Enum.each(fn activity ->
1119 case activity.data["type"] do
1120 "Create" ->
1121 ActivityPub.delete(Object.normalize(activity.data["object"]))
1122
1123 # TODO: Do something with likes, follows, repeats.
1124 _ ->
1125 "Doing nothing"
1126 end
1127 end)
1128
1129 {:ok, user}
1130 end
1131
1132 def html_filter_policy(%User{info: %{no_rich_text: true}}) do
1133 Pleroma.HTML.Scrubber.TwitterText
1134 end
1135
1136 @default_scrubbers Pleroma.Config.get([:markup, :scrub_policy])
1137
1138 def html_filter_policy(_), do: @default_scrubbers
1139
1140 def fetch_by_ap_id(ap_id) do
1141 ap_try = ActivityPub.make_user_from_ap_id(ap_id)
1142
1143 case ap_try do
1144 {:ok, user} ->
1145 user
1146
1147 _ ->
1148 case OStatus.make_user(ap_id) do
1149 {:ok, user} -> user
1150 _ -> {:error, "Could not fetch by AP id"}
1151 end
1152 end
1153 end
1154
1155 def get_or_fetch_by_ap_id(ap_id) do
1156 user = get_by_ap_id(ap_id)
1157
1158 if !is_nil(user) and !User.needs_update?(user) do
1159 user
1160 else
1161 # Whether to fetch initial posts for the user (if it's a new user & the fetching is enabled)
1162 should_fetch_initial = is_nil(user) and Pleroma.Config.get([:fetch_initial_posts, :enabled])
1163
1164 user = fetch_by_ap_id(ap_id)
1165
1166 if should_fetch_initial do
1167 with %User{} = user do
1168 {:ok, _} = Task.start(__MODULE__, :fetch_initial_posts, [user])
1169 end
1170 end
1171
1172 user
1173 end
1174 end
1175
1176 def get_or_create_instance_user do
1177 relay_uri = "#{Pleroma.Web.Endpoint.url()}/relay"
1178
1179 if user = get_by_ap_id(relay_uri) do
1180 user
1181 else
1182 changes =
1183 %User{info: %User.Info{}}
1184 |> cast(%{}, [:ap_id, :nickname, :local])
1185 |> put_change(:ap_id, relay_uri)
1186 |> put_change(:nickname, nil)
1187 |> put_change(:local, true)
1188 |> put_change(:follower_address, relay_uri <> "/followers")
1189
1190 {:ok, user} = Repo.insert(changes)
1191 user
1192 end
1193 end
1194
1195 # AP style
1196 def public_key_from_info(%{
1197 source_data: %{"publicKey" => %{"publicKeyPem" => public_key_pem}}
1198 }) do
1199 key =
1200 public_key_pem
1201 |> :public_key.pem_decode()
1202 |> hd()
1203 |> :public_key.pem_entry_decode()
1204
1205 {:ok, key}
1206 end
1207
1208 # OStatus Magic Key
1209 def public_key_from_info(%{magic_key: magic_key}) do
1210 {:ok, Pleroma.Web.Salmon.decode_key(magic_key)}
1211 end
1212
1213 def get_public_key_for_ap_id(ap_id) do
1214 with %User{} = user <- get_or_fetch_by_ap_id(ap_id),
1215 {:ok, public_key} <- public_key_from_info(user.info) do
1216 {:ok, public_key}
1217 else
1218 _ -> :error
1219 end
1220 end
1221
1222 defp blank?(""), do: nil
1223 defp blank?(n), do: n
1224
1225 def insert_or_update_user(data) do
1226 data =
1227 data
1228 |> Map.put(:name, blank?(data[:name]) || data[:nickname])
1229
1230 cs = User.remote_user_creation(data)
1231
1232 Repo.insert(cs, on_conflict: :replace_all, conflict_target: :nickname)
1233 end
1234
1235 def ap_enabled?(%User{local: true}), do: true
1236 def ap_enabled?(%User{info: info}), do: info.ap_enabled
1237 def ap_enabled?(_), do: false
1238
1239 @doc "Gets or fetch a user by uri or nickname."
1240 @spec get_or_fetch(String.t()) :: User.t()
1241 def get_or_fetch("http" <> _host = uri), do: get_or_fetch_by_ap_id(uri)
1242 def get_or_fetch(nickname), do: get_or_fetch_by_nickname(nickname)
1243
1244 # wait a period of time and return newest version of the User structs
1245 # this is because we have synchronous follow APIs and need to simulate them
1246 # with an async handshake
1247 def wait_and_refresh(_, %User{local: true} = a, %User{local: true} = b) do
1248 with %User{} = a <- Repo.get(User, a.id),
1249 %User{} = b <- Repo.get(User, b.id) do
1250 {:ok, a, b}
1251 else
1252 _e ->
1253 :error
1254 end
1255 end
1256
1257 def wait_and_refresh(timeout, %User{} = a, %User{} = b) do
1258 with :ok <- :timer.sleep(timeout),
1259 %User{} = a <- Repo.get(User, a.id),
1260 %User{} = b <- Repo.get(User, b.id) do
1261 {:ok, a, b}
1262 else
1263 _e ->
1264 :error
1265 end
1266 end
1267
1268 def parse_bio(bio, user \\ %User{info: %{source_data: %{}}})
1269 def parse_bio(nil, _user), do: ""
1270 def parse_bio(bio, _user) when bio == "", do: bio
1271
1272 def parse_bio(bio, user) do
1273 emoji =
1274 (user.info.source_data["tag"] || [])
1275 |> Enum.filter(fn %{"type" => t} -> t == "Emoji" end)
1276 |> Enum.map(fn %{"icon" => %{"url" => url}, "name" => name} ->
1277 {String.trim(name, ":"), url}
1278 end)
1279
1280 # TODO: get profile URLs other than user.ap_id
1281 profile_urls = [user.ap_id]
1282
1283 bio
1284 |> CommonUtils.format_input("text/plain",
1285 mentions_format: :full,
1286 rel: &RelMe.maybe_put_rel_me(&1, profile_urls)
1287 )
1288 |> elem(0)
1289 |> Formatter.emojify(emoji)
1290 end
1291
1292 def tag(user_identifiers, tags) when is_list(user_identifiers) do
1293 Repo.transaction(fn ->
1294 for user_identifier <- user_identifiers, do: tag(user_identifier, tags)
1295 end)
1296 end
1297
1298 def tag(nickname, tags) when is_binary(nickname),
1299 do: tag(User.get_by_nickname(nickname), tags)
1300
1301 def tag(%User{} = user, tags),
1302 do: update_tags(user, Enum.uniq((user.tags || []) ++ normalize_tags(tags)))
1303
1304 def untag(user_identifiers, tags) when is_list(user_identifiers) do
1305 Repo.transaction(fn ->
1306 for user_identifier <- user_identifiers, do: untag(user_identifier, tags)
1307 end)
1308 end
1309
1310 def untag(nickname, tags) when is_binary(nickname),
1311 do: untag(User.get_by_nickname(nickname), tags)
1312
1313 def untag(%User{} = user, tags),
1314 do: update_tags(user, (user.tags || []) -- normalize_tags(tags))
1315
1316 defp update_tags(%User{} = user, new_tags) do
1317 {:ok, updated_user} =
1318 user
1319 |> change(%{tags: new_tags})
1320 |> update_and_set_cache()
1321
1322 updated_user
1323 end
1324
1325 def bookmark(%User{} = user, status_id) do
1326 bookmarks = Enum.uniq(user.bookmarks ++ [status_id])
1327 update_bookmarks(user, bookmarks)
1328 end
1329
1330 def unbookmark(%User{} = user, status_id) do
1331 bookmarks = Enum.uniq(user.bookmarks -- [status_id])
1332 update_bookmarks(user, bookmarks)
1333 end
1334
1335 def update_bookmarks(%User{} = user, bookmarks) do
1336 user
1337 |> change(%{bookmarks: bookmarks})
1338 |> update_and_set_cache
1339 end
1340
1341 defp normalize_tags(tags) do
1342 [tags]
1343 |> List.flatten()
1344 |> Enum.map(&String.downcase(&1))
1345 end
1346
1347 defp local_nickname_regex do
1348 if Pleroma.Config.get([:instance, :extended_nickname_format]) do
1349 @extended_local_nickname_regex
1350 else
1351 @strict_local_nickname_regex
1352 end
1353 end
1354
1355 def local_nickname(nickname_or_mention) do
1356 nickname_or_mention
1357 |> full_nickname()
1358 |> String.split("@")
1359 |> hd()
1360 end
1361
1362 def full_nickname(nickname_or_mention),
1363 do: String.trim_leading(nickname_or_mention, "@")
1364
1365 def error_user(ap_id) do
1366 %User{
1367 name: ap_id,
1368 ap_id: ap_id,
1369 info: %User.Info{},
1370 nickname: "erroruser@example.com",
1371 inserted_at: NaiveDateTime.utc_now()
1372 }
1373 end
1374
1375 def all_superusers do
1376 from(
1377 u in User,
1378 where: u.local == true,
1379 where: fragment("?->'is_admin' @> 'true' OR ?->'is_moderator' @> 'true'", u.info, u.info)
1380 )
1381 |> Repo.all()
1382 end
1383
1384 defp paginate(query, page, page_size) do
1385 from(u in query,
1386 limit: ^page_size,
1387 offset: ^((page - 1) * page_size)
1388 )
1389 end
1390
1391 def showing_reblogs?(%User{} = user, %User{} = target) do
1392 target.ap_id not in user.info.muted_reblogs
1393 end
1394 end