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