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