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