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