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