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