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