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