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