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