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