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