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