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