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