7a6675208173ec1444ca072fb5a7138915667fd1
[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{} = user) do
108 "#{ap_id(user)}/followers"
109 end
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.id)
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 admin: Pleroma.User.t(),
799 local: boolean(),
800 page: number(),
801 page_size: number()
802 }) :: {:ok, [Pleroma.User.t()], number()}
803 def search_for_admin(%{
804 query: term,
805 admin: admin,
806 local: local,
807 page: page,
808 page_size: page_size
809 }) do
810 term = String.trim_leading(term, "@")
811
812 local_paginated_query =
813 User
814 |> maybe_local_user_query(local)
815 |> paginate(page, page_size)
816
817 search_query = fts_search_subquery(term, local_paginated_query)
818
819 count =
820 term
821 |> fts_search_subquery()
822 |> maybe_local_user_query(local)
823 |> Repo.aggregate(:count, :id)
824
825 {:ok, do_search(search_query, admin), count}
826 end
827
828 def search(query, resolve \\ false, for_user \\ nil) do
829 # Strip the beginning @ off if there is a query
830 query = String.trim_leading(query, "@")
831
832 if resolve, do: get_or_fetch(query)
833
834 fts_results = do_search(fts_search_subquery(query), for_user)
835
836 {:ok, trigram_results} =
837 Repo.transaction(fn ->
838 Ecto.Adapters.SQL.query(Repo, "select set_limit(0.25)", [])
839 do_search(trigram_search_subquery(query), for_user)
840 end)
841
842 Enum.uniq_by(fts_results ++ trigram_results, & &1.id)
843 end
844
845 defp do_search(subquery, for_user, options \\ []) do
846 q =
847 from(
848 s in subquery(subquery),
849 order_by: [desc: s.search_rank],
850 limit: ^(options[:limit] || 20)
851 )
852
853 results =
854 q
855 |> Repo.all()
856 |> Enum.filter(&(&1.search_rank > 0))
857
858 boost_search_results(results, for_user)
859 end
860
861 defp fts_search_subquery(term, query \\ User) do
862 processed_query =
863 term
864 |> String.replace(~r/\W+/, " ")
865 |> String.trim()
866 |> String.split()
867 |> Enum.map(&(&1 <> ":*"))
868 |> Enum.join(" | ")
869
870 from(
871 u in query,
872 select_merge: %{
873 search_rank:
874 fragment(
875 """
876 ts_rank_cd(
877 setweight(to_tsvector('simple', regexp_replace(?, '\\W', ' ', 'g')), 'A') ||
878 setweight(to_tsvector('simple', regexp_replace(coalesce(?, ''), '\\W', ' ', 'g')), 'B'),
879 to_tsquery('simple', ?),
880 32
881 )
882 """,
883 u.nickname,
884 u.name,
885 ^processed_query
886 )
887 },
888 where:
889 fragment(
890 """
891 (setweight(to_tsvector('simple', regexp_replace(?, '\\W', ' ', 'g')), 'A') ||
892 setweight(to_tsvector('simple', regexp_replace(coalesce(?, ''), '\\W', ' ', 'g')), 'B')) @@ to_tsquery('simple', ?)
893 """,
894 u.nickname,
895 u.name,
896 ^processed_query
897 )
898 )
899 end
900
901 defp trigram_search_subquery(term) do
902 from(
903 u in User,
904 select_merge: %{
905 search_rank:
906 fragment(
907 "similarity(?, trim(? || ' ' || coalesce(?, '')))",
908 ^term,
909 u.nickname,
910 u.name
911 )
912 },
913 where: fragment("trim(? || ' ' || coalesce(?, '')) % ?", u.nickname, u.name, ^term)
914 )
915 end
916
917 defp boost_search_results(results, nil), do: results
918
919 defp boost_search_results(results, for_user) do
920 friends_ids = get_friends_ids(for_user)
921 followers_ids = get_followers_ids(for_user)
922
923 Enum.map(
924 results,
925 fn u ->
926 search_rank_coef =
927 cond do
928 u.id in friends_ids ->
929 1.2
930
931 u.id in followers_ids ->
932 1.1
933
934 true ->
935 1
936 end
937
938 Map.put(u, :search_rank, u.search_rank * search_rank_coef)
939 end
940 )
941 |> Enum.sort_by(&(-&1.search_rank))
942 end
943
944 def blocks_import(%User{} = blocker, blocked_identifiers) when is_list(blocked_identifiers) do
945 Enum.map(
946 blocked_identifiers,
947 fn blocked_identifier ->
948 with %User{} = blocked <- get_or_fetch(blocked_identifier),
949 {:ok, blocker} <- block(blocker, blocked),
950 {:ok, _} <- ActivityPub.block(blocker, blocked) do
951 blocked
952 else
953 err ->
954 Logger.debug("blocks_import failed for #{blocked_identifier} with: #{inspect(err)}")
955 err
956 end
957 end
958 )
959 end
960
961 def mute(muter, %User{ap_id: ap_id}) do
962 info_cng =
963 muter.info
964 |> User.Info.add_to_mutes(ap_id)
965
966 cng =
967 change(muter)
968 |> put_embed(:info, info_cng)
969
970 update_and_set_cache(cng)
971 end
972
973 def unmute(muter, %{ap_id: ap_id}) do
974 info_cng =
975 muter.info
976 |> User.Info.remove_from_mutes(ap_id)
977
978 cng =
979 change(muter)
980 |> put_embed(:info, info_cng)
981
982 update_and_set_cache(cng)
983 end
984
985 def block(blocker, %User{ap_id: ap_id} = blocked) do
986 # sever any follow relationships to prevent leaks per activitypub (Pleroma issue #213)
987 blocker =
988 if following?(blocker, blocked) do
989 {:ok, blocker, _} = unfollow(blocker, blocked)
990 blocker
991 else
992 blocker
993 end
994
995 if following?(blocked, blocker) do
996 unfollow(blocked, blocker)
997 end
998
999 info_cng =
1000 blocker.info
1001 |> User.Info.add_to_block(ap_id)
1002
1003 cng =
1004 change(blocker)
1005 |> put_embed(:info, info_cng)
1006
1007 update_and_set_cache(cng)
1008 end
1009
1010 # helper to handle the block given only an actor's AP id
1011 def block(blocker, %{ap_id: ap_id}) do
1012 block(blocker, User.get_by_ap_id(ap_id))
1013 end
1014
1015 def unblock(blocker, %{ap_id: ap_id}) do
1016 info_cng =
1017 blocker.info
1018 |> User.Info.remove_from_block(ap_id)
1019
1020 cng =
1021 change(blocker)
1022 |> put_embed(:info, info_cng)
1023
1024 update_and_set_cache(cng)
1025 end
1026
1027 def mutes?(nil, _), do: false
1028 def mutes?(user, %{ap_id: ap_id}), do: Enum.member?(user.info.mutes, ap_id)
1029
1030 def blocks?(user, %{ap_id: ap_id}) do
1031 blocks = user.info.blocks
1032 domain_blocks = user.info.domain_blocks
1033 %{host: host} = URI.parse(ap_id)
1034
1035 Enum.member?(blocks, ap_id) ||
1036 Enum.any?(domain_blocks, fn domain ->
1037 host == domain
1038 end)
1039 end
1040
1041 def muted_users(user),
1042 do: Repo.all(from(u in User, where: u.ap_id in ^user.info.mutes))
1043
1044 def blocked_users(user),
1045 do: Repo.all(from(u in User, where: u.ap_id in ^user.info.blocks))
1046
1047 def block_domain(user, domain) do
1048 info_cng =
1049 user.info
1050 |> User.Info.add_to_domain_block(domain)
1051
1052 cng =
1053 change(user)
1054 |> put_embed(:info, info_cng)
1055
1056 update_and_set_cache(cng)
1057 end
1058
1059 def unblock_domain(user, domain) do
1060 info_cng =
1061 user.info
1062 |> User.Info.remove_from_domain_block(domain)
1063
1064 cng =
1065 change(user)
1066 |> put_embed(:info, info_cng)
1067
1068 update_and_set_cache(cng)
1069 end
1070
1071 def maybe_local_user_query(query, local) do
1072 if local, do: local_user_query(query), else: query
1073 end
1074
1075 def local_user_query(query \\ User) do
1076 from(
1077 u in query,
1078 where: u.local == true,
1079 where: not is_nil(u.nickname)
1080 )
1081 end
1082
1083 def active_local_user_query do
1084 from(
1085 u in local_user_query(),
1086 where: fragment("not (?->'deactivated' @> 'true')", u.info)
1087 )
1088 end
1089
1090 def moderator_user_query do
1091 from(
1092 u in User,
1093 where: u.local == true,
1094 where: fragment("?->'is_moderator' @> 'true'", u.info)
1095 )
1096 end
1097
1098 def deactivate(%User{} = user, status \\ true) do
1099 info_cng = User.Info.set_activation_status(user.info, status)
1100
1101 cng =
1102 change(user)
1103 |> put_embed(:info, info_cng)
1104
1105 update_and_set_cache(cng)
1106 end
1107
1108 def delete(%User{} = user) do
1109 {:ok, user} = User.deactivate(user)
1110
1111 # Remove all relationships
1112 {:ok, followers} = User.get_followers(user)
1113
1114 followers
1115 |> Enum.each(fn follower -> User.unfollow(follower, user) end)
1116
1117 {:ok, friends} = User.get_friends(user)
1118
1119 friends
1120 |> Enum.each(fn followed -> User.unfollow(user, followed) end)
1121
1122 query = from(a in Activity, where: a.actor == ^user.ap_id)
1123
1124 Repo.all(query)
1125 |> Enum.each(fn activity ->
1126 case activity.data["type"] do
1127 "Create" ->
1128 ActivityPub.delete(Object.normalize(activity.data["object"]))
1129
1130 # TODO: Do something with likes, follows, repeats.
1131 _ ->
1132 "Doing nothing"
1133 end
1134 end)
1135
1136 {:ok, user}
1137 end
1138
1139 def html_filter_policy(%User{info: %{no_rich_text: true}}) do
1140 Pleroma.HTML.Scrubber.TwitterText
1141 end
1142
1143 @default_scrubbers Pleroma.Config.get([:markup, :scrub_policy])
1144
1145 def html_filter_policy(_), do: @default_scrubbers
1146
1147 def fetch_by_ap_id(ap_id) do
1148 ap_try = ActivityPub.make_user_from_ap_id(ap_id)
1149
1150 case ap_try do
1151 {:ok, user} ->
1152 user
1153
1154 _ ->
1155 case OStatus.make_user(ap_id) do
1156 {:ok, user} -> user
1157 _ -> {:error, "Could not fetch by AP id"}
1158 end
1159 end
1160 end
1161
1162 def get_or_fetch_by_ap_id(ap_id) do
1163 user = get_by_ap_id(ap_id)
1164
1165 if !is_nil(user) and !User.needs_update?(user) do
1166 user
1167 else
1168 user = fetch_by_ap_id(ap_id)
1169
1170 if Pleroma.Config.get([:fetch_initial_posts, :enabled]) do
1171 with %User{} = user do
1172 {:ok, _} = Task.start(__MODULE__, :fetch_initial_posts, [user])
1173 end
1174 end
1175
1176 user
1177 end
1178 end
1179
1180 def get_or_create_instance_user do
1181 relay_uri = "#{Pleroma.Web.Endpoint.url()}/relay"
1182
1183 if user = get_by_ap_id(relay_uri) do
1184 user
1185 else
1186 changes =
1187 %User{info: %User.Info{}}
1188 |> cast(%{}, [:ap_id, :nickname, :local])
1189 |> put_change(:ap_id, relay_uri)
1190 |> put_change(:nickname, nil)
1191 |> put_change(:local, true)
1192 |> put_change(:follower_address, relay_uri <> "/followers")
1193
1194 {:ok, user} = Repo.insert(changes)
1195 user
1196 end
1197 end
1198
1199 # AP style
1200 def public_key_from_info(%{
1201 source_data: %{"publicKey" => %{"publicKeyPem" => public_key_pem}}
1202 }) do
1203 key =
1204 public_key_pem
1205 |> :public_key.pem_decode()
1206 |> hd()
1207 |> :public_key.pem_entry_decode()
1208
1209 {:ok, key}
1210 end
1211
1212 # OStatus Magic Key
1213 def public_key_from_info(%{magic_key: magic_key}) do
1214 {:ok, Pleroma.Web.Salmon.decode_key(magic_key)}
1215 end
1216
1217 def get_public_key_for_ap_id(ap_id) do
1218 with %User{} = user <- get_or_fetch_by_ap_id(ap_id),
1219 {:ok, public_key} <- public_key_from_info(user.info) do
1220 {:ok, public_key}
1221 else
1222 _ -> :error
1223 end
1224 end
1225
1226 defp blank?(""), do: nil
1227 defp blank?(n), do: n
1228
1229 def insert_or_update_user(data) do
1230 data =
1231 data
1232 |> Map.put(:name, blank?(data[:name]) || data[:nickname])
1233
1234 cs = User.remote_user_creation(data)
1235
1236 Repo.insert(cs, on_conflict: :replace_all, conflict_target: :nickname)
1237 end
1238
1239 def ap_enabled?(%User{local: true}), do: true
1240 def ap_enabled?(%User{info: info}), do: info.ap_enabled
1241 def ap_enabled?(_), do: false
1242
1243 @doc "Gets or fetch a user by uri or nickname."
1244 @spec get_or_fetch(String.t()) :: User.t()
1245 def get_or_fetch("http" <> _host = uri), do: get_or_fetch_by_ap_id(uri)
1246 def get_or_fetch(nickname), do: get_or_fetch_by_nickname(nickname)
1247
1248 # wait a period of time and return newest version of the User structs
1249 # this is because we have synchronous follow APIs and need to simulate them
1250 # with an async handshake
1251 def wait_and_refresh(_, %User{local: true} = a, %User{local: true} = b) do
1252 with %User{} = a <- Repo.get(User, a.id),
1253 %User{} = b <- Repo.get(User, b.id) do
1254 {:ok, a, b}
1255 else
1256 _e ->
1257 :error
1258 end
1259 end
1260
1261 def wait_and_refresh(timeout, %User{} = a, %User{} = b) do
1262 with :ok <- :timer.sleep(timeout),
1263 %User{} = a <- Repo.get(User, a.id),
1264 %User{} = b <- Repo.get(User, b.id) do
1265 {:ok, a, b}
1266 else
1267 _e ->
1268 :error
1269 end
1270 end
1271
1272 def parse_bio(bio, user \\ %User{info: %{source_data: %{}}})
1273 def parse_bio(nil, _user), do: ""
1274 def parse_bio(bio, _user) when bio == "", do: bio
1275
1276 def parse_bio(bio, user) do
1277 emoji =
1278 (user.info.source_data["tag"] || [])
1279 |> Enum.filter(fn %{"type" => t} -> t == "Emoji" end)
1280 |> Enum.map(fn %{"icon" => %{"url" => url}, "name" => name} ->
1281 {String.trim(name, ":"), url}
1282 end)
1283
1284 # TODO: get profile URLs other than user.ap_id
1285 profile_urls = [user.ap_id]
1286
1287 bio
1288 |> CommonUtils.format_input("text/plain",
1289 mentions_format: :full,
1290 rel: &RelMe.maybe_put_rel_me(&1, profile_urls)
1291 )
1292 |> elem(0)
1293 |> Formatter.emojify(emoji)
1294 end
1295
1296 def tag(user_identifiers, tags) when is_list(user_identifiers) do
1297 Repo.transaction(fn ->
1298 for user_identifier <- user_identifiers, do: tag(user_identifier, tags)
1299 end)
1300 end
1301
1302 def tag(nickname, tags) when is_binary(nickname),
1303 do: tag(User.get_by_nickname(nickname), tags)
1304
1305 def tag(%User{} = user, tags),
1306 do: update_tags(user, Enum.uniq((user.tags || []) ++ normalize_tags(tags)))
1307
1308 def untag(user_identifiers, tags) when is_list(user_identifiers) do
1309 Repo.transaction(fn ->
1310 for user_identifier <- user_identifiers, do: untag(user_identifier, tags)
1311 end)
1312 end
1313
1314 def untag(nickname, tags) when is_binary(nickname),
1315 do: untag(User.get_by_nickname(nickname), tags)
1316
1317 def untag(%User{} = user, tags),
1318 do: update_tags(user, (user.tags || []) -- normalize_tags(tags))
1319
1320 defp update_tags(%User{} = user, new_tags) do
1321 {:ok, updated_user} =
1322 user
1323 |> change(%{tags: new_tags})
1324 |> update_and_set_cache()
1325
1326 updated_user
1327 end
1328
1329 def bookmark(%User{} = user, status_id) do
1330 bookmarks = Enum.uniq(user.bookmarks ++ [status_id])
1331 update_bookmarks(user, bookmarks)
1332 end
1333
1334 def unbookmark(%User{} = user, status_id) do
1335 bookmarks = Enum.uniq(user.bookmarks -- [status_id])
1336 update_bookmarks(user, bookmarks)
1337 end
1338
1339 def update_bookmarks(%User{} = user, bookmarks) do
1340 user
1341 |> change(%{bookmarks: bookmarks})
1342 |> update_and_set_cache
1343 end
1344
1345 defp normalize_tags(tags) do
1346 [tags]
1347 |> List.flatten()
1348 |> Enum.map(&String.downcase(&1))
1349 end
1350
1351 defp local_nickname_regex do
1352 if Pleroma.Config.get([:instance, :extended_nickname_format]) do
1353 @extended_local_nickname_regex
1354 else
1355 @strict_local_nickname_regex
1356 end
1357 end
1358
1359 def local_nickname(nickname_or_mention) do
1360 nickname_or_mention
1361 |> full_nickname()
1362 |> String.split("@")
1363 |> hd()
1364 end
1365
1366 def full_nickname(nickname_or_mention),
1367 do: String.trim_leading(nickname_or_mention, "@")
1368
1369 def error_user(ap_id) do
1370 %User{
1371 name: ap_id,
1372 ap_id: ap_id,
1373 info: %User.Info{},
1374 nickname: "erroruser@example.com",
1375 inserted_at: NaiveDateTime.utc_now()
1376 }
1377 end
1378
1379 def all_superusers do
1380 from(
1381 u in User,
1382 where: u.local == true,
1383 where: fragment("?->'is_admin' @> 'true' OR ?->'is_moderator' @> 'true'", u.info, u.info)
1384 )
1385 |> Repo.all()
1386 end
1387
1388 defp paginate(query, page, page_size) do
1389 from(u in query,
1390 limit: ^page_size,
1391 offset: ^((page - 1) * page_size)
1392 )
1393 end
1394
1395 def showing_reblogs?(%User{} = user, %User{} = target) do
1396 target.ap_id not in user.info.muted_reblogs
1397 end
1398 end