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