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