updated tests
[akkoma] / lib / pleroma / user.ex
1 # Pleroma: A lightweight social networking server
2 # Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
3 # SPDX-License-Identifier: AGPL-3.0-only
4
5 defmodule Pleroma.User do
6 use Ecto.Schema
7
8 import Ecto.Changeset
9 import Ecto.Query
10
11 alias Comeonin.Pbkdf2
12 alias Ecto.Multi
13 alias Pleroma.Activity
14 alias Pleroma.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 def get_all_by_ids(ids) do
521 from(u in __MODULE__, where: u.id in ^ids)
522 |> Repo.all()
523 end
524
525 # This is mostly an SPC migration fix. This guesses the user nickname by taking the last part
526 # of the ap_id and the domain and tries to get that user
527 def get_by_guessed_nickname(ap_id) do
528 domain = URI.parse(ap_id).host
529 name = List.last(String.split(ap_id, "/"))
530 nickname = "#{name}@#{domain}"
531
532 get_cached_by_nickname(nickname)
533 end
534
535 def set_cache({:ok, user}), do: set_cache(user)
536 def set_cache({:error, err}), do: {:error, err}
537
538 def set_cache(%User{} = user) do
539 Cachex.put(:user_cache, "ap_id:#{user.ap_id}", user)
540 Cachex.put(:user_cache, "nickname:#{user.nickname}", user)
541 Cachex.put(:user_cache, "user_info:#{user.id}", user_info(user))
542 {:ok, user}
543 end
544
545 def update_and_set_cache(changeset) do
546 with {:ok, user} <- Repo.update(changeset, stale_error_field: :id) do
547 set_cache(user)
548 else
549 e -> e
550 end
551 end
552
553 def invalidate_cache(user) do
554 Cachex.del(:user_cache, "ap_id:#{user.ap_id}")
555 Cachex.del(:user_cache, "nickname:#{user.nickname}")
556 Cachex.del(:user_cache, "user_info:#{user.id}")
557 end
558
559 def get_cached_by_ap_id(ap_id) do
560 key = "ap_id:#{ap_id}"
561 Cachex.fetch!(:user_cache, key, fn _ -> get_by_ap_id(ap_id) end)
562 end
563
564 def get_cached_by_id(id) do
565 key = "id:#{id}"
566
567 ap_id =
568 Cachex.fetch!(:user_cache, key, fn _ ->
569 user = get_by_id(id)
570
571 if user do
572 Cachex.put(:user_cache, "ap_id:#{user.ap_id}", user)
573 {:commit, user.ap_id}
574 else
575 {:ignore, ""}
576 end
577 end)
578
579 get_cached_by_ap_id(ap_id)
580 end
581
582 def get_cached_by_nickname(nickname) do
583 key = "nickname:#{nickname}"
584
585 Cachex.fetch!(:user_cache, key, fn ->
586 user_result = get_or_fetch_by_nickname(nickname)
587
588 case user_result do
589 {:ok, user} -> {:commit, user}
590 {:error, _error} -> {:ignore, nil}
591 end
592 end)
593 end
594
595 def get_cached_by_nickname_or_id(nickname_or_id, opts \\ []) do
596 restrict_to_local = Pleroma.Config.get([:instance, :limit_to_local_content])
597
598 cond do
599 is_integer(nickname_or_id) or Pleroma.FlakeId.is_flake_id?(nickname_or_id) ->
600 get_cached_by_id(nickname_or_id) || get_cached_by_nickname(nickname_or_id)
601
602 restrict_to_local == false ->
603 get_cached_by_nickname(nickname_or_id)
604
605 restrict_to_local == :unauthenticated and match?(%User{}, opts[:for]) ->
606 get_cached_by_nickname(nickname_or_id)
607
608 true ->
609 nil
610 end
611 end
612
613 def get_by_nickname(nickname) do
614 Repo.get_by(User, nickname: nickname) ||
615 if Regex.match?(~r(@#{Pleroma.Web.Endpoint.host()})i, nickname) do
616 Repo.get_by(User, nickname: local_nickname(nickname))
617 end
618 end
619
620 def get_by_email(email), do: Repo.get_by(User, email: email)
621
622 def get_by_nickname_or_email(nickname_or_email) do
623 get_by_nickname(nickname_or_email) || get_by_email(nickname_or_email)
624 end
625
626 def get_cached_user_info(user) do
627 key = "user_info:#{user.id}"
628 Cachex.fetch!(:user_cache, key, fn _ -> user_info(user) end)
629 end
630
631 def fetch_by_nickname(nickname) do
632 ap_try = ActivityPub.make_user_from_nickname(nickname)
633
634 case ap_try do
635 {:ok, user} -> {:ok, user}
636 _ -> OStatus.make_user(nickname)
637 end
638 end
639
640 def get_or_fetch_by_nickname(nickname) do
641 with %User{} = user <- get_by_nickname(nickname) do
642 {:ok, user}
643 else
644 _e ->
645 with [_nick, _domain] <- String.split(nickname, "@"),
646 {:ok, user} <- fetch_by_nickname(nickname) do
647 if Pleroma.Config.get([:fetch_initial_posts, :enabled]) do
648 fetch_initial_posts(user)
649 end
650
651 {:ok, user}
652 else
653 _e -> {:error, "not found " <> nickname}
654 end
655 end
656 end
657
658 @doc "Fetch some posts when the user has just been federated with"
659 def fetch_initial_posts(user) do
660 BackgroundWorker.enqueue("fetch_initial_posts", %{"user_id" => user.id})
661 end
662
663 @spec get_followers_query(User.t(), pos_integer() | nil) :: Ecto.Query.t()
664 def get_followers_query(%User{} = user, nil) do
665 User.Query.build(%{followers: user, deactivated: false})
666 end
667
668 def get_followers_query(user, page) do
669 from(u in get_followers_query(user, nil))
670 |> User.Query.paginate(page, 20)
671 end
672
673 @spec get_followers_query(User.t()) :: Ecto.Query.t()
674 def get_followers_query(user), do: get_followers_query(user, nil)
675
676 @spec get_followers(User.t(), pos_integer()) :: {:ok, list(User.t())}
677 def get_followers(user, page \\ nil) do
678 q = get_followers_query(user, page)
679
680 {:ok, Repo.all(q)}
681 end
682
683 @spec get_external_followers(User.t(), pos_integer()) :: {:ok, list(User.t())}
684 def get_external_followers(user, page \\ nil) do
685 q =
686 user
687 |> get_followers_query(page)
688 |> User.Query.build(%{external: true})
689
690 {:ok, Repo.all(q)}
691 end
692
693 def get_followers_ids(user, page \\ nil) do
694 q = get_followers_query(user, page)
695
696 Repo.all(from(u in q, select: u.id))
697 end
698
699 @spec get_friends_query(User.t(), pos_integer() | nil) :: Ecto.Query.t()
700 def get_friends_query(%User{} = user, nil) do
701 User.Query.build(%{friends: user, deactivated: false})
702 end
703
704 def get_friends_query(user, page) do
705 from(u in get_friends_query(user, nil))
706 |> User.Query.paginate(page, 20)
707 end
708
709 @spec get_friends_query(User.t()) :: Ecto.Query.t()
710 def get_friends_query(user), do: get_friends_query(user, nil)
711
712 def get_friends(user, page \\ nil) do
713 q = get_friends_query(user, page)
714
715 {:ok, Repo.all(q)}
716 end
717
718 def get_friends_ids(user, page \\ nil) do
719 q = get_friends_query(user, page)
720
721 Repo.all(from(u in q, select: u.id))
722 end
723
724 @spec get_follow_requests(User.t()) :: {:ok, [User.t()]}
725 def get_follow_requests(%User{} = user) do
726 users =
727 Activity.follow_requests_for_actor(user)
728 |> join(:inner, [a], u in User, on: a.actor == u.ap_id)
729 |> where([a, u], not fragment("? @> ?", u.following, ^[user.follower_address]))
730 |> group_by([a, u], u.id)
731 |> select([a, u], u)
732 |> Repo.all()
733
734 {:ok, users}
735 end
736
737 def increase_note_count(%User{} = user) do
738 User
739 |> where(id: ^user.id)
740 |> update([u],
741 set: [
742 info:
743 fragment(
744 "jsonb_set(?, '{note_count}', ((?->>'note_count')::int + 1)::varchar::jsonb, true)",
745 u.info,
746 u.info
747 )
748 ]
749 )
750 |> select([u], u)
751 |> Repo.update_all([])
752 |> case do
753 {1, [user]} -> set_cache(user)
754 _ -> {:error, user}
755 end
756 end
757
758 def decrease_note_count(%User{} = user) do
759 User
760 |> where(id: ^user.id)
761 |> update([u],
762 set: [
763 info:
764 fragment(
765 "jsonb_set(?, '{note_count}', (greatest(0, (?->>'note_count')::int - 1))::varchar::jsonb, true)",
766 u.info,
767 u.info
768 )
769 ]
770 )
771 |> select([u], u)
772 |> Repo.update_all([])
773 |> case do
774 {1, [user]} -> set_cache(user)
775 _ -> {:error, user}
776 end
777 end
778
779 def update_note_count(%User{} = user) do
780 note_count_query =
781 from(
782 a in Object,
783 where: fragment("?->>'actor' = ? and ?->>'type' = 'Note'", a.data, ^user.ap_id, a.data),
784 select: count(a.id)
785 )
786
787 note_count = Repo.one(note_count_query)
788
789 info_cng = User.Info.set_note_count(user.info, note_count)
790
791 user
792 |> change()
793 |> put_embed(:info, info_cng)
794 |> update_and_set_cache()
795 end
796
797 def update_mascot(user, url) do
798 info_changeset =
799 User.Info.mascot_update(
800 user.info,
801 url
802 )
803
804 user
805 |> change()
806 |> put_embed(:info, info_changeset)
807 |> update_and_set_cache()
808 end
809
810 @spec maybe_fetch_follow_information(User.t()) :: User.t()
811 def maybe_fetch_follow_information(user) do
812 with {:ok, user} <- fetch_follow_information(user) do
813 user
814 else
815 e ->
816 Logger.error("Follower/Following counter update for #{user.ap_id} failed.\n#{inspect(e)}")
817
818 user
819 end
820 end
821
822 def fetch_follow_information(user) do
823 with {:ok, info} <- ActivityPub.fetch_follow_information_for_user(user) do
824 info_cng = User.Info.follow_information_update(user.info, info)
825
826 changeset =
827 user
828 |> change()
829 |> put_embed(:info, info_cng)
830
831 update_and_set_cache(changeset)
832 else
833 {:error, _} = e -> e
834 e -> {:error, e}
835 end
836 end
837
838 def update_follower_count(%User{} = user) do
839 if user.local or !Pleroma.Config.get([:instance, :external_user_synchronization]) do
840 follower_count_query =
841 User.Query.build(%{followers: user, deactivated: false})
842 |> select([u], %{count: count(u.id)})
843
844 User
845 |> where(id: ^user.id)
846 |> join(:inner, [u], s in subquery(follower_count_query))
847 |> update([u, s],
848 set: [
849 info:
850 fragment(
851 "jsonb_set(?, '{follower_count}', ?::varchar::jsonb, true)",
852 u.info,
853 s.count
854 )
855 ]
856 )
857 |> select([u], u)
858 |> Repo.update_all([])
859 |> case do
860 {1, [user]} -> set_cache(user)
861 _ -> {:error, user}
862 end
863 else
864 {:ok, maybe_fetch_follow_information(user)}
865 end
866 end
867
868 @spec maybe_update_following_count(User.t()) :: User.t()
869 def maybe_update_following_count(%User{local: false} = user) do
870 if Pleroma.Config.get([:instance, :external_user_synchronization]) do
871 maybe_fetch_follow_information(user)
872 else
873 user
874 end
875 end
876
877 def maybe_update_following_count(user), do: user
878
879 def remove_duplicated_following(%User{following: following} = user) do
880 uniq_following = Enum.uniq(following)
881
882 if length(following) == length(uniq_following) do
883 {:ok, user}
884 else
885 user
886 |> update_changeset(%{following: uniq_following})
887 |> update_and_set_cache()
888 end
889 end
890
891 @spec get_users_from_set([String.t()], boolean()) :: [User.t()]
892 def get_users_from_set(ap_ids, local_only \\ true) do
893 criteria = %{ap_id: ap_ids, deactivated: false}
894 criteria = if local_only, do: Map.put(criteria, :local, true), else: criteria
895
896 User.Query.build(criteria)
897 |> Repo.all()
898 end
899
900 @spec get_recipients_from_activity(Activity.t()) :: [User.t()]
901 def get_recipients_from_activity(%Activity{recipients: to}) do
902 User.Query.build(%{recipients_from_activity: to, local: true, deactivated: false})
903 |> Repo.all()
904 end
905
906 @spec mute(User.t(), User.t(), boolean()) :: {:ok, User.t()} | {:error, String.t()}
907 def mute(muter, %User{ap_id: ap_id}, notifications? \\ true) do
908 info = muter.info
909
910 info_cng =
911 User.Info.add_to_mutes(info, ap_id)
912 |> User.Info.add_to_muted_notifications(info, ap_id, notifications?)
913
914 cng =
915 change(muter)
916 |> put_embed(:info, info_cng)
917
918 update_and_set_cache(cng)
919 end
920
921 def unmute(muter, %{ap_id: ap_id}) do
922 info = muter.info
923
924 info_cng =
925 User.Info.remove_from_mutes(info, ap_id)
926 |> User.Info.remove_from_muted_notifications(info, ap_id)
927
928 cng =
929 change(muter)
930 |> put_embed(:info, info_cng)
931
932 update_and_set_cache(cng)
933 end
934
935 def subscribe(subscriber, %{ap_id: ap_id}) do
936 deny_follow_blocked = Pleroma.Config.get([:user, :deny_follow_blocked])
937
938 with %User{} = subscribed <- get_cached_by_ap_id(ap_id) do
939 blocked = blocks?(subscribed, subscriber) and deny_follow_blocked
940
941 if blocked do
942 {:error, "Could not subscribe: #{subscribed.nickname} is blocking you"}
943 else
944 info_cng =
945 subscribed.info
946 |> User.Info.add_to_subscribers(subscriber.ap_id)
947
948 change(subscribed)
949 |> put_embed(:info, info_cng)
950 |> update_and_set_cache()
951 end
952 end
953 end
954
955 def unsubscribe(unsubscriber, %{ap_id: ap_id}) do
956 with %User{} = user <- get_cached_by_ap_id(ap_id) do
957 info_cng = User.Info.remove_from_subscribers(user.info, unsubscriber.ap_id)
958
959 change(user)
960 |> put_embed(:info, info_cng)
961 |> update_and_set_cache()
962 end
963 end
964
965 def block(blocker, %User{ap_id: ap_id} = blocked) do
966 # sever any follow relationships to prevent leaks per activitypub (Pleroma issue #213)
967 blocker =
968 if following?(blocker, blocked) do
969 {:ok, blocker, _} = unfollow(blocker, blocked)
970 blocker
971 else
972 blocker
973 end
974
975 # clear any requested follows as well
976 blocked =
977 case CommonAPI.reject_follow_request(blocked, blocker) do
978 {:ok, %User{} = updated_blocked} -> updated_blocked
979 nil -> blocked
980 end
981
982 blocker =
983 if subscribed_to?(blocked, blocker) do
984 {:ok, blocker} = unsubscribe(blocked, blocker)
985 blocker
986 else
987 blocker
988 end
989
990 if following?(blocked, blocker) do
991 unfollow(blocked, blocker)
992 end
993
994 {:ok, blocker} = update_follower_count(blocker)
995
996 info_cng =
997 blocker.info
998 |> User.Info.add_to_block(ap_id)
999
1000 cng =
1001 change(blocker)
1002 |> put_embed(:info, info_cng)
1003
1004 update_and_set_cache(cng)
1005 end
1006
1007 # helper to handle the block given only an actor's AP id
1008 def block(blocker, %{ap_id: ap_id}) do
1009 block(blocker, get_cached_by_ap_id(ap_id))
1010 end
1011
1012 def unblock(blocker, %{ap_id: ap_id}) do
1013 info_cng =
1014 blocker.info
1015 |> User.Info.remove_from_block(ap_id)
1016
1017 cng =
1018 change(blocker)
1019 |> put_embed(:info, info_cng)
1020
1021 update_and_set_cache(cng)
1022 end
1023
1024 def mutes?(nil, _), do: false
1025 def mutes?(user, %{ap_id: ap_id}), do: Enum.member?(user.info.mutes, ap_id)
1026
1027 @spec muted_notifications?(User.t() | nil, User.t() | map()) :: boolean()
1028 def muted_notifications?(nil, _), do: false
1029
1030 def muted_notifications?(user, %{ap_id: ap_id}),
1031 do: Enum.member?(user.info.muted_notifications, ap_id)
1032
1033 def blocks?(%User{} = user, %User{} = target) do
1034 blocks_ap_id?(user, target) || blocks_domain?(user, target)
1035 end
1036
1037 def blocks?(nil, _), do: false
1038
1039 def blocks_ap_id?(%User{} = user, %User{} = target) do
1040 Enum.member?(user.info.blocks, target.ap_id)
1041 end
1042
1043 def blocks_ap_id?(_, _), do: false
1044
1045 def blocks_domain?(%User{} = user, %User{} = target) do
1046 domain_blocks = Pleroma.Web.ActivityPub.MRF.subdomains_regex(user.info.domain_blocks)
1047 %{host: host} = URI.parse(target.ap_id)
1048 Pleroma.Web.ActivityPub.MRF.subdomain_match?(domain_blocks, host)
1049 end
1050
1051 def blocks_domain?(_, _), do: false
1052
1053 def subscribed_to?(user, %{ap_id: ap_id}) do
1054 with %User{} = target <- get_cached_by_ap_id(ap_id) do
1055 Enum.member?(target.info.subscribers, user.ap_id)
1056 end
1057 end
1058
1059 @spec muted_users(User.t()) :: [User.t()]
1060 def muted_users(user) do
1061 User.Query.build(%{ap_id: user.info.mutes, deactivated: false})
1062 |> Repo.all()
1063 end
1064
1065 @spec blocked_users(User.t()) :: [User.t()]
1066 def blocked_users(user) do
1067 User.Query.build(%{ap_id: user.info.blocks, deactivated: false})
1068 |> Repo.all()
1069 end
1070
1071 @spec subscribers(User.t()) :: [User.t()]
1072 def subscribers(user) do
1073 User.Query.build(%{ap_id: user.info.subscribers, deactivated: false})
1074 |> Repo.all()
1075 end
1076
1077 def block_domain(user, domain) do
1078 info_cng =
1079 user.info
1080 |> User.Info.add_to_domain_block(domain)
1081
1082 cng =
1083 change(user)
1084 |> put_embed(:info, info_cng)
1085
1086 update_and_set_cache(cng)
1087 end
1088
1089 def unblock_domain(user, domain) do
1090 info_cng =
1091 user.info
1092 |> User.Info.remove_from_domain_block(domain)
1093
1094 cng =
1095 change(user)
1096 |> put_embed(:info, info_cng)
1097
1098 update_and_set_cache(cng)
1099 end
1100
1101 def deactivate_async(user, status \\ true) do
1102 BackgroundWorker.enqueue("deactivate_user", %{"user_id" => user.id, "status" => status})
1103 end
1104
1105 def deactivate(%User{} = user, status \\ true) do
1106 info_cng = User.Info.set_activation_status(user.info, status)
1107
1108 with {:ok, friends} <- User.get_friends(user),
1109 {:ok, followers} <- User.get_followers(user),
1110 {:ok, user} <-
1111 user
1112 |> change()
1113 |> put_embed(:info, info_cng)
1114 |> update_and_set_cache() do
1115 Enum.each(followers, &invalidate_cache(&1))
1116 Enum.each(friends, &update_follower_count(&1))
1117
1118 {:ok, user}
1119 end
1120 end
1121
1122 def update_notification_settings(%User{} = user, settings \\ %{}) do
1123 info_changeset = User.Info.update_notification_settings(user.info, settings)
1124
1125 change(user)
1126 |> put_embed(:info, info_changeset)
1127 |> update_and_set_cache()
1128 end
1129
1130 def delete(%User{} = user) do
1131 BackgroundWorker.enqueue("delete_user", %{"user_id" => user.id})
1132 end
1133
1134 @spec perform(atom(), User.t()) :: {:ok, User.t()}
1135 def perform(:delete, %User{} = user) do
1136 {:ok, _user} = ActivityPub.delete(user)
1137
1138 # Remove all relationships
1139 {:ok, followers} = User.get_followers(user)
1140
1141 Enum.each(followers, fn follower ->
1142 ActivityPub.unfollow(follower, user)
1143 User.unfollow(follower, user)
1144 end)
1145
1146 {:ok, friends} = User.get_friends(user)
1147
1148 Enum.each(friends, fn followed ->
1149 ActivityPub.unfollow(user, followed)
1150 User.unfollow(user, followed)
1151 end)
1152
1153 delete_user_activities(user)
1154 invalidate_cache(user)
1155 Repo.delete(user)
1156 end
1157
1158 @spec perform(atom(), User.t()) :: {:ok, User.t()}
1159 def perform(:fetch_initial_posts, %User{} = user) do
1160 pages = Pleroma.Config.get!([:fetch_initial_posts, :pages])
1161
1162 Enum.each(
1163 # Insert all the posts in reverse order, so they're in the right order on the timeline
1164 Enum.reverse(Utils.fetch_ordered_collection(user.info.source_data["outbox"], pages)),
1165 &Pleroma.Web.Federator.incoming_ap_doc/1
1166 )
1167
1168 {:ok, user}
1169 end
1170
1171 def perform(:deactivate_async, user, status), do: deactivate(user, status)
1172
1173 @spec perform(atom(), User.t(), list()) :: list() | {:error, any()}
1174 def perform(:blocks_import, %User{} = blocker, blocked_identifiers)
1175 when is_list(blocked_identifiers) do
1176 Enum.map(
1177 blocked_identifiers,
1178 fn blocked_identifier ->
1179 with {:ok, %User{} = blocked} <- get_or_fetch(blocked_identifier),
1180 {:ok, blocker} <- block(blocker, blocked),
1181 {:ok, _} <- ActivityPub.block(blocker, blocked) do
1182 blocked
1183 else
1184 err ->
1185 Logger.debug("blocks_import failed for #{blocked_identifier} with: #{inspect(err)}")
1186 err
1187 end
1188 end
1189 )
1190 end
1191
1192 @spec perform(atom(), User.t(), list()) :: list() | {:error, any()}
1193 def perform(:follow_import, %User{} = follower, followed_identifiers)
1194 when is_list(followed_identifiers) do
1195 Enum.map(
1196 followed_identifiers,
1197 fn followed_identifier ->
1198 with {:ok, %User{} = followed} <- get_or_fetch(followed_identifier),
1199 {:ok, follower} <- maybe_direct_follow(follower, followed),
1200 {:ok, _} <- ActivityPub.follow(follower, followed) do
1201 followed
1202 else
1203 err ->
1204 Logger.debug("follow_import failed for #{followed_identifier} with: #{inspect(err)}")
1205 err
1206 end
1207 end
1208 )
1209 end
1210
1211 @spec external_users_query() :: Ecto.Query.t()
1212 def external_users_query do
1213 User.Query.build(%{
1214 external: true,
1215 active: true,
1216 order_by: :id
1217 })
1218 end
1219
1220 @spec external_users(keyword()) :: [User.t()]
1221 def external_users(opts \\ []) do
1222 query =
1223 external_users_query()
1224 |> select([u], struct(u, [:id, :ap_id, :info]))
1225
1226 query =
1227 if opts[:max_id],
1228 do: where(query, [u], u.id > ^opts[:max_id]),
1229 else: query
1230
1231 query =
1232 if opts[:limit],
1233 do: limit(query, ^opts[:limit]),
1234 else: query
1235
1236 Repo.all(query)
1237 end
1238
1239 def blocks_import(%User{} = blocker, blocked_identifiers) when is_list(blocked_identifiers) do
1240 BackgroundWorker.enqueue("blocks_import", %{
1241 "blocker_id" => blocker.id,
1242 "blocked_identifiers" => blocked_identifiers
1243 })
1244 end
1245
1246 def follow_import(%User{} = follower, followed_identifiers)
1247 when is_list(followed_identifiers) do
1248 BackgroundWorker.enqueue("follow_import", %{
1249 "follower_id" => follower.id,
1250 "followed_identifiers" => followed_identifiers
1251 })
1252 end
1253
1254 def delete_user_activities(%User{ap_id: ap_id} = user) do
1255 ap_id
1256 |> Activity.Queries.by_actor()
1257 |> RepoStreamer.chunk_stream(50)
1258 |> Stream.each(fn activities ->
1259 Enum.each(activities, &delete_activity(&1))
1260 end)
1261 |> Stream.run()
1262
1263 {:ok, user}
1264 end
1265
1266 defp delete_activity(%{data: %{"type" => "Create"}} = activity) do
1267 activity
1268 |> Object.normalize()
1269 |> ActivityPub.delete()
1270 end
1271
1272 defp delete_activity(%{data: %{"type" => "Like"}} = activity) do
1273 user = get_cached_by_ap_id(activity.actor)
1274 object = Object.normalize(activity)
1275
1276 ActivityPub.unlike(user, object)
1277 end
1278
1279 defp delete_activity(%{data: %{"type" => "Announce"}} = activity) do
1280 user = get_cached_by_ap_id(activity.actor)
1281 object = Object.normalize(activity)
1282
1283 ActivityPub.unannounce(user, object)
1284 end
1285
1286 defp delete_activity(_activity), do: "Doing nothing"
1287
1288 def html_filter_policy(%User{info: %{no_rich_text: true}}) do
1289 Pleroma.HTML.Scrubber.TwitterText
1290 end
1291
1292 def html_filter_policy(_), do: Pleroma.Config.get([:markup, :scrub_policy])
1293
1294 def fetch_by_ap_id(ap_id) do
1295 ap_try = ActivityPub.make_user_from_ap_id(ap_id)
1296
1297 case ap_try do
1298 {:ok, user} ->
1299 {:ok, user}
1300
1301 _ ->
1302 case OStatus.make_user(ap_id) do
1303 {:ok, user} -> {:ok, user}
1304 _ -> {:error, "Could not fetch by AP id"}
1305 end
1306 end
1307 end
1308
1309 def get_or_fetch_by_ap_id(ap_id) do
1310 user = get_cached_by_ap_id(ap_id)
1311
1312 if !is_nil(user) and !User.needs_update?(user) do
1313 {:ok, user}
1314 else
1315 # Whether to fetch initial posts for the user (if it's a new user & the fetching is enabled)
1316 should_fetch_initial = is_nil(user) and Pleroma.Config.get([:fetch_initial_posts, :enabled])
1317
1318 resp = fetch_by_ap_id(ap_id)
1319
1320 if should_fetch_initial do
1321 with {:ok, %User{} = user} <- resp do
1322 fetch_initial_posts(user)
1323 end
1324 end
1325
1326 resp
1327 end
1328 end
1329
1330 @doc "Creates an internal service actor by URI if missing. Optionally takes nickname for addressing."
1331 def get_or_create_service_actor_by_ap_id(uri, nickname \\ nil) do
1332 if user = get_cached_by_ap_id(uri) do
1333 user
1334 else
1335 changes =
1336 %User{info: %User.Info{}}
1337 |> cast(%{}, [:ap_id, :nickname, :local])
1338 |> put_change(:ap_id, uri)
1339 |> put_change(:nickname, nickname)
1340 |> put_change(:local, true)
1341 |> put_change(:follower_address, uri <> "/followers")
1342
1343 {:ok, user} = Repo.insert(changes)
1344 user
1345 end
1346 end
1347
1348 # AP style
1349 def public_key_from_info(%{
1350 source_data: %{"publicKey" => %{"publicKeyPem" => public_key_pem}}
1351 }) do
1352 key =
1353 public_key_pem
1354 |> :public_key.pem_decode()
1355 |> hd()
1356 |> :public_key.pem_entry_decode()
1357
1358 {:ok, key}
1359 end
1360
1361 # OStatus Magic Key
1362 def public_key_from_info(%{magic_key: magic_key}) when not is_nil(magic_key) do
1363 {:ok, Pleroma.Web.Salmon.decode_key(magic_key)}
1364 end
1365
1366 def public_key_from_info(_), do: {:error, "not found key"}
1367
1368 def get_public_key_for_ap_id(ap_id) do
1369 with {:ok, %User{} = user} <- get_or_fetch_by_ap_id(ap_id),
1370 {:ok, public_key} <- public_key_from_info(user.info) do
1371 {:ok, public_key}
1372 else
1373 _ -> :error
1374 end
1375 end
1376
1377 defp blank?(""), do: nil
1378 defp blank?(n), do: n
1379
1380 def insert_or_update_user(data) do
1381 data
1382 |> Map.put(:name, blank?(data[:name]) || data[:nickname])
1383 |> remote_user_creation()
1384 |> Repo.insert(on_conflict: :replace_all_except_primary_key, conflict_target: :nickname)
1385 |> set_cache()
1386 end
1387
1388 def ap_enabled?(%User{local: true}), do: true
1389 def ap_enabled?(%User{info: info}), do: info.ap_enabled
1390 def ap_enabled?(_), do: false
1391
1392 @doc "Gets or fetch a user by uri or nickname."
1393 @spec get_or_fetch(String.t()) :: {:ok, User.t()} | {:error, String.t()}
1394 def get_or_fetch("http" <> _host = uri), do: get_or_fetch_by_ap_id(uri)
1395 def get_or_fetch(nickname), do: get_or_fetch_by_nickname(nickname)
1396
1397 # wait a period of time and return newest version of the User structs
1398 # this is because we have synchronous follow APIs and need to simulate them
1399 # with an async handshake
1400 def wait_and_refresh(_, %User{local: true} = a, %User{local: true} = b) do
1401 with %User{} = a <- User.get_cached_by_id(a.id),
1402 %User{} = b <- User.get_cached_by_id(b.id) do
1403 {:ok, a, b}
1404 else
1405 _e ->
1406 :error
1407 end
1408 end
1409
1410 def wait_and_refresh(timeout, %User{} = a, %User{} = b) do
1411 with :ok <- :timer.sleep(timeout),
1412 %User{} = a <- User.get_cached_by_id(a.id),
1413 %User{} = b <- User.get_cached_by_id(b.id) do
1414 {:ok, a, b}
1415 else
1416 _e ->
1417 :error
1418 end
1419 end
1420
1421 def parse_bio(bio) when is_binary(bio) and bio != "" do
1422 bio
1423 |> CommonUtils.format_input("text/plain", mentions_format: :full)
1424 |> elem(0)
1425 end
1426
1427 def parse_bio(_), do: ""
1428
1429 def parse_bio(bio, user) when is_binary(bio) and bio != "" do
1430 # TODO: get profile URLs other than user.ap_id
1431 profile_urls = [user.ap_id]
1432
1433 bio
1434 |> CommonUtils.format_input("text/plain",
1435 mentions_format: :full,
1436 rel: &RelMe.maybe_put_rel_me(&1, profile_urls)
1437 )
1438 |> elem(0)
1439 end
1440
1441 def parse_bio(_, _), do: ""
1442
1443 def tag(user_identifiers, tags) when is_list(user_identifiers) do
1444 Repo.transaction(fn ->
1445 for user_identifier <- user_identifiers, do: tag(user_identifier, tags)
1446 end)
1447 end
1448
1449 def tag(nickname, tags) when is_binary(nickname),
1450 do: tag(get_by_nickname(nickname), tags)
1451
1452 def tag(%User{} = user, tags),
1453 do: update_tags(user, Enum.uniq((user.tags || []) ++ normalize_tags(tags)))
1454
1455 def untag(user_identifiers, tags) when is_list(user_identifiers) do
1456 Repo.transaction(fn ->
1457 for user_identifier <- user_identifiers, do: untag(user_identifier, tags)
1458 end)
1459 end
1460
1461 def untag(nickname, tags) when is_binary(nickname),
1462 do: untag(get_by_nickname(nickname), tags)
1463
1464 def untag(%User{} = user, tags),
1465 do: update_tags(user, (user.tags || []) -- normalize_tags(tags))
1466
1467 defp update_tags(%User{} = user, new_tags) do
1468 {:ok, updated_user} =
1469 user
1470 |> change(%{tags: new_tags})
1471 |> update_and_set_cache()
1472
1473 updated_user
1474 end
1475
1476 defp normalize_tags(tags) do
1477 [tags]
1478 |> List.flatten()
1479 |> Enum.map(&String.downcase(&1))
1480 end
1481
1482 defp local_nickname_regex do
1483 if Pleroma.Config.get([:instance, :extended_nickname_format]) do
1484 @extended_local_nickname_regex
1485 else
1486 @strict_local_nickname_regex
1487 end
1488 end
1489
1490 def local_nickname(nickname_or_mention) do
1491 nickname_or_mention
1492 |> full_nickname()
1493 |> String.split("@")
1494 |> hd()
1495 end
1496
1497 def full_nickname(nickname_or_mention),
1498 do: String.trim_leading(nickname_or_mention, "@")
1499
1500 def error_user(ap_id) do
1501 %User{
1502 name: ap_id,
1503 ap_id: ap_id,
1504 info: %User.Info{},
1505 nickname: "erroruser@example.com",
1506 inserted_at: NaiveDateTime.utc_now()
1507 }
1508 end
1509
1510 @spec all_superusers() :: [User.t()]
1511 def all_superusers do
1512 User.Query.build(%{super_users: true, local: true, deactivated: false})
1513 |> Repo.all()
1514 end
1515
1516 def showing_reblogs?(%User{} = user, %User{} = target) do
1517 target.ap_id not in user.info.muted_reblogs
1518 end
1519
1520 @doc """
1521 The function returns a query to get users with no activity for given interval of days.
1522 Inactive users are those who didn't read any notification, or had any activity where
1523 the user is the activity's actor, during `inactivity_threshold` days.
1524 Deactivated users will not appear in this list.
1525
1526 ## Examples
1527
1528 iex> Pleroma.User.list_inactive_users()
1529 %Ecto.Query{}
1530 """
1531 @spec list_inactive_users_query(integer()) :: Ecto.Query.t()
1532 def list_inactive_users_query(inactivity_threshold \\ 7) do
1533 negative_inactivity_threshold = -inactivity_threshold
1534 now = NaiveDateTime.truncate(NaiveDateTime.utc_now(), :second)
1535 # Subqueries are not supported in `where` clauses, join gets too complicated.
1536 has_read_notifications =
1537 from(n in Pleroma.Notification,
1538 where: n.seen == true,
1539 group_by: n.id,
1540 having: max(n.updated_at) > datetime_add(^now, ^negative_inactivity_threshold, "day"),
1541 select: n.user_id
1542 )
1543 |> Pleroma.Repo.all()
1544
1545 from(u in Pleroma.User,
1546 left_join: a in Pleroma.Activity,
1547 on: u.ap_id == a.actor,
1548 where: not is_nil(u.nickname),
1549 where: fragment("not (?->'deactivated' @> 'true')", u.info),
1550 where: u.id not in ^has_read_notifications,
1551 group_by: u.id,
1552 having:
1553 max(a.inserted_at) < datetime_add(^now, ^negative_inactivity_threshold, "day") or
1554 is_nil(max(a.inserted_at))
1555 )
1556 end
1557
1558 @doc """
1559 Enable or disable email notifications for user
1560
1561 ## Examples
1562
1563 iex> Pleroma.User.switch_email_notifications(Pleroma.User{info: %{email_notifications: %{"digest" => false}}}, "digest", true)
1564 Pleroma.User{info: %{email_notifications: %{"digest" => true}}}
1565
1566 iex> Pleroma.User.switch_email_notifications(Pleroma.User{info: %{email_notifications: %{"digest" => true}}}, "digest", false)
1567 Pleroma.User{info: %{email_notifications: %{"digest" => false}}}
1568 """
1569 @spec switch_email_notifications(t(), String.t(), boolean()) ::
1570 {:ok, t()} | {:error, Ecto.Changeset.t()}
1571 def switch_email_notifications(user, type, status) do
1572 info = Pleroma.User.Info.update_email_notifications(user.info, %{type => status})
1573
1574 change(user)
1575 |> put_embed(:info, info)
1576 |> update_and_set_cache()
1577 end
1578
1579 @doc """
1580 Set `last_digest_emailed_at` value for the user to current time
1581 """
1582 @spec touch_last_digest_emailed_at(t()) :: t()
1583 def touch_last_digest_emailed_at(user) do
1584 now = NaiveDateTime.truncate(NaiveDateTime.utc_now(), :second)
1585
1586 {:ok, updated_user} =
1587 user
1588 |> change(%{last_digest_emailed_at: now})
1589 |> update_and_set_cache()
1590
1591 updated_user
1592 end
1593
1594 @spec toggle_confirmation(User.t()) :: {:ok, User.t()} | {:error, Changeset.t()}
1595 def toggle_confirmation(%User{} = user) do
1596 need_confirmation? = !user.info.confirmation_pending
1597
1598 info_changeset =
1599 User.Info.confirmation_changeset(user.info, need_confirmation: need_confirmation?)
1600
1601 user
1602 |> change()
1603 |> put_embed(:info, info_changeset)
1604 |> update_and_set_cache()
1605 end
1606
1607 def get_mascot(%{info: %{mascot: %{} = mascot}}) when not is_nil(mascot) do
1608 mascot
1609 end
1610
1611 def get_mascot(%{info: %{mascot: mascot}}) when is_nil(mascot) do
1612 # use instance-default
1613 config = Pleroma.Config.get([:assets, :mascots])
1614 default_mascot = Pleroma.Config.get([:assets, :default_mascot])
1615 mascot = Keyword.get(config, default_mascot)
1616
1617 %{
1618 "id" => "default-mascot",
1619 "url" => mascot[:url],
1620 "preview_url" => mascot[:url],
1621 "pleroma" => %{
1622 "mime_type" => mascot[:mime_type]
1623 }
1624 }
1625 end
1626
1627 def ensure_keys_present(%User{info: info} = user) do
1628 if info.keys do
1629 {:ok, user}
1630 else
1631 {:ok, pem} = Keys.generate_rsa_pem()
1632
1633 user
1634 |> Ecto.Changeset.change()
1635 |> Ecto.Changeset.put_embed(:info, User.Info.set_keys(info, pem))
1636 |> update_and_set_cache()
1637 end
1638 end
1639
1640 def get_ap_ids_by_nicknames(nicknames) do
1641 from(u in User,
1642 where: u.nickname in ^nicknames,
1643 select: u.ap_id
1644 )
1645 |> Repo.all()
1646 end
1647
1648 defdelegate search(query, opts \\ []), to: User.Search
1649
1650 defp put_password_hash(
1651 %Ecto.Changeset{valid?: true, changes: %{password: password}} = changeset
1652 ) do
1653 change(changeset, password_hash: Pbkdf2.hashpwsalt(password))
1654 end
1655
1656 defp put_password_hash(changeset), do: changeset
1657
1658 def is_internal_user?(%User{nickname: nil}), do: true
1659 def is_internal_user?(%User{local: true, nickname: "internal." <> _}), do: true
1660 def is_internal_user?(_), do: false
1661
1662 # A hack because user delete activities have a fake id for whatever reason
1663 # TODO: Get rid of this
1664 def get_delivered_users_by_object_id("pleroma:fake_object_id"), do: []
1665
1666 def get_delivered_users_by_object_id(object_id) do
1667 from(u in User,
1668 inner_join: delivery in assoc(u, :deliveries),
1669 where: delivery.object_id == ^object_id
1670 )
1671 |> Repo.all()
1672 end
1673
1674 def change_email(user, email) do
1675 user
1676 |> cast(%{email: email}, [:email])
1677 |> validate_required([:email])
1678 |> unique_constraint(:email)
1679 |> validate_format(:email, @email_regex)
1680 |> update_and_set_cache()
1681 end
1682 end