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