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