de6624cbd90be19b90c775f020132bc36ba4425f
[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 # Remove all relationships
1433 user
1434 |> get_followers()
1435 |> Enum.each(fn follower ->
1436 ActivityPub.unfollow(follower, user)
1437 unfollow(follower, user)
1438 end)
1439
1440 user
1441 |> get_friends()
1442 |> Enum.each(fn followed ->
1443 ActivityPub.unfollow(user, followed)
1444 unfollow(user, followed)
1445 end)
1446
1447 delete_user_activities(user)
1448
1449 if user.local do
1450 user
1451 |> change(%{deactivated: true, email: nil})
1452 |> update_and_set_cache()
1453 else
1454 invalidate_cache(user)
1455 Repo.delete(user)
1456 end
1457 end
1458
1459 def perform(:deactivate_async, user, status), do: deactivate(user, status)
1460
1461 @spec perform(atom(), User.t(), list()) :: list() | {:error, any()}
1462 def perform(:blocks_import, %User{} = blocker, blocked_identifiers)
1463 when is_list(blocked_identifiers) do
1464 Enum.map(
1465 blocked_identifiers,
1466 fn blocked_identifier ->
1467 with {:ok, %User{} = blocked} <- get_or_fetch(blocked_identifier),
1468 {:ok, _user_block} <- block(blocker, blocked),
1469 {:ok, _} <- ActivityPub.block(blocker, blocked) do
1470 blocked
1471 else
1472 err ->
1473 Logger.debug("blocks_import failed for #{blocked_identifier} with: #{inspect(err)}")
1474 err
1475 end
1476 end
1477 )
1478 end
1479
1480 def perform(:follow_import, %User{} = follower, followed_identifiers)
1481 when is_list(followed_identifiers) do
1482 Enum.map(
1483 followed_identifiers,
1484 fn followed_identifier ->
1485 with {:ok, %User{} = followed} <- get_or_fetch(followed_identifier),
1486 {:ok, follower} <- maybe_direct_follow(follower, followed),
1487 {:ok, _} <- ActivityPub.follow(follower, followed) do
1488 followed
1489 else
1490 err ->
1491 Logger.debug("follow_import failed for #{followed_identifier} with: #{inspect(err)}")
1492 err
1493 end
1494 end
1495 )
1496 end
1497
1498 @spec external_users_query() :: Ecto.Query.t()
1499 def external_users_query do
1500 User.Query.build(%{
1501 external: true,
1502 active: true,
1503 order_by: :id
1504 })
1505 end
1506
1507 @spec external_users(keyword()) :: [User.t()]
1508 def external_users(opts \\ []) do
1509 query =
1510 external_users_query()
1511 |> select([u], struct(u, [:id, :ap_id]))
1512
1513 query =
1514 if opts[:max_id],
1515 do: where(query, [u], u.id > ^opts[:max_id]),
1516 else: query
1517
1518 query =
1519 if opts[:limit],
1520 do: limit(query, ^opts[:limit]),
1521 else: query
1522
1523 Repo.all(query)
1524 end
1525
1526 def blocks_import(%User{} = blocker, blocked_identifiers) when is_list(blocked_identifiers) do
1527 BackgroundWorker.enqueue("blocks_import", %{
1528 "blocker_id" => blocker.id,
1529 "blocked_identifiers" => blocked_identifiers
1530 })
1531 end
1532
1533 def follow_import(%User{} = follower, followed_identifiers)
1534 when is_list(followed_identifiers) do
1535 BackgroundWorker.enqueue("follow_import", %{
1536 "follower_id" => follower.id,
1537 "followed_identifiers" => followed_identifiers
1538 })
1539 end
1540
1541 def delete_user_activities(%User{ap_id: ap_id} = user) do
1542 ap_id
1543 |> Activity.Queries.by_actor()
1544 |> RepoStreamer.chunk_stream(50)
1545 |> Stream.each(fn activities ->
1546 Enum.each(activities, fn activity -> delete_activity(activity, user) end)
1547 end)
1548 |> Stream.run()
1549 end
1550
1551 defp delete_activity(%{data: %{"type" => "Create", "object" => object}}, user) do
1552 {:ok, delete_data, _} = Builder.delete(user, object)
1553
1554 Pipeline.common_pipeline(delete_data, local: true)
1555 end
1556
1557 defp delete_activity(%{data: %{"type" => "Like"}} = activity, _user) do
1558 object = Object.normalize(activity)
1559
1560 activity.actor
1561 |> get_cached_by_ap_id()
1562 |> ActivityPub.unlike(object)
1563 end
1564
1565 defp delete_activity(%{data: %{"type" => "Announce"}} = activity, _user) do
1566 object = Object.normalize(activity)
1567
1568 activity.actor
1569 |> get_cached_by_ap_id()
1570 |> ActivityPub.unannounce(object)
1571 end
1572
1573 defp delete_activity(_activity, _user), do: "Doing nothing"
1574
1575 def html_filter_policy(%User{no_rich_text: true}) do
1576 Pleroma.HTML.Scrubber.TwitterText
1577 end
1578
1579 def html_filter_policy(_), do: Pleroma.Config.get([:markup, :scrub_policy])
1580
1581 def fetch_by_ap_id(ap_id), do: ActivityPub.make_user_from_ap_id(ap_id)
1582
1583 def get_or_fetch_by_ap_id(ap_id) do
1584 user = get_cached_by_ap_id(ap_id)
1585
1586 if !is_nil(user) and !needs_update?(user) do
1587 {:ok, user}
1588 else
1589 fetch_by_ap_id(ap_id)
1590 end
1591 end
1592
1593 @doc """
1594 Creates an internal service actor by URI if missing.
1595 Optionally takes nickname for addressing.
1596 """
1597 @spec get_or_create_service_actor_by_ap_id(String.t(), String.t()) :: User.t() | nil
1598 def get_or_create_service_actor_by_ap_id(uri, nickname) do
1599 {_, user} =
1600 case get_cached_by_ap_id(uri) do
1601 nil ->
1602 with {:error, %{errors: errors}} <- create_service_actor(uri, nickname) do
1603 Logger.error("Cannot create service actor: #{uri}/.\n#{inspect(errors)}")
1604 {:error, nil}
1605 end
1606
1607 %User{invisible: false} = user ->
1608 set_invisible(user)
1609
1610 user ->
1611 {:ok, user}
1612 end
1613
1614 user
1615 end
1616
1617 @spec set_invisible(User.t()) :: {:ok, User.t()}
1618 defp set_invisible(user) do
1619 user
1620 |> change(%{invisible: true})
1621 |> update_and_set_cache()
1622 end
1623
1624 @spec create_service_actor(String.t(), String.t()) ::
1625 {:ok, User.t()} | {:error, Ecto.Changeset.t()}
1626 defp create_service_actor(uri, nickname) do
1627 %User{
1628 invisible: true,
1629 local: true,
1630 ap_id: uri,
1631 nickname: nickname,
1632 follower_address: uri <> "/followers"
1633 }
1634 |> change
1635 |> unique_constraint(:nickname)
1636 |> Repo.insert()
1637 |> set_cache()
1638 end
1639
1640 def public_key(%{public_key: public_key_pem}) when is_binary(public_key_pem) do
1641 key =
1642 public_key_pem
1643 |> :public_key.pem_decode()
1644 |> hd()
1645 |> :public_key.pem_entry_decode()
1646
1647 {:ok, key}
1648 end
1649
1650 def public_key(_), do: {:error, "key not found"}
1651
1652 def get_public_key_for_ap_id(ap_id) do
1653 with {:ok, %User{} = user} <- get_or_fetch_by_ap_id(ap_id),
1654 {:ok, public_key} <- public_key(user) do
1655 {:ok, public_key}
1656 else
1657 _ -> :error
1658 end
1659 end
1660
1661 def ap_enabled?(%User{local: true}), do: true
1662 def ap_enabled?(%User{ap_enabled: ap_enabled}), do: ap_enabled
1663 def ap_enabled?(_), do: false
1664
1665 @doc "Gets or fetch a user by uri or nickname."
1666 @spec get_or_fetch(String.t()) :: {:ok, User.t()} | {:error, String.t()}
1667 def get_or_fetch("http" <> _host = uri), do: get_or_fetch_by_ap_id(uri)
1668 def get_or_fetch(nickname), do: get_or_fetch_by_nickname(nickname)
1669
1670 # wait a period of time and return newest version of the User structs
1671 # this is because we have synchronous follow APIs and need to simulate them
1672 # with an async handshake
1673 def wait_and_refresh(_, %User{local: true} = a, %User{local: true} = b) do
1674 with %User{} = a <- get_cached_by_id(a.id),
1675 %User{} = b <- get_cached_by_id(b.id) do
1676 {:ok, a, b}
1677 else
1678 nil -> :error
1679 end
1680 end
1681
1682 def wait_and_refresh(timeout, %User{} = a, %User{} = b) do
1683 with :ok <- :timer.sleep(timeout),
1684 %User{} = a <- get_cached_by_id(a.id),
1685 %User{} = b <- get_cached_by_id(b.id) do
1686 {:ok, a, b}
1687 else
1688 nil -> :error
1689 end
1690 end
1691
1692 def parse_bio(bio) when is_binary(bio) and bio != "" do
1693 bio
1694 |> CommonUtils.format_input("text/plain", mentions_format: :full)
1695 |> elem(0)
1696 end
1697
1698 def parse_bio(_), do: ""
1699
1700 def parse_bio(bio, user) when is_binary(bio) and bio != "" do
1701 # TODO: get profile URLs other than user.ap_id
1702 profile_urls = [user.ap_id]
1703
1704 bio
1705 |> CommonUtils.format_input("text/plain",
1706 mentions_format: :full,
1707 rel: &RelMe.maybe_put_rel_me(&1, profile_urls)
1708 )
1709 |> elem(0)
1710 end
1711
1712 def parse_bio(_, _), do: ""
1713
1714 def tag(user_identifiers, tags) when is_list(user_identifiers) do
1715 Repo.transaction(fn ->
1716 for user_identifier <- user_identifiers, do: tag(user_identifier, tags)
1717 end)
1718 end
1719
1720 def tag(nickname, tags) when is_binary(nickname),
1721 do: tag(get_by_nickname(nickname), tags)
1722
1723 def tag(%User{} = user, tags),
1724 do: update_tags(user, Enum.uniq((user.tags || []) ++ normalize_tags(tags)))
1725
1726 def untag(user_identifiers, tags) when is_list(user_identifiers) do
1727 Repo.transaction(fn ->
1728 for user_identifier <- user_identifiers, do: untag(user_identifier, tags)
1729 end)
1730 end
1731
1732 def untag(nickname, tags) when is_binary(nickname),
1733 do: untag(get_by_nickname(nickname), tags)
1734
1735 def untag(%User{} = user, tags),
1736 do: update_tags(user, (user.tags || []) -- normalize_tags(tags))
1737
1738 defp update_tags(%User{} = user, new_tags) do
1739 {:ok, updated_user} =
1740 user
1741 |> change(%{tags: new_tags})
1742 |> update_and_set_cache()
1743
1744 updated_user
1745 end
1746
1747 defp normalize_tags(tags) do
1748 [tags]
1749 |> List.flatten()
1750 |> Enum.map(&String.downcase/1)
1751 end
1752
1753 defp local_nickname_regex do
1754 if Pleroma.Config.get([:instance, :extended_nickname_format]) do
1755 @extended_local_nickname_regex
1756 else
1757 @strict_local_nickname_regex
1758 end
1759 end
1760
1761 def local_nickname(nickname_or_mention) do
1762 nickname_or_mention
1763 |> full_nickname()
1764 |> String.split("@")
1765 |> hd()
1766 end
1767
1768 def full_nickname(nickname_or_mention),
1769 do: String.trim_leading(nickname_or_mention, "@")
1770
1771 def error_user(ap_id) do
1772 %User{
1773 name: ap_id,
1774 ap_id: ap_id,
1775 nickname: "erroruser@example.com",
1776 inserted_at: NaiveDateTime.utc_now()
1777 }
1778 end
1779
1780 @spec all_superusers() :: [User.t()]
1781 def all_superusers do
1782 User.Query.build(%{super_users: true, local: true, deactivated: false})
1783 |> Repo.all()
1784 end
1785
1786 def muting_reblogs?(%User{} = user, %User{} = target) do
1787 UserRelationship.reblog_mute_exists?(user, target)
1788 end
1789
1790 def showing_reblogs?(%User{} = user, %User{} = target) do
1791 not muting_reblogs?(user, target)
1792 end
1793
1794 @doc """
1795 The function returns a query to get users with no activity for given interval of days.
1796 Inactive users are those who didn't read any notification, or had any activity where
1797 the user is the activity's actor, during `inactivity_threshold` days.
1798 Deactivated users will not appear in this list.
1799
1800 ## Examples
1801
1802 iex> Pleroma.User.list_inactive_users()
1803 %Ecto.Query{}
1804 """
1805 @spec list_inactive_users_query(integer()) :: Ecto.Query.t()
1806 def list_inactive_users_query(inactivity_threshold \\ 7) do
1807 negative_inactivity_threshold = -inactivity_threshold
1808 now = NaiveDateTime.truncate(NaiveDateTime.utc_now(), :second)
1809 # Subqueries are not supported in `where` clauses, join gets too complicated.
1810 has_read_notifications =
1811 from(n in Pleroma.Notification,
1812 where: n.seen == true,
1813 group_by: n.id,
1814 having: max(n.updated_at) > datetime_add(^now, ^negative_inactivity_threshold, "day"),
1815 select: n.user_id
1816 )
1817 |> Pleroma.Repo.all()
1818
1819 from(u in Pleroma.User,
1820 left_join: a in Pleroma.Activity,
1821 on: u.ap_id == a.actor,
1822 where: not is_nil(u.nickname),
1823 where: u.deactivated != ^true,
1824 where: u.id not in ^has_read_notifications,
1825 group_by: u.id,
1826 having:
1827 max(a.inserted_at) < datetime_add(^now, ^negative_inactivity_threshold, "day") or
1828 is_nil(max(a.inserted_at))
1829 )
1830 end
1831
1832 @doc """
1833 Enable or disable email notifications for user
1834
1835 ## Examples
1836
1837 iex> Pleroma.User.switch_email_notifications(Pleroma.User{email_notifications: %{"digest" => false}}, "digest", true)
1838 Pleroma.User{email_notifications: %{"digest" => true}}
1839
1840 iex> Pleroma.User.switch_email_notifications(Pleroma.User{email_notifications: %{"digest" => true}}, "digest", false)
1841 Pleroma.User{email_notifications: %{"digest" => false}}
1842 """
1843 @spec switch_email_notifications(t(), String.t(), boolean()) ::
1844 {:ok, t()} | {:error, Ecto.Changeset.t()}
1845 def switch_email_notifications(user, type, status) do
1846 User.update_email_notifications(user, %{type => status})
1847 end
1848
1849 @doc """
1850 Set `last_digest_emailed_at` value for the user to current time
1851 """
1852 @spec touch_last_digest_emailed_at(t()) :: t()
1853 def touch_last_digest_emailed_at(user) do
1854 now = NaiveDateTime.truncate(NaiveDateTime.utc_now(), :second)
1855
1856 {:ok, updated_user} =
1857 user
1858 |> change(%{last_digest_emailed_at: now})
1859 |> update_and_set_cache()
1860
1861 updated_user
1862 end
1863
1864 @spec toggle_confirmation(User.t()) :: {:ok, User.t()} | {:error, Changeset.t()}
1865 def toggle_confirmation(%User{} = user) do
1866 user
1867 |> confirmation_changeset(need_confirmation: !user.confirmation_pending)
1868 |> update_and_set_cache()
1869 end
1870
1871 @spec toggle_confirmation([User.t()]) :: [{:ok, User.t()} | {:error, Changeset.t()}]
1872 def toggle_confirmation(users) do
1873 Enum.map(users, &toggle_confirmation/1)
1874 end
1875
1876 def get_mascot(%{mascot: %{} = mascot}) when not is_nil(mascot) do
1877 mascot
1878 end
1879
1880 def get_mascot(%{mascot: mascot}) when is_nil(mascot) do
1881 # use instance-default
1882 config = Pleroma.Config.get([:assets, :mascots])
1883 default_mascot = Pleroma.Config.get([:assets, :default_mascot])
1884 mascot = Keyword.get(config, default_mascot)
1885
1886 %{
1887 "id" => "default-mascot",
1888 "url" => mascot[:url],
1889 "preview_url" => mascot[:url],
1890 "pleroma" => %{
1891 "mime_type" => mascot[:mime_type]
1892 }
1893 }
1894 end
1895
1896 def ensure_keys_present(%{keys: keys} = user) when not is_nil(keys), do: {:ok, user}
1897
1898 def ensure_keys_present(%User{} = user) do
1899 with {:ok, pem} <- Keys.generate_rsa_pem() do
1900 user
1901 |> cast(%{keys: pem}, [:keys])
1902 |> validate_required([:keys])
1903 |> update_and_set_cache()
1904 end
1905 end
1906
1907 def get_ap_ids_by_nicknames(nicknames) do
1908 from(u in User,
1909 where: u.nickname in ^nicknames,
1910 select: u.ap_id
1911 )
1912 |> Repo.all()
1913 end
1914
1915 defdelegate search(query, opts \\ []), to: User.Search
1916
1917 defp put_password_hash(
1918 %Ecto.Changeset{valid?: true, changes: %{password: password}} = changeset
1919 ) do
1920 change(changeset, password_hash: Pbkdf2.hashpwsalt(password))
1921 end
1922
1923 defp put_password_hash(changeset), do: changeset
1924
1925 def is_internal_user?(%User{nickname: nil}), do: true
1926 def is_internal_user?(%User{local: true, nickname: "internal." <> _}), do: true
1927 def is_internal_user?(_), do: false
1928
1929 # A hack because user delete activities have a fake id for whatever reason
1930 # TODO: Get rid of this
1931 def get_delivered_users_by_object_id("pleroma:fake_object_id"), do: []
1932
1933 def get_delivered_users_by_object_id(object_id) do
1934 from(u in User,
1935 inner_join: delivery in assoc(u, :deliveries),
1936 where: delivery.object_id == ^object_id
1937 )
1938 |> Repo.all()
1939 end
1940
1941 def change_email(user, email) do
1942 user
1943 |> cast(%{email: email}, [:email])
1944 |> validate_required([:email])
1945 |> unique_constraint(:email)
1946 |> validate_format(:email, @email_regex)
1947 |> update_and_set_cache()
1948 end
1949
1950 # Internal function; public one is `deactivate/2`
1951 defp set_activation_status(user, deactivated) do
1952 user
1953 |> cast(%{deactivated: deactivated}, [:deactivated])
1954 |> update_and_set_cache()
1955 end
1956
1957 def update_banner(user, banner) do
1958 user
1959 |> cast(%{banner: banner}, [:banner])
1960 |> update_and_set_cache()
1961 end
1962
1963 def update_background(user, background) do
1964 user
1965 |> cast(%{background: background}, [:background])
1966 |> update_and_set_cache()
1967 end
1968
1969 def roles(%{is_moderator: is_moderator, is_admin: is_admin}) do
1970 %{
1971 admin: is_admin,
1972 moderator: is_moderator
1973 }
1974 end
1975
1976 def validate_fields(changeset, remote? \\ false) do
1977 limit_name = if remote?, do: :max_remote_account_fields, else: :max_account_fields
1978 limit = Pleroma.Config.get([:instance, limit_name], 0)
1979
1980 changeset
1981 |> validate_length(:fields, max: limit)
1982 |> validate_change(:fields, fn :fields, fields ->
1983 if Enum.all?(fields, &valid_field?/1) do
1984 []
1985 else
1986 [fields: "invalid"]
1987 end
1988 end)
1989 end
1990
1991 defp valid_field?(%{"name" => name, "value" => value}) do
1992 name_limit = Pleroma.Config.get([:instance, :account_field_name_length], 255)
1993 value_limit = Pleroma.Config.get([:instance, :account_field_value_length], 255)
1994
1995 is_binary(name) && is_binary(value) && String.length(name) <= name_limit &&
1996 String.length(value) <= value_limit
1997 end
1998
1999 defp valid_field?(_), do: false
2000
2001 defp truncate_field(%{"name" => name, "value" => value}) do
2002 {name, _chopped} =
2003 String.split_at(name, Pleroma.Config.get([:instance, :account_field_name_length], 255))
2004
2005 {value, _chopped} =
2006 String.split_at(value, Pleroma.Config.get([:instance, :account_field_value_length], 255))
2007
2008 %{"name" => name, "value" => value}
2009 end
2010
2011 def admin_api_update(user, params) do
2012 user
2013 |> cast(params, [
2014 :is_moderator,
2015 :is_admin,
2016 :show_role
2017 ])
2018 |> update_and_set_cache()
2019 end
2020
2021 @doc "Signs user out of all applications"
2022 def global_sign_out(user) do
2023 OAuth.Authorization.delete_user_authorizations(user)
2024 OAuth.Token.delete_user_tokens(user)
2025 end
2026
2027 def mascot_update(user, url) do
2028 user
2029 |> cast(%{mascot: url}, [:mascot])
2030 |> validate_required([:mascot])
2031 |> update_and_set_cache()
2032 end
2033
2034 def mastodon_settings_update(user, settings) do
2035 user
2036 |> cast(%{settings: settings}, [:settings])
2037 |> validate_required([:settings])
2038 |> update_and_set_cache()
2039 end
2040
2041 @spec confirmation_changeset(User.t(), keyword()) :: Changeset.t()
2042 def confirmation_changeset(user, need_confirmation: need_confirmation?) do
2043 params =
2044 if need_confirmation? do
2045 %{
2046 confirmation_pending: true,
2047 confirmation_token: :crypto.strong_rand_bytes(32) |> Base.url_encode64()
2048 }
2049 else
2050 %{
2051 confirmation_pending: false,
2052 confirmation_token: nil
2053 }
2054 end
2055
2056 cast(user, params, [:confirmation_pending, :confirmation_token])
2057 end
2058
2059 def add_pinnned_activity(user, %Pleroma.Activity{id: id}) do
2060 if id not in user.pinned_activities do
2061 max_pinned_statuses = Pleroma.Config.get([:instance, :max_pinned_statuses], 0)
2062 params = %{pinned_activities: user.pinned_activities ++ [id]}
2063
2064 user
2065 |> cast(params, [:pinned_activities])
2066 |> validate_length(:pinned_activities,
2067 max: max_pinned_statuses,
2068 message: "You have already pinned the maximum number of statuses"
2069 )
2070 else
2071 change(user)
2072 end
2073 |> update_and_set_cache()
2074 end
2075
2076 def remove_pinnned_activity(user, %Pleroma.Activity{id: id}) do
2077 params = %{pinned_activities: List.delete(user.pinned_activities, id)}
2078
2079 user
2080 |> cast(params, [:pinned_activities])
2081 |> update_and_set_cache()
2082 end
2083
2084 def update_email_notifications(user, settings) do
2085 email_notifications =
2086 user.email_notifications
2087 |> Map.merge(settings)
2088 |> Map.take(["digest"])
2089
2090 params = %{email_notifications: email_notifications}
2091 fields = [:email_notifications]
2092
2093 user
2094 |> cast(params, fields)
2095 |> validate_required(fields)
2096 |> update_and_set_cache()
2097 end
2098
2099 defp set_domain_blocks(user, domain_blocks) do
2100 params = %{domain_blocks: domain_blocks}
2101
2102 user
2103 |> cast(params, [:domain_blocks])
2104 |> validate_required([:domain_blocks])
2105 |> update_and_set_cache()
2106 end
2107
2108 def block_domain(user, domain_blocked) do
2109 set_domain_blocks(user, Enum.uniq([domain_blocked | user.domain_blocks]))
2110 end
2111
2112 def unblock_domain(user, domain_blocked) do
2113 set_domain_blocks(user, List.delete(user.domain_blocks, domain_blocked))
2114 end
2115
2116 @spec add_to_block(User.t(), User.t()) ::
2117 {:ok, UserRelationship.t()} | {:error, Ecto.Changeset.t()}
2118 defp add_to_block(%User{} = user, %User{} = blocked) do
2119 UserRelationship.create_block(user, blocked)
2120 end
2121
2122 @spec add_to_block(User.t(), User.t()) ::
2123 {:ok, UserRelationship.t()} | {:ok, nil} | {:error, Ecto.Changeset.t()}
2124 defp remove_from_block(%User{} = user, %User{} = blocked) do
2125 UserRelationship.delete_block(user, blocked)
2126 end
2127
2128 defp add_to_mutes(%User{} = user, %User{} = muted_user, notifications?) do
2129 with {:ok, user_mute} <- UserRelationship.create_mute(user, muted_user),
2130 {:ok, user_notification_mute} <-
2131 (notifications? && UserRelationship.create_notification_mute(user, muted_user)) ||
2132 {:ok, nil} do
2133 {:ok, Enum.filter([user_mute, user_notification_mute], & &1)}
2134 end
2135 end
2136
2137 defp remove_from_mutes(user, %User{} = muted_user) do
2138 with {:ok, user_mute} <- UserRelationship.delete_mute(user, muted_user),
2139 {:ok, user_notification_mute} <-
2140 UserRelationship.delete_notification_mute(user, muted_user) do
2141 {:ok, [user_mute, user_notification_mute]}
2142 end
2143 end
2144
2145 def set_invisible(user, invisible) do
2146 params = %{invisible: invisible}
2147
2148 user
2149 |> cast(params, [:invisible])
2150 |> validate_required([:invisible])
2151 |> update_and_set_cache()
2152 end
2153
2154 def sanitize_html(%User{} = user) do
2155 sanitize_html(user, nil)
2156 end
2157
2158 # User data that mastodon isn't filtering (treated as plaintext):
2159 # - field name
2160 # - display name
2161 def sanitize_html(%User{} = user, filter) do
2162 fields =
2163 Enum.map(user.fields, fn %{"name" => name, "value" => value} ->
2164 %{
2165 "name" => name,
2166 "value" => HTML.filter_tags(value, Pleroma.HTML.Scrubber.LinksOnly)
2167 }
2168 end)
2169
2170 user
2171 |> Map.put(:bio, HTML.filter_tags(user.bio, filter))
2172 |> Map.put(:fields, fields)
2173 end
2174 end