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