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