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