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