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