abd1dd9369d5df794046d43a20be63134779bc57
[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{invisible: true}, _), do: false
135
136 def visible_for?(%User{id: user_id}, %User{id: for_id}) when user_id == for_id, do: true
137
138 def visible_for?(%User{} = user, for_user) do
139 auth_active?(user) || superuser?(for_user)
140 end
141
142 def visible_for?(_, _), do: false
143
144 def superuser?(%User{local: true, is_admin: true}), do: true
145 def superuser?(%User{local: true, is_moderator: true}), do: true
146 def superuser?(_), do: false
147
148 def invisible?(%User{invisible: true}), do: true
149 def invisible?(_), do: false
150
151 def avatar_url(user, options \\ []) do
152 case user.avatar do
153 %{"url" => [%{"href" => href} | _]} -> href
154 _ -> !options[:no_default] && "#{Web.base_url()}/images/avi.png"
155 end
156 end
157
158 def banner_url(user, options \\ []) do
159 case user.banner do
160 %{"url" => [%{"href" => href} | _]} -> href
161 _ -> !options[:no_default] && "#{Web.base_url()}/images/banner.png"
162 end
163 end
164
165 def profile_url(%User{source_data: %{"url" => url}}), do: url
166 def profile_url(%User{ap_id: ap_id}), do: ap_id
167 def profile_url(_), do: nil
168
169 def ap_id(%User{nickname: nickname}), do: "#{Web.base_url()}/users/#{nickname}"
170
171 def ap_followers(%User{follower_address: fa}) when is_binary(fa), do: fa
172 def ap_followers(%User{} = user), do: "#{ap_id(user)}/followers"
173
174 @spec ap_following(User.t()) :: Sring.t()
175 def ap_following(%User{following_address: fa}) when is_binary(fa), do: fa
176 def ap_following(%User{} = user), do: "#{ap_id(user)}/following"
177
178 def user_info(%User{} = user, args \\ %{}) do
179 following_count =
180 Map.get(args, :following_count, user.following_count || following_count(user))
181
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 }
190 |> Map.put(:following_count, following_count)
191 |> Map.put(:follower_count, follower_count)
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 needs_update?(%User{local: true}), do: false
495
496 def needs_update?(%User{local: false, last_refreshed_at: nil}), do: true
497
498 def needs_update?(%User{local: false} = user) do
499 NaiveDateTime.diff(NaiveDateTime.utc_now(), user.last_refreshed_at) >= 86_400
500 end
501
502 def needs_update?(_), do: true
503
504 @spec maybe_direct_follow(User.t(), User.t()) :: {:ok, User.t()} | {:error, String.t()}
505 def maybe_direct_follow(%User{} = follower, %User{local: true, locked: true} = followed) do
506 follow(follower, followed, "pending")
507 end
508
509 def maybe_direct_follow(%User{} = follower, %User{local: true} = followed) do
510 follow(follower, followed)
511 end
512
513 def maybe_direct_follow(%User{} = follower, %User{} = followed) do
514 if not ap_enabled?(followed) do
515 follow(follower, followed)
516 else
517 {:ok, follower}
518 end
519 end
520
521 @doc "A mass follow for local users. Respects blocks in both directions but does not create activities."
522 @spec follow_all(User.t(), list(User.t())) :: {atom(), User.t()}
523 def follow_all(follower, followeds) do
524 followeds =
525 Enum.reject(followeds, fn followed ->
526 blocks?(follower, followed) || blocks?(followed, follower)
527 end)
528
529 Enum.each(followeds, &follow(follower, &1, "accept"))
530
531 Enum.each(followeds, &update_follower_count/1)
532
533 set_cache(follower)
534 end
535
536 defdelegate following(user), to: FollowingRelationship
537
538 def follow(%User{} = follower, %User{} = followed, state \\ "accept") do
539 deny_follow_blocked = Pleroma.Config.get([:user, :deny_follow_blocked])
540
541 cond do
542 followed.deactivated ->
543 {:error, "Could not follow user: #{followed.nickname} is deactivated."}
544
545 deny_follow_blocked and blocks?(followed, follower) ->
546 {:error, "Could not follow user: #{followed.nickname} blocked you."}
547
548 true ->
549 FollowingRelationship.follow(follower, followed, state)
550
551 follower = maybe_update_following_count(follower)
552
553 {:ok, _} = update_follower_count(followed)
554
555 set_cache(follower)
556 end
557 end
558
559 def unfollow(%User{} = follower, %User{} = followed) do
560 if following?(follower, followed) and follower.ap_id != followed.ap_id do
561 FollowingRelationship.unfollow(follower, followed)
562
563 follower = maybe_update_following_count(follower)
564
565 {:ok, followed} = update_follower_count(followed)
566
567 set_cache(follower)
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 maybe_update_following_count(User.t()) :: User.t()
898 def maybe_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 maybe_update_following_count(user), do: user
907
908 def set_unread_conversation_count(%User{local: true} = user) do
909 unread_query = Participation.unread_conversation_count_for_user(user)
910
911 User
912 |> join(:inner, [u], p in subquery(unread_query))
913 |> update([u, p],
914 set: [unread_conversation_count: p.count]
915 )
916 |> where([u], u.id == ^user.id)
917 |> select([u], u)
918 |> Repo.update_all([])
919 |> case do
920 {1, [user]} -> set_cache(user)
921 _ -> {:error, user}
922 end
923 end
924
925 def set_unread_conversation_count(user), do: {:ok, user}
926
927 def increment_unread_conversation_count(conversation, %User{local: true} = user) do
928 unread_query =
929 Participation.unread_conversation_count_for_user(user)
930 |> where([p], p.conversation_id == ^conversation.id)
931
932 User
933 |> join(:inner, [u], p in subquery(unread_query))
934 |> update([u, p],
935 inc: [unread_conversation_count: 1]
936 )
937 |> where([u], u.id == ^user.id)
938 |> where([u, p], p.count == 0)
939 |> select([u], u)
940 |> Repo.update_all([])
941 |> case do
942 {1, [user]} -> set_cache(user)
943 _ -> {:error, user}
944 end
945 end
946
947 def increment_unread_conversation_count(_, user), do: {:ok, user}
948
949 @spec get_users_from_set([String.t()], boolean()) :: [User.t()]
950 def get_users_from_set(ap_ids, local_only \\ true) do
951 criteria = %{ap_id: ap_ids, deactivated: false}
952 criteria = if local_only, do: Map.put(criteria, :local, true), else: criteria
953
954 User.Query.build(criteria)
955 |> Repo.all()
956 end
957
958 @spec get_recipients_from_activity(Activity.t()) :: [User.t()]
959 def get_recipients_from_activity(%Activity{recipients: to}) do
960 User.Query.build(%{recipients_from_activity: to, local: true, deactivated: false})
961 |> Repo.all()
962 end
963
964 @spec mute(User.t(), User.t(), boolean()) :: {:ok, User.t()} | {:error, String.t()}
965 def mute(muter, %User{ap_id: ap_id}, notifications? \\ true) do
966 add_to_mutes(muter, ap_id, notifications?)
967 end
968
969 def unmute(muter, %{ap_id: ap_id}) do
970 remove_from_mutes(muter, ap_id)
971 end
972
973 def subscribe(subscriber, %{ap_id: ap_id}) do
974 with %User{} = subscribed <- get_cached_by_ap_id(ap_id) do
975 deny_follow_blocked = Pleroma.Config.get([:user, :deny_follow_blocked])
976
977 if blocks?(subscribed, subscriber) and deny_follow_blocked do
978 {:error, "Could not subscribe: #{subscribed.nickname} is blocking you"}
979 else
980 User.add_to_subscribers(subscribed, subscriber.ap_id)
981 end
982 end
983 end
984
985 def unsubscribe(unsubscriber, %{ap_id: ap_id}) do
986 with %User{} = user <- get_cached_by_ap_id(ap_id) do
987 User.remove_from_subscribers(user, unsubscriber.ap_id)
988 end
989 end
990
991 def block(blocker, %User{ap_id: ap_id} = blocked) do
992 # sever any follow relationships to prevent leaks per activitypub (Pleroma issue #213)
993 blocker =
994 if following?(blocker, blocked) do
995 {:ok, blocker, _} = unfollow(blocker, blocked)
996 blocker
997 else
998 blocker
999 end
1000
1001 # clear any requested follows as well
1002 blocked =
1003 case CommonAPI.reject_follow_request(blocked, blocker) do
1004 {:ok, %User{} = updated_blocked} -> updated_blocked
1005 nil -> blocked
1006 end
1007
1008 blocker =
1009 if subscribed_to?(blocked, blocker) do
1010 {:ok, blocker} = unsubscribe(blocked, blocker)
1011 blocker
1012 else
1013 blocker
1014 end
1015
1016 if following?(blocked, blocker), do: unfollow(blocked, blocker)
1017
1018 {:ok, blocker} = update_follower_count(blocker)
1019 {:ok, blocker, _} = Participation.mark_all_as_read(blocker, blocked)
1020 add_to_block(blocker, ap_id)
1021 end
1022
1023 # helper to handle the block given only an actor's AP id
1024 def block(blocker, %{ap_id: ap_id}) do
1025 block(blocker, get_cached_by_ap_id(ap_id))
1026 end
1027
1028 def unblock(blocker, %{ap_id: ap_id}) do
1029 remove_from_block(blocker, ap_id)
1030 end
1031
1032 def mutes?(nil, _), do: false
1033 def mutes?(user, %{ap_id: ap_id}), do: Enum.member?(user.mutes, ap_id)
1034
1035 @spec muted_notifications?(User.t() | nil, User.t() | map()) :: boolean()
1036 def muted_notifications?(nil, _), do: false
1037
1038 def muted_notifications?(user, %{ap_id: ap_id}),
1039 do: Enum.member?(user.muted_notifications, ap_id)
1040
1041 def blocks?(%User{} = user, %User{} = target) do
1042 blocks_ap_id?(user, target) || blocks_domain?(user, target)
1043 end
1044
1045 def blocks?(nil, _), do: false
1046
1047 def blocks_ap_id?(%User{} = user, %User{} = target) do
1048 Enum.member?(user.blocks, target.ap_id)
1049 end
1050
1051 def blocks_ap_id?(_, _), do: false
1052
1053 def blocks_domain?(%User{} = user, %User{} = target) do
1054 domain_blocks = Pleroma.Web.ActivityPub.MRF.subdomains_regex(user.domain_blocks)
1055 %{host: host} = URI.parse(target.ap_id)
1056 Pleroma.Web.ActivityPub.MRF.subdomain_match?(domain_blocks, host)
1057 end
1058
1059 def blocks_domain?(_, _), do: false
1060
1061 def subscribed_to?(user, %{ap_id: ap_id}) do
1062 with %User{} = target <- get_cached_by_ap_id(ap_id) do
1063 Enum.member?(target.subscribers, user.ap_id)
1064 end
1065 end
1066
1067 @spec muted_users(User.t()) :: [User.t()]
1068 def muted_users(user) do
1069 User.Query.build(%{ap_id: user.mutes, deactivated: false})
1070 |> Repo.all()
1071 end
1072
1073 @spec blocked_users(User.t()) :: [User.t()]
1074 def blocked_users(user) do
1075 User.Query.build(%{ap_id: user.blocks, deactivated: false})
1076 |> Repo.all()
1077 end
1078
1079 @spec subscribers(User.t()) :: [User.t()]
1080 def subscribers(user) do
1081 User.Query.build(%{ap_id: user.subscribers, deactivated: false})
1082 |> Repo.all()
1083 end
1084
1085 def deactivate_async(user, status \\ true) do
1086 BackgroundWorker.enqueue("deactivate_user", %{"user_id" => user.id, "status" => status})
1087 end
1088
1089 def deactivate(user, status \\ true)
1090
1091 def deactivate(users, status) when is_list(users) do
1092 Repo.transaction(fn ->
1093 for user <- users, do: deactivate(user, status)
1094 end)
1095 end
1096
1097 def deactivate(%User{} = user, status) do
1098 with {:ok, user} <- set_activation_status(user, status) do
1099 Enum.each(get_followers(user), &invalidate_cache/1)
1100
1101 # Only update local user counts, remote will be update during the next pull.
1102 user
1103 |> get_friends()
1104 |> Enum.filter(& &1.local)
1105 |> Enum.each(&update_follower_count/1)
1106
1107 {:ok, user}
1108 end
1109 end
1110
1111 def update_notification_settings(%User{} = user, settings) do
1112 settings =
1113 settings
1114 |> Enum.map(fn {k, v} -> {k, v in [true, "true", "True", "1"]} end)
1115 |> Map.new()
1116
1117 notification_settings =
1118 user.notification_settings
1119 |> Map.merge(settings)
1120 |> Map.take(["followers", "follows", "non_follows", "non_followers"])
1121
1122 params = %{notification_settings: notification_settings}
1123
1124 user
1125 |> cast(params, [:notification_settings])
1126 |> validate_required([:notification_settings])
1127 |> update_and_set_cache()
1128 end
1129
1130 def delete(users) when is_list(users) do
1131 for user <- users, do: delete(user)
1132 end
1133
1134 def delete(%User{} = user) do
1135 BackgroundWorker.enqueue("delete_user", %{"user_id" => user.id})
1136 end
1137
1138 def perform(:force_password_reset, user), do: force_password_reset(user)
1139
1140 @spec perform(atom(), User.t()) :: {:ok, User.t()}
1141 def perform(:delete, %User{} = user) do
1142 {:ok, _user} = ActivityPub.delete(user)
1143
1144 # Remove all relationships
1145 user
1146 |> get_followers()
1147 |> Enum.each(fn follower ->
1148 ActivityPub.unfollow(follower, user)
1149 unfollow(follower, user)
1150 end)
1151
1152 user
1153 |> get_friends()
1154 |> Enum.each(fn followed ->
1155 ActivityPub.unfollow(user, followed)
1156 unfollow(user, followed)
1157 end)
1158
1159 delete_user_activities(user)
1160 invalidate_cache(user)
1161 Repo.delete(user)
1162 end
1163
1164 @spec perform(atom(), User.t()) :: {:ok, User.t()}
1165 def perform(:fetch_initial_posts, %User{} = user) do
1166 pages = Pleroma.Config.get!([:fetch_initial_posts, :pages])
1167
1168 # Insert all the posts in reverse order, so they're in the right order on the timeline
1169 user.source_data["outbox"]
1170 |> Utils.fetch_ordered_collection(pages)
1171 |> Enum.reverse()
1172 |> Enum.each(&Pleroma.Web.Federator.incoming_ap_doc/1)
1173 end
1174
1175 def perform(:deactivate_async, user, status), do: deactivate(user, status)
1176
1177 @spec perform(atom(), User.t(), list()) :: list() | {:error, any()}
1178 def perform(:blocks_import, %User{} = blocker, blocked_identifiers)
1179 when is_list(blocked_identifiers) do
1180 Enum.map(
1181 blocked_identifiers,
1182 fn blocked_identifier ->
1183 with {:ok, %User{} = blocked} <- get_or_fetch(blocked_identifier),
1184 {:ok, blocker} <- block(blocker, blocked),
1185 {:ok, _} <- ActivityPub.block(blocker, blocked) do
1186 blocked
1187 else
1188 err ->
1189 Logger.debug("blocks_import failed for #{blocked_identifier} with: #{inspect(err)}")
1190 err
1191 end
1192 end
1193 )
1194 end
1195
1196 @spec perform(atom(), User.t(), list()) :: list() | {:error, any()}
1197 def perform(:follow_import, %User{} = follower, followed_identifiers)
1198 when is_list(followed_identifiers) do
1199 Enum.map(
1200 followed_identifiers,
1201 fn followed_identifier ->
1202 with {:ok, %User{} = followed} <- get_or_fetch(followed_identifier),
1203 {:ok, follower} <- maybe_direct_follow(follower, followed),
1204 {:ok, _} <- ActivityPub.follow(follower, followed) do
1205 followed
1206 else
1207 err ->
1208 Logger.debug("follow_import failed for #{followed_identifier} with: #{inspect(err)}")
1209 err
1210 end
1211 end
1212 )
1213 end
1214
1215 @spec external_users_query() :: Ecto.Query.t()
1216 def external_users_query do
1217 User.Query.build(%{
1218 external: true,
1219 active: true,
1220 order_by: :id
1221 })
1222 end
1223
1224 @spec external_users(keyword()) :: [User.t()]
1225 def external_users(opts \\ []) do
1226 query =
1227 external_users_query()
1228 |> select([u], struct(u, [:id, :ap_id, :info]))
1229
1230 query =
1231 if opts[:max_id],
1232 do: where(query, [u], u.id > ^opts[:max_id]),
1233 else: query
1234
1235 query =
1236 if opts[:limit],
1237 do: limit(query, ^opts[:limit]),
1238 else: query
1239
1240 Repo.all(query)
1241 end
1242
1243 def blocks_import(%User{} = blocker, blocked_identifiers) when is_list(blocked_identifiers) do
1244 BackgroundWorker.enqueue("blocks_import", %{
1245 "blocker_id" => blocker.id,
1246 "blocked_identifiers" => blocked_identifiers
1247 })
1248 end
1249
1250 def follow_import(%User{} = follower, followed_identifiers)
1251 when is_list(followed_identifiers) do
1252 BackgroundWorker.enqueue("follow_import", %{
1253 "follower_id" => follower.id,
1254 "followed_identifiers" => followed_identifiers
1255 })
1256 end
1257
1258 def delete_user_activities(%User{ap_id: ap_id}) do
1259 ap_id
1260 |> Activity.Queries.by_actor()
1261 |> RepoStreamer.chunk_stream(50)
1262 |> Stream.each(fn activities -> Enum.each(activities, &delete_activity/1) end)
1263 |> Stream.run()
1264 end
1265
1266 defp delete_activity(%{data: %{"type" => "Create"}} = activity) do
1267 activity
1268 |> Object.normalize()
1269 |> ActivityPub.delete()
1270 end
1271
1272 defp delete_activity(%{data: %{"type" => "Like"}} = activity) do
1273 object = Object.normalize(activity)
1274
1275 activity.actor
1276 |> get_cached_by_ap_id()
1277 |> ActivityPub.unlike(object)
1278 end
1279
1280 defp delete_activity(%{data: %{"type" => "Announce"}} = activity) do
1281 object = Object.normalize(activity)
1282
1283 activity.actor
1284 |> get_cached_by_ap_id()
1285 |> ActivityPub.unannounce(object)
1286 end
1287
1288 defp delete_activity(_activity), do: "Doing nothing"
1289
1290 def html_filter_policy(%User{no_rich_text: true}) do
1291 Pleroma.HTML.Scrubber.TwitterText
1292 end
1293
1294 def html_filter_policy(_), do: Pleroma.Config.get([:markup, :scrub_policy])
1295
1296 def fetch_by_ap_id(ap_id), do: ActivityPub.make_user_from_ap_id(ap_id)
1297
1298 def get_or_fetch_by_ap_id(ap_id) do
1299 user = get_cached_by_ap_id(ap_id)
1300
1301 if !is_nil(user) and !needs_update?(user) do
1302 {:ok, user}
1303 else
1304 # Whether to fetch initial posts for the user (if it's a new user & the fetching is enabled)
1305 should_fetch_initial = is_nil(user) and Pleroma.Config.get([:fetch_initial_posts, :enabled])
1306
1307 resp = fetch_by_ap_id(ap_id)
1308
1309 if should_fetch_initial do
1310 with {:ok, %User{} = user} <- resp do
1311 fetch_initial_posts(user)
1312 end
1313 end
1314
1315 resp
1316 end
1317 end
1318
1319 @doc """
1320 Creates an internal service actor by URI if missing.
1321 Optionally takes nickname for addressing.
1322 """
1323 def get_or_create_service_actor_by_ap_id(uri, nickname \\ nil) do
1324 with user when is_nil(user) <- get_cached_by_ap_id(uri) do
1325 {:ok, user} =
1326 %User{
1327 invisible: true,
1328 local: true,
1329 ap_id: uri,
1330 nickname: nickname,
1331 follower_address: uri <> "/followers"
1332 }
1333 |> Repo.insert()
1334
1335 user
1336 end
1337 end
1338
1339 # AP style
1340 def public_key(%{source_data: %{"publicKey" => %{"publicKeyPem" => public_key_pem}}}) do
1341 key =
1342 public_key_pem
1343 |> :public_key.pem_decode()
1344 |> hd()
1345 |> :public_key.pem_entry_decode()
1346
1347 {:ok, key}
1348 end
1349
1350 def public_key(_), do: {:error, "not found key"}
1351
1352 def get_public_key_for_ap_id(ap_id) do
1353 with {:ok, %User{} = user} <- get_or_fetch_by_ap_id(ap_id),
1354 {:ok, public_key} <- public_key(user) do
1355 {:ok, public_key}
1356 else
1357 _ -> :error
1358 end
1359 end
1360
1361 defp blank?(""), do: nil
1362 defp blank?(n), do: n
1363
1364 def insert_or_update_user(data) do
1365 data
1366 |> Map.put(:name, blank?(data[:name]) || data[:nickname])
1367 |> remote_user_creation()
1368 |> Repo.insert(on_conflict: :replace_all_except_primary_key, conflict_target: :nickname)
1369 |> set_cache()
1370 end
1371
1372 def ap_enabled?(%User{local: true}), do: true
1373 def ap_enabled?(%User{ap_enabled: ap_enabled}), do: ap_enabled
1374 def ap_enabled?(_), do: false
1375
1376 @doc "Gets or fetch a user by uri or nickname."
1377 @spec get_or_fetch(String.t()) :: {:ok, User.t()} | {:error, String.t()}
1378 def get_or_fetch("http" <> _host = uri), do: get_or_fetch_by_ap_id(uri)
1379 def get_or_fetch(nickname), do: get_or_fetch_by_nickname(nickname)
1380
1381 # wait a period of time and return newest version of the User structs
1382 # this is because we have synchronous follow APIs and need to simulate them
1383 # with an async handshake
1384 def wait_and_refresh(_, %User{local: true} = a, %User{local: true} = b) do
1385 with %User{} = a <- get_cached_by_id(a.id),
1386 %User{} = b <- get_cached_by_id(b.id) do
1387 {:ok, a, b}
1388 else
1389 nil -> :error
1390 end
1391 end
1392
1393 def wait_and_refresh(timeout, %User{} = a, %User{} = b) do
1394 with :ok <- :timer.sleep(timeout),
1395 %User{} = a <- get_cached_by_id(a.id),
1396 %User{} = b <- get_cached_by_id(b.id) do
1397 {:ok, a, b}
1398 else
1399 nil -> :error
1400 end
1401 end
1402
1403 def parse_bio(bio) when is_binary(bio) and bio != "" do
1404 bio
1405 |> CommonUtils.format_input("text/plain", mentions_format: :full)
1406 |> elem(0)
1407 end
1408
1409 def parse_bio(_), do: ""
1410
1411 def parse_bio(bio, user) when is_binary(bio) and bio != "" do
1412 # TODO: get profile URLs other than user.ap_id
1413 profile_urls = [user.ap_id]
1414
1415 bio
1416 |> CommonUtils.format_input("text/plain",
1417 mentions_format: :full,
1418 rel: &RelMe.maybe_put_rel_me(&1, profile_urls)
1419 )
1420 |> elem(0)
1421 end
1422
1423 def parse_bio(_, _), do: ""
1424
1425 def tag(user_identifiers, tags) when is_list(user_identifiers) do
1426 Repo.transaction(fn ->
1427 for user_identifier <- user_identifiers, do: tag(user_identifier, tags)
1428 end)
1429 end
1430
1431 def tag(nickname, tags) when is_binary(nickname),
1432 do: tag(get_by_nickname(nickname), tags)
1433
1434 def tag(%User{} = user, tags),
1435 do: update_tags(user, Enum.uniq((user.tags || []) ++ normalize_tags(tags)))
1436
1437 def untag(user_identifiers, tags) when is_list(user_identifiers) do
1438 Repo.transaction(fn ->
1439 for user_identifier <- user_identifiers, do: untag(user_identifier, tags)
1440 end)
1441 end
1442
1443 def untag(nickname, tags) when is_binary(nickname),
1444 do: untag(get_by_nickname(nickname), tags)
1445
1446 def untag(%User{} = user, tags),
1447 do: update_tags(user, (user.tags || []) -- normalize_tags(tags))
1448
1449 defp update_tags(%User{} = user, new_tags) do
1450 {:ok, updated_user} =
1451 user
1452 |> change(%{tags: new_tags})
1453 |> update_and_set_cache()
1454
1455 updated_user
1456 end
1457
1458 defp normalize_tags(tags) do
1459 [tags]
1460 |> List.flatten()
1461 |> Enum.map(&String.downcase/1)
1462 end
1463
1464 defp local_nickname_regex do
1465 if Pleroma.Config.get([:instance, :extended_nickname_format]) do
1466 @extended_local_nickname_regex
1467 else
1468 @strict_local_nickname_regex
1469 end
1470 end
1471
1472 def local_nickname(nickname_or_mention) do
1473 nickname_or_mention
1474 |> full_nickname()
1475 |> String.split("@")
1476 |> hd()
1477 end
1478
1479 def full_nickname(nickname_or_mention),
1480 do: String.trim_leading(nickname_or_mention, "@")
1481
1482 def error_user(ap_id) do
1483 %User{
1484 name: ap_id,
1485 ap_id: ap_id,
1486 nickname: "erroruser@example.com",
1487 inserted_at: NaiveDateTime.utc_now()
1488 }
1489 end
1490
1491 @spec all_superusers() :: [User.t()]
1492 def all_superusers do
1493 User.Query.build(%{super_users: true, local: true, deactivated: false})
1494 |> Repo.all()
1495 end
1496
1497 def showing_reblogs?(%User{} = user, %User{} = target) do
1498 target.ap_id not in user.muted_reblogs
1499 end
1500
1501 @doc """
1502 The function returns a query to get users with no activity for given interval of days.
1503 Inactive users are those who didn't read any notification, or had any activity where
1504 the user is the activity's actor, during `inactivity_threshold` days.
1505 Deactivated users will not appear in this list.
1506
1507 ## Examples
1508
1509 iex> Pleroma.User.list_inactive_users()
1510 %Ecto.Query{}
1511 """
1512 @spec list_inactive_users_query(integer()) :: Ecto.Query.t()
1513 def list_inactive_users_query(inactivity_threshold \\ 7) do
1514 negative_inactivity_threshold = -inactivity_threshold
1515 now = NaiveDateTime.truncate(NaiveDateTime.utc_now(), :second)
1516 # Subqueries are not supported in `where` clauses, join gets too complicated.
1517 has_read_notifications =
1518 from(n in Pleroma.Notification,
1519 where: n.seen == true,
1520 group_by: n.id,
1521 having: max(n.updated_at) > datetime_add(^now, ^negative_inactivity_threshold, "day"),
1522 select: n.user_id
1523 )
1524 |> Pleroma.Repo.all()
1525
1526 from(u in Pleroma.User,
1527 left_join: a in Pleroma.Activity,
1528 on: u.ap_id == a.actor,
1529 where: not is_nil(u.nickname),
1530 where: u.deactivated != ^true,
1531 where: u.id not in ^has_read_notifications,
1532 group_by: u.id,
1533 having:
1534 max(a.inserted_at) < datetime_add(^now, ^negative_inactivity_threshold, "day") or
1535 is_nil(max(a.inserted_at))
1536 )
1537 end
1538
1539 @doc """
1540 Enable or disable email notifications for user
1541
1542 ## Examples
1543
1544 iex> Pleroma.User.switch_email_notifications(Pleroma.User{email_notifications: %{"digest" => false}}, "digest", true)
1545 Pleroma.User{email_notifications: %{"digest" => true}}
1546
1547 iex> Pleroma.User.switch_email_notifications(Pleroma.User{email_notifications: %{"digest" => true}}, "digest", false)
1548 Pleroma.User{email_notifications: %{"digest" => false}}
1549 """
1550 @spec switch_email_notifications(t(), String.t(), boolean()) ::
1551 {:ok, t()} | {:error, Ecto.Changeset.t()}
1552 def switch_email_notifications(user, type, status) do
1553 User.update_email_notifications(user, %{type => status})
1554 end
1555
1556 @doc """
1557 Set `last_digest_emailed_at` value for the user to current time
1558 """
1559 @spec touch_last_digest_emailed_at(t()) :: t()
1560 def touch_last_digest_emailed_at(user) do
1561 now = NaiveDateTime.truncate(NaiveDateTime.utc_now(), :second)
1562
1563 {:ok, updated_user} =
1564 user
1565 |> change(%{last_digest_emailed_at: now})
1566 |> update_and_set_cache()
1567
1568 updated_user
1569 end
1570
1571 @spec toggle_confirmation(User.t()) :: {:ok, User.t()} | {:error, Changeset.t()}
1572 def toggle_confirmation(%User{} = user) do
1573 user
1574 |> confirmation_changeset(need_confirmation: !user.confirmation_pending)
1575 |> update_and_set_cache()
1576 end
1577
1578 def get_mascot(%{mascot: %{} = mascot}) when not is_nil(mascot) do
1579 mascot
1580 end
1581
1582 def get_mascot(%{mascot: mascot}) when is_nil(mascot) do
1583 # use instance-default
1584 config = Pleroma.Config.get([:assets, :mascots])
1585 default_mascot = Pleroma.Config.get([:assets, :default_mascot])
1586 mascot = Keyword.get(config, default_mascot)
1587
1588 %{
1589 "id" => "default-mascot",
1590 "url" => mascot[:url],
1591 "preview_url" => mascot[:url],
1592 "pleroma" => %{
1593 "mime_type" => mascot[:mime_type]
1594 }
1595 }
1596 end
1597
1598 def ensure_keys_present(%{keys: keys} = user) when not is_nil(keys), do: {:ok, user}
1599
1600 def ensure_keys_present(%User{} = user) do
1601 with {:ok, pem} <- Keys.generate_rsa_pem() do
1602 user
1603 |> cast(%{keys: pem}, [:keys])
1604 |> validate_required([:keys])
1605 |> update_and_set_cache()
1606 end
1607 end
1608
1609 def get_ap_ids_by_nicknames(nicknames) do
1610 from(u in User,
1611 where: u.nickname in ^nicknames,
1612 select: u.ap_id
1613 )
1614 |> Repo.all()
1615 end
1616
1617 defdelegate search(query, opts \\ []), to: User.Search
1618
1619 defp put_password_hash(
1620 %Ecto.Changeset{valid?: true, changes: %{password: password}} = changeset
1621 ) do
1622 change(changeset, password_hash: Pbkdf2.hashpwsalt(password))
1623 end
1624
1625 defp put_password_hash(changeset), do: changeset
1626
1627 def is_internal_user?(%User{nickname: nil}), do: true
1628 def is_internal_user?(%User{local: true, nickname: "internal." <> _}), do: true
1629 def is_internal_user?(_), do: false
1630
1631 # A hack because user delete activities have a fake id for whatever reason
1632 # TODO: Get rid of this
1633 def get_delivered_users_by_object_id("pleroma:fake_object_id"), do: []
1634
1635 def get_delivered_users_by_object_id(object_id) do
1636 from(u in User,
1637 inner_join: delivery in assoc(u, :deliveries),
1638 where: delivery.object_id == ^object_id
1639 )
1640 |> Repo.all()
1641 end
1642
1643 def change_email(user, email) do
1644 user
1645 |> cast(%{email: email}, [:email])
1646 |> validate_required([:email])
1647 |> unique_constraint(:email)
1648 |> validate_format(:email, @email_regex)
1649 |> update_and_set_cache()
1650 end
1651
1652 # Internal function; public one is `deactivate/2`
1653 defp set_activation_status(user, deactivated) do
1654 user
1655 |> cast(%{deactivated: deactivated}, [:deactivated])
1656 |> update_and_set_cache()
1657 end
1658
1659 def update_banner(user, banner) do
1660 user
1661 |> cast(%{banner: banner}, [:banner])
1662 |> update_and_set_cache()
1663 end
1664
1665 def update_background(user, background) do
1666 user
1667 |> cast(%{background: background}, [:background])
1668 |> update_and_set_cache()
1669 end
1670
1671 def update_source_data(user, source_data) do
1672 user
1673 |> cast(%{source_data: source_data}, [:source_data])
1674 |> update_and_set_cache()
1675 end
1676
1677 def roles(%{is_moderator: is_moderator, is_admin: is_admin}) do
1678 %{
1679 admin: is_admin,
1680 moderator: is_moderator
1681 }
1682 end
1683
1684 # ``fields`` is an array of mastodon profile field, containing ``{"name": "…", "value": "…"}``.
1685 # For example: [{"name": "Pronoun", "value": "she/her"}, …]
1686 def fields(%{fields: nil, source_data: %{"attachment" => attachment}}) do
1687 limit = Pleroma.Config.get([:instance, :max_remote_account_fields], 0)
1688
1689 attachment
1690 |> Enum.filter(fn %{"type" => t} -> t == "PropertyValue" end)
1691 |> Enum.map(fn fields -> Map.take(fields, ["name", "value"]) end)
1692 |> Enum.take(limit)
1693 end
1694
1695 def fields(%{fields: nil}), do: []
1696
1697 def fields(%{fields: fields}), do: fields
1698
1699 def validate_fields(changeset, remote? \\ false) do
1700 limit_name = if remote?, do: :max_remote_account_fields, else: :max_account_fields
1701 limit = Pleroma.Config.get([:instance, limit_name], 0)
1702
1703 changeset
1704 |> validate_length(:fields, max: limit)
1705 |> validate_change(:fields, fn :fields, fields ->
1706 if Enum.all?(fields, &valid_field?/1) do
1707 []
1708 else
1709 [fields: "invalid"]
1710 end
1711 end)
1712 end
1713
1714 defp valid_field?(%{"name" => name, "value" => value}) do
1715 name_limit = Pleroma.Config.get([:instance, :account_field_name_length], 255)
1716 value_limit = Pleroma.Config.get([:instance, :account_field_value_length], 255)
1717
1718 is_binary(name) && is_binary(value) && String.length(name) <= name_limit &&
1719 String.length(value) <= value_limit
1720 end
1721
1722 defp valid_field?(_), do: false
1723
1724 defp truncate_field(%{"name" => name, "value" => value}) do
1725 {name, _chopped} =
1726 String.split_at(name, Pleroma.Config.get([:instance, :account_field_name_length], 255))
1727
1728 {value, _chopped} =
1729 String.split_at(value, Pleroma.Config.get([:instance, :account_field_value_length], 255))
1730
1731 %{"name" => name, "value" => value}
1732 end
1733
1734 def admin_api_update(user, params) do
1735 user
1736 |> cast(params, [
1737 :is_moderator,
1738 :is_admin,
1739 :show_role
1740 ])
1741 |> update_and_set_cache()
1742 end
1743
1744 def mascot_update(user, url) do
1745 user
1746 |> cast(%{mascot: url}, [:mascot])
1747 |> validate_required([:mascot])
1748 |> update_and_set_cache()
1749 end
1750
1751 def mastodon_settings_update(user, settings) do
1752 user
1753 |> cast(%{settings: settings}, [:settings])
1754 |> validate_required([:settings])
1755 |> update_and_set_cache()
1756 end
1757
1758 @spec confirmation_changeset(User.t(), keyword()) :: Changeset.t()
1759 def confirmation_changeset(user, need_confirmation: need_confirmation?) do
1760 params =
1761 if need_confirmation? do
1762 %{
1763 confirmation_pending: true,
1764 confirmation_token: :crypto.strong_rand_bytes(32) |> Base.url_encode64()
1765 }
1766 else
1767 %{
1768 confirmation_pending: false,
1769 confirmation_token: nil
1770 }
1771 end
1772
1773 cast(user, params, [:confirmation_pending, :confirmation_token])
1774 end
1775
1776 def add_pinnned_activity(user, %Pleroma.Activity{id: id}) do
1777 if id not in user.pinned_activities do
1778 max_pinned_statuses = Pleroma.Config.get([:instance, :max_pinned_statuses], 0)
1779 params = %{pinned_activities: user.pinned_activities ++ [id]}
1780
1781 user
1782 |> cast(params, [:pinned_activities])
1783 |> validate_length(:pinned_activities,
1784 max: max_pinned_statuses,
1785 message: "You have already pinned the maximum number of statuses"
1786 )
1787 else
1788 change(user)
1789 end
1790 |> update_and_set_cache()
1791 end
1792
1793 def remove_pinnned_activity(user, %Pleroma.Activity{id: id}) do
1794 params = %{pinned_activities: List.delete(user.pinned_activities, id)}
1795
1796 user
1797 |> cast(params, [:pinned_activities])
1798 |> update_and_set_cache()
1799 end
1800
1801 def update_email_notifications(user, settings) do
1802 email_notifications =
1803 user.email_notifications
1804 |> Map.merge(settings)
1805 |> Map.take(["digest"])
1806
1807 params = %{email_notifications: email_notifications}
1808 fields = [:email_notifications]
1809
1810 user
1811 |> cast(params, fields)
1812 |> validate_required(fields)
1813 |> update_and_set_cache()
1814 end
1815
1816 defp set_subscribers(user, subscribers) do
1817 params = %{subscribers: subscribers}
1818
1819 user
1820 |> cast(params, [:subscribers])
1821 |> validate_required([:subscribers])
1822 |> update_and_set_cache()
1823 end
1824
1825 def add_to_subscribers(user, subscribed) do
1826 set_subscribers(user, Enum.uniq([subscribed | user.subscribers]))
1827 end
1828
1829 def remove_from_subscribers(user, subscribed) do
1830 set_subscribers(user, List.delete(user.subscribers, subscribed))
1831 end
1832
1833 defp set_domain_blocks(user, domain_blocks) do
1834 params = %{domain_blocks: domain_blocks}
1835
1836 user
1837 |> cast(params, [:domain_blocks])
1838 |> validate_required([:domain_blocks])
1839 |> update_and_set_cache()
1840 end
1841
1842 def block_domain(user, domain_blocked) do
1843 set_domain_blocks(user, Enum.uniq([domain_blocked | user.domain_blocks]))
1844 end
1845
1846 def unblock_domain(user, domain_blocked) do
1847 set_domain_blocks(user, List.delete(user.domain_blocks, domain_blocked))
1848 end
1849
1850 defp set_blocks(user, blocks) do
1851 params = %{blocks: blocks}
1852
1853 user
1854 |> cast(params, [:blocks])
1855 |> validate_required([:blocks])
1856 |> update_and_set_cache()
1857 end
1858
1859 def add_to_block(user, blocked) do
1860 set_blocks(user, Enum.uniq([blocked | user.blocks]))
1861 end
1862
1863 def remove_from_block(user, blocked) do
1864 set_blocks(user, List.delete(user.blocks, blocked))
1865 end
1866
1867 defp set_mutes(user, mutes) do
1868 params = %{mutes: mutes}
1869
1870 user
1871 |> cast(params, [:mutes])
1872 |> validate_required([:mutes])
1873 |> update_and_set_cache()
1874 end
1875
1876 def add_to_mutes(user, muted, notifications?) do
1877 with {:ok, user} <- set_mutes(user, Enum.uniq([muted | user.mutes])) do
1878 set_notification_mutes(
1879 user,
1880 Enum.uniq([muted | user.muted_notifications]),
1881 notifications?
1882 )
1883 end
1884 end
1885
1886 def remove_from_mutes(user, muted) do
1887 with {:ok, user} <- set_mutes(user, List.delete(user.mutes, muted)) do
1888 set_notification_mutes(
1889 user,
1890 List.delete(user.muted_notifications, muted),
1891 true
1892 )
1893 end
1894 end
1895
1896 defp set_notification_mutes(user, _muted_notifications, false = _notifications?) do
1897 {:ok, user}
1898 end
1899
1900 defp set_notification_mutes(user, muted_notifications, true = _notifications?) do
1901 params = %{muted_notifications: muted_notifications}
1902
1903 user
1904 |> cast(params, [:muted_notifications])
1905 |> validate_required([:muted_notifications])
1906 |> update_and_set_cache()
1907 end
1908
1909 def add_reblog_mute(user, ap_id) do
1910 params = %{muted_reblogs: user.muted_reblogs ++ [ap_id]}
1911
1912 user
1913 |> cast(params, [:muted_reblogs])
1914 |> update_and_set_cache()
1915 end
1916
1917 def remove_reblog_mute(user, ap_id) do
1918 params = %{muted_reblogs: List.delete(user.muted_reblogs, ap_id)}
1919
1920 user
1921 |> cast(params, [:muted_reblogs])
1922 |> update_and_set_cache()
1923 end
1924
1925 def set_invisible(user, invisible) do
1926 params = %{invisible: invisible}
1927
1928 user
1929 |> cast(params, [:invisible])
1930 |> validate_required([:invisible])
1931 |> update_and_set_cache()
1932 end
1933 end