7a2558c29853b288eec587a66bce0145a92f805e
[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 if Config.get([:instance, :account_activation_required]) do
266 :confirmation_pending
267 else
268 :active
269 end
270 end
271
272 def account_status(%User{}), do: :active
273
274 @spec visible_for(User.t(), User.t() | nil) ::
275 boolean()
276 | :invisible
277 | :restricted_unauthenticated
278 | :deactivated
279 | :confirmation_pending
280 def visible_for(user, for_user \\ nil)
281
282 def visible_for(%User{invisible: true}, _), do: :invisible
283
284 def visible_for(%User{id: user_id}, %User{id: user_id}), do: true
285
286 def visible_for(%User{} = user, nil) do
287 if restrict_unauthenticated?(user) do
288 :restrict_unauthenticated
289 else
290 visible_account_status(user)
291 end
292 end
293
294 def visible_for(%User{} = user, for_user) do
295 superuser?(for_user) || visible_account_status(user)
296 end
297
298 def visible_for(_, _), do: false
299
300 defp restrict_unauthenticated?(%User{local: local}) do
301 config_key = if local, do: :local, else: :remote
302
303 Config.get([:restrict_unauthenticated, :profiles, config_key], false)
304 end
305
306 defp visible_account_status(user) do
307 status = account_status(user)
308 status in [:active, :password_reset_pending] || status
309 end
310
311 @spec superuser?(User.t()) :: boolean()
312 def superuser?(%User{local: true, is_admin: true}), do: true
313 def superuser?(%User{local: true, is_moderator: true}), do: true
314 def superuser?(_), do: false
315
316 @spec invisible?(User.t()) :: boolean()
317 def invisible?(%User{invisible: true}), do: true
318 def invisible?(_), do: false
319
320 def avatar_url(user, options \\ []) do
321 case user.avatar do
322 %{"url" => [%{"href" => href} | _]} -> href
323 _ -> !options[:no_default] && "#{Web.base_url()}/images/avi.png"
324 end
325 end
326
327 def banner_url(user, options \\ []) do
328 case user.banner do
329 %{"url" => [%{"href" => href} | _]} -> href
330 _ -> !options[:no_default] && "#{Web.base_url()}/images/banner.png"
331 end
332 end
333
334 # Should probably be renamed or removed
335 def ap_id(%User{nickname: nickname}), do: "#{Web.base_url()}/users/#{nickname}"
336
337 def ap_followers(%User{follower_address: fa}) when is_binary(fa), do: fa
338 def ap_followers(%User{} = user), do: "#{ap_id(user)}/followers"
339
340 @spec ap_following(User.t()) :: String.t()
341 def ap_following(%User{following_address: fa}) when is_binary(fa), do: fa
342 def ap_following(%User{} = user), do: "#{ap_id(user)}/following"
343
344 @spec restrict_deactivated(Ecto.Query.t()) :: Ecto.Query.t()
345 def restrict_deactivated(query) do
346 from(u in query, where: u.deactivated != ^true)
347 end
348
349 defdelegate following_count(user), to: FollowingRelationship
350
351 defp truncate_fields_param(params) do
352 if Map.has_key?(params, :fields) do
353 Map.put(params, :fields, Enum.map(params[:fields], &truncate_field/1))
354 else
355 params
356 end
357 end
358
359 defp truncate_if_exists(params, key, max_length) do
360 if Map.has_key?(params, key) and is_binary(params[key]) do
361 {value, _chopped} = String.split_at(params[key], max_length)
362 Map.put(params, key, value)
363 else
364 params
365 end
366 end
367
368 defp fix_follower_address(%{follower_address: _, following_address: _} = params), do: params
369
370 defp fix_follower_address(%{nickname: nickname} = params),
371 do: Map.put(params, :follower_address, ap_followers(%User{nickname: nickname}))
372
373 defp fix_follower_address(params), do: params
374
375 def remote_user_changeset(struct \\ %User{local: false}, params) do
376 bio_limit = Pleroma.Config.get([:instance, :user_bio_length], 5000)
377 name_limit = Pleroma.Config.get([:instance, :user_name_length], 100)
378
379 name =
380 case params[:name] do
381 name when is_binary(name) and byte_size(name) > 0 -> name
382 _ -> params[:nickname]
383 end
384
385 params =
386 params
387 |> Map.put(:name, name)
388 |> Map.put_new(:last_refreshed_at, NaiveDateTime.utc_now())
389 |> truncate_if_exists(:name, name_limit)
390 |> truncate_if_exists(:bio, bio_limit)
391 |> truncate_fields_param()
392 |> fix_follower_address()
393
394 struct
395 |> cast(
396 params,
397 [
398 :bio,
399 :name,
400 :emoji,
401 :ap_id,
402 :inbox,
403 :shared_inbox,
404 :nickname,
405 :public_key,
406 :avatar,
407 :ap_enabled,
408 :banner,
409 :locked,
410 :last_refreshed_at,
411 :uri,
412 :follower_address,
413 :following_address,
414 :hide_followers,
415 :hide_follows,
416 :hide_followers_count,
417 :hide_follows_count,
418 :follower_count,
419 :fields,
420 :following_count,
421 :discoverable,
422 :invisible,
423 :actor_type,
424 :also_known_as
425 ]
426 )
427 |> validate_required([:name, :ap_id])
428 |> unique_constraint(:nickname)
429 |> validate_format(:nickname, @email_regex)
430 |> validate_length(:bio, max: bio_limit)
431 |> validate_length(:name, max: name_limit)
432 |> validate_fields(true)
433 end
434
435 def update_changeset(struct, params \\ %{}) do
436 bio_limit = Pleroma.Config.get([:instance, :user_bio_length], 5000)
437 name_limit = Pleroma.Config.get([:instance, :user_name_length], 100)
438
439 struct
440 |> cast(
441 params,
442 [
443 :bio,
444 :name,
445 :emoji,
446 :avatar,
447 :public_key,
448 :inbox,
449 :shared_inbox,
450 :locked,
451 :no_rich_text,
452 :default_scope,
453 :banner,
454 :hide_follows,
455 :hide_followers,
456 :hide_followers_count,
457 :hide_follows_count,
458 :hide_favorites,
459 :allow_following_move,
460 :background,
461 :show_role,
462 :skip_thread_containment,
463 :fields,
464 :raw_fields,
465 :pleroma_settings_store,
466 :discoverable,
467 :actor_type,
468 :also_known_as
469 ]
470 )
471 |> unique_constraint(:nickname)
472 |> validate_format(:nickname, local_nickname_regex())
473 |> validate_length(:bio, max: bio_limit)
474 |> validate_length(:name, min: 1, max: name_limit)
475 |> put_fields()
476 |> put_emoji()
477 |> put_change_if_present(:bio, &{:ok, parse_bio(&1, struct)})
478 |> put_change_if_present(:avatar, &put_upload(&1, :avatar))
479 |> put_change_if_present(:banner, &put_upload(&1, :banner))
480 |> put_change_if_present(:background, &put_upload(&1, :background))
481 |> put_change_if_present(
482 :pleroma_settings_store,
483 &{:ok, Map.merge(struct.pleroma_settings_store, &1)}
484 )
485 |> validate_fields(false)
486 end
487
488 defp put_fields(changeset) do
489 if raw_fields = get_change(changeset, :raw_fields) do
490 raw_fields =
491 raw_fields
492 |> Enum.filter(fn %{"name" => n} -> n != "" end)
493
494 fields =
495 raw_fields
496 |> Enum.map(fn f -> Map.update!(f, "value", &parse_fields(&1)) end)
497
498 changeset
499 |> put_change(:raw_fields, raw_fields)
500 |> put_change(:fields, fields)
501 else
502 changeset
503 end
504 end
505
506 defp parse_fields(value) do
507 value
508 |> Formatter.linkify(mentions_format: :full)
509 |> elem(0)
510 end
511
512 defp put_emoji(changeset) do
513 bio = get_change(changeset, :bio)
514 name = get_change(changeset, :name)
515
516 if bio || name do
517 emoji = Map.merge(Emoji.Formatter.get_emoji_map(bio), Emoji.Formatter.get_emoji_map(name))
518 put_change(changeset, :emoji, emoji)
519 else
520 changeset
521 end
522 end
523
524 defp put_change_if_present(changeset, map_field, value_function) do
525 if value = get_change(changeset, map_field) do
526 with {:ok, new_value} <- value_function.(value) do
527 put_change(changeset, map_field, new_value)
528 else
529 _ -> changeset
530 end
531 else
532 changeset
533 end
534 end
535
536 defp put_upload(value, type) do
537 with %Plug.Upload{} <- value,
538 {:ok, object} <- ActivityPub.upload(value, type: type) do
539 {:ok, object.data}
540 end
541 end
542
543 def update_as_admin_changeset(struct, params) do
544 struct
545 |> update_changeset(params)
546 |> cast(params, [:email])
547 |> delete_change(:also_known_as)
548 |> unique_constraint(:email)
549 |> validate_format(:email, @email_regex)
550 end
551
552 @spec update_as_admin(%User{}, map) :: {:ok, User.t()} | {:error, Ecto.Changeset.t()}
553 def update_as_admin(user, params) do
554 params = Map.put(params, "password_confirmation", params["password"])
555 changeset = update_as_admin_changeset(user, params)
556
557 if params["password"] do
558 reset_password(user, changeset, params)
559 else
560 User.update_and_set_cache(changeset)
561 end
562 end
563
564 def password_update_changeset(struct, params) do
565 struct
566 |> cast(params, [:password, :password_confirmation])
567 |> validate_required([:password, :password_confirmation])
568 |> validate_confirmation(:password)
569 |> put_password_hash()
570 |> put_change(:password_reset_pending, false)
571 end
572
573 @spec reset_password(User.t(), map) :: {:ok, User.t()} | {:error, Ecto.Changeset.t()}
574 def reset_password(%User{} = user, params) do
575 reset_password(user, user, params)
576 end
577
578 def reset_password(%User{id: user_id} = user, struct, params) do
579 multi =
580 Multi.new()
581 |> Multi.update(:user, password_update_changeset(struct, params))
582 |> Multi.delete_all(:tokens, OAuth.Token.Query.get_by_user(user_id))
583 |> Multi.delete_all(:auth, OAuth.Authorization.delete_by_user_query(user))
584
585 case Repo.transaction(multi) do
586 {:ok, %{user: user} = _} -> set_cache(user)
587 {:error, _, changeset, _} -> {:error, changeset}
588 end
589 end
590
591 def update_password_reset_pending(user, value) do
592 user
593 |> change()
594 |> put_change(:password_reset_pending, value)
595 |> update_and_set_cache()
596 end
597
598 def force_password_reset_async(user) do
599 BackgroundWorker.enqueue("force_password_reset", %{"user_id" => user.id})
600 end
601
602 @spec force_password_reset(User.t()) :: {:ok, User.t()} | {:error, Ecto.Changeset.t()}
603 def force_password_reset(user), do: update_password_reset_pending(user, true)
604
605 def register_changeset(struct, params \\ %{}, opts \\ []) do
606 bio_limit = Pleroma.Config.get([:instance, :user_bio_length], 5000)
607 name_limit = Pleroma.Config.get([:instance, :user_name_length], 100)
608
609 need_confirmation? =
610 if is_nil(opts[:need_confirmation]) do
611 Pleroma.Config.get([:instance, :account_activation_required])
612 else
613 opts[:need_confirmation]
614 end
615
616 struct
617 |> confirmation_changeset(need_confirmation: need_confirmation?)
618 |> cast(params, [:bio, :email, :name, :nickname, :password, :password_confirmation, :emoji])
619 |> validate_required([:name, :nickname, :password, :password_confirmation])
620 |> validate_confirmation(:password)
621 |> unique_constraint(:email)
622 |> unique_constraint(:nickname)
623 |> validate_exclusion(:nickname, Pleroma.Config.get([User, :restricted_nicknames]))
624 |> validate_format(:nickname, local_nickname_regex())
625 |> validate_format(:email, @email_regex)
626 |> validate_length(:bio, max: bio_limit)
627 |> validate_length(:name, min: 1, max: name_limit)
628 |> maybe_validate_required_email(opts[:external])
629 |> put_password_hash
630 |> put_ap_id()
631 |> unique_constraint(:ap_id)
632 |> put_following_and_follower_address()
633 end
634
635 def maybe_validate_required_email(changeset, true), do: changeset
636
637 def maybe_validate_required_email(changeset, _) do
638 if Pleroma.Config.get([:instance, :account_activation_required]) do
639 validate_required(changeset, [:email])
640 else
641 changeset
642 end
643 end
644
645 defp put_ap_id(changeset) do
646 ap_id = ap_id(%User{nickname: get_field(changeset, :nickname)})
647 put_change(changeset, :ap_id, ap_id)
648 end
649
650 defp put_following_and_follower_address(changeset) do
651 followers = ap_followers(%User{nickname: get_field(changeset, :nickname)})
652
653 changeset
654 |> put_change(:follower_address, followers)
655 end
656
657 defp autofollow_users(user) do
658 candidates = Pleroma.Config.get([:instance, :autofollowed_nicknames])
659
660 autofollowed_users =
661 User.Query.build(%{nickname: candidates, local: true, deactivated: false})
662 |> Repo.all()
663
664 follow_all(user, autofollowed_users)
665 end
666
667 @doc "Inserts provided changeset, performs post-registration actions (confirmation email sending etc.)"
668 def register(%Ecto.Changeset{} = changeset) do
669 with {:ok, user} <- Repo.insert(changeset) do
670 post_register_action(user)
671 end
672 end
673
674 def post_register_action(%User{} = user) do
675 with {:ok, user} <- autofollow_users(user),
676 {:ok, user} <- set_cache(user),
677 {:ok, _} <- User.WelcomeMessage.post_welcome_message_to_user(user),
678 {:ok, _} <- try_send_confirmation_email(user) do
679 {:ok, user}
680 end
681 end
682
683 def try_send_confirmation_email(%User{} = user) do
684 if user.confirmation_pending &&
685 Pleroma.Config.get([:instance, :account_activation_required]) do
686 user
687 |> Pleroma.Emails.UserEmail.account_confirmation_email()
688 |> Pleroma.Emails.Mailer.deliver_async()
689
690 {:ok, :enqueued}
691 else
692 {:ok, :noop}
693 end
694 end
695
696 def try_send_confirmation_email(users) do
697 Enum.each(users, &try_send_confirmation_email/1)
698 end
699
700 def needs_update?(%User{local: true}), do: false
701
702 def needs_update?(%User{local: false, last_refreshed_at: nil}), do: true
703
704 def needs_update?(%User{local: false} = user) do
705 NaiveDateTime.diff(NaiveDateTime.utc_now(), user.last_refreshed_at) >= 86_400
706 end
707
708 def needs_update?(_), do: true
709
710 @spec maybe_direct_follow(User.t(), User.t()) :: {:ok, User.t()} | {:error, String.t()}
711
712 # "Locked" (self-locked) users demand explicit authorization of follow requests
713 def maybe_direct_follow(%User{} = follower, %User{local: true, locked: true} = followed) do
714 follow(follower, followed, :follow_pending)
715 end
716
717 def maybe_direct_follow(%User{} = follower, %User{local: true} = followed) do
718 follow(follower, followed)
719 end
720
721 def maybe_direct_follow(%User{} = follower, %User{} = followed) do
722 if not ap_enabled?(followed) do
723 follow(follower, followed)
724 else
725 {:ok, follower}
726 end
727 end
728
729 @doc "A mass follow for local users. Respects blocks in both directions but does not create activities."
730 @spec follow_all(User.t(), list(User.t())) :: {atom(), User.t()}
731 def follow_all(follower, followeds) do
732 followeds
733 |> Enum.reject(fn followed -> blocks?(follower, followed) || blocks?(followed, follower) end)
734 |> Enum.each(&follow(follower, &1, :follow_accept))
735
736 set_cache(follower)
737 end
738
739 defdelegate following(user), to: FollowingRelationship
740
741 def follow(%User{} = follower, %User{} = followed, state \\ :follow_accept) do
742 deny_follow_blocked = Pleroma.Config.get([:user, :deny_follow_blocked])
743
744 cond do
745 followed.deactivated ->
746 {:error, "Could not follow user: #{followed.nickname} is deactivated."}
747
748 deny_follow_blocked and blocks?(followed, follower) ->
749 {:error, "Could not follow user: #{followed.nickname} blocked you."}
750
751 true ->
752 FollowingRelationship.follow(follower, followed, state)
753
754 {:ok, _} = update_follower_count(followed)
755
756 follower
757 |> update_following_count()
758 |> set_cache()
759 end
760 end
761
762 def unfollow(%User{ap_id: ap_id}, %User{ap_id: ap_id}) do
763 {:error, "Not subscribed!"}
764 end
765
766 def unfollow(%User{} = follower, %User{} = followed) do
767 case get_follow_state(follower, followed) do
768 state when state in [:follow_pending, :follow_accept] ->
769 FollowingRelationship.unfollow(follower, followed)
770 {:ok, followed} = update_follower_count(followed)
771
772 {:ok, follower} =
773 follower
774 |> update_following_count()
775 |> set_cache()
776
777 {:ok, follower, Utils.fetch_latest_follow(follower, followed)}
778
779 nil ->
780 {:error, "Not subscribed!"}
781 end
782 end
783
784 defdelegate following?(follower, followed), to: FollowingRelationship
785
786 @doc "Returns follow state as Pleroma.FollowingRelationship.State value"
787 def get_follow_state(%User{} = follower, %User{} = following) do
788 following_relationship = FollowingRelationship.get(follower, following)
789 get_follow_state(follower, following, following_relationship)
790 end
791
792 def get_follow_state(
793 %User{} = follower,
794 %User{} = following,
795 following_relationship
796 ) do
797 case {following_relationship, following.local} do
798 {nil, false} ->
799 case Utils.fetch_latest_follow(follower, following) do
800 %Activity{data: %{"state" => state}} when state in ["pending", "accept"] ->
801 FollowingRelationship.state_to_enum(state)
802
803 _ ->
804 nil
805 end
806
807 {%{state: state}, _} ->
808 state
809
810 {nil, _} ->
811 nil
812 end
813 end
814
815 def locked?(%User{} = user) do
816 user.locked || false
817 end
818
819 def get_by_id(id) do
820 Repo.get_by(User, id: id)
821 end
822
823 def get_by_ap_id(ap_id) do
824 Repo.get_by(User, ap_id: ap_id)
825 end
826
827 def get_all_by_ap_id(ap_ids) do
828 from(u in __MODULE__,
829 where: u.ap_id in ^ap_ids
830 )
831 |> Repo.all()
832 end
833
834 def get_all_by_ids(ids) do
835 from(u in __MODULE__, where: u.id in ^ids)
836 |> Repo.all()
837 end
838
839 # This is mostly an SPC migration fix. This guesses the user nickname by taking the last part
840 # of the ap_id and the domain and tries to get that user
841 def get_by_guessed_nickname(ap_id) do
842 domain = URI.parse(ap_id).host
843 name = List.last(String.split(ap_id, "/"))
844 nickname = "#{name}@#{domain}"
845
846 get_cached_by_nickname(nickname)
847 end
848
849 def set_cache({:ok, user}), do: set_cache(user)
850 def set_cache({:error, err}), do: {:error, err}
851
852 def set_cache(%User{} = user) do
853 Cachex.put(:user_cache, "ap_id:#{user.ap_id}", user)
854 Cachex.put(:user_cache, "nickname:#{user.nickname}", user)
855 Cachex.put(:user_cache, "friends_ap_ids:#{user.nickname}", get_user_friends_ap_ids(user))
856 {:ok, user}
857 end
858
859 def update_and_set_cache(struct, params) do
860 struct
861 |> update_changeset(params)
862 |> update_and_set_cache()
863 end
864
865 def update_and_set_cache(changeset) do
866 with {:ok, user} <- Repo.update(changeset, stale_error_field: :id) do
867 set_cache(user)
868 end
869 end
870
871 def get_user_friends_ap_ids(user) do
872 from(u in User.get_friends_query(user), select: u.ap_id)
873 |> Repo.all()
874 end
875
876 @spec get_cached_user_friends_ap_ids(User.t()) :: [String.t()]
877 def get_cached_user_friends_ap_ids(user) do
878 Cachex.fetch!(:user_cache, "friends_ap_ids:#{user.ap_id}", fn _ ->
879 get_user_friends_ap_ids(user)
880 end)
881 end
882
883 def invalidate_cache(user) do
884 Cachex.del(:user_cache, "ap_id:#{user.ap_id}")
885 Cachex.del(:user_cache, "nickname:#{user.nickname}")
886 Cachex.del(:user_cache, "friends_ap_ids:#{user.ap_id}")
887 end
888
889 @spec get_cached_by_ap_id(String.t()) :: User.t() | nil
890 def get_cached_by_ap_id(ap_id) do
891 key = "ap_id:#{ap_id}"
892
893 with {:ok, nil} <- Cachex.get(:user_cache, key),
894 user when not is_nil(user) <- get_by_ap_id(ap_id),
895 {:ok, true} <- Cachex.put(:user_cache, key, user) do
896 user
897 else
898 {:ok, user} -> user
899 nil -> nil
900 end
901 end
902
903 def get_cached_by_id(id) do
904 key = "id:#{id}"
905
906 ap_id =
907 Cachex.fetch!(:user_cache, key, fn _ ->
908 user = get_by_id(id)
909
910 if user do
911 Cachex.put(:user_cache, "ap_id:#{user.ap_id}", user)
912 {:commit, user.ap_id}
913 else
914 {:ignore, ""}
915 end
916 end)
917
918 get_cached_by_ap_id(ap_id)
919 end
920
921 def get_cached_by_nickname(nickname) do
922 key = "nickname:#{nickname}"
923
924 Cachex.fetch!(:user_cache, key, fn ->
925 case get_or_fetch_by_nickname(nickname) do
926 {:ok, user} -> {:commit, user}
927 {:error, _error} -> {:ignore, nil}
928 end
929 end)
930 end
931
932 def get_cached_by_nickname_or_id(nickname_or_id, opts \\ []) do
933 restrict_to_local = Pleroma.Config.get([:instance, :limit_to_local_content])
934
935 cond do
936 is_integer(nickname_or_id) or FlakeId.flake_id?(nickname_or_id) ->
937 get_cached_by_id(nickname_or_id) || get_cached_by_nickname(nickname_or_id)
938
939 restrict_to_local == false or not String.contains?(nickname_or_id, "@") ->
940 get_cached_by_nickname(nickname_or_id)
941
942 restrict_to_local == :unauthenticated and match?(%User{}, opts[:for]) ->
943 get_cached_by_nickname(nickname_or_id)
944
945 true ->
946 nil
947 end
948 end
949
950 @spec get_by_nickname(String.t()) :: User.t() | nil
951 def get_by_nickname(nickname) do
952 Repo.get_by(User, nickname: nickname) ||
953 if Regex.match?(~r(@#{Pleroma.Web.Endpoint.host()})i, nickname) do
954 Repo.get_by(User, nickname: local_nickname(nickname))
955 end
956 end
957
958 def get_by_email(email), do: Repo.get_by(User, email: email)
959
960 def get_by_nickname_or_email(nickname_or_email) do
961 get_by_nickname(nickname_or_email) || get_by_email(nickname_or_email)
962 end
963
964 def fetch_by_nickname(nickname), do: ActivityPub.make_user_from_nickname(nickname)
965
966 def get_or_fetch_by_nickname(nickname) do
967 with %User{} = user <- get_by_nickname(nickname) do
968 {:ok, user}
969 else
970 _e ->
971 with [_nick, _domain] <- String.split(nickname, "@"),
972 {:ok, user} <- fetch_by_nickname(nickname) do
973 {:ok, user}
974 else
975 _e -> {:error, "not found " <> nickname}
976 end
977 end
978 end
979
980 @spec get_followers_query(User.t(), pos_integer() | nil) :: Ecto.Query.t()
981 def get_followers_query(%User{} = user, nil) do
982 User.Query.build(%{followers: user, deactivated: false})
983 end
984
985 def get_followers_query(user, page) do
986 user
987 |> get_followers_query(nil)
988 |> User.Query.paginate(page, 20)
989 end
990
991 @spec get_followers_query(User.t()) :: Ecto.Query.t()
992 def get_followers_query(user), do: get_followers_query(user, nil)
993
994 @spec get_followers(User.t(), pos_integer() | nil) :: {:ok, list(User.t())}
995 def get_followers(user, page \\ nil) do
996 user
997 |> get_followers_query(page)
998 |> Repo.all()
999 end
1000
1001 @spec get_external_followers(User.t(), pos_integer() | nil) :: {:ok, list(User.t())}
1002 def get_external_followers(user, page \\ nil) do
1003 user
1004 |> get_followers_query(page)
1005 |> User.Query.build(%{external: true})
1006 |> Repo.all()
1007 end
1008
1009 def get_followers_ids(user, page \\ nil) do
1010 user
1011 |> get_followers_query(page)
1012 |> select([u], u.id)
1013 |> Repo.all()
1014 end
1015
1016 @spec get_friends_query(User.t(), pos_integer() | nil) :: Ecto.Query.t()
1017 def get_friends_query(%User{} = user, nil) do
1018 User.Query.build(%{friends: user, deactivated: false})
1019 end
1020
1021 def get_friends_query(user, page) do
1022 user
1023 |> get_friends_query(nil)
1024 |> User.Query.paginate(page, 20)
1025 end
1026
1027 @spec get_friends_query(User.t()) :: Ecto.Query.t()
1028 def get_friends_query(user), do: get_friends_query(user, nil)
1029
1030 def get_friends(user, page \\ nil) do
1031 user
1032 |> get_friends_query(page)
1033 |> Repo.all()
1034 end
1035
1036 def get_friends_ap_ids(user) do
1037 user
1038 |> get_friends_query(nil)
1039 |> select([u], u.ap_id)
1040 |> Repo.all()
1041 end
1042
1043 def get_friends_ids(user, page \\ nil) do
1044 user
1045 |> get_friends_query(page)
1046 |> select([u], u.id)
1047 |> Repo.all()
1048 end
1049
1050 defdelegate get_follow_requests(user), to: FollowingRelationship
1051
1052 def increase_note_count(%User{} = user) do
1053 User
1054 |> where(id: ^user.id)
1055 |> update([u], inc: [note_count: 1])
1056 |> select([u], u)
1057 |> Repo.update_all([])
1058 |> case do
1059 {1, [user]} -> set_cache(user)
1060 _ -> {:error, user}
1061 end
1062 end
1063
1064 def decrease_note_count(%User{} = user) do
1065 User
1066 |> where(id: ^user.id)
1067 |> update([u],
1068 set: [
1069 note_count: fragment("greatest(0, note_count - 1)")
1070 ]
1071 )
1072 |> select([u], u)
1073 |> Repo.update_all([])
1074 |> case do
1075 {1, [user]} -> set_cache(user)
1076 _ -> {:error, user}
1077 end
1078 end
1079
1080 def update_note_count(%User{} = user, note_count \\ nil) do
1081 note_count =
1082 note_count ||
1083 from(
1084 a in Object,
1085 where: fragment("?->>'actor' = ? and ?->>'type' = 'Note'", a.data, ^user.ap_id, a.data),
1086 select: count(a.id)
1087 )
1088 |> Repo.one()
1089
1090 user
1091 |> cast(%{note_count: note_count}, [:note_count])
1092 |> update_and_set_cache()
1093 end
1094
1095 @spec maybe_fetch_follow_information(User.t()) :: User.t()
1096 def maybe_fetch_follow_information(user) do
1097 with {:ok, user} <- fetch_follow_information(user) do
1098 user
1099 else
1100 e ->
1101 Logger.error("Follower/Following counter update for #{user.ap_id} failed.\n#{inspect(e)}")
1102
1103 user
1104 end
1105 end
1106
1107 def fetch_follow_information(user) do
1108 with {:ok, info} <- ActivityPub.fetch_follow_information_for_user(user) do
1109 user
1110 |> follow_information_changeset(info)
1111 |> update_and_set_cache()
1112 end
1113 end
1114
1115 defp follow_information_changeset(user, params) do
1116 user
1117 |> cast(params, [
1118 :hide_followers,
1119 :hide_follows,
1120 :follower_count,
1121 :following_count,
1122 :hide_followers_count,
1123 :hide_follows_count
1124 ])
1125 end
1126
1127 def update_follower_count(%User{} = user) do
1128 if user.local or !Pleroma.Config.get([:instance, :external_user_synchronization]) do
1129 follower_count_query =
1130 User.Query.build(%{followers: user, deactivated: false})
1131 |> select([u], %{count: count(u.id)})
1132
1133 User
1134 |> where(id: ^user.id)
1135 |> join(:inner, [u], s in subquery(follower_count_query))
1136 |> update([u, s],
1137 set: [follower_count: s.count]
1138 )
1139 |> select([u], u)
1140 |> Repo.update_all([])
1141 |> case do
1142 {1, [user]} -> set_cache(user)
1143 _ -> {:error, user}
1144 end
1145 else
1146 {:ok, maybe_fetch_follow_information(user)}
1147 end
1148 end
1149
1150 @spec update_following_count(User.t()) :: User.t()
1151 def update_following_count(%User{local: false} = user) do
1152 if Pleroma.Config.get([:instance, :external_user_synchronization]) do
1153 maybe_fetch_follow_information(user)
1154 else
1155 user
1156 end
1157 end
1158
1159 def update_following_count(%User{local: true} = user) do
1160 following_count = FollowingRelationship.following_count(user)
1161
1162 user
1163 |> follow_information_changeset(%{following_count: following_count})
1164 |> Repo.update!()
1165 end
1166
1167 def set_unread_conversation_count(%User{local: true} = user) do
1168 unread_query = Participation.unread_conversation_count_for_user(user)
1169
1170 User
1171 |> join(:inner, [u], p in subquery(unread_query))
1172 |> update([u, p],
1173 set: [unread_conversation_count: p.count]
1174 )
1175 |> where([u], u.id == ^user.id)
1176 |> select([u], u)
1177 |> Repo.update_all([])
1178 |> case do
1179 {1, [user]} -> set_cache(user)
1180 _ -> {:error, user}
1181 end
1182 end
1183
1184 def set_unread_conversation_count(user), do: {:ok, user}
1185
1186 def increment_unread_conversation_count(conversation, %User{local: true} = user) do
1187 unread_query =
1188 Participation.unread_conversation_count_for_user(user)
1189 |> where([p], p.conversation_id == ^conversation.id)
1190
1191 User
1192 |> join(:inner, [u], p in subquery(unread_query))
1193 |> update([u, p],
1194 inc: [unread_conversation_count: 1]
1195 )
1196 |> where([u], u.id == ^user.id)
1197 |> where([u, p], p.count == 0)
1198 |> select([u], u)
1199 |> Repo.update_all([])
1200 |> case do
1201 {1, [user]} -> set_cache(user)
1202 _ -> {:error, user}
1203 end
1204 end
1205
1206 def increment_unread_conversation_count(_, user), do: {:ok, user}
1207
1208 @spec get_users_from_set([String.t()], boolean()) :: [User.t()]
1209 def get_users_from_set(ap_ids, local_only \\ true) do
1210 criteria = %{ap_id: ap_ids, deactivated: false}
1211 criteria = if local_only, do: Map.put(criteria, :local, true), else: criteria
1212
1213 User.Query.build(criteria)
1214 |> Repo.all()
1215 end
1216
1217 @spec get_recipients_from_activity(Activity.t()) :: [User.t()]
1218 def get_recipients_from_activity(%Activity{recipients: to, actor: actor}) do
1219 to = [actor | to]
1220
1221 User.Query.build(%{recipients_from_activity: to, local: true, deactivated: false})
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(fn follower ->
1418 follower |> update_following_count() |> set_cache()
1419 end)
1420
1421 # Only update local user counts, remote will be update during the next pull.
1422 user
1423 |> get_friends()
1424 |> Enum.filter(& &1.local)
1425 |> Enum.each(&update_follower_count/1)
1426
1427 {:ok, user}
1428 end
1429 end
1430
1431 def update_notification_settings(%User{} = user, settings) do
1432 user
1433 |> cast(%{notification_settings: settings}, [])
1434 |> cast_embed(:notification_settings)
1435 |> validate_required([:notification_settings])
1436 |> update_and_set_cache()
1437 end
1438
1439 def delete(users) when is_list(users) do
1440 for user <- users, do: delete(user)
1441 end
1442
1443 def delete(%User{} = user) do
1444 BackgroundWorker.enqueue("delete_user", %{"user_id" => user.id})
1445 end
1446
1447 def perform(:force_password_reset, user), do: force_password_reset(user)
1448
1449 @spec perform(atom(), User.t()) :: {:ok, User.t()}
1450 def perform(:delete, %User{} = user) do
1451 # Remove all relationships
1452 user
1453 |> get_followers()
1454 |> Enum.each(fn follower ->
1455 ActivityPub.unfollow(follower, user)
1456 unfollow(follower, user)
1457 end)
1458
1459 user
1460 |> get_friends()
1461 |> Enum.each(fn followed ->
1462 ActivityPub.unfollow(user, followed)
1463 unfollow(user, followed)
1464 end)
1465
1466 delete_user_activities(user)
1467
1468 if user.local do
1469 user
1470 |> change(%{deactivated: true, email: nil})
1471 |> update_and_set_cache()
1472 else
1473 invalidate_cache(user)
1474 Repo.delete(user)
1475 end
1476 end
1477
1478 def perform(:deactivate_async, user, status), do: deactivate(user, status)
1479
1480 @spec perform(atom(), User.t(), list()) :: list() | {:error, any()}
1481 def perform(:blocks_import, %User{} = blocker, blocked_identifiers)
1482 when is_list(blocked_identifiers) do
1483 Enum.map(
1484 blocked_identifiers,
1485 fn blocked_identifier ->
1486 with {:ok, %User{} = blocked} <- get_or_fetch(blocked_identifier),
1487 {:ok, _user_block} <- block(blocker, blocked),
1488 {:ok, _} <- ActivityPub.block(blocker, blocked) do
1489 blocked
1490 else
1491 err ->
1492 Logger.debug("blocks_import failed for #{blocked_identifier} with: #{inspect(err)}")
1493 err
1494 end
1495 end
1496 )
1497 end
1498
1499 def perform(:follow_import, %User{} = follower, followed_identifiers)
1500 when is_list(followed_identifiers) do
1501 Enum.map(
1502 followed_identifiers,
1503 fn followed_identifier ->
1504 with {:ok, %User{} = followed} <- get_or_fetch(followed_identifier),
1505 {:ok, follower} <- maybe_direct_follow(follower, followed),
1506 {:ok, _} <- ActivityPub.follow(follower, followed) do
1507 followed
1508 else
1509 err ->
1510 Logger.debug("follow_import failed for #{followed_identifier} with: #{inspect(err)}")
1511 err
1512 end
1513 end
1514 )
1515 end
1516
1517 @spec external_users_query() :: Ecto.Query.t()
1518 def external_users_query do
1519 User.Query.build(%{
1520 external: true,
1521 active: true,
1522 order_by: :id
1523 })
1524 end
1525
1526 @spec external_users(keyword()) :: [User.t()]
1527 def external_users(opts \\ []) do
1528 query =
1529 external_users_query()
1530 |> select([u], struct(u, [:id, :ap_id]))
1531
1532 query =
1533 if opts[:max_id],
1534 do: where(query, [u], u.id > ^opts[:max_id]),
1535 else: query
1536
1537 query =
1538 if opts[:limit],
1539 do: limit(query, ^opts[:limit]),
1540 else: query
1541
1542 Repo.all(query)
1543 end
1544
1545 def blocks_import(%User{} = blocker, blocked_identifiers) when is_list(blocked_identifiers) do
1546 BackgroundWorker.enqueue("blocks_import", %{
1547 "blocker_id" => blocker.id,
1548 "blocked_identifiers" => blocked_identifiers
1549 })
1550 end
1551
1552 def follow_import(%User{} = follower, followed_identifiers)
1553 when is_list(followed_identifiers) do
1554 BackgroundWorker.enqueue("follow_import", %{
1555 "follower_id" => follower.id,
1556 "followed_identifiers" => followed_identifiers
1557 })
1558 end
1559
1560 def delete_user_activities(%User{ap_id: ap_id} = user) do
1561 ap_id
1562 |> Activity.Queries.by_actor()
1563 |> RepoStreamer.chunk_stream(50)
1564 |> Stream.each(fn activities ->
1565 Enum.each(activities, fn activity -> delete_activity(activity, user) end)
1566 end)
1567 |> Stream.run()
1568 end
1569
1570 defp delete_activity(%{data: %{"type" => "Create", "object" => object}} = activity, user) do
1571 with {_, %Object{}} <- {:find_object, Object.get_by_ap_id(object)},
1572 {:ok, delete_data, _} <- Builder.delete(user, object) do
1573 Pipeline.common_pipeline(delete_data, local: user.local)
1574 else
1575 {:find_object, nil} ->
1576 # We have the create activity, but not the object, it was probably pruned.
1577 # Insert a tombstone and try again
1578 with {:ok, tombstone_data, _} <- Builder.tombstone(user.ap_id, object),
1579 {:ok, _tombstone} <- Object.create(tombstone_data) do
1580 delete_activity(activity, user)
1581 end
1582
1583 e ->
1584 Logger.error("Could not delete #{object} created by #{activity.data["ap_id"]}")
1585 Logger.error("Error: #{inspect(e)}")
1586 end
1587 end
1588
1589 defp delete_activity(%{data: %{"type" => type}} = activity, user)
1590 when type in ["Like", "Announce"] do
1591 {:ok, undo, _} = Builder.undo(user, activity)
1592 Pipeline.common_pipeline(undo, local: user.local)
1593 end
1594
1595 defp delete_activity(_activity, _user), do: "Doing nothing"
1596
1597 def html_filter_policy(%User{no_rich_text: true}) do
1598 Pleroma.HTML.Scrubber.TwitterText
1599 end
1600
1601 def html_filter_policy(_), do: Pleroma.Config.get([:markup, :scrub_policy])
1602
1603 def fetch_by_ap_id(ap_id), do: ActivityPub.make_user_from_ap_id(ap_id)
1604
1605 def get_or_fetch_by_ap_id(ap_id) do
1606 user = get_cached_by_ap_id(ap_id)
1607
1608 if !is_nil(user) and !needs_update?(user) do
1609 {:ok, user}
1610 else
1611 fetch_by_ap_id(ap_id)
1612 end
1613 end
1614
1615 @doc """
1616 Creates an internal service actor by URI if missing.
1617 Optionally takes nickname for addressing.
1618 """
1619 @spec get_or_create_service_actor_by_ap_id(String.t(), String.t()) :: User.t() | nil
1620 def get_or_create_service_actor_by_ap_id(uri, nickname) do
1621 {_, user} =
1622 case get_cached_by_ap_id(uri) do
1623 nil ->
1624 with {:error, %{errors: errors}} <- create_service_actor(uri, nickname) do
1625 Logger.error("Cannot create service actor: #{uri}/.\n#{inspect(errors)}")
1626 {:error, nil}
1627 end
1628
1629 %User{invisible: false} = user ->
1630 set_invisible(user)
1631
1632 user ->
1633 {:ok, user}
1634 end
1635
1636 user
1637 end
1638
1639 @spec set_invisible(User.t()) :: {:ok, User.t()}
1640 defp set_invisible(user) do
1641 user
1642 |> change(%{invisible: true})
1643 |> update_and_set_cache()
1644 end
1645
1646 @spec create_service_actor(String.t(), String.t()) ::
1647 {:ok, User.t()} | {:error, Ecto.Changeset.t()}
1648 defp create_service_actor(uri, nickname) do
1649 %User{
1650 invisible: true,
1651 local: true,
1652 ap_id: uri,
1653 nickname: nickname,
1654 follower_address: uri <> "/followers"
1655 }
1656 |> change
1657 |> unique_constraint(:nickname)
1658 |> Repo.insert()
1659 |> set_cache()
1660 end
1661
1662 def public_key(%{public_key: public_key_pem}) when is_binary(public_key_pem) do
1663 key =
1664 public_key_pem
1665 |> :public_key.pem_decode()
1666 |> hd()
1667 |> :public_key.pem_entry_decode()
1668
1669 {:ok, key}
1670 end
1671
1672 def public_key(_), do: {:error, "key not found"}
1673
1674 def get_public_key_for_ap_id(ap_id) do
1675 with {:ok, %User{} = user} <- get_or_fetch_by_ap_id(ap_id),
1676 {:ok, public_key} <- public_key(user) do
1677 {:ok, public_key}
1678 else
1679 _ -> :error
1680 end
1681 end
1682
1683 def ap_enabled?(%User{local: true}), do: true
1684 def ap_enabled?(%User{ap_enabled: ap_enabled}), do: ap_enabled
1685 def ap_enabled?(_), do: false
1686
1687 @doc "Gets or fetch a user by uri or nickname."
1688 @spec get_or_fetch(String.t()) :: {:ok, User.t()} | {:error, String.t()}
1689 def get_or_fetch("http" <> _host = uri), do: get_or_fetch_by_ap_id(uri)
1690 def get_or_fetch(nickname), do: get_or_fetch_by_nickname(nickname)
1691
1692 # wait a period of time and return newest version of the User structs
1693 # this is because we have synchronous follow APIs and need to simulate them
1694 # with an async handshake
1695 def wait_and_refresh(_, %User{local: true} = a, %User{local: true} = b) do
1696 with %User{} = a <- get_cached_by_id(a.id),
1697 %User{} = b <- get_cached_by_id(b.id) do
1698 {:ok, a, b}
1699 else
1700 nil -> :error
1701 end
1702 end
1703
1704 def wait_and_refresh(timeout, %User{} = a, %User{} = b) do
1705 with :ok <- :timer.sleep(timeout),
1706 %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 parse_bio(bio) when is_binary(bio) and bio != "" do
1715 bio
1716 |> CommonUtils.format_input("text/plain", mentions_format: :full)
1717 |> elem(0)
1718 end
1719
1720 def parse_bio(_), do: ""
1721
1722 def parse_bio(bio, user) when is_binary(bio) and bio != "" do
1723 # TODO: get profile URLs other than user.ap_id
1724 profile_urls = [user.ap_id]
1725
1726 bio
1727 |> CommonUtils.format_input("text/plain",
1728 mentions_format: :full,
1729 rel: &RelMe.maybe_put_rel_me(&1, profile_urls)
1730 )
1731 |> elem(0)
1732 end
1733
1734 def parse_bio(_, _), do: ""
1735
1736 def tag(user_identifiers, tags) when is_list(user_identifiers) do
1737 Repo.transaction(fn ->
1738 for user_identifier <- user_identifiers, do: tag(user_identifier, tags)
1739 end)
1740 end
1741
1742 def tag(nickname, tags) when is_binary(nickname),
1743 do: tag(get_by_nickname(nickname), tags)
1744
1745 def tag(%User{} = user, tags),
1746 do: update_tags(user, Enum.uniq((user.tags || []) ++ normalize_tags(tags)))
1747
1748 def untag(user_identifiers, tags) when is_list(user_identifiers) do
1749 Repo.transaction(fn ->
1750 for user_identifier <- user_identifiers, do: untag(user_identifier, tags)
1751 end)
1752 end
1753
1754 def untag(nickname, tags) when is_binary(nickname),
1755 do: untag(get_by_nickname(nickname), tags)
1756
1757 def untag(%User{} = user, tags),
1758 do: update_tags(user, (user.tags || []) -- normalize_tags(tags))
1759
1760 defp update_tags(%User{} = user, new_tags) do
1761 {:ok, updated_user} =
1762 user
1763 |> change(%{tags: new_tags})
1764 |> update_and_set_cache()
1765
1766 updated_user
1767 end
1768
1769 defp normalize_tags(tags) do
1770 [tags]
1771 |> List.flatten()
1772 |> Enum.map(&String.downcase/1)
1773 end
1774
1775 defp local_nickname_regex do
1776 if Pleroma.Config.get([:instance, :extended_nickname_format]) do
1777 @extended_local_nickname_regex
1778 else
1779 @strict_local_nickname_regex
1780 end
1781 end
1782
1783 def local_nickname(nickname_or_mention) do
1784 nickname_or_mention
1785 |> full_nickname()
1786 |> String.split("@")
1787 |> hd()
1788 end
1789
1790 def full_nickname(nickname_or_mention),
1791 do: String.trim_leading(nickname_or_mention, "@")
1792
1793 def error_user(ap_id) do
1794 %User{
1795 name: ap_id,
1796 ap_id: ap_id,
1797 nickname: "erroruser@example.com",
1798 inserted_at: NaiveDateTime.utc_now()
1799 }
1800 end
1801
1802 @spec all_superusers() :: [User.t()]
1803 def all_superusers do
1804 User.Query.build(%{super_users: true, local: true, deactivated: false})
1805 |> Repo.all()
1806 end
1807
1808 def muting_reblogs?(%User{} = user, %User{} = target) do
1809 UserRelationship.reblog_mute_exists?(user, target)
1810 end
1811
1812 def showing_reblogs?(%User{} = user, %User{} = target) do
1813 not muting_reblogs?(user, target)
1814 end
1815
1816 @doc """
1817 The function returns a query to get users with no activity for given interval of days.
1818 Inactive users are those who didn't read any notification, or had any activity where
1819 the user is the activity's actor, during `inactivity_threshold` days.
1820 Deactivated users will not appear in this list.
1821
1822 ## Examples
1823
1824 iex> Pleroma.User.list_inactive_users()
1825 %Ecto.Query{}
1826 """
1827 @spec list_inactive_users_query(integer()) :: Ecto.Query.t()
1828 def list_inactive_users_query(inactivity_threshold \\ 7) do
1829 negative_inactivity_threshold = -inactivity_threshold
1830 now = NaiveDateTime.truncate(NaiveDateTime.utc_now(), :second)
1831 # Subqueries are not supported in `where` clauses, join gets too complicated.
1832 has_read_notifications =
1833 from(n in Pleroma.Notification,
1834 where: n.seen == true,
1835 group_by: n.id,
1836 having: max(n.updated_at) > datetime_add(^now, ^negative_inactivity_threshold, "day"),
1837 select: n.user_id
1838 )
1839 |> Pleroma.Repo.all()
1840
1841 from(u in Pleroma.User,
1842 left_join: a in Pleroma.Activity,
1843 on: u.ap_id == a.actor,
1844 where: not is_nil(u.nickname),
1845 where: u.deactivated != ^true,
1846 where: u.id not in ^has_read_notifications,
1847 group_by: u.id,
1848 having:
1849 max(a.inserted_at) < datetime_add(^now, ^negative_inactivity_threshold, "day") or
1850 is_nil(max(a.inserted_at))
1851 )
1852 end
1853
1854 @doc """
1855 Enable or disable email notifications for user
1856
1857 ## Examples
1858
1859 iex> Pleroma.User.switch_email_notifications(Pleroma.User{email_notifications: %{"digest" => false}}, "digest", true)
1860 Pleroma.User{email_notifications: %{"digest" => true}}
1861
1862 iex> Pleroma.User.switch_email_notifications(Pleroma.User{email_notifications: %{"digest" => true}}, "digest", false)
1863 Pleroma.User{email_notifications: %{"digest" => false}}
1864 """
1865 @spec switch_email_notifications(t(), String.t(), boolean()) ::
1866 {:ok, t()} | {:error, Ecto.Changeset.t()}
1867 def switch_email_notifications(user, type, status) do
1868 User.update_email_notifications(user, %{type => status})
1869 end
1870
1871 @doc """
1872 Set `last_digest_emailed_at` value for the user to current time
1873 """
1874 @spec touch_last_digest_emailed_at(t()) :: t()
1875 def touch_last_digest_emailed_at(user) do
1876 now = NaiveDateTime.truncate(NaiveDateTime.utc_now(), :second)
1877
1878 {:ok, updated_user} =
1879 user
1880 |> change(%{last_digest_emailed_at: now})
1881 |> update_and_set_cache()
1882
1883 updated_user
1884 end
1885
1886 @spec toggle_confirmation(User.t()) :: {:ok, User.t()} | {:error, Changeset.t()}
1887 def toggle_confirmation(%User{} = user) do
1888 user
1889 |> confirmation_changeset(need_confirmation: !user.confirmation_pending)
1890 |> update_and_set_cache()
1891 end
1892
1893 @spec toggle_confirmation([User.t()]) :: [{:ok, User.t()} | {:error, Changeset.t()}]
1894 def toggle_confirmation(users) do
1895 Enum.map(users, &toggle_confirmation/1)
1896 end
1897
1898 def get_mascot(%{mascot: %{} = mascot}) when not is_nil(mascot) do
1899 mascot
1900 end
1901
1902 def get_mascot(%{mascot: mascot}) when is_nil(mascot) do
1903 # use instance-default
1904 config = Pleroma.Config.get([:assets, :mascots])
1905 default_mascot = Pleroma.Config.get([:assets, :default_mascot])
1906 mascot = Keyword.get(config, default_mascot)
1907
1908 %{
1909 "id" => "default-mascot",
1910 "url" => mascot[:url],
1911 "preview_url" => mascot[:url],
1912 "pleroma" => %{
1913 "mime_type" => mascot[:mime_type]
1914 }
1915 }
1916 end
1917
1918 def ensure_keys_present(%{keys: keys} = user) when not is_nil(keys), do: {:ok, user}
1919
1920 def ensure_keys_present(%User{} = user) do
1921 with {:ok, pem} <- Keys.generate_rsa_pem() do
1922 user
1923 |> cast(%{keys: pem}, [:keys])
1924 |> validate_required([:keys])
1925 |> update_and_set_cache()
1926 end
1927 end
1928
1929 def get_ap_ids_by_nicknames(nicknames) do
1930 from(u in User,
1931 where: u.nickname in ^nicknames,
1932 select: u.ap_id
1933 )
1934 |> Repo.all()
1935 end
1936
1937 defdelegate search(query, opts \\ []), to: User.Search
1938
1939 defp put_password_hash(
1940 %Ecto.Changeset{valid?: true, changes: %{password: password}} = changeset
1941 ) do
1942 change(changeset, password_hash: Pbkdf2.hash_pwd_salt(password))
1943 end
1944
1945 defp put_password_hash(changeset), do: changeset
1946
1947 def is_internal_user?(%User{nickname: nil}), do: true
1948 def is_internal_user?(%User{local: true, nickname: "internal." <> _}), do: true
1949 def is_internal_user?(_), do: false
1950
1951 # A hack because user delete activities have a fake id for whatever reason
1952 # TODO: Get rid of this
1953 def get_delivered_users_by_object_id("pleroma:fake_object_id"), do: []
1954
1955 def get_delivered_users_by_object_id(object_id) do
1956 from(u in User,
1957 inner_join: delivery in assoc(u, :deliveries),
1958 where: delivery.object_id == ^object_id
1959 )
1960 |> Repo.all()
1961 end
1962
1963 def change_email(user, email) do
1964 user
1965 |> cast(%{email: email}, [:email])
1966 |> validate_required([:email])
1967 |> unique_constraint(:email)
1968 |> validate_format(:email, @email_regex)
1969 |> update_and_set_cache()
1970 end
1971
1972 # Internal function; public one is `deactivate/2`
1973 defp set_activation_status(user, deactivated) do
1974 user
1975 |> cast(%{deactivated: deactivated}, [:deactivated])
1976 |> update_and_set_cache()
1977 end
1978
1979 def update_banner(user, banner) do
1980 user
1981 |> cast(%{banner: banner}, [:banner])
1982 |> update_and_set_cache()
1983 end
1984
1985 def update_background(user, background) do
1986 user
1987 |> cast(%{background: background}, [:background])
1988 |> update_and_set_cache()
1989 end
1990
1991 def roles(%{is_moderator: is_moderator, is_admin: is_admin}) do
1992 %{
1993 admin: is_admin,
1994 moderator: is_moderator
1995 }
1996 end
1997
1998 def validate_fields(changeset, remote? \\ false) do
1999 limit_name = if remote?, do: :max_remote_account_fields, else: :max_account_fields
2000 limit = Pleroma.Config.get([:instance, limit_name], 0)
2001
2002 changeset
2003 |> validate_length(:fields, max: limit)
2004 |> validate_change(:fields, fn :fields, fields ->
2005 if Enum.all?(fields, &valid_field?/1) do
2006 []
2007 else
2008 [fields: "invalid"]
2009 end
2010 end)
2011 end
2012
2013 defp valid_field?(%{"name" => name, "value" => value}) do
2014 name_limit = Pleroma.Config.get([:instance, :account_field_name_length], 255)
2015 value_limit = Pleroma.Config.get([:instance, :account_field_value_length], 255)
2016
2017 is_binary(name) && is_binary(value) && String.length(name) <= name_limit &&
2018 String.length(value) <= value_limit
2019 end
2020
2021 defp valid_field?(_), do: false
2022
2023 defp truncate_field(%{"name" => name, "value" => value}) do
2024 {name, _chopped} =
2025 String.split_at(name, Pleroma.Config.get([:instance, :account_field_name_length], 255))
2026
2027 {value, _chopped} =
2028 String.split_at(value, Pleroma.Config.get([:instance, :account_field_value_length], 255))
2029
2030 %{"name" => name, "value" => value}
2031 end
2032
2033 def admin_api_update(user, params) do
2034 user
2035 |> cast(params, [
2036 :is_moderator,
2037 :is_admin,
2038 :show_role
2039 ])
2040 |> update_and_set_cache()
2041 end
2042
2043 @doc "Signs user out of all applications"
2044 def global_sign_out(user) do
2045 OAuth.Authorization.delete_user_authorizations(user)
2046 OAuth.Token.delete_user_tokens(user)
2047 end
2048
2049 def mascot_update(user, url) do
2050 user
2051 |> cast(%{mascot: url}, [:mascot])
2052 |> validate_required([:mascot])
2053 |> update_and_set_cache()
2054 end
2055
2056 def mastodon_settings_update(user, settings) do
2057 user
2058 |> cast(%{settings: settings}, [:settings])
2059 |> validate_required([:settings])
2060 |> update_and_set_cache()
2061 end
2062
2063 @spec confirmation_changeset(User.t(), keyword()) :: Changeset.t()
2064 def confirmation_changeset(user, need_confirmation: need_confirmation?) do
2065 params =
2066 if need_confirmation? do
2067 %{
2068 confirmation_pending: true,
2069 confirmation_token: :crypto.strong_rand_bytes(32) |> Base.url_encode64()
2070 }
2071 else
2072 %{
2073 confirmation_pending: false,
2074 confirmation_token: nil
2075 }
2076 end
2077
2078 cast(user, params, [:confirmation_pending, :confirmation_token])
2079 end
2080
2081 def add_pinnned_activity(user, %Pleroma.Activity{id: id}) do
2082 if id not in user.pinned_activities do
2083 max_pinned_statuses = Pleroma.Config.get([:instance, :max_pinned_statuses], 0)
2084 params = %{pinned_activities: user.pinned_activities ++ [id]}
2085
2086 user
2087 |> cast(params, [:pinned_activities])
2088 |> validate_length(:pinned_activities,
2089 max: max_pinned_statuses,
2090 message: "You have already pinned the maximum number of statuses"
2091 )
2092 else
2093 change(user)
2094 end
2095 |> update_and_set_cache()
2096 end
2097
2098 def remove_pinnned_activity(user, %Pleroma.Activity{id: id}) do
2099 params = %{pinned_activities: List.delete(user.pinned_activities, id)}
2100
2101 user
2102 |> cast(params, [:pinned_activities])
2103 |> update_and_set_cache()
2104 end
2105
2106 def update_email_notifications(user, settings) do
2107 email_notifications =
2108 user.email_notifications
2109 |> Map.merge(settings)
2110 |> Map.take(["digest"])
2111
2112 params = %{email_notifications: email_notifications}
2113 fields = [:email_notifications]
2114
2115 user
2116 |> cast(params, fields)
2117 |> validate_required(fields)
2118 |> update_and_set_cache()
2119 end
2120
2121 defp set_domain_blocks(user, domain_blocks) do
2122 params = %{domain_blocks: domain_blocks}
2123
2124 user
2125 |> cast(params, [:domain_blocks])
2126 |> validate_required([:domain_blocks])
2127 |> update_and_set_cache()
2128 end
2129
2130 def block_domain(user, domain_blocked) do
2131 set_domain_blocks(user, Enum.uniq([domain_blocked | user.domain_blocks]))
2132 end
2133
2134 def unblock_domain(user, domain_blocked) do
2135 set_domain_blocks(user, List.delete(user.domain_blocks, domain_blocked))
2136 end
2137
2138 @spec add_to_block(User.t(), User.t()) ::
2139 {:ok, UserRelationship.t()} | {:error, Ecto.Changeset.t()}
2140 defp add_to_block(%User{} = user, %User{} = blocked) do
2141 UserRelationship.create_block(user, blocked)
2142 end
2143
2144 @spec add_to_block(User.t(), User.t()) ::
2145 {:ok, UserRelationship.t()} | {:ok, nil} | {:error, Ecto.Changeset.t()}
2146 defp remove_from_block(%User{} = user, %User{} = blocked) do
2147 UserRelationship.delete_block(user, blocked)
2148 end
2149
2150 defp add_to_mutes(%User{} = user, %User{} = muted_user, notifications?) do
2151 with {:ok, user_mute} <- UserRelationship.create_mute(user, muted_user),
2152 {:ok, user_notification_mute} <-
2153 (notifications? && UserRelationship.create_notification_mute(user, muted_user)) ||
2154 {:ok, nil} do
2155 {:ok, Enum.filter([user_mute, user_notification_mute], & &1)}
2156 end
2157 end
2158
2159 defp remove_from_mutes(user, %User{} = muted_user) do
2160 with {:ok, user_mute} <- UserRelationship.delete_mute(user, muted_user),
2161 {:ok, user_notification_mute} <-
2162 UserRelationship.delete_notification_mute(user, muted_user) do
2163 {:ok, [user_mute, user_notification_mute]}
2164 end
2165 end
2166
2167 def set_invisible(user, invisible) do
2168 params = %{invisible: invisible}
2169
2170 user
2171 |> cast(params, [:invisible])
2172 |> validate_required([:invisible])
2173 |> update_and_set_cache()
2174 end
2175
2176 def sanitize_html(%User{} = user) do
2177 sanitize_html(user, nil)
2178 end
2179
2180 # User data that mastodon isn't filtering (treated as plaintext):
2181 # - field name
2182 # - display name
2183 def sanitize_html(%User{} = user, filter) do
2184 fields =
2185 Enum.map(user.fields, fn %{"name" => name, "value" => value} ->
2186 %{
2187 "name" => name,
2188 "value" => HTML.filter_tags(value, Pleroma.HTML.Scrubber.LinksOnly)
2189 }
2190 end)
2191
2192 user
2193 |> Map.put(:bio, HTML.filter_tags(user.bio, filter))
2194 |> Map.put(:fields, fields)
2195 end
2196 end