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