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