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