Merge remote-tracking branch 'origin/develop' into benchmark-finishing
[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 "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 "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 "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{} = user, status \\ true) do
1065 with {:ok, user} <- update_info(user, &User.Info.set_activation_status(&1, status)) do
1066 Enum.each(get_followers(user), &invalidate_cache/1)
1067 Enum.each(get_friends(user), &update_follower_count/1)
1068
1069 {:ok, user}
1070 end
1071 end
1072
1073 def update_notification_settings(%User{} = user, settings \\ %{}) do
1074 update_info(user, &User.Info.update_notification_settings(&1, settings))
1075 end
1076
1077 def delete(%User{} = user) do
1078 BackgroundWorker.enqueue("delete_user", %{"user_id" => user.id})
1079 end
1080
1081 def perform(:force_password_reset, user), do: force_password_reset(user)
1082
1083 @spec perform(atom(), User.t()) :: {:ok, User.t()}
1084 def perform(:delete, %User{} = user) do
1085 {:ok, _user} = ActivityPub.delete(user)
1086
1087 # Remove all relationships
1088 user
1089 |> get_followers()
1090 |> Enum.each(fn follower ->
1091 ActivityPub.unfollow(follower, user)
1092 unfollow(follower, user)
1093 end)
1094
1095 user
1096 |> get_friends()
1097 |> Enum.each(fn followed ->
1098 ActivityPub.unfollow(user, followed)
1099 unfollow(user, followed)
1100 end)
1101
1102 delete_user_activities(user)
1103 invalidate_cache(user)
1104 Repo.delete(user)
1105 end
1106
1107 @spec perform(atom(), User.t()) :: {:ok, User.t()}
1108 def perform(:fetch_initial_posts, %User{} = user) do
1109 pages = Pleroma.Config.get!([:fetch_initial_posts, :pages])
1110
1111 # Insert all the posts in reverse order, so they're in the right order on the timeline
1112 user.info.source_data["outbox"]
1113 |> Utils.fetch_ordered_collection(pages)
1114 |> Enum.reverse()
1115 |> Enum.each(&Pleroma.Web.Federator.incoming_ap_doc/1)
1116 end
1117
1118 def perform(:deactivate_async, user, status), do: deactivate(user, status)
1119
1120 @spec perform(atom(), User.t(), list()) :: list() | {:error, any()}
1121 def perform(:blocks_import, %User{} = blocker, blocked_identifiers)
1122 when is_list(blocked_identifiers) do
1123 Enum.map(
1124 blocked_identifiers,
1125 fn blocked_identifier ->
1126 with {:ok, %User{} = blocked} <- get_or_fetch(blocked_identifier),
1127 {:ok, blocker} <- block(blocker, blocked),
1128 {:ok, _} <- ActivityPub.block(blocker, blocked) do
1129 blocked
1130 else
1131 err ->
1132 Logger.debug("blocks_import failed for #{blocked_identifier} with: #{inspect(err)}")
1133 err
1134 end
1135 end
1136 )
1137 end
1138
1139 @spec perform(atom(), User.t(), list()) :: list() | {:error, any()}
1140 def perform(:follow_import, %User{} = follower, followed_identifiers)
1141 when is_list(followed_identifiers) do
1142 Enum.map(
1143 followed_identifiers,
1144 fn followed_identifier ->
1145 with {:ok, %User{} = followed} <- get_or_fetch(followed_identifier),
1146 {:ok, follower} <- maybe_direct_follow(follower, followed),
1147 {:ok, _} <- ActivityPub.follow(follower, followed) do
1148 followed
1149 else
1150 err ->
1151 Logger.debug("follow_import failed for #{followed_identifier} with: #{inspect(err)}")
1152 err
1153 end
1154 end
1155 )
1156 end
1157
1158 @spec external_users_query() :: Ecto.Query.t()
1159 def external_users_query do
1160 User.Query.build(%{
1161 external: true,
1162 active: true,
1163 order_by: :id
1164 })
1165 end
1166
1167 @spec external_users(keyword()) :: [User.t()]
1168 def external_users(opts \\ []) do
1169 query =
1170 external_users_query()
1171 |> select([u], struct(u, [:id, :ap_id, :info]))
1172
1173 query =
1174 if opts[:max_id],
1175 do: where(query, [u], u.id > ^opts[:max_id]),
1176 else: query
1177
1178 query =
1179 if opts[:limit],
1180 do: limit(query, ^opts[:limit]),
1181 else: query
1182
1183 Repo.all(query)
1184 end
1185
1186 def blocks_import(%User{} = blocker, blocked_identifiers) when is_list(blocked_identifiers) do
1187 BackgroundWorker.enqueue("blocks_import", %{
1188 "blocker_id" => blocker.id,
1189 "blocked_identifiers" => blocked_identifiers
1190 })
1191 end
1192
1193 def follow_import(%User{} = follower, followed_identifiers)
1194 when is_list(followed_identifiers) do
1195 BackgroundWorker.enqueue("follow_import", %{
1196 "follower_id" => follower.id,
1197 "followed_identifiers" => followed_identifiers
1198 })
1199 end
1200
1201 def delete_user_activities(%User{ap_id: ap_id}) do
1202 ap_id
1203 |> Activity.Queries.by_actor()
1204 |> RepoStreamer.chunk_stream(50)
1205 |> Stream.each(fn activities -> Enum.each(activities, &delete_activity/1) end)
1206 |> Stream.run()
1207 end
1208
1209 defp delete_activity(%{data: %{"type" => "Create"}} = activity) do
1210 activity
1211 |> Object.normalize()
1212 |> ActivityPub.delete()
1213 end
1214
1215 defp delete_activity(%{data: %{"type" => "Like"}} = activity) do
1216 object = Object.normalize(activity)
1217
1218 activity.actor
1219 |> get_cached_by_ap_id()
1220 |> ActivityPub.unlike(object)
1221 end
1222
1223 defp delete_activity(%{data: %{"type" => "Announce"}} = activity) do
1224 object = Object.normalize(activity)
1225
1226 activity.actor
1227 |> get_cached_by_ap_id()
1228 |> ActivityPub.unannounce(object)
1229 end
1230
1231 defp delete_activity(_activity), do: "Doing nothing"
1232
1233 def html_filter_policy(%User{info: %{no_rich_text: true}}) do
1234 Pleroma.HTML.Scrubber.TwitterText
1235 end
1236
1237 def html_filter_policy(_), do: Pleroma.Config.get([:markup, :scrub_policy])
1238
1239 def fetch_by_ap_id(ap_id) do
1240 case ActivityPub.make_user_from_ap_id(ap_id) do
1241 {:ok, user} ->
1242 {:ok, user}
1243
1244 _ ->
1245 case OStatus.make_user(ap_id) do
1246 {:ok, user} -> {:ok, user}
1247 _ -> {:error, "Could not fetch by AP id"}
1248 end
1249 end
1250 end
1251
1252 def get_or_fetch_by_ap_id(ap_id) do
1253 user = get_cached_by_ap_id(ap_id)
1254
1255 if !is_nil(user) and !needs_update?(user) do
1256 {:ok, user}
1257 else
1258 # Whether to fetch initial posts for the user (if it's a new user & the fetching is enabled)
1259 should_fetch_initial = is_nil(user) and Pleroma.Config.get([:fetch_initial_posts, :enabled])
1260
1261 resp = fetch_by_ap_id(ap_id)
1262
1263 if should_fetch_initial do
1264 with {:ok, %User{} = user} <- resp do
1265 fetch_initial_posts(user)
1266 end
1267 end
1268
1269 resp
1270 end
1271 end
1272
1273 @doc "Creates an internal service actor by URI if missing. Optionally takes nickname for addressing."
1274 def get_or_create_service_actor_by_ap_id(uri, nickname \\ nil) do
1275 with %User{} = user <- get_cached_by_ap_id(uri) do
1276 user
1277 else
1278 _ ->
1279 {:ok, user} =
1280 %User{info: %User.Info{}}
1281 |> cast(%{}, [:ap_id, :nickname, :local])
1282 |> put_change(:ap_id, uri)
1283 |> put_change(:nickname, nickname)
1284 |> put_change(:local, true)
1285 |> put_change(:follower_address, uri <> "/followers")
1286 |> Repo.insert()
1287
1288 user
1289 end
1290 end
1291
1292 # AP style
1293 def public_key_from_info(%{
1294 source_data: %{"publicKey" => %{"publicKeyPem" => public_key_pem}}
1295 }) do
1296 key =
1297 public_key_pem
1298 |> :public_key.pem_decode()
1299 |> hd()
1300 |> :public_key.pem_entry_decode()
1301
1302 {:ok, key}
1303 end
1304
1305 # OStatus Magic Key
1306 def public_key_from_info(%{magic_key: magic_key}) when not is_nil(magic_key) do
1307 {:ok, Pleroma.Web.Salmon.decode_key(magic_key)}
1308 end
1309
1310 def public_key_from_info(_), do: {:error, "not found key"}
1311
1312 def get_public_key_for_ap_id(ap_id) do
1313 with {:ok, %User{} = user} <- get_or_fetch_by_ap_id(ap_id),
1314 {:ok, public_key} <- public_key_from_info(user.info) do
1315 {:ok, public_key}
1316 else
1317 _ -> :error
1318 end
1319 end
1320
1321 defp blank?(""), do: nil
1322 defp blank?(n), do: n
1323
1324 def insert_or_update_user(data) do
1325 data
1326 |> Map.put(:name, blank?(data[:name]) || data[:nickname])
1327 |> remote_user_creation()
1328 |> Repo.insert(on_conflict: :replace_all_except_primary_key, conflict_target: :nickname)
1329 |> set_cache()
1330 end
1331
1332 def ap_enabled?(%User{local: true}), do: true
1333 def ap_enabled?(%User{info: info}), do: info.ap_enabled
1334 def ap_enabled?(_), do: false
1335
1336 @doc "Gets or fetch a user by uri or nickname."
1337 @spec get_or_fetch(String.t()) :: {:ok, User.t()} | {:error, String.t()}
1338 def get_or_fetch("http" <> _host = uri), do: get_or_fetch_by_ap_id(uri)
1339 def get_or_fetch(nickname), do: get_or_fetch_by_nickname(nickname)
1340
1341 # wait a period of time and return newest version of the User structs
1342 # this is because we have synchronous follow APIs and need to simulate them
1343 # with an async handshake
1344 def wait_and_refresh(_, %User{local: true} = a, %User{local: true} = b) do
1345 with %User{} = a <- get_cached_by_id(a.id),
1346 %User{} = b <- get_cached_by_id(b.id) do
1347 {:ok, a, b}
1348 else
1349 nil -> :error
1350 end
1351 end
1352
1353 def wait_and_refresh(timeout, %User{} = a, %User{} = b) do
1354 with :ok <- :timer.sleep(timeout),
1355 %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 parse_bio(bio) when is_binary(bio) and bio != "" do
1364 bio
1365 |> CommonUtils.format_input("text/plain", mentions_format: :full)
1366 |> elem(0)
1367 end
1368
1369 def parse_bio(_), do: ""
1370
1371 def parse_bio(bio, user) when is_binary(bio) and bio != "" do
1372 # TODO: get profile URLs other than user.ap_id
1373 profile_urls = [user.ap_id]
1374
1375 bio
1376 |> CommonUtils.format_input("text/plain",
1377 mentions_format: :full,
1378 rel: &RelMe.maybe_put_rel_me(&1, profile_urls)
1379 )
1380 |> elem(0)
1381 end
1382
1383 def parse_bio(_, _), do: ""
1384
1385 def tag(user_identifiers, tags) when is_list(user_identifiers) do
1386 Repo.transaction(fn ->
1387 for user_identifier <- user_identifiers, do: tag(user_identifier, tags)
1388 end)
1389 end
1390
1391 def tag(nickname, tags) when is_binary(nickname),
1392 do: tag(get_by_nickname(nickname), tags)
1393
1394 def tag(%User{} = user, tags),
1395 do: update_tags(user, Enum.uniq((user.tags || []) ++ normalize_tags(tags)))
1396
1397 def untag(user_identifiers, tags) when is_list(user_identifiers) do
1398 Repo.transaction(fn ->
1399 for user_identifier <- user_identifiers, do: untag(user_identifier, tags)
1400 end)
1401 end
1402
1403 def untag(nickname, tags) when is_binary(nickname),
1404 do: untag(get_by_nickname(nickname), tags)
1405
1406 def untag(%User{} = user, tags),
1407 do: update_tags(user, (user.tags || []) -- normalize_tags(tags))
1408
1409 defp update_tags(%User{} = user, new_tags) do
1410 {:ok, updated_user} =
1411 user
1412 |> change(%{tags: new_tags})
1413 |> update_and_set_cache()
1414
1415 updated_user
1416 end
1417
1418 defp normalize_tags(tags) do
1419 [tags]
1420 |> List.flatten()
1421 |> Enum.map(&String.downcase/1)
1422 end
1423
1424 defp local_nickname_regex do
1425 if Pleroma.Config.get([:instance, :extended_nickname_format]) do
1426 @extended_local_nickname_regex
1427 else
1428 @strict_local_nickname_regex
1429 end
1430 end
1431
1432 def local_nickname(nickname_or_mention) do
1433 nickname_or_mention
1434 |> full_nickname()
1435 |> String.split("@")
1436 |> hd()
1437 end
1438
1439 def full_nickname(nickname_or_mention),
1440 do: String.trim_leading(nickname_or_mention, "@")
1441
1442 def error_user(ap_id) do
1443 %User{
1444 name: ap_id,
1445 ap_id: ap_id,
1446 info: %User.Info{},
1447 nickname: "erroruser@example.com",
1448 inserted_at: NaiveDateTime.utc_now()
1449 }
1450 end
1451
1452 @spec all_superusers() :: [User.t()]
1453 def all_superusers do
1454 User.Query.build(%{super_users: true, local: true, deactivated: false})
1455 |> Repo.all()
1456 end
1457
1458 def showing_reblogs?(%User{} = user, %User{} = target) do
1459 target.ap_id not in user.info.muted_reblogs
1460 end
1461
1462 @doc """
1463 The function returns a query to get users with no activity for given interval of days.
1464 Inactive users are those who didn't read any notification, or had any activity where
1465 the user is the activity's actor, during `inactivity_threshold` days.
1466 Deactivated users will not appear in this list.
1467
1468 ## Examples
1469
1470 iex> Pleroma.User.list_inactive_users()
1471 %Ecto.Query{}
1472 """
1473 @spec list_inactive_users_query(integer()) :: Ecto.Query.t()
1474 def list_inactive_users_query(inactivity_threshold \\ 7) do
1475 negative_inactivity_threshold = -inactivity_threshold
1476 now = NaiveDateTime.truncate(NaiveDateTime.utc_now(), :second)
1477 # Subqueries are not supported in `where` clauses, join gets too complicated.
1478 has_read_notifications =
1479 from(n in Pleroma.Notification,
1480 where: n.seen == true,
1481 group_by: n.id,
1482 having: max(n.updated_at) > datetime_add(^now, ^negative_inactivity_threshold, "day"),
1483 select: n.user_id
1484 )
1485 |> Pleroma.Repo.all()
1486
1487 from(u in Pleroma.User,
1488 left_join: a in Pleroma.Activity,
1489 on: u.ap_id == a.actor,
1490 where: not is_nil(u.nickname),
1491 where: fragment("not (?->'deactivated' @> 'true')", u.info),
1492 where: u.id not in ^has_read_notifications,
1493 group_by: u.id,
1494 having:
1495 max(a.inserted_at) < datetime_add(^now, ^negative_inactivity_threshold, "day") or
1496 is_nil(max(a.inserted_at))
1497 )
1498 end
1499
1500 @doc """
1501 Enable or disable email notifications for user
1502
1503 ## Examples
1504
1505 iex> Pleroma.User.switch_email_notifications(Pleroma.User{info: %{email_notifications: %{"digest" => false}}}, "digest", true)
1506 Pleroma.User{info: %{email_notifications: %{"digest" => true}}}
1507
1508 iex> Pleroma.User.switch_email_notifications(Pleroma.User{info: %{email_notifications: %{"digest" => true}}}, "digest", false)
1509 Pleroma.User{info: %{email_notifications: %{"digest" => false}}}
1510 """
1511 @spec switch_email_notifications(t(), String.t(), boolean()) ::
1512 {:ok, t()} | {:error, Ecto.Changeset.t()}
1513 def switch_email_notifications(user, type, status) do
1514 update_info(user, &User.Info.update_email_notifications(&1, %{type => status}))
1515 end
1516
1517 @doc """
1518 Set `last_digest_emailed_at` value for the user to current time
1519 """
1520 @spec touch_last_digest_emailed_at(t()) :: t()
1521 def touch_last_digest_emailed_at(user) do
1522 now = NaiveDateTime.truncate(NaiveDateTime.utc_now(), :second)
1523
1524 {:ok, updated_user} =
1525 user
1526 |> change(%{last_digest_emailed_at: now})
1527 |> update_and_set_cache()
1528
1529 updated_user
1530 end
1531
1532 @spec toggle_confirmation(User.t()) :: {:ok, User.t()} | {:error, Changeset.t()}
1533 def toggle_confirmation(%User{} = user) do
1534 need_confirmation? = !user.info.confirmation_pending
1535
1536 user
1537 |> update_info(&User.Info.confirmation_changeset(&1, need_confirmation: need_confirmation?))
1538 end
1539
1540 def get_mascot(%{info: %{mascot: %{} = mascot}}) when not is_nil(mascot) do
1541 mascot
1542 end
1543
1544 def get_mascot(%{info: %{mascot: mascot}}) when is_nil(mascot) do
1545 # use instance-default
1546 config = Pleroma.Config.get([:assets, :mascots])
1547 default_mascot = Pleroma.Config.get([:assets, :default_mascot])
1548 mascot = Keyword.get(config, default_mascot)
1549
1550 %{
1551 "id" => "default-mascot",
1552 "url" => mascot[:url],
1553 "preview_url" => mascot[:url],
1554 "pleroma" => %{
1555 "mime_type" => mascot[:mime_type]
1556 }
1557 }
1558 end
1559
1560 def ensure_keys_present(%{keys: keys} = user) when not is_nil(keys), do: {:ok, user}
1561
1562 def ensure_keys_present(%User{} = user) do
1563 with {:ok, pem} <- Keys.generate_rsa_pem() do
1564 user
1565 |> cast(%{keys: pem}, [:keys])
1566 |> validate_required([:keys])
1567 |> update_and_set_cache()
1568 end
1569 end
1570
1571 def get_ap_ids_by_nicknames(nicknames) do
1572 from(u in User,
1573 where: u.nickname in ^nicknames,
1574 select: u.ap_id
1575 )
1576 |> Repo.all()
1577 end
1578
1579 defdelegate search(query, opts \\ []), to: User.Search
1580
1581 defp put_password_hash(
1582 %Ecto.Changeset{valid?: true, changes: %{password: password}} = changeset
1583 ) do
1584 change(changeset, password_hash: Pbkdf2.hashpwsalt(password))
1585 end
1586
1587 defp put_password_hash(changeset), do: changeset
1588
1589 def is_internal_user?(%User{nickname: nil}), do: true
1590 def is_internal_user?(%User{local: true, nickname: "internal." <> _}), do: true
1591 def is_internal_user?(_), do: false
1592
1593 # A hack because user delete activities have a fake id for whatever reason
1594 # TODO: Get rid of this
1595 def get_delivered_users_by_object_id("pleroma:fake_object_id"), do: []
1596
1597 def get_delivered_users_by_object_id(object_id) do
1598 from(u in User,
1599 inner_join: delivery in assoc(u, :deliveries),
1600 where: delivery.object_id == ^object_id
1601 )
1602 |> Repo.all()
1603 end
1604
1605 def change_email(user, email) do
1606 user
1607 |> cast(%{email: email}, [:email])
1608 |> validate_required([:email])
1609 |> unique_constraint(:email)
1610 |> validate_format(:email, @email_regex)
1611 |> update_and_set_cache()
1612 end
1613
1614 @doc """
1615 Changes `user.info` and returns the user changeset.
1616
1617 `fun` is called with the `user.info`.
1618 """
1619 def change_info(user, fun) do
1620 changeset = change(user)
1621 info = get_field(changeset, :info) || %User.Info{}
1622 put_embed(changeset, :info, fun.(info))
1623 end
1624
1625 @doc """
1626 Updates `user.info` and sets cache.
1627
1628 `fun` is called with the `user.info`.
1629 """
1630 def update_info(user, fun) do
1631 user
1632 |> change_info(fun)
1633 |> update_and_set_cache()
1634 end
1635 end