DELETE /api/pleroma/admin/users now accepts nicknames array
[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 if !followed.local && follower.local && !ap_enabled?(followed) do
441 Websub.subscribe(follower, followed)
442 end
443
444 q =
445 from(u in User,
446 where: u.id == ^follower.id,
447 update: [push: [following: ^ap_followers]],
448 select: u
449 )
450
451 {1, [follower]} = Repo.update_all(q, [])
452
453 follower = maybe_update_following_count(follower)
454
455 {:ok, _} = update_follower_count(followed)
456
457 set_cache(follower)
458 end
459 end
460
461 def unfollow(%User{} = follower, %User{} = followed) do
462 ap_followers = followed.follower_address
463
464 if following?(follower, followed) and follower.ap_id != followed.ap_id do
465 q =
466 from(u in User,
467 where: u.id == ^follower.id,
468 update: [pull: [following: ^ap_followers]],
469 select: u
470 )
471
472 {1, [follower]} = Repo.update_all(q, [])
473
474 follower = maybe_update_following_count(follower)
475
476 {:ok, followed} = update_follower_count(followed)
477
478 set_cache(follower)
479
480 {:ok, follower, Utils.fetch_latest_follow(follower, followed)}
481 else
482 {:error, "Not subscribed!"}
483 end
484 end
485
486 @spec following?(User.t(), User.t()) :: boolean
487 def following?(%User{} = follower, %User{} = followed) do
488 Enum.member?(follower.following, followed.follower_address)
489 end
490
491 def locked?(%User{} = user) do
492 user.info.locked || false
493 end
494
495 def get_by_id(id) do
496 Repo.get_by(User, id: id)
497 end
498
499 def get_by_ap_id(ap_id) do
500 Repo.get_by(User, ap_id: ap_id)
501 end
502
503 def get_all_by_ap_id(ap_ids) do
504 from(u in __MODULE__,
505 where: u.ap_id in ^ap_ids
506 )
507 |> Repo.all()
508 end
509
510 def get_all_by_ids(ids) do
511 from(u in __MODULE__, where: u.id in ^ids)
512 |> Repo.all()
513 end
514
515 # This is mostly an SPC migration fix. This guesses the user nickname by taking the last part
516 # of the ap_id and the domain and tries to get that user
517 def get_by_guessed_nickname(ap_id) do
518 domain = URI.parse(ap_id).host
519 name = List.last(String.split(ap_id, "/"))
520 nickname = "#{name}@#{domain}"
521
522 get_cached_by_nickname(nickname)
523 end
524
525 def set_cache({:ok, user}), do: set_cache(user)
526 def set_cache({:error, err}), do: {:error, err}
527
528 def set_cache(%User{} = user) do
529 Cachex.put(:user_cache, "ap_id:#{user.ap_id}", user)
530 Cachex.put(:user_cache, "nickname:#{user.nickname}", user)
531 Cachex.put(:user_cache, "user_info:#{user.id}", user_info(user))
532 {:ok, user}
533 end
534
535 def update_and_set_cache(changeset) do
536 with {:ok, user} <- Repo.update(changeset, stale_error_field: :id) do
537 set_cache(user)
538 end
539 end
540
541 def invalidate_cache(user) do
542 Cachex.del(:user_cache, "ap_id:#{user.ap_id}")
543 Cachex.del(:user_cache, "nickname:#{user.nickname}")
544 Cachex.del(:user_cache, "user_info:#{user.id}")
545 end
546
547 def get_cached_by_ap_id(ap_id) do
548 key = "ap_id:#{ap_id}"
549 Cachex.fetch!(:user_cache, key, fn _ -> get_by_ap_id(ap_id) end)
550 end
551
552 def get_cached_by_id(id) do
553 key = "id:#{id}"
554
555 ap_id =
556 Cachex.fetch!(:user_cache, key, fn _ ->
557 user = get_by_id(id)
558
559 if user do
560 Cachex.put(:user_cache, "ap_id:#{user.ap_id}", user)
561 {:commit, user.ap_id}
562 else
563 {:ignore, ""}
564 end
565 end)
566
567 get_cached_by_ap_id(ap_id)
568 end
569
570 def get_cached_by_nickname(nickname) do
571 key = "nickname:#{nickname}"
572
573 Cachex.fetch!(:user_cache, key, fn ->
574 case get_or_fetch_by_nickname(nickname) do
575 {:ok, user} -> {:commit, user}
576 {:error, _error} -> {:ignore, nil}
577 end
578 end)
579 end
580
581 def get_cached_by_nickname_or_id(nickname_or_id, opts \\ []) do
582 restrict_to_local = Pleroma.Config.get([:instance, :limit_to_local_content])
583
584 cond do
585 is_integer(nickname_or_id) or FlakeId.flake_id?(nickname_or_id) ->
586 get_cached_by_id(nickname_or_id) || get_cached_by_nickname(nickname_or_id)
587
588 restrict_to_local == false or not String.contains?(nickname_or_id, "@") ->
589 get_cached_by_nickname(nickname_or_id)
590
591 restrict_to_local == :unauthenticated and match?(%User{}, opts[:for]) ->
592 get_cached_by_nickname(nickname_or_id)
593
594 true ->
595 nil
596 end
597 end
598
599 def get_by_nickname(nickname) do
600 Repo.get_by(User, nickname: nickname) ||
601 if Regex.match?(~r(@#{Pleroma.Web.Endpoint.host()})i, nickname) do
602 Repo.get_by(User, nickname: local_nickname(nickname))
603 end
604 end
605
606 def get_by_email(email), do: Repo.get_by(User, email: email)
607
608 def get_by_nickname_or_email(nickname_or_email) do
609 get_by_nickname(nickname_or_email) || get_by_email(nickname_or_email)
610 end
611
612 def get_cached_user_info(user) do
613 key = "user_info:#{user.id}"
614 Cachex.fetch!(:user_cache, key, fn -> user_info(user) end)
615 end
616
617 def fetch_by_nickname(nickname) do
618 case ActivityPub.make_user_from_nickname(nickname) do
619 {:ok, user} -> {:ok, user}
620 _ -> OStatus.make_user(nickname)
621 end
622 end
623
624 def get_or_fetch_by_nickname(nickname) do
625 with %User{} = user <- get_by_nickname(nickname) do
626 {:ok, user}
627 else
628 _e ->
629 with [_nick, _domain] <- String.split(nickname, "@"),
630 {:ok, user} <- fetch_by_nickname(nickname) do
631 if Pleroma.Config.get([:fetch_initial_posts, :enabled]) do
632 fetch_initial_posts(user)
633 end
634
635 {:ok, user}
636 else
637 _e -> {:error, "not found " <> nickname}
638 end
639 end
640 end
641
642 @doc "Fetch some posts when the user has just been federated with"
643 def fetch_initial_posts(user) do
644 BackgroundWorker.enqueue("fetch_initial_posts", %{"user_id" => user.id})
645 end
646
647 @spec get_followers_query(User.t(), pos_integer() | nil) :: Ecto.Query.t()
648 def get_followers_query(%User{} = user, nil) do
649 User.Query.build(%{followers: user, deactivated: false})
650 end
651
652 def get_followers_query(user, page) do
653 user
654 |> get_followers_query(nil)
655 |> User.Query.paginate(page, 20)
656 end
657
658 @spec get_followers_query(User.t()) :: Ecto.Query.t()
659 def get_followers_query(user), do: get_followers_query(user, nil)
660
661 @spec get_followers(User.t(), pos_integer()) :: {:ok, list(User.t())}
662 def get_followers(user, page \\ nil) do
663 user
664 |> get_followers_query(page)
665 |> Repo.all()
666 end
667
668 @spec get_external_followers(User.t(), pos_integer()) :: {:ok, list(User.t())}
669 def get_external_followers(user, page \\ nil) do
670 user
671 |> get_followers_query(page)
672 |> User.Query.build(%{external: true})
673 |> Repo.all()
674 end
675
676 def get_followers_ids(user, page \\ nil) do
677 user
678 |> get_followers_query(page)
679 |> select([u], u.id)
680 |> Repo.all()
681 end
682
683 @spec get_friends_query(User.t(), pos_integer() | nil) :: Ecto.Query.t()
684 def get_friends_query(%User{} = user, nil) do
685 User.Query.build(%{friends: user, deactivated: false})
686 end
687
688 def get_friends_query(user, page) do
689 user
690 |> get_friends_query(nil)
691 |> User.Query.paginate(page, 20)
692 end
693
694 @spec get_friends_query(User.t()) :: Ecto.Query.t()
695 def get_friends_query(user), do: get_friends_query(user, nil)
696
697 def get_friends(user, page \\ nil) do
698 user
699 |> get_friends_query(page)
700 |> Repo.all()
701 end
702
703 def get_friends_ids(user, page \\ nil) do
704 user
705 |> get_friends_query(page)
706 |> select([u], u.id)
707 |> Repo.all()
708 end
709
710 @spec get_follow_requests(User.t()) :: {:ok, [User.t()]}
711 def get_follow_requests(%User{} = user) do
712 user
713 |> Activity.follow_requests_for_actor()
714 |> join(:inner, [a], u in User, on: a.actor == u.ap_id)
715 |> where([a, u], not fragment("? @> ?", u.following, ^[user.follower_address]))
716 |> group_by([a, u], u.id)
717 |> select([a, u], u)
718 |> Repo.all()
719 end
720
721 def increase_note_count(%User{} = user) do
722 User
723 |> where(id: ^user.id)
724 |> update([u],
725 set: [
726 info:
727 fragment(
728 "jsonb_set(?, '{note_count}', ((?->>'note_count')::int + 1)::varchar::jsonb, true)",
729 u.info,
730 u.info
731 )
732 ]
733 )
734 |> select([u], u)
735 |> Repo.update_all([])
736 |> case do
737 {1, [user]} -> set_cache(user)
738 _ -> {:error, user}
739 end
740 end
741
742 def decrease_note_count(%User{} = user) do
743 User
744 |> where(id: ^user.id)
745 |> update([u],
746 set: [
747 info:
748 fragment(
749 "jsonb_set(?, '{note_count}', (greatest(0, (?->>'note_count')::int - 1))::varchar::jsonb, true)",
750 u.info,
751 u.info
752 )
753 ]
754 )
755 |> select([u], u)
756 |> Repo.update_all([])
757 |> case do
758 {1, [user]} -> set_cache(user)
759 _ -> {:error, user}
760 end
761 end
762
763 def update_note_count(%User{} = user) do
764 note_count =
765 from(
766 a in Object,
767 where: fragment("?->>'actor' = ? and ?->>'type' = 'Note'", a.data, ^user.ap_id, a.data),
768 select: count(a.id)
769 )
770 |> Repo.one()
771
772 update_info(user, &User.Info.set_note_count(&1, note_count))
773 end
774
775 def update_mascot(user, url) do
776 info_changeset =
777 User.Info.mascot_update(
778 user.info,
779 url
780 )
781
782 user
783 |> change()
784 |> put_embed(:info, info_changeset)
785 |> update_and_set_cache()
786 end
787
788 @spec maybe_fetch_follow_information(User.t()) :: User.t()
789 def maybe_fetch_follow_information(user) do
790 with {:ok, user} <- fetch_follow_information(user) do
791 user
792 else
793 e ->
794 Logger.error("Follower/Following counter update for #{user.ap_id} failed.\n#{inspect(e)}")
795
796 user
797 end
798 end
799
800 def fetch_follow_information(user) do
801 with {:ok, info} <- ActivityPub.fetch_follow_information_for_user(user) do
802 update_info(user, &User.Info.follow_information_update(&1, info))
803 end
804 end
805
806 def update_follower_count(%User{} = user) do
807 if user.local or !Pleroma.Config.get([:instance, :external_user_synchronization]) do
808 follower_count_query =
809 User.Query.build(%{followers: user, deactivated: false})
810 |> select([u], %{count: count(u.id)})
811
812 User
813 |> where(id: ^user.id)
814 |> join(:inner, [u], s in subquery(follower_count_query))
815 |> update([u, s],
816 set: [
817 info:
818 fragment(
819 "jsonb_set(?, '{follower_count}', ?::varchar::jsonb, true)",
820 u.info,
821 s.count
822 )
823 ]
824 )
825 |> select([u], u)
826 |> Repo.update_all([])
827 |> case do
828 {1, [user]} -> set_cache(user)
829 _ -> {:error, user}
830 end
831 else
832 {:ok, maybe_fetch_follow_information(user)}
833 end
834 end
835
836 @spec maybe_update_following_count(User.t()) :: User.t()
837 def maybe_update_following_count(%User{local: false} = user) do
838 if Pleroma.Config.get([:instance, :external_user_synchronization]) do
839 maybe_fetch_follow_information(user)
840 else
841 user
842 end
843 end
844
845 def maybe_update_following_count(user), do: user
846
847 def set_unread_conversation_count(%User{local: true} = user) do
848 unread_query = Participation.unread_conversation_count_for_user(user)
849
850 User
851 |> join(:inner, [u], p in subquery(unread_query))
852 |> update([u, p],
853 set: [
854 info:
855 fragment(
856 "jsonb_set(?, '{unread_conversation_count}', ?::varchar::jsonb, true)",
857 u.info,
858 p.count
859 )
860 ]
861 )
862 |> where([u], u.id == ^user.id)
863 |> select([u], u)
864 |> Repo.update_all([])
865 |> case do
866 {1, [user]} -> set_cache(user)
867 _ -> {:error, user}
868 end
869 end
870
871 def set_unread_conversation_count(_), do: :noop
872
873 def increment_unread_conversation_count(conversation, %User{local: true} = user) do
874 unread_query =
875 Participation.unread_conversation_count_for_user(user)
876 |> where([p], p.conversation_id == ^conversation.id)
877
878 User
879 |> join(:inner, [u], p in subquery(unread_query))
880 |> update([u, p],
881 set: [
882 info:
883 fragment(
884 "jsonb_set(?, '{unread_conversation_count}', (coalesce((?->>'unread_conversation_count')::int, 0) + 1)::varchar::jsonb, true)",
885 u.info,
886 u.info
887 )
888 ]
889 )
890 |> where([u], u.id == ^user.id)
891 |> where([u, p], p.count == 0)
892 |> select([u], u)
893 |> Repo.update_all([])
894 |> case do
895 {1, [user]} -> set_cache(user)
896 _ -> {:error, user}
897 end
898 end
899
900 def increment_unread_conversation_count(_, _), do: :noop
901
902 def remove_duplicated_following(%User{following: following} = user) do
903 uniq_following = Enum.uniq(following)
904
905 if length(following) == length(uniq_following) do
906 {:ok, user}
907 else
908 user
909 |> update_changeset(%{following: uniq_following})
910 |> update_and_set_cache()
911 end
912 end
913
914 @spec get_users_from_set([String.t()], boolean()) :: [User.t()]
915 def get_users_from_set(ap_ids, local_only \\ true) do
916 criteria = %{ap_id: ap_ids, deactivated: false}
917 criteria = if local_only, do: Map.put(criteria, :local, true), else: criteria
918
919 User.Query.build(criteria)
920 |> Repo.all()
921 end
922
923 @spec get_recipients_from_activity(Activity.t()) :: [User.t()]
924 def get_recipients_from_activity(%Activity{recipients: to}) do
925 User.Query.build(%{recipients_from_activity: to, local: true, deactivated: false})
926 |> Repo.all()
927 end
928
929 @spec mute(User.t(), User.t(), boolean()) :: {:ok, User.t()} | {:error, String.t()}
930 def mute(muter, %User{ap_id: ap_id}, notifications? \\ true) do
931 update_info(muter, &User.Info.add_to_mutes(&1, ap_id, notifications?))
932 end
933
934 def unmute(muter, %{ap_id: ap_id}) do
935 update_info(muter, &User.Info.remove_from_mutes(&1, ap_id))
936 end
937
938 def subscribe(subscriber, %{ap_id: ap_id}) do
939 with %User{} = subscribed <- get_cached_by_ap_id(ap_id) do
940 deny_follow_blocked = Pleroma.Config.get([:user, :deny_follow_blocked])
941
942 if blocks?(subscribed, subscriber) and deny_follow_blocked do
943 {:error, "Could not subscribe: #{subscribed.nickname} is blocking you"}
944 else
945 update_info(subscribed, &User.Info.add_to_subscribers(&1, subscriber.ap_id))
946 end
947 end
948 end
949
950 def unsubscribe(unsubscriber, %{ap_id: ap_id}) do
951 with %User{} = user <- get_cached_by_ap_id(ap_id) do
952 update_info(user, &User.Info.remove_from_subscribers(&1, unsubscriber.ap_id))
953 end
954 end
955
956 def block(blocker, %User{ap_id: ap_id} = blocked) do
957 # sever any follow relationships to prevent leaks per activitypub (Pleroma issue #213)
958 blocker =
959 if following?(blocker, blocked) do
960 {:ok, blocker, _} = unfollow(blocker, blocked)
961 blocker
962 else
963 blocker
964 end
965
966 # clear any requested follows as well
967 blocked =
968 case CommonAPI.reject_follow_request(blocked, blocker) do
969 {:ok, %User{} = updated_blocked} -> updated_blocked
970 nil -> blocked
971 end
972
973 blocker =
974 if subscribed_to?(blocked, blocker) do
975 {:ok, blocker} = unsubscribe(blocked, blocker)
976 blocker
977 else
978 blocker
979 end
980
981 if following?(blocked, blocker), do: unfollow(blocked, blocker)
982
983 {:ok, blocker} = update_follower_count(blocker)
984
985 update_info(blocker, &User.Info.add_to_block(&1, ap_id))
986 end
987
988 # helper to handle the block given only an actor's AP id
989 def block(blocker, %{ap_id: ap_id}) do
990 block(blocker, get_cached_by_ap_id(ap_id))
991 end
992
993 def unblock(blocker, %{ap_id: ap_id}) do
994 update_info(blocker, &User.Info.remove_from_block(&1, ap_id))
995 end
996
997 def mutes?(nil, _), do: false
998 def mutes?(user, %{ap_id: ap_id}), do: Enum.member?(user.info.mutes, ap_id)
999
1000 @spec muted_notifications?(User.t() | nil, User.t() | map()) :: boolean()
1001 def muted_notifications?(nil, _), do: false
1002
1003 def muted_notifications?(user, %{ap_id: ap_id}),
1004 do: Enum.member?(user.info.muted_notifications, ap_id)
1005
1006 def blocks?(%User{} = user, %User{} = target) do
1007 blocks_ap_id?(user, target) || blocks_domain?(user, target)
1008 end
1009
1010 def blocks?(nil, _), do: false
1011
1012 def blocks_ap_id?(%User{} = user, %User{} = target) do
1013 Enum.member?(user.info.blocks, target.ap_id)
1014 end
1015
1016 def blocks_ap_id?(_, _), do: false
1017
1018 def blocks_domain?(%User{} = user, %User{} = target) do
1019 domain_blocks = Pleroma.Web.ActivityPub.MRF.subdomains_regex(user.info.domain_blocks)
1020 %{host: host} = URI.parse(target.ap_id)
1021 Pleroma.Web.ActivityPub.MRF.subdomain_match?(domain_blocks, host)
1022 end
1023
1024 def blocks_domain?(_, _), do: false
1025
1026 def subscribed_to?(user, %{ap_id: ap_id}) do
1027 with %User{} = target <- get_cached_by_ap_id(ap_id) do
1028 Enum.member?(target.info.subscribers, user.ap_id)
1029 end
1030 end
1031
1032 @spec muted_users(User.t()) :: [User.t()]
1033 def muted_users(user) do
1034 User.Query.build(%{ap_id: user.info.mutes, deactivated: false})
1035 |> Repo.all()
1036 end
1037
1038 @spec blocked_users(User.t()) :: [User.t()]
1039 def blocked_users(user) do
1040 User.Query.build(%{ap_id: user.info.blocks, deactivated: false})
1041 |> Repo.all()
1042 end
1043
1044 @spec subscribers(User.t()) :: [User.t()]
1045 def subscribers(user) do
1046 User.Query.build(%{ap_id: user.info.subscribers, deactivated: false})
1047 |> Repo.all()
1048 end
1049
1050 def block_domain(user, domain) do
1051 update_info(user, &User.Info.add_to_domain_block(&1, domain))
1052 end
1053
1054 def unblock_domain(user, domain) do
1055 update_info(user, &User.Info.remove_from_domain_block(&1, domain))
1056 end
1057
1058 def deactivate_async(user, status \\ true) do
1059 BackgroundWorker.enqueue("deactivate_user", %{"user_id" => user.id, "status" => status})
1060 end
1061
1062 def deactivate(user, status \\ true)
1063
1064 def deactivate(users, status) when is_list(users) do
1065 Repo.transaction(fn ->
1066 for user <- users, do: deactivate(user, status)
1067 end)
1068 end
1069
1070 def deactivate(%User{} = user, status) do
1071 with {:ok, user} <- update_info(user, &User.Info.set_activation_status(&1, status)) do
1072 Enum.each(get_followers(user), &invalidate_cache/1)
1073 Enum.each(get_friends(user), &update_follower_count/1)
1074
1075 {:ok, user}
1076 end
1077 end
1078
1079 def update_notification_settings(%User{} = user, settings \\ %{}) do
1080 update_info(user, &User.Info.update_notification_settings(&1, settings))
1081 end
1082
1083 def delete(users) when is_list(users) do
1084 for user <- users, do: delete(user)
1085 end
1086
1087 def delete(%User{} = user) do
1088 BackgroundWorker.enqueue("delete_user", %{"user_id" => user.id})
1089 end
1090
1091 def perform(:force_password_reset, user), do: force_password_reset(user)
1092
1093 @spec perform(atom(), User.t()) :: {:ok, User.t()}
1094 def perform(:delete, %User{} = user) do
1095 {:ok, _user} = ActivityPub.delete(user)
1096
1097 # Remove all relationships
1098 user
1099 |> get_followers()
1100 |> Enum.each(fn follower ->
1101 ActivityPub.unfollow(follower, user)
1102 unfollow(follower, user)
1103 end)
1104
1105 user
1106 |> get_friends()
1107 |> Enum.each(fn followed ->
1108 ActivityPub.unfollow(user, followed)
1109 unfollow(user, followed)
1110 end)
1111
1112 delete_user_activities(user)
1113 invalidate_cache(user)
1114 Repo.delete(user)
1115 end
1116
1117 @spec perform(atom(), User.t()) :: {:ok, User.t()}
1118 def perform(:fetch_initial_posts, %User{} = user) do
1119 pages = Pleroma.Config.get!([:fetch_initial_posts, :pages])
1120
1121 # Insert all the posts in reverse order, so they're in the right order on the timeline
1122 user.info.source_data["outbox"]
1123 |> Utils.fetch_ordered_collection(pages)
1124 |> Enum.reverse()
1125 |> Enum.each(&Pleroma.Web.Federator.incoming_ap_doc/1)
1126 end
1127
1128 def perform(:deactivate_async, user, status), do: deactivate(user, status)
1129
1130 @spec perform(atom(), User.t(), list()) :: list() | {:error, any()}
1131 def perform(:blocks_import, %User{} = blocker, blocked_identifiers)
1132 when is_list(blocked_identifiers) do
1133 Enum.map(
1134 blocked_identifiers,
1135 fn blocked_identifier ->
1136 with {:ok, %User{} = blocked} <- get_or_fetch(blocked_identifier),
1137 {:ok, blocker} <- block(blocker, blocked),
1138 {:ok, _} <- ActivityPub.block(blocker, blocked) do
1139 blocked
1140 else
1141 err ->
1142 Logger.debug("blocks_import failed for #{blocked_identifier} with: #{inspect(err)}")
1143 err
1144 end
1145 end
1146 )
1147 end
1148
1149 @spec perform(atom(), User.t(), list()) :: list() | {:error, any()}
1150 def perform(:follow_import, %User{} = follower, followed_identifiers)
1151 when is_list(followed_identifiers) do
1152 Enum.map(
1153 followed_identifiers,
1154 fn followed_identifier ->
1155 with {:ok, %User{} = followed} <- get_or_fetch(followed_identifier),
1156 {:ok, follower} <- maybe_direct_follow(follower, followed),
1157 {:ok, _} <- ActivityPub.follow(follower, followed) do
1158 followed
1159 else
1160 err ->
1161 Logger.debug("follow_import failed for #{followed_identifier} with: #{inspect(err)}")
1162 err
1163 end
1164 end
1165 )
1166 end
1167
1168 @spec external_users_query() :: Ecto.Query.t()
1169 def external_users_query do
1170 User.Query.build(%{
1171 external: true,
1172 active: true,
1173 order_by: :id
1174 })
1175 end
1176
1177 @spec external_users(keyword()) :: [User.t()]
1178 def external_users(opts \\ []) do
1179 query =
1180 external_users_query()
1181 |> select([u], struct(u, [:id, :ap_id, :info]))
1182
1183 query =
1184 if opts[:max_id],
1185 do: where(query, [u], u.id > ^opts[:max_id]),
1186 else: query
1187
1188 query =
1189 if opts[:limit],
1190 do: limit(query, ^opts[:limit]),
1191 else: query
1192
1193 Repo.all(query)
1194 end
1195
1196 def blocks_import(%User{} = blocker, blocked_identifiers) when is_list(blocked_identifiers) do
1197 BackgroundWorker.enqueue("blocks_import", %{
1198 "blocker_id" => blocker.id,
1199 "blocked_identifiers" => blocked_identifiers
1200 })
1201 end
1202
1203 def follow_import(%User{} = follower, followed_identifiers)
1204 when is_list(followed_identifiers) do
1205 BackgroundWorker.enqueue("follow_import", %{
1206 "follower_id" => follower.id,
1207 "followed_identifiers" => followed_identifiers
1208 })
1209 end
1210
1211 def delete_user_activities(%User{ap_id: ap_id}) do
1212 ap_id
1213 |> Activity.Queries.by_actor()
1214 |> RepoStreamer.chunk_stream(50)
1215 |> Stream.each(fn activities -> Enum.each(activities, &delete_activity/1) end)
1216 |> Stream.run()
1217 end
1218
1219 defp delete_activity(%{data: %{"type" => "Create"}} = activity) do
1220 activity
1221 |> Object.normalize()
1222 |> ActivityPub.delete()
1223 end
1224
1225 defp delete_activity(%{data: %{"type" => "Like"}} = activity) do
1226 object = Object.normalize(activity)
1227
1228 activity.actor
1229 |> get_cached_by_ap_id()
1230 |> ActivityPub.unlike(object)
1231 end
1232
1233 defp delete_activity(%{data: %{"type" => "Announce"}} = activity) do
1234 object = Object.normalize(activity)
1235
1236 activity.actor
1237 |> get_cached_by_ap_id()
1238 |> ActivityPub.unannounce(object)
1239 end
1240
1241 defp delete_activity(_activity), do: "Doing nothing"
1242
1243 def html_filter_policy(%User{info: %{no_rich_text: true}}) do
1244 Pleroma.HTML.Scrubber.TwitterText
1245 end
1246
1247 def html_filter_policy(_), do: Pleroma.Config.get([:markup, :scrub_policy])
1248
1249 def fetch_by_ap_id(ap_id) do
1250 case ActivityPub.make_user_from_ap_id(ap_id) do
1251 {:ok, user} ->
1252 {:ok, user}
1253
1254 _ ->
1255 case OStatus.make_user(ap_id) do
1256 {:ok, user} -> {:ok, user}
1257 _ -> {:error, "Could not fetch by AP id"}
1258 end
1259 end
1260 end
1261
1262 def get_or_fetch_by_ap_id(ap_id) do
1263 user = get_cached_by_ap_id(ap_id)
1264
1265 if !is_nil(user) and !needs_update?(user) do
1266 {:ok, user}
1267 else
1268 # Whether to fetch initial posts for the user (if it's a new user & the fetching is enabled)
1269 should_fetch_initial = is_nil(user) and Pleroma.Config.get([:fetch_initial_posts, :enabled])
1270
1271 resp = fetch_by_ap_id(ap_id)
1272
1273 if should_fetch_initial do
1274 with {:ok, %User{} = user} <- resp do
1275 fetch_initial_posts(user)
1276 end
1277 end
1278
1279 resp
1280 end
1281 end
1282
1283 @doc "Creates an internal service actor by URI if missing. Optionally takes nickname for addressing."
1284 def get_or_create_service_actor_by_ap_id(uri, nickname \\ nil) do
1285 with %User{} = user <- get_cached_by_ap_id(uri) do
1286 user
1287 else
1288 _ ->
1289 {:ok, user} =
1290 %User{info: %User.Info{}}
1291 |> cast(%{}, [:ap_id, :nickname, :local])
1292 |> put_change(:ap_id, uri)
1293 |> put_change(:nickname, nickname)
1294 |> put_change(:local, true)
1295 |> put_change(:follower_address, uri <> "/followers")
1296 |> Repo.insert()
1297
1298 user
1299 end
1300 end
1301
1302 # AP style
1303 def public_key_from_info(%{
1304 source_data: %{"publicKey" => %{"publicKeyPem" => public_key_pem}}
1305 }) do
1306 key =
1307 public_key_pem
1308 |> :public_key.pem_decode()
1309 |> hd()
1310 |> :public_key.pem_entry_decode()
1311
1312 {:ok, key}
1313 end
1314
1315 # OStatus Magic Key
1316 def public_key_from_info(%{magic_key: magic_key}) when not is_nil(magic_key) do
1317 {:ok, Pleroma.Web.Salmon.decode_key(magic_key)}
1318 end
1319
1320 def public_key_from_info(_), do: {:error, "not found key"}
1321
1322 def get_public_key_for_ap_id(ap_id) do
1323 with {:ok, %User{} = user} <- get_or_fetch_by_ap_id(ap_id),
1324 {:ok, public_key} <- public_key_from_info(user.info) do
1325 {:ok, public_key}
1326 else
1327 _ -> :error
1328 end
1329 end
1330
1331 defp blank?(""), do: nil
1332 defp blank?(n), do: n
1333
1334 def insert_or_update_user(data) do
1335 data
1336 |> Map.put(:name, blank?(data[:name]) || data[:nickname])
1337 |> remote_user_creation()
1338 |> Repo.insert(on_conflict: :replace_all_except_primary_key, conflict_target: :nickname)
1339 |> set_cache()
1340 end
1341
1342 def ap_enabled?(%User{local: true}), do: true
1343 def ap_enabled?(%User{info: info}), do: info.ap_enabled
1344 def ap_enabled?(_), do: false
1345
1346 @doc "Gets or fetch a user by uri or nickname."
1347 @spec get_or_fetch(String.t()) :: {:ok, User.t()} | {:error, String.t()}
1348 def get_or_fetch("http" <> _host = uri), do: get_or_fetch_by_ap_id(uri)
1349 def get_or_fetch(nickname), do: get_or_fetch_by_nickname(nickname)
1350
1351 # wait a period of time and return newest version of the User structs
1352 # this is because we have synchronous follow APIs and need to simulate them
1353 # with an async handshake
1354 def wait_and_refresh(_, %User{local: true} = a, %User{local: true} = b) do
1355 with %User{} = a <- get_cached_by_id(a.id),
1356 %User{} = b <- get_cached_by_id(b.id) do
1357 {:ok, a, b}
1358 else
1359 nil -> :error
1360 end
1361 end
1362
1363 def wait_and_refresh(timeout, %User{} = a, %User{} = b) do
1364 with :ok <- :timer.sleep(timeout),
1365 %User{} = a <- get_cached_by_id(a.id),
1366 %User{} = b <- get_cached_by_id(b.id) do
1367 {:ok, a, b}
1368 else
1369 nil -> :error
1370 end
1371 end
1372
1373 def parse_bio(bio) when is_binary(bio) and bio != "" do
1374 bio
1375 |> CommonUtils.format_input("text/plain", mentions_format: :full)
1376 |> elem(0)
1377 end
1378
1379 def parse_bio(_), do: ""
1380
1381 def parse_bio(bio, user) when is_binary(bio) and bio != "" do
1382 # TODO: get profile URLs other than user.ap_id
1383 profile_urls = [user.ap_id]
1384
1385 bio
1386 |> CommonUtils.format_input("text/plain",
1387 mentions_format: :full,
1388 rel: &RelMe.maybe_put_rel_me(&1, profile_urls)
1389 )
1390 |> elem(0)
1391 end
1392
1393 def parse_bio(_, _), do: ""
1394
1395 def tag(user_identifiers, tags) when is_list(user_identifiers) do
1396 Repo.transaction(fn ->
1397 for user_identifier <- user_identifiers, do: tag(user_identifier, tags)
1398 end)
1399 end
1400
1401 def tag(nickname, tags) when is_binary(nickname),
1402 do: tag(get_by_nickname(nickname), tags)
1403
1404 def tag(%User{} = user, tags),
1405 do: update_tags(user, Enum.uniq((user.tags || []) ++ normalize_tags(tags)))
1406
1407 def untag(user_identifiers, tags) when is_list(user_identifiers) do
1408 Repo.transaction(fn ->
1409 for user_identifier <- user_identifiers, do: untag(user_identifier, tags)
1410 end)
1411 end
1412
1413 def untag(nickname, tags) when is_binary(nickname),
1414 do: untag(get_by_nickname(nickname), tags)
1415
1416 def untag(%User{} = user, tags),
1417 do: update_tags(user, (user.tags || []) -- normalize_tags(tags))
1418
1419 defp update_tags(%User{} = user, new_tags) do
1420 {:ok, updated_user} =
1421 user
1422 |> change(%{tags: new_tags})
1423 |> update_and_set_cache()
1424
1425 updated_user
1426 end
1427
1428 defp normalize_tags(tags) do
1429 [tags]
1430 |> List.flatten()
1431 |> Enum.map(&String.downcase/1)
1432 end
1433
1434 defp local_nickname_regex do
1435 if Pleroma.Config.get([:instance, :extended_nickname_format]) do
1436 @extended_local_nickname_regex
1437 else
1438 @strict_local_nickname_regex
1439 end
1440 end
1441
1442 def local_nickname(nickname_or_mention) do
1443 nickname_or_mention
1444 |> full_nickname()
1445 |> String.split("@")
1446 |> hd()
1447 end
1448
1449 def full_nickname(nickname_or_mention),
1450 do: String.trim_leading(nickname_or_mention, "@")
1451
1452 def error_user(ap_id) do
1453 %User{
1454 name: ap_id,
1455 ap_id: ap_id,
1456 info: %User.Info{},
1457 nickname: "erroruser@example.com",
1458 inserted_at: NaiveDateTime.utc_now()
1459 }
1460 end
1461
1462 @spec all_superusers() :: [User.t()]
1463 def all_superusers do
1464 User.Query.build(%{super_users: true, local: true, deactivated: false})
1465 |> Repo.all()
1466 end
1467
1468 def showing_reblogs?(%User{} = user, %User{} = target) do
1469 target.ap_id not in user.info.muted_reblogs
1470 end
1471
1472 @doc """
1473 The function returns a query to get users with no activity for given interval of days.
1474 Inactive users are those who didn't read any notification, or had any activity where
1475 the user is the activity's actor, during `inactivity_threshold` days.
1476 Deactivated users will not appear in this list.
1477
1478 ## Examples
1479
1480 iex> Pleroma.User.list_inactive_users()
1481 %Ecto.Query{}
1482 """
1483 @spec list_inactive_users_query(integer()) :: Ecto.Query.t()
1484 def list_inactive_users_query(inactivity_threshold \\ 7) do
1485 negative_inactivity_threshold = -inactivity_threshold
1486 now = NaiveDateTime.truncate(NaiveDateTime.utc_now(), :second)
1487 # Subqueries are not supported in `where` clauses, join gets too complicated.
1488 has_read_notifications =
1489 from(n in Pleroma.Notification,
1490 where: n.seen == true,
1491 group_by: n.id,
1492 having: max(n.updated_at) > datetime_add(^now, ^negative_inactivity_threshold, "day"),
1493 select: n.user_id
1494 )
1495 |> Pleroma.Repo.all()
1496
1497 from(u in Pleroma.User,
1498 left_join: a in Pleroma.Activity,
1499 on: u.ap_id == a.actor,
1500 where: not is_nil(u.nickname),
1501 where: fragment("not (?->'deactivated' @> 'true')", u.info),
1502 where: u.id not in ^has_read_notifications,
1503 group_by: u.id,
1504 having:
1505 max(a.inserted_at) < datetime_add(^now, ^negative_inactivity_threshold, "day") or
1506 is_nil(max(a.inserted_at))
1507 )
1508 end
1509
1510 @doc """
1511 Enable or disable email notifications for user
1512
1513 ## Examples
1514
1515 iex> Pleroma.User.switch_email_notifications(Pleroma.User{info: %{email_notifications: %{"digest" => false}}}, "digest", true)
1516 Pleroma.User{info: %{email_notifications: %{"digest" => true}}}
1517
1518 iex> Pleroma.User.switch_email_notifications(Pleroma.User{info: %{email_notifications: %{"digest" => true}}}, "digest", false)
1519 Pleroma.User{info: %{email_notifications: %{"digest" => false}}}
1520 """
1521 @spec switch_email_notifications(t(), String.t(), boolean()) ::
1522 {:ok, t()} | {:error, Ecto.Changeset.t()}
1523 def switch_email_notifications(user, type, status) do
1524 update_info(user, &User.Info.update_email_notifications(&1, %{type => status}))
1525 end
1526
1527 @doc """
1528 Set `last_digest_emailed_at` value for the user to current time
1529 """
1530 @spec touch_last_digest_emailed_at(t()) :: t()
1531 def touch_last_digest_emailed_at(user) do
1532 now = NaiveDateTime.truncate(NaiveDateTime.utc_now(), :second)
1533
1534 {:ok, updated_user} =
1535 user
1536 |> change(%{last_digest_emailed_at: now})
1537 |> update_and_set_cache()
1538
1539 updated_user
1540 end
1541
1542 @spec toggle_confirmation(User.t()) :: {:ok, User.t()} | {:error, Changeset.t()}
1543 def toggle_confirmation(%User{} = user) do
1544 need_confirmation? = !user.info.confirmation_pending
1545
1546 user
1547 |> update_info(&User.Info.confirmation_changeset(&1, need_confirmation: need_confirmation?))
1548 end
1549
1550 def get_mascot(%{info: %{mascot: %{} = mascot}}) when not is_nil(mascot) do
1551 mascot
1552 end
1553
1554 def get_mascot(%{info: %{mascot: mascot}}) when is_nil(mascot) do
1555 # use instance-default
1556 config = Pleroma.Config.get([:assets, :mascots])
1557 default_mascot = Pleroma.Config.get([:assets, :default_mascot])
1558 mascot = Keyword.get(config, default_mascot)
1559
1560 %{
1561 "id" => "default-mascot",
1562 "url" => mascot[:url],
1563 "preview_url" => mascot[:url],
1564 "pleroma" => %{
1565 "mime_type" => mascot[:mime_type]
1566 }
1567 }
1568 end
1569
1570 def ensure_keys_present(%{keys: keys} = user) when not is_nil(keys), do: {:ok, user}
1571
1572 def ensure_keys_present(%User{} = user) do
1573 with {:ok, pem} <- Keys.generate_rsa_pem() do
1574 user
1575 |> cast(%{keys: pem}, [:keys])
1576 |> validate_required([:keys])
1577 |> update_and_set_cache()
1578 end
1579 end
1580
1581 def get_ap_ids_by_nicknames(nicknames) do
1582 from(u in User,
1583 where: u.nickname in ^nicknames,
1584 select: u.ap_id
1585 )
1586 |> Repo.all()
1587 end
1588
1589 defdelegate search(query, opts \\ []), to: User.Search
1590
1591 defp put_password_hash(
1592 %Ecto.Changeset{valid?: true, changes: %{password: password}} = changeset
1593 ) do
1594 change(changeset, password_hash: Pbkdf2.hashpwsalt(password))
1595 end
1596
1597 defp put_password_hash(changeset), do: changeset
1598
1599 def is_internal_user?(%User{nickname: nil}), do: true
1600 def is_internal_user?(%User{local: true, nickname: "internal." <> _}), do: true
1601 def is_internal_user?(_), do: false
1602
1603 # A hack because user delete activities have a fake id for whatever reason
1604 # TODO: Get rid of this
1605 def get_delivered_users_by_object_id("pleroma:fake_object_id"), do: []
1606
1607 def get_delivered_users_by_object_id(object_id) do
1608 from(u in User,
1609 inner_join: delivery in assoc(u, :deliveries),
1610 where: delivery.object_id == ^object_id
1611 )
1612 |> Repo.all()
1613 end
1614
1615 def change_email(user, email) do
1616 user
1617 |> cast(%{email: email}, [:email])
1618 |> validate_required([:email])
1619 |> unique_constraint(:email)
1620 |> validate_format(:email, @email_regex)
1621 |> update_and_set_cache()
1622 end
1623
1624 @doc """
1625 Changes `user.info` and returns the user changeset.
1626
1627 `fun` is called with the `user.info`.
1628 """
1629 def change_info(user, fun) do
1630 changeset = change(user)
1631 info = get_field(changeset, :info) || %User.Info{}
1632 put_embed(changeset, :info, fun.(info))
1633 end
1634
1635 @doc """
1636 Updates `user.info` and sets cache.
1637
1638 `fun` is called with the `user.info`.
1639 """
1640 def update_info(users, fun) when is_list(users) do
1641 Repo.transaction(fn ->
1642 for user <- users, do: update_info(user, fun)
1643 end)
1644 end
1645
1646 def update_info(user, fun) do
1647 user
1648 |> change_info(fun)
1649 |> update_and_set_cache()
1650 end
1651 end