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