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