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