Add `also_known_as` field to Pleroma.User
[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.Conversation.Participation
15 alias Pleroma.Delivery
16 alias Pleroma.Keys
17 alias Pleroma.Notification
18 alias Pleroma.Object
19 alias Pleroma.Registration
20 alias Pleroma.Repo
21 alias Pleroma.RepoStreamer
22 alias Pleroma.User
23 alias Pleroma.Web
24 alias Pleroma.Web.ActivityPub.ActivityPub
25 alias Pleroma.Web.ActivityPub.Utils
26 alias Pleroma.Web.CommonAPI
27 alias Pleroma.Web.CommonAPI.Utils, as: CommonUtils
28 alias Pleroma.Web.OAuth
29 alias Pleroma.Web.RelMe
30 alias Pleroma.Workers.BackgroundWorker
31
32 require Logger
33
34 @type t :: %__MODULE__{}
35
36 @primary_key {:id, FlakeId.Ecto.CompatType, autogenerate: true}
37
38 # credo:disable-for-next-line Credo.Check.Readability.MaxLineLength
39 @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])?)*$/
40
41 @strict_local_nickname_regex ~r/^[a-zA-Z\d]+$/
42 @extended_local_nickname_regex ~r/^[a-zA-Z\d_-]+$/
43
44 schema "users" do
45 field(:bio, :string)
46 field(:email, :string)
47 field(:name, :string)
48 field(:nickname, :string)
49 field(:password_hash, :string)
50 field(:password, :string, virtual: true)
51 field(:password_confirmation, :string, virtual: true)
52 field(:keys, :string)
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
65 field(:banner, :map, default: %{})
66 field(:background, :map, default: %{})
67 field(:source_data, :map, default: %{})
68 field(:note_count, :integer, default: 0)
69 field(:follower_count, :integer, default: 0)
70 # Should be filled in only for remote users
71 field(:following_count, :integer, default: nil)
72 field(:locked, :boolean, default: false)
73 field(:confirmation_pending, :boolean, default: false)
74 field(:password_reset_pending, :boolean, default: false)
75 field(:confirmation_token, :string, default: nil)
76 field(:default_scope, :string, default: "public")
77 field(:blocks, {:array, :string}, default: [])
78 field(:domain_blocks, {:array, :string}, default: [])
79 field(:mutes, {:array, :string}, default: [])
80 field(:muted_reblogs, {:array, :string}, default: [])
81 field(:muted_notifications, {:array, :string}, default: [])
82 field(:subscribers, {:array, :string}, default: [])
83 field(:deactivated, :boolean, default: false)
84 field(:no_rich_text, :boolean, default: false)
85 field(:ap_enabled, :boolean, default: false)
86 field(:is_moderator, :boolean, default: false)
87 field(:is_admin, :boolean, default: false)
88 field(:show_role, :boolean, default: true)
89 field(:settings, :map, default: nil)
90 field(:magic_key, :string, default: nil)
91 field(:uri, :string, default: nil)
92 field(:hide_followers_count, :boolean, default: false)
93 field(:hide_follows_count, :boolean, default: false)
94 field(:hide_followers, :boolean, default: false)
95 field(:hide_follows, :boolean, default: false)
96 field(:hide_favorites, :boolean, default: true)
97 field(:unread_conversation_count, :integer, default: 0)
98 field(:pinned_activities, {:array, :string}, default: [])
99 field(:email_notifications, :map, default: %{"digest" => false})
100 field(:mascot, :map, default: nil)
101 field(:emoji, {:array, :map}, default: [])
102 field(:pleroma_settings_store, :map, default: %{})
103 field(:fields, {:array, :map}, default: [])
104 field(:raw_fields, {:array, :map}, default: [])
105 field(:discoverable, :boolean, default: false)
106 field(:invisible, :boolean, default: false)
107 field(:skip_thread_containment, :boolean, default: false)
108 field(:also_known_as, {:array, :string}, default: [])
109
110 field(:notification_settings, :map,
111 default: %{
112 "followers" => true,
113 "follows" => true,
114 "non_follows" => true,
115 "non_followers" => true
116 }
117 )
118
119 has_many(:notifications, Notification)
120 has_many(:registrations, Registration)
121 has_many(:deliveries, Delivery)
122
123 field(:info, :map, default: %{})
124
125 timestamps()
126 end
127
128 def auth_active?(%User{confirmation_pending: true}),
129 do: !Pleroma.Config.get([:instance, :account_activation_required])
130
131 def auth_active?(%User{}), do: true
132
133 def visible_for?(user, for_user \\ nil)
134
135 def visible_for?(%User{id: user_id}, %User{id: for_id}) when user_id == for_id, do: true
136
137 def visible_for?(%User{} = user, for_user) do
138 auth_active?(user) || superuser?(for_user)
139 end
140
141 def visible_for?(_, _), do: false
142
143 def superuser?(%User{local: true, is_admin: true}), do: true
144 def superuser?(%User{local: true, is_moderator: true}), do: true
145 def superuser?(_), do: false
146
147 def invisible?(%User{invisible: true}), do: true
148 def invisible?(_), do: false
149
150 def avatar_url(user, options \\ []) do
151 case user.avatar do
152 %{"url" => [%{"href" => href} | _]} -> href
153 _ -> !options[:no_default] && "#{Web.base_url()}/images/avi.png"
154 end
155 end
156
157 def banner_url(user, options \\ []) do
158 case user.banner do
159 %{"url" => [%{"href" => href} | _]} -> href
160 _ -> !options[:no_default] && "#{Web.base_url()}/images/banner.png"
161 end
162 end
163
164 def profile_url(%User{source_data: %{"url" => url}}), do: url
165 def profile_url(%User{ap_id: ap_id}), do: ap_id
166 def profile_url(_), do: nil
167
168 def ap_id(%User{nickname: nickname}), do: "#{Web.base_url()}/users/#{nickname}"
169
170 def ap_followers(%User{follower_address: fa}) when is_binary(fa), do: fa
171 def ap_followers(%User{} = user), do: "#{ap_id(user)}/followers"
172
173 @spec ap_following(User.t()) :: Sring.t()
174 def ap_following(%User{following_address: fa}) when is_binary(fa), do: fa
175 def ap_following(%User{} = user), do: "#{ap_id(user)}/following"
176
177 def user_info(%User{} = user, args \\ %{}) do
178 following_count =
179 Map.get(args, :following_count, user.following_count || following_count(user))
180
181 follower_count = Map.get(args, :follower_count, user.follower_count)
182
183 %{
184 note_count: user.note_count,
185 locked: user.locked,
186 confirmation_pending: user.confirmation_pending,
187 default_scope: user.default_scope
188 }
189 |> Map.put(:following_count, following_count)
190 |> Map.put(:follower_count, follower_count)
191 end
192
193 def follow_state(%User{} = user, %User{} = target) do
194 case Utils.fetch_latest_follow(user, target) do
195 %{data: %{"state" => state}} -> state
196 # Ideally this would be nil, but then Cachex does not commit the value
197 _ -> false
198 end
199 end
200
201 def get_cached_follow_state(user, target) do
202 key = "follow_state:#{user.ap_id}|#{target.ap_id}"
203 Cachex.fetch!(:user_cache, key, fn _ -> {:commit, follow_state(user, target)} end)
204 end
205
206 @spec set_follow_state_cache(String.t(), String.t(), String.t()) :: {:ok | :error, boolean()}
207 def set_follow_state_cache(user_ap_id, target_ap_id, state) do
208 Cachex.put(:user_cache, "follow_state:#{user_ap_id}|#{target_ap_id}", state)
209 end
210
211 def set_info_cache(user, args) do
212 Cachex.put(:user_cache, "user_info:#{user.id}", user_info(user, args))
213 end
214
215 @spec restrict_deactivated(Ecto.Query.t()) :: Ecto.Query.t()
216 def restrict_deactivated(query) do
217 from(u in query, where: u.deactivated != ^true)
218 end
219
220 def following_count(%User{following: []}), do: 0
221
222 def following_count(%User{} = user) do
223 user
224 |> get_friends_query()
225 |> Repo.aggregate(:count, :id)
226 end
227
228 @info_fields [
229 :banner,
230 :background,
231 :source_data,
232 :note_count,
233 :follower_count,
234 :following_count,
235 :locked,
236 :confirmation_pending,
237 :password_reset_pending,
238 :confirmation_token,
239 :default_scope,
240 :blocks,
241 :domain_blocks,
242 :mutes,
243 :muted_reblogs,
244 :muted_notifications,
245 :subscribers,
246 :deactivated,
247 :no_rich_text,
248 :ap_enabled,
249 :is_moderator,
250 :is_admin,
251 :show_role,
252 :settings,
253 :magic_key,
254 :uri,
255 :hide_followers_count,
256 :hide_follows_count,
257 :hide_followers,
258 :hide_follows,
259 :hide_favorites,
260 :unread_conversation_count,
261 :pinned_activities,
262 :email_notifications,
263 :mascot,
264 :emoji,
265 :pleroma_settings_store,
266 :fields,
267 :raw_fields,
268 :discoverable,
269 :invisible,
270 :skip_thread_containment,
271 :notification_settings
272 ]
273
274 def info_fields, do: @info_fields
275
276 defp truncate_fields_param(params) do
277 if Map.has_key?(params, :fields) do
278 Map.put(params, :fields, Enum.map(params[:fields], &truncate_field/1))
279 else
280 params
281 end
282 end
283
284 defp truncate_if_exists(params, key, max_length) do
285 if Map.has_key?(params, key) and is_binary(params[key]) do
286 {value, _chopped} = String.split_at(params[key], max_length)
287 Map.put(params, key, value)
288 else
289 params
290 end
291 end
292
293 def remote_user_creation(params) do
294 bio_limit = Pleroma.Config.get([:instance, :user_bio_length], 5000)
295 name_limit = Pleroma.Config.get([:instance, :user_name_length], 100)
296
297 params =
298 params
299 |> Map.put(:info, params[:info] || %{})
300 |> truncate_if_exists(:name, name_limit)
301 |> truncate_if_exists(:bio, bio_limit)
302 |> truncate_fields_param()
303
304 changeset =
305 %User{local: false}
306 |> cast(
307 params,
308 [
309 :bio,
310 :name,
311 :ap_id,
312 :nickname,
313 :avatar,
314 :ap_enabled,
315 :source_data,
316 :banner,
317 :locked,
318 :magic_key,
319 :uri,
320 :hide_followers,
321 :hide_follows,
322 :hide_followers_count,
323 :hide_follows_count,
324 :follower_count,
325 :fields,
326 :following_count,
327 :discoverable,
328 :invisible,
329 :also_known_as
330 ]
331 )
332 |> validate_required([:name, :ap_id])
333 |> unique_constraint(:nickname)
334 |> validate_format(:nickname, @email_regex)
335 |> validate_length(:bio, max: bio_limit)
336 |> validate_length(:name, max: name_limit)
337 |> validate_fields(true)
338
339 case params[:source_data] do
340 %{"followers" => followers, "following" => following} ->
341 changeset
342 |> put_change(:follower_address, followers)
343 |> put_change(:following_address, following)
344
345 _ ->
346 followers = ap_followers(%User{nickname: get_field(changeset, :nickname)})
347 put_change(changeset, :follower_address, followers)
348 end
349 end
350
351 def update_changeset(struct, params \\ %{}) do
352 bio_limit = Pleroma.Config.get([:instance, :user_bio_length], 5000)
353 name_limit = Pleroma.Config.get([:instance, :user_name_length], 100)
354
355 struct
356 |> cast(
357 params,
358 [
359 :bio,
360 :name,
361 :avatar,
362 :following,
363 :locked,
364 :no_rich_text,
365 :default_scope,
366 :banner,
367 :hide_follows,
368 :hide_followers,
369 :hide_followers_count,
370 :hide_follows_count,
371 :hide_favorites,
372 :background,
373 :show_role,
374 :skip_thread_containment,
375 :fields,
376 :raw_fields,
377 :pleroma_settings_store,
378 :discoverable,
379 :also_known_as
380 ]
381 )
382 |> unique_constraint(:nickname)
383 |> validate_format(:nickname, local_nickname_regex())
384 |> validate_length(:bio, max: bio_limit)
385 |> validate_length(:name, min: 1, max: name_limit)
386 |> validate_fields(false)
387 end
388
389 def upgrade_changeset(struct, params \\ %{}, remote? \\ false) do
390 bio_limit = Pleroma.Config.get([:instance, :user_bio_length], 5000)
391 name_limit = Pleroma.Config.get([:instance, :user_name_length], 100)
392
393 params = Map.put(params, :last_refreshed_at, NaiveDateTime.utc_now())
394
395 params = if remote?, do: truncate_fields_param(params), else: params
396
397 struct
398 |> cast(
399 params,
400 [
401 :bio,
402 :name,
403 :follower_address,
404 :following_address,
405 :avatar,
406 :last_refreshed_at,
407 :ap_enabled,
408 :source_data,
409 :banner,
410 :locked,
411 :magic_key,
412 :follower_count,
413 :following_count,
414 :hide_follows,
415 :fields,
416 :hide_followers,
417 :discoverable,
418 :hide_followers_count,
419 :hide_follows_count,
420 :also_known_as
421 ]
422 )
423 |> unique_constraint(:nickname)
424 |> validate_format(:nickname, local_nickname_regex())
425 |> validate_length(:bio, max: bio_limit)
426 |> validate_length(:name, max: name_limit)
427 |> validate_fields(remote?)
428 end
429
430 def password_update_changeset(struct, params) do
431 struct
432 |> cast(params, [:password, :password_confirmation])
433 |> validate_required([:password, :password_confirmation])
434 |> validate_confirmation(:password)
435 |> put_password_hash()
436 |> put_change(:password_reset_pending, false)
437 end
438
439 @spec reset_password(User.t(), map) :: {:ok, User.t()} | {:error, Ecto.Changeset.t()}
440 def reset_password(%User{id: user_id} = user, data) do
441 multi =
442 Multi.new()
443 |> Multi.update(:user, password_update_changeset(user, data))
444 |> Multi.delete_all(:tokens, OAuth.Token.Query.get_by_user(user_id))
445 |> Multi.delete_all(:auth, OAuth.Authorization.delete_by_user_query(user))
446
447 case Repo.transaction(multi) do
448 {:ok, %{user: user} = _} -> set_cache(user)
449 {:error, _, changeset, _} -> {:error, changeset}
450 end
451 end
452
453 def update_password_reset_pending(user, value) do
454 user
455 |> change()
456 |> put_change(:password_reset_pending, value)
457 |> update_and_set_cache()
458 end
459
460 def force_password_reset_async(user) do
461 BackgroundWorker.enqueue("force_password_reset", %{"user_id" => user.id})
462 end
463
464 @spec force_password_reset(User.t()) :: {:ok, User.t()} | {:error, Ecto.Changeset.t()}
465 def force_password_reset(user), do: update_password_reset_pending(user, true)
466
467 def register_changeset(struct, params \\ %{}, opts \\ []) do
468 bio_limit = Pleroma.Config.get([:instance, :user_bio_length], 5000)
469 name_limit = Pleroma.Config.get([:instance, :user_name_length], 100)
470
471 need_confirmation? =
472 if is_nil(opts[:need_confirmation]) do
473 Pleroma.Config.get([:instance, :account_activation_required])
474 else
475 opts[:need_confirmation]
476 end
477
478 struct
479 |> confirmation_changeset(need_confirmation: need_confirmation?)
480 |> cast(params, [:bio, :email, :name, :nickname, :password, :password_confirmation])
481 |> validate_required([:name, :nickname, :password, :password_confirmation])
482 |> validate_confirmation(:password)
483 |> unique_constraint(:email)
484 |> unique_constraint(:nickname)
485 |> validate_exclusion(:nickname, Pleroma.Config.get([User, :restricted_nicknames]))
486 |> validate_format(:nickname, local_nickname_regex())
487 |> validate_format(:email, @email_regex)
488 |> validate_length(:bio, max: bio_limit)
489 |> validate_length(:name, min: 1, max: name_limit)
490 |> maybe_validate_required_email(opts[:external])
491 |> put_password_hash
492 |> put_ap_id()
493 |> unique_constraint(:ap_id)
494 |> put_following_and_follower_address()
495 end
496
497 def maybe_validate_required_email(changeset, true), do: changeset
498 def maybe_validate_required_email(changeset, _), do: validate_required(changeset, [:email])
499
500 defp put_ap_id(changeset) do
501 ap_id = ap_id(%User{nickname: get_field(changeset, :nickname)})
502 put_change(changeset, :ap_id, ap_id)
503 end
504
505 defp put_following_and_follower_address(changeset) do
506 followers = ap_followers(%User{nickname: get_field(changeset, :nickname)})
507
508 changeset
509 |> put_change(:following, [followers])
510 |> put_change(:follower_address, followers)
511 end
512
513 defp autofollow_users(user) do
514 candidates = Pleroma.Config.get([:instance, :autofollowed_nicknames])
515
516 autofollowed_users =
517 User.Query.build(%{nickname: candidates, local: true, deactivated: false})
518 |> Repo.all()
519
520 follow_all(user, autofollowed_users)
521 end
522
523 @doc "Inserts provided changeset, performs post-registration actions (confirmation email sending etc.)"
524 def register(%Ecto.Changeset{} = changeset) do
525 with {:ok, user} <- Repo.insert(changeset) do
526 post_register_action(user)
527 end
528 end
529
530 def post_register_action(%User{} = user) do
531 with {:ok, user} <- autofollow_users(user),
532 {:ok, user} <- set_cache(user),
533 {:ok, _} <- User.WelcomeMessage.post_welcome_message_to_user(user),
534 {:ok, _} <- try_send_confirmation_email(user) do
535 {:ok, user}
536 end
537 end
538
539 def try_send_confirmation_email(%User{} = user) do
540 if user.confirmation_pending &&
541 Pleroma.Config.get([:instance, :account_activation_required]) do
542 user
543 |> Pleroma.Emails.UserEmail.account_confirmation_email()
544 |> Pleroma.Emails.Mailer.deliver_async()
545
546 {:ok, :enqueued}
547 else
548 {:ok, :noop}
549 end
550 end
551
552 def needs_update?(%User{local: true}), do: false
553
554 def needs_update?(%User{local: false, last_refreshed_at: nil}), do: true
555
556 def needs_update?(%User{local: false} = user) do
557 NaiveDateTime.diff(NaiveDateTime.utc_now(), user.last_refreshed_at) >= 86_400
558 end
559
560 def needs_update?(_), do: true
561
562 @spec maybe_direct_follow(User.t(), User.t()) :: {:ok, User.t()} | {:error, String.t()}
563 def maybe_direct_follow(%User{} = follower, %User{local: true, locked: true}) do
564 {:ok, follower}
565 end
566
567 def maybe_direct_follow(%User{} = follower, %User{local: true} = followed) do
568 follow(follower, followed)
569 end
570
571 def maybe_direct_follow(%User{} = follower, %User{} = followed) do
572 if not ap_enabled?(followed) do
573 follow(follower, followed)
574 else
575 {:ok, follower}
576 end
577 end
578
579 @doc "A mass follow for local users. Respects blocks in both directions but does not create activities."
580 @spec follow_all(User.t(), list(User.t())) :: {atom(), User.t()}
581 def follow_all(follower, followeds) do
582 followed_addresses =
583 followeds
584 |> Enum.reject(fn followed -> blocks?(follower, followed) || blocks?(followed, follower) end)
585 |> Enum.map(fn %{follower_address: fa} -> fa end)
586
587 q =
588 from(u in User,
589 where: u.id == ^follower.id,
590 update: [
591 set: [
592 following:
593 fragment(
594 "array(select distinct unnest (array_cat(?, ?)))",
595 u.following,
596 ^followed_addresses
597 )
598 ]
599 ],
600 select: u
601 )
602
603 {1, [follower]} = Repo.update_all(q, [])
604
605 Enum.each(followeds, &update_follower_count/1)
606
607 set_cache(follower)
608 end
609
610 def follow(%User{} = follower, %User{} = followed) do
611 deny_follow_blocked = Pleroma.Config.get([:user, :deny_follow_blocked])
612 ap_followers = followed.follower_address
613
614 cond do
615 followed.deactivated ->
616 {:error, "Could not follow user: #{followed.nickname} is deactivated."}
617
618 deny_follow_blocked and blocks?(followed, follower) ->
619 {:error, "Could not follow user: #{followed.nickname} blocked you."}
620
621 true ->
622 q =
623 from(u in User,
624 where: u.id == ^follower.id,
625 update: [push: [following: ^ap_followers]],
626 select: u
627 )
628
629 {1, [follower]} = Repo.update_all(q, [])
630
631 follower = maybe_update_following_count(follower)
632
633 {:ok, _} = update_follower_count(followed)
634
635 set_cache(follower)
636 end
637 end
638
639 def unfollow(%User{} = follower, %User{} = followed) do
640 ap_followers = followed.follower_address
641
642 if following?(follower, followed) and follower.ap_id != followed.ap_id do
643 q =
644 from(u in User,
645 where: u.id == ^follower.id,
646 update: [pull: [following: ^ap_followers]],
647 select: u
648 )
649
650 {1, [follower]} = Repo.update_all(q, [])
651
652 follower = maybe_update_following_count(follower)
653
654 {:ok, followed} = update_follower_count(followed)
655
656 set_cache(follower)
657
658 {:ok, follower, Utils.fetch_latest_follow(follower, followed)}
659 else
660 {:error, "Not subscribed!"}
661 end
662 end
663
664 @spec following?(User.t(), User.t()) :: boolean
665 def following?(%User{} = follower, %User{} = followed) do
666 Enum.member?(follower.following, followed.follower_address)
667 end
668
669 def locked?(%User{} = user) do
670 user.locked || false
671 end
672
673 def get_by_id(id) do
674 Repo.get_by(User, id: id)
675 end
676
677 def get_by_ap_id(ap_id) do
678 Repo.get_by(User, ap_id: ap_id)
679 end
680
681 def get_all_by_ap_id(ap_ids) do
682 from(u in __MODULE__,
683 where: u.ap_id in ^ap_ids
684 )
685 |> Repo.all()
686 end
687
688 def get_all_by_ids(ids) do
689 from(u in __MODULE__, where: u.id in ^ids)
690 |> Repo.all()
691 end
692
693 # This is mostly an SPC migration fix. This guesses the user nickname by taking the last part
694 # of the ap_id and the domain and tries to get that user
695 def get_by_guessed_nickname(ap_id) do
696 domain = URI.parse(ap_id).host
697 name = List.last(String.split(ap_id, "/"))
698 nickname = "#{name}@#{domain}"
699
700 get_cached_by_nickname(nickname)
701 end
702
703 def set_cache({:ok, user}), do: set_cache(user)
704 def set_cache({:error, err}), do: {:error, err}
705
706 def set_cache(%User{} = user) do
707 Cachex.put(:user_cache, "ap_id:#{user.ap_id}", user)
708 Cachex.put(:user_cache, "nickname:#{user.nickname}", user)
709 Cachex.put(:user_cache, "user_info:#{user.id}", user_info(user))
710 {:ok, user}
711 end
712
713 def update_and_set_cache(struct, params) do
714 struct
715 |> update_changeset(params)
716 |> update_and_set_cache()
717 end
718
719 def update_and_set_cache(changeset) do
720 with {:ok, user} <- Repo.update(changeset, stale_error_field: :id) do
721 set_cache(user)
722 end
723 end
724
725 def invalidate_cache(user) do
726 Cachex.del(:user_cache, "ap_id:#{user.ap_id}")
727 Cachex.del(:user_cache, "nickname:#{user.nickname}")
728 Cachex.del(:user_cache, "user_info:#{user.id}")
729 end
730
731 def get_cached_by_ap_id(ap_id) do
732 key = "ap_id:#{ap_id}"
733 Cachex.fetch!(:user_cache, key, fn _ -> get_by_ap_id(ap_id) end)
734 end
735
736 def get_cached_by_id(id) do
737 key = "id:#{id}"
738
739 ap_id =
740 Cachex.fetch!(:user_cache, key, fn _ ->
741 user = get_by_id(id)
742
743 if user do
744 Cachex.put(:user_cache, "ap_id:#{user.ap_id}", user)
745 {:commit, user.ap_id}
746 else
747 {:ignore, ""}
748 end
749 end)
750
751 get_cached_by_ap_id(ap_id)
752 end
753
754 def get_cached_by_nickname(nickname) do
755 key = "nickname:#{nickname}"
756
757 Cachex.fetch!(:user_cache, key, fn ->
758 case get_or_fetch_by_nickname(nickname) do
759 {:ok, user} -> {:commit, user}
760 {:error, _error} -> {:ignore, nil}
761 end
762 end)
763 end
764
765 def get_cached_by_nickname_or_id(nickname_or_id, opts \\ []) do
766 restrict_to_local = Pleroma.Config.get([:instance, :limit_to_local_content])
767
768 cond do
769 is_integer(nickname_or_id) or FlakeId.flake_id?(nickname_or_id) ->
770 get_cached_by_id(nickname_or_id) || get_cached_by_nickname(nickname_or_id)
771
772 restrict_to_local == false or not String.contains?(nickname_or_id, "@") ->
773 get_cached_by_nickname(nickname_or_id)
774
775 restrict_to_local == :unauthenticated and match?(%User{}, opts[:for]) ->
776 get_cached_by_nickname(nickname_or_id)
777
778 true ->
779 nil
780 end
781 end
782
783 def get_by_nickname(nickname) do
784 Repo.get_by(User, nickname: nickname) ||
785 if Regex.match?(~r(@#{Pleroma.Web.Endpoint.host()})i, nickname) do
786 Repo.get_by(User, nickname: local_nickname(nickname))
787 end
788 end
789
790 def get_by_email(email), do: Repo.get_by(User, email: email)
791
792 def get_by_nickname_or_email(nickname_or_email) do
793 get_by_nickname(nickname_or_email) || get_by_email(nickname_or_email)
794 end
795
796 def get_cached_user_info(user) do
797 key = "user_info:#{user.id}"
798 Cachex.fetch!(:user_cache, key, fn -> user_info(user) end)
799 end
800
801 def fetch_by_nickname(nickname), do: ActivityPub.make_user_from_nickname(nickname)
802
803 def get_or_fetch_by_nickname(nickname) do
804 with %User{} = user <- get_by_nickname(nickname) do
805 {:ok, user}
806 else
807 _e ->
808 with [_nick, _domain] <- String.split(nickname, "@"),
809 {:ok, user} <- fetch_by_nickname(nickname) do
810 if Pleroma.Config.get([:fetch_initial_posts, :enabled]) do
811 fetch_initial_posts(user)
812 end
813
814 {:ok, user}
815 else
816 _e -> {:error, "not found " <> nickname}
817 end
818 end
819 end
820
821 @doc "Fetch some posts when the user has just been federated with"
822 def fetch_initial_posts(user) do
823 BackgroundWorker.enqueue("fetch_initial_posts", %{"user_id" => user.id})
824 end
825
826 @spec get_followers_query(User.t(), pos_integer() | nil) :: Ecto.Query.t()
827 def get_followers_query(%User{} = user, nil) do
828 User.Query.build(%{followers: user, deactivated: false})
829 end
830
831 def get_followers_query(user, page) do
832 user
833 |> get_followers_query(nil)
834 |> User.Query.paginate(page, 20)
835 end
836
837 @spec get_followers_query(User.t()) :: Ecto.Query.t()
838 def get_followers_query(user), do: get_followers_query(user, nil)
839
840 @spec get_followers(User.t(), pos_integer()) :: {:ok, list(User.t())}
841 def get_followers(user, page \\ nil) do
842 user
843 |> get_followers_query(page)
844 |> Repo.all()
845 end
846
847 @spec get_external_followers(User.t(), pos_integer()) :: {:ok, list(User.t())}
848 def get_external_followers(user, page \\ nil) do
849 user
850 |> get_followers_query(page)
851 |> User.Query.build(%{external: true})
852 |> Repo.all()
853 end
854
855 def get_followers_ids(user, page \\ nil) do
856 user
857 |> get_followers_query(page)
858 |> select([u], u.id)
859 |> Repo.all()
860 end
861
862 @spec get_friends_query(User.t(), pos_integer() | nil) :: Ecto.Query.t()
863 def get_friends_query(%User{} = user, nil) do
864 User.Query.build(%{friends: user, deactivated: false})
865 end
866
867 def get_friends_query(user, page) do
868 user
869 |> get_friends_query(nil)
870 |> User.Query.paginate(page, 20)
871 end
872
873 @spec get_friends_query(User.t()) :: Ecto.Query.t()
874 def get_friends_query(user), do: get_friends_query(user, nil)
875
876 def get_friends(user, page \\ nil) do
877 user
878 |> get_friends_query(page)
879 |> Repo.all()
880 end
881
882 def get_friends_ids(user, page \\ nil) do
883 user
884 |> get_friends_query(page)
885 |> select([u], u.id)
886 |> Repo.all()
887 end
888
889 @spec get_follow_requests(User.t()) :: {:ok, [User.t()]}
890 def get_follow_requests(%User{} = user) do
891 user
892 |> Activity.follow_requests_for_actor()
893 |> join(:inner, [a], u in User, on: a.actor == u.ap_id)
894 |> where([a, u], not fragment("? @> ?", u.following, ^[user.follower_address]))
895 |> group_by([a, u], u.id)
896 |> select([a, u], u)
897 |> Repo.all()
898 end
899
900 def increase_note_count(%User{} = user) do
901 User
902 |> where(id: ^user.id)
903 |> update([u], inc: [note_count: 1])
904 |> select([u], u)
905 |> Repo.update_all([])
906 |> case do
907 {1, [user]} -> set_cache(user)
908 _ -> {:error, user}
909 end
910 end
911
912 def decrease_note_count(%User{} = user) do
913 User
914 |> where(id: ^user.id)
915 |> update([u],
916 set: [
917 note_count: fragment("greatest(0, note_count - 1)")
918 ]
919 )
920 |> select([u], u)
921 |> Repo.update_all([])
922 |> case do
923 {1, [user]} -> set_cache(user)
924 _ -> {:error, user}
925 end
926 end
927
928 def update_note_count(%User{} = user, note_count \\ nil) do
929 note_count =
930 note_count ||
931 from(
932 a in Object,
933 where: fragment("?->>'actor' = ? and ?->>'type' = 'Note'", a.data, ^user.ap_id, a.data),
934 select: count(a.id)
935 )
936 |> Repo.one()
937
938 user
939 |> cast(%{note_count: note_count}, [:note_count])
940 |> update_and_set_cache()
941 end
942
943 @spec maybe_fetch_follow_information(User.t()) :: User.t()
944 def maybe_fetch_follow_information(user) do
945 with {:ok, user} <- fetch_follow_information(user) do
946 user
947 else
948 e ->
949 Logger.error("Follower/Following counter update for #{user.ap_id} failed.\n#{inspect(e)}")
950
951 user
952 end
953 end
954
955 def fetch_follow_information(user) do
956 with {:ok, info} <- ActivityPub.fetch_follow_information_for_user(user) do
957 user
958 |> follow_information_changeset(info)
959 |> update_and_set_cache()
960 end
961 end
962
963 defp follow_information_changeset(user, params) do
964 user
965 |> cast(params, [
966 :hide_followers,
967 :hide_follows,
968 :follower_count,
969 :following_count,
970 :hide_followers_count,
971 :hide_follows_count
972 ])
973 end
974
975 def update_follower_count(%User{} = user) do
976 if user.local or !Pleroma.Config.get([:instance, :external_user_synchronization]) do
977 follower_count_query =
978 User.Query.build(%{followers: user, deactivated: false})
979 |> select([u], %{count: count(u.id)})
980
981 User
982 |> where(id: ^user.id)
983 |> join(:inner, [u], s in subquery(follower_count_query))
984 |> update([u, s],
985 set: [follower_count: s.count]
986 )
987 |> select([u], u)
988 |> Repo.update_all([])
989 |> case do
990 {1, [user]} -> set_cache(user)
991 _ -> {:error, user}
992 end
993 else
994 {:ok, maybe_fetch_follow_information(user)}
995 end
996 end
997
998 @spec maybe_update_following_count(User.t()) :: User.t()
999 def maybe_update_following_count(%User{local: false} = user) do
1000 if Pleroma.Config.get([:instance, :external_user_synchronization]) do
1001 maybe_fetch_follow_information(user)
1002 else
1003 user
1004 end
1005 end
1006
1007 def maybe_update_following_count(user), do: user
1008
1009 def set_unread_conversation_count(%User{local: true} = user) do
1010 unread_query = Participation.unread_conversation_count_for_user(user)
1011
1012 User
1013 |> join(:inner, [u], p in subquery(unread_query))
1014 |> update([u, p],
1015 set: [unread_conversation_count: p.count]
1016 )
1017 |> where([u], u.id == ^user.id)
1018 |> select([u], u)
1019 |> Repo.update_all([])
1020 |> case do
1021 {1, [user]} -> set_cache(user)
1022 _ -> {:error, user}
1023 end
1024 end
1025
1026 def set_unread_conversation_count(_), do: :noop
1027
1028 def increment_unread_conversation_count(conversation, %User{local: true} = user) do
1029 unread_query =
1030 Participation.unread_conversation_count_for_user(user)
1031 |> where([p], p.conversation_id == ^conversation.id)
1032
1033 User
1034 |> join(:inner, [u], p in subquery(unread_query))
1035 |> update([u, p],
1036 inc: [unread_conversation_count: 1]
1037 )
1038 |> where([u], u.id == ^user.id)
1039 |> where([u, p], p.count == 0)
1040 |> select([u], u)
1041 |> Repo.update_all([])
1042 |> case do
1043 {1, [user]} -> set_cache(user)
1044 _ -> {:error, user}
1045 end
1046 end
1047
1048 def increment_unread_conversation_count(_, _), do: :noop
1049
1050 def remove_duplicated_following(%User{following: following} = user) do
1051 uniq_following = Enum.uniq(following)
1052
1053 if length(following) == length(uniq_following) do
1054 {:ok, user}
1055 else
1056 user
1057 |> update_changeset(%{following: uniq_following})
1058 |> update_and_set_cache()
1059 end
1060 end
1061
1062 @spec get_users_from_set([String.t()], boolean()) :: [User.t()]
1063 def get_users_from_set(ap_ids, local_only \\ true) do
1064 criteria = %{ap_id: ap_ids, deactivated: false}
1065 criteria = if local_only, do: Map.put(criteria, :local, true), else: criteria
1066
1067 User.Query.build(criteria)
1068 |> Repo.all()
1069 end
1070
1071 @spec get_recipients_from_activity(Activity.t()) :: [User.t()]
1072 def get_recipients_from_activity(%Activity{recipients: to}) do
1073 User.Query.build(%{recipients_from_activity: to, local: true, deactivated: false})
1074 |> Repo.all()
1075 end
1076
1077 @spec mute(User.t(), User.t(), boolean()) :: {:ok, User.t()} | {:error, String.t()}
1078 def mute(muter, %User{ap_id: ap_id}, notifications? \\ true) do
1079 add_to_mutes(muter, ap_id, notifications?)
1080 end
1081
1082 def unmute(muter, %{ap_id: ap_id}) do
1083 remove_from_mutes(muter, ap_id)
1084 end
1085
1086 def subscribe(subscriber, %{ap_id: ap_id}) do
1087 with %User{} = subscribed <- get_cached_by_ap_id(ap_id) do
1088 deny_follow_blocked = Pleroma.Config.get([:user, :deny_follow_blocked])
1089
1090 if blocks?(subscribed, subscriber) and deny_follow_blocked do
1091 {:error, "Could not subscribe: #{subscribed.nickname} is blocking you"}
1092 else
1093 User.add_to_subscribers(subscribed, subscriber.ap_id)
1094 end
1095 end
1096 end
1097
1098 def unsubscribe(unsubscriber, %{ap_id: ap_id}) do
1099 with %User{} = user <- get_cached_by_ap_id(ap_id) do
1100 User.remove_from_subscribers(user, unsubscriber.ap_id)
1101 end
1102 end
1103
1104 def block(blocker, %User{ap_id: ap_id} = blocked) do
1105 # sever any follow relationships to prevent leaks per activitypub (Pleroma issue #213)
1106 blocker =
1107 if following?(blocker, blocked) do
1108 {:ok, blocker, _} = unfollow(blocker, blocked)
1109 blocker
1110 else
1111 blocker
1112 end
1113
1114 # clear any requested follows as well
1115 blocked =
1116 case CommonAPI.reject_follow_request(blocked, blocker) do
1117 {:ok, %User{} = updated_blocked} -> updated_blocked
1118 nil -> blocked
1119 end
1120
1121 blocker =
1122 if subscribed_to?(blocked, blocker) do
1123 {:ok, blocker} = unsubscribe(blocked, blocker)
1124 blocker
1125 else
1126 blocker
1127 end
1128
1129 if following?(blocked, blocker), do: unfollow(blocked, blocker)
1130
1131 {:ok, blocker} = update_follower_count(blocker)
1132
1133 add_to_block(blocker, ap_id)
1134 end
1135
1136 # helper to handle the block given only an actor's AP id
1137 def block(blocker, %{ap_id: ap_id}) do
1138 block(blocker, get_cached_by_ap_id(ap_id))
1139 end
1140
1141 def unblock(blocker, %{ap_id: ap_id}) do
1142 remove_from_block(blocker, ap_id)
1143 end
1144
1145 def mutes?(nil, _), do: false
1146 def mutes?(user, %{ap_id: ap_id}), do: Enum.member?(user.mutes, ap_id)
1147
1148 @spec muted_notifications?(User.t() | nil, User.t() | map()) :: boolean()
1149 def muted_notifications?(nil, _), do: false
1150
1151 def muted_notifications?(user, %{ap_id: ap_id}),
1152 do: Enum.member?(user.muted_notifications, ap_id)
1153
1154 def blocks?(%User{} = user, %User{} = target) do
1155 blocks_ap_id?(user, target) || blocks_domain?(user, target)
1156 end
1157
1158 def blocks?(nil, _), do: false
1159
1160 def blocks_ap_id?(%User{} = user, %User{} = target) do
1161 Enum.member?(user.blocks, target.ap_id)
1162 end
1163
1164 def blocks_ap_id?(_, _), do: false
1165
1166 def blocks_domain?(%User{} = user, %User{} = target) do
1167 domain_blocks = Pleroma.Web.ActivityPub.MRF.subdomains_regex(user.domain_blocks)
1168 %{host: host} = URI.parse(target.ap_id)
1169 Pleroma.Web.ActivityPub.MRF.subdomain_match?(domain_blocks, host)
1170 end
1171
1172 def blocks_domain?(_, _), do: false
1173
1174 def subscribed_to?(user, %{ap_id: ap_id}) do
1175 with %User{} = target <- get_cached_by_ap_id(ap_id) do
1176 Enum.member?(target.subscribers, user.ap_id)
1177 end
1178 end
1179
1180 @spec muted_users(User.t()) :: [User.t()]
1181 def muted_users(user) do
1182 User.Query.build(%{ap_id: user.mutes, deactivated: false})
1183 |> Repo.all()
1184 end
1185
1186 @spec blocked_users(User.t()) :: [User.t()]
1187 def blocked_users(user) do
1188 User.Query.build(%{ap_id: user.blocks, deactivated: false})
1189 |> Repo.all()
1190 end
1191
1192 @spec subscribers(User.t()) :: [User.t()]
1193 def subscribers(user) do
1194 User.Query.build(%{ap_id: user.subscribers, deactivated: false})
1195 |> Repo.all()
1196 end
1197
1198 def deactivate_async(user, status \\ true) do
1199 BackgroundWorker.enqueue("deactivate_user", %{"user_id" => user.id, "status" => status})
1200 end
1201
1202 def deactivate(user, status \\ true)
1203
1204 def deactivate(users, status) when is_list(users) do
1205 Repo.transaction(fn ->
1206 for user <- users, do: deactivate(user, status)
1207 end)
1208 end
1209
1210 def deactivate(%User{} = user, status) do
1211 with {:ok, user} <- set_activation_status(user, status) do
1212 Enum.each(get_followers(user), &invalidate_cache/1)
1213 Enum.each(get_friends(user), &update_follower_count/1)
1214
1215 {:ok, user}
1216 end
1217 end
1218
1219 def update_notification_settings(%User{} = user, settings) do
1220 settings =
1221 settings
1222 |> Enum.map(fn {k, v} -> {k, v in [true, "true", "True", "1"]} end)
1223 |> Map.new()
1224
1225 notification_settings =
1226 user.notification_settings
1227 |> Map.merge(settings)
1228 |> Map.take(["followers", "follows", "non_follows", "non_followers"])
1229
1230 params = %{notification_settings: notification_settings}
1231
1232 user
1233 |> cast(params, [:notification_settings])
1234 |> validate_required([:notification_settings])
1235 |> update_and_set_cache()
1236 end
1237
1238 def delete(users) when is_list(users) do
1239 for user <- users, do: delete(user)
1240 end
1241
1242 def delete(%User{} = user) do
1243 BackgroundWorker.enqueue("delete_user", %{"user_id" => user.id})
1244 end
1245
1246 def perform(:force_password_reset, user), do: force_password_reset(user)
1247
1248 @spec perform(atom(), User.t()) :: {:ok, User.t()}
1249 def perform(:delete, %User{} = user) do
1250 {:ok, _user} = ActivityPub.delete(user)
1251
1252 # Remove all relationships
1253 user
1254 |> get_followers()
1255 |> Enum.each(fn follower ->
1256 ActivityPub.unfollow(follower, user)
1257 unfollow(follower, user)
1258 end)
1259
1260 user
1261 |> get_friends()
1262 |> Enum.each(fn followed ->
1263 ActivityPub.unfollow(user, followed)
1264 unfollow(user, followed)
1265 end)
1266
1267 delete_user_activities(user)
1268 invalidate_cache(user)
1269 Repo.delete(user)
1270 end
1271
1272 @spec perform(atom(), User.t()) :: {:ok, User.t()}
1273 def perform(:fetch_initial_posts, %User{} = user) do
1274 pages = Pleroma.Config.get!([:fetch_initial_posts, :pages])
1275
1276 # Insert all the posts in reverse order, so they're in the right order on the timeline
1277 user.source_data["outbox"]
1278 |> Utils.fetch_ordered_collection(pages)
1279 |> Enum.reverse()
1280 |> Enum.each(&Pleroma.Web.Federator.incoming_ap_doc/1)
1281 end
1282
1283 def perform(:deactivate_async, user, status), do: deactivate(user, status)
1284
1285 @spec perform(atom(), User.t(), list()) :: list() | {:error, any()}
1286 def perform(:blocks_import, %User{} = blocker, blocked_identifiers)
1287 when is_list(blocked_identifiers) do
1288 Enum.map(
1289 blocked_identifiers,
1290 fn blocked_identifier ->
1291 with {:ok, %User{} = blocked} <- get_or_fetch(blocked_identifier),
1292 {:ok, blocker} <- block(blocker, blocked),
1293 {:ok, _} <- ActivityPub.block(blocker, blocked) do
1294 blocked
1295 else
1296 err ->
1297 Logger.debug("blocks_import failed for #{blocked_identifier} with: #{inspect(err)}")
1298 err
1299 end
1300 end
1301 )
1302 end
1303
1304 @spec perform(atom(), User.t(), list()) :: list() | {:error, any()}
1305 def perform(:follow_import, %User{} = follower, followed_identifiers)
1306 when is_list(followed_identifiers) do
1307 Enum.map(
1308 followed_identifiers,
1309 fn followed_identifier ->
1310 with {:ok, %User{} = followed} <- get_or_fetch(followed_identifier),
1311 {:ok, follower} <- maybe_direct_follow(follower, followed),
1312 {:ok, _} <- ActivityPub.follow(follower, followed) do
1313 followed
1314 else
1315 err ->
1316 Logger.debug("follow_import failed for #{followed_identifier} with: #{inspect(err)}")
1317 err
1318 end
1319 end
1320 )
1321 end
1322
1323 @spec external_users_query() :: Ecto.Query.t()
1324 def external_users_query do
1325 User.Query.build(%{
1326 external: true,
1327 active: true,
1328 order_by: :id
1329 })
1330 end
1331
1332 @spec external_users(keyword()) :: [User.t()]
1333 def external_users(opts \\ []) do
1334 query =
1335 external_users_query()
1336 |> select([u], struct(u, [:id, :ap_id, :info]))
1337
1338 query =
1339 if opts[:max_id],
1340 do: where(query, [u], u.id > ^opts[:max_id]),
1341 else: query
1342
1343 query =
1344 if opts[:limit],
1345 do: limit(query, ^opts[:limit]),
1346 else: query
1347
1348 Repo.all(query)
1349 end
1350
1351 def blocks_import(%User{} = blocker, blocked_identifiers) when is_list(blocked_identifiers) do
1352 BackgroundWorker.enqueue("blocks_import", %{
1353 "blocker_id" => blocker.id,
1354 "blocked_identifiers" => blocked_identifiers
1355 })
1356 end
1357
1358 def follow_import(%User{} = follower, followed_identifiers)
1359 when is_list(followed_identifiers) do
1360 BackgroundWorker.enqueue("follow_import", %{
1361 "follower_id" => follower.id,
1362 "followed_identifiers" => followed_identifiers
1363 })
1364 end
1365
1366 def delete_user_activities(%User{ap_id: ap_id}) do
1367 ap_id
1368 |> Activity.Queries.by_actor()
1369 |> RepoStreamer.chunk_stream(50)
1370 |> Stream.each(fn activities -> Enum.each(activities, &delete_activity/1) end)
1371 |> Stream.run()
1372 end
1373
1374 defp delete_activity(%{data: %{"type" => "Create"}} = activity) do
1375 activity
1376 |> Object.normalize()
1377 |> ActivityPub.delete()
1378 end
1379
1380 defp delete_activity(%{data: %{"type" => "Like"}} = activity) do
1381 object = Object.normalize(activity)
1382
1383 activity.actor
1384 |> get_cached_by_ap_id()
1385 |> ActivityPub.unlike(object)
1386 end
1387
1388 defp delete_activity(%{data: %{"type" => "Announce"}} = activity) do
1389 object = Object.normalize(activity)
1390
1391 activity.actor
1392 |> get_cached_by_ap_id()
1393 |> ActivityPub.unannounce(object)
1394 end
1395
1396 defp delete_activity(_activity), do: "Doing nothing"
1397
1398 def html_filter_policy(%User{no_rich_text: true}) do
1399 Pleroma.HTML.Scrubber.TwitterText
1400 end
1401
1402 def html_filter_policy(_), do: Pleroma.Config.get([:markup, :scrub_policy])
1403
1404 def fetch_by_ap_id(ap_id), do: ActivityPub.make_user_from_ap_id(ap_id)
1405
1406 def get_or_fetch_by_ap_id(ap_id) do
1407 user = get_cached_by_ap_id(ap_id)
1408
1409 if !is_nil(user) and !needs_update?(user) do
1410 {:ok, user}
1411 else
1412 # Whether to fetch initial posts for the user (if it's a new user & the fetching is enabled)
1413 should_fetch_initial = is_nil(user) and Pleroma.Config.get([:fetch_initial_posts, :enabled])
1414
1415 resp = fetch_by_ap_id(ap_id)
1416
1417 if should_fetch_initial do
1418 with {:ok, %User{} = user} <- resp do
1419 fetch_initial_posts(user)
1420 end
1421 end
1422
1423 resp
1424 end
1425 end
1426
1427 @doc "Creates an internal service actor by URI if missing. Optionally takes nickname for addressing."
1428 def get_or_create_service_actor_by_ap_id(uri, nickname \\ nil) do
1429 with %User{} = user <- get_cached_by_ap_id(uri) do
1430 user
1431 else
1432 _ ->
1433 {:ok, user} =
1434 %User{}
1435 |> cast(%{}, [:ap_id, :nickname, :local])
1436 |> put_change(:ap_id, uri)
1437 |> put_change(:nickname, nickname)
1438 |> put_change(:local, true)
1439 |> put_change(:follower_address, uri <> "/followers")
1440 |> Repo.insert()
1441
1442 user
1443 end
1444 end
1445
1446 # AP style
1447 def public_key(%{source_data: %{"publicKey" => %{"publicKeyPem" => public_key_pem}}}) do
1448 key =
1449 public_key_pem
1450 |> :public_key.pem_decode()
1451 |> hd()
1452 |> :public_key.pem_entry_decode()
1453
1454 {:ok, key}
1455 end
1456
1457 def public_key(_), do: {:error, "not found key"}
1458
1459 def get_public_key_for_ap_id(ap_id) do
1460 with {:ok, %User{} = user} <- get_or_fetch_by_ap_id(ap_id),
1461 {:ok, public_key} <- public_key(user) do
1462 {:ok, public_key}
1463 else
1464 _ -> :error
1465 end
1466 end
1467
1468 defp blank?(""), do: nil
1469 defp blank?(n), do: n
1470
1471 def insert_or_update_user(data) do
1472 data
1473 |> Map.put(:name, blank?(data[:name]) || data[:nickname])
1474 |> remote_user_creation()
1475 |> Repo.insert(on_conflict: :replace_all_except_primary_key, conflict_target: :nickname)
1476 |> set_cache()
1477 end
1478
1479 def ap_enabled?(%User{local: true}), do: true
1480 def ap_enabled?(%User{ap_enabled: ap_enabled}), do: ap_enabled
1481 def ap_enabled?(_), do: false
1482
1483 @doc "Gets or fetch a user by uri or nickname."
1484 @spec get_or_fetch(String.t()) :: {:ok, User.t()} | {:error, String.t()}
1485 def get_or_fetch("http" <> _host = uri), do: get_or_fetch_by_ap_id(uri)
1486 def get_or_fetch(nickname), do: get_or_fetch_by_nickname(nickname)
1487
1488 # wait a period of time and return newest version of the User structs
1489 # this is because we have synchronous follow APIs and need to simulate them
1490 # with an async handshake
1491 def wait_and_refresh(_, %User{local: true} = a, %User{local: true} = b) do
1492 with %User{} = a <- get_cached_by_id(a.id),
1493 %User{} = b <- get_cached_by_id(b.id) do
1494 {:ok, a, b}
1495 else
1496 nil -> :error
1497 end
1498 end
1499
1500 def wait_and_refresh(timeout, %User{} = a, %User{} = b) do
1501 with :ok <- :timer.sleep(timeout),
1502 %User{} = a <- get_cached_by_id(a.id),
1503 %User{} = b <- get_cached_by_id(b.id) do
1504 {:ok, a, b}
1505 else
1506 nil -> :error
1507 end
1508 end
1509
1510 def parse_bio(bio) when is_binary(bio) and bio != "" do
1511 bio
1512 |> CommonUtils.format_input("text/plain", mentions_format: :full)
1513 |> elem(0)
1514 end
1515
1516 def parse_bio(_), do: ""
1517
1518 def parse_bio(bio, user) when is_binary(bio) and bio != "" do
1519 # TODO: get profile URLs other than user.ap_id
1520 profile_urls = [user.ap_id]
1521
1522 bio
1523 |> CommonUtils.format_input("text/plain",
1524 mentions_format: :full,
1525 rel: &RelMe.maybe_put_rel_me(&1, profile_urls)
1526 )
1527 |> elem(0)
1528 end
1529
1530 def parse_bio(_, _), do: ""
1531
1532 def tag(user_identifiers, tags) when is_list(user_identifiers) do
1533 Repo.transaction(fn ->
1534 for user_identifier <- user_identifiers, do: tag(user_identifier, tags)
1535 end)
1536 end
1537
1538 def tag(nickname, tags) when is_binary(nickname),
1539 do: tag(get_by_nickname(nickname), tags)
1540
1541 def tag(%User{} = user, tags),
1542 do: update_tags(user, Enum.uniq((user.tags || []) ++ normalize_tags(tags)))
1543
1544 def untag(user_identifiers, tags) when is_list(user_identifiers) do
1545 Repo.transaction(fn ->
1546 for user_identifier <- user_identifiers, do: untag(user_identifier, tags)
1547 end)
1548 end
1549
1550 def untag(nickname, tags) when is_binary(nickname),
1551 do: untag(get_by_nickname(nickname), tags)
1552
1553 def untag(%User{} = user, tags),
1554 do: update_tags(user, (user.tags || []) -- normalize_tags(tags))
1555
1556 defp update_tags(%User{} = user, new_tags) do
1557 {:ok, updated_user} =
1558 user
1559 |> change(%{tags: new_tags})
1560 |> update_and_set_cache()
1561
1562 updated_user
1563 end
1564
1565 defp normalize_tags(tags) do
1566 [tags]
1567 |> List.flatten()
1568 |> Enum.map(&String.downcase/1)
1569 end
1570
1571 defp local_nickname_regex do
1572 if Pleroma.Config.get([:instance, :extended_nickname_format]) do
1573 @extended_local_nickname_regex
1574 else
1575 @strict_local_nickname_regex
1576 end
1577 end
1578
1579 def local_nickname(nickname_or_mention) do
1580 nickname_or_mention
1581 |> full_nickname()
1582 |> String.split("@")
1583 |> hd()
1584 end
1585
1586 def full_nickname(nickname_or_mention),
1587 do: String.trim_leading(nickname_or_mention, "@")
1588
1589 def error_user(ap_id) do
1590 %User{
1591 name: ap_id,
1592 ap_id: ap_id,
1593 nickname: "erroruser@example.com",
1594 inserted_at: NaiveDateTime.utc_now()
1595 }
1596 end
1597
1598 @spec all_superusers() :: [User.t()]
1599 def all_superusers do
1600 User.Query.build(%{super_users: true, local: true, deactivated: false})
1601 |> Repo.all()
1602 end
1603
1604 def showing_reblogs?(%User{} = user, %User{} = target) do
1605 target.ap_id not in user.muted_reblogs
1606 end
1607
1608 @doc """
1609 The function returns a query to get users with no activity for given interval of days.
1610 Inactive users are those who didn't read any notification, or had any activity where
1611 the user is the activity's actor, during `inactivity_threshold` days.
1612 Deactivated users will not appear in this list.
1613
1614 ## Examples
1615
1616 iex> Pleroma.User.list_inactive_users()
1617 %Ecto.Query{}
1618 """
1619 @spec list_inactive_users_query(integer()) :: Ecto.Query.t()
1620 def list_inactive_users_query(inactivity_threshold \\ 7) do
1621 negative_inactivity_threshold = -inactivity_threshold
1622 now = NaiveDateTime.truncate(NaiveDateTime.utc_now(), :second)
1623 # Subqueries are not supported in `where` clauses, join gets too complicated.
1624 has_read_notifications =
1625 from(n in Pleroma.Notification,
1626 where: n.seen == true,
1627 group_by: n.id,
1628 having: max(n.updated_at) > datetime_add(^now, ^negative_inactivity_threshold, "day"),
1629 select: n.user_id
1630 )
1631 |> Pleroma.Repo.all()
1632
1633 from(u in Pleroma.User,
1634 left_join: a in Pleroma.Activity,
1635 on: u.ap_id == a.actor,
1636 where: not is_nil(u.nickname),
1637 where: u.deactivated != ^true,
1638 where: u.id not in ^has_read_notifications,
1639 group_by: u.id,
1640 having:
1641 max(a.inserted_at) < datetime_add(^now, ^negative_inactivity_threshold, "day") or
1642 is_nil(max(a.inserted_at))
1643 )
1644 end
1645
1646 @doc """
1647 Enable or disable email notifications for user
1648
1649 ## Examples
1650
1651 iex> Pleroma.User.switch_email_notifications(Pleroma.User{email_notifications: %{"digest" => false}}, "digest", true)
1652 Pleroma.User{email_notifications: %{"digest" => true}}
1653
1654 iex> Pleroma.User.switch_email_notifications(Pleroma.User{email_notifications: %{"digest" => true}}, "digest", false)
1655 Pleroma.User{email_notifications: %{"digest" => false}}
1656 """
1657 @spec switch_email_notifications(t(), String.t(), boolean()) ::
1658 {:ok, t()} | {:error, Ecto.Changeset.t()}
1659 def switch_email_notifications(user, type, status) do
1660 User.update_email_notifications(user, %{type => status})
1661 end
1662
1663 @doc """
1664 Set `last_digest_emailed_at` value for the user to current time
1665 """
1666 @spec touch_last_digest_emailed_at(t()) :: t()
1667 def touch_last_digest_emailed_at(user) do
1668 now = NaiveDateTime.truncate(NaiveDateTime.utc_now(), :second)
1669
1670 {:ok, updated_user} =
1671 user
1672 |> change(%{last_digest_emailed_at: now})
1673 |> update_and_set_cache()
1674
1675 updated_user
1676 end
1677
1678 @spec toggle_confirmation(User.t()) :: {:ok, User.t()} | {:error, Changeset.t()}
1679 def toggle_confirmation(%User{} = user) do
1680 user
1681 |> confirmation_changeset(need_confirmation: !user.confirmation_pending)
1682 |> update_and_set_cache()
1683 end
1684
1685 def get_mascot(%{mascot: %{} = mascot}) when not is_nil(mascot) do
1686 mascot
1687 end
1688
1689 def get_mascot(%{mascot: mascot}) when is_nil(mascot) do
1690 # use instance-default
1691 config = Pleroma.Config.get([:assets, :mascots])
1692 default_mascot = Pleroma.Config.get([:assets, :default_mascot])
1693 mascot = Keyword.get(config, default_mascot)
1694
1695 %{
1696 "id" => "default-mascot",
1697 "url" => mascot[:url],
1698 "preview_url" => mascot[:url],
1699 "pleroma" => %{
1700 "mime_type" => mascot[:mime_type]
1701 }
1702 }
1703 end
1704
1705 def ensure_keys_present(%{keys: keys} = user) when not is_nil(keys), do: {:ok, user}
1706
1707 def ensure_keys_present(%User{} = user) do
1708 with {:ok, pem} <- Keys.generate_rsa_pem() do
1709 user
1710 |> cast(%{keys: pem}, [:keys])
1711 |> validate_required([:keys])
1712 |> update_and_set_cache()
1713 end
1714 end
1715
1716 def get_ap_ids_by_nicknames(nicknames) do
1717 from(u in User,
1718 where: u.nickname in ^nicknames,
1719 select: u.ap_id
1720 )
1721 |> Repo.all()
1722 end
1723
1724 defdelegate search(query, opts \\ []), to: User.Search
1725
1726 defp put_password_hash(
1727 %Ecto.Changeset{valid?: true, changes: %{password: password}} = changeset
1728 ) do
1729 change(changeset, password_hash: Pbkdf2.hashpwsalt(password))
1730 end
1731
1732 defp put_password_hash(changeset), do: changeset
1733
1734 def is_internal_user?(%User{nickname: nil}), do: true
1735 def is_internal_user?(%User{local: true, nickname: "internal." <> _}), do: true
1736 def is_internal_user?(_), do: false
1737
1738 # A hack because user delete activities have a fake id for whatever reason
1739 # TODO: Get rid of this
1740 def get_delivered_users_by_object_id("pleroma:fake_object_id"), do: []
1741
1742 def get_delivered_users_by_object_id(object_id) do
1743 from(u in User,
1744 inner_join: delivery in assoc(u, :deliveries),
1745 where: delivery.object_id == ^object_id
1746 )
1747 |> Repo.all()
1748 end
1749
1750 def change_email(user, email) do
1751 user
1752 |> cast(%{email: email}, [:email])
1753 |> validate_required([:email])
1754 |> unique_constraint(:email)
1755 |> validate_format(:email, @email_regex)
1756 |> update_and_set_cache()
1757 end
1758
1759 # Internal function; public one is `deactivate/2`
1760 defp set_activation_status(user, deactivated) do
1761 user
1762 |> cast(%{deactivated: deactivated}, [:deactivated])
1763 |> update_and_set_cache()
1764 end
1765
1766 def update_banner(user, banner) do
1767 user
1768 |> cast(%{banner: banner}, [:banner])
1769 |> update_and_set_cache()
1770 end
1771
1772 def update_background(user, background) do
1773 user
1774 |> cast(%{background: background}, [:background])
1775 |> update_and_set_cache()
1776 end
1777
1778 def update_source_data(user, source_data) do
1779 user
1780 |> cast(%{source_data: source_data}, [:source_data])
1781 |> update_and_set_cache()
1782 end
1783
1784 def roles(%{is_moderator: is_moderator, is_admin: is_admin}) do
1785 %{
1786 admin: is_admin,
1787 moderator: is_moderator
1788 }
1789 end
1790
1791 # ``fields`` is an array of mastodon profile field, containing ``{"name": "…", "value": "…"}``.
1792 # For example: [{"name": "Pronoun", "value": "she/her"}, …]
1793 def fields(%{fields: nil, source_data: %{"attachment" => attachment}}) do
1794 limit = Pleroma.Config.get([:instance, :max_remote_account_fields], 0)
1795
1796 attachment
1797 |> Enum.filter(fn %{"type" => t} -> t == "PropertyValue" end)
1798 |> Enum.map(fn fields -> Map.take(fields, ["name", "value"]) end)
1799 |> Enum.take(limit)
1800 end
1801
1802 def fields(%{fields: nil}), do: []
1803
1804 def fields(%{fields: fields}), do: fields
1805
1806 def validate_fields(changeset, remote? \\ false) do
1807 limit_name = if remote?, do: :max_remote_account_fields, else: :max_account_fields
1808 limit = Pleroma.Config.get([:instance, limit_name], 0)
1809
1810 changeset
1811 |> validate_length(:fields, max: limit)
1812 |> validate_change(:fields, fn :fields, fields ->
1813 if Enum.all?(fields, &valid_field?/1) do
1814 []
1815 else
1816 [fields: "invalid"]
1817 end
1818 end)
1819 end
1820
1821 defp valid_field?(%{"name" => name, "value" => value}) do
1822 name_limit = Pleroma.Config.get([:instance, :account_field_name_length], 255)
1823 value_limit = Pleroma.Config.get([:instance, :account_field_value_length], 255)
1824
1825 is_binary(name) && is_binary(value) && String.length(name) <= name_limit &&
1826 String.length(value) <= value_limit
1827 end
1828
1829 defp valid_field?(_), do: false
1830
1831 defp truncate_field(%{"name" => name, "value" => value}) do
1832 {name, _chopped} =
1833 String.split_at(name, Pleroma.Config.get([:instance, :account_field_name_length], 255))
1834
1835 {value, _chopped} =
1836 String.split_at(value, Pleroma.Config.get([:instance, :account_field_value_length], 255))
1837
1838 %{"name" => name, "value" => value}
1839 end
1840
1841 def admin_api_update(user, params) do
1842 user
1843 |> cast(params, [
1844 :is_moderator,
1845 :is_admin,
1846 :show_role
1847 ])
1848 |> update_and_set_cache()
1849 end
1850
1851 def mascot_update(user, url) do
1852 user
1853 |> cast(%{mascot: url}, [:mascot])
1854 |> validate_required([:mascot])
1855 |> update_and_set_cache()
1856 end
1857
1858 def mastodon_settings_update(user, settings) do
1859 user
1860 |> cast(%{settings: settings}, [:settings])
1861 |> validate_required([:settings])
1862 |> update_and_set_cache()
1863 end
1864
1865 @spec confirmation_changeset(User.t(), keyword()) :: Changeset.t()
1866 def confirmation_changeset(user, need_confirmation: need_confirmation?) do
1867 params =
1868 if need_confirmation? do
1869 %{
1870 confirmation_pending: true,
1871 confirmation_token: :crypto.strong_rand_bytes(32) |> Base.url_encode64()
1872 }
1873 else
1874 %{
1875 confirmation_pending: false,
1876 confirmation_token: nil
1877 }
1878 end
1879
1880 cast(user, params, [:confirmation_pending, :confirmation_token])
1881 end
1882
1883 def add_pinnned_activity(user, %Pleroma.Activity{id: id}) do
1884 if id not in user.pinned_activities do
1885 max_pinned_statuses = Pleroma.Config.get([:instance, :max_pinned_statuses], 0)
1886 params = %{pinned_activities: user.pinned_activities ++ [id]}
1887
1888 user
1889 |> cast(params, [:pinned_activities])
1890 |> validate_length(:pinned_activities,
1891 max: max_pinned_statuses,
1892 message: "You have already pinned the maximum number of statuses"
1893 )
1894 else
1895 change(user)
1896 end
1897 |> update_and_set_cache()
1898 end
1899
1900 def remove_pinnned_activity(user, %Pleroma.Activity{id: id}) do
1901 params = %{pinned_activities: List.delete(user.pinned_activities, id)}
1902
1903 user
1904 |> cast(params, [:pinned_activities])
1905 |> update_and_set_cache()
1906 end
1907
1908 def update_email_notifications(user, settings) do
1909 email_notifications =
1910 user.email_notifications
1911 |> Map.merge(settings)
1912 |> Map.take(["digest"])
1913
1914 params = %{email_notifications: email_notifications}
1915 fields = [:email_notifications]
1916
1917 user
1918 |> cast(params, fields)
1919 |> validate_required(fields)
1920 |> update_and_set_cache()
1921 end
1922
1923 defp set_subscribers(user, subscribers) do
1924 params = %{subscribers: subscribers}
1925
1926 user
1927 |> cast(params, [:subscribers])
1928 |> validate_required([:subscribers])
1929 |> update_and_set_cache()
1930 end
1931
1932 def add_to_subscribers(user, subscribed) do
1933 set_subscribers(user, Enum.uniq([subscribed | user.subscribers]))
1934 end
1935
1936 def remove_from_subscribers(user, subscribed) do
1937 set_subscribers(user, List.delete(user.subscribers, subscribed))
1938 end
1939
1940 defp set_domain_blocks(user, domain_blocks) do
1941 params = %{domain_blocks: domain_blocks}
1942
1943 user
1944 |> cast(params, [:domain_blocks])
1945 |> validate_required([:domain_blocks])
1946 |> update_and_set_cache()
1947 end
1948
1949 def block_domain(user, domain_blocked) do
1950 set_domain_blocks(user, Enum.uniq([domain_blocked | user.domain_blocks]))
1951 end
1952
1953 def unblock_domain(user, domain_blocked) do
1954 set_domain_blocks(user, List.delete(user.domain_blocks, domain_blocked))
1955 end
1956
1957 defp set_blocks(user, blocks) do
1958 params = %{blocks: blocks}
1959
1960 user
1961 |> cast(params, [:blocks])
1962 |> validate_required([:blocks])
1963 |> update_and_set_cache()
1964 end
1965
1966 def add_to_block(user, blocked) do
1967 set_blocks(user, Enum.uniq([blocked | user.blocks]))
1968 end
1969
1970 def remove_from_block(user, blocked) do
1971 set_blocks(user, List.delete(user.blocks, blocked))
1972 end
1973
1974 defp set_mutes(user, mutes) do
1975 params = %{mutes: mutes}
1976
1977 user
1978 |> cast(params, [:mutes])
1979 |> validate_required([:mutes])
1980 |> update_and_set_cache()
1981 end
1982
1983 def add_to_mutes(user, muted, notifications?) do
1984 with {:ok, user} <- set_mutes(user, Enum.uniq([muted | user.mutes])) do
1985 set_notification_mutes(
1986 user,
1987 Enum.uniq([muted | user.muted_notifications]),
1988 notifications?
1989 )
1990 end
1991 end
1992
1993 def remove_from_mutes(user, muted) do
1994 with {:ok, user} <- set_mutes(user, List.delete(user.mutes, muted)) do
1995 set_notification_mutes(
1996 user,
1997 List.delete(user.muted_notifications, muted),
1998 true
1999 )
2000 end
2001 end
2002
2003 defp set_notification_mutes(user, _muted_notifications, false = _notifications?) do
2004 {:ok, user}
2005 end
2006
2007 defp set_notification_mutes(user, muted_notifications, true = _notifications?) do
2008 params = %{muted_notifications: muted_notifications}
2009
2010 user
2011 |> cast(params, [:muted_notifications])
2012 |> validate_required([:muted_notifications])
2013 |> update_and_set_cache()
2014 end
2015
2016 def add_reblog_mute(user, ap_id) do
2017 params = %{muted_reblogs: user.muted_reblogs ++ [ap_id]}
2018
2019 user
2020 |> cast(params, [:muted_reblogs])
2021 |> update_and_set_cache()
2022 end
2023
2024 def remove_reblog_mute(user, ap_id) do
2025 params = %{muted_reblogs: List.delete(user.muted_reblogs, ap_id)}
2026
2027 user
2028 |> cast(params, [:muted_reblogs])
2029 |> update_and_set_cache()
2030 end
2031
2032 def set_invisible(user, invisible) do
2033 params = %{invisible: invisible}
2034
2035 user
2036 |> cast(params, [:invisible])
2037 |> validate_required([:invisible])
2038 |> update_and_set_cache()
2039 end
2040 end