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