Merge branch 'recipients-query-speedup' 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 =
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, actor: actor}) do
1203 to = [actor | to]
1204
1205 query = User.Query.build(%{recipients_from_activity: to, local: true, deactivated: false})
1206
1207 query
1208 |> Repo.all()
1209 end
1210
1211 @spec mute(User.t(), User.t(), boolean()) ::
1212 {:ok, list(UserRelationship.t())} | {:error, String.t()}
1213 def mute(%User{} = muter, %User{} = mutee, notifications? \\ true) do
1214 add_to_mutes(muter, mutee, notifications?)
1215 end
1216
1217 def unmute(%User{} = muter, %User{} = mutee) do
1218 remove_from_mutes(muter, mutee)
1219 end
1220
1221 def subscribe(%User{} = subscriber, %User{} = target) do
1222 deny_follow_blocked = Pleroma.Config.get([:user, :deny_follow_blocked])
1223
1224 if blocks?(target, subscriber) and deny_follow_blocked do
1225 {:error, "Could not subscribe: #{target.nickname} is blocking you"}
1226 else
1227 # Note: the relationship is inverse: subscriber acts as relationship target
1228 UserRelationship.create_inverse_subscription(target, subscriber)
1229 end
1230 end
1231
1232 def subscribe(%User{} = subscriber, %{ap_id: ap_id}) do
1233 with %User{} = subscribee <- get_cached_by_ap_id(ap_id) do
1234 subscribe(subscriber, subscribee)
1235 end
1236 end
1237
1238 def unsubscribe(%User{} = unsubscriber, %User{} = target) do
1239 # Note: the relationship is inverse: subscriber acts as relationship target
1240 UserRelationship.delete_inverse_subscription(target, unsubscriber)
1241 end
1242
1243 def unsubscribe(%User{} = unsubscriber, %{ap_id: ap_id}) do
1244 with %User{} = user <- get_cached_by_ap_id(ap_id) do
1245 unsubscribe(unsubscriber, user)
1246 end
1247 end
1248
1249 def block(%User{} = blocker, %User{} = blocked) do
1250 # sever any follow relationships to prevent leaks per activitypub (Pleroma issue #213)
1251 blocker =
1252 if following?(blocker, blocked) do
1253 {:ok, blocker, _} = unfollow(blocker, blocked)
1254 blocker
1255 else
1256 blocker
1257 end
1258
1259 # clear any requested follows as well
1260 blocked =
1261 case CommonAPI.reject_follow_request(blocked, blocker) do
1262 {:ok, %User{} = updated_blocked} -> updated_blocked
1263 nil -> blocked
1264 end
1265
1266 unsubscribe(blocked, blocker)
1267
1268 if following?(blocked, blocker), do: unfollow(blocked, blocker)
1269
1270 {:ok, blocker} = update_follower_count(blocker)
1271 {:ok, blocker, _} = Participation.mark_all_as_read(blocker, blocked)
1272 add_to_block(blocker, blocked)
1273 end
1274
1275 # helper to handle the block given only an actor's AP id
1276 def block(%User{} = blocker, %{ap_id: ap_id}) do
1277 block(blocker, get_cached_by_ap_id(ap_id))
1278 end
1279
1280 def unblock(%User{} = blocker, %User{} = blocked) do
1281 remove_from_block(blocker, blocked)
1282 end
1283
1284 # helper to handle the block given only an actor's AP id
1285 def unblock(%User{} = blocker, %{ap_id: ap_id}) do
1286 unblock(blocker, get_cached_by_ap_id(ap_id))
1287 end
1288
1289 def mutes?(nil, _), do: false
1290 def mutes?(%User{} = user, %User{} = target), do: mutes_user?(user, target)
1291
1292 def mutes_user?(%User{} = user, %User{} = target) do
1293 UserRelationship.mute_exists?(user, target)
1294 end
1295
1296 @spec muted_notifications?(User.t() | nil, User.t() | map()) :: boolean()
1297 def muted_notifications?(nil, _), do: false
1298
1299 def muted_notifications?(%User{} = user, %User{} = target),
1300 do: UserRelationship.notification_mute_exists?(user, target)
1301
1302 def blocks?(nil, _), do: false
1303
1304 def blocks?(%User{} = user, %User{} = target) do
1305 blocks_user?(user, target) ||
1306 (blocks_domain?(user, target) and not User.following?(user, target))
1307 end
1308
1309 def blocks_user?(%User{} = user, %User{} = target) do
1310 UserRelationship.block_exists?(user, target)
1311 end
1312
1313 def blocks_user?(_, _), do: false
1314
1315 def blocks_domain?(%User{} = user, %User{} = target) do
1316 domain_blocks = Pleroma.Web.ActivityPub.MRF.subdomains_regex(user.domain_blocks)
1317 %{host: host} = URI.parse(target.ap_id)
1318 Pleroma.Web.ActivityPub.MRF.subdomain_match?(domain_blocks, host)
1319 end
1320
1321 def blocks_domain?(_, _), do: false
1322
1323 def subscribed_to?(%User{} = user, %User{} = target) do
1324 # Note: the relationship is inverse: subscriber acts as relationship target
1325 UserRelationship.inverse_subscription_exists?(target, user)
1326 end
1327
1328 def subscribed_to?(%User{} = user, %{ap_id: ap_id}) do
1329 with %User{} = target <- get_cached_by_ap_id(ap_id) do
1330 subscribed_to?(user, target)
1331 end
1332 end
1333
1334 @doc """
1335 Returns map of outgoing (blocked, muted etc.) relationships' user AP IDs by relation type.
1336 E.g. `outgoing_relationships_ap_ids(user, [:block])` -> `%{block: ["https://some.site/users/userapid"]}`
1337 """
1338 @spec outgoing_relationships_ap_ids(User.t(), list(atom())) :: %{atom() => list(String.t())}
1339 def outgoing_relationships_ap_ids(_user, []), do: %{}
1340
1341 def outgoing_relationships_ap_ids(nil, _relationship_types), do: %{}
1342
1343 def outgoing_relationships_ap_ids(%User{} = user, relationship_types)
1344 when is_list(relationship_types) do
1345 db_result =
1346 user
1347 |> assoc(:outgoing_relationships)
1348 |> join(:inner, [user_rel], u in assoc(user_rel, :target))
1349 |> where([user_rel, u], user_rel.relationship_type in ^relationship_types)
1350 |> select([user_rel, u], [user_rel.relationship_type, fragment("array_agg(?)", u.ap_id)])
1351 |> group_by([user_rel, u], user_rel.relationship_type)
1352 |> Repo.all()
1353 |> Enum.into(%{}, fn [k, v] -> {k, v} end)
1354
1355 Enum.into(
1356 relationship_types,
1357 %{},
1358 fn rel_type -> {rel_type, db_result[rel_type] || []} end
1359 )
1360 end
1361
1362 def incoming_relationships_ungrouped_ap_ids(user, relationship_types, ap_ids \\ nil)
1363
1364 def incoming_relationships_ungrouped_ap_ids(_user, [], _ap_ids), do: []
1365
1366 def incoming_relationships_ungrouped_ap_ids(nil, _relationship_types, _ap_ids), do: []
1367
1368 def incoming_relationships_ungrouped_ap_ids(%User{} = user, relationship_types, ap_ids)
1369 when is_list(relationship_types) do
1370 user
1371 |> assoc(:incoming_relationships)
1372 |> join(:inner, [user_rel], u in assoc(user_rel, :source))
1373 |> where([user_rel, u], user_rel.relationship_type in ^relationship_types)
1374 |> maybe_filter_on_ap_id(ap_ids)
1375 |> select([user_rel, u], u.ap_id)
1376 |> distinct(true)
1377 |> Repo.all()
1378 end
1379
1380 defp maybe_filter_on_ap_id(query, ap_ids) when is_list(ap_ids) do
1381 where(query, [user_rel, u], u.ap_id in ^ap_ids)
1382 end
1383
1384 defp maybe_filter_on_ap_id(query, _ap_ids), do: query
1385
1386 def deactivate_async(user, status \\ true) do
1387 BackgroundWorker.enqueue("deactivate_user", %{"user_id" => user.id, "status" => status})
1388 end
1389
1390 def deactivate(user, status \\ true)
1391
1392 def deactivate(users, status) when is_list(users) do
1393 Repo.transaction(fn ->
1394 for user <- users, do: deactivate(user, status)
1395 end)
1396 end
1397
1398 def deactivate(%User{} = user, status) do
1399 with {:ok, user} <- set_activation_status(user, status) do
1400 user
1401 |> get_followers()
1402 |> Enum.filter(& &1.local)
1403 |> Enum.each(fn follower ->
1404 follower |> update_following_count() |> set_cache()
1405 end)
1406
1407 # Only update local user counts, remote will be update during the next pull.
1408 user
1409 |> get_friends()
1410 |> Enum.filter(& &1.local)
1411 |> Enum.each(&update_follower_count/1)
1412
1413 {:ok, user}
1414 end
1415 end
1416
1417 def update_notification_settings(%User{} = user, settings) do
1418 user
1419 |> cast(%{notification_settings: settings}, [])
1420 |> cast_embed(:notification_settings)
1421 |> validate_required([:notification_settings])
1422 |> update_and_set_cache()
1423 end
1424
1425 def delete(users) when is_list(users) do
1426 for user <- users, do: delete(user)
1427 end
1428
1429 def delete(%User{} = user) do
1430 BackgroundWorker.enqueue("delete_user", %{"user_id" => user.id})
1431 end
1432
1433 def perform(:force_password_reset, user), do: force_password_reset(user)
1434
1435 @spec perform(atom(), User.t()) :: {:ok, User.t()}
1436 def perform(:delete, %User{} = user) do
1437 {:ok, _user} = ActivityPub.delete(user)
1438
1439 # Remove all relationships
1440 user
1441 |> get_followers()
1442 |> Enum.each(fn follower ->
1443 ActivityPub.unfollow(follower, user)
1444 unfollow(follower, user)
1445 end)
1446
1447 user
1448 |> get_friends()
1449 |> Enum.each(fn followed ->
1450 ActivityPub.unfollow(user, followed)
1451 unfollow(user, followed)
1452 end)
1453
1454 delete_user_activities(user)
1455
1456 if user.local do
1457 user
1458 |> change(%{deactivated: true, email: nil})
1459 |> update_and_set_cache()
1460 else
1461 invalidate_cache(user)
1462 Repo.delete(user)
1463 end
1464 end
1465
1466 def perform(:deactivate_async, user, status), do: deactivate(user, status)
1467
1468 @spec perform(atom(), User.t(), list()) :: list() | {:error, any()}
1469 def perform(:blocks_import, %User{} = blocker, blocked_identifiers)
1470 when is_list(blocked_identifiers) do
1471 Enum.map(
1472 blocked_identifiers,
1473 fn blocked_identifier ->
1474 with {:ok, %User{} = blocked} <- get_or_fetch(blocked_identifier),
1475 {:ok, _user_block} <- block(blocker, blocked),
1476 {:ok, _} <- ActivityPub.block(blocker, blocked) do
1477 blocked
1478 else
1479 err ->
1480 Logger.debug("blocks_import failed for #{blocked_identifier} with: #{inspect(err)}")
1481 err
1482 end
1483 end
1484 )
1485 end
1486
1487 def perform(:follow_import, %User{} = follower, followed_identifiers)
1488 when is_list(followed_identifiers) do
1489 Enum.map(
1490 followed_identifiers,
1491 fn followed_identifier ->
1492 with {:ok, %User{} = followed} <- get_or_fetch(followed_identifier),
1493 {:ok, follower} <- maybe_direct_follow(follower, followed),
1494 {:ok, _} <- ActivityPub.follow(follower, followed) do
1495 followed
1496 else
1497 err ->
1498 Logger.debug("follow_import failed for #{followed_identifier} with: #{inspect(err)}")
1499 err
1500 end
1501 end
1502 )
1503 end
1504
1505 @spec external_users_query() :: Ecto.Query.t()
1506 def external_users_query do
1507 User.Query.build(%{
1508 external: true,
1509 active: true,
1510 order_by: :id
1511 })
1512 end
1513
1514 @spec external_users(keyword()) :: [User.t()]
1515 def external_users(opts \\ []) do
1516 query =
1517 external_users_query()
1518 |> select([u], struct(u, [:id, :ap_id]))
1519
1520 query =
1521 if opts[:max_id],
1522 do: where(query, [u], u.id > ^opts[:max_id]),
1523 else: query
1524
1525 query =
1526 if opts[:limit],
1527 do: limit(query, ^opts[:limit]),
1528 else: query
1529
1530 Repo.all(query)
1531 end
1532
1533 def blocks_import(%User{} = blocker, blocked_identifiers) when is_list(blocked_identifiers) do
1534 BackgroundWorker.enqueue("blocks_import", %{
1535 "blocker_id" => blocker.id,
1536 "blocked_identifiers" => blocked_identifiers
1537 })
1538 end
1539
1540 def follow_import(%User{} = follower, followed_identifiers)
1541 when is_list(followed_identifiers) do
1542 BackgroundWorker.enqueue("follow_import", %{
1543 "follower_id" => follower.id,
1544 "followed_identifiers" => followed_identifiers
1545 })
1546 end
1547
1548 def delete_user_activities(%User{ap_id: ap_id}) do
1549 ap_id
1550 |> Activity.Queries.by_actor()
1551 |> RepoStreamer.chunk_stream(50)
1552 |> Stream.each(fn activities -> Enum.each(activities, &delete_activity/1) end)
1553 |> Stream.run()
1554 end
1555
1556 defp delete_activity(%{data: %{"type" => "Create"}} = activity) do
1557 activity
1558 |> Object.normalize()
1559 |> ActivityPub.delete()
1560 end
1561
1562 defp delete_activity(%{data: %{"type" => "Like"}} = activity) do
1563 object = Object.normalize(activity)
1564
1565 activity.actor
1566 |> get_cached_by_ap_id()
1567 |> ActivityPub.unlike(object)
1568 end
1569
1570 defp delete_activity(%{data: %{"type" => "Announce"}} = activity) do
1571 object = Object.normalize(activity)
1572
1573 activity.actor
1574 |> get_cached_by_ap_id()
1575 |> ActivityPub.unannounce(object)
1576 end
1577
1578 defp delete_activity(_activity), do: "Doing nothing"
1579
1580 def html_filter_policy(%User{no_rich_text: true}) do
1581 Pleroma.HTML.Scrubber.TwitterText
1582 end
1583
1584 def html_filter_policy(_), do: Pleroma.Config.get([:markup, :scrub_policy])
1585
1586 def fetch_by_ap_id(ap_id), do: ActivityPub.make_user_from_ap_id(ap_id)
1587
1588 def get_or_fetch_by_ap_id(ap_id) do
1589 user = get_cached_by_ap_id(ap_id)
1590
1591 if !is_nil(user) and !needs_update?(user) do
1592 {:ok, user}
1593 else
1594 fetch_by_ap_id(ap_id)
1595 end
1596 end
1597
1598 @doc """
1599 Creates an internal service actor by URI if missing.
1600 Optionally takes nickname for addressing.
1601 """
1602 @spec get_or_create_service_actor_by_ap_id(String.t(), String.t()) :: User.t() | nil
1603 def get_or_create_service_actor_by_ap_id(uri, nickname) do
1604 {_, user} =
1605 case get_cached_by_ap_id(uri) do
1606 nil ->
1607 with {:error, %{errors: errors}} <- create_service_actor(uri, nickname) do
1608 Logger.error("Cannot create service actor: #{uri}/.\n#{inspect(errors)}")
1609 {:error, nil}
1610 end
1611
1612 %User{invisible: false} = user ->
1613 set_invisible(user)
1614
1615 user ->
1616 {:ok, user}
1617 end
1618
1619 user
1620 end
1621
1622 @spec set_invisible(User.t()) :: {:ok, User.t()}
1623 defp set_invisible(user) do
1624 user
1625 |> change(%{invisible: true})
1626 |> update_and_set_cache()
1627 end
1628
1629 @spec create_service_actor(String.t(), String.t()) ::
1630 {:ok, User.t()} | {:error, Ecto.Changeset.t()}
1631 defp create_service_actor(uri, nickname) do
1632 %User{
1633 invisible: true,
1634 local: true,
1635 ap_id: uri,
1636 nickname: nickname,
1637 follower_address: uri <> "/followers"
1638 }
1639 |> change
1640 |> unique_constraint(:nickname)
1641 |> Repo.insert()
1642 |> set_cache()
1643 end
1644
1645 # AP style
1646 def public_key(%{source_data: %{"publicKey" => %{"publicKeyPem" => public_key_pem}}}) do
1647 key =
1648 public_key_pem
1649 |> :public_key.pem_decode()
1650 |> hd()
1651 |> :public_key.pem_entry_decode()
1652
1653 {:ok, key}
1654 end
1655
1656 def public_key(_), do: {:error, "not found key"}
1657
1658 def get_public_key_for_ap_id(ap_id) do
1659 with {:ok, %User{} = user} <- get_or_fetch_by_ap_id(ap_id),
1660 {:ok, public_key} <- public_key(user) do
1661 {:ok, public_key}
1662 else
1663 _ -> :error
1664 end
1665 end
1666
1667 defp blank?(""), do: nil
1668 defp blank?(n), do: n
1669
1670 def insert_or_update_user(data) do
1671 data
1672 |> Map.put(:name, blank?(data[:name]) || data[:nickname])
1673 |> remote_user_creation()
1674 |> Repo.insert(on_conflict: {:replace_all_except, [:id]}, conflict_target: :nickname)
1675 |> set_cache()
1676 end
1677
1678 def ap_enabled?(%User{local: true}), do: true
1679 def ap_enabled?(%User{ap_enabled: ap_enabled}), do: ap_enabled
1680 def ap_enabled?(_), do: false
1681
1682 @doc "Gets or fetch a user by uri or nickname."
1683 @spec get_or_fetch(String.t()) :: {:ok, User.t()} | {:error, String.t()}
1684 def get_or_fetch("http" <> _host = uri), do: get_or_fetch_by_ap_id(uri)
1685 def get_or_fetch(nickname), do: get_or_fetch_by_nickname(nickname)
1686
1687 # wait a period of time and return newest version of the User structs
1688 # this is because we have synchronous follow APIs and need to simulate them
1689 # with an async handshake
1690 def wait_and_refresh(_, %User{local: true} = a, %User{local: true} = b) do
1691 with %User{} = a <- get_cached_by_id(a.id),
1692 %User{} = b <- get_cached_by_id(b.id) do
1693 {:ok, a, b}
1694 else
1695 nil -> :error
1696 end
1697 end
1698
1699 def wait_and_refresh(timeout, %User{} = a, %User{} = b) do
1700 with :ok <- :timer.sleep(timeout),
1701 %User{} = a <- get_cached_by_id(a.id),
1702 %User{} = b <- get_cached_by_id(b.id) do
1703 {:ok, a, b}
1704 else
1705 nil -> :error
1706 end
1707 end
1708
1709 def parse_bio(bio) when is_binary(bio) and bio != "" do
1710 bio
1711 |> CommonUtils.format_input("text/plain", mentions_format: :full)
1712 |> elem(0)
1713 end
1714
1715 def parse_bio(_), do: ""
1716
1717 def parse_bio(bio, user) when is_binary(bio) and bio != "" do
1718 # TODO: get profile URLs other than user.ap_id
1719 profile_urls = [user.ap_id]
1720
1721 bio
1722 |> CommonUtils.format_input("text/plain",
1723 mentions_format: :full,
1724 rel: &RelMe.maybe_put_rel_me(&1, profile_urls)
1725 )
1726 |> elem(0)
1727 end
1728
1729 def parse_bio(_, _), do: ""
1730
1731 def tag(user_identifiers, tags) when is_list(user_identifiers) do
1732 Repo.transaction(fn ->
1733 for user_identifier <- user_identifiers, do: tag(user_identifier, tags)
1734 end)
1735 end
1736
1737 def tag(nickname, tags) when is_binary(nickname),
1738 do: tag(get_by_nickname(nickname), tags)
1739
1740 def tag(%User{} = user, tags),
1741 do: update_tags(user, Enum.uniq((user.tags || []) ++ normalize_tags(tags)))
1742
1743 def untag(user_identifiers, tags) when is_list(user_identifiers) do
1744 Repo.transaction(fn ->
1745 for user_identifier <- user_identifiers, do: untag(user_identifier, tags)
1746 end)
1747 end
1748
1749 def untag(nickname, tags) when is_binary(nickname),
1750 do: untag(get_by_nickname(nickname), tags)
1751
1752 def untag(%User{} = user, tags),
1753 do: update_tags(user, (user.tags || []) -- normalize_tags(tags))
1754
1755 defp update_tags(%User{} = user, new_tags) do
1756 {:ok, updated_user} =
1757 user
1758 |> change(%{tags: new_tags})
1759 |> update_and_set_cache()
1760
1761 updated_user
1762 end
1763
1764 defp normalize_tags(tags) do
1765 [tags]
1766 |> List.flatten()
1767 |> Enum.map(&String.downcase/1)
1768 end
1769
1770 defp local_nickname_regex do
1771 if Pleroma.Config.get([:instance, :extended_nickname_format]) do
1772 @extended_local_nickname_regex
1773 else
1774 @strict_local_nickname_regex
1775 end
1776 end
1777
1778 def local_nickname(nickname_or_mention) do
1779 nickname_or_mention
1780 |> full_nickname()
1781 |> String.split("@")
1782 |> hd()
1783 end
1784
1785 def full_nickname(nickname_or_mention),
1786 do: String.trim_leading(nickname_or_mention, "@")
1787
1788 def error_user(ap_id) do
1789 %User{
1790 name: ap_id,
1791 ap_id: ap_id,
1792 nickname: "erroruser@example.com",
1793 inserted_at: NaiveDateTime.utc_now()
1794 }
1795 end
1796
1797 @spec all_superusers() :: [User.t()]
1798 def all_superusers do
1799 User.Query.build(%{super_users: true, local: true, deactivated: false})
1800 |> Repo.all()
1801 end
1802
1803 def showing_reblogs?(%User{} = user, %User{} = target) do
1804 not UserRelationship.reblog_mute_exists?(user, target)
1805 end
1806
1807 @doc """
1808 The function returns a query to get users with no activity for given interval of days.
1809 Inactive users are those who didn't read any notification, or had any activity where
1810 the user is the activity's actor, during `inactivity_threshold` days.
1811 Deactivated users will not appear in this list.
1812
1813 ## Examples
1814
1815 iex> Pleroma.User.list_inactive_users()
1816 %Ecto.Query{}
1817 """
1818 @spec list_inactive_users_query(integer()) :: Ecto.Query.t()
1819 def list_inactive_users_query(inactivity_threshold \\ 7) do
1820 negative_inactivity_threshold = -inactivity_threshold
1821 now = NaiveDateTime.truncate(NaiveDateTime.utc_now(), :second)
1822 # Subqueries are not supported in `where` clauses, join gets too complicated.
1823 has_read_notifications =
1824 from(n in Pleroma.Notification,
1825 where: n.seen == true,
1826 group_by: n.id,
1827 having: max(n.updated_at) > datetime_add(^now, ^negative_inactivity_threshold, "day"),
1828 select: n.user_id
1829 )
1830 |> Pleroma.Repo.all()
1831
1832 from(u in Pleroma.User,
1833 left_join: a in Pleroma.Activity,
1834 on: u.ap_id == a.actor,
1835 where: not is_nil(u.nickname),
1836 where: u.deactivated != ^true,
1837 where: u.id not in ^has_read_notifications,
1838 group_by: u.id,
1839 having:
1840 max(a.inserted_at) < datetime_add(^now, ^negative_inactivity_threshold, "day") or
1841 is_nil(max(a.inserted_at))
1842 )
1843 end
1844
1845 @doc """
1846 Enable or disable email notifications for user
1847
1848 ## Examples
1849
1850 iex> Pleroma.User.switch_email_notifications(Pleroma.User{email_notifications: %{"digest" => false}}, "digest", true)
1851 Pleroma.User{email_notifications: %{"digest" => true}}
1852
1853 iex> Pleroma.User.switch_email_notifications(Pleroma.User{email_notifications: %{"digest" => true}}, "digest", false)
1854 Pleroma.User{email_notifications: %{"digest" => false}}
1855 """
1856 @spec switch_email_notifications(t(), String.t(), boolean()) ::
1857 {:ok, t()} | {:error, Ecto.Changeset.t()}
1858 def switch_email_notifications(user, type, status) do
1859 User.update_email_notifications(user, %{type => status})
1860 end
1861
1862 @doc """
1863 Set `last_digest_emailed_at` value for the user to current time
1864 """
1865 @spec touch_last_digest_emailed_at(t()) :: t()
1866 def touch_last_digest_emailed_at(user) do
1867 now = NaiveDateTime.truncate(NaiveDateTime.utc_now(), :second)
1868
1869 {:ok, updated_user} =
1870 user
1871 |> change(%{last_digest_emailed_at: now})
1872 |> update_and_set_cache()
1873
1874 updated_user
1875 end
1876
1877 @spec toggle_confirmation(User.t()) :: {:ok, User.t()} | {:error, Changeset.t()}
1878 def toggle_confirmation(%User{} = user) do
1879 user
1880 |> confirmation_changeset(need_confirmation: !user.confirmation_pending)
1881 |> update_and_set_cache()
1882 end
1883
1884 @spec toggle_confirmation([User.t()]) :: [{:ok, User.t()} | {:error, Changeset.t()}]
1885 def toggle_confirmation(users) do
1886 Enum.map(users, &toggle_confirmation/1)
1887 end
1888
1889 def get_mascot(%{mascot: %{} = mascot}) when not is_nil(mascot) do
1890 mascot
1891 end
1892
1893 def get_mascot(%{mascot: mascot}) when is_nil(mascot) do
1894 # use instance-default
1895 config = Pleroma.Config.get([:assets, :mascots])
1896 default_mascot = Pleroma.Config.get([:assets, :default_mascot])
1897 mascot = Keyword.get(config, default_mascot)
1898
1899 %{
1900 "id" => "default-mascot",
1901 "url" => mascot[:url],
1902 "preview_url" => mascot[:url],
1903 "pleroma" => %{
1904 "mime_type" => mascot[:mime_type]
1905 }
1906 }
1907 end
1908
1909 def ensure_keys_present(%{keys: keys} = user) when not is_nil(keys), do: {:ok, user}
1910
1911 def ensure_keys_present(%User{} = user) do
1912 with {:ok, pem} <- Keys.generate_rsa_pem() do
1913 user
1914 |> cast(%{keys: pem}, [:keys])
1915 |> validate_required([:keys])
1916 |> update_and_set_cache()
1917 end
1918 end
1919
1920 def get_ap_ids_by_nicknames(nicknames) do
1921 from(u in User,
1922 where: u.nickname in ^nicknames,
1923 select: u.ap_id
1924 )
1925 |> Repo.all()
1926 end
1927
1928 defdelegate search(query, opts \\ []), to: User.Search
1929
1930 defp put_password_hash(
1931 %Ecto.Changeset{valid?: true, changes: %{password: password}} = changeset
1932 ) do
1933 change(changeset, password_hash: Pbkdf2.hashpwsalt(password))
1934 end
1935
1936 defp put_password_hash(changeset), do: changeset
1937
1938 def is_internal_user?(%User{nickname: nil}), do: true
1939 def is_internal_user?(%User{local: true, nickname: "internal." <> _}), do: true
1940 def is_internal_user?(_), do: false
1941
1942 # A hack because user delete activities have a fake id for whatever reason
1943 # TODO: Get rid of this
1944 def get_delivered_users_by_object_id("pleroma:fake_object_id"), do: []
1945
1946 def get_delivered_users_by_object_id(object_id) do
1947 from(u in User,
1948 inner_join: delivery in assoc(u, :deliveries),
1949 where: delivery.object_id == ^object_id
1950 )
1951 |> Repo.all()
1952 end
1953
1954 def change_email(user, email) do
1955 user
1956 |> cast(%{email: email}, [:email])
1957 |> validate_required([:email])
1958 |> unique_constraint(:email)
1959 |> validate_format(:email, @email_regex)
1960 |> update_and_set_cache()
1961 end
1962
1963 # Internal function; public one is `deactivate/2`
1964 defp set_activation_status(user, deactivated) do
1965 user
1966 |> cast(%{deactivated: deactivated}, [:deactivated])
1967 |> update_and_set_cache()
1968 end
1969
1970 def update_banner(user, banner) do
1971 user
1972 |> cast(%{banner: banner}, [:banner])
1973 |> update_and_set_cache()
1974 end
1975
1976 def update_background(user, background) do
1977 user
1978 |> cast(%{background: background}, [:background])
1979 |> update_and_set_cache()
1980 end
1981
1982 def update_source_data(user, source_data) do
1983 user
1984 |> cast(%{source_data: source_data}, [:source_data])
1985 |> update_and_set_cache()
1986 end
1987
1988 def roles(%{is_moderator: is_moderator, is_admin: is_admin}) do
1989 %{
1990 admin: is_admin,
1991 moderator: is_moderator
1992 }
1993 end
1994
1995 # ``fields`` is an array of mastodon profile field, containing ``{"name": "…", "value": "…"}``.
1996 # For example: [{"name": "Pronoun", "value": "she/her"}, …]
1997 def fields(%{fields: nil, source_data: %{"attachment" => attachment}}) do
1998 limit = Pleroma.Config.get([:instance, :max_remote_account_fields], 0)
1999
2000 attachment
2001 |> Enum.filter(fn %{"type" => t} -> t == "PropertyValue" end)
2002 |> Enum.map(fn fields -> Map.take(fields, ["name", "value"]) end)
2003 |> Enum.take(limit)
2004 end
2005
2006 def fields(%{fields: nil}), do: []
2007
2008 def fields(%{fields: fields}), do: fields
2009
2010 def validate_fields(changeset, remote? \\ false) do
2011 limit_name = if remote?, do: :max_remote_account_fields, else: :max_account_fields
2012 limit = Pleroma.Config.get([:instance, limit_name], 0)
2013
2014 changeset
2015 |> validate_length(:fields, max: limit)
2016 |> validate_change(:fields, fn :fields, fields ->
2017 if Enum.all?(fields, &valid_field?/1) do
2018 []
2019 else
2020 [fields: "invalid"]
2021 end
2022 end)
2023 end
2024
2025 defp valid_field?(%{"name" => name, "value" => value}) do
2026 name_limit = Pleroma.Config.get([:instance, :account_field_name_length], 255)
2027 value_limit = Pleroma.Config.get([:instance, :account_field_value_length], 255)
2028
2029 is_binary(name) && is_binary(value) && String.length(name) <= name_limit &&
2030 String.length(value) <= value_limit
2031 end
2032
2033 defp valid_field?(_), do: false
2034
2035 defp truncate_field(%{"name" => name, "value" => value}) do
2036 {name, _chopped} =
2037 String.split_at(name, Pleroma.Config.get([:instance, :account_field_name_length], 255))
2038
2039 {value, _chopped} =
2040 String.split_at(value, Pleroma.Config.get([:instance, :account_field_value_length], 255))
2041
2042 %{"name" => name, "value" => value}
2043 end
2044
2045 def admin_api_update(user, params) do
2046 user
2047 |> cast(params, [
2048 :is_moderator,
2049 :is_admin,
2050 :show_role
2051 ])
2052 |> update_and_set_cache()
2053 end
2054
2055 @doc "Signs user out of all applications"
2056 def global_sign_out(user) do
2057 OAuth.Authorization.delete_user_authorizations(user)
2058 OAuth.Token.delete_user_tokens(user)
2059 end
2060
2061 def mascot_update(user, url) do
2062 user
2063 |> cast(%{mascot: url}, [:mascot])
2064 |> validate_required([:mascot])
2065 |> update_and_set_cache()
2066 end
2067
2068 def mastodon_settings_update(user, settings) do
2069 user
2070 |> cast(%{settings: settings}, [:settings])
2071 |> validate_required([:settings])
2072 |> update_and_set_cache()
2073 end
2074
2075 @spec confirmation_changeset(User.t(), keyword()) :: Changeset.t()
2076 def confirmation_changeset(user, need_confirmation: need_confirmation?) do
2077 params =
2078 if need_confirmation? do
2079 %{
2080 confirmation_pending: true,
2081 confirmation_token: :crypto.strong_rand_bytes(32) |> Base.url_encode64()
2082 }
2083 else
2084 %{
2085 confirmation_pending: false,
2086 confirmation_token: nil
2087 }
2088 end
2089
2090 cast(user, params, [:confirmation_pending, :confirmation_token])
2091 end
2092
2093 def add_pinnned_activity(user, %Pleroma.Activity{id: id}) do
2094 if id not in user.pinned_activities do
2095 max_pinned_statuses = Pleroma.Config.get([:instance, :max_pinned_statuses], 0)
2096 params = %{pinned_activities: user.pinned_activities ++ [id]}
2097
2098 user
2099 |> cast(params, [:pinned_activities])
2100 |> validate_length(:pinned_activities,
2101 max: max_pinned_statuses,
2102 message: "You have already pinned the maximum number of statuses"
2103 )
2104 else
2105 change(user)
2106 end
2107 |> update_and_set_cache()
2108 end
2109
2110 def remove_pinnned_activity(user, %Pleroma.Activity{id: id}) do
2111 params = %{pinned_activities: List.delete(user.pinned_activities, id)}
2112
2113 user
2114 |> cast(params, [:pinned_activities])
2115 |> update_and_set_cache()
2116 end
2117
2118 def update_email_notifications(user, settings) do
2119 email_notifications =
2120 user.email_notifications
2121 |> Map.merge(settings)
2122 |> Map.take(["digest"])
2123
2124 params = %{email_notifications: email_notifications}
2125 fields = [:email_notifications]
2126
2127 user
2128 |> cast(params, fields)
2129 |> validate_required(fields)
2130 |> update_and_set_cache()
2131 end
2132
2133 defp set_domain_blocks(user, domain_blocks) do
2134 params = %{domain_blocks: domain_blocks}
2135
2136 user
2137 |> cast(params, [:domain_blocks])
2138 |> validate_required([:domain_blocks])
2139 |> update_and_set_cache()
2140 end
2141
2142 def block_domain(user, domain_blocked) do
2143 set_domain_blocks(user, Enum.uniq([domain_blocked | user.domain_blocks]))
2144 end
2145
2146 def unblock_domain(user, domain_blocked) do
2147 set_domain_blocks(user, List.delete(user.domain_blocks, domain_blocked))
2148 end
2149
2150 @spec add_to_block(User.t(), User.t()) ::
2151 {:ok, UserRelationship.t()} | {:error, Ecto.Changeset.t()}
2152 defp add_to_block(%User{} = user, %User{} = blocked) do
2153 UserRelationship.create_block(user, blocked)
2154 end
2155
2156 @spec add_to_block(User.t(), User.t()) ::
2157 {:ok, UserRelationship.t()} | {:ok, nil} | {:error, Ecto.Changeset.t()}
2158 defp remove_from_block(%User{} = user, %User{} = blocked) do
2159 UserRelationship.delete_block(user, blocked)
2160 end
2161
2162 defp add_to_mutes(%User{} = user, %User{} = muted_user, notifications?) do
2163 with {:ok, user_mute} <- UserRelationship.create_mute(user, muted_user),
2164 {:ok, user_notification_mute} <-
2165 (notifications? && UserRelationship.create_notification_mute(user, muted_user)) ||
2166 {:ok, nil} do
2167 {:ok, Enum.filter([user_mute, user_notification_mute], & &1)}
2168 end
2169 end
2170
2171 defp remove_from_mutes(user, %User{} = muted_user) do
2172 with {:ok, user_mute} <- UserRelationship.delete_mute(user, muted_user),
2173 {:ok, user_notification_mute} <-
2174 UserRelationship.delete_notification_mute(user, muted_user) do
2175 {:ok, [user_mute, user_notification_mute]}
2176 end
2177 end
2178
2179 def set_invisible(user, invisible) do
2180 params = %{invisible: invisible}
2181
2182 user
2183 |> cast(params, [:invisible])
2184 |> validate_required([:invisible])
2185 |> update_and_set_cache()
2186 end
2187
2188 def sanitize_html(%User{} = user) do
2189 sanitize_html(user, nil)
2190 end
2191
2192 # User data that mastodon isn't filtering (treated as plaintext):
2193 # - field name
2194 # - display name
2195 def sanitize_html(%User{} = user, filter) do
2196 fields =
2197 user
2198 |> User.fields()
2199 |> Enum.map(fn %{"name" => name, "value" => value} ->
2200 %{
2201 "name" => name,
2202 "value" => HTML.filter_tags(value, Pleroma.HTML.Scrubber.LinksOnly)
2203 }
2204 end)
2205
2206 user
2207 |> Map.put(:bio, HTML.filter_tags(user.bio, filter))
2208 |> Map.put(:fields, fields)
2209 end
2210 end