9449a88d04e20336882eb66e32a8456c185ee113
[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 @doc "A mass follow for local users. Respects blocks in both directions but does not create activities."
328 @spec follow_all(User.t(), list(User.t())) :: {atom(), User.t()}
329 def follow_all(follower, followeds) do
330 followed_addresses =
331 followeds
332 |> Enum.reject(fn followed -> blocks?(follower, followed) || blocks?(followed, follower) end)
333 |> Enum.map(fn %{follower_address: fa} -> fa end)
334
335 q =
336 from(u in User,
337 where: u.id == ^follower.id,
338 update: [
339 set: [
340 following:
341 fragment(
342 "array(select distinct unnest (array_cat(?, ?)))",
343 u.following,
344 ^followed_addresses
345 )
346 ]
347 ],
348 select: u
349 )
350
351 {1, [follower]} = Repo.update_all(q, [])
352
353 Enum.each(followeds, fn followed ->
354 update_follower_count(followed)
355 end)
356
357 set_cache(follower)
358 end
359
360 def follow(%User{} = follower, %User{info: info} = followed) do
361 deny_follow_blocked = Pleroma.Config.get([:user, :deny_follow_blocked])
362 ap_followers = followed.follower_address
363
364 cond do
365 info.deactivated ->
366 {:error, "Could not follow user: You are deactivated."}
367
368 deny_follow_blocked and blocks?(followed, follower) ->
369 {:error, "Could not follow user: #{followed.nickname} blocked you."}
370
371 true ->
372 if !followed.local && follower.local && !ap_enabled?(followed) do
373 Websub.subscribe(follower, followed)
374 end
375
376 q =
377 from(u in User,
378 where: u.id == ^follower.id,
379 update: [push: [following: ^ap_followers]],
380 select: u
381 )
382
383 {1, [follower]} = Repo.update_all(q, [])
384
385 {:ok, _} = update_follower_count(followed)
386
387 set_cache(follower)
388 end
389 end
390
391 def unfollow(%User{} = follower, %User{} = followed) do
392 ap_followers = followed.follower_address
393
394 if following?(follower, followed) and follower.ap_id != followed.ap_id do
395 q =
396 from(u in User,
397 where: u.id == ^follower.id,
398 update: [pull: [following: ^ap_followers]],
399 select: u
400 )
401
402 {1, [follower]} = Repo.update_all(q, [])
403
404 {:ok, followed} = update_follower_count(followed)
405
406 set_cache(follower)
407
408 {:ok, follower, Utils.fetch_latest_follow(follower, followed)}
409 else
410 {:error, "Not subscribed!"}
411 end
412 end
413
414 @spec following?(User.t(), User.t()) :: boolean
415 def following?(%User{} = follower, %User{} = followed) do
416 Enum.member?(follower.following, followed.follower_address)
417 end
418
419 def locked?(%User{} = user) do
420 user.info.locked || false
421 end
422
423 def get_by_id(id) do
424 Repo.get_by(User, id: id)
425 end
426
427 def get_by_ap_id(ap_id) do
428 Repo.get_by(User, ap_id: ap_id)
429 end
430
431 # This is mostly an SPC migration fix. This guesses the user nickname by taking the last part
432 # of the ap_id and the domain and tries to get that user
433 def get_by_guessed_nickname(ap_id) do
434 domain = URI.parse(ap_id).host
435 name = List.last(String.split(ap_id, "/"))
436 nickname = "#{name}@#{domain}"
437
438 get_cached_by_nickname(nickname)
439 end
440
441 def set_cache({:ok, user}), do: set_cache(user)
442 def set_cache({:error, err}), do: {:error, err}
443
444 def set_cache(%User{} = user) do
445 Cachex.put(:user_cache, "ap_id:#{user.ap_id}", user)
446 Cachex.put(:user_cache, "nickname:#{user.nickname}", user)
447 Cachex.put(:user_cache, "user_info:#{user.id}", user_info(user))
448 {:ok, user}
449 end
450
451 def update_and_set_cache(changeset) do
452 with {:ok, user} <- Repo.update(changeset) do
453 set_cache(user)
454 else
455 e -> e
456 end
457 end
458
459 def invalidate_cache(user) do
460 Cachex.del(:user_cache, "ap_id:#{user.ap_id}")
461 Cachex.del(:user_cache, "nickname:#{user.nickname}")
462 Cachex.del(:user_cache, "user_info:#{user.id}")
463 end
464
465 def get_cached_by_ap_id(ap_id) do
466 key = "ap_id:#{ap_id}"
467 Cachex.fetch!(:user_cache, key, fn _ -> get_by_ap_id(ap_id) end)
468 end
469
470 def get_cached_by_id(id) do
471 key = "id:#{id}"
472
473 ap_id =
474 Cachex.fetch!(:user_cache, key, fn _ ->
475 user = get_by_id(id)
476
477 if user do
478 Cachex.put(:user_cache, "ap_id:#{user.ap_id}", user)
479 {:commit, user.ap_id}
480 else
481 {:ignore, ""}
482 end
483 end)
484
485 get_cached_by_ap_id(ap_id)
486 end
487
488 def get_cached_by_nickname(nickname) do
489 key = "nickname:#{nickname}"
490
491 Cachex.fetch!(:user_cache, key, fn ->
492 user_result = get_or_fetch_by_nickname(nickname)
493
494 case user_result do
495 {:ok, user} -> {:commit, user}
496 {:error, _error} -> {:ignore, nil}
497 end
498 end)
499 end
500
501 def get_cached_by_nickname_or_id(nickname_or_id) do
502 get_cached_by_id(nickname_or_id) || get_cached_by_nickname(nickname_or_id)
503 end
504
505 def get_by_nickname(nickname) do
506 Repo.get_by(User, nickname: nickname) ||
507 if Regex.match?(~r(@#{Pleroma.Web.Endpoint.host()})i, nickname) do
508 Repo.get_by(User, nickname: local_nickname(nickname))
509 end
510 end
511
512 def get_by_email(email), do: Repo.get_by(User, email: email)
513
514 def get_by_nickname_or_email(nickname_or_email) do
515 get_by_nickname(nickname_or_email) || get_by_email(nickname_or_email)
516 end
517
518 def get_cached_user_info(user) do
519 key = "user_info:#{user.id}"
520 Cachex.fetch!(:user_cache, key, fn _ -> user_info(user) end)
521 end
522
523 def fetch_by_nickname(nickname) do
524 ap_try = ActivityPub.make_user_from_nickname(nickname)
525
526 case ap_try do
527 {:ok, user} -> {:ok, user}
528 _ -> OStatus.make_user(nickname)
529 end
530 end
531
532 def get_or_fetch_by_nickname(nickname) do
533 with %User{} = user <- get_by_nickname(nickname) do
534 {:ok, user}
535 else
536 _e ->
537 with [_nick, _domain] <- String.split(nickname, "@"),
538 {:ok, user} <- fetch_by_nickname(nickname) do
539 if Pleroma.Config.get([:fetch_initial_posts, :enabled]) do
540 fetch_initial_posts(user)
541 end
542
543 {:ok, user}
544 else
545 _e -> {:error, "not found " <> nickname}
546 end
547 end
548 end
549
550 @doc "Fetch some posts when the user has just been federated with"
551 def fetch_initial_posts(user),
552 do: PleromaJobQueue.enqueue(:background, __MODULE__, [:fetch_initial_posts, user])
553
554 @spec get_followers_query(User.t(), pos_integer() | nil) :: Ecto.Query.t()
555 def get_followers_query(%User{} = user, nil) do
556 User.Query.build(%{followers: user, deactivated: false})
557 end
558
559 def get_followers_query(user, page) do
560 from(u in get_followers_query(user, nil))
561 |> User.Query.paginate(page, 20)
562 end
563
564 @spec get_followers_query(User.t()) :: Ecto.Query.t()
565 def get_followers_query(user), do: get_followers_query(user, nil)
566
567 def get_followers(user, page \\ nil) do
568 q = get_followers_query(user, page)
569
570 {:ok, Repo.all(q)}
571 end
572
573 def get_followers_ids(user, page \\ nil) do
574 q = get_followers_query(user, page)
575
576 Repo.all(from(u in q, select: u.id))
577 end
578
579 @spec get_friends_query(User.t(), pos_integer() | nil) :: Ecto.Query.t()
580 def get_friends_query(%User{} = user, nil) do
581 User.Query.build(%{friends: user, deactivated: false})
582 end
583
584 def get_friends_query(user, page) do
585 from(u in get_friends_query(user, nil))
586 |> User.Query.paginate(page, 20)
587 end
588
589 @spec get_friends_query(User.t()) :: Ecto.Query.t()
590 def get_friends_query(user), do: get_friends_query(user, nil)
591
592 def get_friends(user, page \\ nil) do
593 q = get_friends_query(user, page)
594
595 {:ok, Repo.all(q)}
596 end
597
598 def get_friends_ids(user, page \\ nil) do
599 q = get_friends_query(user, page)
600
601 Repo.all(from(u in q, select: u.id))
602 end
603
604 @spec get_follow_requests(User.t()) :: {:ok, [User.t()]}
605 def get_follow_requests(%User{} = user) do
606 users =
607 Activity.follow_requests_for_actor(user)
608 |> join(:inner, [a], u in User, on: a.actor == u.ap_id)
609 |> where([a, u], not fragment("? @> ?", u.following, ^[user.follower_address]))
610 |> group_by([a, u], u.id)
611 |> select([a, u], u)
612 |> Repo.all()
613
614 {:ok, users}
615 end
616
617 def increase_note_count(%User{} = user) do
618 User
619 |> where(id: ^user.id)
620 |> update([u],
621 set: [
622 info:
623 fragment(
624 "jsonb_set(?, '{note_count}', ((?->>'note_count')::int + 1)::varchar::jsonb, true)",
625 u.info,
626 u.info
627 )
628 ]
629 )
630 |> select([u], u)
631 |> Repo.update_all([])
632 |> case do
633 {1, [user]} -> set_cache(user)
634 _ -> {:error, user}
635 end
636 end
637
638 def decrease_note_count(%User{} = user) do
639 User
640 |> where(id: ^user.id)
641 |> update([u],
642 set: [
643 info:
644 fragment(
645 "jsonb_set(?, '{note_count}', (greatest(0, (?->>'note_count')::int - 1))::varchar::jsonb, true)",
646 u.info,
647 u.info
648 )
649 ]
650 )
651 |> select([u], u)
652 |> Repo.update_all([])
653 |> case do
654 {1, [user]} -> set_cache(user)
655 _ -> {:error, user}
656 end
657 end
658
659 def update_note_count(%User{} = user) do
660 note_count_query =
661 from(
662 a in Object,
663 where: fragment("?->>'actor' = ? and ?->>'type' = 'Note'", a.data, ^user.ap_id, a.data),
664 select: count(a.id)
665 )
666
667 note_count = Repo.one(note_count_query)
668
669 info_cng = User.Info.set_note_count(user.info, note_count)
670
671 user
672 |> change()
673 |> put_embed(:info, info_cng)
674 |> update_and_set_cache()
675 end
676
677 def update_follower_count(%User{} = user) do
678 follower_count_query =
679 User.Query.build(%{followers: user, deactivated: false})
680 |> select([u], %{count: count(u.id)})
681
682 User
683 |> where(id: ^user.id)
684 |> join(:inner, [u], s in subquery(follower_count_query))
685 |> update([u, s],
686 set: [
687 info:
688 fragment(
689 "jsonb_set(?, '{follower_count}', ?::varchar::jsonb, true)",
690 u.info,
691 s.count
692 )
693 ]
694 )
695 |> select([u], u)
696 |> Repo.update_all([])
697 |> case do
698 {1, [user]} -> set_cache(user)
699 _ -> {:error, user}
700 end
701 end
702
703 def remove_duplicated_following(%User{following: following} = user) do
704 uniq_following = Enum.uniq(following)
705
706 if length(following) == length(uniq_following) do
707 {:ok, user}
708 else
709 user
710 |> update_changeset(%{following: uniq_following})
711 |> update_and_set_cache()
712 end
713 end
714
715 @spec get_users_from_set([String.t()], boolean()) :: [User.t()]
716 def get_users_from_set(ap_ids, local_only \\ true) do
717 criteria = %{ap_id: ap_ids, deactivated: false}
718 criteria = if local_only, do: Map.put(criteria, :local, true), else: criteria
719
720 User.Query.build(criteria)
721 |> Repo.all()
722 end
723
724 @spec get_recipients_from_activity(Activity.t()) :: [User.t()]
725 def get_recipients_from_activity(%Activity{recipients: to}) do
726 User.Query.build(%{recipients_from_activity: to, local: true, deactivated: false})
727 |> Repo.all()
728 end
729
730 def mute(muter, %User{ap_id: ap_id}) do
731 info_cng =
732 muter.info
733 |> User.Info.add_to_mutes(ap_id)
734
735 cng =
736 change(muter)
737 |> put_embed(:info, info_cng)
738
739 update_and_set_cache(cng)
740 end
741
742 def unmute(muter, %{ap_id: ap_id}) do
743 info_cng =
744 muter.info
745 |> User.Info.remove_from_mutes(ap_id)
746
747 cng =
748 change(muter)
749 |> put_embed(:info, info_cng)
750
751 update_and_set_cache(cng)
752 end
753
754 def subscribe(subscriber, %{ap_id: ap_id}) do
755 deny_follow_blocked = Pleroma.Config.get([:user, :deny_follow_blocked])
756
757 with %User{} = subscribed <- get_cached_by_ap_id(ap_id) do
758 blocked = blocks?(subscribed, subscriber) and deny_follow_blocked
759
760 if blocked do
761 {:error, "Could not subscribe: #{subscribed.nickname} is blocking you"}
762 else
763 info_cng =
764 subscribed.info
765 |> User.Info.add_to_subscribers(subscriber.ap_id)
766
767 change(subscribed)
768 |> put_embed(:info, info_cng)
769 |> update_and_set_cache()
770 end
771 end
772 end
773
774 def unsubscribe(unsubscriber, %{ap_id: ap_id}) do
775 with %User{} = user <- get_cached_by_ap_id(ap_id) do
776 info_cng =
777 user.info
778 |> User.Info.remove_from_subscribers(unsubscriber.ap_id)
779
780 change(user)
781 |> put_embed(:info, info_cng)
782 |> update_and_set_cache()
783 end
784 end
785
786 def block(blocker, %User{ap_id: ap_id} = blocked) do
787 # sever any follow relationships to prevent leaks per activitypub (Pleroma issue #213)
788 blocker =
789 if following?(blocker, blocked) do
790 {:ok, blocker, _} = unfollow(blocker, blocked)
791 blocker
792 else
793 blocker
794 end
795
796 blocker =
797 if subscribed_to?(blocked, blocker) do
798 {:ok, blocker} = unsubscribe(blocked, blocker)
799 blocker
800 else
801 blocker
802 end
803
804 if following?(blocked, blocker) do
805 unfollow(blocked, blocker)
806 end
807
808 {:ok, blocker} = update_follower_count(blocker)
809
810 info_cng =
811 blocker.info
812 |> User.Info.add_to_block(ap_id)
813
814 cng =
815 change(blocker)
816 |> put_embed(:info, info_cng)
817
818 update_and_set_cache(cng)
819 end
820
821 # helper to handle the block given only an actor's AP id
822 def block(blocker, %{ap_id: ap_id}) do
823 block(blocker, get_cached_by_ap_id(ap_id))
824 end
825
826 def unblock(blocker, %{ap_id: ap_id}) do
827 info_cng =
828 blocker.info
829 |> User.Info.remove_from_block(ap_id)
830
831 cng =
832 change(blocker)
833 |> put_embed(:info, info_cng)
834
835 update_and_set_cache(cng)
836 end
837
838 def mutes?(nil, _), do: false
839 def mutes?(user, %{ap_id: ap_id}), do: Enum.member?(user.info.mutes, ap_id)
840
841 def blocks?(user, %{ap_id: ap_id}) do
842 blocks = user.info.blocks
843 domain_blocks = user.info.domain_blocks
844 %{host: host} = URI.parse(ap_id)
845
846 Enum.member?(blocks, ap_id) ||
847 Enum.any?(domain_blocks, fn domain ->
848 host == domain
849 end)
850 end
851
852 def subscribed_to?(user, %{ap_id: ap_id}) do
853 with %User{} = target <- get_cached_by_ap_id(ap_id) do
854 Enum.member?(target.info.subscribers, user.ap_id)
855 end
856 end
857
858 @spec muted_users(User.t()) :: [User.t()]
859 def muted_users(user) do
860 User.Query.build(%{ap_id: user.info.mutes, deactivated: false})
861 |> Repo.all()
862 end
863
864 @spec blocked_users(User.t()) :: [User.t()]
865 def blocked_users(user) do
866 User.Query.build(%{ap_id: user.info.blocks, deactivated: false})
867 |> Repo.all()
868 end
869
870 @spec subscribers(User.t()) :: [User.t()]
871 def subscribers(user) do
872 User.Query.build(%{ap_id: user.info.subscribers, deactivated: false})
873 |> Repo.all()
874 end
875
876 def block_domain(user, domain) do
877 info_cng =
878 user.info
879 |> User.Info.add_to_domain_block(domain)
880
881 cng =
882 change(user)
883 |> put_embed(:info, info_cng)
884
885 update_and_set_cache(cng)
886 end
887
888 def unblock_domain(user, domain) do
889 info_cng =
890 user.info
891 |> User.Info.remove_from_domain_block(domain)
892
893 cng =
894 change(user)
895 |> put_embed(:info, info_cng)
896
897 update_and_set_cache(cng)
898 end
899
900 def deactivate_async(user, status \\ true) do
901 PleromaJobQueue.enqueue(:background, __MODULE__, [:deactivate_async, user, status])
902 end
903
904 def deactivate(%User{} = user, status \\ true) do
905 info_cng = User.Info.set_activation_status(user.info, status)
906
907 with {:ok, friends} <- User.get_friends(user),
908 {:ok, followers} <- User.get_followers(user),
909 {:ok, user} <-
910 user
911 |> change()
912 |> put_embed(:info, info_cng)
913 |> update_and_set_cache() do
914 Enum.each(followers, &invalidate_cache(&1))
915 Enum.each(friends, &update_follower_count(&1))
916
917 {:ok, user}
918 end
919 end
920
921 def update_notification_settings(%User{} = user, settings \\ %{}) do
922 info_changeset = User.Info.update_notification_settings(user.info, settings)
923
924 change(user)
925 |> put_embed(:info, info_changeset)
926 |> update_and_set_cache()
927 end
928
929 @spec delete(User.t()) :: :ok
930 def delete(%User{} = user),
931 do: PleromaJobQueue.enqueue(:background, __MODULE__, [:delete, user])
932
933 @spec perform(atom(), User.t()) :: {:ok, User.t()}
934 def perform(:delete, %User{} = user) do
935 {:ok, user} = User.deactivate(user)
936
937 # Remove all relationships
938 {:ok, followers} = User.get_followers(user)
939
940 Enum.each(followers, fn follower -> User.unfollow(follower, user) end)
941
942 {:ok, friends} = User.get_friends(user)
943
944 Enum.each(friends, fn followed -> User.unfollow(user, followed) end)
945
946 delete_user_activities(user)
947 end
948
949 @spec perform(atom(), User.t()) :: {:ok, User.t()}
950 def perform(:fetch_initial_posts, %User{} = user) do
951 pages = Pleroma.Config.get!([:fetch_initial_posts, :pages])
952
953 Enum.each(
954 # Insert all the posts in reverse order, so they're in the right order on the timeline
955 Enum.reverse(Utils.fetch_ordered_collection(user.info.source_data["outbox"], pages)),
956 &Pleroma.Web.Federator.incoming_ap_doc/1
957 )
958
959 {:ok, user}
960 end
961
962 def perform(:deactivate_async, user, status), do: deactivate(user, status)
963
964 @spec perform(atom(), User.t(), list()) :: list() | {:error, any()}
965 def perform(:blocks_import, %User{} = blocker, blocked_identifiers)
966 when is_list(blocked_identifiers) do
967 Enum.map(
968 blocked_identifiers,
969 fn blocked_identifier ->
970 with {:ok, %User{} = blocked} <- get_or_fetch(blocked_identifier),
971 {:ok, blocker} <- block(blocker, blocked),
972 {:ok, _} <- ActivityPub.block(blocker, blocked) do
973 blocked
974 else
975 err ->
976 Logger.debug("blocks_import failed for #{blocked_identifier} with: #{inspect(err)}")
977 err
978 end
979 end
980 )
981 end
982
983 @spec perform(atom(), User.t(), list()) :: list() | {:error, any()}
984 def perform(:follow_import, %User{} = follower, followed_identifiers)
985 when is_list(followed_identifiers) do
986 Enum.map(
987 followed_identifiers,
988 fn followed_identifier ->
989 with {:ok, %User{} = followed} <- get_or_fetch(followed_identifier),
990 {:ok, follower} <- maybe_direct_follow(follower, followed),
991 {:ok, _} <- ActivityPub.follow(follower, followed) do
992 followed
993 else
994 err ->
995 Logger.debug("follow_import failed for #{followed_identifier} with: #{inspect(err)}")
996 err
997 end
998 end
999 )
1000 end
1001
1002 def blocks_import(%User{} = blocker, blocked_identifiers) when is_list(blocked_identifiers),
1003 do:
1004 PleromaJobQueue.enqueue(:background, __MODULE__, [
1005 :blocks_import,
1006 blocker,
1007 blocked_identifiers
1008 ])
1009
1010 def follow_import(%User{} = follower, followed_identifiers) when is_list(followed_identifiers),
1011 do:
1012 PleromaJobQueue.enqueue(:background, __MODULE__, [
1013 :follow_import,
1014 follower,
1015 followed_identifiers
1016 ])
1017
1018 def delete_user_activities(%User{ap_id: ap_id} = user) do
1019 stream =
1020 ap_id
1021 |> Activity.query_by_actor()
1022 |> Repo.stream()
1023
1024 Repo.transaction(fn -> Enum.each(stream, &delete_activity(&1)) end, timeout: :infinity)
1025
1026 {:ok, user}
1027 end
1028
1029 defp delete_activity(%{data: %{"type" => "Create"}} = activity) do
1030 Object.normalize(activity) |> ActivityPub.delete()
1031 end
1032
1033 defp delete_activity(_activity), do: "Doing nothing"
1034
1035 def html_filter_policy(%User{info: %{no_rich_text: true}}) do
1036 Pleroma.HTML.Scrubber.TwitterText
1037 end
1038
1039 @default_scrubbers Pleroma.Config.get([:markup, :scrub_policy])
1040
1041 def html_filter_policy(_), do: @default_scrubbers
1042
1043 def fetch_by_ap_id(ap_id) do
1044 ap_try = ActivityPub.make_user_from_ap_id(ap_id)
1045
1046 case ap_try do
1047 {:ok, user} ->
1048 {:ok, user}
1049
1050 _ ->
1051 case OStatus.make_user(ap_id) do
1052 {:ok, user} -> {:ok, user}
1053 _ -> {:error, "Could not fetch by AP id"}
1054 end
1055 end
1056 end
1057
1058 def get_or_fetch_by_ap_id(ap_id) do
1059 user = get_cached_by_ap_id(ap_id)
1060
1061 if !is_nil(user) and !User.needs_update?(user) do
1062 {:ok, user}
1063 else
1064 # Whether to fetch initial posts for the user (if it's a new user & the fetching is enabled)
1065 should_fetch_initial = is_nil(user) and Pleroma.Config.get([:fetch_initial_posts, :enabled])
1066
1067 resp = fetch_by_ap_id(ap_id)
1068
1069 if should_fetch_initial do
1070 with {:ok, %User{} = user} <- resp do
1071 fetch_initial_posts(user)
1072 end
1073 end
1074
1075 resp
1076 end
1077 end
1078
1079 def get_or_create_instance_user do
1080 relay_uri = "#{Pleroma.Web.Endpoint.url()}/relay"
1081
1082 if user = get_cached_by_ap_id(relay_uri) do
1083 user
1084 else
1085 changes =
1086 %User{info: %User.Info{}}
1087 |> cast(%{}, [:ap_id, :nickname, :local])
1088 |> put_change(:ap_id, relay_uri)
1089 |> put_change(:nickname, nil)
1090 |> put_change(:local, true)
1091 |> put_change(:follower_address, relay_uri <> "/followers")
1092
1093 {:ok, user} = Repo.insert(changes)
1094 user
1095 end
1096 end
1097
1098 # AP style
1099 def public_key_from_info(%{
1100 source_data: %{"publicKey" => %{"publicKeyPem" => public_key_pem}}
1101 }) do
1102 key =
1103 public_key_pem
1104 |> :public_key.pem_decode()
1105 |> hd()
1106 |> :public_key.pem_entry_decode()
1107
1108 {:ok, key}
1109 end
1110
1111 # OStatus Magic Key
1112 def public_key_from_info(%{magic_key: magic_key}) do
1113 {:ok, Pleroma.Web.Salmon.decode_key(magic_key)}
1114 end
1115
1116 def get_public_key_for_ap_id(ap_id) do
1117 with {:ok, %User{} = user} <- get_or_fetch_by_ap_id(ap_id),
1118 {:ok, public_key} <- public_key_from_info(user.info) do
1119 {:ok, public_key}
1120 else
1121 _ -> :error
1122 end
1123 end
1124
1125 defp blank?(""), do: nil
1126 defp blank?(n), do: n
1127
1128 def insert_or_update_user(data) do
1129 data
1130 |> Map.put(:name, blank?(data[:name]) || data[:nickname])
1131 |> remote_user_creation()
1132 |> Repo.insert(on_conflict: :replace_all, conflict_target: :nickname)
1133 |> set_cache()
1134 end
1135
1136 def ap_enabled?(%User{local: true}), do: true
1137 def ap_enabled?(%User{info: info}), do: info.ap_enabled
1138 def ap_enabled?(_), do: false
1139
1140 @doc "Gets or fetch a user by uri or nickname."
1141 @spec get_or_fetch(String.t()) :: {:ok, User.t()} | {:error, String.t()}
1142 def get_or_fetch("http" <> _host = uri), do: get_or_fetch_by_ap_id(uri)
1143 def get_or_fetch(nickname), do: get_or_fetch_by_nickname(nickname)
1144
1145 # wait a period of time and return newest version of the User structs
1146 # this is because we have synchronous follow APIs and need to simulate them
1147 # with an async handshake
1148 def wait_and_refresh(_, %User{local: true} = a, %User{local: true} = b) do
1149 with %User{} = a <- User.get_cached_by_id(a.id),
1150 %User{} = b <- User.get_cached_by_id(b.id) do
1151 {:ok, a, b}
1152 else
1153 _e ->
1154 :error
1155 end
1156 end
1157
1158 def wait_and_refresh(timeout, %User{} = a, %User{} = b) do
1159 with :ok <- :timer.sleep(timeout),
1160 %User{} = a <- User.get_cached_by_id(a.id),
1161 %User{} = b <- User.get_cached_by_id(b.id) do
1162 {:ok, a, b}
1163 else
1164 _e ->
1165 :error
1166 end
1167 end
1168
1169 def parse_bio(bio) when is_binary(bio) and bio != "" do
1170 bio
1171 |> CommonUtils.format_input("text/plain", mentions_format: :full)
1172 |> elem(0)
1173 end
1174
1175 def parse_bio(_), do: ""
1176
1177 def parse_bio(bio, user) when is_binary(bio) and bio != "" do
1178 # TODO: get profile URLs other than user.ap_id
1179 profile_urls = [user.ap_id]
1180
1181 bio
1182 |> CommonUtils.format_input("text/plain",
1183 mentions_format: :full,
1184 rel: &RelMe.maybe_put_rel_me(&1, profile_urls)
1185 )
1186 |> elem(0)
1187 end
1188
1189 def parse_bio(_, _), do: ""
1190
1191 def tag(user_identifiers, tags) when is_list(user_identifiers) do
1192 Repo.transaction(fn ->
1193 for user_identifier <- user_identifiers, do: tag(user_identifier, tags)
1194 end)
1195 end
1196
1197 def tag(nickname, tags) when is_binary(nickname),
1198 do: tag(get_by_nickname(nickname), tags)
1199
1200 def tag(%User{} = user, tags),
1201 do: update_tags(user, Enum.uniq((user.tags || []) ++ normalize_tags(tags)))
1202
1203 def untag(user_identifiers, tags) when is_list(user_identifiers) do
1204 Repo.transaction(fn ->
1205 for user_identifier <- user_identifiers, do: untag(user_identifier, tags)
1206 end)
1207 end
1208
1209 def untag(nickname, tags) when is_binary(nickname),
1210 do: untag(get_by_nickname(nickname), tags)
1211
1212 def untag(%User{} = user, tags),
1213 do: update_tags(user, (user.tags || []) -- normalize_tags(tags))
1214
1215 defp update_tags(%User{} = user, new_tags) do
1216 {:ok, updated_user} =
1217 user
1218 |> change(%{tags: new_tags})
1219 |> update_and_set_cache()
1220
1221 updated_user
1222 end
1223
1224 defp normalize_tags(tags) do
1225 [tags]
1226 |> List.flatten()
1227 |> Enum.map(&String.downcase(&1))
1228 end
1229
1230 defp local_nickname_regex do
1231 if Pleroma.Config.get([:instance, :extended_nickname_format]) do
1232 @extended_local_nickname_regex
1233 else
1234 @strict_local_nickname_regex
1235 end
1236 end
1237
1238 def local_nickname(nickname_or_mention) do
1239 nickname_or_mention
1240 |> full_nickname()
1241 |> String.split("@")
1242 |> hd()
1243 end
1244
1245 def full_nickname(nickname_or_mention),
1246 do: String.trim_leading(nickname_or_mention, "@")
1247
1248 def error_user(ap_id) do
1249 %User{
1250 name: ap_id,
1251 ap_id: ap_id,
1252 info: %User.Info{},
1253 nickname: "erroruser@example.com",
1254 inserted_at: NaiveDateTime.utc_now()
1255 }
1256 end
1257
1258 @spec all_superusers() :: [User.t()]
1259 def all_superusers do
1260 User.Query.build(%{super_users: true, local: true, deactivated: false})
1261 |> Repo.all()
1262 end
1263
1264 def showing_reblogs?(%User{} = user, %User{} = target) do
1265 target.ap_id not in user.info.muted_reblogs
1266 end
1267
1268 @spec toggle_confirmation(User.t()) :: {:ok, User.t()} | {:error, Changeset.t()}
1269 def toggle_confirmation(%User{} = user) do
1270 need_confirmation? = !user.info.confirmation_pending
1271
1272 info_changeset =
1273 User.Info.confirmation_changeset(user.info, need_confirmation: need_confirmation?)
1274
1275 user
1276 |> change()
1277 |> put_embed(:info, info_changeset)
1278 |> update_and_set_cache()
1279 end
1280
1281 def get_mascot(%{info: %{mascot: %{} = mascot}}) when not is_nil(mascot) do
1282 mascot
1283 end
1284
1285 def get_mascot(%{info: %{mascot: mascot}}) when is_nil(mascot) do
1286 # use instance-default
1287 config = Pleroma.Config.get([:assets, :mascots])
1288 default_mascot = Pleroma.Config.get([:assets, :default_mascot])
1289 mascot = Keyword.get(config, default_mascot)
1290
1291 %{
1292 "id" => "default-mascot",
1293 "url" => mascot[:url],
1294 "preview_url" => mascot[:url],
1295 "pleroma" => %{
1296 "mime_type" => mascot[:mime_type]
1297 }
1298 }
1299 end
1300
1301 def ensure_keys_present(user) do
1302 info = user.info
1303
1304 if info.keys do
1305 {:ok, user}
1306 else
1307 {:ok, pem} = Keys.generate_rsa_pem()
1308
1309 info_cng =
1310 info
1311 |> User.Info.set_keys(pem)
1312
1313 cng =
1314 Ecto.Changeset.change(user)
1315 |> Ecto.Changeset.put_embed(:info, info_cng)
1316
1317 update_and_set_cache(cng)
1318 end
1319 end
1320
1321 def get_ap_ids_by_nicknames(nicknames) do
1322 from(u in User,
1323 where: u.nickname in ^nicknames,
1324 select: u.ap_id
1325 )
1326 |> Repo.all()
1327 end
1328
1329 defdelegate search(query, opts \\ []), to: User.Search
1330 end