[#1149] Added Oban job for "activity_expiration". Merged remote-tracking branch ...
[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 alias Pleroma.Workers.BackgroundWorker
31
32 require Logger
33
34 @type t :: %__MODULE__{}
35
36 @primary_key {:id, Pleroma.FlakeId, autogenerate: true}
37
38 # credo:disable-for-next-line Credo.Check.Readability.MaxLineLength
39 @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])?)*$/
40
41 @strict_local_nickname_regex ~r/^[a-zA-Z\d]+$/
42 @extended_local_nickname_regex ~r/^[a-zA-Z\d_-]+$/
43
44 defdelegate worker_args(queue), to: Pleroma.Workers.Helper
45
46 schema "users" do
47 field(:bio, :string)
48 field(:email, :string)
49 field(:name, :string)
50 field(:nickname, :string)
51 field(:password_hash, :string)
52 field(:password, :string, virtual: true)
53 field(:password_confirmation, :string, virtual: true)
54 field(:following, {:array, :string}, default: [])
55 field(:ap_id, :string)
56 field(:avatar, :map)
57 field(:local, :boolean, default: true)
58 field(:follower_address, :string)
59 field(:following_address, :string)
60 field(:search_rank, :float, virtual: true)
61 field(:search_type, :integer, virtual: true)
62 field(:tags, {:array, :string}, default: [])
63 field(:last_refreshed_at, :naive_datetime_usec)
64 field(:last_digest_emailed_at, :naive_datetime)
65 has_many(:notifications, Notification)
66 has_many(:registrations, Registration)
67 embeds_one(:info, User.Info)
68
69 timestamps()
70 end
71
72 def auth_active?(%User{info: %User.Info{confirmation_pending: true}}),
73 do: !Pleroma.Config.get([:instance, :account_activation_required])
74
75 def auth_active?(%User{}), do: true
76
77 def visible_for?(user, for_user \\ nil)
78
79 def visible_for?(%User{id: user_id}, %User{id: for_id}) when user_id == for_id, do: true
80
81 def visible_for?(%User{} = user, for_user) do
82 auth_active?(user) || superuser?(for_user)
83 end
84
85 def visible_for?(_, _), do: false
86
87 def superuser?(%User{local: true, info: %User.Info{is_admin: true}}), do: true
88 def superuser?(%User{local: true, info: %User.Info{is_moderator: true}}), do: true
89 def superuser?(_), do: false
90
91 def avatar_url(user, options \\ []) do
92 case user.avatar do
93 %{"url" => [%{"href" => href} | _]} -> href
94 _ -> !options[:no_default] && "#{Web.base_url()}/images/avi.png"
95 end
96 end
97
98 def banner_url(user, options \\ []) do
99 case user.info.banner do
100 %{"url" => [%{"href" => href} | _]} -> href
101 _ -> !options[:no_default] && "#{Web.base_url()}/images/banner.png"
102 end
103 end
104
105 def profile_url(%User{info: %{source_data: %{"url" => url}}}), do: url
106 def profile_url(%User{ap_id: ap_id}), do: ap_id
107 def profile_url(_), do: nil
108
109 def ap_id(%User{nickname: nickname}) do
110 "#{Web.base_url()}/users/#{nickname}"
111 end
112
113 def ap_followers(%User{follower_address: fa}) when is_binary(fa), do: fa
114 def ap_followers(%User{} = user), do: "#{ap_id(user)}/followers"
115
116 @spec ap_following(User.t()) :: Sring.t()
117 def ap_following(%User{following_address: fa}) when is_binary(fa), do: fa
118 def ap_following(%User{} = user), do: "#{ap_id(user)}/following"
119
120 def user_info(%User{} = user, args \\ %{}) do
121 following_count =
122 if args[:following_count],
123 do: args[:following_count],
124 else: user.info.following_count || following_count(user)
125
126 follower_count =
127 if args[:follower_count], do: args[:follower_count], else: user.info.follower_count
128
129 %{
130 note_count: user.info.note_count,
131 locked: user.info.locked,
132 confirmation_pending: user.info.confirmation_pending,
133 default_scope: user.info.default_scope
134 }
135 |> Map.put(:following_count, following_count)
136 |> Map.put(:follower_count, follower_count)
137 end
138
139 def follow_state(%User{} = user, %User{} = target) do
140 follow_activity = Utils.fetch_latest_follow(user, target)
141
142 if follow_activity,
143 do: follow_activity.data["state"],
144 # Ideally this would be nil, but then Cachex does not commit the value
145 else: false
146 end
147
148 def get_cached_follow_state(user, target) do
149 key = "follow_state:#{user.ap_id}|#{target.ap_id}"
150 Cachex.fetch!(:user_cache, key, fn _ -> {:commit, follow_state(user, target)} end)
151 end
152
153 def set_follow_state_cache(user_ap_id, target_ap_id, state) do
154 Cachex.put(
155 :user_cache,
156 "follow_state:#{user_ap_id}|#{target_ap_id}",
157 state
158 )
159 end
160
161 def set_info_cache(user, args) do
162 Cachex.put(:user_cache, "user_info:#{user.id}", user_info(user, args))
163 end
164
165 @spec restrict_deactivated(Ecto.Query.t()) :: Ecto.Query.t()
166 def restrict_deactivated(query) do
167 from(u in query,
168 where: not fragment("? \\? 'deactivated' AND ?->'deactivated' @> 'true'", u.info, u.info)
169 )
170 end
171
172 def following_count(%User{following: []}), do: 0
173
174 def following_count(%User{} = user) do
175 user
176 |> get_friends_query()
177 |> Repo.aggregate(:count, :id)
178 end
179
180 def remote_user_creation(params) do
181 bio_limit = Pleroma.Config.get([:instance, :user_bio_length], 5000)
182 name_limit = Pleroma.Config.get([:instance, :user_name_length], 100)
183
184 params = Map.put(params, :info, params[:info] || %{})
185 info_cng = User.Info.remote_user_creation(%User.Info{}, params[:info])
186
187 changes =
188 %User{}
189 |> cast(params, [:bio, :name, :ap_id, :nickname, :avatar])
190 |> validate_required([:name, :ap_id])
191 |> unique_constraint(:nickname)
192 |> validate_format(:nickname, @email_regex)
193 |> validate_length(:bio, max: bio_limit)
194 |> validate_length(:name, max: name_limit)
195 |> put_change(:local, false)
196 |> put_embed(:info, info_cng)
197
198 if changes.valid? do
199 case info_cng.changes[:source_data] do
200 %{"followers" => followers, "following" => following} ->
201 changes
202 |> put_change(:follower_address, followers)
203 |> put_change(:following_address, following)
204
205 _ ->
206 followers = User.ap_followers(%User{nickname: changes.changes[:nickname]})
207
208 changes
209 |> put_change(:follower_address, followers)
210 end
211 else
212 changes
213 end
214 end
215
216 def update_changeset(struct, params \\ %{}) do
217 bio_limit = Pleroma.Config.get([:instance, :user_bio_length], 5000)
218 name_limit = Pleroma.Config.get([:instance, :user_name_length], 100)
219
220 struct
221 |> cast(params, [:bio, :name, :avatar, :following])
222 |> unique_constraint(:nickname)
223 |> validate_format(:nickname, local_nickname_regex())
224 |> validate_length(:bio, max: bio_limit)
225 |> validate_length(:name, min: 1, max: name_limit)
226 end
227
228 def upgrade_changeset(struct, params \\ %{}, remote? \\ false) do
229 bio_limit = Pleroma.Config.get([:instance, :user_bio_length], 5000)
230 name_limit = Pleroma.Config.get([:instance, :user_name_length], 100)
231
232 params = Map.put(params, :last_refreshed_at, NaiveDateTime.utc_now())
233 info_cng = User.Info.user_upgrade(struct.info, params[:info], remote?)
234
235 struct
236 |> cast(params, [
237 :bio,
238 :name,
239 :follower_address,
240 :following_address,
241 :avatar,
242 :last_refreshed_at
243 ])
244 |> unique_constraint(:nickname)
245 |> validate_format(:nickname, local_nickname_regex())
246 |> validate_length(:bio, max: bio_limit)
247 |> validate_length(:name, max: name_limit)
248 |> put_embed(:info, info_cng)
249 end
250
251 def password_update_changeset(struct, params) do
252 struct
253 |> cast(params, [:password, :password_confirmation])
254 |> validate_required([:password, :password_confirmation])
255 |> validate_confirmation(:password)
256 |> put_password_hash
257 end
258
259 @spec reset_password(User.t(), map) :: {:ok, User.t()} | {:error, Ecto.Changeset.t()}
260 def reset_password(%User{id: user_id} = user, data) do
261 multi =
262 Multi.new()
263 |> Multi.update(:user, password_update_changeset(user, data))
264 |> Multi.delete_all(:tokens, OAuth.Token.Query.get_by_user(user_id))
265 |> Multi.delete_all(:auth, OAuth.Authorization.delete_by_user_query(user))
266
267 case Repo.transaction(multi) do
268 {:ok, %{user: user} = _} -> set_cache(user)
269 {:error, _, changeset, _} -> {:error, changeset}
270 end
271 end
272
273 def register_changeset(struct, params \\ %{}, opts \\ []) do
274 bio_limit = Pleroma.Config.get([:instance, :user_bio_length], 5000)
275 name_limit = Pleroma.Config.get([:instance, :user_name_length], 100)
276
277 need_confirmation? =
278 if is_nil(opts[:need_confirmation]) do
279 Pleroma.Config.get([:instance, :account_activation_required])
280 else
281 opts[:need_confirmation]
282 end
283
284 info_change =
285 User.Info.confirmation_changeset(%User.Info{}, need_confirmation: need_confirmation?)
286
287 changeset =
288 struct
289 |> cast(params, [:bio, :email, :name, :nickname, :password, :password_confirmation])
290 |> validate_required([:name, :nickname, :password, :password_confirmation])
291 |> validate_confirmation(:password)
292 |> unique_constraint(:email)
293 |> unique_constraint(:nickname)
294 |> validate_exclusion(:nickname, Pleroma.Config.get([User, :restricted_nicknames]))
295 |> validate_format(:nickname, local_nickname_regex())
296 |> validate_format(:email, @email_regex)
297 |> validate_length(:bio, max: bio_limit)
298 |> validate_length(:name, min: 1, max: name_limit)
299 |> put_change(:info, info_change)
300
301 changeset =
302 if opts[:external] do
303 changeset
304 else
305 validate_required(changeset, [:email])
306 end
307
308 if changeset.valid? do
309 ap_id = User.ap_id(%User{nickname: changeset.changes[:nickname]})
310 followers = User.ap_followers(%User{nickname: changeset.changes[:nickname]})
311
312 changeset
313 |> put_password_hash
314 |> put_change(:ap_id, ap_id)
315 |> unique_constraint(:ap_id)
316 |> put_change(:following, [followers])
317 |> put_change(:follower_address, followers)
318 else
319 changeset
320 end
321 end
322
323 defp autofollow_users(user) do
324 candidates = Pleroma.Config.get([:instance, :autofollowed_nicknames])
325
326 autofollowed_users =
327 User.Query.build(%{nickname: candidates, local: true, deactivated: false})
328 |> Repo.all()
329
330 follow_all(user, autofollowed_users)
331 end
332
333 @doc "Inserts provided changeset, performs post-registration actions (confirmation email sending etc.)"
334 def register(%Ecto.Changeset{} = changeset) do
335 with {:ok, user} <- Repo.insert(changeset),
336 {:ok, user} <- post_register_action(user) do
337 {:ok, user}
338 end
339 end
340
341 def post_register_action(%User{} = user) do
342 with {:ok, user} <- autofollow_users(user),
343 {:ok, user} <- set_cache(user),
344 {:ok, _} <- User.WelcomeMessage.post_welcome_message_to_user(user),
345 {:ok, _} <- try_send_confirmation_email(user) do
346 {:ok, user}
347 end
348 end
349
350 def try_send_confirmation_email(%User{} = user) do
351 if user.info.confirmation_pending &&
352 Pleroma.Config.get([:instance, :account_activation_required]) do
353 user
354 |> Pleroma.Emails.UserEmail.account_confirmation_email()
355 |> Pleroma.Emails.Mailer.deliver_async()
356
357 {:ok, :enqueued}
358 else
359 {:ok, :noop}
360 end
361 end
362
363 def needs_update?(%User{local: true}), do: false
364
365 def needs_update?(%User{local: false, last_refreshed_at: nil}), do: true
366
367 def needs_update?(%User{local: false} = user) do
368 NaiveDateTime.diff(NaiveDateTime.utc_now(), user.last_refreshed_at) >= 86_400
369 end
370
371 def needs_update?(_), do: true
372
373 @spec maybe_direct_follow(User.t(), User.t()) :: {:ok, User.t()} | {:error, String.t()}
374 def maybe_direct_follow(%User{} = follower, %User{local: true, info: %{locked: true}}) do
375 {:ok, follower}
376 end
377
378 def maybe_direct_follow(%User{} = follower, %User{local: true} = followed) do
379 follow(follower, followed)
380 end
381
382 def maybe_direct_follow(%User{} = follower, %User{} = followed) do
383 if not User.ap_enabled?(followed) do
384 follow(follower, followed)
385 else
386 {:ok, follower}
387 end
388 end
389
390 @doc "A mass follow for local users. Respects blocks in both directions but does not create activities."
391 @spec follow_all(User.t(), list(User.t())) :: {atom(), User.t()}
392 def follow_all(follower, followeds) do
393 followed_addresses =
394 followeds
395 |> Enum.reject(fn followed -> blocks?(follower, followed) || blocks?(followed, follower) end)
396 |> Enum.map(fn %{follower_address: fa} -> fa end)
397
398 q =
399 from(u in User,
400 where: u.id == ^follower.id,
401 update: [
402 set: [
403 following:
404 fragment(
405 "array(select distinct unnest (array_cat(?, ?)))",
406 u.following,
407 ^followed_addresses
408 )
409 ]
410 ],
411 select: u
412 )
413
414 {1, [follower]} = Repo.update_all(q, [])
415
416 Enum.each(followeds, fn followed ->
417 update_follower_count(followed)
418 end)
419
420 set_cache(follower)
421 end
422
423 def follow(%User{} = follower, %User{info: info} = followed) do
424 deny_follow_blocked = Pleroma.Config.get([:user, :deny_follow_blocked])
425 ap_followers = followed.follower_address
426
427 cond do
428 info.deactivated ->
429 {:error, "Could not follow user: You are deactivated."}
430
431 deny_follow_blocked and blocks?(followed, follower) ->
432 {:error, "Could not follow user: #{followed.nickname} blocked you."}
433
434 true ->
435 if !followed.local && follower.local && !ap_enabled?(followed) do
436 Websub.subscribe(follower, followed)
437 end
438
439 q =
440 from(u in User,
441 where: u.id == ^follower.id,
442 update: [push: [following: ^ap_followers]],
443 select: u
444 )
445
446 {1, [follower]} = Repo.update_all(q, [])
447
448 follower = maybe_update_following_count(follower)
449
450 {:ok, _} = update_follower_count(followed)
451
452 set_cache(follower)
453 end
454 end
455
456 def unfollow(%User{} = follower, %User{} = followed) do
457 ap_followers = followed.follower_address
458
459 if following?(follower, followed) and follower.ap_id != followed.ap_id do
460 q =
461 from(u in User,
462 where: u.id == ^follower.id,
463 update: [pull: [following: ^ap_followers]],
464 select: u
465 )
466
467 {1, [follower]} = Repo.update_all(q, [])
468
469 follower = maybe_update_following_count(follower)
470
471 {:ok, followed} = update_follower_count(followed)
472
473 set_cache(follower)
474
475 {:ok, follower, Utils.fetch_latest_follow(follower, followed)}
476 else
477 {:error, "Not subscribed!"}
478 end
479 end
480
481 @spec following?(User.t(), User.t()) :: boolean
482 def following?(%User{} = follower, %User{} = followed) do
483 Enum.member?(follower.following, followed.follower_address)
484 end
485
486 def locked?(%User{} = user) do
487 user.info.locked || false
488 end
489
490 def get_by_id(id) do
491 Repo.get_by(User, id: id)
492 end
493
494 def get_by_ap_id(ap_id) do
495 Repo.get_by(User, ap_id: ap_id)
496 end
497
498 def get_all_by_ap_id(ap_ids) do
499 from(u in __MODULE__,
500 where: u.ap_id in ^ap_ids
501 )
502 |> Repo.all()
503 end
504
505 # This is mostly an SPC migration fix. This guesses the user nickname by taking the last part
506 # of the ap_id and the domain and tries to get that user
507 def get_by_guessed_nickname(ap_id) do
508 domain = URI.parse(ap_id).host
509 name = List.last(String.split(ap_id, "/"))
510 nickname = "#{name}@#{domain}"
511
512 get_cached_by_nickname(nickname)
513 end
514
515 def set_cache({:ok, user}), do: set_cache(user)
516 def set_cache({:error, err}), do: {:error, err}
517
518 def set_cache(%User{} = user) do
519 Cachex.put(:user_cache, "ap_id:#{user.ap_id}", user)
520 Cachex.put(:user_cache, "nickname:#{user.nickname}", user)
521 Cachex.put(:user_cache, "user_info:#{user.id}", user_info(user))
522 {:ok, user}
523 end
524
525 def update_and_set_cache(changeset) do
526 with {:ok, user} <- Repo.update(changeset, stale_error_field: :id) do
527 set_cache(user)
528 else
529 e -> e
530 end
531 end
532
533 def invalidate_cache(user) do
534 Cachex.del(:user_cache, "ap_id:#{user.ap_id}")
535 Cachex.del(:user_cache, "nickname:#{user.nickname}")
536 Cachex.del(:user_cache, "user_info:#{user.id}")
537 end
538
539 def get_cached_by_ap_id(ap_id) do
540 key = "ap_id:#{ap_id}"
541 Cachex.fetch!(:user_cache, key, fn _ -> get_by_ap_id(ap_id) end)
542 end
543
544 def get_cached_by_id(id) do
545 key = "id:#{id}"
546
547 ap_id =
548 Cachex.fetch!(:user_cache, key, fn _ ->
549 user = get_by_id(id)
550
551 if user do
552 Cachex.put(:user_cache, "ap_id:#{user.ap_id}", user)
553 {:commit, user.ap_id}
554 else
555 {:ignore, ""}
556 end
557 end)
558
559 get_cached_by_ap_id(ap_id)
560 end
561
562 def get_cached_by_nickname(nickname) do
563 key = "nickname:#{nickname}"
564
565 Cachex.fetch!(:user_cache, key, fn ->
566 user_result = get_or_fetch_by_nickname(nickname)
567
568 case user_result do
569 {:ok, user} -> {:commit, user}
570 {:error, _error} -> {:ignore, nil}
571 end
572 end)
573 end
574
575 def get_cached_by_nickname_or_id(nickname_or_id) do
576 get_cached_by_id(nickname_or_id) || get_cached_by_nickname(nickname_or_id)
577 end
578
579 def get_by_nickname(nickname) do
580 Repo.get_by(User, nickname: nickname) ||
581 if Regex.match?(~r(@#{Pleroma.Web.Endpoint.host()})i, nickname) do
582 Repo.get_by(User, nickname: local_nickname(nickname))
583 end
584 end
585
586 def get_by_email(email), do: Repo.get_by(User, email: email)
587
588 def get_by_nickname_or_email(nickname_or_email) do
589 get_by_nickname(nickname_or_email) || get_by_email(nickname_or_email)
590 end
591
592 def get_cached_user_info(user) do
593 key = "user_info:#{user.id}"
594 Cachex.fetch!(:user_cache, key, fn _ -> user_info(user) end)
595 end
596
597 def fetch_by_nickname(nickname) do
598 ap_try = ActivityPub.make_user_from_nickname(nickname)
599
600 case ap_try do
601 {:ok, user} -> {:ok, user}
602 _ -> OStatus.make_user(nickname)
603 end
604 end
605
606 def get_or_fetch_by_nickname(nickname) do
607 with %User{} = user <- get_by_nickname(nickname) do
608 {:ok, user}
609 else
610 _e ->
611 with [_nick, _domain] <- String.split(nickname, "@"),
612 {:ok, user} <- fetch_by_nickname(nickname) do
613 if Pleroma.Config.get([:fetch_initial_posts, :enabled]) do
614 fetch_initial_posts(user)
615 end
616
617 {:ok, user}
618 else
619 _e -> {:error, "not found " <> nickname}
620 end
621 end
622 end
623
624 @doc "Fetch some posts when the user has just been federated with"
625 def fetch_initial_posts(user) do
626 %{"op" => "fetch_initial_posts", "user_id" => user.id}
627 |> BackgroundWorker.new(worker_args(:background))
628 |> Repo.insert()
629 end
630
631 @spec get_followers_query(User.t(), pos_integer() | nil) :: Ecto.Query.t()
632 def get_followers_query(%User{} = user, nil) do
633 User.Query.build(%{followers: user, deactivated: false})
634 end
635
636 def get_followers_query(user, page) do
637 from(u in get_followers_query(user, nil))
638 |> User.Query.paginate(page, 20)
639 end
640
641 @spec get_followers_query(User.t()) :: Ecto.Query.t()
642 def get_followers_query(user), do: get_followers_query(user, nil)
643
644 @spec get_followers(User.t(), pos_integer()) :: {:ok, list(User.t())}
645 def get_followers(user, page \\ nil) do
646 q = get_followers_query(user, page)
647
648 {:ok, Repo.all(q)}
649 end
650
651 @spec get_external_followers(User.t(), pos_integer()) :: {:ok, list(User.t())}
652 def get_external_followers(user, page \\ nil) do
653 q =
654 user
655 |> get_followers_query(page)
656 |> User.Query.build(%{external: true})
657
658 {:ok, Repo.all(q)}
659 end
660
661 def get_followers_ids(user, page \\ nil) do
662 q = get_followers_query(user, page)
663
664 Repo.all(from(u in q, select: u.id))
665 end
666
667 @spec get_friends_query(User.t(), pos_integer() | nil) :: Ecto.Query.t()
668 def get_friends_query(%User{} = user, nil) do
669 User.Query.build(%{friends: user, deactivated: false})
670 end
671
672 def get_friends_query(user, page) do
673 from(u in get_friends_query(user, nil))
674 |> User.Query.paginate(page, 20)
675 end
676
677 @spec get_friends_query(User.t()) :: Ecto.Query.t()
678 def get_friends_query(user), do: get_friends_query(user, nil)
679
680 def get_friends(user, page \\ nil) do
681 q = get_friends_query(user, page)
682
683 {:ok, Repo.all(q)}
684 end
685
686 def get_friends_ids(user, page \\ nil) do
687 q = get_friends_query(user, page)
688
689 Repo.all(from(u in q, select: u.id))
690 end
691
692 @spec get_follow_requests(User.t()) :: {:ok, [User.t()]}
693 def get_follow_requests(%User{} = user) do
694 users =
695 Activity.follow_requests_for_actor(user)
696 |> join(:inner, [a], u in User, on: a.actor == u.ap_id)
697 |> where([a, u], not fragment("? @> ?", u.following, ^[user.follower_address]))
698 |> group_by([a, u], u.id)
699 |> select([a, u], u)
700 |> Repo.all()
701
702 {:ok, users}
703 end
704
705 def increase_note_count(%User{} = user) do
706 User
707 |> where(id: ^user.id)
708 |> update([u],
709 set: [
710 info:
711 fragment(
712 "jsonb_set(?, '{note_count}', ((?->>'note_count')::int + 1)::varchar::jsonb, true)",
713 u.info,
714 u.info
715 )
716 ]
717 )
718 |> select([u], u)
719 |> Repo.update_all([])
720 |> case do
721 {1, [user]} -> set_cache(user)
722 _ -> {:error, user}
723 end
724 end
725
726 def decrease_note_count(%User{} = user) do
727 User
728 |> where(id: ^user.id)
729 |> update([u],
730 set: [
731 info:
732 fragment(
733 "jsonb_set(?, '{note_count}', (greatest(0, (?->>'note_count')::int - 1))::varchar::jsonb, true)",
734 u.info,
735 u.info
736 )
737 ]
738 )
739 |> select([u], u)
740 |> Repo.update_all([])
741 |> case do
742 {1, [user]} -> set_cache(user)
743 _ -> {:error, user}
744 end
745 end
746
747 def update_note_count(%User{} = user) do
748 note_count_query =
749 from(
750 a in Object,
751 where: fragment("?->>'actor' = ? and ?->>'type' = 'Note'", a.data, ^user.ap_id, a.data),
752 select: count(a.id)
753 )
754
755 note_count = Repo.one(note_count_query)
756
757 info_cng = User.Info.set_note_count(user.info, note_count)
758
759 user
760 |> change()
761 |> put_embed(:info, info_cng)
762 |> update_and_set_cache()
763 end
764
765 @spec maybe_fetch_follow_information(User.t()) :: User.t()
766 def maybe_fetch_follow_information(user) do
767 with {:ok, user} <- fetch_follow_information(user) do
768 user
769 else
770 e ->
771 Logger.error("Follower/Following counter update for #{user.ap_id} failed.\n#{inspect(e)}")
772
773 user
774 end
775 end
776
777 def fetch_follow_information(user) do
778 with {:ok, info} <- ActivityPub.fetch_follow_information_for_user(user) do
779 info_cng = User.Info.follow_information_update(user.info, info)
780
781 changeset =
782 user
783 |> change()
784 |> put_embed(:info, info_cng)
785
786 update_and_set_cache(changeset)
787 else
788 {:error, _} = e -> e
789 e -> {:error, e}
790 end
791 end
792
793 def update_follower_count(%User{} = user) do
794 if user.local or !Pleroma.Config.get([:instance, :external_user_synchronization]) do
795 follower_count_query =
796 User.Query.build(%{followers: user, deactivated: false})
797 |> select([u], %{count: count(u.id)})
798
799 User
800 |> where(id: ^user.id)
801 |> join(:inner, [u], s in subquery(follower_count_query))
802 |> update([u, s],
803 set: [
804 info:
805 fragment(
806 "jsonb_set(?, '{follower_count}', ?::varchar::jsonb, true)",
807 u.info,
808 s.count
809 )
810 ]
811 )
812 |> select([u], u)
813 |> Repo.update_all([])
814 |> case do
815 {1, [user]} -> set_cache(user)
816 _ -> {:error, user}
817 end
818 else
819 {:ok, maybe_fetch_follow_information(user)}
820 end
821 end
822
823 @spec maybe_update_following_count(User.t()) :: User.t()
824 def maybe_update_following_count(%User{local: false} = user) do
825 if Pleroma.Config.get([:instance, :external_user_synchronization]) do
826 maybe_fetch_follow_information(user)
827 else
828 user
829 end
830 end
831
832 def maybe_update_following_count(user), do: user
833
834 def remove_duplicated_following(%User{following: following} = user) do
835 uniq_following = Enum.uniq(following)
836
837 if length(following) == length(uniq_following) do
838 {:ok, user}
839 else
840 user
841 |> update_changeset(%{following: uniq_following})
842 |> update_and_set_cache()
843 end
844 end
845
846 @spec get_users_from_set([String.t()], boolean()) :: [User.t()]
847 def get_users_from_set(ap_ids, local_only \\ true) do
848 criteria = %{ap_id: ap_ids, deactivated: false}
849 criteria = if local_only, do: Map.put(criteria, :local, true), else: criteria
850
851 User.Query.build(criteria)
852 |> Repo.all()
853 end
854
855 @spec get_recipients_from_activity(Activity.t()) :: [User.t()]
856 def get_recipients_from_activity(%Activity{recipients: to}) do
857 User.Query.build(%{recipients_from_activity: to, local: true, deactivated: false})
858 |> Repo.all()
859 end
860
861 @spec mute(User.t(), User.t(), boolean()) :: {:ok, User.t()} | {:error, String.t()}
862 def mute(muter, %User{ap_id: ap_id}, notifications? \\ true) do
863 info = muter.info
864
865 info_cng =
866 User.Info.add_to_mutes(info, ap_id)
867 |> User.Info.add_to_muted_notifications(info, ap_id, notifications?)
868
869 cng =
870 change(muter)
871 |> put_embed(:info, info_cng)
872
873 update_and_set_cache(cng)
874 end
875
876 def unmute(muter, %{ap_id: ap_id}) do
877 info = muter.info
878
879 info_cng =
880 User.Info.remove_from_mutes(info, ap_id)
881 |> User.Info.remove_from_muted_notifications(info, ap_id)
882
883 cng =
884 change(muter)
885 |> put_embed(:info, info_cng)
886
887 update_and_set_cache(cng)
888 end
889
890 def subscribe(subscriber, %{ap_id: ap_id}) do
891 deny_follow_blocked = Pleroma.Config.get([:user, :deny_follow_blocked])
892
893 with %User{} = subscribed <- get_cached_by_ap_id(ap_id) do
894 blocked = blocks?(subscribed, subscriber) and deny_follow_blocked
895
896 if blocked do
897 {:error, "Could not subscribe: #{subscribed.nickname} is blocking you"}
898 else
899 info_cng =
900 subscribed.info
901 |> User.Info.add_to_subscribers(subscriber.ap_id)
902
903 change(subscribed)
904 |> put_embed(:info, info_cng)
905 |> update_and_set_cache()
906 end
907 end
908 end
909
910 def unsubscribe(unsubscriber, %{ap_id: ap_id}) do
911 with %User{} = user <- get_cached_by_ap_id(ap_id) do
912 info_cng =
913 user.info
914 |> User.Info.remove_from_subscribers(unsubscriber.ap_id)
915
916 change(user)
917 |> put_embed(:info, info_cng)
918 |> update_and_set_cache()
919 end
920 end
921
922 def block(blocker, %User{ap_id: ap_id} = blocked) do
923 # sever any follow relationships to prevent leaks per activitypub (Pleroma issue #213)
924 blocker =
925 if following?(blocker, blocked) do
926 {:ok, blocker, _} = unfollow(blocker, blocked)
927 blocker
928 else
929 blocker
930 end
931
932 # clear any requested follows as well
933 blocked =
934 case CommonAPI.reject_follow_request(blocked, blocker) do
935 {:ok, %User{} = updated_blocked} -> updated_blocked
936 nil -> blocked
937 end
938
939 blocker =
940 if subscribed_to?(blocked, blocker) do
941 {:ok, blocker} = unsubscribe(blocked, blocker)
942 blocker
943 else
944 blocker
945 end
946
947 if following?(blocked, blocker) do
948 unfollow(blocked, blocker)
949 end
950
951 {:ok, blocker} = update_follower_count(blocker)
952
953 info_cng =
954 blocker.info
955 |> User.Info.add_to_block(ap_id)
956
957 cng =
958 change(blocker)
959 |> put_embed(:info, info_cng)
960
961 update_and_set_cache(cng)
962 end
963
964 # helper to handle the block given only an actor's AP id
965 def block(blocker, %{ap_id: ap_id}) do
966 block(blocker, get_cached_by_ap_id(ap_id))
967 end
968
969 def unblock(blocker, %{ap_id: ap_id}) do
970 info_cng =
971 blocker.info
972 |> User.Info.remove_from_block(ap_id)
973
974 cng =
975 change(blocker)
976 |> put_embed(:info, info_cng)
977
978 update_and_set_cache(cng)
979 end
980
981 def mutes?(nil, _), do: false
982 def mutes?(user, %{ap_id: ap_id}), do: Enum.member?(user.info.mutes, ap_id)
983
984 @spec muted_notifications?(User.t() | nil, User.t() | map()) :: boolean()
985 def muted_notifications?(nil, _), do: false
986
987 def muted_notifications?(user, %{ap_id: ap_id}),
988 do: Enum.member?(user.info.muted_notifications, ap_id)
989
990 def blocks?(%User{} = user, %User{} = target) do
991 blocks_ap_id?(user, target) || blocks_domain?(user, target)
992 end
993
994 def blocks?(nil, _), do: false
995
996 def blocks_ap_id?(%User{} = user, %User{} = target) do
997 Enum.member?(user.info.blocks, target.ap_id)
998 end
999
1000 def blocks_ap_id?(_, _), do: false
1001
1002 def blocks_domain?(%User{} = user, %User{} = target) do
1003 domain_blocks = Pleroma.Web.ActivityPub.MRF.subdomains_regex(user.info.domain_blocks)
1004 %{host: host} = URI.parse(target.ap_id)
1005 Pleroma.Web.ActivityPub.MRF.subdomain_match?(domain_blocks, host)
1006 end
1007
1008 def blocks_domain?(_, _), do: false
1009
1010 def subscribed_to?(user, %{ap_id: ap_id}) do
1011 with %User{} = target <- get_cached_by_ap_id(ap_id) do
1012 Enum.member?(target.info.subscribers, user.ap_id)
1013 end
1014 end
1015
1016 @spec muted_users(User.t()) :: [User.t()]
1017 def muted_users(user) do
1018 User.Query.build(%{ap_id: user.info.mutes, deactivated: false})
1019 |> Repo.all()
1020 end
1021
1022 @spec blocked_users(User.t()) :: [User.t()]
1023 def blocked_users(user) do
1024 User.Query.build(%{ap_id: user.info.blocks, deactivated: false})
1025 |> Repo.all()
1026 end
1027
1028 @spec subscribers(User.t()) :: [User.t()]
1029 def subscribers(user) do
1030 User.Query.build(%{ap_id: user.info.subscribers, deactivated: false})
1031 |> Repo.all()
1032 end
1033
1034 def block_domain(user, domain) do
1035 info_cng =
1036 user.info
1037 |> User.Info.add_to_domain_block(domain)
1038
1039 cng =
1040 change(user)
1041 |> put_embed(:info, info_cng)
1042
1043 update_and_set_cache(cng)
1044 end
1045
1046 def unblock_domain(user, domain) do
1047 info_cng =
1048 user.info
1049 |> User.Info.remove_from_domain_block(domain)
1050
1051 cng =
1052 change(user)
1053 |> put_embed(:info, info_cng)
1054
1055 update_and_set_cache(cng)
1056 end
1057
1058 def deactivate_async(user, status \\ true) do
1059 %{"op" => "deactivate_user", "user_id" => user.id, "status" => status}
1060 |> BackgroundWorker.new(worker_args(:background))
1061 |> Repo.insert()
1062 end
1063
1064 def deactivate(%User{} = user, status \\ true) do
1065 info_cng = User.Info.set_activation_status(user.info, status)
1066
1067 with {:ok, friends} <- User.get_friends(user),
1068 {:ok, followers} <- User.get_followers(user),
1069 {:ok, user} <-
1070 user
1071 |> change()
1072 |> put_embed(:info, info_cng)
1073 |> update_and_set_cache() do
1074 Enum.each(followers, &invalidate_cache(&1))
1075 Enum.each(friends, &update_follower_count(&1))
1076
1077 {:ok, user}
1078 end
1079 end
1080
1081 def update_notification_settings(%User{} = user, settings \\ %{}) do
1082 info_changeset = User.Info.update_notification_settings(user.info, settings)
1083
1084 change(user)
1085 |> put_embed(:info, info_changeset)
1086 |> update_and_set_cache()
1087 end
1088
1089 def delete(%User{} = user) do
1090 %{"op" => "delete_user", "user_id" => user.id}
1091 |> BackgroundWorker.new(worker_args(:background))
1092 |> Repo.insert()
1093 end
1094
1095 @spec perform(atom(), User.t()) :: {:ok, User.t()}
1096 def perform(:delete, %User{} = user) do
1097 {:ok, _user} = ActivityPub.delete(user)
1098
1099 # Remove all relationships
1100 {:ok, followers} = User.get_followers(user)
1101
1102 Enum.each(followers, fn follower ->
1103 ActivityPub.unfollow(follower, user)
1104 User.unfollow(follower, user)
1105 end)
1106
1107 {:ok, friends} = User.get_friends(user)
1108
1109 Enum.each(friends, fn followed ->
1110 ActivityPub.unfollow(user, followed)
1111 User.unfollow(user, followed)
1112 end)
1113
1114 delete_user_activities(user)
1115 invalidate_cache(user)
1116 Repo.delete(user)
1117 end
1118
1119 @spec perform(atom(), User.t()) :: {:ok, User.t()}
1120 def perform(:fetch_initial_posts, %User{} = user) do
1121 pages = Pleroma.Config.get!([:fetch_initial_posts, :pages])
1122
1123 Enum.each(
1124 # Insert all the posts in reverse order, so they're in the right order on the timeline
1125 Enum.reverse(Utils.fetch_ordered_collection(user.info.source_data["outbox"], pages)),
1126 &Pleroma.Web.Federator.incoming_ap_doc/1
1127 )
1128
1129 {:ok, user}
1130 end
1131
1132 def perform(:deactivate_async, user, status), do: deactivate(user, status)
1133
1134 @spec perform(atom(), User.t(), list()) :: list() | {:error, any()}
1135 def perform(:blocks_import, %User{} = blocker, blocked_identifiers)
1136 when is_list(blocked_identifiers) do
1137 Enum.map(
1138 blocked_identifiers,
1139 fn blocked_identifier ->
1140 with {:ok, %User{} = blocked} <- get_or_fetch(blocked_identifier),
1141 {:ok, blocker} <- block(blocker, blocked),
1142 {:ok, _} <- ActivityPub.block(blocker, blocked) do
1143 blocked
1144 else
1145 err ->
1146 Logger.debug("blocks_import failed for #{blocked_identifier} with: #{inspect(err)}")
1147 err
1148 end
1149 end
1150 )
1151 end
1152
1153 @spec perform(atom(), User.t(), list()) :: list() | {:error, any()}
1154 def perform(:follow_import, %User{} = follower, followed_identifiers)
1155 when is_list(followed_identifiers) do
1156 Enum.map(
1157 followed_identifiers,
1158 fn followed_identifier ->
1159 with {:ok, %User{} = followed} <- get_or_fetch(followed_identifier),
1160 {:ok, follower} <- maybe_direct_follow(follower, followed),
1161 {:ok, _} <- ActivityPub.follow(follower, followed) do
1162 followed
1163 else
1164 err ->
1165 Logger.debug("follow_import failed for #{followed_identifier} with: #{inspect(err)}")
1166 err
1167 end
1168 end
1169 )
1170 end
1171
1172 @spec external_users_query() :: Ecto.Query.t()
1173 def external_users_query do
1174 User.Query.build(%{
1175 external: true,
1176 active: true,
1177 order_by: :id
1178 })
1179 end
1180
1181 @spec external_users(keyword()) :: [User.t()]
1182 def external_users(opts \\ []) do
1183 query =
1184 external_users_query()
1185 |> select([u], struct(u, [:id, :ap_id, :info]))
1186
1187 query =
1188 if opts[:max_id],
1189 do: where(query, [u], u.id > ^opts[:max_id]),
1190 else: query
1191
1192 query =
1193 if opts[:limit],
1194 do: limit(query, ^opts[:limit]),
1195 else: query
1196
1197 Repo.all(query)
1198 end
1199
1200 def blocks_import(%User{} = blocker, blocked_identifiers) when is_list(blocked_identifiers) do
1201 %{
1202 "op" => "blocks_import",
1203 "blocker_id" => blocker.id,
1204 "blocked_identifiers" => blocked_identifiers
1205 }
1206 |> BackgroundWorker.new(worker_args(:background))
1207 |> Repo.insert()
1208 end
1209
1210 def follow_import(%User{} = follower, followed_identifiers)
1211 when is_list(followed_identifiers) do
1212 %{
1213 "op" => "follow_import",
1214 "follower_id" => follower.id,
1215 "followed_identifiers" => followed_identifiers
1216 }
1217 |> BackgroundWorker.new(worker_args(:background))
1218 |> Repo.insert()
1219 end
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