User: Add raw_bio, storing unformatted bio
[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.Emoji
18 alias Pleroma.FollowingRelationship
19 alias Pleroma.Formatter
20 alias Pleroma.HTML
21 alias Pleroma.Keys
22 alias Pleroma.MFA
23 alias Pleroma.Notification
24 alias Pleroma.Object
25 alias Pleroma.Registration
26 alias Pleroma.Repo
27 alias Pleroma.RepoStreamer
28 alias Pleroma.User
29 alias Pleroma.UserRelationship
30 alias Pleroma.Web
31 alias Pleroma.Web.ActivityPub.ActivityPub
32 alias Pleroma.Web.ActivityPub.Builder
33 alias Pleroma.Web.ActivityPub.ObjectValidators.Types
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, Types.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 |> set_cache()
762 end
763 end
764
765 def unfollow(%User{ap_id: ap_id}, %User{ap_id: ap_id}) do
766 {:error, "Not subscribed!"}
767 end
768
769 @spec unfollow(User.t(), User.t()) :: {:ok, User.t(), Activity.t()} | {:error, String.t()}
770 def unfollow(%User{} = follower, %User{} = followed) do
771 case do_unfollow(follower, followed) do
772 {:ok, follower, followed} ->
773 {:ok, follower, Utils.fetch_latest_follow(follower, followed)}
774
775 error ->
776 error
777 end
778 end
779
780 @spec do_unfollow(User.t(), User.t()) :: {:ok, User.t(), User.t()} | {:error, String.t()}
781 defp do_unfollow(%User{} = follower, %User{} = followed) do
782 case get_follow_state(follower, followed) do
783 state when state in [:follow_pending, :follow_accept] ->
784 FollowingRelationship.unfollow(follower, followed)
785 {:ok, followed} = update_follower_count(followed)
786
787 {:ok, follower} =
788 follower
789 |> update_following_count()
790 |> set_cache()
791
792 {:ok, follower, followed}
793
794 nil ->
795 {:error, "Not subscribed!"}
796 end
797 end
798
799 defdelegate following?(follower, followed), to: FollowingRelationship
800
801 @doc "Returns follow state as Pleroma.FollowingRelationship.State value"
802 def get_follow_state(%User{} = follower, %User{} = following) do
803 following_relationship = FollowingRelationship.get(follower, following)
804 get_follow_state(follower, following, following_relationship)
805 end
806
807 def get_follow_state(
808 %User{} = follower,
809 %User{} = following,
810 following_relationship
811 ) do
812 case {following_relationship, following.local} do
813 {nil, false} ->
814 case Utils.fetch_latest_follow(follower, following) do
815 %Activity{data: %{"state" => state}} when state in ["pending", "accept"] ->
816 FollowingRelationship.state_to_enum(state)
817
818 _ ->
819 nil
820 end
821
822 {%{state: state}, _} ->
823 state
824
825 {nil, _} ->
826 nil
827 end
828 end
829
830 def locked?(%User{} = user) do
831 user.locked || false
832 end
833
834 def get_by_id(id) do
835 Repo.get_by(User, id: id)
836 end
837
838 def get_by_ap_id(ap_id) do
839 Repo.get_by(User, ap_id: ap_id)
840 end
841
842 def get_all_by_ap_id(ap_ids) do
843 from(u in __MODULE__,
844 where: u.ap_id in ^ap_ids
845 )
846 |> Repo.all()
847 end
848
849 def get_all_by_ids(ids) do
850 from(u in __MODULE__, where: u.id in ^ids)
851 |> Repo.all()
852 end
853
854 # This is mostly an SPC migration fix. This guesses the user nickname by taking the last part
855 # of the ap_id and the domain and tries to get that user
856 def get_by_guessed_nickname(ap_id) do
857 domain = URI.parse(ap_id).host
858 name = List.last(String.split(ap_id, "/"))
859 nickname = "#{name}@#{domain}"
860
861 get_cached_by_nickname(nickname)
862 end
863
864 def set_cache({:ok, user}), do: set_cache(user)
865 def set_cache({:error, err}), do: {:error, err}
866
867 def set_cache(%User{} = user) do
868 Cachex.put(:user_cache, "ap_id:#{user.ap_id}", user)
869 Cachex.put(:user_cache, "nickname:#{user.nickname}", user)
870 Cachex.put(:user_cache, "friends_ap_ids:#{user.nickname}", get_user_friends_ap_ids(user))
871 {:ok, user}
872 end
873
874 def update_and_set_cache(struct, params) do
875 struct
876 |> update_changeset(params)
877 |> update_and_set_cache()
878 end
879
880 def update_and_set_cache(changeset) do
881 with {:ok, user} <- Repo.update(changeset, stale_error_field: :id) do
882 set_cache(user)
883 end
884 end
885
886 def get_user_friends_ap_ids(user) do
887 from(u in User.get_friends_query(user), select: u.ap_id)
888 |> Repo.all()
889 end
890
891 @spec get_cached_user_friends_ap_ids(User.t()) :: [String.t()]
892 def get_cached_user_friends_ap_ids(user) do
893 Cachex.fetch!(:user_cache, "friends_ap_ids:#{user.ap_id}", fn _ ->
894 get_user_friends_ap_ids(user)
895 end)
896 end
897
898 def invalidate_cache(user) do
899 Cachex.del(:user_cache, "ap_id:#{user.ap_id}")
900 Cachex.del(:user_cache, "nickname:#{user.nickname}")
901 Cachex.del(:user_cache, "friends_ap_ids:#{user.ap_id}")
902 end
903
904 @spec get_cached_by_ap_id(String.t()) :: User.t() | nil
905 def get_cached_by_ap_id(ap_id) do
906 key = "ap_id:#{ap_id}"
907
908 with {:ok, nil} <- Cachex.get(:user_cache, key),
909 user when not is_nil(user) <- get_by_ap_id(ap_id),
910 {:ok, true} <- Cachex.put(:user_cache, key, user) do
911 user
912 else
913 {:ok, user} -> user
914 nil -> nil
915 end
916 end
917
918 def get_cached_by_id(id) do
919 key = "id:#{id}"
920
921 ap_id =
922 Cachex.fetch!(:user_cache, key, fn _ ->
923 user = get_by_id(id)
924
925 if user do
926 Cachex.put(:user_cache, "ap_id:#{user.ap_id}", user)
927 {:commit, user.ap_id}
928 else
929 {:ignore, ""}
930 end
931 end)
932
933 get_cached_by_ap_id(ap_id)
934 end
935
936 def get_cached_by_nickname(nickname) do
937 key = "nickname:#{nickname}"
938
939 Cachex.fetch!(:user_cache, key, fn ->
940 case get_or_fetch_by_nickname(nickname) do
941 {:ok, user} -> {:commit, user}
942 {:error, _error} -> {:ignore, nil}
943 end
944 end)
945 end
946
947 def get_cached_by_nickname_or_id(nickname_or_id, opts \\ []) do
948 restrict_to_local = Pleroma.Config.get([:instance, :limit_to_local_content])
949
950 cond do
951 is_integer(nickname_or_id) or FlakeId.flake_id?(nickname_or_id) ->
952 get_cached_by_id(nickname_or_id) || get_cached_by_nickname(nickname_or_id)
953
954 restrict_to_local == false or not String.contains?(nickname_or_id, "@") ->
955 get_cached_by_nickname(nickname_or_id)
956
957 restrict_to_local == :unauthenticated and match?(%User{}, opts[:for]) ->
958 get_cached_by_nickname(nickname_or_id)
959
960 true ->
961 nil
962 end
963 end
964
965 @spec get_by_nickname(String.t()) :: User.t() | nil
966 def get_by_nickname(nickname) do
967 Repo.get_by(User, nickname: nickname) ||
968 if Regex.match?(~r(@#{Pleroma.Web.Endpoint.host()})i, nickname) do
969 Repo.get_by(User, nickname: local_nickname(nickname))
970 end
971 end
972
973 def get_by_email(email), do: Repo.get_by(User, email: email)
974
975 def get_by_nickname_or_email(nickname_or_email) do
976 get_by_nickname(nickname_or_email) || get_by_email(nickname_or_email)
977 end
978
979 def fetch_by_nickname(nickname), do: ActivityPub.make_user_from_nickname(nickname)
980
981 def get_or_fetch_by_nickname(nickname) do
982 with %User{} = user <- get_by_nickname(nickname) do
983 {:ok, user}
984 else
985 _e ->
986 with [_nick, _domain] <- String.split(nickname, "@"),
987 {:ok, user} <- fetch_by_nickname(nickname) do
988 {:ok, user}
989 else
990 _e -> {:error, "not found " <> nickname}
991 end
992 end
993 end
994
995 @spec get_followers_query(User.t(), pos_integer() | nil) :: Ecto.Query.t()
996 def get_followers_query(%User{} = user, nil) do
997 User.Query.build(%{followers: user, deactivated: false})
998 end
999
1000 def get_followers_query(user, page) do
1001 user
1002 |> get_followers_query(nil)
1003 |> User.Query.paginate(page, 20)
1004 end
1005
1006 @spec get_followers_query(User.t()) :: Ecto.Query.t()
1007 def get_followers_query(user), do: get_followers_query(user, nil)
1008
1009 @spec get_followers(User.t(), pos_integer() | nil) :: {:ok, list(User.t())}
1010 def get_followers(user, page \\ nil) do
1011 user
1012 |> get_followers_query(page)
1013 |> Repo.all()
1014 end
1015
1016 @spec get_external_followers(User.t(), pos_integer() | nil) :: {:ok, list(User.t())}
1017 def get_external_followers(user, page \\ nil) do
1018 user
1019 |> get_followers_query(page)
1020 |> User.Query.build(%{external: true})
1021 |> Repo.all()
1022 end
1023
1024 def get_followers_ids(user, page \\ nil) do
1025 user
1026 |> get_followers_query(page)
1027 |> select([u], u.id)
1028 |> Repo.all()
1029 end
1030
1031 @spec get_friends_query(User.t(), pos_integer() | nil) :: Ecto.Query.t()
1032 def get_friends_query(%User{} = user, nil) do
1033 User.Query.build(%{friends: user, deactivated: false})
1034 end
1035
1036 def get_friends_query(user, page) do
1037 user
1038 |> get_friends_query(nil)
1039 |> User.Query.paginate(page, 20)
1040 end
1041
1042 @spec get_friends_query(User.t()) :: Ecto.Query.t()
1043 def get_friends_query(user), do: get_friends_query(user, nil)
1044
1045 def get_friends(user, page \\ nil) do
1046 user
1047 |> get_friends_query(page)
1048 |> Repo.all()
1049 end
1050
1051 def get_friends_ap_ids(user) do
1052 user
1053 |> get_friends_query(nil)
1054 |> select([u], u.ap_id)
1055 |> Repo.all()
1056 end
1057
1058 def get_friends_ids(user, page \\ nil) do
1059 user
1060 |> get_friends_query(page)
1061 |> select([u], u.id)
1062 |> Repo.all()
1063 end
1064
1065 defdelegate get_follow_requests(user), to: FollowingRelationship
1066
1067 def increase_note_count(%User{} = user) do
1068 User
1069 |> where(id: ^user.id)
1070 |> update([u], inc: [note_count: 1])
1071 |> select([u], u)
1072 |> Repo.update_all([])
1073 |> case do
1074 {1, [user]} -> set_cache(user)
1075 _ -> {:error, user}
1076 end
1077 end
1078
1079 def decrease_note_count(%User{} = user) do
1080 User
1081 |> where(id: ^user.id)
1082 |> update([u],
1083 set: [
1084 note_count: fragment("greatest(0, note_count - 1)")
1085 ]
1086 )
1087 |> select([u], u)
1088 |> Repo.update_all([])
1089 |> case do
1090 {1, [user]} -> set_cache(user)
1091 _ -> {:error, user}
1092 end
1093 end
1094
1095 def update_note_count(%User{} = user, note_count \\ nil) do
1096 note_count =
1097 note_count ||
1098 from(
1099 a in Object,
1100 where: fragment("?->>'actor' = ? and ?->>'type' = 'Note'", a.data, ^user.ap_id, a.data),
1101 select: count(a.id)
1102 )
1103 |> Repo.one()
1104
1105 user
1106 |> cast(%{note_count: note_count}, [:note_count])
1107 |> update_and_set_cache()
1108 end
1109
1110 @spec maybe_fetch_follow_information(User.t()) :: User.t()
1111 def maybe_fetch_follow_information(user) do
1112 with {:ok, user} <- fetch_follow_information(user) do
1113 user
1114 else
1115 e ->
1116 Logger.error("Follower/Following counter update for #{user.ap_id} failed.\n#{inspect(e)}")
1117
1118 user
1119 end
1120 end
1121
1122 def fetch_follow_information(user) do
1123 with {:ok, info} <- ActivityPub.fetch_follow_information_for_user(user) do
1124 user
1125 |> follow_information_changeset(info)
1126 |> update_and_set_cache()
1127 end
1128 end
1129
1130 defp follow_information_changeset(user, params) do
1131 user
1132 |> cast(params, [
1133 :hide_followers,
1134 :hide_follows,
1135 :follower_count,
1136 :following_count,
1137 :hide_followers_count,
1138 :hide_follows_count
1139 ])
1140 end
1141
1142 def update_follower_count(%User{} = user) do
1143 if user.local or !Pleroma.Config.get([:instance, :external_user_synchronization]) do
1144 follower_count_query =
1145 User.Query.build(%{followers: user, deactivated: false})
1146 |> select([u], %{count: count(u.id)})
1147
1148 User
1149 |> where(id: ^user.id)
1150 |> join(:inner, [u], s in subquery(follower_count_query))
1151 |> update([u, s],
1152 set: [follower_count: s.count]
1153 )
1154 |> select([u], u)
1155 |> Repo.update_all([])
1156 |> case do
1157 {1, [user]} -> set_cache(user)
1158 _ -> {:error, user}
1159 end
1160 else
1161 {:ok, maybe_fetch_follow_information(user)}
1162 end
1163 end
1164
1165 @spec update_following_count(User.t()) :: User.t()
1166 def update_following_count(%User{local: false} = user) do
1167 if Pleroma.Config.get([:instance, :external_user_synchronization]) do
1168 maybe_fetch_follow_information(user)
1169 else
1170 user
1171 end
1172 end
1173
1174 def update_following_count(%User{local: true} = user) do
1175 following_count = FollowingRelationship.following_count(user)
1176
1177 user
1178 |> follow_information_changeset(%{following_count: following_count})
1179 |> Repo.update!()
1180 end
1181
1182 def set_unread_conversation_count(%User{local: true} = user) do
1183 unread_query = Participation.unread_conversation_count_for_user(user)
1184
1185 User
1186 |> join(:inner, [u], p in subquery(unread_query))
1187 |> update([u, p],
1188 set: [unread_conversation_count: p.count]
1189 )
1190 |> where([u], u.id == ^user.id)
1191 |> select([u], u)
1192 |> Repo.update_all([])
1193 |> case do
1194 {1, [user]} -> set_cache(user)
1195 _ -> {:error, user}
1196 end
1197 end
1198
1199 def set_unread_conversation_count(user), do: {:ok, user}
1200
1201 def increment_unread_conversation_count(conversation, %User{local: true} = user) do
1202 unread_query =
1203 Participation.unread_conversation_count_for_user(user)
1204 |> where([p], p.conversation_id == ^conversation.id)
1205
1206 User
1207 |> join(:inner, [u], p in subquery(unread_query))
1208 |> update([u, p],
1209 inc: [unread_conversation_count: 1]
1210 )
1211 |> where([u], u.id == ^user.id)
1212 |> where([u, p], p.count == 0)
1213 |> select([u], u)
1214 |> Repo.update_all([])
1215 |> case do
1216 {1, [user]} -> set_cache(user)
1217 _ -> {:error, user}
1218 end
1219 end
1220
1221 def increment_unread_conversation_count(_, user), do: {:ok, user}
1222
1223 @spec get_users_from_set([String.t()], keyword()) :: [User.t()]
1224 def get_users_from_set(ap_ids, opts \\ []) do
1225 local_only = Keyword.get(opts, :local_only, true)
1226 criteria = %{ap_id: ap_ids, deactivated: false}
1227 criteria = if local_only, do: Map.put(criteria, :local, true), else: criteria
1228
1229 User.Query.build(criteria)
1230 |> Repo.all()
1231 end
1232
1233 @spec get_recipients_from_activity(Activity.t()) :: [User.t()]
1234 def get_recipients_from_activity(%Activity{recipients: to, actor: actor}) do
1235 to = [actor | to]
1236
1237 query = User.Query.build(%{recipients_from_activity: to, local: true, deactivated: false})
1238
1239 query
1240 |> Repo.all()
1241 end
1242
1243 @spec mute(User.t(), User.t(), boolean()) ::
1244 {:ok, list(UserRelationship.t())} | {:error, String.t()}
1245 def mute(%User{} = muter, %User{} = mutee, notifications? \\ true) do
1246 add_to_mutes(muter, mutee, notifications?)
1247 end
1248
1249 def unmute(%User{} = muter, %User{} = mutee) do
1250 remove_from_mutes(muter, mutee)
1251 end
1252
1253 def subscribe(%User{} = subscriber, %User{} = target) do
1254 deny_follow_blocked = Pleroma.Config.get([:user, :deny_follow_blocked])
1255
1256 if blocks?(target, subscriber) and deny_follow_blocked do
1257 {:error, "Could not subscribe: #{target.nickname} is blocking you"}
1258 else
1259 # Note: the relationship is inverse: subscriber acts as relationship target
1260 UserRelationship.create_inverse_subscription(target, subscriber)
1261 end
1262 end
1263
1264 def subscribe(%User{} = subscriber, %{ap_id: ap_id}) do
1265 with %User{} = subscribee <- get_cached_by_ap_id(ap_id) do
1266 subscribe(subscriber, subscribee)
1267 end
1268 end
1269
1270 def unsubscribe(%User{} = unsubscriber, %User{} = target) do
1271 # Note: the relationship is inverse: subscriber acts as relationship target
1272 UserRelationship.delete_inverse_subscription(target, unsubscriber)
1273 end
1274
1275 def unsubscribe(%User{} = unsubscriber, %{ap_id: ap_id}) do
1276 with %User{} = user <- get_cached_by_ap_id(ap_id) do
1277 unsubscribe(unsubscriber, user)
1278 end
1279 end
1280
1281 def block(%User{} = blocker, %User{} = blocked) do
1282 # sever any follow relationships to prevent leaks per activitypub (Pleroma issue #213)
1283 blocker =
1284 if following?(blocker, blocked) do
1285 {:ok, blocker, _} = unfollow(blocker, blocked)
1286 blocker
1287 else
1288 blocker
1289 end
1290
1291 # clear any requested follows as well
1292 blocked =
1293 case CommonAPI.reject_follow_request(blocked, blocker) do
1294 {:ok, %User{} = updated_blocked} -> updated_blocked
1295 nil -> blocked
1296 end
1297
1298 unsubscribe(blocked, blocker)
1299
1300 if following?(blocked, blocker), do: unfollow(blocked, blocker)
1301
1302 {:ok, blocker} = update_follower_count(blocker)
1303 {:ok, blocker, _} = Participation.mark_all_as_read(blocker, blocked)
1304 add_to_block(blocker, blocked)
1305 end
1306
1307 # helper to handle the block given only an actor's AP id
1308 def block(%User{} = blocker, %{ap_id: ap_id}) do
1309 block(blocker, get_cached_by_ap_id(ap_id))
1310 end
1311
1312 def unblock(%User{} = blocker, %User{} = blocked) do
1313 remove_from_block(blocker, blocked)
1314 end
1315
1316 # helper to handle the block given only an actor's AP id
1317 def unblock(%User{} = blocker, %{ap_id: ap_id}) do
1318 unblock(blocker, get_cached_by_ap_id(ap_id))
1319 end
1320
1321 def mutes?(nil, _), do: false
1322 def mutes?(%User{} = user, %User{} = target), do: mutes_user?(user, target)
1323
1324 def mutes_user?(%User{} = user, %User{} = target) do
1325 UserRelationship.mute_exists?(user, target)
1326 end
1327
1328 @spec muted_notifications?(User.t() | nil, User.t() | map()) :: boolean()
1329 def muted_notifications?(nil, _), do: false
1330
1331 def muted_notifications?(%User{} = user, %User{} = target),
1332 do: UserRelationship.notification_mute_exists?(user, target)
1333
1334 def blocks?(nil, _), do: false
1335
1336 def blocks?(%User{} = user, %User{} = target) do
1337 blocks_user?(user, target) ||
1338 (blocks_domain?(user, target) and not User.following?(user, target))
1339 end
1340
1341 def blocks_user?(%User{} = user, %User{} = target) do
1342 UserRelationship.block_exists?(user, target)
1343 end
1344
1345 def blocks_user?(_, _), do: false
1346
1347 def blocks_domain?(%User{} = user, %User{} = target) do
1348 domain_blocks = Pleroma.Web.ActivityPub.MRF.subdomains_regex(user.domain_blocks)
1349 %{host: host} = URI.parse(target.ap_id)
1350 Pleroma.Web.ActivityPub.MRF.subdomain_match?(domain_blocks, host)
1351 end
1352
1353 def blocks_domain?(_, _), do: false
1354
1355 def subscribed_to?(%User{} = user, %User{} = target) do
1356 # Note: the relationship is inverse: subscriber acts as relationship target
1357 UserRelationship.inverse_subscription_exists?(target, user)
1358 end
1359
1360 def subscribed_to?(%User{} = user, %{ap_id: ap_id}) do
1361 with %User{} = target <- get_cached_by_ap_id(ap_id) do
1362 subscribed_to?(user, target)
1363 end
1364 end
1365
1366 @doc """
1367 Returns map of outgoing (blocked, muted etc.) relationships' user AP IDs by relation type.
1368 E.g. `outgoing_relationships_ap_ids(user, [:block])` -> `%{block: ["https://some.site/users/userapid"]}`
1369 """
1370 @spec outgoing_relationships_ap_ids(User.t(), list(atom())) :: %{atom() => list(String.t())}
1371 def outgoing_relationships_ap_ids(_user, []), do: %{}
1372
1373 def outgoing_relationships_ap_ids(nil, _relationship_types), do: %{}
1374
1375 def outgoing_relationships_ap_ids(%User{} = user, relationship_types)
1376 when is_list(relationship_types) do
1377 db_result =
1378 user
1379 |> assoc(:outgoing_relationships)
1380 |> join(:inner, [user_rel], u in assoc(user_rel, :target))
1381 |> where([user_rel, u], user_rel.relationship_type in ^relationship_types)
1382 |> select([user_rel, u], [user_rel.relationship_type, fragment("array_agg(?)", u.ap_id)])
1383 |> group_by([user_rel, u], user_rel.relationship_type)
1384 |> Repo.all()
1385 |> Enum.into(%{}, fn [k, v] -> {k, v} end)
1386
1387 Enum.into(
1388 relationship_types,
1389 %{},
1390 fn rel_type -> {rel_type, db_result[rel_type] || []} end
1391 )
1392 end
1393
1394 def incoming_relationships_ungrouped_ap_ids(user, relationship_types, ap_ids \\ nil)
1395
1396 def incoming_relationships_ungrouped_ap_ids(_user, [], _ap_ids), do: []
1397
1398 def incoming_relationships_ungrouped_ap_ids(nil, _relationship_types, _ap_ids), do: []
1399
1400 def incoming_relationships_ungrouped_ap_ids(%User{} = user, relationship_types, ap_ids)
1401 when is_list(relationship_types) do
1402 user
1403 |> assoc(:incoming_relationships)
1404 |> join(:inner, [user_rel], u in assoc(user_rel, :source))
1405 |> where([user_rel, u], user_rel.relationship_type in ^relationship_types)
1406 |> maybe_filter_on_ap_id(ap_ids)
1407 |> select([user_rel, u], u.ap_id)
1408 |> distinct(true)
1409 |> Repo.all()
1410 end
1411
1412 defp maybe_filter_on_ap_id(query, ap_ids) when is_list(ap_ids) do
1413 where(query, [user_rel, u], u.ap_id in ^ap_ids)
1414 end
1415
1416 defp maybe_filter_on_ap_id(query, _ap_ids), do: query
1417
1418 def deactivate_async(user, status \\ true) do
1419 BackgroundWorker.enqueue("deactivate_user", %{"user_id" => user.id, "status" => status})
1420 end
1421
1422 def deactivate(user, status \\ true)
1423
1424 def deactivate(users, status) when is_list(users) do
1425 Repo.transaction(fn ->
1426 for user <- users, do: deactivate(user, status)
1427 end)
1428 end
1429
1430 def deactivate(%User{} = user, status) do
1431 with {:ok, user} <- set_activation_status(user, status) do
1432 user
1433 |> get_followers()
1434 |> Enum.filter(& &1.local)
1435 |> Enum.each(&set_cache(update_following_count(&1)))
1436
1437 # Only update local user counts, remote will be update during the next pull.
1438 user
1439 |> get_friends()
1440 |> Enum.filter(& &1.local)
1441 |> Enum.each(&do_unfollow(user, &1))
1442
1443 {:ok, user}
1444 end
1445 end
1446
1447 def update_notification_settings(%User{} = user, settings) do
1448 user
1449 |> cast(%{notification_settings: settings}, [])
1450 |> cast_embed(:notification_settings)
1451 |> validate_required([:notification_settings])
1452 |> update_and_set_cache()
1453 end
1454
1455 def delete(users) when is_list(users) do
1456 for user <- users, do: delete(user)
1457 end
1458
1459 def delete(%User{} = user) do
1460 BackgroundWorker.enqueue("delete_user", %{"user_id" => user.id})
1461 end
1462
1463 defp delete_and_invalidate_cache(%User{} = user) do
1464 invalidate_cache(user)
1465 Repo.delete(user)
1466 end
1467
1468 defp delete_or_deactivate(%User{local: false} = user), do: delete_and_invalidate_cache(user)
1469
1470 defp delete_or_deactivate(%User{local: true} = user) do
1471 status = account_status(user)
1472
1473 if status == :confirmation_pending do
1474 delete_and_invalidate_cache(user)
1475 else
1476 user
1477 |> change(%{deactivated: true, email: nil})
1478 |> update_and_set_cache()
1479 end
1480 end
1481
1482 def perform(:force_password_reset, user), do: force_password_reset(user)
1483
1484 @spec perform(atom(), User.t()) :: {:ok, User.t()}
1485 def perform(:delete, %User{} = user) do
1486 # Remove all relationships
1487 user
1488 |> get_followers()
1489 |> Enum.each(fn follower ->
1490 ActivityPub.unfollow(follower, user)
1491 unfollow(follower, user)
1492 end)
1493
1494 user
1495 |> get_friends()
1496 |> Enum.each(fn followed ->
1497 ActivityPub.unfollow(user, followed)
1498 unfollow(user, followed)
1499 end)
1500
1501 delete_user_activities(user)
1502
1503 delete_or_deactivate(user)
1504 end
1505
1506 def perform(:deactivate_async, user, status), do: deactivate(user, status)
1507
1508 @spec perform(atom(), User.t(), list()) :: list() | {:error, any()}
1509 def perform(:blocks_import, %User{} = blocker, blocked_identifiers)
1510 when is_list(blocked_identifiers) do
1511 Enum.map(
1512 blocked_identifiers,
1513 fn blocked_identifier ->
1514 with {:ok, %User{} = blocked} <- get_or_fetch(blocked_identifier),
1515 {:ok, _user_block} <- block(blocker, blocked),
1516 {:ok, _} <- ActivityPub.block(blocker, blocked) do
1517 blocked
1518 else
1519 err ->
1520 Logger.debug("blocks_import failed for #{blocked_identifier} with: #{inspect(err)}")
1521 err
1522 end
1523 end
1524 )
1525 end
1526
1527 def perform(:follow_import, %User{} = follower, followed_identifiers)
1528 when is_list(followed_identifiers) do
1529 Enum.map(
1530 followed_identifiers,
1531 fn followed_identifier ->
1532 with {:ok, %User{} = followed} <- get_or_fetch(followed_identifier),
1533 {:ok, follower} <- maybe_direct_follow(follower, followed),
1534 {:ok, _} <- ActivityPub.follow(follower, followed) do
1535 followed
1536 else
1537 err ->
1538 Logger.debug("follow_import failed for #{followed_identifier} with: #{inspect(err)}")
1539 err
1540 end
1541 end
1542 )
1543 end
1544
1545 @spec external_users_query() :: Ecto.Query.t()
1546 def external_users_query do
1547 User.Query.build(%{
1548 external: true,
1549 active: true,
1550 order_by: :id
1551 })
1552 end
1553
1554 @spec external_users(keyword()) :: [User.t()]
1555 def external_users(opts \\ []) do
1556 query =
1557 external_users_query()
1558 |> select([u], struct(u, [:id, :ap_id]))
1559
1560 query =
1561 if opts[:max_id],
1562 do: where(query, [u], u.id > ^opts[:max_id]),
1563 else: query
1564
1565 query =
1566 if opts[:limit],
1567 do: limit(query, ^opts[:limit]),
1568 else: query
1569
1570 Repo.all(query)
1571 end
1572
1573 def blocks_import(%User{} = blocker, blocked_identifiers) when is_list(blocked_identifiers) do
1574 BackgroundWorker.enqueue("blocks_import", %{
1575 "blocker_id" => blocker.id,
1576 "blocked_identifiers" => blocked_identifiers
1577 })
1578 end
1579
1580 def follow_import(%User{} = follower, followed_identifiers)
1581 when is_list(followed_identifiers) do
1582 BackgroundWorker.enqueue("follow_import", %{
1583 "follower_id" => follower.id,
1584 "followed_identifiers" => followed_identifiers
1585 })
1586 end
1587
1588 def delete_user_activities(%User{ap_id: ap_id} = user) do
1589 ap_id
1590 |> Activity.Queries.by_actor()
1591 |> RepoStreamer.chunk_stream(50)
1592 |> Stream.each(fn activities ->
1593 Enum.each(activities, fn activity -> delete_activity(activity, user) end)
1594 end)
1595 |> Stream.run()
1596 end
1597
1598 defp delete_activity(%{data: %{"type" => "Create", "object" => object}} = activity, user) do
1599 with {_, %Object{}} <- {:find_object, Object.get_by_ap_id(object)},
1600 {:ok, delete_data, _} <- Builder.delete(user, object) do
1601 Pipeline.common_pipeline(delete_data, local: user.local)
1602 else
1603 {:find_object, nil} ->
1604 # We have the create activity, but not the object, it was probably pruned.
1605 # Insert a tombstone and try again
1606 with {:ok, tombstone_data, _} <- Builder.tombstone(user.ap_id, object),
1607 {:ok, _tombstone} <- Object.create(tombstone_data) do
1608 delete_activity(activity, user)
1609 end
1610
1611 e ->
1612 Logger.error("Could not delete #{object} created by #{activity.data["ap_id"]}")
1613 Logger.error("Error: #{inspect(e)}")
1614 end
1615 end
1616
1617 defp delete_activity(%{data: %{"type" => type}} = activity, user)
1618 when type in ["Like", "Announce"] do
1619 {:ok, undo, _} = Builder.undo(user, activity)
1620 Pipeline.common_pipeline(undo, local: user.local)
1621 end
1622
1623 defp delete_activity(_activity, _user), do: "Doing nothing"
1624
1625 def html_filter_policy(%User{no_rich_text: true}) do
1626 Pleroma.HTML.Scrubber.TwitterText
1627 end
1628
1629 def html_filter_policy(_), do: Pleroma.Config.get([:markup, :scrub_policy])
1630
1631 def fetch_by_ap_id(ap_id), do: ActivityPub.make_user_from_ap_id(ap_id)
1632
1633 def get_or_fetch_by_ap_id(ap_id) do
1634 cached_user = get_cached_by_ap_id(ap_id)
1635
1636 maybe_fetched_user = needs_update?(cached_user) && fetch_by_ap_id(ap_id)
1637
1638 case {cached_user, maybe_fetched_user} do
1639 {_, {:ok, %User{} = user}} ->
1640 {:ok, user}
1641
1642 {%User{} = user, _} ->
1643 {:ok, user}
1644
1645 _ ->
1646 {:error, :not_found}
1647 end
1648 end
1649
1650 @doc """
1651 Creates an internal service actor by URI if missing.
1652 Optionally takes nickname for addressing.
1653 """
1654 @spec get_or_create_service_actor_by_ap_id(String.t(), String.t()) :: User.t() | nil
1655 def get_or_create_service_actor_by_ap_id(uri, nickname) do
1656 {_, user} =
1657 case get_cached_by_ap_id(uri) do
1658 nil ->
1659 with {:error, %{errors: errors}} <- create_service_actor(uri, nickname) do
1660 Logger.error("Cannot create service actor: #{uri}/.\n#{inspect(errors)}")
1661 {:error, nil}
1662 end
1663
1664 %User{invisible: false} = user ->
1665 set_invisible(user)
1666
1667 user ->
1668 {:ok, user}
1669 end
1670
1671 user
1672 end
1673
1674 @spec set_invisible(User.t()) :: {:ok, User.t()}
1675 defp set_invisible(user) do
1676 user
1677 |> change(%{invisible: true})
1678 |> update_and_set_cache()
1679 end
1680
1681 @spec create_service_actor(String.t(), String.t()) ::
1682 {:ok, User.t()} | {:error, Ecto.Changeset.t()}
1683 defp create_service_actor(uri, nickname) do
1684 %User{
1685 invisible: true,
1686 local: true,
1687 ap_id: uri,
1688 nickname: nickname,
1689 follower_address: uri <> "/followers"
1690 }
1691 |> change
1692 |> unique_constraint(:nickname)
1693 |> Repo.insert()
1694 |> set_cache()
1695 end
1696
1697 def public_key(%{public_key: public_key_pem}) when is_binary(public_key_pem) do
1698 key =
1699 public_key_pem
1700 |> :public_key.pem_decode()
1701 |> hd()
1702 |> :public_key.pem_entry_decode()
1703
1704 {:ok, key}
1705 end
1706
1707 def public_key(_), do: {:error, "key not found"}
1708
1709 def get_public_key_for_ap_id(ap_id) do
1710 with {:ok, %User{} = user} <- get_or_fetch_by_ap_id(ap_id),
1711 {:ok, public_key} <- public_key(user) do
1712 {:ok, public_key}
1713 else
1714 _ -> :error
1715 end
1716 end
1717
1718 def ap_enabled?(%User{local: true}), do: true
1719 def ap_enabled?(%User{ap_enabled: ap_enabled}), do: ap_enabled
1720 def ap_enabled?(_), do: false
1721
1722 @doc "Gets or fetch a user by uri or nickname."
1723 @spec get_or_fetch(String.t()) :: {:ok, User.t()} | {:error, String.t()}
1724 def get_or_fetch("http" <> _host = uri), do: get_or_fetch_by_ap_id(uri)
1725 def get_or_fetch(nickname), do: get_or_fetch_by_nickname(nickname)
1726
1727 # wait a period of time and return newest version of the User structs
1728 # this is because we have synchronous follow APIs and need to simulate them
1729 # with an async handshake
1730 def wait_and_refresh(_, %User{local: true} = a, %User{local: true} = b) do
1731 with %User{} = a <- get_cached_by_id(a.id),
1732 %User{} = b <- get_cached_by_id(b.id) do
1733 {:ok, a, b}
1734 else
1735 nil -> :error
1736 end
1737 end
1738
1739 def wait_and_refresh(timeout, %User{} = a, %User{} = b) do
1740 with :ok <- :timer.sleep(timeout),
1741 %User{} = a <- get_cached_by_id(a.id),
1742 %User{} = b <- get_cached_by_id(b.id) do
1743 {:ok, a, b}
1744 else
1745 nil -> :error
1746 end
1747 end
1748
1749 def parse_bio(bio) when is_binary(bio) and bio != "" do
1750 bio
1751 |> CommonUtils.format_input("text/plain", mentions_format: :full)
1752 |> elem(0)
1753 end
1754
1755 def parse_bio(_), do: ""
1756
1757 def parse_bio(bio, user) when is_binary(bio) and bio != "" do
1758 # TODO: get profile URLs other than user.ap_id
1759 profile_urls = [user.ap_id]
1760
1761 bio
1762 |> CommonUtils.format_input("text/plain",
1763 mentions_format: :full,
1764 rel: &RelMe.maybe_put_rel_me(&1, profile_urls)
1765 )
1766 |> elem(0)
1767 end
1768
1769 def parse_bio(_, _), do: ""
1770
1771 def tag(user_identifiers, tags) when is_list(user_identifiers) do
1772 Repo.transaction(fn ->
1773 for user_identifier <- user_identifiers, do: tag(user_identifier, tags)
1774 end)
1775 end
1776
1777 def tag(nickname, tags) when is_binary(nickname),
1778 do: tag(get_by_nickname(nickname), tags)
1779
1780 def tag(%User{} = user, tags),
1781 do: update_tags(user, Enum.uniq((user.tags || []) ++ normalize_tags(tags)))
1782
1783 def untag(user_identifiers, tags) when is_list(user_identifiers) do
1784 Repo.transaction(fn ->
1785 for user_identifier <- user_identifiers, do: untag(user_identifier, tags)
1786 end)
1787 end
1788
1789 def untag(nickname, tags) when is_binary(nickname),
1790 do: untag(get_by_nickname(nickname), tags)
1791
1792 def untag(%User{} = user, tags),
1793 do: update_tags(user, (user.tags || []) -- normalize_tags(tags))
1794
1795 defp update_tags(%User{} = user, new_tags) do
1796 {:ok, updated_user} =
1797 user
1798 |> change(%{tags: new_tags})
1799 |> update_and_set_cache()
1800
1801 updated_user
1802 end
1803
1804 defp normalize_tags(tags) do
1805 [tags]
1806 |> List.flatten()
1807 |> Enum.map(&String.downcase/1)
1808 end
1809
1810 defp local_nickname_regex do
1811 if Pleroma.Config.get([:instance, :extended_nickname_format]) do
1812 @extended_local_nickname_regex
1813 else
1814 @strict_local_nickname_regex
1815 end
1816 end
1817
1818 def local_nickname(nickname_or_mention) do
1819 nickname_or_mention
1820 |> full_nickname()
1821 |> String.split("@")
1822 |> hd()
1823 end
1824
1825 def full_nickname(nickname_or_mention),
1826 do: String.trim_leading(nickname_or_mention, "@")
1827
1828 def error_user(ap_id) do
1829 %User{
1830 name: ap_id,
1831 ap_id: ap_id,
1832 nickname: "erroruser@example.com",
1833 inserted_at: NaiveDateTime.utc_now()
1834 }
1835 end
1836
1837 @spec all_superusers() :: [User.t()]
1838 def all_superusers do
1839 User.Query.build(%{super_users: true, local: true, deactivated: false})
1840 |> Repo.all()
1841 end
1842
1843 def muting_reblogs?(%User{} = user, %User{} = target) do
1844 UserRelationship.reblog_mute_exists?(user, target)
1845 end
1846
1847 def showing_reblogs?(%User{} = user, %User{} = target) do
1848 not muting_reblogs?(user, target)
1849 end
1850
1851 @doc """
1852 The function returns a query to get users with no activity for given interval of days.
1853 Inactive users are those who didn't read any notification, or had any activity where
1854 the user is the activity's actor, during `inactivity_threshold` days.
1855 Deactivated users will not appear in this list.
1856
1857 ## Examples
1858
1859 iex> Pleroma.User.list_inactive_users()
1860 %Ecto.Query{}
1861 """
1862 @spec list_inactive_users_query(integer()) :: Ecto.Query.t()
1863 def list_inactive_users_query(inactivity_threshold \\ 7) do
1864 negative_inactivity_threshold = -inactivity_threshold
1865 now = NaiveDateTime.truncate(NaiveDateTime.utc_now(), :second)
1866 # Subqueries are not supported in `where` clauses, join gets too complicated.
1867 has_read_notifications =
1868 from(n in Pleroma.Notification,
1869 where: n.seen == true,
1870 group_by: n.id,
1871 having: max(n.updated_at) > datetime_add(^now, ^negative_inactivity_threshold, "day"),
1872 select: n.user_id
1873 )
1874 |> Pleroma.Repo.all()
1875
1876 from(u in Pleroma.User,
1877 left_join: a in Pleroma.Activity,
1878 on: u.ap_id == a.actor,
1879 where: not is_nil(u.nickname),
1880 where: u.deactivated != ^true,
1881 where: u.id not in ^has_read_notifications,
1882 group_by: u.id,
1883 having:
1884 max(a.inserted_at) < datetime_add(^now, ^negative_inactivity_threshold, "day") or
1885 is_nil(max(a.inserted_at))
1886 )
1887 end
1888
1889 @doc """
1890 Enable or disable email notifications for user
1891
1892 ## Examples
1893
1894 iex> Pleroma.User.switch_email_notifications(Pleroma.User{email_notifications: %{"digest" => false}}, "digest", true)
1895 Pleroma.User{email_notifications: %{"digest" => true}}
1896
1897 iex> Pleroma.User.switch_email_notifications(Pleroma.User{email_notifications: %{"digest" => true}}, "digest", false)
1898 Pleroma.User{email_notifications: %{"digest" => false}}
1899 """
1900 @spec switch_email_notifications(t(), String.t(), boolean()) ::
1901 {:ok, t()} | {:error, Ecto.Changeset.t()}
1902 def switch_email_notifications(user, type, status) do
1903 User.update_email_notifications(user, %{type => status})
1904 end
1905
1906 @doc """
1907 Set `last_digest_emailed_at` value for the user to current time
1908 """
1909 @spec touch_last_digest_emailed_at(t()) :: t()
1910 def touch_last_digest_emailed_at(user) do
1911 now = NaiveDateTime.truncate(NaiveDateTime.utc_now(), :second)
1912
1913 {:ok, updated_user} =
1914 user
1915 |> change(%{last_digest_emailed_at: now})
1916 |> update_and_set_cache()
1917
1918 updated_user
1919 end
1920
1921 @spec toggle_confirmation(User.t()) :: {:ok, User.t()} | {:error, Changeset.t()}
1922 def toggle_confirmation(%User{} = user) do
1923 user
1924 |> confirmation_changeset(need_confirmation: !user.confirmation_pending)
1925 |> update_and_set_cache()
1926 end
1927
1928 @spec toggle_confirmation([User.t()]) :: [{:ok, User.t()} | {:error, Changeset.t()}]
1929 def toggle_confirmation(users) do
1930 Enum.map(users, &toggle_confirmation/1)
1931 end
1932
1933 def get_mascot(%{mascot: %{} = mascot}) when not is_nil(mascot) do
1934 mascot
1935 end
1936
1937 def get_mascot(%{mascot: mascot}) when is_nil(mascot) do
1938 # use instance-default
1939 config = Pleroma.Config.get([:assets, :mascots])
1940 default_mascot = Pleroma.Config.get([:assets, :default_mascot])
1941 mascot = Keyword.get(config, default_mascot)
1942
1943 %{
1944 "id" => "default-mascot",
1945 "url" => mascot[:url],
1946 "preview_url" => mascot[:url],
1947 "pleroma" => %{
1948 "mime_type" => mascot[:mime_type]
1949 }
1950 }
1951 end
1952
1953 def ensure_keys_present(%{keys: keys} = user) when not is_nil(keys), do: {:ok, user}
1954
1955 def ensure_keys_present(%User{} = user) do
1956 with {:ok, pem} <- Keys.generate_rsa_pem() do
1957 user
1958 |> cast(%{keys: pem}, [:keys])
1959 |> validate_required([:keys])
1960 |> update_and_set_cache()
1961 end
1962 end
1963
1964 def get_ap_ids_by_nicknames(nicknames) do
1965 from(u in User,
1966 where: u.nickname in ^nicknames,
1967 select: u.ap_id
1968 )
1969 |> Repo.all()
1970 end
1971
1972 defdelegate search(query, opts \\ []), to: User.Search
1973
1974 defp put_password_hash(
1975 %Ecto.Changeset{valid?: true, changes: %{password: password}} = changeset
1976 ) do
1977 change(changeset, password_hash: Pbkdf2.hash_pwd_salt(password))
1978 end
1979
1980 defp put_password_hash(changeset), do: changeset
1981
1982 def is_internal_user?(%User{nickname: nil}), do: true
1983 def is_internal_user?(%User{local: true, nickname: "internal." <> _}), do: true
1984 def is_internal_user?(_), do: false
1985
1986 # A hack because user delete activities have a fake id for whatever reason
1987 # TODO: Get rid of this
1988 def get_delivered_users_by_object_id("pleroma:fake_object_id"), do: []
1989
1990 def get_delivered_users_by_object_id(object_id) do
1991 from(u in User,
1992 inner_join: delivery in assoc(u, :deliveries),
1993 where: delivery.object_id == ^object_id
1994 )
1995 |> Repo.all()
1996 end
1997
1998 def change_email(user, email) do
1999 user
2000 |> cast(%{email: email}, [:email])
2001 |> validate_required([:email])
2002 |> unique_constraint(:email)
2003 |> validate_format(:email, @email_regex)
2004 |> update_and_set_cache()
2005 end
2006
2007 # Internal function; public one is `deactivate/2`
2008 defp set_activation_status(user, deactivated) do
2009 user
2010 |> cast(%{deactivated: deactivated}, [:deactivated])
2011 |> update_and_set_cache()
2012 end
2013
2014 def update_banner(user, banner) do
2015 user
2016 |> cast(%{banner: banner}, [:banner])
2017 |> update_and_set_cache()
2018 end
2019
2020 def update_background(user, background) do
2021 user
2022 |> cast(%{background: background}, [:background])
2023 |> update_and_set_cache()
2024 end
2025
2026 def roles(%{is_moderator: is_moderator, is_admin: is_admin}) do
2027 %{
2028 admin: is_admin,
2029 moderator: is_moderator
2030 }
2031 end
2032
2033 def validate_fields(changeset, remote? \\ false) do
2034 limit_name = if remote?, do: :max_remote_account_fields, else: :max_account_fields
2035 limit = Pleroma.Config.get([:instance, limit_name], 0)
2036
2037 changeset
2038 |> validate_length(:fields, max: limit)
2039 |> validate_change(:fields, fn :fields, fields ->
2040 if Enum.all?(fields, &valid_field?/1) do
2041 []
2042 else
2043 [fields: "invalid"]
2044 end
2045 end)
2046 end
2047
2048 defp valid_field?(%{"name" => name, "value" => value}) do
2049 name_limit = Pleroma.Config.get([:instance, :account_field_name_length], 255)
2050 value_limit = Pleroma.Config.get([:instance, :account_field_value_length], 255)
2051
2052 is_binary(name) && is_binary(value) && String.length(name) <= name_limit &&
2053 String.length(value) <= value_limit
2054 end
2055
2056 defp valid_field?(_), do: false
2057
2058 defp truncate_field(%{"name" => name, "value" => value}) do
2059 {name, _chopped} =
2060 String.split_at(name, Pleroma.Config.get([:instance, :account_field_name_length], 255))
2061
2062 {value, _chopped} =
2063 String.split_at(value, Pleroma.Config.get([:instance, :account_field_value_length], 255))
2064
2065 %{"name" => name, "value" => value}
2066 end
2067
2068 def admin_api_update(user, params) do
2069 user
2070 |> cast(params, [
2071 :is_moderator,
2072 :is_admin,
2073 :show_role
2074 ])
2075 |> update_and_set_cache()
2076 end
2077
2078 @doc "Signs user out of all applications"
2079 def global_sign_out(user) do
2080 OAuth.Authorization.delete_user_authorizations(user)
2081 OAuth.Token.delete_user_tokens(user)
2082 end
2083
2084 def mascot_update(user, url) do
2085 user
2086 |> cast(%{mascot: url}, [:mascot])
2087 |> validate_required([:mascot])
2088 |> update_and_set_cache()
2089 end
2090
2091 def mastodon_settings_update(user, settings) do
2092 user
2093 |> cast(%{settings: settings}, [:settings])
2094 |> validate_required([:settings])
2095 |> update_and_set_cache()
2096 end
2097
2098 @spec confirmation_changeset(User.t(), keyword()) :: Changeset.t()
2099 def confirmation_changeset(user, need_confirmation: need_confirmation?) do
2100 params =
2101 if need_confirmation? do
2102 %{
2103 confirmation_pending: true,
2104 confirmation_token: :crypto.strong_rand_bytes(32) |> Base.url_encode64()
2105 }
2106 else
2107 %{
2108 confirmation_pending: false,
2109 confirmation_token: nil
2110 }
2111 end
2112
2113 cast(user, params, [:confirmation_pending, :confirmation_token])
2114 end
2115
2116 def add_pinnned_activity(user, %Pleroma.Activity{id: id}) do
2117 if id not in user.pinned_activities do
2118 max_pinned_statuses = Pleroma.Config.get([:instance, :max_pinned_statuses], 0)
2119 params = %{pinned_activities: user.pinned_activities ++ [id]}
2120
2121 user
2122 |> cast(params, [:pinned_activities])
2123 |> validate_length(:pinned_activities,
2124 max: max_pinned_statuses,
2125 message: "You have already pinned the maximum number of statuses"
2126 )
2127 else
2128 change(user)
2129 end
2130 |> update_and_set_cache()
2131 end
2132
2133 def remove_pinnned_activity(user, %Pleroma.Activity{id: id}) do
2134 params = %{pinned_activities: List.delete(user.pinned_activities, id)}
2135
2136 user
2137 |> cast(params, [:pinned_activities])
2138 |> update_and_set_cache()
2139 end
2140
2141 def update_email_notifications(user, settings) do
2142 email_notifications =
2143 user.email_notifications
2144 |> Map.merge(settings)
2145 |> Map.take(["digest"])
2146
2147 params = %{email_notifications: email_notifications}
2148 fields = [:email_notifications]
2149
2150 user
2151 |> cast(params, fields)
2152 |> validate_required(fields)
2153 |> update_and_set_cache()
2154 end
2155
2156 defp set_domain_blocks(user, domain_blocks) do
2157 params = %{domain_blocks: domain_blocks}
2158
2159 user
2160 |> cast(params, [:domain_blocks])
2161 |> validate_required([:domain_blocks])
2162 |> update_and_set_cache()
2163 end
2164
2165 def block_domain(user, domain_blocked) do
2166 set_domain_blocks(user, Enum.uniq([domain_blocked | user.domain_blocks]))
2167 end
2168
2169 def unblock_domain(user, domain_blocked) do
2170 set_domain_blocks(user, List.delete(user.domain_blocks, domain_blocked))
2171 end
2172
2173 @spec add_to_block(User.t(), User.t()) ::
2174 {:ok, UserRelationship.t()} | {:error, Ecto.Changeset.t()}
2175 defp add_to_block(%User{} = user, %User{} = blocked) do
2176 UserRelationship.create_block(user, blocked)
2177 end
2178
2179 @spec add_to_block(User.t(), User.t()) ::
2180 {:ok, UserRelationship.t()} | {:ok, nil} | {:error, Ecto.Changeset.t()}
2181 defp remove_from_block(%User{} = user, %User{} = blocked) do
2182 UserRelationship.delete_block(user, blocked)
2183 end
2184
2185 defp add_to_mutes(%User{} = user, %User{} = muted_user, notifications?) do
2186 with {:ok, user_mute} <- UserRelationship.create_mute(user, muted_user),
2187 {:ok, user_notification_mute} <-
2188 (notifications? && UserRelationship.create_notification_mute(user, muted_user)) ||
2189 {:ok, nil} do
2190 {:ok, Enum.filter([user_mute, user_notification_mute], & &1)}
2191 end
2192 end
2193
2194 defp remove_from_mutes(user, %User{} = muted_user) do
2195 with {:ok, user_mute} <- UserRelationship.delete_mute(user, muted_user),
2196 {:ok, user_notification_mute} <-
2197 UserRelationship.delete_notification_mute(user, muted_user) do
2198 {:ok, [user_mute, user_notification_mute]}
2199 end
2200 end
2201
2202 def set_invisible(user, invisible) do
2203 params = %{invisible: invisible}
2204
2205 user
2206 |> cast(params, [:invisible])
2207 |> validate_required([:invisible])
2208 |> update_and_set_cache()
2209 end
2210
2211 def sanitize_html(%User{} = user) do
2212 sanitize_html(user, nil)
2213 end
2214
2215 # User data that mastodon isn't filtering (treated as plaintext):
2216 # - field name
2217 # - display name
2218 def sanitize_html(%User{} = user, filter) do
2219 fields =
2220 Enum.map(user.fields, fn %{"name" => name, "value" => value} ->
2221 %{
2222 "name" => name,
2223 "value" => HTML.filter_tags(value, Pleroma.HTML.Scrubber.LinksOnly)
2224 }
2225 end)
2226
2227 user
2228 |> Map.put(:bio, HTML.filter_tags(user.bio, filter))
2229 |> Map.put(:fields, fields)
2230 end
2231 end