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