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