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