Merge branch 'domain-blocking-relationship-api' into 'develop'
[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 # This is mostly an SPC migration fix. This guesses the user nickname by taking the last part
454 # of the ap_id and the domain and tries to get that user
455 def get_by_guessed_nickname(ap_id) do
456 domain = URI.parse(ap_id).host
457 name = List.last(String.split(ap_id, "/"))
458 nickname = "#{name}@#{domain}"
459
460 get_cached_by_nickname(nickname)
461 end
462
463 def set_cache({:ok, user}), do: set_cache(user)
464 def set_cache({:error, err}), do: {:error, err}
465
466 def set_cache(%User{} = user) do
467 Cachex.put(:user_cache, "ap_id:#{user.ap_id}", user)
468 Cachex.put(:user_cache, "nickname:#{user.nickname}", user)
469 Cachex.put(:user_cache, "user_info:#{user.id}", user_info(user))
470 {:ok, user}
471 end
472
473 def update_and_set_cache(changeset) do
474 with {:ok, user} <- Repo.update(changeset) do
475 set_cache(user)
476 else
477 e -> e
478 end
479 end
480
481 def invalidate_cache(user) do
482 Cachex.del(:user_cache, "ap_id:#{user.ap_id}")
483 Cachex.del(:user_cache, "nickname:#{user.nickname}")
484 Cachex.del(:user_cache, "user_info:#{user.id}")
485 end
486
487 def get_cached_by_ap_id(ap_id) do
488 key = "ap_id:#{ap_id}"
489 Cachex.fetch!(:user_cache, key, fn _ -> get_by_ap_id(ap_id) end)
490 end
491
492 def get_cached_by_id(id) do
493 key = "id:#{id}"
494
495 ap_id =
496 Cachex.fetch!(:user_cache, key, fn _ ->
497 user = get_by_id(id)
498
499 if user do
500 Cachex.put(:user_cache, "ap_id:#{user.ap_id}", user)
501 {:commit, user.ap_id}
502 else
503 {:ignore, ""}
504 end
505 end)
506
507 get_cached_by_ap_id(ap_id)
508 end
509
510 def get_cached_by_nickname(nickname) do
511 key = "nickname:#{nickname}"
512
513 Cachex.fetch!(:user_cache, key, fn ->
514 user_result = get_or_fetch_by_nickname(nickname)
515
516 case user_result do
517 {:ok, user} -> {:commit, user}
518 {:error, _error} -> {:ignore, nil}
519 end
520 end)
521 end
522
523 def get_cached_by_nickname_or_id(nickname_or_id) do
524 get_cached_by_id(nickname_or_id) || get_cached_by_nickname(nickname_or_id)
525 end
526
527 def get_by_nickname(nickname) do
528 Repo.get_by(User, nickname: nickname) ||
529 if Regex.match?(~r(@#{Pleroma.Web.Endpoint.host()})i, nickname) do
530 Repo.get_by(User, nickname: local_nickname(nickname))
531 end
532 end
533
534 def get_by_email(email), do: Repo.get_by(User, email: email)
535
536 def get_by_nickname_or_email(nickname_or_email) do
537 get_by_nickname(nickname_or_email) || get_by_email(nickname_or_email)
538 end
539
540 def get_cached_user_info(user) do
541 key = "user_info:#{user.id}"
542 Cachex.fetch!(:user_cache, key, fn _ -> user_info(user) end)
543 end
544
545 def fetch_by_nickname(nickname) do
546 ap_try = ActivityPub.make_user_from_nickname(nickname)
547
548 case ap_try do
549 {:ok, user} -> {:ok, user}
550 _ -> OStatus.make_user(nickname)
551 end
552 end
553
554 def get_or_fetch_by_nickname(nickname) do
555 with %User{} = user <- get_by_nickname(nickname) do
556 {:ok, user}
557 else
558 _e ->
559 with [_nick, _domain] <- String.split(nickname, "@"),
560 {:ok, user} <- fetch_by_nickname(nickname) do
561 if Pleroma.Config.get([:fetch_initial_posts, :enabled]) do
562 fetch_initial_posts(user)
563 end
564
565 {:ok, user}
566 else
567 _e -> {:error, "not found " <> nickname}
568 end
569 end
570 end
571
572 @doc "Fetch some posts when the user has just been federated with"
573 def fetch_initial_posts(user),
574 do: PleromaJobQueue.enqueue(:background, __MODULE__, [:fetch_initial_posts, user])
575
576 @spec get_followers_query(User.t(), pos_integer() | nil) :: Ecto.Query.t()
577 def get_followers_query(%User{} = user, nil) do
578 User.Query.build(%{followers: user, deactivated: false})
579 end
580
581 def get_followers_query(user, page) do
582 from(u in get_followers_query(user, nil))
583 |> User.Query.paginate(page, 20)
584 end
585
586 @spec get_followers_query(User.t()) :: Ecto.Query.t()
587 def get_followers_query(user), do: get_followers_query(user, nil)
588
589 @spec get_followers(User.t(), pos_integer()) :: {:ok, list(User.t())}
590 def get_followers(user, page \\ nil) do
591 q = get_followers_query(user, page)
592
593 {:ok, Repo.all(q)}
594 end
595
596 @spec get_external_followers(User.t(), pos_integer()) :: {:ok, list(User.t())}
597 def get_external_followers(user, page \\ nil) do
598 q =
599 user
600 |> get_followers_query(page)
601 |> User.Query.build(%{external: true})
602
603 {:ok, Repo.all(q)}
604 end
605
606 def get_followers_ids(user, page \\ nil) do
607 q = get_followers_query(user, page)
608
609 Repo.all(from(u in q, select: u.id))
610 end
611
612 @spec get_friends_query(User.t(), pos_integer() | nil) :: Ecto.Query.t()
613 def get_friends_query(%User{} = user, nil) do
614 User.Query.build(%{friends: user, deactivated: false})
615 end
616
617 def get_friends_query(user, page) do
618 from(u in get_friends_query(user, nil))
619 |> User.Query.paginate(page, 20)
620 end
621
622 @spec get_friends_query(User.t()) :: Ecto.Query.t()
623 def get_friends_query(user), do: get_friends_query(user, nil)
624
625 def get_friends(user, page \\ nil) do
626 q = get_friends_query(user, page)
627
628 {:ok, Repo.all(q)}
629 end
630
631 def get_friends_ids(user, page \\ nil) do
632 q = get_friends_query(user, page)
633
634 Repo.all(from(u in q, select: u.id))
635 end
636
637 @spec get_follow_requests(User.t()) :: {:ok, [User.t()]}
638 def get_follow_requests(%User{} = user) do
639 users =
640 Activity.follow_requests_for_actor(user)
641 |> join(:inner, [a], u in User, on: a.actor == u.ap_id)
642 |> where([a, u], not fragment("? @> ?", u.following, ^[user.follower_address]))
643 |> group_by([a, u], u.id)
644 |> select([a, u], u)
645 |> Repo.all()
646
647 {:ok, users}
648 end
649
650 def increase_note_count(%User{} = user) do
651 User
652 |> where(id: ^user.id)
653 |> update([u],
654 set: [
655 info:
656 fragment(
657 "jsonb_set(?, '{note_count}', ((?->>'note_count')::int + 1)::varchar::jsonb, true)",
658 u.info,
659 u.info
660 )
661 ]
662 )
663 |> select([u], u)
664 |> Repo.update_all([])
665 |> case do
666 {1, [user]} -> set_cache(user)
667 _ -> {:error, user}
668 end
669 end
670
671 def decrease_note_count(%User{} = user) do
672 User
673 |> where(id: ^user.id)
674 |> update([u],
675 set: [
676 info:
677 fragment(
678 "jsonb_set(?, '{note_count}', (greatest(0, (?->>'note_count')::int - 1))::varchar::jsonb, true)",
679 u.info,
680 u.info
681 )
682 ]
683 )
684 |> select([u], u)
685 |> Repo.update_all([])
686 |> case do
687 {1, [user]} -> set_cache(user)
688 _ -> {:error, user}
689 end
690 end
691
692 def update_note_count(%User{} = user) do
693 note_count_query =
694 from(
695 a in Object,
696 where: fragment("?->>'actor' = ? and ?->>'type' = 'Note'", a.data, ^user.ap_id, a.data),
697 select: count(a.id)
698 )
699
700 note_count = Repo.one(note_count_query)
701
702 info_cng = User.Info.set_note_count(user.info, note_count)
703
704 user
705 |> change()
706 |> put_embed(:info, info_cng)
707 |> update_and_set_cache()
708 end
709
710 def update_follower_count(%User{} = user) do
711 follower_count_query =
712 User.Query.build(%{followers: user, deactivated: false})
713 |> select([u], %{count: count(u.id)})
714
715 User
716 |> where(id: ^user.id)
717 |> join(:inner, [u], s in subquery(follower_count_query))
718 |> update([u, s],
719 set: [
720 info:
721 fragment(
722 "jsonb_set(?, '{follower_count}', ?::varchar::jsonb, true)",
723 u.info,
724 s.count
725 )
726 ]
727 )
728 |> select([u], u)
729 |> Repo.update_all([])
730 |> case do
731 {1, [user]} -> set_cache(user)
732 _ -> {:error, user}
733 end
734 end
735
736 def remove_duplicated_following(%User{following: following} = user) do
737 uniq_following = Enum.uniq(following)
738
739 if length(following) == length(uniq_following) do
740 {:ok, user}
741 else
742 user
743 |> update_changeset(%{following: uniq_following})
744 |> update_and_set_cache()
745 end
746 end
747
748 @spec get_users_from_set([String.t()], boolean()) :: [User.t()]
749 def get_users_from_set(ap_ids, local_only \\ true) do
750 criteria = %{ap_id: ap_ids, deactivated: false}
751 criteria = if local_only, do: Map.put(criteria, :local, true), else: criteria
752
753 User.Query.build(criteria)
754 |> Repo.all()
755 end
756
757 @spec get_recipients_from_activity(Activity.t()) :: [User.t()]
758 def get_recipients_from_activity(%Activity{recipients: to}) do
759 User.Query.build(%{recipients_from_activity: to, local: true, deactivated: false})
760 |> Repo.all()
761 end
762
763 @spec mute(User.t(), User.t(), boolean()) :: {:ok, User.t()} | {:error, String.t()}
764 def mute(muter, %User{ap_id: ap_id}, notifications? \\ true) do
765 info = muter.info
766
767 info_cng =
768 User.Info.add_to_mutes(info, ap_id)
769 |> User.Info.add_to_muted_notifications(info, ap_id, notifications?)
770
771 cng =
772 change(muter)
773 |> put_embed(:info, info_cng)
774
775 update_and_set_cache(cng)
776 end
777
778 def unmute(muter, %{ap_id: ap_id}) do
779 info = muter.info
780
781 info_cng =
782 User.Info.remove_from_mutes(info, ap_id)
783 |> User.Info.remove_from_muted_notifications(info, ap_id)
784
785 cng =
786 change(muter)
787 |> put_embed(:info, info_cng)
788
789 update_and_set_cache(cng)
790 end
791
792 def subscribe(subscriber, %{ap_id: ap_id}) do
793 deny_follow_blocked = Pleroma.Config.get([:user, :deny_follow_blocked])
794
795 with %User{} = subscribed <- get_cached_by_ap_id(ap_id) do
796 blocked = blocks?(subscribed, subscriber) and deny_follow_blocked
797
798 if blocked do
799 {:error, "Could not subscribe: #{subscribed.nickname} is blocking you"}
800 else
801 info_cng =
802 subscribed.info
803 |> User.Info.add_to_subscribers(subscriber.ap_id)
804
805 change(subscribed)
806 |> put_embed(:info, info_cng)
807 |> update_and_set_cache()
808 end
809 end
810 end
811
812 def unsubscribe(unsubscriber, %{ap_id: ap_id}) do
813 with %User{} = user <- get_cached_by_ap_id(ap_id) do
814 info_cng =
815 user.info
816 |> User.Info.remove_from_subscribers(unsubscriber.ap_id)
817
818 change(user)
819 |> put_embed(:info, info_cng)
820 |> update_and_set_cache()
821 end
822 end
823
824 def block(blocker, %User{ap_id: ap_id} = blocked) do
825 # sever any follow relationships to prevent leaks per activitypub (Pleroma issue #213)
826 blocker =
827 if following?(blocker, blocked) do
828 {:ok, blocker, _} = unfollow(blocker, blocked)
829 blocker
830 else
831 blocker
832 end
833
834 blocker =
835 if subscribed_to?(blocked, blocker) do
836 {:ok, blocker} = unsubscribe(blocked, blocker)
837 blocker
838 else
839 blocker
840 end
841
842 if following?(blocked, blocker) do
843 unfollow(blocked, blocker)
844 end
845
846 {:ok, blocker} = update_follower_count(blocker)
847
848 info_cng =
849 blocker.info
850 |> User.Info.add_to_block(ap_id)
851
852 cng =
853 change(blocker)
854 |> put_embed(:info, info_cng)
855
856 update_and_set_cache(cng)
857 end
858
859 # helper to handle the block given only an actor's AP id
860 def block(blocker, %{ap_id: ap_id}) do
861 block(blocker, get_cached_by_ap_id(ap_id))
862 end
863
864 def unblock(blocker, %{ap_id: ap_id}) do
865 info_cng =
866 blocker.info
867 |> User.Info.remove_from_block(ap_id)
868
869 cng =
870 change(blocker)
871 |> put_embed(:info, info_cng)
872
873 update_and_set_cache(cng)
874 end
875
876 def mutes?(nil, _), do: false
877 def mutes?(user, %{ap_id: ap_id}), do: Enum.member?(user.info.mutes, ap_id)
878
879 @spec muted_notifications?(User.t() | nil, User.t() | map()) :: boolean()
880 def muted_notifications?(nil, _), do: false
881
882 def muted_notifications?(user, %{ap_id: ap_id}),
883 do: Enum.member?(user.info.muted_notifications, ap_id)
884
885 def blocks?(%User{} = user, %User{} = target) do
886 blocks_ap_id?(user, target) || blocks_domain?(user, target)
887 end
888
889 def blocks?(nil, _), do: false
890
891 def blocks_ap_id?(%User{} = user, %User{} = target) do
892 Enum.member?(user.info.blocks, target.ap_id)
893 end
894
895 def blocks_ap_id?(_, _), do: false
896
897 def blocks_domain?(%User{} = user, %User{} = target) do
898 domain_blocks = Pleroma.Web.ActivityPub.MRF.subdomains_regex(user.info.domain_blocks)
899 %{host: host} = URI.parse(target.ap_id)
900 Pleroma.Web.ActivityPub.MRF.subdomain_match?(domain_blocks, host)
901 end
902
903 def blocks_domain?(_, _), do: false
904
905 def subscribed_to?(user, %{ap_id: ap_id}) do
906 with %User{} = target <- get_cached_by_ap_id(ap_id) do
907 Enum.member?(target.info.subscribers, user.ap_id)
908 end
909 end
910
911 @spec muted_users(User.t()) :: [User.t()]
912 def muted_users(user) do
913 User.Query.build(%{ap_id: user.info.mutes, deactivated: false})
914 |> Repo.all()
915 end
916
917 @spec blocked_users(User.t()) :: [User.t()]
918 def blocked_users(user) do
919 User.Query.build(%{ap_id: user.info.blocks, deactivated: false})
920 |> Repo.all()
921 end
922
923 @spec subscribers(User.t()) :: [User.t()]
924 def subscribers(user) do
925 User.Query.build(%{ap_id: user.info.subscribers, deactivated: false})
926 |> Repo.all()
927 end
928
929 def block_domain(user, domain) do
930 info_cng =
931 user.info
932 |> User.Info.add_to_domain_block(domain)
933
934 cng =
935 change(user)
936 |> put_embed(:info, info_cng)
937
938 update_and_set_cache(cng)
939 end
940
941 def unblock_domain(user, domain) do
942 info_cng =
943 user.info
944 |> User.Info.remove_from_domain_block(domain)
945
946 cng =
947 change(user)
948 |> put_embed(:info, info_cng)
949
950 update_and_set_cache(cng)
951 end
952
953 def deactivate_async(user, status \\ true) do
954 PleromaJobQueue.enqueue(:background, __MODULE__, [:deactivate_async, user, status])
955 end
956
957 def deactivate(%User{} = user, status \\ true) do
958 info_cng = User.Info.set_activation_status(user.info, status)
959
960 with {:ok, friends} <- User.get_friends(user),
961 {:ok, followers} <- User.get_followers(user),
962 {:ok, user} <-
963 user
964 |> change()
965 |> put_embed(:info, info_cng)
966 |> update_and_set_cache() do
967 Enum.each(followers, &invalidate_cache(&1))
968 Enum.each(friends, &update_follower_count(&1))
969
970 {:ok, user}
971 end
972 end
973
974 def update_notification_settings(%User{} = user, settings \\ %{}) do
975 info_changeset = User.Info.update_notification_settings(user.info, settings)
976
977 change(user)
978 |> put_embed(:info, info_changeset)
979 |> update_and_set_cache()
980 end
981
982 @spec delete(User.t()) :: :ok
983 def delete(%User{} = user),
984 do: PleromaJobQueue.enqueue(:background, __MODULE__, [:delete, user])
985
986 @spec perform(atom(), User.t()) :: {:ok, User.t()}
987 def perform(:delete, %User{} = user) do
988 {:ok, _user} = ActivityPub.delete(user)
989
990 # Remove all relationships
991 {:ok, followers} = User.get_followers(user)
992
993 Enum.each(followers, fn follower ->
994 ActivityPub.unfollow(follower, user)
995 User.unfollow(follower, user)
996 end)
997
998 {:ok, friends} = User.get_friends(user)
999
1000 Enum.each(friends, fn followed ->
1001 ActivityPub.unfollow(user, followed)
1002 User.unfollow(user, followed)
1003 end)
1004
1005 delete_user_activities(user)
1006 invalidate_cache(user)
1007 Repo.delete(user)
1008 end
1009
1010 @spec perform(atom(), User.t()) :: {:ok, User.t()}
1011 def perform(:fetch_initial_posts, %User{} = user) do
1012 pages = Pleroma.Config.get!([:fetch_initial_posts, :pages])
1013
1014 Enum.each(
1015 # Insert all the posts in reverse order, so they're in the right order on the timeline
1016 Enum.reverse(Utils.fetch_ordered_collection(user.info.source_data["outbox"], pages)),
1017 &Pleroma.Web.Federator.incoming_ap_doc/1
1018 )
1019
1020 {:ok, user}
1021 end
1022
1023 def perform(:deactivate_async, user, status), do: deactivate(user, status)
1024
1025 @spec perform(atom(), User.t(), list()) :: list() | {:error, any()}
1026 def perform(:blocks_import, %User{} = blocker, blocked_identifiers)
1027 when is_list(blocked_identifiers) do
1028 Enum.map(
1029 blocked_identifiers,
1030 fn blocked_identifier ->
1031 with {:ok, %User{} = blocked} <- get_or_fetch(blocked_identifier),
1032 {:ok, blocker} <- block(blocker, blocked),
1033 {:ok, _} <- ActivityPub.block(blocker, blocked) do
1034 blocked
1035 else
1036 err ->
1037 Logger.debug("blocks_import failed for #{blocked_identifier} with: #{inspect(err)}")
1038 err
1039 end
1040 end
1041 )
1042 end
1043
1044 @spec perform(atom(), User.t(), list()) :: list() | {:error, any()}
1045 def perform(:follow_import, %User{} = follower, followed_identifiers)
1046 when is_list(followed_identifiers) do
1047 Enum.map(
1048 followed_identifiers,
1049 fn followed_identifier ->
1050 with {:ok, %User{} = followed} <- get_or_fetch(followed_identifier),
1051 {:ok, follower} <- maybe_direct_follow(follower, followed),
1052 {:ok, _} <- ActivityPub.follow(follower, followed) do
1053 followed
1054 else
1055 err ->
1056 Logger.debug("follow_import failed for #{followed_identifier} with: #{inspect(err)}")
1057 err
1058 end
1059 end
1060 )
1061 end
1062
1063 @spec external_users_query() :: Ecto.Query.t()
1064 def external_users_query do
1065 User.Query.build(%{
1066 external: true,
1067 active: true,
1068 order_by: :id
1069 })
1070 end
1071
1072 @spec external_users(keyword()) :: [User.t()]
1073 def external_users(opts \\ []) do
1074 query =
1075 external_users_query()
1076 |> select([u], struct(u, [:id, :ap_id, :info]))
1077
1078 query =
1079 if opts[:max_id],
1080 do: where(query, [u], u.id > ^opts[:max_id]),
1081 else: query
1082
1083 query =
1084 if opts[:limit],
1085 do: limit(query, ^opts[:limit]),
1086 else: query
1087
1088 Repo.all(query)
1089 end
1090
1091 def blocks_import(%User{} = blocker, blocked_identifiers) when is_list(blocked_identifiers),
1092 do:
1093 PleromaJobQueue.enqueue(:background, __MODULE__, [
1094 :blocks_import,
1095 blocker,
1096 blocked_identifiers
1097 ])
1098
1099 def follow_import(%User{} = follower, followed_identifiers) when is_list(followed_identifiers),
1100 do:
1101 PleromaJobQueue.enqueue(:background, __MODULE__, [
1102 :follow_import,
1103 follower,
1104 followed_identifiers
1105 ])
1106
1107 def delete_user_activities(%User{ap_id: ap_id} = user) do
1108 ap_id
1109 |> Activity.query_by_actor()
1110 |> RepoStreamer.chunk_stream(50)
1111 |> Stream.each(fn activities ->
1112 Enum.each(activities, &delete_activity(&1))
1113 end)
1114 |> Stream.run()
1115
1116 {:ok, user}
1117 end
1118
1119 defp delete_activity(%{data: %{"type" => "Create"}} = activity) do
1120 activity
1121 |> Object.normalize()
1122 |> ActivityPub.delete()
1123 end
1124
1125 defp delete_activity(%{data: %{"type" => "Like"}} = activity) do
1126 user = get_cached_by_ap_id(activity.actor)
1127 object = Object.normalize(activity)
1128
1129 ActivityPub.unlike(user, object)
1130 end
1131
1132 defp delete_activity(%{data: %{"type" => "Announce"}} = activity) do
1133 user = get_cached_by_ap_id(activity.actor)
1134 object = Object.normalize(activity)
1135
1136 ActivityPub.unannounce(user, object)
1137 end
1138
1139 defp delete_activity(_activity), do: "Doing nothing"
1140
1141 def html_filter_policy(%User{info: %{no_rich_text: true}}) do
1142 Pleroma.HTML.Scrubber.TwitterText
1143 end
1144
1145 def html_filter_policy(_), do: Pleroma.Config.get([:markup, :scrub_policy])
1146
1147 def fetch_by_ap_id(ap_id) do
1148 ap_try = ActivityPub.make_user_from_ap_id(ap_id)
1149
1150 case ap_try do
1151 {:ok, user} ->
1152 {:ok, user}
1153
1154 _ ->
1155 case OStatus.make_user(ap_id) do
1156 {:ok, user} -> {:ok, user}
1157 _ -> {:error, "Could not fetch by AP id"}
1158 end
1159 end
1160 end
1161
1162 def get_or_fetch_by_ap_id(ap_id) do
1163 user = get_cached_by_ap_id(ap_id)
1164
1165 if !is_nil(user) and !User.needs_update?(user) do
1166 {:ok, user}
1167 else
1168 # Whether to fetch initial posts for the user (if it's a new user & the fetching is enabled)
1169 should_fetch_initial = is_nil(user) and Pleroma.Config.get([:fetch_initial_posts, :enabled])
1170
1171 resp = fetch_by_ap_id(ap_id)
1172
1173 if should_fetch_initial do
1174 with {:ok, %User{} = user} <- resp do
1175 fetch_initial_posts(user)
1176 end
1177 end
1178
1179 resp
1180 end
1181 end
1182
1183 @doc "Creates an internal service actor by URI if missing. Optionally takes nickname for addressing."
1184 def get_or_create_service_actor_by_ap_id(uri, nickname \\ nil) do
1185 if user = get_cached_by_ap_id(uri) do
1186 user
1187 else
1188 changes =
1189 %User{info: %User.Info{}}
1190 |> cast(%{}, [:ap_id, :nickname, :local])
1191 |> put_change(:ap_id, uri)
1192 |> put_change(:nickname, nickname)
1193 |> put_change(:local, true)
1194 |> put_change(:follower_address, uri <> "/followers")
1195
1196 {:ok, user} = Repo.insert(changes)
1197 user
1198 end
1199 end
1200
1201 # AP style
1202 def public_key_from_info(%{
1203 source_data: %{"publicKey" => %{"publicKeyPem" => public_key_pem}}
1204 }) do
1205 key =
1206 public_key_pem
1207 |> :public_key.pem_decode()
1208 |> hd()
1209 |> :public_key.pem_entry_decode()
1210
1211 {:ok, key}
1212 end
1213
1214 # OStatus Magic Key
1215 def public_key_from_info(%{magic_key: magic_key}) when not is_nil(magic_key) do
1216 {:ok, Pleroma.Web.Salmon.decode_key(magic_key)}
1217 end
1218
1219 def public_key_from_info(_), do: {:error, "not found key"}
1220
1221 def get_public_key_for_ap_id(ap_id) do
1222 with {:ok, %User{} = user} <- get_or_fetch_by_ap_id(ap_id),
1223 {:ok, public_key} <- public_key_from_info(user.info) do
1224 {:ok, public_key}
1225 else
1226 _ -> :error
1227 end
1228 end
1229
1230 defp blank?(""), do: nil
1231 defp blank?(n), do: n
1232
1233 def insert_or_update_user(data) do
1234 data
1235 |> Map.put(:name, blank?(data[:name]) || data[:nickname])
1236 |> remote_user_creation()
1237 |> Repo.insert(on_conflict: :replace_all_except_primary_key, conflict_target: :nickname)
1238 |> set_cache()
1239 end
1240
1241 def ap_enabled?(%User{local: true}), do: true
1242 def ap_enabled?(%User{info: info}), do: info.ap_enabled
1243 def ap_enabled?(_), do: false
1244
1245 @doc "Gets or fetch a user by uri or nickname."
1246 @spec get_or_fetch(String.t()) :: {:ok, User.t()} | {:error, String.t()}
1247 def get_or_fetch("http" <> _host = uri), do: get_or_fetch_by_ap_id(uri)
1248 def get_or_fetch(nickname), do: get_or_fetch_by_nickname(nickname)
1249
1250 # wait a period of time and return newest version of the User structs
1251 # this is because we have synchronous follow APIs and need to simulate them
1252 # with an async handshake
1253 def wait_and_refresh(_, %User{local: true} = a, %User{local: true} = b) do
1254 with %User{} = a <- User.get_cached_by_id(a.id),
1255 %User{} = b <- User.get_cached_by_id(b.id) do
1256 {:ok, a, b}
1257 else
1258 _e ->
1259 :error
1260 end
1261 end
1262
1263 def wait_and_refresh(timeout, %User{} = a, %User{} = b) do
1264 with :ok <- :timer.sleep(timeout),
1265 %User{} = a <- User.get_cached_by_id(a.id),
1266 %User{} = b <- User.get_cached_by_id(b.id) do
1267 {:ok, a, b}
1268 else
1269 _e ->
1270 :error
1271 end
1272 end
1273
1274 def parse_bio(bio) when is_binary(bio) and bio != "" do
1275 bio
1276 |> CommonUtils.format_input("text/plain", mentions_format: :full)
1277 |> elem(0)
1278 end
1279
1280 def parse_bio(_), do: ""
1281
1282 def parse_bio(bio, user) when is_binary(bio) and bio != "" do
1283 # TODO: get profile URLs other than user.ap_id
1284 profile_urls = [user.ap_id]
1285
1286 bio
1287 |> CommonUtils.format_input("text/plain",
1288 mentions_format: :full,
1289 rel: &RelMe.maybe_put_rel_me(&1, profile_urls)
1290 )
1291 |> elem(0)
1292 end
1293
1294 def parse_bio(_, _), do: ""
1295
1296 def tag(user_identifiers, tags) when is_list(user_identifiers) do
1297 Repo.transaction(fn ->
1298 for user_identifier <- user_identifiers, do: tag(user_identifier, tags)
1299 end)
1300 end
1301
1302 def tag(nickname, tags) when is_binary(nickname),
1303 do: tag(get_by_nickname(nickname), tags)
1304
1305 def tag(%User{} = user, tags),
1306 do: update_tags(user, Enum.uniq((user.tags || []) ++ normalize_tags(tags)))
1307
1308 def untag(user_identifiers, tags) when is_list(user_identifiers) do
1309 Repo.transaction(fn ->
1310 for user_identifier <- user_identifiers, do: untag(user_identifier, tags)
1311 end)
1312 end
1313
1314 def untag(nickname, tags) when is_binary(nickname),
1315 do: untag(get_by_nickname(nickname), tags)
1316
1317 def untag(%User{} = user, tags),
1318 do: update_tags(user, (user.tags || []) -- normalize_tags(tags))
1319
1320 defp update_tags(%User{} = user, new_tags) do
1321 {:ok, updated_user} =
1322 user
1323 |> change(%{tags: new_tags})
1324 |> update_and_set_cache()
1325
1326 updated_user
1327 end
1328
1329 defp normalize_tags(tags) do
1330 [tags]
1331 |> List.flatten()
1332 |> Enum.map(&String.downcase(&1))
1333 end
1334
1335 defp local_nickname_regex do
1336 if Pleroma.Config.get([:instance, :extended_nickname_format]) do
1337 @extended_local_nickname_regex
1338 else
1339 @strict_local_nickname_regex
1340 end
1341 end
1342
1343 def local_nickname(nickname_or_mention) do
1344 nickname_or_mention
1345 |> full_nickname()
1346 |> String.split("@")
1347 |> hd()
1348 end
1349
1350 def full_nickname(nickname_or_mention),
1351 do: String.trim_leading(nickname_or_mention, "@")
1352
1353 def error_user(ap_id) do
1354 %User{
1355 name: ap_id,
1356 ap_id: ap_id,
1357 info: %User.Info{},
1358 nickname: "erroruser@example.com",
1359 inserted_at: NaiveDateTime.utc_now()
1360 }
1361 end
1362
1363 @spec all_superusers() :: [User.t()]
1364 def all_superusers do
1365 User.Query.build(%{super_users: true, local: true, deactivated: false})
1366 |> Repo.all()
1367 end
1368
1369 def showing_reblogs?(%User{} = user, %User{} = target) do
1370 target.ap_id not in user.info.muted_reblogs
1371 end
1372
1373 @spec toggle_confirmation(User.t()) :: {:ok, User.t()} | {:error, Changeset.t()}
1374 def toggle_confirmation(%User{} = user) do
1375 need_confirmation? = !user.info.confirmation_pending
1376
1377 info_changeset =
1378 User.Info.confirmation_changeset(user.info, need_confirmation: need_confirmation?)
1379
1380 user
1381 |> change()
1382 |> put_embed(:info, info_changeset)
1383 |> update_and_set_cache()
1384 end
1385
1386 def get_mascot(%{info: %{mascot: %{} = mascot}}) when not is_nil(mascot) do
1387 mascot
1388 end
1389
1390 def get_mascot(%{info: %{mascot: mascot}}) when is_nil(mascot) do
1391 # use instance-default
1392 config = Pleroma.Config.get([:assets, :mascots])
1393 default_mascot = Pleroma.Config.get([:assets, :default_mascot])
1394 mascot = Keyword.get(config, default_mascot)
1395
1396 %{
1397 "id" => "default-mascot",
1398 "url" => mascot[:url],
1399 "preview_url" => mascot[:url],
1400 "pleroma" => %{
1401 "mime_type" => mascot[:mime_type]
1402 }
1403 }
1404 end
1405
1406 def ensure_keys_present(%User{info: info} = user) do
1407 if info.keys do
1408 {:ok, user}
1409 else
1410 {:ok, pem} = Keys.generate_rsa_pem()
1411
1412 user
1413 |> Ecto.Changeset.change()
1414 |> Ecto.Changeset.put_embed(:info, User.Info.set_keys(info, pem))
1415 |> update_and_set_cache()
1416 end
1417 end
1418
1419 def get_ap_ids_by_nicknames(nicknames) do
1420 from(u in User,
1421 where: u.nickname in ^nicknames,
1422 select: u.ap_id
1423 )
1424 |> Repo.all()
1425 end
1426
1427 defdelegate search(query, opts \\ []), to: User.Search
1428
1429 defp put_password_hash(
1430 %Ecto.Changeset{valid?: true, changes: %{password: password}} = changeset
1431 ) do
1432 change(changeset, password_hash: Pbkdf2.hashpwsalt(password))
1433 end
1434
1435 defp put_password_hash(changeset), do: changeset
1436
1437 def is_internal_user?(%User{nickname: nil}), do: true
1438 def is_internal_user?(%User{local: true, nickname: "internal." <> _}), do: true
1439 def is_internal_user?(_), do: false
1440 end