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