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