cae2c14e310858b94ff03cf24f9af4dfdf5e37c4
[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 def mute(muter, %User{ap_id: ap_id}) do
739 info_cng =
740 muter.info
741 |> User.Info.add_to_mutes(ap_id)
742
743 cng =
744 change(muter)
745 |> put_embed(:info, info_cng)
746
747 update_and_set_cache(cng)
748 end
749
750 def unmute(muter, %{ap_id: ap_id}) do
751 info_cng =
752 muter.info
753 |> User.Info.remove_from_mutes(ap_id)
754
755 cng =
756 change(muter)
757 |> put_embed(:info, info_cng)
758
759 update_and_set_cache(cng)
760 end
761
762 def subscribe(subscriber, %{ap_id: ap_id}) do
763 deny_follow_blocked = Pleroma.Config.get([:user, :deny_follow_blocked])
764
765 with %User{} = subscribed <- get_cached_by_ap_id(ap_id) do
766 blocked = blocks?(subscribed, subscriber) and deny_follow_blocked
767
768 if blocked do
769 {:error, "Could not subscribe: #{subscribed.nickname} is blocking you"}
770 else
771 info_cng =
772 subscribed.info
773 |> User.Info.add_to_subscribers(subscriber.ap_id)
774
775 change(subscribed)
776 |> put_embed(:info, info_cng)
777 |> update_and_set_cache()
778 end
779 end
780 end
781
782 def unsubscribe(unsubscriber, %{ap_id: ap_id}) do
783 with %User{} = user <- get_cached_by_ap_id(ap_id) do
784 info_cng =
785 user.info
786 |> User.Info.remove_from_subscribers(unsubscriber.ap_id)
787
788 change(user)
789 |> put_embed(:info, info_cng)
790 |> update_and_set_cache()
791 end
792 end
793
794 def block(blocker, %User{ap_id: ap_id} = blocked) do
795 # sever any follow relationships to prevent leaks per activitypub (Pleroma issue #213)
796 blocker =
797 if following?(blocker, blocked) do
798 {:ok, blocker, _} = unfollow(blocker, blocked)
799 blocker
800 else
801 blocker
802 end
803
804 blocker =
805 if subscribed_to?(blocked, blocker) do
806 {:ok, blocker} = unsubscribe(blocked, blocker)
807 blocker
808 else
809 blocker
810 end
811
812 if following?(blocked, blocker) do
813 unfollow(blocked, blocker)
814 end
815
816 {:ok, blocker} = update_follower_count(blocker)
817
818 info_cng =
819 blocker.info
820 |> User.Info.add_to_block(ap_id)
821
822 cng =
823 change(blocker)
824 |> put_embed(:info, info_cng)
825
826 update_and_set_cache(cng)
827 end
828
829 # helper to handle the block given only an actor's AP id
830 def block(blocker, %{ap_id: ap_id}) do
831 block(blocker, get_cached_by_ap_id(ap_id))
832 end
833
834 def unblock(blocker, %{ap_id: ap_id}) do
835 info_cng =
836 blocker.info
837 |> User.Info.remove_from_block(ap_id)
838
839 cng =
840 change(blocker)
841 |> put_embed(:info, info_cng)
842
843 update_and_set_cache(cng)
844 end
845
846 def mutes?(nil, _), do: false
847 def mutes?(user, %{ap_id: ap_id}), do: Enum.member?(user.info.mutes, ap_id)
848
849 def blocks?(user, %{ap_id: ap_id}) do
850 blocks = user.info.blocks
851 domain_blocks = user.info.domain_blocks
852 %{host: host} = URI.parse(ap_id)
853
854 Enum.member?(blocks, ap_id) ||
855 Enum.any?(domain_blocks, fn domain ->
856 host == domain
857 end)
858 end
859
860 def subscribed_to?(user, %{ap_id: ap_id}) do
861 with %User{} = target <- get_cached_by_ap_id(ap_id) do
862 Enum.member?(target.info.subscribers, user.ap_id)
863 end
864 end
865
866 @spec muted_users(User.t()) :: [User.t()]
867 def muted_users(user) do
868 User.Query.build(%{ap_id: user.info.mutes, deactivated: false})
869 |> Repo.all()
870 end
871
872 @spec blocked_users(User.t()) :: [User.t()]
873 def blocked_users(user) do
874 User.Query.build(%{ap_id: user.info.blocks, deactivated: false})
875 |> Repo.all()
876 end
877
878 @spec subscribers(User.t()) :: [User.t()]
879 def subscribers(user) do
880 User.Query.build(%{ap_id: user.info.subscribers, deactivated: false})
881 |> Repo.all()
882 end
883
884 def block_domain(user, domain) do
885 info_cng =
886 user.info
887 |> User.Info.add_to_domain_block(domain)
888
889 cng =
890 change(user)
891 |> put_embed(:info, info_cng)
892
893 update_and_set_cache(cng)
894 end
895
896 def unblock_domain(user, domain) do
897 info_cng =
898 user.info
899 |> User.Info.remove_from_domain_block(domain)
900
901 cng =
902 change(user)
903 |> put_embed(:info, info_cng)
904
905 update_and_set_cache(cng)
906 end
907
908 def deactivate_async(user, status \\ true) do
909 PleromaJobQueue.enqueue(:background, __MODULE__, [:deactivate_async, user, status])
910 end
911
912 def deactivate(%User{} = user, status \\ true) do
913 info_cng = User.Info.set_activation_status(user.info, status)
914
915 with {:ok, friends} <- User.get_friends(user),
916 {:ok, followers} <- User.get_followers(user),
917 {:ok, user} <-
918 user
919 |> change()
920 |> put_embed(:info, info_cng)
921 |> update_and_set_cache() do
922 Enum.each(followers, &invalidate_cache(&1))
923 Enum.each(friends, &update_follower_count(&1))
924
925 {:ok, user}
926 end
927 end
928
929 def update_notification_settings(%User{} = user, settings \\ %{}) do
930 info_changeset = User.Info.update_notification_settings(user.info, settings)
931
932 change(user)
933 |> put_embed(:info, info_changeset)
934 |> update_and_set_cache()
935 end
936
937 @spec delete(User.t()) :: :ok
938 def delete(%User{} = user),
939 do: PleromaJobQueue.enqueue(:background, __MODULE__, [:delete, user])
940
941 @spec perform(atom(), User.t()) :: {:ok, User.t()}
942 def perform(:delete, %User{} = user) do
943 {:ok, user} = User.deactivate(user)
944
945 # Remove all relationships
946 {:ok, followers} = User.get_followers(user)
947
948 Enum.each(followers, fn follower -> User.unfollow(follower, user) end)
949
950 {:ok, friends} = User.get_friends(user)
951
952 Enum.each(friends, fn followed -> User.unfollow(user, followed) end)
953
954 delete_user_activities(user)
955 end
956
957 @spec perform(atom(), User.t()) :: {:ok, User.t()}
958 def perform(:fetch_initial_posts, %User{} = user) do
959 pages = Pleroma.Config.get!([:fetch_initial_posts, :pages])
960
961 Enum.each(
962 # Insert all the posts in reverse order, so they're in the right order on the timeline
963 Enum.reverse(Utils.fetch_ordered_collection(user.info.source_data["outbox"], pages)),
964 &Pleroma.Web.Federator.incoming_ap_doc/1
965 )
966
967 {:ok, user}
968 end
969
970 def perform(:deactivate_async, user, status), do: deactivate(user, status)
971
972 @spec perform(atom(), User.t(), list()) :: list() | {:error, any()}
973 def perform(:blocks_import, %User{} = blocker, blocked_identifiers)
974 when is_list(blocked_identifiers) do
975 Enum.map(
976 blocked_identifiers,
977 fn blocked_identifier ->
978 with {:ok, %User{} = blocked} <- get_or_fetch(blocked_identifier),
979 {:ok, blocker} <- block(blocker, blocked),
980 {:ok, _} <- ActivityPub.block(blocker, blocked) do
981 blocked
982 else
983 err ->
984 Logger.debug("blocks_import failed for #{blocked_identifier} with: #{inspect(err)}")
985 err
986 end
987 end
988 )
989 end
990
991 @spec perform(atom(), User.t(), list()) :: list() | {:error, any()}
992 def perform(:follow_import, %User{} = follower, followed_identifiers)
993 when is_list(followed_identifiers) do
994 Enum.map(
995 followed_identifiers,
996 fn followed_identifier ->
997 with {:ok, %User{} = followed} <- get_or_fetch(followed_identifier),
998 {:ok, follower} <- maybe_direct_follow(follower, followed),
999 {:ok, _} <- ActivityPub.follow(follower, followed) do
1000 followed
1001 else
1002 err ->
1003 Logger.debug("follow_import failed for #{followed_identifier} with: #{inspect(err)}")
1004 err
1005 end
1006 end
1007 )
1008 end
1009
1010 def blocks_import(%User{} = blocker, blocked_identifiers) when is_list(blocked_identifiers),
1011 do:
1012 PleromaJobQueue.enqueue(:background, __MODULE__, [
1013 :blocks_import,
1014 blocker,
1015 blocked_identifiers
1016 ])
1017
1018 def follow_import(%User{} = follower, followed_identifiers) when is_list(followed_identifiers),
1019 do:
1020 PleromaJobQueue.enqueue(:background, __MODULE__, [
1021 :follow_import,
1022 follower,
1023 followed_identifiers
1024 ])
1025
1026 def delete_user_activities(%User{ap_id: ap_id} = user) do
1027 stream =
1028 ap_id
1029 |> Activity.query_by_actor()
1030 |> Repo.stream()
1031
1032 Repo.transaction(fn -> Enum.each(stream, &delete_activity(&1)) end, timeout: :infinity)
1033
1034 {:ok, user}
1035 end
1036
1037 defp delete_activity(%{data: %{"type" => "Create"}} = activity) do
1038 Object.normalize(activity) |> ActivityPub.delete()
1039 end
1040
1041 defp delete_activity(_activity), do: "Doing nothing"
1042
1043 def html_filter_policy(%User{info: %{no_rich_text: true}}) do
1044 Pleroma.HTML.Scrubber.TwitterText
1045 end
1046
1047 @default_scrubbers Pleroma.Config.get([:markup, :scrub_policy])
1048
1049 def html_filter_policy(_), do: @default_scrubbers
1050
1051 def fetch_by_ap_id(ap_id) do
1052 ap_try = ActivityPub.make_user_from_ap_id(ap_id)
1053
1054 case ap_try do
1055 {:ok, user} ->
1056 {:ok, user}
1057
1058 _ ->
1059 case OStatus.make_user(ap_id) do
1060 {:ok, user} -> {:ok, user}
1061 _ -> {:error, "Could not fetch by AP id"}
1062 end
1063 end
1064 end
1065
1066 def get_or_fetch_by_ap_id(ap_id) do
1067 user = get_cached_by_ap_id(ap_id)
1068
1069 if !is_nil(user) and !User.needs_update?(user) do
1070 {:ok, user}
1071 else
1072 # Whether to fetch initial posts for the user (if it's a new user & the fetching is enabled)
1073 should_fetch_initial = is_nil(user) and Pleroma.Config.get([:fetch_initial_posts, :enabled])
1074
1075 resp = fetch_by_ap_id(ap_id)
1076
1077 if should_fetch_initial do
1078 with {:ok, %User{} = user} <- resp do
1079 fetch_initial_posts(user)
1080 end
1081 end
1082
1083 resp
1084 end
1085 end
1086
1087 def get_or_create_instance_user do
1088 relay_uri = "#{Pleroma.Web.Endpoint.url()}/relay"
1089
1090 if user = get_cached_by_ap_id(relay_uri) do
1091 user
1092 else
1093 changes =
1094 %User{info: %User.Info{}}
1095 |> cast(%{}, [:ap_id, :nickname, :local])
1096 |> put_change(:ap_id, relay_uri)
1097 |> put_change(:nickname, nil)
1098 |> put_change(:local, true)
1099 |> put_change(:follower_address, relay_uri <> "/followers")
1100
1101 {:ok, user} = Repo.insert(changes)
1102 user
1103 end
1104 end
1105
1106 # AP style
1107 def public_key_from_info(%{
1108 source_data: %{"publicKey" => %{"publicKeyPem" => public_key_pem}}
1109 }) do
1110 key =
1111 public_key_pem
1112 |> :public_key.pem_decode()
1113 |> hd()
1114 |> :public_key.pem_entry_decode()
1115
1116 {:ok, key}
1117 end
1118
1119 # OStatus Magic Key
1120 def public_key_from_info(%{magic_key: magic_key}) do
1121 {:ok, Pleroma.Web.Salmon.decode_key(magic_key)}
1122 end
1123
1124 def get_public_key_for_ap_id(ap_id) do
1125 with {:ok, %User{} = user} <- get_or_fetch_by_ap_id(ap_id),
1126 {:ok, public_key} <- public_key_from_info(user.info) do
1127 {:ok, public_key}
1128 else
1129 _ -> :error
1130 end
1131 end
1132
1133 defp blank?(""), do: nil
1134 defp blank?(n), do: n
1135
1136 def insert_or_update_user(data) do
1137 data
1138 |> Map.put(:name, blank?(data[:name]) || data[:nickname])
1139 |> remote_user_creation()
1140 |> Repo.insert(on_conflict: :replace_all, conflict_target: :nickname)
1141 |> set_cache()
1142 end
1143
1144 def ap_enabled?(%User{local: true}), do: true
1145 def ap_enabled?(%User{info: info}), do: info.ap_enabled
1146 def ap_enabled?(_), do: false
1147
1148 @doc "Gets or fetch a user by uri or nickname."
1149 @spec get_or_fetch(String.t()) :: {:ok, User.t()} | {:error, String.t()}
1150 def get_or_fetch("http" <> _host = uri), do: get_or_fetch_by_ap_id(uri)
1151 def get_or_fetch(nickname), do: get_or_fetch_by_nickname(nickname)
1152
1153 # wait a period of time and return newest version of the User structs
1154 # this is because we have synchronous follow APIs and need to simulate them
1155 # with an async handshake
1156 def wait_and_refresh(_, %User{local: true} = a, %User{local: true} = b) do
1157 with %User{} = a <- User.get_cached_by_id(a.id),
1158 %User{} = b <- User.get_cached_by_id(b.id) do
1159 {:ok, a, b}
1160 else
1161 _e ->
1162 :error
1163 end
1164 end
1165
1166 def wait_and_refresh(timeout, %User{} = a, %User{} = b) do
1167 with :ok <- :timer.sleep(timeout),
1168 %User{} = a <- User.get_cached_by_id(a.id),
1169 %User{} = b <- User.get_cached_by_id(b.id) do
1170 {:ok, a, b}
1171 else
1172 _e ->
1173 :error
1174 end
1175 end
1176
1177 def parse_bio(bio) when is_binary(bio) and bio != "" do
1178 bio
1179 |> CommonUtils.format_input("text/plain", mentions_format: :full)
1180 |> elem(0)
1181 end
1182
1183 def parse_bio(_), do: ""
1184
1185 def parse_bio(bio, user) when is_binary(bio) and bio != "" do
1186 # TODO: get profile URLs other than user.ap_id
1187 profile_urls = [user.ap_id]
1188
1189 bio
1190 |> CommonUtils.format_input("text/plain",
1191 mentions_format: :full,
1192 rel: &RelMe.maybe_put_rel_me(&1, profile_urls)
1193 )
1194 |> elem(0)
1195 end
1196
1197 def parse_bio(_, _), do: ""
1198
1199 def tag(user_identifiers, tags) when is_list(user_identifiers) do
1200 Repo.transaction(fn ->
1201 for user_identifier <- user_identifiers, do: tag(user_identifier, tags)
1202 end)
1203 end
1204
1205 def tag(nickname, tags) when is_binary(nickname),
1206 do: tag(get_by_nickname(nickname), tags)
1207
1208 def tag(%User{} = user, tags),
1209 do: update_tags(user, Enum.uniq((user.tags || []) ++ normalize_tags(tags)))
1210
1211 def untag(user_identifiers, tags) when is_list(user_identifiers) do
1212 Repo.transaction(fn ->
1213 for user_identifier <- user_identifiers, do: untag(user_identifier, tags)
1214 end)
1215 end
1216
1217 def untag(nickname, tags) when is_binary(nickname),
1218 do: untag(get_by_nickname(nickname), tags)
1219
1220 def untag(%User{} = user, tags),
1221 do: update_tags(user, (user.tags || []) -- normalize_tags(tags))
1222
1223 defp update_tags(%User{} = user, new_tags) do
1224 {:ok, updated_user} =
1225 user
1226 |> change(%{tags: new_tags})
1227 |> update_and_set_cache()
1228
1229 updated_user
1230 end
1231
1232 defp normalize_tags(tags) do
1233 [tags]
1234 |> List.flatten()
1235 |> Enum.map(&String.downcase(&1))
1236 end
1237
1238 defp local_nickname_regex do
1239 if Pleroma.Config.get([:instance, :extended_nickname_format]) do
1240 @extended_local_nickname_regex
1241 else
1242 @strict_local_nickname_regex
1243 end
1244 end
1245
1246 def local_nickname(nickname_or_mention) do
1247 nickname_or_mention
1248 |> full_nickname()
1249 |> String.split("@")
1250 |> hd()
1251 end
1252
1253 def full_nickname(nickname_or_mention),
1254 do: String.trim_leading(nickname_or_mention, "@")
1255
1256 def error_user(ap_id) do
1257 %User{
1258 name: ap_id,
1259 ap_id: ap_id,
1260 info: %User.Info{},
1261 nickname: "erroruser@example.com",
1262 inserted_at: NaiveDateTime.utc_now()
1263 }
1264 end
1265
1266 @spec all_superusers() :: [User.t()]
1267 def all_superusers do
1268 User.Query.build(%{super_users: true, local: true, deactivated: false})
1269 |> Repo.all()
1270 end
1271
1272 def showing_reblogs?(%User{} = user, %User{} = target) do
1273 target.ap_id not in user.info.muted_reblogs
1274 end
1275
1276 @spec toggle_confirmation(User.t()) :: {:ok, User.t()} | {:error, Changeset.t()}
1277 def toggle_confirmation(%User{} = user) do
1278 need_confirmation? = !user.info.confirmation_pending
1279
1280 info_changeset =
1281 User.Info.confirmation_changeset(user.info, need_confirmation: need_confirmation?)
1282
1283 user
1284 |> change()
1285 |> put_embed(:info, info_changeset)
1286 |> update_and_set_cache()
1287 end
1288
1289 def get_mascot(%{info: %{mascot: %{} = mascot}}) when not is_nil(mascot) do
1290 mascot
1291 end
1292
1293 def get_mascot(%{info: %{mascot: mascot}}) when is_nil(mascot) do
1294 # use instance-default
1295 config = Pleroma.Config.get([:assets, :mascots])
1296 default_mascot = Pleroma.Config.get([:assets, :default_mascot])
1297 mascot = Keyword.get(config, default_mascot)
1298
1299 %{
1300 "id" => "default-mascot",
1301 "url" => mascot[:url],
1302 "preview_url" => mascot[:url],
1303 "pleroma" => %{
1304 "mime_type" => mascot[:mime_type]
1305 }
1306 }
1307 end
1308
1309 def ensure_keys_present(user) do
1310 info = user.info
1311
1312 if info.keys do
1313 {:ok, user}
1314 else
1315 {:ok, pem} = Keys.generate_rsa_pem()
1316
1317 info_cng =
1318 info
1319 |> User.Info.set_keys(pem)
1320
1321 cng =
1322 Ecto.Changeset.change(user)
1323 |> Ecto.Changeset.put_embed(:info, info_cng)
1324
1325 update_and_set_cache(cng)
1326 end
1327 end
1328
1329 def get_ap_ids_by_nicknames(nicknames) do
1330 from(u in User,
1331 where: u.nickname in ^nicknames,
1332 select: u.ap_id
1333 )
1334 |> Repo.all()
1335 end
1336
1337 defdelegate search(query, opts \\ []), to: User.Search
1338 end