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