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