it is changed in compile time
[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 def html_filter_policy(_), do: Pleroma.Config.get([:markup, :scrub_policy])
1040
1041 def fetch_by_ap_id(ap_id) do
1042 ap_try = ActivityPub.make_user_from_ap_id(ap_id)
1043
1044 case ap_try do
1045 {:ok, user} ->
1046 {:ok, user}
1047
1048 _ ->
1049 case OStatus.make_user(ap_id) do
1050 {:ok, user} -> {:ok, user}
1051 _ -> {:error, "Could not fetch by AP id"}
1052 end
1053 end
1054 end
1055
1056 def get_or_fetch_by_ap_id(ap_id) do
1057 user = get_cached_by_ap_id(ap_id)
1058
1059 if !is_nil(user) and !User.needs_update?(user) do
1060 {:ok, user}
1061 else
1062 # Whether to fetch initial posts for the user (if it's a new user & the fetching is enabled)
1063 should_fetch_initial = is_nil(user) and Pleroma.Config.get([:fetch_initial_posts, :enabled])
1064
1065 resp = fetch_by_ap_id(ap_id)
1066
1067 if should_fetch_initial do
1068 with {:ok, %User{} = user} <- resp do
1069 fetch_initial_posts(user)
1070 end
1071 end
1072
1073 resp
1074 end
1075 end
1076
1077 def get_or_create_instance_user do
1078 relay_uri = "#{Pleroma.Web.Endpoint.url()}/relay"
1079
1080 if user = get_cached_by_ap_id(relay_uri) do
1081 user
1082 else
1083 changes =
1084 %User{info: %User.Info{}}
1085 |> cast(%{}, [:ap_id, :nickname, :local])
1086 |> put_change(:ap_id, relay_uri)
1087 |> put_change(:nickname, nil)
1088 |> put_change(:local, true)
1089 |> put_change(:follower_address, relay_uri <> "/followers")
1090
1091 {:ok, user} = Repo.insert(changes)
1092 user
1093 end
1094 end
1095
1096 # AP style
1097 def public_key_from_info(%{
1098 source_data: %{"publicKey" => %{"publicKeyPem" => public_key_pem}}
1099 }) do
1100 key =
1101 public_key_pem
1102 |> :public_key.pem_decode()
1103 |> hd()
1104 |> :public_key.pem_entry_decode()
1105
1106 {:ok, key}
1107 end
1108
1109 # OStatus Magic Key
1110 def public_key_from_info(%{magic_key: magic_key}) do
1111 {:ok, Pleroma.Web.Salmon.decode_key(magic_key)}
1112 end
1113
1114 def get_public_key_for_ap_id(ap_id) do
1115 with {:ok, %User{} = user} <- get_or_fetch_by_ap_id(ap_id),
1116 {:ok, public_key} <- public_key_from_info(user.info) do
1117 {:ok, public_key}
1118 else
1119 _ -> :error
1120 end
1121 end
1122
1123 defp blank?(""), do: nil
1124 defp blank?(n), do: n
1125
1126 def insert_or_update_user(data) do
1127 data
1128 |> Map.put(:name, blank?(data[:name]) || data[:nickname])
1129 |> remote_user_creation()
1130 |> Repo.insert(on_conflict: :replace_all, conflict_target: :nickname)
1131 |> set_cache()
1132 end
1133
1134 def ap_enabled?(%User{local: true}), do: true
1135 def ap_enabled?(%User{info: info}), do: info.ap_enabled
1136 def ap_enabled?(_), do: false
1137
1138 @doc "Gets or fetch a user by uri or nickname."
1139 @spec get_or_fetch(String.t()) :: {:ok, User.t()} | {:error, String.t()}
1140 def get_or_fetch("http" <> _host = uri), do: get_or_fetch_by_ap_id(uri)
1141 def get_or_fetch(nickname), do: get_or_fetch_by_nickname(nickname)
1142
1143 # wait a period of time and return newest version of the User structs
1144 # this is because we have synchronous follow APIs and need to simulate them
1145 # with an async handshake
1146 def wait_and_refresh(_, %User{local: true} = a, %User{local: true} = b) do
1147 with %User{} = a <- User.get_cached_by_id(a.id),
1148 %User{} = b <- User.get_cached_by_id(b.id) do
1149 {:ok, a, b}
1150 else
1151 _e ->
1152 :error
1153 end
1154 end
1155
1156 def wait_and_refresh(timeout, %User{} = a, %User{} = b) do
1157 with :ok <- :timer.sleep(timeout),
1158 %User{} = a <- User.get_cached_by_id(a.id),
1159 %User{} = b <- User.get_cached_by_id(b.id) do
1160 {:ok, a, b}
1161 else
1162 _e ->
1163 :error
1164 end
1165 end
1166
1167 def parse_bio(bio) when is_binary(bio) and bio != "" do
1168 bio
1169 |> CommonUtils.format_input("text/plain", mentions_format: :full)
1170 |> elem(0)
1171 end
1172
1173 def parse_bio(_), do: ""
1174
1175 def parse_bio(bio, user) when is_binary(bio) and bio != "" do
1176 # TODO: get profile URLs other than user.ap_id
1177 profile_urls = [user.ap_id]
1178
1179 bio
1180 |> CommonUtils.format_input("text/plain",
1181 mentions_format: :full,
1182 rel: &RelMe.maybe_put_rel_me(&1, profile_urls)
1183 )
1184 |> elem(0)
1185 end
1186
1187 def parse_bio(_, _), do: ""
1188
1189 def tag(user_identifiers, tags) when is_list(user_identifiers) do
1190 Repo.transaction(fn ->
1191 for user_identifier <- user_identifiers, do: tag(user_identifier, tags)
1192 end)
1193 end
1194
1195 def tag(nickname, tags) when is_binary(nickname),
1196 do: tag(get_by_nickname(nickname), tags)
1197
1198 def tag(%User{} = user, tags),
1199 do: update_tags(user, Enum.uniq((user.tags || []) ++ normalize_tags(tags)))
1200
1201 def untag(user_identifiers, tags) when is_list(user_identifiers) do
1202 Repo.transaction(fn ->
1203 for user_identifier <- user_identifiers, do: untag(user_identifier, tags)
1204 end)
1205 end
1206
1207 def untag(nickname, tags) when is_binary(nickname),
1208 do: untag(get_by_nickname(nickname), tags)
1209
1210 def untag(%User{} = user, tags),
1211 do: update_tags(user, (user.tags || []) -- normalize_tags(tags))
1212
1213 defp update_tags(%User{} = user, new_tags) do
1214 {:ok, updated_user} =
1215 user
1216 |> change(%{tags: new_tags})
1217 |> update_and_set_cache()
1218
1219 updated_user
1220 end
1221
1222 defp normalize_tags(tags) do
1223 [tags]
1224 |> List.flatten()
1225 |> Enum.map(&String.downcase(&1))
1226 end
1227
1228 defp local_nickname_regex do
1229 if Pleroma.Config.get([:instance, :extended_nickname_format]) do
1230 @extended_local_nickname_regex
1231 else
1232 @strict_local_nickname_regex
1233 end
1234 end
1235
1236 def local_nickname(nickname_or_mention) do
1237 nickname_or_mention
1238 |> full_nickname()
1239 |> String.split("@")
1240 |> hd()
1241 end
1242
1243 def full_nickname(nickname_or_mention),
1244 do: String.trim_leading(nickname_or_mention, "@")
1245
1246 def error_user(ap_id) do
1247 %User{
1248 name: ap_id,
1249 ap_id: ap_id,
1250 info: %User.Info{},
1251 nickname: "erroruser@example.com",
1252 inserted_at: NaiveDateTime.utc_now()
1253 }
1254 end
1255
1256 @spec all_superusers() :: [User.t()]
1257 def all_superusers do
1258 User.Query.build(%{super_users: true, local: true, deactivated: false})
1259 |> Repo.all()
1260 end
1261
1262 def showing_reblogs?(%User{} = user, %User{} = target) do
1263 target.ap_id not in user.info.muted_reblogs
1264 end
1265
1266 @spec toggle_confirmation(User.t()) :: {:ok, User.t()} | {:error, Changeset.t()}
1267 def toggle_confirmation(%User{} = user) do
1268 need_confirmation? = !user.info.confirmation_pending
1269
1270 info_changeset =
1271 User.Info.confirmation_changeset(user.info, need_confirmation: need_confirmation?)
1272
1273 user
1274 |> change()
1275 |> put_embed(:info, info_changeset)
1276 |> update_and_set_cache()
1277 end
1278
1279 def get_mascot(%{info: %{mascot: %{} = mascot}}) when not is_nil(mascot) do
1280 mascot
1281 end
1282
1283 def get_mascot(%{info: %{mascot: mascot}}) when is_nil(mascot) do
1284 # use instance-default
1285 config = Pleroma.Config.get([:assets, :mascots])
1286 default_mascot = Pleroma.Config.get([:assets, :default_mascot])
1287 mascot = Keyword.get(config, default_mascot)
1288
1289 %{
1290 "id" => "default-mascot",
1291 "url" => mascot[:url],
1292 "preview_url" => mascot[:url],
1293 "pleroma" => %{
1294 "mime_type" => mascot[:mime_type]
1295 }
1296 }
1297 end
1298
1299 def ensure_keys_present(user) do
1300 info = user.info
1301
1302 if info.keys do
1303 {:ok, user}
1304 else
1305 {:ok, pem} = Keys.generate_rsa_pem()
1306
1307 info_cng =
1308 info
1309 |> User.Info.set_keys(pem)
1310
1311 cng =
1312 Ecto.Changeset.change(user)
1313 |> Ecto.Changeset.put_embed(:info, info_cng)
1314
1315 update_and_set_cache(cng)
1316 end
1317 end
1318
1319 def get_ap_ids_by_nicknames(nicknames) do
1320 from(u in User,
1321 where: u.nickname in ^nicknames,
1322 select: u.ap_id
1323 )
1324 |> Repo.all()
1325 end
1326
1327 defdelegate search(query, opts \\ []), to: User.Search
1328 end