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