Add more user filters + move search to its own module
[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(:search_type, :integer, virtual: true)
54 field(:tags, {:array, :string}, default: [])
55 field(:bookmarks, {:array, :string}, default: [])
56 field(:last_refreshed_at, :naive_datetime_usec)
57 has_many(:notifications, Notification)
58 embeds_one(:info, Pleroma.User.Info)
59
60 timestamps()
61 end
62
63 def auth_active?(%User{local: false}), do: true
64
65 def auth_active?(%User{info: %User.Info{confirmation_pending: false}}), do: true
66
67 def auth_active?(%User{info: %User.Info{confirmation_pending: true}}),
68 do: !Pleroma.Config.get([:instance, :account_activation_required])
69
70 def auth_active?(_), do: false
71
72 def visible_for?(user, for_user \\ nil)
73
74 def visible_for?(%User{id: user_id}, %User{id: for_id}) when user_id == for_id, do: true
75
76 def visible_for?(%User{} = user, for_user) do
77 auth_active?(user) || superuser?(for_user)
78 end
79
80 def visible_for?(_, _), do: false
81
82 def superuser?(%User{local: true, info: %User.Info{is_admin: true}}), do: true
83 def superuser?(%User{local: true, info: %User.Info{is_moderator: true}}), do: true
84 def superuser?(_), do: false
85
86 def avatar_url(user) do
87 case user.avatar do
88 %{"url" => [%{"href" => href} | _]} -> href
89 _ -> "#{Web.base_url()}/images/avi.png"
90 end
91 end
92
93 def banner_url(user) do
94 case user.info.banner do
95 %{"url" => [%{"href" => href} | _]} -> href
96 _ -> "#{Web.base_url()}/images/banner.png"
97 end
98 end
99
100 def profile_url(%User{info: %{source_data: %{"url" => url}}}), do: url
101 def profile_url(%User{ap_id: ap_id}), do: ap_id
102 def profile_url(_), do: nil
103
104 def ap_id(%User{nickname: nickname}) do
105 "#{Web.base_url()}/users/#{nickname}"
106 end
107
108 def ap_followers(%User{follower_address: fa}) when is_binary(fa), do: fa
109 def ap_followers(%User{} = user), do: "#{ap_id(user)}/followers"
110
111 def user_info(%User{} = user) do
112 oneself = if user.local, do: 1, else: 0
113
114 %{
115 following_count: length(user.following) - oneself,
116 note_count: user.info.note_count,
117 follower_count: user.info.follower_count,
118 locked: user.info.locked,
119 confirmation_pending: user.info.confirmation_pending,
120 default_scope: user.info.default_scope
121 }
122 end
123
124 def remote_user_creation(params) do
125 params =
126 params
127 |> Map.put(:info, params[:info] || %{})
128
129 info_cng = User.Info.remote_user_creation(%User.Info{}, params[:info])
130
131 changes =
132 %User{}
133 |> cast(params, [:bio, :name, :ap_id, :nickname, :avatar])
134 |> validate_required([:name, :ap_id])
135 |> unique_constraint(:nickname)
136 |> validate_format(:nickname, @email_regex)
137 |> validate_length(:bio, max: 5000)
138 |> validate_length(:name, max: 100)
139 |> put_change(:local, false)
140 |> put_embed(:info, info_cng)
141
142 if changes.valid? do
143 case info_cng.changes[:source_data] do
144 %{"followers" => followers} ->
145 changes
146 |> put_change(:follower_address, followers)
147
148 _ ->
149 followers = User.ap_followers(%User{nickname: changes.changes[:nickname]})
150
151 changes
152 |> put_change(:follower_address, followers)
153 end
154 else
155 changes
156 end
157 end
158
159 def update_changeset(struct, params \\ %{}) do
160 struct
161 |> cast(params, [:bio, :name, :avatar])
162 |> unique_constraint(:nickname)
163 |> validate_format(:nickname, local_nickname_regex())
164 |> validate_length(:bio, max: 5000)
165 |> validate_length(:name, min: 1, max: 100)
166 end
167
168 def upgrade_changeset(struct, params \\ %{}) do
169 params =
170 params
171 |> Map.put(:last_refreshed_at, NaiveDateTime.utc_now())
172
173 info_cng =
174 struct.info
175 |> User.Info.user_upgrade(params[:info])
176
177 struct
178 |> cast(params, [:bio, :name, :follower_address, :avatar, :last_refreshed_at])
179 |> unique_constraint(:nickname)
180 |> validate_format(:nickname, local_nickname_regex())
181 |> validate_length(:bio, max: 5000)
182 |> validate_length(:name, max: 100)
183 |> put_embed(:info, info_cng)
184 end
185
186 def password_update_changeset(struct, params) do
187 changeset =
188 struct
189 |> cast(params, [:password, :password_confirmation])
190 |> validate_required([:password, :password_confirmation])
191 |> validate_confirmation(:password)
192
193 OAuth.Token.delete_user_tokens(struct)
194 OAuth.Authorization.delete_user_authorizations(struct)
195
196 if changeset.valid? do
197 hashed = Pbkdf2.hashpwsalt(changeset.changes[:password])
198
199 changeset
200 |> put_change(:password_hash, hashed)
201 else
202 changeset
203 end
204 end
205
206 def reset_password(user, data) do
207 update_and_set_cache(password_update_changeset(user, data))
208 end
209
210 def register_changeset(struct, params \\ %{}, opts \\ []) do
211 confirmation_status =
212 if opts[:confirmed] || !Pleroma.Config.get([:instance, :account_activation_required]) do
213 :confirmed
214 else
215 :unconfirmed
216 end
217
218 info_change = User.Info.confirmation_changeset(%User.Info{}, confirmation_status)
219
220 changeset =
221 struct
222 |> cast(params, [:bio, :email, :name, :nickname, :password, :password_confirmation])
223 |> validate_required([:email, :name, :nickname, :password, :password_confirmation])
224 |> validate_confirmation(:password)
225 |> unique_constraint(:email)
226 |> unique_constraint(:nickname)
227 |> validate_exclusion(:nickname, Pleroma.Config.get([Pleroma.User, :restricted_nicknames]))
228 |> validate_format(:nickname, local_nickname_regex())
229 |> validate_format(:email, @email_regex)
230 |> validate_length(:bio, max: 1000)
231 |> validate_length(:name, min: 1, max: 100)
232 |> put_change(:info, info_change)
233
234 if changeset.valid? do
235 hashed = Pbkdf2.hashpwsalt(changeset.changes[:password])
236 ap_id = User.ap_id(%User{nickname: changeset.changes[:nickname]})
237 followers = User.ap_followers(%User{nickname: changeset.changes[:nickname]})
238
239 changeset
240 |> put_change(:password_hash, hashed)
241 |> put_change(:ap_id, ap_id)
242 |> unique_constraint(:ap_id)
243 |> put_change(:following, [followers])
244 |> put_change(:follower_address, followers)
245 else
246 changeset
247 end
248 end
249
250 defp autofollow_users(user) do
251 candidates = Pleroma.Config.get([:instance, :autofollowed_nicknames])
252
253 autofollowed_users =
254 from(u in User,
255 where: u.local == true,
256 where: u.nickname in ^candidates
257 )
258 |> Repo.all()
259
260 follow_all(user, autofollowed_users)
261 end
262
263 @doc "Inserts provided changeset, performs post-registration actions (confirmation email sending etc.)"
264 def register(%Ecto.Changeset{} = changeset) do
265 with {:ok, user} <- Repo.insert(changeset),
266 {:ok, user} <- autofollow_users(user),
267 {:ok, _} <- Pleroma.User.WelcomeMessage.post_welcome_message_to_user(user),
268 {:ok, _} <- try_send_confirmation_email(user) do
269 {:ok, user}
270 end
271 end
272
273 def try_send_confirmation_email(%User{} = user) do
274 if user.info.confirmation_pending &&
275 Pleroma.Config.get([:instance, :account_activation_required]) do
276 user
277 |> Pleroma.UserEmail.account_confirmation_email()
278 |> Pleroma.Mailer.deliver_async()
279 else
280 {:ok, :noop}
281 end
282 end
283
284 def needs_update?(%User{local: true}), do: false
285
286 def needs_update?(%User{local: false, last_refreshed_at: nil}), do: true
287
288 def needs_update?(%User{local: false} = user) do
289 NaiveDateTime.diff(NaiveDateTime.utc_now(), user.last_refreshed_at) >= 86_400
290 end
291
292 def needs_update?(_), do: true
293
294 def maybe_direct_follow(%User{} = follower, %User{local: true, info: %{locked: true}}) do
295 {:ok, follower}
296 end
297
298 def maybe_direct_follow(%User{} = follower, %User{local: true} = followed) do
299 follow(follower, followed)
300 end
301
302 def maybe_direct_follow(%User{} = follower, %User{} = followed) do
303 if not User.ap_enabled?(followed) do
304 follow(follower, followed)
305 else
306 {:ok, follower}
307 end
308 end
309
310 def maybe_follow(%User{} = follower, %User{info: _info} = followed) do
311 if not following?(follower, followed) do
312 follow(follower, followed)
313 else
314 {:ok, follower}
315 end
316 end
317
318 @doc "A mass follow for local users. Respects blocks in both directions but does not create activities."
319 @spec follow_all(User.t(), list(User.t())) :: {atom(), User.t()}
320 def follow_all(follower, followeds) do
321 followed_addresses =
322 followeds
323 |> Enum.reject(fn followed -> blocks?(follower, followed) || blocks?(followed, follower) end)
324 |> Enum.map(fn %{follower_address: fa} -> fa end)
325
326 q =
327 from(u in User,
328 where: u.id == ^follower.id,
329 update: [
330 set: [
331 following:
332 fragment(
333 "array(select distinct unnest (array_cat(?, ?)))",
334 u.following,
335 ^followed_addresses
336 )
337 ]
338 ],
339 select: u
340 )
341
342 {1, [follower]} = Repo.update_all(q, [])
343
344 Enum.each(followeds, fn followed ->
345 update_follower_count(followed)
346 end)
347
348 set_cache(follower)
349 end
350
351 def follow(%User{} = follower, %User{info: info} = followed) do
352 user_config = Application.get_env(:pleroma, :user)
353 deny_follow_blocked = Keyword.get(user_config, :deny_follow_blocked)
354
355 ap_followers = followed.follower_address
356
357 cond do
358 following?(follower, followed) or info.deactivated ->
359 {:error, "Could not follow user: #{followed.nickname} is already on your list."}
360
361 deny_follow_blocked and blocks?(followed, follower) ->
362 {:error, "Could not follow user: #{followed.nickname} blocked you."}
363
364 true ->
365 if !followed.local && follower.local && !ap_enabled?(followed) do
366 Websub.subscribe(follower, followed)
367 end
368
369 q =
370 from(u in User,
371 where: u.id == ^follower.id,
372 update: [push: [following: ^ap_followers]],
373 select: u
374 )
375
376 {1, [follower]} = Repo.update_all(q, [])
377
378 {:ok, _} = update_follower_count(followed)
379
380 set_cache(follower)
381 end
382 end
383
384 def unfollow(%User{} = follower, %User{} = followed) do
385 ap_followers = followed.follower_address
386
387 if following?(follower, followed) and follower.ap_id != followed.ap_id do
388 q =
389 from(u in User,
390 where: u.id == ^follower.id,
391 update: [pull: [following: ^ap_followers]],
392 select: u
393 )
394
395 {1, [follower]} = Repo.update_all(q, [])
396
397 {:ok, followed} = update_follower_count(followed)
398
399 set_cache(follower)
400
401 {:ok, follower, Utils.fetch_latest_follow(follower, followed)}
402 else
403 {:error, "Not subscribed!"}
404 end
405 end
406
407 @spec following?(User.t(), User.t()) :: boolean
408 def following?(%User{} = follower, %User{} = followed) do
409 Enum.member?(follower.following, followed.follower_address)
410 end
411
412 def follow_import(%User{} = follower, followed_identifiers)
413 when is_list(followed_identifiers) do
414 Enum.map(
415 followed_identifiers,
416 fn followed_identifier ->
417 with %User{} = followed <- get_or_fetch(followed_identifier),
418 {:ok, follower} <- maybe_direct_follow(follower, followed),
419 {:ok, _} <- ActivityPub.follow(follower, followed) do
420 followed
421 else
422 err ->
423 Logger.debug("follow_import failed for #{followed_identifier} with: #{inspect(err)}")
424 err
425 end
426 end
427 )
428 end
429
430 def locked?(%User{} = user) do
431 user.info.locked || false
432 end
433
434 def get_by_id(id) do
435 Repo.get_by(User, id: id)
436 end
437
438 def get_by_ap_id(ap_id) do
439 Repo.get_by(User, ap_id: ap_id)
440 end
441
442 # This is mostly an SPC migration fix. This guesses the user nickname by taking the last part
443 # of the ap_id and the domain and tries to get that user
444 def get_by_guessed_nickname(ap_id) do
445 domain = URI.parse(ap_id).host
446 name = List.last(String.split(ap_id, "/"))
447 nickname = "#{name}@#{domain}"
448
449 get_by_nickname(nickname)
450 end
451
452 def set_cache(user) do
453 Cachex.put(:user_cache, "ap_id:#{user.ap_id}", user)
454 Cachex.put(:user_cache, "nickname:#{user.nickname}", user)
455 Cachex.put(:user_cache, "user_info:#{user.id}", user_info(user))
456 {:ok, user}
457 end
458
459 def update_and_set_cache(changeset) do
460 with {:ok, user} <- Repo.update(changeset) do
461 set_cache(user)
462 else
463 e -> e
464 end
465 end
466
467 def invalidate_cache(user) do
468 Cachex.del(:user_cache, "ap_id:#{user.ap_id}")
469 Cachex.del(:user_cache, "nickname:#{user.nickname}")
470 Cachex.del(:user_cache, "user_info:#{user.id}")
471 end
472
473 def get_cached_by_ap_id(ap_id) do
474 key = "ap_id:#{ap_id}"
475 Cachex.fetch!(:user_cache, key, fn _ -> get_by_ap_id(ap_id) end)
476 end
477
478 def get_cached_by_id(id) do
479 key = "id:#{id}"
480
481 ap_id =
482 Cachex.fetch!(:user_cache, key, fn _ ->
483 user = get_by_id(id)
484
485 if user do
486 Cachex.put(:user_cache, "ap_id:#{user.ap_id}", user)
487 {:commit, user.ap_id}
488 else
489 {:ignore, ""}
490 end
491 end)
492
493 get_cached_by_ap_id(ap_id)
494 end
495
496 def get_cached_by_nickname(nickname) do
497 key = "nickname:#{nickname}"
498 Cachex.fetch!(:user_cache, key, fn _ -> get_or_fetch_by_nickname(nickname) end)
499 end
500
501 def get_cached_by_nickname_or_id(nickname_or_id) do
502 get_cached_by_id(nickname_or_id) || get_cached_by_nickname(nickname_or_id)
503 end
504
505 def get_by_nickname(nickname) do
506 Repo.get_by(User, nickname: nickname) ||
507 if Regex.match?(~r(@#{Pleroma.Web.Endpoint.host()})i, nickname) do
508 Repo.get_by(User, nickname: local_nickname(nickname))
509 end
510 end
511
512 def get_by_nickname_or_email(nickname_or_email) do
513 case user = Repo.get_by(User, nickname: nickname_or_email) do
514 %User{} -> user
515 nil -> Repo.get_by(User, email: nickname_or_email)
516 end
517 end
518
519 def get_cached_user_info(user) do
520 key = "user_info:#{user.id}"
521 Cachex.fetch!(:user_cache, key, fn _ -> user_info(user) end)
522 end
523
524 def fetch_by_nickname(nickname) do
525 ap_try = ActivityPub.make_user_from_nickname(nickname)
526
527 case ap_try do
528 {:ok, user} -> {:ok, user}
529 _ -> OStatus.make_user(nickname)
530 end
531 end
532
533 def get_or_fetch_by_nickname(nickname) do
534 with %User{} = user <- get_by_nickname(nickname) do
535 user
536 else
537 _e ->
538 with [_nick, _domain] <- String.split(nickname, "@"),
539 {:ok, user} <- fetch_by_nickname(nickname) do
540 if Pleroma.Config.get([:fetch_initial_posts, :enabled]) do
541 {:ok, _} = Task.start(__MODULE__, :fetch_initial_posts, [user])
542 end
543
544 user
545 else
546 _e -> nil
547 end
548 end
549 end
550
551 @doc "Fetch some posts when the user has just been federated with"
552 def fetch_initial_posts(user) do
553 pages = Pleroma.Config.get!([:fetch_initial_posts, :pages])
554
555 Enum.each(
556 # Insert all the posts in reverse order, so they're in the right order on the timeline
557 Enum.reverse(Utils.fetch_ordered_collection(user.info.source_data["outbox"], pages)),
558 &Pleroma.Web.Federator.incoming_ap_doc/1
559 )
560 end
561
562 def get_followers_query(%User{id: id, follower_address: follower_address}, nil) do
563 from(
564 u in User,
565 where: fragment("? <@ ?", ^[follower_address], u.following),
566 where: u.id != ^id
567 )
568 end
569
570 def get_followers_query(user, page) do
571 from(u in get_followers_query(user, nil))
572 |> paginate(page, 20)
573 end
574
575 def get_followers_query(user), do: get_followers_query(user, nil)
576
577 def get_followers(user, page \\ nil) do
578 q = get_followers_query(user, page)
579
580 {:ok, Repo.all(q)}
581 end
582
583 def get_followers_ids(user, page \\ nil) do
584 q = get_followers_query(user, page)
585
586 Repo.all(from(u in q, select: u.id))
587 end
588
589 def get_friends_query(%User{id: id, following: following}, nil) do
590 from(
591 u in User,
592 where: u.follower_address in ^following,
593 where: u.id != ^id
594 )
595 end
596
597 def get_friends_query(user, page) do
598 from(u in get_friends_query(user, nil))
599 |> paginate(page, 20)
600 end
601
602 def get_friends_query(user), do: get_friends_query(user, nil)
603
604 def get_friends(user, page \\ nil) do
605 q = get_friends_query(user, page)
606
607 {:ok, Repo.all(q)}
608 end
609
610 def get_friends_ids(user, page \\ nil) do
611 q = get_friends_query(user, page)
612
613 Repo.all(from(u in q, select: u.id))
614 end
615
616 def get_follow_requests_query(%User{} = user) do
617 from(
618 a in Activity,
619 where:
620 fragment(
621 "? ->> 'type' = 'Follow'",
622 a.data
623 ),
624 where:
625 fragment(
626 "? ->> 'state' = 'pending'",
627 a.data
628 ),
629 where:
630 fragment(
631 "coalesce((?)->'object'->>'id', (?)->>'object') = ?",
632 a.data,
633 a.data,
634 ^user.ap_id
635 )
636 )
637 end
638
639 def get_follow_requests(%User{} = user) do
640 users =
641 user
642 |> User.get_follow_requests_query()
643 |> join(:inner, [a], u in User, on: a.actor == u.ap_id)
644 |> where([a, u], not fragment("? @> ?", u.following, ^[user.follower_address]))
645 |> group_by([a, u], u.id)
646 |> select([a, u], u)
647 |> Repo.all()
648
649 {:ok, users}
650 end
651
652 def increase_note_count(%User{} = user) do
653 User
654 |> where(id: ^user.id)
655 |> update([u],
656 set: [
657 info:
658 fragment(
659 "jsonb_set(?, '{note_count}', ((?->>'note_count')::int + 1)::varchar::jsonb, true)",
660 u.info,
661 u.info
662 )
663 ]
664 )
665 |> select([u], u)
666 |> Repo.update_all([])
667 |> case do
668 {1, [user]} -> set_cache(user)
669 _ -> {:error, user}
670 end
671 end
672
673 def decrease_note_count(%User{} = user) do
674 User
675 |> where(id: ^user.id)
676 |> update([u],
677 set: [
678 info:
679 fragment(
680 "jsonb_set(?, '{note_count}', (greatest(0, (?->>'note_count')::int - 1))::varchar::jsonb, true)",
681 u.info,
682 u.info
683 )
684 ]
685 )
686 |> select([u], u)
687 |> Repo.update_all([])
688 |> case do
689 {1, [user]} -> set_cache(user)
690 _ -> {:error, user}
691 end
692 end
693
694 def update_note_count(%User{} = user) do
695 note_count_query =
696 from(
697 a in Object,
698 where: fragment("?->>'actor' = ? and ?->>'type' = 'Note'", a.data, ^user.ap_id, a.data),
699 select: count(a.id)
700 )
701
702 note_count = Repo.one(note_count_query)
703
704 info_cng = User.Info.set_note_count(user.info, note_count)
705
706 cng =
707 change(user)
708 |> put_embed(:info, info_cng)
709
710 update_and_set_cache(cng)
711 end
712
713 def update_follower_count(%User{} = user) do
714 follower_count_query =
715 User
716 |> where([u], ^user.follower_address in u.following)
717 |> where([u], u.id != ^user.id)
718 |> select([u], %{count: count(u.id)})
719
720 User
721 |> where(id: ^user.id)
722 |> join(:inner, [u], s in subquery(follower_count_query))
723 |> update([u, s],
724 set: [
725 info:
726 fragment(
727 "jsonb_set(?, '{follower_count}', ?::varchar::jsonb, true)",
728 u.info,
729 s.count
730 )
731 ]
732 )
733 |> select([u], u)
734 |> Repo.update_all([])
735 |> case do
736 {1, [user]} -> set_cache(user)
737 _ -> {:error, user}
738 end
739 end
740
741 def get_users_from_set_query(ap_ids, false) do
742 from(
743 u in User,
744 where: u.ap_id in ^ap_ids
745 )
746 end
747
748 def get_users_from_set_query(ap_ids, true) do
749 query = get_users_from_set_query(ap_ids, false)
750
751 from(
752 u in query,
753 where: u.local == true
754 )
755 end
756
757 def get_users_from_set(ap_ids, local_only \\ true) do
758 get_users_from_set_query(ap_ids, local_only)
759 |> Repo.all()
760 end
761
762 def get_recipients_from_activity(%Activity{recipients: to}) do
763 query =
764 from(
765 u in User,
766 where: u.ap_id in ^to,
767 or_where: fragment("? && ?", u.following, ^to)
768 )
769
770 query = from(u in query, where: u.local == true)
771
772 Repo.all(query)
773 end
774
775 def search(query, resolve \\ false, for_user \\ nil) do
776 # Strip the beginning @ off if there is a query
777 query = String.trim_leading(query, "@")
778
779 if resolve, do: get_or_fetch(query)
780
781 {:ok, results} =
782 Repo.transaction(fn ->
783 Ecto.Adapters.SQL.query(Repo, "select set_limit(0.25)", [])
784 Repo.all(search_query(query, for_user))
785 end)
786
787 results
788 end
789
790 def search_query(query, for_user) do
791 fts_subquery = fts_search_subquery(query)
792 trigram_subquery = trigram_search_subquery(query)
793 union_query = from(s in trigram_subquery, union_all: ^fts_subquery)
794 distinct_query = from(s in subquery(union_query), order_by: s.search_type, distinct: s.id)
795
796 from(s in subquery(boost_search_rank_query(distinct_query, for_user)),
797 order_by: [desc: s.search_rank],
798 limit: 20
799 )
800 end
801
802 defp boost_search_rank_query(query, nil), do: query
803
804 defp boost_search_rank_query(query, for_user) do
805 friends_ids = get_friends_ids(for_user)
806 followers_ids = get_followers_ids(for_user)
807
808 from(u in subquery(query),
809 select_merge: %{
810 search_rank:
811 fragment(
812 """
813 CASE WHEN (?) THEN (?) * 1.3
814 WHEN (?) THEN (?) * 1.2
815 WHEN (?) THEN (?) * 1.1
816 ELSE (?) END
817 """,
818 u.id in ^friends_ids and u.id in ^followers_ids,
819 u.search_rank,
820 u.id in ^friends_ids,
821 u.search_rank,
822 u.id in ^followers_ids,
823 u.search_rank,
824 u.search_rank
825 )
826 }
827 )
828 end
829
830 defp fts_search_subquery(term, query \\ User) do
831 processed_query =
832 term
833 |> String.replace(~r/\W+/, " ")
834 |> String.trim()
835 |> String.split()
836 |> Enum.map(&(&1 <> ":*"))
837 |> Enum.join(" | ")
838
839 from(
840 u in query,
841 select_merge: %{
842 search_type: ^0,
843 search_rank:
844 fragment(
845 """
846 ts_rank_cd(
847 setweight(to_tsvector('simple', regexp_replace(?, '\\W', ' ', 'g')), 'A') ||
848 setweight(to_tsvector('simple', regexp_replace(coalesce(?, ''), '\\W', ' ', 'g')), 'B'),
849 to_tsquery('simple', ?),
850 32
851 )
852 """,
853 u.nickname,
854 u.name,
855 ^processed_query
856 )
857 },
858 where:
859 fragment(
860 """
861 (setweight(to_tsvector('simple', regexp_replace(?, '\\W', ' ', 'g')), 'A') ||
862 setweight(to_tsvector('simple', regexp_replace(coalesce(?, ''), '\\W', ' ', 'g')), 'B')) @@ to_tsquery('simple', ?)
863 """,
864 u.nickname,
865 u.name,
866 ^processed_query
867 )
868 )
869 end
870
871 defp trigram_search_subquery(term) do
872 from(
873 u in User,
874 select_merge: %{
875 # ^1 gives 'Postgrex expected a binary, got 1' for some weird reason
876 search_type: fragment("?", 1),
877 search_rank:
878 fragment(
879 "similarity(?, trim(? || ' ' || coalesce(?, '')))",
880 ^term,
881 u.nickname,
882 u.name
883 )
884 },
885 where: fragment("trim(? || ' ' || coalesce(?, '')) % ?", u.nickname, u.name, ^term)
886 )
887 end
888
889 def blocks_import(%User{} = blocker, blocked_identifiers) when is_list(blocked_identifiers) do
890 Enum.map(
891 blocked_identifiers,
892 fn blocked_identifier ->
893 with %User{} = blocked <- get_or_fetch(blocked_identifier),
894 {:ok, blocker} <- block(blocker, blocked),
895 {:ok, _} <- ActivityPub.block(blocker, blocked) do
896 blocked
897 else
898 err ->
899 Logger.debug("blocks_import failed for #{blocked_identifier} with: #{inspect(err)}")
900 err
901 end
902 end
903 )
904 end
905
906 def mute(muter, %User{ap_id: ap_id}) do
907 info_cng =
908 muter.info
909 |> User.Info.add_to_mutes(ap_id)
910
911 cng =
912 change(muter)
913 |> put_embed(:info, info_cng)
914
915 update_and_set_cache(cng)
916 end
917
918 def unmute(muter, %{ap_id: ap_id}) do
919 info_cng =
920 muter.info
921 |> User.Info.remove_from_mutes(ap_id)
922
923 cng =
924 change(muter)
925 |> put_embed(:info, info_cng)
926
927 update_and_set_cache(cng)
928 end
929
930 def block(blocker, %User{ap_id: ap_id} = blocked) do
931 # sever any follow relationships to prevent leaks per activitypub (Pleroma issue #213)
932 blocker =
933 if following?(blocker, blocked) do
934 {:ok, blocker, _} = unfollow(blocker, blocked)
935 blocker
936 else
937 blocker
938 end
939
940 if following?(blocked, blocker) do
941 unfollow(blocked, blocker)
942 end
943
944 info_cng =
945 blocker.info
946 |> User.Info.add_to_block(ap_id)
947
948 cng =
949 change(blocker)
950 |> put_embed(:info, info_cng)
951
952 update_and_set_cache(cng)
953 end
954
955 # helper to handle the block given only an actor's AP id
956 def block(blocker, %{ap_id: ap_id}) do
957 block(blocker, User.get_by_ap_id(ap_id))
958 end
959
960 def unblock(blocker, %{ap_id: ap_id}) do
961 info_cng =
962 blocker.info
963 |> User.Info.remove_from_block(ap_id)
964
965 cng =
966 change(blocker)
967 |> put_embed(:info, info_cng)
968
969 update_and_set_cache(cng)
970 end
971
972 def mutes?(nil, _), do: false
973 def mutes?(user, %{ap_id: ap_id}), do: Enum.member?(user.info.mutes, ap_id)
974
975 def blocks?(user, %{ap_id: ap_id}) do
976 blocks = user.info.blocks
977 domain_blocks = user.info.domain_blocks
978 %{host: host} = URI.parse(ap_id)
979
980 Enum.member?(blocks, ap_id) ||
981 Enum.any?(domain_blocks, fn domain ->
982 host == domain
983 end)
984 end
985
986 def muted_users(user),
987 do: Repo.all(from(u in User, where: u.ap_id in ^user.info.mutes))
988
989 def blocked_users(user),
990 do: Repo.all(from(u in User, where: u.ap_id in ^user.info.blocks))
991
992 def block_domain(user, domain) do
993 info_cng =
994 user.info
995 |> User.Info.add_to_domain_block(domain)
996
997 cng =
998 change(user)
999 |> put_embed(:info, info_cng)
1000
1001 update_and_set_cache(cng)
1002 end
1003
1004 def unblock_domain(user, domain) do
1005 info_cng =
1006 user.info
1007 |> User.Info.remove_from_domain_block(domain)
1008
1009 cng =
1010 change(user)
1011 |> put_embed(:info, info_cng)
1012
1013 update_and_set_cache(cng)
1014 end
1015
1016 def maybe_local_user_query(query, local) do
1017 if local, do: local_user_query(query), else: query
1018 end
1019
1020 def local_user_query(query \\ User) do
1021 from(
1022 u in query,
1023 where: u.local == true,
1024 where: not is_nil(u.nickname)
1025 )
1026 end
1027
1028 def maybe_external_user_query(query, external) do
1029 if external, do: external_user_query(query), else: query
1030 end
1031
1032 def external_user_query(query \\ User) do
1033 from(
1034 u in query,
1035 where: u.local == false,
1036 where: not is_nil(u.nickname)
1037 )
1038 end
1039
1040 def maybe_active_user_query(query, active) do
1041 if active, do: active_user_query(query), else: query
1042 end
1043
1044 def active_user_query(query \\ User) do
1045 from(
1046 u in query,
1047 where: fragment("not (?->'deactivated' @> 'true')", u.info),
1048 where: not is_nil(u.nickname)
1049 )
1050 end
1051
1052 def maybe_deactivated_user_query(query, deactivated) do
1053 if deactivated, do: deactivated_user_query(query), else: query
1054 end
1055
1056 def deactivated_user_query(query \\ User) do
1057 from(
1058 u in query,
1059 where: fragment("(?->'deactivated' @> 'true')", u.info),
1060 where: not is_nil(u.nickname)
1061 )
1062 end
1063
1064 def active_local_user_query do
1065 from(
1066 u in local_user_query(),
1067 where: fragment("not (?->'deactivated' @> 'true')", u.info)
1068 )
1069 end
1070
1071 def moderator_user_query do
1072 from(
1073 u in User,
1074 where: u.local == true,
1075 where: fragment("?->'is_moderator' @> 'true'", u.info)
1076 )
1077 end
1078
1079 def deactivate(%User{} = user, status \\ true) do
1080 info_cng = User.Info.set_activation_status(user.info, status)
1081
1082 cng =
1083 change(user)
1084 |> put_embed(:info, info_cng)
1085
1086 update_and_set_cache(cng)
1087 end
1088
1089 def delete(%User{} = user) do
1090 {:ok, user} = User.deactivate(user)
1091
1092 # Remove all relationships
1093 {:ok, followers} = User.get_followers(user)
1094
1095 followers
1096 |> Enum.each(fn follower -> User.unfollow(follower, user) end)
1097
1098 {:ok, friends} = User.get_friends(user)
1099
1100 friends
1101 |> Enum.each(fn followed -> User.unfollow(user, followed) end)
1102
1103 query =
1104 from(a in Activity, where: a.actor == ^user.ap_id)
1105 |> Activity.with_preloaded_object()
1106
1107 Repo.all(query)
1108 |> Enum.each(fn activity ->
1109 case activity.data["type"] do
1110 "Create" ->
1111 ActivityPub.delete(Object.normalize(activity))
1112
1113 # TODO: Do something with likes, follows, repeats.
1114 _ ->
1115 "Doing nothing"
1116 end
1117 end)
1118
1119 {:ok, user}
1120 end
1121
1122 def html_filter_policy(%User{info: %{no_rich_text: true}}) do
1123 Pleroma.HTML.Scrubber.TwitterText
1124 end
1125
1126 @default_scrubbers Pleroma.Config.get([:markup, :scrub_policy])
1127
1128 def html_filter_policy(_), do: @default_scrubbers
1129
1130 def fetch_by_ap_id(ap_id) do
1131 ap_try = ActivityPub.make_user_from_ap_id(ap_id)
1132
1133 case ap_try do
1134 {:ok, user} ->
1135 user
1136
1137 _ ->
1138 case OStatus.make_user(ap_id) do
1139 {:ok, user} -> user
1140 _ -> {:error, "Could not fetch by AP id"}
1141 end
1142 end
1143 end
1144
1145 def get_or_fetch_by_ap_id(ap_id) do
1146 user = get_by_ap_id(ap_id)
1147
1148 if !is_nil(user) and !User.needs_update?(user) do
1149 user
1150 else
1151 # Whether to fetch initial posts for the user (if it's a new user & the fetching is enabled)
1152 should_fetch_initial = is_nil(user) and Pleroma.Config.get([:fetch_initial_posts, :enabled])
1153
1154 user = fetch_by_ap_id(ap_id)
1155
1156 if should_fetch_initial do
1157 with %User{} = user do
1158 {:ok, _} = Task.start(__MODULE__, :fetch_initial_posts, [user])
1159 end
1160 end
1161
1162 user
1163 end
1164 end
1165
1166 def get_or_create_instance_user do
1167 relay_uri = "#{Pleroma.Web.Endpoint.url()}/relay"
1168
1169 if user = get_by_ap_id(relay_uri) do
1170 user
1171 else
1172 changes =
1173 %User{info: %User.Info{}}
1174 |> cast(%{}, [:ap_id, :nickname, :local])
1175 |> put_change(:ap_id, relay_uri)
1176 |> put_change(:nickname, nil)
1177 |> put_change(:local, true)
1178 |> put_change(:follower_address, relay_uri <> "/followers")
1179
1180 {:ok, user} = Repo.insert(changes)
1181 user
1182 end
1183 end
1184
1185 # AP style
1186 def public_key_from_info(%{
1187 source_data: %{"publicKey" => %{"publicKeyPem" => public_key_pem}}
1188 }) do
1189 key =
1190 public_key_pem
1191 |> :public_key.pem_decode()
1192 |> hd()
1193 |> :public_key.pem_entry_decode()
1194
1195 {:ok, key}
1196 end
1197
1198 # OStatus Magic Key
1199 def public_key_from_info(%{magic_key: magic_key}) do
1200 {:ok, Pleroma.Web.Salmon.decode_key(magic_key)}
1201 end
1202
1203 def get_public_key_for_ap_id(ap_id) do
1204 with %User{} = user <- get_or_fetch_by_ap_id(ap_id),
1205 {:ok, public_key} <- public_key_from_info(user.info) do
1206 {:ok, public_key}
1207 else
1208 _ -> :error
1209 end
1210 end
1211
1212 defp blank?(""), do: nil
1213 defp blank?(n), do: n
1214
1215 def insert_or_update_user(data) do
1216 data =
1217 data
1218 |> Map.put(:name, blank?(data[:name]) || data[:nickname])
1219
1220 cs = User.remote_user_creation(data)
1221
1222 Repo.insert(cs, on_conflict: :replace_all, conflict_target: :nickname)
1223 end
1224
1225 def ap_enabled?(%User{local: true}), do: true
1226 def ap_enabled?(%User{info: info}), do: info.ap_enabled
1227 def ap_enabled?(_), do: false
1228
1229 @doc "Gets or fetch a user by uri or nickname."
1230 @spec get_or_fetch(String.t()) :: User.t()
1231 def get_or_fetch("http" <> _host = uri), do: get_or_fetch_by_ap_id(uri)
1232 def get_or_fetch(nickname), do: get_or_fetch_by_nickname(nickname)
1233
1234 # wait a period of time and return newest version of the User structs
1235 # this is because we have synchronous follow APIs and need to simulate them
1236 # with an async handshake
1237 def wait_and_refresh(_, %User{local: true} = a, %User{local: true} = b) do
1238 with %User{} = a <- Repo.get(User, a.id),
1239 %User{} = b <- Repo.get(User, b.id) do
1240 {:ok, a, b}
1241 else
1242 _e ->
1243 :error
1244 end
1245 end
1246
1247 def wait_and_refresh(timeout, %User{} = a, %User{} = b) do
1248 with :ok <- :timer.sleep(timeout),
1249 %User{} = a <- Repo.get(User, a.id),
1250 %User{} = b <- Repo.get(User, b.id) do
1251 {:ok, a, b}
1252 else
1253 _e ->
1254 :error
1255 end
1256 end
1257
1258 def parse_bio(bio, user \\ %User{info: %{source_data: %{}}})
1259 def parse_bio(nil, _user), do: ""
1260 def parse_bio(bio, _user) when bio == "", do: bio
1261
1262 def parse_bio(bio, user) do
1263 emoji =
1264 (user.info.source_data["tag"] || [])
1265 |> Enum.filter(fn %{"type" => t} -> t == "Emoji" end)
1266 |> Enum.map(fn %{"icon" => %{"url" => url}, "name" => name} ->
1267 {String.trim(name, ":"), url}
1268 end)
1269
1270 # TODO: get profile URLs other than user.ap_id
1271 profile_urls = [user.ap_id]
1272
1273 bio
1274 |> CommonUtils.format_input("text/plain",
1275 mentions_format: :full,
1276 rel: &RelMe.maybe_put_rel_me(&1, profile_urls)
1277 )
1278 |> elem(0)
1279 |> Formatter.emojify(emoji)
1280 end
1281
1282 def tag(user_identifiers, tags) when is_list(user_identifiers) do
1283 Repo.transaction(fn ->
1284 for user_identifier <- user_identifiers, do: tag(user_identifier, tags)
1285 end)
1286 end
1287
1288 def tag(nickname, tags) when is_binary(nickname),
1289 do: tag(User.get_by_nickname(nickname), tags)
1290
1291 def tag(%User{} = user, tags),
1292 do: update_tags(user, Enum.uniq((user.tags || []) ++ normalize_tags(tags)))
1293
1294 def untag(user_identifiers, tags) when is_list(user_identifiers) do
1295 Repo.transaction(fn ->
1296 for user_identifier <- user_identifiers, do: untag(user_identifier, tags)
1297 end)
1298 end
1299
1300 def untag(nickname, tags) when is_binary(nickname),
1301 do: untag(User.get_by_nickname(nickname), tags)
1302
1303 def untag(%User{} = user, tags),
1304 do: update_tags(user, (user.tags || []) -- normalize_tags(tags))
1305
1306 defp update_tags(%User{} = user, new_tags) do
1307 {:ok, updated_user} =
1308 user
1309 |> change(%{tags: new_tags})
1310 |> update_and_set_cache()
1311
1312 updated_user
1313 end
1314
1315 def bookmark(%User{} = user, status_id) do
1316 bookmarks = Enum.uniq(user.bookmarks ++ [status_id])
1317 update_bookmarks(user, bookmarks)
1318 end
1319
1320 def unbookmark(%User{} = user, status_id) do
1321 bookmarks = Enum.uniq(user.bookmarks -- [status_id])
1322 update_bookmarks(user, bookmarks)
1323 end
1324
1325 def update_bookmarks(%User{} = user, bookmarks) do
1326 user
1327 |> change(%{bookmarks: bookmarks})
1328 |> update_and_set_cache
1329 end
1330
1331 defp normalize_tags(tags) do
1332 [tags]
1333 |> List.flatten()
1334 |> Enum.map(&String.downcase(&1))
1335 end
1336
1337 defp local_nickname_regex do
1338 if Pleroma.Config.get([:instance, :extended_nickname_format]) do
1339 @extended_local_nickname_regex
1340 else
1341 @strict_local_nickname_regex
1342 end
1343 end
1344
1345 def local_nickname(nickname_or_mention) do
1346 nickname_or_mention
1347 |> full_nickname()
1348 |> String.split("@")
1349 |> hd()
1350 end
1351
1352 def full_nickname(nickname_or_mention),
1353 do: String.trim_leading(nickname_or_mention, "@")
1354
1355 def error_user(ap_id) do
1356 %User{
1357 name: ap_id,
1358 ap_id: ap_id,
1359 info: %User.Info{},
1360 nickname: "erroruser@example.com",
1361 inserted_at: NaiveDateTime.utc_now()
1362 }
1363 end
1364
1365 def all_superusers do
1366 from(
1367 u in User,
1368 where: u.local == true,
1369 where: fragment("?->'is_admin' @> 'true' OR ?->'is_moderator' @> 'true'", u.info, u.info)
1370 )
1371 |> Repo.all()
1372 end
1373
1374 defp paginate(query, page, page_size) do
1375 from(u in query,
1376 limit: ^page_size,
1377 offset: ^((page - 1) * page_size)
1378 )
1379 end
1380
1381 def showing_reblogs?(%User{} = user, %User{} = target) do
1382 target.ap_id not in user.info.muted_reblogs
1383 end
1384 end