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