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