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