Fix with expression always matching
[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 with %User{} = user <- fetch_by_ap_id(ap_id) do
1171 if Pleroma.Config.get([:fetch_initial_posts, :enabled]) do
1172 {:ok, _} = Task.start(__MODULE__, :fetch_initial_posts, [user])
1173 end
1174
1175 {:ok, user}
1176 else
1177 _ -> {:error, "Could not fetch by AP id"}
1178 end
1179 end
1180 end
1181
1182 def get_or_create_instance_user do
1183 relay_uri = "#{Pleroma.Web.Endpoint.url()}/relay"
1184
1185 if user = get_by_ap_id(relay_uri) do
1186 user
1187 else
1188 changes =
1189 %User{info: %User.Info{}}
1190 |> cast(%{}, [:ap_id, :nickname, :local])
1191 |> put_change(:ap_id, relay_uri)
1192 |> put_change(:nickname, nil)
1193 |> put_change(:local, true)
1194 |> put_change(:follower_address, relay_uri <> "/followers")
1195
1196 {:ok, user} = Repo.insert(changes)
1197 user
1198 end
1199 end
1200
1201 # AP style
1202 def public_key_from_info(%{
1203 source_data: %{"publicKey" => %{"publicKeyPem" => public_key_pem}}
1204 }) do
1205 key =
1206 public_key_pem
1207 |> :public_key.pem_decode()
1208 |> hd()
1209 |> :public_key.pem_entry_decode()
1210
1211 {:ok, key}
1212 end
1213
1214 # OStatus Magic Key
1215 def public_key_from_info(%{magic_key: magic_key}) do
1216 {:ok, Pleroma.Web.Salmon.decode_key(magic_key)}
1217 end
1218
1219 def get_public_key_for_ap_id(ap_id) do
1220 with {:ok, %User{} = user} <- get_or_fetch_by_ap_id(ap_id),
1221 {:ok, public_key} <- public_key_from_info(user.info) do
1222 {:ok, public_key}
1223 else
1224 _ -> :error
1225 end
1226 end
1227
1228 defp blank?(""), do: nil
1229 defp blank?(n), do: n
1230
1231 def insert_or_update_user(data) do
1232 data =
1233 data
1234 |> Map.put(:name, blank?(data[:name]) || data[:nickname])
1235
1236 cs = User.remote_user_creation(data)
1237
1238 Repo.insert(cs, on_conflict: :replace_all, conflict_target: :nickname)
1239 end
1240
1241 def ap_enabled?(%User{local: true}), do: true
1242 def ap_enabled?(%User{info: info}), do: info.ap_enabled
1243 def ap_enabled?(_), do: false
1244
1245 @doc "Gets or fetch a user by uri or nickname."
1246 @spec get_or_fetch(String.t()) :: User.t()
1247 def get_or_fetch("http" <> _host = uri), do: get_or_fetch_by_ap_id(uri)
1248 def get_or_fetch(nickname), do: get_or_fetch_by_nickname(nickname)
1249
1250 # wait a period of time and return newest version of the User structs
1251 # this is because we have synchronous follow APIs and need to simulate them
1252 # with an async handshake
1253 def wait_and_refresh(_, %User{local: true} = a, %User{local: true} = b) do
1254 with %User{} = a <- Repo.get(User, a.id),
1255 %User{} = b <- Repo.get(User, b.id) do
1256 {:ok, a, b}
1257 else
1258 _e ->
1259 :error
1260 end
1261 end
1262
1263 def wait_and_refresh(timeout, %User{} = a, %User{} = b) do
1264 with :ok <- :timer.sleep(timeout),
1265 %User{} = a <- Repo.get(User, a.id),
1266 %User{} = b <- Repo.get(User, b.id) do
1267 {:ok, a, b}
1268 else
1269 _e ->
1270 :error
1271 end
1272 end
1273
1274 def parse_bio(bio, user \\ %User{info: %{source_data: %{}}})
1275 def parse_bio(nil, _user), do: ""
1276 def parse_bio(bio, _user) when bio == "", do: bio
1277
1278 def parse_bio(bio, user) do
1279 emoji =
1280 (user.info.source_data["tag"] || [])
1281 |> Enum.filter(fn %{"type" => t} -> t == "Emoji" end)
1282 |> Enum.map(fn %{"icon" => %{"url" => url}, "name" => name} ->
1283 {String.trim(name, ":"), url}
1284 end)
1285
1286 # TODO: get profile URLs other than user.ap_id
1287 profile_urls = [user.ap_id]
1288
1289 bio
1290 |> CommonUtils.format_input("text/plain",
1291 mentions_format: :full,
1292 rel: &RelMe.maybe_put_rel_me(&1, profile_urls)
1293 )
1294 |> elem(0)
1295 |> Formatter.emojify(emoji)
1296 end
1297
1298 def tag(user_identifiers, tags) when is_list(user_identifiers) do
1299 Repo.transaction(fn ->
1300 for user_identifier <- user_identifiers, do: tag(user_identifier, tags)
1301 end)
1302 end
1303
1304 def tag(nickname, tags) when is_binary(nickname),
1305 do: tag(User.get_by_nickname(nickname), tags)
1306
1307 def tag(%User{} = user, tags),
1308 do: update_tags(user, Enum.uniq((user.tags || []) ++ normalize_tags(tags)))
1309
1310 def untag(user_identifiers, tags) when is_list(user_identifiers) do
1311 Repo.transaction(fn ->
1312 for user_identifier <- user_identifiers, do: untag(user_identifier, tags)
1313 end)
1314 end
1315
1316 def untag(nickname, tags) when is_binary(nickname),
1317 do: untag(User.get_by_nickname(nickname), tags)
1318
1319 def untag(%User{} = user, tags),
1320 do: update_tags(user, (user.tags || []) -- normalize_tags(tags))
1321
1322 defp update_tags(%User{} = user, new_tags) do
1323 {:ok, updated_user} =
1324 user
1325 |> change(%{tags: new_tags})
1326 |> update_and_set_cache()
1327
1328 updated_user
1329 end
1330
1331 def bookmark(%User{} = user, status_id) do
1332 bookmarks = Enum.uniq(user.bookmarks ++ [status_id])
1333 update_bookmarks(user, bookmarks)
1334 end
1335
1336 def unbookmark(%User{} = user, status_id) do
1337 bookmarks = Enum.uniq(user.bookmarks -- [status_id])
1338 update_bookmarks(user, bookmarks)
1339 end
1340
1341 def update_bookmarks(%User{} = user, bookmarks) do
1342 user
1343 |> change(%{bookmarks: bookmarks})
1344 |> update_and_set_cache
1345 end
1346
1347 defp normalize_tags(tags) do
1348 [tags]
1349 |> List.flatten()
1350 |> Enum.map(&String.downcase(&1))
1351 end
1352
1353 defp local_nickname_regex do
1354 if Pleroma.Config.get([:instance, :extended_nickname_format]) do
1355 @extended_local_nickname_regex
1356 else
1357 @strict_local_nickname_regex
1358 end
1359 end
1360
1361 def local_nickname(nickname_or_mention) do
1362 nickname_or_mention
1363 |> full_nickname()
1364 |> String.split("@")
1365 |> hd()
1366 end
1367
1368 def full_nickname(nickname_or_mention),
1369 do: String.trim_leading(nickname_or_mention, "@")
1370
1371 def error_user(ap_id) do
1372 %User{
1373 name: ap_id,
1374 ap_id: ap_id,
1375 info: %User.Info{},
1376 nickname: "erroruser@example.com",
1377 inserted_at: NaiveDateTime.utc_now()
1378 }
1379 end
1380
1381 def all_superusers do
1382 from(
1383 u in User,
1384 where: u.local == true,
1385 where: fragment("?->'is_admin' @> 'true' OR ?->'is_moderator' @> 'true'", u.info, u.info)
1386 )
1387 |> Repo.all()
1388 end
1389
1390 defp paginate(query, page, page_size) do
1391 from(u in query,
1392 limit: ^page_size,
1393 offset: ^((page - 1) * page_size)
1394 )
1395 end
1396 end