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