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