Merge branch 'develop' of https://git.pleroma.social/pleroma/pleroma into develop
[akkoma] / lib / pleroma / user.ex
1 # Pleroma: A lightweight social networking server
2 # Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
3 # SPDX-License-Identifier: AGPL-3.0-only
4
5 defmodule Pleroma.User do
6 use Ecto.Schema
7
8 import Ecto.Changeset
9 import Ecto.Query
10
11 alias Comeonin.Pbkdf2
12 alias Ecto.Multi
13 alias Pleroma.Activity
14 alias Pleroma.Delivery
15 alias Pleroma.Keys
16 alias Pleroma.Notification
17 alias Pleroma.Object
18 alias Pleroma.Registration
19 alias Pleroma.Repo
20 alias Pleroma.RepoStreamer
21 alias Pleroma.User
22 alias Pleroma.Web
23 alias Pleroma.Web.ActivityPub.ActivityPub
24 alias Pleroma.Web.ActivityPub.Utils
25 alias Pleroma.Web.CommonAPI
26 alias Pleroma.Web.CommonAPI.Utils, as: CommonUtils
27 alias Pleroma.Web.OAuth
28 alias Pleroma.Web.OStatus
29 alias Pleroma.Web.RelMe
30 alias Pleroma.Web.Websub
31 alias Pleroma.Workers.BackgroundWorker
32
33 require Logger
34
35 @type t :: %__MODULE__{}
36
37 @primary_key {:id, Pleroma.FlakeId, autogenerate: true}
38
39 # credo:disable-for-next-line Credo.Check.Readability.MaxLineLength
40 @email_regex ~r/^[a-zA-Z0-9.!#$%&'*+\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/
41
42 @strict_local_nickname_regex ~r/^[a-zA-Z\d]+$/
43 @extended_local_nickname_regex ~r/^[a-zA-Z\d_-]+$/
44
45 schema "users" do
46 field(:bio, :string)
47 field(:email, :string)
48 field(:name, :string)
49 field(:nickname, :string)
50 field(:password_hash, :string)
51 field(:password, :string, virtual: true)
52 field(:password_confirmation, :string, virtual: true)
53 field(:following, {:array, :string}, default: [])
54 field(:ap_id, :string)
55 field(:avatar, :map)
56 field(:local, :boolean, default: true)
57 field(:follower_address, :string)
58 field(:following_address, :string)
59 field(:search_rank, :float, virtual: true)
60 field(:search_type, :integer, virtual: true)
61 field(:tags, {:array, :string}, default: [])
62 field(:last_refreshed_at, :naive_datetime_usec)
63 field(:last_digest_emailed_at, :naive_datetime)
64 has_many(:notifications, Notification)
65 has_many(:registrations, Registration)
66 has_many(:deliveries, Delivery)
67 embeds_one(:info, User.Info)
68
69 timestamps()
70 end
71
72 def auth_active?(%User{info: %User.Info{confirmation_pending: true}}),
73 do: !Pleroma.Config.get([:instance, :account_activation_required])
74
75 def auth_active?(%User{}), do: true
76
77 def visible_for?(user, for_user \\ nil)
78
79 def visible_for?(%User{id: user_id}, %User{id: for_id}) when user_id == for_id, do: true
80
81 def visible_for?(%User{} = user, for_user) do
82 auth_active?(user) || superuser?(for_user)
83 end
84
85 def visible_for?(_, _), do: false
86
87 def superuser?(%User{local: true, info: %User.Info{is_admin: true}}), do: true
88 def superuser?(%User{local: true, info: %User.Info{is_moderator: true}}), do: true
89 def superuser?(_), do: false
90
91 def avatar_url(user, options \\ []) do
92 case user.avatar do
93 %{"url" => [%{"href" => href} | _]} -> href
94 _ -> !options[:no_default] && "#{Web.base_url()}/images/avi.png"
95 end
96 end
97
98 def banner_url(user, options \\ []) do
99 case user.info.banner do
100 %{"url" => [%{"href" => href} | _]} -> href
101 _ -> !options[:no_default] && "#{Web.base_url()}/images/banner.png"
102 end
103 end
104
105 def profile_url(%User{info: %{source_data: %{"url" => url}}}), do: url
106 def profile_url(%User{ap_id: ap_id}), do: ap_id
107 def profile_url(_), do: nil
108
109 def ap_id(%User{nickname: nickname}) do
110 "#{Web.base_url()}/users/#{nickname}"
111 end
112
113 def ap_followers(%User{follower_address: fa}) when is_binary(fa), do: fa
114 def ap_followers(%User{} = user), do: "#{ap_id(user)}/followers"
115
116 @spec ap_following(User.t()) :: Sring.t()
117 def ap_following(%User{following_address: fa}) when is_binary(fa), do: fa
118 def ap_following(%User{} = user), do: "#{ap_id(user)}/following"
119
120 def user_info(%User{} = user, args \\ %{}) do
121 following_count =
122 if args[:following_count],
123 do: args[:following_count],
124 else: user.info.following_count || following_count(user)
125
126 follower_count =
127 if args[:follower_count], do: args[:follower_count], else: user.info.follower_count
128
129 %{
130 note_count: user.info.note_count,
131 locked: user.info.locked,
132 confirmation_pending: user.info.confirmation_pending,
133 default_scope: user.info.default_scope
134 }
135 |> Map.put(:following_count, following_count)
136 |> Map.put(:follower_count, follower_count)
137 end
138
139 def follow_state(%User{} = user, %User{} = target) do
140 follow_activity = Utils.fetch_latest_follow(user, target)
141
142 if follow_activity,
143 do: follow_activity.data["state"],
144 # Ideally this would be nil, but then Cachex does not commit the value
145 else: false
146 end
147
148 def get_cached_follow_state(user, target) do
149 key = "follow_state:#{user.ap_id}|#{target.ap_id}"
150 Cachex.fetch!(:user_cache, key, fn _ -> {:commit, follow_state(user, target)} end)
151 end
152
153 @spec set_follow_state_cache(String.t(), String.t(), String.t()) :: {:ok | :error, boolean()}
154 def set_follow_state_cache(user_ap_id, target_ap_id, state) do
155 Cachex.put(
156 :user_cache,
157 "follow_state:#{user_ap_id}|#{target_ap_id}",
158 state
159 )
160 end
161
162 def set_info_cache(user, args) do
163 Cachex.put(:user_cache, "user_info:#{user.id}", user_info(user, args))
164 end
165
166 @spec restrict_deactivated(Ecto.Query.t()) :: Ecto.Query.t()
167 def restrict_deactivated(query) do
168 from(u in query,
169 where: not fragment("? \\? 'deactivated' AND ?->'deactivated' @> 'true'", u.info, u.info)
170 )
171 end
172
173 def following_count(%User{following: []}), do: 0
174
175 def following_count(%User{} = user) do
176 user
177 |> get_friends_query()
178 |> Repo.aggregate(:count, :id)
179 end
180
181 defp truncate_if_exists(params, key, max_length) do
182 if Map.has_key?(params, key) and is_binary(params[key]) do
183 {value, _chopped} = String.split_at(params[key], max_length)
184 Map.put(params, key, value)
185 else
186 params
187 end
188 end
189
190 def remote_user_creation(params) do
191 bio_limit = Pleroma.Config.get([:instance, :user_bio_length], 5000)
192 name_limit = Pleroma.Config.get([:instance, :user_name_length], 100)
193
194 params =
195 params
196 |> Map.put(:info, params[:info] || %{})
197 |> truncate_if_exists(:name, name_limit)
198 |> truncate_if_exists(:bio, bio_limit)
199
200 info_cng = User.Info.remote_user_creation(%User.Info{}, params[:info])
201
202 changes =
203 %User{}
204 |> cast(params, [:bio, :name, :ap_id, :nickname, :avatar])
205 |> validate_required([:name, :ap_id])
206 |> unique_constraint(:nickname)
207 |> validate_format(:nickname, @email_regex)
208 |> validate_length(:bio, max: bio_limit)
209 |> validate_length(:name, max: name_limit)
210 |> put_change(:local, false)
211 |> put_embed(:info, info_cng)
212
213 if changes.valid? do
214 case info_cng.changes[:source_data] do
215 %{"followers" => followers, "following" => following} ->
216 changes
217 |> put_change(:follower_address, followers)
218 |> put_change(:following_address, following)
219
220 _ ->
221 followers = User.ap_followers(%User{nickname: changes.changes[:nickname]})
222
223 changes
224 |> put_change(:follower_address, followers)
225 end
226 else
227 changes
228 end
229 end
230
231 def update_changeset(struct, params \\ %{}) do
232 bio_limit = Pleroma.Config.get([:instance, :user_bio_length], 5000)
233 name_limit = Pleroma.Config.get([:instance, :user_name_length], 100)
234
235 struct
236 |> cast(params, [:bio, :name, :avatar, :following])
237 |> unique_constraint(:nickname)
238 |> validate_format(:nickname, local_nickname_regex())
239 |> validate_length(:bio, max: bio_limit)
240 |> validate_length(:name, min: 1, max: name_limit)
241 end
242
243 def upgrade_changeset(struct, params \\ %{}, remote? \\ false) do
244 bio_limit = Pleroma.Config.get([:instance, :user_bio_length], 5000)
245 name_limit = Pleroma.Config.get([:instance, :user_name_length], 100)
246
247 params = Map.put(params, :last_refreshed_at, NaiveDateTime.utc_now())
248 info_cng = User.Info.user_upgrade(struct.info, params[:info], remote?)
249
250 struct
251 |> cast(params, [
252 :bio,
253 :name,
254 :follower_address,
255 :following_address,
256 :avatar,
257 :last_refreshed_at
258 ])
259 |> unique_constraint(:nickname)
260 |> validate_format(:nickname, local_nickname_regex())
261 |> validate_length(:bio, max: bio_limit)
262 |> validate_length(:name, max: name_limit)
263 |> put_embed(:info, info_cng)
264 end
265
266 def password_update_changeset(struct, params) do
267 struct
268 |> cast(params, [:password, :password_confirmation])
269 |> validate_required([:password, :password_confirmation])
270 |> validate_confirmation(:password)
271 |> put_password_hash
272 |> 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 q = get_followers_query(user, page)
689
690 {:ok, Repo.all(q)}
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 q = get_friends_query(user, page)
724
725 {:ok, Repo.all(q)}
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 users =
737 Activity.follow_requests_for_actor(user)
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
744 {:ok, users}
745 end
746
747 def increase_note_count(%User{} = user) do
748 User
749 |> where(id: ^user.id)
750 |> update([u],
751 set: [
752 info:
753 fragment(
754 "jsonb_set(?, '{note_count}', ((?->>'note_count')::int + 1)::varchar::jsonb, true)",
755 u.info,
756 u.info
757 )
758 ]
759 )
760 |> select([u], u)
761 |> Repo.update_all([])
762 |> case do
763 {1, [user]} -> set_cache(user)
764 _ -> {:error, user}
765 end
766 end
767
768 def decrease_note_count(%User{} = user) do
769 User
770 |> where(id: ^user.id)
771 |> update([u],
772 set: [
773 info:
774 fragment(
775 "jsonb_set(?, '{note_count}', (greatest(0, (?->>'note_count')::int - 1))::varchar::jsonb, true)",
776 u.info,
777 u.info
778 )
779 ]
780 )
781 |> select([u], u)
782 |> Repo.update_all([])
783 |> case do
784 {1, [user]} -> set_cache(user)
785 _ -> {:error, user}
786 end
787 end
788
789 def update_note_count(%User{} = user) do
790 note_count_query =
791 from(
792 a in Object,
793 where: fragment("?->>'actor' = ? and ?->>'type' = 'Note'", a.data, ^user.ap_id, a.data),
794 select: count(a.id)
795 )
796
797 note_count = Repo.one(note_count_query)
798
799 info_cng = User.Info.set_note_count(user.info, note_count)
800
801 user
802 |> change()
803 |> put_embed(:info, info_cng)
804 |> update_and_set_cache()
805 end
806
807 @spec maybe_fetch_follow_information(User.t()) :: User.t()
808 def maybe_fetch_follow_information(user) do
809 with {:ok, user} <- fetch_follow_information(user) do
810 user
811 else
812 e ->
813 Logger.error("Follower/Following counter update for #{user.ap_id} failed.\n#{inspect(e)}")
814
815 user
816 end
817 end
818
819 def fetch_follow_information(user) do
820 with {:ok, info} <- ActivityPub.fetch_follow_information_for_user(user) do
821 info_cng = User.Info.follow_information_update(user.info, info)
822
823 changeset =
824 user
825 |> change()
826 |> put_embed(:info, info_cng)
827
828 update_and_set_cache(changeset)
829 else
830 {:error, _} = e -> e
831 e -> {:error, e}
832 end
833 end
834
835 def update_follower_count(%User{} = user) do
836 if user.local or !Pleroma.Config.get([:instance, :external_user_synchronization]) do
837 follower_count_query =
838 User.Query.build(%{followers: user, deactivated: false})
839 |> select([u], %{count: count(u.id)})
840
841 User
842 |> where(id: ^user.id)
843 |> join(:inner, [u], s in subquery(follower_count_query))
844 |> update([u, s],
845 set: [
846 info:
847 fragment(
848 "jsonb_set(?, '{follower_count}', ?::varchar::jsonb, true)",
849 u.info,
850 s.count
851 )
852 ]
853 )
854 |> select([u], u)
855 |> Repo.update_all([])
856 |> case do
857 {1, [user]} -> set_cache(user)
858 _ -> {:error, user}
859 end
860 else
861 {:ok, maybe_fetch_follow_information(user)}
862 end
863 end
864
865 @spec maybe_update_following_count(User.t()) :: User.t()
866 def maybe_update_following_count(%User{local: false} = user) do
867 if Pleroma.Config.get([:instance, :external_user_synchronization]) do
868 maybe_fetch_follow_information(user)
869 else
870 user
871 end
872 end
873
874 def maybe_update_following_count(user), do: user
875
876 def remove_duplicated_following(%User{following: following} = user) do
877 uniq_following = Enum.uniq(following)
878
879 if length(following) == length(uniq_following) do
880 {:ok, user}
881 else
882 user
883 |> update_changeset(%{following: uniq_following})
884 |> update_and_set_cache()
885 end
886 end
887
888 @spec get_users_from_set([String.t()], boolean()) :: [User.t()]
889 def get_users_from_set(ap_ids, local_only \\ true) do
890 criteria = %{ap_id: ap_ids, deactivated: false}
891 criteria = if local_only, do: Map.put(criteria, :local, true), else: criteria
892
893 User.Query.build(criteria)
894 |> Repo.all()
895 end
896
897 @spec get_recipients_from_activity(Activity.t()) :: [User.t()]
898 def get_recipients_from_activity(%Activity{recipients: to}) do
899 User.Query.build(%{recipients_from_activity: to, local: true, deactivated: false})
900 |> Repo.all()
901 end
902
903 @spec mute(User.t(), User.t(), boolean()) :: {:ok, User.t()} | {:error, String.t()}
904 def mute(muter, %User{ap_id: ap_id}, notifications? \\ true) do
905 info = muter.info
906
907 info_cng =
908 User.Info.add_to_mutes(info, ap_id)
909 |> User.Info.add_to_muted_notifications(info, ap_id, notifications?)
910
911 cng =
912 change(muter)
913 |> put_embed(:info, info_cng)
914
915 update_and_set_cache(cng)
916 end
917
918 def unmute(muter, %{ap_id: ap_id}) do
919 info = muter.info
920
921 info_cng =
922 User.Info.remove_from_mutes(info, ap_id)
923 |> User.Info.remove_from_muted_notifications(info, ap_id)
924
925 cng =
926 change(muter)
927 |> put_embed(:info, info_cng)
928
929 update_and_set_cache(cng)
930 end
931
932 def subscribe(subscriber, %{ap_id: ap_id}) do
933 deny_follow_blocked = Pleroma.Config.get([:user, :deny_follow_blocked])
934
935 with %User{} = subscribed <- get_cached_by_ap_id(ap_id) do
936 blocked = blocks?(subscribed, subscriber) and deny_follow_blocked
937
938 if blocked do
939 {:error, "Could not subscribe: #{subscribed.nickname} is blocking you"}
940 else
941 info_cng =
942 subscribed.info
943 |> User.Info.add_to_subscribers(subscriber.ap_id)
944
945 change(subscribed)
946 |> put_embed(:info, info_cng)
947 |> update_and_set_cache()
948 end
949 end
950 end
951
952 def unsubscribe(unsubscriber, %{ap_id: ap_id}) do
953 with %User{} = user <- get_cached_by_ap_id(ap_id) do
954 info_cng =
955 user.info
956 |> User.Info.remove_from_subscribers(unsubscriber.ap_id)
957
958 change(user)
959 |> put_embed(:info, info_cng)
960 |> update_and_set_cache()
961 end
962 end
963
964 def block(blocker, %User{ap_id: ap_id} = blocked) do
965 # sever any follow relationships to prevent leaks per activitypub (Pleroma issue #213)
966 blocker =
967 if following?(blocker, blocked) do
968 {:ok, blocker, _} = unfollow(blocker, blocked)
969 blocker
970 else
971 blocker
972 end
973
974 # clear any requested follows as well
975 blocked =
976 case CommonAPI.reject_follow_request(blocked, blocker) do
977 {:ok, %User{} = updated_blocked} -> updated_blocked
978 nil -> blocked
979 end
980
981 blocker =
982 if subscribed_to?(blocked, blocker) do
983 {:ok, blocker} = unsubscribe(blocked, blocker)
984 blocker
985 else
986 blocker
987 end
988
989 if following?(blocked, blocker) do
990 unfollow(blocked, blocker)
991 end
992
993 {:ok, blocker} = update_follower_count(blocker)
994
995 info_cng =
996 blocker.info
997 |> User.Info.add_to_block(ap_id)
998
999 cng =
1000 change(blocker)
1001 |> put_embed(:info, info_cng)
1002
1003 update_and_set_cache(cng)
1004 end
1005
1006 # helper to handle the block given only an actor's AP id
1007 def block(blocker, %{ap_id: ap_id}) do
1008 block(blocker, get_cached_by_ap_id(ap_id))
1009 end
1010
1011 def unblock(blocker, %{ap_id: ap_id}) do
1012 info_cng =
1013 blocker.info
1014 |> User.Info.remove_from_block(ap_id)
1015
1016 cng =
1017 change(blocker)
1018 |> put_embed(:info, info_cng)
1019
1020 update_and_set_cache(cng)
1021 end
1022
1023 def mutes?(nil, _), do: false
1024 def mutes?(user, %{ap_id: ap_id}), do: Enum.member?(user.info.mutes, ap_id)
1025
1026 @spec muted_notifications?(User.t() | nil, User.t() | map()) :: boolean()
1027 def muted_notifications?(nil, _), do: false
1028
1029 def muted_notifications?(user, %{ap_id: ap_id}),
1030 do: Enum.member?(user.info.muted_notifications, ap_id)
1031
1032 def blocks?(%User{} = user, %User{} = target) do
1033 blocks_ap_id?(user, target) || blocks_domain?(user, target)
1034 end
1035
1036 def blocks?(nil, _), do: false
1037
1038 def blocks_ap_id?(%User{} = user, %User{} = target) do
1039 Enum.member?(user.info.blocks, target.ap_id)
1040 end
1041
1042 def blocks_ap_id?(_, _), do: false
1043
1044 def blocks_domain?(%User{} = user, %User{} = target) do
1045 domain_blocks = Pleroma.Web.ActivityPub.MRF.subdomains_regex(user.info.domain_blocks)
1046 %{host: host} = URI.parse(target.ap_id)
1047 Pleroma.Web.ActivityPub.MRF.subdomain_match?(domain_blocks, host)
1048 end
1049
1050 def blocks_domain?(_, _), do: false
1051
1052 def subscribed_to?(user, %{ap_id: ap_id}) do
1053 with %User{} = target <- get_cached_by_ap_id(ap_id) do
1054 Enum.member?(target.info.subscribers, user.ap_id)
1055 end
1056 end
1057
1058 @spec muted_users(User.t()) :: [User.t()]
1059 def muted_users(user) do
1060 User.Query.build(%{ap_id: user.info.mutes, deactivated: false})
1061 |> Repo.all()
1062 end
1063
1064 @spec blocked_users(User.t()) :: [User.t()]
1065 def blocked_users(user) do
1066 User.Query.build(%{ap_id: user.info.blocks, deactivated: false})
1067 |> Repo.all()
1068 end
1069
1070 @spec subscribers(User.t()) :: [User.t()]
1071 def subscribers(user) do
1072 User.Query.build(%{ap_id: user.info.subscribers, deactivated: false})
1073 |> Repo.all()
1074 end
1075
1076 def block_domain(user, domain) do
1077 info_cng =
1078 user.info
1079 |> User.Info.add_to_domain_block(domain)
1080
1081 cng =
1082 change(user)
1083 |> put_embed(:info, info_cng)
1084
1085 update_and_set_cache(cng)
1086 end
1087
1088 def unblock_domain(user, domain) do
1089 info_cng =
1090 user.info
1091 |> User.Info.remove_from_domain_block(domain)
1092
1093 cng =
1094 change(user)
1095 |> put_embed(:info, info_cng)
1096
1097 update_and_set_cache(cng)
1098 end
1099
1100 def deactivate_async(user, status \\ true) do
1101 BackgroundWorker.enqueue("deactivate_user", %{"user_id" => user.id, "status" => status})
1102 end
1103
1104 def deactivate(%User{} = user, status \\ true) do
1105 info_cng = User.Info.set_activation_status(user.info, status)
1106
1107 with {:ok, friends} <- User.get_friends(user),
1108 {:ok, followers} <- User.get_followers(user),
1109 {:ok, user} <-
1110 user
1111 |> change()
1112 |> put_embed(:info, info_cng)
1113 |> update_and_set_cache() do
1114 Enum.each(followers, &invalidate_cache(&1))
1115 Enum.each(friends, &update_follower_count(&1))
1116
1117 {:ok, user}
1118 end
1119 end
1120
1121 def update_notification_settings(%User{} = user, settings \\ %{}) do
1122 info_changeset = User.Info.update_notification_settings(user.info, settings)
1123
1124 change(user)
1125 |> put_embed(:info, info_changeset)
1126 |> update_and_set_cache()
1127 end
1128
1129 def delete(%User{} = user) do
1130 BackgroundWorker.enqueue("delete_user", %{"user_id" => user.id})
1131 end
1132
1133 def perform(:force_password_reset, user), do: force_password_reset(user)
1134
1135 @spec perform(atom(), User.t()) :: {:ok, User.t()}
1136 def perform(:delete, %User{} = user) do
1137 {:ok, _user} = ActivityPub.delete(user)
1138
1139 # Remove all relationships
1140 {:ok, followers} = User.get_followers(user)
1141
1142 Enum.each(followers, fn follower ->
1143 ActivityPub.unfollow(follower, user)
1144 User.unfollow(follower, user)
1145 end)
1146
1147 {:ok, friends} = User.get_friends(user)
1148
1149 Enum.each(friends, fn followed ->
1150 ActivityPub.unfollow(user, followed)
1151 User.unfollow(user, followed)
1152 end)
1153
1154 delete_user_activities(user)
1155 invalidate_cache(user)
1156 Repo.delete(user)
1157 end
1158
1159 @spec perform(atom(), User.t()) :: {:ok, User.t()}
1160 def perform(:fetch_initial_posts, %User{} = user) do
1161 pages = Pleroma.Config.get!([:fetch_initial_posts, :pages])
1162
1163 Enum.each(
1164 # Insert all the posts in reverse order, so they're in the right order on the timeline
1165 Enum.reverse(Utils.fetch_ordered_collection(user.info.source_data["outbox"], pages)),
1166 &Pleroma.Web.Federator.incoming_ap_doc/1
1167 )
1168
1169 {:ok, user}
1170 end
1171
1172 def perform(:deactivate_async, user, status), do: deactivate(user, status)
1173
1174 @spec perform(atom(), User.t(), list()) :: list() | {:error, any()}
1175 def perform(:blocks_import, %User{} = blocker, blocked_identifiers)
1176 when is_list(blocked_identifiers) do
1177 Enum.map(
1178 blocked_identifiers,
1179 fn blocked_identifier ->
1180 with {:ok, %User{} = blocked} <- get_or_fetch(blocked_identifier),
1181 {:ok, blocker} <- block(blocker, blocked),
1182 {:ok, _} <- ActivityPub.block(blocker, blocked) do
1183 blocked
1184 else
1185 err ->
1186 Logger.debug("blocks_import failed for #{blocked_identifier} with: #{inspect(err)}")
1187 err
1188 end
1189 end
1190 )
1191 end
1192
1193 @spec perform(atom(), User.t(), list()) :: list() | {:error, any()}
1194 def perform(:follow_import, %User{} = follower, followed_identifiers)
1195 when is_list(followed_identifiers) do
1196 Enum.map(
1197 followed_identifiers,
1198 fn followed_identifier ->
1199 with {:ok, %User{} = followed} <- get_or_fetch(followed_identifier),
1200 {:ok, follower} <- maybe_direct_follow(follower, followed),
1201 {:ok, _} <- ActivityPub.follow(follower, followed) do
1202 followed
1203 else
1204 err ->
1205 Logger.debug("follow_import failed for #{followed_identifier} with: #{inspect(err)}")
1206 err
1207 end
1208 end
1209 )
1210 end
1211
1212 @spec external_users_query() :: Ecto.Query.t()
1213 def external_users_query do
1214 User.Query.build(%{
1215 external: true,
1216 active: true,
1217 order_by: :id
1218 })
1219 end
1220
1221 @spec external_users(keyword()) :: [User.t()]
1222 def external_users(opts \\ []) do
1223 query =
1224 external_users_query()
1225 |> select([u], struct(u, [:id, :ap_id, :info]))
1226
1227 query =
1228 if opts[:max_id],
1229 do: where(query, [u], u.id > ^opts[:max_id]),
1230 else: query
1231
1232 query =
1233 if opts[:limit],
1234 do: limit(query, ^opts[:limit]),
1235 else: query
1236
1237 Repo.all(query)
1238 end
1239
1240 def blocks_import(%User{} = blocker, blocked_identifiers) when is_list(blocked_identifiers) do
1241 BackgroundWorker.enqueue("blocks_import", %{
1242 "blocker_id" => blocker.id,
1243 "blocked_identifiers" => blocked_identifiers
1244 })
1245 end
1246
1247 def follow_import(%User{} = follower, followed_identifiers)
1248 when is_list(followed_identifiers) do
1249 BackgroundWorker.enqueue("follow_import", %{
1250 "follower_id" => follower.id,
1251 "followed_identifiers" => followed_identifiers
1252 })
1253 end
1254
1255 def delete_user_activities(%User{ap_id: ap_id} = user) do
1256 ap_id
1257 |> Activity.Queries.by_actor()
1258 |> RepoStreamer.chunk_stream(50)
1259 |> Stream.each(fn activities ->
1260 Enum.each(activities, &delete_activity(&1))
1261 end)
1262 |> Stream.run()
1263
1264 {:ok, user}
1265 end
1266
1267 defp delete_activity(%{data: %{"type" => "Create"}} = activity) do
1268 activity
1269 |> Object.normalize()
1270 |> ActivityPub.delete()
1271 end
1272
1273 defp delete_activity(%{data: %{"type" => "Like"}} = activity) do
1274 user = get_cached_by_ap_id(activity.actor)
1275 object = Object.normalize(activity)
1276
1277 ActivityPub.unlike(user, object)
1278 end
1279
1280 defp delete_activity(%{data: %{"type" => "Announce"}} = activity) do
1281 user = get_cached_by_ap_id(activity.actor)
1282 object = Object.normalize(activity)
1283
1284 ActivityPub.unannounce(user, object)
1285 end
1286
1287 defp delete_activity(_activity), do: "Doing nothing"
1288
1289 def html_filter_policy(%User{info: %{no_rich_text: true}}) do
1290 Pleroma.HTML.Scrubber.TwitterText
1291 end
1292
1293 def html_filter_policy(_), do: Pleroma.Config.get([:markup, :scrub_policy])
1294
1295 def fetch_by_ap_id(ap_id) do
1296 ap_try = ActivityPub.make_user_from_ap_id(ap_id)
1297
1298 case ap_try do
1299 {:ok, user} ->
1300 {:ok, user}
1301
1302 _ ->
1303 case OStatus.make_user(ap_id) do
1304 {:ok, user} -> {:ok, user}
1305 _ -> {:error, "Could not fetch by AP id"}
1306 end
1307 end
1308 end
1309
1310 def get_or_fetch_by_ap_id(ap_id) do
1311 user = get_cached_by_ap_id(ap_id)
1312
1313 if !is_nil(user) and !User.needs_update?(user) do
1314 {:ok, user}
1315 else
1316 # Whether to fetch initial posts for the user (if it's a new user & the fetching is enabled)
1317 should_fetch_initial = is_nil(user) and Pleroma.Config.get([:fetch_initial_posts, :enabled])
1318
1319 resp = fetch_by_ap_id(ap_id)
1320
1321 if should_fetch_initial do
1322 with {:ok, %User{} = user} <- resp do
1323 fetch_initial_posts(user)
1324 end
1325 end
1326
1327 resp
1328 end
1329 end
1330
1331 @doc "Creates an internal service actor by URI if missing. Optionally takes nickname for addressing."
1332 def get_or_create_service_actor_by_ap_id(uri, nickname \\ nil) do
1333 if user = get_cached_by_ap_id(uri) do
1334 user
1335 else
1336 changes =
1337 %User{info: %User.Info{}}
1338 |> cast(%{}, [:ap_id, :nickname, :local])
1339 |> put_change(:ap_id, uri)
1340 |> put_change(:nickname, nickname)
1341 |> put_change(:local, true)
1342 |> put_change(:follower_address, uri <> "/followers")
1343
1344 {:ok, user} = Repo.insert(changes)
1345 user
1346 end
1347 end
1348
1349 # AP style
1350 def public_key_from_info(%{
1351 source_data: %{"publicKey" => %{"publicKeyPem" => public_key_pem}}
1352 }) do
1353 key =
1354 public_key_pem
1355 |> :public_key.pem_decode()
1356 |> hd()
1357 |> :public_key.pem_entry_decode()
1358
1359 {:ok, key}
1360 end
1361
1362 # OStatus Magic Key
1363 def public_key_from_info(%{magic_key: magic_key}) when not is_nil(magic_key) do
1364 {:ok, Pleroma.Web.Salmon.decode_key(magic_key)}
1365 end
1366
1367 def public_key_from_info(_), do: {:error, "not found key"}
1368
1369 def get_public_key_for_ap_id(ap_id) do
1370 with {:ok, %User{} = user} <- get_or_fetch_by_ap_id(ap_id),
1371 {:ok, public_key} <- public_key_from_info(user.info) do
1372 {:ok, public_key}
1373 else
1374 _ -> :error
1375 end
1376 end
1377
1378 defp blank?(""), do: nil
1379 defp blank?(n), do: n
1380
1381 def insert_or_update_user(data) do
1382 data
1383 |> Map.put(:name, blank?(data[:name]) || data[:nickname])
1384 |> remote_user_creation()
1385 |> Repo.insert(on_conflict: :replace_all_except_primary_key, conflict_target: :nickname)
1386 |> set_cache()
1387 end
1388
1389 def ap_enabled?(%User{local: true}), do: true
1390 def ap_enabled?(%User{info: info}), do: info.ap_enabled
1391 def ap_enabled?(_), do: false
1392
1393 @doc "Gets or fetch a user by uri or nickname."
1394 @spec get_or_fetch(String.t()) :: {:ok, User.t()} | {:error, String.t()}
1395 def get_or_fetch("http" <> _host = uri), do: get_or_fetch_by_ap_id(uri)
1396 def get_or_fetch(nickname), do: get_or_fetch_by_nickname(nickname)
1397
1398 # wait a period of time and return newest version of the User structs
1399 # this is because we have synchronous follow APIs and need to simulate them
1400 # with an async handshake
1401 def wait_and_refresh(_, %User{local: true} = a, %User{local: true} = b) do
1402 with %User{} = a <- User.get_cached_by_id(a.id),
1403 %User{} = b <- User.get_cached_by_id(b.id) do
1404 {:ok, a, b}
1405 else
1406 _e ->
1407 :error
1408 end
1409 end
1410
1411 def wait_and_refresh(timeout, %User{} = a, %User{} = b) do
1412 with :ok <- :timer.sleep(timeout),
1413 %User{} = a <- User.get_cached_by_id(a.id),
1414 %User{} = b <- User.get_cached_by_id(b.id) do
1415 {:ok, a, b}
1416 else
1417 _e ->
1418 :error
1419 end
1420 end
1421
1422 def parse_bio(bio) when is_binary(bio) and bio != "" do
1423 bio
1424 |> CommonUtils.format_input("text/plain", mentions_format: :full)
1425 |> elem(0)
1426 end
1427
1428 def parse_bio(_), do: ""
1429
1430 def parse_bio(bio, user) when is_binary(bio) and bio != "" do
1431 # TODO: get profile URLs other than user.ap_id
1432 profile_urls = [user.ap_id]
1433
1434 bio
1435 |> CommonUtils.format_input("text/plain",
1436 mentions_format: :full,
1437 rel: &RelMe.maybe_put_rel_me(&1, profile_urls)
1438 )
1439 |> elem(0)
1440 end
1441
1442 def parse_bio(_, _), do: ""
1443
1444 def tag(user_identifiers, tags) when is_list(user_identifiers) do
1445 Repo.transaction(fn ->
1446 for user_identifier <- user_identifiers, do: tag(user_identifier, tags)
1447 end)
1448 end
1449
1450 def tag(nickname, tags) when is_binary(nickname),
1451 do: tag(get_by_nickname(nickname), tags)
1452
1453 def tag(%User{} = user, tags),
1454 do: update_tags(user, Enum.uniq((user.tags || []) ++ normalize_tags(tags)))
1455
1456 def untag(user_identifiers, tags) when is_list(user_identifiers) do
1457 Repo.transaction(fn ->
1458 for user_identifier <- user_identifiers, do: untag(user_identifier, tags)
1459 end)
1460 end
1461
1462 def untag(nickname, tags) when is_binary(nickname),
1463 do: untag(get_by_nickname(nickname), tags)
1464
1465 def untag(%User{} = user, tags),
1466 do: update_tags(user, (user.tags || []) -- normalize_tags(tags))
1467
1468 defp update_tags(%User{} = user, new_tags) do
1469 {:ok, updated_user} =
1470 user
1471 |> change(%{tags: new_tags})
1472 |> update_and_set_cache()
1473
1474 updated_user
1475 end
1476
1477 defp normalize_tags(tags) do
1478 [tags]
1479 |> List.flatten()
1480 |> Enum.map(&String.downcase(&1))
1481 end
1482
1483 defp local_nickname_regex do
1484 if Pleroma.Config.get([:instance, :extended_nickname_format]) do
1485 @extended_local_nickname_regex
1486 else
1487 @strict_local_nickname_regex
1488 end
1489 end
1490
1491 def local_nickname(nickname_or_mention) do
1492 nickname_or_mention
1493 |> full_nickname()
1494 |> String.split("@")
1495 |> hd()
1496 end
1497
1498 def full_nickname(nickname_or_mention),
1499 do: String.trim_leading(nickname_or_mention, "@")
1500
1501 def error_user(ap_id) do
1502 %User{
1503 name: ap_id,
1504 ap_id: ap_id,
1505 info: %User.Info{},
1506 nickname: "erroruser@example.com",
1507 inserted_at: NaiveDateTime.utc_now()
1508 }
1509 end
1510
1511 @spec all_superusers() :: [User.t()]
1512 def all_superusers do
1513 User.Query.build(%{super_users: true, local: true, deactivated: false})
1514 |> Repo.all()
1515 end
1516
1517 def showing_reblogs?(%User{} = user, %User{} = target) do
1518 target.ap_id not in user.info.muted_reblogs
1519 end
1520
1521 @doc """
1522 The function returns a query to get users with no activity for given interval of days.
1523 Inactive users are those who didn't read any notification, or had any activity where
1524 the user is the activity's actor, during `inactivity_threshold` days.
1525 Deactivated users will not appear in this list.
1526
1527 ## Examples
1528
1529 iex> Pleroma.User.list_inactive_users()
1530 %Ecto.Query{}
1531 """
1532 @spec list_inactive_users_query(integer()) :: Ecto.Query.t()
1533 def list_inactive_users_query(inactivity_threshold \\ 7) do
1534 negative_inactivity_threshold = -inactivity_threshold
1535 now = NaiveDateTime.truncate(NaiveDateTime.utc_now(), :second)
1536 # Subqueries are not supported in `where` clauses, join gets too complicated.
1537 has_read_notifications =
1538 from(n in Pleroma.Notification,
1539 where: n.seen == true,
1540 group_by: n.id,
1541 having: max(n.updated_at) > datetime_add(^now, ^negative_inactivity_threshold, "day"),
1542 select: n.user_id
1543 )
1544 |> Pleroma.Repo.all()
1545
1546 from(u in Pleroma.User,
1547 left_join: a in Pleroma.Activity,
1548 on: u.ap_id == a.actor,
1549 where: not is_nil(u.nickname),
1550 where: fragment("not (?->'deactivated' @> 'true')", u.info),
1551 where: u.id not in ^has_read_notifications,
1552 group_by: u.id,
1553 having:
1554 max(a.inserted_at) < datetime_add(^now, ^negative_inactivity_threshold, "day") or
1555 is_nil(max(a.inserted_at))
1556 )
1557 end
1558
1559 @doc """
1560 Enable or disable email notifications for user
1561
1562 ## Examples
1563
1564 iex> Pleroma.User.switch_email_notifications(Pleroma.User{info: %{email_notifications: %{"digest" => false}}}, "digest", true)
1565 Pleroma.User{info: %{email_notifications: %{"digest" => true}}}
1566
1567 iex> Pleroma.User.switch_email_notifications(Pleroma.User{info: %{email_notifications: %{"digest" => true}}}, "digest", false)
1568 Pleroma.User{info: %{email_notifications: %{"digest" => false}}}
1569 """
1570 @spec switch_email_notifications(t(), String.t(), boolean()) ::
1571 {:ok, t()} | {:error, Ecto.Changeset.t()}
1572 def switch_email_notifications(user, type, status) do
1573 info = Pleroma.User.Info.update_email_notifications(user.info, %{type => status})
1574
1575 change(user)
1576 |> put_embed(:info, info)
1577 |> update_and_set_cache()
1578 end
1579
1580 @doc """
1581 Set `last_digest_emailed_at` value for the user to current time
1582 """
1583 @spec touch_last_digest_emailed_at(t()) :: t()
1584 def touch_last_digest_emailed_at(user) do
1585 now = NaiveDateTime.truncate(NaiveDateTime.utc_now(), :second)
1586
1587 {:ok, updated_user} =
1588 user
1589 |> change(%{last_digest_emailed_at: now})
1590 |> update_and_set_cache()
1591
1592 updated_user
1593 end
1594
1595 @spec toggle_confirmation(User.t()) :: {:ok, User.t()} | {:error, Changeset.t()}
1596 def toggle_confirmation(%User{} = user) do
1597 need_confirmation? = !user.info.confirmation_pending
1598
1599 info_changeset =
1600 User.Info.confirmation_changeset(user.info, need_confirmation: need_confirmation?)
1601
1602 user
1603 |> change()
1604 |> put_embed(:info, info_changeset)
1605 |> update_and_set_cache()
1606 end
1607
1608 def get_mascot(%{info: %{mascot: %{} = mascot}}) when not is_nil(mascot) do
1609 mascot
1610 end
1611
1612 def get_mascot(%{info: %{mascot: mascot}}) when is_nil(mascot) do
1613 # use instance-default
1614 config = Pleroma.Config.get([:assets, :mascots])
1615 default_mascot = Pleroma.Config.get([:assets, :default_mascot])
1616 mascot = Keyword.get(config, default_mascot)
1617
1618 %{
1619 "id" => "default-mascot",
1620 "url" => mascot[:url],
1621 "preview_url" => mascot[:url],
1622 "pleroma" => %{
1623 "mime_type" => mascot[:mime_type]
1624 }
1625 }
1626 end
1627
1628 def ensure_keys_present(%User{info: info} = user) do
1629 if info.keys do
1630 {:ok, user}
1631 else
1632 {:ok, pem} = Keys.generate_rsa_pem()
1633
1634 user
1635 |> Ecto.Changeset.change()
1636 |> Ecto.Changeset.put_embed(:info, User.Info.set_keys(info, pem))
1637 |> update_and_set_cache()
1638 end
1639 end
1640
1641 def get_ap_ids_by_nicknames(nicknames) do
1642 from(u in User,
1643 where: u.nickname in ^nicknames,
1644 select: u.ap_id
1645 )
1646 |> Repo.all()
1647 end
1648
1649 defdelegate search(query, opts \\ []), to: User.Search
1650
1651 defp put_password_hash(
1652 %Ecto.Changeset{valid?: true, changes: %{password: password}} = changeset
1653 ) do
1654 change(changeset, password_hash: Pbkdf2.hashpwsalt(password))
1655 end
1656
1657 defp put_password_hash(changeset), do: changeset
1658
1659 def is_internal_user?(%User{nickname: nil}), do: true
1660 def is_internal_user?(%User{local: true, nickname: "internal." <> _}), do: true
1661 def is_internal_user?(_), do: false
1662
1663 # A hack because user delete activities have a fake id for whatever reason
1664 # TODO: Get rid of this
1665 def get_delivered_users_by_object_id("pleroma:fake_object_id"), do: []
1666
1667 def get_delivered_users_by_object_id(object_id) do
1668 from(u in User,
1669 inner_join: delivery in assoc(u, :deliveries),
1670 where: delivery.object_id == ^object_id
1671 )
1672 |> Repo.all()
1673 end
1674
1675 def change_email(user, email) do
1676 user
1677 |> cast(%{email: email}, [:email])
1678 |> validate_required([:email])
1679 |> unique_constraint(:email)
1680 |> validate_format(:email, @email_regex)
1681 |> update_and_set_cache()
1682 end
1683 end