Merge remote-tracking branch 'remotes/origin/develop' into feature/object-hashtags...
[akkoma] / lib / pleroma / user.ex
1 # Pleroma: A lightweight social networking server
2 # Copyright © 2017-2021 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 Ecto.Multi
13 alias Pleroma.Activity
14 alias Pleroma.Config
15 alias Pleroma.Conversation.Participation
16 alias Pleroma.Delivery
17 alias Pleroma.EctoType.ActivityPub.ObjectValidators
18 alias Pleroma.Emoji
19 alias Pleroma.FollowingRelationship
20 alias Pleroma.Formatter
21 alias Pleroma.HTML
22 alias Pleroma.Keys
23 alias Pleroma.MFA
24 alias Pleroma.Notification
25 alias Pleroma.Object
26 alias Pleroma.Registration
27 alias Pleroma.Repo
28 alias Pleroma.User
29 alias Pleroma.UserRelationship
30 alias Pleroma.Web
31 alias Pleroma.Web.ActivityPub.ActivityPub
32 alias Pleroma.Web.ActivityPub.Builder
33 alias Pleroma.Web.ActivityPub.Pipeline
34 alias Pleroma.Web.ActivityPub.Utils
35 alias Pleroma.Web.CommonAPI
36 alias Pleroma.Web.CommonAPI.Utils, as: CommonUtils
37 alias Pleroma.Web.OAuth
38 alias Pleroma.Web.RelMe
39 alias Pleroma.Workers.BackgroundWorker
40
41 require Logger
42
43 @type t :: %__MODULE__{}
44 @type account_status ::
45 :active
46 | :deactivated
47 | :password_reset_pending
48 | :confirmation_pending
49 | :approval_pending
50 @primary_key {:id, FlakeId.Ecto.CompatType, autogenerate: true}
51
52 # credo:disable-for-next-line Credo.Check.Readability.MaxLineLength
53 @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])?)*$/
54
55 @strict_local_nickname_regex ~r/^[a-zA-Z\d]+$/
56 @extended_local_nickname_regex ~r/^[a-zA-Z\d_-]+$/
57
58 # AP ID user relationships (blocks, mutes etc.)
59 # Format: [rel_type: [outgoing_rel: :outgoing_rel_target, incoming_rel: :incoming_rel_source]]
60 @user_relationships_config [
61 block: [
62 blocker_blocks: :blocked_users,
63 blockee_blocks: :blocker_users
64 ],
65 mute: [
66 muter_mutes: :muted_users,
67 mutee_mutes: :muter_users
68 ],
69 reblog_mute: [
70 reblog_muter_mutes: :reblog_muted_users,
71 reblog_mutee_mutes: :reblog_muter_users
72 ],
73 notification_mute: [
74 notification_muter_mutes: :notification_muted_users,
75 notification_mutee_mutes: :notification_muter_users
76 ],
77 # Note: `inverse_subscription` relationship is inverse: subscriber acts as relationship target
78 inverse_subscription: [
79 subscribee_subscriptions: :subscriber_users,
80 subscriber_subscriptions: :subscribee_users
81 ]
82 ]
83
84 @cachex Pleroma.Config.get([:cachex, :provider], Cachex)
85
86 schema "users" do
87 field(:bio, :string, default: "")
88 field(:raw_bio, :string)
89 field(:email, :string)
90 field(:name, :string)
91 field(:nickname, :string)
92 field(:password_hash, :string)
93 field(:password, :string, virtual: true)
94 field(:password_confirmation, :string, virtual: true)
95 field(:keys, :string)
96 field(:public_key, :string)
97 field(:ap_id, :string)
98 field(:avatar, :map, default: %{})
99 field(:local, :boolean, default: true)
100 field(:follower_address, :string)
101 field(:following_address, :string)
102 field(:search_rank, :float, virtual: true)
103 field(:search_type, :integer, virtual: true)
104 field(:tags, {:array, :string}, default: [])
105 field(:last_refreshed_at, :naive_datetime_usec)
106 field(:last_digest_emailed_at, :naive_datetime)
107 field(:banner, :map, default: %{})
108 field(:background, :map, default: %{})
109 field(:note_count, :integer, default: 0)
110 field(:follower_count, :integer, default: 0)
111 field(:following_count, :integer, default: 0)
112 field(:is_locked, :boolean, default: false)
113 field(:is_confirmed, :boolean, default: true)
114 field(:password_reset_pending, :boolean, default: false)
115 field(:is_approved, :boolean, default: true)
116 field(:registration_reason, :string, default: nil)
117 field(:confirmation_token, :string, default: nil)
118 field(:default_scope, :string, default: "public")
119 field(:domain_blocks, {:array, :string}, default: [])
120 field(:is_active, :boolean, default: true)
121 field(:no_rich_text, :boolean, default: false)
122 field(:ap_enabled, :boolean, default: false)
123 field(:is_moderator, :boolean, default: false)
124 field(:is_admin, :boolean, default: false)
125 field(:show_role, :boolean, default: true)
126 field(:mastofe_settings, :map, default: nil)
127 field(:uri, ObjectValidators.Uri, default: nil)
128 field(:hide_followers_count, :boolean, default: false)
129 field(:hide_follows_count, :boolean, default: false)
130 field(:hide_followers, :boolean, default: false)
131 field(:hide_follows, :boolean, default: false)
132 field(:hide_favorites, :boolean, default: true)
133 field(:pinned_activities, {:array, :string}, default: [])
134 field(:email_notifications, :map, default: %{"digest" => false})
135 field(:mascot, :map, default: nil)
136 field(:emoji, :map, default: %{})
137 field(:pleroma_settings_store, :map, default: %{})
138 field(:fields, {:array, :map}, default: [])
139 field(:raw_fields, {:array, :map}, default: [])
140 field(:is_discoverable, :boolean, default: false)
141 field(:invisible, :boolean, default: false)
142 field(:allow_following_move, :boolean, default: true)
143 field(:skip_thread_containment, :boolean, default: false)
144 field(:actor_type, :string, default: "Person")
145 field(:also_known_as, {:array, ObjectValidators.ObjectID}, default: [])
146 field(:inbox, :string)
147 field(:shared_inbox, :string)
148 field(:accepts_chat_messages, :boolean, default: nil)
149 field(:last_active_at, :naive_datetime)
150
151 embeds_one(
152 :notification_settings,
153 Pleroma.User.NotificationSetting,
154 on_replace: :update
155 )
156
157 has_many(:notifications, Notification)
158 has_many(:registrations, Registration)
159 has_many(:deliveries, Delivery)
160
161 has_many(:outgoing_relationships, UserRelationship, foreign_key: :source_id)
162 has_many(:incoming_relationships, UserRelationship, foreign_key: :target_id)
163
164 for {relationship_type,
165 [
166 {outgoing_relation, outgoing_relation_target},
167 {incoming_relation, incoming_relation_source}
168 ]} <- @user_relationships_config do
169 # Definitions of `has_many` relations: :blocker_blocks, :muter_mutes, :reblog_muter_mutes,
170 # :notification_muter_mutes, :subscribee_subscriptions
171 has_many(outgoing_relation, UserRelationship,
172 foreign_key: :source_id,
173 where: [relationship_type: relationship_type]
174 )
175
176 # Definitions of `has_many` relations: :blockee_blocks, :mutee_mutes, :reblog_mutee_mutes,
177 # :notification_mutee_mutes, :subscriber_subscriptions
178 has_many(incoming_relation, UserRelationship,
179 foreign_key: :target_id,
180 where: [relationship_type: relationship_type]
181 )
182
183 # Definitions of `has_many` relations: :blocked_users, :muted_users, :reblog_muted_users,
184 # :notification_muted_users, :subscriber_users
185 has_many(outgoing_relation_target, through: [outgoing_relation, :target])
186
187 # Definitions of `has_many` relations: :blocker_users, :muter_users, :reblog_muter_users,
188 # :notification_muter_users, :subscribee_users
189 has_many(incoming_relation_source, through: [incoming_relation, :source])
190 end
191
192 # `:blocks` is deprecated (replaced with `blocked_users` relation)
193 field(:blocks, {:array, :string}, default: [])
194 # `:mutes` is deprecated (replaced with `muted_users` relation)
195 field(:mutes, {:array, :string}, default: [])
196 # `:muted_reblogs` is deprecated (replaced with `reblog_muted_users` relation)
197 field(:muted_reblogs, {:array, :string}, default: [])
198 # `:muted_notifications` is deprecated (replaced with `notification_muted_users` relation)
199 field(:muted_notifications, {:array, :string}, default: [])
200 # `:subscribers` is deprecated (replaced with `subscriber_users` relation)
201 field(:subscribers, {:array, :string}, default: [])
202
203 embeds_one(
204 :multi_factor_authentication_settings,
205 MFA.Settings,
206 on_replace: :delete
207 )
208
209 timestamps()
210 end
211
212 for {_relationship_type, [{_outgoing_relation, outgoing_relation_target}, _]} <-
213 @user_relationships_config do
214 # `def blocked_users_relation/2`, `def muted_users_relation/2`,
215 # `def reblog_muted_users_relation/2`, `def notification_muted_users/2`,
216 # `def subscriber_users/2`
217 def unquote(:"#{outgoing_relation_target}_relation")(user, restrict_deactivated? \\ false) do
218 target_users_query = assoc(user, unquote(outgoing_relation_target))
219
220 if restrict_deactivated? do
221 target_users_query
222 |> User.Query.build(%{deactivated: false})
223 else
224 target_users_query
225 end
226 end
227
228 # `def blocked_users/2`, `def muted_users/2`, `def reblog_muted_users/2`,
229 # `def notification_muted_users/2`, `def subscriber_users/2`
230 def unquote(outgoing_relation_target)(user, restrict_deactivated? \\ false) do
231 __MODULE__
232 |> apply(unquote(:"#{outgoing_relation_target}_relation"), [
233 user,
234 restrict_deactivated?
235 ])
236 |> Repo.all()
237 end
238
239 # `def blocked_users_ap_ids/2`, `def muted_users_ap_ids/2`, `def reblog_muted_users_ap_ids/2`,
240 # `def notification_muted_users_ap_ids/2`, `def subscriber_users_ap_ids/2`
241 def unquote(:"#{outgoing_relation_target}_ap_ids")(user, restrict_deactivated? \\ false) do
242 __MODULE__
243 |> apply(unquote(:"#{outgoing_relation_target}_relation"), [
244 user,
245 restrict_deactivated?
246 ])
247 |> select([u], u.ap_id)
248 |> Repo.all()
249 end
250 end
251
252 def cached_blocked_users_ap_ids(user) do
253 @cachex.fetch!(:user_cache, "blocked_users_ap_ids:#{user.ap_id}", fn _ ->
254 blocked_users_ap_ids(user)
255 end)
256 end
257
258 def cached_muted_users_ap_ids(user) do
259 @cachex.fetch!(:user_cache, "muted_users_ap_ids:#{user.ap_id}", fn _ ->
260 muted_users_ap_ids(user)
261 end)
262 end
263
264 defdelegate following_count(user), to: FollowingRelationship
265 defdelegate following(user), to: FollowingRelationship
266 defdelegate following?(follower, followed), to: FollowingRelationship
267 defdelegate following_ap_ids(user), to: FollowingRelationship
268 defdelegate get_follow_requests(user), to: FollowingRelationship
269 defdelegate search(query, opts \\ []), to: User.Search
270
271 @doc """
272 Dumps Flake Id to SQL-compatible format (16-byte UUID).
273 E.g. "9pQtDGXuq4p3VlcJEm" -> <<0, 0, 1, 110, 179, 218, 42, 92, 213, 41, 44, 227, 95, 213, 0, 0>>
274 """
275 def binary_id(source_id) when is_binary(source_id) do
276 with {:ok, dumped_id} <- FlakeId.Ecto.CompatType.dump(source_id) do
277 dumped_id
278 else
279 _ -> source_id
280 end
281 end
282
283 def binary_id(source_ids) when is_list(source_ids) do
284 Enum.map(source_ids, &binary_id/1)
285 end
286
287 def binary_id(%User{} = user), do: binary_id(user.id)
288
289 @doc "Returns status account"
290 @spec account_status(User.t()) :: account_status()
291 def account_status(%User{is_active: false}), do: :deactivated
292 def account_status(%User{password_reset_pending: true}), do: :password_reset_pending
293 def account_status(%User{local: true, is_approved: false}), do: :approval_pending
294 def account_status(%User{local: true, is_confirmed: false}), do: :confirmation_pending
295 def account_status(%User{}), do: :active
296
297 @spec visible_for(User.t(), User.t() | nil) ::
298 :visible
299 | :invisible
300 | :restricted_unauthenticated
301 | :deactivated
302 | :confirmation_pending
303 def visible_for(user, for_user \\ nil)
304
305 def visible_for(%User{invisible: true}, _), do: :invisible
306
307 def visible_for(%User{id: user_id}, %User{id: user_id}), do: :visible
308
309 def visible_for(%User{} = user, nil) do
310 if restrict_unauthenticated?(user) do
311 :restrict_unauthenticated
312 else
313 visible_account_status(user)
314 end
315 end
316
317 def visible_for(%User{} = user, for_user) do
318 if superuser?(for_user) do
319 :visible
320 else
321 visible_account_status(user)
322 end
323 end
324
325 def visible_for(_, _), do: :invisible
326
327 defp restrict_unauthenticated?(%User{local: true}) do
328 Config.restrict_unauthenticated_access?(:profiles, :local)
329 end
330
331 defp restrict_unauthenticated?(%User{local: _}) do
332 Config.restrict_unauthenticated_access?(:profiles, :remote)
333 end
334
335 defp visible_account_status(user) do
336 status = account_status(user)
337
338 if status in [:active, :password_reset_pending] do
339 :visible
340 else
341 status
342 end
343 end
344
345 @spec superuser?(User.t()) :: boolean()
346 def superuser?(%User{local: true, is_admin: true}), do: true
347 def superuser?(%User{local: true, is_moderator: true}), do: true
348 def superuser?(_), do: false
349
350 @spec invisible?(User.t()) :: boolean()
351 def invisible?(%User{invisible: true}), do: true
352 def invisible?(_), do: false
353
354 def avatar_url(user, options \\ []) do
355 case user.avatar do
356 %{"url" => [%{"href" => href} | _]} ->
357 href
358
359 _ ->
360 unless options[:no_default] do
361 Config.get([:assets, :default_user_avatar], "#{Web.base_url()}/images/avi.png")
362 end
363 end
364 end
365
366 def banner_url(user, options \\ []) do
367 case user.banner do
368 %{"url" => [%{"href" => href} | _]} -> href
369 _ -> !options[:no_default] && "#{Web.base_url()}/images/banner.png"
370 end
371 end
372
373 # Should probably be renamed or removed
374 def ap_id(%User{nickname: nickname}), do: "#{Web.base_url()}/users/#{nickname}"
375
376 def ap_followers(%User{follower_address: fa}) when is_binary(fa), do: fa
377 def ap_followers(%User{} = user), do: "#{ap_id(user)}/followers"
378
379 @spec ap_following(User.t()) :: String.t()
380 def ap_following(%User{following_address: fa}) when is_binary(fa), do: fa
381 def ap_following(%User{} = user), do: "#{ap_id(user)}/following"
382
383 defp truncate_fields_param(params) do
384 if Map.has_key?(params, :fields) do
385 Map.put(params, :fields, Enum.map(params[:fields], &truncate_field/1))
386 else
387 params
388 end
389 end
390
391 defp truncate_if_exists(params, key, max_length) do
392 if Map.has_key?(params, key) and is_binary(params[key]) do
393 {value, _chopped} = String.split_at(params[key], max_length)
394 Map.put(params, key, value)
395 else
396 params
397 end
398 end
399
400 defp fix_follower_address(%{follower_address: _, following_address: _} = params), do: params
401
402 defp fix_follower_address(%{nickname: nickname} = params),
403 do: Map.put(params, :follower_address, ap_followers(%User{nickname: nickname}))
404
405 defp fix_follower_address(params), do: params
406
407 def remote_user_changeset(struct \\ %User{local: false}, params) do
408 bio_limit = Config.get([:instance, :user_bio_length], 5000)
409 name_limit = Config.get([:instance, :user_name_length], 100)
410
411 name =
412 case params[:name] do
413 name when is_binary(name) and byte_size(name) > 0 -> name
414 _ -> params[:nickname]
415 end
416
417 params =
418 params
419 |> Map.put(:name, name)
420 |> Map.put_new(:last_refreshed_at, NaiveDateTime.utc_now())
421 |> truncate_if_exists(:name, name_limit)
422 |> truncate_if_exists(:bio, bio_limit)
423 |> truncate_fields_param()
424 |> fix_follower_address()
425
426 struct
427 |> cast(
428 params,
429 [
430 :bio,
431 :emoji,
432 :ap_id,
433 :inbox,
434 :shared_inbox,
435 :nickname,
436 :public_key,
437 :avatar,
438 :ap_enabled,
439 :banner,
440 :is_locked,
441 :last_refreshed_at,
442 :uri,
443 :follower_address,
444 :following_address,
445 :hide_followers,
446 :hide_follows,
447 :hide_followers_count,
448 :hide_follows_count,
449 :follower_count,
450 :fields,
451 :following_count,
452 :is_discoverable,
453 :invisible,
454 :actor_type,
455 :also_known_as,
456 :accepts_chat_messages
457 ]
458 )
459 |> cast(params, [:name], empty_values: [])
460 |> validate_required([:ap_id])
461 |> validate_required([:name], trim: false)
462 |> unique_constraint(:nickname)
463 |> validate_format(:nickname, @email_regex)
464 |> validate_length(:bio, max: bio_limit)
465 |> validate_length(:name, max: name_limit)
466 |> validate_fields(true)
467 |> validate_non_local()
468 end
469
470 defp validate_non_local(cng) do
471 local? = get_field(cng, :local)
472
473 if local? do
474 cng
475 |> add_error(:local, "User is local, can't update with this changeset.")
476 else
477 cng
478 end
479 end
480
481 def update_changeset(struct, params \\ %{}) do
482 bio_limit = Config.get([:instance, :user_bio_length], 5000)
483 name_limit = Config.get([:instance, :user_name_length], 100)
484
485 struct
486 |> cast(
487 params,
488 [
489 :bio,
490 :raw_bio,
491 :name,
492 :emoji,
493 :avatar,
494 :public_key,
495 :inbox,
496 :shared_inbox,
497 :is_locked,
498 :no_rich_text,
499 :default_scope,
500 :banner,
501 :hide_follows,
502 :hide_followers,
503 :hide_followers_count,
504 :hide_follows_count,
505 :hide_favorites,
506 :allow_following_move,
507 :also_known_as,
508 :background,
509 :show_role,
510 :skip_thread_containment,
511 :fields,
512 :raw_fields,
513 :pleroma_settings_store,
514 :is_discoverable,
515 :actor_type,
516 :accepts_chat_messages
517 ]
518 )
519 |> unique_constraint(:nickname)
520 |> validate_format(:nickname, local_nickname_regex())
521 |> validate_length(:bio, max: bio_limit)
522 |> validate_length(:name, min: 1, max: name_limit)
523 |> validate_inclusion(:actor_type, ["Person", "Service"])
524 |> put_fields()
525 |> put_emoji()
526 |> put_change_if_present(:bio, &{:ok, parse_bio(&1, struct)})
527 |> put_change_if_present(:avatar, &put_upload(&1, :avatar))
528 |> put_change_if_present(:banner, &put_upload(&1, :banner))
529 |> put_change_if_present(:background, &put_upload(&1, :background))
530 |> put_change_if_present(
531 :pleroma_settings_store,
532 &{:ok, Map.merge(struct.pleroma_settings_store, &1)}
533 )
534 |> validate_fields(false)
535 end
536
537 defp put_fields(changeset) do
538 if raw_fields = get_change(changeset, :raw_fields) do
539 raw_fields =
540 raw_fields
541 |> Enum.filter(fn %{"name" => n} -> n != "" end)
542
543 fields =
544 raw_fields
545 |> Enum.map(fn f -> Map.update!(f, "value", &parse_fields(&1)) end)
546
547 changeset
548 |> put_change(:raw_fields, raw_fields)
549 |> put_change(:fields, fields)
550 else
551 changeset
552 end
553 end
554
555 defp parse_fields(value) do
556 value
557 |> Formatter.linkify(mentions_format: :full)
558 |> elem(0)
559 end
560
561 defp put_emoji(changeset) do
562 emojified_fields = [:bio, :name, :raw_fields]
563
564 if Enum.any?(changeset.changes, fn {k, _} -> k in emojified_fields end) do
565 bio = Emoji.Formatter.get_emoji_map(get_field(changeset, :bio))
566 name = Emoji.Formatter.get_emoji_map(get_field(changeset, :name))
567
568 emoji = Map.merge(bio, name)
569
570 emoji =
571 changeset
572 |> get_field(:raw_fields)
573 |> Enum.reduce(emoji, fn x, acc ->
574 Map.merge(acc, Emoji.Formatter.get_emoji_map(x["name"] <> x["value"]))
575 end)
576
577 put_change(changeset, :emoji, emoji)
578 else
579 changeset
580 end
581 end
582
583 defp put_change_if_present(changeset, map_field, value_function) do
584 with {:ok, value} <- fetch_change(changeset, map_field),
585 {:ok, new_value} <- value_function.(value) do
586 put_change(changeset, map_field, new_value)
587 else
588 _ -> changeset
589 end
590 end
591
592 defp put_upload(value, type) do
593 with %Plug.Upload{} <- value,
594 {:ok, object} <- ActivityPub.upload(value, type: type) do
595 {:ok, object.data}
596 end
597 end
598
599 def update_as_admin_changeset(struct, params) do
600 struct
601 |> update_changeset(params)
602 |> cast(params, [:email])
603 |> delete_change(:also_known_as)
604 |> unique_constraint(:email)
605 |> validate_format(:email, @email_regex)
606 |> validate_inclusion(:actor_type, ["Person", "Service"])
607 end
608
609 @spec update_as_admin(User.t(), map()) :: {:ok, User.t()} | {:error, Changeset.t()}
610 def update_as_admin(user, params) do
611 params = Map.put(params, "password_confirmation", params["password"])
612 changeset = update_as_admin_changeset(user, params)
613
614 if params["password"] do
615 reset_password(user, changeset, params)
616 else
617 User.update_and_set_cache(changeset)
618 end
619 end
620
621 def password_update_changeset(struct, params) do
622 struct
623 |> cast(params, [:password, :password_confirmation])
624 |> validate_required([:password, :password_confirmation])
625 |> validate_confirmation(:password)
626 |> put_password_hash()
627 |> put_change(:password_reset_pending, false)
628 end
629
630 @spec reset_password(User.t(), map()) :: {:ok, User.t()} | {:error, Changeset.t()}
631 def reset_password(%User{} = user, params) do
632 reset_password(user, user, params)
633 end
634
635 def reset_password(%User{id: user_id} = user, struct, params) do
636 multi =
637 Multi.new()
638 |> Multi.update(:user, password_update_changeset(struct, params))
639 |> Multi.delete_all(:tokens, OAuth.Token.Query.get_by_user(user_id))
640 |> Multi.delete_all(:auth, OAuth.Authorization.delete_by_user_query(user))
641
642 case Repo.transaction(multi) do
643 {:ok, %{user: user} = _} -> set_cache(user)
644 {:error, _, changeset, _} -> {:error, changeset}
645 end
646 end
647
648 def update_password_reset_pending(user, value) do
649 user
650 |> change()
651 |> put_change(:password_reset_pending, value)
652 |> update_and_set_cache()
653 end
654
655 def force_password_reset_async(user) do
656 BackgroundWorker.enqueue("force_password_reset", %{"user_id" => user.id})
657 end
658
659 @spec force_password_reset(User.t()) :: {:ok, User.t()} | {:error, Ecto.Changeset.t()}
660 def force_password_reset(user), do: update_password_reset_pending(user, true)
661
662 # Used to auto-register LDAP accounts which won't have a password hash stored locally
663 def register_changeset_ldap(struct, params = %{password: password})
664 when is_nil(password) do
665 params = Map.put_new(params, :accepts_chat_messages, true)
666
667 params =
668 if Map.has_key?(params, :email) do
669 Map.put_new(params, :email, params[:email])
670 else
671 params
672 end
673
674 struct
675 |> cast(params, [
676 :name,
677 :nickname,
678 :email,
679 :accepts_chat_messages
680 ])
681 |> validate_required([:name, :nickname])
682 |> unique_constraint(:nickname)
683 |> validate_exclusion(:nickname, Config.get([User, :restricted_nicknames]))
684 |> validate_format(:nickname, local_nickname_regex())
685 |> put_ap_id()
686 |> unique_constraint(:ap_id)
687 |> put_following_and_follower_address()
688 end
689
690 def register_changeset(struct, params \\ %{}, opts \\ []) do
691 bio_limit = Config.get([:instance, :user_bio_length], 5000)
692 name_limit = Config.get([:instance, :user_name_length], 100)
693 reason_limit = Config.get([:instance, :registration_reason_length], 500)
694 params = Map.put_new(params, :accepts_chat_messages, true)
695
696 confirmed? =
697 if is_nil(opts[:confirmed]) do
698 !Config.get([:instance, :account_activation_required])
699 else
700 opts[:confirmed]
701 end
702
703 approved? =
704 if is_nil(opts[:approved]) do
705 !Config.get([:instance, :account_approval_required])
706 else
707 opts[:approved]
708 end
709
710 struct
711 |> confirmation_changeset(set_confirmation: confirmed?)
712 |> approval_changeset(set_approval: approved?)
713 |> cast(params, [
714 :bio,
715 :raw_bio,
716 :email,
717 :name,
718 :nickname,
719 :password,
720 :password_confirmation,
721 :emoji,
722 :accepts_chat_messages,
723 :registration_reason
724 ])
725 |> validate_required([:name, :nickname, :password, :password_confirmation])
726 |> validate_confirmation(:password)
727 |> unique_constraint(:email)
728 |> validate_format(:email, @email_regex)
729 |> validate_change(:email, fn :email, email ->
730 valid? =
731 Config.get([User, :email_blacklist])
732 |> Enum.all?(fn blacklisted_domain ->
733 !String.ends_with?(email, ["@" <> blacklisted_domain, "." <> blacklisted_domain])
734 end)
735
736 if valid?, do: [], else: [email: "Invalid email"]
737 end)
738 |> unique_constraint(:nickname)
739 |> validate_exclusion(:nickname, Config.get([User, :restricted_nicknames]))
740 |> validate_format(:nickname, local_nickname_regex())
741 |> validate_length(:bio, max: bio_limit)
742 |> validate_length(:name, min: 1, max: name_limit)
743 |> validate_length(:registration_reason, max: reason_limit)
744 |> maybe_validate_required_email(opts[:external])
745 |> put_password_hash
746 |> put_ap_id()
747 |> unique_constraint(:ap_id)
748 |> put_following_and_follower_address()
749 end
750
751 def maybe_validate_required_email(changeset, true), do: changeset
752
753 def maybe_validate_required_email(changeset, _) do
754 if Config.get([:instance, :account_activation_required]) do
755 validate_required(changeset, [:email])
756 else
757 changeset
758 end
759 end
760
761 defp put_ap_id(changeset) do
762 ap_id = ap_id(%User{nickname: get_field(changeset, :nickname)})
763 put_change(changeset, :ap_id, ap_id)
764 end
765
766 defp put_following_and_follower_address(changeset) do
767 followers = ap_followers(%User{nickname: get_field(changeset, :nickname)})
768
769 changeset
770 |> put_change(:follower_address, followers)
771 end
772
773 defp autofollow_users(user) do
774 candidates = Config.get([:instance, :autofollowed_nicknames])
775
776 autofollowed_users =
777 User.Query.build(%{nickname: candidates, local: true, is_active: true})
778 |> Repo.all()
779
780 follow_all(user, autofollowed_users)
781 end
782
783 defp autofollowing_users(user) do
784 candidates = Config.get([:instance, :autofollowing_nicknames])
785
786 User.Query.build(%{nickname: candidates, local: true, deactivated: false})
787 |> Repo.all()
788 |> Enum.each(&follow(&1, user, :follow_accept))
789
790 {:ok, :success}
791 end
792
793 @doc "Inserts provided changeset, performs post-registration actions (confirmation email sending etc.)"
794 def register(%Ecto.Changeset{} = changeset) do
795 with {:ok, user} <- Repo.insert(changeset) do
796 post_register_action(user)
797 end
798 end
799
800 def post_register_action(%User{is_confirmed: false} = user) do
801 with {:ok, _} <- try_send_confirmation_email(user) do
802 {:ok, user}
803 end
804 end
805
806 def post_register_action(%User{is_approved: false} = user) do
807 with {:ok, _} <- send_user_approval_email(user),
808 {:ok, _} <- send_admin_approval_emails(user) do
809 {:ok, user}
810 end
811 end
812
813 def post_register_action(%User{is_approved: true, is_confirmed: true} = user) do
814 with {:ok, user} <- autofollow_users(user),
815 {:ok, _} <- autofollowing_users(user),
816 {:ok, user} <- set_cache(user),
817 {:ok, _} <- send_welcome_email(user),
818 {:ok, _} <- send_welcome_message(user),
819 {:ok, _} <- send_welcome_chat_message(user) do
820 {:ok, user}
821 end
822 end
823
824 defp send_user_approval_email(user) do
825 user
826 |> Pleroma.Emails.UserEmail.approval_pending_email()
827 |> Pleroma.Emails.Mailer.deliver_async()
828
829 {:ok, :enqueued}
830 end
831
832 defp send_admin_approval_emails(user) do
833 all_superusers()
834 |> Enum.filter(fn user -> not is_nil(user.email) end)
835 |> Enum.each(fn superuser ->
836 superuser
837 |> Pleroma.Emails.AdminEmail.new_unapproved_registration(user)
838 |> Pleroma.Emails.Mailer.deliver_async()
839 end)
840
841 {:ok, :enqueued}
842 end
843
844 def send_welcome_message(user) do
845 if User.WelcomeMessage.enabled?() do
846 User.WelcomeMessage.post_message(user)
847 {:ok, :enqueued}
848 else
849 {:ok, :noop}
850 end
851 end
852
853 def send_welcome_chat_message(user) do
854 if User.WelcomeChatMessage.enabled?() do
855 User.WelcomeChatMessage.post_message(user)
856 {:ok, :enqueued}
857 else
858 {:ok, :noop}
859 end
860 end
861
862 def send_welcome_email(%User{email: email} = user) when is_binary(email) do
863 if User.WelcomeEmail.enabled?() do
864 User.WelcomeEmail.send_email(user)
865 {:ok, :enqueued}
866 else
867 {:ok, :noop}
868 end
869 end
870
871 def send_welcome_email(_), do: {:ok, :noop}
872
873 @spec try_send_confirmation_email(User.t()) :: {:ok, :enqueued | :noop}
874 def try_send_confirmation_email(%User{is_confirmed: false, email: email} = user)
875 when is_binary(email) do
876 if Config.get([:instance, :account_activation_required]) do
877 send_confirmation_email(user)
878 {:ok, :enqueued}
879 else
880 {:ok, :noop}
881 end
882 end
883
884 def try_send_confirmation_email(_), do: {:ok, :noop}
885
886 @spec send_confirmation_email(Uset.t()) :: User.t()
887 def send_confirmation_email(%User{} = user) do
888 user
889 |> Pleroma.Emails.UserEmail.account_confirmation_email()
890 |> Pleroma.Emails.Mailer.deliver_async()
891
892 user
893 end
894
895 def needs_update?(%User{local: true}), do: false
896
897 def needs_update?(%User{local: false, last_refreshed_at: nil}), do: true
898
899 def needs_update?(%User{local: false} = user) do
900 NaiveDateTime.diff(NaiveDateTime.utc_now(), user.last_refreshed_at) >= 86_400
901 end
902
903 def needs_update?(_), do: true
904
905 @spec maybe_direct_follow(User.t(), User.t()) :: {:ok, User.t()} | {:error, String.t()}
906
907 # "Locked" (self-locked) users demand explicit authorization of follow requests
908 def maybe_direct_follow(%User{} = follower, %User{local: true, is_locked: true} = followed) do
909 follow(follower, followed, :follow_pending)
910 end
911
912 def maybe_direct_follow(%User{} = follower, %User{local: true} = followed) do
913 follow(follower, followed)
914 end
915
916 def maybe_direct_follow(%User{} = follower, %User{} = followed) do
917 if not ap_enabled?(followed) do
918 follow(follower, followed)
919 else
920 {:ok, follower, followed}
921 end
922 end
923
924 @doc "A mass follow for local users. Respects blocks in both directions but does not create activities."
925 @spec follow_all(User.t(), list(User.t())) :: {atom(), User.t()}
926 def follow_all(follower, followeds) do
927 followeds
928 |> Enum.reject(fn followed -> blocks?(follower, followed) || blocks?(followed, follower) end)
929 |> Enum.each(&follow(follower, &1, :follow_accept))
930
931 set_cache(follower)
932 end
933
934 def follow(%User{} = follower, %User{} = followed, state \\ :follow_accept) do
935 deny_follow_blocked = Config.get([:user, :deny_follow_blocked])
936
937 cond do
938 not followed.is_active ->
939 {:error, "Could not follow user: #{followed.nickname} is deactivated."}
940
941 deny_follow_blocked and blocks?(followed, follower) ->
942 {:error, "Could not follow user: #{followed.nickname} blocked you."}
943
944 true ->
945 FollowingRelationship.follow(follower, followed, state)
946 end
947 end
948
949 def unfollow(%User{ap_id: ap_id}, %User{ap_id: ap_id}) do
950 {:error, "Not subscribed!"}
951 end
952
953 @spec unfollow(User.t(), User.t()) :: {:ok, User.t(), Activity.t()} | {:error, String.t()}
954 def unfollow(%User{} = follower, %User{} = followed) do
955 case do_unfollow(follower, followed) do
956 {:ok, follower, followed} ->
957 {:ok, follower, Utils.fetch_latest_follow(follower, followed)}
958
959 error ->
960 error
961 end
962 end
963
964 @spec do_unfollow(User.t(), User.t()) :: {:ok, User.t(), User.t()} | {:error, String.t()}
965 defp do_unfollow(%User{} = follower, %User{} = followed) do
966 case get_follow_state(follower, followed) do
967 state when state in [:follow_pending, :follow_accept] ->
968 FollowingRelationship.unfollow(follower, followed)
969
970 nil ->
971 {:error, "Not subscribed!"}
972 end
973 end
974
975 @doc "Returns follow state as Pleroma.FollowingRelationship.State value"
976 def get_follow_state(%User{} = follower, %User{} = following) do
977 following_relationship = FollowingRelationship.get(follower, following)
978 get_follow_state(follower, following, following_relationship)
979 end
980
981 def get_follow_state(
982 %User{} = follower,
983 %User{} = following,
984 following_relationship
985 ) do
986 case {following_relationship, following.local} do
987 {nil, false} ->
988 case Utils.fetch_latest_follow(follower, following) do
989 %Activity{data: %{"state" => state}} when state in ["pending", "accept"] ->
990 FollowingRelationship.state_to_enum(state)
991
992 _ ->
993 nil
994 end
995
996 {%{state: state}, _} ->
997 state
998
999 {nil, _} ->
1000 nil
1001 end
1002 end
1003
1004 def locked?(%User{} = user) do
1005 user.is_locked || false
1006 end
1007
1008 def get_by_id(id) do
1009 Repo.get_by(User, id: id)
1010 end
1011
1012 def get_by_ap_id(ap_id) do
1013 Repo.get_by(User, ap_id: ap_id)
1014 end
1015
1016 def get_all_by_ap_id(ap_ids) do
1017 from(u in __MODULE__,
1018 where: u.ap_id in ^ap_ids
1019 )
1020 |> Repo.all()
1021 end
1022
1023 def get_all_by_ids(ids) do
1024 from(u in __MODULE__, where: u.id in ^ids)
1025 |> Repo.all()
1026 end
1027
1028 # This is mostly an SPC migration fix. This guesses the user nickname by taking the last part
1029 # of the ap_id and the domain and tries to get that user
1030 def get_by_guessed_nickname(ap_id) do
1031 domain = URI.parse(ap_id).host
1032 name = List.last(String.split(ap_id, "/"))
1033 nickname = "#{name}@#{domain}"
1034
1035 get_cached_by_nickname(nickname)
1036 end
1037
1038 def set_cache({:ok, user}), do: set_cache(user)
1039 def set_cache({:error, err}), do: {:error, err}
1040
1041 def set_cache(%User{} = user) do
1042 @cachex.put(:user_cache, "ap_id:#{user.ap_id}", user)
1043 @cachex.put(:user_cache, "nickname:#{user.nickname}", user)
1044 @cachex.put(:user_cache, "friends_ap_ids:#{user.nickname}", get_user_friends_ap_ids(user))
1045 {:ok, user}
1046 end
1047
1048 def update_and_set_cache(struct, params) do
1049 struct
1050 |> update_changeset(params)
1051 |> update_and_set_cache()
1052 end
1053
1054 def update_and_set_cache(changeset) do
1055 with {:ok, user} <- Repo.update(changeset, stale_error_field: :id) do
1056 set_cache(user)
1057 end
1058 end
1059
1060 def get_user_friends_ap_ids(user) do
1061 from(u in User.get_friends_query(user), select: u.ap_id)
1062 |> Repo.all()
1063 end
1064
1065 @spec get_cached_user_friends_ap_ids(User.t()) :: [String.t()]
1066 def get_cached_user_friends_ap_ids(user) do
1067 @cachex.fetch!(:user_cache, "friends_ap_ids:#{user.ap_id}", fn _ ->
1068 get_user_friends_ap_ids(user)
1069 end)
1070 end
1071
1072 def invalidate_cache(user) do
1073 @cachex.del(:user_cache, "ap_id:#{user.ap_id}")
1074 @cachex.del(:user_cache, "nickname:#{user.nickname}")
1075 @cachex.del(:user_cache, "friends_ap_ids:#{user.ap_id}")
1076 @cachex.del(:user_cache, "blocked_users_ap_ids:#{user.ap_id}")
1077 @cachex.del(:user_cache, "muted_users_ap_ids:#{user.ap_id}")
1078 end
1079
1080 @spec get_cached_by_ap_id(String.t()) :: User.t() | nil
1081 def get_cached_by_ap_id(ap_id) do
1082 key = "ap_id:#{ap_id}"
1083
1084 with {:ok, nil} <- @cachex.get(:user_cache, key),
1085 user when not is_nil(user) <- get_by_ap_id(ap_id),
1086 {:ok, true} <- @cachex.put(:user_cache, key, user) do
1087 user
1088 else
1089 {:ok, user} -> user
1090 nil -> nil
1091 end
1092 end
1093
1094 def get_cached_by_id(id) do
1095 key = "id:#{id}"
1096
1097 ap_id =
1098 @cachex.fetch!(:user_cache, key, fn _ ->
1099 user = get_by_id(id)
1100
1101 if user do
1102 @cachex.put(:user_cache, "ap_id:#{user.ap_id}", user)
1103 {:commit, user.ap_id}
1104 else
1105 {:ignore, ""}
1106 end
1107 end)
1108
1109 get_cached_by_ap_id(ap_id)
1110 end
1111
1112 def get_cached_by_nickname(nickname) do
1113 key = "nickname:#{nickname}"
1114
1115 @cachex.fetch!(:user_cache, key, fn _ ->
1116 case get_or_fetch_by_nickname(nickname) do
1117 {:ok, user} -> {:commit, user}
1118 {:error, _error} -> {:ignore, nil}
1119 end
1120 end)
1121 end
1122
1123 def get_cached_by_nickname_or_id(nickname_or_id, opts \\ []) do
1124 restrict_to_local = Config.get([:instance, :limit_to_local_content])
1125
1126 cond do
1127 is_integer(nickname_or_id) or FlakeId.flake_id?(nickname_or_id) ->
1128 get_cached_by_id(nickname_or_id) || get_cached_by_nickname(nickname_or_id)
1129
1130 restrict_to_local == false or not String.contains?(nickname_or_id, "@") ->
1131 get_cached_by_nickname(nickname_or_id)
1132
1133 restrict_to_local == :unauthenticated and match?(%User{}, opts[:for]) ->
1134 get_cached_by_nickname(nickname_or_id)
1135
1136 true ->
1137 nil
1138 end
1139 end
1140
1141 @spec get_by_nickname(String.t()) :: User.t() | nil
1142 def get_by_nickname(nickname) do
1143 Repo.get_by(User, nickname: nickname) ||
1144 if Regex.match?(~r(@#{Pleroma.Web.Endpoint.host()})i, nickname) do
1145 Repo.get_by(User, nickname: local_nickname(nickname))
1146 end
1147 end
1148
1149 def get_by_email(email), do: Repo.get_by(User, email: email)
1150
1151 def get_by_nickname_or_email(nickname_or_email) do
1152 get_by_nickname(nickname_or_email) || get_by_email(nickname_or_email)
1153 end
1154
1155 def fetch_by_nickname(nickname), do: ActivityPub.make_user_from_nickname(nickname)
1156
1157 def get_or_fetch_by_nickname(nickname) do
1158 with %User{} = user <- get_by_nickname(nickname) do
1159 {:ok, user}
1160 else
1161 _e ->
1162 with [_nick, _domain] <- String.split(nickname, "@"),
1163 {:ok, user} <- fetch_by_nickname(nickname) do
1164 {:ok, user}
1165 else
1166 _e -> {:error, "not found " <> nickname}
1167 end
1168 end
1169 end
1170
1171 @spec get_followers_query(User.t(), pos_integer() | nil) :: Ecto.Query.t()
1172 def get_followers_query(%User{} = user, nil) do
1173 User.Query.build(%{followers: user, is_active: true})
1174 end
1175
1176 def get_followers_query(%User{} = user, page) do
1177 user
1178 |> get_followers_query(nil)
1179 |> User.Query.paginate(page, 20)
1180 end
1181
1182 @spec get_followers_query(User.t()) :: Ecto.Query.t()
1183 def get_followers_query(%User{} = user), do: get_followers_query(user, nil)
1184
1185 @spec get_followers(User.t(), pos_integer() | nil) :: {:ok, list(User.t())}
1186 def get_followers(%User{} = user, page \\ nil) do
1187 user
1188 |> get_followers_query(page)
1189 |> Repo.all()
1190 end
1191
1192 @spec get_external_followers(User.t(), pos_integer() | nil) :: {:ok, list(User.t())}
1193 def get_external_followers(%User{} = user, page \\ nil) do
1194 user
1195 |> get_followers_query(page)
1196 |> User.Query.build(%{external: true})
1197 |> Repo.all()
1198 end
1199
1200 def get_followers_ids(%User{} = user, page \\ nil) do
1201 user
1202 |> get_followers_query(page)
1203 |> select([u], u.id)
1204 |> Repo.all()
1205 end
1206
1207 @spec get_friends_query(User.t(), pos_integer() | nil) :: Ecto.Query.t()
1208 def get_friends_query(%User{} = user, nil) do
1209 User.Query.build(%{friends: user, deactivated: false})
1210 end
1211
1212 def get_friends_query(%User{} = user, page) do
1213 user
1214 |> get_friends_query(nil)
1215 |> User.Query.paginate(page, 20)
1216 end
1217
1218 @spec get_friends_query(User.t()) :: Ecto.Query.t()
1219 def get_friends_query(%User{} = user), do: get_friends_query(user, nil)
1220
1221 def get_friends(%User{} = user, page \\ nil) do
1222 user
1223 |> get_friends_query(page)
1224 |> Repo.all()
1225 end
1226
1227 def get_friends_ap_ids(%User{} = user) do
1228 user
1229 |> get_friends_query(nil)
1230 |> select([u], u.ap_id)
1231 |> Repo.all()
1232 end
1233
1234 def get_friends_ids(%User{} = user, page \\ nil) do
1235 user
1236 |> get_friends_query(page)
1237 |> select([u], u.id)
1238 |> Repo.all()
1239 end
1240
1241 def increase_note_count(%User{} = user) do
1242 User
1243 |> where(id: ^user.id)
1244 |> update([u], inc: [note_count: 1])
1245 |> select([u], u)
1246 |> Repo.update_all([])
1247 |> case do
1248 {1, [user]} -> set_cache(user)
1249 _ -> {:error, user}
1250 end
1251 end
1252
1253 def decrease_note_count(%User{} = user) do
1254 User
1255 |> where(id: ^user.id)
1256 |> update([u],
1257 set: [
1258 note_count: fragment("greatest(0, note_count - 1)")
1259 ]
1260 )
1261 |> select([u], u)
1262 |> Repo.update_all([])
1263 |> case do
1264 {1, [user]} -> set_cache(user)
1265 _ -> {:error, user}
1266 end
1267 end
1268
1269 def update_note_count(%User{} = user, note_count \\ nil) do
1270 note_count =
1271 note_count ||
1272 from(
1273 a in Object,
1274 where: fragment("?->>'actor' = ? and ?->>'type' = 'Note'", a.data, ^user.ap_id, a.data),
1275 select: count(a.id)
1276 )
1277 |> Repo.one()
1278
1279 user
1280 |> cast(%{note_count: note_count}, [:note_count])
1281 |> update_and_set_cache()
1282 end
1283
1284 @spec maybe_fetch_follow_information(User.t()) :: User.t()
1285 def maybe_fetch_follow_information(user) do
1286 with {:ok, user} <- fetch_follow_information(user) do
1287 user
1288 else
1289 e ->
1290 Logger.error("Follower/Following counter update for #{user.ap_id} failed.\n#{inspect(e)}")
1291
1292 user
1293 end
1294 end
1295
1296 def fetch_follow_information(user) do
1297 with {:ok, info} <- ActivityPub.fetch_follow_information_for_user(user) do
1298 user
1299 |> follow_information_changeset(info)
1300 |> update_and_set_cache()
1301 end
1302 end
1303
1304 defp follow_information_changeset(user, params) do
1305 user
1306 |> cast(params, [
1307 :hide_followers,
1308 :hide_follows,
1309 :follower_count,
1310 :following_count,
1311 :hide_followers_count,
1312 :hide_follows_count
1313 ])
1314 end
1315
1316 @spec update_follower_count(User.t()) :: {:ok, User.t()}
1317 def update_follower_count(%User{} = user) do
1318 if user.local or !Config.get([:instance, :external_user_synchronization]) do
1319 follower_count = FollowingRelationship.follower_count(user)
1320
1321 user
1322 |> follow_information_changeset(%{follower_count: follower_count})
1323 |> update_and_set_cache
1324 else
1325 {:ok, maybe_fetch_follow_information(user)}
1326 end
1327 end
1328
1329 @spec update_following_count(User.t()) :: {:ok, User.t()}
1330 def update_following_count(%User{local: false} = user) do
1331 if Config.get([:instance, :external_user_synchronization]) do
1332 {:ok, maybe_fetch_follow_information(user)}
1333 else
1334 {:ok, user}
1335 end
1336 end
1337
1338 def update_following_count(%User{local: true} = user) do
1339 following_count = FollowingRelationship.following_count(user)
1340
1341 user
1342 |> follow_information_changeset(%{following_count: following_count})
1343 |> update_and_set_cache()
1344 end
1345
1346 @spec get_users_from_set([String.t()], keyword()) :: [User.t()]
1347 def get_users_from_set(ap_ids, opts \\ []) do
1348 local_only = Keyword.get(opts, :local_only, true)
1349 criteria = %{ap_id: ap_ids, is_active: true}
1350 criteria = if local_only, do: Map.put(criteria, :local, true), else: criteria
1351
1352 User.Query.build(criteria)
1353 |> Repo.all()
1354 end
1355
1356 @spec get_recipients_from_activity(Activity.t()) :: [User.t()]
1357 def get_recipients_from_activity(%Activity{recipients: to, actor: actor}) do
1358 to = [actor | to]
1359
1360 query = User.Query.build(%{recipients_from_activity: to, local: true, is_active: true})
1361
1362 query
1363 |> Repo.all()
1364 end
1365
1366 @spec mute(User.t(), User.t(), map()) ::
1367 {:ok, list(UserRelationship.t())} | {:error, String.t()}
1368 def mute(%User{} = muter, %User{} = mutee, params \\ %{}) do
1369 notifications? = Map.get(params, :notifications, true)
1370 expires_in = Map.get(params, :expires_in, 0)
1371
1372 with {:ok, user_mute} <- UserRelationship.create_mute(muter, mutee),
1373 {:ok, user_notification_mute} <-
1374 (notifications? && UserRelationship.create_notification_mute(muter, mutee)) ||
1375 {:ok, nil} do
1376 if expires_in > 0 do
1377 Pleroma.Workers.MuteExpireWorker.enqueue(
1378 "unmute_user",
1379 %{"muter_id" => muter.id, "mutee_id" => mutee.id},
1380 schedule_in: expires_in
1381 )
1382 end
1383
1384 @cachex.del(:user_cache, "muted_users_ap_ids:#{muter.ap_id}")
1385
1386 {:ok, Enum.filter([user_mute, user_notification_mute], & &1)}
1387 end
1388 end
1389
1390 def unmute(%User{} = muter, %User{} = mutee) do
1391 with {:ok, user_mute} <- UserRelationship.delete_mute(muter, mutee),
1392 {:ok, user_notification_mute} <-
1393 UserRelationship.delete_notification_mute(muter, mutee) do
1394 @cachex.del(:user_cache, "muted_users_ap_ids:#{muter.ap_id}")
1395 {:ok, [user_mute, user_notification_mute]}
1396 end
1397 end
1398
1399 def unmute(muter_id, mutee_id) do
1400 with {:muter, %User{} = muter} <- {:muter, User.get_by_id(muter_id)},
1401 {:mutee, %User{} = mutee} <- {:mutee, User.get_by_id(mutee_id)} do
1402 unmute(muter, mutee)
1403 else
1404 {who, result} = error ->
1405 Logger.warn(
1406 "User.unmute/2 failed. #{who}: #{result}, muter_id: #{muter_id}, mutee_id: #{mutee_id}"
1407 )
1408
1409 {:error, error}
1410 end
1411 end
1412
1413 def subscribe(%User{} = subscriber, %User{} = target) do
1414 deny_follow_blocked = Config.get([:user, :deny_follow_blocked])
1415
1416 if blocks?(target, subscriber) and deny_follow_blocked do
1417 {:error, "Could not subscribe: #{target.nickname} is blocking you"}
1418 else
1419 # Note: the relationship is inverse: subscriber acts as relationship target
1420 UserRelationship.create_inverse_subscription(target, subscriber)
1421 end
1422 end
1423
1424 def subscribe(%User{} = subscriber, %{ap_id: ap_id}) do
1425 with %User{} = subscribee <- get_cached_by_ap_id(ap_id) do
1426 subscribe(subscriber, subscribee)
1427 end
1428 end
1429
1430 def unsubscribe(%User{} = unsubscriber, %User{} = target) do
1431 # Note: the relationship is inverse: subscriber acts as relationship target
1432 UserRelationship.delete_inverse_subscription(target, unsubscriber)
1433 end
1434
1435 def unsubscribe(%User{} = unsubscriber, %{ap_id: ap_id}) do
1436 with %User{} = user <- get_cached_by_ap_id(ap_id) do
1437 unsubscribe(unsubscriber, user)
1438 end
1439 end
1440
1441 def block(%User{} = blocker, %User{} = blocked) do
1442 # sever any follow relationships to prevent leaks per activitypub (Pleroma issue #213)
1443 blocker =
1444 if following?(blocker, blocked) do
1445 {:ok, blocker, _} = unfollow(blocker, blocked)
1446 blocker
1447 else
1448 blocker
1449 end
1450
1451 # clear any requested follows as well
1452 blocked =
1453 case CommonAPI.reject_follow_request(blocked, blocker) do
1454 {:ok, %User{} = updated_blocked} -> updated_blocked
1455 nil -> blocked
1456 end
1457
1458 unsubscribe(blocked, blocker)
1459
1460 unfollowing_blocked = Config.get([:activitypub, :unfollow_blocked], true)
1461 if unfollowing_blocked && following?(blocked, blocker), do: unfollow(blocked, blocker)
1462
1463 {:ok, blocker} = update_follower_count(blocker)
1464 {:ok, blocker, _} = Participation.mark_all_as_read(blocker, blocked)
1465 add_to_block(blocker, blocked)
1466 end
1467
1468 # helper to handle the block given only an actor's AP id
1469 def block(%User{} = blocker, %{ap_id: ap_id}) do
1470 block(blocker, get_cached_by_ap_id(ap_id))
1471 end
1472
1473 def unblock(%User{} = blocker, %User{} = blocked) do
1474 remove_from_block(blocker, blocked)
1475 end
1476
1477 # helper to handle the block given only an actor's AP id
1478 def unblock(%User{} = blocker, %{ap_id: ap_id}) do
1479 unblock(blocker, get_cached_by_ap_id(ap_id))
1480 end
1481
1482 def mutes?(nil, _), do: false
1483 def mutes?(%User{} = user, %User{} = target), do: mutes_user?(user, target)
1484
1485 def mutes_user?(%User{} = user, %User{} = target) do
1486 UserRelationship.mute_exists?(user, target)
1487 end
1488
1489 @spec muted_notifications?(User.t() | nil, User.t() | map()) :: boolean()
1490 def muted_notifications?(nil, _), do: false
1491
1492 def muted_notifications?(%User{} = user, %User{} = target),
1493 do: UserRelationship.notification_mute_exists?(user, target)
1494
1495 def blocks?(nil, _), do: false
1496
1497 def blocks?(%User{} = user, %User{} = target) do
1498 blocks_user?(user, target) ||
1499 (blocks_domain?(user, target) and not User.following?(user, target))
1500 end
1501
1502 def blocks_user?(%User{} = user, %User{} = target) do
1503 UserRelationship.block_exists?(user, target)
1504 end
1505
1506 def blocks_user?(_, _), do: false
1507
1508 def blocks_domain?(%User{} = user, %User{} = target) do
1509 domain_blocks = Pleroma.Web.ActivityPub.MRF.subdomains_regex(user.domain_blocks)
1510 %{host: host} = URI.parse(target.ap_id)
1511 Pleroma.Web.ActivityPub.MRF.subdomain_match?(domain_blocks, host)
1512 end
1513
1514 def blocks_domain?(_, _), do: false
1515
1516 def subscribed_to?(%User{} = user, %User{} = target) do
1517 # Note: the relationship is inverse: subscriber acts as relationship target
1518 UserRelationship.inverse_subscription_exists?(target, user)
1519 end
1520
1521 def subscribed_to?(%User{} = user, %{ap_id: ap_id}) do
1522 with %User{} = target <- get_cached_by_ap_id(ap_id) do
1523 subscribed_to?(user, target)
1524 end
1525 end
1526
1527 @doc """
1528 Returns map of outgoing (blocked, muted etc.) relationships' user AP IDs by relation type.
1529 E.g. `outgoing_relationships_ap_ids(user, [:block])` -> `%{block: ["https://some.site/users/userapid"]}`
1530 """
1531 @spec outgoing_relationships_ap_ids(User.t(), list(atom())) :: %{atom() => list(String.t())}
1532 def outgoing_relationships_ap_ids(_user, []), do: %{}
1533
1534 def outgoing_relationships_ap_ids(nil, _relationship_types), do: %{}
1535
1536 def outgoing_relationships_ap_ids(%User{} = user, relationship_types)
1537 when is_list(relationship_types) do
1538 db_result =
1539 user
1540 |> assoc(:outgoing_relationships)
1541 |> join(:inner, [user_rel], u in assoc(user_rel, :target))
1542 |> where([user_rel, u], user_rel.relationship_type in ^relationship_types)
1543 |> select([user_rel, u], [user_rel.relationship_type, fragment("array_agg(?)", u.ap_id)])
1544 |> group_by([user_rel, u], user_rel.relationship_type)
1545 |> Repo.all()
1546 |> Enum.into(%{}, fn [k, v] -> {k, v} end)
1547
1548 Enum.into(
1549 relationship_types,
1550 %{},
1551 fn rel_type -> {rel_type, db_result[rel_type] || []} end
1552 )
1553 end
1554
1555 def incoming_relationships_ungrouped_ap_ids(user, relationship_types, ap_ids \\ nil)
1556
1557 def incoming_relationships_ungrouped_ap_ids(_user, [], _ap_ids), do: []
1558
1559 def incoming_relationships_ungrouped_ap_ids(nil, _relationship_types, _ap_ids), do: []
1560
1561 def incoming_relationships_ungrouped_ap_ids(%User{} = user, relationship_types, ap_ids)
1562 when is_list(relationship_types) do
1563 user
1564 |> assoc(:incoming_relationships)
1565 |> join(:inner, [user_rel], u in assoc(user_rel, :source))
1566 |> where([user_rel, u], user_rel.relationship_type in ^relationship_types)
1567 |> maybe_filter_on_ap_id(ap_ids)
1568 |> select([user_rel, u], u.ap_id)
1569 |> distinct(true)
1570 |> Repo.all()
1571 end
1572
1573 defp maybe_filter_on_ap_id(query, ap_ids) when is_list(ap_ids) do
1574 where(query, [user_rel, u], u.ap_id in ^ap_ids)
1575 end
1576
1577 defp maybe_filter_on_ap_id(query, _ap_ids), do: query
1578
1579 def set_activation_async(user, status \\ true) do
1580 BackgroundWorker.enqueue("user_activation", %{"user_id" => user.id, "status" => status})
1581 end
1582
1583 @spec set_activation([User.t()], boolean()) :: {:ok, User.t()} | {:error, Changeset.t()}
1584 def set_activation(users, status) when is_list(users) do
1585 Repo.transaction(fn ->
1586 for user <- users, do: set_activation(user, status)
1587 end)
1588 end
1589
1590 @spec set_activation(User.t(), boolean()) :: {:ok, User.t()} | {:error, Changeset.t()}
1591 def set_activation(%User{} = user, status) do
1592 with {:ok, user} <- set_activation_status(user, status) do
1593 user
1594 |> get_followers()
1595 |> Enum.filter(& &1.local)
1596 |> Enum.each(&set_cache(update_following_count(&1)))
1597
1598 # Only update local user counts, remote will be update during the next pull.
1599 user
1600 |> get_friends()
1601 |> Enum.filter(& &1.local)
1602 |> Enum.each(&do_unfollow(user, &1))
1603
1604 {:ok, user}
1605 end
1606 end
1607
1608 def approve(users) when is_list(users) do
1609 Repo.transaction(fn ->
1610 Enum.map(users, fn user ->
1611 with {:ok, user} <- approve(user), do: user
1612 end)
1613 end)
1614 end
1615
1616 def approve(%User{is_approved: false} = user) do
1617 with chg <- change(user, is_approved: true),
1618 {:ok, user} <- update_and_set_cache(chg) do
1619 post_register_action(user)
1620 {:ok, user}
1621 end
1622 end
1623
1624 def approve(%User{} = user), do: {:ok, user}
1625
1626 def confirm(users) when is_list(users) do
1627 Repo.transaction(fn ->
1628 Enum.map(users, fn user ->
1629 with {:ok, user} <- confirm(user), do: user
1630 end)
1631 end)
1632 end
1633
1634 def confirm(%User{is_confirmed: false} = user) do
1635 with chg <- confirmation_changeset(user, set_confirmation: true),
1636 {:ok, user} <- update_and_set_cache(chg) do
1637 post_register_action(user)
1638 {:ok, user}
1639 end
1640 end
1641
1642 def confirm(%User{} = user), do: {:ok, user}
1643
1644 def update_notification_settings(%User{} = user, settings) do
1645 user
1646 |> cast(%{notification_settings: settings}, [])
1647 |> cast_embed(:notification_settings)
1648 |> validate_required([:notification_settings])
1649 |> update_and_set_cache()
1650 end
1651
1652 @spec purge_user_changeset(User.t()) :: Changeset.t()
1653 def purge_user_changeset(user) do
1654 # "Right to be forgotten"
1655 # https://gdpr.eu/right-to-be-forgotten/
1656 change(user, %{
1657 bio: "",
1658 raw_bio: nil,
1659 email: nil,
1660 name: nil,
1661 password_hash: nil,
1662 keys: nil,
1663 public_key: nil,
1664 avatar: %{},
1665 tags: [],
1666 last_refreshed_at: nil,
1667 last_digest_emailed_at: nil,
1668 banner: %{},
1669 background: %{},
1670 note_count: 0,
1671 follower_count: 0,
1672 following_count: 0,
1673 is_locked: false,
1674 is_confirmed: true,
1675 password_reset_pending: false,
1676 is_approved: true,
1677 registration_reason: nil,
1678 confirmation_token: nil,
1679 domain_blocks: [],
1680 is_active: false,
1681 ap_enabled: false,
1682 is_moderator: false,
1683 is_admin: false,
1684 mastofe_settings: nil,
1685 mascot: nil,
1686 emoji: %{},
1687 pleroma_settings_store: %{},
1688 fields: [],
1689 raw_fields: [],
1690 is_discoverable: false,
1691 also_known_as: []
1692 })
1693 end
1694
1695 def delete(users) when is_list(users) do
1696 for user <- users, do: delete(user)
1697 end
1698
1699 def delete(%User{} = user) do
1700 BackgroundWorker.enqueue("delete_user", %{"user_id" => user.id})
1701 end
1702
1703 defp delete_and_invalidate_cache(%User{} = user) do
1704 invalidate_cache(user)
1705 Repo.delete(user)
1706 end
1707
1708 defp delete_or_deactivate(%User{local: false} = user), do: delete_and_invalidate_cache(user)
1709
1710 defp delete_or_deactivate(%User{local: true} = user) do
1711 status = account_status(user)
1712
1713 case status do
1714 :confirmation_pending ->
1715 delete_and_invalidate_cache(user)
1716
1717 :approval_pending ->
1718 delete_and_invalidate_cache(user)
1719
1720 _ ->
1721 user
1722 |> purge_user_changeset()
1723 |> update_and_set_cache()
1724 end
1725 end
1726
1727 def perform(:force_password_reset, user), do: force_password_reset(user)
1728
1729 @spec perform(atom(), User.t()) :: {:ok, User.t()}
1730 def perform(:delete, %User{} = user) do
1731 # Remove all relationships
1732 user
1733 |> get_followers()
1734 |> Enum.each(fn follower ->
1735 ActivityPub.unfollow(follower, user)
1736 unfollow(follower, user)
1737 end)
1738
1739 user
1740 |> get_friends()
1741 |> Enum.each(fn followed ->
1742 ActivityPub.unfollow(user, followed)
1743 unfollow(user, followed)
1744 end)
1745
1746 delete_user_activities(user)
1747 delete_notifications_from_user_activities(user)
1748
1749 delete_outgoing_pending_follow_requests(user)
1750
1751 delete_or_deactivate(user)
1752 end
1753
1754 def perform(:set_activation_async, user, status), do: set_activation(user, status)
1755
1756 @spec external_users_query() :: Ecto.Query.t()
1757 def external_users_query do
1758 User.Query.build(%{
1759 external: true,
1760 active: true,
1761 order_by: :id
1762 })
1763 end
1764
1765 @spec external_users(keyword()) :: [User.t()]
1766 def external_users(opts \\ []) do
1767 query =
1768 external_users_query()
1769 |> select([u], struct(u, [:id, :ap_id]))
1770
1771 query =
1772 if opts[:max_id],
1773 do: where(query, [u], u.id > ^opts[:max_id]),
1774 else: query
1775
1776 query =
1777 if opts[:limit],
1778 do: limit(query, ^opts[:limit]),
1779 else: query
1780
1781 Repo.all(query)
1782 end
1783
1784 def delete_notifications_from_user_activities(%User{ap_id: ap_id}) do
1785 Notification
1786 |> join(:inner, [n], activity in assoc(n, :activity))
1787 |> where([n, a], fragment("? = ?", a.actor, ^ap_id))
1788 |> Repo.delete_all()
1789 end
1790
1791 def delete_user_activities(%User{ap_id: ap_id} = user) do
1792 ap_id
1793 |> Activity.Queries.by_actor()
1794 |> Repo.chunk_stream(50, :batches)
1795 |> Stream.each(fn activities ->
1796 Enum.each(activities, fn activity -> delete_activity(activity, user) end)
1797 end)
1798 |> Stream.run()
1799 end
1800
1801 defp delete_activity(%{data: %{"type" => "Create", "object" => object}} = activity, user) do
1802 with {_, %Object{}} <- {:find_object, Object.get_by_ap_id(object)},
1803 {:ok, delete_data, _} <- Builder.delete(user, object) do
1804 Pipeline.common_pipeline(delete_data, local: user.local)
1805 else
1806 {:find_object, nil} ->
1807 # We have the create activity, but not the object, it was probably pruned.
1808 # Insert a tombstone and try again
1809 with {:ok, tombstone_data, _} <- Builder.tombstone(user.ap_id, object),
1810 {:ok, _tombstone} <- Object.create(tombstone_data) do
1811 delete_activity(activity, user)
1812 end
1813
1814 e ->
1815 Logger.error("Could not delete #{object} created by #{activity.data["ap_id"]}")
1816 Logger.error("Error: #{inspect(e)}")
1817 end
1818 end
1819
1820 defp delete_activity(%{data: %{"type" => type}} = activity, user)
1821 when type in ["Like", "Announce"] do
1822 {:ok, undo, _} = Builder.undo(user, activity)
1823 Pipeline.common_pipeline(undo, local: user.local)
1824 end
1825
1826 defp delete_activity(_activity, _user), do: "Doing nothing"
1827
1828 defp delete_outgoing_pending_follow_requests(user) do
1829 user
1830 |> FollowingRelationship.outgoing_pending_follow_requests_query()
1831 |> Repo.delete_all()
1832 end
1833
1834 def html_filter_policy(%User{no_rich_text: true}) do
1835 Pleroma.HTML.Scrubber.TwitterText
1836 end
1837
1838 def html_filter_policy(_), do: Config.get([:markup, :scrub_policy])
1839
1840 def fetch_by_ap_id(ap_id), do: ActivityPub.make_user_from_ap_id(ap_id)
1841
1842 def get_or_fetch_by_ap_id(ap_id) do
1843 cached_user = get_cached_by_ap_id(ap_id)
1844
1845 maybe_fetched_user = needs_update?(cached_user) && fetch_by_ap_id(ap_id)
1846
1847 case {cached_user, maybe_fetched_user} do
1848 {_, {:ok, %User{} = user}} ->
1849 {:ok, user}
1850
1851 {%User{} = user, _} ->
1852 {:ok, user}
1853
1854 _ ->
1855 {:error, :not_found}
1856 end
1857 end
1858
1859 @doc """
1860 Creates an internal service actor by URI if missing.
1861 Optionally takes nickname for addressing.
1862 """
1863 @spec get_or_create_service_actor_by_ap_id(String.t(), String.t()) :: User.t() | nil
1864 def get_or_create_service_actor_by_ap_id(uri, nickname) do
1865 {_, user} =
1866 case get_cached_by_ap_id(uri) do
1867 nil ->
1868 with {:error, %{errors: errors}} <- create_service_actor(uri, nickname) do
1869 Logger.error("Cannot create service actor: #{uri}/.\n#{inspect(errors)}")
1870 {:error, nil}
1871 end
1872
1873 %User{invisible: false} = user ->
1874 set_invisible(user)
1875
1876 user ->
1877 {:ok, user}
1878 end
1879
1880 user
1881 end
1882
1883 @spec set_invisible(User.t()) :: {:ok, User.t()}
1884 defp set_invisible(user) do
1885 user
1886 |> change(%{invisible: true})
1887 |> update_and_set_cache()
1888 end
1889
1890 @spec create_service_actor(String.t(), String.t()) ::
1891 {:ok, User.t()} | {:error, Ecto.Changeset.t()}
1892 defp create_service_actor(uri, nickname) do
1893 %User{
1894 invisible: true,
1895 local: true,
1896 ap_id: uri,
1897 nickname: nickname,
1898 follower_address: uri <> "/followers"
1899 }
1900 |> change
1901 |> unique_constraint(:nickname)
1902 |> Repo.insert()
1903 |> set_cache()
1904 end
1905
1906 def public_key(%{public_key: public_key_pem}) when is_binary(public_key_pem) do
1907 key =
1908 public_key_pem
1909 |> :public_key.pem_decode()
1910 |> hd()
1911 |> :public_key.pem_entry_decode()
1912
1913 {:ok, key}
1914 end
1915
1916 def public_key(_), do: {:error, "key not found"}
1917
1918 def get_public_key_for_ap_id(ap_id) do
1919 with {:ok, %User{} = user} <- get_or_fetch_by_ap_id(ap_id),
1920 {:ok, public_key} <- public_key(user) do
1921 {:ok, public_key}
1922 else
1923 _ -> :error
1924 end
1925 end
1926
1927 def ap_enabled?(%User{local: true}), do: true
1928 def ap_enabled?(%User{ap_enabled: ap_enabled}), do: ap_enabled
1929 def ap_enabled?(_), do: false
1930
1931 @doc "Gets or fetch a user by uri or nickname."
1932 @spec get_or_fetch(String.t()) :: {:ok, User.t()} | {:error, String.t()}
1933 def get_or_fetch("http" <> _host = uri), do: get_or_fetch_by_ap_id(uri)
1934 def get_or_fetch(nickname), do: get_or_fetch_by_nickname(nickname)
1935
1936 # wait a period of time and return newest version of the User structs
1937 # this is because we have synchronous follow APIs and need to simulate them
1938 # with an async handshake
1939 def wait_and_refresh(_, %User{local: true} = a, %User{local: true} = b) do
1940 with %User{} = a <- get_cached_by_id(a.id),
1941 %User{} = b <- get_cached_by_id(b.id) do
1942 {:ok, a, b}
1943 else
1944 nil -> :error
1945 end
1946 end
1947
1948 def wait_and_refresh(timeout, %User{} = a, %User{} = b) do
1949 with :ok <- :timer.sleep(timeout),
1950 %User{} = a <- get_cached_by_id(a.id),
1951 %User{} = b <- get_cached_by_id(b.id) do
1952 {:ok, a, b}
1953 else
1954 nil -> :error
1955 end
1956 end
1957
1958 def parse_bio(bio) when is_binary(bio) and bio != "" do
1959 bio
1960 |> CommonUtils.format_input("text/plain", mentions_format: :full)
1961 |> elem(0)
1962 end
1963
1964 def parse_bio(_), do: ""
1965
1966 def parse_bio(bio, user) when is_binary(bio) and bio != "" do
1967 # TODO: get profile URLs other than user.ap_id
1968 profile_urls = [user.ap_id]
1969
1970 bio
1971 |> CommonUtils.format_input("text/plain",
1972 mentions_format: :full,
1973 rel: &RelMe.maybe_put_rel_me(&1, profile_urls)
1974 )
1975 |> elem(0)
1976 end
1977
1978 def parse_bio(_, _), do: ""
1979
1980 def tag(user_identifiers, tags) when is_list(user_identifiers) do
1981 Repo.transaction(fn ->
1982 for user_identifier <- user_identifiers, do: tag(user_identifier, tags)
1983 end)
1984 end
1985
1986 def tag(nickname, tags) when is_binary(nickname),
1987 do: tag(get_by_nickname(nickname), tags)
1988
1989 def tag(%User{} = user, tags),
1990 do: update_tags(user, Enum.uniq((user.tags || []) ++ normalize_tags(tags)))
1991
1992 def untag(user_identifiers, tags) when is_list(user_identifiers) do
1993 Repo.transaction(fn ->
1994 for user_identifier <- user_identifiers, do: untag(user_identifier, tags)
1995 end)
1996 end
1997
1998 def untag(nickname, tags) when is_binary(nickname),
1999 do: untag(get_by_nickname(nickname), tags)
2000
2001 def untag(%User{} = user, tags),
2002 do: update_tags(user, (user.tags || []) -- normalize_tags(tags))
2003
2004 defp update_tags(%User{} = user, new_tags) do
2005 {:ok, updated_user} =
2006 user
2007 |> change(%{tags: new_tags})
2008 |> update_and_set_cache()
2009
2010 updated_user
2011 end
2012
2013 defp normalize_tags(tags) do
2014 [tags]
2015 |> List.flatten()
2016 |> Enum.map(&String.downcase/1)
2017 end
2018
2019 defp local_nickname_regex do
2020 if Config.get([:instance, :extended_nickname_format]) do
2021 @extended_local_nickname_regex
2022 else
2023 @strict_local_nickname_regex
2024 end
2025 end
2026
2027 def local_nickname(nickname_or_mention) do
2028 nickname_or_mention
2029 |> full_nickname()
2030 |> String.split("@")
2031 |> hd()
2032 end
2033
2034 def full_nickname(%User{} = user) do
2035 if String.contains?(user.nickname, "@") do
2036 user.nickname
2037 else
2038 %{host: host} = URI.parse(user.ap_id)
2039 user.nickname <> "@" <> host
2040 end
2041 end
2042
2043 def full_nickname(nickname_or_mention),
2044 do: String.trim_leading(nickname_or_mention, "@")
2045
2046 def error_user(ap_id) do
2047 %User{
2048 name: ap_id,
2049 ap_id: ap_id,
2050 nickname: "erroruser@example.com",
2051 inserted_at: NaiveDateTime.utc_now()
2052 }
2053 end
2054
2055 @spec all_superusers() :: [User.t()]
2056 def all_superusers do
2057 User.Query.build(%{super_users: true, local: true, is_active: true})
2058 |> Repo.all()
2059 end
2060
2061 def muting_reblogs?(%User{} = user, %User{} = target) do
2062 UserRelationship.reblog_mute_exists?(user, target)
2063 end
2064
2065 def showing_reblogs?(%User{} = user, %User{} = target) do
2066 not muting_reblogs?(user, target)
2067 end
2068
2069 @doc """
2070 The function returns a query to get users with no activity for given interval of days.
2071 Inactive users are those who didn't read any notification, or had any activity where
2072 the user is the activity's actor, during `inactivity_threshold` days.
2073 Deactivated users will not appear in this list.
2074
2075 ## Examples
2076
2077 iex> Pleroma.User.list_inactive_users()
2078 %Ecto.Query{}
2079 """
2080 @spec list_inactive_users_query(integer()) :: Ecto.Query.t()
2081 def list_inactive_users_query(inactivity_threshold \\ 7) do
2082 negative_inactivity_threshold = -inactivity_threshold
2083 now = NaiveDateTime.truncate(NaiveDateTime.utc_now(), :second)
2084 # Subqueries are not supported in `where` clauses, join gets too complicated.
2085 has_read_notifications =
2086 from(n in Pleroma.Notification,
2087 where: n.seen == true,
2088 group_by: n.id,
2089 having: max(n.updated_at) > datetime_add(^now, ^negative_inactivity_threshold, "day"),
2090 select: n.user_id
2091 )
2092 |> Pleroma.Repo.all()
2093
2094 from(u in Pleroma.User,
2095 left_join: a in Pleroma.Activity,
2096 on: u.ap_id == a.actor,
2097 where: not is_nil(u.nickname),
2098 where: u.is_active == ^true,
2099 where: u.id not in ^has_read_notifications,
2100 group_by: u.id,
2101 having:
2102 max(a.inserted_at) < datetime_add(^now, ^negative_inactivity_threshold, "day") or
2103 is_nil(max(a.inserted_at))
2104 )
2105 end
2106
2107 @doc """
2108 Enable or disable email notifications for user
2109
2110 ## Examples
2111
2112 iex> Pleroma.User.switch_email_notifications(Pleroma.User{email_notifications: %{"digest" => false}}, "digest", true)
2113 Pleroma.User{email_notifications: %{"digest" => true}}
2114
2115 iex> Pleroma.User.switch_email_notifications(Pleroma.User{email_notifications: %{"digest" => true}}, "digest", false)
2116 Pleroma.User{email_notifications: %{"digest" => false}}
2117 """
2118 @spec switch_email_notifications(t(), String.t(), boolean()) ::
2119 {:ok, t()} | {:error, Ecto.Changeset.t()}
2120 def switch_email_notifications(user, type, status) do
2121 User.update_email_notifications(user, %{type => status})
2122 end
2123
2124 @doc """
2125 Set `last_digest_emailed_at` value for the user to current time
2126 """
2127 @spec touch_last_digest_emailed_at(t()) :: t()
2128 def touch_last_digest_emailed_at(user) do
2129 now = NaiveDateTime.truncate(NaiveDateTime.utc_now(), :second)
2130
2131 {:ok, updated_user} =
2132 user
2133 |> change(%{last_digest_emailed_at: now})
2134 |> update_and_set_cache()
2135
2136 updated_user
2137 end
2138
2139 @spec set_confirmation(User.t(), boolean()) :: {:ok, User.t()} | {:error, Changeset.t()}
2140 def set_confirmation(%User{} = user, bool) do
2141 user
2142 |> confirmation_changeset(set_confirmation: bool)
2143 |> update_and_set_cache()
2144 end
2145
2146 def get_mascot(%{mascot: %{} = mascot}) when not is_nil(mascot) do
2147 mascot
2148 end
2149
2150 def get_mascot(%{mascot: mascot}) when is_nil(mascot) do
2151 # use instance-default
2152 config = Config.get([:assets, :mascots])
2153 default_mascot = Config.get([:assets, :default_mascot])
2154 mascot = Keyword.get(config, default_mascot)
2155
2156 %{
2157 "id" => "default-mascot",
2158 "url" => mascot[:url],
2159 "preview_url" => mascot[:url],
2160 "pleroma" => %{
2161 "mime_type" => mascot[:mime_type]
2162 }
2163 }
2164 end
2165
2166 def ensure_keys_present(%{keys: keys} = user) when not is_nil(keys), do: {:ok, user}
2167
2168 def ensure_keys_present(%User{} = user) do
2169 with {:ok, pem} <- Keys.generate_rsa_pem() do
2170 user
2171 |> cast(%{keys: pem}, [:keys])
2172 |> validate_required([:keys])
2173 |> update_and_set_cache()
2174 end
2175 end
2176
2177 def get_ap_ids_by_nicknames(nicknames) do
2178 from(u in User,
2179 where: u.nickname in ^nicknames,
2180 select: u.ap_id
2181 )
2182 |> Repo.all()
2183 end
2184
2185 defp put_password_hash(
2186 %Ecto.Changeset{valid?: true, changes: %{password: password}} = changeset
2187 ) do
2188 change(changeset, password_hash: Pleroma.Password.Pbkdf2.hash_pwd_salt(password))
2189 end
2190
2191 defp put_password_hash(changeset), do: changeset
2192
2193 def is_internal_user?(%User{nickname: nil}), do: true
2194 def is_internal_user?(%User{local: true, nickname: "internal." <> _}), do: true
2195 def is_internal_user?(_), do: false
2196
2197 # A hack because user delete activities have a fake id for whatever reason
2198 # TODO: Get rid of this
2199 def get_delivered_users_by_object_id("pleroma:fake_object_id"), do: []
2200
2201 def get_delivered_users_by_object_id(object_id) do
2202 from(u in User,
2203 inner_join: delivery in assoc(u, :deliveries),
2204 where: delivery.object_id == ^object_id
2205 )
2206 |> Repo.all()
2207 end
2208
2209 def change_email(user, email) do
2210 user
2211 |> cast(%{email: email}, [:email])
2212 |> validate_required([:email])
2213 |> unique_constraint(:email)
2214 |> validate_format(:email, @email_regex)
2215 |> update_and_set_cache()
2216 end
2217
2218 # Internal function; public one is `deactivate/2`
2219 defp set_activation_status(user, status) do
2220 user
2221 |> cast(%{is_active: status}, [:is_active])
2222 |> update_and_set_cache()
2223 end
2224
2225 def update_banner(user, banner) do
2226 user
2227 |> cast(%{banner: banner}, [:banner])
2228 |> update_and_set_cache()
2229 end
2230
2231 def update_background(user, background) do
2232 user
2233 |> cast(%{background: background}, [:background])
2234 |> update_and_set_cache()
2235 end
2236
2237 def roles(%{is_moderator: is_moderator, is_admin: is_admin}) do
2238 %{
2239 admin: is_admin,
2240 moderator: is_moderator
2241 }
2242 end
2243
2244 def validate_fields(changeset, remote? \\ false) do
2245 limit_name = if remote?, do: :max_remote_account_fields, else: :max_account_fields
2246 limit = Config.get([:instance, limit_name], 0)
2247
2248 changeset
2249 |> validate_length(:fields, max: limit)
2250 |> validate_change(:fields, fn :fields, fields ->
2251 if Enum.all?(fields, &valid_field?/1) do
2252 []
2253 else
2254 [fields: "invalid"]
2255 end
2256 end)
2257 end
2258
2259 defp valid_field?(%{"name" => name, "value" => value}) do
2260 name_limit = Config.get([:instance, :account_field_name_length], 255)
2261 value_limit = Config.get([:instance, :account_field_value_length], 255)
2262
2263 is_binary(name) && is_binary(value) && String.length(name) <= name_limit &&
2264 String.length(value) <= value_limit
2265 end
2266
2267 defp valid_field?(_), do: false
2268
2269 defp truncate_field(%{"name" => name, "value" => value}) do
2270 {name, _chopped} =
2271 String.split_at(name, Config.get([:instance, :account_field_name_length], 255))
2272
2273 {value, _chopped} =
2274 String.split_at(value, Config.get([:instance, :account_field_value_length], 255))
2275
2276 %{"name" => name, "value" => value}
2277 end
2278
2279 def admin_api_update(user, params) do
2280 user
2281 |> cast(params, [
2282 :is_moderator,
2283 :is_admin,
2284 :show_role
2285 ])
2286 |> update_and_set_cache()
2287 end
2288
2289 @doc "Signs user out of all applications"
2290 def global_sign_out(user) do
2291 OAuth.Authorization.delete_user_authorizations(user)
2292 OAuth.Token.delete_user_tokens(user)
2293 end
2294
2295 def mascot_update(user, url) do
2296 user
2297 |> cast(%{mascot: url}, [:mascot])
2298 |> validate_required([:mascot])
2299 |> update_and_set_cache()
2300 end
2301
2302 def mastodon_settings_update(user, settings) do
2303 user
2304 |> cast(%{mastofe_settings: settings}, [:mastofe_settings])
2305 |> validate_required([:mastofe_settings])
2306 |> update_and_set_cache()
2307 end
2308
2309 @spec confirmation_changeset(User.t(), keyword()) :: Changeset.t()
2310 def confirmation_changeset(user, set_confirmation: confirmed?) do
2311 params =
2312 if confirmed? do
2313 %{
2314 is_confirmed: true,
2315 confirmation_token: nil
2316 }
2317 else
2318 %{
2319 is_confirmed: false,
2320 confirmation_token: :crypto.strong_rand_bytes(32) |> Base.url_encode64()
2321 }
2322 end
2323
2324 cast(user, params, [:is_confirmed, :confirmation_token])
2325 end
2326
2327 @spec approval_changeset(User.t(), keyword()) :: Changeset.t()
2328 def approval_changeset(user, set_approval: approved?) do
2329 cast(user, %{is_approved: approved?}, [:is_approved])
2330 end
2331
2332 def add_pinnned_activity(user, %Pleroma.Activity{id: id}) do
2333 if id not in user.pinned_activities do
2334 max_pinned_statuses = Config.get([:instance, :max_pinned_statuses], 0)
2335 params = %{pinned_activities: user.pinned_activities ++ [id]}
2336
2337 # if pinned activity was scheduled for deletion, we remove job
2338 if expiration = Pleroma.Workers.PurgeExpiredActivity.get_expiration(id) do
2339 Oban.cancel_job(expiration.id)
2340 end
2341
2342 user
2343 |> cast(params, [:pinned_activities])
2344 |> validate_length(:pinned_activities,
2345 max: max_pinned_statuses,
2346 message: "You have already pinned the maximum number of statuses"
2347 )
2348 else
2349 change(user)
2350 end
2351 |> update_and_set_cache()
2352 end
2353
2354 def remove_pinnned_activity(user, %Pleroma.Activity{id: id, data: data}) do
2355 params = %{pinned_activities: List.delete(user.pinned_activities, id)}
2356
2357 # if pinned activity was scheduled for deletion, we reschedule it for deletion
2358 if data["expires_at"] do
2359 # MRF.ActivityExpirationPolicy used UTC timestamps for expires_at in original implementation
2360 {:ok, expires_at} =
2361 data["expires_at"] |> Pleroma.EctoType.ActivityPub.ObjectValidators.DateTime.cast()
2362
2363 Pleroma.Workers.PurgeExpiredActivity.enqueue(%{
2364 activity_id: id,
2365 expires_at: expires_at
2366 })
2367 end
2368
2369 user
2370 |> cast(params, [:pinned_activities])
2371 |> update_and_set_cache()
2372 end
2373
2374 def update_email_notifications(user, settings) do
2375 email_notifications =
2376 user.email_notifications
2377 |> Map.merge(settings)
2378 |> Map.take(["digest"])
2379
2380 params = %{email_notifications: email_notifications}
2381 fields = [:email_notifications]
2382
2383 user
2384 |> cast(params, fields)
2385 |> validate_required(fields)
2386 |> update_and_set_cache()
2387 end
2388
2389 defp set_domain_blocks(user, domain_blocks) do
2390 params = %{domain_blocks: domain_blocks}
2391
2392 user
2393 |> cast(params, [:domain_blocks])
2394 |> validate_required([:domain_blocks])
2395 |> update_and_set_cache()
2396 end
2397
2398 def block_domain(user, domain_blocked) do
2399 set_domain_blocks(user, Enum.uniq([domain_blocked | user.domain_blocks]))
2400 end
2401
2402 def unblock_domain(user, domain_blocked) do
2403 set_domain_blocks(user, List.delete(user.domain_blocks, domain_blocked))
2404 end
2405
2406 @spec add_to_block(User.t(), User.t()) ::
2407 {:ok, UserRelationship.t()} | {:error, Ecto.Changeset.t()}
2408 defp add_to_block(%User{} = user, %User{} = blocked) do
2409 with {:ok, relationship} <- UserRelationship.create_block(user, blocked) do
2410 @cachex.del(:user_cache, "blocked_users_ap_ids:#{user.ap_id}")
2411 {:ok, relationship}
2412 end
2413 end
2414
2415 @spec add_to_block(User.t(), User.t()) ::
2416 {:ok, UserRelationship.t()} | {:ok, nil} | {:error, Ecto.Changeset.t()}
2417 defp remove_from_block(%User{} = user, %User{} = blocked) do
2418 with {:ok, relationship} <- UserRelationship.delete_block(user, blocked) do
2419 @cachex.del(:user_cache, "blocked_users_ap_ids:#{user.ap_id}")
2420 {:ok, relationship}
2421 end
2422 end
2423
2424 def set_invisible(user, invisible) do
2425 params = %{invisible: invisible}
2426
2427 user
2428 |> cast(params, [:invisible])
2429 |> validate_required([:invisible])
2430 |> update_and_set_cache()
2431 end
2432
2433 def sanitize_html(%User{} = user) do
2434 sanitize_html(user, nil)
2435 end
2436
2437 # User data that mastodon isn't filtering (treated as plaintext):
2438 # - field name
2439 # - display name
2440 def sanitize_html(%User{} = user, filter) do
2441 fields =
2442 Enum.map(user.fields, fn %{"name" => name, "value" => value} ->
2443 %{
2444 "name" => name,
2445 "value" => HTML.filter_tags(value, Pleroma.HTML.Scrubber.LinksOnly)
2446 }
2447 end)
2448
2449 user
2450 |> Map.put(:bio, HTML.filter_tags(user.bio, filter))
2451 |> Map.put(:fields, fields)
2452 end
2453
2454 def get_host(%User{ap_id: ap_id} = _user) do
2455 URI.parse(ap_id).host
2456 end
2457
2458 def update_last_active_at(%__MODULE__{local: true} = user) do
2459 user
2460 |> cast(%{last_active_at: NaiveDateTime.utc_now()}, [:last_active_at])
2461 |> update_and_set_cache()
2462 end
2463
2464 def active_user_count(weeks \\ 4) do
2465 active_after = Timex.shift(NaiveDateTime.utc_now(), weeks: -weeks)
2466
2467 __MODULE__
2468 |> where([u], u.last_active_at >= ^active_after)
2469 |> where([u], u.local == true)
2470 |> Repo.aggregate(:count)
2471 end
2472 end