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