Merge branch 'feature/rework-user-deletion' into 'develop'
[akkoma] / lib / pleroma / user.ex
1 # Pleroma: A lightweight social networking server
2 # Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
3 # SPDX-License-Identifier: AGPL-3.0-only
4
5 defmodule Pleroma.User do
6 use Ecto.Schema
7
8 import Ecto.Changeset
9 import Ecto.Query
10
11 alias Comeonin.Pbkdf2
12 alias Pleroma.Activity
13 alias Pleroma.Keys
14 alias Pleroma.Notification
15 alias Pleroma.Object
16 alias Pleroma.Registration
17 alias Pleroma.Repo
18 alias Pleroma.RepoStreamer
19 alias Pleroma.User
20 alias Pleroma.Web
21 alias Pleroma.Web.ActivityPub.ActivityPub
22 alias Pleroma.Web.ActivityPub.Utils
23 alias Pleroma.Web.CommonAPI.Utils, as: CommonUtils
24 alias Pleroma.Web.OAuth
25 alias Pleroma.Web.OStatus
26 alias Pleroma.Web.RelMe
27 alias Pleroma.Web.Websub
28
29 require Logger
30
31 @type t :: %__MODULE__{}
32
33 @primary_key {:id, Pleroma.FlakeId, autogenerate: true}
34
35 # credo:disable-for-next-line Credo.Check.Readability.MaxLineLength
36 @email_regex ~r/^[a-zA-Z0-9.!#$%&'*+\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/
37
38 @strict_local_nickname_regex ~r/^[a-zA-Z\d]+$/
39 @extended_local_nickname_regex ~r/^[a-zA-Z\d_-]+$/
40
41 schema "users" do
42 field(:bio, :string)
43 field(:email, :string)
44 field(:name, :string)
45 field(:nickname, :string)
46 field(:password_hash, :string)
47 field(:password, :string, virtual: true)
48 field(:password_confirmation, :string, virtual: true)
49 field(:following, {:array, :string}, default: [])
50 field(:ap_id, :string)
51 field(:avatar, :map)
52 field(:local, :boolean, default: true)
53 field(:follower_address, :string)
54 field(:search_rank, :float, virtual: true)
55 field(:search_type, :integer, virtual: true)
56 field(:tags, {:array, :string}, default: [])
57 field(:last_refreshed_at, :naive_datetime_usec)
58 has_many(: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 # Remove all relationships
937 {:ok, followers} = User.get_followers(user)
938
939 Enum.each(followers, fn follower ->
940 ActivityPub.unfollow(follower, user)
941 User.unfollow(follower, user)
942 end)
943
944 {:ok, friends} = User.get_friends(user)
945
946 Enum.each(friends, fn followed ->
947 ActivityPub.unfollow(user, followed)
948 User.unfollow(user, followed)
949 end)
950
951 delete_user_activities(user)
952
953 {:ok, _user} = Repo.delete(user)
954 end
955
956 @spec perform(atom(), User.t()) :: {:ok, User.t()}
957 def perform(:fetch_initial_posts, %User{} = user) do
958 pages = Pleroma.Config.get!([:fetch_initial_posts, :pages])
959
960 Enum.each(
961 # Insert all the posts in reverse order, so they're in the right order on the timeline
962 Enum.reverse(Utils.fetch_ordered_collection(user.info.source_data["outbox"], pages)),
963 &Pleroma.Web.Federator.incoming_ap_doc/1
964 )
965
966 {:ok, user}
967 end
968
969 def perform(:deactivate_async, user, status), do: deactivate(user, status)
970
971 @spec perform(atom(), User.t(), list()) :: list() | {:error, any()}
972 def perform(:blocks_import, %User{} = blocker, blocked_identifiers)
973 when is_list(blocked_identifiers) do
974 Enum.map(
975 blocked_identifiers,
976 fn blocked_identifier ->
977 with {:ok, %User{} = blocked} <- get_or_fetch(blocked_identifier),
978 {:ok, blocker} <- block(blocker, blocked),
979 {:ok, _} <- ActivityPub.block(blocker, blocked) do
980 blocked
981 else
982 err ->
983 Logger.debug("blocks_import failed for #{blocked_identifier} with: #{inspect(err)}")
984 err
985 end
986 end
987 )
988 end
989
990 @spec perform(atom(), User.t(), list()) :: list() | {:error, any()}
991 def perform(:follow_import, %User{} = follower, followed_identifiers)
992 when is_list(followed_identifiers) do
993 Enum.map(
994 followed_identifiers,
995 fn followed_identifier ->
996 with {:ok, %User{} = followed} <- get_or_fetch(followed_identifier),
997 {:ok, follower} <- maybe_direct_follow(follower, followed),
998 {:ok, _} <- ActivityPub.follow(follower, followed) do
999 followed
1000 else
1001 err ->
1002 Logger.debug("follow_import failed for #{followed_identifier} with: #{inspect(err)}")
1003 err
1004 end
1005 end
1006 )
1007 end
1008
1009 def blocks_import(%User{} = blocker, blocked_identifiers) when is_list(blocked_identifiers),
1010 do:
1011 PleromaJobQueue.enqueue(:background, __MODULE__, [
1012 :blocks_import,
1013 blocker,
1014 blocked_identifiers
1015 ])
1016
1017 def follow_import(%User{} = follower, followed_identifiers) when is_list(followed_identifiers),
1018 do:
1019 PleromaJobQueue.enqueue(:background, __MODULE__, [
1020 :follow_import,
1021 follower,
1022 followed_identifiers
1023 ])
1024
1025 def delete_user_activities(%User{ap_id: ap_id} = user) do
1026 ap_id
1027 |> Activity.query_by_actor()
1028 |> RepoStreamer.chunk_stream(50)
1029 |> Stream.each(fn activities ->
1030 Enum.each(activities, &delete_activity(&1))
1031 end)
1032 |> Stream.run()
1033
1034 {:ok, user}
1035 end
1036
1037 defp delete_activity(%{data: %{"type" => "Create"}} = activity) do
1038 activity
1039 |> Object.normalize()
1040 |> ActivityPub.delete()
1041 end
1042
1043 defp delete_activity(%{data: %{"type" => "Like"}} = activity) do
1044 user = get_cached_by_ap_id(activity.actor)
1045 object = Object.normalize(activity)
1046
1047 ActivityPub.unlike(user, object)
1048 end
1049
1050 defp delete_activity(%{data: %{"type" => "Announce"}} = activity) do
1051 user = get_cached_by_ap_id(activity.actor)
1052 object = Object.normalize(activity)
1053
1054 ActivityPub.unannounce(user, object)
1055 end
1056
1057 defp delete_activity(_activity), do: "Doing nothing"
1058
1059 def html_filter_policy(%User{info: %{no_rich_text: true}}) do
1060 Pleroma.HTML.Scrubber.TwitterText
1061 end
1062
1063 def html_filter_policy(_), do: Pleroma.Config.get([:markup, :scrub_policy])
1064
1065 def fetch_by_ap_id(ap_id) do
1066 ap_try = ActivityPub.make_user_from_ap_id(ap_id)
1067
1068 case ap_try do
1069 {:ok, user} ->
1070 {:ok, user}
1071
1072 _ ->
1073 case OStatus.make_user(ap_id) do
1074 {:ok, user} -> {:ok, user}
1075 _ -> {:error, "Could not fetch by AP id"}
1076 end
1077 end
1078 end
1079
1080 def get_or_fetch_by_ap_id(ap_id) do
1081 user = get_cached_by_ap_id(ap_id)
1082
1083 if !is_nil(user) and !User.needs_update?(user) do
1084 {:ok, user}
1085 else
1086 # Whether to fetch initial posts for the user (if it's a new user & the fetching is enabled)
1087 should_fetch_initial = is_nil(user) and Pleroma.Config.get([:fetch_initial_posts, :enabled])
1088
1089 resp = fetch_by_ap_id(ap_id)
1090
1091 if should_fetch_initial do
1092 with {:ok, %User{} = user} <- resp do
1093 fetch_initial_posts(user)
1094 end
1095 end
1096
1097 resp
1098 end
1099 end
1100
1101 def get_or_create_instance_user do
1102 relay_uri = "#{Pleroma.Web.Endpoint.url()}/relay"
1103
1104 if user = get_cached_by_ap_id(relay_uri) do
1105 user
1106 else
1107 changes =
1108 %User{info: %User.Info{}}
1109 |> cast(%{}, [:ap_id, :nickname, :local])
1110 |> put_change(:ap_id, relay_uri)
1111 |> put_change(:nickname, nil)
1112 |> put_change(:local, true)
1113 |> put_change(:follower_address, relay_uri <> "/followers")
1114
1115 {:ok, user} = Repo.insert(changes)
1116 user
1117 end
1118 end
1119
1120 # AP style
1121 def public_key_from_info(%{
1122 source_data: %{"publicKey" => %{"publicKeyPem" => public_key_pem}}
1123 }) do
1124 key =
1125 public_key_pem
1126 |> :public_key.pem_decode()
1127 |> hd()
1128 |> :public_key.pem_entry_decode()
1129
1130 {:ok, key}
1131 end
1132
1133 # OStatus Magic Key
1134 def public_key_from_info(%{magic_key: magic_key}) do
1135 {:ok, Pleroma.Web.Salmon.decode_key(magic_key)}
1136 end
1137
1138 def get_public_key_for_ap_id(ap_id) do
1139 with {:ok, %User{} = user} <- get_or_fetch_by_ap_id(ap_id),
1140 {:ok, public_key} <- public_key_from_info(user.info) do
1141 {:ok, public_key}
1142 else
1143 _ -> :error
1144 end
1145 end
1146
1147 defp blank?(""), do: nil
1148 defp blank?(n), do: n
1149
1150 def insert_or_update_user(data) do
1151 data
1152 |> Map.put(:name, blank?(data[:name]) || data[:nickname])
1153 |> remote_user_creation()
1154 |> Repo.insert(on_conflict: :replace_all, conflict_target: :nickname)
1155 |> set_cache()
1156 end
1157
1158 def ap_enabled?(%User{local: true}), do: true
1159 def ap_enabled?(%User{info: info}), do: info.ap_enabled
1160 def ap_enabled?(_), do: false
1161
1162 @doc "Gets or fetch a user by uri or nickname."
1163 @spec get_or_fetch(String.t()) :: {:ok, User.t()} | {:error, String.t()}
1164 def get_or_fetch("http" <> _host = uri), do: get_or_fetch_by_ap_id(uri)
1165 def get_or_fetch(nickname), do: get_or_fetch_by_nickname(nickname)
1166
1167 # wait a period of time and return newest version of the User structs
1168 # this is because we have synchronous follow APIs and need to simulate them
1169 # with an async handshake
1170 def wait_and_refresh(_, %User{local: true} = a, %User{local: true} = b) do
1171 with %User{} = a <- User.get_cached_by_id(a.id),
1172 %User{} = b <- User.get_cached_by_id(b.id) do
1173 {:ok, a, b}
1174 else
1175 _e ->
1176 :error
1177 end
1178 end
1179
1180 def wait_and_refresh(timeout, %User{} = a, %User{} = b) do
1181 with :ok <- :timer.sleep(timeout),
1182 %User{} = a <- User.get_cached_by_id(a.id),
1183 %User{} = b <- User.get_cached_by_id(b.id) do
1184 {:ok, a, b}
1185 else
1186 _e ->
1187 :error
1188 end
1189 end
1190
1191 def parse_bio(bio) when is_binary(bio) and bio != "" do
1192 bio
1193 |> CommonUtils.format_input("text/plain", mentions_format: :full)
1194 |> elem(0)
1195 end
1196
1197 def parse_bio(_), do: ""
1198
1199 def parse_bio(bio, user) when is_binary(bio) and bio != "" do
1200 # TODO: get profile URLs other than user.ap_id
1201 profile_urls = [user.ap_id]
1202
1203 bio
1204 |> CommonUtils.format_input("text/plain",
1205 mentions_format: :full,
1206 rel: &RelMe.maybe_put_rel_me(&1, profile_urls)
1207 )
1208 |> elem(0)
1209 end
1210
1211 def parse_bio(_, _), do: ""
1212
1213 def tag(user_identifiers, tags) when is_list(user_identifiers) do
1214 Repo.transaction(fn ->
1215 for user_identifier <- user_identifiers, do: tag(user_identifier, tags)
1216 end)
1217 end
1218
1219 def tag(nickname, tags) when is_binary(nickname),
1220 do: tag(get_by_nickname(nickname), tags)
1221
1222 def tag(%User{} = user, tags),
1223 do: update_tags(user, Enum.uniq((user.tags || []) ++ normalize_tags(tags)))
1224
1225 def untag(user_identifiers, tags) when is_list(user_identifiers) do
1226 Repo.transaction(fn ->
1227 for user_identifier <- user_identifiers, do: untag(user_identifier, tags)
1228 end)
1229 end
1230
1231 def untag(nickname, tags) when is_binary(nickname),
1232 do: untag(get_by_nickname(nickname), tags)
1233
1234 def untag(%User{} = user, tags),
1235 do: update_tags(user, (user.tags || []) -- normalize_tags(tags))
1236
1237 defp update_tags(%User{} = user, new_tags) do
1238 {:ok, updated_user} =
1239 user
1240 |> change(%{tags: new_tags})
1241 |> update_and_set_cache()
1242
1243 updated_user
1244 end
1245
1246 defp normalize_tags(tags) do
1247 [tags]
1248 |> List.flatten()
1249 |> Enum.map(&String.downcase(&1))
1250 end
1251
1252 defp local_nickname_regex do
1253 if Pleroma.Config.get([:instance, :extended_nickname_format]) do
1254 @extended_local_nickname_regex
1255 else
1256 @strict_local_nickname_regex
1257 end
1258 end
1259
1260 def local_nickname(nickname_or_mention) do
1261 nickname_or_mention
1262 |> full_nickname()
1263 |> String.split("@")
1264 |> hd()
1265 end
1266
1267 def full_nickname(nickname_or_mention),
1268 do: String.trim_leading(nickname_or_mention, "@")
1269
1270 def error_user(ap_id) do
1271 %User{
1272 name: ap_id,
1273 ap_id: ap_id,
1274 info: %User.Info{},
1275 nickname: "erroruser@example.com",
1276 inserted_at: NaiveDateTime.utc_now()
1277 }
1278 end
1279
1280 @spec all_superusers() :: [User.t()]
1281 def all_superusers do
1282 User.Query.build(%{super_users: true, local: true, deactivated: false})
1283 |> Repo.all()
1284 end
1285
1286 def showing_reblogs?(%User{} = user, %User{} = target) do
1287 target.ap_id not in user.info.muted_reblogs
1288 end
1289
1290 @spec toggle_confirmation(User.t()) :: {:ok, User.t()} | {:error, Changeset.t()}
1291 def toggle_confirmation(%User{} = user) do
1292 need_confirmation? = !user.info.confirmation_pending
1293
1294 info_changeset =
1295 User.Info.confirmation_changeset(user.info, need_confirmation: need_confirmation?)
1296
1297 user
1298 |> change()
1299 |> put_embed(:info, info_changeset)
1300 |> update_and_set_cache()
1301 end
1302
1303 def get_mascot(%{info: %{mascot: %{} = mascot}}) when not is_nil(mascot) do
1304 mascot
1305 end
1306
1307 def get_mascot(%{info: %{mascot: mascot}}) when is_nil(mascot) do
1308 # use instance-default
1309 config = Pleroma.Config.get([:assets, :mascots])
1310 default_mascot = Pleroma.Config.get([:assets, :default_mascot])
1311 mascot = Keyword.get(config, default_mascot)
1312
1313 %{
1314 "id" => "default-mascot",
1315 "url" => mascot[:url],
1316 "preview_url" => mascot[:url],
1317 "pleroma" => %{
1318 "mime_type" => mascot[:mime_type]
1319 }
1320 }
1321 end
1322
1323 def ensure_keys_present(user) do
1324 info = user.info
1325
1326 if info.keys do
1327 {:ok, user}
1328 else
1329 {:ok, pem} = Keys.generate_rsa_pem()
1330
1331 info_cng =
1332 info
1333 |> User.Info.set_keys(pem)
1334
1335 cng =
1336 Ecto.Changeset.change(user)
1337 |> Ecto.Changeset.put_embed(:info, info_cng)
1338
1339 update_and_set_cache(cng)
1340 end
1341 end
1342
1343 def get_ap_ids_by_nicknames(nicknames) do
1344 from(u in User,
1345 where: u.nickname in ^nicknames,
1346 select: u.ap_id
1347 )
1348 |> Repo.all()
1349 end
1350
1351 defdelegate search(query, opts \\ []), to: User.Search
1352 end